From 2cbdceff0dbad0a619a18ee2d7282b85dc430ee4 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Thu, 1 Oct 2026 13:55:34 -0400 Subject: [PATCH 001/235] Fuse source-plane metadata projection through nominal transformations --- openhcs/core/runtime_image_values.py | 181 +++++++++++++----- openhcs/core/runtime_measurements.py | 5 +- openhcs/core/runtime_object_labels.py | 8 +- openhcs/core/runtime_relationships.py | 4 +- openhcs/core/runtime_spatial_graph.py | 4 +- openhcs/core/source_image_provenance.py | 8 +- .../test_image_payload_projection_algebra.py | 154 +++++++++++++++ ..._source_provenance_constructor_defaults.py | 99 ++++++++++ 8 files changed, 397 insertions(+), 66 deletions(-) create mode 100644 tests/unit/test_image_payload_projection_algebra.py create mode 100644 tests/unit/test_source_provenance_constructor_defaults.py diff --git a/openhcs/core/runtime_image_values.py b/openhcs/core/runtime_image_values.py index a29f7af397..e7ae62ffb7 100644 --- a/openhcs/core/runtime_image_values.py +++ b/openhcs/core/runtime_image_values.py @@ -250,15 +250,21 @@ def for_array_payload( ) def __post_init__(self, *source_provenance_values: object) -> None: - self.absorb_explicit_source_provenance( - SourceImageProvenance.from_init_values(source_provenance_values) - ) + self.absorb_explicit_source_provenance(source_provenance_values) self.source_voxel_spacing = self.source_voxel_spacing.with_missing_from( SourceVoxelSpacing.from_source_metadata(self.source_component_metadata) ) self.normalize_source_provenance_fields() self.normalize_source_spatial_domain_fields() self.normalize_source_voxel_spacing_fields() + self.validate_source_channel_axis() + if self.plane_axis is not None: + self.plane_axis = RuntimePlaneAxis( + self.plane_axis, + ) + + def validate_source_channel_axis(self) -> None: + """Validate the authored channel declaration before transforming axes.""" if self.source_channel_axis is not None and ( not isinstance(self.source_channel_axis, int) or isinstance(self.source_channel_axis, bool) @@ -266,10 +272,6 @@ def __post_init__(self, *source_provenance_values: object) -> None: raise TypeError( "ImagePayloadMetadata.source_channel_axis must be int or None." ) - if self.plane_axis is not None: - self.plane_axis = RuntimePlaneAxis( - self.plane_axis, - ) @property def has_values(self) -> bool: @@ -649,37 +651,11 @@ def without_source_channel_axis(self) -> "ImagePayloadMetadata": def for_leading_source_plane(self, plane_index: int) -> "ImagePayloadMetadata": """Project metadata after explicitly removing its leading plane axis.""" - if self.plane_axis is None: - raise ValueError( - "Leading source-plane projection requires a declared plane axis." - ) - return self.for_source_plane(plane_index).without_leading_plane_axis() + return LeadingSourcePlaneMetadataProjection(self, plane_index).project() def without_leading_plane_axis(self) -> "ImagePayloadMetadata": """Return metadata after an explicitly declared leading axis is removed.""" - if self.plane_axis is None: - raise ValueError("Image metadata has no leading plane axis to remove.") - source_channel_axis = self.source_channel_axis - if source_channel_axis == 0: - raise ValueError( - "Image metadata cannot declare the same leading axis as both " - "plane and channel." - ) - if source_channel_axis is not None and source_channel_axis > 0: - source_channel_axis -= 1 - return self.with_source_provenance( - self.source_provenance.with_runtime_planes_as_contributors() - ).replace_fields( - plane_axis=None, - source_channel_axis=source_channel_axis, - source_plane_intensity_scales=(), - source_plane_dtypes=(), - unit_interval_intensity=( - None - if self.unit_interval_intensity is None - else self.unit_interval_intensity.without_source_planes() - ), - ) + return LeadingPlaneAxisMetadataProjection(self).project() def collapse_leading_plane_axis(self) -> "ImagePayloadMetadata": """Return scalar metadata after reducing every plane of the leading axis.""" @@ -786,20 +762,7 @@ def payload_with(self, data: Any, mask: Any | None = None) -> Any: def for_source_plane(self, plane_index: int) -> "ImagePayloadMetadata": """Return metadata for one source plane sliced from a stacked payload.""" - source_provenance = self.source_provenance.for_source_plane(plane_index) - return self.replace_fields( - intensity_scale=self.intensity_scale_for_source_plane(plane_index), - source_dtype=_tuple_value(self.source_plane_dtypes, plane_index) - or self.source_dtype, - source_provenance=source_provenance, - unit_interval_intensity=( - None - if self.unit_interval_intensity is None - else self.unit_interval_intensity.for_source_plane(plane_index) - ), - source_plane_intensity_scales=(), - source_plane_dtypes=(), - ) + return SourcePlaneImageMetadataProjection(self, plane_index).project() def for_source_planes( self, @@ -1447,6 +1410,126 @@ def with_image_payload_data( return resolved_metadata.payload_with(data, resolved_mask) +@dataclass(frozen=True, slots=True) +class ImageMetadataProjection(ABC): + """Construct one metadata result from nominal source and axis transformations.""" + + metadata: ImagePayloadMetadata + + def project(self) -> ImagePayloadMetadata: + return self.metadata.replace_fields(**self.projected_fields()) + + @property + def projected_source_provenance(self) -> SourceImageProvenance: + return self.metadata.source_provenance + + @property + def projected_unit_interval_intensity( + self, + ) -> ImageUnitIntervalIntensityMetadata | None: + return self.metadata.unit_interval_intensity + + @abstractmethod + def projected_fields(self) -> dict[str, Any]: + """Derive constructor instructions, never an independently stored fact set.""" + return dict( + source_provenance=self.projected_source_provenance, + unit_interval_intensity=self.projected_unit_interval_intensity, + ) + + +@dataclass(frozen=True, slots=True) +class SourcePlaneImageMetadataProjection(ImageMetadataProjection): + """Select scalar source-image facts without removing an array axis.""" + + plane_index: int + + @property + def projected_source_provenance(self) -> SourceImageProvenance: + return super( + SourcePlaneImageMetadataProjection, self + ).projected_source_provenance.for_source_plane(self.plane_index) + + @property + def projected_unit_interval_intensity( + self, + ) -> ImageUnitIntervalIntensityMetadata | None: + intensity = super( + SourcePlaneImageMetadataProjection, self + ).projected_unit_interval_intensity + return ( + None if intensity is None else intensity.for_source_plane(self.plane_index) + ) + + def projected_fields(self) -> dict[str, Any]: + changes = super(SourcePlaneImageMetadataProjection, self).projected_fields() + changes.update( + intensity_scale=self.metadata.intensity_scale_for_source_plane( + self.plane_index + ), + source_dtype=_tuple_value( + self.metadata.source_plane_dtypes, self.plane_index + ) + or self.metadata.source_dtype, + source_plane_intensity_scales=(), + source_plane_dtypes=(), + ) + return changes + + +class LeadingPlaneAxisMetadataProjection(ImageMetadataProjection): + """Remove a declared leading axis while preserving its source contributors.""" + + @property + def projected_source_provenance(self) -> SourceImageProvenance: + return super().projected_source_provenance.with_runtime_planes_as_contributors() + + @property + def projected_unit_interval_intensity( + self, + ) -> ImageUnitIntervalIntensityMetadata | None: + intensity = super().projected_unit_interval_intensity + return None if intensity is None else intensity.without_source_planes() + + def projected_fields(self) -> dict[str, Any]: + if self.metadata.plane_axis is None: + raise ValueError("Image metadata has no leading plane axis to remove.") + source_channel_axis = self.metadata.source_channel_axis + if source_channel_axis == 0: + raise ValueError( + "Image metadata cannot declare the same leading axis as both " + "plane and channel." + ) + self.metadata.validate_source_channel_axis() + RuntimePlaneAxis(self.metadata.plane_axis) + if source_channel_axis is not None and source_channel_axis > 0: + source_channel_axis -= 1 + changes = super().projected_fields() + changes.update( + plane_axis=None, + source_channel_axis=source_channel_axis, + source_plane_intensity_scales=(), + source_plane_dtypes=(), + ) + return changes + + +class LeadingSourcePlaneMetadataProjection( + LeadingPlaneAxisMetadataProjection, + SourcePlaneImageMetadataProjection, +): + """Compose source selection and axis removal before constructing metadata.""" + + def projected_fields(self) -> dict[str, Any]: + if self.metadata.plane_axis is None: + raise ValueError( + "Leading source-plane projection requires a declared plane axis." + ) + self.metadata.validate_source_channel_axis() + RuntimePlaneAxis(self.metadata.plane_axis) + return super().projected_fields() + + @dataclass(frozen=True, slots=True) class ImagePayloadSliceProjector: """Project payload context from a parent image into one child image slice.""" diff --git a/openhcs/core/runtime_measurements.py b/openhcs/core/runtime_measurements.py index b0c613097d..a213bd62d8 100644 --- a/openhcs/core/runtime_measurements.py +++ b/openhcs/core/runtime_measurements.py @@ -15,7 +15,6 @@ MeasurementObjectRowIdentity, ) from openhcs.core.source_image_provenance import ( - SourceImageProvenance, SourceImageProvenanceFields, ) @@ -75,9 +74,7 @@ def iter_row_mappings(self) -> Iterable[Mapping[str, object]]: return self.rows.iter_row_mappings() def __post_init__(self, *source_provenance_values: object) -> None: - self.absorb_explicit_source_provenance( - SourceImageProvenance.from_init_values(source_provenance_values) - ) + self.absorb_explicit_source_provenance(source_provenance_values) self.normalize_source_provenance_fields() self.validate_artifact_name() if self.source_image_name == "": diff --git a/openhcs/core/runtime_object_labels.py b/openhcs/core/runtime_object_labels.py index 69e07dbd46..056dc95c89 100644 --- a/openhcs/core/runtime_object_labels.py +++ b/openhcs/core/runtime_object_labels.py @@ -928,9 +928,7 @@ class ObjectLabelPayload(ObjectLabelValue): def __post_init__(self, *source_provenance_values: object) -> None: self.validate_object_label_variants() - self.absorb_explicit_source_provenance( - SourceImageProvenance.from_init_values(source_provenance_values) - ) + self.absorb_explicit_source_provenance(source_provenance_values) self.normalize_object_label_metadata("ObjectLabelPayload") @@ -994,9 +992,7 @@ def from_payload( def __post_init__(self, *source_provenance_values: object) -> None: self.validate_object_label_variants() - self.absorb_explicit_source_provenance( - SourceImageProvenance.from_init_values(source_provenance_values) - ) + self.absorb_explicit_source_provenance(source_provenance_values) self.validate_artifact_name() if self.source_image_name == "": raise ValueError("ObjectLabelSet.source_image_name cannot be empty.") diff --git a/openhcs/core/runtime_relationships.py b/openhcs/core/runtime_relationships.py index 4abca79cfa..1d542b67e1 100644 --- a/openhcs/core/runtime_relationships.py +++ b/openhcs/core/runtime_relationships.py @@ -218,9 +218,7 @@ def from_payload( def __post_init__(self, *source_provenance_values: object) -> None: self.validate_artifact_name() - self.absorb_explicit_source_provenance( - SourceImageProvenance.from_init_values(source_provenance_values) - ) + self.absorb_explicit_source_provenance(source_provenance_values) self.normalize_source_provenance_fields() if not isinstance(self.declaration, ObjectRelationshipDeclaration): raise TypeError( diff --git a/openhcs/core/runtime_spatial_graph.py b/openhcs/core/runtime_spatial_graph.py index f12cc3abaf..db26f65412 100644 --- a/openhcs/core/runtime_spatial_graph.py +++ b/openhcs/core/runtime_spatial_graph.py @@ -201,9 +201,7 @@ class SpatialGraph(SourceImageProvenanceFields, NamedArtifactPayload): source_plane_index: int | None = None def __post_init__(self, *source_provenance_values: object) -> None: - self.absorb_explicit_source_provenance( - SourceImageProvenance.from_init_values(source_provenance_values) - ) + self.absorb_explicit_source_provenance(source_provenance_values) self.normalize_source_provenance_fields() self.validate_artifact_name() nodes = tuple(self.nodes) diff --git a/openhcs/core/source_image_provenance.py b/openhcs/core/source_image_provenance.py index 71ff6ef174..3b16137a42 100644 --- a/openhcs/core/source_image_provenance.py +++ b/openhcs/core/source_image_provenance.py @@ -1775,8 +1775,14 @@ class SourceImageProvenanceFields: def absorb_explicit_source_provenance( self, - explicit: SourceImageProvenance, + values: SourceProvenanceInitValues, ) -> None: + """Decode authored aliases once, leaving absent constructor facts absent.""" + if all(value is None for value in values[:-1]): + names = values[-1] + if type(names) is tuple and not names: + return + explicit = SourceImageProvenance.from_init_values(values) if not explicit.has_values: return self.source_provenance = explicit.with_missing_from(self.source_provenance) diff --git a/tests/unit/test_image_payload_projection_algebra.py b/tests/unit/test_image_payload_projection_algebra.py new file mode 100644 index 0000000000..ef898d9870 --- /dev/null +++ b/tests/unit/test_image_payload_projection_algebra.py @@ -0,0 +1,154 @@ +"""Physical source-plane projection preserves composition and mutable isolation.""" + +from dataclasses import fields + +import numpy as np +import pytest + +from openhcs.core.runtime_image_values import ( + ImagePayloadMetadata, + ImagePayloadSliceProjector, + ImageUnitIntervalIntensityMetadata, + image_payload_mask, + image_payload_metadata, +) +from openhcs.core.runtime_plane_projection import RuntimePlaneAxis +from openhcs.core.source_image_provenance import ( + RuntimeSourceImageProvenancePlane, + SourceImageIdentity, + SourceImageProvenanceContributor, + SourceImageProvenancePlanes, +) + + +def _metadata(axis, channel_axis): + return ImagePayloadMetadata( + source_path="/tmp/parent.tif", + source_component_metadata={"well": "A01", "extension": ".tif"}, + source_image_provenance_planes=SourceImageProvenancePlanes.from_components( + paths=("/tmp/first.tif", "/tmp/second.tif"), + component_metadata=({"site": "1"}, {"site": "2"}), + ), + intensity_scale=255.0, + source_dtype="uint8", + source_plane_intensity_scales=(255.0, 65535.0), + source_plane_dtypes=("uint8", "uint16"), + unit_interval_intensity=ImageUnitIntervalIntensityMetadata( + scale=255, source_plane_scales=(255, 65535) + ), + source_image_names=("DNA", "RNA"), + source_channel_axis=channel_axis, + plane_axis=axis, + ) + + +@pytest.mark.parametrize("axis", tuple(RuntimePlaneAxis)) +@pytest.mark.parametrize("channel_axis", (None, 1, 3, -1)) +@pytest.mark.parametrize("plane", (0, 1)) +def test_physical_projection_agrees_with_separate_semantic_operations( + axis, channel_axis, plane +): + metadata = _metadata(axis, channel_axis) + sequential = metadata.for_source_plane(plane).without_leading_plane_axis() + projected = metadata.for_leading_source_plane(plane) + for member in fields(ImagePayloadMetadata): + assert getattr(projected, member.name) == getattr(sequential, member.name) + assert projected.source_path == ("/tmp/first.tif", "/tmp/second.tif")[plane] + assert projected.source_component_metadata["well"] == "A01" + assert projected.source_component_metadata["site"] == str(plane + 1) + 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 + 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) + calls = [] + original = ImagePayloadMetadata.__post_init__ + + def observe(self, *values): + calls.append(self) + original(self, *values) + + monkeypatch.setattr(ImagePayloadMetadata, "__post_init__", observe) + projected = metadata.for_leading_source_plane(1) + assert calls == [projected] + + +def test_physical_projection_rejects_leading_channel_axis(): + metadata = _metadata(RuntimePlaneAxis.RUNTIME_SLICE, 0) + with pytest.raises(ValueError, match="both plane and channel"): + metadata.for_leading_source_plane(1) + + +def test_physical_projection_requires_declared_plane_axis(): + metadata = _metadata(None, None) + with pytest.raises(ValueError, match="declared plane axis"): + metadata.for_leading_source_plane(1) + + +@pytest.mark.parametrize("channel_axis", (False, True, "1", 1.5)) +def test_physical_projection_preserves_invalid_mutated_channel_error(channel_axis): + metadata = _metadata(RuntimePlaneAxis.RUNTIME_SLICE, None) + metadata.source_channel_axis = channel_axis + with pytest.raises(TypeError, match="must be int or None"): + metadata.for_source_plane(1).without_leading_plane_axis() + with pytest.raises(TypeError, match="must be int or None"): + metadata.for_leading_source_plane(1) + + +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, + source_image_provenance_planes=SourceImageProvenancePlanes( + ( + RuntimeSourceImageProvenancePlane( + SourceImageIdentity( + "/tmp/selected.tif", + {"OpenHCSSourceVoxelSpacingZYX": "4,2,1"}, + ), + contributors=(contributor,), + source_image_name="DNA", + ), + ) + ), + ) + sequential = metadata.for_source_plane(0).without_leading_plane_axis() + projected = metadata.for_leading_source_plane(0) + for member in fields(ImagePayloadMetadata): + assert getattr(projected, member.name) == getattr(sequential, member.name) + assert projected.source_image_provenance_planes.planes == (contributor,) + + +def test_projection_observes_mask_and_metadata_mutation(): + metadata = _metadata(RuntimePlaneAxis.RUNTIME_SLICE, None) + mask = np.ones((2, 3, 4), dtype=bool) + projector = ImagePayloadSliceProjector(mask, metadata) + data = np.zeros((3, 4)) + first = projector.payload_for_slice(data, 1) + assert image_payload_metadata(first).intensity_scale == 65535.0 + metadata.source_plane_intensity_scales = (1.0, 2.0) + mask[1, 0, 0] = False + second = projector.payload_for_slice(data, 1) + assert image_payload_metadata(second).intensity_scale == 2.0 + assert not image_payload_mask(second)[0, 0] + mask.resize((2, 2, 6), refcheck=False) + with pytest.raises(ValueError, match="cannot be projected"): + projector.payload_for_slice(data, 1) diff --git a/tests/unit/test_source_provenance_constructor_defaults.py b/tests/unit/test_source_provenance_constructor_defaults.py new file mode 100644 index 0000000000..f25eaa478c --- /dev/null +++ b/tests/unit/test_source_provenance_constructor_defaults.py @@ -0,0 +1,99 @@ +"""All source-metadata carriers decode authored aliases at the same boundary.""" + +from functools import partial + +import numpy as np +import pytest + +from openhcs.core.artifacts import ArtifactSpec, ObjectLabelsArtifactType +from openhcs.core.runtime_image_values import ImagePayloadMetadata +from openhcs.core.runtime_measurements import ( + MeasurementScope, + MeasurementSubject, + MeasurementTable, +) +from openhcs.core.runtime_object_labels import ( + ObjectLabelPayload, + ObjectLabelSet, + ObjectLabelVariantData, +) +from openhcs.core.runtime_relationships import ( + DirectedObjectRelationshipPayload, + ObjectRelationship, + ObjectRelationshipDeclaration, +) +from openhcs.core.runtime_spatial_graph import SpatialGraph +from openhcs.core.runtime_tabular_values import ColumnarRows +from openhcs.core.source_image_provenance import SourceImageProvenance + + +class EmptyRows(ColumnarRows): + columns = {} + fields = () + + +def _carriers(): + labels = ObjectLabelVariantData(labels=np.zeros((2, 3), dtype=np.int32)) + relationship = ObjectRelationshipDeclaration.parent_child( + source=ArtifactSpec.output("Cells", ObjectLabelsArtifactType).ref(), + target=ArtifactSpec.output("Nuclei", ObjectLabelsArtifactType).ref(), + producer_module_number=1, + ) + return ( + ImagePayloadMetadata, + partial(ObjectLabelPayload, variant_data=labels), + partial(ObjectLabelSet, name="Cells", variant_data=labels), + partial( + MeasurementTable, + name="Intensity", + rows=EmptyRows(), + subject=MeasurementSubject(MeasurementScope.IMAGE, "DNA"), + ), + partial(SpatialGraph, name="Graph", nodes=(), edges=()), + partial( + ObjectRelationship, + name="ParentChild", + declaration=relationship, + payload=DirectedObjectRelationshipPayload(source_ids=(), target_ids=()), + ), + ) + + +@pytest.mark.parametrize("construct", _carriers()) +def test_absent_aliases_do_not_create_empty_provenance(construct, monkeypatch): + supplied = SourceImageProvenance( + source_path="/tmp/source.tif", source_component_metadata={"well": "A01"} + ) + + def unexpected(cls, values): + pytest.fail("Absent source aliases must not allocate a decoded value.") + + monkeypatch.setattr( + SourceImageProvenance, "from_init_values", classmethod(unexpected) + ) + carrier = construct(source_provenance=supplied) + assert carrier.source_path == "/tmp/source.tif" + assert carrier.source_component_metadata["well"] == "A01" + assert carrier.source_provenance is not supplied + carrier.source_provenance.source_identity.path = "/tmp/changed.tif" + assert supplied.source_path == "/tmp/source.tif" + + +@pytest.mark.parametrize("construct", _carriers()) +def test_explicit_empty_metadata_and_aliases_still_decode(construct, monkeypatch): + calls = [] + original = SourceImageProvenance.from_init_values + + def observe(cls, values): + calls.append(values) + return original(values) + + monkeypatch.setattr(SourceImageProvenance, "from_init_values", classmethod(observe)) + empty = construct(source_component_metadata={}) + assert calls == [(None, {}, None, ())] + assert empty.source_component_metadata is not None + assert dict(empty.source_component_metadata) == {} + authored = construct(source_path="/tmp/authored.tif", source_image_names=("DNA",)) + assert authored.source_path == "/tmp/authored.tif" + assert authored.source_image_names == ("DNA",) + assert len(calls) == 2 From f0c1207b673d651a9d5210a024340b7bebac0689 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Thu, 1 Oct 2026 13:57:07 -0400 Subject: [PATCH 002/235] Derive absent source aliases from declared constructor defaults --- openhcs/core/source_image_provenance.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/openhcs/core/source_image_provenance.py b/openhcs/core/source_image_provenance.py index 3b16137a42..519003cbb0 100644 --- a/openhcs/core/source_image_provenance.py +++ b/openhcs/core/source_image_provenance.py @@ -1778,10 +1778,13 @@ def absorb_explicit_source_provenance( values: SourceProvenanceInitValues, ) -> None: """Decode authored aliases once, leaving absent constructor facts absent.""" - if all(value is None for value in values[:-1]): - names = values[-1] - if type(names) is tuple and not names: - return + if all( + value is default + for value, default in zip( + values, SourceImageProvenance.__init__.__defaults__, strict=True + ) + ): + return explicit = SourceImageProvenance.from_init_values(values) if not explicit.has_values: return From 33aa3fc04a4657411288d8a5640ddf345c89fde8 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Thu, 1 Oct 2026 14:00:15 -0400 Subject: [PATCH 003/235] Keep required plane validation on metadata authority --- openhcs/core/runtime_image_values.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/openhcs/core/runtime_image_values.py b/openhcs/core/runtime_image_values.py index e7ae62ffb7..0053bcf5fa 100644 --- a/openhcs/core/runtime_image_values.py +++ b/openhcs/core/runtime_image_values.py @@ -273,6 +273,12 @@ def validate_source_channel_axis(self) -> None: "ImagePayloadMetadata.source_channel_axis must be int or None." ) + def require_leading_plane_axis(self, message: str) -> RuntimePlaneAxis: + """Validate a required plane declaration at its metadata authority.""" + if self.plane_axis is None: + raise ValueError(message) + return RuntimePlaneAxis(self.plane_axis) + @property def has_values(self) -> bool: """Return whether this metadata carries any semantic image facts.""" @@ -1492,8 +1498,9 @@ def projected_unit_interval_intensity( return None if intensity is None else intensity.without_source_planes() def projected_fields(self) -> dict[str, Any]: - if self.metadata.plane_axis is None: - raise ValueError("Image metadata has no leading plane axis to remove.") + self.metadata.require_leading_plane_axis( + "Image metadata has no leading plane axis to remove." + ) source_channel_axis = self.metadata.source_channel_axis if source_channel_axis == 0: raise ValueError( @@ -1501,7 +1508,6 @@ def projected_fields(self) -> dict[str, Any]: "plane and channel." ) self.metadata.validate_source_channel_axis() - RuntimePlaneAxis(self.metadata.plane_axis) if source_channel_axis is not None and source_channel_axis > 0: source_channel_axis -= 1 changes = super().projected_fields() @@ -1521,12 +1527,10 @@ class LeadingSourcePlaneMetadataProjection( """Compose source selection and axis removal before constructing metadata.""" def projected_fields(self) -> dict[str, Any]: - if self.metadata.plane_axis is None: - raise ValueError( - "Leading source-plane projection requires a declared plane axis." - ) + self.metadata.require_leading_plane_axis( + "Leading source-plane projection requires a declared plane axis." + ) self.metadata.validate_source_channel_axis() - RuntimePlaneAxis(self.metadata.plane_axis) return super().projected_fields() From 9c070354c3b5e3b6a4f8fe41127dfdaee79e22ee Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Thu, 1 Oct 2026 14:09:36 -0400 Subject: [PATCH 004/235] Share actual materialization outputs with publication and observations (#384) --- .../materialization_plumbing_384_20261001.rst | 90 +++++++ .../orchestrator/compiled_plate_execution.py | 31 ++- openhcs/core/orchestrator/worker_execution.py | 70 +++--- openhcs/core/runtime_execution_validation.py | 14 +- openhcs/core/runtime_exports.py | 21 -- openhcs/core/steps/abstract.py | 24 +- .../function_artifact_materialization.py | 119 ++++----- openhcs/core/steps/function_outputs.py | 150 ++++++----- .../processing/materialization/__init__.py | 6 + openhcs/processing/materialization/core.py | 152 +++++++++--- openhcs/runtime/zmq_execution_observation.py | 9 +- openhcs/runtime/zmq_execution_server.py | 12 +- .../test_function_artifact_materialization.py | 145 ++++++----- tests/unit/test_function_outputs.py | 135 +++++++--- .../test_materialization_batch_outcomes.py | 232 ++++++++++++++++++ .../unit/test_runtime_execution_validation.py | 63 +++-- tests/unit/test_zmq_execution_server.py | 33 +-- tests/unit/test_zmq_runtime_outcome_export.py | 3 + 18 files changed, 930 insertions(+), 379 deletions(-) create mode 100644 docs/validation/materialization_plumbing_384_20261001.rst create mode 100644 tests/unit/test_materialization_batch_outcomes.py diff --git a/docs/validation/materialization_plumbing_384_20261001.rst b/docs/validation/materialization_plumbing_384_20261001.rst new file mode 100644 index 0000000000..a0dc46bcbf --- /dev/null +++ b/docs/validation/materialization_plumbing_384_20261001.rst @@ -0,0 +1,90 @@ +One materialization batch for saving, publication and observation +=============================================================== + +This candidate addresses the generic plumbing tracked in issue #384. It is not +an accepted end-to-end performance result. The separate light owner trace bounded +eight repeated renders at approximately 0.297 seconds and metadata finalization +at 0.896 seconds; removing repeated rendering alone cannot close the generic +runtime gap. This is a structural dependency of the broader runtime boundary work. + +Ownership and production flow +----------------------------- + +``MaterializationBatch`` owns one writer-group rendering and saving algorithm; +its primary path is a derived view. ``SavedMaterializationOutputs`` owns only +completed backend outputs. ``MaterializedRuntimeArtifact`` inherits that shared +backend projection and adds the original reduced artifact binding and observation +behavior. Backend-indexed mappings contain actual destination values, not behavior +handlers. Existing WriterSpec declarations continue to determine format-specific +writing. No new runtime ledger, context cache or identity memoization was added. + +``BackendSaver`` reports only payloads accepted by each backend after successful +save calls. Failed saves raise without returning successful outcomes. Artifact +finalization passes those exact outputs to the existing nominal OutputTarget +family. Transient publication targets own the outcomes until publication finishes; +they are excluded from target equality and hashing. Image directories and source +projections consume these outputs without rendering the mutable logical artifact +again. Rendered pixel content is not carried into worker observation transport. + +The already declared ``StepExecutionObservation`` now carries actual persistent +locations and declared export paths from finalization. Ordinary worker progress +consumes returned locations; worker result observations accumulate returned paths +before image resources are released. Plate-scoped execution uses the same actual +materialization handoff. Existing ``CompiledPlateExecutionExtras`` carries its +RuntimeExecutionObservation; ``CompiledPlateExecutionResults.runtime_observations`` +is a derived collection of worker and parent observations. Both ZMQ value and +outcome exports consume that collection. RuntimeExportObservation no longer +reconstructs output paths from compiled contexts and mutable logical values. + +Terminal materializations persist without external-export participation; +streaming-only specs report no persistent destinations. Measurement reducers can +produce synthetic records absent from RuntimeValueStore; outcomes remain bound to +the record actually reduced and materialized. Main-flow manifests retain their +independent inheritance and semantic-address contracts. + +Removed code and intentionally retained consumers +------------------------------------------------ + +Removed ``_materialization_output_groups`` after moving its shared algorithm to +MaterializationBatch.render. Removed the tests-only materialized and observed +materialized path helpers, the obsolete current-runtime export-path helper, the +single-call observed-materialization wrapper, and +RuntimeExportObservation.from_execution_contexts. Routing tests now intercept +batch preparation; physical/export tests pass actual outcome records explicitly. + +Historical debug reuse still has an explicit prediction surface named +``preview_reused_*``. It does not create a new-save StepExecutionObservation. +Snapshot-owned physical receipts remain OPEN for that separate debug lifecycle; +these prediction functions are not an ordinary execution fallback. Historical +viewer expectations retain their real declaration-based rendering consumer. +Analysis consolidation retains its real CSV-content derivation consumer; it is a +separate reduction, not a replacement authority for observed export paths. + +Evidence and limits +------------------- + +Original source: 10f7e7075b412ccea8959f2c4e1aa818455149fa. NRA original syntax/class +census covers 703 modules, 5157 original ClassDefs, 5145 canonical projections and +12 retained OPEN syntax rows. The bounded ownership decision and complete census +are retained in the external benchmark evidence directory. The authored lifecycle, +new return values and argument migration exceed declaration-movement DSL proof +scope; no native behavioral equivalence proof is asserted. + +The first consumer gate retained 154 passing and 33 failing tests. Routing spies +intercepted the removed production boundary; metadata fixtures omitted the actual +output handoff. Their assertions were preserved while moving those fixtures to the +new boundary. The next six-module gate passed all 197 tests. A later parent gate +passed 264 tests with three new worker-fixture failures: the fixture failed the +existing DebugExecutionContext nominal boundary. It was replaced with a genuine +ProcessingContext subclass; the production boundary was not weakened. + +The final eleven-module gate passes 267 tests in 5.12 seconds. It includes actual +backend acceptance and failed-save controls, exact output-object reuse after +logical input mutation, rejection of re-rendering during ordinary worker progress +and export, all three runtime observation modes, pickle transport after resource +release, exact combined worker/parent export paths, materialization formats, +metadata publication/reconciliation, runtime/viewer validation and ZMQ consumers. + +R0/R1 source guard qualification, representative ordinary unprofiled timing and +native numerical parity remain required before promotion. No benchmark ratios or +speedup claim are attached to this receipt. diff --git a/openhcs/core/orchestrator/compiled_plate_execution.py b/openhcs/core/orchestrator/compiled_plate_execution.py index f2e49a305c..a5fe8a7921 100644 --- a/openhcs/core/orchestrator/compiled_plate_execution.py +++ b/openhcs/core/orchestrator/compiled_plate_execution.py @@ -104,6 +104,9 @@ class CompiledPlateExecutionExtras: RESULTS_SUMMARY_KEY: ClassVar[str] = "viewer_states_by_port" viewer_states_by_port: Mapping[int, "ViewerControlResponse"] + runtime_observation: RuntimeExecutionObservation = field( + default_factory=RuntimeExecutionObservation + ) class CompiledPlateExecutionResults(dict[str, ExecutionResult]): @@ -120,6 +123,13 @@ def __init__( viewer_states_by_port=MappingProxyType({}) ) + @property + def runtime_observations(self) -> tuple[RuntimeExecutionObservation, ...]: + return ( + *tuple(result.runtime_observation for result in self.values()), + self.extras.runtime_observation, + ) + @dataclass(frozen=True, slots=True) class CompiledPlateExecutionRequest(ProgressExecutionContext): @@ -249,6 +259,7 @@ def execute_compiled_plate_request( executor_resources.clear_execution_bundle() executor_resources.release_parent_runtime_resources(execution_bundle) + plate_runtime_observation = RuntimeExecutionObservation() if all(result.is_success() for result in execution_results.values()): plate_runtime_observation = execute_plate_scoped_steps( validated.compiled_contexts, @@ -285,7 +296,8 @@ def execute_compiled_plate_request( return CompiledPlateExecutionResults( execution_results, extras=CompiledPlateExecutionExtras( - viewer_states_by_port=viewer_states_by_port + viewer_states_by_port=viewer_states_by_port, + runtime_observation=plate_runtime_observation, ), ) except ExecutionCancelledError: @@ -445,6 +457,7 @@ def execute_plate_scoped_steps( plate_step_indexes = validate_plate_scoped_contexts(compiled_contexts) total_steps = max(next(iter(compiled_contexts.values())).step_plans) + 1 records_by_axis = _runtime_record_snapshot(compiled_contexts) + runtime_export_paths_by_context = {key: [] for key in compiled_contexts} for step_index in plate_step_indexes: owner_context, owner_plan = _plate_output_owner( compiled_contexts, @@ -523,13 +536,24 @@ def execute_plate_scoped_steps( ) records_by_axis = _records_with_output(records_by_axis, record) - RuntimeArtifactMaterializationAuthority.materialize( + materializations = RuntimeArtifactMaterializationAuthority.materialize( owner_context, owner_plan.require_function_execution_ready(), ) OpenHCSMetadataWriter.write( owner_context, owner_plan.require_function_execution_ready(), + artifact_materializations=materializations, + ) + owner_context_key = next( + key + for key, context in compiled_contexts.items() + if context is owner_context + ) + runtime_export_paths_by_context[owner_context_key].extend( + path + for materialization in materializations + for path in materialization.observation(owner_plan).runtime_export_paths ) _emit_execution_progress( progress_queue=progress_queue, @@ -547,6 +571,9 @@ def execute_plate_scoped_steps( RuntimeContextObservation( context_key=context_key, records=records, + runtime_export_paths=tuple( + dict.fromkeys(runtime_export_paths_by_context[context_key]) + ), ) for context_key, context in compiled_contexts.items() if ( diff --git a/openhcs/core/orchestrator/worker_execution.py b/openhcs/core/orchestrator/worker_execution.py index 7f82c7bafc..fe7776e554 100644 --- a/openhcs/core/orchestrator/worker_execution.py +++ b/openhcs/core/orchestrator/worker_execution.py @@ -48,11 +48,15 @@ from openhcs.core.progress.runtime_artifacts import ( runtime_artifact_context_for_records, ) -from openhcs.core.runtime_stores import StoredRuntimeValue +from openhcs.core.runtime_stores import ( + StoredRuntimeValue, + RuntimeArtifactAddress, + RuntimeArtifactLocation, +) from openhcs.core.steps.abstract import AbstractStep, StepExecutionObservation from openhcs.core.steps.function_artifact_materialization import ( - observed_materialized_artifact_locations_by_address, - observed_runtime_export_artifact_output_paths, + preview_reused_materialized_artifact_locations, + preview_reused_runtime_export_paths, ) from openhcs.utils.environment import OpenHCSProcessEnvironment @@ -63,21 +67,16 @@ def _runtime_observation_progress_context( records: tuple[StoredRuntimeValue, ...], *, - plan: CompiledStepPlan, - context: ProcessingContext, + materialized_locations_by_address: Mapping[ + RuntimeArtifactAddress, tuple[RuntimeArtifactLocation, ...] + ], ) -> dict | None: """Project one RuntimeValueStore observation delta through owned payloads.""" runtime_artifacts = runtime_artifact_context_for_records(records) live_measurements = live_measurement_context_for_records( records, - materialized_locations_by_address=( - observed_materialized_artifact_locations_by_address( - plan, - context, - records, - ) - ), + materialized_locations_by_address=materialized_locations_by_address, ) if runtime_artifacts is None: return live_measurements @@ -795,22 +794,16 @@ def _execute_axis_with_sequential_combinations( pipeline_definition, frozen_context, lane_context, + context_key=context_key, cancellation=cancellation, ) observed_records = runtime_store.observed_values_after( execution_observation_cursor ) runtime_export_paths = tuple( - dict.fromkeys( - path - for step_plan in frozen_context.step_plans.values() - if step_plan.owns_runtime_outputs - for path in observed_runtime_export_artifact_output_paths( - step_plan, - frozen_context, - observed_records, - ) - ) + path + for observation in result.runtime_observation.contexts + for path in observation.runtime_export_paths ) finally: # This cache is context-local even when lanes share a process. @@ -932,6 +925,8 @@ def _execute_single_axis_static( pipeline_definition: List[AbstractStep], frozen_context: ProcessingContext, lane_context: WorkerLaneExecutionContext, + *, + context_key: str, cancellation: ExecutionCancellationSignal | None = None, ) -> ExecutionResult: """Execute one frozen axis context against the compiled pipeline.""" @@ -952,6 +947,7 @@ def _execute_single_axis_static( frozen_context.bind_execution_runtime(lane_context) lane_context.install_debug_sink(frozen_context) runtime_value_store = frozen_context.runtime_value_store + runtime_export_paths = [] for step_index, step in enumerate(pipeline_definition): if cancellation is not None: @@ -981,8 +977,15 @@ def _execute_single_axis_static( ) runtime_progress_context = _runtime_observation_progress_context( observed_records, - plan=step_plan, - context=frozen_context, + # Historical debug previews are not a new-save observation. + materialized_locations_by_address=preview_reused_materialized_artifact_locations( + step_plan, frozen_context, observed_records + ), + ) + runtime_export_paths.extend( + preview_reused_runtime_export_paths( + step_plan, frozen_context, observed_records + ) ) emit( execution_id=lane_context.execution_id, @@ -1016,12 +1019,12 @@ def _execute_single_axis_static( ) observation_cursor = runtime_value_store.observation_cursor() - step.process(frozen_context, step_index) + step_observation = step.process(frozen_context, step_index) + runtime_export_paths.extend(step_observation.runtime_export_paths) observed_records = runtime_value_store.observed_values_after(observation_cursor) runtime_progress_context = _runtime_observation_progress_context( observed_records, - plan=step_plan, - context=frozen_context, + materialized_locations_by_address=step_observation.materialized_locations_by_address, ) emit( @@ -1044,7 +1047,18 @@ def _execute_single_axis_static( ): break - return ExecutionResult.success(axis_id=axis_id) + return ExecutionResult.success( + axis_id=axis_id, + runtime_observation=RuntimeExecutionObservation( + contexts=( + RuntimeContextObservation( + context_key=context_key, + records=(), + runtime_export_paths=tuple(dict.fromkeys(runtime_export_paths)), + ), + ) + ), + ) def execute_worker_lane( diff --git a/openhcs/core/runtime_execution_validation.py b/openhcs/core/runtime_execution_validation.py index 8f1c90d4a1..b47296cc06 100644 --- a/openhcs/core/runtime_execution_validation.py +++ b/openhcs/core/runtime_execution_validation.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections import Counter -from collections.abc import Iterable, Mapping +from collections.abc import Iterable, Mapping, Sequence from dataclasses import dataclass from pathlib import Path from types import MappingProxyType @@ -12,6 +12,7 @@ from openhcs.core.artifacts import ArtifactSpec, ArtifactType from openhcs.core.context.processing_context import ProcessingContext +from openhcs.core.orchestrator.execution_result import RuntimeExecutionObservation from openhcs.core.runtime_exports import ( RuntimeExportExpectation, RuntimeExportObservation, @@ -235,12 +236,13 @@ def runtime_artifact_viewer_expectations( if not plan.streaming_configs or not plan.owns_runtime_outputs: continue from openhcs.core.steps.function_artifact_materialization import ( - observed_runtime_artifact_materializations, + runtime_artifact_materializations_from_records, ) - for materialization in observed_runtime_artifact_materializations( + for materialization in runtime_artifact_materializations_from_records( plan, context, + context.runtime_value_store.observed_values, ): output_plan = materialization.output_plan if plan.compiled_function_pattern.publishes_output_to_main_flow( @@ -319,6 +321,8 @@ class RuntimeArtifactExecutionObservation: def from_contexts( cls, execution_contexts: Mapping[str, ProcessingContext], + *, + runtime_observations: Sequence[RuntimeExecutionObservation], ) -> "RuntimeArtifactExecutionObservation": identity_policies = frozenset( context.source_image_set_identity_policy @@ -331,8 +335,8 @@ def from_contexts( ) return cls( records_by_axis=runtime_records_by_axis(execution_contexts), - exports=RuntimeExportObservation.from_execution_contexts( - execution_contexts + exports=RuntimeExportObservation.from_runtime_observations( + runtime_observations ), source_image_set_identity_policy=next( iter(identity_policies), diff --git a/openhcs/core/runtime_exports.py b/openhcs/core/runtime_exports.py index 1cc9d50317..ee5f64cc05 100644 --- a/openhcs/core/runtime_exports.py +++ b/openhcs/core/runtime_exports.py @@ -28,7 +28,6 @@ from openhcs.processing.materialization.core import materialization_is_empty if TYPE_CHECKING: - from openhcs.core.context.processing_context import ProcessingContext from openhcs.core.orchestrator.execution_result import RuntimeExecutionObservation @@ -113,26 +112,6 @@ class RuntimeExportObservation: table_row_counts_by_path: Mapping[Path, int] output_files: tuple[Path, ...] = () - @classmethod - def from_execution_contexts( - cls, - execution_contexts: Mapping[str, ProcessingContext], - ) -> RuntimeExportObservation: - """Read contract-owned export paths without retaining runtime values.""" - from openhcs.core.steps.function_artifact_materialization import ( - runtime_export_artifact_output_paths, - ) - - return cls.from_output_paths( - tuple( - path - for context in execution_contexts.values() - for plan in context.step_plans.values() - if plan.owns_runtime_outputs - for path in runtime_export_artifact_output_paths(plan, context) - ) - ) - @classmethod def from_runtime_observations( cls, diff --git a/openhcs/core/steps/abstract.py b/openhcs/core/steps/abstract.py index e82003ec93..19402fa4d3 100644 --- a/openhcs/core/steps/abstract.py +++ b/openhcs/core/steps/abstract.py @@ -8,10 +8,11 @@ import abc import inspect +from pathlib import Path from collections.abc import Mapping -from dataclasses import dataclass, is_dataclass +from dataclasses import dataclass, field, is_dataclass from types import MappingProxyType -from typing import TYPE_CHECKING, get_type_hints +from typing import TYPE_CHECKING, Iterable, get_type_hints from objectstate import get_base_type_for_lazy, semantic_values_equal @@ -49,10 +50,29 @@ class StepExecutionObservation: tuple[RuntimeArtifactLocation, ...], ] + runtime_export_paths: tuple[Path, ...] = field(default_factory=tuple) + @classmethod def empty(cls) -> "StepExecutionObservation": return cls(MappingProxyType({})) + @classmethod + def combine( + cls, observations: Iterable["StepExecutionObservation"] + ) -> "StepExecutionObservation": + locations = {} + paths = [] + for observation in observations: + for ( + address, + values, + ) in observation.materialized_locations_by_address.items(): + locations[address] = tuple( + dict.fromkeys((*locations.get(address, ()), *values)) + ) + paths.extend(observation.runtime_export_paths) + return cls(MappingProxyType(locations), tuple(dict.fromkeys(paths))) + # def get_step_id(step: 'AbstractStep') -> str: # """ diff --git a/openhcs/core/steps/function_artifact_materialization.py b/openhcs/core/steps/function_artifact_materialization.py index 7d4a0da320..7510f0f61d 100644 --- a/openhcs/core/steps/function_artifact_materialization.py +++ b/openhcs/core/steps/function_artifact_materialization.py @@ -6,6 +6,7 @@ from collections.abc import Callable, Mapping from dataclasses import dataclass from pathlib import Path +from types import MappingProxyType from typing import TYPE_CHECKING, ClassVar, cast import numpy as np @@ -50,6 +51,7 @@ with_source_component_metadata, ) from openhcs.core.source_projection import OpenHCSPlaneAddress +from openhcs.core.steps.abstract import StepExecutionObservation from openhcs.core.steps.function_output_identity import ( FunctionOutputIdentity, FunctionOutputIdentityAuthority, @@ -71,9 +73,11 @@ MaterializationSpec, MaterializationValue, Output, + SavedMaterializationOutputs, RawBackendKwargs, ViewerStreamBackendCallKwargs, materialization_outputs, + prepare_materialization, ) if TYPE_CHECKING: @@ -1221,18 +1225,6 @@ def runtime_artifact_materializations( return tuple(materializations) -def observed_runtime_artifact_materializations( - plan: CompiledStepPlan, - context: "ProcessingContext", -) -> tuple[RuntimeArtifactMaterialization, ...]: - """Derive historical materializations from the runtime observation ledger.""" - return runtime_artifact_materializations_from_records( - plan, - context, - context.runtime_value_store.observed_values, - ) - - def runtime_artifact_materializations_from_records( plan: CompiledStepPlan, context: "ProcessingContext", @@ -1271,62 +1263,12 @@ def runtime_artifact_materializations_from_records( return tuple(materializations) -def materialized_artifact_output_paths( - plan: CompiledStepPlan, - context: "ProcessingContext", -) -> tuple[Path, ...]: - """Re-derive exact persistent output paths from runtime materializations.""" - - if not plan.runtime_artifact_materialization.has_persistent_target: - return () - return tuple( - Path(output.path) - for materialization in runtime_artifact_materializations(plan, context) - if materialization.spec.participates_in_persistent_materialization() - for output in materialization.outputs(plan, context) - ) - - -def runtime_export_artifact_output_paths( - plan: CompiledStepPlan, - context: "ProcessingContext", -) -> tuple[Path, ...]: - """Derive exact pipeline-declared export paths from runtime materializations.""" - - if not plan.runtime_artifact_materialization.has_persistent_target: - return () - return tuple( - Path(output.path) - for materialization in runtime_artifact_materializations(plan, context) - if materialization.spec.participates_in_runtime_export_observation() - for output in materialization.outputs(plan, context) - ) - - -def observed_materialized_artifact_output_paths( +def preview_reused_runtime_export_paths( plan: CompiledStepPlan, context: "ProcessingContext", records: tuple[StoredRuntimeValue, ...], ) -> tuple[Path, ...]: - """Derive exact persistent outputs from one execution-owned observation.""" - - return tuple( - Path(location.path) - for locations in observed_materialized_artifact_locations_by_address( - plan, - context, - records, - ).values() - for location in locations - ) - - -def observed_runtime_export_artifact_output_paths( - plan: CompiledStepPlan, - context: "ProcessingContext", - records: tuple[StoredRuntimeValue, ...], -) -> tuple[Path, ...]: - """Derive exact pipeline-declared exports from worker-observed records.""" + """Predict historical debug-reuse exports, independently of new save outcomes.""" if not plan.runtime_artifact_materialization.has_persistent_target: return () @@ -1342,12 +1284,12 @@ def observed_runtime_export_artifact_output_paths( ) -def observed_materialized_artifact_locations_by_address( +def preview_reused_materialized_artifact_locations( plan: CompiledStepPlan, context: "ProcessingContext", records: tuple[StoredRuntimeValue, ...], ) -> Mapping[RuntimeArtifactAddress, tuple[RuntimeArtifactLocation, ...]]: - """Project exact persistent destinations for observed runtime artifacts.""" + """Predict historical debug-reuse destinations from retained logical values.""" if not plan.runtime_artifact_materialization.has_persistent_target: return {} @@ -1432,15 +1374,41 @@ def _planned_materialization_path( ) +@dataclass(frozen=True, slots=True) +class MaterializedRuntimeArtifact(SavedMaterializationOutputs): + """Actual writer outputs saved for one reduced runtime artifact.""" + + materialization: RuntimeArtifactMaterialization + + def observation(self, plan: CompiledStepPlan) -> StepExecutionObservation: + target = plan.runtime_artifact_materialization + if not target.has_persistent_target: + return StepExecutionObservation.empty() + backend = target.require_persistent_backend() + outputs = self.outputs_for_backend(backend) + if not outputs: + return StepExecutionObservation.empty() + address = RuntimeArtifactAddress.from_record(self.materialization.record) + locations = tuple( + RuntimeArtifactLocation(path=output.path, backend=backend) + for output in outputs + ) + paths = ( + tuple(Path(output.path) for output in outputs) + if self.materialization.spec.participates_in_runtime_export_observation() + else () + ) + return StepExecutionObservation(MappingProxyType({address: locations}), paths) + + def materialize_artifact_outputs( filemanager: "FileManager", plan: CompiledStepPlan, target_plan: ArtifactMaterializationTargetPlan, context: "ProcessingContext", -) -> None: - """Materialize planned artifact outputs to persistent and streaming backends.""" - from openhcs.processing.materialization import materialize - +) -> tuple[MaterializedRuntimeArtifact, ...]: + """Save each exact artifact batch once and return its successful outputs.""" + saved_materializations = [] images_dir = plan.artifact_images_dir for materialization in runtime_artifact_materializations(plan, context): @@ -1458,7 +1426,7 @@ def materialize_artifact_outputs( ) if not backends: continue - materialize( + batch = prepare_materialization( materialization.spec, data, str(materialization.base_path), @@ -1488,3 +1456,12 @@ def materialize_artifact_outputs( pipeline_position=plan.pipeline_position, output_plan=materialization.output_plan, ) + saved_materializations.append( + MaterializedRuntimeArtifact( + outputs_by_backend=MappingProxyType( + dict(batch.save().outputs_by_backend) + ), + materialization=materialization, + ) + ) + return tuple(saved_materializations) diff --git a/openhcs/core/steps/function_outputs.py b/openhcs/core/steps/function_outputs.py index 919d4acaa5..eda426ccc8 100644 --- a/openhcs/core/steps/function_outputs.py +++ b/openhcs/core/steps/function_outputs.py @@ -6,9 +6,9 @@ import time from abc import ABC, abstractmethod from collections.abc import Callable, Mapping -from dataclasses import dataclass, replace +from dataclasses import dataclass, field, replace from pathlib import Path -from typing import ClassVar +from typing import ClassVar, TypeVar import numpy as np from metaclass_registry import ( @@ -55,11 +55,12 @@ SourceProjectionMetadataSerializer, SourceProjectionSet, ) +from openhcs.core.steps.abstract import StepExecutionObservation from openhcs.core.steps.function_artifact_materialization import ( + MaterializedRuntimeArtifact, PersistentArtifactMaterializationTargetPlan, StreamingOnlyArtifactMaterializationTargetPlan, materialize_artifact_outputs, - runtime_artifact_materializations, ) from openhcs.core.steps.function_io import ( prepare_storage_image_payloads, @@ -137,50 +138,61 @@ def memory_path( def finalize_function_step_outputs( context: ProcessingContext, plan: CompiledStepPlan, -) -> None: - """Persist images, streams, metadata, and non-image artifacts for one step.""" +) -> StepExecutionObservation: + """Save and publish one step's actual outputs before releasing their payloads.""" if not RuntimeProfileLogger.enabled(): MemoryOutputWriter.write_if_needed(context, plan) MaterializedImageOutputWriter.write_if_needed(context, plan) StreamOutputsAuthority.stream_outputs(context, plan) - RuntimeArtifactMaterializationAuthority.materialize(context, plan) - OpenHCSMetadataWriter.write(context, plan) - return - - _profile_finalization_phase( - "finalize_memory_outputs", - lambda: MemoryOutputWriter.write_if_needed(context, plan), - plan, - ) - _profile_finalization_phase( - "finalize_materialized_images", - lambda: MaterializedImageOutputWriter.write_if_needed(context, plan), - plan, - ) - _profile_finalization_phase( - "finalize_stream_outputs", - lambda: StreamOutputsAuthority.stream_outputs(context, plan), - plan, - ) - _profile_finalization_phase( - "finalize_runtime_artifacts", - lambda: RuntimeArtifactMaterializationAuthority.materialize(context, plan), - plan, - ) - _profile_finalization_phase( - "finalize_openhcs_metadata", - lambda: OpenHCSMetadataWriter.write(context, plan), - plan, + materializations = RuntimeArtifactMaterializationAuthority.materialize( + context, plan + ) + OpenHCSMetadataWriter.write( + context, plan, artifact_materializations=materializations + ) + else: + _profile_finalization_phase( + "finalize_memory_outputs", + lambda: MemoryOutputWriter.write_if_needed(context, plan), + plan, + ) + _profile_finalization_phase( + "finalize_materialized_images", + lambda: MaterializedImageOutputWriter.write_if_needed(context, plan), + plan, + ) + _profile_finalization_phase( + "finalize_stream_outputs", + lambda: StreamOutputsAuthority.stream_outputs(context, plan), + plan, + ) + materializations = _profile_finalization_phase( + "finalize_runtime_artifacts", + lambda: RuntimeArtifactMaterializationAuthority.materialize(context, plan), + plan, + ) + _profile_finalization_phase( + "finalize_openhcs_metadata", + lambda: OpenHCSMetadataWriter.write( + context, plan, artifact_materializations=materializations + ), + plan, + ) + return StepExecutionObservation.combine( + item.observation(plan) for item in materializations ) +_FinalizationResult = TypeVar("_FinalizationResult") + + def _profile_finalization_phase( label: str, - operation: Callable[[], None], + operation: Callable[[], _FinalizationResult], plan: CompiledStepPlan, -) -> None: +) -> _FinalizationResult: started_at = time.perf_counter() - operation() + result = operation() RuntimeProfileLogger.log( logger, label, @@ -189,6 +201,7 @@ def _profile_finalization_phase( step_name=plan.step_name, axis_id=plan.axis_id, ) + return result class MemoryOutputWriter: @@ -691,6 +704,9 @@ class OutputTarget(ABC, metaclass=AutoRegisterMeta): plate_root: str sub_dir: str results_dir: str | None + artifact_materializations: tuple[MaterializedRuntimeArtifact, ...] = field( + default=(), compare=False, hash=False, repr=False + ) @classmethod @abstractmethod @@ -719,17 +735,25 @@ def from_execution( @classmethod def for_execution( - cls, context: ProcessingContext, plan: CompiledStepPlan + cls, + context: ProcessingContext, + plan: CompiledStepPlan, + *, + artifact_materializations: tuple[MaterializedRuntimeArtifact, ...] = (), ) -> tuple[OpenHCSMetadataWriter.OutputTarget, ...]: return tuple( target for declaration in cls.__registry__.values() if (owner := declaration.from_execution(context, plan)) is not None - for target in owner.production_targets(context, plan) + for target in replace( + owner, artifact_materializations=artifact_materializations + ).production_targets(context, plan) ) def production_targets( - self, context: ProcessingContext, plan: CompiledStepPlan + self, + context: ProcessingContext, + plan: CompiledStepPlan, ) -> tuple[OpenHCSMetadataWriter.OutputTarget, ...]: """Project this declaration's exact storage destinations for the step.""" return (self,) @@ -747,7 +771,9 @@ def produced_records( return () def runtime_artifact_projection_paths( - self, context: ProcessingContext, plan: CompiledStepPlan + self, + context: ProcessingContext, + plan: CompiledStepPlan, ) -> tuple[tuple[SourceArtifactProjection, str], ...]: """Publish artifacts persisted in this declared storage target.""" materialization = plan.runtime_artifact_materialization @@ -820,7 +846,9 @@ def produced_projection_metadata( if context.filemanager is None: raise ValueError("OpenHCS metadata requires a file manager.") target = type(self).from_plan(plan) - if target is None or self not in target.production_targets(context, plan): + if target is None or self not in replace( + target, artifact_materializations=self.artifact_materializations + ).production_targets(context, plan): raise ValueError( "Produced metadata plan does not own this output target." ) @@ -910,16 +938,9 @@ def project_runtime_artifacts( """Project persisted image artifacts into the target source authority.""" projection_paths = [] - for materialization in runtime_artifact_materializations(plan, context): - if ( - not materialization.spec.participates_in_persistent_materialization() - ): - continue - for output in materialization.outputs( - plan, - context, - output_path_filter=ImageFileFormat.is_image_path, - ): + for saved_artifact in self.artifact_materializations: + materialization = saved_artifact.materialization + for output in saved_artifact.outputs_for_backend(self.backend): if not ImageFileFormat.is_image_path(output.path) or Path( output.path ).parent != Path(self.output_dir): @@ -1005,8 +1026,12 @@ def write( cls, context: ProcessingContext, plan: CompiledStepPlan, + *, + artifact_materializations: tuple[MaterializedRuntimeArtifact, ...] = (), ) -> None: - for target in cls.OutputTarget.for_execution(context, plan): + for target in cls.OutputTarget.for_execution( + context, plan, artifact_materializations=artifact_materializations + ): if not plan.create_openhcs_metadata: structured_metadata = target.produced_projection_metadata(context, plan) if structured_metadata is None: @@ -1124,23 +1149,21 @@ class RuntimeArtifactMetadataTarget(OpenHCSMetadataWriter.OutputTarget): """Image artifacts persist independently of main-flow image/checkpoint storage.""" def production_targets( - self, context: ProcessingContext, plan: CompiledStepPlan + self, + context: ProcessingContext, + plan: CompiledStepPlan, ) -> tuple[RuntimeArtifactMetadataTarget, ...]: """Derive directories from the same writer outputs used to save artifacts.""" directories = dict.fromkeys( Path(output.path).parent - for materialization in runtime_artifact_materializations(plan, context) - if materialization.spec.participates_in_persistent_materialization() - for output in materialization.outputs( - plan, context, output_path_filter=ImageFileFormat.is_image_path - ) + for materialization in self.artifact_materializations + for output in materialization.outputs_for_backend(self.backend) if ImageFileFormat.is_image_path(output.path) ) return tuple( target for directory in directories for target in (self.for_directory(directory),) - if target.contains_images(context) ) def reconciliation_targets( @@ -1193,27 +1216,28 @@ def materialize( cls, context: ProcessingContext, plan: CompiledStepPlan, - ) -> None: + ) -> tuple[MaterializedRuntimeArtifact, ...]: if not plan.artifact_outputs: - return + return () materialization_plan = plan.runtime_artifact_materialization has_persistent_target = materialization_plan.has_persistent_target has_streaming_target = bool(plan.streaming_configs) if not has_persistent_target and not has_streaming_target: logger.info("Skipping runtime artifact materialization and streaming") - return + return () logger.info( "Starting materialization for %s artifact outputs", len(plan.artifact_outputs), ) - materialize_artifact_outputs( + materializations = materialize_artifact_outputs( context.filemanager, plan, cls.target_plan(materialization_plan), context, ) logger.info("Completed artifact materialization") + return materializations @staticmethod def target_plan( diff --git a/openhcs/processing/materialization/__init__.py b/openhcs/processing/materialization/__init__.py index 4e920f0ee9..c161998f13 100644 --- a/openhcs/processing/materialization/__init__.py +++ b/openhcs/processing/materialization/__init__.py @@ -6,11 +6,14 @@ ) from openhcs.processing.materialization.core import ( BackendSaver, + MaterializationBatch, MaterializationContext, MaterializationSpec, Output, + SavedMaterializationOutputs, PathHelper, materialize, + prepare_materialization, materialization_outputs, registered_materialization_option_types, tabular_field_names_from_materialization, @@ -57,11 +60,14 @@ "MaterializationSpec", "TerminalMaterializationSpec", "StreamingOnlyMaterializationSpec", + "MaterializationBatch", "MaterializationContext", "Output", + "SavedMaterializationOutputs", "PathHelper", "BackendSaver", "materialize", + "prepare_materialization", "materialization_outputs", "registered_materialization_option_types", "FileOutputOptions", diff --git a/openhcs/processing/materialization/core.py b/openhcs/processing/materialization/core.py index faff29684a..a095267a20 100644 --- a/openhcs/processing/materialization/core.py +++ b/openhcs/processing/materialization/core.py @@ -17,6 +17,7 @@ from dataclasses import dataclass, field, is_dataclass, replace from functools import lru_cache, singledispatch from pathlib import Path, PurePosixPath, PureWindowsPath +from types import MappingProxyType from typing import TYPE_CHECKING, ClassVar, TypeAlias import numpy as np @@ -1651,7 +1652,8 @@ def __init__( def save_all( self, outputs: Sequence[Output], - ) -> None: + ) -> Mapping[str, tuple[Output, ...]]: + saved_outputs: dict[str, tuple[Output, ...]] = {} for backend in self.backends: backend_instance = self.filemanager._get_backend(backend) output_acceptance = tuple( @@ -1690,6 +1692,8 @@ def save_all( backend, **kwargs, ) + saved_outputs[backend] = supported_outputs + return saved_outputs def _prepare_path(self, backend: str, backend_instance, path: str) -> None: if not backend_instance.requires_filesystem_validation: @@ -4129,28 +4133,69 @@ def validate(spec: MaterializationSpec, backends: list[str]) -> None: ) -def _materialization_output_groups( - spec: MaterializationSpec, - data: MaterializationValue, - context: MaterializationContext, - *, - output_path_filter: Callable[[Path], bool] | None = None, -) -> tuple[tuple[WriterSpec, tuple[Output, ...]], ...]: - """Render requested outputs through each writer's declared path projection.""" +@dataclass(frozen=True) +class SavedMaterializationOutputs: + """Backend-indexed outputs from completed saves, independent of artifact kind.""" - return tuple( - ( - writer, - tuple( - output.with_source_identity_fallback( - context.artifact_source_identity - ).with_variable_components(context.variable_components) - for output in writer.outputs(data, options, context, output_path_filter) - ), + outputs_by_backend: Mapping[str, tuple[Output, ...]] + + def outputs_for_backend(self, backend: str) -> tuple[Output, ...]: + return self.outputs_by_backend.get(backend, ()) + + +@dataclass(frozen=True) +class MaterializationBatch: + """One rendered writer batch shared by saving and immediate publication.""" + + context: MaterializationContext + output_groups: tuple[tuple[WriterSpec, tuple[Output, ...]], ...] + primary: int + + @classmethod + def render( + cls, + spec: MaterializationSpec, + data: MaterializationValue, + context: MaterializationContext, + *, + output_path_filter: Callable[[Path], bool] | None = None, + ) -> MaterializationBatch: + groups = tuple( + ( + writer, + tuple( + output.with_source_identity_fallback( + context.artifact_source_identity + ).with_variable_components(context.variable_components) + for output in writer.outputs( + data, options, context, output_path_filter + ) + ), + ) + for options in spec.outputs + for writer in (_WRITERS_BY_OPTIONS[options.__class__],) + ) + return cls(context=context, output_groups=groups, primary=spec.primary) + + @property + def outputs(self) -> tuple[Output, ...]: + return tuple( + output for _writer, outputs in self.output_groups for output in outputs ) - for options in spec.outputs - for writer in (_WRITERS_BY_OPTIONS[options.__class__],) - ) + + @property + def primary_path(self) -> str: + writer, outputs = self.output_groups[self.primary] + return writer.primary_path(list(outputs)) + + def save(self) -> SavedMaterializationOutputs: + """Return only outputs accepted and successfully saved by each backend.""" + saved: dict[str, tuple[Output, ...]] = {} + saver = self.context.saver + for _writer, outputs in self.output_groups: + for backend, saved_outputs in saver.save_all(outputs).items(): + saved[backend] = (*saved.get(backend, ()), *saved_outputs) + return SavedMaterializationOutputs(MappingProxyType(saved)) def materialization_outputs( @@ -4190,19 +4235,15 @@ def materialization_outputs( pipeline_position=pipeline_position, output_plan=output_plan, ) - return tuple( - output - for _writer, outputs in _materialization_output_groups( - spec, - data, - materialization_context, - output_path_filter=output_path_filter, - ) - for output in outputs - ) + return MaterializationBatch.render( + spec, + data, + materialization_context, + output_path_filter=output_path_filter, + ).outputs -def materialize( +def prepare_materialization( spec: MaterializationSpec, data: MaterializationValue, path: str, @@ -4218,8 +4259,8 @@ def materialize( source_paths: Sequence[str] = (), pipeline_position: int | None = None, output_plan: ArtifactOutputPlan | None = None, -) -> str: - """Materialize data to one or more backends.""" +) -> MaterializationBatch: + """Prepare one exact batch for saving and immediate publication.""" normalized_backends = BackendSequenceAuthority.normalize(backends) AllowedBackendsAuthority.validate(spec, normalized_backends) @@ -4245,11 +4286,42 @@ def materialize( output_plan=output_plan, ) - primary_path = "" + return MaterializationBatch.render(spec, data, ctx) - for i, (writer, outs) in enumerate(_materialization_output_groups(spec, data, ctx)): - ctx.saver.save_all(outs) - if i == spec.primary: - primary_path = writer.primary_path(list(outs)) - return primary_path +def materialize( + spec: MaterializationSpec, + data: MaterializationValue, + path: str, + filemanager: FileManager, + backends: Sequence[str] | str, + backend_kwargs: BackendKwargsInput = BACKEND_KWARGS_ABSENT, + context: ProcessingContext | None = None, + extra_inputs: dict | None = None, + *, + artifact_source_identity: SourceImageIdentity | None = None, + artifact_filename_identity: SourceImageIdentity | None = None, + variable_components: Sequence[VariableComponents] = (), + source_paths: Sequence[str] = (), + pipeline_position: int | None = None, + output_plan: ArtifactOutputPlan | None = None, +) -> str: + """Materialize data and return the primary path derived from its writer batch.""" + batch = prepare_materialization( + spec, + data, + path, + filemanager, + backends, + backend_kwargs, + context=context, + extra_inputs=extra_inputs, + artifact_source_identity=artifact_source_identity, + artifact_filename_identity=artifact_filename_identity, + variable_components=variable_components, + source_paths=source_paths, + pipeline_position=pipeline_position, + output_plan=output_plan, + ) + batch.save() + return batch.primary_path diff --git a/openhcs/runtime/zmq_execution_observation.py b/openhcs/runtime/zmq_execution_observation.py index fecffce412..f75dc3072d 100644 --- a/openhcs/runtime/zmq_execution_observation.py +++ b/openhcs/runtime/zmq_execution_observation.py @@ -9,7 +9,11 @@ from pathlib import Path from openhcs.core.context.processing_context import ProcessingContext -from openhcs.core.orchestrator.execution_result import ExecutionResult, ExecutionStatus +from openhcs.core.orchestrator.execution_result import ( + ExecutionResult, + RuntimeExecutionObservation, + ExecutionStatus, +) from openhcs.core.runtime_execution_validation import ( RuntimeArtifactExecutionExpectation, RuntimeArtifactExecutionObservation, @@ -215,11 +219,12 @@ def from_execution( compiled_contexts: Mapping[str, ProcessingContext], execution_results: Mapping[str, ExecutionResult], output_roots: tuple[Path, ...], + runtime_observations: tuple[RuntimeExecutionObservation, ...], server_environment: RuntimeEnvironmentSnapshot | None = None, execution_id: str | None = None, ) -> ZMQRuntimeExecutionObservationExport: observation = RuntimeArtifactExecutionObservation.from_contexts( - compiled_contexts + compiled_contexts, runtime_observations=runtime_observations ) return cls( schema_version=ZMQ_RUNTIME_OBSERVATION_EXPORT_SCHEMA_VERSION, diff --git a/openhcs/runtime/zmq_execution_server.py b/openhcs/runtime/zmq_execution_server.py index ce858a0a2f..0c2bcce4d5 100644 --- a/openhcs/runtime/zmq_execution_server.py +++ b/openhcs/runtime/zmq_execution_server.py @@ -770,17 +770,8 @@ def _export_runtime_observation( is ZMQRuntimeObservationExportScope.OUTCOMES ): observed_exports = RuntimeExportObservation.from_runtime_observations( - tuple( - result.runtime_observation for result in execution_results.values() - ) + execution_results.runtime_observations ) - if execution_bundle.requires_parent_runtime_observation: - parent_exports = RuntimeExportObservation.from_execution_contexts( - execution_bundle.runtime_contexts - ) - observed_exports = RuntimeExportObservation.from_output_paths( - (*observed_exports.output_files, *parent_exports.output_files) - ) export = ZMQRuntimeExecutionOutcomeExport.from_execution( compiled_axis_ids=execution_bundle.runtime_contexts, execution_results=execution_results, @@ -793,6 +784,7 @@ def _export_runtime_observation( export = ZMQRuntimeExecutionObservationExport.from_execution( compiled_contexts=execution_bundle.runtime_contexts, execution_results=execution_results, + runtime_observations=execution_results.runtime_observations, output_roots=output_roots, server_environment=self._server_environment, execution_id=request_context.execution_id, diff --git a/tests/unit/test_function_artifact_materialization.py b/tests/unit/test_function_artifact_materialization.py index bca6321c5a..2a889182ae 100644 --- a/tests/unit/test_function_artifact_materialization.py +++ b/tests/unit/test_function_artifact_materialization.py @@ -119,13 +119,10 @@ StreamingOnlyArtifactMaterializationTargetPlan, actual_materialization_records, materialize_artifact_outputs, - materialized_artifact_output_paths, - observed_materialized_artifact_locations_by_address, - observed_materialized_artifact_output_paths, + preview_reused_materialized_artifact_locations, planned_materialization_preview, runtime_artifact_materializations, runtime_artifact_materializations_from_records, - runtime_export_artifact_output_paths, ) from openhcs.core.steps.function_output_identity import ( IncompleteFunctionOutputFilenameIdentityError, @@ -155,6 +152,7 @@ from openhcs.processing.materialization.core import ( MaterializationSpec, Output, + SavedMaterializationOutputs, materialization_outputs, ) from openhcs.processing.materialization.options import ( @@ -789,10 +787,10 @@ def test_materialize_artifact_outputs_uses_runtime_store_payload( def fake_materialize(_spec, data, path, *_args, **_kwargs): materialized.append((data, path)) - return path + return SimpleNamespace(save=lambda: SavedMaterializationOutputs({})) monkeypatch.setattr( - "openhcs.processing.materialization.materialize", + "openhcs.core.steps.function_artifact_materialization.prepare_materialization", fake_materialize, ) @@ -839,10 +837,10 @@ def test_materialize_artifact_outputs_attaches_image_schema_provenance(monkeypat def fake_materialize(_spec, data, path, *_args, **_kwargs): materialized.append((data, path)) - return path + return SimpleNamespace(save=lambda: SavedMaterializationOutputs({})) monkeypatch.setattr( - "openhcs.processing.materialization.materialize", + "openhcs.core.steps.function_artifact_materialization.prepare_materialization", fake_materialize, ) @@ -922,10 +920,10 @@ def fake_materialize(spec, data, path, manager, *_args, **kwargs): variable_components=kwargs["variable_components"], ) ) - return output_paths[0] + return SimpleNamespace(save=lambda: SavedMaterializationOutputs({})) monkeypatch.setattr( - "openhcs.processing.materialization.materialize", + "openhcs.core.steps.function_artifact_materialization.prepare_materialization", fake_materialize, ) @@ -940,7 +938,14 @@ def fake_materialize(spec, data, path, manager, *_args, **kwargs): expected_paths = tuple( Path(f"/images/A01_s{site:03d}_w1_z001_t001_saved.tif") for site in range(1, 4) ) - assert materialized_artifact_output_paths(plan, context) == expected_paths + assert ( + tuple( + Path(output.path) + for materialization in runtime_artifact_materializations(plan, context) + for output in materialization.outputs(plan, context) + ) + == expected_paths + ) materialize_artifact_outputs( filemanager, @@ -990,10 +995,10 @@ def test_materialize_artifact_outputs_does_not_require_vfs_payload_for_store_rec def fake_materialize(_spec, data, path, *_args, **_kwargs): materialized.append((data, path)) - return path + return SimpleNamespace(save=lambda: SavedMaterializationOutputs({})) monkeypatch.setattr( - "openhcs.processing.materialization.materialize", + "openhcs.core.steps.function_artifact_materialization.prepare_materialization", fake_materialize, ) @@ -1045,10 +1050,10 @@ def test_materialize_artifact_outputs_uses_runtime_record_identity_not_final_pat def fake_materialize(_spec, data, path, *_args, **_kwargs): materialized.append((data, path)) - return path + return SimpleNamespace(save=lambda: SavedMaterializationOutputs({})) monkeypatch.setattr( - "openhcs.processing.materialization.materialize", + "openhcs.core.steps.function_artifact_materialization.prepare_materialization", fake_materialize, ) @@ -1099,10 +1104,10 @@ def test_materialize_artifact_outputs_uses_declared_measurement_csv_spec( def fake_materialize(spec, data, path, *_args, **_kwargs): materialized.append((spec, data, path)) - return path + return SimpleNamespace(save=lambda: SavedMaterializationOutputs({})) monkeypatch.setattr( - "openhcs.processing.materialization.materialize", + "openhcs.core.steps.function_artifact_materialization.prepare_materialization", fake_materialize, ) @@ -1711,17 +1716,19 @@ def record_for_site(site: str) -> None: current_execution_records = context.runtime_value_store.observed_values_after( current_execution_cursor ) - assert observed_materialized_artifact_output_paths( - plan, - context, - current_execution_records, + assert tuple( + Path(location.path) + for locations in preview_reused_materialized_artifact_locations( + plan, context, current_execution_records + ).values() + for location in locations ) == ( Path( "/analysis/A01_site-2_z_index-1_timepoint-1_" "cell_counts_step7_details.csv" ), ) - assert observed_materialized_artifact_locations_by_address( + assert preview_reused_materialized_artifact_locations( plan, context, current_execution_records, @@ -1794,7 +1801,7 @@ def test_terminal_persistence_is_reported_without_becoming_declared_export() -> persistent_backend="disk", ) - locations = observed_materialized_artifact_locations_by_address( + locations = preview_reused_materialized_artifact_locations( plan, context, (record,), @@ -1808,7 +1815,6 @@ def test_terminal_persistence_is_reported_without_becoming_declared_export() -> ), ), } - assert runtime_export_artifact_output_paths(plan, context) == () assert not output_plan.materialization.participates_in_runtime_export_observation() @@ -1929,10 +1935,10 @@ def test_materialize_artifact_outputs_unions_measurement_subject_records( def fake_materialize(_spec, data, path, *_args, **_kwargs): materialized.append((tuple(data.iter_row_mappings()), path)) - return path + return SimpleNamespace(save=lambda: SavedMaterializationOutputs({})) monkeypatch.setattr( - "openhcs.processing.materialization.materialize", + "openhcs.core.steps.function_artifact_materialization.prepare_materialization", fake_materialize, ) @@ -2232,10 +2238,10 @@ def fake_materialize( **_kwargs, ): materialized.append((spec, data, path, backends, backend_kwargs)) - return path + return SimpleNamespace(save=lambda: SavedMaterializationOutputs({})) monkeypatch.setattr( - "openhcs.processing.materialization.materialize", + "openhcs.core.steps.function_artifact_materialization.prepare_materialization", fake_materialize, ) @@ -2298,10 +2304,10 @@ def test_materialize_artifact_outputs_uses_actual_group_records(monkeypatch): def fake_materialize(spec, data, path, *_args, **_kwargs): materialized.append((spec, data, path)) - return path + return SimpleNamespace(save=lambda: SavedMaterializationOutputs({})) monkeypatch.setattr( - "openhcs.processing.materialization.materialize", + "openhcs.core.steps.function_artifact_materialization.prepare_materialization", fake_materialize, ) @@ -2578,10 +2584,10 @@ def test_materialize_artifact_outputs_uses_group_measurement_artifact_identity( def fake_materialize(spec, data, path, *_args, **_kwargs): materialized.append((spec, data, path)) - return path + return SimpleNamespace(save=lambda: SavedMaterializationOutputs({})) monkeypatch.setattr( - "openhcs.processing.materialization.materialize", + "openhcs.core.steps.function_artifact_materialization.prepare_materialization", fake_materialize, ) @@ -2642,10 +2648,10 @@ def test_materialize_artifact_outputs_keeps_grouped_artifact_record_path( def fake_materialize(spec, data, path, *_args, **_kwargs): materialized.append((spec, data, path)) - return path + return SimpleNamespace(save=lambda: SavedMaterializationOutputs({})) monkeypatch.setattr( - "openhcs.processing.materialization.materialize", + "openhcs.core.steps.function_artifact_materialization.prepare_materialization", fake_materialize, ) @@ -2708,10 +2714,10 @@ def fake_materialize( **_kwargs, ): materialized.append((spec, data, path, backends, backend_kwargs)) - return path + return SimpleNamespace(save=lambda: SavedMaterializationOutputs({})) monkeypatch.setattr( - "openhcs.processing.materialization.materialize", + "openhcs.core.steps.function_artifact_materialization.prepare_materialization", fake_materialize, ) @@ -2778,10 +2784,10 @@ def fake_materialize( **_kwargs, ): materialized.append((spec, data, path, backends, backend_kwargs)) - return path + return SimpleNamespace(save=lambda: SavedMaterializationOutputs({})) monkeypatch.setattr( - "openhcs.processing.materialization.materialize", + "openhcs.core.steps.function_artifact_materialization.prepare_materialization", fake_materialize, ) @@ -2928,10 +2934,10 @@ def test_materialize_artifact_outputs_uses_declared_metadata_json_spec( def fake_materialize(spec, data, path, *_args, **_kwargs): materialized.append((spec, data, path)) - return path + return SimpleNamespace(save=lambda: SavedMaterializationOutputs({})) monkeypatch.setattr( - "openhcs.processing.materialization.materialize", + "openhcs.core.steps.function_artifact_materialization.prepare_materialization", fake_materialize, ) @@ -2962,9 +2968,10 @@ def test_materialize_artifact_outputs_skips_special_without_explicit_spec( def fake_materialize(*args, **kwargs): materialized.append((args, kwargs)) + return SimpleNamespace(save=lambda: SavedMaterializationOutputs({})) monkeypatch.setattr( - "openhcs.processing.materialization.materialize", + "openhcs.core.steps.function_artifact_materialization.prepare_materialization", fake_materialize, ) @@ -3057,9 +3064,10 @@ def test_materialize_artifact_outputs_skips_explicitly_disabled_artifact_without def fake_materialize(*args, **kwargs): materialized.append((args, kwargs)) + return SimpleNamespace(save=lambda: SavedMaterializationOutputs({})) monkeypatch.setattr( - "openhcs.processing.materialization.materialize", + "openhcs.core.steps.function_artifact_materialization.prepare_materialization", fake_materialize, ) @@ -3097,10 +3105,10 @@ def test_materialize_artifact_outputs_uses_declared_object_labels_roi_spec(monke def fake_materialize(spec, data, path, *_args, **_kwargs): materialized.append((spec, data, path)) - return path + return SimpleNamespace(save=lambda: SavedMaterializationOutputs({})) monkeypatch.setattr( - "openhcs.processing.materialization.materialize", + "openhcs.core.steps.function_artifact_materialization.prepare_materialization", fake_materialize, ) @@ -3154,10 +3162,10 @@ def fake_materialize( **_kwargs, ): materialized.append((spec, data, path, backends, backend_kwargs)) - return path + return SimpleNamespace(save=lambda: SavedMaterializationOutputs({})) monkeypatch.setattr( - "openhcs.processing.materialization.materialize", + "openhcs.core.steps.function_artifact_materialization.prepare_materialization", fake_materialize, ) @@ -3243,9 +3251,10 @@ def fake_materialize( **_kwargs, ): materialized.append((backends, backend_kwargs)) + return SimpleNamespace(save=lambda: SavedMaterializationOutputs({})) monkeypatch.setattr( - "openhcs.processing.materialization.materialize", + "openhcs.core.steps.function_artifact_materialization.prepare_materialization", fake_materialize, ) @@ -3307,10 +3316,10 @@ def fake_materialize( **_kwargs, ): materialized.append((spec, data, path, backends, backend_kwargs)) - return path + return SimpleNamespace(save=lambda: SavedMaterializationOutputs({})) monkeypatch.setattr( - "openhcs.processing.materialization.materialize", + "openhcs.core.steps.function_artifact_materialization.prepare_materialization", fake_materialize, ) @@ -3389,10 +3398,10 @@ def fake_materialize( **_kwargs, ): materialized.append((spec, data, path, backends, backend_kwargs)) - return path + return SimpleNamespace(save=lambda: SavedMaterializationOutputs({})) monkeypatch.setattr( - "openhcs.processing.materialization.materialize", + "openhcs.core.steps.function_artifact_materialization.prepare_materialization", fake_materialize, ) @@ -3451,10 +3460,10 @@ def test_materialize_artifact_outputs_uses_runtime_plane_group_identity( def fake_materialize(_spec, data, path, *_args, **_kwargs): materialized.append((data, path)) - return path + return SimpleNamespace(save=lambda: SavedMaterializationOutputs({})) monkeypatch.setattr( - "openhcs.processing.materialization.materialize", + "openhcs.core.steps.function_artifact_materialization.prepare_materialization", fake_materialize, ) @@ -3522,10 +3531,10 @@ def fake_materialize( **_kwargs, ): materialized.append((spec, data, path, backends, backend_kwargs)) - return path + return SimpleNamespace(save=lambda: SavedMaterializationOutputs({})) monkeypatch.setattr( - "openhcs.processing.materialization.materialize", + "openhcs.core.steps.function_artifact_materialization.prepare_materialization", fake_materialize, ) @@ -3623,10 +3632,10 @@ def fake_materialize( **_kwargs, ): materialized.append((spec, data, path, backends, backend_kwargs)) - return path + return SimpleNamespace(save=lambda: SavedMaterializationOutputs({})) monkeypatch.setattr( - "openhcs.processing.materialization.materialize", + "openhcs.core.steps.function_artifact_materialization.prepare_materialization", fake_materialize, ) @@ -3795,10 +3804,10 @@ def fake_materialize( **_kwargs, ): materialized.append((spec, data, path, backends, backend_kwargs)) - return path + return SimpleNamespace(save=lambda: SavedMaterializationOutputs({})) monkeypatch.setattr( - "openhcs.processing.materialization.materialize", + "openhcs.core.steps.function_artifact_materialization.prepare_materialization", fake_materialize, ) @@ -3898,10 +3907,10 @@ def fake_materialize( **_kwargs, ): materialized.append((spec, data, path, backends, backend_kwargs)) - return path + return SimpleNamespace(save=lambda: SavedMaterializationOutputs({})) monkeypatch.setattr( - "openhcs.processing.materialization.materialize", + "openhcs.core.steps.function_artifact_materialization.prepare_materialization", fake_materialize, ) @@ -3984,10 +3993,10 @@ def test_materialize_image_uses_declared_filename_source_identity(monkeypatch): def fake_materialize(_spec, _data, path, *_args, **_kwargs): materialized.append(path) - return path + return SimpleNamespace(save=lambda: SavedMaterializationOutputs({})) monkeypatch.setattr( - "openhcs.processing.materialization.materialize", + "openhcs.core.steps.function_artifact_materialization.prepare_materialization", fake_materialize, ) @@ -4144,7 +4153,11 @@ def test_compiled_z_axis_reaches_source_named_image_materialization() -> None: assert image_payload_metadata(saved_payload).plane_axis is ( RuntimePlaneAxis.RUNTIME_SLICE ) - assert materialized_artifact_output_paths(plan, context) == ( + assert tuple( + Path(output.path) + for materialization in runtime_artifact_materializations(plan, context) + for output in materialization.outputs(plan, context) + ) == ( Path("/images/A01_s001_w2_z001_t001_NucleiLabels.tif"), Path("/images/A01_s001_w2_z002_t001_NucleiLabels.tif"), ) @@ -4214,10 +4227,10 @@ def fake_materialize( **_kwargs, ): materialized.append((spec, data, path, backends, backend_kwargs)) - return path + return SimpleNamespace(save=lambda: SavedMaterializationOutputs({})) monkeypatch.setattr( - "openhcs.processing.materialization.materialize", + "openhcs.core.steps.function_artifact_materialization.prepare_materialization", fake_materialize, ) diff --git a/tests/unit/test_function_outputs.py b/tests/unit/test_function_outputs.py index 3e2a76a8fc..a97b9bbb33 100644 --- a/tests/unit/test_function_outputs.py +++ b/tests/unit/test_function_outputs.py @@ -1,5 +1,6 @@ import json from pathlib import Path +from dataclasses import replace from types import SimpleNamespace import numpy as np @@ -39,6 +40,7 @@ from openhcs.core.runtime_image_values import ( ImageMetadataPayload, ImagePayloadMetadata, + image_payload_metadata, ) from openhcs.core.runtime_plane_projection import RuntimePlaneAxis from openhcs.core.source_image_provenance import ( @@ -66,6 +68,10 @@ ProducedOutputSemantics, step_output_manifest, ) +from openhcs.core.steps.function_artifact_materialization import ( + MaterializedRuntimeArtifact, + RuntimeArtifactMaterialization, +) from openhcs.core.steps.function_outputs import ( MaterializedImageOutputWriter, MemoryOutputWriter, @@ -97,6 +103,7 @@ MaterializedFilenameIdentity, ) + @pytest.mark.parametrize("backend", [Backend.ZARR.value, "custom-array-store"]) def test_memory_output_writer_projects_runtime_image_payload_for_array_storage( backend, @@ -137,7 +144,9 @@ def test_function_step_metadata_follows_runtime_artifact_persistence( monkeypatch.setattr( authority, method_name, - lambda _context, _plan, event=event: events.append(event), + lambda _context, _plan, event=event, **_kwargs: (events.append(event), ())[ + 1 + ], ) finalize_function_step_outputs(context, plan) @@ -1371,10 +1380,13 @@ def test_image_persistence_skips_object_label_main_flow_payloads(): assert ProducedMemoryPathsAuthority.paths(context, plan) == [] -def test_metadata_target_family_discovers_new_declaration_without_consumer_edits(tmp_path): +def test_metadata_target_family_discovers_new_declaration_without_consumer_edits( + tmp_path, +): registry = OpenHCSMetadataWriter.OutputTarget.__registry__ original_keys = set(registry) try: + class SupplementalImageMetadataTarget(OpenHCSMetadataWriter.OutputTarget): @classmethod def from_plan(cls, plan): @@ -1391,7 +1403,9 @@ def from_plan(cls, plan): context = context_stub(FileManagerStub({})) (target,) = OpenHCSMetadataWriter.OutputTarget.for_plan(plan) assert type(target) is SupplementalImageMetadataTarget - assert OpenHCSMetadataWriter.OutputTarget.for_execution(context, plan) == (target,) + assert OpenHCSMetadataWriter.OutputTarget.for_execution(context, plan) == ( + target, + ) assert target.produced_projection_metadata(context, plan) is None finally: for key in set(registry) - original_keys: @@ -1638,13 +1652,16 @@ def test_runtime_image_artifact_projects_persisted_source_binding( ), outputs=lambda _plan, _context, **_kwargs: (output,), ) - monkeypatch.setattr( - "openhcs.core.steps.function_outputs.runtime_artifact_materializations", - lambda _plan, _context: (materialization,), + saved_artifacts = ( + MaterializedRuntimeArtifact( + outputs_by_backend={Backend.DISK.value: (output,)}, + materialization=materialization, + ), ) target = MaterializedImageMetadataTarget.from_plan(plan) assert target is not None + target = replace(target, artifact_materializations=saved_artifacts) [(projection, virtual_path)] = target.runtime_artifact_projection_paths( context, plan ) @@ -1755,9 +1772,11 @@ def test_runtime_multiplane_label_artifact_projects_persisted_source_binding( ), outputs=lambda _plan, _context, **_kwargs: (output,), ) - monkeypatch.setattr( - "openhcs.core.steps.function_outputs.runtime_artifact_materializations", - lambda _plan, _context: (materialization,), + saved_artifacts = ( + MaterializedRuntimeArtifact( + outputs_by_backend={Backend.DISK.value: (output,)}, + materialization=materialization, + ), ) output_plan = ArtifactOutputPlan( @@ -1769,6 +1788,7 @@ def test_runtime_multiplane_label_artifact_projects_persisted_source_binding( plan.artifact_outputs = {output_plan.ref(): output_plan} target = RuntimeArtifactMetadataTarget.from_plan(plan) assert target is not None + target = replace(target, artifact_materializations=saved_artifacts) [(projection, virtual_path)] = target.runtime_artifact_projection_paths( context, plan ) @@ -1933,10 +1953,12 @@ def test_produced_address_publication_never_parses_generated_filenames( output_dir = plate_root / "images" output_dir.mkdir(parents=True) parser = SourceSchemaFilenameParser() - filemanager = FileManager({ - Backend.DISK.value: DiskStorageBackend(), - Backend.MEMORY.value: MemoryStorageBackend(), - }) + filemanager = FileManager( + { + Backend.DISK.value: DiskStorageBackend(), + Backend.MEMORY.value: MemoryStorageBackend(), + } + ) context = context_stub(filemanager, parser=parser) context.metadata_cache = { AllComponents.WELL: {well: None, "not-produced": None}, @@ -1962,12 +1984,17 @@ def test_produced_address_publication_never_parses_generated_filenames( metadata = ImagePayloadMetadata( source_path=source_path, source_component_metadata={ - "well": well, "site": 1, "channel": 1, - "z_index": z_index, "timepoint": 1, + "well": well, + "site": 1, + "channel": 1, + "z_index": z_index, + "timepoint": 1, }, source_voxel_spacing=SourceVoxelSpacing((0.5, 0.5)), ) - identity = FunctionOutputIdentityAuthority.identity_from_metadata(parser, metadata) + identity = FunctionOutputIdentityAuthority.identity_from_metadata( + parser, metadata + ) assert identity is not None assert identity.extension == extension identity = identity.with_filename_qualifier("centre_dots") @@ -1980,14 +2007,20 @@ def test_produced_address_publication_never_parses_generated_filenames( assert parsed.value_for(AllComponents.WELL) == well path = output_dir / filename tifffile.imwrite(path, pixels) - filemanager.save(metadata.payload_with(pixels, None), str(path), Backend.MEMORY.value) - records.append(ProducedOutputSemantics.from_output( - plan, path, identity, - output_context=AlignedImageSliceContext.main_flow( - output_key="centre_dots", artifact_kind=ImageArtifactType.value - ), - image_metadata=metadata, - )) + filemanager.save( + metadata.payload_with(pixels, None), str(path), Backend.MEMORY.value + ) + records.append( + ProducedOutputSemantics.from_output( + plan, + path, + identity, + output_context=AlignedImageSliceContext.main_flow( + output_key="centre_dots", artifact_kind=ImageArtifactType.value + ), + image_metadata=metadata, + ) + ) paths.append(f"images/{filename}") step_output_manifest(context).record_outputs(plan, records) @@ -2029,11 +2062,15 @@ def reject_generated_path_parse(_filename): removed = paths[0] (plate_root / removed).unlink() OpenHCSMetadataWriter.finalize_completed_plate({well: context}) - reconciled = json.loads(metadata_path.read_text())[FIELDS.SUBDIRECTORIES]["images"] + reconciled = json.loads(metadata_path.read_text())[FIELDS.SUBDIRECTORIES][ + "images" + ] assert set(reconciled[FIELDS.IMAGE_FILES]) == set(paths[1:]) assert removed not in reconciled[FIELDS.WORKSPACE_MAPPING] assert removed not in reconciled[FIELDS.SOURCE_METADATA] - assert {record["virtual_path"] for record in reconciled[FIELDS.SOURCE_PROJECTION]} == set(paths[1:]) + assert { + record["virtual_path"] for record in reconciled[FIELDS.SOURCE_PROJECTION] + } == set(paths[1:]) assert set(reconciled["z_indexes"]) == {str(z) for z in z_values[1:]} @@ -2044,8 +2081,7 @@ def test_produced_stacked_dotted_identity_keeps_declared_extension( ): parser = SourceSchemaFilenameParser() source_paths = tuple( - f"/source/{well}_s001_w1_z{z:03d}_t001{extension}" - for z in (3, 1, 2) + f"/source/{well}_s001_w1_z{z:03d}_t001{extension}" for z in (3, 1, 2) ) metadata = ImagePayloadMetadata( source_image_provenance_planes=SourceImageProvenancePlanes.from_components( @@ -2055,7 +2091,9 @@ def test_produced_stacked_dotted_identity_keeps_declared_extension( request = FunctionOutputPathRequest( parser=parser, output_dir=tmp_path, - output_payload=metadata.payload_with(np.zeros((3, 4, 5), dtype=np.uint16), None), + output_payload=metadata.payload_with( + np.zeros((3, 4, 5), dtype=np.uint16), None + ), input_path=Path(source_paths[0]).name, variable_components=(VariableComponents.Z_INDEX,), ) @@ -2176,7 +2214,9 @@ def test_runtime_image_metadata_target_requires_persisted_images(tmp_path, conte if contents == "tables": (directory / "measurements.csv").write_text("ObjectNumber,Area\n1,4\n") else: - tifffile.imwrite(directory / "A01_s001_w2_z001_t001.tif", np.ones((2, 2), dtype=np.uint8)) + tifffile.imwrite( + directory / "A01_s001_w2_z001_t001.tif", np.ones((2, 2), dtype=np.uint8) + ) context = context_stub(FileManager({Backend.DISK.value: DiskStorageBackend()})) plan = function_step_plan("SaveImages") plan.write_backend = Backend.MEMORY.value @@ -2223,7 +2263,32 @@ def test_runtime_image_metadata_target_requires_persisted_images(tmp_path, conte target = RuntimeArtifactMetadataTarget.from_plan(plan) assert target is not None assert target.output_dir == directory - selected = OpenHCSMetadataWriter.OutputTarget.for_execution(context, plan) + saved_artifacts = () + if contents == "images": + record = context.runtime_value_store.values()[0] + materialization = RuntimeArtifactMaterialization.from_record( + output_plan=output_plan, + record=record, + plan=plan, + context=context, + ) + saved_artifacts = ( + MaterializedRuntimeArtifact( + outputs_by_backend={ + Backend.DISK.value: ( + Output.from_metadata( + path=str(directory / "A01_s001_w2_z001_t001.tif"), + content=record.value.data, + metadata=image_payload_metadata(record.value.data), + ), + ) + }, + materialization=materialization, + ), + ) + selected = OpenHCSMetadataWriter.OutputTarget.for_execution( + context, plan, artifact_materializations=saved_artifacts + ) if contents == "images": assert selected == (target,) else: @@ -2295,8 +2360,12 @@ def test_declared_image_destinations_publish_and_reconcile_after_value_cleanup( path=output_plan.path, backend=Backend.MEMORY.value, ) - RuntimeArtifactMaterializationAuthority.materialize(context, plan) - OpenHCSMetadataWriter.write(context, plan) + materializations = RuntimeArtifactMaterializationAuthority.materialize( + context, plan + ) + OpenHCSMetadataWriter.write( + context, plan, artifact_materializations=materializations + ) context.runtime_value_store.clear() if stray_image: tifffile.imwrite(tmp_path / "images/review/unowned.tif", pixels) diff --git a/tests/unit/test_materialization_batch_outcomes.py b/tests/unit/test_materialization_batch_outcomes.py new file mode 100644 index 0000000000..a24b092c58 --- /dev/null +++ b/tests/unit/test_materialization_batch_outcomes.py @@ -0,0 +1,232 @@ +"""Actual batch outcomes, independent of subsequent logical-value rendering.""" + +from dataclasses import replace +from pathlib import Path +import pickle +from types import MappingProxyType + +import pytest +from polystore.disk import DiskStorageBackend +from polystore.filemanager import FileManager +from polystore.memory import MemoryStorageBackend + +from openhcs.core.context.processing_context import ProcessingContext +from openhcs.core.compiled_step_plan import CompiledStepPlan +from openhcs.core.debug import NoOpDebugExecutionPolicy +from openhcs.core.function_patterns import compile_function_pattern +from openhcs.core.orchestrator.compiled_plate_execution import ( + CompiledPlateExecutionExtras, + CompiledPlateExecutionResults, +) +from openhcs.core.orchestrator.execution_result import ( + ExecutionResult, + RuntimeContextObservation, + RuntimeExecutionObservation, + RuntimeObservationMode, +) +from openhcs.core.orchestrator.worker_execution import ( + _execute_axis_with_sequential_combinations, +) +from openhcs.core.orchestrator.worker_lanes import WorkerLaneExecutionContext +from openhcs.core.runtime_exports import RuntimeExportObservation +from openhcs.core.steps.abstract import StepExecutionObservation +from openhcs.core.steps.function_step import FunctionStep +from openhcs.processing.materialization import ( + CsvOptions, + JsonOptions, + MaterializationSpec, + SavedMaterializationOutputs, + prepare_materialization, +) +from openhcs.processing.materialization.core import _WRITERS_BY_OPTIONS + + +class CsvOnlyMemoryBackend(MemoryStorageBackend): + def accepts_payload(self, data, path): + return Path(path).suffix == ".csv" + + +def test_rendered_batch_reuses_exact_outputs_after_logical_payload_mutates( + tmp_path, monkeypatch +): + manager = FileManager({"disk": DiskStorageBackend()}) + rows = [{"Area": 42}] + batch = prepare_materialization( + MaterializationSpec(CsvOptions(), JsonOptions()), + rows, + str(tmp_path / "measurements"), + manager, + ("disk",), + ) + original_outputs = batch.outputs + rows[0]["Area"] = 999 + + def render_again(*_args): + pytest.fail( + "Saving a prepared batch must not render mutable logical input again" + ) + + for option_type in (CsvOptions, JsonOptions): + monkeypatch.setitem( + _WRITERS_BY_OPTIONS, + option_type, + replace(_WRITERS_BY_OPTIONS[option_type], write=render_again), + ) + outcome = batch.save() + + assert isinstance(outcome, SavedMaterializationOutputs) + assert outcome.outputs_for_backend("disk") == original_outputs + assert all( + left is right + for left, right in zip( + outcome.outputs_for_backend("disk"), original_outputs, strict=True + ) + ) + assert Path(batch.primary_path).read_text().splitlines() == ["Area", "42"] + assert Path(original_outputs[1].path).read_text().find("999") == -1 + + +def test_saved_outcomes_include_only_backend_accepted_outputs(tmp_path): + manager = FileManager({"memory": CsvOnlyMemoryBackend()}) + batch = prepare_materialization( + MaterializationSpec(CsvOptions(), JsonOptions()), + [{"Area": 42}], + str(tmp_path / "measurements"), + manager, + ("memory",), + ) + + outcome = batch.save() + + assert outcome.outputs_for_backend("memory") == (batch.outputs[0],) + assert outcome.outputs_for_backend("disk") == () + assert manager.exists(batch.outputs[0].path, "memory") + assert not manager.exists(batch.outputs[1].path, "memory") + + +def test_failed_save_does_not_return_a_successful_outcome(tmp_path, monkeypatch): + manager = FileManager({"disk": DiskStorageBackend()}) + batch = prepare_materialization( + MaterializationSpec(CsvOptions()), + [{"Area": 42}], + str(tmp_path / "measurements"), + manager, + ("disk",), + ) + + def failed_save(*_args, **_kwargs): + raise OSError("destination is unavailable") + + monkeypatch.setattr(manager, "save_batch", failed_save) + with pytest.raises(OSError, match="destination is unavailable"): + batch.save() + assert not Path(batch.primary_path).exists() + + +class EmittedOutputStep(FunctionStep): + def __init__(self, path): + super().__init__(lambda image: image) + self.path = path + + def process(self, context, step_index): + self.path.write_text("Area\n42\n") + return StepExecutionObservation(MappingProxyType({}), (self.path,)) + + +class FrozenWorkerContext(ProcessingContext): + def __init__(self): + super().__init__( + axis_id="A01", + filemanager=FileManager({"memory": MemoryStorageBackend()}), + step_plans={ + 0: CompiledStepPlan( + step_index=0, + step_name="Save", + step_type="FunctionStep", + axis_id="A01", + compiled_function_pattern=compile_function_pattern( + lambda image: image, {}, {} + ), + ) + }, + ) + self.freeze() + self.released = False + + def release_execution_image_cache(self): + super().release_execution_image_cache() + self.released = True + + +@pytest.mark.parametrize("mode", tuple(RuntimeObservationMode)) +def test_worker_exports_actual_step_outcomes_after_resource_release( + tmp_path, monkeypatch, mode +): + import openhcs.core.orchestrator.worker_execution as worker + + path = tmp_path / "actual.csv" + context = FrozenWorkerContext() + lane = WorkerLaneExecutionContext( + execution_id="execution", + plate_id="plate", + debug_execution_policy=NoOpDebugExecutionPolicy(), + worker_slot="worker-0", + worker_assignments={"worker-0": ("A01",)}, + ) + monkeypatch.setattr(worker, "emit", lambda **_kwargs: None) + + def forbidden_reconstruction(*_args, **_kwargs): + pytest.fail("Ordinary execution must consume actual step outcomes") + + monkeypatch.setattr( + worker, + "preview_reused_materialized_artifact_locations", + forbidden_reconstruction, + ) + monkeypatch.setattr( + worker, + "preview_reused_runtime_export_paths", + forbidden_reconstruction, + ) + result = _execute_axis_with_sequential_combinations( + [EmittedOutputStep(path)], + [("A01", context)], + lane, + mode, + release_axis_resources=False, + ) + transported = pickle.loads(pickle.dumps(result)) + + assert context.released + assert transported.is_success() + assert transported.runtime_observation.contexts[0].runtime_export_paths == (path,) + assert transported.runtime_observation.contexts[0].records == () + assert RuntimeExportObservation.from_runtime_observations( + (transported.runtime_observation,) + ).table_outputs == (path,) + + +def test_parent_and_worker_exports_share_existing_observation_authority(tmp_path): + worker_path = tmp_path / "worker.csv" + plate_path = tmp_path / "plate.csv" + unrelated = tmp_path / "unrelated.csv" + for path in (worker_path, plate_path, unrelated): + path.write_text("Area\n42\n") + worker = RuntimeExecutionObservation( + contexts=(RuntimeContextObservation("A01", (), (worker_path,)),) + ) + parent = RuntimeExecutionObservation( + contexts=(RuntimeContextObservation("A01", (), (plate_path,)),) + ) + results = CompiledPlateExecutionResults( + {"A01": ExecutionResult.success("A01", runtime_observation=worker)}, + extras=CompiledPlateExecutionExtras( + viewer_states_by_port={}, runtime_observation=parent + ), + ) + exports = RuntimeExportObservation.from_runtime_observations( + results.runtime_observations + ) + + assert exports.output_files == (worker_path, plate_path) + assert unrelated not in exports.output_files diff --git a/tests/unit/test_runtime_execution_validation.py b/tests/unit/test_runtime_execution_validation.py index 077d4a5e41..9906fed3bb 100644 --- a/tests/unit/test_runtime_execution_validation.py +++ b/tests/unit/test_runtime_execution_validation.py @@ -33,7 +33,11 @@ from openhcs.core.measurement_row_materialization import ( MeasurementSparseColumnarRows, ) -from openhcs.core.orchestrator.execution_result import ExecutionResult +from openhcs.core.orchestrator.execution_result import ( + ExecutionResult, + RuntimeContextObservation, + RuntimeExecutionObservation, +) from openhcs.core.pipeline.function_contracts import execution_scope from openhcs.core.runtime_artifact_values import ( ArtifactKey, @@ -120,7 +124,8 @@ def _streaming_context( def test_runtime_execution_validation_detects_missing_artifact_kind() -> None: observation = RuntimeArtifactExecutionObservation.from_contexts( - {"A01": ProcessingContext(axis_id="A01")} + {"A01": ProcessingContext(axis_id="A01")}, + runtime_observations=(), ) failures = runtime_artifact_execution_failures( @@ -186,7 +191,9 @@ def test_runtime_execution_observation_reads_context_stores() -> None: backend="memory", ) - observation = RuntimeArtifactExecutionObservation.from_contexts({"A01": context}) + observation = RuntimeArtifactExecutionObservation.from_contexts( + {"A01": context}, runtime_observations=() + ) assert observation.record_counts_by_axis["A01"][MeasurementsArtifactType] == 1 @@ -204,7 +211,9 @@ def test_runtime_execution_observation_ignores_uncontracted_files( context = ProcessingContext(axis_id="A01") - observation = RuntimeArtifactExecutionObservation.from_contexts({"A01": context}) + observation = RuntimeArtifactExecutionObservation.from_contexts( + {"A01": context}, runtime_observations=() + ) assert observation.exports.table_outputs == () assert observation.exports.image_outputs == () @@ -239,16 +248,23 @@ def test_zmq_observation_exports_exact_compiler_owned_artifacts( }, axis_id="A01", ) - monkeypatch.setattr( - "openhcs.core.steps.function_artifact_materialization." - "runtime_export_artifact_output_paths", - lambda _plan, _context: (contracted_output,), + runtime_observations = ( + RuntimeExecutionObservation( + contexts=( + RuntimeContextObservation( + context_key="A01", + records=(), + runtime_export_paths=(contracted_output,), + ), + ) + ), ) export = ZMQRuntimeExecutionObservationExport.from_execution( compiled_contexts={"A01": context}, execution_results={"A01": ExecutionResult.success("A01")}, output_roots=(tmp_path,), + runtime_observations=runtime_observations, ) assert export.expectation.artifact_kinds == frozenset((MeasurementsArtifactType,)) @@ -258,7 +274,9 @@ def test_zmq_observation_exports_exact_compiler_owned_artifacts( assert export.exports.output_files == (contracted_output,) assert unrelated_output not in export.exports.output_files assert ( - RuntimeExportObservation.from_execution_contexts({"A01": context}).output_files + RuntimeExportObservation.from_runtime_observations( + runtime_observations + ).output_files == export.exports.output_files ) @@ -514,22 +532,22 @@ def test_runtime_execution_observation_reads_plate_export_from_exact_owner( ) for axis_id, owns_output in (("A01", True), ("A02", False)) } - observed_axes: list[str] = [] - - def output_paths(plan, _context): - observed_axes.append(plan.axis_id) - return (contracted_output,) - - monkeypatch.setattr( - "openhcs.core.steps.function_artifact_materialization." - "runtime_export_artifact_output_paths", - output_paths, + runtime_observations = ( + RuntimeExecutionObservation( + contexts=( + RuntimeContextObservation( + context_key="A01", + records=(), + runtime_export_paths=(contracted_output,), + ), + ) + ), + ) + observation = RuntimeArtifactExecutionObservation.from_contexts( + contexts, runtime_observations=runtime_observations ) - - observation = RuntimeArtifactExecutionObservation.from_contexts(contexts) expectation = RuntimeArtifactExecutionExpectation.from_compiled_contexts(contexts) - assert observed_axes == ["A01"] assert observation.exports.table_outputs == (contracted_output,) assert expectation.artifact_kinds == frozenset((MeasurementsArtifactType,)) assert tuple( @@ -578,6 +596,7 @@ def test_zmq_observation_compresses_and_preserves_exact_runtime_records( compiled_contexts={"A01": context}, execution_results={"A01": ExecutionResult.success("A01")}, output_roots=(tmp_path,), + runtime_observations=(), ) path = tmp_path / "observation.pkl" diff --git a/tests/unit/test_zmq_execution_server.py b/tests/unit/test_zmq_execution_server.py index 45e8070fa3..85599621bf 100644 --- a/tests/unit/test_zmq_execution_server.py +++ b/tests/unit/test_zmq_execution_server.py @@ -16,6 +16,9 @@ ProcessingConfig, ) from openhcs.core.execution_state import ExecutionOutputPlateSummary +from openhcs.core.orchestrator.compiled_plate_execution import ( + CompiledPlateExecutionResults, +) from openhcs.core.orchestrator.execution_result import ( ExecutionResult, RuntimeContextObservation, @@ -196,20 +199,22 @@ def test_server_exports_outcomes_without_projecting_compiled_values( server._export_runtime_observation( request_context=request_context, compilation=compilation, - execution_results={ - "A01": ExecutionResult.success( - "A01", - runtime_observation=RuntimeExecutionObservation( - contexts=( - RuntimeContextObservation( - "context", - (), - runtime_export_paths=(declared_output,), - ), - ) - ), - ) - }, + execution_results=CompiledPlateExecutionResults( + { + "A01": ExecutionResult.success( + "A01", + runtime_observation=RuntimeExecutionObservation( + contexts=( + RuntimeContextObservation( + "context", + (), + runtime_export_paths=(declared_output,), + ), + ) + ), + ) + } + ), ) export = ZMQRuntimeExecutionOutcomeExport.read(export_path) diff --git a/tests/unit/test_zmq_runtime_outcome_export.py b/tests/unit/test_zmq_runtime_outcome_export.py index f358c89ca1..041116b678 100644 --- a/tests/unit/test_zmq_runtime_outcome_export.py +++ b/tests/unit/test_zmq_runtime_outcome_export.py @@ -110,6 +110,7 @@ def test_value_export_rejects_an_uncompiled_execution_axis() -> None: compiled_contexts={}, execution_results={"A01": ExecutionResult.success("A01")}, output_roots=(), + runtime_observations=(), ) with pytest.raises(RuntimeError, match="execution outcomes have no compiled axis"): exported.require_valid_observation() @@ -167,6 +168,7 @@ def test_previous_runtime_export_versions_remain_readable(tmp_path: Path) -> Non execution_results={}, output_roots=(), execution_id="new-job", + runtime_observations=(), ), schema_version=6, execution_id=None, @@ -178,6 +180,7 @@ def test_previous_runtime_export_versions_remain_readable(tmp_path: Path) -> Non execution_results={}, output_roots=(), execution_id="other-job", + runtime_observations=(), ).write(observation_path) assert observation_path.read_bytes() == retained_bytes assert ( From e4d786cbf6a688d9543213bc314cac5a3717bb97 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Thu, 1 Oct 2026 14:09:54 -0400 Subject: [PATCH 005/235] Resolve runtime source metadata through nominal record lifetimes --- openhcs/core/runtime_adapters.py | 19 +- openhcs/core/runtime_source_binding_cache.py | 102 ++++++- openhcs/core/source_binding_selection.py | 134 ++++++--- openhcs/core/steps/function_execution.py | 9 +- openhcs/core/steps/function_runtime.py | 2 +- ...test_function_runtime_source_projection.py | 2 + ...test_runtime_source_resolution_snapshot.py | 276 ++++++++++++++++++ 7 files changed, 498 insertions(+), 46 deletions(-) create mode 100644 tests/unit/test_runtime_source_resolution_snapshot.py diff --git a/openhcs/core/runtime_adapters.py b/openhcs/core/runtime_adapters.py index 14e0bb43cc..e8b2d3cf2a 100644 --- a/openhcs/core/runtime_adapters.py +++ b/openhcs/core/runtime_adapters.py @@ -42,7 +42,6 @@ ) from openhcs.core.source_binding_selection import ( SourceBindingMatchedImageSet, - SourcePatternResolutionContext, ) from openhcs.core.source_image_provenance import ( SourceImageIdentity, @@ -305,10 +304,12 @@ def source_artifact_payload(self, ref: ArtifactSpecRef) -> object: projection, axis_id=self.axis_scope.axis_id, ) - source_context = SourcePatternResolutionContext.from_projection( - parser=self.context.microscope_handler.parser, - projection=projection, - metadata_rules=self.source_binding_plan.metadata_rules, + source_context = ( + self.context.runtime_source_binding_context_cache.source_pattern_context( + parser=self.context.microscope_handler.parser, + projection=projection, + metadata_rules=self.source_binding_plan.metadata_rules, + ) ) matched_set = SourceBindingMatchedImageSet.from_plan( bindings=self.source_binding_plan.binding_declarations, @@ -475,7 +476,9 @@ class RuntimeAdapterSpec: parameter_name: str factory: RuntimeAdapterFactory manages_artifact_inputs: bool = False - artifact_output_policy: type[ArtifactOutputPolicy] = NativeReturnArtifactOutputPolicy + artifact_output_policy: type[ArtifactOutputPolicy] = ( + NativeReturnArtifactOutputPolicy + ) runtime_callable_factory: RuntimeCallableFactory | None = None def __post_init__(self) -> None: @@ -530,7 +533,9 @@ def runtime_adapter( factory: RuntimeAdapterFactory, *, manages_artifact_inputs: bool = False, - artifact_output_policy: type[ArtifactOutputPolicy] = NativeReturnArtifactOutputPolicy, + artifact_output_policy: type[ + ArtifactOutputPolicy + ] = NativeReturnArtifactOutputPolicy, runtime_callable_factory: RuntimeCallableFactory | None = None, ) -> Callable[[_F], _F]: """Declare that a callable needs an invocation-scoped runtime adapter.""" diff --git a/openhcs/core/runtime_source_binding_cache.py b/openhcs/core/runtime_source_binding_cache.py index 7e2ffb242d..d067f2b5fe 100644 --- a/openhcs/core/runtime_source_binding_cache.py +++ b/openhcs/core/runtime_source_binding_cache.py @@ -3,17 +3,85 @@ from __future__ import annotations from collections.abc import Mapping -from dataclasses import dataclass, field -from typing import Any, TYPE_CHECKING +from dataclasses import dataclass, field, replace +from types import MappingProxyType +from typing import Any, Hashable, TYPE_CHECKING from openhcs.core.source_bindings import ( SourceBindingRuntimeContext, SourceBindingRuntimeMetadataNormalizer, + MetadataExtractionRule, ) from openhcs.core.source_metadata import SourceMetadataMapping if TYPE_CHECKING: - from openhcs.core.source_binding_selection import SourceUniverseRuntimeState + from openhcs.core.source_binding_selection import ( + SourcePatternResolutionContext, + SourceUniverseRuntimeState, + ) + from openhcs.core.source_workspace_projection import ( + VirtualWorkspaceSourceProjection, + ) + from openhcs.microscopes.microscope_interfaces import FilenameParser + + +@dataclass(frozen=True, slots=True) +class RuntimeSourceResolutionSnapshot: + """A derived resolution view retaining its exact projection and parser owners.""" + + projection: "VirtualWorkspaceSourceProjection" + context: "SourcePatternResolutionContext" + + @classmethod + def from_projection( + cls, + *, + parser: "FilenameParser", + projection: "VirtualWorkspaceSourceProjection", + metadata_rules: tuple[MetadataExtractionRule, ...], + ) -> "RuntimeSourceResolutionSnapshot": + """Resolve declared positions into one independently owned runtime view.""" + from openhcs.core.source_binding_selection import ( + ResolvedSourceMetadataRecord, + SourceMetadataRecord, + SourcePatternResolutionContext, + ) + + context = SourcePatternResolutionContext.from_projection( + parser=parser, + projection=projection, + metadata_rules=metadata_rules, + ) + normalized_metadata = SourceBindingRuntimeMetadataNormalizer( + projection.source_metadata_by_path + ).normalized() + context = replace( + context, + source_metadata_by_path=MappingProxyType( + { + path: SourceMetadataRecord.from_mapping(metadata) + for path, metadata in normalized_metadata.items() + } + ), + source_projections_by_virtual_path=MappingProxyType( + dict(context.source_projections_by_virtual_path) + ), + ) + paths = dict.fromkeys( + ( + *context.source_metadata_by_path, + *context.source_paths_by_virtual_path, + *context.source_paths_by_virtual_path.values(), + ) + ) + records = {} + for path in paths: + metadata = context.metadata_for_path(path) + records[path] = ResolvedSourceMetadataRecord.from_resolved_mapping( + {} if metadata is None else metadata + ) + context = replace(context, source_metadata_by_path=MappingProxyType(records)) + return cls(projection=projection, context=context) @dataclass(slots=True) @@ -32,6 +100,34 @@ class RuntimeSourceBindingContextCache: tuple[int, tuple[str, ...], object, int | None], "SourceUniverseRuntimeState", ] = field(default_factory=dict) + source_resolution_snapshots: dict[ + tuple[int, int, tuple[Hashable, ...], tuple[MetadataExtractionRule, ...]], + RuntimeSourceResolutionSnapshot, + ] = field(default_factory=dict) + + def source_pattern_context( + self, + *, + parser: "FilenameParser", + projection: "VirtualWorkspaceSourceProjection", + metadata_rules: tuple[MetadataExtractionRule, ...], + ) -> "SourcePatternResolutionContext": + """Own one normalized runtime snapshot for exact source declarations.""" + key = (id(projection), id(parser), parser.semantic_identity(), metadata_rules) + cached = self.source_resolution_snapshots.get(key) + if cached is not None: + return cached.context + snapshot = RuntimeSourceResolutionSnapshot.from_projection( + parser=parser, + projection=projection, + metadata_rules=metadata_rules, + ) + self.source_resolution_snapshots[key] = snapshot + return snapshot.context + + def __reduce__(self) -> tuple[type["RuntimeSourceBindingContextCache"], tuple[()]]: + """Transport reconstructs all derived caches from declaration defaults.""" + return type(self), () def normalized_source_metadata( self, diff --git a/openhcs/core/source_binding_selection.py b/openhcs/core/source_binding_selection.py index 90c686c60a..13e7673ba9 100644 --- a/openhcs/core/source_binding_selection.py +++ b/openhcs/core/source_binding_selection.py @@ -26,6 +26,7 @@ SourceBindingMatchMethod, SourceBindingMatchPlan, SourceBindingRuntimeContext, + SourceBindingRuntimeMetadataNormalizer, SourceSetRole, ) from openhcs.core.source_image_provenance import ( @@ -77,7 +78,7 @@ def _cached_source_candidate_pattern_keys(pattern_path: str) -> tuple[str, ...]: return tuple(dict.fromkeys((pattern_path, path.as_posix(), path.name))) -@dataclass(frozen=True, slots=True) +@dataclass(frozen=True, slots=True, eq=False) class SourceMetadataRecord(Mapping[str, SourceMetadataValue]): """Normalized source metadata carried across source-binding selection.""" @@ -85,7 +86,51 @@ class SourceMetadataRecord(Mapping[str, SourceMetadataValue]): @classmethod def from_mapping(cls, metadata: SourceMetadataMapping) -> "SourceMetadataRecord": - return cls(tuple((str(key), value) for key, value in metadata.items())) + return DeclaredSourceMetadataRecord( + tuple((str(key), value) for key, value in metadata.items()) + ) + + @abstractmethod + def resolve( + self, + path: str, + parser: "FilenameParser", + metadata_rules: tuple[MetadataExtractionRule, ...], + ) -> "SourceMetadataRecord | None": + """Return this path's metadata through its declared resolution lifetime.""" + + def resolve_with_fallback( + self, + path: str, + parser: "FilenameParser", + metadata_rules: tuple[MetadataExtractionRule, ...], + ) -> "SourceMetadataRecord | None": + """Fill parser and extraction-rule fields absent from declarations.""" + metadata: dict[str, SourceMetadataValue] = {} + merge_source_metadata(metadata, self, path=path) + parsed_metadata = parser.parse_filename(path) + if parsed_metadata is not None: + merge_source_metadata( + metadata, + { + key: value + for key, value in parsed_metadata.wire_mapping().items() + if key not in metadata + }, + path=path, + ) + rule_metadata = metadata_from_rules(path, metadata_rules) + if rule_metadata: + merge_source_metadata( + metadata, + { + key: value + for key, value in rule_metadata.items() + if key not in metadata + }, + path=path, + ) + return self.from_mapping(metadata) if metadata else None def __getitem__(self, key: str) -> SourceMetadataValue: for field_key, value in self.fields: @@ -99,6 +144,54 @@ def __iter__(self) -> Iterator[str]: def __len__(self) -> int: return len(self.fields) + def __eq__(self, other: object) -> bool: + if not isinstance(other, SourceMetadataRecord): + return NotImplemented + return self.fields == other.fields + + def __hash__(self) -> int: + return hash(self.fields) + + +@dataclass(frozen=True, slots=True, eq=False) +class DeclaredSourceMetadataRecord(SourceMetadataRecord): + """Live declared metadata that still requires path-specific fallbacks.""" + + def resolve( + self, + path: str, + parser: "FilenameParser", + metadata_rules: tuple[MetadataExtractionRule, ...], + ) -> SourceMetadataRecord | None: + return self.resolve_with_fallback(path, parser, metadata_rules) + + +@dataclass(frozen=True, slots=True, eq=False) +class ResolvedSourceMetadataRecord(SourceMetadataRecord): + """Immutable path-specific metadata resolved at a runtime snapshot boundary.""" + + @classmethod + def from_resolved_mapping( + cls, metadata: SourceMetadataMapping + ) -> "ResolvedSourceMetadataRecord": + return cls( + tuple( + ( + str(key), + SourceBindingRuntimeMetadataNormalizer.normalized_value(value), + ) + for key, value in metadata.items() + ) + ) + + def resolve( + self, + path: str, + parser: "FilenameParser", + metadata_rules: tuple[MetadataExtractionRule, ...], + ) -> SourceMetadataRecord | None: + return self if self.fields else None + @dataclass(frozen=True, slots=True) class SourceMetadataCandidates: @@ -404,35 +497,10 @@ def metadata_for_paths( ) def metadata_for_path(self, path: str) -> SourceMetadataRecord | None: - metadata: dict[str, SourceMetadataValue] = {} - declared_metadata = self.source_metadata_by_path.get(path) - if declared_metadata is not None: - merge_source_metadata(metadata, declared_metadata, path=path) - parsed_metadata = self.parser.parse_filename(path) - if parsed_metadata is not None: - merge_source_metadata( - metadata, - { - key: value - for key, value in parsed_metadata.wire_mapping().items() - if key not in metadata - }, - path=path, - ) - rule_metadata = metadata_from_rules(path, self.metadata_rules) - if rule_metadata: - merge_source_metadata( - metadata, - { - key: value - for key, value in rule_metadata.items() - if key not in metadata - }, - path=path, - ) - if metadata: - return SourceMetadataRecord.from_mapping(metadata) - return None + record = self.source_metadata_by_path.get(path) + if record is None: + record = DeclaredSourceMetadataRecord(()) + return record.resolve(path, self.parser, self.metadata_rules) def merged_metadata_for_paths( self, @@ -1369,7 +1437,7 @@ def _source_image_set_identity( metadata = ( metadata_candidates.values[0] if metadata_candidates.values - else SourceMetadataRecord(()) + else DeclaredSourceMetadataRecord(()) ) return SourceImageSetIdentity.from_metadata( metadata, @@ -1708,7 +1776,7 @@ def source_metadata_by_path(self) -> Mapping[str, SourceMetadataMapping]: def source_context(self) -> SourcePatternResolutionContext: metadata_rules = self.plan.source_binding_plan.metadata_rules if self.source_projection is not None: - return SourcePatternResolutionContext.from_projection( + return self.context.runtime_source_binding_context_cache.source_pattern_context( parser=self.context.microscope_handler.parser, projection=self.source_projection, metadata_rules=metadata_rules, diff --git a/openhcs/core/steps/function_execution.py b/openhcs/core/steps/function_execution.py index 1b075b9d82..6cd8cf743c 100644 --- a/openhcs/core/steps/function_execution.py +++ b/openhcs/core/steps/function_execution.py @@ -7,7 +7,7 @@ import time import traceback from collections.abc import Callable, Iterator, Mapping, Sequence -from dataclasses import dataclass +from dataclasses import dataclass, field from itertools import zip_longest from typing import TYPE_CHECKING @@ -28,6 +28,7 @@ ) from openhcs.core.progress import ProgressPhase, ProgressStatus, emit from openhcs.core.runtime_pattern_cache import RuntimePatternDiscoveryCacheKey +from openhcs.core.runtime_source_binding_cache import RuntimeSourceBindingContextCache from openhcs.core.source_binding_selection import ( SourceBoundAnchorPatternPolicy, SourceCandidatePath, @@ -223,6 +224,9 @@ class StepAnchorPatternFilter: output_manifest: StepOutputManifestStore source_workspace_authority: VirtualWorkspaceSourceProjectionAuthority source_workspace_projection_cache: VirtualWorkspaceSourceProjectionCache + source_binding_context_cache: RuntimeSourceBindingContextCache = field( + default_factory=RuntimeSourceBindingContextCache + ) @classmethod def from_context( @@ -243,6 +247,7 @@ def from_context( source_workspace_projection_cache=( context.runtime_source_workspace_projection_cache ), + source_binding_context_cache=context.runtime_source_binding_context_cache, ) def filtered(self, grouped_patterns: PatternGroups) -> PatternGroups: @@ -611,7 +616,7 @@ def source_pattern_context(self) -> SourcePatternResolutionContext: """Return source-path context used to filter source-bound anchors.""" projection = self.source_workspace_authority.projection_or_empty() - return SourcePatternResolutionContext.from_projection( + return self.source_binding_context_cache.source_pattern_context( parser=self.parser, projection=self.source_workspace_projection_cache.filtered_by_axis( projection, diff --git a/openhcs/core/steps/function_runtime.py b/openhcs/core/steps/function_runtime.py index e5fb4cf954..266ed9b128 100644 --- a/openhcs/core/steps/function_runtime.py +++ b/openhcs/core/steps/function_runtime.py @@ -2948,7 +2948,7 @@ def _source_binding_load_universe(self) -> tuple[str, ...]: def _source_binding_candidate_context(self) -> SourcePatternResolutionContext: projection = self.source_workspace_projection_authority().projection_or_empty() - return SourcePatternResolutionContext.from_projection( + return self.request.context.runtime_source_binding_context_cache.source_pattern_context( parser=self.request.context.microscope_handler.parser, projection=self.source_workspace_projection_cache().filtered_by_axis( projection, diff --git a/tests/unit/test_function_runtime_source_projection.py b/tests/unit/test_function_runtime_source_projection.py index d031997b88..016ae53eaf 100644 --- a/tests/unit/test_function_runtime_source_projection.py +++ b/tests/unit/test_function_runtime_source_projection.py @@ -3733,6 +3733,7 @@ def test_producer_anchored_pipeline_start_paths_use_exact_source_projection_bund """Producer bookkeeping must not override exact workspace source ownership.""" from openhcs.core.steps import function_runtime + from openhcs.core.runtime_source_binding_cache import RuntimeSourceBindingContextCache virtual_paths = ( "A01_s001_w1_z001_t001.tif", @@ -3870,6 +3871,7 @@ def resolve_address(backend_address, backend, *, base_path): ), filemanager=SourceFileManager(), runtime_image_stack_cache=RuntimeImageStackCache(), + runtime_source_binding_context_cache=RuntimeSourceBindingContextCache(), runtime_source_workspace_projection_cache=( VirtualWorkspaceSourceProjectionCache() ), diff --git a/tests/unit/test_runtime_source_resolution_snapshot.py b/tests/unit/test_runtime_source_resolution_snapshot.py new file mode 100644 index 0000000000..9033f38827 --- /dev/null +++ b/tests/unit/test_runtime_source_resolution_snapshot.py @@ -0,0 +1,276 @@ +"""Resolution lifetimes across generic source selectors and artifact adapters.""" + +import pickle +from types import MappingProxyType + +import pytest +from polystore.virtual_workspace import SourcePixelRef + +from openhcs.constants.constants import AllComponents +from openhcs.core.runtime_source_binding_cache import RuntimeSourceBindingContextCache +from openhcs.core.source_binding_selection import ( + DeclaredSourceMetadataRecord, + ResolvedSourceMetadataRecord, + SourceBindingMatchedImageSet, + SourceMetadataRecord, + SourcePatternResolutionContext, +) +from openhcs.core.source_bindings import ( + MetadataExtractionRule, + MetadataSource, + SourceBindingRuntimeContext, +) +from openhcs.core.source_matching import SourceImageSetIdentityPolicy +from openhcs.core.source_metadata import ORIGINAL_SOURCE_METADATA_FIELD +from openhcs.core.source_projection import OpenHCSPlaneAddress, SourcePlaneProjection +from openhcs.core.source_workspace_projection import VirtualWorkspaceSourceProjection +from openhcs.microscopes.microscope_interfaces import FilenameParseResult +from openhcs.microscopes.source_schema import SourceSchemaFilenameParser + +PATH = "A01_s001_w1_z001_t001.tif" + + +class CountingParser(SourceSchemaFilenameParser): + def __init__(self): + super().__init__() + self.calls = [] + + def parse_filename(self, path): + self.calls.append(path) + result = super().parse_filename(path) + if result is None or self.pattern_format is None: + return result + return FilenameParseResult( + tuple( + ( + component, + ( + int(self.pattern_format) + if component is AllComponents.SITE + else value + ), + ) + for component, value in result.declared_values() + ), + extension=result.extension, + ) + + +def projection(metadata): + return VirtualWorkspaceSourceProjection( + source_refs_by_virtual_path={PATH: SourcePixelRef("disk", "/source/image.tif")}, + source_metadata_by_path={PATH: metadata}, + ) + + +def snapshot(cache, source_projection, parser, rules=()): + return cache.source_pattern_context( + parser=parser, projection=source_projection, metadata_rules=rules + ) + + +def test_declared_record_resolves_live_parser_and_nested_metadata(): + nested = {"Plate": "before"} + record = SourceMetadataRecord.from_mapping( + {ORIGINAL_SOURCE_METADATA_FIELD: nested, "site": 8} + ) + assert isinstance(record, DeclaredSourceMetadataRecord) + parser = CountingParser() + first = record.resolve(PATH, parser, ()) + nested["Plate"] = "after" + second = record.resolve(PATH, parser, ()) + assert first["site"] == second["site"] == 8 + assert first[ORIGINAL_SOURCE_METADATA_FIELD]["Plate"] == "before" + assert second[ORIGINAL_SOURCE_METADATA_FIELD]["Plate"] == "after" + assert parser.calls == [PATH, PATH] + + +def test_runtime_snapshot_owns_nested_metadata_and_unknown_fallback(): + nested = {"Plate": "before"} + source_projection = projection({ORIGINAL_SOURCE_METADATA_FIELD: nested}) + parser = CountingParser() + cache = RuntimeSourceBindingContextCache() + context = snapshot(cache, source_projection, parser) + calls = tuple(parser.calls) + nested["Plate"] = "after" + for _ in range(3): + metadata = context.metadata_for_path(PATH) + assert isinstance(metadata, ResolvedSourceMetadataRecord) + assert metadata[ORIGINAL_SOURCE_METADATA_FIELD]["Plate"] == "before" + assert tuple(parser.calls) == calls + with pytest.raises(TypeError): + metadata[ORIGINAL_SOURCE_METADATA_FIELD]["Plate"] = "invalid" + unknown = "A02_s001_w1_z001_t001.tif" + assert context.metadata_for_path(unknown)["well"] == "A02" + assert parser.calls[-1] == unknown + assert snapshot(cache, source_projection, parser) is context + entry = next(iter(cache.source_resolution_snapshots.values())) + assert entry.projection is source_projection + assert entry.context.parser is parser + + +def test_direct_projection_context_remains_live(): + nested = {"Plate": "before"} + parser = CountingParser() + context = SourcePatternResolutionContext.from_projection( + parser=parser, + projection=projection({ORIGINAL_SOURCE_METADATA_FIELD: nested}), + ) + assert ( + context.metadata_for_path(PATH)[ORIGINAL_SOURCE_METADATA_FIELD]["Plate"] + == "before" + ) + nested["Plate"] = "after" + assert ( + context.metadata_for_path(PATH)[ORIGINAL_SOURCE_METADATA_FIELD]["Plate"] + == "after" + ) + assert parser.calls == [PATH, PATH] + + +def test_snapshot_replacement_tracks_projection_parser_semantics_and_rules(): + cache = RuntimeSourceBindingContextCache() + parser = CountingParser() + source_projection = projection({}) + original = snapshot(cache, source_projection, parser) + parser.pattern_format = "9" + changed_parser = snapshot(cache, source_projection, parser) + assert changed_parser is not original + assert original.metadata_for_path(PATH)["site"] == 1 + assert changed_parser.metadata_for_path(PATH)["site"] == 9 + replaced_projection = snapshot(cache, projection({"site": 7}), parser) + assert replaced_projection.metadata_for_path(PATH)["site"] == 7 + replaced_parser = snapshot(cache, source_projection, CountingParser()) + assert replaced_parser is not original + assert replaced_parser.metadata_for_path(PATH)["site"] == 1 + rules = ( + MetadataExtractionRule( + source=MetadataSource.FILE_NAME, pattern=r"(?PA01)" + ), + ) + changed_rules = snapshot(cache, source_projection, parser, rules) + assert changed_rules is not changed_parser + assert ( + changed_rules.metadata_for_path(PATH)[ORIGINAL_SOURCE_METADATA_FIELD]["Plate"] + == "A01" + ) + + +def test_matched_set_preserves_resolved_record_behavior_and_precedence(): + parser = CountingParser() + context = snapshot( + RuntimeSourceBindingContextCache(), projection({"site": None}), parser + ) + matched = SourceBindingMatchedImageSet.from_plan( + bindings=(), + match_plan=None, + source_context=context, + identity_policy=SourceImageSetIdentityPolicy(), + ) + calls = tuple(parser.calls) + assert ( + matched.source_metadata_by_path[PATH] is context.source_metadata_by_path[PATH] + ) + assert matched.metadata_for_path(PATH)["site"] == 1 + assert tuple(parser.calls) == calls + + +def test_virtual_positions_remain_distinct_with_shared_physical_address(): + other = "A01_s002_w1_z001_t001.tif" + source_projection = VirtualWorkspaceSourceProjection( + source_refs_by_virtual_path={ + PATH: SourcePixelRef("disk", "/source/shared.tif"), + other: SourcePixelRef("disk", "/source/shared.tif"), + }, + source_metadata_by_path=MappingProxyType( + {PATH: {"site": 1}, other: {"site": 2}} + ), + ) + context = snapshot( + RuntimeSourceBindingContextCache(), source_projection, CountingParser() + ) + assert context.virtual_paths_for_source("/source/shared.tif") == (PATH, other) + assert context.metadata_for_path(PATH)["site"] == 1 + assert context.metadata_for_path(other)["site"] == 2 + + +def test_known_empty_record_remains_none_without_repeated_parsing(): + parser = CountingParser() + source_projection = VirtualWorkspaceSourceProjection( + source_refs_by_virtual_path={"opaque": SourcePixelRef("disk", "opaque")}, + source_metadata_by_path={}, + ) + context = snapshot(RuntimeSourceBindingContextCache(), source_projection, parser) + calls = tuple(parser.calls) + assert context.metadata_for_path("opaque") is None + assert context.metadata_for_path("opaque") is None + assert tuple(parser.calls) == calls + + +def test_snapshot_rejects_nested_values_outside_existing_metadata_grammar(): + with pytest.raises(TypeError, match="Source metadata scalar values"): + snapshot( + RuntimeSourceBindingContextCache(), + projection({"nested": {"unsupported": {"deep": 1}}}), + CountingParser(), + ) + + +def test_declared_and_resolved_records_preserve_family_value_identity(): + declared = SourceMetadataRecord.from_mapping({"site": 1, "well": "A01"}) + resolved = ResolvedSourceMetadataRecord.from_resolved_mapping( + {"site": 1, "well": "A01"} + ) + assert declared == resolved + assert hash(declared) == hash(resolved) + assert declared != SourceMetadataRecord.from_mapping({"site": 2, "well": "A01"}) + nested = {ORIGINAL_SOURCE_METADATA_FIELD: {"Plate": "A"}} + with pytest.raises(TypeError): + hash(SourceMetadataRecord.from_mapping(nested)) + with pytest.raises(TypeError): + hash(ResolvedSourceMetadataRecord.from_resolved_mapping(nested)) + + +def test_snapshot_owns_position_and_projection_map_views(): + original_ref = SourcePixelRef("disk", "/source/original.tif") + refs = {PATH: original_ref} + positions = { + PATH: SourcePlaneProjection( + address=OpenHCSPlaneAddress.from_values("A01", 1, 1, 1, 1), + ref=original_ref, + source_alias="original", + ) + } + original_projection = positions[PATH] + source_projection = VirtualWorkspaceSourceProjection( + source_refs_by_virtual_path=refs, + source_metadata_by_path={PATH: {}}, + source_projections_by_virtual_path=positions, + ) + context = snapshot( + RuntimeSourceBindingContextCache(), source_projection, CountingParser() + ) + refs[PATH] = SourcePixelRef("disk", "/source/replaced.tif") + positions.clear() + assert context.source_paths_by_virtual_path[PATH] == "/source/original.tif" + assert context.source_projections_by_virtual_path[PATH] is original_projection + with pytest.raises(TypeError): + context.source_projections_by_virtual_path[PATH] = original_projection + + +def test_warmed_cache_transport_reconstructs_all_derived_defaults(): + cache = RuntimeSourceBindingContextCache() + parser = CountingParser() + source_projection = projection({ORIGINAL_SOURCE_METADATA_FIELD: {"Plate": "A"}}) + context = snapshot(cache, source_projection, parser) + cache.normalized_source_metadata(source_projection.source_metadata_by_path) + cache.runtime_context_by_request_identity[(1, (), "disk", None)] = ( + SourceBindingRuntimeContext() + ) + assert cache.source_resolution_snapshots + assert cache.source_metadata_by_mapping_identity + restored = pickle.loads(pickle.dumps(cache)) + assert restored == RuntimeSourceBindingContextCache() + rebuilt = snapshot(restored, source_projection, parser) + assert rebuilt is not context + assert rebuilt.metadata_for_path(PATH) == context.metadata_for_path(PATH) From e98da6b9fa5e1f811c9e69924a1511b2f2b8b551 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Thu, 1 Oct 2026 14:11:20 -0400 Subject: [PATCH 006/235] Exercise source snapshot transport through processing context --- ...test_function_runtime_source_projection.py | 66 +++++++++++++------ ...test_runtime_source_resolution_snapshot.py | 22 ++++++- 2 files changed, 65 insertions(+), 23 deletions(-) diff --git a/tests/unit/test_function_runtime_source_projection.py b/tests/unit/test_function_runtime_source_projection.py index 016ae53eaf..fc4fc52482 100644 --- a/tests/unit/test_function_runtime_source_projection.py +++ b/tests/unit/test_function_runtime_source_projection.py @@ -3733,7 +3733,9 @@ def test_producer_anchored_pipeline_start_paths_use_exact_source_projection_bund """Producer bookkeeping must not override exact workspace source ownership.""" from openhcs.core.steps import function_runtime - from openhcs.core.runtime_source_binding_cache import RuntimeSourceBindingContextCache + from openhcs.core.runtime_source_binding_cache import ( + RuntimeSourceBindingContextCache, + ) virtual_paths = ( "A01_s001_w1_z001_t001.tif", @@ -5342,8 +5344,11 @@ def save_batch( def qualified_producer_manifest(tmp_path): parser = SourceSchemaFilenameParser() producer = SimpleNamespace( - step_scope_id="producer", step_name="Producer", pipeline_position=0, - axis_id="A01", output_dir=tmp_path, + step_scope_id="producer", + step_name="Producer", + pipeline_position=0, + axis_id="A01", + output_dir=tmp_path, ) consumer = SimpleNamespace( axis_id="A01", @@ -5357,14 +5362,22 @@ def qualified_producer_manifest(tmp_path): parsed = parser.parse_filename(f"A01_s001_w2_z{plane:03d}_t001.tif") identity = FunctionOutputIdentity( component_values=dict(parsed.component_wire_mapping()), - extension=".tif", source="test", filename_qualifier=f"Output{plane}", + extension=".tif", + source="test", + filename_qualifier=f"Output{plane}", + ) + records.append( + ProducedOutputSemantics.from_output( + producer, + tmp_path + / FunctionOutputPathAuthority.filename_for_identity(parser, identity), + identity, + output_context=AlignedImageSliceContext.main_flow( + output_key=f"Output{plane}", + artifact_kind=ImageArtifactType.value, + ), + ) ) - records.append(ProducedOutputSemantics.from_output( - producer, tmp_path / FunctionOutputPathAuthority.filename_for_identity(parser, identity), - identity, output_context=AlignedImageSliceContext.main_flow( - output_key=f"Output{plane}", artifact_kind=ImageArtifactType.value, - ), - )) store = StepOutputManifestStore() store.begin_step(producer) store.record_outputs(producer, records) @@ -5372,7 +5385,8 @@ def qualified_producer_manifest(tmp_path): def test_step_output_manifest_batch_lookup_preserves_aliases_order_and_duplicates( - qualified_producer_manifest, monkeypatch, + qualified_producer_manifest, + monkeypatch, ): store, _producer, consumer, records, parser = qualified_producer_manifest calls = [] @@ -5382,7 +5396,9 @@ def count_filename(parser, identity): calls.append(identity) return original(parser, identity) - monkeypatch.setattr(FunctionOutputPathAuthority, "filename_for_identity", count_filename) + monkeypatch.setattr( + FunctionOutputPathAuthority, "filename_for_identity", count_filename + ) paths = ( records[1].output_path, "A01_s001_w2_z001_t001.tif", @@ -5391,7 +5407,10 @@ def count_filename(parser, identity): ) result = store.producer_output_contexts_for_paths(consumer, paths, parser) assert tuple(context.output_key for context in result) == ( - "Output2", "Output1", "Output2", "Output1", + "Output2", + "Output1", + "Output2", + "Output1", ) assert len(calls) == len(records) @@ -5409,12 +5428,17 @@ def test_step_output_manifest_batch_lookup_template_deduplicates_record_aliases( assert index.matching_records(path) == (records[0],) -@pytest.mark.parametrize("path, count", [ - ("missing.tif", 0), - ("A01_s001_w2_z{plane}_t001.tif", 2), -]) +@pytest.mark.parametrize( + "path, count", + [ + ("missing.tif", 0), + ("A01_s001_w2_z{plane}_t001.tif", 2), + ], +) def test_step_output_manifest_batch_lookup_rejects_missing_and_ambiguous_templates( - qualified_producer_manifest, path, count, + qualified_producer_manifest, + path, + count, ): store, _producer, consumer, _records, parser = qualified_producer_manifest with pytest.raises(NoStepOutputManifestMatch, match=f"found {count}"): @@ -5427,10 +5451,12 @@ def test_step_output_manifest_batch_lookup_rejects_shared_basename( store, producer, consumer, records, parser = qualified_producer_manifest alias = records[0].relative_output_path other = ProducedOutputSemantics.from_output( - producer, producer.output_dir / "another" / alias, + producer, + producer.output_dir / "another" / alias, FunctionOutputIdentity( component_values=records[1].component_values, - extension=".tif", source="test", + extension=".tif", + source="test", ), output_context=records[1].output_context, ) diff --git a/tests/unit/test_runtime_source_resolution_snapshot.py b/tests/unit/test_runtime_source_resolution_snapshot.py index 9033f38827..d934eee0b1 100644 --- a/tests/unit/test_runtime_source_resolution_snapshot.py +++ b/tests/unit/test_runtime_source_resolution_snapshot.py @@ -7,6 +7,7 @@ from polystore.virtual_workspace import SourcePixelRef from openhcs.constants.constants import AllComponents +from openhcs.core.context.processing_context import ProcessingContext from openhcs.core.runtime_source_binding_cache import RuntimeSourceBindingContextCache from openhcs.core.source_binding_selection import ( DeclaredSourceMetadataRecord, @@ -258,8 +259,14 @@ def test_snapshot_owns_position_and_projection_map_views(): context.source_projections_by_virtual_path[PATH] = original_projection -def test_warmed_cache_transport_reconstructs_all_derived_defaults(): - cache = RuntimeSourceBindingContextCache() +@pytest.mark.parametrize("context_owned", (False, True)) +def test_warmed_cache_transport_reconstructs_all_derived_defaults(context_owned): + processing_context = ProcessingContext(axis_id="A01") + cache = ( + processing_context.runtime_source_binding_context_cache + if context_owned + else RuntimeSourceBindingContextCache() + ) parser = CountingParser() source_projection = projection({ORIGINAL_SOURCE_METADATA_FIELD: {"Plate": "A"}}) context = snapshot(cache, source_projection, parser) @@ -269,7 +276,16 @@ def test_warmed_cache_transport_reconstructs_all_derived_defaults(): ) assert cache.source_resolution_snapshots assert cache.source_metadata_by_mapping_identity - restored = pickle.loads(pickle.dumps(cache)) + restored_owner = pickle.loads( + pickle.dumps(processing_context if context_owned else cache) + ) + restored = ( + restored_owner.runtime_source_binding_context_cache + if context_owned + else restored_owner + ) + if context_owned: + assert restored_owner.axis_id == "A01" assert restored == RuntimeSourceBindingContextCache() rebuilt = snapshot(restored, source_projection, parser) assert rebuilt is not context From 0c9521abb9b4bb1a3ef2a2cb86003a48d37fffd6 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Thu, 1 Oct 2026 14:12:33 -0400 Subject: [PATCH 007/235] Preserve original metadata record hash projection --- openhcs/core/source_binding_selection.py | 2 +- tests/unit/test_runtime_source_resolution_snapshot.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/openhcs/core/source_binding_selection.py b/openhcs/core/source_binding_selection.py index 13e7673ba9..c4c9ca4007 100644 --- a/openhcs/core/source_binding_selection.py +++ b/openhcs/core/source_binding_selection.py @@ -150,7 +150,7 @@ def __eq__(self, other: object) -> bool: return self.fields == other.fields def __hash__(self) -> int: - return hash(self.fields) + return hash((self.fields,)) @dataclass(frozen=True, slots=True, eq=False) diff --git a/tests/unit/test_runtime_source_resolution_snapshot.py b/tests/unit/test_runtime_source_resolution_snapshot.py index d934eee0b1..e49a9c0f8a 100644 --- a/tests/unit/test_runtime_source_resolution_snapshot.py +++ b/tests/unit/test_runtime_source_resolution_snapshot.py @@ -224,6 +224,7 @@ def test_declared_and_resolved_records_preserve_family_value_identity(): ) assert declared == resolved assert hash(declared) == hash(resolved) + assert hash(declared) == hash((declared.fields,)) assert declared != SourceMetadataRecord.from_mapping({"site": 2, "well": "A01"}) nested = {ORIGINAL_SOURCE_METADATA_FIELD: {"Plate": "A"}} with pytest.raises(TypeError): From bc2beb50152c5db0d42d976d5a6ab16aecf3c26f Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Thu, 1 Oct 2026 14:16:47 -0400 Subject: [PATCH 008/235] Record materialization source ratchet qualification (#384) --- .../validation/materialization_plumbing_384_20261001.rst | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/validation/materialization_plumbing_384_20261001.rst b/docs/validation/materialization_plumbing_384_20261001.rst index a0dc46bcbf..4d7669a4ea 100644 --- a/docs/validation/materialization_plumbing_384_20261001.rst +++ b/docs/validation/materialization_plumbing_384_20261001.rst @@ -85,6 +85,13 @@ and export, all three runtime observation modes, pickle transport after resource release, exact combined worker/parent export paths, materialization formats, metadata publication/reconciliation, runtime/viewer validation and ZMQ consumers. -R0/R1 source guard qualification, representative ordinary unprofiled timing and +The original unmodified R0 guard passes for a264ee95546ab17a3ded40f81759cc6234d08817 +against main e341ac9b55cb2b6a143df9083785b296c23000df. There are no positive debt +deltas; ZMQExecutionServer god-class excess decreases by eight lines and the +artifact materialization foreign-absence probe decreases by one. This revision +includes the normal merge of main's fixture/documentation fix; production sources +are identical to the 267-test candidate. + +Combined R1 source qualification, representative ordinary unprofiled timing and native numerical parity remain required before promotion. No benchmark ratios or speedup claim are attached to this receipt. From ea73dadc1dbdf518c267da6545cd012528bebaef Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Thu, 1 Oct 2026 14:28:35 -0400 Subject: [PATCH 009/235] Enforce resolved metadata invariants and verify adapter provenance --- openhcs/core/runtime_source_binding_cache.py | 2 +- openhcs/core/source_binding_selection.py | 19 ++++--- .../test_runtime_adapter_source_artifacts.py | 50 ++++++++++++++++--- ...test_runtime_source_resolution_snapshot.py | 11 ++++ 4 files changed, 68 insertions(+), 14 deletions(-) diff --git a/openhcs/core/runtime_source_binding_cache.py b/openhcs/core/runtime_source_binding_cache.py index d067f2b5fe..0231210c85 100644 --- a/openhcs/core/runtime_source_binding_cache.py +++ b/openhcs/core/runtime_source_binding_cache.py @@ -125,7 +125,7 @@ def source_pattern_context( self.source_resolution_snapshots[key] = snapshot return snapshot.context - def __reduce__(self) -> tuple[type["RuntimeSourceBindingContextCache"], tuple[()]]: + def __reduce__(self) -> tuple[type[RuntimeSourceBindingContextCache], tuple[()]]: """Transport reconstructs all derived caches from declaration defaults.""" return type(self), () diff --git a/openhcs/core/source_binding_selection.py b/openhcs/core/source_binding_selection.py index c4c9ca4007..fde043f4dc 100644 --- a/openhcs/core/source_binding_selection.py +++ b/openhcs/core/source_binding_selection.py @@ -170,20 +170,25 @@ def resolve( class ResolvedSourceMetadataRecord(SourceMetadataRecord): """Immutable path-specific metadata resolved at a runtime snapshot boundary.""" - @classmethod - def from_resolved_mapping( - cls, metadata: SourceMetadataMapping - ) -> "ResolvedSourceMetadataRecord": - return cls( + def __post_init__(self) -> None: + object.__setattr__( + self, + "fields", tuple( ( str(key), SourceBindingRuntimeMetadataNormalizer.normalized_value(value), ) - for key, value in metadata.items() - ) + for key, value in self.fields + ), ) + @classmethod + def from_resolved_mapping( + cls, metadata: SourceMetadataMapping + ) -> "ResolvedSourceMetadataRecord": + return cls(tuple(metadata.items())) + def resolve( self, path: str, diff --git a/tests/unit/test_runtime_adapter_source_artifacts.py b/tests/unit/test_runtime_adapter_source_artifacts.py index 28595b345a..dd4e4357fe 100644 --- a/tests/unit/test_runtime_adapter_source_artifacts.py +++ b/tests/unit/test_runtime_adapter_source_artifacts.py @@ -2,6 +2,7 @@ from types import SimpleNamespace import numpy as np +import pytest from polystore.base import ensure_storage_registry, storage_registry from polystore.filemanager import FileManager from scipy.io import savemat @@ -12,7 +13,9 @@ from openhcs.core.component_group_scope import RuntimeExecutionAxisScope from openhcs.core.runtime_adapters import RuntimeAdapterRequest from openhcs.core.runtime_image_loading import ImagePayloadSourceMetadataContext -from openhcs.core.runtime_image_values import image_payload_data +from openhcs.core.runtime_image_values import image_payload_data, image_payload_metadata +from openhcs.core.runtime_source_binding_cache import RuntimeSourceBindingContextCache +from openhcs.core.source_binding_selection import SourcePatternResolutionContext from openhcs.core.runtime_object_labels import ObjectLabelSet from openhcs.core.source_binding_workspace import SourceBindingWorkspaceProjector from openhcs.core.source_bindings import ( @@ -54,6 +57,7 @@ def _filemanager() -> FileManager: def test_source_artifact_inputs_share_workspace_vfs_and_contract_resolution( tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, ) -> None: source_root = tmp_path / "source" workspace_root = tmp_path / "workspace" @@ -141,6 +145,7 @@ def binding( filemanager=filemanager, microscope_handler=microscope_handler, runtime_source_workspace_projection_cache=projection_cache, + runtime_source_binding_context_cache=RuntimeSourceBindingContextCache(), source_image_set_identity_policy=SourceImageSetIdentityPolicy( frozenset((AllComponents.CHANNEL,)) ), @@ -215,12 +220,11 @@ def request() -> RuntimeAdapterRequest: ) illumination_request = request() + illumination_payload = illumination_request.source_artifact_payload( + illumination_binding.input_spec().ref() + ) np.testing.assert_array_equal( - image_payload_data( - illumination_request.source_artifact_payload( - illumination_binding.input_spec().ref() - ) - ), + image_payload_data(illumination_payload), illumination[np.newaxis, ...], ) @@ -245,3 +249,37 @@ def request() -> RuntimeAdapterRequest: ) assert isinstance(label_set, ObjectLabelSet) np.testing.assert_array_equal(label_set.labels, labels[np.newaxis, ...]) + + # Exercise the actual source adapter and VFS under the independent live + # selector route. Payload identity belongs to the unchanged typed source + # projection, not the selector's resolved metadata record representation. + with monkeypatch.context() as live_selector: + live_selector.setattr( + RuntimeSourceBindingContextCache, + "source_pattern_context", + lambda self, **kwargs: SourcePatternResolutionContext.from_projection( + **kwargs + ), + ) + live_illumination = request().source_artifact_payload( + illumination_binding.input_spec().ref() + ) + live_labels = request().source_artifact_payload(labels_spec.ref()) + for produced, live in ( + (illumination_payload, live_illumination), + (labels_payload, live_labels), + ): + np.testing.assert_array_equal( + image_payload_data(produced), image_payload_data(live) + ) + produced_metadata = image_payload_metadata(produced) + live_metadata = image_payload_metadata(live) + assert produced_metadata == live_metadata + assert ( + produced_metadata.source_provenance.source_identity.identity + == live_metadata.source_provenance.source_identity.identity + ) + assert ( + produced_metadata.source_provenance.represented_source_identities + == live_metadata.source_provenance.represented_source_identities + ) diff --git a/tests/unit/test_runtime_source_resolution_snapshot.py b/tests/unit/test_runtime_source_resolution_snapshot.py index e49a9c0f8a..1a2662a51c 100644 --- a/tests/unit/test_runtime_source_resolution_snapshot.py +++ b/tests/unit/test_runtime_source_resolution_snapshot.py @@ -260,6 +260,17 @@ def test_snapshot_owns_position_and_projection_map_views(): context.source_projections_by_virtual_path[PATH] = original_projection +def test_resolved_direct_constructor_owns_deep_immutable_invariant(): + nested = {"Plate": "before"} + resolved = ResolvedSourceMetadataRecord(((ORIGINAL_SOURCE_METADATA_FIELD, nested),)) + nested["Plate"] = "after" + assert resolved[ORIGINAL_SOURCE_METADATA_FIELD]["Plate"] == "before" + with pytest.raises(TypeError): + resolved[ORIGINAL_SOURCE_METADATA_FIELD]["Plate"] = "invalid" + with pytest.raises(TypeError, match="Source metadata scalar values"): + ResolvedSourceMetadataRecord((("nested", {"unsupported": {"deep": 1}}),)) + + @pytest.mark.parametrize("context_owned", (False, True)) def test_warmed_cache_transport_reconstructs_all_derived_defaults(context_owned): processing_context = ProcessingContext(axis_id="A01") From 74ecf9d3ea167a24cc316be11e1a71f43795de7a Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Thu, 1 Oct 2026 14:41:21 -0400 Subject: [PATCH 010/235] Document integrated runtime plumbing qualification and limits (#384) --- .../shared_runtime_plumbing_384_20261001.rst | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 docs/validation/shared_runtime_plumbing_384_20261001.rst diff --git a/docs/validation/shared_runtime_plumbing_384_20261001.rst b/docs/validation/shared_runtime_plumbing_384_20261001.rst new file mode 100644 index 0000000000..b7fee9f740 --- /dev/null +++ b/docs/validation/shared_runtime_plumbing_384_20261001.rst @@ -0,0 +1,64 @@ +Shared runtime plumbing qualification +==================================== + +Issue #384 covers repeated source resolution, metadata construction and artifact +rendering outside callable execution. This branch combines three production +dependencies under their existing lifecycle owners. It does not yet establish an +end-to-end speedup. + +The unmodified-main light trace located about 3.44 seconds in loading, unstacking, +saving and finalization for the representative 3D pipeline. CP request and output +context construction account for another approximately 1.216 seconds. These are +diagnostic bounds, not additive nested timer totals or accepted benchmark clocks. +Repeated artifact rendering alone is bounded at about 0.297 seconds; saved plane +projection replay estimates about 0.20 seconds for that individual component. +Neither is admitted as a sufficient standalone performance route. The integrated +boundary must demonstrate a material improvement in ordinary execution. + +Production authorities +---------------------- + +``ImageMetadataProjection`` owns the common single-pass image metadata algorithm. +Source-plane and leading-axis specializations provide their declared projection +semantics through inheritance. Provenance constructors consume the actual +``SourceImageProvenance`` constructor defaults, avoiding absent-field allocation +without a second default roster. Mutable scalar payload metadata retains its +independent ownership and mutation controls. + +``SourceMetadataRecord`` owns declared/parser/rule resolution. Its declared leaf +retains live input semantics, while its resolved leaf enforces owned immutable +metadata at construction. ``RuntimeSourceResolutionSnapshot`` belongs to the +existing context-local runtime source cache and retains its determining inputs. +Only the explicit execution factories take this boundary; direct constructors and +unknown paths preserve live resolution. Derived caches are discarded on process +transport. The original adapter provenance construction remains authoritative: +deep-freezing selector records must not change repr-based image identities. + +``MaterializationBatch`` owns rendering and saving. Actual saved outputs flow into +the existing metadata target family, step observation, worker observation and +plate observation. Export paths derive from these outcomes after payload resource +release. Removed rerender helpers and the old context-derived export path API have +no remaining consumers. Historical debug reuse retains explicitly named previews; +these do not claim new successful saves. The component receipt describes the +retained CSV consolidation and independent viewer expectation consumers. + +Qualification and disposition +---------------------------- + +The integrated source at b88eb36604f22e12f3fbc4123c251b0f12b8e48f passed 1,362 +behavioral tests in 43.25 seconds. After the normal merge of main's classification +fix 7a5ee630c, all 18 classification controls passed in 3.35 seconds. Each of the +three component source changes passed the original unmodified native R0 guard. +Combined R0/R1, current-main correctness synchronization, ordinary paired pipeline +timings and CellProfiler numerical parity remain required before promotion. + +The earlier runtime-plumbing prototype 2253ec63b is rejected. Its paired timing +did not establish a gain and its original R0 gate failed. It is not part of this +branch or the accepted evidence. Failed controls and diagnostic observations are +retained in the external benchmark evidence directory rather than replaced with +the fastest observations. + +Pipeline clocks exclude ZMQ server startup, mandatory registry/kernel prewarming +and shutdown. Timed runs use the ordinary public OUTCOMES path and default memory +observer, without profiling injection. Candidate and baseline must have frozen +clean source revisions and the same dependencies, affinity and environment. From 97f1160aade32613db4fe544339a8457dfde3fa3 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Thu, 1 Oct 2026 15:15:11 -0400 Subject: [PATCH 011/235] fix: retain raster admission for actual artifact publication --- .../materialization_plumbing_384_20261001.rst | 35 +++++ openhcs/core/steps/function_outputs.py | 1 + tests/unit/test_function_outputs.py | 124 ++++++++++++++++++ 3 files changed, 160 insertions(+) diff --git a/docs/validation/materialization_plumbing_384_20261001.rst b/docs/validation/materialization_plumbing_384_20261001.rst index 4d7669a4ea..8125d2b4b7 100644 --- a/docs/validation/materialization_plumbing_384_20261001.rst +++ b/docs/validation/materialization_plumbing_384_20261001.rst @@ -95,3 +95,38 @@ are identical to the 267-test candidate. Combined R1 source qualification, representative ordinary unprofiled timing and native numerical parity remain required before promotion. No benchmark ratios or speedup claim are attached to this receipt. + +Actual array outputs and raster publication +------------------------------------------ + +The first combined native comparison retained 28 numerically equivalent cases +and two illumination metadata failures. The saved illumination NumPy arrays were +actual outputs, but their NumPy-only directory had incorrectly become eligible +for raster image inventory publication. The existing FileManager inventory +includes declared raster formats; NumPy array exports are outside that inventory. +The candidate had removed OutputTarget's inherited ``contains_images`` admission +gate while switching directory discovery to actual saved outputs. + +The correction restores that existing nominal gate. Actual NumPy saved outputs +and their observation locations remain intact; publication and completed-plate +reconciliation consume only directories with images in the existing inventory. +No second format registry, rerendering fallback or empty source projection was +introduced. + +Independent controls use a real FileManager and physical NumPy-only and mixed +TIFF/NumPy outputs. The original candidate fails the NumPy-only control with the +same empty SourceProjectionSet error; its mixed-directory control passes. Both +controls pass after restoration, verifying exact saved pixels, actual observation +paths, raster-only inventory and typed calibration. The focused red and green +receipts are ``materialization-npy-publication-red-v3-20261001.log/xml`` and +``materialization-npy-publication-green-20261001.log/xml`` in the external benchmark +evidence directory. Earlier import-order and typed-unit fixture errors are also +retained separately; they are harness failures, not the product regression. + +The expanded eleven-module consumer gate passes all 269 tests using only the +original nominal viewer route fixture from the root test configuration. Two +initial broader invocations retained 264 passes and five missing-fixture setup +errors; the corrected fixture registration preserves those same five controls. +The final receipt is ``materialization-npy-publication-consumers-v3-20261001.log/xml``. +Full native parity and fresh accepted timing remain required for the corrected +combined source; the original 28-pass/two-error receipt is not replaced. diff --git a/openhcs/core/steps/function_outputs.py b/openhcs/core/steps/function_outputs.py index eda426ccc8..05b7fb539c 100644 --- a/openhcs/core/steps/function_outputs.py +++ b/openhcs/core/steps/function_outputs.py @@ -1164,6 +1164,7 @@ def production_targets( target for directory in directories for target in (self.for_directory(directory),) + if target.contains_images(context) ) def reconciliation_targets( diff --git a/tests/unit/test_function_outputs.py b/tests/unit/test_function_outputs.py index a97b9bbb33..7d887b0825 100644 --- a/tests/unit/test_function_outputs.py +++ b/tests/unit/test_function_outputs.py @@ -54,6 +54,7 @@ SOURCE_VOXEL_SPACING_FIELD, SOURCE_VOXEL_SPACING_UNIT_FIELD, SourceVoxelSpacing, + SourceVoxelSpacingUnit, ) from openhcs.core.source_projection import SourceProjectionMetadataSerializer from openhcs.core.source_spatial_domain import SourceSpatialDomain @@ -2388,3 +2389,126 @@ def test_declared_image_destinations_publish_and_reconcile_after_value_cleanup( assert projection.component_value(AllComponents.CHANNEL) == "2" assert projection.source_metadata["site"] == "1" np.testing.assert_array_equal(tifffile.imread(tmp_path / path), pixels) + + +@pytest.mark.parametrize("with_raster", [False, True]) +def test_actual_array_exports_only_publish_declared_raster_inventory( + tmp_path, with_raster +): + """A saved NumPy array is an actual output, without being a raster inventory.""" + filemanager = FileManager( + { + Backend.DISK.value: DiskStorageBackend(), + Backend.MEMORY.value: MemoryStorageBackend(), + } + ) + context = context_stub(filemanager, parser=SourceSchemaFilenameParser()) + context.runtime_value_store = RuntimeValueStore() + context.metadata_cache = {} + context.tiff_config = None + plan = function_step_plan("Independent array and raster exports") + plan.streaming_configs = {} + plan.write_backend = Backend.MEMORY.value + plan.output_plate_root = str(tmp_path) + plan.output_dir = tmp_path / "images" + plan.sub_dir = "images" + plan.analysis_results_dir = str(tmp_path / "results") + plan.create_openhcs_metadata = True + plan.runtime_artifact_materialization = RuntimeArtifactMaterializationPlan( + persistent_enabled=True, + persistent_backend=Backend.DISK.value, + ) + context.step_plans = {plan.step_index: plan} + pixels = np.arange(20, dtype=np.float32).reshape(4, 5) / 7 + payload = ImageMetadataPayload( + pixels, + ImagePayloadMetadata( + source_component_metadata={ + "well": "A01", + "site": "1", + "channel": "2", + "z_index": "1", + "timepoint": "1", + }, + source_dtype="float32", + source_voxel_spacing=SourceVoxelSpacing( + (0.65, 0.65), SourceVoxelSpacingUnit.MICROMETERS + ), + ), + ) + declarations = [("NumericField", "exports/Illum.npy")] + if with_raster: + declarations.append(("RasterField", "exports/A01_s001_w2_z001_t001.tif")) + for name, relative_path in declarations: + output_plan = ArtifactOutputPlan( + name=name, + path=f"/memory/{name}.pkl", + artifact_type=ImageArtifactType, + materialization=MaterializationSpec( + ImageFileOptions( + filename_suffix=Path(relative_path).suffix, + relative_path_template=relative_path, + ) + ), + ) + plan.artifact_outputs[output_plan.ref()] = output_plan + context.runtime_value_store.record( + RuntimeValue.normalize(output_plan, payload, axis_id="A01"), + path=output_plan.path, + backend=Backend.MEMORY.value, + ) + + materializations = RuntimeArtifactMaterializationAuthority.materialize( + context, plan + ) + saved_outputs = tuple( + output + for artifact in materializations + for output in artifact.outputs_for_backend(Backend.DISK.value) + ) + expected_paths = { + tmp_path / "images" / relative_path for _name, relative_path in declarations + } + assert {Path(output.path) for output in saved_outputs} == expected_paths + assert all(path.is_file() for path in expected_paths) + np.testing.assert_array_equal( + np.load(tmp_path / "images/exports/Illum.npy", allow_pickle=False), pixels + ) + observed_locations = tuple( + location + for artifact in materializations + for locations in ( + artifact.observation(plan).materialized_locations_by_address.values() + ) + for location in locations + ) + assert {Path(location.path) for location in observed_locations} == expected_paths + + OpenHCSMetadataWriter.write( + context, plan, artifact_materializations=materializations + ) + context.runtime_value_store.clear() + OpenHCSMetadataWriter.finalize_completed_plate({"A01": context}) + metadata_path = tmp_path / "openhcs_metadata.json" + if not with_raster: + assert not metadata_path.exists() + return + np.testing.assert_array_equal( + tifffile.imread(tmp_path / "images/exports/A01_s001_w2_z001_t001.tif"), pixels + ) + subdirectories = json.loads(metadata_path.read_text())[FIELDS.SUBDIRECTORIES] + assert set(subdirectories) == {"images/exports"} + subdirectory = subdirectories["images/exports"] + assert subdirectory[FIELDS.IMAGE_FILES] == [ + "images/exports/A01_s001_w2_z001_t001.tif" + ] + entries = VirtualWorkspaceSourceProjectionEntries.from_subdirectory( + subdirectory + ).entries + raster = entries["images/exports/A01_s001_w2_z001_t001.tif"] + assert raster.source_alias == "RasterField" + assert raster.image_metadata.source_voxel_spacing.values_zyx == (0.65, 0.65) + assert ( + raster.image_metadata.source_voxel_spacing.native_coordinate_unit + == "micrometer" + ) From 6ee21ac8cbac4763619166c06fc6d7fb6da151db Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Thu, 1 Oct 2026 16:01:03 -0400 Subject: [PATCH 012/235] docs: record repaired qualification and formal issue-link audit for #384 --- .../shared_runtime_plumbing_384_20261001.rst | 108 +++++++++++++++++- 1 file changed, 102 insertions(+), 6 deletions(-) diff --git a/docs/validation/shared_runtime_plumbing_384_20261001.rst b/docs/validation/shared_runtime_plumbing_384_20261001.rst index b7fee9f740..9f09bf5cae 100644 --- a/docs/validation/shared_runtime_plumbing_384_20261001.rst +++ b/docs/validation/shared_runtime_plumbing_384_20261001.rst @@ -45,12 +45,108 @@ retained CSV consolidation and independent viewer expectation consumers. Qualification and disposition ---------------------------- -The integrated source at b88eb36604f22e12f3fbc4123c251b0f12b8e48f passed 1,362 -behavioral tests in 43.25 seconds. After the normal merge of main's classification -fix 7a5ee630c, all 18 classification controls passed in 3.35 seconds. Each of the -three component source changes passed the original unmodified native R0 guard. -Combined R0/R1, current-main correctness synchronization, ordinary paired pipeline -timings and CellProfiler numerical parity remain required before promotion. +The integrated source passed 1,362 behavioral tests. Each of the three components +passed the original unmodified R0 guard. Final combined R0/R1 passed against main +91dc52d4040890e1b88494acba7368196f6cd1e8 at tested source +dc1a371b06079ff249c8c6432953d21e0c05917b, without increasing debt allowances or +source checks. The main synchronization passed 85 help/output/source-resolution +controls. These source ratchets are scoped guards, not a formal proof of every +dynamic binding. + +The first complete saved-native gate exposed two illumination-correction +publication failures: actual numeric NPY saves admitted directories without any +raster images into metadata publication. The inherited raster-inventory admission +now governs directories derived from successful saves. A real FileManager NPY-only +control reproduced the failure; a mixed TIFF/NPY control preserves exact pixels, +raster inventory, alias and calibration. All 269 related controls passed. The +repaired full gate completed: all 30 cases equivalent, zero differences. The +original 28-pass/two-error gate is retained. + +Repaired ordinary ABBA evidence +------------------------------ + +Two main and two candidate observations per case were retained, without excluding +samples. Revisions are the exact dc1a371b/91dc52d4 sources above. In seconds: + +========================= ======================= ======================= +Case Mean execution Mean pipeline total +========================= ======================= ======================= +3D monolayer 10.278553 -> 9.657654 12.241441 -> 11.607624 +Imaging Flow Cytometry 15.506204 -> 15.423871 17.719883 -> 17.949389 +Advanced segmentation 8.658128 -> 8.887496 11.158074 -> 11.397346 +========================= ======================= ======================= + +3D execution improves 6.04%, while advanced execution regresses 2.65%. The default +memory observer's mean peak process-tree RSS changes by +14.65% for 3D, -3.41% for +Imaging Flow Cytometry and +10.87% for advanced segmentation. Process-tree RSS can +count shared fork pages repeatedly; these figures do not prove distinct physical +allocation. Candidate memory varies substantially between the two observations. +This is mixed evidence, not an accepted broad optimization. PR394 remains draft. + +The pre-publication-repair eight-sweep observations remain retained separately; +their larger improvement is not substituted for the repaired source's results. +External evidence: shared-runtime-plumbing-repaired-qualified-abba-summary- +20261001.json and every corresponding source/input/observation receipt in the +openhcs-benchmark-runs evidence directory. + +Fresh native qualification and next dominant route +------------------------------------------------ + +The revision-pinned matched Imaging Flow Cytometry driver completed three observed +native CP/OpenHCS repetitions and one warmup. All observed repetitions have zero +output differences under the existing numerical tolerances, exact discrete/schema +checks and strict declared output inventory. The input is one well with two native +image sets, one OpenHCS worker and one native job; the configured worker start +method is fork. Native and candidate phase boundaries are retained separately. +The driver's timing_claim explicitly remains none until first-module/first-axis +boundary equivalence is qualified. No native throughput ratio is asserted here; +the physical 3D volume/plane consumer gap also remains unfinished. + +Fresh paired diagnostic traces cover all three workloads with identical owner +hooks. Their high-frequency instrumentation perturbs costs, so these are authority +and call-count evidence, not accepted timing clocks. The 3D trace reduces +SourceImageIdentity construction from 142,412 to 78,769, but still calls +SourceMetadataRoleView.scalar_items 194,349 times and source metadata field identity +1,120,064 times. Image-processing invocation remains approximately unchanged in +these traces. Repeated metadata queries are the next potentially material generic +route; saved representative replay must establish its transferable payoff before +another production authority is admitted. Immutable query ownership must retain +live public mapping behavior, lazy validation, alias/role semantics and image +identity fingerprints. + +Source snapshots structurally retain historical projection owners, but output +publication does not normally replace these workspaces' cached metadata documents. +The observed RSS increase is therefore not attributed to snapshot generations. +Rendered outputs also remain visible through immediate publication; many arrays +already belong to runtime storage. Per-process memory and ownership diagnostics +must distinguish extra references from newly allocated buffers before changing +that boundary. + +The subsequent normal main396 merge changes documentation, knowledge resources +and five knowledge-transfer test assertions. OpenHCS production source bytes are +unchanged from the corresponding measured candidate and baseline revisions; the +existing source/benchmark evidence remains pinned to its original revisions. + +Issue and branch audit +---------------------- + +The source components are integrated in actual draft PR394, formally linked to +issue384. A branch/history audit covered 46 local worktrees and 194 remote branches; +the two subsequently created branches have PR396 (merged) and PR397 (draft). +Current branch tips were checked beyond historical PR association, including exact +source patch equivalence for squash/rebase history. No additional useful +unpublished fix branch was identified. Rejected prototypes and obsolete planning +branches are not nominated as unfinished production fixes. + +The fresh open-issue audit verifies 7 of 29 have formal closing PR references; +22 still have no actual future fixing PR. Mere cross-references and diagnosis-only +drafts are not counted. Missing manual closing links were added for +138/207, 213/217, 320/217, 355/358 and 395/397. Existing 384/394 and 386/388 links +have closing cross-reference events. GitHub's official manual mutation is a no-op +for these already linked pairs; no exact ConnectedEvent is claimed for them. +Audit evidence: current-open-issues-connected-closing-audit-20261001.json/.md, +remaining-remote-branch-pr-audit-20261001.json and +local-worktree-branch-inventory-20261001.json in the external evidence directory. The earlier runtime-plumbing prototype 2253ec63b is rejected. Its paired timing did not establish a gain and its original R0 gate failed. It is not part of this From 5d1f1a781eda851b4783b68db342b76eeef9dea9 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Thu, 1 Oct 2026 17:28:43 -0400 Subject: [PATCH 013/235] refactor(runtime): store cached image payloads directly (#384) --- openhcs/core/runtime_stack_cache.py | 13 +++---------- openhcs/core/steps/function_runtime.py | 2 +- tests/unit/test_runtime_stack_cache_lifetime.py | 2 +- 3 files changed, 5 insertions(+), 12 deletions(-) diff --git a/openhcs/core/runtime_stack_cache.py b/openhcs/core/runtime_stack_cache.py index 4b92cb0020..6c2248777b 100644 --- a/openhcs/core/runtime_stack_cache.py +++ b/openhcs/core/runtime_stack_cache.py @@ -27,18 +27,11 @@ def from_paths( return cls(tuple(str(path) for path in paths), str(memory_type)) -@dataclass(frozen=True, slots=True) -class RuntimeImageStackCacheValue: - """Cached payload for an ordered runtime-slice path set.""" - - stack: RuntimeArrayData - - @dataclass(slots=True) class RuntimeImageStackCache: """Processing-context-local cache for adjacent step stack reuse.""" - stacks: dict[RuntimeImageStackCacheKey, RuntimeImageStackCacheValue] = field( + stacks: dict[RuntimeImageStackCacheKey, RuntimeArrayData] = field( default_factory=dict ) @@ -47,7 +40,7 @@ def get( paths: tuple[str, ...], *, memory_type: str, - ) -> RuntimeImageStackCacheValue | None: + ) -> RuntimeArrayData | None: """Return a cached stack for the exact ordered path and memory contract.""" return self.stacks.get( RuntimeImageStackCacheKey.from_paths(paths, memory_type=memory_type) @@ -63,7 +56,7 @@ def store( """Store a stack produced for a just-saved ordered path set.""" self.stacks[ RuntimeImageStackCacheKey.from_paths(paths, memory_type=memory_type) - ] = RuntimeImageStackCacheValue(stack=stack) + ] = stack def discard_paths(self, paths: tuple[str, ...]) -> None: """Discard cached stacks that include any of the supplied paths.""" diff --git a/openhcs/core/steps/function_runtime.py b/openhcs/core/steps/function_runtime.py index 266ed9b128..8b4ad2f902 100644 --- a/openhcs/core/steps/function_runtime.py +++ b/openhcs/core/steps/function_runtime.py @@ -2821,7 +2821,7 @@ def _load_input_stack(self) -> PatternGroupData: metadata_mode=metadata_mode, ).compose() else: - main_data_stack = cached_stack.stack + main_data_stack = cached_stack return PatternGroupData( matching_files=matching_files, diff --git a/tests/unit/test_runtime_stack_cache_lifetime.py b/tests/unit/test_runtime_stack_cache_lifetime.py index 78470ea371..f735926289 100644 --- a/tests/unit/test_runtime_stack_cache_lifetime.py +++ b/tests/unit/test_runtime_stack_cache_lifetime.py @@ -38,7 +38,7 @@ def test_release_drops_all_cached_views_and_allows_reuse(context_owned: bool) -> assert all(cache.get(entry, memory_type="numpy") is None for entry in paths) replacement = np.zeros((1, 8, 8), dtype=np.uint16) cache.store(paths[0], memory_type="numpy", stack=replacement) - assert cache.get(paths[0], memory_type="numpy").stack is replacement + assert cache.get(paths[0], memory_type="numpy") is replacement def test_context_image_cache_release_preserves_runtime_artifacts_and_observations() -> ( From b4cbcb6cb92b6181d822f59188dcb46f89a41aa8 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Thu, 1 Oct 2026 17:38:13 -0400 Subject: [PATCH 014/235] perf(metadata): derive geometry from transaction-owned projections (#384) --- openhcs/core/virtual_workspace_metadata.py | 36 ++++-- tests/unit/test_persisted_image_metadata.py | 124 +++++++++++++++++++- 2 files changed, 148 insertions(+), 12 deletions(-) diff --git a/openhcs/core/virtual_workspace_metadata.py b/openhcs/core/virtual_workspace_metadata.py index 6ef9709f73..ff4abc6d0c 100644 --- a/openhcs/core/virtual_workspace_metadata.py +++ b/openhcs/core/virtual_workspace_metadata.py @@ -5,7 +5,7 @@ import json import logging import os -from collections.abc import Mapping, Sequence +from collections.abc import Iterable, Mapping, Sequence from dataclasses import dataclass, field from pathlib import Path from types import MappingProxyType @@ -97,7 +97,12 @@ def update(data: dict[str, Any] | None) -> dict[str, Any]: } else: subdirectory[key] = value - self._update_projection_geometry(subdirectory) + self._update_projection_geometry( + subdirectory, + VirtualWorkspaceSourceProjectionEntries.from_subdirectory( + subdirectory + ).entries.values(), + ) return data self._execute_update( @@ -139,7 +144,12 @@ def update(data): subdirectory_name, {} ) self._merge_source_projection_fields(subdirectory, projection_metadata) - self._update_projection_geometry(subdirectory) + self._update_projection_geometry( + subdirectory, + VirtualWorkspaceSourceProjectionEntries.from_subdirectory( + subdirectory + ).entries.values(), + ) return data self._execute_update(metadata_path, update) @@ -238,21 +248,25 @@ def update(data): subdirectory[serializer.MAIN_FIELD] = True if results_dir is not None: subdirectory[serializer.RESULTS_DIR_FIELD] = results_dir - self._update_projection_geometry(subdirectory) + self._update_projection_geometry( + subdirectory, + (projection for projection, _path in retained_paths), + ) return data self._execute_update(metadata_path, update) @staticmethod - def _update_projection_geometry(subdirectory: dict[str, Any]) -> None: - entries = VirtualWorkspaceSourceProjectionEntries.from_subdirectory( - subdirectory - ).entries - if not entries: - return + def _update_projection_geometry( + subdirectory: dict[str, Any], + source_projections: Iterable[SourceProjection], + ) -> None: + """Derive geometry from the transaction's current nominal projections.""" unique_projections: dict[tuple[object, ...], SourceProjection] = {} - for projection in entries.values(): + for projection in source_projections: unique_projections.setdefault(projection.identity_key, projection) + if not unique_projections: + return projections = SourceProjectionSet(tuple(unique_projections.values())) subdirectory[FIELDS.GRID_DIMENSIONS] = ( SourceTileLayout.metadata_grid_dimensions(projections) diff --git a/tests/unit/test_persisted_image_metadata.py b/tests/unit/test_persisted_image_metadata.py index 7df244dfc0..6dd325823b 100644 --- a/tests/unit/test_persisted_image_metadata.py +++ b/tests/unit/test_persisted_image_metadata.py @@ -33,7 +33,13 @@ SourceProjectionMetadataSerializer, ) from openhcs.core.source_spatial_domain import SourceSpatialDomain -from openhcs.core.virtual_workspace_metadata import FIELDS, AtomicMetadataWriter +from openhcs.core.virtual_workspace_metadata import ( + FIELDS, + AtomicMetadataWriter, + MetadataWriteError, + VirtualWorkspaceSourceProjectionEntries, +) +from openhcs.microscopes.source_schema import SourceSchemaFilenameParser from openhcs.serialization.json import to_jsonable @@ -189,6 +195,122 @@ def merge(well): assert len(subdir[FIELDS.SOURCE_METADATA]) == 4 +def calibrated_projection(well, path, pixel_size): + spacing = SourceVoxelSpacing((pixel_size, pixel_size)) + source_metadata = {} + spacing.merge_into(source_metadata, path=path) + return SourcePlaneProjection( + OpenHCSPlaneAddress.from_values(well, 1, 1, 1, 1), + SourcePixelRef("disk", path), + source_metadata=source_metadata, + image_metadata=metadata_fixture().replace_fields(source_voxel_spacing=spacing), + ) + + +def publish_projection_inventory(writer, path, projection_metadata, saved_paths): + writer.publish_source_projection_metadata( + path, + "images", + projection_metadata, + serializer=SourceProjectionMetadataSerializer(SourceSchemaFilenameParser()), + saved_image_paths=saved_paths, + microscope_handler_name="VirtualWorkspaceMicroscopeHandler", + source_filename_parser_name="SourceSchemaFilenameParser", + component_labels={}, + backend="disk", + is_main=True, + results_dir=None, + ) + + +@pytest.mark.parametrize( + "merge_method", ("merge_subdirectory_metadata", "merge_source_projection_metadata") +) +def test_atomic_projection_merge_refreshes_geometry_from_current_document( + tmp_path, merge_method +): + path = tmp_path / "openhcs_metadata.json" + writer = AtomicMetadataWriter() + serializer = SourceProjectionMetadataSerializer(SourceSchemaFilenameParser()) + first = calibrated_projection("A01", "images/first.tif", 0.5) + second = calibrated_projection("A02", "images/second.tif", 2.0) + first_fields = serializer.projection_fields(((first, "images/first.tif"),)) + writer.replace_subdirectory_metadata(path, "images", first_fields) + second_fields = serializer.projection_fields(((second, "images/second.tif"),)) + if merge_method == "merge_subdirectory_metadata": + # This API replaces the projection field with the authoritative new list. + writer.merge_subdirectory_metadata(path, {"images": second_fields}) + expected_pixel_size = 2.0 + else: + writer.merge_source_projection_metadata(path, "images", second_fields) + expected_pixel_size = 1.0 # Conflicting calibration has no physical scalar. + subdir = json.loads(path.read_text())[FIELDS.SUBDIRECTORIES]["images"] + assert subdir[FIELDS.PIXEL_SIZE] == expected_pixel_size + + +def test_final_publication_prunes_geometry_and_preserves_other_directories(tmp_path): + path = tmp_path / "openhcs_metadata.json" + writer = AtomicMetadataWriter() + serializer = SourceProjectionMetadataSerializer(SourceSchemaFilenameParser()) + retained = calibrated_projection("A01", "images/retained.tif", 0.5) + deleted = calibrated_projection("A02", "images/deleted.tif", 2.0) + elsewhere = calibrated_projection("A03", "other/retained.tif", 0.5) + projection_paths = ( + (retained, "images/retained.tif"), + (deleted, "images/deleted.tif"), + (elsewhere, "other/retained.tif"), + ) + writer.merge_source_projection_metadata( + path, "images", serializer.projection_fields(projection_paths) + ) + before = json.loads(path.read_text())[FIELDS.SUBDIRECTORIES]["images"] + assert before[FIELDS.PIXEL_SIZE] == 1.0 + publish_projection_inventory(writer, path, None, ("images/retained.tif",)) + subdir = json.loads(path.read_text())[FIELDS.SUBDIRECTORIES]["images"] + assert subdir[FIELDS.PIXEL_SIZE] == 0.5 + assert subdir[FIELDS.IMAGE_FILES] == ["images/retained.tif"] + entries = VirtualWorkspaceSourceProjectionEntries.from_subdirectory(subdir).entries + assert set(entries) == {"images/retained.tif", "other/retained.tif"} + assert entries["images/retained.tif"].image_metadata == retained.image_metadata + assert entries["other/retained.tif"].image_metadata == elsewhere.image_metadata + # A failed final inventory cannot publish fields or prune existing records. + before_bytes = path.read_bytes() + with pytest.raises(MetadataWriteError, match="lack typed produced addresses"): + publish_projection_inventory(writer, path, None, ("images/unowned.tif",)) + assert path.read_bytes() == before_bytes + + +def test_concurrent_step_publication_retains_records_outside_partial_inventory( + tmp_path, +): + path = tmp_path / "openhcs_metadata.json" + writer = AtomicMetadataWriter() + serializer = SourceProjectionMetadataSerializer(SourceSchemaFilenameParser()) + paths = tuple(f"images/{well}.tif" for well in ("A01", "A02", "A03", "A04")) + + def publish(item): + index, image_path = item + projection = calibrated_projection(f"A0{index + 1}", image_path, 0.5) + publish_projection_inventory( + writer, + path, + serializer.projection_fields(((projection, image_path),)), + (image_path,), + ) + + with ThreadPoolExecutor(max_workers=4) as pool: + list(pool.map(publish, enumerate(paths))) + subdir = json.loads(path.read_text())[FIELDS.SUBDIRECTORIES]["images"] + assert set(subdir[FIELDS.WORKSPACE_MAPPING]) == set(paths) + assert len(subdir[FIELDS.IMAGE_FILES]) == 1 # Most recent partial step inventory. + assert subdir[FIELDS.PIXEL_SIZE] == 0.5 + publish_projection_inventory(writer, path, None, paths) + subdir = json.loads(path.read_text())[FIELDS.SUBDIRECTORIES]["images"] + assert subdir[FIELDS.IMAGE_FILES] == list(paths) + assert len(subdir[FIELDS.SOURCE_PROJECTION]) == len(paths) + assert subdir["wells"] == {f"A0{index + 1}": None for index in range(4)} + + def test_metadata_codec_rejects_unknown_declarations(): with pytest.raises(ValueError, match="undeclared"): ImagePayloadMetadata.from_mapping({"imagined_tile_axis": 9}) From aa07241fa1d3a6f3688494c89cd65de6c81e14cd Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Thu, 1 Oct 2026 17:44:36 -0400 Subject: [PATCH 015/235] docs(perf): prioritize shared plumbing and record owner-level evidence (#384) --- .../shared_runtime_plumbing_384_20261001.rst | 84 +++++++++++++++++-- 1 file changed, 75 insertions(+), 9 deletions(-) diff --git a/docs/validation/shared_runtime_plumbing_384_20261001.rst b/docs/validation/shared_runtime_plumbing_384_20261001.rst index 9f09bf5cae..784ecf732e 100644 --- a/docs/validation/shared_runtime_plumbing_384_20261001.rst +++ b/docs/validation/shared_runtime_plumbing_384_20261001.rst @@ -6,6 +6,69 @@ rendering outside callable execution. This branch combines three production dependencies under their existing lifecycle owners. It does not yet establish an end-to-end speedup. +Primary target and abstraction admission +--------------------------------------- + +The primary performance goal is shared runtime work outside callable execution: +invocation and adapter preparation, stack loading, output registration, +finalization, saving, publication and provenance. Each existing abstraction must +own a distinct invariant, behavior or useful validated view. Objects that only +forward or reconstruct facts already owned elsewhere should be removed, with +consumers deriving directly from the determining owner. Shared algorithms belong +on existing nominal ancestors; a new class is not evidence of improved ownership. +Small measured redundancies are folded into the coherent migration even when +they cannot independently close the performance gap. + +The ordinary 3D probe at clean revision 5d2fd489946e6885f161a7379e4eec1d14e307fd +records 9.860550 seconds of public server execution. Its same-thread root takes +9.757393 seconds, including 4.097127 seconds at runtime callable boundaries and +5.118847 seconds elsewhere inside steps. Final plate publication adds 0.476819 +seconds. The callable boundary includes target resolution and signature filtering; +it is not an exact naked-kernel timer. Execution is inline for this one-worker +configuration; a configured fork policy does not imply a separate worker process. + +A deeper probe on the same source records 10.275668 seconds, including 0.023519 +seconds of explicitly isolated input capture. Its nonoverlapping coarse phases +locate 0.697612 seconds in output recording, 0.478745 in image-request preparation, +0.795071 in three atomic publication transactions and another 0.143612 in durable +target reconciliation. Remaining loading and output-registration work takes +0.739507 and 0.722780 seconds respectively. Stack metadata composition takes only +0.102824 seconds across all consumers. These diagnostic clocks locate work and +must not substitute for an ordinary uninstrumented paired comparison. + +Required output work remains included: this pipeline saves 120 TIFF planes and +six result CSVs. Some work outside the callable boundary is numerical table +assembly or actual saving, so the full noncallable duration is not asserted to be +removable indirection. Nine stack-composition calls belong to loading; nineteen +belong to unstacking. Aggregate composition counts do not establish cache misses. + +Saved real-payload replays confirm that duplicated input/name resolution and +rebuilding a compiled callable contract have small individual payoffs: about +0.112 milliseconds per input pair and 4.386 milliseconds per 211 resolutions. +These are included for ownership cleanup, without attributing the larger adapter +gap to them. The direct cache payload migration removes +``RuntimeImageStackCacheValue``, which owned only a forwarded ``stack`` field; +the cache retains path/memory identity, invalidation and execution lifetime. +All 102 related lifetime/source-projection controls pass for that migration. + +Complete saved atomic publication transactions retain actual parser, labels, +saved paths, pretransaction JSON, locking and filesystem writes. Deriving +geometry from the already decoded nominal projections reduces a 60-path +transaction from median 0.13556 to 0.07601 seconds and a 120-path transaction from +0.27224 to 0.15354 seconds, with byte-identical complete JSON. The production +implementation also reproduces both baseline byte hashes, and 57 metadata, +projection and image-output controls pass, including pruning, concurrent partial +inventory updates, fresh geometry and atomic rollback. This is stage replay +evidence, not an end-to-end speedup claim; integrated qualification remains in +progress. + +The existing CellProfiler executor now resolves the compiled raw callable once +at its validated invocation boundary and passes that target through the existing +processing and slice dispatch. The existing callable-view policy accepts that +already resolved target; no additional generic invocation type or parallel +contract/callable fields are needed. Image source-name projection consumes the +already resolved payload through the existing artifact strategy family. + The unmodified-main light trace located about 3.44 seconds in loading, unstacking, saving and finalization for the representative 3D pipeline. CP request and output context construction account for another approximately 1.216 seconds. These are @@ -138,15 +201,18 @@ source patch equivalence for squash/rebase history. No additional useful unpublished fix branch was identified. Rejected prototypes and obsolete planning branches are not nominated as unfinished production fixes. -The fresh open-issue audit verifies 7 of 29 have formal closing PR references; -22 still have no actual future fixing PR. Mere cross-references and diagnosis-only -drafts are not counted. Missing manual closing links were added for -138/207, 213/217, 320/217, 355/358 and 395/397. Existing 384/394 and 386/388 links -have closing cross-reference events. GitHub's official manual mutation is a no-op -for these already linked pairs; no exact ConnectedEvent is claimed for them. -Audit evidence: current-open-issues-connected-closing-audit-20261001.json/.md, -remaining-remote-branch-pr-audit-20261001.json and -local-worktree-branch-inventory-20261001.json in the external evidence directory. +The subsequent complete audit verifies eight of 26 open issues have formal +closing PR references and exact manual ConnectedEvent entries; 18 still lack an +actual complete fixing PR. Mere cross-references and diagnosis-only drafts are +not counted. The pairs are 138/207, 213/217, 320/217, 355/358, 384/394, 386/388, +395/397 and 398/399. For the three automatically detected links whose manual +mutation was initially a no-op, the exact original PR bodies were restored after +the manual links were created. Head, base and draft state were preserved. +Audit evidence: official-development-link-final-20261001.json/.md, +manual-development-relink-exact-events-v3-20261001.json, +remaining-remote-branch-pr-audit-20261001.json and local-worktree-branch-inventory- +20261001.json in the external evidence directory. Counts describe that pinned +audit rather than future repository state. The earlier runtime-plumbing prototype 2253ec63b is rejected. Its paired timing did not establish a gain and its original R0 gate failed. It is not part of this From 1b53d09f36e7bc38aa49109eecdbb6cb5418a0f1 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Thu, 1 Oct 2026 17:47:23 -0400 Subject: [PATCH 016/235] perf(runtime): reuse compiled callable and resolved image inputs (#384) --- .../cellprofiler/runtime/artifact_binding.py | 26 +++- .../runtime/function_contract_execution.py | 19 ++- .../cellprofiler/runtime/module_execution.py | 7 +- ...ellprofiler_function_contract_execution.py | 122 +++++++++++++++++- .../test_cellprofiler_module_execution.py | 32 ++++- 5 files changed, 192 insertions(+), 14 deletions(-) diff --git a/openhcs/interop/cellprofiler/runtime/artifact_binding.py b/openhcs/interop/cellprofiler/runtime/artifact_binding.py index 0fbfc14e41..34f1dc72b1 100644 --- a/openhcs/interop/cellprofiler/runtime/artifact_binding.py +++ b/openhcs/interop/cellprofiler/runtime/artifact_binding.py @@ -181,6 +181,14 @@ def source_image_name( del request return None + def source_image_name_from_value( + self, + value: RuntimeCallableArgument, + ) -> str | None: + """Project a source name from an already resolved artifact value.""" + del value + return None + def source_image_payload( self, request: RuntimeArtifactInputRequest, @@ -253,11 +261,15 @@ def runtime_input_value( def source_image_name( self, request: RuntimeArtifactInputRequest, + ) -> str | None: + return self.source_image_name_from_value(self.raw_runtime_input_value(request)) + + def source_image_name_from_value( + self, + value: RuntimeCallableArgument, ) -> str | None: return single_source_name( - image_payload_metadata( - self.raw_runtime_input_value(request) - ).source_provenance.represented_source_image_names + image_payload_metadata(value).source_provenance.represented_source_image_names ) def source_image_payload( @@ -331,7 +343,13 @@ def source_image_name( self, request: RuntimeArtifactInputRequest, ) -> str | None: - return self.object_labels(request).source_image_name + return self.source_image_name_from_value(self.object_labels(request)) + + def source_image_name_from_value( + self, + value: RuntimeCallableArgument, + ) -> str | None: + return cast(ObjectLabelSet, value).source_image_name def source_image_payload( self, diff --git a/openhcs/interop/cellprofiler/runtime/function_contract_execution.py b/openhcs/interop/cellprofiler/runtime/function_contract_execution.py index c1279ed9cd..79e9a04279 100644 --- a/openhcs/interop/cellprofiler/runtime/function_contract_execution.py +++ b/openhcs/interop/cellprofiler/runtime/function_contract_execution.py @@ -62,7 +62,8 @@ ) _CELLPROFILER_RUNTIME_CALLABLE_POLICY = RuntimeCallablePolicy( - callable_view=RuntimeCallableView.RAW, + # execute() resolves the compiled raw target before processing dispatch. + callable_view=RuntimeCallableView.DECORATED, kwarg_policy=RuntimeInvocationKwargPolicy.SIGNATURE_FILTERED, ) @@ -112,6 +113,7 @@ def execute( executor = type(self)(plane_projection=plane_projection) function_name = callable_contract.function_name processing_contract = callable_contract.require_processing_contract() + runtime_func = callable_contract.resolve_raw_runtime_callable() mode = execution_mode CellProfilerRuntimeProfileLogger.log_module_profile( "cp_executor_mode_resolution", @@ -131,12 +133,19 @@ def execute( | ProcessingContract.FLEXIBLE | ProcessingContract.VOLUMETRIC_TO_SLICE, ): + runtime_kwargs = dict(kwargs) + runtime_kwargs.update( + processing_contract.declaration.consume_semantic_controls( + runtime_kwargs, + func=func, + ) + ) result = processing_contract.execute( executor, - func, + runtime_func, image, callable_contract=callable_contract, - **dict(kwargs), + **runtime_kwargs, ) case ( ImagePayloadExecutionMode.FULL_STACK, @@ -146,7 +155,7 @@ def execute( | ProcessingContract.VOLUMETRIC_TO_SLICE, ): result = executor.execute_pure_3d( - func, + runtime_func, image, callable_contract=callable_contract, **dict(kwargs), @@ -160,7 +169,7 @@ def execute( ): result = executor._execute_aligned_multi_image_stack( callable_contract, - func, + runtime_func, image, **dict(kwargs), ) diff --git a/openhcs/interop/cellprofiler/runtime/module_execution.py b/openhcs/interop/cellprofiler/runtime/module_execution.py index 840624bfaf..54c6661583 100644 --- a/openhcs/interop/cellprofiler/runtime/module_execution.py +++ b/openhcs/interop/cellprofiler/runtime/module_execution.py @@ -1332,8 +1332,11 @@ def _image_request( input_binding = replace(input_binding, current_image=current_runtime_payload) for spec in image_inputs: request = input_binding.artifact_request_for_spec(spec) - payloads.append(image_strategy.runtime_input_value(request)) - source_names.append(image_strategy.source_image_name(request)) + payload = image_strategy.runtime_input_value(request) + payloads.append(payload) + source_names.append( + image_strategy.source_image_name_from_value(payload) + ) parameter_image_inputs = input_binding.image_inputs broadcast_sources = tuple( sources[0] diff --git a/tests/unit/test_cellprofiler_function_contract_execution.py b/tests/unit/test_cellprofiler_function_contract_execution.py index 2a61995cec..af8e2adff9 100644 --- a/tests/unit/test_cellprofiler_function_contract_execution.py +++ b/tests/unit/test_cellprofiler_function_contract_execution.py @@ -3,6 +3,9 @@ from __future__ import annotations from collections.abc import Callable +from dataclasses import dataclass, replace +from functools import wraps +import inspect import numpy as np import pytest @@ -18,7 +21,11 @@ ImageArtifactType, MeasurementsArtifactType, ) -from openhcs.core.callable_contract import CallableContract, CallableMetadata +from openhcs.core.callable_contract import ( + CallableContract, + CallableMetadata, + callable_request, +) from openhcs.core.runtime_plane_projection import ( RuntimePlaneAxis, RuntimePlaneAxisValueProjection, @@ -76,6 +83,119 @@ def test_morphological_skeleton_executes_one_planar_runtime_image() -> None: assert image_payload_data(result).shape == image.shape +def test_compiled_slice_execution_resolves_raw_target_once_and_preserves_request_binding( + monkeypatch: pytest.MonkeyPatch, +) -> None: + @dataclass(frozen=True, slots=True) + class ScaleRequest: + image: object + scale: int + + seen: list[tuple[int, ...]] = [] + + @callable_request(ScaleRequest) + def request_bound(request: ScaleRequest) -> object: + data = image_payload_data(request.image) + seen.append(data.shape) + return data * request.scale + + @wraps(request_bound) + def decorated(*args: object, **kwargs: object) -> object: + raise AssertionError("CellProfiler invoked an outer runtime wrapper") + + contract = CallableContract.from_callable(decorated) + contract = replace( + contract, + metadata=replace( + contract.metadata, + processing_contract=ProcessingContract.PURE_2D, + ), + ) + raw_target = contract.resolve_canonical_raw_callable() + resolve_raw = CallableContract.resolve_raw_runtime_callable + resolutions: list[CallableContract] = [] + + def resolve_once(self: CallableContract) -> Callable[..., object]: + resolutions.append(self) + return resolve_raw(self) + + def reject_reconstruction(*args: object, **kwargs: object) -> CallableContract: + raise AssertionError("Runtime execution rebuilt its compiled contract") + + monkeypatch.setattr(CallableContract, "resolve_raw_runtime_callable", resolve_once) + monkeypatch.setattr(CallableContract, "from_callable", reject_reconstruction) + image = ImagePayloadMetadata( + plane_axis=RuntimePlaneAxis.RUNTIME_SLICE + ).payload_with(np.ones((3, 4, 5), dtype=np.float32), None) + + result = CellProfilerFunctionContractExecutor().execute( + contract, + raw_target, + image, + {"scale": 4, "adapter_control": 17}, + execution_mode=ImagePayloadExecutionMode.NATURAL, + plane_projection=RuntimePlaneAxisValueProjection.preserve( + axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis_size=3, + ), + ) + + assert resolutions == [contract] + assert seen == [(4, 5)] * 3 + assert isinstance(result, RuntimeSliceAlignedValues) + np.testing.assert_array_equal( + np.stack(tuple(image_payload_data(value) for value in result.slices)), + np.full((3, 4, 5), 4.0), + ) + + +@pytest.mark.parametrize("canonical_default", (False, True)) +def test_flexible_dispatch_preserves_canonical_signature_control_defaults( + canonical_default: bool, +) -> None: + seen: list[tuple[tuple[int, ...], bool]] = [] + + def raw(image: np.ndarray, *, slice_by_slice: bool = False) -> np.ndarray: + seen.append((image.shape, slice_by_slice)) + return image + + @wraps(raw) + def decorated(*args: object, **kwargs: object) -> object: + raise AssertionError("CellProfiler invoked an outer runtime wrapper") + + signature = inspect.signature(raw) + decorated.__signature__ = signature.replace( + parameters=( + parameter.replace(default=canonical_default) + if parameter.name == "slice_by_slice" + else parameter + for parameter in signature.parameters.values() + ) + ) + contract = _compiled_contract(decorated, ProcessingContract.FLEXIBLE) + image = ImagePayloadMetadata( + plane_axis=RuntimePlaneAxis.RUNTIME_SLICE + ).payload_with(np.ones((3, 4, 5), dtype=np.float32), None) + + CellProfilerFunctionContractExecutor().execute( + contract, + decorated, + image, + {}, + execution_mode=ImagePayloadExecutionMode.NATURAL, + plane_projection=RuntimePlaneAxisValueProjection.preserve( + axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis_size=3, + ), + ) + + assert seen == ( + [((4, 5), True)] * 3 + if canonical_default + else [((3, 4, 5), False)] + ) + + @pytest.mark.parametrize("processing_contract", tuple(ProcessingContract)) @pytest.mark.parametrize( ("image_mode", "expected_call_count"), diff --git a/tests/unit/test_cellprofiler_module_execution.py b/tests/unit/test_cellprofiler_module_execution.py index 9ba979906e..99a4b572ba 100644 --- a/tests/unit/test_cellprofiler_module_execution.py +++ b/tests/unit/test_cellprofiler_module_execution.py @@ -1368,7 +1368,9 @@ def test_runtime_binding_uses_full_compiled_special_input_contract() -> None: np.testing.assert_array_equal(image_payload_data(topology_inputs[1]), 1.0) -def test_image_artifact_resolution_uses_declared_artifact_alias() -> None: +def test_image_artifact_resolution_uses_declared_artifact_alias( + monkeypatch: pytest.MonkeyPatch, +) -> None: spec = ArtifactSpec.input("IllumStain1", ImageArtifactType) source = ImagePayloadMetadata( source_image_provenance_planes=SourceImageProvenancePlanes.from_components( @@ -1394,6 +1396,17 @@ def test_image_artifact_resolution_uses_declared_artifact_alias() -> None: ).source_image_name(RuntimeArtifactInputRequest(spec=spec, value=source)) is None ) + strategy = RuntimeArtifactTypeStrategy.for_artifact_type(ImageArtifactType) + + def reject_second_resolution(*args, **kwargs): + raise AssertionError("Source-name projection resolved the image again") + + monkeypatch.setattr( + type(strategy), "raw_runtime_input_value", reject_second_resolution + ) + # The original name remains a contributor; the alias alone is not a + # unique represented source name. + assert strategy.source_image_name_from_value(payload) is None def test_main_flow_image_artifact_selects_declared_alias_plane() -> None: @@ -1590,7 +1603,9 @@ def test_main_flow_image_artifact_preserves_declared_runtime_slice_stack() -> No assert image_payload_metadata(payload).source_image_names == ("Stain1",) -def test_main_flow_projection_binds_singleton_broadcast_artifact_as_2d() -> None: +def test_main_flow_projection_binds_singleton_broadcast_artifact_as_2d( + monkeypatch: pytest.MonkeyPatch, +) -> None: original_spec = ArtifactSpec.input("OrigStain1", ImageArtifactType) illumination_spec = ArtifactSpec.input( "IllumStain1", @@ -1643,12 +1658,25 @@ def test_main_flow_projection_binds_singleton_broadcast_artifact_as_2d() -> None executor = _module_executor(contract) _activate_runtime_contract(executor.callable_contract, runtime) + strategy_type = type( + RuntimeArtifactTypeStrategy.for_artifact_type(ImageArtifactType) + ) + resolve_image = strategy_type.raw_runtime_input_value + resolved_specs: list[ArtifactSpec] = [] + + def record_resolution(self, request: RuntimeArtifactInputRequest): + resolved_specs.append(request.spec) + return resolve_image(self, request) + + monkeypatch.setattr(strategy_type, "raw_runtime_input_value", record_resolution) + image_request = executor._image_request( current_image, runtime, module_type=CorrectIlluminationApplyModule, active_input_specs=contract.artifact_inputs.specs, ) + assert resolved_specs == [original_spec] runtime_kwargs = executor._runtime_input_kwargs( runtime, current_image, From bbc75a380d950f24b526bab8ca63220d7a4eb6ca Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Thu, 1 Oct 2026 18:02:10 -0400 Subject: [PATCH 017/235] refactor: derive broadcast and output contexts from existing owners Move pure input-relation resolution to ArtifactSpecCollection and canonical output contextualization to RuntimeReturnedOutputMatcher. Delete duplicate CP executor helpers and retain composed measurement aliases from their declared input source. Refs #384. Validation: 454 focused artifact/output/CellProfiler tests pass; fresh import smoke and diff checks pass. --- openhcs/core/artifacts.py | 31 ++++++++ openhcs/core/runtime_output_matching.py | 76 ++++++++++++++++++ .../runtime/function_contract_execution.py | 77 +------------------ .../cellprofiler/runtime/module_execution.py | 43 ++--------- tests/unit/test_artifact_spec_collection.py | 42 ++++++++++ .../test_cellprofiler_module_execution.py | 50 ++++++++++++ tests/unit/test_runtime_output_matching.py | 58 ++++++++++++++ 7 files changed, 265 insertions(+), 112 deletions(-) diff --git a/openhcs/core/artifacts.py b/openhcs/core/artifacts.py index 04755abaa8..7078339c22 100644 --- a/openhcs/core/artifacts.py +++ b/openhcs/core/artifacts.py @@ -2110,6 +2110,37 @@ def __iter__(self): def __getitem__(self, index): return self.specs[index] + def stack_broadcast_source_indices(self) -> tuple[int | None, ...]: + """Resolve exact stack-broadcast owners from declared input relations.""" + + indices_by_ref: dict[ArtifactSpecRef, list[int]] = {} + for input_index, spec in enumerate(self.specs): + indices_by_ref.setdefault(spec.ref(), []).append(input_index) + + result: list[int | None] = [] + for input_index, spec in enumerate(self.specs): + sources = spec.stack_broadcast_sources() + if len(sources) > 1: + raise ValueError( + f"Input {spec.ref()!r} declares multiple stack-broadcast " + f"owners: {sources!r}." + ) + if not sources: + result.append(None) + continue + source_indices = tuple(indices_by_ref.get(sources[0], ())) + if len(source_indices) != 1: + raise ValueError( + f"Input {spec.ref()!r} requires exactly one active occurrence " + f"of stack-broadcast owner {sources[0]!r}, got " + f"{source_indices!r}." + ) + source_index = source_indices[0] + if source_index == input_index: + raise ValueError(f"Input {spec.ref()!r} cannot broadcast from itself.") + result.append(source_index) + return tuple(result) + def of_artifact_type( self, artifact_type: ArtifactTypeValue, diff --git a/openhcs/core/runtime_output_matching.py b/openhcs/core/runtime_output_matching.py index 44cc127d39..74a02ca0a7 100644 --- a/openhcs/core/runtime_output_matching.py +++ b/openhcs/core/runtime_output_matching.py @@ -9,6 +9,7 @@ from openhcs.core.aligned_image_payload import ( AlignedImageSliceContext, AlignedImageStack, + pack_aligned_image_outputs, ) from openhcs.core.artifacts import ( ArtifactOutputPlan, @@ -16,6 +17,12 @@ ArtifactSpecRef, ) from openhcs.core.callable_contract import CallableContract +from openhcs.core.runtime_image_values import image_payload_metadata +from openhcs.core.runtime_plane_projection import RuntimePlaneAxisValueProjection +from openhcs.core.runtime_slice_projection import ( + RuntimeSliceProjection, + RuntimeSliceProjectionDeclarationError, +) class RuntimeOutputBundle(ABC): @@ -61,6 +68,75 @@ def canonical_output(self) -> Any: return split_runtime_output(self.returned_output)[0] + def contextualize_canonical_output( + self, + *, + plane_projection: RuntimePlaneAxisValueProjection | None = None, + ) -> Any: + """Attach the compiled canonical ABI to one exact returned image axis.""" + + canonical_specs = self.callable_contract.canonical_return_output_specs.specs + if len(canonical_specs) <= 1: + return self.returned_output + canonical_output, trailing_outputs = split_runtime_output(self.returned_output) + if isinstance(canonical_output, AlignedImageStack): + if canonical_output.slice_contexts: + return self.returned_output + output_values = canonical_output.slices + else: + projection = plane_projection + function_name = self.callable_contract.function_name + if projection is None: + raise RuntimeSliceProjectionDeclarationError( + f"{function_name} declares {len(canonical_specs)} canonical " + "outputs but returned a non-aligned payload without a " + "compiled plane projection." + ) + if projection.plane_index is not None: + raise RuntimeSliceProjectionDeclarationError( + f"{function_name} declares {len(canonical_specs)} canonical " + "outputs after the compiled plane projection already selected " + f"plane {projection.plane_index}." + ) + if projection.axis_size != len(canonical_specs): + raise ValueError( + f"{function_name} declares {len(canonical_specs)} canonical " + "outputs but its compiled plane projection declares " + f"{projection.axis_size} value(s)." + ) + output_axis = image_payload_metadata(canonical_output).plane_axis + if output_axis is not projection.axis: + 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"{output_axis!r}." + ) + output_values = tuple( + RuntimeSliceProjection.value_for_slice( + canonical_output, + projection.selected_plane(output_index), + ) + for output_index in range(projection.axis_size) + ) + if len(output_values) != len(canonical_specs): + raise ValueError( + f"{self.callable_contract.function_name} returned {len(output_values)} " + "canonical output value(s) for " + f"{len(canonical_specs)} compiled output spec(s)." + ) + contextualized_output = pack_aligned_image_outputs( + output_values, + slice_contexts=AlignedImageSliceContext.main_flow_for_artifact_specs( + canonical_specs + ), + ) + return ( + (contextualized_output, *trailing_outputs) + if trailing_outputs + else contextualized_output + ) + def resolve(self) -> dict[ArtifactSpecRef, Any]: """Resolve every output in the callable ABI.""" diff --git a/openhcs/interop/cellprofiler/runtime/function_contract_execution.py b/openhcs/interop/cellprofiler/runtime/function_contract_execution.py index 79e9a04279..7ce0d2275a 100644 --- a/openhcs/interop/cellprofiler/runtime/function_contract_execution.py +++ b/openhcs/interop/cellprofiler/runtime/function_contract_execution.py @@ -8,11 +8,9 @@ import numpy as np from openhcs.core.aligned_image_payload import ( - AlignedImageSliceContext, AlignedImageStack, ImagePayloadExecutionMode, aligned_image_stack_kwargs, - pack_aligned_image_outputs, ) from openhcs.core.callable_contract import CallableContract from openhcs.core.image_shapes import ArrayShape @@ -30,7 +28,7 @@ image_payload_mask, image_payload_metadata, ) -from openhcs.core.runtime_output_matching import split_runtime_output +from openhcs.core.runtime_output_matching import RuntimeReturnedOutputMatcher from openhcs.core.runtime_plane_projection import ( RuntimePlaneAxis, RuntimePlaneAxisValueProjection, @@ -185,79 +183,10 @@ def execute( function=function_name, mode=mode.value, ) - return executor._contextualize_multi_canonical_output( + return RuntimeReturnedOutputMatcher( callable_contract, result, - ) - - def _contextualize_multi_canonical_output( - self, - callable_contract: CallableContract, - result: RuntimeCallableArgument, - ) -> RuntimeCallableArgument: - """Attach the compiled canonical ABI to one exact returned image axis.""" - - canonical_specs = callable_contract.canonical_return_output_specs.specs - if len(canonical_specs) <= 1: - return result - canonical_output, trailing_outputs = split_runtime_output(result) - if isinstance(canonical_output, AlignedImageStack): - if canonical_output.slice_contexts: - return result - output_values = canonical_output.slices - else: - projection = self.plane_projection - function_name = callable_contract.function_name - if projection is None: - raise RuntimeSliceProjectionDeclarationError( - f"{function_name} declares {len(canonical_specs)} canonical " - "outputs but returned a non-aligned payload without a " - "compiled plane projection." - ) - if projection.plane_index is not None: - raise RuntimeSliceProjectionDeclarationError( - f"{function_name} declares {len(canonical_specs)} canonical " - "outputs after the compiled plane projection already selected " - f"plane {projection.plane_index}." - ) - if projection.axis_size != len(canonical_specs): - raise ValueError( - f"{function_name} declares {len(canonical_specs)} canonical " - "outputs but its compiled plane projection declares " - f"{projection.axis_size} value(s)." - ) - output_axis = image_payload_metadata(canonical_output).plane_axis - if output_axis is not projection.axis: - 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"{output_axis!r}." - ) - output_values = tuple( - RuntimeSliceProjection.value_for_slice( - canonical_output, - projection.selected_plane(output_index), - ) - for output_index in range(projection.axis_size) - ) - if len(output_values) != len(canonical_specs): - raise ValueError( - f"{callable_contract.function_name} returned {len(output_values)} " - "canonical output value(s) for " - f"{len(canonical_specs)} compiled output spec(s)." - ) - contextualized_output = pack_aligned_image_outputs( - output_values, - slice_contexts=AlignedImageSliceContext.main_flow_for_artifact_specs( - canonical_specs - ), - ) - return ( - (contextualized_output, *trailing_outputs) - if trailing_outputs - else contextualized_output - ) + ).contextualize_canonical_output(plane_projection=executor.plane_projection) def execute_pure_2d_slice_batch( self, diff --git a/openhcs/interop/cellprofiler/runtime/module_execution.py b/openhcs/interop/cellprofiler/runtime/module_execution.py index 54c6661583..63c112c16b 100644 --- a/openhcs/interop/cellprofiler/runtime/module_execution.py +++ b/openhcs/interop/cellprofiler/runtime/module_execution.py @@ -1324,6 +1324,7 @@ def _image_request( adapter, ), ) + image_input_specs = ArtifactSpecCollection(image_inputs) payloads = [] source_names: list[str | None] = [] image_strategy = RuntimeArtifactTypeStrategy.for_artifact_type( @@ -1362,8 +1363,8 @@ def _image_request( if align_primary_images else () ), - stack_broadcast_source_indices=self._stack_broadcast_source_indices( - image_inputs + stack_broadcast_source_indices=( + image_input_specs.stack_broadcast_source_indices() ), ) return CellProfilerImageRequest( @@ -1371,12 +1372,12 @@ def _image_request( source_image_name=self._primary_image_source_name_from_sources( image_inputs, tuple(source_names) ), - source_aliases=ArtifactSpecCollection(image_inputs).names(), + source_aliases=image_input_specs.names(), image_count=len(payloads), execution_mode=composition.execution_mode, plane_projection=composition.preserved_plane_projection( adapter, - source_aliases=ArtifactSpecCollection(image_inputs).names(), + source_aliases=image_input_specs.names(), ), ) @@ -1413,40 +1414,6 @@ def _primary_image_source_name_from_sources( return None return source_names[0] - @staticmethod - def _stack_broadcast_source_indices( - input_specs: tuple[ArtifactSpec, ...], - ) -> tuple[int | None, ...]: - """Resolve exact stack-broadcast owners from declared input relations.""" - - indices_by_ref: dict[ArtifactSpecRef, list[int]] = {} - for input_index, spec in enumerate(input_specs): - indices_by_ref.setdefault(spec.ref(), []).append(input_index) - - result: list[int | None] = [] - for input_index, spec in enumerate(input_specs): - sources = spec.stack_broadcast_sources() - if len(sources) > 1: - raise ValueError( - f"Input {spec.ref()!r} declares multiple stack-broadcast " - f"owners: {sources!r}." - ) - if not sources: - result.append(None) - continue - source_indices = tuple(indices_by_ref.get(sources[0], ())) - if len(source_indices) != 1: - raise ValueError( - f"Input {spec.ref()!r} requires exactly one active occurrence " - f"of stack-broadcast owner {sources[0]!r}, got " - f"{source_indices!r}." - ) - source_index = source_indices[0] - if source_index == input_index: - raise ValueError(f"Input {spec.ref()!r} cannot broadcast from itself.") - result.append(source_index) - return tuple(result) - def _invocation_request( self, *, diff --git a/tests/unit/test_artifact_spec_collection.py b/tests/unit/test_artifact_spec_collection.py index db6e37dd1c..829bc2bcab 100644 --- a/tests/unit/test_artifact_spec_collection.py +++ b/tests/unit/test_artifact_spec_collection.py @@ -1,5 +1,7 @@ from __future__ import annotations +from dataclasses import replace + import pytest from openhcs.core.artifacts import ( @@ -9,6 +11,7 @@ ArtifactSpecCollection, GroupLineageSourceRelation, ImageArtifactType, + InputStackBroadcastSourceRelation, MainFlowPlaneProjectionOutputSpec, MeasurementsArtifactType, ObjectLabelsArtifactType, @@ -51,6 +54,45 @@ def test_artifact_spec_collection_queries_ordered_artifact_contracts() -> None: assert collection.by_name_and_artifact_type("Nuclei", ImageArtifactType) is None +def test_stack_broadcast_indices_use_exact_ordered_occurrences() -> None: + primary = ArtifactSpec.input("Primary", ImageArtifactType) + secondary = ArtifactSpec.input( + "Secondary", + ImageArtifactType, + relations=(InputStackBroadcastSourceRelation(source=primary.ref()),), + ) + unrelated = ArtifactSpec.input("Primary", ObjectLabelsArtifactType) + + assert ArtifactSpecCollection(()).stack_broadcast_source_indices() == () + assert ArtifactSpecCollection( + (secondary, unrelated, primary) + ).stack_broadcast_source_indices() == (2, None, None) + + for specs in ((secondary,), (primary, secondary, primary)): + with pytest.raises(ValueError, match="exactly one active occurrence"): + ArtifactSpecCollection(specs).stack_broadcast_source_indices() + + self_broadcast = replace( + primary, + relations=(InputStackBroadcastSourceRelation(source=primary.ref()),), + ) + with pytest.raises(ValueError, match="cannot broadcast from itself"): + ArtifactSpecCollection((self_broadcast,)).stack_broadcast_source_indices() + + other = ArtifactSpec.input("Other", ImageArtifactType) + multiple_owners = replace( + secondary, + relations=( + InputStackBroadcastSourceRelation(source=primary.ref()), + InputStackBroadcastSourceRelation(source=other.ref()), + ), + ) + with pytest.raises(ValueError, match="multiple stack-broadcast owners"): + ArtifactSpecCollection( + (primary, other, multiple_owners) + ).stack_broadcast_source_indices() + + def test_artifact_spec_collection_deduplicates_or_fails_loudly() -> None: image = ArtifactSpec.output("DNA", ImageArtifactType) diff --git a/tests/unit/test_cellprofiler_module_execution.py b/tests/unit/test_cellprofiler_module_execution.py index 99a4b572ba..90f612d3e4 100644 --- a/tests/unit/test_cellprofiler_module_execution.py +++ b/tests/unit/test_cellprofiler_module_execution.py @@ -50,6 +50,7 @@ from openhcs.core.callable_contract import ( CallableContract, CallableMetadata, + ImagePayloadConsumption, attach_callable_contract_metadata, runtime_image_execution_mode, ) @@ -1689,6 +1690,55 @@ def record_resolution(self, request: RuntimeArtifactInputRequest): assert image_payload_data(runtime_kwargs["illumination_function"]).shape == (4, 5) +def test_composed_measurement_images_keep_declared_aliases_and_resolved_payload() -> None: + image_spec = ArtifactSpec.input("DeclaredImage", ImageArtifactType) + contract = _compiled_callable_contract( + CellProfilerModule.require_module("MeasureObjectIntensity").require_callable(), + artifact_inputs=(image_spec,), + ) + contract = replace( + contract, + metadata=replace( + contract.metadata, + image_payload_consumption=ImagePayloadConsumption.COMPOSED, + ), + ) + image = np.ones((4, 5), dtype=np.float32) + runtime = _FakeCellProfilerRuntime( + {image_spec.name: image}, + artifact_input_edges=(_artifact_input_edge_for_test(image_spec),), + ) + executor = _module_executor(contract) + _activate_runtime_contract(contract, runtime) + image_request = CellProfilerImageRequest( + payload=image, + source_image_name="RuntimeAlias", + source_aliases=("RuntimeAlias",), + image_count=1, + execution_mode=ImagePayloadExecutionMode.NATURAL, + ) + + (measurement_image,) = executor._measurement_image_inputs( + runtime, + image, + image_request, + module_type=MeasureObjectIntensityModule, + ) + + assert measurement_image.source_aliases == (image_spec.name,) + assert measurement_image.payload is image_request.payload + assert measurement_image.execution_mode is image_request.execution_mode + assert measurement_image.plane_projection is image_request.plane_projection + assert not measurement_image.align_to_labels + with pytest.raises(ValueError, match="requires a composed measurement image request"): + executor._measurement_image_inputs( + runtime, + image, + None, + module_type=MeasureObjectIntensityModule, + ) + + def test_repeated_broadcast_inputs_consume_their_declared_source_group_axes() -> None: original_specs = tuple( ArtifactSpec.input(f"OrigStain{index}", ImageArtifactType) for index in (1, 2) diff --git a/tests/unit/test_runtime_output_matching.py b/tests/unit/test_runtime_output_matching.py index 74eb8a2e32..968ecb23ec 100644 --- a/tests/unit/test_runtime_output_matching.py +++ b/tests/unit/test_runtime_output_matching.py @@ -21,6 +21,12 @@ FunctionStepExecutionScope, ) from openhcs.core.runtime_output_matching import RuntimeReturnedOutputMatcher +from openhcs.core.runtime_image_values import ImagePayloadMetadata, image_payload_data +from openhcs.core.runtime_plane_projection import ( + RuntimePlaneAxis, + RuntimePlaneAxisValueProjection, +) +from openhcs.core.runtime_slice_projection import RuntimeSliceProjectionDeclarationError from openhcs.core.function_patterns import ( CompiledFunctionInvocation, FunctionInvocationKey, @@ -59,6 +65,58 @@ def test_runtime_output_matcher_maps_canonical_and_trailing_slots() -> None: } +def test_matcher_contextualizes_declared_axis_before_resolving_complete_abi() -> None: + first = ArtifactSpec.output("First", ImageArtifactType) + second = ArtifactSpec.output("Second", ImageArtifactType) + measurements = ArtifactSpec.output("Measurements", MeasurementsArtifactType) + data = np.arange(24, dtype=np.float32).reshape((2, 3, 4)) + payload = ImagePayloadMetadata( + plane_axis=RuntimePlaneAxis.RUNTIME_SLICE + ).payload_with(data, None) + trailing = object() + contract = _contract(first, second, measurements) + matcher = RuntimeReturnedOutputMatcher(contract, (payload, trailing)) + projection = RuntimePlaneAxisValueProjection.preserve( + axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis_size=2, + ) + + returned = matcher.contextualize_canonical_output(plane_projection=projection) + resolved = RuntimeReturnedOutputMatcher(contract, returned).resolve() + + assert returned[1] is trailing + assert resolved[measurements.ref()] is trailing + for index, spec in enumerate((first, second)): + selected = image_payload_data(resolved[spec.ref()]) + np.testing.assert_array_equal(selected, data[index]) + assert np.shares_memory(selected, data) + assert ( + RuntimeReturnedOutputMatcher(contract, returned).contextualize_canonical_output() + is returned + ) + + with pytest.raises(RuntimeSliceProjectionDeclarationError, match="without a compiled"): + matcher.contextualize_canonical_output() + with pytest.raises(RuntimeSliceProjectionDeclarationError, match="already selected"): + matcher.contextualize_canonical_output( + plane_projection=projection.selected_plane(0) + ) + with pytest.raises(ValueError, match="projection declares 3 value"): + matcher.contextualize_canonical_output( + plane_projection=RuntimePlaneAxisValueProjection.preserve( + axis=RuntimePlaneAxis.RUNTIME_SLICE, + axis_size=3, + ) + ) + with pytest.raises(RuntimeSliceProjectionDeclarationError, match="plane axis"): + matcher.contextualize_canonical_output( + plane_projection=RuntimePlaneAxisValueProjection.preserve( + axis=RuntimePlaneAxis.SOURCE_BINDING, + axis_size=2, + ) + ) + + def test_runtime_output_matcher_uses_exact_multi_canonical_contexts() -> None: first = ArtifactSpec.output("First", ImageArtifactType) second = ArtifactSpec.output("Second", ImageArtifactType) From e82b2c072924d4c8d8161cab45ba0b0cc812ae31 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Thu, 1 Oct 2026 18:07:39 -0400 Subject: [PATCH 018/235] docs: retain shared plumbing bounds and rejected cache assumptions Refs #384. Separate same-run diagnostic owner costs from ordinary paired timing and explain the existing metadata owner route. --- .../shared_runtime_plumbing_384_20261001.rst | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/docs/validation/shared_runtime_plumbing_384_20261001.rst b/docs/validation/shared_runtime_plumbing_384_20261001.rst index 784ecf732e..3e44bf056e 100644 --- a/docs/validation/shared_runtime_plumbing_384_20261001.rst +++ b/docs/validation/shared_runtime_plumbing_384_20261001.rst @@ -69,6 +69,34 @@ already resolved target; no additional generic invocation type or parallel contract/callable fields are needed. Image source-name projection consumes the already resolved payload through the existing artifact strategy family. +The executor does not own a second interpretation of input relations or returned +artifact contexts. ``ArtifactSpecCollection`` derives exact broadcast indices +from its ordered declarations; ``RuntimeReturnedOutputMatcher`` contextualizes +the compiled canonical return ABI. The old CP helpers are deleted. The existing +contract, collection and returned value determine these answers directly. + +The deeper clean-source probe at 5d1f1a781eda851b4783b68db342b76eeef9dea9 records +10.053237 seconds of public execution, including 4.465789 seconds at the callable +boundary and 4.939001 seconds elsewhere inside steps. Loading takes 0.699904 +exclusive seconds, image recording 0.519127, output identity 0.412492 and image +request preparation 0.388981. Explicit diagnostic capture costs 0.044853 seconds. +These partitions are diagnostic evidence, not accepted A/B measurements. + +All 23 produced-stack cache hits correspond to previously stored output paths. +The nine misses are original inputs; seven repeat earlier source reads and cost +0.5149 seconds inclusively. Those inputs are already in memory. Caching their +mutable arrays by path alone is rejected: storage has no mutation revision and +loaded source semantics depend on binding, aliases and plane contracts. + +The next shared route is metadata ownership, rather than a second pixel cache. +Changing spatial context, intensity or image names repeatedly constructs source +provenance and copies/fingerprints its component mapping. Source loading and CP +image recording consume this common path. The saved typed ImageMath fixture +retains actual nested mapping ownership for a cold-promotion and complete-workflow +replay. An isolated query-cache payoff does not admit this migration. CP-recorded +outputs already bypass generic postprocessing; removing that hypothetical second +contextualization would not affect the measured recorder. + The unmodified-main light trace located about 3.44 seconds in loading, unstacking, saving and finalization for the representative 3D pipeline. CP request and output context construction account for another approximately 1.216 seconds. These are From f3d798e39b27abac0a95ed8f329c246b21286df7 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Thu, 1 Oct 2026 18:10:29 -0400 Subject: [PATCH 019/235] refactor: bind declared output contexts on aligned image stack AlignedImageStack owns complete exact-context matching; RuntimeReturnedOutputMatcher retains canonical/trailing ABI and projection handling. Delete the detached canonical-values helper and preserve all roster guards in one linear traversal. Refs #384. Validation: 475 focused controls pass; receiver-normalized algorithm AST retains original guards and messages. --- openhcs/core/aligned_image_payload.py | 52 ++++++++++++ openhcs/core/runtime_output_matching.py | 80 +++---------------- .../unit/test_aligned_main_flow_projection.py | 56 +++++++++++++ 3 files changed, 119 insertions(+), 69 deletions(-) diff --git a/openhcs/core/aligned_image_payload.py b/openhcs/core/aligned_image_payload.py index 23e33482c5..115ece9341 100644 --- a/openhcs/core/aligned_image_payload.py +++ b/openhcs/core/aligned_image_payload.py @@ -1194,6 +1194,58 @@ def projected_output_slices( for output_slice in payload_slices_for_alignment(payload): yield output_slice, context + def output_values_for_artifact_specs( + self, + canonical_specs: tuple[ArtifactSpec, ...], + ) -> dict[ArtifactSpecRef, Any]: + """Bind one complete declared main-flow roster to exact slice contexts.""" + + if not self.slice_contexts: + raise ValueError( + "Multiple canonical output specs require exact AlignedImageStack " + "slice contexts; positional slice order is not artifact identity." + ) + + specs_by_context = { + (spec.name, spec.artifact_type.value): spec for spec in canonical_specs + } + if len(specs_by_context) != len(canonical_specs): + raise ValueError("Canonical output ABI contains duplicate named contexts.") + resolved: dict[ArtifactSpecRef, Any] = {} + for payload, context in zip( + self.slices, + self.slice_contexts, + strict=True, + ): + if context.output_kind != AlignedImageSliceContext.MAIN_FLOW_OUTPUT_KIND: + raise ValueError( + "Canonical AlignedImageStack contains a non-main-flow slice " + f"context: {context!r}." + ) + context_key = (context.output_key, context.artifact_kind) + spec = specs_by_context.get(context_key) + if spec is None: + raise ValueError( + "Canonical AlignedImageStack context is not declared by the " + f"callable ABI: {context!r}." + ) + ref = spec.ref() + if ref in resolved: + raise ValueError( + "Canonical AlignedImageStack contains duplicate context for " + f"{ref!r}." + ) + resolved[ref] = payload + missing = tuple( + spec.ref() for spec in canonical_specs if spec.ref() not in resolved + ) + if missing: + raise ValueError( + "Canonical AlignedImageStack does not carry every declared output: " + f"{missing!r}." + ) + return resolved + def output_payload( self, artifact_ref: ArtifactSpecRef, diff --git a/openhcs/core/runtime_output_matching.py b/openhcs/core/runtime_output_matching.py index 74a02ca0a7..7e6dd98b49 100644 --- a/openhcs/core/runtime_output_matching.py +++ b/openhcs/core/runtime_output_matching.py @@ -161,12 +161,18 @@ def resolve(self) -> dict[ArtifactSpecRef, Any]: strict=True, ) } - resolved.update( - self._canonical_values( - self.callable_contract.canonical_return_output_specs.specs, - canonical_output, + canonical_specs = self.callable_contract.canonical_return_output_specs.specs + if len(canonical_specs) == 1: + resolved[canonical_specs[0].ref()] = canonical_output + elif canonical_specs: + if not isinstance(canonical_output, AlignedImageStack): + raise TypeError( + "Multiple canonical output specs require an AlignedImageStack with " + "one exact named slice context per output." + ) + resolved.update( + canonical_output.output_values_for_artifact_specs(canonical_specs) ) - ) return resolved def resolve_plan_values( @@ -205,70 +211,6 @@ def resolve_plan_values( matched_outputs.append((plan, spec, returned_values[ref])) return returned_values, tuple(matched_outputs) - @classmethod - def _canonical_values( - cls, - canonical_specs: tuple[ArtifactSpec, ...], - canonical_output: Any, - ) -> dict[ArtifactSpecRef, Any]: - """Resolve named outputs represented by the one canonical return slot.""" - - if not canonical_specs: - return {} - if len(canonical_specs) == 1: - spec = canonical_specs[0] - return {spec.ref(): canonical_output} - if not isinstance(canonical_output, AlignedImageStack): - raise TypeError( - "Multiple canonical output specs require an AlignedImageStack with " - "one exact named slice context per output." - ) - if not canonical_output.slice_contexts: - raise ValueError( - "Multiple canonical output specs require exact AlignedImageStack " - "slice contexts; positional slice order is not artifact identity." - ) - - specs_by_context = { - (spec.name, spec.artifact_type.value): spec for spec in canonical_specs - } - if len(specs_by_context) != len(canonical_specs): - raise ValueError("Canonical output ABI contains duplicate named contexts.") - resolved: dict[ArtifactSpecRef, Any] = {} - for payload, context in zip( - canonical_output.slices, - canonical_output.slice_contexts, - strict=True, - ): - if context.output_kind != AlignedImageSliceContext.MAIN_FLOW_OUTPUT_KIND: - raise ValueError( - "Canonical AlignedImageStack contains a non-main-flow slice " - f"context: {context!r}." - ) - context_key = (context.output_key, context.artifact_kind) - spec = specs_by_context.get(context_key) - if spec is None: - raise ValueError( - "Canonical AlignedImageStack context is not declared by the " - f"callable ABI: {context!r}." - ) - ref = spec.ref() - if ref in resolved: - raise ValueError( - "Canonical AlignedImageStack contains duplicate context for " - f"{ref!r}." - ) - resolved[ref] = payload - missing = tuple( - spec.ref() for spec in canonical_specs if spec.ref() not in resolved - ) - if missing: - raise ValueError( - "Canonical AlignedImageStack does not carry every declared output: " - f"{missing!r}." - ) - return resolved - @staticmethod def _specs_by_ref( specs: tuple[ArtifactSpec, ...], diff --git a/tests/unit/test_aligned_main_flow_projection.py b/tests/unit/test_aligned_main_flow_projection.py index b4e4e05b03..0bc40ba4da 100644 --- a/tests/unit/test_aligned_main_flow_projection.py +++ b/tests/unit/test_aligned_main_flow_projection.py @@ -52,6 +52,62 @@ def _input_ref( return ArtifactSpec.input(name, artifact_type).ref() +@pytest.mark.parametrize("carrier_type", (AlignedImageStack, ImageOutputBundle)) +def test_aligned_stack_binds_complete_declared_output_roster( + carrier_type: type[AlignedImageStack], +) -> None: + first = ArtifactSpec.output("First", ImageArtifactType) + second = ArtifactSpec.output("Second", ImageArtifactType) + first_context, second_context = AlignedImageSliceContext.main_flow_for_artifact_specs( + (first, second) + ) + first_value = object() + carrier = carrier_type( + (None, first_value), + (second_context, first_context), + ) + + assert carrier.output_values_for_artifact_specs((first, second)) == { + first.ref(): first_value, + second.ref(): None, + } + with pytest.raises(ValueError, match="duplicate named contexts"): + carrier.output_values_for_artifact_specs((first, first)) + + +@pytest.mark.parametrize( + ("contexts", "message"), + ( + ((), "require exact AlignedImageStack"), + (("first", "first"), "contains duplicate context"), + (("first",), "does not carry every declared output"), + (("first", "unknown"), "context is not declared"), + (("first", "non-main"), "contains a non-main-flow"), + ), +) +def test_aligned_stack_rejects_incomplete_or_conflicting_output_rosters( + contexts: tuple[str, ...], + message: str, +) -> None: + first = ArtifactSpec.output("First", ImageArtifactType) + second = ArtifactSpec.output("Second", ImageArtifactType) + first_context, second_context = AlignedImageSliceContext.main_flow_for_artifact_specs( + (first, second) + ) + selected = { + "first": first_context, + "unknown": replace(second_context, output_key="Unknown"), + "non-main": replace(second_context, output_kind="artifact"), + } + carrier = AlignedImageStack( + tuple(object() for _ in contexts) if contexts else (object(), object()), + tuple(selected[name] for name in contexts), + ) + + with pytest.raises(ValueError, match=message): + carrier.output_values_for_artifact_specs((first, second)) + + def _named_carrier() -> tuple[AlignedImageStack, np.ndarray, np.ndarray]: mcherry = np.full((3, 4), 3.0, dtype=np.float32) gfp = np.full((3, 4), 2.0, dtype=np.float32) From d9c9f8fe4b7e0361509bc975e0839c36477f04d1 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Thu, 1 Oct 2026 18:14:07 -0400 Subject: [PATCH 020/235] docs: record existing-owner gates against synchronized main Refs #384. Preserve failed guards and qualify the 955-control and original R0/R1 checks at their frozen source revision. --- .../shared_runtime_plumbing_384_20261001.rst | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/docs/validation/shared_runtime_plumbing_384_20261001.rst b/docs/validation/shared_runtime_plumbing_384_20261001.rst index 3e44bf056e..67dbff5668 100644 --- a/docs/validation/shared_runtime_plumbing_384_20261001.rst +++ b/docs/validation/shared_runtime_plumbing_384_20261001.rst @@ -72,8 +72,10 @@ already resolved payload through the existing artifact strategy family. The executor does not own a second interpretation of input relations or returned artifact contexts. ``ArtifactSpecCollection`` derives exact broadcast indices from its ordered declarations; ``RuntimeReturnedOutputMatcher`` contextualizes -the compiled canonical return ABI. The old CP helpers are deleted. The existing -contract, collection and returned value determine these answers directly. +the compiled canonical return ABI. ``AlignedImageStack`` binds the complete +declared output roster to its exact slice contexts in one linear traversal. The +old CP and canonical-values helpers are deleted. The existing contract, +collection and returned value determine these answers directly. The deeper clean-source probe at 5d1f1a781eda851b4783b68db342b76eeef9dea9 records 10.053237 seconds of public execution, including 4.465789 seconds at the callable @@ -144,6 +146,18 @@ source checks. The main synchronization passed 85 help/output/source-resolution controls. These source ratchets are scoped guards, not a formal proof of every dynamic binding. +The subsequent ownership/publication follow-up normally merges main +76a2d392056f4fa6cc74bc122a552edc32f24239 at candidate +84057fbefd0ad5469c766051e19b34f42e851102, including the declaration-owned +classification and singleton image-selection repairs. All 955 controls across +26 changed-path and relevant main-owner test files pass, with two existing +watershed warnings. Original unmodified R0 and R1 pass against that main, with +no metric budgets or exclusions changed. The R1 scan uses the current NRA +checkout's virtual environment and its original 120-second deadline. Earlier +failed owner-growth/context-inspection guards and the wrong-interpreter import +failure are retained. Evidence: shared-plumbing-final-local-gates-20261001.json +and the original R0/R1 logs in the external evidence directory. + The first complete saved-native gate exposed two illumination-correction publication failures: actual numeric NPY saves admitted directories without any raster images into metadata publication. The inherited raster-inventory admission From 01af3e031af46399e11a1375094925bf4d69ac1d Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Thu, 1 Oct 2026 18:25:27 -0400 Subject: [PATCH 021/235] docs: retain mixed current-main plumbing benchmark outcomes Refs #384. Preserve all ordinary ABBA observations and the unresolved Imaging Flow Cytometry regression; no broad optimization or native-ratio claim. --- .../shared_runtime_plumbing_384_20261001.rst | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/docs/validation/shared_runtime_plumbing_384_20261001.rst b/docs/validation/shared_runtime_plumbing_384_20261001.rst index 67dbff5668..611ecffb5c 100644 --- a/docs/validation/shared_runtime_plumbing_384_20261001.rst +++ b/docs/validation/shared_runtime_plumbing_384_20261001.rst @@ -194,6 +194,36 @@ External evidence: shared-runtime-plumbing-repaired-qualified-abba-summary- 20261001.json and every corresponding source/input/observation receipt in the openhcs-benchmark-runs evidence directory. +Existing-owner follow-up ordinary ABBA +------------------------------------- + +The synchronized follow-up compares main 76a2d392 against candidate d9c9f8fe4. +The candidate's production source is the locally qualified 84057fb revision; +subsequent changes only retain the qualification receipt. Two observations per +side use the same ordinary public path and retain every sample. In seconds: + +========================= ======================= ======================= +Case Mean execution Mean pipeline total +========================= ======================= ======================= +3D monolayer 11.173033 -> 9.979570 13.166303 -> 11.915196 +Imaging Flow Cytometry 16.847054 -> 18.891492 19.058024 -> 21.417949 +Advanced segmentation 8.802229 -> 8.616838 11.271471 -> 11.113117 +========================= ======================= ======================= + +3D execution improves 10.68% and advanced execution 2.11%, while Imaging Flow +Cytometry regresses 12.14%. Its candidate observations are 17.371243 and 20.411741 +seconds, compared with 17.554674 and 16.139433 for main. Compilation is also +inflated in the slower candidate sweep. These samples establish mixed evidence, +not a statistical significance or causal attribution; PR394 remains draft. + +All 24 ordered Imaging Flow Cytometry step occurrences align. Most of the extra +time is inside steps, distributed across the slower candidate sweep. Between-step +gaps remain about 0.017-0.023 seconds; execution outside the complete step span +does not explain the regression. Existing output comparisons and shared metadata +ownership investigation continue before promotion. Evidence: +shared-plumbing-owner-followup-qualified-abba-20261001.json and its summary, +source receipts and all per-step/progress observations. + Fresh native qualification and next dominant route ------------------------------------------------ From 94c20da113968325cb7ff8ee8088d840d41620a3 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Thu, 1 Oct 2026 18:41:07 -0400 Subject: [PATCH 022/235] docs: retain output equality and current startup integration gates Refs #384. Consume already merged issue355 repair with recorded ZMQ dependency; retain source-pinned performance and exact ordinary output checks. --- .../shared_runtime_plumbing_384_20261001.rst | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/validation/shared_runtime_plumbing_384_20261001.rst b/docs/validation/shared_runtime_plumbing_384_20261001.rst index 611ecffb5c..94faedbd04 100644 --- a/docs/validation/shared_runtime_plumbing_384_20261001.rst +++ b/docs/validation/shared_runtime_plumbing_384_20261001.rst @@ -224,6 +224,26 @@ ownership investigation continue before promotion. Evidence: shared-plumbing-owner-followup-qualified-abba-20261001.json and its summary, source receipts and all per-step/progress observations. +Across all six run pairs, Imaging Flow Cytometry's two header rows and all +1,800-by-527 measurement cells match exactly. All six 3D CSV schemas, rows and +cells match, as do all 120 TIFF arrays, shapes and dtypes. Ordinary output +inventories match. Outcome-wire differences are fresh execution UUIDs and actual +run-root path prefixes; no scientific values are present in those outcome views. +Advanced's database/properties were not numerically compared, so absence of +ordinary CSV/TIFF output does not establish its parity. Evidence: +shared-plumbing-owner-followup-abba-step-output-audit-20261001.json. + +The next normal synchronization integrates main +0d07909b07870557a04f125452dc6944db77cb6c, including PR358's startup feedback and +worker-budget repair, at candidate fdf50241aa9adc6d96bc9c530dcc9acd19692b64. +The recorded ZMQRuntime dependency 668edafcb5ee530a163377c936431ae1e8334e85 is +checked out in both trees and consumed through the shared editable environment. +All 498 preparation/startup/CP integration controls pass, with six skips and two +existing warnings; original R0/R1 pass with unchanged budgets and exclusions. +Issue355 is closed by its existing merged PR. The ABBA remains pinned to its +measured sources; no timing result is projected through this synchronization. +Evidence: shared-plumbing-main355-local-gates-20261001.json. + Fresh native qualification and next dominant route ------------------------------------------------ From faf61ad87c550fa2d8ef44313344db74b378b857 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Thu, 1 Oct 2026 18:57:37 -0400 Subject: [PATCH 023/235] docs: record current main sync and metadata equality review (#384) --- .../shared_runtime_plumbing_384_20261001.rst | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/validation/shared_runtime_plumbing_384_20261001.rst b/docs/validation/shared_runtime_plumbing_384_20261001.rst index 94faedbd04..d1ca5d920c 100644 --- a/docs/validation/shared_runtime_plumbing_384_20261001.rst +++ b/docs/validation/shared_runtime_plumbing_384_20261001.rst @@ -244,6 +244,21 @@ Issue355 is closed by its existing merged PR. The ABBA remains pinned to its measured sources; no timing result is projected through this synchronization. Evidence: shared-plumbing-main355-local-gates-20261001.json. +Subsequent normal synchronization consumes documentation/knowledge changes in +main PR405 and PR406, through e66500e7ae3804abca1396681c50e1cef5e9a11e. These merges +do not change production source or dependencies. All 23 knowledge-transfer controls +pass on the final merged declarations, with two existing warnings. Benchmark and source-gate +observations remain pinned above. + +Independent review of the isolated metadata-owner migration found the important +distinction between ordered selection-record equality and public mapping-content +equality. It also checked duplicate stringified keys, normalization error order, +output-extension insertion order and fields-only transport. That migration remains +outside this PR until the affected consumers preserve their existing semantics +and the complete production-owner replay and source gates pass. Review evidence: +source-metadata-owner-independent-review-20261001.json. No stage replay is promoted +as an end-to-end gain. + Fresh native qualification and next dominant route ------------------------------------------------ From a698d275fb8657c08c1e7417f60570ade7f40870 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Thu, 1 Oct 2026 19:21:42 -0400 Subject: [PATCH 024/235] perf: retain source metadata on its nominal lifetime owner (#384) --- openhcs/core/component_group_scope.py | 7 +- openhcs/core/runtime_image_values.py | 22 +- openhcs/core/runtime_source_binding_cache.py | 12 +- openhcs/core/source_binding_selection.py | 101 +--- openhcs/core/source_binding_workspace.py | 4 +- openhcs/core/source_bindings.py | 34 +- openhcs/core/source_image_provenance.py | 75 ++- openhcs/core/source_matching.py | 77 +-- openhcs/core/source_metadata.py | 552 +++++++++++++++--- openhcs/core/source_projection.py | 32 +- openhcs/core/source_workspace_projection.py | 15 +- .../function_artifact_materialization.py | 2 +- .../core/steps/function_output_identity.py | 46 +- openhcs/core/steps/function_runtime.py | 2 +- openhcs/core/virtual_workspace_metadata.py | 55 +- .../interop/cellprofiler/analyst_export.py | 4 +- .../cellprofiler/spreadsheet_export.py | 5 +- .../test_source_store_code_mode_zmq.py | 6 +- ...test_runtime_source_resolution_snapshot.py | 50 +- tests/unit/test_source_binding_workspace.py | 28 +- .../test_source_image_provenance_hotpath.py | 6 +- .../test_source_metadata_live_mutation.py | 4 +- tests/unit/test_source_metadata_owner.py | 490 ++++++++++++++++ tests/unit/test_source_projection.py | 4 +- 24 files changed, 1192 insertions(+), 441 deletions(-) create mode 100644 tests/unit/test_source_metadata_owner.py diff --git a/openhcs/core/component_group_scope.py b/openhcs/core/component_group_scope.py index 78b18d51af..9a5ca314f5 100644 --- a/openhcs/core/component_group_scope.py +++ b/openhcs/core/component_group_scope.py @@ -10,6 +10,7 @@ from openhcs.constants.constants import AllComponents from openhcs.core.component_set import ComponentSet from openhcs.core.source_metadata import ( + SourceMetadataFields, SourceMetadataMapping, SourceMetadataScalar, SourceMetadataValue, @@ -467,7 +468,7 @@ def for_group_coordinate( def fixed_component_metadata( self, metadata: SourceMetadataMapping | None = None, - ) -> dict[str, SourceMetadataValue]: + ) -> SourceMetadataMapping: """Merge fixed execution coordinates into source component metadata.""" from openhcs.constants.constants import get_multiprocessing_axis @@ -476,7 +477,9 @@ def fixed_component_metadata( with_source_component_metadata, ) - merged: dict[str, SourceMetadataValue] = dict(metadata or {}) + merged = SourceMetadataFields.composition_snapshot( + metadata if metadata is not None else {} + ) fixed_values = ( (get_multiprocessing_axis(), self.axis_id), *self.fixed_component_values, diff --git a/openhcs/core/runtime_image_values.py b/openhcs/core/runtime_image_values.py index 3a55746a9f..9e9f03e92e 100644 --- a/openhcs/core/runtime_image_values.py +++ b/openhcs/core/runtime_image_values.py @@ -47,6 +47,8 @@ ) from openhcs.core.source_metadata import ( SourceMetadataScalar, + OwnedSourceMetadataFields, + SourceMetadataFields, SourceMetadataValue, SourceVoxelSpacing, SourceVoxelSpacingFields, @@ -1884,7 +1886,9 @@ def common_source_component_metadata( """Return source metadata shared by the composed payload.""" metadata_values = tuple(values) metadata_by_plane = tuple( - dict(metadata.source_component_metadata) + SourceMetadataFields.composition_snapshot( + metadata.source_component_metadata + ) for metadata in metadata_values if metadata.source_component_metadata is not None ) @@ -1915,7 +1919,21 @@ def common_source_component_metadata( ) if not common_metadata: return None - return MappingProxyType(common_metadata) + owner = ( + metadata_by_plane[0] + if metadata_by_plane + and all( + isinstance(metadata, OwnedSourceMetadataFields) + for metadata in metadata_by_plane + ) + else {} + ) + derived = SourceMetadataFields.derived_mapping(owner, common_metadata) + return ( + derived + if isinstance(derived, OwnedSourceMetadataFields) + else MappingProxyType(derived) + ) @staticmethod def common_metadata_value( diff --git a/openhcs/core/runtime_source_binding_cache.py b/openhcs/core/runtime_source_binding_cache.py index 0231210c85..8b074a55c7 100644 --- a/openhcs/core/runtime_source_binding_cache.py +++ b/openhcs/core/runtime_source_binding_cache.py @@ -12,7 +12,10 @@ SourceBindingRuntimeMetadataNormalizer, MetadataExtractionRule, ) -from openhcs.core.source_metadata import SourceMetadataMapping +from openhcs.core.source_metadata import ( + ResolvedSourceMetadataRecord, + SourceMetadataMapping, +) if TYPE_CHECKING: from openhcs.core.source_binding_selection import ( @@ -42,8 +45,7 @@ def from_projection( ) -> "RuntimeSourceResolutionSnapshot": """Resolve declared positions into one independently owned runtime view.""" from openhcs.core.source_binding_selection import ( - ResolvedSourceMetadataRecord, - SourceMetadataRecord, + DeclaredSourceMetadataRecord, SourcePatternResolutionContext, ) @@ -59,7 +61,7 @@ def from_projection( context, source_metadata_by_path=MappingProxyType( { - path: SourceMetadataRecord.from_mapping(metadata) + path: DeclaredSourceMetadataRecord.from_mapping(metadata) for path, metadata in normalized_metadata.items() } ), @@ -77,7 +79,7 @@ def from_projection( records = {} for path in paths: metadata = context.metadata_for_path(path) - records[path] = ResolvedSourceMetadataRecord.from_resolved_mapping( + records[path] = ResolvedSourceMetadataRecord.from_mapping( {} if metadata is None else metadata ) context = replace(context, source_metadata_by_path=MappingProxyType(records)) diff --git a/openhcs/core/source_binding_selection.py b/openhcs/core/source_binding_selection.py index fde043f4dc..329813c31f 100644 --- a/openhcs/core/source_binding_selection.py +++ b/openhcs/core/source_binding_selection.py @@ -3,7 +3,6 @@ from __future__ import annotations from abc import ABC, abstractmethod -from collections.abc import Iterator from dataclasses import dataclass, field, replace from enum import Enum from functools import lru_cache @@ -26,7 +25,6 @@ SourceBindingMatchMethod, SourceBindingMatchPlan, SourceBindingRuntimeContext, - SourceBindingRuntimeMetadataNormalizer, SourceSetRole, ) from openhcs.core.source_image_provenance import ( @@ -34,8 +32,9 @@ SourceImageProvenance, ) from openhcs.core.source_metadata import ( + SourceMetadataFields, SourceMetadataMapping, - SourceMetadataRoleView, + SourceMetadataRecord, SourceMetadataValue, ) from openhcs.core.source_matching import ( @@ -79,31 +78,20 @@ def _cached_source_candidate_pattern_keys(pattern_path: str) -> tuple[str, ...]: @dataclass(frozen=True, slots=True, eq=False) -class SourceMetadataRecord(Mapping[str, SourceMetadataValue]): - """Normalized source metadata carried across source-binding selection.""" - - fields: tuple[tuple[str, SourceMetadataValue], ...] +class DeclaredSourceMetadataRecord(SourceMetadataRecord): + """Live declared metadata that still requires path-specific fallbacks.""" @classmethod - def from_mapping(cls, metadata: SourceMetadataMapping) -> "SourceMetadataRecord": - return DeclaredSourceMetadataRecord( - tuple((str(key), value) for key, value in metadata.items()) - ) + def from_mapping( + cls, metadata: SourceMetadataMapping + ) -> "DeclaredSourceMetadataRecord": + return cls(tuple((str(key), value) for key, value in metadata.items())) - @abstractmethod def resolve( self, path: str, parser: "FilenameParser", metadata_rules: tuple[MetadataExtractionRule, ...], - ) -> "SourceMetadataRecord | None": - """Return this path's metadata through its declared resolution lifetime.""" - - def resolve_with_fallback( - self, - path: str, - parser: "FilenameParser", - metadata_rules: tuple[MetadataExtractionRule, ...], ) -> "SourceMetadataRecord | None": """Fill parser and extraction-rule fields absent from declarations.""" metadata: dict[str, SourceMetadataValue] = {} @@ -132,71 +120,6 @@ def resolve_with_fallback( ) return self.from_mapping(metadata) if metadata else None - def __getitem__(self, key: str) -> SourceMetadataValue: - for field_key, value in self.fields: - if field_key == key: - return value - raise KeyError(key) - - def __iter__(self) -> Iterator[str]: - return (key for key, _value in self.fields) - - def __len__(self) -> int: - return len(self.fields) - - def __eq__(self, other: object) -> bool: - if not isinstance(other, SourceMetadataRecord): - return NotImplemented - return self.fields == other.fields - - def __hash__(self) -> int: - return hash((self.fields,)) - - -@dataclass(frozen=True, slots=True, eq=False) -class DeclaredSourceMetadataRecord(SourceMetadataRecord): - """Live declared metadata that still requires path-specific fallbacks.""" - - def resolve( - self, - path: str, - parser: "FilenameParser", - metadata_rules: tuple[MetadataExtractionRule, ...], - ) -> SourceMetadataRecord | None: - return self.resolve_with_fallback(path, parser, metadata_rules) - - -@dataclass(frozen=True, slots=True, eq=False) -class ResolvedSourceMetadataRecord(SourceMetadataRecord): - """Immutable path-specific metadata resolved at a runtime snapshot boundary.""" - - def __post_init__(self) -> None: - object.__setattr__( - self, - "fields", - tuple( - ( - str(key), - SourceBindingRuntimeMetadataNormalizer.normalized_value(value), - ) - for key, value in self.fields - ), - ) - - @classmethod - def from_resolved_mapping( - cls, metadata: SourceMetadataMapping - ) -> "ResolvedSourceMetadataRecord": - return cls(tuple(metadata.items())) - - def resolve( - self, - path: str, - parser: "FilenameParser", - metadata_rules: tuple[MetadataExtractionRule, ...], - ) -> SourceMetadataRecord | None: - return self if self.fields else None - @dataclass(frozen=True, slots=True) class SourceMetadataCandidates: @@ -274,7 +197,7 @@ def from_sources( str(path): ( metadata if isinstance(metadata, SourceMetadataRecord) - else SourceMetadataRecord.from_mapping(metadata) + else DeclaredSourceMetadataRecord.from_mapping(metadata) ) for path, metadata in source_metadata_by_path.items() } @@ -342,9 +265,9 @@ def candidate_filter_paths(self, pattern: SourceCandidatePath) -> tuple[str, ... dict.fromkeys( source_filter_path for metadata in self.metadata_for_paths(resolution.metadata_paths()) - for source_filter_path in SourceMetadataRoleView( + for source_filter_path in SourceMetadataFields.source_filter_paths( metadata - ).source_filter_paths() + ) ) ) if source_filter_paths: @@ -518,7 +441,7 @@ def merged_metadata_for_paths( if path_metadata is not None: merge_source_metadata(metadata, path_metadata, path=path) if metadata: - return SourceMetadataRecord.from_mapping(metadata) + return DeclaredSourceMetadataRecord.from_mapping(metadata) return None def source_metadata_by_paths( diff --git a/openhcs/core/source_binding_workspace.py b/openhcs/core/source_binding_workspace.py index 02230380d8..3567219876 100644 --- a/openhcs/core/source_binding_workspace.py +++ b/openhcs/core/source_binding_workspace.py @@ -45,7 +45,7 @@ OriginalSourceMetadata, SourceFilterPathMetadata, SourceMetadataMapping, - SourceMetadataRoleView, + SourceMetadataFields, SourceMetadataScalar, SourceVoxelSpacing, SourceComponentProjectionStrategy, @@ -1373,7 +1373,7 @@ def _shared_candidate_metadata( value_sets_by_key: dict[str, set[object]] = {} counts_by_key: dict[str, int] = {} for candidate in candidates: - for key, value in SourceMetadataRoleView(candidate.metadata).scalar_items(): + for key, value in SourceMetadataFields.scalar_items(candidate.metadata): value_sets_by_key.setdefault(key, set()).add(value) counts_by_key[key] = counts_by_key.get(key, 0) + 1 candidate_count = len(candidates) diff --git a/openhcs/core/source_bindings.py b/openhcs/core/source_bindings.py index 7f2bf54e30..9b00e410dd 100644 --- a/openhcs/core/source_bindings.py +++ b/openhcs/core/source_bindings.py @@ -38,9 +38,9 @@ from openhcs.core.runtime_tabular_values import FieldSpec from openhcs.core.source_metadata import ( SourceMetadataIdentityItems, - SourceMetadataIdentityProjection, + SourceMetadataFields, + ResolvedSourceMetadataRecord, SourceMetadataMapping, - SourceMetadataRoleView, SourceMetadataScalar, SourceMetadataValue, SourceVoxelSpacing, @@ -2000,7 +2000,7 @@ def metadata_fields_for_realized_source_metadata( declared_names = frozenset(field.name for field in declared_fields) values_by_name: dict[str, list[SourceMetadataScalar]] = {} for metadata in realized_source_metadata: - for field_name, value in SourceMetadataRoleView(metadata).original_items(): + for field_name, value in SourceMetadataFields.original_items(metadata): if ( field_name not in declared_names and field_name not in excluded_names @@ -2360,31 +2360,13 @@ class SourceBindingRuntimeMetadataNormalizer: def normalized(self) -> Mapping[str, SourceMetadataMapping]: return MappingProxyType( { - str(path): MappingProxyType( - { - str(key): self.normalized_value(value) - for key, value in source_metadata_dict(metadata).items() - } + str(path): ResolvedSourceMetadataRecord.normalized_mapping( + source_metadata_dict(metadata) ) for path, metadata in self.source_metadata_by_path.items() } ) - @classmethod - def normalized_value(cls, value: SourceMetadataValue) -> SourceMetadataValue: - if isinstance(value, Mapping): - return MappingProxyType( - { - str(key): cls.normalized_scalar(nested_value) - for key, nested_value in value.items() - } - ) - return cls.normalized_scalar(value) - - @staticmethod - def normalized_scalar(value: SourceMetadataScalar) -> SourceMetadataScalar: - return source_metadata_scalar(value) - @dataclass(frozen=True) class SourceBindingRuntimeContext: @@ -2538,7 +2520,7 @@ def source_metadata_identity( cached = self._source_metadata_identity if cached is None: cached = tuple( - (path, SourceMetadataIdentityProjection(metadata).items()) + (path, SourceMetadataFields.identity_items(metadata)) for path, metadata in sorted(self.source_metadata_by_path.items()) ) object.__setattr__(self, "_source_metadata_identity", cached) @@ -2673,9 +2655,9 @@ def metadata_identity_for_paths( identity: list[tuple[str, SourceMetadataIdentityItems]] = [] for path in paths: if path in self.source_metadata_by_path: - metadata = SourceMetadataIdentityProjection( + metadata = SourceMetadataFields.identity_items( self.source_metadata_by_path[path] - ).items() + ) else: metadata = () identity.append((path, metadata)) diff --git a/openhcs/core/source_image_provenance.py b/openhcs/core/source_image_provenance.py index 519003cbb0..025d77b87f 100644 --- a/openhcs/core/source_image_provenance.py +++ b/openhcs/core/source_image_provenance.py @@ -20,6 +20,8 @@ from openhcs.core.source_metadata import ( SOURCE_PLANE_COUNT_FIELD, SOURCE_PLANE_INDEX_FIELD, + OwnedSourceMetadataFields, + SourceMetadataFields, SourceMetadataMapping, SourceMetadataScalar, SourceMetadataValue, @@ -69,20 +71,12 @@ def normalize_source_path(source_path: str | None) -> str | None: return str(source_path) -def _normalize_component_metadata( - metadata: SourceComponentMetadata | None, -) -> SourceComponentMetadata | None: - if metadata is None: - return None - return MappingProxyType(dict(metadata)) - - def _component_metadata_identity( metadata: SourceComponentMetadata | None, ) -> tuple[tuple[str, str], ...] | None: if metadata is None: return None - return tuple(sorted((str(key), repr(value)) for key, value in metadata.items())) + return SourceMetadataFields.provenance_identity_items(metadata) def _tuple_value(values: tuple[Any, ...], index: int) -> Any | None: @@ -101,7 +95,10 @@ class SourceImageIdentity: def __post_init__(self) -> None: self.path = normalize_source_path(self.path) - self.component_metadata = _normalize_component_metadata(self.component_metadata) + if self.component_metadata is not None: + self.component_metadata = SourceMetadataFields.readonly_snapshot( + self.component_metadata + ) self._identity = ( self.path, _component_metadata_identity(self.component_metadata), @@ -147,21 +144,10 @@ def component_metadata_with_missing_from( if fallback.component_metadata is None: return self.component_metadata - merged = dict(self.component_metadata) - for component in AllComponents: - if source_component_metadata_value(merged, component) is not None: - continue - fallback_value = source_component_metadata_raw_value( - fallback.component_metadata, - component, - ) - if fallback_value is not None: - merged[component.value] = fallback_value - if self.filename_extension is None: - extension = fallback.filename_extension - if extension is not None: - merged["extension"] = extension - return MappingProxyType(merged) + merged = SourceMetadataFields.with_missing_from( + self.component_metadata, fallback.component_metadata + ) + return SourceMetadataFields.readonly_snapshot(merged) def with_parsed_path_components( self, @@ -1505,18 +1491,15 @@ def component_metadata_for_plane( self, plane_index: int, ) -> SourceComponentMetadata: - metadata = { - **dict(self.scalar_metadata), - SOURCE_PLANE_INDEX_FIELD: str(plane_index), - SOURCE_PLANE_COUNT_FIELD: str(self.source_plane_count), - } - return MappingProxyType( - with_source_component_metadata( - metadata, - AllComponents.Z_INDEX, - self.z_index_for_plane(plane_index), - ) + metadata = SourceMetadataFields.with_fields( + SourceMetadataFields.composition_snapshot(self.scalar_metadata), + { + SOURCE_PLANE_INDEX_FIELD: str(plane_index), + SOURCE_PLANE_COUNT_FIELD: str(self.source_plane_count), + }, + components=((AllComponents.Z_INDEX, self.z_index_for_plane(plane_index)),), ) + return SourceMetadataFields.readonly_snapshot(metadata) def z_index_for_plane(self, plane_index: int) -> int: scalar_z_index = source_component_metadata_value( @@ -1585,7 +1568,9 @@ def common_source_component_metadata( } if not common_metadata: return None - return MappingProxyType(common_metadata) + return SourceMetadataFields.readonly_snapshot( + SourceMetadataFields.derived_mapping(consensus, common_metadata) + ) def source_component_metadata_consensus( @@ -1594,7 +1579,9 @@ def source_component_metadata_consensus( """Return every metadata field, nulling values that differ between planes.""" metadata_values = tuple( - dict(metadata) for metadata in metadata_by_plane if metadata is not None + SourceMetadataFields.composition_snapshot(metadata) + for metadata in metadata_by_plane + if metadata is not None ) if len(metadata_values) != len(metadata_by_plane): return None @@ -1618,7 +1605,17 @@ def source_component_metadata_consensus( and all(value == present_values[0] for value in present_values) else None ) - return MappingProxyType(consensus) + owner = ( + metadata_values[0] + if all( + isinstance(metadata, OwnedSourceMetadataFields) + for metadata in metadata_values + ) + else {} + ) + return SourceMetadataFields.readonly_snapshot( + SourceMetadataFields.derived_mapping(owner, consensus) + ) def common_source_path(paths: Sequence[str | None]) -> str | None: diff --git a/openhcs/core/source_matching.py b/openhcs/core/source_matching.py index 3c9d1e0d86..66108ec9fe 100644 --- a/openhcs/core/source_matching.py +++ b/openhcs/core/source_matching.py @@ -30,12 +30,13 @@ OriginalSourceMetadata, SourceFilterPathMetadata, SourceMetadataMapping, - SourceMetadataRoleView, + SourceMetadataFields, SourceMetadataScalar, SourceMetadataValue, SourceComponentProjectionStrategy, path_metadata_values_equivalent, source_metadata_field_identity, + source_metadata_component, source_metadata_dict, source_metadata_scalar, ) @@ -709,7 +710,7 @@ def overlay_source_metadata( for component, value in projected_components.items(): overlaid = with_source_component_metadata(overlaid, component, value) - for field, value in SourceMetadataRoleView(additions).scalar_items(): + for field, value in SourceMetadataFields.scalar_items(additions): if value is None: continue component = source_metadata_component(field) @@ -770,7 +771,7 @@ def source_metadata_value( key: str, ) -> SourceMetadataScalar: """Return one source-literal metadata value by its exact declared key.""" - return _source_metadata_lookup_projection(metadata).value(key) + return SourceMetadataFields.literal_value(metadata, key) def source_component_metadata_value( @@ -819,7 +820,7 @@ def semantic_source_metadata_value( field_identity = source_metadata_field_identity(field_name) semantic_values = tuple( value - for field, value in SourceMetadataRoleView(metadata).scalar_items() + for field, value in SourceMetadataFields.scalar_items(metadata) if value is not None and source_metadata_field_identity(str(field)) == field_identity ) @@ -846,14 +847,14 @@ def source_component_metadata_values( component: AllComponents, ) -> tuple[str, ...]: """Return all metadata values that semantically describe a component.""" - return _source_metadata_lookup_projection(metadata).component_values(component) + return SourceMetadataFields.component_values(metadata, component) def with_source_component_metadata( metadata: SourceMetadataMapping, component: AllComponents, value: SourceMetadataScalar, -) -> dict[str, SourceMetadataValue]: +) -> SourceMetadataMapping: """Return metadata with one canonical component value. Source schemas can carry CellProfiler spellings such as ``Well`` and OpenHCS @@ -861,15 +862,7 @@ def with_source_component_metadata( every semantic spelling so later normalized lookups cannot observe both the old and new component values. """ - return { - **{ - key: field_value - for key, field_value in metadata.items() - if key == ORIGINAL_SOURCE_METADATA_FIELD - or source_metadata_component(str(key)) is not component - }, - component.value: str(value), - } + return SourceMetadataFields.with_component(metadata, component, value) def source_metadata_values_equal( @@ -969,62 +962,10 @@ def constraint_matches_metadata( return any( metadata_value is not None and source_metadata_values_equal(str(metadata_value), value) - for metadata_value in SourceMetadataRoleView(metadata).scalar_values() + for metadata_value in SourceMetadataFields.scalar_values(metadata) ) -@lru_cache(maxsize=256) -def source_metadata_component(field: str) -> AllComponents | None: - """Return the nominal component owner of a metadata field.""" - - return SourceComponentProjectionStrategy.component_for_metadata_field(field) - - -@dataclass(frozen=True, slots=True) -class SourceMetadataLookupProjection: - """Lookup projection over the current contents of one source metadata mapping.""" - - scalar_items: tuple[tuple[str, SourceMetadataScalar], ...] - original_items: tuple[tuple[str, SourceMetadataScalar], ...] - - def value(self, key: str) -> SourceMetadataScalar: - """Return a source-literal metadata value by exact key.""" - for candidate_key, value in self.original_items: - if str(candidate_key) == key and value is not None: - return value - for candidate_key, value in self.scalar_items: - if str(candidate_key) == key and value is not None: - return value - return None - - def component_values(self, component: AllComponents) -> tuple[str, ...]: - """Return all metadata values that semantically describe a component.""" - values: list[str] = [] - for field, field_value in self.scalar_items: - field_text = str(field) - if field_text == component.value and field_value is not None: - values.append(str(field_value)) - for field, field_value in self.scalar_items: - field_text = str(field) - if ( - field_text != component.value - and source_metadata_component(field_text) is component - ) and field_value is not None: - values.append(str(field_value)) - return tuple(dict.fromkeys(values)) - - -def _source_metadata_lookup_projection( - metadata: SourceMetadataMapping, -) -> SourceMetadataLookupProjection: - """Derive a lookup projection from the current nominal metadata role view.""" - role_view = SourceMetadataRoleView(metadata) - return SourceMetadataLookupProjection( - scalar_items=role_view.scalar_items(), - original_items=role_view.original_items(), - ) - - def _require_filter_value(clause: SourceFilterClause) -> str: if clause.value is None: raise ValueError( diff --git a/openhcs/core/source_metadata.py b/openhcs/core/source_metadata.py index 63c7e4e049..cdc7d47641 100644 --- a/openhcs/core/source_metadata.py +++ b/openhcs/core/source_metadata.py @@ -3,13 +3,14 @@ from __future__ import annotations from abc import ABC, abstractmethod -from collections.abc import Callable, Iterable, Mapping +from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence from dataclasses import dataclass, field from enum import Enum from functools import lru_cache from math import isfinite from pathlib import Path -from typing import ClassVar, TypeAlias +from types import MappingProxyType +from typing import ClassVar, Self, TYPE_CHECKING, TypeAlias, TypeVar from metaclass_registry import AutoRegisterMeta from zmqruntime.viewer_protocol import ViewerWireField @@ -17,6 +18,11 @@ from openhcs.constants.constants import AllComponents from openhcs.core.registry_strategies import EnumKeyedStrategyMixin +if TYPE_CHECKING: + from openhcs.core.source_bindings import MetadataExtractionRule + from openhcs.microscopes.microscope_interfaces import FilenameParser + + ORIGINAL_SOURCE_METADATA_FIELD = "OpenHCSOriginalSourceMetadata" SOURCE_FILTER_PATHS_METADATA_FIELD = "OpenHCSSourceFilterPaths" SOURCE_PLANE_INDEX_FIELD = "source_plane_index" @@ -38,6 +44,476 @@ ] +_MetadataViewT = TypeVar("_MetadataViewT") + + +@dataclass(frozen=True, slots=True, eq=False) +class SourceMetadataFields(Mapping[str, SourceMetadataValue]): + """Common field projection algorithms with nominal lifetime-selected views.""" + + fields: tuple[tuple[str, SourceMetadataValue], ...] + + @classmethod + def from_mapping(cls, metadata: SourceMetadataMapping) -> Self: + return cls(tuple(metadata.items())) + + @classmethod + def normalized_value(cls, value: SourceMetadataValue) -> SourceMetadataValue: + if isinstance(value, Mapping): + return MappingProxyType( + { + str(key): cls.normalized_scalar(nested_value) + for key, nested_value in value.items() + } + ) + return cls.normalized_scalar(value) + + @classmethod + def normalized_mapping( + cls, metadata: SourceMetadataMapping + ) -> SourceMetadataMapping: + return MappingProxyType( + {str(key): cls.normalized_value(value) for key, value in metadata.items()} + ) + + @staticmethod + def normalized_scalar(value: SourceMetadataScalar) -> SourceMetadataScalar: + return source_metadata_scalar(value) + + def __getitem__(self, key: str) -> SourceMetadataValue: + for field_key, value in self.fields: + if field_key == key: + return value + raise KeyError(key) + + def __iter__(self) -> Iterator[str]: + return (key for key, _value in self.fields) + + def __len__(self) -> int: + return len(self.fields) + + @abstractmethod + def __eq__(self, other: object) -> bool: + """Compare through the declared record or mapping namespace.""" + + def _derived_view( + self, key: str | AllComponents, derive: Callable[[], _MetadataViewT] + ) -> _MetadataViewT: + return derive() + + @staticmethod + def _view( + metadata: SourceMetadataMapping, + key: str | AllComponents, + derive: Callable[[], _MetadataViewT], + ) -> _MetadataViewT: + if isinstance(metadata, SourceMetadataFields): + return metadata._derived_view(key, derive) + return derive() + + @classmethod + def scalar_items( + cls, metadata: SourceMetadataMapping + ) -> tuple[tuple[str, SourceMetadataScalar], ...]: + """Read current raw fields or the immutable owner's scalar projection.""" + return cls._view( + metadata, + "scalar_items", + lambda: tuple( + (str(key), value) + for key, value in metadata.items() + if key != ORIGINAL_SOURCE_METADATA_FIELD + and not isinstance(value, Mapping) + ), + ) + + @classmethod + def scalar_values( + cls, metadata: SourceMetadataMapping + ) -> tuple[SourceMetadataScalar, ...]: + return tuple(value for _key, value in cls.scalar_items(metadata)) + + @classmethod + def original_items( + cls, metadata: SourceMetadataMapping + ) -> tuple[tuple[str, SourceMetadataScalar], ...]: + def project() -> tuple[tuple[str, SourceMetadataScalar], ...]: + original_metadata = metadata.get(ORIGINAL_SOURCE_METADATA_FIELD) + if original_metadata is None: + return () + return OriginalSourceMetadata.from_reserved_value( + original_metadata, path=ORIGINAL_SOURCE_METADATA_FIELD + ).fields + + return cls._view(metadata, "original_items", project) + + @classmethod + def source_filter_paths(cls, metadata: SourceMetadataMapping) -> tuple[str, ...]: + def project() -> tuple[str, ...]: + paths = metadata.get(SOURCE_FILTER_PATHS_METADATA_FIELD) + if paths is None: + return () + return SourceFilterPathMetadata.from_reserved_value( + paths, path=SOURCE_FILTER_PATHS_METADATA_FIELD + ).paths + + return cls._view(metadata, "source_filter_paths", project) + + @classmethod + def identity_items( + cls, metadata: SourceMetadataMapping + ) -> SourceMetadataIdentityItems: + def project() -> SourceMetadataIdentityItems: + return tuple( + sorted( + ( + str(key), + ( + tuple(sorted((str(k), v) for k, v in value.items())) + if isinstance(value, Mapping) + else value + ), + ) + for key, value in metadata.items() + ) + ) + + return cls._view(metadata, "identity_items", project) + + @classmethod + def provenance_identity_items( + cls, metadata: SourceMetadataMapping + ) -> tuple[tuple[str, str], ...]: + """Preserve the exact stored repr, including nested mapping insertion order.""" + return cls._view( + metadata, + "provenance_identity_items", + lambda: tuple(sorted((str(k), repr(v)) for k, v in metadata.items())), + ) + + @classmethod + def literal_value( + cls, metadata: SourceMetadataMapping, key: str + ) -> SourceMetadataScalar: + # Preserve the existing lookup's role-validation order even when the + # requested literal could be read without the original metadata role. + scalars = cls.scalar_items(metadata) + original = cls.original_items(metadata) + for fields in (original, scalars): + for candidate_key, value in fields: + if str(candidate_key) == key and value is not None: + return value + return None + + @classmethod + def component_value( + cls, metadata: SourceMetadataMapping, component: AllComponents + ) -> str | None: + return cls._view( + metadata, + component, + lambda: SourceComponentProjectionStrategy.for_enum_member( + component + ).metadata_value(metadata), + ) + + @classmethod + def component_values( + cls, metadata: SourceMetadataMapping, component: AllComponents + ) -> tuple[str, ...]: + scalars = cls.scalar_items(metadata) + cls.original_items(metadata) + values = [ + str(value) + for field, value in scalars + if str(field) == component.value and value is not None + ] + values.extend( + str(value) + for field, value in scalars + if str(field) != component.value + and source_metadata_component(str(field)) is component + and value is not None + ) + return tuple(dict.fromkeys(values)) + + @staticmethod + def readonly_snapshot(metadata: SourceMetadataMapping) -> SourceMetadataMapping: + """Preserve the old mapping snapshot while retaining an owned record's lifetime.""" + if isinstance(metadata, SourceMetadataFields): + return metadata._readonly_snapshot() + return MappingProxyType(dict(metadata)) + + def _readonly_snapshot(self) -> SourceMetadataMapping: + return MappingProxyType(dict(self)) + + @staticmethod + def composition_snapshot(metadata: SourceMetadataMapping) -> SourceMetadataMapping: + """Capture one composition's raw fields; owned records already hold that snapshot.""" + if isinstance(metadata, SourceMetadataFields): + return metadata._composition_snapshot() + return dict(metadata) + + def _composition_snapshot(self) -> SourceMetadataMapping: + return dict(self) + + @staticmethod + def derived_mapping( + metadata: SourceMetadataMapping, + fields: dict[str, SourceMetadataValue] | None, + ) -> SourceMetadataMapping: + """Keep owned lifetime on derived fields; raw callers receive a fresh dict.""" + if isinstance(metadata, SourceMetadataFields): + return metadata._derive_fields(fields) + return dict(metadata) if fields is None else fields + + def _derive_fields( + self, fields: dict[str, SourceMetadataValue] | None + ) -> SourceMetadataMapping: + return dict(self) if fields is None else fields + + @classmethod + def with_fields( + cls, + metadata: SourceMetadataMapping, + updates: SourceMetadataMapping, + *, + without: Iterable[str] = (), + components: Iterable[tuple[AllComponents, SourceMetadataScalar]] = (), + after_components: SourceMetadataMapping | None = None, + ) -> SourceMetadataMapping: + excluded = frozenset(without) + fields = {key: value for key, value in metadata.items() if key not in excluded} + fields.update(updates) + for component, value in components: + fields = { + key: field_value + for key, field_value in fields.items() + if key == ORIGINAL_SOURCE_METADATA_FIELD + or source_metadata_component(str(key)) is not component + } + fields[component.value] = str(value) + if after_components is not None: + fields.update(after_components) + return cls.derived_mapping(metadata, fields) + + @classmethod + def with_component( + cls, + metadata: SourceMetadataMapping, + component: AllComponents, + value: SourceMetadataScalar, + ) -> SourceMetadataMapping: + return cls.with_fields(metadata, {}, components=((component, value),)) + + @classmethod + def with_missing_from( + cls, metadata: SourceMetadataMapping, fallback: SourceMetadataMapping + ) -> SourceMetadataMapping: + merged: dict[str, SourceMetadataValue] | None = None + current = metadata + for component in AllComponents: + if cls.component_value(current, component) is not None: + continue + value = cls.component_value(fallback, component) + if value is not None: + if merged is None: + merged = dict(metadata) + merged[component.value] = value + current = merged + if cls.literal_value(metadata, "extension") is None: + extension = cls.literal_value(fallback, "extension") + if extension is not None: + if merged is None: + merged = dict(metadata) + merged["extension"] = str(extension) + if merged is None: + return cls.derived_mapping(metadata, None) + return cls.derived_mapping(metadata, merged) + + +@dataclass(frozen=True, slots=True, eq=False) +class SourceMetadataRecord(SourceMetadataFields): + """Ordered source metadata carried through declared selector resolution.""" + + @abstractmethod + def resolve( + self, + path: str, + parser: "FilenameParser", + metadata_rules: tuple["MetadataExtractionRule", ...], + ) -> "SourceMetadataRecord | None": + """Resolve fields through their declared metadata lifetime.""" + + def __eq__(self, other: object) -> bool: + if not isinstance(other, SourceMetadataRecord): + return NotImplemented + return self.fields == other.fields + + def __hash__(self) -> int: + return hash((self.fields,)) + + +@dataclass(frozen=True, slots=True, eq=False) +class OwnedSourceMetadataFields(SourceMetadataFields): + """Deep field ownership, indexed lookup, bounded views and local transport lifetime.""" + + _field_index: Mapping[str, SourceMetadataValue] = field( + init=False, repr=False, compare=False + ) + _views: dict[str | AllComponents, object] = field( + default_factory=dict, init=False, repr=False, compare=False + ) + _cacheable: bool = field(init=False, repr=False, compare=False) + + def __post_init__(self) -> None: + fields = tuple( + (str(key), self.normalized_value(value)) for key, value in self.fields + ) + object.__setattr__(self, "fields", self._normalized_fields(fields)) + self._initialize_derived_views() + + @staticmethod + def _normalized_fields( + fields: tuple[tuple[str, SourceMetadataValue], ...], + ) -> tuple[tuple[str, SourceMetadataValue], ...]: + return fields + + def _initialize_derived_views(self) -> None: + object.__setattr__(self, "_views", {}) + index: dict[str, SourceMetadataValue] = {} + for key, value in self.fields: + index.setdefault(key, value) + object.__setattr__(self, "_field_index", MappingProxyType(index)) + object.__setattr__( + self, + "_cacheable", + all( + ( + all( + type(v) in (str, int, float, bool, type(None)) + for v in value.values() + ) + if isinstance(value, Mapping) + else type(value) in (str, int, float, bool, type(None)) + ) + for _key, value in self.fields + ), + ) + + def __reduce__(self): + fields = tuple( + (key, dict(value) if isinstance(value, Mapping) else value) + for key, value in self.fields + ) + return (type(self)._from_serialized_fields, (fields,)) + + @classmethod + def _from_serialized_fields( + cls, fields: tuple[tuple[str, SourceMetadataValue], ...] + ) -> Self: + """Restore stored spelling and ownership without serializing derived views.""" + record = cls.__new__(cls) + object.__setattr__( + record, + "fields", + tuple( + ( + key, + ( + MappingProxyType(dict(value)) + if isinstance(value, Mapping) + else value + ), + ) + for key, value in fields + ), + ) + record._initialize_derived_views() + return record + + def __getitem__(self, key: str) -> SourceMetadataValue: + return self._field_index[key] + + @property + def metadata_contents(self) -> SourceMetadataMapping: + """Read-only mapping contents, separate from ordered record equality.""" + return self._field_index + + def _derived_view( + self, key: str | AllComponents, derive: Callable[[], _MetadataViewT] + ) -> _MetadataViewT: + if not self._cacheable: + return derive() + if key not in self._views: + self._views[key] = derive() + return self._views[key] + + def _readonly_snapshot(self) -> SourceMetadataMapping: + return self + + def _composition_snapshot(self) -> SourceMetadataMapping: + return self + + def _derive_fields( + self, fields: dict[str, SourceMetadataValue] | None + ) -> SourceMetadataMapping: + return self if fields is None else DurableSourceMetadata.from_mapping(fields) + + +@dataclass(frozen=True, slots=True, eq=False) +class ResolvedSourceMetadataRecord(OwnedSourceMetadataFields, SourceMetadataRecord): + """Owned runtime fields with the existing ordered selector record contract.""" + + def _readonly_snapshot(self) -> SourceMetadataMapping: + return self._derived_view( + "readonly_snapshot", + lambda: DurableSourceMetadata.from_mapping(self._field_index), + ) + + def resolve( + self, + path: str, + parser: "FilenameParser", + metadata_rules: tuple["MetadataExtractionRule", ...], + ) -> SourceMetadataRecord | None: + return self if self.fields else None + + +@dataclass(frozen=True, slots=True, eq=False) +class DurableSourceMetadata(OwnedSourceMetadataFields): + """Owned literal metadata from durable storage and image field derivation.""" + + @staticmethod + def _normalized_fields( + fields: tuple[tuple[str, SourceMetadataValue], ...], + ) -> tuple[tuple[str, SourceMetadataValue], ...]: + return tuple(dict(fields).items()) + + def __eq__(self, other: object) -> bool: + if isinstance(other, SourceMetadataRecord): + return NotImplemented + return Mapping.__eq__(self, other) + + __hash__ = None + + @staticmethod + def normalized_scalar(value: SourceMetadataScalar) -> SourceMetadataScalar: + if value is None or isinstance(value, (str, int, float, bool)): + return value + if isinstance(value, Mapping) or ( + isinstance(value, Sequence) and not isinstance(value, str) + ): + raise RuntimeError( + "virtual_workspace source metadata supports scalar values and " + "one-level scalar mappings only." + ) + raise RuntimeError( + "virtual_workspace source metadata scalar values must be strings, " + "numbers, booleans, or null." + ) + + def source_metadata_dict( metadata: SourceMetadataMapping, ) -> dict[str, SourceMetadataValue]: @@ -489,68 +965,6 @@ def normalize_source_voxel_spacing_fields(self) -> None: ) -@dataclass(frozen=True, slots=True) -class SourceMetadataRoleView: - """Role-aware metadata view for literal selectors versus component axes.""" - - metadata: SourceMetadataMapping - - def scalar_items(self) -> tuple[tuple[str, SourceMetadataScalar], ...]: - items: list[tuple[str, SourceMetadataScalar]] = [] - for key, value in self.metadata.items(): - if key == ORIGINAL_SOURCE_METADATA_FIELD or isinstance(value, Mapping): - continue - items.append((str(key), value)) - return tuple(items) - - def scalar_values(self) -> tuple[SourceMetadataScalar, ...]: - return tuple(value for _key, value in self.scalar_items()) - - def original_items(self) -> tuple[tuple[str, SourceMetadataScalar], ...]: - original_metadata = self.metadata.get(ORIGINAL_SOURCE_METADATA_FIELD) - if original_metadata is None: - return () - return OriginalSourceMetadata.from_reserved_value( - original_metadata, - path=ORIGINAL_SOURCE_METADATA_FIELD, - ).fields - - def source_filter_paths(self) -> tuple[str, ...]: - source_filter_paths = self.metadata.get(SOURCE_FILTER_PATHS_METADATA_FIELD) - if source_filter_paths is None: - return () - return SourceFilterPathMetadata.from_reserved_value( - source_filter_paths, - path=SOURCE_FILTER_PATHS_METADATA_FIELD, - ).paths - - -@dataclass(frozen=True, slots=True) -class SourceMetadataIdentityProjection: - """Stable hash projection for source metadata records.""" - - metadata: SourceMetadataMapping - - def items(self) -> SourceMetadataIdentityItems: - projected: list[tuple[str, SourceMetadataIdentityValue]] = [] - for key, value in self.metadata.items(): - if isinstance(value, Mapping): - projected.append( - ( - str(key), - tuple( - sorted( - (str(nested_key), nested_value) - for nested_key, nested_value in value.items() - ) - ), - ) - ) - continue - projected.append((str(key), value)) - return tuple(sorted(projected)) - - @lru_cache(maxsize=4096) def source_metadata_field_identity(field: str) -> str: """Return the canonical semantic identity of one source metadata field.""" @@ -565,6 +979,12 @@ def source_metadata_field_identity(field: str) -> str: ) +@lru_cache(maxsize=256) +def source_metadata_component(field: str) -> AllComponents | None: + """Return the nominal component owner of a metadata field.""" + return SourceComponentProjectionStrategy.component_for_metadata_field(field) + + class SourceComponentProjectionStrategy( EnumKeyedStrategyMixin[AllComponents], ABC, @@ -591,7 +1011,7 @@ def metadata_component( component: AllComponents, metadata: SourceMetadataMapping, ) -> str | None: - return cls.for_enum_member(component).metadata_value(metadata) + return SourceMetadataFields.component_value(metadata, component) @classmethod def component_for_metadata_field( @@ -625,7 +1045,7 @@ def _metadata_group_value( metadata: SourceMetadataMapping, group: tuple[str, ...], ) -> str | None: - scalar_items = SourceMetadataRoleView(metadata).scalar_items() + scalar_items = SourceMetadataFields.scalar_items(metadata) for alias in group: alias_identity = source_metadata_field_identity(alias) for field_name, value in scalar_items: diff --git a/openhcs/core/source_projection.py b/openhcs/core/source_projection.py index 1ef3bb0834..40c1321c60 100644 --- a/openhcs/core/source_projection.py +++ b/openhcs/core/source_projection.py @@ -34,9 +34,9 @@ ) from openhcs.core.source_metadata import ( SourceComponentProjectionStrategy, - SourceMetadataIdentityProjection, + SourceMetadataFields, + ResolvedSourceMetadataRecord, SourceMetadataMapping, - SourceMetadataRoleView, SourceMetadataValue, source_metadata_dict, source_metadata_scalar, @@ -448,7 +448,7 @@ def identity_key(self) -> tuple[object, ...]: self.declared_address, self.dataset_identity, self.store_identity, - SourceMetadataIdentityProjection(self.metadata).items(), + SourceMetadataFields.identity_items(self.metadata), ) @@ -1219,7 +1219,7 @@ def _source_metadata( metadata = source_metadata_dict(projection.source_metadata) source_component_fields = { field: value - for field, value in SourceMetadataRoleView(metadata).scalar_items() + for field, value in SourceMetadataFields.scalar_items(metadata) if ( (component := source_metadata_component(field)) is not None and field != component.value @@ -1231,7 +1231,7 @@ def _source_metadata( source_component_fields, path=projection.ref.backend_address, ) - original_metadata = dict(SourceMetadataRoleView(metadata).original_items()) + original_metadata = dict(SourceMetadataFields.original_items(metadata)) for component, value in projection.source_component_values(): canonical_value = metadata.get(component.value) conflicts_with_address = ( @@ -1303,21 +1303,6 @@ def _padded(value: str, width: int) -> str: return f"{int(value):0{width}d}" if value.isdecimal() else value -def _normalized_source_metadata_value( - value: SourceMetadataValue, -) -> SourceMetadataValue: - """Freeze one source-metadata value without erasing its nominal shape.""" - - if isinstance(value, Mapping): - return MappingProxyType( - { - str(key): source_metadata_scalar(nested_value) - for key, nested_value in value.items() - } - ) - return source_metadata_scalar(value) - - def _normalize_projection(projection: SourceProjection) -> None: """Normalize fields shared by every nominal source projection.""" @@ -1338,12 +1323,7 @@ def _normalize_projection(projection: SourceProjection) -> None: object.__setattr__( projection, "source_metadata", - MappingProxyType( - { - str(key): _normalized_source_metadata_value(value) - for key, value in projection.source_metadata.items() - } - ), + ResolvedSourceMetadataRecord.normalized_mapping(projection.source_metadata), ) object.__setattr__( projection, diff --git a/openhcs/core/source_workspace_projection.py b/openhcs/core/source_workspace_projection.py index 84878b8851..8caef9e15f 100644 --- a/openhcs/core/source_workspace_projection.py +++ b/openhcs/core/source_workspace_projection.py @@ -23,7 +23,10 @@ SOURCE_BINDING_ALIAS_METADATA_FIELD, SourceProjectionRole, ) -from openhcs.core.source_metadata import SourceMetadataMapping +from openhcs.core.source_metadata import ( + SourceMetadataFields, + SourceMetadataMapping, +) from openhcs.core.source_matching import ( source_component_metadata_values, source_metadata_value, @@ -531,12 +534,8 @@ def apply(self, payload: RuntimeArrayData) -> RuntimeArrayData: """Apply declared component metadata and source aliases to one payload.""" source_metadata = self.source_metadata if source_metadata is not None: - source_metadata = MappingProxyType( - { - field: value - for field, value in source_metadata.items() - if field != SOURCE_BINDING_ALIAS_METADATA_FIELD - } + source_metadata = SourceMetadataFields.with_fields( + source_metadata, {}, without=(SOURCE_BINDING_ALIAS_METADATA_FIELD,) ) current_metadata = image_payload_metadata(payload) metadata = self.metadata(current_metadata) @@ -817,7 +816,7 @@ def record_source_metadata( virtual_path: str, metadata_fields: SourceMetadataMapping, ) -> None: - normalized_metadata = MappingProxyType(dict(metadata_fields)) + normalized_metadata = SourceMetadataFields.readonly_snapshot(metadata_fields) self.source_metadata_by_path[virtual_path] = normalized_metadata self.source_metadata_by_path[str(self.plate_path / virtual_path)] = ( normalized_metadata diff --git a/openhcs/core/steps/function_artifact_materialization.py b/openhcs/core/steps/function_artifact_materialization.py index 7510f0f61d..35effbb957 100644 --- a/openhcs/core/steps/function_artifact_materialization.py +++ b/openhcs/core/steps/function_artifact_materialization.py @@ -758,7 +758,7 @@ def record_metadata_with_runtime_scope( ) elif scope.component is not None: component_metadata = with_source_component_metadata( - dict(metadata.source_component_metadata or {}), + metadata.source_component_metadata or {}, scope.component, scope.require_value_text(), ) diff --git a/openhcs/core/steps/function_output_identity.py b/openhcs/core/steps/function_output_identity.py index f83da45f2f..c606b20dd5 100644 --- a/openhcs/core/steps/function_output_identity.py +++ b/openhcs/core/steps/function_output_identity.py @@ -23,9 +23,9 @@ SourceImageIdentity, SourceImageProvenanceIdentity, ) +from openhcs.core.source_metadata import SourceMetadataFields from openhcs.core.source_matching import ( source_component_metadata_items, - with_source_component_metadata, ) from openhcs.core.source_projection import OpenHCSPlaneAddress from openhcs.microscopes.microscope_interfaces import FilenameParser @@ -144,13 +144,15 @@ def component_metadata( source_metadata: SourceComponentMetadata | None = None, ) -> SourceComponentMetadata: """Return parser-compatible component metadata for this output identity.""" - metadata = dict(source_metadata or {}) - metadata.update(self.component_values) - for component, value in source_component_metadata_items(self.component_values): - metadata = with_source_component_metadata(metadata, component, value) - if self.extension is not None: - metadata["extension"] = self.extension - return metadata + metadata = source_metadata if source_metadata is not None else {} + return SourceMetadataFields.with_fields( + SourceMetadataFields.composition_snapshot(metadata), + self.component_values, + components=source_component_metadata_items(self.component_values), + after_components=( + {"extension": self.extension} if self.extension is not None else None + ), + ) def filename_component_metadata(self) -> SourceComponentMetadata: """Return parser-compatible metadata used to construct the output filename.""" @@ -318,7 +320,9 @@ def _qualified_filename( if qualifier is None: return filename if not filename.endswith(extension): - raise ValueError("Constructed filename does not retain its declared extension.") + raise ValueError( + "Constructed filename does not retain its declared extension." + ) return f"{filename[:-len(extension)]}_{qualifier}{extension}" @staticmethod @@ -584,8 +588,10 @@ def _identity_from_metadata_uncached( identity = cls._identity_from_metadata( metadata.source_component_metadata, extension=FunctionOutputExtensionAuthority.from_source( - metadata.source_component_metadata, metadata.source_path, - parser=parser, identity_cache=identity_cache, + metadata.source_component_metadata, + metadata.source_path, + parser=parser, + identity_cache=identity_cache, ), source="payload component metadata", ) @@ -639,8 +645,10 @@ def _identity_from_metadata_uncached( identity = cls._identity_from_metadata( source_identity.component_metadata, extension=FunctionOutputExtensionAuthority.from_source( - source_identity.component_metadata, source_identity.path, - parser=parser, identity_cache=identity_cache, + source_identity.component_metadata, + source_identity.path, + parser=parser, + identity_cache=identity_cache, ), source="single represented payload source metadata", ) @@ -698,8 +706,10 @@ def filename_identity_from_metadata( identity = cls._identity_from_metadata( metadata.source_component_metadata, extension=FunctionOutputExtensionAuthority.from_source( - metadata.source_component_metadata, metadata.source_path, - parser=parser, identity_cache=identity_cache, + metadata.source_component_metadata, + metadata.source_path, + parser=parser, + identity_cache=identity_cache, ), source="payload component metadata", ) @@ -898,8 +908,10 @@ def _source_stack_source_identity( identity = cls._identity_from_metadata( source_identity.component_metadata, extension=FunctionOutputExtensionAuthority.from_source( - source_identity.component_metadata, source_identity.path, - parser=parser, identity_cache=identity_cache, + source_identity.component_metadata, + source_identity.path, + parser=parser, + identity_cache=identity_cache, ), source=f"represented source identity {identity_index} metadata", ) diff --git a/openhcs/core/steps/function_runtime.py b/openhcs/core/steps/function_runtime.py index 35681e5fb9..948d937ec2 100644 --- a/openhcs/core/steps/function_runtime.py +++ b/openhcs/core/steps/function_runtime.py @@ -2084,7 +2084,7 @@ def execution_group_source_payload( if component is None or self.group_key is None: return source_payload metadata = image_payload_metadata(source_payload) - component_metadata = dict(metadata.source_component_metadata or {}) + component_metadata = metadata.source_component_metadata or {} component_metadata = with_source_component_metadata( component_metadata, component, diff --git a/openhcs/core/virtual_workspace_metadata.py b/openhcs/core/virtual_workspace_metadata.py index ff4abc6d0c..ded7fe31be 100644 --- a/openhcs/core/virtual_workspace_metadata.py +++ b/openhcs/core/virtual_workspace_metadata.py @@ -21,9 +21,9 @@ from openhcs.core.runtime_image_values import ImagePayloadMetadata from openhcs.core.source_bindings import SourceProjectionRole from openhcs.core.source_metadata import ( + DurableSourceMetadata, SourceMetadataMapping, SourceMetadataScalar, - SourceMetadataValue, SourceVoxelSpacing, ) from openhcs.core.source_projection import ( @@ -240,9 +240,12 @@ def update(data): ) subdirectory[FIELDS.IMAGE_FILES] = list(published_paths) subdirectory[FIELDS.MICROSCOPE_HANDLER_NAME] = microscope_handler_name - subdirectory[FIELDS.SOURCE_FILENAME_PARSER_NAME] = source_filename_parser_name + subdirectory[FIELDS.SOURCE_FILENAME_PARSER_NAME] = ( + source_filename_parser_name + ) subdirectory[FIELDS.AVAILABLE_BACKENDS] = { - **subdirectory.get(FIELDS.AVAILABLE_BACKENDS, {}), backend: True + **subdirectory.get(FIELDS.AVAILABLE_BACKENDS, {}), + backend: True, } if is_main: subdirectory[serializer.MAIN_FIELD] = True @@ -688,51 +691,7 @@ def normalize_metadata_fields(metadata_fields: JsonValue) -> SourceMetadataMappi raise RuntimeError( "virtual_workspace source metadata values must be mappings." ) - return MappingProxyType( - { - str( - key - ): VirtualWorkspaceSourceMetadataEntries.normalize_metadata_value(value) - for key, value in metadata_fields.items() - } - ) - - @staticmethod - def normalize_metadata_value(value: JsonValue) -> SourceMetadataValue: - if isinstance(value, Mapping): - return MappingProxyType( - { - str( - nested_key - ): VirtualWorkspaceSourceMetadataEntries.require_scalar_metadata_value( - nested_value - ) - for nested_key, nested_value in value.items() - } - ) - return VirtualWorkspaceSourceMetadataEntries.require_scalar_metadata_value( - value - ) - - @staticmethod - def require_scalar_metadata_value(value: JsonValue) -> SourceMetadataScalar: - # Scalar fast path first: metadata values are overwhelmingly scalars, - # and the container ABC isinstance checks below are comparatively - # expensive per field. - if value is None or isinstance(value, (str, int, float, bool)): - return value - if isinstance(value, Mapping) or ( - isinstance(value, Sequence) and not isinstance(value, str) - ): - raise RuntimeError( - "virtual_workspace source metadata supports scalar values and " - "one-level scalar mappings only." - ) - raise RuntimeError( - "virtual_workspace source metadata scalar values must be strings, " - "numbers, booleans, or null." - ) - return value + return DurableSourceMetadata.from_mapping(metadata_fields) def metadata_for(self, virtual_path: str) -> SourceMetadataMapping: metadata = self.entries.get(virtual_path) diff --git a/openhcs/interop/cellprofiler/analyst_export.py b/openhcs/interop/cellprofiler/analyst_export.py index 7c61aa1e44..4d1df75243 100644 --- a/openhcs/interop/cellprofiler/analyst_export.py +++ b/openhcs/interop/cellprofiler/analyst_export.py @@ -63,7 +63,7 @@ SourceImageProvenance, ) from openhcs.core.source_bindings import CompiledSourceBindingPlan -from openhcs.core.source_metadata import SourceMetadataRoleView, SourceMetadataScalar +from openhcs.core.source_metadata import SourceMetadataFields, SourceMetadataScalar from openhcs.core.source_workspace_projection import ( VirtualWorkspacePathLookup, VirtualWorkspaceSourceProjectionAuthority, @@ -919,7 +919,7 @@ def collect_image_provenance( ] = source_path.as_uri() if component_metadata is not None: metadata_items.update( - SourceMetadataRoleView(component_metadata).original_items() + SourceMetadataFields.original_items(component_metadata) ) image_number = self.image_set_numbering.for_source_slice( scope=scope, diff --git a/openhcs/processing/backends/cellprofiler/spreadsheet_export.py b/openhcs/processing/backends/cellprofiler/spreadsheet_export.py index 3682699839..3c0745c822 100644 --- a/openhcs/processing/backends/cellprofiler/spreadsheet_export.py +++ b/openhcs/processing/backends/cellprofiler/spreadsheet_export.py @@ -58,7 +58,7 @@ source_component_metadata_consensus, ) from openhcs.core.source_metadata import ( - SourceMetadataRoleView, + SourceMetadataFields, ) from openhcs.interop.cellprofiler.image_set_numbering import ( CellProfilerImageSetNumbering, @@ -583,8 +583,7 @@ def _source_metadata_measurement_rows( ).source_component_metadata if metadata is None: continue - role_view = SourceMetadataRoleView(metadata) - original_metadata = dict(role_view.original_items()) + original_metadata = dict(SourceMetadataFields.original_items(metadata)) if original_metadata: rows.append((image_numbers_by_slice[slice_index], original_metadata)) return tuple(rows) diff --git a/tests/integration/test_source_store_code_mode_zmq.py b/tests/integration/test_source_store_code_mode_zmq.py index f8858301d9..749b273b4a 100644 --- a/tests/integration/test_source_store_code_mode_zmq.py +++ b/tests/integration/test_source_store_code_mode_zmq.py @@ -42,7 +42,7 @@ SourceFilterSubject, SourceSelector, ) -from openhcs.core.source_metadata import SourceMetadataRoleView +from openhcs.core.source_metadata import SourceMetadataFields from openhcs.core.steps.function_step import FunctionStep from openhcs.microscopes.bioformats_adapter import ( BioFormatsAdapterUnavailableError, @@ -316,9 +316,7 @@ def test_code_mode_and_zmq_wire_preserve_mixed_store_sources( } for alias, (source_path, pixels) in stores.items(): virtual_path, record = records_by_alias[alias] - filter_paths = SourceMetadataRoleView( - record.source_metadata - ).source_filter_paths() + filter_paths = SourceMetadataFields.source_filter_paths(record.source_metadata) assert source_path.name in filter_paths assert str(source_path) in filter_paths np.testing.assert_array_equal( diff --git a/tests/unit/test_runtime_source_resolution_snapshot.py b/tests/unit/test_runtime_source_resolution_snapshot.py index 1a2662a51c..5d3660d75c 100644 --- a/tests/unit/test_runtime_source_resolution_snapshot.py +++ b/tests/unit/test_runtime_source_resolution_snapshot.py @@ -11,9 +11,7 @@ from openhcs.core.runtime_source_binding_cache import RuntimeSourceBindingContextCache from openhcs.core.source_binding_selection import ( DeclaredSourceMetadataRecord, - ResolvedSourceMetadataRecord, SourceBindingMatchedImageSet, - SourceMetadataRecord, SourcePatternResolutionContext, ) from openhcs.core.source_bindings import ( @@ -22,7 +20,10 @@ SourceBindingRuntimeContext, ) from openhcs.core.source_matching import SourceImageSetIdentityPolicy -from openhcs.core.source_metadata import ORIGINAL_SOURCE_METADATA_FIELD +from openhcs.core.source_metadata import ( + ORIGINAL_SOURCE_METADATA_FIELD, + ResolvedSourceMetadataRecord, +) from openhcs.core.source_projection import OpenHCSPlaneAddress, SourcePlaneProjection from openhcs.core.source_workspace_projection import VirtualWorkspaceSourceProjection from openhcs.microscopes.microscope_interfaces import FilenameParseResult @@ -72,7 +73,7 @@ def snapshot(cache, source_projection, parser, rules=()): def test_declared_record_resolves_live_parser_and_nested_metadata(): nested = {"Plate": "before"} - record = SourceMetadataRecord.from_mapping( + record = DeclaredSourceMetadataRecord.from_mapping( {ORIGINAL_SOURCE_METADATA_FIELD: nested, "site": 8} ) assert isinstance(record, DeclaredSourceMetadataRecord) @@ -218,19 +219,19 @@ def test_snapshot_rejects_nested_values_outside_existing_metadata_grammar(): def test_declared_and_resolved_records_preserve_family_value_identity(): - declared = SourceMetadataRecord.from_mapping({"site": 1, "well": "A01"}) - resolved = ResolvedSourceMetadataRecord.from_resolved_mapping( - {"site": 1, "well": "A01"} - ) + declared = DeclaredSourceMetadataRecord.from_mapping({"site": 1, "well": "A01"}) + resolved = ResolvedSourceMetadataRecord.from_mapping({"site": 1, "well": "A01"}) assert declared == resolved assert hash(declared) == hash(resolved) assert hash(declared) == hash((declared.fields,)) - assert declared != SourceMetadataRecord.from_mapping({"site": 2, "well": "A01"}) + assert declared != DeclaredSourceMetadataRecord.from_mapping( + {"site": 2, "well": "A01"} + ) nested = {ORIGINAL_SOURCE_METADATA_FIELD: {"Plate": "A"}} with pytest.raises(TypeError): - hash(SourceMetadataRecord.from_mapping(nested)) + hash(DeclaredSourceMetadataRecord.from_mapping(nested)) with pytest.raises(TypeError): - hash(ResolvedSourceMetadataRecord.from_resolved_mapping(nested)) + hash(ResolvedSourceMetadataRecord.from_mapping(nested)) def test_snapshot_owns_position_and_projection_map_views(): @@ -302,3 +303,30 @@ def test_warmed_cache_transport_reconstructs_all_derived_defaults(context_owned) rebuilt = snapshot(restored, source_projection, parser) assert rebuilt is not context assert rebuilt.metadata_for_path(PATH) == context.metadata_for_path(PATH) + + +def test_durable_workspace_mapping_still_runs_selector_parser_and_rule_fallbacks(): + from openhcs.core.source_metadata import DurableSourceMetadata, SourceMetadataRecord + + metadata = DurableSourceMetadata.from_mapping({"site": 8, "literal": "kept"}) + parser = CountingParser() + context = SourcePatternResolutionContext.from_sources( + parser=parser, + source_paths_by_virtual_path={PATH: "/source/image.tif"}, + source_metadata_by_path={PATH: metadata}, + metadata_rules=( + MetadataExtractionRule( + source=MetadataSource.FILE_NAME, pattern=r"(?PA01)" + ), + ), + ) + assert not isinstance(metadata, SourceMetadataRecord) + assert isinstance( + context.source_metadata_by_path[PATH], DeclaredSourceMetadataRecord + ) + resolved = context.metadata_for_path(PATH) + assert resolved["site"] == 8 + assert resolved["well"] == "A01" + assert resolved["literal"] == "kept" + assert resolved[ORIGINAL_SOURCE_METADATA_FIELD]["Plate"] == "A01" + assert parser.calls == [PATH] diff --git a/tests/unit/test_source_binding_workspace.py b/tests/unit/test_source_binding_workspace.py index 32436c2527..94c7c0c6c5 100644 --- a/tests/unit/test_source_binding_workspace.py +++ b/tests/unit/test_source_binding_workspace.py @@ -12,7 +12,7 @@ from openhcs.core.source_binding_workspace import SourceBindingWorkspaceProjector from openhcs.core.runtime_tabular_values import FieldSpec from openhcs.core.source_metadata import ( - SourceMetadataRoleView, + SourceMetadataFields, SourceVoxelSpacing, SourceVoxelSpacingUnit, SOURCE_VOXEL_SPACING_FIELD, @@ -330,9 +330,7 @@ def test_source_binding_workspace_projector_assigns_selector_channels(tmp_path): } assert all("image_type" not in source for source in serialized_sources) filter_paths_by_alias = { - str(source["source_alias"]): SourceMetadataRoleView( - source - ).source_filter_paths() + str(source["source_alias"]): SourceMetadataFields.source_filter_paths(source) for source in serialized_sources } assert filter_paths_by_alias == { @@ -386,7 +384,7 @@ def test_source_binding_workspace_projects_semantic_identity_after_raw_selection assert projection.address.value_for(AllComponents.CHANNEL) == "MCP_DNA" original_metadata = dict( - SourceMetadataRoleView(projection.source_metadata).original_items() + SourceMetadataFields.original_items(projection.source_metadata) ) assert original_metadata[AllComponents.CHANNEL.value] == "1" @@ -609,7 +607,7 @@ def test_group_address_preserves_distinct_source_well_as_literal_metadata(tmp_pa source_metadata = next(iter(metadata[FIELDS.SOURCE_METADATA].values())) assert source_metadata["well"] == "Sequence1" - assert dict(SourceMetadataRoleView(source_metadata).original_items()) == { + assert dict(SourceMetadataFields.original_items(source_metadata)) == { "Well": "A01", "FrameNumber": "0000", "Run": "Sequence1", @@ -788,7 +786,7 @@ def test_order_source_sets_join_imported_metadata_across_aliases(tmp_path): assert {projection.source_alias for projection in projections} == {"DNA", "Actin"} for projection in projections: original = dict( - SourceMetadataRoleView(projection.source_metadata).original_items() + SourceMetadataFields.original_items(projection.source_metadata) ) assert original["Compound"] == "DMSO" assert original["Dose"] == "0" @@ -912,7 +910,7 @@ def test_metadata_source_sets_propagate_imported_metadata(tmp_path): assert len(projections) == 2 assert all( - dict(SourceMetadataRoleView(item.source_metadata).original_items())["Compound"] + dict(SourceMetadataFields.original_items(item.source_metadata))["Compound"] == "DrugA" for item in projections ) @@ -958,7 +956,7 @@ def test_imported_metadata_duplicate_join_uses_first_matching_row(tmp_path): assert metadata["Site"] == "1" assert metadata["Compound"] == "First" - original = dict(SourceMetadataRoleView(metadata).original_items()) + original = dict(SourceMetadataFields.original_items(metadata)) assert original["Site"] == "1" assert original["Plate"] == "20585" assert original["Well"] == "A01" @@ -1006,7 +1004,7 @@ def test_imported_metadata_later_stage_overrides_extracted_field(tmp_path): assert metadata["Plate"] == "plate_1" assert metadata["Dose"] == "0" assert projection.address.value_for(AllComponents.WELL) == "A01" - original = dict(SourceMetadataRoleView(metadata).original_items()) + original = dict(SourceMetadataFields.original_items(metadata)) assert original["Plate"] == "plate_1" assert original["Well"] == "A01" assert original["ChannelNumber"] == "2" @@ -1058,7 +1056,7 @@ def test_imported_metadata_coerces_join_and_payload_values_through_declared_type filemanager=_filemanager(), ) projection = projection_set.projections[0] - original = dict(SourceMetadataRoleView(projection.source_metadata).original_items()) + original = dict(SourceMetadataFields.original_items(projection.source_metadata)) assert original["Site"] == 5 assert original["Dose"] == 0.25 @@ -1072,7 +1070,7 @@ def test_imported_metadata_coerces_join_and_payload_values_through_declared_type pixel_size=1.0, ) serialized = next(iter(metadata[FIELDS.SOURCE_METADATA].values())) - serialized_original = dict(SourceMetadataRoleView(serialized).original_items()) + serialized_original = dict(SourceMetadataFields.original_items(serialized)) assert serialized_original["Dose"] == 0.25 assert serialized_original["Frame"] == 7 @@ -1161,7 +1159,7 @@ def test_imported_metadata_skips_source_sets_with_partial_join_identity(tmp_path ) assert "Compound" not in dict( - SourceMetadataRoleView(projection.source_metadata).original_items() + SourceMetadataFields.original_items(projection.source_metadata) ) @@ -1285,7 +1283,7 @@ def test_source_bindings_reinitialization_resolves_current_imported_metadata(tmp ) ) assert ( - dict(SourceMetadataRoleView(first_metadata).original_items())["Compound"] + dict(SourceMetadataFields.original_items(first_metadata))["Compound"] == "First" ) @@ -1300,7 +1298,7 @@ def test_source_bindings_reinitialization_resolves_current_imported_metadata(tmp assert metadata_path.read_bytes() != first_payload assert ( - dict(SourceMetadataRoleView(second_metadata).original_items())["Compound"] + dict(SourceMetadataFields.original_items(second_metadata))["Compound"] == "Second" ) diff --git a/tests/unit/test_source_image_provenance_hotpath.py b/tests/unit/test_source_image_provenance_hotpath.py index bf3e167d50..552cdadf8e 100644 --- a/tests/unit/test_source_image_provenance_hotpath.py +++ b/tests/unit/test_source_image_provenance_hotpath.py @@ -14,7 +14,7 @@ def test_empty_explicit_provenance_reuses_authoritative_value() -> None: ) fields = SourceImageProvenanceFields(source_provenance=retained) - fields.absorb_explicit_source_provenance(SourceImageProvenance()) + fields.absorb_explicit_source_provenance((None, None, None, ())) assert fields.source_provenance is retained @@ -30,7 +30,9 @@ def test_populated_explicit_provenance_preserves_merge_precedence() -> None: ) fields = SourceImageProvenanceFields(source_provenance=fallback) - fields.absorb_explicit_source_provenance(explicit) + fields.absorb_explicit_source_provenance( + (None, {"channel": "2"}, None, ("Explicit",)) + ) assert fields.source_provenance == SourceImageProvenance( source_path="/input/A01_s001_w1.tif", diff --git a/tests/unit/test_source_metadata_live_mutation.py b/tests/unit/test_source_metadata_live_mutation.py index 19cf5086cd..935fc63373 100644 --- a/tests/unit/test_source_metadata_live_mutation.py +++ b/tests/unit/test_source_metadata_live_mutation.py @@ -3,7 +3,7 @@ import pytest from openhcs.constants.constants import AllComponents -from openhcs.core.source_binding_selection import SourceMetadataRecord +from openhcs.core.source_binding_selection import DeclaredSourceMetadataRecord from openhcs.core.source_matching import ( ORIGINAL_SOURCE_METADATA_FIELD, semantic_source_metadata_value, @@ -31,7 +31,7 @@ def test_metadata_queries_observe_scalar_mutation(readonly_outer): @pytest.mark.parametrize( - "view", (dict, MappingProxyType, SourceMetadataRecord.from_mapping) + "view", (dict, MappingProxyType, DeclaredSourceMetadataRecord.from_mapping) ) def test_literal_queries_observe_nested_original_mutation(view): original = {"Well": "LiteralA"} diff --git a/tests/unit/test_source_metadata_owner.py b/tests/unit/test_source_metadata_owner.py new file mode 100644 index 0000000000..451d5cbb7a --- /dev/null +++ b/tests/unit/test_source_metadata_owner.py @@ -0,0 +1,490 @@ +"""Owned metadata lifetime and its production provenance consumers.""" + +import pickle +from collections.abc import Mapping + +import cloudpickle +from types import MappingProxyType + +import numpy as np +import pytest +from polystore.virtual_workspace import SourcePixelRef + +from openhcs.constants.constants import AllComponents +from openhcs.core.artifacts import ImageArtifactType +from openhcs.core.runtime_image_values import ( + ImagePayloadMetadata, + image_payload_metadata, +) +from openhcs.core.source_binding_selection import DeclaredSourceMetadataRecord +from openhcs.core.source_bindings import ( + SOURCE_BINDING_ALIAS_METADATA_FIELD, + SourceBindingRuntimeContext, +) +from openhcs.core.source_image_provenance import ( + SourceImageIdentity, + SourceImageProvenance, + SourceImageProvenancePlaneRecord, + SourcePlaneIndexedMetadata, + source_component_metadata_consensus, +) +from openhcs.core.source_matching import ( + source_component_metadata_items, + source_component_metadata_value, + source_component_metadata_values, + source_metadata_value, + with_source_component_metadata, +) +from openhcs.core.source_metadata import ( + ORIGINAL_SOURCE_METADATA_FIELD, + DurableSourceMetadata, + ResolvedSourceMetadataRecord, + SourceMetadataRecord, + SourceMetadataFields, +) +from openhcs.core.source_projection import OpenHCSPlaneAddress, SourcePlaneProjection +from openhcs.core.steps.function_output_identity import FunctionOutputIdentity +from openhcs.core.source_workspace_projection import ( + VirtualWorkspaceImagePayloadProjection, + VirtualWorkspaceSourceProjection, +) +from openhcs.core.virtual_workspace_metadata import ( + VirtualWorkspaceSourceMetadataEntries, +) +from openhcs.serialization.json import to_jsonable + + +@pytest.mark.parametrize("owner", (ResolvedSourceMetadataRecord, DurableSourceMetadata)) +def test_owned_snapshot_detaches_nested_fields_and_preserves_record_contract(owner): + nested = {"Well": "literal"} + record = owner.from_mapping({"well": "A01", ORIGINAL_SOURCE_METADATA_FIELD: nested}) + nested["Well"] = "changed" + assert source_metadata_value(record, "Well") == "literal" + with pytest.raises(TypeError): + record[ORIGINAL_SOURCE_METADATA_FIELD]["Well"] = "changed" + with pytest.raises(TypeError): + hash(record) + declared = DeclaredSourceMetadataRecord.from_mapping( + {"well": "A01", ORIGINAL_SOURCE_METADATA_FIELD: {"Well": "literal"}} + ) + reversed_record = owner.from_mapping( + {ORIGINAL_SOURCE_METADATA_FIELD: {"Well": "literal"}, "well": "A01"} + ) + if isinstance(record, SourceMetadataRecord): + assert record == declared + assert record != reversed_record + else: + assert record != declared and declared != record + assert record == reversed_record + assert record == dict(declared) + + +def test_owned_lookup_keeps_first_duplicate_field_and_full_ordered_record(): + record = ResolvedSourceMetadataRecord((("well", "A01"), ("well", "A02"))) + assert record["well"] == "A01" + assert tuple(record.items()) == (("well", "A01"), ("well", "A01")) + assert record.fields == (("well", "A01"), ("well", "A02")) + assert dict(record) == {"well": "A01"} + assert record != ResolvedSourceMetadataRecord((("well", "A01"),)) + + +def test_runtime_birth_canonicalizes_but_durable_and_derived_values_keep_literal_spelling( + tmp_path, +): + spelling = str(tmp_path / "unused" / ".." / "image.tif") + durable = DurableSourceMetadata.from_mapping({"path": spelling}) + runtime = ResolvedSourceMetadataRecord.from_mapping(durable) + assert durable["path"] == spelling + assert runtime["path"] == str(tmp_path / "image.tif") + derived = SourceMetadataFields.with_fields(runtime, {"new_path": spelling}) + assert isinstance(derived, DurableSourceMetadata) + assert derived["path"] == str(tmp_path / "image.tif") + assert derived["new_path"] == spelling + projection = SourcePlaneProjection( + address=OpenHCSPlaneAddress.from_values( + well="A01", site="1", channel="1", z_index="1", timepoint="1" + ), + ref=SourcePixelRef("disk", "plane.tif"), + source_metadata=derived, + ) + assert projection.source_metadata["new_path"] == str(tmp_path / "image.tif") + + +def test_owned_scalar_identity_reuses_literal_metadata_but_not_mutable_carriers(): + fields = {"well": "A01", "channel": "1", "extension": ".tif"} + owner = DurableSourceMetadata.from_mapping(fields) + original = ImagePayloadMetadata( + source_provenance=SourceImageProvenance( + source_path="/input/source.tif", + source_component_metadata=owner, + source_image_names=("DNA",), + ) + ) + first = original.replace_fields() + second = original.with_source_context_from(original) + assert first.source_provenance is not original.source_provenance + assert ( + first.source_provenance.source_identity + is not original.source_provenance.source_identity + ) + assert first.source_component_metadata is owner + assert second.source_component_metadata is owner + assert ( + first.source_provenance.equality_identity + == original.source_provenance.equality_identity + ) + assert to_jsonable(first) == to_jsonable(original) + first.source_provenance.source_identity.path = "/changed.tif" + assert original.source_path == "/input/source.tif" + assert second.source_path == "/input/source.tif" + + +def test_workspace_alias_projection_and_artifact_naming_preserve_owned_metadata(): + source = VirtualWorkspaceSourceMetadataEntries.normalize_metadata_fields( + {"well": "A01", "channel": "1", SOURCE_BINDING_ALIAS_METADATA_FIELD: "DNA"} + ) + payload = ImagePayloadMetadata().payload_with(np.ones((2, 2), dtype=np.float32)) + projected = VirtualWorkspaceImagePayloadProjection( + source_metadata=source, source_alias="DNA" + ).apply(payload) + metadata = image_payload_metadata(projected) + assert isinstance(metadata.source_component_metadata, DurableSourceMetadata) + assert SOURCE_BINDING_ALIAS_METADATA_FIELD not in metadata.source_component_metadata + named = ImageArtifactType.normalize_runtime_payload("DerivedDNA", projected) + named_metadata = image_payload_metadata(named) + assert ( + named_metadata.source_component_metadata is metadata.source_component_metadata + ) + assert named_metadata.source_image_names == ("DerivedDNA",) + assert metadata.source_image_names == ("DNA",) + assert ( + named_metadata.source_provenance.source_image_provenance_planes.represented_source_image_names + == ("DNA",) + ) + + +def test_owned_updates_replace_aliases_and_keep_original_literals(): + owner = DurableSourceMetadata.from_mapping( + { + "channel": "1", + "ChannelNumber": "2", + ORIGINAL_SOURCE_METADATA_FIELD: {"ChannelNumber": "literal"}, + } + ) + updated = with_source_component_metadata(owner, AllComponents.CHANNEL, "3") + assert isinstance(updated, DurableSourceMetadata) + assert "ChannelNumber" not in updated + assert source_metadata_value(updated, "ChannelNumber") == "literal" + assert source_component_metadata_values(updated, AllComponents.CHANNEL) == ("3",) + assert owner["channel"] == "1" + + +@pytest.mark.parametrize("owner", (ResolvedSourceMetadataRecord, DurableSourceMetadata)) +def test_custom_scalar_remains_live_for_queries_and_new_fingerprints(owner): + class MutableInteger(int): + def __str__(self): + return self.tag + + def __repr__(self): + return self.tag + + value = MutableInteger(1) + value.tag = "before" + record = owner.from_mapping({"channel": value}) + first = SourceImageIdentity(component_metadata=record) + assert source_component_metadata_value(record, AllComponents.CHANNEL) == "before" + value.tag = "after" + assert source_component_metadata_value(record, AllComponents.CHANNEL) == "after" + assert SourceImageIdentity(component_metadata=record).identity != first.identity + assert first.identity[1] == (("channel", "before"),) + + +def test_lazy_role_errors_are_unchanged_by_owned_construction(): + record = DurableSourceMetadata.from_mapping( + { + "channel": "1", + "wellrow": "A", + "wellcolumn": "bad", + ORIGINAL_SOURCE_METADATA_FIELD: 7, + } + ) + assert source_component_metadata_value(record, AllComponents.CHANNEL) == "1" + with pytest.raises(ValueError): + source_component_metadata_value(record, AllComponents.WELL) + with pytest.raises(RuntimeError, match="must be a mapping"): + source_metadata_value(record, "channel") + with pytest.raises(RuntimeError, match="must be a mapping"): + source_component_metadata_values(record, AllComponents.CHANNEL) + + +@pytest.mark.parametrize("invalid", ([1], {"deep": {"unsupported": 1}}, object())) +def test_durable_metadata_keeps_existing_invalid_value_domain(invalid): + with pytest.raises(RuntimeError): + VirtualWorkspaceSourceMetadataEntries.normalize_metadata_fields( + {"field": invalid} + ) + + +def test_source_identity_mapping_equality_keeps_original_class_and_current_field_boundaries(): + owner = DurableSourceMetadata.from_mapping({"flag": True}) + identity = SourceImageIdentity("source.tif", owner) + raw = SourceImageIdentity("source.tif", MappingProxyType({"flag": True})) + assert identity == raw and raw == identity + assert identity != SourceImageIdentity("source.tif", {"flag": 1}) + assert SourceImageIdentity(component_metadata=None) != SourceImageIdentity( + component_metadata={} + ) + with pytest.raises(TypeError): + hash(identity) + + class Child(SourceImageIdentity): + pass + + assert identity.__eq__(Child("source.tif", owner)) is NotImplemented + + class ReportedIdentity: + @property + def __class__(self): + return SourceImageIdentity + + def __getattribute__(self, name): + if name in ("path", "component_metadata", "_identity"): + reads.append(name) + return getattr(raw, name) + return object.__getattribute__(self, name) + + reads = [] + assert identity.__eq__(ReportedIdentity()) is True + assert reads == ["path", "component_metadata", "_identity"] + + nested = {"literal": "before"} + left = SourceImageIdentity( + component_metadata={ORIGINAL_SOURCE_METADATA_FIELD: nested} + ) + right = SourceImageIdentity( + component_metadata={ORIGINAL_SOURCE_METADATA_FIELD: {"literal": "before"}} + ) + captured = left.identity + assert left == right + nested["literal"] = "after" + assert left != right and left.identity == captured + + +def test_raw_composition_snapshots_once_before_consensus_reads(): + class ObservableMapping(Mapping): + def __init__(self, values): + self.values = values + self.key_reads = self.value_reads = 0 + + def __iter__(self): + self.key_reads += 1 + return iter(self.values) + + def __len__(self): + return len(self.values) + + def __getitem__(self, key): + self.value_reads += 1 + return self.values[key] + + raw = ObservableMapping({"well": "A01"}) + consensus = source_component_metadata_consensus((raw, raw)) + assert dict(consensus) == {"well": "A01"} + assert raw.key_reads == 2 and raw.value_reads == 2 + snapshot = SourceMetadataFields.composition_snapshot(raw) + raw.values["well"] = "A02" + assert snapshot["well"] == "A01" + assert source_component_metadata_consensus((raw, raw))["well"] == "A02" + + +@pytest.mark.parametrize("owner", (ResolvedSourceMetadataRecord, DurableSourceMetadata)) +@pytest.mark.parametrize("serializer", (pickle, cloudpickle)) +def test_owned_transport_stores_only_authoritative_fields_and_rebuilds_local_views( + owner, serializer +): + record = owner.from_mapping( + {"well": "A01", ORIGINAL_SOURCE_METADATA_FIELD: {"literal": "value"}} + ) + before = SourceImageIdentity(component_metadata=record).identity + assert source_metadata_value(record, "literal") == "value" + assert source_component_metadata_value(record, AllComponents.WELL) == "A01" + assert record._views + restored = serializer.loads(serializer.dumps(record)) + assert isinstance(restored, owner) + assert restored.fields == record.fields + assert not restored._views + assert restored.metadata_contents is not record.metadata_contents + assert to_jsonable(restored) == to_jsonable(record) + assert SourceImageIdentity(component_metadata=restored).identity == before + with pytest.raises(TypeError): + restored[ORIGINAL_SOURCE_METADATA_FIELD]["literal"] = "changed" + + +@pytest.mark.parametrize("owner", (ResolvedSourceMetadataRecord, DurableSourceMetadata)) +def test_transport_does_not_recanonicalize_stored_absolute_values_or_validate_unused_roles( + owner, tmp_path +): + directory = tmp_path / "original" + directory.mkdir() + spelling = str(directory / "image.tif") + record = owner.from_mapping({"path": spelling, ORIGINAL_SOURCE_METADATA_FIELD: 7}) + before = SourceImageIdentity(component_metadata=record).identity + directory.rmdir() + target = tmp_path / "replacement" + target.mkdir() + directory.symlink_to(target, target_is_directory=True) + assert str((directory / "image.tif").resolve()) != spelling + restored = pickle.loads(pickle.dumps(record)) + assert restored["path"] == spelling + assert SourceImageIdentity(component_metadata=restored).identity == before + with pytest.raises(RuntimeError, match="must be a mapping"): + source_metadata_value(restored, "unused") + + +def test_ordered_component_batch_matches_sequential_raw_updates(): + fields = { + "Well": "old", + "Site": "old", + "ChannelNumber": "old", + "ZIndex": "old", + "Timepoint": "old", + ORIGINAL_SOURCE_METADATA_FIELD: {"Well": "literal"}, + } + components = tuple( + (component, str(index)) for index, component in enumerate(AllComponents, 1) + ) + raw = dict(fields) + for component, value in components: + raw = with_source_component_metadata(raw, component, value) + record = DurableSourceMetadata.from_mapping(fields) + updated = SourceMetadataFields.with_fields(record, {}, components=components) + assert isinstance(updated, DurableSourceMetadata) + assert to_jsonable(updated) == to_jsonable(raw) + assert tuple(updated) == tuple(raw) + assert source_metadata_value(updated, "Well") == "literal" + + +@pytest.mark.parametrize("existing_extension", (False, True)) +def test_output_identity_batch_keeps_original_field_order_and_extension_precedence( + existing_extension, +): + fields = {"literal": "kept", "Well": "old"} + if existing_extension: + fields["extension"] = ".old" + identity = FunctionOutputIdentity( + component_values={"well": "A01", "channel": "1"}, + extension=".tif", + source="control", + ) + expected = dict(fields) + expected.update(identity.component_values) + for component, value in source_component_metadata_items(identity.component_values): + expected = with_source_component_metadata(expected, component, value) + expected["extension"] = ".tif" + actual = identity.component_metadata(DurableSourceMetadata.from_mapping(fields)) + assert tuple(actual.items()) == tuple(expected.items()) + assert to_jsonable(actual) == expected + + +def test_duplicate_record_keys_collapse_at_source_identity_mapping_boundary(): + record = ResolvedSourceMetadataRecord((("well", "A01"), ("well", "A02"))) + expected = SourceImageIdentity(component_metadata=dict(record)) + actual = SourceImageIdentity(component_metadata=record) + assert actual == expected + assert actual.identity == expected.identity + assert tuple(actual.component_metadata.items()) == (("well", "A01"),) + assert record.fields == (("well", "A01"), ("well", "A02")) + + +def test_stringified_key_collisions_keep_platform_map_last_value_at_owned_birth(): + fields = {1: "first", "1": "last"} + durable = VirtualWorkspaceSourceMetadataEntries.normalize_metadata_fields(fields) + assert tuple(durable.items()) == (("1", "last"),) + projection = SourcePlaneProjection( + address=OpenHCSPlaneAddress.from_values( + well="A01", site="1", channel="1", z_index="1", timepoint="1" + ), + ref=SourcePixelRef("disk", "plane.tif"), + source_metadata=fields, + ) + assert tuple(projection.source_metadata.items()) == (("1", "last"),) + declaration = DeclaredSourceMetadataRecord.from_mapping(fields) + assert declaration.fields == (("1", "first"), ("1", "last")) + assert declaration["1"] == "first" + + +@pytest.mark.parametrize( + "decoder", + ( + ResolvedSourceMetadataRecord.from_mapping, + DurableSourceMetadata.from_mapping, + VirtualWorkspaceSourceMetadataEntries.normalize_metadata_fields, + ), +) +def test_owned_map_birth_validates_overwritten_stringified_key_values(decoder): + with pytest.raises((TypeError, RuntimeError)): + decoder({1: [], "1": "valid"}) + + +def test_mapping_and_ordered_record_equality_namespaces_stay_separate(): + ordered = ResolvedSourceMetadataRecord.from_mapping({"well": "A01", "site": "1"}) + reordered = ResolvedSourceMetadataRecord.from_mapping({"site": "1", "well": "A01"}) + mapping = DurableSourceMetadata.from_mapping(ordered) + reordered_mapping = DurableSourceMetadata.from_mapping(reordered) + assert ordered != reordered + assert mapping == reordered_mapping + assert mapping == dict(ordered) and dict(ordered) == mapping + for record in ( + ordered, + reordered, + DeclaredSourceMetadataRecord.from_mapping(dict(ordered)), + ): + assert mapping != record and record != mapping + assert hash(ordered) == hash( + DeclaredSourceMetadataRecord.from_mapping(dict(ordered)) + ) + with pytest.raises(TypeError): + hash(mapping) + + +@pytest.mark.parametrize( + "factory", + ( + lambda metadata: VirtualWorkspaceSourceMetadataEntries({"plane.tif": metadata}), + lambda metadata: VirtualWorkspaceImagePayloadProjection( + source_metadata=metadata + ), + lambda metadata: VirtualWorkspaceSourceProjection({}, {"plane.tif": metadata}), + lambda metadata: SourceImageProvenancePlaneRecord(component_metadata=metadata), + lambda metadata: SourcePlaneIndexedMetadata(metadata, 0, 1), + lambda metadata: SourceBindingRuntimeContext( + source_metadata_by_path={"plane.tif": metadata} + ), + lambda metadata: SourcePlaneProjection( + address=OpenHCSPlaneAddress.from_values( + well="A01", site="1", channel="1", z_index="1", timepoint="1" + ), + ref=SourcePixelRef("disk", "plane.tif"), + source_metadata=metadata, + ), + ), +) +def test_public_mapping_field_carriers_keep_content_equality(factory): + owner = DurableSourceMetadata.from_mapping({"well": "A01", "site": "1"}) + reordered = DurableSourceMetadata.from_mapping({"site": "1", "well": "A01"}) + raw = MappingProxyType({"well": "A01", "site": "1"}) + assert factory(owner) == factory(reordered) + assert factory(owner) == factory(raw) + assert factory(raw) == factory(owner) + with pytest.raises(TypeError): + hash(factory(owner)) + + +def test_durable_tuple_birth_uses_mapping_last_value_policy(): + metadata = DurableSourceMetadata((("well", "A01"), ("well", "A02"))) + assert metadata.fields == (("well", "A02"),) + assert metadata == {"well": "A02"} + assert ( + SourceImageIdentity(component_metadata=metadata).identity + == SourceImageIdentity(component_metadata={"well": "A02"}).identity + ) diff --git a/tests/unit/test_source_projection.py b/tests/unit/test_source_projection.py index a2a590d50a..5871a9745f 100644 --- a/tests/unit/test_source_projection.py +++ b/tests/unit/test_source_projection.py @@ -11,7 +11,7 @@ from openhcs.core.source_bindings import SourceProjectionRole from openhcs.core.source_metadata import ( ORIGINAL_SOURCE_METADATA_FIELD, - SourceMetadataRoleView, + SourceMetadataFields, ) from openhcs.core.source_projection import ( OpenHCSPlaneAddress, @@ -269,7 +269,7 @@ def test_source_projection_preserves_provenance_owned_component_remaps( assert source_metadata[component.value] == address_value assert ( - dict(SourceMetadataRoleView(source_metadata).original_items())[component.value] + dict(SourceMetadataFields.original_items(source_metadata))[component.value] == source_value ) From d2ae15940246ca0fe143c5300ea4b1f3a395f2ec Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Thu, 1 Oct 2026 19:34:14 -0400 Subject: [PATCH 025/235] refactor(metadata): derive nullable scalar grammar from primitive contract --- openhcs/core/source_metadata.py | 7 +- tests/unit/test_source_metadata_owner.py | 119 ++++++++++++++++++++++- 2 files changed, 122 insertions(+), 4 deletions(-) diff --git a/openhcs/core/source_metadata.py b/openhcs/core/source_metadata.py index cdc7d47641..2b819bb0e9 100644 --- a/openhcs/core/source_metadata.py +++ b/openhcs/core/source_metadata.py @@ -30,7 +30,8 @@ SOURCE_VOXEL_SPACING_FIELD = "OpenHCSSourceVoxelSpacingZYX" SOURCE_VOXEL_SPACING_UNIT_FIELD = "OpenHCSSourceVoxelSpacingUnit" -SourceMetadataScalar: TypeAlias = str | int | float | bool | None +SourceMetadataNonNullScalar: TypeAlias = str | int | float | bool +SourceMetadataScalar: TypeAlias = SourceMetadataNonNullScalar | None SourceMetadataValue: TypeAlias = ( SourceMetadataScalar | Mapping[str, SourceMetadataScalar] ) @@ -499,7 +500,7 @@ def __eq__(self, other: object) -> bool: @staticmethod def normalized_scalar(value: SourceMetadataScalar) -> SourceMetadataScalar: - if value is None or isinstance(value, (str, int, float, bool)): + if value is None or isinstance(value, SourceMetadataNonNullScalar): return value if isinstance(value, Mapping) or ( isinstance(value, Sequence) and not isinstance(value, str) @@ -547,7 +548,7 @@ def source_metadata_scalar(value: SourceMetadataScalar) -> SourceMetadataScalar: if value is None: return None - if not isinstance(value, (str, int, float, bool)): + if not isinstance(value, SourceMetadataNonNullScalar): raise TypeError( "Source metadata scalar values must be str, int, float, bool, or None, " f"got {type(value).__name__}." diff --git a/tests/unit/test_source_metadata_owner.py b/tests/unit/test_source_metadata_owner.py index 451d5cbb7a..a092129dfd 100644 --- a/tests/unit/test_source_metadata_owner.py +++ b/tests/unit/test_source_metadata_owner.py @@ -1,7 +1,7 @@ """Owned metadata lifetime and its production provenance consumers.""" import pickle -from collections.abc import Mapping +from collections.abc import Mapping, Sequence import cloudpickle from types import MappingProxyType @@ -41,6 +41,7 @@ ResolvedSourceMetadataRecord, SourceMetadataRecord, SourceMetadataFields, + source_metadata_scalar, ) from openhcs.core.source_projection import OpenHCSPlaneAddress, SourcePlaneProjection from openhcs.core.steps.function_output_identity import FunctionOutputIdentity @@ -488,3 +489,119 @@ def test_durable_tuple_birth_uses_mapping_last_value_policy(): SourceImageIdentity(component_metadata=metadata).identity == SourceImageIdentity(component_metadata={"well": "A02"}).identity ) + + +def test_scalar_admission_preserves_primitives_subclasses_and_scalar_container_precedence(): + class Integer(int): + pass + + class Real(float): + pass + + class TextMapping(str, Mapping): + pass + + values = ( + None, + False, + True, + 0, + -3, + 1.25, + "literal", + Integer(7), + Real(2.5), + TextMapping("literal"), + ) + for value in values: + assert DurableSourceMetadata.normalized_scalar(value) is value + assert source_metadata_scalar(value) is value + assert isinstance(values[-1], Mapping) + assert isinstance(values[-1], Sequence) + + +def test_durable_scalar_admission_preserves_rejected_container_and_scalar_errors(): + class MappingSequence(Mapping, Sequence): + def __getitem__(self, key): + raise KeyError(key) + + def __iter__(self): + return iter(()) + + def __len__(self): + return 0 + + container_error = ( + "virtual_workspace source metadata supports scalar values and " + "one-level scalar mappings only." + ) + scalar_error = ( + "virtual_workspace source metadata scalar values must be strings, " + "numbers, booleans, or null." + ) + for value in ( + {}, + [], + (), + range(1), + b"bytes", + bytearray(b"bytes"), + MappingSequence(), + ): + with pytest.raises(RuntimeError) as caught: + DurableSourceMetadata.normalized_scalar(value) + assert str(caught.value) == container_error + for value in (set(), frozenset(), 1j, object()): + with pytest.raises(RuntimeError) as caught: + DurableSourceMetadata.normalized_scalar(value) + assert str(caught.value) == scalar_error + + +def test_scalar_admission_keeps_none_identity_and_reported_class_read_order(): + class ReportedClass: + def __init__(self, reports): + self.reports = reports + self.reads = [] + + @property + def __class__(self): + reported = self.reports[min(len(self.reads), len(self.reports) - 1)] + self.reads.append(reported) + return reported + + reported_none = ReportedClass((type(None),)) + with pytest.raises(RuntimeError) as caught: + DurableSourceMetadata.normalized_scalar(reported_none) + assert str(caught.value) == ( + "virtual_workspace source metadata scalar values must be strings, " + "numbers, booleans, or null." + ) + assert reported_none.reads == [type(None)] * 6 + + reported_none = ReportedClass((type(None),)) + with pytest.raises(TypeError, match="Source metadata scalar values must be"): + source_metadata_scalar(reported_none) + assert reported_none.reads == [type(None)] * 4 + + reported_integer = ReportedClass((object, int, object)) + assert DurableSourceMetadata.normalized_scalar(reported_integer) is reported_integer + assert reported_integer.reads == [object, int] + + changing_class = ReportedClass( + (object, object, object, object, object, Sequence, str) + ) + with pytest.raises(RuntimeError) as caught: + DurableSourceMetadata.normalized_scalar(changing_class) + assert str(caught.value) == ( + "virtual_workspace source metadata scalar values must be strings, " + "numbers, booleans, or null." + ) + assert changing_class.reads == [ + object, + object, + object, + object, + object, + Sequence, + str, + ] From fbb7907c6c5ca7e80179bfffaf03e06bf951644a Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Thu, 1 Oct 2026 19:43:40 -0400 Subject: [PATCH 026/235] chore: drop unrelated output identity formatting from parked metadata work --- .../core/steps/function_output_identity.py | 28 ++++++------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/openhcs/core/steps/function_output_identity.py b/openhcs/core/steps/function_output_identity.py index c606b20dd5..a9775369ec 100644 --- a/openhcs/core/steps/function_output_identity.py +++ b/openhcs/core/steps/function_output_identity.py @@ -320,9 +320,7 @@ def _qualified_filename( if qualifier is None: return filename if not filename.endswith(extension): - raise ValueError( - "Constructed filename does not retain its declared extension." - ) + raise ValueError("Constructed filename does not retain its declared extension.") return f"{filename[:-len(extension)]}_{qualifier}{extension}" @staticmethod @@ -588,10 +586,8 @@ def _identity_from_metadata_uncached( identity = cls._identity_from_metadata( metadata.source_component_metadata, extension=FunctionOutputExtensionAuthority.from_source( - metadata.source_component_metadata, - metadata.source_path, - parser=parser, - identity_cache=identity_cache, + metadata.source_component_metadata, metadata.source_path, + parser=parser, identity_cache=identity_cache, ), source="payload component metadata", ) @@ -645,10 +641,8 @@ def _identity_from_metadata_uncached( identity = cls._identity_from_metadata( source_identity.component_metadata, extension=FunctionOutputExtensionAuthority.from_source( - source_identity.component_metadata, - source_identity.path, - parser=parser, - identity_cache=identity_cache, + source_identity.component_metadata, source_identity.path, + parser=parser, identity_cache=identity_cache, ), source="single represented payload source metadata", ) @@ -706,10 +700,8 @@ def filename_identity_from_metadata( identity = cls._identity_from_metadata( metadata.source_component_metadata, extension=FunctionOutputExtensionAuthority.from_source( - metadata.source_component_metadata, - metadata.source_path, - parser=parser, - identity_cache=identity_cache, + metadata.source_component_metadata, metadata.source_path, + parser=parser, identity_cache=identity_cache, ), source="payload component metadata", ) @@ -908,10 +900,8 @@ def _source_stack_source_identity( identity = cls._identity_from_metadata( source_identity.component_metadata, extension=FunctionOutputExtensionAuthority.from_source( - source_identity.component_metadata, - source_identity.path, - parser=parser, - identity_cache=identity_cache, + source_identity.component_metadata, source_identity.path, + parser=parser, identity_cache=identity_cache, ), source=f"represented source identity {identity_index} metadata", ) From 1087c9330f368b6d6b9928b97f0eb6d9d5c85d2f Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Thu, 1 Oct 2026 21:03:33 -0400 Subject: [PATCH 027/235] Document measured payoff bounds and rejected standalone routes for #384 --- .../shared_runtime_plumbing_384_20261001.rst | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/docs/validation/shared_runtime_plumbing_384_20261001.rst b/docs/validation/shared_runtime_plumbing_384_20261001.rst index d1ca5d920c..ae18505207 100644 --- a/docs/validation/shared_runtime_plumbing_384_20261001.rst +++ b/docs/validation/shared_runtime_plumbing_384_20261001.rst @@ -331,3 +331,53 @@ Pipeline clocks exclude ZMQ server startup, mandatory registry/kernel prewarming and shutdown. Timed runs use the ordinary public OUTCOMES path and default memory observer, without profiling injection. Candidate and baseline must have frozen clean source revisions and the same dependencies, affinity and environment. + +Global lifetime counterfactuals: rejected routes +---------------------------------------------- + +The next ordinary diagnostics and exact saved-input replays reject several +insufficient standalone routes before further implementation. These are payoff +bounds, not candidate speedups. Their observations remain revision-pinned; they +do not supersede the mixed ordinary ABBA above. + +* The eight complete saved IFC geometry owners take 0.4745 seconds in aggregate + median replay; the actual complete spreadsheet renderer takes 1.3662 seconds. + Even eliminating both separate phases has an optimistic ceiling below two + seconds. The isolated geometry/export route is stopped. +* On main 18317499be069fbe58387137f326ac7b6ff57988, all 634 original Advanced + measurement queries take 0.6422 seconds, while complete CPA collection takes + 1.1943 seconds. Fabricated missing-cell counts alone do not admit this route: + eliminating both whole phases still cannot supply two seconds. The saved + query capture is a bounded subset, not a substitute for the complete clock. +* On that same main revision, all 32 3D stack loads and all saves take 0.9927 + and 0.9526 exclusive diagnostic seconds respectively. Eighteen loads select + an actual previously stored named producer; all eighteen allocate independent + main-flow pixel buffers. The full saved step-14 Resize input matches the + stored Monolayer pixels exactly, but differs in filename-derived extension + and per-plane source-name metadata. Direct value reuse would violate existing + buffer isolation and provenance. A narrow named-handoff migration is stopped. +* A low-overhead aggregate diagnostic on that revision measures all 40,673 + ``ImagePayloadMetadata.__post_init__`` calls at 0.8919 inclusive seconds and + all 147,354 ``SourceImageIdentity.__post_init__`` calls at 0.5544 inclusive + seconds. These totals overlap and must never be added. Constructor count + contraction alone cannot justify the multi-second target, even though counts + identify repeated work. The proposed 44.6 percent birth reduction is not a + measured speedup. The wider field-derivation lifetime remains unqualified. + +Every diagnostic uses the ordinary OUTCOMES path, one worker/thread and the +default memory observer. Hook overhead is retained; server startup, prewarming, +compilation and shutdown are outside execution. Exact original-method timing +does not establish that the whole measured duration can be removed. Controllers, +source/dependency freezes, successful outputs and failed earlier captures are +retained under ``/var/tmp`` and the external benchmark evidence directory. + +Evidence: ``measurement-lifetime-actual-input-baseline-replay-20261001/receipt.json``, +``global-plumbing-three-workload-coarse-lifetime-20261001.json``, +``runtime-value-handoff-step14-fixture-metadata-inspection-20261001.json``, and +``metadata-initialization-aggregate-decision-20261001.json``. The full query and +handoff observations reside in their corresponding ``/var/tmp`` captures. + +Main 9a04107492ad90233394cf17524d0ca8e74062bb is normally integrated, including +the merged native-module source-discovery repair in PR416 and source projection +capability repair in PR418. Earlier behavior/source/performance gates retain +their exact source pins. PR394 remains a draft formally closing issue384. From e32b402af17a9baad3c51074e28dbcdd5cf23aa7 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Thu, 1 Oct 2026 21:12:52 -0400 Subject: [PATCH 028/235] Record complete field-derivation counterfactual scope for #384 --- .../shared_runtime_plumbing_384_20261001.rst | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/validation/shared_runtime_plumbing_384_20261001.rst b/docs/validation/shared_runtime_plumbing_384_20261001.rst index ae18505207..eae4701dee 100644 --- a/docs/validation/shared_runtime_plumbing_384_20261001.rst +++ b/docs/validation/shared_runtime_plumbing_384_20261001.rst @@ -381,3 +381,18 @@ Main 9a04107492ad90233394cf17524d0ca8e74062bb is normally integrated, including the merged native-module source-discovery repair in PR416 and source projection capability repair in PR418. Earlier behavior/source/performance gates retain their exact source pins. PR394 remains a draft formally closing issue384. + +The next bounded union-depth diagnostic on that main measures 3.6594 seconds +across selected metadata construction, provenance projection, component/literal +field lookup and merges. Its 972,354 selected calls enter 102,445 outer selected +regions; nested work is timed once and the final depth is zero. It retains hook +overhead, includes metadata inside callable wrappers and excludes physical +processing, I/O, complete writer preparation and publication ancestors. Thus +this is a plausible wider counterfactual scope, not 3.6594 recoverable seconds. +Reflection controls compare original and wrapped constructors: hints and errors +agree, including the original generated image-metadata constructor's existing +``NameError('InitVar')``. Actual provenance mapping decode succeeds through the +hooks. The next gate is an ordinary production comparison against the existing +nominal immutable-field owner, before further implementation or broad gates. +Evidence: ``metadata-lifetime-union-production-counterfactual-admission-20261001.json`` +and ``metadata-lifetime-union-reflection-pair-control-20261001.json``. From bffa1535ad440d4a0803b369727d737d16b6fce1 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Thu, 1 Oct 2026 21:41:04 -0400 Subject: [PATCH 029/235] Preserve metadata projection effect and validation order for #384 --- openhcs/core/runtime_image_values.py | 117 ++++++------- openhcs/core/source_image_provenance.py | 16 +- openhcs/core/source_metadata.py | 12 +- .../test_image_payload_projection_algebra.py | 163 ++++++++++++++++++ 4 files changed, 235 insertions(+), 73 deletions(-) diff --git a/openhcs/core/runtime_image_values.py b/openhcs/core/runtime_image_values.py index 9e9f03e92e..5963e79799 100644 --- a/openhcs/core/runtime_image_values.py +++ b/openhcs/core/runtime_image_values.py @@ -239,17 +239,33 @@ def for_array_payload( def __post_init__(self, *source_provenance_values: object) -> None: self.absorb_explicit_source_provenance(source_provenance_values) - self.source_voxel_spacing = self.source_voxel_spacing.with_missing_from( - SourceVoxelSpacing.from_source_metadata(self.source_component_metadata) + for name, value in self.normalized_fields({}).items(): + setattr(self, name, value) + + def normalized_fields(self, changes: Mapping[str, Any]) -> dict[str, Any]: + """Normalize constructor instructions in their source-context effect order.""" + provenance = changes.get("source_provenance", self.source_provenance) + spacing = changes.get("source_voxel_spacing", self.source_voxel_spacing) + spacing = spacing.with_missing_from( + SourceVoxelSpacing.from_source_metadata( + provenance.source_component_metadata + ) ) - self.normalize_source_provenance_fields() - self.normalize_source_spatial_domain_fields() - self.normalize_source_voxel_spacing_fields() + provenance = SourceImageProvenance.normalized(provenance) + domain = changes.get( + "source_spatial_domain", self.source_spatial_domain + ).normalized() + spacing = SourceVoxelSpacing.coerce(spacing) self.validate_source_channel_axis() - if self.plane_axis is not None: - self.plane_axis = RuntimePlaneAxis( - self.plane_axis, - ) + plane_axis = changes.get("plane_axis", self.plane_axis) + if plane_axis is not None: + plane_axis = RuntimePlaneAxis(plane_axis) + return dict( + source_provenance=provenance, + source_voxel_spacing=spacing, + source_spatial_domain=domain, + plane_axis=plane_axis, + ) def validate_source_channel_axis(self) -> None: """Validate the authored channel declaration before transforming axes.""" @@ -261,12 +277,6 @@ def validate_source_channel_axis(self) -> None: "ImagePayloadMetadata.source_channel_axis must be int or None." ) - def require_leading_plane_axis(self, message: str) -> RuntimePlaneAxis: - """Validate a required plane declaration at its metadata authority.""" - if self.plane_axis is None: - raise ValueError(message) - return RuntimePlaneAxis(self.plane_axis) - @property def has_values(self) -> bool: """Return whether this metadata carries any semantic image facts.""" @@ -1413,22 +1423,12 @@ class ImageMetadataProjection(ABC): def project(self) -> ImagePayloadMetadata: return self.metadata.replace_fields(**self.projected_fields()) - @property - def projected_source_provenance(self) -> SourceImageProvenance: - return self.metadata.source_provenance - - @property - def projected_unit_interval_intensity( - self, - ) -> ImageUnitIntervalIntensityMetadata | None: - return self.metadata.unit_interval_intensity - @abstractmethod def projected_fields(self) -> dict[str, Any]: """Derive constructor instructions, never an independently stored fact set.""" return dict( - source_provenance=self.projected_source_provenance, - unit_interval_intensity=self.projected_unit_interval_intensity, + source_provenance=self.metadata.source_provenance, + unit_interval_intensity=self.metadata.unit_interval_intensity, ) @@ -1438,25 +1438,11 @@ class SourcePlaneImageMetadataProjection(ImageMetadataProjection): plane_index: int - @property - def projected_source_provenance(self) -> SourceImageProvenance: - return super( - SourcePlaneImageMetadataProjection, self - ).projected_source_provenance.for_source_plane(self.plane_index) - - @property - def projected_unit_interval_intensity( - self, - ) -> ImageUnitIntervalIntensityMetadata | None: - intensity = super( - SourcePlaneImageMetadataProjection, self - ).projected_unit_interval_intensity - return ( - None if intensity is None else intensity.for_source_plane(self.plane_index) - ) - def projected_fields(self) -> dict[str, Any]: changes = super(SourcePlaneImageMetadataProjection, self).projected_fields() + changes["source_provenance"] = changes["source_provenance"].for_source_plane( + self.plane_index + ) changes.update( intensity_scale=self.metadata.intensity_scale_for_source_plane( self.plane_index @@ -1465,45 +1451,48 @@ def projected_fields(self) -> dict[str, Any]: self.metadata.source_plane_dtypes, self.plane_index ) or self.metadata.source_dtype, + unit_interval_intensity=( + None + if changes["unit_interval_intensity"] is None + else changes["unit_interval_intensity"].for_source_plane( + self.plane_index + ) + ), source_plane_intensity_scales=(), source_plane_dtypes=(), ) + changes.update(self.metadata.normalized_fields(changes)) return changes class LeadingPlaneAxisMetadataProjection(ImageMetadataProjection): """Remove a declared leading axis while preserving its source contributors.""" - @property - def projected_source_provenance(self) -> SourceImageProvenance: - return super().projected_source_provenance.with_runtime_planes_as_contributors() - - @property - def projected_unit_interval_intensity( - self, - ) -> ImageUnitIntervalIntensityMetadata | None: - intensity = super().projected_unit_interval_intensity - return None if intensity is None else intensity.without_source_planes() - def projected_fields(self) -> dict[str, Any]: - self.metadata.require_leading_plane_axis( - "Image metadata has no leading plane axis to remove." - ) + if self.metadata.plane_axis is None: + raise ValueError("Image metadata has no leading plane axis to remove.") + changes = super().projected_fields() source_channel_axis = self.metadata.source_channel_axis if source_channel_axis == 0: raise ValueError( "Image metadata cannot declare the same leading axis as both " "plane and channel." ) - self.metadata.validate_source_channel_axis() if source_channel_axis is not None and source_channel_axis > 0: source_channel_axis -= 1 - changes = super().projected_fields() + changes["source_provenance"] = changes[ + "source_provenance" + ].with_runtime_planes_as_contributors() + changes.update(self.metadata.normalized_fields(changes)) + intensity = changes["unit_interval_intensity"] changes.update( plane_axis=None, source_channel_axis=source_channel_axis, source_plane_intensity_scales=(), source_plane_dtypes=(), + unit_interval_intensity=( + None if intensity is None else intensity.without_source_planes() + ), ) return changes @@ -1515,10 +1504,10 @@ class LeadingSourcePlaneMetadataProjection( """Compose source selection and axis removal before constructing metadata.""" def projected_fields(self) -> dict[str, Any]: - self.metadata.require_leading_plane_axis( - "Leading source-plane projection requires a declared plane axis." - ) - self.metadata.validate_source_channel_axis() + if self.metadata.plane_axis is None: + raise ValueError( + "Leading source-plane projection requires a declared plane axis." + ) return super().projected_fields() diff --git a/openhcs/core/source_image_provenance.py b/openhcs/core/source_image_provenance.py index 025d77b87f..366a8d5fd4 100644 --- a/openhcs/core/source_image_provenance.py +++ b/openhcs/core/source_image_provenance.py @@ -702,6 +702,15 @@ def __init__( self.source_image_names, ) + def normalized(self) -> "SourceImageProvenance": + """Snapshot provenance through the constructor's concrete field policy.""" + return SourceImageProvenance( + self.source_path, + self.source_component_metadata, + self.source_image_provenance_planes, + self.source_image_names, + ) + @classmethod def from_init_values( cls, @@ -1788,11 +1797,8 @@ def absorb_explicit_source_provenance( self.source_provenance = explicit.with_missing_from(self.source_provenance) def normalize_source_provenance_fields(self) -> None: - self.source_provenance = SourceImageProvenance( - self.source_provenance.source_path, - self.source_provenance.source_component_metadata, - self.source_provenance.source_image_provenance_planes, - self.source_provenance.source_image_names, + self.source_provenance = SourceImageProvenance.normalized( + self.source_provenance ) def replace_fields(self, **changes: Any) -> Self: diff --git a/openhcs/core/source_metadata.py b/openhcs/core/source_metadata.py index 2b819bb0e9..cdc93922fd 100644 --- a/openhcs/core/source_metadata.py +++ b/openhcs/core/source_metadata.py @@ -798,6 +798,13 @@ def __post_init__(self) -> None: if not isinstance(self.unit, SourceVoxelSpacingUnit): raise TypeError("SourceVoxelSpacing.unit must be SourceVoxelSpacingUnit.") + @classmethod + def coerce( + cls, value: "SourceVoxelSpacing | Sequence[float]" + ) -> "SourceVoxelSpacing": + """Retain nominal spacing or validate authored coordinate values.""" + return value if isinstance(value, cls) else cls(tuple(value)) + @property def has_values(self) -> bool: return bool(self.values_zyx) @@ -960,10 +967,7 @@ class SourceVoxelSpacingFields: ) def normalize_source_voxel_spacing_fields(self) -> None: - if not isinstance(self.source_voxel_spacing, SourceVoxelSpacing): - self.source_voxel_spacing = SourceVoxelSpacing( - tuple(self.source_voxel_spacing) - ) + self.source_voxel_spacing = SourceVoxelSpacing.coerce(self.source_voxel_spacing) @lru_cache(maxsize=4096) diff --git a/tests/unit/test_image_payload_projection_algebra.py b/tests/unit/test_image_payload_projection_algebra.py index ef898d9870..23bbc68f1c 100644 --- a/tests/unit/test_image_payload_projection_algebra.py +++ b/tests/unit/test_image_payload_projection_algebra.py @@ -19,6 +19,7 @@ SourceImageProvenanceContributor, SourceImageProvenancePlanes, ) +from openhcs.core.source_spatial_domain import SourceSpatialDomain def _metadata(axis, channel_axis): @@ -152,3 +153,165 @@ def test_projection_observes_mask_and_metadata_mutation(): mask.resize((2, 2, 6), refcheck=False) with pytest.raises(ValueError, match="cannot be projected"): projector.payload_for_slice(data, 1) + + +def _metadata_with_observed_plane_fields(*, proof_raises=False): + events = [] + + class ObservedInt(int): + def __int__(self): + events.append("proof-int") + if proof_raises: + raise RuntimeError("proof conversion observed") + return 1 + + class ObservedTuple(tuple): + def __getitem__(self, index): + events.append("intensity-getitem") + return super().__getitem__(index) + + metadata = ImagePayloadMetadata( + plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + source_plane_intensity_scales=ObservedTuple((2.0,)), + unit_interval_intensity=ImageUnitIntervalIntensityMetadata( + source_plane_scales=(ObservedInt(1),) + ), + ) + return metadata, events + + +def test_plane_proof_failure_precedes_leading_channel_guard(): + metadata, events = _metadata_with_observed_plane_fields(proof_raises=True) + metadata.source_channel_axis = 0 + with pytest.raises(RuntimeError, match="proof conversion observed"): + metadata.for_leading_source_plane(0) + assert events == ["intensity-getitem", "proof-int"] + + +def test_plane_field_effects_precede_leading_channel_guard(): + metadata, events = _metadata_with_observed_plane_fields() + metadata.source_channel_axis = 0 + with pytest.raises(ValueError, match="both plane and channel"): + metadata.for_leading_source_plane(0) + assert events == ["intensity-getitem", "proof-int"] + + +def test_source_plane_preserves_intensity_then_proof_effect_order(): + metadata, events = _metadata_with_observed_plane_fields() + projected = metadata.for_source_plane(0) + assert events == ["intensity-getitem", "proof-int"] + assert projected.intensity_scale == 2.0 + assert projected.unit_interval_intensity.scale == 1 + + +def test_plane_proof_failure_precedes_mutated_channel_type_guard(): + metadata, events = _metadata_with_observed_plane_fields(proof_raises=True) + metadata.source_channel_axis = False + with pytest.raises(RuntimeError, match="proof conversion observed"): + metadata.for_leading_source_plane(0) + assert events == ["intensity-getitem", "proof-int"] + + +@pytest.mark.parametrize( + "operation,channel_axis,broken,error,message", + ( + ("for_leading_source_plane", 0, "domain", ValueError, "spatial_origin_yx"), + ("for_leading_source_plane", 0, "spacing", ValueError, "finite and positive"), + ( + "for_leading_source_plane", + 0, + "spacing-value", + AttributeError, + "with_missing_from", + ), + ("for_leading_source_plane", 0, "plane", ValueError, "invalid-axis"), + ("for_leading_source_plane", False, "domain", ValueError, "spatial_origin_yx"), + ( + "for_leading_source_plane", + False, + "spacing", + ValueError, + "finite and positive", + ), + ("for_leading_source_plane", False, "plane", TypeError, "must be int or None"), + ("for_source_plane", False, "domain", ValueError, "spatial_origin_yx"), + ("for_source_plane", False, "spacing", ValueError, "finite and positive"), + ( + "for_source_plane", + False, + "spacing-value", + AttributeError, + "with_missing_from", + ), + ("for_source_plane", False, "plane", TypeError, "must be int or None"), + ( + "without_leading_plane_axis", + 0, + "domain", + ValueError, + "both plane and channel", + ), + ( + "without_leading_plane_axis", + 0, + "spacing", + ValueError, + "both plane and channel", + ), + ( + "without_leading_plane_axis", + 0, + "spacing-value", + ValueError, + "both plane and channel", + ), + ( + "without_leading_plane_axis", + 0, + "plane", + ValueError, + "both plane and channel", + ), + ("without_leading_plane_axis", "1", "plane", TypeError, "not supported"), + ), +) +def test_projection_preserves_cross_invalid_constructor_guard_order( + operation, channel_axis, broken, error, message +): + metadata = ImagePayloadMetadata( + source_path="/tmp/source.tif", + plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + ) + metadata.source_channel_axis = 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" + with pytest.raises(error, match=message): + if operation == "without_leading_plane_axis": + metadata.without_leading_plane_axis() + else: + getattr(metadata, operation)(0) + + +def test_missing_leading_axis_precedes_plane_field_effects(): + metadata, events = _metadata_with_observed_plane_fields(proof_raises=True) + metadata.plane_axis = None + metadata.source_channel_axis = 0 + with pytest.raises(ValueError, match="requires a declared plane axis"): + metadata.for_leading_source_plane(0) + assert events == [] + + +def test_projection_spacing_failure_precedes_other_invalid_source_fields(): + metadata = ImagePayloadMetadata(plane_axis=RuntimePlaneAxis.RUNTIME_SLICE) + metadata.source_component_metadata = {"OpenHCSSourceVoxelSpacingZYX": "0,1,1"} + metadata.source_spatial_domain = SourceSpatialDomain(origin_yx=(1,)) + metadata.source_channel_axis = False + metadata.plane_axis = "invalid-axis" + with pytest.raises(ValueError, match="finite and positive"): + metadata.for_leading_source_plane(0) From 5bc21242352c08f17e6e9815d2a6dbb000141b3d Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Thu, 1 Oct 2026 21:43:31 -0400 Subject: [PATCH 030/235] Record complete-owner execution gain and cold compilation regression for #384 --- .../shared_runtime_plumbing_384_20261001.rst | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/docs/validation/shared_runtime_plumbing_384_20261001.rst b/docs/validation/shared_runtime_plumbing_384_20261001.rst index eae4701dee..b897c1393e 100644 --- a/docs/validation/shared_runtime_plumbing_384_20261001.rst +++ b/docs/validation/shared_runtime_plumbing_384_20261001.rst @@ -396,3 +396,60 @@ hooks. The next gate is an ordinary production comparison against the existing nominal immutable-field owner, before further implementation or broad gates. Evidence: ``metadata-lifetime-union-production-counterfactual-admission-20261001.json`` and ``metadata-lifetime-union-reflection-pair-control-20261001.json``. + +Initial ordinary complete-field-owner counterfactual +--------------------------------------------------- + +The existing immutable field-owner migration is now included in this draft, +rather than left on an unpublished independent branch. ``SourceMetadataFields`` +owns the common algorithms; live declared fields and owned resolved/durable +fields provide their actual lifetime through inheritance. Owned builtin values +permit local derived-view reuse, indexed lookup and readonly snapshot reuse. +Raw mutable mappings retain fresh derivation. The obsolete role/identity view +types and duplicate field algorithms are removed. Fields-only transport drops +process-local derived caches. + +One initial ordinary, uninstrumented 3D pair compares main +9a04107492ad90233394cf17524d0ca8e74062bb against the complete candidate +401cd69fa718d6b19ce4dc35fb5f7c2d70f2d219: + +=================== ============ ============ +Metric Main Candidate +=================== ============ ============ +Execution 11.6153 s 8.1170 s +Compilation 2.2212 s 50.7997 s +Pipeline total 14.5439 s 59.6670 s +=================== ============ ============ + +Execution improves by 3.4984 seconds in this initial pair; total time regresses +by 45.1231 seconds. All six actual result CSVs have exact rows and all 120 TIFF +arrays have exact dtype, shape and pixels, with equal complete inventories. +This is an initial counterfactual, not accepted ABBA or all-case native parity. + +The compilation regression is not startup time: 239 Numba cache files are written +after axis compilation, over 48.2219 seconds, ending immediately before compilation +completes. The cold candidate source-path kernel cache exposes a startup regression. +Commit 2b86bfbe49f36d302dcd67c5b0a0cb4f656f1eb0 implemented registry kernel warming; +ffba8426cefcaa87b6b00ff056e4cb96855233e4 in PR358 subsequently removed that loop +and deferred readiness to selected compilation. Restoring the existing registry +warmup lifecycle is tracked by issue162. No total-time improvement is claimed +until that repair is verified with fresh caches. + +The field-owner integration into this draft passes 81 scoped lifetime/ownership +controls with metaclass-registry 0.2.2. A subsequent projection correctness repair +passes 173 scoped controls in its isolated source. It restores source-plane +derivation and cross-invalid field/error ordering before leading-axis guards, +shares normalization on the existing metadata owner, and deletes premature +projection properties. That repair requires performance requalification; the +initial pair is not projected through it. Arbitrary external constructor/mapping +callback equivalence remains an OPEN obligation, not a claimed proof. + +Evidence: ``metadata-owner-ordinary-pair-output-parity-20261001.json``, +``metadata-owner-cold-compilation-regression-cause-20261001.json``, +``source-projection-order-384-correctness-patch-20261002.json``, +``/var/tmp/openhcs-metadata-owner-ordinary-pair-20261001/observations.json`` and +``/var/tmp/openhcs-integrated-metadata-owner-scoped-gates-20261002.log``. +Main 25d56ae3fb9b80acda80f3cf4e1c8667939147eb is normally merged; the shared +environment and PR submodule consume the declared published metaclass-registry +0.2.2 source 393a7e03003cdc56df9013f932ed4f26e632d77a. Earlier observations retain +their measured source/dependency pins. From 02609e12b41669b63c11526496eecadbb7fbc87c Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Thu, 1 Oct 2026 22:01:13 -0400 Subject: [PATCH 031/235] Record verified empty-cache startup readiness restoration Retain the cold compilation failure and distinguish the main lifecycle fix from integrated candidate performance. Refs #384 and #162. --- .../shared_runtime_plumbing_384_20261001.rst | 38 +++++++++++++++++-- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/docs/validation/shared_runtime_plumbing_384_20261001.rst b/docs/validation/shared_runtime_plumbing_384_20261001.rst index b897c1393e..4ed582c9e5 100644 --- a/docs/validation/shared_runtime_plumbing_384_20261001.rst +++ b/docs/validation/shared_runtime_plumbing_384_20261001.rst @@ -431,9 +431,9 @@ after axis compilation, over 48.2219 seconds, ending immediately before compilat completes. The cold candidate source-path kernel cache exposes a startup regression. Commit 2b86bfbe49f36d302dcd67c5b0a0cb4f656f1eb0 implemented registry kernel warming; ffba8426cefcaa87b6b00ff056e4cb96855233e4 in PR358 subsequently removed that loop -and deferred readiness to selected compilation. Restoring the existing registry -warmup lifecycle is tracked by issue162. No total-time improvement is claimed -until that repair is verified with fresh caches. +and deferred readiness to selected compilation. PR420 restores the existing registry warmup lifecycle and closes issue162. +Its separate empty-cache regression gate is recorded below; the integrated +field-owner performance still requires requalification. The field-owner integration into this draft passes 81 scoped lifetime/ownership controls with metaclass-registry 0.2.2. A subsequent projection correctness repair @@ -453,3 +453,35 @@ Main 25d56ae3fb9b80acda80f3cf4e1c8667939147eb is normally merged; the shared environment and PR submodule consume the declared published metaclass-registry 0.2.2 source 393a7e03003cdc56df9013f932ed4f26e632d77a. Earlier observations retain their measured source/dependency pins. + +Restored registry readiness on main +---------------------------------- + +PR420 merges the startup correction to main +107498cfacf3b12d926c55f84c4b3cb7c04aa50f. That main is normally merged into +this performance branch. Existing affinity admission, cancellation ownership +and compiler guards for declarations introduced after startup are retained. + +An ordinary, uninstrumented 3D 1w_1t run at fix source +e55e2ec0bc98ad750837cd4dcfd830ad04096286 starts with a new empty Numba cache. +Compilation is 1.720214 s, execution 11.151797 s and pipeline total 13.637182 s. +All 481 cache files are written before pipeline submission; the last write is +6.001317 s before run creation and no cache files are written during compilation +or execution. Server startup, warmup and shutdown are excluded from pipeline +clocks. All six CSVs and 120 TIFF arrays exactly match retained main9a output, +including dtype, shape and matching inventories. This is a lifecycle regression +gate, not an ABBA performance comparison or current integrated-candidate timing. + +The fix passes 37 scoped tests with five single-affinity skips and both actual +two-core fork controls. Original unmodified R0 passes for openhcs, scripts and +benchmark; R1 reports no increases against main25d56ae under its unchanged +160-second budget. The failed Python3.12 ratchet launch and uninitialized +worktree R1 launch remain retained; final guards use Python3.14 and the initialized +main repository, respectively, without modifying either detector. + +Evidence: ``registry-startup-readiness-162-empty-cache-pipeline-20261002.json``, +``registry-startup-readiness-162-validation-20261001.json`` and +``registry-startup-readiness-162-original-r0-py314-20261002.log`` plus +``registry-startup-readiness-162-original-r1-initialized-root-20261002.log``. +The original 50.800-second compilation regression remains retained. Current +combined performance, IFC behavior and source guards remain draft gates. From 5337fda87b571580862d917fde95a5397cb5e4fe Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Thu, 1 Oct 2026 22:27:12 -0400 Subject: [PATCH 032/235] Share scalar admission through nominal source field ownership Canonical and durable owners retain their normalization and error policies while sharing one primitive admission algorithm. Preserve observed scalar class checks and remove duplicate validation. Refs #384. --- openhcs/core/source_metadata.py | 47 ++++++++++++++++-------- tests/unit/test_source_metadata_owner.py | 19 ++++++++++ 2 files changed, 50 insertions(+), 16 deletions(-) diff --git a/openhcs/core/source_metadata.py b/openhcs/core/source_metadata.py index cdc93922fd..c8ab6df14b 100644 --- a/openhcs/core/source_metadata.py +++ b/openhcs/core/source_metadata.py @@ -10,7 +10,7 @@ from math import isfinite from pathlib import Path from types import MappingProxyType -from typing import ClassVar, Self, TYPE_CHECKING, TypeAlias, TypeVar +from typing import ClassVar, NoReturn, Self, TYPE_CHECKING, TypeAlias, TypeVar from metaclass_registry import AutoRegisterMeta from zmqruntime.viewer_protocol import ViewerWireField @@ -77,9 +77,29 @@ def normalized_mapping( {str(key): cls.normalized_value(value) for key, value in metadata.items()} ) + @classmethod + def normalized_scalar(cls, value: SourceMetadataScalar) -> SourceMetadataScalar: + """Admit the shared scalar grammar before applying the owner's policy.""" + if value is None: + return None + if isinstance(value, SourceMetadataNonNullScalar): + return cls._normalize_admitted_scalar(value) + return cls._reject_scalar(value) + + @staticmethod + def _normalize_admitted_scalar( + value: SourceMetadataNonNullScalar, + ) -> SourceMetadataNonNullScalar: + if isinstance(value, str): + return canonical_path_metadata_value(value) + return value + @staticmethod - def normalized_scalar(value: SourceMetadataScalar) -> SourceMetadataScalar: - return source_metadata_scalar(value) + def _reject_scalar(value: object) -> NoReturn: + raise TypeError( + "Source metadata scalar values must be str, int, float, bool, or None, " + f"got {type(value).__name__}." + ) def __getitem__(self, key: str) -> SourceMetadataValue: for field_key, value in self.fields: @@ -499,9 +519,13 @@ def __eq__(self, other: object) -> bool: __hash__ = None @staticmethod - def normalized_scalar(value: SourceMetadataScalar) -> SourceMetadataScalar: - if value is None or isinstance(value, SourceMetadataNonNullScalar): - return value + def _normalize_admitted_scalar( + value: SourceMetadataNonNullScalar, + ) -> SourceMetadataNonNullScalar: + return value + + @staticmethod + def _reject_scalar(value: object) -> NoReturn: if isinstance(value, Mapping) or ( isinstance(value, Sequence) and not isinstance(value, str) ): @@ -546,16 +570,7 @@ def source_metadata_dict( def source_metadata_scalar(value: SourceMetadataScalar) -> SourceMetadataScalar: """Return the canonical scalar representation stored in source metadata.""" - if value is None: - return None - if not isinstance(value, SourceMetadataNonNullScalar): - raise TypeError( - "Source metadata scalar values must be str, int, float, bool, or None, " - f"got {type(value).__name__}." - ) - if isinstance(value, str): - return canonical_path_metadata_value(value) - return value + return SourceMetadataFields.normalized_scalar(value) def canonical_path_metadata_value(value: str) -> str: diff --git a/tests/unit/test_source_metadata_owner.py b/tests/unit/test_source_metadata_owner.py index a092129dfd..4a492b864d 100644 --- a/tests/unit/test_source_metadata_owner.py +++ b/tests/unit/test_source_metadata_owner.py @@ -587,6 +587,25 @@ def __class__(self): assert DurableSourceMetadata.normalized_scalar(reported_integer) is reported_integer assert reported_integer.reads == [object, int] + for normalize in ( + source_metadata_scalar, + SourceMetadataFields.normalized_scalar, + ResolvedSourceMetadataRecord.normalized_scalar, + ): + reported_integer = ReportedClass((object, int, object)) + assert normalize(reported_integer) is reported_integer + assert reported_integer.reads == [object, int, object] + + # Scalar admission and string normalization are separate observations. + # A changing reported class must retain the previous normalization order. + reported_string = ReportedClass((str, object)) + assert normalize(reported_string) is reported_string + assert reported_string.reads == [str, object] + + reported_string = ReportedClass((str, object)) + assert DurableSourceMetadata.normalized_scalar(reported_string) is reported_string + assert reported_string.reads == [str] + changing_class = ReportedClass( (object, object, object, object, object, Sequence, str) ) From 80d92db0edb472dd8c81a169bc58d8549f4fb95c Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Thu, 1 Oct 2026 22:35:32 -0400 Subject: [PATCH 033/235] Keep projection normalization on owned image metadata Compose source selection and axis transformation on one independently owned metadata result. Retain constructor effect ordering through typed field owners, remove redundant dictionary state and provenance forwarding, and preserve input isolation. Refs #384. --- openhcs/core/runtime_image_values.py | 176 ++++++++++-------- openhcs/core/source_image_provenance.py | 16 +- .../test_image_payload_projection_algebra.py | 111 +++++++++++ 3 files changed, 212 insertions(+), 91 deletions(-) diff --git a/openhcs/core/runtime_image_values.py b/openhcs/core/runtime_image_values.py index 5963e79799..896de868c3 100644 --- a/openhcs/core/runtime_image_values.py +++ b/openhcs/core/runtime_image_values.py @@ -239,33 +239,24 @@ def for_array_payload( def __post_init__(self, *source_provenance_values: object) -> None: self.absorb_explicit_source_provenance(source_provenance_values) - for name, value in self.normalized_fields({}).items(): - setattr(self, name, value) - - def normalized_fields(self, changes: Mapping[str, Any]) -> dict[str, Any]: - """Normalize constructor instructions in their source-context effect order.""" - provenance = changes.get("source_provenance", self.source_provenance) - spacing = changes.get("source_voxel_spacing", self.source_voxel_spacing) - spacing = spacing.with_missing_from( - SourceVoxelSpacing.from_source_metadata( - provenance.source_component_metadata - ) + self.normalize_metadata_fields() + + def normalize_metadata_fields(self) -> None: + """Normalize this metadata's typed fields in their constructor effect order.""" + self.source_voxel_spacing = self.source_voxel_spacing.with_missing_from( + SourceVoxelSpacing.from_source_metadata(self.source_component_metadata) ) - provenance = SourceImageProvenance.normalized(provenance) - domain = changes.get( - "source_spatial_domain", self.source_spatial_domain - ).normalized() - spacing = SourceVoxelSpacing.coerce(spacing) + self.normalize_source_provenance_fields() + self.normalize_source_spatial_domain_fields() + self.normalize_source_voxel_spacing_fields() self.validate_source_channel_axis() - plane_axis = changes.get("plane_axis", self.plane_axis) - if plane_axis is not None: - plane_axis = RuntimePlaneAxis(plane_axis) - return dict( - source_provenance=provenance, - source_voxel_spacing=spacing, - source_spatial_domain=domain, - plane_axis=plane_axis, - ) + if self.plane_axis is not None: + self.plane_axis = RuntimePlaneAxis(self.plane_axis) + + def require_leading_plane_axis(self, message: str) -> None: + """Require axis presence before later ordered projection validation.""" + if self.plane_axis is None: + raise ValueError(message) def validate_source_channel_axis(self) -> None: """Validate the authored channel declaration before transforming axes.""" @@ -657,9 +648,37 @@ def for_leading_source_plane(self, plane_index: int) -> "ImagePayloadMetadata": """Project metadata after explicitly removing its leading plane axis.""" return LeadingSourcePlaneMetadataProjection(self, plane_index).project() - def without_leading_plane_axis(self) -> "ImagePayloadMetadata": - """Return metadata after an explicitly declared leading axis is removed.""" - return LeadingPlaneAxisMetadataProjection(self).project() + def without_leading_plane_axis( + self, *, projection: "ImageMetadataProjection | None" = None + ) -> "ImagePayloadMetadata": + """Remove an axis through the projection's declared result ownership.""" + self.require_leading_plane_axis( + "Image metadata has no leading plane axis to remove." + ) + source_channel_axis = self.source_channel_axis + if source_channel_axis == 0: + raise ValueError( + "Image metadata cannot declare the same leading axis as both " + "plane and channel." + ) + if source_channel_axis is not None and source_channel_axis > 0: + source_channel_axis -= 1 + if projection is None: + projection = LeadingPlaneAxisMetadataProjection(self) + projected = projection.project_source_provenance( + self, self.source_provenance.with_runtime_planes_as_contributors() + ) + projected.plane_axis = None + projected.source_channel_axis = source_channel_axis + projected.source_plane_intensity_scales = () + projected.source_plane_dtypes = () + projected.unit_interval_intensity = ( + None + if self.unit_interval_intensity is None + else self.unit_interval_intensity.without_source_planes() + ) + projected.normalize_metadata_fields() + return projected def collapse_leading_plane_axis(self) -> "ImagePayloadMetadata": """Return scalar metadata after reducing every plane of the leading axis.""" @@ -1421,15 +1440,23 @@ class ImageMetadataProjection(ABC): metadata: ImagePayloadMetadata def project(self) -> ImagePayloadMetadata: - return self.metadata.replace_fields(**self.projected_fields()) + return self.project_axis_metadata(self.project_source_metadata()) @abstractmethod - def projected_fields(self) -> dict[str, Any]: - """Derive constructor instructions, never an independently stored fact set.""" - return dict( - source_provenance=self.metadata.source_provenance, - unit_interval_intensity=self.metadata.unit_interval_intensity, - ) + def project_source_metadata(self) -> ImagePayloadMetadata: + """Select source metadata with this projection's declared ownership.""" + + def project_axis_metadata( + self, metadata: ImagePayloadMetadata + ) -> ImagePayloadMetadata: + """Retain declared axes unless a nominal axis capability transforms them.""" + return metadata + + @abstractmethod + def project_source_provenance( + self, metadata: ImagePayloadMetadata, provenance: SourceImageProvenance + ) -> ImagePayloadMetadata: + """Apply provenance using the result ownership established by source selection.""" @dataclass(frozen=True, slots=True) @@ -1438,12 +1465,9 @@ class SourcePlaneImageMetadataProjection(ImageMetadataProjection): plane_index: int - def projected_fields(self) -> dict[str, Any]: - changes = super(SourcePlaneImageMetadataProjection, self).projected_fields() - changes["source_provenance"] = changes["source_provenance"].for_source_plane( - self.plane_index - ) - changes.update( + def project_source_metadata(self) -> ImagePayloadMetadata: + provenance = self.metadata.source_provenance.for_source_plane(self.plane_index) + return self.metadata.replace_fields( intensity_scale=self.metadata.intensity_scale_for_source_plane( self.plane_index ), @@ -1451,64 +1475,56 @@ def projected_fields(self) -> dict[str, Any]: self.metadata.source_plane_dtypes, self.plane_index ) or self.metadata.source_dtype, + source_provenance=provenance, unit_interval_intensity=( None - if changes["unit_interval_intensity"] is None - else changes["unit_interval_intensity"].for_source_plane( + if self.metadata.unit_interval_intensity is None + else self.metadata.unit_interval_intensity.for_source_plane( self.plane_index ) ), source_plane_intensity_scales=(), source_plane_dtypes=(), ) - changes.update(self.metadata.normalized_fields(changes)) - return changes + + def project_source_provenance( + self, metadata: ImagePayloadMetadata, provenance: SourceImageProvenance + ) -> ImagePayloadMetadata: + """Normalize the independently owned result created by source selection.""" + metadata.source_provenance = provenance + metadata.normalize_metadata_fields() + return metadata class LeadingPlaneAxisMetadataProjection(ImageMetadataProjection): """Remove a declared leading axis while preserving its source contributors.""" - def projected_fields(self) -> dict[str, Any]: - if self.metadata.plane_axis is None: - raise ValueError("Image metadata has no leading plane axis to remove.") - changes = super().projected_fields() - source_channel_axis = self.metadata.source_channel_axis - if source_channel_axis == 0: - raise ValueError( - "Image metadata cannot declare the same leading axis as both " - "plane and channel." - ) - if source_channel_axis is not None and source_channel_axis > 0: - source_channel_axis -= 1 - changes["source_provenance"] = changes[ - "source_provenance" - ].with_runtime_planes_as_contributors() - changes.update(self.metadata.normalized_fields(changes)) - intensity = changes["unit_interval_intensity"] - changes.update( - plane_axis=None, - source_channel_axis=source_channel_axis, - source_plane_intensity_scales=(), - source_plane_dtypes=(), - unit_interval_intensity=( - None if intensity is None else intensity.without_source_planes() - ), - ) - return changes + def project_source_metadata(self) -> ImagePayloadMetadata: + return self.metadata + + def project_axis_metadata( + self, metadata: ImagePayloadMetadata + ) -> ImagePayloadMetadata: + return metadata.without_leading_plane_axis(projection=self) + + def project_source_provenance( + self, metadata: ImagePayloadMetadata, provenance: SourceImageProvenance + ) -> ImagePayloadMetadata: + """Create the standalone result after its axis guards and source derivation.""" + return metadata.replace_fields(source_provenance=provenance) class LeadingSourcePlaneMetadataProjection( - LeadingPlaneAxisMetadataProjection, SourcePlaneImageMetadataProjection, + LeadingPlaneAxisMetadataProjection, ): - """Compose source selection and axis removal before constructing metadata.""" + """Remove the leading axis of independently owned selected-source metadata.""" - def projected_fields(self) -> dict[str, Any]: - if self.metadata.plane_axis is None: - raise ValueError( - "Leading source-plane projection requires a declared plane axis." - ) - return super().projected_fields() + def project_source_metadata(self) -> ImagePayloadMetadata: + self.metadata.require_leading_plane_axis( + "Leading source-plane projection requires a declared plane axis." + ) + return super().project_source_metadata() @dataclass(frozen=True, slots=True) diff --git a/openhcs/core/source_image_provenance.py b/openhcs/core/source_image_provenance.py index 366a8d5fd4..025d77b87f 100644 --- a/openhcs/core/source_image_provenance.py +++ b/openhcs/core/source_image_provenance.py @@ -702,15 +702,6 @@ def __init__( self.source_image_names, ) - def normalized(self) -> "SourceImageProvenance": - """Snapshot provenance through the constructor's concrete field policy.""" - return SourceImageProvenance( - self.source_path, - self.source_component_metadata, - self.source_image_provenance_planes, - self.source_image_names, - ) - @classmethod def from_init_values( cls, @@ -1797,8 +1788,11 @@ def absorb_explicit_source_provenance( self.source_provenance = explicit.with_missing_from(self.source_provenance) def normalize_source_provenance_fields(self) -> None: - self.source_provenance = SourceImageProvenance.normalized( - self.source_provenance + self.source_provenance = SourceImageProvenance( + self.source_provenance.source_path, + self.source_provenance.source_component_metadata, + self.source_provenance.source_image_provenance_planes, + self.source_provenance.source_image_names, ) def replace_fields(self, **changes: Any) -> Self: diff --git a/tests/unit/test_image_payload_projection_algebra.py b/tests/unit/test_image_payload_projection_algebra.py index 23bbc68f1c..086520fd4c 100644 --- a/tests/unit/test_image_payload_projection_algebra.py +++ b/tests/unit/test_image_payload_projection_algebra.py @@ -315,3 +315,114 @@ def test_projection_spacing_failure_precedes_other_invalid_source_fields(): metadata.plane_axis = "invalid-axis" with pytest.raises(ValueError, match="finite and positive"): metadata.for_leading_source_plane(0) + + +@pytest.mark.parametrize( + "operation,normalization_axes", + ( + ("without_leading_plane_axis", (RuntimePlaneAxis.RUNTIME_SLICE, None)), + ( + "for_leading_source_plane", + (RuntimePlaneAxis.RUNTIME_SLICE, RuntimePlaneAxis.RUNTIME_SLICE, None), + ), + ), +) +def test_projection_normalizes_owned_metadata_at_each_historical_phase( + monkeypatch, operation, normalization_axes +): + metadata = _metadata(RuntimePlaneAxis.RUNTIME_SLICE, 2) + input_provenance = metadata.source_provenance + input_proof = metadata.unit_interval_intensity + births = [] + phases = [] + original_constructor = ImagePayloadMetadata.__post_init__ + original_normalize = ImagePayloadMetadata.normalize_metadata_fields + + def observe_constructor(self, *values): + births.append((self, self.plane_axis, self.source_channel_axis)) + original_constructor(self, *values) + + def observe_normalization(self): + phases.append((self, self.plane_axis, self.source_channel_axis)) + original_normalize(self) + + monkeypatch.setattr(ImagePayloadMetadata, "__post_init__", observe_constructor) + monkeypatch.setattr( + ImagePayloadMetadata, "normalize_metadata_fields", observe_normalization + ) + projected = ( + metadata.without_leading_plane_axis() + if operation == "without_leading_plane_axis" + else metadata.for_leading_source_plane(1) + ) + + assert births == [(projected, RuntimePlaneAxis.RUNTIME_SLICE, 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,) * ( + len(normalization_axes) - 1 + ) + (1,) + 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.source_channel_axis == 2 + assert metadata.source_plane_intensity_scales == (255.0, 65535.0) + assert projected.source_provenance is not input_provenance + assert projected.unit_interval_intensity is not input_proof + + +@pytest.mark.parametrize("combined", (False, True)) +def test_projection_preserves_contributor_derivation_constructor_error_order( + monkeypatch, combined +): + metadata = _metadata(RuntimePlaneAxis.RUNTIME_SLICE, None) + invalid_domain = SourceSpatialDomain(origin_yx=(1,)) + metadata.source_spatial_domain = invalid_domain + provenance = metadata.source_provenance + calls = [] + + def fail_contributors(self): + calls.append(self) + raise RuntimeError("contributor derivation observed") + + monkeypatch.setattr( + type(provenance), "with_runtime_planes_as_contributors", fail_contributors + ) + if combined: + with pytest.raises(ValueError, match="spatial_origin_yx"): + metadata.for_leading_source_plane(1) + assert calls == [] + else: + with pytest.raises(RuntimeError, match="contributor derivation observed"): + metadata.without_leading_plane_axis() + 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.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) + provenance = metadata.source_provenance + proof = metadata.unit_interval_intensity + + def fail_proof(self): + raise RuntimeError("axis proof derivation observed") + + monkeypatch.setattr( + ImageUnitIntervalIntensityMetadata, "without_source_planes", fail_proof + ) + with pytest.raises(RuntimeError, match="axis proof derivation observed"): + if combined: + metadata.for_leading_source_plane(1) + else: + 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.source_channel_axis == 2 + assert metadata.source_plane_intensity_scales == (255.0, 65535.0) + assert metadata.source_plane_dtypes == ("uint8", "uint16") From 90c30e1adfacd25a834ed48949b430099c866134 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Thu, 1 Oct 2026 22:51:53 -0400 Subject: [PATCH 034/235] Share nullable intensity proof projection on image metadata Address #384 by consolidating source-plane and axis-removal proof handling on the existing metadata owner, and deriving scale queries from their existing scalar and quantization authorities. --- openhcs/core/runtime_image_values.py | 39 +++++------ .../test_image_payload_projection_algebra.py | 67 +++++++++++++++++++ 2 files changed, 85 insertions(+), 21 deletions(-) diff --git a/openhcs/core/runtime_image_values.py b/openhcs/core/runtime_image_values.py index 896de868c3..6ac2a80ae6 100644 --- a/openhcs/core/runtime_image_values.py +++ b/openhcs/core/runtime_image_values.py @@ -672,11 +672,7 @@ def without_leading_plane_axis( projected.source_channel_axis = source_channel_axis projected.source_plane_intensity_scales = () projected.source_plane_dtypes = () - projected.unit_interval_intensity = ( - None - if self.unit_interval_intensity is None - else self.unit_interval_intensity.without_source_planes() - ) + projected.unit_interval_intensity = self.project_intensity_proof(None) projected.normalize_metadata_fields() return projected @@ -714,22 +710,27 @@ def source_image_paths(self) -> tuple[str, ...]: def intensity_scale_for_source_plane(self, plane_index: int) -> float | None: """Return the best available intensity scale for one source plane.""" - if 0 <= plane_index < len(self.source_plane_intensity_scales): - plane_scale = self.source_plane_intensity_scales[plane_index] - if plane_scale is not None: - return plane_scale - return self.intensity_scale + plane_scale = _tuple_value(self.source_plane_intensity_scales, plane_index) + return self.intensity_scale if plane_scale is None else plane_scale def unit_interval_intensity_scale_for_source_plane( self, plane_index: int, ) -> int | None: """Return the scale proving current pixels are exact integer/scale values.""" - if 0 <= plane_index < len(self.source_plane_unit_interval_intensity_scales): - plane_scale = self.source_plane_unit_interval_intensity_scales[plane_index] - if plane_scale is not None: - return int(plane_scale) - return self.unit_interval_intensity_scale + if self.unit_interval_intensity is None: + return None + return self.unit_interval_intensity.scale_for_source_plane(plane_index) + + def project_intensity_proof( + self, plane_index: int | None + ) -> ImageUnitIntervalIntensityMetadata | None: + """Select a plane's proof, or retain only the proof for the whole image.""" + if self.unit_interval_intensity is None: + return None + if plane_index is None: + return self.unit_interval_intensity.without_source_planes() + return self.unit_interval_intensity.for_source_plane(plane_index) @property def source_plane_metadata_count(self) -> int: @@ -1476,12 +1477,8 @@ def project_source_metadata(self) -> ImagePayloadMetadata: ) or self.metadata.source_dtype, source_provenance=provenance, - unit_interval_intensity=( - None - if self.metadata.unit_interval_intensity is None - else self.metadata.unit_interval_intensity.for_source_plane( - self.plane_index - ) + unit_interval_intensity=self.metadata.project_intensity_proof( + self.plane_index ), source_plane_intensity_scales=(), source_plane_dtypes=(), diff --git a/tests/unit/test_image_payload_projection_algebra.py b/tests/unit/test_image_payload_projection_algebra.py index 086520fd4c..677ed7beb1 100644 --- a/tests/unit/test_image_payload_projection_algebra.py +++ b/tests/unit/test_image_payload_projection_algebra.py @@ -426,3 +426,70 @@ def fail_proof(self): assert metadata.source_channel_axis == 2 assert metadata.source_plane_intensity_scales == (255.0, 65535.0) assert metadata.source_plane_dtypes == ("uint8", "uint16") + + +@pytest.mark.parametrize( + "plane_index,expected_scale", + ((None, 255), (-1, 255), (0, 255), (1, 65535), (2, 255)), +) +def test_owned_proof_projection_preserves_scalar_fallback_and_axis_removal( + plane_index, expected_scale +): + proof = ImageUnitIntervalIntensityMetadata( + scale=255, source_plane_scales=(None, 65535) + ) + metadata = ImagePayloadMetadata(unit_interval_intensity=proof) + projected = metadata.project_intensity_proof(plane_index) + assert projected is not proof + assert projected.scale == expected_scale + assert projected.source_plane_scales == () + assert metadata.unit_interval_intensity is proof + assert proof.source_plane_scales == (None, 65535) + if plane_index is not None: + assert metadata.unit_interval_intensity_scale_for_source_plane(plane_index) == ( + expected_scale + ) + + +@pytest.mark.parametrize("plane_index", (None, 0, 1)) +def test_owned_proof_projection_observes_absence_and_live_replacement(plane_index): + metadata = ImagePayloadMetadata(intensity_scale=255) + assert metadata.project_intensity_proof(plane_index) is None + if plane_index is not None: + assert ( + metadata.unit_interval_intensity_scale_for_source_plane(plane_index) is None + ) + proof = ImageUnitIntervalIntensityMetadata(scale=7, source_plane_scales=(11, 13)) + metadata.unit_interval_intensity = proof + assert metadata.project_intensity_proof(plane_index).scale == ( + 7 if plane_index is None else (11, 13)[plane_index] + ) + metadata.unit_interval_intensity = None + assert metadata.project_intensity_proof(plane_index) is None + + +def test_quantization_queries_preserve_integer_conversion_and_fallback_identity(): + calls = [] + + class ObservedInt(int): + def __int__(self): + calls.append(self) + return super().__int__() + + scalar = ObservedInt(255) + plane = ObservedInt(65535) + metadata = ImagePayloadMetadata( + unit_interval_intensity=ImageUnitIntervalIntensityMetadata( + scale=scalar, source_plane_scales=(None, plane) + ) + ) + assert metadata.unit_interval_intensity_scale_for_source_plane(0) is scalar + assert metadata.unit_interval_intensity_scale_for_source_plane(-1) is scalar + assert calls == [] + assert metadata.unit_interval_intensity_scale_for_source_plane(1) == 65535 + assert calls == [plane] + calls.clear() + assert metadata.project_intensity_proof(1).scale == 65535 + assert calls == [plane] + assert metadata.project_intensity_proof(None).scale is scalar + assert calls == [plane] From 37726ea29c7d2e5efed210bf51544d167fe72d66 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Thu, 1 Oct 2026 23:00:25 -0400 Subject: [PATCH 035/235] Check slice projection on concrete runtime parameter declarations Preserve strict concrete projection coverage while excluding genuinely abstract classification families using their existing abstract-method authority. Address the broader #384 consumer gate. --- tests/unit/test_runtime_slice_projection.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/unit/test_runtime_slice_projection.py b/tests/unit/test_runtime_slice_projection.py index 78aec1511f..a3cc39fdf8 100644 --- a/tests/unit/test_runtime_slice_projection.py +++ b/tests/unit/test_runtime_slice_projection.py @@ -1,4 +1,5 @@ from dataclasses import dataclass +import inspect from typing import Any, get_args, get_origin import numpy as np @@ -85,6 +86,8 @@ def _annotation_leaf_types(annotation: object) -> tuple[type, ...]: def test_cellprofiler_runtime_parameter_types_declare_slice_projection() -> None: missing: list[str] = [] for parameter_type in _subclasses(KeywordRuntimeParameter): + if inspect.isabstract(parameter_type): + continue if not parameter_type.__module__.startswith( ("openhcs.processing.backends.cellprofiler", "openhcs.interop.cellprofiler") ): From 7e0da269643a5d51122ee556611f9dfa81fb80fe Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Thu, 1 Oct 2026 23:11:30 -0400 Subject: [PATCH 036/235] Record complete ordinary runtime qualification and preserved guard evidence Cite #384 results with exact source and dependency pins, all exported science comparisons, current main synchronization and remaining whole-value runtime frontier. --- .../shared_runtime_plumbing_384_20261001.rst | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/docs/validation/shared_runtime_plumbing_384_20261001.rst b/docs/validation/shared_runtime_plumbing_384_20261001.rst index 4ed582c9e5..35c5379c89 100644 --- a/docs/validation/shared_runtime_plumbing_384_20261001.rst +++ b/docs/validation/shared_runtime_plumbing_384_20261001.rst @@ -485,3 +485,110 @@ Evidence: ``registry-startup-readiness-162-empty-cache-pipeline-20261002.json``, ``registry-startup-readiness-162-original-r1-initialized-root-20261002.log``. The original 50.800-second compilation regression remains retained. Current combined performance, IFC behavior and source guards remain draft gates. + +Complete-field ordinary ABBA and current ownership repairs +--------------------------------------------------------- + +The completed uninstrumented ABBA at main107498cf against candidate02609e12 +has two observations per source for each workload. Execution/total means are +11.162969/13.119510 s versus 8.295599/10.351318 s for 3D, +8.706567/11.262818 s versus 8.474416/11.132766 s for Advanced, and +16.500067/18.794964 s versus 16.920803/19.454312 s for IFC. Thus 3D saves +2.867370 s execution and 2.768192 s total, while IFC regresses 0.420735 s +execution and 0.659348 s total in these samples. No statistical significance +or noise explanation is asserted. All observations remain retained. + +Original unmodified R0/R1 rejected02609e12. The actual metadata owner now owns +normalization and leading-axis transformation; the redundant raw projected +field dictionary, provenance normalization forwarder and foreign optional-state +handling are removed. Source and axis projection roles compose through +inheritance while preserving effect/error order and independent result ownership. +Common scalar admission is shared on SourceMetadataFields. The optional +intensity-proof projection and scale queries consume their existing authorities +instead of duplicating algorithms. Component controls include 41 scalar tests, +1,212 original-source behavior comparisons and 197 metadata/proof controls. +The collected classification projection census was also repaired to skip actually +abstract parameter declarations while still checking concrete descendants; +429 focused and 1,111 broader consumer controls passed before dependency update. +The inherited census failure was reproduced on main under the same imports. + +The subsequent ordinary ABBA measures frozen production candidate +90c30e1adfacd25a834ed48949b430099c866134 against the same main107498cf: + +==================== ======================== ======================== +Workload Execution main/candidate Total main/candidate +==================== ======================== ======================== +3D 11.089386 / 8.172661 s 13.063124 / 10.106094 s +Advanced 8.854658 / 8.988559 s 11.408556 / 11.590607 s +IFC 17.382926 / 16.212604 s 19.628127 / 18.661190 s +==================== ======================== ======================== + +3D saves 2.916725 s execution and 2.957031 s total; compilation means are +1.214262 / 1.209572 s. Advanced regresses 0.133900 s execution and 0.182051 s +total. IFC improves 1.170322 s execution and 0.966938 s total in this batch. +These means do not erase the adverse026 IFC evidence or establish significance. +All twelve case observations succeed through ordinary OUTCOMES, default memory +observation, one well/thread and CPU5. Both sources use identical public drivers, +shared dependencies/native binaries and mandatory registry preparation before +readiness. Server startup, prewarming and shutdown are excluded. There are no +profiling hooks, captures or scientific substitutions in either ABBA. + +For each ABBA, all six output pairs pass scientific comparison. All six 3D +CSV exports and all120TIFF arrays match exactly, including complete scientific +inventories, dtype and shape. IFC has exact headers and all1,800x527 result cells. +Advanced uses the existing CellProfiler database/export comparator at absolute +and relative1e-6, with exact schemas, cardinalities, discrete values, identifier +relationships and categorical data. Eight scientific subject tables and all +relationships are nonempty. Unsaved final segmentation masks were not observed. +The90 comparison reads actualaf45 source against its still unchanged pre-update +dependency environment, attested before and after. Output files remain unchanged. + +At90c30, original R1 passes with no increases under the unchanged160-second +budget, and original R0 scripts/benchmark pass. R0 openhcs remains RED solely +because the scalar/container rejection classifier moved from virtual workspace +decoding to its actual field owner: source_metadata gains one local switch and +three arms, while virtual_workspace_metadata loses one switch and four arms. +The original per-file gate is not waived or declared passed. Across all changed +roots the global delta is zero switches and minus one arm; untouched files cancel +because these original metrics are file-local AST measurements. Existing error +and reported-class observation ordering is preserved rather than altered to +satisfy the metric. The global702-module authority audit found no existing +replacement retaining that complete grammar/error contract. These are distinct +architecture and behavioral gates, not interchangeable claims. + +After both ABBA and all their comparisons finished, main +c32447f1c86a1878a313d1643a398e30ac20f75e was normally merged into this branch. +The shared environment now installs declared published ArrayBridge0.3.6 at +source1e53d03d9f468322a1c085c8be29485fb139caf2, openhcs-basicpy1.3.1 and +JAX/jaxlib0.9.2. Metaclass-registry0.2.2 source393a7e0, NumPy/SciPy/Numba +and both native extension hashes are unchanged. Pip check reports no broken +requirements and actual package imports pass. The separate native CellProfiler +4.2.8.1 environment remains unchanged. Earlier timings retain their measured +source/dependency pins; they are not projected through this new main/environment. +New combined qualification and full native parity remain required. + +A capture-free coarse diagnostic on frozen80d92 source partitions one9.208835 s +job into4.164762 s inside RuntimeCallableInvocation.call and5.044073 s outside. +The callable boundary includes decorated processing/metadata and is not a pure +image-kernel measurement. Diagnostic wrappers are not accepted performance +comparisons. Full load, unstack, save, CP image recording and image request phases +have a nonoverlapping3.009640 s upper bound. Recovering2 s requires eliminating +at least66.453% of those complete phases, not just a cached recomposition or +an original-source cache miss. The existing named-value to eager-plane metadata +to MemoryVFS to whole-stack round trip is the next structural premise. Mandatory +named/mainflow pixel-copy isolation and concurrent durable publication remain +constraints. No new production route is admitted without a representative replay +showing that complete consumer closure can realize a material payoff. + +Retained evidence is in the shared benchmark-runs directory: +``owned-metadata-90c30-ordinary-abba-csv-tiff-parity-20261002.json``, +``advanced-ordinary90-six-pair-sqlite-parity-20261002.json``, +``pr394-integrated-90c30-original-source-guards-20261002.json``, +``scalar-owner-original-r0-global-delta-counterevidence-20261002.json``, +``integrated90-classification-concrete-projection-census-fix-20261002.json``, +``shared-main-dependency-update-c32447-20261002.json`` and +``pr394-90c30-whole-mainflow-value-frontier-20261002.json``. +Source freezes, complete observations and diagnostic boundaries are retained in +``/var/tmp/openhcs-owned-metadata-90c30-ordinary-abba-20261002/`` and +``/var/tmp/openhcs-owned-metadata-phase-probe-20261002/``. The original026 +comparison and original failures remain separate pinned evidence. From 4afd5141554299680fa0326b31334a448fbf20bf Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Thu, 1 Oct 2026 23:14:32 -0400 Subject: [PATCH 037/235] Clarify pinned runtime validation prose --- .../shared_runtime_plumbing_384_20261001.rst | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/validation/shared_runtime_plumbing_384_20261001.rst b/docs/validation/shared_runtime_plumbing_384_20261001.rst index 35c5379c89..c6181ca111 100644 --- a/docs/validation/shared_runtime_plumbing_384_20261001.rst +++ b/docs/validation/shared_runtime_plumbing_384_20261001.rst @@ -489,7 +489,7 @@ combined performance, IFC behavior and source guards remain draft gates. Complete-field ordinary ABBA and current ownership repairs --------------------------------------------------------- -The completed uninstrumented ABBA at main107498cf against candidate02609e12 +The completed uninstrumented ABBA at main 107498cf against candidate 02609e12 has two observations per source for each workload. Execution/total means are 11.162969/13.119510 s versus 8.295599/10.351318 s for 3D, 8.706567/11.262818 s versus 8.474416/11.132766 s for Advanced, and @@ -498,7 +498,7 @@ has two observations per source for each workload. Execution/total means are execution and 0.659348 s total in these samples. No statistical significance or noise explanation is asserted. All observations remain retained. -Original unmodified R0/R1 rejected02609e12. The actual metadata owner now owns +Original unmodified R0/R1 rejected 02609e12. The actual metadata owner now owns normalization and leading-axis transformation; the redundant raw projected field dictionary, provenance normalization forwarder and foreign optional-state handling are removed. Source and axis projection roles compose through @@ -513,7 +513,7 @@ abstract parameter declarations while still checking concrete descendants; The inherited census failure was reproduced on main under the same imports. The subsequent ordinary ABBA measures frozen production candidate -90c30e1adfacd25a834ed48949b430099c866134 against the same main107498cf: +90c30e1adfacd25a834ed48949b430099c866134 against the same main 107498cf: ==================== ======================== ======================== Workload Execution main/candidate Total main/candidate @@ -534,16 +534,16 @@ readiness. Server startup, prewarming and shutdown are excluded. There are no profiling hooks, captures or scientific substitutions in either ABBA. For each ABBA, all six output pairs pass scientific comparison. All six 3D -CSV exports and all120TIFF arrays match exactly, including complete scientific -inventories, dtype and shape. IFC has exact headers and all1,800x527 result cells. +CSV exports and all 120 TIFF arrays match exactly, including complete scientific +inventories, dtype and shape. IFC has exact headers and all 1,800 x 527 result cells. Advanced uses the existing CellProfiler database/export comparator at absolute -and relative1e-6, with exact schemas, cardinalities, discrete values, identifier +and relative 1e-6, with exact schemas, cardinalities, discrete values, identifier relationships and categorical data. Eight scientific subject tables and all relationships are nonempty. Unsaved final segmentation masks were not observed. The90 comparison reads actualaf45 source against its still unchanged pre-update dependency environment, attested before and after. Output files remain unchanged. -At90c30, original R1 passes with no increases under the unchanged160-second +At 90c30, original R1 passes with no increases under the unchanged 160-second budget, and original R0 scripts/benchmark pass. R0 openhcs remains RED solely because the scalar/container rejection classifier moved from virtual workspace decoding to its actual field owner: source_metadata gains one local switch and @@ -552,28 +552,28 @@ The original per-file gate is not waived or declared passed. Across all changed roots the global delta is zero switches and minus one arm; untouched files cancel because these original metrics are file-local AST measurements. Existing error and reported-class observation ordering is preserved rather than altered to -satisfy the metric. The global702-module authority audit found no existing +satisfy the metric. The global 702-module authority audit found no existing replacement retaining that complete grammar/error contract. These are distinct architecture and behavioral gates, not interchangeable claims. After both ABBA and all their comparisons finished, main c32447f1c86a1878a313d1643a398e30ac20f75e was normally merged into this branch. -The shared environment now installs declared published ArrayBridge0.3.6 at -source1e53d03d9f468322a1c085c8be29485fb139caf2, openhcs-basicpy1.3.1 and -JAX/jaxlib0.9.2. Metaclass-registry0.2.2 source393a7e0, NumPy/SciPy/Numba +The shared environment now installs declared published ArrayBridge 0.3.6 at +source 1e53d03d9f468322a1c085c8be29485fb139caf2, openhcs-basicpy 1.3.1 and +JAX/jaxlib 0.9.2. Metaclass-registry 0.2.2 source 393a7e0, NumPy/SciPy/Numba and both native extension hashes are unchanged. Pip check reports no broken requirements and actual package imports pass. The separate native CellProfiler 4.2.8.1 environment remains unchanged. Earlier timings retain their measured source/dependency pins; they are not projected through this new main/environment. New combined qualification and full native parity remain required. -A capture-free coarse diagnostic on frozen80d92 source partitions one9.208835 s -job into4.164762 s inside RuntimeCallableInvocation.call and5.044073 s outside. +A capture-free coarse diagnostic on frozen 80d92 source partitions one 9.208835 s +job into 4.164762 s inside RuntimeCallableInvocation.call and 5.044073 s outside. The callable boundary includes decorated processing/metadata and is not a pure image-kernel measurement. Diagnostic wrappers are not accepted performance comparisons. Full load, unstack, save, CP image recording and image request phases -have a nonoverlapping3.009640 s upper bound. Recovering2 s requires eliminating -at least66.453% of those complete phases, not just a cached recomposition or +have a nonoverlapping 3.009640 s upper bound. Recovering 2 s requires eliminating +at least 66.453% of those complete phases, not just a cached recomposition or an original-source cache miss. The existing named-value to eager-plane metadata to MemoryVFS to whole-stack round trip is the next structural premise. Mandatory named/mainflow pixel-copy isolation and concurrent durable publication remain @@ -590,5 +590,5 @@ Retained evidence is in the shared benchmark-runs directory: ``pr394-90c30-whole-mainflow-value-frontier-20261002.json``. Source freezes, complete observations and diagnostic boundaries are retained in ``/var/tmp/openhcs-owned-metadata-90c30-ordinary-abba-20261002/`` and -``/var/tmp/openhcs-owned-metadata-phase-probe-20261002/``. The original026 +``/var/tmp/openhcs-owned-metadata-phase-probe-20261002/``. The original 026 comparison and original failures remain separate pinned evidence. From 82243634ce097d0d1a2d9e77664cbfeac3e6ec94 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Thu, 1 Oct 2026 23:52:10 -0400 Subject: [PATCH 038/235] Publish every successfully saved result destination through output owners (#134) --- ...sisted_result_publication_134_20261002.rst | 47 +++ openhcs/core/steps/function_outputs.py | 52 ++- openhcs/core/virtual_workspace_metadata.py | 13 +- tests/unit/test_function_outputs.py | 17 +- .../test_persisted_roi_only_publication.py | 313 ++++++++++++++++++ 5 files changed, 416 insertions(+), 26 deletions(-) create mode 100644 docs/validation/persisted_result_publication_134_20261002.rst create mode 100644 tests/unit/test_persisted_roi_only_publication.py diff --git a/docs/validation/persisted_result_publication_134_20261002.rst b/docs/validation/persisted_result_publication_134_20261002.rst new file mode 100644 index 0000000000..fe8e061086 --- /dev/null +++ b/docs/validation/persisted_result_publication_134_20261002.rst @@ -0,0 +1,47 @@ +Saved-result publication through the existing output owners +========================================================== + +PR394 integrates the source proposal supplied by PR404 (issue134), against +shared runtime source4afd51415. Successfully saved result directories participate +in publication even when their files are not raster images. This changes the +shared publication contract, not an ROI-specific writer or a function case. + +Existing OutputTarget owns guarded contains_outputs and a stored_output_paths +hook. RuntimeArtifactMetadataTarget overrides that hook for all saved formats; +no additional capability class is needed for its sole existing consumer. +The original MaterializationBatch saved outcomes determine runtime destinations. +Publication cannot rerender a mutable batch or invent an outcome from unrelated +filesystem files. Exact plate-relative result-directory ancestry is retained. +Final reconciliation derives previously declared directories from the existing +OpenHCSMetadataHandler after runtime values have been released. + +AtomicMetadataWriter remains the single transaction. It constructs image +component projections only for nonempty addressed image sets. SourceProjectionSet +retains its nonempty invariant, and actual saved images without produced source +addresses still fail without changing the metadata document. Result-only +publication produces an empty image inventory and projection, not an invented +image or source address. Existing format inventory remains authoritative. + +The current-source original control reproduces four failures and one pass. +The actual two-owner implementation passes89 publication, checkpoint, metadata +and materialization-journey tests. Five source cases exercise real bundle writer +saving, simple and nested result-only directories, original public result queries, +reconciliation after release, byte-exact refusal of unaddressed images, and a +new independently declared target with cooperative publication hooks. Its test +declaration explicitly requests its own registry key and restores the complete +registry after use; the initially inherited-key test contamination is retained. +The existing NumPy export control now requires a declared result destination +with empty raster inventory, instead of incorrectly requiring no metadata file. + +The NRA owner inventory joins137 original declarations across the selected +existing owners, scans1,498 caller modules without parse failures and records +alternative owners and open binding leads. Those syntax facts are not proof of +runtime binding or an architecture-gate pass. Original-source failure, failed +initial test path, registry-contamination failure, corrected consumer gate and +inventory receipts remain in the shared benchmark-runs workspace under +pr394-roi-publication prefixes. + +This is saved-output admission correctness and establishes no performance gain. +Synthetic archive bytes do not establish native ROI geometry, scientific pixels, +source provenance reopening or complete issue134 graph acceptance. Those broader +PR404 gates remain open. No additional scientific pipeline is executed here. diff --git a/openhcs/core/steps/function_outputs.py b/openhcs/core/steps/function_outputs.py index 05b7fb539c..d8f615c97e 100644 --- a/openhcs/core/steps/function_outputs.py +++ b/openhcs/core/steps/function_outputs.py @@ -781,19 +781,20 @@ def runtime_artifact_projection_paths( return self.project_runtime_artifacts(context, plan) return () - def contains_images(self, context: ProcessingContext) -> bool: - """Return whether the completed target contains image outputs.""" + def stored_output_paths(self, context: ProcessingContext) -> tuple[str, ...]: + """Return this declaration's outputs eligible for publication.""" + return tuple( + context.filemanager.list_image_files(self.output_dir, self.backend) + ) + + def contains_outputs(self, context: ProcessingContext) -> bool: + """Return whether this declared destination has publishable outputs.""" if context.filemanager is None: raise ValueError("OpenHCS metadata requires a file manager.") if not context.filemanager.is_dir(self.output_dir, self.backend): return False - return bool( - context.filemanager.list_image_files( - self.output_dir, - self.backend, - ) - ) + return bool(self.stored_output_paths(context)) def write( self, @@ -832,7 +833,10 @@ def write( component_labels=context.metadata_cache, backend=self.backend, is_main=self.is_main, - results_dir=(Path(self.results_dir).name if self.results_dir else None), + results_dir=( + str(Path(self.results_dir).relative_to(self.plate_root)) + if self.results_dir is not None else None + ), ) def produced_projection_metadata( @@ -1063,7 +1067,7 @@ def finalize_completed_plate( for owner, context in target_contexts.items(): for target in owner.reconciliation_targets(context): - if target.contains_images(context): + if target.contains_outputs(context): target.write(context) @@ -1146,7 +1150,11 @@ def from_plan( class RuntimeArtifactMetadataTarget(OpenHCSMetadataWriter.OutputTarget): - """Image artifacts persist independently of main-flow image/checkpoint storage.""" + """Saved artifacts own result destinations independently of image storage.""" + + def stored_output_paths(self, context: ProcessingContext) -> tuple[str, ...]: + """Include every saved format in this declared result destination.""" + return tuple(context.filemanager.list_files(self.output_dir, self.backend)) def production_targets( self, @@ -1158,19 +1166,20 @@ def production_targets( Path(output.path).parent for materialization in self.artifact_materializations for output in materialization.outputs_for_backend(self.backend) - if ImageFileFormat.is_image_path(output.path) ) return tuple( target for directory in directories for target in (self.for_directory(directory),) - if target.contains_images(context) + if target.contains_outputs(context) ) def reconciliation_targets( self, context: ProcessingContext ) -> tuple[RuntimeArtifactMetadataTarget, ...]: """Use durable typed projections, without reloading cleaned artifact values.""" + from openhcs.microscopes.openhcs import OpenHCSMetadataHandler + subdirectories = OpenHCSMetadataSubdirectories.from_path( METADATA_CONFIG.metadata_path(self.plate_root) ) @@ -1183,15 +1192,26 @@ def reconciliation_targets( if isinstance(projection, SourceArtifactProjection) and projection.ref.backend == self.backend ) + result_directories = OpenHCSMetadataHandler( + context.filemanager + ).analysis_result_directories(self.plate_root) return tuple( self.for_directory(directory) - for directory in dict.fromkeys((self.output_dir, *directories)) + for directory in dict.fromkeys( + ( + self.output_dir, + *directories, + *(directory.path for directory in result_directories), + ) + ) ) def for_directory(self, directory: Path) -> RuntimeArtifactMetadataTarget: """Retain the compiled plate/backend while projecting one declared directory.""" sub_dir = directory.relative_to(self.plate_root) - return replace(self, output_dir=directory, sub_dir=str(sub_dir)) + return replace( + self, output_dir=directory, sub_dir=str(sub_dir), results_dir=str(directory) + ) @classmethod def from_plan(cls, plan: CompiledStepPlan) -> RuntimeArtifactMetadataTarget | None: @@ -1205,7 +1225,7 @@ def from_plan(cls, plan: CompiledStepPlan) -> RuntimeArtifactMetadataTarget | No backend=materialization.require_persistent_backend(), plate_root=plate_root, sub_dir=str(output_dir.relative_to(plate_root)), - results_dir=None, + results_dir=str(output_dir), ) diff --git a/openhcs/core/virtual_workspace_metadata.py b/openhcs/core/virtual_workspace_metadata.py index ded7fe31be..fc0c25cee3 100644 --- a/openhcs/core/virtual_workspace_metadata.py +++ b/openhcs/core/virtual_workspace_metadata.py @@ -232,12 +232,13 @@ def update(data): ) ) subdirectory.update(serializer.projection_fields(retained_paths)) - projections = SourceProjectionSet( - tuple(entries[path] for path in published_paths) - ) - subdirectory.update( - serializer.component_metadata(projections, labels=component_labels) - ) + if published_paths: + projections = SourceProjectionSet( + tuple(entries[path] for path in published_paths) + ) + subdirectory.update( + serializer.component_metadata(projections, labels=component_labels) + ) subdirectory[FIELDS.IMAGE_FILES] = list(published_paths) subdirectory[FIELDS.MICROSCOPE_HANDLER_NAME] = microscope_handler_name subdirectory[FIELDS.SOURCE_FILENAME_PARSER_NAME] = ( diff --git a/tests/unit/test_function_outputs.py b/tests/unit/test_function_outputs.py index 7d887b0825..98d2cad9f9 100644 --- a/tests/unit/test_function_outputs.py +++ b/tests/unit/test_function_outputs.py @@ -2490,15 +2490,24 @@ def test_actual_array_exports_only_publish_declared_raster_inventory( context.runtime_value_store.clear() OpenHCSMetadataWriter.finalize_completed_plate({"A01": context}) metadata_path = tmp_path / "openhcs_metadata.json" + subdirectories = json.loads(metadata_path.read_text())[FIELDS.SUBDIRECTORIES] + assert set(subdirectories) == {"images/exports"} + subdirectory = subdirectories["images/exports"] + assert subdirectory[SourceProjectionMetadataSerializer.RESULTS_DIR_FIELD] == ( + "images/exports" + ) if not with_raster: - assert not metadata_path.exists() + assert subdirectory[FIELDS.IMAGE_FILES] == [] + assert ( + VirtualWorkspaceSourceProjectionEntries.from_subdirectory( + subdirectory + ).entries + == {} + ) return np.testing.assert_array_equal( tifffile.imread(tmp_path / "images/exports/A01_s001_w2_z001_t001.tif"), pixels ) - subdirectories = json.loads(metadata_path.read_text())[FIELDS.SUBDIRECTORIES] - assert set(subdirectories) == {"images/exports"} - subdirectory = subdirectories["images/exports"] assert subdirectory[FIELDS.IMAGE_FILES] == [ "images/exports/A01_s001_w2_z001_t001.tif" ] diff --git a/tests/unit/test_persisted_roi_only_publication.py b/tests/unit/test_persisted_roi_only_publication.py new file mode 100644 index 0000000000..7338a41787 --- /dev/null +++ b/tests/unit/test_persisted_roi_only_publication.py @@ -0,0 +1,313 @@ +"""Saved-output publication only: synthetic bytes, no ROI or pixel decoding.""" + +from dataclasses import replace +from types import SimpleNamespace + +from polystore.disk import DiskStorageBackend +from polystore.filemanager import FileManager +from openhcs.core.context.processing_context import ProcessingContext +from openhcs.microscopes.source_schema import SourceSchemaFilenameParser +import json + +import pytest + +from openhcs.agent.dto.plate import PlateFileQueryRequest +from openhcs.agent.path_policy import AgentPathPolicy +from openhcs.agent.services.plate_inspection_service import ( + PlateInspectionFileManagerFactory, + PlateInspectionService, +) +from openhcs.constants.constants import Backend +from openhcs.core.artifacts import ArtifactOutputPlan, MetadataArtifactType +from openhcs.core.compiled_step_plan import ( + CompiledStepPlan, + RuntimeArtifactMaterializationPlan, +) +from openhcs.core.component_group_scope import RuntimeExecutionAxisScope +from openhcs.core.runtime_artifact_values import RuntimeValue +from openhcs.core.runtime_stores import RuntimeArtifactLocation, StoredRuntimeValue +from openhcs.core.steps.function_artifact_materialization import ( + MaterializedRuntimeArtifact, + RuntimeArtifactMaterialization, +) +from openhcs.core.steps.function_outputs import ( + OpenHCSMetadataWriter, + ProducedImageMetadataCapability, + RuntimeArtifactMetadataTarget, +) +from openhcs.core.virtual_workspace_metadata import METADATA_CONFIG, MetadataWriteError +from openhcs.processing.materialization.core import ( + MaterializationSpec, + _WRITERS_BY_OPTIONS, + prepare_materialization, +) +from openhcs.processing.materialization.options import FileBundleOptions + + +@pytest.fixture +def publication_context(tmp_path): + filemanager = FileManager({Backend.DISK.value: DiskStorageBackend()}) + context = ProcessingContext(filemanager=filemanager) + context.metadata_cache = {} + context.microscope_handler = SimpleNamespace( + parser=SourceSchemaFilenameParser(), microscope_type="openhcsdata" + ) + context.publication_audit = [] + return context + + +class PublicationAuditCapability: + """Independent audit capability extends, rather than replaces, publication.""" + + def write(self, context, *, produced_plan=None): + context.publication_audit.append(("before", self.sub_dir)) + super().write(context, produced_plan=produced_plan) + context.publication_audit.append(("after", self.sub_dir)) + + +def plan_for(plate): + return CompiledStepPlan( + step_index=0, + step_name="synthetic saved result", + step_type="FunctionStep", + axis_id="A01", + write_backend=Backend.MEMORY.value, + create_openhcs_metadata=True, + output_plate_root=str(plate), + analysis_results_dir=str(plate / "compiled_default"), + runtime_artifact_materialization=RuntimeArtifactMaterializationPlan( + persistent_enabled=True, + persistent_backend=Backend.DISK.value, + ), + ) + + +def save_archive_outcome(context, plate, destination, monkeypatch): + """Use the original declared bundle writer, batch and disk save boundary.""" + spec = MaterializationSpec(FileBundleOptions()) + payload = { + f"{destination}/independent-outline.roi.zip": b"synthetic admission only" + } + base_path = plate / "bundle" + batch = prepare_materialization( + spec, + payload, + str(base_path), + context.filemanager, + (Backend.DISK.value,), + ) + saved = batch.save() + (output,) = saved.outputs_for_backend(Backend.DISK.value) + assert output is batch.outputs[0] + # Source qualification must consume the original successful save, not render + # a mutable logical payload again or use planned names as proof of a save. + payload.clear() + + def forbidden_render(*args, **kwargs): + pytest.fail("Publication rendered the already-saved batch again") + + writer = _WRITERS_BY_OPTIONS[FileBundleOptions] + monkeypatch.setitem( + _WRITERS_BY_OPTIONS, FileBundleOptions, replace(writer, write=forbidden_render) + ) + artifact_plan = ArtifactOutputPlan( + name="synthetic archive bundle", + path="/memory/bundle.pkl", + artifact_type=MetadataArtifactType, + materialization=spec, + ) + record = StoredRuntimeValue( + value=RuntimeValue.from_output_plan( + artifact_plan, + {}, + execution_scope=RuntimeExecutionAxisScope(axis_id="A01"), + ), + location=RuntimeArtifactLocation( + path=artifact_plan.path, backend=Backend.MEMORY.value + ), + ) + materialization = RuntimeArtifactMaterialization( + output_plan=artifact_plan, + spec=spec, + record=record, + data=payload, + base_path=base_path, + source_identity=None, + filename_source_identity=None, + ) + return ( + MaterializedRuntimeArtifact( + outputs_by_backend=saved.outputs_by_backend, + materialization=materialization, + ), + output, + ) + + +class ExistingDiskInspectionFactory(PlateInspectionFileManagerFactory): + """Original public service receives the same real disk manager as saving.""" + + def __init__(self, manager): + self.manager = manager + + def create(self): + return self.manager + + +@pytest.fixture(autouse=True) +def forbid_optional_backend_bootstrap(monkeypatch): + import polystore.base + + def forbidden_bootstrap(*args, **kwargs): + pytest.fail("Source-only qualification must not bootstrap optional backends") + + monkeypatch.setattr(polystore.base, "ensure_storage_registry", forbidden_bootstrap) + + +def result_paths(plate, context): + service = PlateInspectionService( + AgentPathPolicy.with_roots(readable_roots=(plate,), writable_roots=()), + filemanager_factory=ExistingDiskInspectionFactory(context.filemanager), + ) + result = service.query_files( + PlateFileQueryRequest.from_fields( + plate_path=str(plate), + microscope_type="openhcsdata", + kind="result", + include_previews=False, + ) + ) + assert result.errors == () + assert all(record.preview is None for record in result.records) + return tuple(record.full_path for record in result.records) + + +@pytest.mark.parametrize("destination", ("roi_only", "independent/nested/roi_only")) +def test_saved_roi_only_batch_publishes_and_reconciles_without_rendering( + tmp_path, + publication_context, + monkeypatch, + destination, +): + plate = tmp_path / "plate" + outcome, output = save_archive_outcome( + publication_context, plate, destination, monkeypatch + ) + plan = plan_for(plate) + targets = OpenHCSMetadataWriter.OutputTarget.for_execution( + publication_context, + plan, + artifact_materializations=(outcome,), + ) + assert tuple(target.output_dir for target in targets) == (plate / destination,) + OpenHCSMetadataWriter.write( + publication_context, plan, artifact_materializations=(outcome,) + ) + assert result_paths(plate, publication_context) == (output.path,) + published = json.loads(METADATA_CONFIG.metadata_path(plate).read_text()) + assert published["subdirectories"][destination]["image_files"] == [] + assert published["subdirectories"][destination]["source_projection"] == [] + # Final reconciliation has no runtime outcome/value to reconstruct. Its + # destination comes from the same durable declaration the reader admitted. + publication_context.step_plans = {0: plan} + owner = RuntimeArtifactMetadataTarget.from_plan(plan) + reconciled = tuple( + target.output_dir + for target in owner.reconciliation_targets(publication_context) + ) + assert plate / destination in reconciled + OpenHCSMetadataWriter.finalize_completed_plate({"A01": publication_context}) + assert result_paths(plate, publication_context) == (output.path,) + + +def test_shared_writer_accepts_result_only_but_not_unaddressed_images( + tmp_path, + publication_context, +): + plate = tmp_path / "plate" + directory = plate / "standalone_result" + directory.mkdir(parents=True) + roi = directory / "independent-outline.roi.zip" + roi.write_bytes(b"synthetic admission only") + target = RuntimeArtifactMetadataTarget.from_plan(plan_for(plate)).for_directory( + directory + ) + target.write(publication_context) + assert result_paths(plate, publication_context) == (str(roi),) + metadata_path = METADATA_CONFIG.metadata_path(plate) + before = metadata_path.read_bytes() + (directory / "unaddressed-image.tif").write_bytes(b"never decoded") + with pytest.raises( + MetadataWriteError, match="Saved images lack typed produced addresses" + ): + target.write(publication_context) + assert metadata_path.read_bytes() == before + + +def test_new_result_declaration_composes_cooperative_hooks_without_consumer_edits( + tmp_path, + publication_context, + monkeypatch, +): + plate = tmp_path / "plate" + destination = "new/declaration_only" + outcome, output = save_archive_outcome( + publication_context, plate, destination, monkeypatch + ) + registry = OpenHCSMetadataWriter.OutputTarget.__registry__ + original_declarations = dict(registry) + try: + + class IndependentSavedResultMetadataTarget( + PublicationAuditCapability, + ProducedImageMetadataCapability, + RuntimeArtifactMetadataTarget, + ): + declaration_key = None + + @classmethod + def from_plan(cls, plan): + return cls( + output_dir=plate / destination, + backend=Backend.DISK.value, + plate_root=str(plate), + sub_dir=destination, + results_dir=str(plate / destination), + ) + + plan = replace( + plan_for(plate), + runtime_artifact_materialization=RuntimeArtifactMaterializationPlan(), + ) + (target,) = OpenHCSMetadataWriter.OutputTarget.for_execution( + publication_context, + plan, + artifact_materializations=(outcome,), + ) + target.write(publication_context, produced_plan=plan) + assert publication_context.publication_audit == [ + ("before", destination), + ("after", destination), + ] + assert result_paths(plate, publication_context) == (output.path,) + finally: + registry.clear() + registry.update(original_declarations) + + +def test_files_without_successful_batch_outcomes_do_not_create_step_targets( + tmp_path, + publication_context, +): + plate = tmp_path / "plate" + directory = plate / "unclaimed" + directory.mkdir(parents=True) + (directory / "not_a_saved_outcome.roi.zip").write_bytes(b"synthetic admission only") + assert ( + OpenHCSMetadataWriter.OutputTarget.for_execution( + publication_context, + plan_for(plate), + artifact_materializations=(), + ) + == () + ) From 77a5f0a4b2f1116a988e79d675de2a8b5ae61864 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 01:06:16 -0400 Subject: [PATCH 039/235] Prepare canonical and raw callable contracts before readiness and compilation (#419) --- ...pared_callable_signatures_419_20261002.rst | 54 +++ openhcs/core/callable_contract.py | 267 ++++++++--- openhcs/core/function_contract_metadata.py | 2 + openhcs/core/function_patterns.py | 2 +- openhcs/core/pipeline/function_contracts.py | 17 +- openhcs/core/runtime_batch_contracts.py | 13 +- openhcs/core/steps/function_runtime.py | 4 - .../runtime/function_contract_execution.py | 63 ++- .../backends/cellprofiler/classification.py | 2 +- .../backends/cellprofiler/colocalization.py | 8 +- .../processing/backends/cellprofiler/color.py | 26 +- .../backends/cellprofiler/display_modules.py | 3 +- .../processing/backends/cellprofiler/edge.py | 3 +- .../cellprofiler/feature_enhancement.py | 3 +- .../backends/cellprofiler/gaussian_filter.py | 3 +- .../backends/cellprofiler/illumination.py | 2 +- .../backends/cellprofiler/image_geometry.py | 10 +- .../backends/cellprofiler/intensity.py | 3 +- .../cellprofiler/intensity_distribution.py | 4 +- .../backends/cellprofiler/median_filter.py | 3 +- .../backends/cellprofiler/morphology.py | 7 +- .../backends/cellprofiler/outlines.py | 10 +- .../backends/cellprofiler/primary_objects.py | 3 +- .../backends/cellprofiler/secondary.py | 7 +- .../backends/cellprofiler/skeleton.py | 2 +- .../backends/cellprofiler/smoothing.py | 7 +- .../processing/backends/cellprofiler/worms.py | 2 +- .../backends/lib_registry/registry_service.py | 9 +- .../backends/lib_registry/unified_registry.py | 35 +- tests/unit/test_callable_abi_preparation.py | 306 ++++++++++++ ...ellprofiler_function_contract_execution.py | 444 +++++++++++++++++- .../test_cellprofiler_prepared_payload_abi.py | 127 +++++ tests/unit/test_preparation_admission.py | 4 +- 33 files changed, 1293 insertions(+), 162 deletions(-) create mode 100644 docs/validation/prepared_callable_signatures_419_20261002.rst create mode 100644 tests/unit/test_callable_abi_preparation.py create mode 100644 tests/unit/test_cellprofiler_prepared_payload_abi.py diff --git a/docs/validation/prepared_callable_signatures_419_20261002.rst b/docs/validation/prepared_callable_signatures_419_20261002.rst new file mode 100644 index 0000000000..e987dab3b9 --- /dev/null +++ b/docs/validation/prepared_callable_signatures_419_20261002.rst @@ -0,0 +1,54 @@ +Prepared callable signatures and raw argument admission +====================================================== + +Registered argument contracts belong in function-library warmup before server +readiness. Newly authored declarations prepare before compiler validation and +resolve once per compilation. Invocation must consume those prepared contracts. +This is a correctness/preparation dependency of the plumbing work, not a claim +that signature caching alone removes multiple seconds. + +CallableMetadata carries the resolved canonical inspect.Signature. Parameter +names, annotations, defaults and semantic controls derive from that declaration. +Preparation publishes it only after every registered preparation hook finishes. +FunctionReference and existing namespace/pickle/cloudpickle transport retain it. +The compiler reuses registered snapshots and captures fresh authored snapshots; +the old late invocation preparation is removed. + +Actual boundary inspection found that most CP raw targets differ from their +semantic signatures because injected controls are absent from the raw function. +The existing metadata owner therefore also carries a raw-runtime Signature only +when required by that distinct boundary. Identical boundaries reuse canonical; +default objects are retained by identity and never compared through value equality. +Existing invocation and batch-request owners receive the exact raw view for +filtering/defaults. Three independent signature/default/type LRUs and the obsolete +signature-parameter helper are removed. Unprepared authoring and distinct generic +wrapper queries remain live. Explicit library rewarm or supported declaration +invalidation refreshes registered snapshots; arbitrary direct annotation/default +mutation does not retroactively rewrite compiled contracts. + +Raw array admission is derived from the callable's declared nominal type, including +unions and Annotated. The adapter retains context until the actual raw boundary. +Array-only callables receive pixels; declared RuntimeArrayData consumers retain +metadata. Their truthful annotation migration changes no scientific function body, +defaults or decorators, as checked across all 19 affected CP modules by AST. + +Controls: 646 passed, 5 existing skips, 2 existing warnings. They cover preparation +ordering, rewarm/new compilation, request bindings, custom signatures/defaults, +transport, and 12-plane compiled execution with all signature/hint queries forbidden. +An additional 75 publication/real-payload controls pass, including actual compiled +ColorToGray spatial-mask/channel collapse, MedianFilter intensity scale/mask and +IdentifySecondaryObjects source coordinates/provenance. + +The final real isolated registry check prepares 267 catalog entries and 976 targets; +all 267 contracts have snapshots, with zero missing signatures and zero subsequent +owner signature/hint resolutions in contract queries/projections. Its 16.67s +cached-startup duration is diagnostic startup and excluded from pipeline clocks. +The earlier fresh-cache 91.01s startup check remains separately preserved. + +Evidence is retained under /home/ts/code/projects/openhcs-benchmark-runs: +issue419-shared-signature-closure-final-receipt-20261002.json, +issue419-actual-shared-signature-closure-readiness-20261002.json and +issue419-truthful-annotation-actual-source-consumers-20261002.json. Earlier failed +intermediates are retained. Current all-case scientific parity and ordinary +performance are separate pending gates; earlier 90c30 timings do not qualify this +new source or updated shared dependencies. diff --git a/openhcs/core/callable_contract.py b/openhcs/core/callable_contract.py index d6de5863a5..d462097d29 100644 --- a/openhcs/core/callable_contract.py +++ b/openhcs/core/callable_contract.py @@ -22,6 +22,7 @@ Mapping, TypeVar, cast, + get_args, get_type_hints, overload, ) @@ -33,6 +34,7 @@ coerce_enum_member, declared_enum_type, enum_member_type, + is_union_type, resolve_annotated, validate_annotation_value, ) @@ -292,9 +294,28 @@ class CallableMetadata: image_payload_consumption: ImagePayloadConsumption = ImagePayloadConsumption.NATURAL request_binding: "CallableRequestBinding | None" = None prepare: Callable[..., object] | None = None + canonical_signature: inspect.Signature | None = None + raw_runtime_signature: inspect.Signature | None = None primary_image_carrier_requirement: PrimaryImageCarrierRequirement | None = None primary_image_carrier_transition: PrimaryImageCarrierTransition | None = None + @staticmethod + def prepared_callable_signature(func: Callable[..., object]) -> inspect.Signature | None: + """Admit an owned snapshot only for its exact semantic callable boundary.""" + raw = getattr(func, FunctionContractAttribute.raw_processing_function, None) + if raw is not None and raw is not func: + return None + signature = getattr(func, FunctionContractAttribute.canonical_signature, None) + if signature is not None and not isinstance(signature, inspect.Signature): + raise TypeError("Prepared callable signature must be an inspect.Signature or None.") + return signature + + @classmethod + def callable_signature(cls, func: Callable[..., object]) -> inspect.Signature: + """Read the prepared exact ABI; unprepared wrapper/authoring views stay live.""" + signature = cls.prepared_callable_signature(func) + return inspect.signature(func) if signature is None else signature + @property def artifact_output_policy(self) -> type[ArtifactOutputPolicy]: """Project recording ownership from this callable's adapter declaration.""" @@ -378,13 +399,14 @@ def from_projection( runtime_adapter = runtime_adapter_spec_from_callable(projection.func) if runtime_adapter is not None and callable(projection.func): runtime_adapter.validate_callable_signature(projection.func) + declared_artifact_inputs = _artifact_specs_from_namespace( + namespace, projection.name, FunctionContractAttribute.artifact_inputs, + ) + signature = ( + cls.callable_signature(projection.func) if callable(projection.func) else None + ) artifact_inputs = _artifact_input_specs_from_projection( - projection, - _artifact_specs_from_namespace( - namespace, - projection.name, - FunctionContractAttribute.artifact_inputs, - ), + projection, declared_artifact_inputs, signature, ) return cls( input_memory_type=reader.optional_string( @@ -401,6 +423,7 @@ def from_projection( projection, reader, artifact_inputs, + signature, ), artifact_outputs=_artifact_specs_from_namespace( namespace, @@ -422,7 +445,7 @@ def from_projection( FunctionContractAttribute.allowed_group_by, ), runtime_adapter=runtime_adapter, - runtime_context_parameter=_runtime_context_parameter(projection, reader), + runtime_context_parameter=_runtime_context_parameter(projection, reader, signature), execution_scope=reader.optional_execution_scope( FunctionContractAttribute.execution_scope, ), @@ -457,6 +480,12 @@ def from_projection( prepare=reader.optional_callable( FunctionContractAttribute.processing_prepare ), + canonical_signature=reader.optional_signature( + FunctionContractAttribute.canonical_signature, + ), + raw_runtime_signature=reader.optional_signature( + FunctionContractAttribute.raw_runtime_signature, + ), ) def without_prepare(self) -> "CallableMetadata": @@ -476,6 +505,10 @@ def with_raw_processing_function( def as_namespace(self) -> dict[str, object]: """Project typed metadata into callable declaration keys.""" namespace: dict[str, object] = {} + if self.canonical_signature is not None: + namespace[FunctionContractAttribute.canonical_signature] = self.canonical_signature + if self.raw_runtime_signature is not None: + namespace[FunctionContractAttribute.raw_runtime_signature] = self.raw_runtime_signature if self.input_memory_type is not None: MemoryContractAttribute.INPUT.write(namespace, self.input_memory_type) if self.output_memory_type is not None: @@ -777,10 +810,7 @@ def config_bound_parameters(self) -> tuple[inspect.Parameter, ...]: from openhcs.core.config import runtime_config_parameter - signature = inspect.signature( - self.resolve_canonical_raw_callable(), - eval_str=True, - ) + signature = self.canonical_signature return tuple( normalized for parameter in signature.parameters.values() @@ -843,7 +873,7 @@ def declared_path_values( ) -> Mapping[str, tuple["PlatePathDeclaration", object]]: """Return authored or signature-default values for declared paths.""" - signature = inspect.signature(self.resolve_canonical_raw_callable()) + signature = self.canonical_signature values: dict[str, tuple["PlatePathDeclaration", object]] = {} for parameter_name, declaration in self.declared_path_parameters.items(): value = kwargs.get(parameter_name, signature.parameters[parameter_name].default) @@ -923,17 +953,49 @@ def require_processing_contract(self) -> "ProcessingContract": ) return processing_contract - def raw_main_flow_call_argument(self, source_payload: Any) -> Any: + def runtime_main_flow_call_argument(self, source_payload: Any) -> Any: """Project this callable's ABI without discarding an adapter's context.""" - from arraybridge import ArrayPayload - from openhcs.core.runtime_image_values import image_payload_data - if self.runtime_adapter is not None: return source_payload - annotation = self.canonical_parameter_annotations.get(self.primary_input_parameter_name) - if isinstance(annotation, type) and issubclass(annotation, ArrayPayload): - return source_payload - return image_payload_data(source_payload) + return self.raw_main_flow_call_argument(source_payload) + + def raw_main_flow_call_argument( + self, source_payload: Any, + ) -> Any: + """Project one image argument at the canonical raw callable boundary.""" + from openhcs.core.runtime_image_values import image_payload_data + + return ( + source_payload + if any( + isinstance(source_payload, argument_type) + for argument_type in self.raw_main_flow_argument_types() + ) + else image_payload_data(source_payload) + ) + + def raw_main_flow_argument_types(self) -> tuple[type, ...]: + """Derive the nominal argument view for one canonical invocation.""" + parameters = self.canonical_parameter_declarations + primary_name = self._primary_input_name(parameters) + annotation = ( + None if primary_name is None else parameters[primary_name].annotation + ) + return self._nominal_argument_types(annotation) + + @classmethod + def _nominal_argument_types(cls, annotation: object) -> tuple[type, ...]: + annotation = resolve_annotated(annotation) + if is_union_type(annotation): + return tuple( + argument_type for member in get_args(annotation) + for argument_type in cls._nominal_argument_types(member) + ) + return ( + (annotation,) + if annotation is not Any and isinstance(annotation, type) + else () + ) def main_flow_call_argument(self, source_payload: Any) -> Any: """Let the processing declaration retain context needed before raw calls.""" @@ -1001,8 +1063,12 @@ def artifact_specs(self) -> ArtifactSpecCollection: @property def primary_input_parameter_name(self) -> str | None: """FunctionStep input payload parameter declared by callable signature.""" - signature = inspect.signature(self.resolve_canonical_raw_callable()) - for parameter in signature.parameters.values(): + signature = self.canonical_signature + return self._primary_input_name(signature.parameters) + + @staticmethod + def _primary_input_name(parameters: Mapping[str, inspect.Parameter]) -> str | None: + for parameter in parameters.values(): if parameter.kind in ( inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD, @@ -1089,14 +1155,102 @@ def is_request_boundary(candidate: Callable[..., object]) -> bool: @property def canonical_parameter_annotations(self) -> Mapping[str, object]: """Derive annotations from the original semantic signature owner.""" - - raw_callable = self.resolve_canonical_raw_callable() - annotations = get_type_hints(raw_callable, include_extras=True) return { - name: annotations.get(name, parameter.annotation) - for name, parameter in inspect.signature(raw_callable).parameters.items() + name: parameter.annotation + for name, parameter in self.canonical_parameter_declarations.items() } + @property + def canonical_parameter_declarations(self) -> Mapping[str, inspect.Parameter]: + """Resolve signature and annotations together at their declaration owner.""" + return self.canonical_signature.parameters + + @property + def canonical_signature(self) -> inspect.Signature: + """Use the prepared declaration, or query an uncompiled authoring callable.""" + if self.metadata.canonical_signature is not None: + return self.metadata.canonical_signature + return self.resolve_signature(self.resolve_canonical_raw_callable()) + + def resolve_signature(self, raw_callable: Callable[..., object]) -> inspect.Signature: + """Resolve the semantic ABI once after declaration preparation.""" + signature = inspect.signature(raw_callable) + annotations = ( + self.request_binding.public_annotations_dict + if self.request_binding is not None + else get_type_hints(raw_callable, include_extras=True) + ) + return signature.replace( + parameters=tuple( + parameter.replace(annotation=annotations.get(name, parameter.annotation)) + for name, parameter in signature.parameters.items() + ), + return_annotation=annotations.get("return", signature.return_annotation), + ) + + def with_prepared_signature(self) -> "CallableContract": + """Capture the prepared ABI on the declaration transported to workers.""" + canonical = self.resolve_canonical_raw_callable() + signature = self.resolve_signature(canonical) + runtime = self.resolve_raw_runtime_callable() + runtime_signature = None + if runtime is not canonical: + candidate = self.resolve_signature(runtime) + canonical_parameters = tuple(signature.parameters.values()) + runtime_parameters = tuple(candidate.parameters.values()) + same_parameters = len(canonical_parameters) == len(runtime_parameters) and all( + left.name == right.name and left.kind is right.kind + and left.default is right.default and left.annotation is right.annotation + for left, right in zip(canonical_parameters, runtime_parameters) + ) + if not (same_parameters and signature.return_annotation is candidate.return_annotation): + runtime_signature = candidate + return dataclasses.replace( + self, + metadata=dataclasses.replace( + self.metadata, canonical_signature=signature, + raw_runtime_signature=runtime_signature, + ), + ) + + @property + def raw_runtime_signature(self) -> inspect.Signature: + """The exact unwrapped execution ABI, preserving request boundaries.""" + if self.metadata.raw_runtime_signature is not None: + return self.metadata.raw_runtime_signature + if self.metadata.canonical_signature is not None: + return self.metadata.canonical_signature + return self.resolve_signature(self.resolve_raw_runtime_callable()) + + @classmethod + def warm_canonical_signature(cls, func: Callable[..., object]) -> None: + """Publish one refreshed ABI after all library preparation hooks finish.""" + contract = cls.from_callable(func).with_prepared_signature() + _mutable_callable_namespace(func)[FunctionContractAttribute.canonical_signature] = ( + contract.metadata.canonical_signature + ) + namespace = _mutable_callable_namespace(func) + namespace.pop(FunctionContractAttribute.raw_runtime_signature, None) + if contract.metadata.raw_runtime_signature is not None: + namespace[FunctionContractAttribute.raw_runtime_signature] = contract.metadata.raw_runtime_signature + + @classmethod + def from_prepared_callable(cls, func: Callable[..., object]) -> "CallableContract": + """Reuse library warmup; prepare authored declarations before validation.""" + projection = CallableProjection.from_callable(func) + if projection.namespace.get(FunctionContractAttribute.canonical_signature) is not None: + return cls.from_callable(func) + resolved = _resolve_declared_callable(func) + for target in CallableProjection.from_callable(resolved).prepare_targets(): + prepare_processing_callable(target) + prepared = cls.from_callable(resolved) + if prepared.metadata.canonical_signature is None: + prepared = prepared.with_prepared_signature() + return dataclasses.replace( + prepared, func=func, function_name=projection.name, + module_name=projection.module_name, + ) + def decode_public_kwargs(self, kwargs: Mapping[str, object]) -> dict[str, object]: """Descend declared scalar enums at authoring, before source projection.""" @@ -1123,7 +1277,7 @@ def validate_public_kwargs( raise TypeError( "CallableContract.validate_public_kwargs requires a mapping." ) - signature = inspect.signature(self.resolve_canonical_raw_callable()) + signature = self.canonical_signature runtime_owned_value = object() call_kwargs = dict(kwargs) runtime_loaded_parameters = frozenset(runtime_loaded_artifact_parameter_names) @@ -1194,15 +1348,19 @@ def validate_artifact_input_parameter_bindings(self) -> None: """Validate exact artifact occurrences against the normalized callable ABI.""" from openhcs.core.pipeline.function_contracts import ( - resolved_callable_parameter, special_input_parameter_accepts_sequence, ) - raw_callable = self.resolve_canonical_raw_callable() - artifact_parameters = { - parameter_name: resolved_callable_parameter(raw_callable, parameter_name) - for parameter_name in self.artifact_input_parameter_names - } + artifact_parameters = {} + parameters = self.canonical_parameter_declarations + for parameter_name in self.artifact_input_parameter_names: + parameter = parameters.get(parameter_name) + if parameter is None: + raise ValueError( + f"Callable {self.function_name!r} does not declare parameter " + f"{parameter_name!r}." + ) + artifact_parameters[parameter_name] = parameter specs_by_parameter: dict[str, list[ArtifactSpec]] = {} for spec in self.artifact_inputs: parameter_name = spec.parameter_name @@ -1419,6 +1577,8 @@ def wrapper(*args: Any, **kwargs: Any) -> Any: return func(**binding.implementation_kwargs(bound.arguments)) wrapper_namespace = _mutable_callable_namespace(wrapper) + wrapper_namespace.pop(FunctionContractAttribute.canonical_signature, None) + wrapper_namespace.pop(FunctionContractAttribute.raw_runtime_signature, None) wrapper_namespace[FunctionContractAttribute.callable_request_binding] = binding wrapper_namespace["__signature__"] = public_signature return wrapper @@ -1473,6 +1633,8 @@ def attach_callable_contract_metadata( primary_image_carrier_transition: PrimaryImageCarrierTransition | None = None, ) -> None: """Attach OpenHCS callable metadata used by compiler/runtime phases.""" + _mutable_callable_namespace(func).pop(FunctionContractAttribute.canonical_signature, None) + _mutable_callable_namespace(func).pop(FunctionContractAttribute.raw_runtime_signature, None) if declared_processing_contract is not None: if ( not isinstance(declared_processing_contract, str) @@ -1649,9 +1811,10 @@ def attach_processing_prepare(func: Any, prepare: Any) -> None: f"got {type(prepare).__name__}." ) for target in CallableProjection.from_callable(func).prepare_targets(): - _mutable_callable_namespace(target)[ - FunctionContractAttribute.processing_prepare - ] = prepare + namespace = _mutable_callable_namespace(target) + namespace.pop(FunctionContractAttribute.canonical_signature, None) + namespace.pop(FunctionContractAttribute.raw_runtime_signature, None) + namespace[FunctionContractAttribute.processing_prepare] = prepare def runtime_image_execution_mode( @@ -1813,6 +1976,7 @@ def prepare_targets(self) -> tuple[Any, ...]: def _runtime_context_parameter( projection: CallableProjection, reader: "CallableMetadataReader", + signature: inspect.Signature | None, ) -> str | None: declared = reader.optional_string( FunctionContractAttribute.runtime_context_parameter @@ -1821,19 +1985,8 @@ def _runtime_context_parameter( return declared from openhcs.core.context.processing_context import ProcessingContext - return _callable_signature_parameter( - projection.func, - ProcessingContext.require_parameter_name(), - ) - - -def _callable_signature_parameter(func: Any, parameter_name: str) -> str | None: - """Return a callable parameter name accepted by live signatures only.""" - if not callable(func): - return None - if parameter_name not in inspect.signature(func).parameters: - return None - return parameter_name + parameter_name = ProcessingContext.require_parameter_name() + return parameter_name if signature is not None and parameter_name in signature.parameters else None @dataclass(frozen=True, slots=True) @@ -1843,6 +1996,12 @@ class CallableMetadataReader: namespace: CallableNamespace function_name: str + def optional_signature(self, field_name: str) -> inspect.Signature | None: + value = self.namespace.get(field_name) + if value is not None and not isinstance(value, inspect.Signature): + raise TypeError(f"{self.function_name!r}.{field_name} must be an inspect.Signature or None.") + return value + def optional_string(self, field_name: str) -> str | None: """Return an optional string metadata field.""" value = self.namespace.get(field_name) @@ -2193,12 +2352,12 @@ def _artifact_input_parameter_names( def _artifact_input_specs_from_projection( projection: CallableProjection, artifact_inputs: tuple[ArtifactSpec, ...], + signature: inspect.Signature | None, ) -> tuple[ArtifactSpec, ...]: """Bind exact same-name input declarations at the metadata boundary.""" - if not callable(projection.func): + if signature is None: return artifact_inputs - signature = inspect.signature(projection.func) primary_parameter_name = next( ( parameter.name @@ -2228,6 +2387,7 @@ def _artifact_input_parameter_names_from_projection( projection: CallableProjection, reader: CallableMetadataReader, artifact_inputs: tuple[ArtifactSpec, ...], + signature: inspect.Signature | None, ) -> tuple[str, ...]: """Normalize compatibility and exact artifact parameter declarations once.""" @@ -2281,9 +2441,8 @@ def _artifact_input_parameter_names_from_projection( f"disagree: {declarations}." ) - if not callable(projection.func): + if signature is None: return first_names - signature = inspect.signature(projection.func) ordered_names = tuple(name for name in signature.parameters if name in first_set) return ( *ordered_names, diff --git a/openhcs/core/function_contract_metadata.py b/openhcs/core/function_contract_metadata.py index b269577cb8..09713a46ce 100644 --- a/openhcs/core/function_contract_metadata.py +++ b/openhcs/core/function_contract_metadata.py @@ -16,6 +16,8 @@ class FunctionContractAttribute: "__openhcs_declared_processing_contract__" ) raw_processing_function: ClassVar[str] = "__openhcs_raw_processing_function__" + canonical_signature: ClassVar[str] = "__openhcs_canonical_signature__" + raw_runtime_signature: ClassVar[str] = "__openhcs_raw_runtime_signature__" processing_prepare: ClassVar[str] = "__openhcs_prepare__" runtime_adapter: ClassVar[str] = "__runtime_adapter__" runtime_image_execution_mode: ClassVar[str] = ( diff --git a/openhcs/core/function_patterns.py b/openhcs/core/function_patterns.py index b79b4fc695..f41e9ad424 100644 --- a/openhcs/core/function_patterns.py +++ b/openhcs/core/function_patterns.py @@ -1265,7 +1265,7 @@ def normalize( func, kwargs = _split_function_item(item) if RUNTIME_CALLABLE_KWARG_POLICY.item_is_disabled(kwargs): continue - contract = CallableContract.from_callable(func) + contract = CallableContract.from_prepared_callable(func) position = len(normalized_items) normalized_items.append( NormalizedFunctionItem( diff --git a/openhcs/core/pipeline/function_contracts.py b/openhcs/core/pipeline/function_contracts.py index b75413a3a9..413f266de8 100644 --- a/openhcs/core/pipeline/function_contracts.py +++ b/openhcs/core/pipeline/function_contracts.py @@ -3,7 +3,6 @@ from collections.abc import Sequence from dataclasses import replace from enum import Enum -from functools import lru_cache import inspect from types import UnionType from typing import ( @@ -43,11 +42,19 @@ F = TypeVar("F", bound=Callable) -@lru_cache(maxsize=256) def resolved_callable_type_hints(func: Callable) -> dict[str, Any]: """Return the callable's resolved type contract or propagate its error.""" - return get_type_hints(func) + signature = CallableMetadata.prepared_callable_signature(func) + if signature is None: + return get_type_hints(func) + from typing import _strip_annotations + + return { + **{name: _strip_annotations(parameter.annotation) for name, parameter in signature.parameters.items() + if parameter.annotation is not inspect.Parameter.empty}, + **({} if signature.return_annotation is inspect.Signature.empty else {"return": _strip_annotations(signature.return_annotation)}), + } def annotation_accepts_runtime_type(annotation: object, value_type: type[Any]) -> bool: @@ -101,7 +108,7 @@ def resolved_callable_parameter( ) -> inspect.Parameter: """Return one callable parameter with its runtime type hint resolved.""" - signature = inspect.signature(func) + signature = CallableMetadata.callable_signature(func) parameter = signature.parameters.get(parameter_name) if parameter is None: raise ValueError( @@ -429,7 +436,7 @@ def special_input_parameters_from_callable( """Return special-input parameters in their canonical signature order.""" declared_names = special_input_names_from_callable(func) - signature = inspect.signature(func) + signature = CallableMetadata.callable_signature(func) missing = tuple(name for name in declared_names if name not in signature.parameters) if missing: raise ValueError( diff --git a/openhcs/core/runtime_batch_contracts.py b/openhcs/core/runtime_batch_contracts.py index 6933013d51..cf48ff9032 100644 --- a/openhcs/core/runtime_batch_contracts.py +++ b/openhcs/core/runtime_batch_contracts.py @@ -6,14 +6,13 @@ from collections.abc import Callable, Hashable, Mapping from dataclasses import dataclass, replace from enum import Enum -from functools import lru_cache import inspect from types import MappingProxyType from typing import ClassVar, Generic, TypeVar from metaclass_registry import AutoRegisterMeta -from openhcs.core.callable_contract import KeywordRuntimeParameter +from openhcs.core.callable_contract import CallableMetadata, KeywordRuntimeParameter from openhcs.core.aligned_image_payload import ( AlignedImageStack, ImagePayloadExecutionMode, @@ -48,11 +47,12 @@ class RuntimePlaneAxisValueProjectionParameter(KeywordRuntimeParameter): parameter_default = None -@lru_cache(maxsize=1024) -def runtime_callable_defaults(func: Callable[..., object]) -> Mapping[str, object]: +def runtime_callable_defaults( + func: Callable[..., object], *, signature: inspect.Signature | None = None, +) -> Mapping[str, object]: """Return callable defaults visible to runtime batch executors.""" try: - callable_signature = inspect.signature(func) + callable_signature = CallableMetadata.callable_signature(func) if signature is None else signature except (TypeError, ValueError) as exc: raise TypeError( f"Runtime batch function {func!r} must expose an inspectable signature." @@ -190,6 +190,7 @@ class RuntimePure2DSliceBatchRequest( ], RuntimeSliceResultT, ] + signature: inspect.Signature | None = None def __post_init__(self) -> None: object.__setattr__( @@ -197,7 +198,7 @@ def __post_init__(self) -> None: "kwargs", MappingProxyType( { - **runtime_callable_defaults(self.func), + **runtime_callable_defaults(self.func, signature=self.signature), **dict(self.kwargs), } ), diff --git a/openhcs/core/steps/function_runtime.py b/openhcs/core/steps/function_runtime.py index 948d937ec2..21f0ea6762 100644 --- a/openhcs/core/steps/function_runtime.py +++ b/openhcs/core/steps/function_runtime.py @@ -43,7 +43,6 @@ ) from openhcs.core.callable_contract import ( CallableRuntimeCacheKey, - prepare_processing_callable, ) from openhcs.core.component_group_scope import ( ComponentGroupScope, @@ -245,9 +244,6 @@ class FunctionInvocationCallableResolver: def prepare(cls, invocation: CompiledFunctionInvocation) -> None: """Resolve and cache one invocation callable before timed execution.""" cls.resolve(invocation) - prepare_processing_callable( - invocation.contract.resolve_canonical_raw_callable() - ) @classmethod def resolve(cls, invocation: CompiledFunctionInvocation) -> Callable: diff --git a/openhcs/interop/cellprofiler/runtime/function_contract_execution.py b/openhcs/interop/cellprofiler/runtime/function_contract_execution.py index 7ce0d2275a..76ab77ecca 100644 --- a/openhcs/interop/cellprofiler/runtime/function_contract_execution.py +++ b/openhcs/interop/cellprofiler/runtime/function_contract_execution.py @@ -57,6 +57,7 @@ RuntimeCallablePolicy, RuntimeCallableView, RuntimeInvocationKwargPolicy, + contextualize_main_image_output, ) _CELLPROFILER_RUNTIME_CALLABLE_POLICY = RuntimeCallablePolicy( @@ -75,6 +76,21 @@ def __init__( ) -> None: self.plane_projection = plane_projection + def invoke_raw( + self, + callable_contract: CallableContract, + func: Callable[..., RuntimeFunctionOutput], + image: RuntimeCallableArgument, + kwargs: RuntimeCallableKwargs, + ) -> RuntimeCallableArgument: + """Project the canonical ABI after the input domain has been selected.""" + return _CELLPROFILER_RUNTIME_CALLABLE_POLICY.invocation( + func, + (callable_contract.raw_main_flow_call_argument(image),), + kwargs, + signature=callable_contract.raw_runtime_signature, + ).call() + def execute( self, callable_contract: CallableContract, @@ -135,7 +151,7 @@ def execute( runtime_kwargs.update( processing_contract.declaration.consume_semantic_controls( runtime_kwargs, - func=func, + parameters=callable_contract.canonical_signature.parameters, ) ) result = processing_contract.execute( @@ -211,6 +227,7 @@ def execute_pure_2d_slice_batch( slices_2d=slices_2d, kwargs=kwargs, execute_slice=execute_slice, + signature=callable_contract.raw_runtime_signature, ) if slice_request.slice_count <= 0: return [], 0.0 @@ -261,11 +278,12 @@ def execute_pure_3d( ), ) call_started_at = time.perf_counter() - result = _CELLPROFILER_RUNTIME_CALLABLE_POLICY.invocation( + result = self.invoke_raw( + callable_contract, func, - (projected_image,), + projected_image, projected_kwargs, - ).call() + ) CellProfilerRuntimeProfileLogger.log_module_profile( "cp_full_stack_raw_call", time.perf_counter() - call_started_at, @@ -339,16 +357,17 @@ def execute_aligned_stack_slice( slice_index: int, slice_count: int, ) -> RuntimeCallableArgument: - return _CELLPROFILER_RUNTIME_CALLABLE_POLICY.invocation( + return self.invoke_raw( + callable_contract, slice_func, - (slice_payload,), + slice_payload, aligned_image_stack_kwargs( slice_kwargs, slice_index, slice_count, reference_payload=slice_payload, ), - ).call() + ) slice_results, _slice_execute_seconds = self.execute_pure_2d_slice_batch( callable_contract, @@ -457,11 +476,12 @@ def execute_pure_2d( ) image_data = image_payload_data(image) if not isinstance(image_data, np.ndarray): - return _CELLPROFILER_RUNTIME_CALLABLE_POLICY.invocation( + return self.invoke_raw( + callable_contract, func, - (image,), + image, kwargs, - ).call() + ) prepare_started_at = time.perf_counter() memory_type = detect_memory_type(image_data) @@ -484,11 +504,12 @@ def execute_pure_2d( "declared plane projection. Kwargs cannot create image-axis " "execution semantics." ) - return _CELLPROFILER_RUNTIME_CALLABLE_POLICY.invocation( + return self.invoke_raw( + callable_contract, func, - (image,), + image, kwargs, - ).call() + ) declared_plane_axis = image_payload_metadata(image).plane_axis if declared_plane_axis is not self.plane_projection.axis: raise RuntimeSliceProjectionDeclarationError( @@ -588,12 +609,12 @@ def execute_volumetric_to_slice( callable_contract: CallableContract, **kwargs: RuntimeCallableArgument, ) -> RuntimeCallableArgument: - del callable_contract - result_2d = _CELLPROFILER_RUNTIME_CALLABLE_POLICY.invocation( + result_2d = self.invoke_raw( + callable_contract, func, - (image,), + image, kwargs, - ).call() + ) result_data = image_payload_data(result_2d) result_mask = image_payload_mask(result_2d) result_metadata = image_payload_metadata(result_2d) @@ -621,11 +642,13 @@ def execute_pure_2d_slice( sliced_kwargs = dict(sliced_kwargs) slice_index_name = SliceIndexRuntimeParameter.require_parameter_name() sliced_kwargs[slice_index_name] = slice_index - return _CELLPROFILER_RUNTIME_CALLABLE_POLICY.invocation( + result = self.invoke_raw( + callable_contract, func, - (slice_2d,), + slice_2d, sliced_kwargs, - ).call() + ) + return contextualize_main_image_output(slice_2d, result) def slice_pure_2d_kwargs( self, diff --git a/openhcs/processing/backends/cellprofiler/classification.py b/openhcs/processing/backends/cellprofiler/classification.py index 4783e7e795..e7e22bd10a 100644 --- a/openhcs/processing/backends/cellprofiler/classification.py +++ b/openhcs/processing/backends/cellprofiler/classification.py @@ -2028,7 +2028,7 @@ def object_classification_backend( SliceIndexRuntimeParameter, ) def classify_objects_single_measurement( - image: np.ndarray, + image: RuntimeArrayData, labels: ObjectLabelValue, measurement_feature: str = "", measurement_values: np.ndarray | None = None, diff --git a/openhcs/processing/backends/cellprofiler/colocalization.py b/openhcs/processing/backends/cellprofiler/colocalization.py index 12e4d220ff..75b4ae097e 100644 --- a/openhcs/processing/backends/cellprofiler/colocalization.py +++ b/openhcs/processing/backends/cellprofiler/colocalization.py @@ -2498,7 +2498,7 @@ def _colocalization_unit_interval_scale( @numpy(contract=ProcessingContract.FLEXIBLE) @runtime_bound_parameters(_ColocalizationThresholdMaskOutputsRuntimeParameter) def measure_colocalization( - image: np.ndarray, + image: RuntimeArrayData, channel_1: int = 0, channel_2: int = 1, threshold_percent: float = 15.0, @@ -2766,7 +2766,7 @@ def _measure_colocalization_objects_core( _ColocalizationCostesThresholdBatchRuntimeParameter, ) def measure_colocalization_objects( - image: np.ndarray, + image: RuntimeArrayData, labels: ObjectLabelValue, measurement_scope: CellProfilerMeasurementTargetScope = CellProfilerMeasurementTargetScope.OBJECT, channel_1: int = 0, @@ -2857,7 +2857,7 @@ def measure_colocalization_objects( def _colocalization_threshold_mask_canonical_output( - image: np.ndarray, + image: RuntimeArrayData, *, threshold_mask_groups: tuple[ColocalizationThresholdMaskGroup, ...], threshold_mask_outputs: tuple[ColocalizationThresholdMaskRuntimeOutput, ...], @@ -2887,7 +2887,7 @@ def _colocalization_threshold_mask_canonical_output( def _colocalization_threshold_mask( - image: np.ndarray, + image: RuntimeArrayData, request: ColocalizationThresholdMaskRuntimeOutput, ) -> RuntimeArrayData: """Apply CellProfiler's whole-image or per-object percentage threshold.""" diff --git a/openhcs/processing/backends/cellprofiler/color.py b/openhcs/processing/backends/cellprofiler/color.py index b7e5ad86ca..3bd92535dd 100644 --- a/openhcs/processing/backends/cellprofiler/color.py +++ b/openhcs/processing/backends/cellprofiler/color.py @@ -1494,7 +1494,7 @@ def run(self, request: GrayToColorRequest) -> np.ndarray: @composed_image_payload @numpy(contract=ProcessingContract.PURE_3D) def gray_to_color( - image: np.ndarray, + image: RuntimeArrayData, color_scheme: GrayToColorModule.Scheme = GrayToColorModule.Scheme.RGB, rescale_intensity: bool = True, red_channel: int = -1, @@ -1593,7 +1593,7 @@ def gray_to_color( ) @numpy(contract=ProcessingContract.FLEXIBLE) def color_to_gray( - image: np.ndarray, + image: RuntimeArrayData, mode: ColorToGrayMode = ColorToGrayMode.SPLIT, image_type: ImageChannelType = ImageChannelType.RGB, channel_indices: tuple[int, ...] = ColorToGrayModule.default_channel_indices, @@ -1634,7 +1634,7 @@ def color_to_gray( def _invert_for_printing_channels( - image: np.ndarray, + image: RuntimeArrayData, *, input_mode: InvertInputMode, use_red_input: bool, @@ -1676,7 +1676,7 @@ def _invert_for_printing_channels( def _invert_for_printing_result( - image: np.ndarray, + image: RuntimeArrayData, *, input_mode: InvertInputMode, use_red_input: bool, @@ -1758,7 +1758,7 @@ def _invert_for_printing_result( @composed_image_payload @numpy(contract=ProcessingContract.PURE_3D) def invert_for_printing( - image: np.ndarray, + image: RuntimeArrayData, input_mode: InvertInputMode = InvertInputMode.COLOR, use_red_input: bool = True, use_green_input: bool = True, @@ -1794,7 +1794,7 @@ def invert_for_printing( @composed_image_payload @numpy(contract=ProcessingContract.PURE_3D) def invert_for_printing_grayscale( - image: np.ndarray, + image: RuntimeArrayData, input_mode: InvertInputMode = InvertInputMode.COLOR, use_red_input: bool = True, use_green_input: bool = True, @@ -1837,7 +1837,7 @@ def invert_for_printing_grayscale( @composed_image_payload @numpy(contract=ProcessingContract.PURE_3D) def invert_for_printing_without_output( - image: np.ndarray, + image: RuntimeArrayData, input_mode: InvertInputMode = InvertInputMode.COLOR, use_red_input: bool = True, use_green_input: bool = True, @@ -1876,7 +1876,7 @@ def invert_for_printing_without_output( def combine_color_to_gray( - image: np.ndarray, + image: RuntimeArrayData, channel_indices: tuple[int, ...], contributions: tuple[float, ...], ) -> np.ndarray: @@ -1892,7 +1892,7 @@ def combine_color_to_gray( return restore_color_to_gray_shape(image, result) -def color_to_gray_combine_output_metadata(image: np.ndarray): +def color_to_gray_combine_output_metadata(image: RuntimeArrayData): """Return metadata for a color-to-grayscale semantic collapse.""" return ( image_payload_metadata(image) @@ -1902,7 +1902,7 @@ def color_to_gray_combine_output_metadata(image: np.ndarray): def split_color_to_gray( - image: np.ndarray, image_type: ImageChannelType, channel_indices: tuple[int, ...] + image: RuntimeArrayData, image_type: ImageChannelType, channel_indices: tuple[int, ...] ) -> tuple[np.ndarray, ...]: color_stack = nhwc_color_stack(image).astype(np.float32) source_stack = ( @@ -1928,7 +1928,7 @@ def color_to_gray_channel(color_stack: np.ndarray, channel_index: int) -> np.nda return color_stack[..., channel_index] -def nhwc_color_stack(image: np.ndarray) -> np.ndarray: +def nhwc_color_stack(image: RuntimeArrayData) -> np.ndarray: """Return NHWC pixels from explicitly declared image layout metadata.""" image_data = np.asarray(image_payload_data(image)) metadata = image_payload_metadata(image) @@ -1959,7 +1959,7 @@ def nhwc_color_stack(image: np.ndarray) -> np.ndarray: return channel_last -def restore_color_to_gray_shape(original: np.ndarray, stack: np.ndarray) -> np.ndarray: +def restore_color_to_gray_shape(original: RuntimeArrayData, stack: np.ndarray) -> np.ndarray: metadata = image_payload_metadata(original) if metadata.is_declared_source_channel_plane(original): if stack.shape[0] != 1: @@ -2017,7 +2017,7 @@ def rgb_to_hsv_stack(rgb_stack: np.ndarray) -> np.ndarray: @numpy(contract=ProcessingContract.FLEXIBLE) def unmix_colors( - image: np.ndarray, + image: RuntimeArrayData, stain_names: Sequence[StainType] = (), custom_absorbances: Sequence[Sequence[float] | None] = (), stain1: StainType = StainType.HEMATOXYLIN, diff --git a/openhcs/processing/backends/cellprofiler/display_modules.py b/openhcs/processing/backends/cellprofiler/display_modules.py index 28c40ce4f4..d5566f0a94 100644 --- a/openhcs/processing/backends/cellprofiler/display_modules.py +++ b/openhcs/processing/backends/cellprofiler/display_modules.py @@ -29,6 +29,7 @@ special_inputs, ) from openhcs.core.registry_strategies import EnumKeyedStrategyMixin +from openhcs.core.runtime_array_values import RuntimeArrayData from openhcs.core.runtime_image_values import ( image_payload_data, image_payload_metadata, @@ -492,7 +493,7 @@ def render_slice(self) -> np.ndarray: @special_inputs("labels") @runtime_bound_parameters(_DisplayMeasurementsRuntimeParameter) def display_data_on_image( - image: np.ndarray, + image: RuntimeArrayData, labels: Optional[ObjectLabelValue] = None, measurements: Optional[np.ndarray] = None, measurement_feature: Optional[str] = None, diff --git a/openhcs/processing/backends/cellprofiler/edge.py b/openhcs/processing/backends/cellprofiler/edge.py index 30bc44280d..5e01b30a19 100644 --- a/openhcs/processing/backends/cellprofiler/edge.py +++ b/openhcs/processing/backends/cellprofiler/edge.py @@ -50,6 +50,7 @@ class EnhanceEdgesModule(CellProfilerModule): from numba import njit from openhcs.core.memory.decorators import numpy as numpy_decorator from openhcs.core.public_api import public_names_from_objects +from openhcs.core.runtime_array_values import RuntimeArrayData from openhcs.core.runtime_image_values import ( image_payload_data, image_payload_mask, @@ -508,7 +509,7 @@ def _native_kirsch(image: np.ndarray) -> np.ndarray: @numpy_decorator(contract=ProcessingContract.PURE_2D) def enhance_edges( - image: np.ndarray, + image: RuntimeArrayData, method: EdgeMethod = EdgeMethod.SOBEL, direction: EdgeDirection = EdgeDirection.ALL, edge_backend_provider: BackendProviderInput = DEFAULT_CELLPROFILER_BACKEND_SELECTION, diff --git a/openhcs/processing/backends/cellprofiler/feature_enhancement.py b/openhcs/processing/backends/cellprofiler/feature_enhancement.py index b8f6bd7bda..a2b813799b 100644 --- a/openhcs/processing/backends/cellprofiler/feature_enhancement.py +++ b/openhcs/processing/backends/cellprofiler/feature_enhancement.py @@ -13,6 +13,7 @@ from openhcs.core.callable_contract import processing_prepare from openhcs.core.memory.decorators import numpy from openhcs.core.registry_strategies import EnumKeyedStrategyMixin +from openhcs.core.runtime_array_values import RuntimeArrayData from openhcs.core.runtime_image_values import ( image_payload_data, image_payload_mask, @@ -65,7 +66,7 @@ class NeuriteMethod(Enum): @numpy(contract=ProcessingContract.PURE_2D) def enhance_or_suppress_features( - image: np.ndarray, + image: RuntimeArrayData, method: OperationMethod = OperationMethod.ENHANCE, enhance_method: EnhanceMethod = EnhanceMethod.SPECKLES, radius: float = 10.0, diff --git a/openhcs/processing/backends/cellprofiler/gaussian_filter.py b/openhcs/processing/backends/cellprofiler/gaussian_filter.py index 187d4d972d..7018d62595 100644 --- a/openhcs/processing/backends/cellprofiler/gaussian_filter.py +++ b/openhcs/processing/backends/cellprofiler/gaussian_filter.py @@ -7,6 +7,7 @@ from openhcs.core.aligned_image_payload import ImagePayloadExecutionMode from openhcs.core.callable_contract import runtime_image_execution_mode +from openhcs.core.runtime_array_values import RuntimeArrayData from openhcs.core.runtime_image_values import ( image_payload_data, image_payload_metadata, @@ -41,7 +42,7 @@ class GaussianFilterModule(CellProfilerModule): @runtime_image_execution_mode(ImagePayloadExecutionMode.FULL_STACK) @numpy(contract=ProcessingContract.FLEXIBLE) -def gaussian_filter(image: np.ndarray, sigma: float = 1.0) -> np.ndarray: +def gaussian_filter(image: RuntimeArrayData, sigma: float = 1.0) -> np.ndarray: """ Apply CellProfiler-compatible Gaussian smoothing to an image. diff --git a/openhcs/processing/backends/cellprofiler/illumination.py b/openhcs/processing/backends/cellprofiler/illumination.py index db2ba6b1f3..d86e17def8 100644 --- a/openhcs/processing/backends/cellprofiler/illumination.py +++ b/openhcs/processing/backends/cellprofiler/illumination.py @@ -920,7 +920,7 @@ def image_payload( @numpy(contract=ProcessingContract.FLEXIBLE) def correct_illumination_calculate( - image: np.ndarray, + image: RuntimeArrayData, intensity_choice: IntensityChoice = IntensityChoice.REGULAR, dilate_objects: bool = False, object_dilation_radius: int = 1, diff --git a/openhcs/processing/backends/cellprofiler/image_geometry.py b/openhcs/processing/backends/cellprofiler/image_geometry.py index 61bdddcca5..37b24b3ce3 100644 --- a/openhcs/processing/backends/cellprofiler/image_geometry.py +++ b/openhcs/processing/backends/cellprofiler/image_geometry.py @@ -1093,7 +1093,7 @@ def align_label_plane_to_shape( @numpy(contract=ProcessingContract.FLEXIBLE) @special_inputs("mask") def mask_image( - image: np.ndarray, + image: RuntimeArrayData, mask: np.ndarray | ObjectLabelValue, mask_source: MaskSource = MaskSource.IMAGE, invert_mask: bool = False, @@ -1130,7 +1130,7 @@ def mask_image( def masked_image_plane( - image: np.ndarray, binary_mask: np.ndarray, *, invert_mask: bool + image: RuntimeArrayData, binary_mask: np.ndarray, *, invert_mask: bool ) -> tuple[np.ndarray, np.ndarray]: image_data = image_payload_data(image) if invert_mask: @@ -1159,7 +1159,7 @@ def masked_image_plane( @numpy(contract=ProcessingContract.PURE_2D) def mask_image_with_binary( - image: np.ndarray, invert_mask: bool = False + image: RuntimeArrayData, invert_mask: bool = False ) -> RuntimeArrayData: """Return a binary mask plane, optionally inverted.""" binary_mask = image_payload_data(image) > 0.5 @@ -1220,7 +1220,7 @@ def tile_output_shape( @numpy(contract=ProcessingContract.FLEXIBLE) def tile( - image: np.ndarray, + image: RuntimeArrayData, rows: int = 8, columns: int = 12, place_first: PlaceFirst = PlaceFirst.TOP_LEFT, @@ -1322,7 +1322,7 @@ def resize_volumetric( @numpy(contract=ProcessingContract.PURE_2D) def flip_and_rotate( - image: np.ndarray, + image: RuntimeArrayData, flip_method: FlipMethod = FlipMethod.NONE, rotate_method: RotateMethod = RotateMethod.NONE, rotation_angle: float = 0.0, diff --git a/openhcs/processing/backends/cellprofiler/intensity.py b/openhcs/processing/backends/cellprofiler/intensity.py index cbb6dbcde0..aae7b8583d 100644 --- a/openhcs/processing/backends/cellprofiler/intensity.py +++ b/openhcs/processing/backends/cellprofiler/intensity.py @@ -302,6 +302,7 @@ def active_artifact_bindings( from openhcs.core.runtime_plane_projection import ( RuntimePlaneAxis, ) +from openhcs.core.runtime_array_values import RuntimeArrayData from openhcs.core.runtime_image_values import ( ImageMetadataPayload, MaskedImagePayload, @@ -1856,7 +1857,7 @@ def run(self, context: RescaleIntensityContext) -> np.ndarray: @runtime_image_execution_mode(ImagePayloadExecutionMode.FULL_STACK) @numpy_decorator(contract=ProcessingContract.PURE_2D) def rescale_intensity( - image: np.ndarray, + image: RuntimeArrayData, rescale_method: RescaleMethod = RescaleMethod.STRETCH, automatic_low: AutomaticLow = AutomaticLow.EACH_IMAGE, automatic_high: AutomaticHigh = AutomaticHigh.EACH_IMAGE, diff --git a/openhcs/processing/backends/cellprofiler/intensity_distribution.py b/openhcs/processing/backends/cellprofiler/intensity_distribution.py index c9c89ba06d..75d78d4341 100644 --- a/openhcs/processing/backends/cellprofiler/intensity_distribution.py +++ b/openhcs/processing/backends/cellprofiler/intensity_distribution.py @@ -2461,7 +2461,7 @@ def _radial_distribution_arrays_from_bin_totals_numba( _IntensityDistributionHeatmapOutputsRuntimeParameter, ) def measure_object_intensity_distribution( - image: np.ndarray, + image: RuntimeArrayData, labels: ObjectLabelValue, bin_count: int = 4, wants_scaled: bool = True, @@ -2562,7 +2562,7 @@ def measure_object_intensity_distribution( def _intensity_distribution_heatmap( - image: np.ndarray, + image: RuntimeArrayData, request: IntensityDistributionHeatmapRuntimeOutput, *, radial_backend: "RadialDistributionBackendStrategy", diff --git a/openhcs/processing/backends/cellprofiler/median_filter.py b/openhcs/processing/backends/cellprofiler/median_filter.py index ca24bd2c00..ec19edfcc9 100644 --- a/openhcs/processing/backends/cellprofiler/median_filter.py +++ b/openhcs/processing/backends/cellprofiler/median_filter.py @@ -19,6 +19,7 @@ RuntimePure2DSliceBatchRequest, pure_2d_batch_executor, ) +from openhcs.core.runtime_array_values import RuntimeArrayData from openhcs.core.runtime_image_values import ( image_payload_data, with_image_payload_data, @@ -371,7 +372,7 @@ def median_filter_backend( @runtime_image_execution_mode(ImagePayloadExecutionMode.FULL_STACK) @numpy(contract=ProcessingContract.FLEXIBLE) def medianfilter( - image: np.ndarray, + image: RuntimeArrayData, window_size: int = 3, mode: ScipyBoundaryMode = ScipyBoundaryMode.CONSTANT, ) -> np.ndarray: diff --git a/openhcs/processing/backends/cellprofiler/morphology.py b/openhcs/processing/backends/cellprofiler/morphology.py index ae58161498..8247ca9b44 100644 --- a/openhcs/processing/backends/cellprofiler/morphology.py +++ b/openhcs/processing/backends/cellprofiler/morphology.py @@ -690,6 +690,7 @@ def artifact_output_relations( object_label_identity_lineage_payload, object_label_parent_child_payload, ) +from openhcs.core.runtime_array_values import RuntimeArrayData from openhcs.core.runtime_image_values import ( ImagePayloadMetadata, image_payload_data, @@ -1491,7 +1492,7 @@ def _morph_image_pixels( def _morph_image_payload( - image: np.ndarray, + image: RuntimeArrayData, structuring_element: StructuringElement, size: int, operation: Callable[[np.ndarray, np.ndarray], np.ndarray], @@ -1508,7 +1509,7 @@ def _morph_image_payload( @runtime_image_execution_mode(ImagePayloadExecutionMode.FULL_STACK) @numpy_decorator(contract=ProcessingContract.FLEXIBLE) def closing( - image: np.ndarray, + image: RuntimeArrayData, structuring_element: StructuringElementInput = StructuringElement.DISK, size: StructuringElementSize = 3, morphology_backend_provider: BackendProviderInput = CellProfilerBackendProvider.NATIVE, @@ -1530,7 +1531,7 @@ def closing( @runtime_image_execution_mode(ImagePayloadExecutionMode.FULL_STACK) @numpy_decorator(contract=ProcessingContract.FLEXIBLE) def opening( - image: np.ndarray, + image: RuntimeArrayData, structuring_element: StructuringElementInput = StructuringElement.DISK, size: StructuringElementSize = 3, morphology_backend_provider: BackendProviderInput = CellProfilerBackendProvider.NATIVE, diff --git a/openhcs/processing/backends/cellprofiler/outlines.py b/openhcs/processing/backends/cellprofiler/outlines.py index cdaeb5cefd..d0e4791fe7 100644 --- a/openhcs/processing/backends/cellprofiler/outlines.py +++ b/openhcs/processing/backends/cellprofiler/outlines.py @@ -878,7 +878,7 @@ def render_single_plane(self, image_sources: tuple[np.ndarray, ...]) -> np.ndarr @object_label_input_execution_mode(ObjectLabelInputExecutionMode.MATCH_IMAGE_STACK) @special_inputs("object_labels") def overlay_outlines( - image: np.ndarray, + image: RuntimeArrayData, *, blank_image: bool = False, display_mode: OutlineDisplayMode = OutlineDisplayMode.COLOR, @@ -922,7 +922,7 @@ def overlay_outlines( @object_label_input_execution_mode(ObjectLabelInputExecutionMode.MATCH_IMAGE_STACK) @special_inputs("labels") def overlay_objects( - image: np.ndarray, + image: RuntimeArrayData, labels: ObjectLabelValue, opacity: float = 0.3, max_label: int | None = None, @@ -1000,7 +1000,7 @@ def overlay_objects( def _overlay_objects_array( - image: np.ndarray, + image: RuntimeArrayData, labels: np.ndarray, *, opacity: float, @@ -1181,7 +1181,7 @@ def _draw_object_labels( def _draw_outline_image( output: np.ndarray, - outline_image: np.ndarray, + outline_image: RuntimeArrayData, color: tuple[float, float, float], *, outline_intensity: float, @@ -1200,7 +1200,7 @@ def _draw_outline_image( return output -def _outline_image_mask(outline_image: np.ndarray) -> np.ndarray: +def _outline_image_mask(outline_image: RuntimeArrayData) -> np.ndarray: mask = np.asarray(image_payload_data(outline_image)) > 0 channel_axis = image_payload_metadata(outline_image).normalized_source_channel_axis( outline_image diff --git a/openhcs/processing/backends/cellprofiler/primary_objects.py b/openhcs/processing/backends/cellprofiler/primary_objects.py index 70c083129a..9f51d8e0ba 100644 --- a/openhcs/processing/backends/cellprofiler/primary_objects.py +++ b/openhcs/processing/backends/cellprofiler/primary_objects.py @@ -11,6 +11,7 @@ from enum import Enum from openhcs.core.memory import numpy from openhcs.core.public_api import public_names_from_objects +from openhcs.core.runtime_array_values import RuntimeArrayData from openhcs.core.runtime_image_values import ( ImagePayloadMetadata, image_payload_data, @@ -123,7 +124,7 @@ class ExcessObjectHandling(CellProfilerEnumAttributeMixin, Enum): @numpy(contract=ProcessingContract.PURE_2D) def identify_primary_objects( - image: np.ndarray, + image: RuntimeArrayData, min_diameter: int = 10, max_diameter: int = 40, exclude_size: bool = True, diff --git a/openhcs/processing/backends/cellprofiler/secondary.py b/openhcs/processing/backends/cellprofiler/secondary.py index 973a360374..d31dbc0f8c 100644 --- a/openhcs/processing/backends/cellprofiler/secondary.py +++ b/openhcs/processing/backends/cellprofiler/secondary.py @@ -38,6 +38,7 @@ EnumKeyedStrategyMixin, RegisteredLeafClassSpec, ) +from openhcs.core.runtime_array_values import RuntimeArrayData from openhcs.core.runtime_image_values import ( image_payload_data, image_payload_mask, @@ -1147,7 +1148,7 @@ def _replacement_primary_output_from_relationship( def _execute_identify_secondary_objects( - image: np.ndarray, + image: RuntimeArrayData, primary_labels: ObjectLabelValue, method: SecondaryMethod = SecondaryMethod.PROPAGATION, threshold_scope: CellProfilerThresholdScope = CellProfilerThresholdScope.GLOBAL, @@ -1337,7 +1338,7 @@ def _execute_identify_secondary_objects( @numpy(contract=ProcessingContract.PURE_2D) @special_inputs("primary_labels") def identify_secondary_objects( - image: np.ndarray, + image: RuntimeArrayData, primary_labels: ObjectLabelValue, method: SecondaryMethod = SecondaryMethod.PROPAGATION, threshold_scope: CellProfilerThresholdScope = CellProfilerThresholdScope.GLOBAL, @@ -1407,7 +1408,7 @@ def identify_secondary_objects( @numpy(contract=ProcessingContract.PURE_2D) @special_inputs("primary_labels") def identify_secondary_objects_with_replacement_primary( - image: np.ndarray, + image: RuntimeArrayData, primary_labels: ObjectLabelValue, method: SecondaryMethod = SecondaryMethod.PROPAGATION, threshold_scope: CellProfilerThresholdScope = CellProfilerThresholdScope.GLOBAL, diff --git a/openhcs/processing/backends/cellprofiler/skeleton.py b/openhcs/processing/backends/cellprofiler/skeleton.py index 947ca74c01..ba8c710ced 100644 --- a/openhcs/processing/backends/cellprofiler/skeleton.py +++ b/openhcs/processing/backends/cellprofiler/skeleton.py @@ -359,7 +359,7 @@ def measure_object_skeleton( @numpy_backend(contract=ProcessingContract.PURE_2D) @special_inputs("seed_labels") def measure_object_skeleton_with_branchpoint_image( - image: np.ndarray, + image: RuntimeArrayData, seed_labels: SeedObjectLabelsInput, fill_small_holes: bool = True, maximum_hole_size: int = 10, diff --git a/openhcs/processing/backends/cellprofiler/smoothing.py b/openhcs/processing/backends/cellprofiler/smoothing.py index 0b803b2294..6ac97257a9 100644 --- a/openhcs/processing/backends/cellprofiler/smoothing.py +++ b/openhcs/processing/backends/cellprofiler/smoothing.py @@ -5,6 +5,7 @@ from typing import Any from openhcs.core.aligned_image_payload import ImagePayloadExecutionMode from openhcs.core.callable_contract import runtime_image_execution_mode +from openhcs.core.runtime_array_values import RuntimeArrayData from openhcs.core.runtime_image_values import ( image_payload_data, with_image_payload_data, @@ -790,7 +791,7 @@ def _fit_polynomial( def smooth_image( - image: np.ndarray, + image: RuntimeArrayData, smoothing_method: SmoothingMethod = SmoothingMethod.GAUSSIAN_FILTER, auto_object_size: bool = True, object_size: float = 16.0, @@ -839,7 +840,7 @@ def smooth_image( @numpy_decorator(contract=ProcessingContract.PURE_2D) def smooth( - image: np.ndarray, + image: RuntimeArrayData, smoothing_method: SmoothingMethod = SmoothingMethod.GAUSSIAN_FILTER, auto_object_size: bool = True, object_size: float = 16.0, @@ -862,7 +863,7 @@ def smooth( @runtime_image_execution_mode(ImagePayloadExecutionMode.FULL_STACK) @numpy_decorator(contract=ProcessingContract.FLEXIBLE) def reducenoise( - image: np.ndarray, + image: RuntimeArrayData, patch_size: int = 5, patch_distance: int = 6, cutoff_distance: float = 0.1, diff --git a/openhcs/processing/backends/cellprofiler/worms.py b/openhcs/processing/backends/cellprofiler/worms.py index 240070daef..74359cf712 100644 --- a/openhcs/processing/backends/cellprofiler/worms.py +++ b/openhcs/processing/backends/cellprofiler/worms.py @@ -2112,7 +2112,7 @@ def untangle_worms_both( @special_inputs("worm_labels") @runtime_bound_parameters(_StraightenWormControlPointsRuntimeParameter) def straighten_worms( - image: np.ndarray, + image: RuntimeArrayData, worm_labels: ObjectLabelValue, control_points: np.ndarray | None = None, worm_width: int = 20, diff --git a/openhcs/processing/backends/lib_registry/registry_service.py b/openhcs/processing/backends/lib_registry/registry_service.py index 5f1c3d226a..36459249c1 100644 --- a/openhcs/processing/backends/lib_registry/registry_service.py +++ b/openhcs/processing/backends/lib_registry/registry_service.py @@ -21,7 +21,7 @@ from zmqruntime import OperationCancellation from zmqruntime.client import endpoint_process -from openhcs.core.callable_contract import CallableProjection +from openhcs.core.callable_contract import CallableContract, CallableProjection from openhcs.core.function_reference import ResolvedRegistryFunction from openhcs.core.processing_preparation import ( CallablePreparation, @@ -145,11 +145,11 @@ def prepare_in_current_process( cls._metadata_cache = cls._metadata_from_instances( cls._available_registry_instances() ) - callables = tuple( + callables = tuple(dict.fromkeys( target for metadata in cls._metadata_cache.values() for target in CallableProjection.from_callable(metadata.func).prepare_targets() - ) + )) emit_status("Preparing registered kernel caches") PreparationCacheBatch.from_callables(callables).populate_child_caches( max_workers=os.cpu_count() or 1, @@ -161,6 +161,9 @@ def prepare_in_current_process( emit_status( f"Prepared callable {preparation.projection.module_name}.{preparation.projection.name}" ) + emit_status("Resolving prepared callable signatures") + for func in callables: + CallableContract.warm_canonical_signature(func) emit_status(f"Registered kernels ready ({len(callables)} callables)") emit_status(f"Function catalog ready ({len(cls._metadata_cache)} callables)") return cls._metadata_cache diff --git a/openhcs/processing/backends/lib_registry/unified_registry.py b/openhcs/processing/backends/lib_registry/unified_registry.py index 4e60c05d3d..f6097a5e74 100644 --- a/openhcs/processing/backends/lib_registry/unified_registry.py +++ b/openhcs/processing/backends/lib_registry/unified_registry.py @@ -207,6 +207,7 @@ def accepted_kwargs( self, func: Callable[..., Any], kwargs: Mapping[str, Any], + *, signature: inspect.Signature | None = None, ) -> dict[str, Any]: """Return kwargs accepted by ``func`` under this policy.""" @@ -220,8 +221,9 @@ def accepted_kwargs( self, func: Callable[..., Any], kwargs: Mapping[str, Any], + *, signature: inspect.Signature | None = None, ) -> dict[str, Any]: - del func + del func, signature return dict(kwargs) @@ -234,8 +236,11 @@ def accepted_kwargs( self, func: Callable[..., Any], kwargs: Mapping[str, Any], + *, signature: inspect.Signature | None = None, ) -> dict[str, Any]: - parameters = _runtime_callable_parameters(func) + from openhcs.core.callable_contract import CallableMetadata + + parameters = (CallableMetadata.callable_signature(func) if signature is None else signature).parameters if any( parameter.kind is inspect.Parameter.VAR_KEYWORD for parameter in parameters.values() @@ -255,6 +260,7 @@ class RuntimeCallableInvocation: kwarg_policy: RuntimeInvocationKwargPolicy = ( RuntimeInvocationKwargPolicy.PASS_THROUGH ) + signature: inspect.Signature | None = None def call(self) -> Any: target = RuntimeCallableViewStrategy.for_view(self.callable_view).resolve( @@ -264,7 +270,7 @@ def call(self) -> Any: *self.args, **RuntimeInvocationKwargPolicyStrategy.for_policy( self.kwarg_policy - ).accepted_kwargs(target, self.kwargs), + ).accepted_kwargs(target, self.kwargs, signature=self.signature), ) @@ -282,6 +288,7 @@ def invocation( func: Callable[..., Any], args: tuple[Any, ...], kwargs: Mapping[str, Any], + *, signature: inspect.Signature | None = None, ) -> RuntimeCallableInvocation: return RuntimeCallableInvocation( func=func, @@ -289,16 +296,10 @@ def invocation( kwargs=kwargs, callable_view=self.callable_view, kwarg_policy=self.kwarg_policy, + signature=signature, ) -@lru_cache(maxsize=256) -def _runtime_callable_parameters( - func: Callable[..., Any], -) -> Mapping[str, inspect.Parameter]: - return inspect.signature(func).parameters - - def _registry_runtime_parameter_exclusions( signature: inspect.Signature, parameter_names: tuple[str, ...], @@ -1012,7 +1013,7 @@ def main_flow_call_argument( self, callable_contract: "CallableContract", source_payload: Any, ) -> Any: """Expose the raw ABI when this contract needs no earlier plane slicing.""" - return callable_contract.raw_main_flow_call_argument(source_payload) + return callable_contract.runtime_main_flow_call_argument(source_payload) def injected_semantic_control_parameter_names(self) -> frozenset[str]: """Semantic controls that this contract may inject into public callables.""" @@ -1027,10 +1028,14 @@ def consume_semantic_controls( kwargs: MutableMapping[str, Any], *, func: Callable[..., Any] | None = None, + parameters: Mapping[str, inspect.Parameter] | None = None, ) -> dict[str, Any]: """Consume and return semantic selectors owned by this declaration.""" - parameters = {} if func is None else inspect.signature(func).parameters + if parameters is None: + from openhcs.core.callable_contract import CallableMetadata + + parameters = {} if func is None else CallableMetadata.callable_signature(func).parameters values: dict[str, Any] = {} for parameter_type in self.runtime_parameter_types(): if not parameter_type.is_semantic_control: @@ -1739,7 +1744,7 @@ def execute_pure_3d(self, func, image, *args, **kwargs): RuntimeCallablePolicy() .invocation( func, - (callable_contract.raw_main_flow_call_argument(image), *args), + (callable_contract.runtime_main_flow_call_argument(image), *args), kwargs, ) .call() @@ -1773,7 +1778,7 @@ def execute_pure_2d(self, func, image, *args, **kwargs): result = ( RuntimeCallablePolicy().invocation( func, - (callable_contract.raw_main_flow_call_argument(image), *args), + (callable_contract.runtime_main_flow_call_argument(image), *args), kwargs, ).call() ) @@ -1837,7 +1842,7 @@ def execute_slice( RuntimeCallablePolicy() .invocation( slice_func, - (callable_contract.raw_main_flow_call_argument(slice_2d), *args), + (callable_contract.runtime_main_flow_call_argument(slice_2d), *args), projected_kwargs, ) .call() diff --git a/tests/unit/test_callable_abi_preparation.py b/tests/unit/test_callable_abi_preparation.py new file mode 100644 index 0000000000..4e3e816fa5 --- /dev/null +++ b/tests/unit/test_callable_abi_preparation.py @@ -0,0 +1,306 @@ +"""Prepared canonical signatures survive compilation and worker transport.""" + +import inspect +import pickle +from types import SimpleNamespace +from dataclasses import replace +from functools import wraps +from unittest.mock import Mock + +import cloudpickle +import numpy as np +import pytest +from arraybridge import ArrayPayload + +import openhcs.core.callable_contract as contract_module +from openhcs.core.callable_contract import ( + CallableContract, CallableImportIdentity, CallableMetadata, + attach_callable_contract_metadata, attach_processing_prepare, +) +from openhcs.core.function_contract_metadata import FunctionContractAttribute +from openhcs.core.function_patterns import NormalizeFunctionGroupAuthority +from openhcs.core.function_reference import ImportableFunctionReference +from openhcs.core.processing_preparation import CallablePreparation, PreparationCacheBatch +from openhcs.core.runtime_image_values import ImagePayloadMetadata, image_payload_data +from openhcs.processing.backends.lib_registry.registry_service import RegistryService +from openhcs.core.runtime_batch_contracts import runtime_callable_defaults +from openhcs.core.pipeline.function_contracts import resolved_callable_parameter +from openhcs.processing.backends.lib_registry.unified_registry import ( + RuntimeCallablePolicy, RuntimeInvocationKwargPolicy, +) + + +def raw_numeric(image: np.ndarray, scale: float = 2.0) -> np.ndarray: + return image * scale + + +@pytest.fixture +def fake_preparation(monkeypatch): + prepare = Mock() + monkeypatch.setattr(CallablePreparation, "prepare", prepare) + monkeypatch.setattr(PreparationCacheBatch, "populate_child_caches", lambda *a, **k: None) + return prepare + + +def test_registry_resolves_signatures_after_all_hooks_and_before_ready(monkeypatch): + events = [] + + def first(image: np.ndarray): + return image + + def last(image: np.ndarray): + return image + + first.__dict__[FunctionContractAttribute.raw_processing_function] = last + metadata = {"first": SimpleNamespace(func=first), "alias": SimpleNamespace(func=first)} + monkeypatch.setattr(RegistryService, "_metadata_cache", metadata) + monkeypatch.setattr(PreparationCacheBatch, "populate_child_caches", lambda *a, **k: None) + + def prepare(owner): + events.append(("prepare", owner.projection.func)) + if owner.projection.func is last: + last.__annotations__["image"] = ArrayPayload + + original_hints = contract_module.get_type_hints + + def hints(func, **kwargs): + events.append(("signature", func)) + return original_hints(func, **kwargs) + + monkeypatch.setattr(CallablePreparation, "prepare", prepare) + monkeypatch.setattr(contract_module, "get_type_hints", hints) + assert RegistryService.prepare_in_current_process(status_callback=events.append) is metadata + preparations = [i for i, item in enumerate(events) if isinstance(item, tuple) and item[0] == "prepare"] + signatures = [i for i, item in enumerate(events) if isinstance(item, tuple) and item[0] == "signature"] + assert len(preparations) == 2 + assert max(preparations) < min(signatures) + assert max(signatures) < next(i for i, item in enumerate(events) if isinstance(item, str) and "kernels ready" in item) + assert CallableContract.from_callable(first).canonical_parameter_annotations["image"] is ArrayPayload + assert CallableContract.from_callable(last).canonical_parameter_annotations["image"] is ArrayPayload + + +def test_explicit_library_rewarm_refreshes_new_contract_not_existing_snapshot(monkeypatch, fake_preparation): + def raw(image: np.ndarray): + return image + + metadata = {"raw": SimpleNamespace(func=raw)} + monkeypatch.setattr(RegistryService, "_metadata_cache", metadata) + RegistryService.prepare_in_current_process() + first = CallableContract.from_callable(raw) + raw.__annotations__["image"] = ArrayPayload + assert CallableContract.from_callable(raw).canonical_parameter_annotations["image"] is np.ndarray + RegistryService.prepare_in_current_process() + second = CallableContract.from_callable(raw) + assert first.canonical_parameter_annotations["image"] is np.ndarray + assert second.canonical_parameter_annotations["image"] is ArrayPayload + + +def test_authored_compilation_prepares_before_signature_and_refreshes_each_compilation(monkeypatch): + def raw(image: np.ndarray, scale=2): + return image + + desired_annotations = [ArrayPayload, np.ndarray] + + def prepare(owner): + raw.__annotations__["image"] = desired_annotations.pop(0) + + monkeypatch.setattr(CallablePreparation, "prepare", prepare) + first = NormalizeFunctionGroupAuthority().normalize("group", raw).items[0].contract + second = NormalizeFunctionGroupAuthority().normalize("group", raw).items[0].contract + assert first.canonical_parameter_annotations["image"] is ArrayPayload + assert second.canonical_parameter_annotations["image"] is np.ndarray + assert FunctionContractAttribute.canonical_signature not in vars(raw) + + +def test_authored_preparation_precedes_metadata_validation(monkeypatch): + def raw(image: np.ndarray): + return image + events = [] + original = CallableMetadata.from_projection.__func__ + + def validate(cls, projection): + assert events == ["prepared"] + return original(cls, projection) + + monkeypatch.setattr(CallablePreparation, "prepare", lambda self: events.append("prepared")) + monkeypatch.setattr(CallableMetadata, "from_projection", classmethod(validate)) + contract = CallableContract.from_prepared_callable(raw) + assert contract.canonical_parameter_annotations["image"] is np.ndarray + + +def test_unprepared_function_reference_compilation_reads_prepared_live_declaration(monkeypatch, fake_preparation): + reference = ImportableFunctionReference( + import_identity=CallableImportIdentity.from_callable(raw_numeric), + composite_key="test:raw_numeric", + ) + contract = NormalizeFunctionGroupAuthority().normalize("group", reference).items[0].contract + assert contract.func is reference + assert contract.canonical_parameter_annotations["image"] is np.ndarray + assert fake_preparation.call_count == 1 + + +@pytest.mark.parametrize("serializer", [pickle, cloudpickle]) +def test_prepared_signature_metadata_namespace_and_reference_transport(serializer, fake_preparation): + contract = CallableContract.from_prepared_callable(raw_numeric) + metadata = contract.metadata + + def proxy(image): + return image + + proxy.__dict__.update(metadata.as_namespace()) + assert CallableMetadata.from_callable(proxy).canonical_signature == metadata.canonical_signature + reference = ImportableFunctionReference( + import_identity=CallableImportIdentity.from_callable(raw_numeric), + composite_key="test:raw_numeric", metadata=metadata, + ) + restored = serializer.loads(serializer.dumps(reference)) + assert restored.metadata.canonical_signature == metadata.canonical_signature + restored_contract = CallableContract.from_callable(restored) + assert restored_contract.canonical_parameter_annotations["image"] is np.ndarray + assert restored_contract.canonical_signature.parameters["scale"].default == 2.0 + assert restored_contract.canonical_signature.return_annotation is np.ndarray + + +def test_compiled_raw_projection_never_queries_signature_or_annotations(monkeypatch, fake_preparation): + contract = CallableContract.from_prepared_callable(raw_numeric) + reference = ImportableFunctionReference( + import_identity=CallableImportIdentity.from_callable(raw_numeric), + composite_key="test:raw_numeric", metadata=contract.metadata, + ) + restored = CallableContract.from_callable(pickle.loads(pickle.dumps(reference))) + def forbidden(*args, **kwargs): + raise AssertionError("Compiled runtime queried a live signature") + monkeypatch.setattr(contract_module, "get_type_hints", forbidden) + monkeypatch.setattr(inspect, "signature", forbidden) + payload = ImagePayloadMetadata(source_image_names=("DNA",)).payload_with(np.ones((2, 3)), None) + for _ in range(40): + assert restored.raw_main_flow_call_argument(payload) is image_payload_data(payload) + assert restored.primary_input_parameter_name == "image" + assert restored.canonical_parameter_annotations["image"] is np.ndarray + + +def test_custom_signature_and_mutable_default_are_retained_until_recompilation(monkeypatch, fake_preparation): + default = [] + def raw(image, values=None): + return image + raw.__signature__ = inspect.Signature([ + inspect.Parameter("image", inspect.Parameter.POSITIONAL_ONLY, annotation=ArrayPayload), + inspect.Parameter("values", inspect.Parameter.KEYWORD_ONLY, default=default), + ]) + first = CallableContract.from_prepared_callable(raw) + assert first.canonical_signature.parameters["values"].default is default + raw.__signature__ = raw.__signature__.replace(parameters=[ + inspect.Parameter("image", inspect.Parameter.POSITIONAL_ONLY, annotation=np.ndarray), + ]) + second = CallableContract.from_prepared_callable(raw) + assert first.canonical_parameter_annotations["image"] is ArrayPayload + assert second.canonical_parameter_annotations["image"] is np.ndarray + + +@pytest.mark.parametrize("mutation", [ + lambda func: attach_processing_prepare(func, lambda: None), + lambda func: attach_callable_contract_metadata(func, raw_processing_function=raw_numeric), +]) +def test_supported_declaration_changes_invalidate_published_signature(mutation): + def raw(image: np.ndarray): + return image + CallableContract.warm_canonical_signature(raw) + assert FunctionContractAttribute.canonical_signature in vars(raw) + mutation(raw) + assert FunctionContractAttribute.canonical_signature not in vars(raw) + + +def test_malformed_signature_declaration_is_rejected(): + def raw(image): + return image + raw.__dict__[FunctionContractAttribute.canonical_signature] = "not a signature" + with pytest.raises(TypeError, match="inspect.Signature"): + CallableMetadata.from_callable(raw) + + +def test_exact_raw_default_filter_and_type_views_refresh_with_library_snapshot(): + def raw(image: np.ndarray, scale: float = 2): + return image * scale + CallableContract.warm_canonical_signature(raw) + policy = RuntimeCallablePolicy(kwarg_policy=RuntimeInvocationKwargPolicy.SIGNATURE_FILTERED) + assert runtime_callable_defaults(raw)["scale"] == 2 + assert resolved_callable_parameter(raw, "scale").annotation is float + assert policy.invocation(raw, (np.ones((2, 3)),), {"scale":3, "unknown":9}).call()[0,0] == 3 + raw.__defaults__ = (4,) + raw.__annotations__["scale"] = int + assert runtime_callable_defaults(raw)["scale"] == 2 + assert resolved_callable_parameter(raw, "scale").annotation is float + CallableContract.warm_canonical_signature(raw) + assert runtime_callable_defaults(raw)["scale"] == 4 + assert resolved_callable_parameter(raw, "scale").annotation is int + + +def test_different_wrapper_signature_remains_live_for_default_filter_and_parameter_queries(): + def raw(image: np.ndarray, private: int = 8): + return image + def wrapper(image: np.ndarray, public: int = 2): + return image * public + wrapper.__dict__[FunctionContractAttribute.raw_processing_function] = raw + CallableContract.warm_canonical_signature(wrapper) + assert runtime_callable_defaults(wrapper) == {"public":2} + assert resolved_callable_parameter(wrapper, "public").annotation is int + policy = RuntimeCallablePolicy(kwarg_policy=RuntimeInvocationKwargPolicy.SIGNATURE_FILTERED) + assert policy.invocation(wrapper, (np.ones((2, 3)),), {"public":3, "private":99}).call()[0,0] == 3 + wrapper.__defaults__ = (5,) + assert runtime_callable_defaults(wrapper) == {"public":5} + assert policy.invocation(wrapper, (np.ones((2, 3)),), {"private":99}).call()[0,0] == 5 + + +def test_prepared_type_query_preserves_legacy_nested_annotation_stripping(): + from typing import Annotated, get_type_hints + def raw(image, labels): + return image + raw.__annotations__ = {"image":Annotated[np.ndarray,"image"],"labels":tuple[Annotated[int,"id"],...],"return":Annotated[np.ndarray,"result"]} + expected = get_type_hints(raw) + CallableContract.warm_canonical_signature(raw) + assert resolved_callable_parameter(raw,"image").annotation == expected["image"] + assert resolved_callable_parameter(raw,"labels").annotation == expected["labels"] + assert CallableContract.from_callable(raw).canonical_parameter_annotations["labels"] == raw.__annotations__["labels"] + + +def test_distinct_authored_runtime_signature_transport_and_new_compilation_refresh(fake_preparation): + def raw(image: np.ndarray, scale: int = 2) -> np.ndarray: + return image * scale + @wraps(raw) + def wrapper(*args, **kwargs): + return raw(*args, **kwargs) + wrapper.__signature__ = inspect.signature(raw).replace(parameters=[ + *inspect.signature(raw).parameters.values(), + inspect.Parameter("injected",inspect.Parameter.KEYWORD_ONLY,default=False), + ]) + first = CallableContract.from_prepared_callable(wrapper) + assert "injected" in first.canonical_signature.parameters + assert "injected" not in first.raw_runtime_signature.parameters + assert first.metadata.raw_runtime_signature is not None + restored = pickle.loads(pickle.dumps(first.metadata)) + assert restored.raw_runtime_signature == first.raw_runtime_signature + assert restored.as_namespace()[FunctionContractAttribute.raw_runtime_signature] == first.raw_runtime_signature + assert FunctionContractAttribute.canonical_signature not in vars(raw) + assert FunctionContractAttribute.canonical_signature not in vars(wrapper) + raw.__defaults__ = (4,) + raw.__annotations__["scale"] = float + second = CallableContract.from_prepared_callable(wrapper) + assert first.raw_runtime_signature.parameters["scale"].default == 2 + assert second.raw_runtime_signature.parameters["scale"].default == 4 + assert second.raw_runtime_signature.parameters["scale"].annotation is float + + +def test_same_runtime_layout_reuses_canonical_without_comparing_mutable_defaults(fake_preparation): + class RejectEquality: + def __eq__(self, other): + raise AssertionError("Signature reuse compared a mutable default") + default = RejectEquality() + def raw(image: np.ndarray, values=default) -> np.ndarray: + return image + @wraps(raw) + def wrapper(*args, **kwargs): + return raw(*args, **kwargs) + contract = CallableContract.from_prepared_callable(wrapper) + assert contract.metadata.raw_runtime_signature is None + assert contract.raw_runtime_signature is contract.canonical_signature + assert contract.raw_runtime_signature.parameters["values"].default is default diff --git a/tests/unit/test_cellprofiler_function_contract_execution.py b/tests/unit/test_cellprofiler_function_contract_execution.py index af8e2adff9..8ad2e4dbef 100644 --- a/tests/unit/test_cellprofiler_function_contract_execution.py +++ b/tests/unit/test_cellprofiler_function_contract_execution.py @@ -2,13 +2,16 @@ from __future__ import annotations -from collections.abc import Callable +from collections.abc import Callable, Sequence from dataclasses import dataclass, replace from functools import wraps import inspect +from typing import Annotated, Any, Union import numpy as np import pytest +from arraybridge import ArrayPayload +import openhcs.core.callable_contract as callable_contract_module from openhcs.core.aligned_image_payload import ( AlignedImageStack, @@ -35,14 +38,22 @@ from openhcs.core.runtime_image_values import ( ImagePayloadMetadata, image_payload_data, + image_payload_mask, + image_payload_metadata, ) +from openhcs.core.runtime_array_values import RuntimeArrayData from openhcs.core.source_spatial_domain import SourceSpatialDomain from openhcs.core.source_image_provenance import SourceImageProvenancePlanes from openhcs.interop.cellprofiler.runtime.function_contract_execution import ( CellProfilerFunctionContractExecutor, ) +from openhcs.interop.cellprofiler.runtime.adapter import CellProfilerRuntimeAdapter from openhcs.processing.backends.cellprofiler.alignment import AlignShiftMeasurement -from openhcs.processing.backends.cellprofiler.morphology import morphologicalskeleton +from openhcs.processing.backends.cellprofiler.morphology import ( + MorphOperation, + morph, + morphologicalskeleton, +) from openhcs.processing.backends.lib_registry.unified_registry import ProcessingContract @@ -62,7 +73,7 @@ def _compiled_contract( artifact_inputs=artifact_inputs, artifact_outputs=artifact_outputs, ), - ) + ).with_prepared_signature() def test_morphological_skeleton_executes_one_planar_runtime_image() -> None: @@ -83,6 +94,431 @@ def test_morphological_skeleton_executes_one_planar_runtime_image() -> None: assert image_payload_data(result).shape == image.shape +def test_registered_morph_distance_projects_raw_abi_and_retains_source_context() -> None: + contract = CallableContract.from_callable(morph) + contract = replace( + contract, + metadata=replace( + contract.metadata, + runtime_adapter=CellProfilerRuntimeAdapter.runtime_adapter_spec(), + artifact_outputs=(ArtifactSpec.output("Distance", ImageArtifactType),), + ), + ) + pixels = np.zeros((1, 7, 7), dtype=np.float32) + pixels[:, 2:5, 2:5] = 1 + mask = np.ones_like(pixels, dtype=bool) + source = ImagePayloadMetadata( + plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + source_image_names=("SavedLabels",), + source_image_provenance_planes=SourceImageProvenancePlanes.from_components( + paths=("/tmp/saved-labels.tif",), + component_metadata=({"channel": "2", "well": "A01"},), + ), + ).payload_with(pixels, mask) + expected = contract.resolve_raw_runtime_callable()( + pixels[0], operation=MorphOperation.DISTANCE + ) + + result = CellProfilerFunctionContractExecutor().execute( + contract, + contract.resolve_canonical_raw_callable(), + source, + {"operation": MorphOperation.DISTANCE}, + execution_mode=ImagePayloadExecutionMode.NATURAL, + plane_projection=RuntimePlaneAxisValueProjection.preserve( + axis=RuntimePlaneAxis.RUNTIME_SLICE, axis_size=1, + ), + ) + + np.testing.assert_array_equal(image_payload_data(result), expected[None]) + np.testing.assert_array_equal(image_payload_mask(result), mask) + metadata = image_payload_metadata(result) + assert metadata.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + 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 == ( + {"channel": "2", "well": "A01"}, + ) + + +@pytest.mark.parametrize("payload_annotation", (False, True)) +def test_raw_slice_abi_retains_declared_payloads_and_projects_kwargs( + payload_annotation: bool, +) -> None: + seen = [] + + def array_raw(image: np.ndarray, *, labels: np.ndarray) -> np.ndarray: + assert isinstance(image, np.ndarray) + seen.append((image, labels)) + return image + labels + + def payload_raw(image: ArrayPayload, *, labels: np.ndarray) -> ArrayPayload: + assert isinstance(image, ArrayPayload) + seen.append((image, labels)) + return image_payload_metadata(image).payload_with( + image_payload_data(image) + labels, image_payload_mask(image), + ) + + raw = payload_raw if payload_annotation else array_raw + contract = _compiled_contract(raw, ProcessingContract.PURE_2D) + contract = replace( + contract, + metadata=replace( + contract.metadata, + runtime_adapter=CellProfilerRuntimeAdapter.runtime_adapter_spec(), + ), + ) + source = ImagePayloadMetadata(source_image_names=("DNA",)).payload_with( + np.ones((3, 4), dtype=np.float32), np.ones((3, 4), dtype=bool), + ) + selected_labels = np.full((3, 4), 5, dtype=np.float32) + executor = CellProfilerFunctionContractExecutor( + plane_projection=RuntimePlaneAxisValueProjection.preserve( + axis=RuntimePlaneAxis.RUNTIME_SLICE, axis_size=2, + ), + ) + + assert contract.runtime_main_flow_call_argument(source) is source + result = executor.execute_pure_2d_slice( + contract, raw, source, + {"labels": RuntimeSliceAlignedValues(slices=(np.zeros((3, 4)), selected_labels)), + "adapter_only_control": 1}, + 1, 2, + ) + + assert seen[0][0] is (source if payload_annotation else image_payload_data(source)) + assert seen[0][1] is selected_labels + np.testing.assert_array_equal(image_payload_data(result), np.full((3, 4), 6)) + np.testing.assert_array_equal(image_payload_mask(result), image_payload_mask(source)) + assert image_payload_metadata(result).source_image_names == ("DNA",) + + +def test_raw_slice_abi_does_not_relax_output_mask_shape_validation() -> None: + def bad_shape(image: np.ndarray) -> np.ndarray: + assert isinstance(image, np.ndarray) + return image[:1] + + contract = _compiled_contract(bad_shape, ProcessingContract.PURE_2D) + source = ImagePayloadMetadata(source_image_names=("DNA",)).payload_with( + np.ones((3, 4)), np.ones((3, 4), dtype=bool), + ) + executor = CellProfilerFunctionContractExecutor( + plane_projection=RuntimePlaneAxisValueProjection.preserve( + axis=RuntimePlaneAxis.RUNTIME_SLICE, axis_size=1, + ), + ) + with pytest.raises(ValueError, match="[Mm]ask.*shape|shape.*[Mm]ask"): + executor.execute_pure_2d_slice(contract, bad_shape, source, {}, 0, 1) + + +@pytest.mark.parametrize("payload_annotation", (False, True)) +@pytest.mark.parametrize( + ("processing_contract", "execution_mode"), + ( + (ProcessingContract.PURE_2D, ImagePayloadExecutionMode.NATURAL), + (ProcessingContract.PURE_3D, ImagePayloadExecutionMode.NATURAL), + (ProcessingContract.PURE_2D, ImagePayloadExecutionMode.FULL_STACK), + (ProcessingContract.PURE_3D, ImagePayloadExecutionMode.FULL_STACK), + ), +) +def test_raw_abi_projection_matches_declared_argument_in_single_and_full_stack_modes( + payload_annotation: bool, + processing_contract: ProcessingContract, + execution_mode: ImagePayloadExecutionMode, +) -> None: + seen = [] + + def array_raw(image: np.ndarray) -> np.ndarray: + assert isinstance(image, np.ndarray) + seen.append(image) + return image + + def payload_raw(image: ArrayPayload) -> ArrayPayload: + assert isinstance(image, ArrayPayload) + seen.append(image) + return image + + raw = payload_raw if payload_annotation else array_raw + contract = _compiled_contract(raw, processing_contract) + contract = replace( + contract, + metadata=replace( + contract.metadata, + runtime_adapter=CellProfilerRuntimeAdapter.runtime_adapter_spec(), + artifact_outputs=(ArtifactSpec.output("Result", ImageArtifactType),), + ), + ) + source = ImagePayloadMetadata(source_image_names=("DNA",)).payload_with( + np.ones((3, 4)), None, + ) + + result = CellProfilerFunctionContractExecutor().execute( + contract, raw, source, {}, execution_mode=execution_mode, + ) + + assert seen == [source if payload_annotation else image_payload_data(source)] + assert seen[0] is (source if payload_annotation else image_payload_data(source)) + assert result is seen[0] + + +@pytest.mark.parametrize( + ("annotation", "retains_payload"), + ( + (np.ndarray, False), + (ArrayPayload, True), + (RuntimeArrayData, True), + (Union[ArrayPayload, np.ndarray], True), + (Annotated[ArrayPayload, "source context"], True), + (Annotated[RuntimeArrayData, "source context"], True), + (Union[Annotated[ArrayPayload, "source context"], np.ndarray], True), + (Union[Annotated[RuntimeArrayData, "source context"], None], True), + (object, True), + (Any, False), + (inspect.Parameter.empty, False), + (Sequence[ArrayPayload], False), + (tuple[ArrayPayload, ...], False), + ), +) +def test_canonical_argument_projection_uses_declared_nominal_annotation( + annotation: object, retains_payload: bool, +) -> None: + def raw(image): + return image + + if annotation is not inspect.Parameter.empty: + raw.__annotations__["image"] = annotation + contract = _compiled_contract(raw, ProcessingContract.PURE_2D) + source = ImagePayloadMetadata(source_image_names=("DNA",)).payload_with( + np.ones((3, 4)), None, + ) + expected = source if retains_payload else image_payload_data(source) + + assert contract.raw_main_flow_call_argument(source) is expected + assert contract.runtime_main_flow_call_argument(source) is expected + adapted = replace( + contract, + metadata=replace( + contract.metadata, + runtime_adapter=CellProfilerRuntimeAdapter.runtime_adapter_spec(), + ), + ) + assert adapted.raw_main_flow_call_argument(source) is expected + assert adapted.runtime_main_flow_call_argument(source) is source + + +def test_raw_slice_argument_retains_buffer_identity_for_inplace_array_callable() -> None: + def inplace(image: np.ndarray) -> np.ndarray: + image[0, 0] = 7 + return image + + contract = _compiled_contract(inplace, ProcessingContract.PURE_2D) + pixels = np.zeros((3, 4)) + source = ImagePayloadMetadata(source_image_names=("DNA",)).payload_with( + pixels, None, + ) + executor = CellProfilerFunctionContractExecutor( + plane_projection=RuntimePlaneAxisValueProjection.preserve( + axis=RuntimePlaneAxis.RUNTIME_SLICE, axis_size=1, + ), + ) + result = executor.execute_pure_2d_slice(contract, inplace, source, {}, 0, 1) + + assert image_payload_data(result) is pixels + assert pixels[0, 0] == 7 + + +def test_prepared_raw_abi_performs_no_introspection_until_explicit_refresh( + monkeypatch: pytest.MonkeyPatch, +) -> None: + seen = [] + + def raw(image: np.ndarray) -> np.ndarray: + seen.append(isinstance(image, ArrayPayload)) + return image_payload_data(image) + + contract = _compiled_contract( + raw, ProcessingContract.PURE_2D, + 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, + ) + executor = CellProfilerFunctionContractExecutor(plane_projection=projection) + hints = [] + signatures = [] + original_hints = callable_contract_module.get_type_hints + original_signature = inspect.signature + + def counted_hints(func, **kwargs): + hints.append((func, kwargs)) + return original_hints(func, **kwargs) + + def counted_signature(func, **kwargs): + caller = inspect.currentframe().f_back + if caller.f_globals is callable_contract_module.__dict__: + signatures.append(caller.f_code.co_name) + return original_signature(func, **kwargs) + + monkeypatch.setattr(callable_contract_module, "get_type_hints", counted_hints) + monkeypatch.setattr(inspect, "signature", counted_signature) + + for annotation in (np.ndarray, ArrayPayload): + raw.__annotations__["image"] = annotation + contract = contract.with_prepared_signature() + result = executor.execute( + contract, raw, source, {}, + execution_mode=ImagePayloadExecutionMode.NATURAL, + plane_projection=projection, + ) + np.testing.assert_array_equal(image_payload_data(result), image_payload_data(source)) + + assert seen == [False] * 12 + [True] * 12 + assert hints == [(raw, {"include_extras": True})] * 2 + assert signatures == ["resolve_signature"] * 2 + assert not hasattr(executor, "_raw_argument_types") + + +def test_raw_abi_derivation_follows_existing_input_guards(monkeypatch) -> None: + def raw(image: np.ndarray) -> np.ndarray: + raise AssertionError("Invalid image reached raw execution") + + contract = _compiled_contract(raw, ProcessingContract.PURE_2D) + + def reject_premature_annotations(self): + raise AssertionError("Input guard resolved the raw ABI prematurely") + + monkeypatch.setattr( + CallableContract, "canonical_parameter_declarations", + property(reject_premature_annotations), + ) + with pytest.raises(TypeError, match="requires AlignedImageStack"): + CellProfilerFunctionContractExecutor().execute( + contract, raw, np.zeros((2, 3)), {}, + execution_mode=ImagePayloadExecutionMode.ALIGNED_MULTI_IMAGE_STACK, + ) + projection = RuntimePlaneAxisValueProjection.preserve( + axis=RuntimePlaneAxis.RUNTIME_SLICE, axis_size=2, + ) + mismatched_image = ImagePayloadMetadata( + plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + ).payload_with(np.zeros((2, 3, 4)), None) + with pytest.raises(RuntimeSliceProjectionDeclarationError, match="conflicts"): + CellProfilerFunctionContractExecutor().execute( + contract, raw, mismatched_image, {}, + execution_mode=ImagePayloadExecutionMode.NATURAL, + plane_projection=projection, + ) + + +def test_raw_abi_child_preserves_cooperative_executor_constructor_contract() -> None: + constructed = [] + raw_calls = [] + + class CooperativeExecutor(CellProfilerFunctionContractExecutor): + def __init__(self, plane_projection=None): + super().__init__(plane_projection=plane_projection) + constructed.append(self) + + def invoke_raw(self, callable_contract, func, image, kwargs): + raw_calls.append(self) + return super().invoke_raw(callable_contract, func, image, kwargs) + + def raw(image: np.ndarray) -> np.ndarray: + assert isinstance(image, np.ndarray) + return image + + contract = _compiled_contract( + raw, ProcessingContract.PURE_2D, + artifact_outputs=(ArtifactSpec.output("Result", ImageArtifactType),), + ) + source = ImagePayloadMetadata(plane_axis=RuntimePlaneAxis.RUNTIME_SLICE).payload_with( + np.ones((2, 3, 4)), None, + ) + executor = CooperativeExecutor() + result = executor.execute( + contract, raw, source, {}, execution_mode=ImagePayloadExecutionMode.NATURAL, + plane_projection=RuntimePlaneAxisValueProjection.preserve( + axis=RuntimePlaneAxis.RUNTIME_SLICE, axis_size=2, + ), + ) + + assert len(constructed) == 2 + assert raw_calls == [constructed[1]] * 2 + assert not hasattr(executor, "_raw_argument_types") + np.testing.assert_array_equal(image_payload_data(result), image_payload_data(source)) + + def foreign_raw(image: ArrayPayload) -> ArrayPayload: + assert isinstance(image, ArrayPayload) + return image + + foreign_contract = _compiled_contract(foreign_raw, ProcessingContract.PURE_2D) + child = constructed[1] + assert child.invoke_raw(foreign_contract, foreign_raw, source, {}) is source + assert not hasattr(child, "_scope_contract") + + +def test_actual_prepared_executor_planes_make_no_signature_or_hint_queries(monkeypatch) -> None: + calls = [] + def raw(image: np.ndarray, scale: int = 2) -> np.ndarray: + calls.append(image.shape) + return image * scale + CallableContract.warm_canonical_signature(raw) + contract = _compiled_contract( + raw, ProcessingContract.PURE_2D, + 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) + def forbidden(*args, **kwargs): + raise AssertionError("Prepared execution introspected a live callable") + monkeypatch.setattr(inspect,"signature",forbidden) + monkeypatch.setattr(callable_contract_module,"get_type_hints",forbidden) + result = CellProfilerFunctionContractExecutor().execute( + contract,raw,source,{},execution_mode=ImagePayloadExecutionMode.NATURAL, + plane_projection=projection, + ) + np.testing.assert_array_equal(image_payload_data(result),np.full((12,3,4),2)) + assert calls == [(3,4)]*12 + + +def test_authored_distinct_raw_abi_batch_has_no_runtime_queries_and_filters_controls(monkeypatch) -> None: + from openhcs.core.processing_preparation import CallablePreparation + calls = [] + def raw(image: np.ndarray, scale: int = 2) -> np.ndarray: + calls.append(image.shape) + return image * scale + @wraps(raw) + def wrapper(*args, **kwargs): + return raw(*args, **kwargs) + wrapper.__signature__ = inspect.signature(raw).replace(parameters=[ + *inspect.signature(raw).parameters.values(), + inspect.Parameter("injected",inspect.Parameter.KEYWORD_ONLY,default=False), + ]) + monkeypatch.setattr(CallablePreparation,"prepare",lambda self:None) + contract = CallableContract.from_prepared_callable(wrapper) + contract = replace(contract,metadata=replace( + contract.metadata,processing_contract=ProcessingContract.PURE_2D, + 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) + def forbidden(*args, **kwargs): + raise AssertionError("Prepared authored raw target queried a live signature") + monkeypatch.setattr(inspect,"signature",forbidden) + monkeypatch.setattr(callable_contract_module,"get_type_hints",forbidden) + result = CellProfilerFunctionContractExecutor().execute( + contract,wrapper,source,{"injected":True}, + execution_mode=ImagePayloadExecutionMode.NATURAL,plane_projection=projection, + ) + np.testing.assert_array_equal(image_payload_data(result),np.full((12,3,4),2)) + assert calls == [(3,4)]*12 + + def test_compiled_slice_execution_resolves_raw_target_once_and_preserves_request_binding( monkeypatch: pytest.MonkeyPatch, ) -> None: @@ -103,7 +539,7 @@ def request_bound(request: ScaleRequest) -> object: def decorated(*args: object, **kwargs: object) -> object: raise AssertionError("CellProfiler invoked an outer runtime wrapper") - contract = CallableContract.from_callable(decorated) + contract = CallableContract.from_callable(decorated).with_prepared_signature() contract = replace( contract, metadata=replace( diff --git a/tests/unit/test_cellprofiler_prepared_payload_abi.py b/tests/unit/test_cellprofiler_prepared_payload_abi.py new file mode 100644 index 0000000000..bf0f122d11 --- /dev/null +++ b/tests/unit/test_cellprofiler_prepared_payload_abi.py @@ -0,0 +1,127 @@ +"""Compiled raw calls preserve metadata required by declared image consumers.""" + +from dataclasses import replace + +import numpy as np +from scipy import ndimage + +from openhcs.core.aligned_image_payload import ImagePayloadExecutionMode +from openhcs.core.function_patterns import NormalizeFunctionGroupAuthority +from openhcs.core.runtime_image_values import ( + ImagePayloadMetadata, + image_payload_data, + image_payload_mask, + image_payload_metadata, + normalize_image_payload_intensity, +) +from openhcs.core.runtime_object_label_building import SourceImageObjectLabelBuildRequest +from openhcs.core.runtime_object_labels import object_label_dense_array +from openhcs.core.source_spatial_domain import SourceSpatialDomain +from openhcs.interop.cellprofiler.runtime.adapter import CellProfilerRuntimeAdapter +from openhcs.interop.cellprofiler.runtime.function_contract_execution import ( + CellProfilerFunctionContractExecutor, +) +from openhcs.processing.backends.cellprofiler.color import ColorToGrayMode, color_to_gray +from openhcs.processing.backends.cellprofiler.median_filter import medianfilter +from openhcs.processing.backends.cellprofiler.secondary import ( + SecondaryMethod, + identify_secondary_objects, +) + + +def _execute_prepared(func, source, kwargs, mode=ImagePayloadExecutionMode.NATURAL): + contract = NormalizeFunctionGroupAuthority().normalize("image", func).items[0].contract + contract = replace( + contract, + metadata=replace( + contract.metadata, + runtime_adapter=CellProfilerRuntimeAdapter.runtime_adapter_spec(), + ), + ) + assert contract.metadata.canonical_signature is not None + assert contract.raw_main_flow_call_argument(source) is source + return CellProfilerFunctionContractExecutor().execute( + contract, + contract.resolve_canonical_raw_callable(), + source, + kwargs, + execution_mode=mode, + ) + + +def test_prepared_color_to_gray_preserves_channel_mask_and_spatial_domain(): + pixels = np.arange(4 * 5 * 3, dtype=np.float32).reshape(4, 5, 3) + mask = np.ones(pixels.shape[:2], dtype=bool) + mask[0] = False + mask[-1] = False + domain = SourceSpatialDomain(origin_yx=(2, 3), source_shape_yx=(8, 10)) + source = ImagePayloadMetadata( + source_channel_axis=-1, + source_image_names=("RGB",), + source_path="/input/rgb.tif", + source_spatial_domain=domain, + ).payload_with(pixels, mask) + + result = _execute_prepared( + color_to_gray, source, + {"mode": ColorToGrayMode.COMBINE, + "channel_indices": (0, 2), "contributions": (1.0, 3.0)}, + ) + + np.testing.assert_array_equal(image_payload_data(result), (pixels[..., 0] + 3 * pixels[..., 2]) / 4) + np.testing.assert_array_equal(image_payload_mask(result), mask) + metadata = image_payload_metadata(result) + assert metadata.source_channel_axis is None + assert metadata.source_spatial_domain == domain + assert metadata.source_image_paths == ("/input/rgb.tif",) + + +def test_prepared_medianfilter_keeps_original_intensity_scale_and_mask(): + pixels = np.arange(25, dtype=np.uint16).reshape(5, 5) + mask = np.ones(pixels.shape, dtype=bool) + mask[0] = False + source = normalize_image_payload_intensity( + ImagePayloadMetadata.for_array(pixels).payload_with(pixels, mask), + dtype=np.float32, + ) + + result = _execute_prepared( + medianfilter, source, {"window_size": 3}, ImagePayloadExecutionMode.FULL_STACK, + ) + + np.testing.assert_array_equal( + image_payload_data(result), + ndimage.median_filter(image_payload_data(source), size=3, mode="constant"), + ) + np.testing.assert_array_equal(image_payload_mask(result), mask) + assert image_payload_metadata(result).unit_interval_intensity_scale == 65535 + assert image_payload_data(result).dtype == np.float32 + + +def test_prepared_secondary_object_output_keeps_actual_source_spatial_domain(): + pixels = np.zeros((9, 9), dtype=np.float32) + domain = SourceSpatialDomain(origin_yx=(3, 4), source_shape_yx=(15, 16)) + source = ImagePayloadMetadata( + source_image_names=("DNA",), source_path="/input/dna.tif", + source_spatial_domain=domain, + ).payload_with(pixels, None) + seed_pixels = np.zeros(pixels.shape, dtype=np.int32) + seed_pixels[4, 4] = 1 + seeds = SourceImageObjectLabelBuildRequest( + image=source, labels=seed_pixels, declared_object_ids=(1,), + ).payload() + + result = _execute_prepared( + identify_secondary_objects, source, + {"primary_labels": seeds, "method": SecondaryMethod.DISTANCE_N, + "distance_to_dilate": 1, "fill_holes": False}, + ) + + _image, _rows, relationship, objects = result + expected = ndimage.distance_transform_edt(seed_pixels == 0) <= 1 + np.testing.assert_array_equal(object_label_dense_array(objects), expected.astype(np.int32)) + assert objects.source_spatial_domain.origin_yx == domain.origin_yx + assert objects.source_spatial_domain.source_shape_yx == domain.source_shape_yx + assert objects.source_provenance == image_payload_metadata(source).source_provenance + assert relationship.source_ids == (1,) + assert relationship.target_ids == (1,) diff --git a/tests/unit/test_preparation_admission.py b/tests/unit/test_preparation_admission.py index 3ed203e53d..99c99a3a55 100644 --- a/tests/unit/test_preparation_admission.py +++ b/tests/unit/test_preparation_admission.py @@ -189,8 +189,10 @@ def prepare_selected(): ) dynamic_invocation = CompiledFunctionInvocation( key=FunctionInvocationKey("dynamic", "default", 1), - contract=CallableContract.from_callable(dynamic), + contract=CallableContract.from_prepared_callable(dynamic), ) + assert events == ["selected", "unselected", "dynamic"] + assert dynamic_invocation.contract.metadata.canonical_signature is not None group = CompiledFunctionGroup("default", (invocation, dynamic_invocation)) pattern = CompiledFunctionPattern(groups=(group,), is_grouped=False) context = SimpleNamespace(step_plans={0: SimpleNamespace(step_index=0, compiled_function_pattern=pattern)}) From 760e9ae8613d8737411a627b6abfe6a15f4ca0b9 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 01:42:38 -0400 Subject: [PATCH 040/235] Keep prepared callable ABI algorithms on metadata and invocation owners --- openhcs/core/callable_contract.py | 310 +++++++++--------- openhcs/core/runtime_batch_contracts.py | 8 + .../runtime/function_contract_execution.py | 49 +-- .../backends/lib_registry/registry_service.py | 4 +- .../backends/lib_registry/unified_registry.py | 20 +- tests/unit/test_callable_abi_preparation.py | 46 ++- ...ellprofiler_function_contract_execution.py | 13 +- 7 files changed, 240 insertions(+), 210 deletions(-) diff --git a/openhcs/core/callable_contract.py b/openhcs/core/callable_contract.py index d462097d29..5d0640ef6a 100644 --- a/openhcs/core/callable_contract.py +++ b/openhcs/core/callable_contract.py @@ -316,6 +316,125 @@ def callable_signature(cls, func: Callable[..., object]) -> inspect.Signature: signature = cls.prepared_callable_signature(func) return inspect.signature(func) if signature is None else signature + def resolve_canonical_raw_callable(self, func: Any) -> Callable[..., object]: + """Return the declaration-owned callable whose signature defines behavior.""" + + declared = self.raw_processing_function + if declared is None: + declared = func + return _resolve_declared_callable(declared) + + def resolve_raw_runtime_callable(self, func: Any) -> Callable[..., object]: + """Remove runtime wrappers without crossing a semantic request binding.""" + + canonical = self.resolve_canonical_raw_callable(func) + if self.request_binding is None: + return inspect.unwrap(canonical) + + binding_key = FunctionContractAttribute.callable_request_binding + + def is_request_boundary(candidate: Callable[..., object]) -> bool: + namespace = _callable_namespace(candidate) + if binding_key not in namespace: + return False + wrapped = namespace.get("__wrapped__") + return not callable(wrapped) or binding_key not in _callable_namespace( + wrapped + ) + + resolved = inspect.unwrap(canonical, stop=is_request_boundary) + if binding_key not in _callable_namespace(resolved): + raise RuntimeError( + f"Callable {CallableProjection.from_callable(func).name!r} declares a request binding, but " + "its canonical raw callable has no request-binding boundary." + ) + return resolved + + def resolve_signature(self, raw_callable: Callable[..., object]) -> inspect.Signature: + """Resolve the semantic ABI once after declaration preparation.""" + signature = inspect.signature(raw_callable) + annotations = ( + self.request_binding.public_annotations_dict + if self.request_binding is not None + else get_type_hints(raw_callable, include_extras=True) + ) + return signature.replace( + parameters=tuple( + parameter.replace(annotation=annotations.get(name, parameter.annotation)) + for name, parameter in signature.parameters.items() + ), + return_annotation=annotations.get("return", signature.return_annotation), + ) + + @staticmethod + def primary_input_name(parameters: Mapping[str, inspect.Parameter]) -> str | None: + for parameter in parameters.values(): + if parameter.kind in ( + inspect.Parameter.POSITIONAL_ONLY, + inspect.Parameter.POSITIONAL_OR_KEYWORD, + ): + return parameter.name + return None + + @classmethod + def _nominal_argument_types(cls, annotation: object) -> tuple[type, ...]: + annotation = resolve_annotated(annotation) + if is_union_type(annotation): + return tuple( + argument_type for member in get_args(annotation) + for argument_type in cls._nominal_argument_types(member) + ) + return ( + (annotation,) + if annotation is not Any and isinstance(annotation, type) + else () + ) + + def raw_main_flow_call_argument(self, source_payload: Any, signature: inspect.Signature) -> Any: + """Admit a declared nominal carrier at the canonical image boundary.""" + from openhcs.core.runtime_image_values import image_payload_data + + primary_name = self.primary_input_name(signature.parameters) + annotation = None if primary_name is None else signature.parameters[primary_name].annotation + return source_payload if any( + isinstance(source_payload, argument_type) + for argument_type in self._nominal_argument_types(annotation) + ) else image_payload_data(source_payload) + + def canonical_signature_for(self, func: Any) -> inspect.Signature: + """Use the prepared semantic ABI; unprepared authoring remains live.""" + if self.canonical_signature is not None: + return self.canonical_signature + return self.resolve_signature(self.resolve_canonical_raw_callable(func)) + + def raw_runtime_signature_for(self, func: Any) -> inspect.Signature: + """Own the distinct raw ABI or the shared canonical target snapshot.""" + if self.raw_runtime_signature is not None: + return self.raw_runtime_signature + if self.canonical_signature is not None: + return self.canonical_signature + return self.resolve_signature(self.resolve_raw_runtime_callable(func)) + + def with_prepared_signatures(self, canonical: Callable, runtime: Callable) -> "CallableMetadata": + """Snapshot each actual target independently after declaration preparation.""" + return dataclasses.replace( + self, canonical_signature=self.resolve_signature(canonical), + raw_runtime_signature=None if runtime is canonical else self.resolve_signature(runtime), + ) + + def with_missing_signatures(self, canonical: Callable[[], Callable], runtime: Callable[[], Callable]) -> "CallableMetadata": + """Reuse library readiness or capture the newly prepared authored targets.""" + if self.canonical_signature is not None: + return self + return self.with_prepared_signatures(canonical(), runtime()) + + def publish_signatures(self, namespace: MutableMapping[str, Any]) -> None: + """Replace the namespace pair as one library preparation epoch.""" + namespace[FunctionContractAttribute.canonical_signature] = self.canonical_signature + namespace.pop(FunctionContractAttribute.raw_runtime_signature, None) + if self.raw_runtime_signature is not None: + namespace[FunctionContractAttribute.raw_runtime_signature] = self.raw_runtime_signature + @property def artifact_output_policy(self) -> type[ArtifactOutputPolicy]: """Project recording ownership from this callable's adapter declaration.""" @@ -959,43 +1078,9 @@ def runtime_main_flow_call_argument(self, source_payload: Any) -> Any: return source_payload return self.raw_main_flow_call_argument(source_payload) - def raw_main_flow_call_argument( - self, source_payload: Any, - ) -> Any: - """Project one image argument at the canonical raw callable boundary.""" - from openhcs.core.runtime_image_values import image_payload_data - - return ( - source_payload - if any( - isinstance(source_payload, argument_type) - for argument_type in self.raw_main_flow_argument_types() - ) - else image_payload_data(source_payload) - ) - - def raw_main_flow_argument_types(self) -> tuple[type, ...]: - """Derive the nominal argument view for one canonical invocation.""" - parameters = self.canonical_parameter_declarations - primary_name = self._primary_input_name(parameters) - annotation = ( - None if primary_name is None else parameters[primary_name].annotation - ) - return self._nominal_argument_types(annotation) - - @classmethod - def _nominal_argument_types(cls, annotation: object) -> tuple[type, ...]: - annotation = resolve_annotated(annotation) - if is_union_type(annotation): - return tuple( - argument_type for member in get_args(annotation) - for argument_type in cls._nominal_argument_types(member) - ) - return ( - (annotation,) - if annotation is not Any and isinstance(annotation, type) - else () - ) + def raw_main_flow_call_argument(self, source_payload: Any) -> Any: + """Project the metadata-owned raw carrier declaration for this contract.""" + return self.metadata.raw_main_flow_call_argument(source_payload, self.canonical_signature) def main_flow_call_argument(self, source_payload: Any) -> Any: """Let the processing declaration retain context needed before raw calls.""" @@ -1064,17 +1149,7 @@ def artifact_specs(self) -> ArtifactSpecCollection: def primary_input_parameter_name(self) -> str | None: """FunctionStep input payload parameter declared by callable signature.""" signature = self.canonical_signature - return self._primary_input_name(signature.parameters) - - @staticmethod - def _primary_input_name(parameters: Mapping[str, inspect.Parameter]) -> str | None: - for parameter in parameters.values(): - if parameter.kind in ( - inspect.Parameter.POSITIONAL_ONLY, - inspect.Parameter.POSITIONAL_OR_KEYWORD, - ): - return parameter.name - return None + return self.metadata.primary_input_name(signature.parameters) @property def accepts_implicit_main_flow_input(self) -> bool: @@ -1112,12 +1187,8 @@ def resolve_runtime_callable(self) -> Callable[..., object]: return runtime_adapter.executable_callable(resolved, self) def resolve_canonical_raw_callable(self) -> Callable[..., object]: - """Return the declaration-owned callable whose signature defines behavior.""" - - declared = self.raw_processing_function - if declared is None: - declared = self.func - return _resolve_declared_callable(declared) + """Return the declaration-owned semantic target.""" + return self.metadata.resolve_canonical_raw_callable(self.func) def canonical_raw_import_identity(self) -> CallableImportIdentity: """Return the complete import identity of the canonical raw callable.""" @@ -1127,30 +1198,8 @@ def canonical_raw_import_identity(self) -> CallableImportIdentity: ) def resolve_raw_runtime_callable(self) -> Callable[..., object]: - """Remove runtime wrappers without crossing a semantic request binding.""" - - canonical = self.resolve_canonical_raw_callable() - if self.request_binding is None: - return inspect.unwrap(canonical) - - binding_key = FunctionContractAttribute.callable_request_binding - - def is_request_boundary(candidate: Callable[..., object]) -> bool: - namespace = _callable_namespace(candidate) - if binding_key not in namespace: - return False - wrapped = namespace.get("__wrapped__") - return not callable(wrapped) or binding_key not in _callable_namespace( - wrapped - ) - - resolved = inspect.unwrap(canonical, stop=is_request_boundary) - if binding_key not in _callable_namespace(resolved): - raise RuntimeError( - f"Callable {self.function_name!r} declares a request binding, but " - "its canonical raw callable has no request-binding boundary." - ) - return resolved + """Resolve the metadata-owned raw target without crossing request binding.""" + return self.metadata.resolve_raw_runtime_callable(self.func) @property def canonical_parameter_annotations(self) -> Mapping[str, object]: @@ -1167,88 +1216,30 @@ def canonical_parameter_declarations(self) -> Mapping[str, inspect.Parameter]: @property def canonical_signature(self) -> inspect.Signature: - """Use the prepared declaration, or query an uncompiled authoring callable.""" - if self.metadata.canonical_signature is not None: - return self.metadata.canonical_signature - return self.resolve_signature(self.resolve_canonical_raw_callable()) - - def resolve_signature(self, raw_callable: Callable[..., object]) -> inspect.Signature: - """Resolve the semantic ABI once after declaration preparation.""" - signature = inspect.signature(raw_callable) - annotations = ( - self.request_binding.public_annotations_dict - if self.request_binding is not None - else get_type_hints(raw_callable, include_extras=True) - ) - return signature.replace( - parameters=tuple( - parameter.replace(annotation=annotations.get(name, parameter.annotation)) - for name, parameter in signature.parameters.items() - ), - return_annotation=annotations.get("return", signature.return_annotation), - ) + """The metadata-owned semantic signature for this contract's target.""" + return self.metadata.canonical_signature_for(self.func) def with_prepared_signature(self) -> "CallableContract": - """Capture the prepared ABI on the declaration transported to workers.""" - canonical = self.resolve_canonical_raw_callable() - signature = self.resolve_signature(canonical) - runtime = self.resolve_raw_runtime_callable() - runtime_signature = None - if runtime is not canonical: - candidate = self.resolve_signature(runtime) - canonical_parameters = tuple(signature.parameters.values()) - runtime_parameters = tuple(candidate.parameters.values()) - same_parameters = len(canonical_parameters) == len(runtime_parameters) and all( - left.name == right.name and left.kind is right.kind - and left.default is right.default and left.annotation is right.annotation - for left, right in zip(canonical_parameters, runtime_parameters) - ) - if not (same_parameters and signature.return_annotation is candidate.return_annotation): - runtime_signature = candidate - return dataclasses.replace( - self, - metadata=dataclasses.replace( - self.metadata, canonical_signature=signature, - raw_runtime_signature=runtime_signature, - ), - ) + """Freeze the prepared declaration into its existing compiled contract.""" + return dataclasses.replace(self, metadata=self.metadata.with_prepared_signatures( + self.resolve_canonical_raw_callable(), self.resolve_raw_runtime_callable(), + )) @property def raw_runtime_signature(self) -> inspect.Signature: - """The exact unwrapped execution ABI, preserving request boundaries.""" - if self.metadata.raw_runtime_signature is not None: - return self.metadata.raw_runtime_signature - if self.metadata.canonical_signature is not None: - return self.metadata.canonical_signature - return self.resolve_signature(self.resolve_raw_runtime_callable()) - - @classmethod - def warm_canonical_signature(cls, func: Callable[..., object]) -> None: - """Publish one refreshed ABI after all library preparation hooks finish.""" - contract = cls.from_callable(func).with_prepared_signature() - _mutable_callable_namespace(func)[FunctionContractAttribute.canonical_signature] = ( - contract.metadata.canonical_signature - ) - namespace = _mutable_callable_namespace(func) - namespace.pop(FunctionContractAttribute.raw_runtime_signature, None) - if contract.metadata.raw_runtime_signature is not None: - namespace[FunctionContractAttribute.raw_runtime_signature] = contract.metadata.raw_runtime_signature + """The metadata-owned execution signature for this contract's target.""" + return self.metadata.raw_runtime_signature_for(self.func) @classmethod def from_prepared_callable(cls, func: Callable[..., object]) -> "CallableContract": - """Reuse library warmup; prepare authored declarations before validation.""" + """Construct a compiled view only after the declaration owner prepares it.""" projection = CallableProjection.from_callable(func) - if projection.namespace.get(FunctionContractAttribute.canonical_signature) is not None: - return cls.from_callable(func) - resolved = _resolve_declared_callable(func) - for target in CallableProjection.from_callable(resolved).prepare_targets(): - prepare_processing_callable(target) - prepared = cls.from_callable(resolved) - if prepared.metadata.canonical_signature is None: - prepared = prepared.with_prepared_signature() + prepared = cls.from_callable(projection.prepared_callable()) return dataclasses.replace( - prepared, func=func, function_name=projection.name, - module_name=projection.module_name, + prepared, func=func, function_name=projection.name, module_name=projection.module_name, + metadata=prepared.metadata.with_missing_signatures( + prepared.resolve_canonical_raw_callable, prepared.resolve_raw_runtime_callable, + ), ) def decode_public_kwargs(self, kwargs: Mapping[str, object]) -> dict[str, object]: @@ -1951,6 +1942,21 @@ def from_callable(cls, func: Any) -> "CallableProjection": ) return cls(func=func, name=name, module_name=module_name, namespace=namespace) + def prepared_callable(self) -> Any: + """Keep ready library references or prepare live authored targets before validation.""" + reader = CallableMetadataReader(self.namespace, self.name) + if reader.has_prepared_signature(): + return self.func + resolved = _resolve_declared_callable(self.func) + for target in type(self).from_callable(resolved).prepare_targets(): + prepare_processing_callable(target) + return resolved + + def warm_canonical_signature(self) -> None: + """Publish prepared target signatures only after all library hooks finish.""" + contract = CallableContract.from_callable(self.func).with_prepared_signature() + contract.metadata.publish_signatures(_mutable_callable_namespace(self.func)) + def prepare_targets(self) -> tuple[Any, ...]: """Return wrapper/raw callables that may carry preparation metadata.""" targets: list[Any] = [] @@ -1996,6 +2002,10 @@ class CallableMetadataReader: namespace: CallableNamespace function_name: str + def has_prepared_signature(self) -> bool: + """Validate the namespace-owned readiness declaration before phase selection.""" + return self.optional_signature(FunctionContractAttribute.canonical_signature) is not None + def optional_signature(self, field_name: str) -> inspect.Signature | None: value = self.namespace.get(field_name) if value is not None and not isinstance(value, inspect.Signature): diff --git a/openhcs/core/runtime_batch_contracts.py b/openhcs/core/runtime_batch_contracts.py index cf48ff9032..d3eef11fcc 100644 --- a/openhcs/core/runtime_batch_contracts.py +++ b/openhcs/core/runtime_batch_contracts.py @@ -280,6 +280,14 @@ def executors(self) -> Mapping[RuntimeBatchExecutionDomain, Callable]: class Pure2DSliceBatchExecutor(RuntimeBatchExecutor): """Base contract for equivalent pure-2D slice batch execution.""" + @classmethod + def from_executors( + cls, executors: Mapping[RuntimeBatchExecutionDomain, Callable] | None, + ) -> Callable: + """Honor a declared pure-2D executor before the nominal serial default.""" + declared = None if executors is None else executors.get(RuntimeBatchExecutionDomain.PURE_2D_SLICES) + return declared if callable(declared) else cls.default_executor() + @classmethod def default_executor(cls) -> "Pure2DSliceBatchExecutor": """Return the single-thread default pure-2D batch executor.""" diff --git a/openhcs/interop/cellprofiler/runtime/function_contract_execution.py b/openhcs/interop/cellprofiler/runtime/function_contract_execution.py index 76ab77ecca..199f2dea38 100644 --- a/openhcs/interop/cellprofiler/runtime/function_contract_execution.py +++ b/openhcs/interop/cellprofiler/runtime/function_contract_execution.py @@ -18,7 +18,6 @@ from openhcs.core.memory import detect_memory_type, stack_slices from openhcs.core.runtime_batch_contracts import ( Pure2DSliceBatchExecutor, - RuntimeBatchExecutionDomain, RuntimeBatchInvocationRequest, RuntimePure2DSliceBatchRequest, SliceIndexRuntimeParameter, @@ -76,21 +75,6 @@ def __init__( ) -> None: self.plane_projection = plane_projection - def invoke_raw( - self, - callable_contract: CallableContract, - func: Callable[..., RuntimeFunctionOutput], - image: RuntimeCallableArgument, - kwargs: RuntimeCallableKwargs, - ) -> RuntimeCallableArgument: - """Project the canonical ABI after the input domain has been selected.""" - return _CELLPROFILER_RUNTIME_CALLABLE_POLICY.invocation( - func, - (callable_contract.raw_main_flow_call_argument(image),), - kwargs, - signature=callable_contract.raw_runtime_signature, - ).call() - def execute( self, callable_contract: CallableContract, @@ -232,14 +216,7 @@ def execute_pure_2d_slice_batch( if slice_request.slice_count <= 0: return [], 0.0 - declared_batch_executor = callable_contract.runtime_batch_executor( - RuntimeBatchExecutionDomain.PURE_2D_SLICES - ) - batch_executor = ( - declared_batch_executor - if callable(declared_batch_executor) - else Pure2DSliceBatchExecutor.default_executor() - ) + batch_executor = Pure2DSliceBatchExecutor.from_executors(callable_contract.runtime_batch_executors) slice_started_at = time.perf_counter() if batch_executor is not None and slice_request.slice_count > 1: slice_results = list(batch_executor(slice_request)) @@ -278,12 +255,12 @@ def execute_pure_3d( ), ) call_started_at = time.perf_counter() - result = self.invoke_raw( + result = _CELLPROFILER_RUNTIME_CALLABLE_POLICY.contract_invocation( callable_contract, func, projected_image, projected_kwargs, - ) + ).call() CellProfilerRuntimeProfileLogger.log_module_profile( "cp_full_stack_raw_call", time.perf_counter() - call_started_at, @@ -357,7 +334,7 @@ def execute_aligned_stack_slice( slice_index: int, slice_count: int, ) -> RuntimeCallableArgument: - return self.invoke_raw( + return _CELLPROFILER_RUNTIME_CALLABLE_POLICY.contract_invocation( callable_contract, slice_func, slice_payload, @@ -367,7 +344,7 @@ def execute_aligned_stack_slice( slice_count, reference_payload=slice_payload, ), - ) + ).call() slice_results, _slice_execute_seconds = self.execute_pure_2d_slice_batch( callable_contract, @@ -476,12 +453,12 @@ def execute_pure_2d( ) image_data = image_payload_data(image) if not isinstance(image_data, np.ndarray): - return self.invoke_raw( + return _CELLPROFILER_RUNTIME_CALLABLE_POLICY.contract_invocation( callable_contract, func, image, kwargs, - ) + ).call() prepare_started_at = time.perf_counter() memory_type = detect_memory_type(image_data) @@ -504,12 +481,12 @@ def execute_pure_2d( "declared plane projection. Kwargs cannot create image-axis " "execution semantics." ) - return self.invoke_raw( + return _CELLPROFILER_RUNTIME_CALLABLE_POLICY.contract_invocation( callable_contract, func, image, kwargs, - ) + ).call() declared_plane_axis = image_payload_metadata(image).plane_axis if declared_plane_axis is not self.plane_projection.axis: raise RuntimeSliceProjectionDeclarationError( @@ -609,12 +586,12 @@ def execute_volumetric_to_slice( callable_contract: CallableContract, **kwargs: RuntimeCallableArgument, ) -> RuntimeCallableArgument: - result_2d = self.invoke_raw( + result_2d = _CELLPROFILER_RUNTIME_CALLABLE_POLICY.contract_invocation( callable_contract, func, image, kwargs, - ) + ).call() result_data = image_payload_data(result_2d) result_mask = image_payload_mask(result_2d) result_metadata = image_payload_metadata(result_2d) @@ -642,12 +619,12 @@ def execute_pure_2d_slice( sliced_kwargs = dict(sliced_kwargs) slice_index_name = SliceIndexRuntimeParameter.require_parameter_name() sliced_kwargs[slice_index_name] = slice_index - result = self.invoke_raw( + result = _CELLPROFILER_RUNTIME_CALLABLE_POLICY.contract_invocation( callable_contract, func, slice_2d, sliced_kwargs, - ) + ).call() return contextualize_main_image_output(slice_2d, result) def slice_pure_2d_kwargs( diff --git a/openhcs/processing/backends/lib_registry/registry_service.py b/openhcs/processing/backends/lib_registry/registry_service.py index 36459249c1..b730021e07 100644 --- a/openhcs/processing/backends/lib_registry/registry_service.py +++ b/openhcs/processing/backends/lib_registry/registry_service.py @@ -21,7 +21,7 @@ from zmqruntime import OperationCancellation from zmqruntime.client import endpoint_process -from openhcs.core.callable_contract import CallableContract, CallableProjection +from openhcs.core.callable_contract import CallableProjection from openhcs.core.function_reference import ResolvedRegistryFunction from openhcs.core.processing_preparation import ( CallablePreparation, @@ -163,7 +163,7 @@ def prepare_in_current_process( ) emit_status("Resolving prepared callable signatures") for func in callables: - CallableContract.warm_canonical_signature(func) + CallableProjection.from_callable(func).warm_canonical_signature() emit_status(f"Registered kernels ready ({len(callables)} callables)") emit_status(f"Function catalog ready ({len(cls._metadata_cache)} callables)") return cls._metadata_cache diff --git a/openhcs/processing/backends/lib_registry/unified_registry.py b/openhcs/processing/backends/lib_registry/unified_registry.py index f6097a5e74..a30e9af840 100644 --- a/openhcs/processing/backends/lib_registry/unified_registry.py +++ b/openhcs/processing/backends/lib_registry/unified_registry.py @@ -75,7 +75,6 @@ from openhcs.core.runtime_array_values import RuntimeArrayPayload, is_array_payload from openhcs.core.runtime_batch_contracts import ( Pure2DSliceBatchExecutor, - RuntimeBatchExecutionDomain, RuntimePure2DSliceBatchRequest, runtime_batch_executors_from_callable, ) @@ -283,6 +282,16 @@ class RuntimeCallablePolicy: RuntimeInvocationKwargPolicy.PASS_THROUGH ) + def contract_invocation( + self, contract: "CallableContract", func: Callable[..., Any], + image: Any, kwargs: Mapping[str, Any], + ) -> RuntimeCallableInvocation: + """Bind the prepared main-image carrier and actual raw execution ABI.""" + return self.invocation( + func, (contract.raw_main_flow_call_argument(image),), kwargs, + signature=contract.raw_runtime_signature, + ) + def invocation( self, func: Callable[..., Any], @@ -1813,14 +1822,7 @@ def execute_pure_2d(self, func, image, *args, **kwargs): kwargs.setdefault(parameter.name, value) args = args[len(positional_parameters) :] slices = slicer.slice_value(image, input_memory_type) - declared_batch_executor = runtime_batch_executors_from_callable(func).get( - RuntimeBatchExecutionDomain.PURE_2D_SLICES - ) - batch_executor = ( - declared_batch_executor - if callable(declared_batch_executor) - else Pure2DSliceBatchExecutor.default_executor() - ) + batch_executor = Pure2DSliceBatchExecutor.from_executors(runtime_batch_executors_from_callable(func)) def execute_slice( slice_func: Callable[..., Any], diff --git a/tests/unit/test_callable_abi_preparation.py b/tests/unit/test_callable_abi_preparation.py index 4e3e816fa5..8b050ad204 100644 --- a/tests/unit/test_callable_abi_preparation.py +++ b/tests/unit/test_callable_abi_preparation.py @@ -14,7 +14,7 @@ import openhcs.core.callable_contract as contract_module from openhcs.core.callable_contract import ( - CallableContract, CallableImportIdentity, CallableMetadata, + CallableContract, CallableImportIdentity, CallableMetadata, CallableProjection, attach_callable_contract_metadata, attach_processing_prepare, ) from openhcs.core.function_contract_metadata import FunctionContractAttribute @@ -23,7 +23,10 @@ from openhcs.core.processing_preparation import CallablePreparation, PreparationCacheBatch from openhcs.core.runtime_image_values import ImagePayloadMetadata, image_payload_data from openhcs.processing.backends.lib_registry.registry_service import RegistryService -from openhcs.core.runtime_batch_contracts import runtime_callable_defaults +from openhcs.core.runtime_batch_contracts import ( + Pure2DSliceBatchExecutor, RuntimeBatchExecutionDomain, + SerialPure2DSliceBatchExecutor, runtime_callable_defaults, +) from openhcs.core.pipeline.function_contracts import resolved_callable_parameter from openhcs.processing.backends.lib_registry.unified_registry import ( RuntimeCallablePolicy, RuntimeInvocationKwargPolicy, @@ -204,7 +207,7 @@ def raw(image, values=None): def test_supported_declaration_changes_invalidate_published_signature(mutation): def raw(image: np.ndarray): return image - CallableContract.warm_canonical_signature(raw) + CallableProjection.from_callable(raw).warm_canonical_signature() assert FunctionContractAttribute.canonical_signature in vars(raw) mutation(raw) assert FunctionContractAttribute.canonical_signature not in vars(raw) @@ -221,7 +224,7 @@ def raw(image): def test_exact_raw_default_filter_and_type_views_refresh_with_library_snapshot(): def raw(image: np.ndarray, scale: float = 2): return image * scale - CallableContract.warm_canonical_signature(raw) + CallableProjection.from_callable(raw).warm_canonical_signature() policy = RuntimeCallablePolicy(kwarg_policy=RuntimeInvocationKwargPolicy.SIGNATURE_FILTERED) assert runtime_callable_defaults(raw)["scale"] == 2 assert resolved_callable_parameter(raw, "scale").annotation is float @@ -230,7 +233,7 @@ def raw(image: np.ndarray, scale: float = 2): raw.__annotations__["scale"] = int assert runtime_callable_defaults(raw)["scale"] == 2 assert resolved_callable_parameter(raw, "scale").annotation is float - CallableContract.warm_canonical_signature(raw) + CallableProjection.from_callable(raw).warm_canonical_signature() assert runtime_callable_defaults(raw)["scale"] == 4 assert resolved_callable_parameter(raw, "scale").annotation is int @@ -241,7 +244,7 @@ def raw(image: np.ndarray, private: int = 8): def wrapper(image: np.ndarray, public: int = 2): return image * public wrapper.__dict__[FunctionContractAttribute.raw_processing_function] = raw - CallableContract.warm_canonical_signature(wrapper) + CallableProjection.from_callable(wrapper).warm_canonical_signature() assert runtime_callable_defaults(wrapper) == {"public":2} assert resolved_callable_parameter(wrapper, "public").annotation is int policy = RuntimeCallablePolicy(kwarg_policy=RuntimeInvocationKwargPolicy.SIGNATURE_FILTERED) @@ -257,7 +260,7 @@ def raw(image, labels): return image raw.__annotations__ = {"image":Annotated[np.ndarray,"image"],"labels":tuple[Annotated[int,"id"],...],"return":Annotated[np.ndarray,"result"]} expected = get_type_hints(raw) - CallableContract.warm_canonical_signature(raw) + CallableProjection.from_callable(raw).warm_canonical_signature() assert resolved_callable_parameter(raw,"image").annotation == expected["image"] assert resolved_callable_parameter(raw,"labels").annotation == expected["labels"] assert CallableContract.from_callable(raw).canonical_parameter_annotations["labels"] == raw.__annotations__["labels"] @@ -290,7 +293,7 @@ def wrapper(*args, **kwargs): assert second.raw_runtime_signature.parameters["scale"].annotation is float -def test_same_runtime_layout_reuses_canonical_without_comparing_mutable_defaults(fake_preparation): +def test_distinct_targets_own_signatures_even_when_layouts_match_without_comparing_defaults(fake_preparation): class RejectEquality: def __eq__(self, other): raise AssertionError("Signature reuse compared a mutable default") @@ -301,6 +304,31 @@ def raw(image: np.ndarray, values=default) -> np.ndarray: def wrapper(*args, **kwargs): return raw(*args, **kwargs) contract = CallableContract.from_prepared_callable(wrapper) + assert contract.metadata.raw_runtime_signature is not None + assert contract.raw_runtime_signature is not contract.canonical_signature + assert contract.raw_runtime_signature.parameters["values"].default is default + raw.__defaults__ = ("new raw default",) + second = CallableContract.from_prepared_callable(wrapper) + assert second.raw_runtime_signature.parameters["values"].default == "new raw default" + assert contract.raw_runtime_signature.parameters["values"].default is default + + +def test_same_actual_target_shares_canonical_signature(fake_preparation): + contract = CallableContract.from_prepared_callable(raw_numeric) assert contract.metadata.raw_runtime_signature is None assert contract.raw_runtime_signature is contract.canonical_signature - assert contract.raw_runtime_signature.parameters["values"].default is default + + +@pytest.mark.parametrize("executors", (None, {}, {RuntimeBatchExecutionDomain.PURE_2D_SLICES: None})) +def test_pure_2d_executor_selection_retains_serial_fallback(executors): + assert isinstance(Pure2DSliceBatchExecutor.from_executors(executors), SerialPure2DSliceBatchExecutor) + + +def test_pure_2d_executor_selection_retains_exact_declared_executor(): + def declared(*args, **kwargs): + raise AssertionError("Selecting an executor must not execute it") + + selected = Pure2DSliceBatchExecutor.from_executors({ + RuntimeBatchExecutionDomain.PURE_2D_SLICES: declared, + }) + assert selected is declared diff --git a/tests/unit/test_cellprofiler_function_contract_execution.py b/tests/unit/test_cellprofiler_function_contract_execution.py index 8ad2e4dbef..76ca8ec07f 100644 --- a/tests/unit/test_cellprofiler_function_contract_execution.py +++ b/tests/unit/test_cellprofiler_function_contract_execution.py @@ -27,6 +27,7 @@ from openhcs.core.callable_contract import ( CallableContract, CallableMetadata, + CallableProjection, callable_request, ) from openhcs.core.runtime_plane_projection import ( @@ -55,6 +56,9 @@ morphologicalskeleton, ) from openhcs.processing.backends.lib_registry.unified_registry import ProcessingContract +from openhcs.processing.backends.lib_registry.unified_registry import ( + RuntimeCallablePolicy, RuntimeInvocationKwargPolicy, +) def _compiled_contract( @@ -422,9 +426,9 @@ def __init__(self, plane_projection=None): super().__init__(plane_projection=plane_projection) constructed.append(self) - def invoke_raw(self, callable_contract, func, image, kwargs): + def execute_pure_2d_slice(self, *args, **kwargs): raw_calls.append(self) - return super().invoke_raw(callable_contract, func, image, kwargs) + return super().execute_pure_2d_slice(*args, **kwargs) def raw(image: np.ndarray) -> np.ndarray: assert isinstance(image, np.ndarray) @@ -456,7 +460,8 @@ def foreign_raw(image: ArrayPayload) -> ArrayPayload: foreign_contract = _compiled_contract(foreign_raw, ProcessingContract.PURE_2D) child = constructed[1] - assert child.invoke_raw(foreign_contract, foreign_raw, source, {}) is source + policy = RuntimeCallablePolicy(kwarg_policy=RuntimeInvocationKwargPolicy.SIGNATURE_FILTERED) + assert policy.contract_invocation(foreign_contract, foreign_raw, source, {}).call() is source assert not hasattr(child, "_scope_contract") @@ -465,7 +470,7 @@ def test_actual_prepared_executor_planes_make_no_signature_or_hint_queries(monke def raw(image: np.ndarray, scale: int = 2) -> np.ndarray: calls.append(image.shape) return image * scale - CallableContract.warm_canonical_signature(raw) + CallableProjection.from_callable(raw).warm_canonical_signature() contract = _compiled_contract( raw, ProcessingContract.PURE_2D, artifact_outputs=(ArtifactSpec.output("Result",ImageArtifactType),), From 0b7f6f98a4b19d85d0dbb20f4db49e45cd0d90c1 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 01:45:24 -0400 Subject: [PATCH 041/235] Validate explicit object planes without requiring acquisition provenance Refs #419. Follow up #429: independently authored runtime planes can own an exact object projection without acquisition source-plane records. Preserve source-axis and nonempty provenance cardinality checks, plus physical label/source shape validation. --- openhcs/core/runtime_object_label_building.py | 13 +++++---- .../test_saved_source_label_plane_domain.py | 28 +++++++++++++++++++ 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/openhcs/core/runtime_object_label_building.py b/openhcs/core/runtime_object_label_building.py index cac8c54e0a..e7dba0d23b 100644 --- a/openhcs/core/runtime_object_label_building.py +++ b/openhcs/core/runtime_object_label_building.py @@ -142,13 +142,14 @@ def plane_semantics(self) -> RuntimePlaneAxisValueProjection | None: ) return None metadata = self.metadata - declared_projection = RuntimePlaneAxisValueProjection.from_source_declaration( - metadata.plane_axis, metadata.source_provenance, - ) - if declared_projection is not None: + if metadata.plane_axis is not None: + declared_count = metadata.source_provenance.source_plane_count if ( - projection.axis is not declared_projection.axis - or projection.axis_size != declared_projection.axis_size + projection.axis is not metadata.plane_axis + or ( + declared_count > 0 + and projection.axis_size != declared_count + ) ): raise ValueError( "Object-label plane projection conflicts with the source-image " diff --git a/tests/unit/test_saved_source_label_plane_domain.py b/tests/unit/test_saved_source_label_plane_domain.py index 2383b24259..a8c9f6c7ef 100644 --- a/tests/unit/test_saved_source_label_plane_domain.py +++ b/tests/unit/test_saved_source_label_plane_domain.py @@ -41,6 +41,34 @@ def _saved_label_input(source): ) +@pytest.mark.parametrize("axis", tuple(RuntimePlaneAxis)) +@pytest.mark.parametrize("plane_count", (1, 2)) +def test_explicit_object_projection_preserves_axis_without_acquisition_provenance( + axis, plane_count, +): + labels = np.ones((plane_count, 6, 7), dtype=np.int32) + source = ImagePayloadMetadata(plane_axis=axis).payload_with(labels) + projection = RuntimePlaneAxisValueProjection.preserve( + axis=axis, axis_size=plane_count, + ) + payload = SourceImageObjectLabelBuildRequest( + image=source, labels=labels, plane_projection=projection, + ).payload() + + assert payload.plane_axis is axis + 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) + with pytest.raises(ValueError, match="conflicts with the source-image axis"): + SourceImageObjectLabelBuildRequest( + image=source, labels=labels, + plane_projection=RuntimePlaneAxisValueProjection.preserve( + axis=other_axis, axis_size=plane_count, + ), + ).payload() + + @pytest.mark.parametrize("axis", tuple(RuntimePlaneAxis)) def test_original_artifact_admission_and_full_stack_shape_keep_2d_planes(axis): labels = np.zeros((2, 8, 9), dtype=np.int32) From 585413317f513b8b183326e5880504c8ac17700a Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 01:52:59 -0400 Subject: [PATCH 042/235] Record prepared ABI owner gates and unresolved scientific qualification failures Refs #384, #419, #433. Preserve exact source-pinned controls, original architecture guard outcomes, confirmed OOM evidence and partial cohort capture limits. --- .../shared_runtime_plumbing_384_20261001.rst | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/docs/validation/shared_runtime_plumbing_384_20261001.rst b/docs/validation/shared_runtime_plumbing_384_20261001.rst index c6181ca111..13157c1bda 100644 --- a/docs/validation/shared_runtime_plumbing_384_20261001.rst +++ b/docs/validation/shared_runtime_plumbing_384_20261001.rst @@ -592,3 +592,61 @@ Source freezes, complete observations and diagnostic boundaries are retained in ``/var/tmp/openhcs-owned-metadata-90c30-ordinary-abba-20261002/`` and ``/var/tmp/openhcs-owned-metadata-phase-probe-20261002/``. The original 026 comparison and original failures remain separate pinned evidence. + + +Prepared callable owner and independent plane follow-up (2026-10-02) +------------------------------------------------------------------ + +Registry preparation now resolves canonical and actual raw signatures after all +function preparation hooks and before READY. Authored declarations resolve in +compilation. Immutable signature pairs travel on the existing CallableMetadata +through existing function-reference/worker transport. Runtime filtering, batch +defaults and canonical argument admission consume the associated prepared view; +three separate signature/default/type LRU caches and late kernel preparation are +removed. Server preparation remains outside pipeline clocks. + +The isolated owner repair ``760e9ae8613d8737411a627b6abfe6a15f4ca0b9`` moves actual +target resolution, signature state and carrier grammar onto CallableMetadata; +CallableProjection/Reader own preparation admission, RuntimeCallablePolicy owns +invocation, and Pure2DSliceBatchExecutor owns the formerly duplicated executor +selection. Distinct actual targets always receive independent raw snapshots; +only the same actual target shares its canonical snapshot. No new wrapper class +or equality comparison over arbitrary callable defaults is introduced. + +Original, unchanged R0 passes against both the prepared-callable commit and the +frozen qualification source, eliminating its new class-size, Boolean-chain and +foreign-absence debt. Original R1 passes within the unchanged 160-second budget +with 3,084 projections. These are isolated gates; the inherited scalar-owner +per-file R0 gate on the whole branch remains open. Focused owner tests pass 105 +controls; the larger 648-pass/five-skip suite initially retained two baseline-red +plane-domain controls. Their failure also reproduces on untouched ``94915``. + +The independent repair ``0b7f6f98a4b19d85d0dbb20f4db49e45cd0d90c1`` validates an +explicit object projection against its declared source axis and nonempty +acquisition cardinality directly. Independently authored runtime planes can own +an exact projection without acquisition-plane provenance. Physical source/label +cardinality and spatial-shape checks remain mandatory. Thirty-nine controls pass, +including both axes at one/two planes and wrong-axis refusal. No object-domain +semantics are inferred from source storage axes. + +The all-30 scientific qualification on frozen ``94915`` is RED: 24 cases pass; +four fail at missing payload context in Align/Crop, one loses its server to a +confirmed kernel OOM kill, and one differs in threshold entropy. Saved input +replay traces that entropy mismatch to missing Threshold source scale/dtype at +the canonical raw boundary. Truthful declarations are being corrected without +scientific-body or tolerance changes. Linux killed endpoint PID 2412356 with +13,360,004 KiB anonymous RSS; the dominant allocation/retaining owner is still +unmeasured. Issue #433 tracks that distinct execution-server acceptance gate and +is formally linked to this draft PR. Retained native clocks are not current +paired speedup evidence. + +Evidence stays in the external benchmark RUNS directory, including +``pr394-94915-all30-native-science-qualification-red-20261002.json``, +``pr394-signature-owner-760-source-inventory-20261002.json``, the original R0/R1 +receipts, and ``issue419-explicit-object-plane-cardinality-followup-20261002.json``. +Two partial cohort captures complete all 31 production steps and match all six +CSV files and 120 TIFF dtype/shape/pixel arrays exactly, but each admits only six +of ten graph snapshots. V4 identifies an unsupported native allocation owner in +the four remaining before-state graphs; those graphs remain missing. Diagnostic +capture clocks do not establish a speedup, and the additional multi-second +whole-value plumbing payoff remains unmeasured. From 9ae4c9426cb767d3b2cbe997799bd0d211f16c4d Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 01:54:23 -0400 Subject: [PATCH 043/235] Preserve image context for prepared CellProfiler calls (#419) --- .../backends/cellprofiler/alignment.py | 3 +- .../backends/cellprofiler/area_occupied.py | 2 +- .../processing/backends/cellprofiler/crop.py | 5 +- .../backends/cellprofiler/image_quality.py | 3 +- .../backends/cellprofiler/intensity.py | 2 +- .../backends/cellprofiler/manual_objects.py | 3 +- .../backends/cellprofiler/object_images.py | 3 +- .../backends/cellprofiler/thresholding.py | 5 +- .../processing/backends/cellprofiler/worms.py | 12 +- ...rofiler_additional_prepared_payload_abi.py | 184 ++++++++++++++++++ 10 files changed, 206 insertions(+), 16 deletions(-) create mode 100644 tests/unit/test_cellprofiler_additional_prepared_payload_abi.py diff --git a/openhcs/processing/backends/cellprofiler/alignment.py b/openhcs/processing/backends/cellprofiler/alignment.py index 9ef9b11ba2..b32e4a5ef5 100644 --- a/openhcs/processing/backends/cellprofiler/alignment.py +++ b/openhcs/processing/backends/cellprofiler/alignment.py @@ -40,6 +40,7 @@ RuntimePlaneAxisValueProjection, ) from openhcs.core.runtime_slice_projection import RuntimeSliceProjection +from openhcs.core.runtime_array_values import RuntimeArrayData from openhcs.core.runtime_image_values import ( ImagePayloadMetadata, MaskedImagePayload, @@ -1083,7 +1084,7 @@ def prepare_align() -> None: @required_variable_components(VariableComponents.CHANNEL) @numpy(contract=ProcessingContract.PURE_3D) def align( - image: np.ndarray, + image: RuntimeArrayData, *, method: AlignModule.Method = AlignModule.Method.MUTUAL_INFORMATION, crop_mode: AlignModule.CropMode = AlignModule.CropMode.KEEP_SIZE, diff --git a/openhcs/processing/backends/cellprofiler/area_occupied.py b/openhcs/processing/backends/cellprofiler/area_occupied.py index 44049e52b8..e748871e96 100644 --- a/openhcs/processing/backends/cellprofiler/area_occupied.py +++ b/openhcs/processing/backends/cellprofiler/area_occupied.py @@ -728,7 +728,7 @@ def measure(self) -> AreaOccupiedMeasurement: SliceIndexRuntimeParameter, ) def measure_image_area_occupied( - image: np.ndarray, + image: RuntimeArrayData, *, operand_choices: Sequence[OperandChoice] = (OperandChoice.BINARY_IMAGE,), area_occupied_rows: Sequence[AreaOccupiedRow] = (), diff --git a/openhcs/processing/backends/cellprofiler/crop.py b/openhcs/processing/backends/cellprofiler/crop.py index 62fd7e1ba9..79db9fe1e5 100644 --- a/openhcs/processing/backends/cellprofiler/crop.py +++ b/openhcs/processing/backends/cellprofiler/crop.py @@ -24,6 +24,7 @@ ) from openhcs.core.pipeline.function_contracts import special_inputs from openhcs.core.runtime_measurements import RuntimeMeasurementFeature +from openhcs.core.runtime_array_values import RuntimeArrayData from openhcs.core.runtime_image_values import ( ImagePayloadMetadata, image_payload_data, @@ -608,7 +609,7 @@ def cropped_pixels(self) -> np.ndarray: class CropRequest: """Executable CellProfiler Crop request.""" - image: np.ndarray + image: RuntimeArrayData mask_plane: np.ndarray | None = None crop_shape: CropModule.Shape = CropModule.Shape.RECTANGLE cropping_method: CropModule.Method = CropModule.Method.COORDINATES @@ -857,7 +858,7 @@ def crop_output_metadata( @numpy_decorator(contract=ProcessingContract.PURE_2D) @special_inputs("topology_inputs") def crop( - image: np.ndarray, + image: RuntimeArrayData, topology_inputs: tuple[np.ndarray | ObjectLabelValue, ...] = (), crop_shape: CropModule.Shape = CropModule.Shape.RECTANGLE, cropping_method: CropModule.Method = CropModule.Method.COORDINATES, diff --git a/openhcs/processing/backends/cellprofiler/image_quality.py b/openhcs/processing/backends/cellprofiler/image_quality.py index e4338ba2f8..741d26e074 100644 --- a/openhcs/processing/backends/cellprofiler/image_quality.py +++ b/openhcs/processing/backends/cellprofiler/image_quality.py @@ -2,6 +2,7 @@ from __future__ import annotations +from openhcs.core.runtime_array_values import RuntimeArrayData from openhcs.core.process_local_cache import RegisteredProcessLocalBoundedCache from abc import ABC, abstractmethod @@ -1565,7 +1566,7 @@ def _haralick_h3_numba(image: np.ndarray, scale: int) -> float: @numpy(contract=ProcessingContract.PURE_2D) def measure_image_quality( - image: np.ndarray, + image: RuntimeArrayData, include_scaling: bool = True, calculate_blur: bool = True, calculate_saturation: bool = True, diff --git a/openhcs/processing/backends/cellprofiler/intensity.py b/openhcs/processing/backends/cellprofiler/intensity.py index aae7b8583d..b400696f06 100644 --- a/openhcs/processing/backends/cellprofiler/intensity.py +++ b/openhcs/processing/backends/cellprofiler/intensity.py @@ -1560,7 +1560,7 @@ def _object_intensity_batch_key( @object_label_input_execution_mode(ObjectLabelInputExecutionMode.SLICE_ALIGNED) @runtime_bound_parameters(SliceIndexRuntimeParameter) def measure_object_intensity( - image: np.ndarray, + image: RuntimeArrayData, labels: ObjectLabelValue, object_intensity_backend_provider: BackendProviderInput = DEFAULT_CELLPROFILER_BACKEND_SELECTION, slice_index: int = OBJECT_INTENSITY_DEFAULT_SLICE_INDEX, diff --git a/openhcs/processing/backends/cellprofiler/manual_objects.py b/openhcs/processing/backends/cellprofiler/manual_objects.py index f4c64435b8..6ae87fa586 100644 --- a/openhcs/processing/backends/cellprofiler/manual_objects.py +++ b/openhcs/processing/backends/cellprofiler/manual_objects.py @@ -16,6 +16,7 @@ import numpy as np +from openhcs.core.runtime_array_values import RuntimeArrayData from openhcs.core.artifacts import ( ArtifactSpec, ImageArtifactType, @@ -63,7 +64,7 @@ class ManualObjectStats: ) @numpy(contract=ProcessingContract.PURE_2D) def identify_objects_manually( - image: np.ndarray, + image: RuntimeArrayData, labels_input: np.ndarray | None = None, ) -> tuple[ np.ndarray, diff --git a/openhcs/processing/backends/cellprofiler/object_images.py b/openhcs/processing/backends/cellprofiler/object_images.py index 2ec98235ff..81912cc368 100644 --- a/openhcs/processing/backends/cellprofiler/object_images.py +++ b/openhcs/processing/backends/cellprofiler/object_images.py @@ -10,6 +10,7 @@ from metaclass_registry import AutoRegisterMeta import numpy as np +from openhcs.core.runtime_array_values import RuntimeArrayData from openhcs.core.artifacts import ImageArtifactType, ObjectLabelsArtifactType from openhcs.core.memory import numpy as numpy_decorator from openhcs.core.measurement_row_materialization import ( @@ -181,7 +182,7 @@ def object_label_colormap(colormap_name: str, num_labels: int) -> np.ndarray: @numpy_decorator(contract=ProcessingContract.PURE_2D) def convert_image_to_objects( - image: np.ndarray, + image: RuntimeArrayData, cast_to_bool: bool = False, preserve_label: bool = False, background: int = 0, diff --git a/openhcs/processing/backends/cellprofiler/thresholding.py b/openhcs/processing/backends/cellprofiler/thresholding.py index eeac9dd3fb..7cd50a34d1 100644 --- a/openhcs/processing/backends/cellprofiler/thresholding.py +++ b/openhcs/processing/backends/cellprofiler/thresholding.py @@ -45,6 +45,7 @@ from openhcs.core.pipeline.function_contracts import runtime_bound_parameters from openhcs.core.public_api import public_names_from_objects from openhcs.core.registry_strategies import EnumKeyedStrategyMixin +from openhcs.core.runtime_array_values import RuntimeArrayData from openhcs.core.runtime_image_values import ( ImagePayloadMetadata, image_intensity_scale_for_dtype, @@ -549,7 +550,7 @@ class GlobalThresholdSourceSelection: method_parameters: GlobalThresholdMethodParameters -def normalize_cellprofiler_image(image: np.ndarray) -> np.ndarray: +def normalize_cellprofiler_image(image: RuntimeArrayData) -> np.ndarray: """Return an image in CellProfiler's normalized pixel-data convention.""" return image_payload_data( normalize_cellprofiler_image_payload( @@ -2578,7 +2579,7 @@ class _ThresholdEmbeddedMaskRuntimeParameter(KeywordRuntimeParameter): @runtime_image_execution_mode(ImagePayloadExecutionMode.FULL_STACK) @numpy(contract=ProcessingContract.PURE_2D) def threshold( - image: np.ndarray, + image: RuntimeArrayData, mask: np.ndarray | None = None, threshold_scope: ThresholdScope = ThresholdScope.GLOBAL, threshold_method: ThresholdMethod = ThresholdMethod.OTSU, diff --git a/openhcs/processing/backends/cellprofiler/worms.py b/openhcs/processing/backends/cellprofiler/worms.py index 74359cf712..1f0964c1a6 100644 --- a/openhcs/processing/backends/cellprofiler/worms.py +++ b/openhcs/processing/backends/cellprofiler/worms.py @@ -1709,7 +1709,7 @@ def canonical_image_output( def _execute_untangle_worms( - image: np.ndarray, + image: RuntimeArrayData, min_worm_area: float = 100.0, max_worm_area: float = 5000.0, num_control_points: int = 21, @@ -1868,7 +1868,7 @@ def _execute_untangle_worms( def _untangle_worms_output( - image: np.ndarray, + image: RuntimeArrayData, *, expected_style: OverlapStyle, overlap_style: OverlapStyle, @@ -1940,7 +1940,7 @@ def _untangle_worms_output( @numpy(contract=ProcessingContract.PURE_2D) def untangle_worms( - image: np.ndarray, + image: RuntimeArrayData, overlap_style: OverlapStyle = OverlapStyle.WITHOUT_OVERLAP, min_worm_area: float = 100.0, max_worm_area: float = 5000.0, @@ -1993,7 +1993,7 @@ def untangle_worms( @numpy(contract=ProcessingContract.PURE_2D) def untangle_worms_with_overlap( - image: np.ndarray, + image: RuntimeArrayData, overlap_style: OverlapStyle = OverlapStyle.WITH_OVERLAP, min_worm_area: float = 100.0, max_worm_area: float = 5000.0, @@ -2046,7 +2046,7 @@ def untangle_worms_with_overlap( @numpy(contract=ProcessingContract.PURE_2D) def untangle_worms_both( - image: np.ndarray, + image: RuntimeArrayData, overlap_style: OverlapStyle = OverlapStyle.BOTH, min_worm_area: float = 100.0, max_worm_area: float = 5000.0, @@ -2226,7 +2226,7 @@ def straighten_worms( @numpy(contract=ProcessingContract.PURE_2D) def identify_dead_worms( - image: np.ndarray, + image: RuntimeArrayData, worm_width: int = 10, worm_length: int = 100, angle_count: int = 32, diff --git a/tests/unit/test_cellprofiler_additional_prepared_payload_abi.py b/tests/unit/test_cellprofiler_additional_prepared_payload_abi.py new file mode 100644 index 0000000000..39fa425644 --- /dev/null +++ b/tests/unit/test_cellprofiler_additional_prepared_payload_abi.py @@ -0,0 +1,184 @@ +"""Prepared raw arguments retain context consumed by nominal CP request owners.""" +from dataclasses import replace + +import numpy as np +import pytest + +from openhcs.core.aligned_image_payload import ImagePayloadExecutionMode +from openhcs.core.callable_contract import CallableContract +from openhcs.core.runtime_image_values import ( + ImagePayloadMetadata, + image_payload_data, + image_payload_mask, + image_payload_metadata, + normalize_image_payload_intensity, +) +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 +from openhcs.interop.cellprofiler.runtime.function_contract_execution import ( + CellProfilerFunctionContractExecutor, +) +from openhcs.processing.backends.cellprofiler.alignment import AlignModule, align +from openhcs.processing.backends.cellprofiler.area_occupied import measure_image_area_occupied +from openhcs.processing.backends.cellprofiler.crop import CropModule, crop +from openhcs.processing.backends.cellprofiler.image_quality import measure_image_quality +from openhcs.processing.backends.cellprofiler.intensity import measure_object_intensity +from openhcs.processing.backends.cellprofiler.manual_objects import identify_objects_manually +from openhcs.processing.backends.cellprofiler.object_images import convert_image_to_objects +from openhcs.processing.backends.cellprofiler.thresholding import threshold +from openhcs.processing.backends.cellprofiler.worms import ( + identify_dead_worms, + untangle_worms, + untangle_worms_both, + untangle_worms_with_overlap, +) + + +def _prepared_contract(func): + contract = CallableContract.from_callable(func).with_prepared_signature() + return replace( + contract, + metadata=replace( + contract.metadata, + runtime_adapter=CellProfilerRuntimeAdapter.runtime_adapter_spec(), + ), + ) + + +@pytest.mark.parametrize( + "func", + ( + align, + crop, + measure_image_area_occupied, + measure_object_intensity, + measure_image_quality, + identify_objects_manually, + convert_image_to_objects, + identify_dead_worms, + threshold, + untangle_worms, + untangle_worms_with_overlap, + untangle_worms_both, + ), +) +def test_prepared_primary_context_consumers_keep_nominal_payload_and_bare_pixels(func): + pixels = np.arange(60, dtype=np.float32).reshape(4, 5, 3) + mask = np.ones((4, 5), dtype=bool) + mask[0] = False + source = ImagePayloadMetadata( + source_path="/input/rgb.tif", + source_image_names=("RGB",), + source_channel_axis=-1, + source_spatial_domain=SourceSpatialDomain( + origin_yx=(2, 3), source_shape_yx=(8, 10) + ), + ).payload_with(pixels, mask) + contract = _prepared_contract(func) + + projected = contract.raw_main_flow_call_argument(source) + + assert projected is source + assert image_payload_mask(projected) is mask + assert image_payload_metadata(projected).source_spatial_domain.origin_yx == (2, 3) + assert image_payload_metadata(projected).source_image_paths == ("/input/rgb.tif",) + assert contract.raw_main_flow_call_argument(pixels) is pixels + + +@pytest.mark.parametrize("color", (False, True)) +def test_prepared_align_preserves_named_plane_axis_masks_and_source_identity(color): + shape = (8, 9, 3) if color else (8, 9) + image = np.random.default_rng(1).random(shape, dtype=np.float32) + pixels = np.stack((image, image)) + masks = np.ones((2, 8, 9), dtype=bool) + masks[:, 0] = False + metadata = ImagePayloadMetadata( + source_image_names=("First", "Second"), + source_image_provenance_planes=SourceImageProvenancePlanes.from_components( + paths=("/input/first.tif", "/input/second.tif"), + component_metadata=({"well": "A01", "channel": "1"}, {"well": "A01", "channel": "2"}), + ), + plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + source_channel_axis=3 if color else None, + source_spatial_domain=SourceSpatialDomain( + origin_yx=(2, 3), source_shape_yx=(14, 17) + ), + ) + source = metadata.payload_with(pixels, masks) + contract = _prepared_contract(align) + + outputs, measurements = CellProfilerFunctionContractExecutor().execute( + contract, + contract.resolve_canonical_raw_callable(), + source, + {"method": AlignModule.Method.NORMALIZED_CROSS_CORRELATION, + "crop_mode": AlignModule.CropMode.KEEP_SIZE}, + execution_mode=ImagePayloadExecutionMode.FULL_STACK, + ) + + assert len(outputs.slices) == 2 + assert measurements.row_count() == 2 + for index, output in enumerate(outputs.slices): + np.testing.assert_array_equal(image_payload_data(output), pixels[index]) + np.testing.assert_array_equal(image_payload_mask(output), masks[index]) + output_metadata = image_payload_metadata(output) + assert output_metadata.source_image_paths == (("/input/first.tif", "/input/second.tif")[index],) + assert output_metadata.source_spatial_domain == metadata.source_spatial_domain + assert output_metadata.source_channel_axis == (2 if color else None) + + +def test_prepared_crop_preserves_rgb_spatial_mask_and_calibrated_source_domain(): + pixels = np.arange(60, dtype=np.float32).reshape(4, 5, 3) + parent_mask = np.ones((4, 5), dtype=bool) + parent_mask[0] = False + domain = SourceSpatialDomain(origin_yx=(2, 3), source_shape_yx=(8, 10)) + source = ImagePayloadMetadata( + source_channel_axis=-1, + source_image_names=("RGB",), + source_path="/input/rgb.tif", + source_spatial_domain=domain, + ).payload_with(pixels, parent_mask) + contract = _prepared_contract(crop) + + output, cropping, measurements = CellProfilerFunctionContractExecutor().execute( + contract, + contract.resolve_canonical_raw_callable(), + source, + {"left_right_rectangle_positions": (1, 4), + "top_bottom_rectangle_positions": (0, 3), + "removal_method": CropModule.RemovalMethod.NO}, + execution_mode=ImagePayloadExecutionMode.NATURAL, + ) + + expected_crop = np.zeros((4, 5), dtype=bool) + expected_crop[:3, 1:4] = True + expected_pixels = pixels.copy() + expected_pixels[~expected_crop] = 0 + np.testing.assert_array_equal(cropping, expected_crop) + np.testing.assert_array_equal(image_payload_data(output), expected_pixels) + np.testing.assert_array_equal(image_payload_mask(output), expected_crop & parent_mask) + output_metadata = image_payload_metadata(output) + assert output_metadata.source_channel_axis == -1 + assert output_metadata.source_spatial_domain == domain + assert output_metadata.source_image_paths == ("/input/rgb.tif",) + assert measurements.row_count() == 1 + + +def test_prepared_threshold_keeps_owned_unit_interval_proof_and_mask(): + pixels = np.arange(20, dtype=np.uint8).reshape(4, 5) + mask = np.ones(pixels.shape, dtype=bool) + mask[0] = False + source = normalize_image_payload_intensity( + ImagePayloadMetadata.for_array(pixels).payload_with(pixels, mask), + dtype=np.float32, + ) + contract = _prepared_contract(threshold) + + projected = contract.raw_main_flow_call_argument(source) + + assert projected is source + assert image_payload_metadata(projected).unit_interval_intensity_scale == 255 + assert image_payload_mask(projected) is mask + np.testing.assert_array_equal(image_payload_data(projected), pixels.astype(np.float32) / 255) From 640cffa1c64e71e118f6a077549c98b45940049f Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 02:01:09 -0400 Subject: [PATCH 044/235] Let the explicit plane projection validate its source declaration Refs #419. Keep axis/cardinality validation on the actual nominal proof owner, eliminating foreign metadata absence checks in the object builder. Existing independent-plane and acquisition-provenance controls pass unchanged. --- openhcs/core/runtime_object_label_building.py | 16 +++------------- openhcs/core/runtime_plane_projection.py | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/openhcs/core/runtime_object_label_building.py b/openhcs/core/runtime_object_label_building.py index e7dba0d23b..e47b3b06a2 100644 --- a/openhcs/core/runtime_object_label_building.py +++ b/openhcs/core/runtime_object_label_building.py @@ -142,19 +142,9 @@ def plane_semantics(self) -> RuntimePlaneAxisValueProjection | None: ) return None metadata = self.metadata - if metadata.plane_axis is not None: - declared_count = metadata.source_provenance.source_plane_count - if ( - projection.axis is not metadata.plane_axis - or ( - declared_count > 0 - and projection.axis_size != declared_count - ) - ): - raise ValueError( - "Object-label plane projection conflicts with the source-image " - "axis declaration." - ) + projection.validate_source_declaration( + metadata.plane_axis, metadata.source_provenance, + ) label_array = np.asarray(self.labels) projection.validate_shape( label_array.shape, diff --git a/openhcs/core/runtime_plane_projection.py b/openhcs/core/runtime_plane_projection.py index fe1bf18bc0..9f7affad17 100644 --- a/openhcs/core/runtime_plane_projection.py +++ b/openhcs/core/runtime_plane_projection.py @@ -297,6 +297,21 @@ def from_projector( axis_size=axis_size, ) + def validate_source_declaration( + self, axis: RuntimePlaneAxis | None, source: "SourceImageProvenance", + ) -> None: + """Check an explicit projection against retained acquisition declarations.""" + if axis is None: + return + declared_count = source.source_plane_count + if self.axis is not axis or ( + declared_count > 0 and self.axis_size != declared_count + ): + raise ValueError( + "Object-label plane projection conflicts with the source-image " + "axis declaration." + ) + @classmethod def require_from_projector( cls, From 7241ca99b4f00e48f2a742c8a1fdf57c2bd896db Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 02:03:27 -0400 Subject: [PATCH 045/235] Declare the intended object domain in the retained-plane regression control Refs #419, #427. The existing main test implicitly produced a PAYLOAD object domain then expected 2D plane measurements. Declare its exact singleton object-plane projection; keep source storage axes independent from global object IDs and genuine volumes. --- tests/unit/test_rescale_stream_plane_declaration.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_rescale_stream_plane_declaration.py b/tests/unit/test_rescale_stream_plane_declaration.py index 50e2cc6c1b..981d2d3c79 100644 --- a/tests/unit/test_rescale_stream_plane_declaration.py +++ b/tests/unit/test_rescale_stream_plane_declaration.py @@ -16,7 +16,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 +from openhcs.core.runtime_plane_projection import RuntimePlaneAxis, RuntimePlaneAxisValueProjection from openhcs.core.source_image_provenance import SourceImageProvenancePlanes from openhcs.core.config import StepSourceBindingsConfig from openhcs.core.source_bindings import NamedSourceBinding @@ -162,7 +162,16 @@ def test_declared_source_plane_has_area_not_volume(retained_site): plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, ) payload = metadata.payload_with(pixels, None) - objects = SourceImageObjectLabelBuildRequest(image=payload, labels=labels).payload() + objects = SourceImageObjectLabelBuildRequest( + image=payload, + labels=labels, + plane_projection=( + RuntimePlaneAxisValueProjection.preserve( + axis=RuntimePlaneAxis.RUNTIME_SLICE, axis_size=1, + ) + if retained_site else None + ), + ).payload() _, rows = measure_object_size_shape.__wrapped__( payload, objects, calculate_advanced=False, calculate_zernikes=False, ) From 84e99c0ddefdc3adf656246254484c36b42ededd Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 02:19:15 -0400 Subject: [PATCH 046/235] Preserve source image context in prepared grid object calls (#419) --- .../processing/backends/cellprofiler/grid.py | 7 +-- ...rofiler_additional_prepared_payload_abi.py | 50 +++++++++++++++++++ 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/openhcs/processing/backends/cellprofiler/grid.py b/openhcs/processing/backends/cellprofiler/grid.py index ad92c9b044..3c83012bde 100644 --- a/openhcs/processing/backends/cellprofiler/grid.py +++ b/openhcs/processing/backends/cellprofiler/grid.py @@ -18,6 +18,7 @@ from openhcs.constants.constants import VariableComponents from openhcs.core.aligned_image_payload import ImagePayloadExecutionMode +from openhcs.core.runtime_array_values import RuntimeArrayData from openhcs.core.artifacts import ( ArtifactSpecCollection, ArtifactSpecRelation, @@ -1293,14 +1294,14 @@ class IdentifyObjectsInGridRequest(GridShapeContext): """Executable request for CellProfiler IdentifyObjectsInGrid semantics.""" registry_key = "identify_objects" - image: np.ndarray + image: RuntimeArrayData shape_choice: ShapeChoice @classmethod def from_runtime( cls, *, - image: np.ndarray, + image: RuntimeArrayData, grid_definition: GridRuntimeDefinitionRequest, shape_choice: ShapeChoice, diameter_choice: DiameterChoice, @@ -1485,7 +1486,7 @@ def draw_grid_overlay( @numpy(contract=ProcessingContract.PURE_2D) @special_inputs("topology_inputs") def identify_objects_in_grid( - image: np.ndarray, + image: RuntimeArrayData, topology_inputs: tuple[SpatialGrid | ObjectLabelValue, ...], grid_rows: int = 8, grid_columns: int = 12, diff --git a/tests/unit/test_cellprofiler_additional_prepared_payload_abi.py b/tests/unit/test_cellprofiler_additional_prepared_payload_abi.py index 39fa425644..1ceb91da58 100644 --- a/tests/unit/test_cellprofiler_additional_prepared_payload_abi.py +++ b/tests/unit/test_cellprofiler_additional_prepared_payload_abi.py @@ -23,6 +23,10 @@ from openhcs.processing.backends.cellprofiler.alignment import AlignModule, align from openhcs.processing.backends.cellprofiler.area_occupied import measure_image_area_occupied from openhcs.processing.backends.cellprofiler.crop import CropModule, crop +from openhcs.processing.backends.cellprofiler.grid import ShapeChoice, identify_objects_in_grid +from openhcs.core.runtime_object_label_building import SourceImageObjectLabelBuildRequest +from openhcs.core.runtime_spatial_grid import SpatialGrid +from openhcs.core.source_metadata import SourceVoxelSpacing from openhcs.processing.backends.cellprofiler.image_quality import measure_image_quality from openhcs.processing.backends.cellprofiler.intensity import measure_object_intensity from openhcs.processing.backends.cellprofiler.manual_objects import identify_objects_manually @@ -56,6 +60,7 @@ def _prepared_contract(func): measure_object_intensity, measure_image_quality, identify_objects_manually, + identify_objects_in_grid, convert_image_to_objects, identify_dead_worms, threshold, @@ -87,6 +92,51 @@ def test_prepared_primary_context_consumers_keep_nominal_payload_and_bare_pixels assert contract.raw_main_flow_call_argument(pixels) is pixels +@pytest.mark.parametrize("shape", ((5, 5), (4, 7))) +@pytest.mark.parametrize("shape_choice", (ShapeChoice.RECTANGLE, ShapeChoice.NATURAL, ShapeChoice.CIRCLE_NATURAL)) +def test_prepared_grid_objects_preserve_rgb_mask_calibration_and_crop_domain(shape, shape_choice): + pixels = np.arange(np.prod(shape) * 3, dtype=np.float32).reshape(*shape, 3) + mask = np.ones(shape, dtype=bool) + mask[0] = False + metadata = ImagePayloadMetadata( + source_path="/input/grid_rgb.tif", + source_channel_axis=-1, + source_voxel_spacing=SourceVoxelSpacing((2.0, 0.7, 0.9)), + source_spatial_domain=SourceSpatialDomain( + origin_yx=(2, 3), source_shape_yx=(shape[0] + 4, shape[1] + 6), + ), + ) + source = metadata.payload_with(pixels, mask) + grid = SpatialGrid( + name="Grid", rows=1, columns=1, x_spacing=4.0, y_spacing=4.0, + x_origin=2.0, y_origin=2.0, source_spatial_shape_yx=shape, + ) + guide_pixels = np.zeros(shape, dtype=np.int32) + guide_pixels[1:3, 1:4] = 1 + guides = SourceImageObjectLabelBuildRequest(image=source, labels=guide_pixels).payload() + topology_inputs = (grid,) if shape_choice is ShapeChoice.RECTANGLE else (grid, guides) + contract = _prepared_contract(identify_objects_in_grid) + + returned_image, _rows, objects = CellProfilerFunctionContractExecutor().execute( + contract, contract.resolve_canonical_raw_callable(), source, + {"topology_inputs": topology_inputs, "shape_choice": shape_choice}, + execution_mode=ImagePayloadExecutionMode.FULL_STACK, + ) + + assert returned_image is source + assert image_payload_mask(returned_image) is mask + assert objects.source_provenance == metadata.source_provenance + assert objects.source_spatial_domain.origin_yx == (2, 3) + assert objects.source_spatial_domain.source_shape_yx == metadata.source_spatial_domain.source_shape_yx + assert objects.parent_image_source_voxel_spacing == metadata.source_voxel_spacing + assert objects.labels.shape == shape + assert objects.domain.declared_object_count == 1 + assert objects.domain.declared_object_ids == (1,) + expanded = objects.source_spatial_domain.materialize(objects.labels, spatial_axes_yx=(0, 1)) + assert expanded.shape == metadata.source_spatial_domain.source_shape_yx + np.testing.assert_array_equal(expanded[2:2 + shape[0], 3:3 + shape[1]], objects.labels) + + @pytest.mark.parametrize("color", (False, True)) def test_prepared_align_preserves_named_plane_axis_masks_and_source_identity(color): shape = (8, 9, 3) if color else (8, 9) From 38067a4a5c348e0c7fcedd1d48f69bb99b7be19b Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 02:24:01 -0400 Subject: [PATCH 047/235] Record prepared payload readiness and native parity qualification (#419) --- .../shared_runtime_plumbing_384_20261001.rst | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/docs/validation/shared_runtime_plumbing_384_20261001.rst b/docs/validation/shared_runtime_plumbing_384_20261001.rst index 13157c1bda..dcfe8e9257 100644 --- a/docs/validation/shared_runtime_plumbing_384_20261001.rst +++ b/docs/validation/shared_runtime_plumbing_384_20261001.rst @@ -650,3 +650,53 @@ of ten graph snapshots. V4 identifies an unsupported native allocation owner in the four remaining before-state graphs; those graphs remain missing. Diagnostic capture clocks do not establish a speedup, and the additional multi-second whole-value plumbing payoff remains unmeasured. + + +Prepared payload contract qualification (2026-10-02) +-------------------------------------------------- + +The integrated library/kernel readiness run at ``1ad3898`` prepares all 267 +functions and 976 targets before READY. It creates 265 distinct actual raw +signature snapshots and reuses the canonical snapshot for two identical actual +targets. Three rounds of prepared annotation, carrier, raw-signature, batch +default and invocation-construction queries perform zero additional live +signature/type-hint resolutions. The fresh-cache 90.381-second preparation is +server startup diagnostics, excluded from pipeline clocks. Later explicit-plane +validation and test-producer changes do not change these warming/signature owner +sources. This finite check does not claim zero reflection for arbitrary +unprepared authoring or unsupported callable mutation. + +The annotation-only follow-up ``9ae4c9426`` corrects twelve existing context +consumers, three forwarding helpers and CropRequest.image. Existing +RuntimeArrayData preserves their nominal image payload at the actual raw +boundary; NumPy-only consumers still receive NumPy arrays. Scientific bodies, +defaults, decorators and return declarations are unchanged by AST comparison. +Frozen ``7241ca99b`` passes native-science comparison for Colocalization, +Neighbors, YeastColonies and pixel classification with zero differences. +YeastPatches passes its previous Crop failure but subsequently exposes an +incorrect RGB source domain from IdentifyObjectsInGrid. + +Saved actual RGB pixels, source metadata and both 93-ID label artifacts isolate +the remaining defect: IdentifyObjectsInGrid's primary argument and its existing +request field/from_runtime argument falsely declare np.ndarray despite passing +the image to SourceImageObjectLabelBuildRequest. Their raw ABI strips the +1200-by-1600 source domain and (150, 170) crop origin, producing a (1255, 3) +domain for (835, 1255) labels. ``84e99c0dd`` changes only those declarations and +their direct RuntimeArrayData import. Scientific bodies and the refusing domain +guard are unchanged. Seventy-seven scoped controls and 127 integrated controls +pass. A fresh YeastPatches execution at that revision passes the existing native +comparator with zero differences. Its replay uses retained source-pinned native +artifacts, not newly timed native execution. The original 84-entry/38-module AST +frontier now has no remaining exact classmethod context leads; aliases, inherited +dispatch and dynamic routes remain outside that finite inventory. + +The five affected cases have passed across these two targeted sources. The full +all-30 latest-head science gate, the execution-server OOM retaining-owner gate, +the whole-branch scalar per-file architecture gate and fresh ordinary performance +qualification remain open. No current native speedup is inferred from retained +clocks or instrumented captures. Evidence includes +``issue419-actual-integrated-1ad-signature-owner-readiness-20261002.json``, +``pr394-7241-context5-science-qualification-20261002.json``, +``issue419-grid-context-fix-20261002.json`` and +``pr394-84e99-grid-science-qualification-20261002.json`` in the benchmark RUNS +directory. All original failed source-pinned runs remain retained. From 45f3a721e115b37ab662f362fa5ab4d5e58e1f6e Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 02:28:08 -0400 Subject: [PATCH 048/235] Bind graph ROI source metadata through existing archive owners (#134) --- .../shared_runtime_plumbing_384_20261001.rst | 28 ++ openhcs/processing/materialization/core.py | 10 +- .../test_graph_roi_source_roundtrip_134.py | 272 ++++++++++++++++++ .../test_spatial_graph_materialization.py | 3 +- 4 files changed, 311 insertions(+), 2 deletions(-) create mode 100644 tests/unit/test_graph_roi_source_roundtrip_134.py diff --git a/docs/validation/shared_runtime_plumbing_384_20261001.rst b/docs/validation/shared_runtime_plumbing_384_20261001.rst index dcfe8e9257..023b75894e 100644 --- a/docs/validation/shared_runtime_plumbing_384_20261001.rst +++ b/docs/validation/shared_runtime_plumbing_384_20261001.rst @@ -700,3 +700,31 @@ clocks or instrumented captures. Evidence includes ``issue419-grid-context-fix-20261002.json`` and ``pr394-84e99-grid-science-qualification-20261002.json`` in the benchmark RUNS directory. All original failed source-pinned runs remain retained. + +Graph ROI source-bearing writer integration (issue #134) +------------------------------------------------------- + +The receiving owner requested narrow integration from PR #404 checkpoint +``f3e566485a391a270a6d2c40dbfddf07881dcd3d``. The exact patch binds existing +ROIArchiveSourceMetadata to graph ROI content and places the same existing +ImagePayloadMetadata on Output. Graph provenance already selects its declared +source plane; no second global-plane selection is performed. Coordinate spacing +uses existing SourceVoxelSpacing. The binder, disk ZIP transport, geometry +projection and native admission retain their existing owners and guards. + +On current ``38067a4a5`` before the production patch, the receiving seven-case +fixture reproduces three source-bearing failures and four refusal controls +pass. After integration, all seven pass alongside the original graph, batch +outcome and materialization controls: 87 PASS in 2.28 seconds. The existing +geometry test obtains geometry through the original archive projection before +its unchanged coordinate and feature assertions; bound transport metadata is not +mistaken for a graph feature. An independent cooperative graph subtype and new +feature require no new writer or consumer registration. Missing, unbound, mixed +and conflicting native source declarations remain refused before transport. + +This is current-source synthetic writer/disk/archive qualification. Actual +installed public/native graph reopening remains with the receiving owner, and +no performance or biological acceptance claim follows. Original failures remain +in ``pr394-38067-graph-roi-before-20261002.log``; integrated controls are in +``pr394-graph-roi-integrated-after-20261002.log`` in RUNS. The historical PR #404 +branch and its older publication proposal are not merged as a whole. diff --git a/openhcs/processing/materialization/core.py b/openhcs/processing/materialization/core.py index a095267a20..61340a6503 100644 --- a/openhcs/processing/materialization/core.py +++ b/openhcs/processing/materialization/core.py @@ -73,6 +73,7 @@ source_component_metadata_value, source_metadata_value, ) +from openhcs.core.source_metadata import SourceVoxelSpacing from openhcs.core.source_spatial_domain import SourceSpatialDomain from openhcs.core.steps.stream_component_semantics import ( StreamImagePayloadMetadataProjector, @@ -3284,6 +3285,8 @@ def _write_spatial_graph_roi_zip( from polystore.roi import ROI, PolylineShape + from openhcs.core.roi_source_metadata import ROIArchiveSourceMetadata + materialization_input = MaterializationInput.from_value(data, options) graph = materialization_input.data if not isinstance(graph, SpatialGraph): @@ -3292,6 +3295,10 @@ def _write_spatial_graph_roi_zip( f"got {type(graph).__name__}." ) + source_metadata = ImagePayloadMetadata( + source_provenance=graph.source_provenance, + source_voxel_spacing=SourceVoxelSpacing(graph.coordinate_spacing), + ) rois = [] for edge in graph.edges: coordinates = np.asarray(edge.coordinates, dtype=float) @@ -3333,7 +3340,8 @@ def _write_spatial_graph_roi_zip( return [ Output( path=ctx.paths(options).primary_output_path(options), - content=rois, + content=ROIArchiveSourceMetadata.bind(rois, source_metadata), + metadata=source_metadata, ) ] diff --git a/tests/unit/test_graph_roi_source_roundtrip_134.py b/tests/unit/test_graph_roi_source_roundtrip_134.py new file mode 100644 index 0000000000..83f0b9b478 --- /dev/null +++ b/tests/unit/test_graph_roi_source_roundtrip_134.py @@ -0,0 +1,272 @@ +"""Receiving #134: actual graph context/writer/ZIP, without a native process.""" + +from pathlib import Path + +import numpy as np +import pytest +from polystore.disk import DiskStorageBackend +from polystore.filemanager import FileManager +from polystore.roi import load_rois_from_zip + +from openhcs.core.artifacts import ( + ArtifactOutputPlan, + ArtifactSpec, + ObjectArtifactMemberSubjectRelation, + ObjectArtifactSubjectBinding, + ObjectLabelsArtifactType, + SpatialGraphArtifactType, +) +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, + SpatialGraphNode, +) +from openhcs.core.source_image_provenance import SourceImageProvenancePlanes +from openhcs.core.source_metadata import SourceVoxelSpacing +from openhcs.core.steps.function_runtime import FunctionOutputContextStrategy +from openhcs.core.viewer_streaming_service import RoiStreamingRequest, StreamingService +from openhcs.processing.materialization import ( + MaterializationSpec, + SpatialGraphROIOptions, + materialization_outputs, + materialize, +) + + +@pytest.fixture(autouse=True) +def no_optional_storage_bootstrap(monkeypatch): + import polystore.base + + monkeypatch.setattr( + polystore.base, + "ensure_storage_registry", + lambda: pytest.fail("Provider/bootstrap execution is forbidden in this test"), + ) + + +class ProjectionAudit: + """Independent capability exercises cooperative construction and projection.""" + + def __init__(self, **kwargs): + self.projection_events = [] + super().__init__(**kwargs) + + def contextualized_source_provenance(self, provenance): + self.projection_events.append("before") + result = super().contextualized_source_provenance(provenance) + self.projection_events.append("after") + return result + + +class AuditedGraph(ProjectionAudit, SpatialGraph): + """New declaration; no writer, context strategy or reader registration.""" + + +def graph_and_plan(*, plane, graph_type=SpatialGraph, extra_features=None): + nodes = tuple( + SpatialGraphNode(index + 1, coordinate) + for index, coordinate in enumerate(((2.25, 3.5), (5.25, 9.5), (7.25, 12.5))) + ) + edges = tuple( + SpatialGraphEdge.from_features( + edge_id=index + 1, + source=nodes[index], + target=nodes[index + 1], + coordinates=np.asarray( + (nodes[index].coordinates, nodes[index + 1].coordinates) + ), + features={ + "neuron_label": 7, + "branch_distance_um": 8.5, + **(extra_features or {}), + }, + ) + for index in range(2) + ) + graph = graph_type( + name="declared_graph", + nodes=nodes, + edges=edges, + coordinate_spacing=(1.3556, 1.3556), + source_plane_index=plane, + ) + neurons = ArtifactSpec.output("neurons", ObjectLabelsArtifactType) + plan = ArtifactOutputPlan( + name=graph.name, + path="/engineering/graph.pkl", + artifact_type=SpatialGraphArtifactType, + relations=( + ObjectArtifactMemberSubjectRelation( + source=neurons.ref(), member_id_field="neuron_label" + ), + ), + producer_step_index=4, + producer_step_scope_id="engineering-step", + ) + return graph, plan + + +def contextualize(graph, plan): + metadata = ImagePayloadMetadata( + plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + source_image_provenance_planes=SourceImageProvenancePlanes.from_components( + paths=("/engineering/nuclear.tif", "/engineering/body.tif"), + component_metadata=tuple( + { + "well": "A01", + "site": 1, + "channel": channel, + "z_index": 1, + "timepoint": 1, + } + for channel in (1, 2) + ), + ), + source_image_names=("nuclear", "body"), + source_voxel_spacing=SourceVoxelSpacing(graph.coordinate_spacing), + ) + source = metadata.payload_with(np.zeros((2, 16, 16), dtype=np.uint8)) + result = FunctionOutputContextStrategy.for_output_plan(plan).contextualize( + source, graph, plan, None + ) + assert result.source_provenance == metadata.source_provenance.for_source_plane( + graph.source_plane_index + ) + return result + + +def save_graph(tmp_path, graph, plan): + manager = FileManager({"disk": DiskStorageBackend()}) + path = materialize( + MaterializationSpec(SpatialGraphROIOptions()), + data=graph, + path=str(tmp_path / "misleading_w99_z999.roi.zip"), + filemanager=manager, + backends=["disk"], + backend_kwargs={}, + output_plan=plan, + ) + return Path(path), load_rois_from_zip(Path(path)) + + +@pytest.mark.parametrize("plane", [0, 1]) +def test_declared_source_plane_survives_graph_writer_zip_and_subject_features( + tmp_path, plane +): + graph, plan = graph_and_plan(plane=plane) + graph = contextualize(graph, plan) + (output,) = materialization_outputs( + MaterializationSpec(SpatialGraphROIOptions()), + data=graph, + path=str(tmp_path / "graph.roi.zip"), + filemanager=FileManager({"disk": DiskStorageBackend()}), + output_plan=plan, + ) + path, restored = save_graph(tmp_path, graph, plan) + metadata = ROIArchiveSourceMetadata.decode(restored) + assert metadata is not None, "Native graph ZIP lost its existing source declaration" + assert output.metadata.source_provenance == graph.source_provenance + assert metadata.source_provenance == graph.source_provenance + assert ( + metadata.source_path + == ("/engineering/nuclear.tif", "/engineering/body.tif")[plane] + ) + assert metadata.source_component_metadata["channel"] == plane + 1 + assert metadata.source_voxel_spacing == SourceVoxelSpacing(graph.coordinate_spacing) + assert metadata.plane_axis is None + assert metadata.source_spatial_domain == output.metadata.source_spatial_domain + assert path.name == "misleading_w99_z999.graph.roi.zip" + geometry = ROIArchiveSourceMetadata.geometry(restored) + assert len(geometry) == len(graph.edges) + for roi, edge in zip(geometry, graph.edges, strict=True): + np.testing.assert_array_equal(roi.shapes[0].coordinates, edge.coordinates) + assert roi.metadata["edge_id"] == edge.edge_id + assert roi.metadata["source_node_id"] == edge.source_node_id + assert roi.metadata["target_node_id"] == edge.target_node_id + assert roi.metadata["neuron_label"] == 7 + assert roi.metadata["branch_distance_um"] == 8.5 + assert roi.metadata[ObjectArtifactSubjectBinding.SUBJECT_ID_FEATURE] == 7 + assert ( + '"object_labels","neurons","engineering-step",4' + in roi.metadata[ObjectArtifactSubjectBinding.SUBJECT_FEATURE] + ) + assert ROIArchiveSourceMetadata.FIELD not in roi.metadata + assert ( + len( + { + roi.metadata[ObjectArtifactSubjectBinding.SUBJECT_FEATURE] + for roi in geometry + } + ) + == 1 + ) + + +def test_independent_graph_capability_and_new_feature_need_no_consumers(tmp_path): + original, plan = graph_and_plan( + plane=1, + graph_type=AuditedGraph, + extra_features={"engineering_confidence": 0.375}, + ) + graph = contextualize(original, plan) + assert original.projection_events == ["before", "after"] + assert ( + graph.projection_events == [] + ) # cooperative constructor for contextualized copy + _, restored = save_graph(tmp_path, graph, plan) + metadata = ROIArchiveSourceMetadata.decode(restored) + assert metadata is not None, "New graph declaration lost its source declaration" + assert metadata.source_provenance == graph.source_provenance + assert all(roi.metadata["engineering_confidence"] == 0.375 for roi in restored) + assert all( + ROIArchiveSourceMetadata.FIELD not in roi.metadata + for roi in ROIArchiveSourceMetadata.geometry(restored) + ) + + +@pytest.mark.parametrize("invalid", ["absent", "unbound", "mixed", "conflicting"]) +def test_original_native_admission_refuses_bad_graph_sources_before_transport( + tmp_path, invalid +): + graph, plan = graph_and_plan(plane=1) + graph = contextualize(graph, plan) + path, rois = save_graph(tmp_path, graph, plan) + geometry = ROIArchiveSourceMetadata.geometry(rois) + metadata = ImagePayloadMetadata(source_provenance=graph.source_provenance) + bad_archives = { + "absent": geometry, + "unbound": ROIArchiveSourceMetadata.bind(geometry, ImagePayloadMetadata()), + "mixed": [ + ROIArchiveSourceMetadata.bind(geometry[:1], metadata)[0], + geometry[1], + ], + "conflicting": [ + ROIArchiveSourceMetadata.bind(geometry[:1], metadata)[0], + ROIArchiveSourceMetadata.bind( + geometry[1:], + metadata.replace_fields(source_path="/engineering/other.tif"), + )[0], + ], + } + DiskStorageBackend().save(bad_archives[invalid], path) + # No viewer/handler object: original source admission must reject before using either. + service = StreamingService( + FileManager({"disk": DiskStorageBackend()}), None, tmp_path + ) + with pytest.raises( + ValueError, match="Native ROI source metadata|missing or conflicting" + ): + service.stream_rois( + RoiStreamingRequest( + viewer=None, + config=NapariStreamingConfig(enabled=True), + status_callback=lambda _: None, + error_callback=lambda error: pytest.fail(error), + roi_filenames=(path.name,), + require_source_metadata=True, + ) + ) diff --git a/tests/unit/test_spatial_graph_materialization.py b/tests/unit/test_spatial_graph_materialization.py index 6dc0145d26..0c0e70ef37 100644 --- a/tests/unit/test_spatial_graph_materialization.py +++ b/tests/unit/test_spatial_graph_materialization.py @@ -17,6 +17,7 @@ ObjectLabelsArtifactType, SpatialGraphArtifactType, ) +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 ( @@ -340,7 +341,7 @@ def test_graph_roi_projection_preserves_paths_and_graph_features() -> None: assert roi_output.path == "/tmp/A01_neurite_graph_step3.graph.roi.zip" assert len(roi_output.content) == 3 - first_roi = roi_output.content[0] + first_roi = ROIArchiveSourceMetadata.geometry(roi_output.content)[0] assert isinstance(first_roi.shapes[0], PolylineShape) np.testing.assert_array_equal( first_roi.shapes[0].coordinates, From d092a03af34c3a4193084579d5e1a0f648c8102b Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 03:38:30 -0400 Subject: [PATCH 049/235] fix(runtime): preserve whole-image domains across main-flow handoff Derive persisted image domains from original nominal producer members before physical leaf projection. Keep opaque images intact through copied cache reuse, reload, and artifact-only passthrough while preserving declared runtime/source-binding axes and legacy raw-array unstacking. Reject mixed joint domains and reuse existing bundle composition grammar. Resolves the false leading axis that sent physical 3D images into rank-four scientific execution (#419, #433). --- openhcs/core/aligned_image_payload.py | 38 ++ .../core/steps/function_output_manifest.py | 47 +- openhcs/core/steps/function_runtime.py | 204 ++++++--- tests/unit/test_function_artifact_outputs.py | 8 +- .../test_opaque_main_flow_image_domain.py | 420 ++++++++++++++++++ 5 files changed, 655 insertions(+), 62 deletions(-) create mode 100644 tests/unit/test_opaque_main_flow_image_domain.py diff --git a/openhcs/core/aligned_image_payload.py b/openhcs/core/aligned_image_payload.py index 115ece9341..6c0f82bfd2 100644 --- a/openhcs/core/aligned_image_payload.py +++ b/openhcs/core/aligned_image_payload.py @@ -1140,6 +1140,17 @@ def composition_payloads(self) -> tuple[Any, ...]: def composition_metadata_mode(self) -> ImagePayloadMetadataCompositionMode: return ImagePayloadMetadataCompositionMode.STACK + @property + def projected_output_composition_mode(self) -> ImagePayloadMetadataCompositionMode | None: + """Declare the outer runtime axis retained by projected output members.""" + return self.composition_metadata_mode + + def plane_axis_for_output_context( + self, context: AlignedImageSliceContext, + ) -> RuntimePlaneAxis | None: + """An explicitly aligned stack declares its outer runtime-slice domain.""" + return RuntimePlaneAxis.RUNTIME_SLICE + def composition_payload_metadata( self, metadata: ImagePayloadMetadata ) -> ImagePayloadMetadata: @@ -1286,6 +1297,33 @@ def composition_payloads(self) -> tuple[Any, ...]: def composition_metadata_mode(self) -> ImagePayloadMetadataCompositionMode: return ImagePayloadMetadataCompositionMode.BUNDLE + @property + def projected_output_composition_mode(self) -> ImagePayloadMetadataCompositionMode | None: + """Flatten declared inner runtime planes, retaining other named image domains.""" + if any( + image_payload_metadata(payload).plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + for payload in self.slices + ): + return ImagePayloadMetadataCompositionMode.STACK + if len(self.slices) == 1: + return None + return self.composition_metadata_mode + + def plane_axis_for_output_context( + self, context: AlignedImageSliceContext, + ) -> RuntimePlaneAxis | None: + """Resolve a named output's original inner domain before leaf projection.""" + payloads = tuple( + payload for payload, declared_context in zip(self.slices, self.slice_contexts, strict=True) + if declared_context == context + ) + if len(payloads) != 1: + raise ValueError( + "Named image output context requires exactly one original payload: " + f"{context!r}; found {len(payloads)}." + ) + return image_payload_metadata(payloads[0]).plane_axis + def composition_payload_metadata( self, metadata: ImagePayloadMetadata ) -> ImagePayloadMetadata: diff --git a/openhcs/core/steps/function_output_manifest.py b/openhcs/core/steps/function_output_manifest.py index ea93bbff1e..24ed7998dd 100644 --- a/openhcs/core/steps/function_output_manifest.py +++ b/openhcs/core/steps/function_output_manifest.py @@ -2,7 +2,7 @@ from __future__ import annotations -from dataclasses import dataclass, field +from dataclasses import dataclass, field, replace from pathlib import Path from typing import ClassVar, Iterator, Sequence from weakref import WeakKeyDictionary @@ -23,6 +23,7 @@ ImagePayloadMetadata, image_payload_metadata, ) +from openhcs.core.runtime_plane_projection import RuntimePlaneAxis from openhcs.core.step_dependencies import StepInputDependencyKind from openhcs.core.steps.function_output_identity import ( @@ -113,6 +114,19 @@ class ProducedOutputSemantics(FunctionOutputIdentity): output_path: str relative_output_path: str image_metadata: ImagePayloadMetadata | None = None + main_flow_plane_axis: RuntimePlaneAxis | None = RuntimePlaneAxis.RUNTIME_SLICE + + def passed_through(self, plan: CompiledStepPlan) -> "ProducedOutputSemantics": + """Retain the image domain and physical identity under the next producer.""" + return replace( + self, + producer_identity=FunctionStepOutputProducerIdentityAuthority.build( + FunctionStepOutputProducerIdentityRequest.from_main_flow( + plan, self.output_context, + ) + ), + relative_output_path=source_path_identity(self.output_path).name, + ) def contextualize_image_payload( self, @@ -175,6 +189,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, ) -> "ProducedOutputSemantics": output_path_text = str(output_path) if output_context is None: @@ -197,6 +212,7 @@ def from_output( Path(plan.output_dir), ), image_metadata=image_metadata, + main_flow_plane_axis=main_flow_plane_axis, ) @classmethod @@ -407,14 +423,27 @@ def producer_output_contexts_for_paths( ) -> tuple[AlignedImageSliceContext, ...]: """Return producer output contexts aligned to concrete input paths.""" - producer_records = self._selected_unique_producer_records_for(plan) - if producer_records is None: + records = self.producer_output_records_for_paths(plan, paths, parser) + if records is None: return tuple( AlignedImageSliceContext.anonymous_main_flow() for _path in paths ) + return tuple(record.output_context for record in records) + + def producer_output_records_for_paths( + self, + plan: CompiledStepPlan, + paths: Sequence[str], + parser: FilenameParser, + ) -> tuple[ProducedOutputSemantics, ...] | None: + """Resolve exact producer declarations in the requested physical path order.""" + + producer_records = self._selected_unique_producer_records_for(plan) + if producer_records is None: + return None path_index = ProducedPathRecordIndex.from_records(producer_records, parser) - contexts: list[AlignedImageSliceContext] = [] + records: list[ProducedOutputSemantics] = [] for path in paths: matching_records = path_index.matching_records(path) if len(matching_records) != 1: @@ -422,8 +451,8 @@ def producer_output_contexts_for_paths( "Expected one producer output context for input path " f"{path!r}, found {len(matching_records)}." ) - contexts.append(matching_records[0].output_context) - return tuple(contexts) + records.append(matching_records[0]) + return tuple(records) def filter_to_producer_paths( self, @@ -501,6 +530,12 @@ def _unique_output_path_records( ) -> tuple[ProducedOutputSemantics, ...]: records_by_path: dict[str, ProducedOutputSemantics] = {} for record in records: + existing = records_by_path.get(record.output_path) + if existing is not None and existing.main_flow_plane_axis is not record.main_flow_plane_axis: + raise ValueError( + "One produced output path cannot declare conflicting main-flow image axes: " + f"{record.output_path!r}." + ) records_by_path.setdefault(record.output_path, record) return tuple(records_by_path.values()) diff --git a/openhcs/core/steps/function_runtime.py b/openhcs/core/steps/function_runtime.py index 21f0ea6762..25825890b0 100644 --- a/openhcs/core/steps/function_runtime.py +++ b/openhcs/core/steps/function_runtime.py @@ -2421,6 +2421,7 @@ class PatternGroupOutputData: slices: list[RuntimeArrayData] slice_contexts: tuple[AlignedImageSliceContext, ...] = () stack_payload: RuntimeArrayData | None = None + main_flow_source: RuntimeArrayData | None = None def __post_init__(self) -> None: if not self.slice_contexts: @@ -2433,6 +2434,77 @@ def __post_init__(self) -> None: f"got {len(self.slice_contexts)} context(s) for {len(self.slices)} slice(s)." ) + @classmethod + def from_single_image( + cls, + value: RuntimeArrayData, + *, + slice_contexts: tuple[AlignedImageSliceContext, ...] = (), + memory_type: MemoryType, + device_id: int | None, + ) -> "PatternGroupOutputData": + """Copy one whole image for main-flow reuse without declaring a new axis.""" + copied_data = stack_runtime_slices( + (image_payload_data(value),), memory_type, device_id, + )[0] + mask = image_payload_mask(value) + copied_mask = ( + None if mask is None + else stack_runtime_slices((mask,), memory_type, device_id)[0] + ) + return cls( + slices=(value,), + slice_contexts=slice_contexts, + stack_payload=image_payload_metadata(value).replace_fields().payload_with( + copied_data, copied_mask, + ), + main_flow_source=value, + ) + + def plane_axis_for_output_context( + self, context: AlignedImageSliceContext, + ) -> RuntimePlaneAxis | None: + """Derive each persisted member's domain from its original nominal producer.""" + if isinstance(self.main_flow_source, AlignedImageStack): + return self.main_flow_source.plane_axis_for_output_context(context) + if isinstance(self.main_flow_source, ImagePayloadMetadataCarrier): + return image_payload_metadata(self.main_flow_source).plane_axis + return RuntimePlaneAxis.RUNTIME_SLICE + + def cache_payload_for_outputs( + self, + payloads: Sequence[RuntimeArrayData], + metadata: Sequence[ImagePayloadMetadata], + ) -> RuntimeArrayData | None: + """Keep the original whole-image domain while applying saved output context.""" + if self.stack_payload is None: + return None + data = image_payload_data(self.stack_payload) + if ( + len(payloads) == 1 + and self.plane_axis_for_output_context(self.slice_contexts[0]) is metadata[0].plane_axis + and np.shape(data) == np.shape(image_payload_data(payloads[0])) + ): + return metadata[0].replace_fields().payload_with( + data, image_payload_mask(self.stack_payload), + ) + if np.shape(data)[:1] != (len(payloads),): + raise ValueError( + "PatternGroupOutputData.stack_payload must match its declared " + f"output slice count: stack shape {np.shape(data)!r}, " + f"slice count {len(payloads)}." + ) + return stack_image_payload_context_from_metadata( + payloads, data, metadata, + metadata_mode=( + ImagePayloadMetadataCompositionMode.for_plane_axis( + image_payload_metadata(self.stack_payload).plane_axis, + ) + if isinstance(self.stack_payload, ImagePayloadMetadataCarrier) + else ImagePayloadMetadataCompositionMode.STACK + ), + ) + @classmethod def from_aligned_stack( cls, @@ -2444,15 +2516,32 @@ def from_aligned_stack( """Materialize correlated output slices through the alignment owner.""" projected_outputs = tuple(value.projected_output_slices()) payloads = [payload for payload, _context in projected_outputs] + metadata_mode = value.projected_output_composition_mode + if metadata_mode is None: + result = cls.from_single_image( + payloads[0], + slice_contexts=tuple(context for _payload, context in projected_outputs), + memory_type=memory_type, + device_id=device_id, + ) + result.main_flow_source = value + return result data = tuple(image_payload_data(payload) for payload in payloads) stack_payload = None - if len({tuple(np.shape(item)) for item in data}) == 1: - stacked = stack_runtime_slices(data, memory_type, device_id) - stack_payload = stack_image_payload_context( - payloads, - stacked, - metadata_mode=ImagePayloadMetadataCompositionMode.STACK, - ) + declared_axes = { + value.plane_axis_for_output_context(context) + for _payload, context in projected_outputs + } + if len(declared_axes) == 1 and len({tuple(np.shape(item)) for item in data}) == 1: + if metadata_mode is ImagePayloadMetadataCompositionMode.BUNDLE: + stack_payload = ImagePayloadBundleContext.from_payloads( + tuple(payloads), metadata_mode=metadata_mode, + ).compose() + else: + stacked = stack_runtime_slices(data, memory_type, device_id) + stack_payload = stack_image_payload_context( + payloads, stacked, metadata_mode=metadata_mode, + ) return cls( slices=payloads, slice_contexts=( @@ -2461,6 +2550,7 @@ def from_aligned_stack( else () ), stack_payload=stack_payload, + main_flow_source=value, ) def __iter__(self) -> Iterator[RuntimeArrayData]: @@ -2643,21 +2733,20 @@ def _record_main_flow_passthrough(self, matching_files: Sequence[str]) -> None: return plan = self.request.execution_plan parser = self.request.context.microscope_handler.parser - output_contexts = self._producer_output_contexts(matching_files) - records = tuple( - ProducedOutputSemantics.from_existing_main_flow_path( - plan, - self._input_memory_path(plan.input_dir, matching_file), - parser, - output_context=output_context, - ) - for matching_file, output_context in zip( - matching_files, - output_contexts, - strict=True, + manifest = step_output_manifest(self.request.context) + producer_records = manifest.producer_output_records_for_paths(plan, matching_files, parser) + if producer_records is None: + records = tuple( + ProducedOutputSemantics.from_existing_main_flow_path( + plan, + self._input_memory_path(plan.input_dir, matching_file), + parser, + ) + for matching_file in matching_files ) - ) - step_output_manifest(self.request.context).record_outputs(plan, records) + else: + records = tuple(record.passed_through(plan) for record in producer_records) + manifest.record_outputs(plan, records) def _producer_output_contexts( self, @@ -2757,6 +2846,15 @@ def _load_input_stack(self) -> PatternGroupData: matching_files=matching_files, source_projection=source_projection, ).runtime_context() + producer_records = ( + output_manifest.producer_output_records_for_paths( + plan, matching_files, context.microscope_handler.parser, + ) + if plan.main_input_dependency.kind is StepInputDependencyKind.STEP_OUTPUT + else None + ) + if producer_records and len({record.main_flow_plane_axis for record in producer_records}) != 1: + raise ValueError("One main-flow cohort cannot combine different declared image axes.") cached_stack = context.runtime_image_stack_cache.get( tuple(full_file_paths), memory_type=plan.input_memory_type, @@ -2793,7 +2891,30 @@ def _load_input_stack(self) -> PatternGroupData: f"Check file integrity and format compatibility." ) + if ( + producer_records + and len(producer_records) == 1 + and producer_records[0].main_flow_plane_axis is image_payload_metadata(raw_slices[0]).plane_axis + ): + main_data_stack = PatternGroupOutputData.from_single_image( + raw_slices[0], + memory_type=plan.input_memory_type, + device_id=plan.device_id_for(plan.input_memory_type), + ).stack_payload + return PatternGroupData( + matching_files=matching_files, + main_data_stack=main_data_stack, + source_binding_context=source_binding_context, + ) + metadata_mode = ImagePayloadMetadataCompositionMode.STACK + if producer_records: + declared_axis = producer_records[0].main_flow_plane_axis + metadata_mode = ( + ImagePayloadMetadataCompositionMode.BUNDLE + if declared_axis is None + else ImagePayloadMetadataCompositionMode.for_plane_axis(declared_axis) + ) if source_projection is not None and workspace_source_lookups: metadata_mode = source_projection.payload_composition_mode( workspace_source_lookups @@ -3117,16 +3238,8 @@ def _validate_and_unstack( and processed_stack.metadata.plane_axis is None ): output_context = self._unwrapped_main_flow_output_context() - scalar_data = image_payload_data(processed_stack) - stacked_data = stack_runtime_slices( - (scalar_data,), - self.request.execution_plan.output_memory_type, - self.request.execution_plan.device_id_for( - self.request.execution_plan.output_memory_type - ), - ) - return PatternGroupOutputData( - slices=(processed_stack,), + return PatternGroupOutputData.from_single_image( + processed_stack, slice_contexts=( (output_context,) if output_context is not None @@ -3136,10 +3249,9 @@ def _validate_and_unstack( else () ) ), - stack_payload=stack_image_payload_context( - (processed_stack,), - stacked_data, - metadata_mode=ImagePayloadMetadataCompositionMode.STACK, + memory_type=self.request.execution_plan.output_memory_type, + device_id=self.request.execution_plan.device_id_for( + self.request.execution_plan.output_memory_type ), ) if isinstance(processed_stack, AlignedImageStack): @@ -3229,6 +3341,7 @@ def _validate_and_unstack( slices=output_payloads, slice_contexts=slice_contexts, stack_payload=processed_stack, + main_flow_source=processed_stack, ) def _unwrapped_main_flow_output_context( @@ -3336,6 +3449,7 @@ def _save_outputs( output_identity, output_context=output_context, image_metadata=output_metadata, + main_flow_plane_axis=output_data.plane_axis_for_output_context(output_context), ) if output_directory_exists and context.filemanager.exists( @@ -3373,24 +3487,10 @@ def _save_outputs( output_paths_batch, Backend.MEMORY.value, ) - stack_payload_data = ( - image_payload_data(output_data.stack_payload) - if output_data.stack_payload is not None - else None + stack_payload = output_data.cache_payload_for_outputs( + output_payloads, output_payload_metadata, ) - if output_data.stack_payload is not None: - if np.shape(stack_payload_data)[:1] != (len(output_payloads),): - raise ValueError( - "PatternGroupOutputData.stack_payload must match its declared " - f"output slice count: stack shape {np.shape(stack_payload_data)!r}, " - f"slice count {len(output_payloads)}." - ) - stack_payload = stack_image_payload_context_from_metadata( - output_payloads, - stack_payload_data, - output_payload_metadata, - metadata_mode=ImagePayloadMetadataCompositionMode.STACK, - ) + if stack_payload is not None: context.runtime_image_stack_cache.store( tuple(output_paths_batch), memory_type=self.request.execution_plan.output_memory_type, diff --git a/tests/unit/test_function_artifact_outputs.py b/tests/unit/test_function_artifact_outputs.py index 5cda2912d1..7bbeb49d34 100644 --- a/tests/unit/test_function_artifact_outputs.py +++ b/tests/unit/test_function_artifact_outputs.py @@ -1704,7 +1704,7 @@ def enhance(image, *, runtime): } -def test_pattern_group_runtime_stacks_nominal_scalar_rgb_output_as_one_slice(): +def test_pattern_group_runtime_retains_nominal_scalar_rgb_output_as_one_image(): scalar_output = ImagePayloadMetadata( source_path="/input/A01_s1_w3.tif", source_component_metadata={ @@ -1767,10 +1767,10 @@ def rgb_output(image): artifact_kind=ImageArtifactType.value, ), ) - assert image_payload_data(output.stack_payload).shape == (1, 2, 3, 3) + assert image_payload_data(output.stack_payload).shape == (2, 3, 3) stack_metadata = image_payload_metadata(output.stack_payload) - assert stack_metadata.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE - assert stack_metadata.source_channel_axis == 3 + assert stack_metadata.plane_axis is None + assert stack_metadata.source_channel_axis == -1 def test_pattern_group_runtime_uses_declared_output_slice_cardinality(): diff --git a/tests/unit/test_opaque_main_flow_image_domain.py b/tests/unit/test_opaque_main_flow_image_domain.py new file mode 100644 index 0000000000..619c93677d --- /dev/null +++ b/tests/unit/test_opaque_main_flow_image_domain.py @@ -0,0 +1,420 @@ +"""Whole image domains survive the producer/cache/next-load handoff.""" + +from dataclasses import replace +from types import SimpleNamespace + +import numpy as np +import pytest + +from openhcs.constants.constants import MEMORY_TYPE_NUMPY +from openhcs.core.compiled_step_plan import CompiledStepPlan +from openhcs.core.component_group_scope import ComponentGroupScope +from openhcs.core.function_patterns import compile_function_pattern +from openhcs.core.runtime_image_values import ( + ImagePayloadMetadata, + image_payload_data, + image_payload_mask, + image_payload_metadata, +) +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 PatternGroupData, PatternGroupRuntime +from openhcs.core.aligned_image_payload import AlignedImageStack, ImageOutputBundle, AlignedImageSliceContext +from openhcs.core.context.processing_context import ProcessingContext +from openhcs.core.steps.function_output_manifest import step_output_manifest, StepOutputManifestStore +from openhcs.core.source_bindings import CompiledSourceBindingPlan, SourceBindingRuntimeContext +from openhcs.core.steps.function_runtime import SourceBindingRuntimeContextRequest +from openhcs.core.step_dependencies import StepInputDependency +from openhcs.microscopes.source_schema import SourceSchemaFilenameParser +from openhcs.core.source_image_provenance import SourceImageProvenancePlanes + + +def _identity(image): + return image + + +def _runtime(): + pattern = compile_function_pattern(_identity, {}, {}) + runtime = object.__new__(PatternGroupRuntime) + runtime.request = SimpleNamespace( + compiled_group=pattern.default_group, + execution_plan=CompiledStepPlan( + step_index=0, + step_name="WholeImage", + step_type="FunctionStep", + axis_id="A01", + output_memory_type=MEMORY_TYPE_NUMPY, + input_memory_type=MEMORY_TYPE_NUMPY, + variable_components=(), + artifact_inputs={}, + artifact_outputs={}, + execution_group_scope=ComponentGroupScope.ungrouped(), + ), + component_key=None, + ) + return runtime + + +@pytest.mark.parametrize( + ("shape", "channel_axis"), + [((4, 5), None), ((4, 5, 3), -1), ((3, 4, 5), None), ((1, 4, 5), None)], + ids=("plane", "rgb", "volume", "one-z-volume"), +) +def test_opaque_whole_image_is_copied_without_a_new_execution_axis(shape, channel_axis): + pixels = np.arange(np.prod(shape), dtype=np.float32).reshape(shape) + mask_shape = shape[:2] if channel_axis is not None else shape + mask = np.ones(mask_shape, dtype=bool) + metadata = ImagePayloadMetadata( + source_path="/input/A01_s001_w1_z001_t001.tif", + source_component_metadata={"well": "A01", "site": "1", "channel": "1"}, + source_image_names=("WholeImage",), + source_channel_axis=channel_axis, + source_spatial_domain=SourceSpatialDomain( + origin_yx=(2, 3), source_shape_yx=(12, 13), + ), + ) + payload = metadata.payload_with(pixels, mask) + output = _runtime()._validate_and_unstack( + payload, + PatternGroupData( + matching_files=["/input/A01_s001_w1_z001_t001.tif", "/input/A01_s001_w1_z002_t001.tif"], + main_data_stack=np.zeros((2, 4, 5), dtype=np.float32), + ), + ) + assert len(output.slices) == 1 + assert output.slices[0] is payload + cached = output.stack_payload + assert image_payload_data(cached).shape == shape + assert image_payload_metadata(cached) == metadata + np.testing.assert_array_equal(image_payload_data(cached), pixels) + np.testing.assert_array_equal(image_payload_mask(cached), mask) + assert not np.shares_memory(image_payload_data(cached), pixels) + assert not np.shares_memory(image_payload_mask(cached), mask) + assert image_payload_metadata(cached) is not metadata + assert RuntimeSliceProjection.full_stack_value(cached) is cached + image_payload_data(cached).flat[0] = -100 + image_payload_mask(cached).flat[0] = False + assert pixels.flat[0] == 0 + assert mask.flat[0] + image_payload_metadata(cached).source_channel_axis = 0 + assert metadata.source_channel_axis == channel_axis + + +@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) + output = _runtime()._validate_and_unstack( + payload, + PatternGroupData(matching_files=["source-1.tif", "source-2.tif"], main_data_stack=pixels), + ) + assert len(output.slices) == count + assert output.stack_payload is payload + assert image_payload_data(output.stack_payload).shape == (count, 4, 5) + assert image_payload_metadata(output.stack_payload).plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + + +@pytest.mark.parametrize("named", [False, True]) +@pytest.mark.parametrize("axis", [None, RuntimePlaneAxis.RUNTIME_SLICE, RuntimePlaneAxis.SOURCE_BINDING]) +def test_nominal_alignment_owner_preserves_single_output_topology(named, axis): + shape = (3, 4, 5) if axis is None 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,)) + output = _runtime()._validate_and_unstack( + value, PatternGroupData(matching_files=["source.tif"], main_data_stack=payload), + ) + if named and axis is not RuntimePlaneAxis.RUNTIME_SLICE: + 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 + assert image_payload_data(output.stack_payload).shape == expected_shape + assert image_payload_metadata(output.stack_payload).plane_axis is expected_axis + + +@pytest.mark.parametrize("count", [1, 3]) +def test_raw_array_fallback_keeps_explicit_runtime_unstack_contract(count): + runtime = _runtime() + runtime.request.context = ProcessingContext(axis_id="A01") + runtime.request.context.microscope_handler = SimpleNamespace(parser=SourceSchemaFilenameParser()) + pixels = np.arange(count * 4 * 5, dtype=np.float32).reshape(count, 4, 5) + output = runtime._validate_and_unstack( + pixels, PatternGroupData(matching_files=[f"source-{i}.tif" for i in range(count)], main_data_stack=pixels), + ) + cached = output.cache_payload_for_outputs(output.slices, [image_payload_metadata(p) for p in output.slices]) + assert image_payload_data(cached).shape == (count, 4, 5) + assert image_payload_metadata(cached).plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + + +class _MemoryFiles: + def __init__(self): + self.values = {} + + def exists(self, path, backend): + return path in self.values or any(value_path.startswith(path.rstrip("/") + "/") for value_path in self.values) + + def ensure_directory(self, path, backend): + pass + + def save_batch(self, payloads, paths, backend): + self.values.update(zip(paths, payloads, strict=True)) + + def load_batch(self, paths, backend): + return [self.values[path] for path in paths] + + def delete(self, path, backend): + del self.values[path] + + +@pytest.mark.parametrize("cache_hit", [False, True]) +@pytest.mark.parametrize("axis", [None, RuntimePlaneAxis.RUNTIME_SLICE, RuntimePlaneAxis.SOURCE_BINDING]) +@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: + 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) + path = "/input/A01_s001_w1_z001_t001.tif" + components = {"well": "A01", "site": "1", "channel": "1", "z_index": "1", "timepoint": "1"} + metadata = ImagePayloadMetadata( + source_path=path, source_component_metadata=components, source_image_names=("WholeImage",), + source_channel_axis=channel_axis, plane_axis=axis, + source_image_provenance_planes=( + SourceImageProvenancePlanes.from_components(paths=(path,), component_metadata=(components,)) + if axis is not None else SourceImageProvenancePlanes() + ), + ) + payload = pixels if raw_named else metadata.payload_with(pixels, mask) + if raw_named: + mask = None + producer = _runtime() + pattern = compile_function_pattern(_identity, {}, {}) + plan = replace(producer.request.execution_plan, output_dir=tmp_path, step_scope_id="whole-producer", compiled_function_pattern=pattern) + producer.request.execution_plan = plan + files = _MemoryFiles() + context = ProcessingContext(axis_id="A01", filemanager=files) + context.microscope_handler = SimpleNamespace(parser=SourceSchemaFilenameParser()) + producer.request.context = context + producer.pattern_repr = "whole" + output = producer._validate_and_unstack( + ImageOutputBundle((payload,), (AlignedImageSliceContext.main_flow("WholeImage"),)) if axis is not None or raw_named else payload, + PatternGroupData(matching_files=[path, "/input/A01_s001_w1_z002_t001.tif"], main_data_stack=pixels), + ) + records = producer._save_outputs(output, [path]) + manifest = step_output_manifest(context) + manifest.record_outputs(plan, records) + assert records[0].main_flow_plane_axis is axis + physical = files.values[records[0].output_path] + assert np.shares_memory(image_payload_data(physical), pixels) + if not cache_hit: + context.runtime_image_stack_cache.clear() + consumer_plan = replace( + plan, step_index=1, step_scope_id="whole-consumer", input_dir=tmp_path, + main_input_dependency=StepInputDependency.step_output(source_step_index=0, source_step_scope_id=plan.step_scope_id), + ) + consumer = PatternGroupRuntime(SimpleNamespace( + context=context, execution_plan=consumer_plan, compiled_group=pattern.default_group, + source_binding_plan=CompiledSourceBindingPlan.empty(), component_key=None, + pattern_group_info="A01_s001_w1_z{iii}_t001.tif", + )) + monkeypatch.setattr(consumer, "source_workspace_projection_authority", lambda: SimpleNamespace(projection_if_available=lambda: None)) + monkeypatch.setattr(SourceBindingRuntimeContextRequest, "from_context", classmethod(lambda cls, **kwargs: SimpleNamespace(runtime_context=SourceBindingRuntimeContext.empty))) + loaded = consumer._load_input_stack().main_data_stack + assert image_payload_data(loaded).shape == pixels.shape + assert image_payload_metadata(loaded).plane_axis is axis + np.testing.assert_array_equal(image_payload_data(loaded), pixels) + np.testing.assert_array_equal(image_payload_mask(loaded), mask) + assert not np.shares_memory(image_payload_data(loaded), pixels) + if mask is not None: + assert not np.shares_memory(image_payload_mask(loaded), mask) + assert image_payload_metadata(loaded) is not image_payload_metadata(physical) + image_payload_metadata(loaded).source_provenance = image_payload_metadata(loaded).source_provenance.with_source_image_names(("Changed",)) + assert image_payload_metadata(physical).source_image_names == ("WholeImage",) + image_payload_data(loaded).flat[0] = -100 + if mask is not None: + image_payload_mask(loaded).flat[0] = False + assert mask.flat[0] + assert pixels.flat[0] == 0 + consumer._record_main_flow_passthrough([records[0].relative_output_path]) + (passed,) = manifest.produced_records_for(consumer_plan) + assert passed.main_flow_plane_axis is axis + assert passed.image_metadata is records[0].image_metadata + + +@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)) + volume = ImagePayloadMetadata().payload_with(np.ones((4, 5) if runtime_count == 1 else (2, 4, 5), dtype=np.float32)) + bundle = ImageOutputBundle((planes, volume), contexts) + output = _runtime()._validate_and_unstack(bundle, PatternGroupData(matching_files=[], main_data_stack=planes)) + assert output.stack_payload is None + assert [output.plane_axis_for_output_context(c) for c in output.slice_contexts] == [RuntimePlaneAxis.RUNTIME_SLICE] * runtime_count + [None] + assert output.main_flow_source is bundle + + +def test_passed_through_record_preserves_collapsed_and_filename_coordinates(tmp_path): + from openhcs.core.steps.function_output_manifest import ProducedOutputSemantics + from openhcs.core.steps.function_output_identity import FunctionOutputIdentity + plan = replace(_runtime().request.execution_plan, output_dir=tmp_path) + 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, + ) + 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 + + +def test_duplicate_physical_path_rejects_contradictory_whole_axis(tmp_path): + from openhcs.core.steps.function_output_manifest import ProducedOutputSemantics + from openhcs.core.steps.function_output_identity import FunctionOutputIdentity + plan = replace(_runtime().request.execution_plan, output_dir=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, + ) + 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))) + + +@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, + ) + + +def test_record_axis_survives_replace_and_pickle(tmp_path): + import pickle + from openhcs.core.steps.function_output_manifest import ProducedOutputSemantics + from openhcs.core.steps.function_output_identity import FunctionOutputIdentity + plan = replace(_runtime().request.execution_plan, output_dir=tmp_path) + for axis in (None, RuntimePlaneAxis.RUNTIME_SLICE, RuntimePlaneAxis.SOURCE_BINDING): + record = 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=axis, + ) + assert replace(record).main_flow_plane_axis is axis + restored = pickle.loads(pickle.dumps(record)) + assert restored == record + assert restored.main_flow_plane_axis is axis + + +def _selected_pattern(name): + from openhcs.core.artifacts import ArtifactSpec, ImageArtifactType + from openhcs.core.pipeline.function_contracts import artifact_inputs + from openhcs.core.function_patterns import InvocationArtifactInputEdgePlan, InvocationArtifactInputProjectionKey + spec = ArtifactSpec.input(name, ImageArtifactType) + + @artifact_inputs(spec) + def consume(image): + return image + + pattern = compile_function_pattern(consume, {}, {}) + invocation = pattern.default_group.invocations[0] + invocation = invocation.with_artifact_input_edges((InvocationArtifactInputEdgePlan( + key=InvocationArtifactInputProjectionKey(invocation_key=invocation.key, input_index=0), + spec=spec, storage_plan=None, projection=None, consumes_main_flow=True, + ),)) + return replace(pattern, groups=(replace(pattern.default_group, invocations=(invocation,)),)) + + +@pytest.mark.parametrize("cache_hit", [False, True]) +def test_saved_mixed_named_cohort_rejects_joint_load_and_preserves_selected_domains(tmp_path, monkeypatch, cache_hit): + from openhcs.core.artifacts import ImageArtifactType + source = "/input/A01_s001_w1_z001_t001.tif" + components = {"well": "A01", "site": "1", "channel": "1", "z_index": "1", "timepoint": "1"} + 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, + 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)) + opaque_pixels = np.arange(20, dtype=np.float32).reshape(4, 5) + 100 + opaque = scalar_metadata.payload_with(opaque_pixels) + producer = _runtime() + pattern = compile_function_pattern(_identity, {}, {}) + plan = replace(producer.request.execution_plan, output_dir=tmp_path, step_scope_id="mixed-producer", compiled_function_pattern=pattern) + producer.request.execution_plan = plan + files = _MemoryFiles() + context = ProcessingContext(axis_id="A01", filemanager=files) + context.microscope_handler = SimpleNamespace(parser=SourceSchemaFilenameParser()) + producer.request.context = context + producer.pattern_repr = "mixed" + loaded = PatternGroupData(matching_files=[source, source], main_data_stack=planes) + + # A prior homogeneous producer caches the same physical paths. Mixed replacement + # must invalidate that cache rather than leave a false joint runtime domain. + old_bundle = ImageOutputBundle((planes, declared_metadata.payload_with(opaque_pixels[None])), contexts) + old_records = producer._save_outputs(producer._validate_and_unstack(old_bundle, loaded), [source, source]) + assert context.runtime_image_stack_cache.get(tuple(r.output_path for r in old_records), memory_type=MEMORY_TYPE_NUMPY) is not None + bundle = ImageOutputBundle((planes, opaque), contexts) + output = producer._validate_and_unstack(bundle, loaded) + records = producer._save_outputs(output, [source, source]) + 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 context.runtime_image_stack_cache.get(tuple(r.output_path for r in records), memory_type=MEMORY_TYPE_NUMPY) is None + monkeypatch.setattr(SourceBindingRuntimeContextRequest, "from_context", classmethod(lambda cls, **kwargs: SimpleNamespace(runtime_context=SourceBindingRuntimeContext.empty))) + + def consumer_for(compiled): + consumer_plan = replace(plan, step_index=1, step_scope_id="mixed-consumer", input_dir=tmp_path, + compiled_function_pattern=compiled, + main_input_dependency=StepInputDependency.step_output(source_step_index=0, source_step_scope_id=plan.step_scope_id)) + consumer = PatternGroupRuntime(SimpleNamespace(context=context, execution_plan=consumer_plan, + compiled_group=compiled.default_group, source_binding_plan=CompiledSourceBindingPlan.empty(), + component_key=None, pattern_group_info="A01_s001_w1_z{iii}_t001.tif")) + monkeypatch.setattr(consumer, "source_workspace_projection_authority", lambda: SimpleNamespace(projection_if_available=lambda: None)) + return consumer + + with pytest.raises(ValueError, match="different declared image axes"): + consumer_for(pattern)._load_input_stack() + for name, expected, axis in (("Planes", image_payload_data(planes), RuntimePlaneAxis.RUNTIME_SLICE), ("Volume", opaque_pixels, None)): + selected = consumer_for(_selected_pattern(name)) + actual = selected._load_input_stack().main_data_stack + np.testing.assert_array_equal(image_payload_data(actual), expected) + assert image_payload_metadata(actual).plane_axis is axis + if not cache_hit: + context.runtime_image_stack_cache.clear() + else: + selected_path = next(r.output_path for r in records if r.producer_identity.output_key == name) + context.runtime_image_stack_cache.store((selected_path,), memory_type=MEMORY_TYPE_NUMPY, stack=actual) + repeated = selected._load_input_stack().main_data_stack + np.testing.assert_array_equal(image_payload_data(repeated), expected) + assert image_payload_metadata(repeated).plane_axis is axis + + +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( + np.ones((1, 4, 5), dtype=np.float32)) for _ in contexts) + with pytest.raises(ValueError, match="plane axis to be projected"): + _runtime()._validate_and_unstack(ImageOutputBundle(payloads, contexts), PatternGroupData(matching_files=[], main_data_stack=payloads[0])) + + +def test_opaque_named_bundle_uses_existing_same_slice_mask_composition(): + from openhcs.core.aligned_image_payload import ImagePayloadBundleContext + contexts = tuple(AlignedImageSliceContext.main_flow(name) for name in ("First", "Second")) + first_mask = np.ones((4, 5), dtype=bool) + second_mask = first_mask.copy() + second_mask[0, 0] = False + payloads = tuple(ImagePayloadMetadata(source_image_names=(name,)).payload_with( + np.ones((4, 5), dtype=np.float32) * i, mask) + for i, (name, mask) in enumerate(zip(("First", "Second"), (first_mask, second_mask), strict=True))) + output = _runtime()._validate_and_unstack(ImageOutputBundle(payloads, contexts), PatternGroupData(matching_files=[], main_data_stack=payloads[0])) + expected = ImagePayloadBundleContext.from_payloads(payloads).compose() + assert image_payload_metadata(output.stack_payload).plane_axis is RuntimePlaneAxis.SOURCE_BINDING + np.testing.assert_array_equal(image_payload_data(output.stack_payload), image_payload_data(expected)) + np.testing.assert_array_equal(image_payload_mask(output.stack_payload), image_payload_mask(expected)) From 6dd47dd7e97784dc491b1fc47cfbee3e27cddb38 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 03:51:02 -0400 Subject: [PATCH 050/235] refactor(runtime): let input data own main-flow image composition Move declared cohort admissibility and loaded-image composition from the oversized runtime loader to PatternGroupData. Reuse ImagePayloadStackComposition for independently copied whole images across input and output owners, retaining plan device resolution order. Keep exact producer-domain records as the authority and remove the input-to-output factory dependency. Related to #419 and #433. --- openhcs/core/aligned_image_payload.py | 20 +++ openhcs/core/steps/function_runtime.py | 135 ++++++++++-------- .../test_opaque_main_flow_image_domain.py | 25 ++++ 3 files changed, 121 insertions(+), 59 deletions(-) diff --git a/openhcs/core/aligned_image_payload.py b/openhcs/core/aligned_image_payload.py index 6c0f82bfd2..98451b0e19 100644 --- a/openhcs/core/aligned_image_payload.py +++ b/openhcs/core/aligned_image_payload.py @@ -423,6 +423,26 @@ def composition_payloads(self) -> tuple[Any, ...]: ... @abstractmethod def composition_metadata_mode(self) -> ImagePayloadMetadataCompositionMode: ... + @staticmethod + def copy_whole_image( + value: RuntimeArrayData, + *, + memory_type: str, + device_id: int | None, + ) -> RuntimeArrayData: + """Copy pixels, mask and metadata without adding a composition axis.""" + copied_data = stack_runtime_slices( + (image_payload_data(value),), memory_type, device_id, + )[0] + mask = image_payload_mask(value) + copied_mask = ( + None if mask is None + else stack_runtime_slices((mask,), memory_type, device_id)[0] + ) + return image_payload_metadata(value).replace_fields().payload_with( + copied_data, copied_mask, + ) + def composition_source_metadata(self) -> tuple[ImagePayloadMetadata, ...]: return tuple( self.composition_payload_metadata(image_payload_metadata(payload)) diff --git a/openhcs/core/steps/function_runtime.py b/openhcs/core/steps/function_runtime.py index 25825890b0..e0fbc70fea 100644 --- a/openhcs/core/steps/function_runtime.py +++ b/openhcs/core/steps/function_runtime.py @@ -74,6 +74,7 @@ AlignedImageStack, AlignedImageSliceContext, ImagePayloadBundleContext, + ImagePayloadStackComposition, ImageOutputBundle, flatten_aligned_image_payload_slices, stack_image_payload_context, @@ -1576,6 +1577,69 @@ class PatternGroupData: ) + @staticmethod + def validate_main_flow_cohort( + producer_records: Sequence[ProducedOutputSemantics] | None, + ) -> None: + """One input cohort has one declared whole-image composition domain.""" + if producer_records and len({record.main_flow_plane_axis for record in producer_records}) != 1: + raise ValueError("One main-flow cohort cannot combine different declared image axes.") + + @classmethod + def from_loaded_images( + cls, + matching_files: list[str], + payloads: Sequence[RuntimeArrayData], + *, + producer_records: Sequence[ProducedOutputSemantics] | None, + source_binding_context: SourceBindingRuntimeContext, + execution_plan: CompiledStepPlan, + source_projection: VirtualWorkspaceSourceProjection | None, + workspace_source_lookups: Sequence[VirtualWorkspacePathLookup], + ) -> "PatternGroupData": + """Compose a selected admissible input cohort in its declared image domain.""" + if ( + producer_records + and len(producer_records) == 1 + and producer_records[0].main_flow_plane_axis is image_payload_metadata(payloads[0]).plane_axis + ): + main_data_stack = ImagePayloadStackComposition.copy_whole_image( + payloads[0], memory_type=execution_plan.input_memory_type, + device_id=execution_plan.device_id_for(execution_plan.input_memory_type), + ) + else: + metadata_mode = ImagePayloadMetadataCompositionMode.STACK + if producer_records: + declared_axis = producer_records[0].main_flow_plane_axis + metadata_mode = ( + ImagePayloadMetadataCompositionMode.BUNDLE + if declared_axis is None + else ImagePayloadMetadataCompositionMode.for_plane_axis(declared_axis) + ) + 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: + main_data_stack = stack_runtime_slices( + tuple(image_payload_data(payload) for payload in payloads), + execution_plan.input_memory_type, + execution_plan.device_id_for(execution_plan.input_memory_type), + ) + main_data_stack = stack_image_payload_context( + payloads, main_data_stack, metadata_mode=metadata_mode, + ) + elif metadata_mode is ImagePayloadMetadataCompositionMode.BUNDLE: + main_data_stack = ImagePayloadBundleContext.from_payloads( + tuple(payloads), metadata_mode=metadata_mode, + ).compose() + return cls( + matching_files=matching_files, + main_data_stack=main_data_stack, + source_binding_context=source_binding_context, + ) + + @dataclass(frozen=True, slots=True) class OutputPathBatchEntry: """Resolved identity for one output path in a runtime save batch.""" @@ -2444,19 +2508,11 @@ def from_single_image( device_id: int | None, ) -> "PatternGroupOutputData": """Copy one whole image for main-flow reuse without declaring a new axis.""" - copied_data = stack_runtime_slices( - (image_payload_data(value),), memory_type, device_id, - )[0] - mask = image_payload_mask(value) - copied_mask = ( - None if mask is None - else stack_runtime_slices((mask,), memory_type, device_id)[0] - ) return cls( slices=(value,), slice_contexts=slice_contexts, - stack_payload=image_payload_metadata(value).replace_fields().payload_with( - copied_data, copied_mask, + stack_payload=ImagePayloadStackComposition.copy_whole_image( + value, memory_type=memory_type, device_id=device_id, ), main_flow_source=value, ) @@ -2853,8 +2909,7 @@ def _load_input_stack(self) -> PatternGroupData: if plan.main_input_dependency.kind is StepInputDependencyKind.STEP_OUTPUT else None ) - if producer_records and len({record.main_flow_plane_axis for record in producer_records}) != 1: - raise ValueError("One main-flow cohort cannot combine different declared image axes.") + PatternGroupData.validate_main_flow_cohort(producer_records) cached_stack = context.runtime_image_stack_cache.get( tuple(full_file_paths), memory_type=plan.input_memory_type, @@ -2891,53 +2946,15 @@ def _load_input_stack(self) -> PatternGroupData: f"Check file integrity and format compatibility." ) - if ( - producer_records - and len(producer_records) == 1 - and producer_records[0].main_flow_plane_axis is image_payload_metadata(raw_slices[0]).plane_axis - ): - main_data_stack = PatternGroupOutputData.from_single_image( - raw_slices[0], - memory_type=plan.input_memory_type, - device_id=plan.device_id_for(plan.input_memory_type), - ).stack_payload - return PatternGroupData( - matching_files=matching_files, - main_data_stack=main_data_stack, - source_binding_context=source_binding_context, - ) - - metadata_mode = ImagePayloadMetadataCompositionMode.STACK - if producer_records: - declared_axis = producer_records[0].main_flow_plane_axis - metadata_mode = ( - ImagePayloadMetadataCompositionMode.BUNDLE - if declared_axis is None - else ImagePayloadMetadataCompositionMode.for_plane_axis(declared_axis) - ) - 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: - raw_slice_data = tuple( - image_payload_data(slice_data) for slice_data in raw_slices - ) - main_data_stack = stack_runtime_slices( - raw_slice_data, - plan.input_memory_type, - plan.device_id_for(plan.input_memory_type), - ) - main_data_stack = stack_image_payload_context( - raw_slices, - main_data_stack, - metadata_mode=metadata_mode, - ) - elif metadata_mode is ImagePayloadMetadataCompositionMode.BUNDLE: - main_data_stack = ImagePayloadBundleContext.from_payloads( - tuple(raw_slices), - metadata_mode=metadata_mode, - ).compose() + return PatternGroupData.from_loaded_images( + matching_files, + raw_slices, + producer_records=producer_records, + source_binding_context=source_binding_context, + execution_plan=plan, + source_projection=source_projection, + workspace_source_lookups=workspace_source_lookups, + ) else: main_data_stack = cached_stack diff --git a/tests/unit/test_opaque_main_flow_image_domain.py b/tests/unit/test_opaque_main_flow_image_domain.py index 619c93677d..6553bf96ad 100644 --- a/tests/unit/test_opaque_main_flow_image_domain.py +++ b/tests/unit/test_opaque_main_flow_image_domain.py @@ -418,3 +418,28 @@ def test_opaque_named_bundle_uses_existing_same_slice_mask_composition(): assert image_payload_metadata(output.stack_payload).plane_axis is RuntimePlaneAxis.SOURCE_BINDING np.testing.assert_array_equal(image_payload_data(output.stack_payload), image_payload_data(expected)) np.testing.assert_array_equal(image_payload_mask(output.stack_payload), image_payload_mask(expected)) + + +def test_input_bundle_composition_retains_lazy_plan_device_resolution(tmp_path, monkeypatch): + from openhcs.core.steps.function_output_manifest import ProducedOutputSemantics + from openhcs.core.steps.function_output_identity import FunctionOutputIdentity + plan = replace(_runtime().request.execution_plan, output_dir=tmp_path) + records = tuple(ProducedOutputSemantics.from_output( + plan, tmp_path / f"A01_s001_w1_z001_t001_{name}.tif", + FunctionOutputIdentity(component_values={"well": "A01"}, extension=".tif", source="test"), + main_flow_plane_axis=None, + ) 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"))) + + def unexpected_device_resolution(self, memory_type): + raise AssertionError("BUNDLE composition must retain its own memory allocation authority") + + monkeypatch.setattr(CompiledStepPlan, "device_id_for", unexpected_device_resolution) + output = PatternGroupData.from_loaded_images( + [r.output_path for r in records], payloads, + producer_records=records, source_binding_context=SourceBindingRuntimeContext.empty(), + execution_plan=plan, source_projection=None, workspace_source_lookups=(), + ) + assert image_payload_metadata(output.main_data_stack).plane_axis is RuntimePlaneAxis.SOURCE_BINDING + np.testing.assert_array_equal(image_payload_data(output.main_data_stack), np.stack([image_payload_data(p) for p in payloads])) From 130c03a26684a2f3b6eb262e3ede7b2ce31bed64 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 04:13:34 -0400 Subject: [PATCH 051/235] docs(perf): record opaque-domain repair and remaining operand projection Refs #384, #419, #433. Retain original guards, latest-main baseline failures, ordinary 3D failure and source-domain proof limits. --- .../shared_runtime_plumbing_384_20261001.rst | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/docs/validation/shared_runtime_plumbing_384_20261001.rst b/docs/validation/shared_runtime_plumbing_384_20261001.rst index 023b75894e..f225b0b549 100644 --- a/docs/validation/shared_runtime_plumbing_384_20261001.rst +++ b/docs/validation/shared_runtime_plumbing_384_20261001.rst @@ -728,3 +728,79 @@ no performance or biological acceptance claim follows. Original failures remain in ``pr394-38067-graph-roi-before-20261002.log``; integrated controls are in ``pr394-graph-roi-integrated-after-20261002.log`` in RUNS. The historical PR #404 branch and its older publication proposal are not merged as a whole. + +Opaque image domain and current 3D frontier (2026-10-02) +------------------------------------------------------ + +Frozen ``45f3a721e`` diagnoses the dominant current 3D failure: scalar output +composition adds a runtime axis to an opaque whole volume. Watershed receives +``(1, 60, 128, 128)`` and fails after 118.394 seconds inside its raw callable. +The failed job peaks at 3745.977 MiB. Installed Mahotas 1.4.19's filter-offset +formula requires a 2 GiB table for that incorrect 4D seed-label neighborhood; +the singleton changes neighborhood rank without changing image voxel count. +This is a source-backed bound, not an allocator-stack capture or attribution of +the original 13.36 GB OOM. No NumBa cache files are created during the observed +pipeline window. Cache warming does not correct an invalid scientific domain. + +``d092a03af`` repairs composition using the existing nominal alignment owners. +``AlignedImageStack`` retains its declared outer runtime axis; named +``ImageOutputBundle`` members supply their original individual domains. +``PatternGroupOutputData`` derives saved member declarations from those original +values, and ``ProducedOutputSemantics`` retains that domain across physical-leaf +projection and passthrough. This execution-local fact distinguishes an opaque +single-Z volume from an explicitly declared one-plane runtime stack after both +have otherwise similar leaf metadata. It does not replace durable source ingress. +Mixed cohorts cannot manufacture one combined domain. Cache misses, overwrites, +named selection and independent pixel/mask/metadata ownership are covered. +Producer and loader bundle composition use the same existing authority. + +The first scoped original R0 rejects ten added lines in the oversized runtime +class. ``6dd47dd7e`` moves actual input assembly onto existing +``PatternGroupData`` and shared independent copying onto +``ImagePayloadStackComposition``, removing the input loader's call into the +output factory. Both unchanged original scoped R0 and R1 then pass against +``45f3a721e``; 271 controls pass, including retained lazy device resolution for +bundle composition. The whole-branch scalar relocation gate remains separate +and is not waived. Cross-device composition equivalence remains unqualified. + +A fresh ordinary CPU5/one-inline-worker execution at frozen ``d092a03af`` gets +through the first 18 steps. The entire Watershed step completes in 0.156588 +seconds. The older figure is an instrumented raw-call failure, so these are not +paired successful timings or a native speedup ratio. The full pipeline still +fails at step 18, MaskImage: a correctly selected 2D invocation receives the +opaque secondary volume without a source-proved plane projection. The refusing +2D mask guard is correct. Failure-row zero compile/execute values are wrapper +placeholders; the public compile-event span is 1.619 seconds. Pipeline clocks +exclude mandatory registry/kernel server preparation and shutdown. + +The generic relation-aware repair belongs at ``RuntimeInputBindingRequest``. +MaskImage, Crop and CorrectIlluminationApply already declare +``InputStackBroadcastSourceRelation``. That declaration permits broadcasts but +does not prove spatial-Z ordering. Contributor provenance or equal array lengths +cannot supply the missing pixel-coordinate proof. ``SourceSpatialDomain`` is +currently XY-only; source/transform ownership is being traced before extending +its existing contract. No function-specific mask path or geometry-guard +relaxation is included. Latest-head all30/native parity and ordinary performance +remain open. + +Main ``902913616`` (merged PR #434) is integrated at ``ff9d7c79d``. The combined +14-file source suite has 810 passes and two unary GrayToColor failures. Both +failures reproduce unchanged on clean main ``902913616``; they are retained, +not converted into successful assertions or treated as axis-fix regressions. +No foreign PR implementation is duplicated. + +Three completed same-server Human jobs at ``45f3a721e`` pass their scientific +comparisons and leave zero backend image-array bytes after each job. This +rejects the proposed completed-job backend leak for that workload. Generic +worker resource release already executes for inline jobs. The older OOM's full +allocation mix remains unknown; no speculative global cleanup is added. + +Receipts remain in the external benchmark RUNS directory: +``issue433-rank4-watershed-large-allocation-source-bound-20261002.json``, +``issue433-three-complete-job-retention-counterevidence-20261002.json``, +``pr394-opaque-axis-input-owner-original-r0-openhcs-20261002.json``, +``pr394-opaque-axis-input-owner-original-r1-20261002.json`` and +``d092-mask-secondary-domain-caller-audit-20261002.json``. Frozen ordinary +outputs/source hashes are retained under +``/var/tmp/openhcs-opaque-axis-d092-ordinary-20261002``. The initial failed +guard-launch arguments and the original R0 growth finding are also retained. From 6dfda95975f975f04fd2c71ddbc596dc221ed95f Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 05:13:35 -0400 Subject: [PATCH 052/235] fix(runtime): publish each persisted image occurrence once (#435) --- .../core/steps/function_output_manifest.py | 22 +++ openhcs/core/steps/function_outputs.py | 41 ++++- .../unit/test_derived_role_publication_435.py | 160 ++++++++++++++++++ 3 files changed, 221 insertions(+), 2 deletions(-) create mode 100644 tests/unit/test_derived_role_publication_435.py diff --git a/openhcs/core/steps/function_output_manifest.py b/openhcs/core/steps/function_output_manifest.py index 24ed7998dd..af9dff075f 100644 --- a/openhcs/core/steps/function_output_manifest.py +++ b/openhcs/core/steps/function_output_manifest.py @@ -144,6 +144,28 @@ def path_under(self, output_dir: str | Path) -> str: return str(Path(output_dir) / self.relative_output_path) + @property + def persisted_source_alias(self) -> str | None: + """Retain a declared artifact name on its persisted main-flow image.""" + identity = self.producer_identity + return identity.output_key if identity.artifact_kind is not None else None + + def owns_persisted_artifact( + self, + output_plan: ArtifactOutputPlan, + output_path: str, + output_dir: str | Path, + ) -> bool: + """Identify the same declared producer and exact saved occurrence.""" + identity = self.producer_identity + return ( + identity.artifact_kind is not None + and identity.artifact_kind == output_plan.artifact_type.value + and identity.output_key == output_plan.name + and identity.step_scope_id == output_plan.producer_step_scope_id + and Path(self.path_under(output_dir)) == Path(output_path) + ) + @property def output_context(self) -> AlignedImageSliceContext: """Return the declared main-flow context for this produced output.""" diff --git a/openhcs/core/steps/function_outputs.py b/openhcs/core/steps/function_outputs.py index d8f615c97e..6b1cb7dde6 100644 --- a/openhcs/core/steps/function_outputs.py +++ b/openhcs/core/steps/function_outputs.py @@ -8,6 +8,7 @@ from collections.abc import Callable, Mapping from dataclasses import dataclass, field, replace from pathlib import Path +from types import MappingProxyType from typing import ClassVar, TypeVar import numpy as np @@ -52,6 +53,7 @@ OpenHCSPlaneAddress, SourceArtifactProjection, SourcePlaneProjection, + SourceProjection, SourceProjectionMetadataSerializer, SourceProjectionSet, ) @@ -774,11 +776,17 @@ def runtime_artifact_projection_paths( self, context: ProcessingContext, plan: CompiledStepPlan, + *, + produced_projections: Mapping[ + Path, tuple[ProducedOutputSemantics, SourceProjection] + ] = MappingProxyType({}), ) -> tuple[tuple[SourceArtifactProjection, str], ...]: """Publish artifacts persisted in this declared storage target.""" materialization = plan.runtime_artifact_materialization if materialization.persists_to_backend(self.backend): - return self.project_runtime_artifacts(context, plan) + return self.project_runtime_artifacts( + context, plan, produced_projections=produced_projections + ) return () def stored_output_paths(self, context: ProcessingContext) -> tuple[str, ...]: @@ -869,6 +877,7 @@ def produced_projection_metadata( else () ) projection_paths = [] + produced_projections = {} declared_addresses: set[OpenHCSPlaneAddress] = set() parser_context = FunctionOutputParserContext.from_processing_context( context @@ -895,12 +904,16 @@ def produced_projection_metadata( SourcePlaneProjection( address=address, ref=SourcePixelRef(self.backend, virtual_path), + source_alias=record.persisted_source_alias, source_metadata=source_metadata, image_metadata=metadata, ), virtual_path, ) ) + produced_projections[Path(destination)] = ( + record, projection_paths[-1][0] + ) continue projection_paths.append( ( @@ -915,8 +928,13 @@ def produced_projection_metadata( virtual_path, ) ) + produced_projections[Path(destination)] = ( + record, projection_paths[-1][0] + ) projection_paths.extend( - self.runtime_artifact_projection_paths(context, plan) + self.runtime_artifact_projection_paths( + context, plan, produced_projections=produced_projections + ) ) if not projection_paths: return None @@ -938,6 +956,10 @@ def project_runtime_artifacts( self, context: ProcessingContext, plan: CompiledStepPlan, + *, + produced_projections: Mapping[ + Path, tuple[ProducedOutputSemantics, SourceProjection] + ] = MappingProxyType({}), ) -> tuple[tuple[SourceArtifactProjection, str], ...]: """Project persisted image artifacts into the target source authority.""" @@ -967,6 +989,21 @@ def project_runtime_artifacts( metadata ) ) + produced = produced_projections.get(Path(destination)) + if produced is not None: + record, projection = produced + if record.owns_persisted_artifact( + materialization.output_plan, destination, self.output_dir + ): + if ( + projection.address != address + or projection.image_metadata != metadata + ): + raise ValueError( + "Conflicting metadata for persisted image " + f"occurrence {destination!r}." + ) + continue source_metadata = metadata.source_component_metadata or {} persisted_source_metadata = dict(source_metadata) metadata.source_voxel_spacing.merge_into( diff --git a/tests/unit/test_derived_role_publication_435.py b/tests/unit/test_derived_role_publication_435.py new file mode 100644 index 0000000000..4a0155c0f1 --- /dev/null +++ b/tests/unit/test_derived_role_publication_435.py @@ -0,0 +1,160 @@ +"""Issue435: real saved image occurrences projected through two existing owners.""" + +from dataclasses import asdict, replace +import json + +import pytest +import numpy as np +import tifffile +from polystore.disk import DiskStorageBackend +from polystore.filemanager import FileManager +from polystore.memory import MemoryStorageBackend + +from test_function_outputs import ( + context_stub, function_step_plan, record_output_path, +) +from openhcs.microscopes.source_schema import SourceSchemaFilenameParser +from openhcs.constants.constants import Backend, Microscope +from openhcs.core.aligned_image_payload import AlignedImageSliceContext +from openhcs.core.artifacts import ArtifactOutputPlan, ImageArtifactType, ObjectLabelsArtifactType +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 +from openhcs.core.runtime_stores import RuntimeValueStore +from openhcs.core.runtime_object_label_building import SourceImageObjectLabelBuildRequest +from openhcs.core.source_metadata import SourceVoxelSpacing +from openhcs.core.source_workspace_projection import VirtualWorkspaceSourceProjection +from openhcs.core.steps.function_outputs import ( + PrimaryImageMetadataTarget, RuntimeArtifactMaterializationAuthority, +) +from openhcs.processing.materialization import ( + ImageFileOptions, MaterializationSpec, MaterializedFilenameIdentity, +) + + +@pytest.mark.parametrize("scenario", ("same_occurrence", "different_path", "stale_scope", "conflicting_metadata", "different_kind")) +def test_saved_roles_publish_once_per_persisted_occurrence(tmp_path, scenario): + same_persisted_path = scenario != "different_path" + filemanager = FileManager({ + Backend.DISK.value: DiskStorageBackend(), + Backend.MEMORY.value: MemoryStorageBackend(), + }) + context = context_stub(filemanager, parser=SourceSchemaFilenameParser()) + context.microscope_handler.microscope_type = Microscope.OPENHCS.value + context.runtime_value_store = RuntimeValueStore() + context.metadata_cache = {} + context.tiff_config = None + plan = function_step_plan("Derived role images") + plan.streaming_configs = {} + plan.write_backend = Backend.DISK.value + plan.output_plate_root = str(tmp_path) + plan.output_dir = tmp_path / "images" + plan.sub_dir = "images" + plan.analysis_results_dir = str(plan.output_dir) + plan.runtime_artifact_materialization = RuntimeArtifactMaterializationPlan( + persistent_enabled=True, persistent_backend=Backend.DISK.value, + ) + context.step_plans = {plan.step_index: plan} + filemanager.ensure_directory(plan.output_dir, Backend.MEMORY.value) + components = { + "well": "A01", "site": "1", "channel": "2", + "z_index": "1", "timepoint": "1", "extension": ".tif", + } + for role, scale in (("RawRole", 1.0), ("CappedRole", 0.5)): + filename = f"A01_s001_w2_z001_t001_{role}.tif" + output_plan = ArtifactOutputPlan( + name=role, path=f"/memory/A01_s001_w2_z001_t001_{role}.pkl", + artifact_type=(ObjectLabelsArtifactType if scenario == "different_kind" and role == "CappedRole" else ImageArtifactType), + producer_step_index=plan.step_index, + producer_step_scope_id=("different-step-scope" if scenario == "stale_scope" and role == "CappedRole" else plan.step_scope_id), + producer_step_name=plan.step_name, + materialization=MaterializationSpec(ImageFileOptions( + filename_suffix=".tif", + filename_identity=MaterializedFilenameIdentity.SOURCE_IDENTITY, + relative_path_template=(filename if same_persisted_path else filename.replace(".tif", "_independent.tif")), + )), + ) + plan.artifact_outputs[output_plan.ref()] = output_plan + payload = ImageMetadataPayload( + np.ones((4, 5), dtype=np.float32) * scale, + ImagePayloadMetadata( + source_component_metadata=components, + source_image_names=(role,), + source_voxel_spacing=SourceVoxelSpacing((0.65, 0.65)), + ), + ) + artifact_payload = payload + if role == "CappedRole" and scenario == "conflicting_metadata": + artifact_payload = payload.metadata.replace_fields(source_image_names=("IndependentRole",)).payload_with(payload.data) + if role == "CappedRole" and scenario == "different_kind": + artifact_payload = SourceImageObjectLabelBuildRequest( + image=payload, labels=np.ones(payload.data.shape, dtype=np.int32), + declared_object_count=1, declared_object_ids=(1,), + ).payload() + context.runtime_value_store.record( + RuntimeValue.normalize(output_plan, artifact_payload, axis_id="A01"), + path=output_plan.path, backend=Backend.MEMORY.value, + ) + 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) + record_output_path( + context, plan, str(destination), image_metadata=payload.metadata, + output_context=AlignedImageSliceContext.main_flow( + output_key=role, artifact_kind=ImageArtifactType.value, + ), + ) + saved = RuntimeArtifactMaterializationAuthority.materialize(context, plan) + assert len(saved) == 2 + target = replace(PrimaryImageMetadataTarget.from_plan(plan), artifact_materializations=saved) + records = target.produced_records(context, plan) + artifacts = target.runtime_artifact_projection_paths(context, plan) + receipt = { + "source_scope": plan.step_scope_id, + "produced": [{"path": str(record.path_under(plan.output_dir)), + "identity": asdict(record.producer_identity), + "address": record.filename_address.as_component_metadata()} for record in records], + "materialized": [{"path": output.path, + "role": item.materialization.output_plan.name, + "producer_scope": item.materialization.output_plan.producer_step_scope_id, + "stored_key": repr(item.materialization.record.key)} + for item in saved for output in item.outputs_for_backend(Backend.DISK.value)], + "artifact_projections": [{"path": path, + "role": projection.projection_role.value, + "address": projection.address.as_component_metadata(), + "alias": projection.source_alias, + "kind": projection.artifact_kind.value} + for projection, path in artifacts], + } + (tmp_path / "persisted-occurrence-witness.json").write_text(json.dumps(receipt, indent=2)) + produced_paths = {item["path"] for item in receipt["produced"]} + materialized_paths = {item["path"] for item in receipt["materialized"]} + if same_persisted_path: + assert produced_paths == materialized_paths + else: + assert produced_paths.isdisjoint(materialized_paths) + expected_sums = {20.0} if scenario == "different_kind" else {20.0, 10.0} + assert {float(tifffile.imread(item["path"]).sum()) for item in receipt["materialized"]} == expected_sums + if scenario != "same_occurrence": + message = "Conflicting metadata for persisted image" if scenario == "conflicting_metadata" else "Duplicate source projection address" + with pytest.raises(ValueError, match=message) as rejection: + target.produced_projection_metadata(context, plan) + receipt["rejection"] = str(rejection.value) + (tmp_path / "persisted-occurrence-witness.json").write_text(json.dumps(receipt, indent=2)) + return + structured = target.produced_projection_metadata(context, plan) + assert len(structured["source_projection"]) == 2 + assert {item["source_alias"] for item in structured["source_projection"]} == {"RawRole", "CappedRole"} + target.write(context, produced_plan=plan) + document = json.loads((tmp_path / "openhcs_metadata.json").read_text()) + reopened = VirtualWorkspaceSourceProjection.from_openhcs_metadata(tmp_path, document) + projections = reopened.source_projections_by_virtual_path + relative_paths = {item["path"].removeprefix(str(tmp_path) + "/") for item in receipt["produced"]} + assert set(projections) == relative_paths | produced_paths + assert {projection.source_alias for projection in projections.values()} == {"RawRole", "CappedRole"} + for relative_path in relative_paths: + projection = projections[relative_path] + assert projection.address.as_component_metadata() == {key: components[key] for key in ("well", "site", "channel", "z_index", "timepoint")} + expected_scale = 1.0 if projection.source_alias == "RawRole" else 0.5 + np.testing.assert_array_equal(tifffile.imread(tmp_path / relative_path), np.ones((4, 5), dtype=np.float32) * expected_scale) From ce1b7acfc086e16044649fbfb67b2d244e703496 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 05:25:39 -0400 Subject: [PATCH 053/235] refactor(runtime): derive saved alias from output context (#435) --- openhcs/core/aligned_image_payload.py | 5 +++++ .../core/steps/function_output_manifest.py | 20 +++++++++---------- openhcs/core/steps/function_outputs.py | 2 +- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/openhcs/core/aligned_image_payload.py b/openhcs/core/aligned_image_payload.py index 98451b0e19..ac312ba295 100644 --- a/openhcs/core/aligned_image_payload.py +++ b/openhcs/core/aligned_image_payload.py @@ -1012,6 +1012,11 @@ class AlignedImageSliceContext: projection_key: str artifact_kind: str | None = None + @property + def persisted_source_alias(self) -> str | None: + """Expose a declared artifact name, preserving anonymous main flow.""" + return self.output_key if self.artifact_kind is not None else None + @classmethod def main_flow( cls, diff --git a/openhcs/core/steps/function_output_manifest.py b/openhcs/core/steps/function_output_manifest.py index af9dff075f..daef36dea2 100644 --- a/openhcs/core/steps/function_output_manifest.py +++ b/openhcs/core/steps/function_output_manifest.py @@ -144,12 +144,6 @@ def path_under(self, output_dir: str | Path) -> str: return str(Path(output_dir) / self.relative_output_path) - @property - def persisted_source_alias(self) -> str | None: - """Retain a declared artifact name on its persisted main-flow image.""" - identity = self.producer_identity - return identity.output_key if identity.artifact_kind is not None else None - def owns_persisted_artifact( self, output_plan: ArtifactOutputPlan, @@ -159,11 +153,15 @@ def owns_persisted_artifact( """Identify the same declared producer and exact saved occurrence.""" identity = self.producer_identity return ( - identity.artifact_kind is not None - and identity.artifact_kind == output_plan.artifact_type.value - and identity.output_key == output_plan.name - and identity.step_scope_id == output_plan.producer_step_scope_id - and Path(self.path_under(output_dir)) == Path(output_path) + self.output_context.persisted_source_alias, + identity.artifact_kind, + identity.step_scope_id, + Path(self.path_under(output_dir)), + ) == ( + output_plan.name, + output_plan.artifact_type.value, + output_plan.producer_step_scope_id, + Path(output_path), ) @property diff --git a/openhcs/core/steps/function_outputs.py b/openhcs/core/steps/function_outputs.py index 6b1cb7dde6..fcaef7bf7f 100644 --- a/openhcs/core/steps/function_outputs.py +++ b/openhcs/core/steps/function_outputs.py @@ -904,7 +904,7 @@ def produced_projection_metadata( SourcePlaneProjection( address=address, ref=SourcePixelRef(self.backend, virtual_path), - source_alias=record.persisted_source_alias, + source_alias=record.output_context.persisted_source_alias, source_metadata=source_metadata, image_metadata=metadata, ), From 919fee286f8003a00aafc023ad28767d9713842b Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 05:28:59 -0400 Subject: [PATCH 054/235] docs(runtime): qualify derived-role publication repair (#435) --- .../shared_runtime_plumbing_384_20261001.rst | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/docs/validation/shared_runtime_plumbing_384_20261001.rst b/docs/validation/shared_runtime_plumbing_384_20261001.rst index f225b0b549..dc29a52481 100644 --- a/docs/validation/shared_runtime_plumbing_384_20261001.rst +++ b/docs/validation/shared_runtime_plumbing_384_20261001.rst @@ -804,3 +804,47 @@ Receipts remain in the external benchmark RUNS directory: outputs/source hashes are retained under ``/var/tmp/openhcs-opaque-axis-d092-ordinary-20261002``. The initial failed guard-launch arguments and the original R0 growth finding are also retained. + +Persisted derived-role publication (issue #435) +---------------------------------------------- + +``ce1b7acfc`` repairs duplicate publication of one saved image occurrence in the +existing metadata writer. The existing output context derives the declared +source alias; anonymous main flow remains anonymous. Produced records and +successful materialization outcomes share a path index. A second projection is +omitted only when declared alias, artifact kind, producer scope, exact path, +scalar acquisition address and full persisted image metadata agree. Distinct +files with colliding identities, stale scopes, different kinds and conflicting +metadata retain their original refusal. No source-projection guard is relaxed. +The index is linear in produced records and successful outputs. + +The actual writer reproducer at baseline ``130c03a26`` saves two distinct role +images with real C2 addresses and calibration, then reproduces the duplicate +projection failure. At ``ce1b7acfc``, both roles publish once and reopen through +the existing virtual workspace; exact pixels, addresses and spacing pass. All +four independent refusal controls pass, together with 232 focused controls in +9.42 seconds. These are writer/materialization/reopening controls, not an +installed five-step acceptance or a performance benchmark. + +Unchanged original scoped R0 and R1 pass for ``130c03a26`` to ``ce1b7acfc``. +The first candidate ``6dfda9597`` failed original R0 for a long producer-match +Boolean and a foreign absence probe. Moving alias derivation to its existing +context owner and comparing exact producer tuples resolves those findings. +The whole-branch scalar gate remains separate. Independent review finds no +blocker; persisted occurrence identity does not prove stale in-memory pixels +equal the final overwritten file. Pixel parity remains a separate gate. + +The parent has supplied the original five-step source and bounded saved-path +facts in issue comment ``5948666249``. Actual produced-record and materialized +output pairs were not serialized there. Installed original-path acceptance +remains open; no biological replay or fabricated collision pair is claimed. +The separate #437 owner retains its singleton-consumption repair. + +Main ``8551a4864`` (merged #438 and #439) is integrated at ``3de415d63``. +The integrated publication, axis and new MCP compilation controls pass: +247 tests in 51.50 seconds, with two existing pytest configuration warnings. +The scoped guard receipts are retained externally as +``issue435-ce1-original-r0-retry-20261002.json`` and +``issue435-ce1-original-r1-20261002.json``; independent review is +``/var/tmp/openhcs-issue435-independent-publication-review-20261002.json``. +Original failures and failed guard launches remain retained. From a5134a0b85182078700ac06f27dffd3e5788302b Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 06:26:30 -0400 Subject: [PATCH 055/235] docs(perf): record fresh CP comparison and measurement frontier (#384, #444) --- .../shared_runtime_plumbing_384_20261001.rst | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/docs/validation/shared_runtime_plumbing_384_20261001.rst b/docs/validation/shared_runtime_plumbing_384_20261001.rst index dc29a52481..8aaa9949d8 100644 --- a/docs/validation/shared_runtime_plumbing_384_20261001.rst +++ b/docs/validation/shared_runtime_plumbing_384_20261001.rst @@ -848,3 +848,69 @@ The scoped guard receipts are retained externally as ``issue435-ce1-original-r1-20261002.json``; independent review is ``/var/tmp/openhcs-issue435-independent-publication-review-20261002.json``. Original failures and failed guard launches remain retained. + +Fresh native comparison and measurement lifetime frontier (2026-10-02) +-------------------------------------------------------------------- + +Frozen ``919fee286f8003a00aafc023ad28767d9713842b`` completed two ordinary +public-driver observations on CPU5, one inline worker/thread, with default +OUTCOMES and the default memory observer. Server startup, mandatory registry +and kernel warmup, and shutdown are excluded from pipeline clocks. + +===================== =========== ============== ============== ============== +Case Compilation Server job Pipeline total Native CP run +===================== =========== ============== ============== ============== +Advanced segmentation 1.324832 s 8.078740 s 10.650795 s 34.327521 s +Imaging flow cytometry 1.941105 s 15.497744 s 18.177965 s 77.795280 s +===================== =========== ============== ============== ============== + +Native CellProfiler 4.2.8.1 uses the same physical input paths and SHA256 +values, CPU5 and one thread. Its invocation excludes interpreter/JVM startup, +pipeline load and one complete warmup, but includes image-set preparation, +modules, post_run and Measurements.close. These are single observations with +different explicit setup/publication accounting; they do not establish a +kernel ratio, statistical significance or a new patch's speedup. + +IFC's existing strict exported-science comparison passes with zero differences, +1800 nonempty rows and 527 columns in each CSV. The observed native/OH ratios +are 5.019781 for server-job execution and 4.279647 for pipeline total. Unsaved +segmentation masks are outside these retained outputs. Advanced's existing +scientific database comparison also passes with zero differences, but complete +source-information acceptance remains RED: five illumination sources across +two sites have 90 NULL cells instead of native values. The original integer +gate detects 40 Frame/Series/Height/Width cells; independent inventory adds +ten Scaling and forty FileName/PathName/URL/MD5Digest cells. Issue #444 is +formally linked to PR #394. No field exclusion or tolerance change is admitted. + +A separate coarse diagnostic on the same source partitions IFC's 13.838053 s +execution into 9.373829 s inside the raw-callable boundary and 4.464224 s +outside. Advanced's 8.087572 s partitions into 3.836250 s inside and 4.251322 s +outside. The callable boundary includes decorated processing and result +assembly, not just numerical kernels. Nested inclusive spans are not added. +Exclusive recording, table assembly and export work has a combined envelope +of 2.812474 s for IFC and 2.882479 s for Advanced. Saving two seconds requires +roughly 71.11% and 69.38% reduction respectively; recording alone is insufficient. +Stack loading costs 0.126811 s / 0.106307 s. Ordinary progress events bound +between-step/axis work below 17 ms and other job-minus-axis gaps at +0.196432 s / 0.100492 s. Those routes cannot close the multi-second target. +Diagnostic clocks are not ordinary speedup evidence. + +Retained evidence lives under ``/var/tmp``: ordinary outputs and source freeze +in ``openhcs-latest-ordinary-advanced-ifc-20261002``, fresh native inputs, +reports and IFC comparison in +``openhcs-current-native-advanced-ifc-v2-919fee-20261002``, and coarse regions +in ``openhcs-current919-runtime-phase-probe-20261002``. Independent source-field +inventory is ``advanced-fresh-native-missing-illum-metadata-20261002.json``; +the dominant cost/owner receipt is +``openhcs-919-measurement-lifetime-frontier-20261002.json``. Original failed +native preparation and the exact metadata comparison failure remain retained. +Actual typed measurement-request/export capture is in progress; no production +optimization or replay acceptance follows from the cost envelope alone. + +Latest main ``3df650bf7`` (merged #442/#443) is integrated at ``436e49755``. +The incoming BioFormats calibration, MCP streaming and existing publication/ +opaque-domain controls pass: 141 tests in 16.72 seconds. The initial narrow +launch failed collection because the MCP test imports the streaming fixture +module by its short name; the corrected command includes that actual fixture +owner first. Both logs are retained in ``/var/tmp``. No source/test guard was +modified to obtain the pass. From 8f994c81a9aa7f2c87f29dda5aec2316bc1dacd3 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 06:45:02 -0400 Subject: [PATCH 056/235] docs(perf): retain failed recording premise and qualify export replay (#384) --- .../shared_runtime_plumbing_384_20261001.rst | 35 +++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/docs/validation/shared_runtime_plumbing_384_20261001.rst b/docs/validation/shared_runtime_plumbing_384_20261001.rst index 8aaa9949d8..29acca111b 100644 --- a/docs/validation/shared_runtime_plumbing_384_20261001.rst +++ b/docs/validation/shared_runtime_plumbing_384_20261001.rst @@ -871,8 +871,12 @@ modules, post_run and Measurements.close. These are single observations with different explicit setup/publication accounting; they do not establish a kernel ratio, statistical significance or a new patch's speedup. -IFC's existing strict exported-science comparison passes with zero differences, -1800 nonempty rows and 527 columns in each CSV. The observed native/OH ratios +IFC's existing strict exported-science comparison passes with zero differences +and 1800 nonempty rows in each CSV. Native has 528 physical columns and OH 527: +the native redundant Number_Object_Number equals ObjectNumber for all 1800 +rows, and the existing nominal identity policy recognizes that declaration. +This is semantic measurement equality, not identical cross-tool CSV headers. +The observed native/OH ratios are 5.019781 for server-job execution and 4.279647 for pipeline total. Unsaved segmentation masks are outside these retained outputs. Advanced's existing scientific database comparison also passes with zero differences, but complete @@ -914,3 +918,30 @@ launch failed collection because the MCP test imports the streaming fixture module by its short name; the corrected command includes that actual fixture owner first. Both logs are retained in ``/var/tmp``. No source/test guard was modified to obtain the pass. + +The actual IFC capture completed the public pipeline and exact source/dependency +freeze, but its four-snapshot admission remains RED: step22 never invokes +MeasurementsOutputRecorder.record. Only the terminal spreadsheet before/after +graphs were captured (44,935,114 and 56,109,057 bytes), with no graph or budget +rejection. The nine real recorder calls publish identification/filtering/ +expansion/relationship core tables. Texture and intensity-distribution tables +use the separate per-object recorder. Thus the proposed capture and the premise +that all outer recording cost is redundant wide-feature conversion are rejected. + +Independent export-only qualification passes both unchanged V6 loads (615 arrays +each), exact original CSV bytes, existing strict CP science, and all determining +field/source/array aliases. Thirty-five immutable metadata owners drop their +derived views by their existing declared transport policy; every populated view +matches fresh recomputation from the same fields. Original raw-heap equality +remains RED for that intentionally untransported cache state, separately from +declared-state equality. The actual exporter graph has 31 measurement tables +and two relationship records, with no label pixels; it cannot validate a dense +label-storage optimization. Local diagnostic profiling finds repeated row +identity/axis scanning and assignment; it is not end-to-end speedup evidence. +The ordinary export ceiling is 1.4077 seconds, so export alone cannot save two +seconds. Preserving the native CSV span leaves an optimistic combined outer +recording/export ceiling of 2.4189 seconds; a two-second saving requires about +82.68% collapse. No implementation is admitted by that ceiling alone. +Receipts are ``openhcs-measurement-export-declared-state-profile-20261002`` and +``openhcs-measurement-export-declared-fields-alias-control-20261002.json`` under +``/var/tmp``. The original four-snapshot and raw-heap failures remain retained. From dd22e1bf98c38f4bd04e4a8d6cb934f0c8c93681 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 06:44:21 -0400 Subject: [PATCH 057/235] fix: export declared secondary image source occurrences Collect source-bound CPA image metadata independently of recorded producer outputs, while retaining executed image-set correlation, exact source occurrence scope, and conflicting-record refusal. Read TIFF and NumPy header geometry through the existing file-format metadata owner and revision cache, including explicit TIFF frame selection. Refs #444; production Advanced export parity remains pending. --- openhcs/core/image_file_serialization.py | 85 ++++++- openhcs/core/source_projection.py | 5 + openhcs/core/source_workspace_projection.py | 22 ++ .../interop/cellprofiler/analyst_export.py | 150 +++++++++--- .../cellprofiler/image_set_numbering.py | 36 ++- .../unit/test_cellprofiler_analyst_export.py | 231 ++++++++++++++++++ tests/unit/test_image_file_serialization.py | 68 ++++++ 7 files changed, 551 insertions(+), 46 deletions(-) diff --git a/openhcs/core/image_file_serialization.py b/openhcs/core/image_file_serialization.py index a83b2b3bbc..4ab05b2b94 100644 --- a/openhcs/core/image_file_serialization.py +++ b/openhcs/core/image_file_serialization.py @@ -69,6 +69,62 @@ class ImageFileSourceMetadata: source_dtype: Any | None = None intensity_scale: float | None = None pixel_semantics: SourceImagePixelSemantics = SourceImagePixelSemantics() + image_shape_yx: tuple[int, int] | None = None + source_frame_shape: tuple[int, ...] | None = () + + def __post_init__(self) -> None: + if self.image_shape_yx is not None: + if len(self.image_shape_yx) != 2 or any( + not isinstance(value, int) or isinstance(value, bool) or value <= 0 + for value in self.image_shape_yx + ): + raise ValueError( + "Image-file YX shape requires two positive integer dimensions." + ) + object.__setattr__(self, "image_shape_yx", tuple(self.image_shape_yx)) + if self.source_frame_shape is not None: + if any( + not isinstance(value, int) or isinstance(value, bool) or value <= 0 + for value in self.source_frame_shape + ): + raise ValueError( + "Image-file frame shape requires positive integer dimensions." + ) + object.__setattr__( + self, "source_frame_shape", tuple(self.source_frame_shape) + ) + + def frame_for_source_indices(self, indices: tuple[int, ...]) -> int: + """Map explicitly selected leading source axes to a container frame.""" + + if not indices: + return 0 + if self.source_frame_shape is None or len(indices) != len( + self.source_frame_shape + ): + raise ValueError( + "Source selection has no declared complete container-frame mapping." + ) + frame = 0 + for index, size in zip(indices, self.source_frame_shape, strict=True): + if ( + not isinstance(index, int) + or isinstance(index, bool) + or not 0 <= index < size + ): + raise ValueError( + f"Source frame index {index!r} is outside dimension {size}." + ) + frame = frame * size + index + return frame + + def require_image_geometry(self) -> tuple[Any, int, int]: + """Require complete format-declared dtype and physical image dimensions.""" + + if self.image_shape_yx is None or self.source_dtype is None: + raise ValueError("Image-file header does not declare image dtype and YX geometry.") + height, width = self.image_shape_yx + return self.source_dtype, height, width def project_image_metadata( self, metadata: ImagePayloadMetadata, *, values_preserved: bool @@ -219,14 +275,23 @@ def _read_source_metadata(cls, path: Path) -> ImageFileSourceMetadata: import imageio.v3 as iio - dtype = iio.improps(path).dtype + properties = iio.improps(path) + dtype = properties.dtype + pixel_semantics = cls.require_pixel_semantics(path) + shape = tuple(properties.shape) + if pixel_semantics.channel_axis is not None: + axis = pixel_semantics.channel_axis % len(shape) + shape = shape[:axis] + shape[axis + 1 :] return ImageFileSourceMetadata( source_dtype=dtype, intensity_scale=( cls.declared_intensity_scale(path) or image_intensity_scale_for_dtype(dtype) ), - pixel_semantics=cls.require_pixel_semantics(path), + pixel_semantics=pixel_semantics, + image_shape_yx=( + tuple(int(value) for value in shape) if len(shape) == 2 else None + ), ) def preserves_pixel_values(self, source_dtype: Any) -> bool: @@ -313,6 +378,8 @@ def _read_source_metadata(cls, path: Path) -> ImageFileSourceMetadata: return ImageFileSourceMetadata( source_dtype=array.dtype, intensity_scale=image_intensity_scale_for_dtype(array.dtype), + image_shape_yx=tuple(array.shape) if array.ndim == 2 else None, + source_frame_shape=() if array.ndim == 2 else None, ) @@ -346,6 +413,18 @@ def _read_source_metadata(cls, path: Path) -> ImageFileSourceMetadata: series = tif.series[0] dtype = series.dtype declared_scale = cls._declared_intensity_scale_from_page(tif.pages[0]) + image_shape_yx = ( + int(tif.pages[0].imagelength), + int(tif.pages[0].imagewidth), + ) + spatial_axis = series.axes.find("Y") + frame_shape = tuple(int(value) for value in series.shape[:spatial_axis]) + if ( + spatial_axis < 0 + or "S" in series.axes[:spatial_axis] + or len(series.pages) != np.prod(frame_shape) + ): + frame_shape = None sample_axis = series.axes.find("S") sample_count = int(series.shape[sample_axis]) if sample_axis >= 0 else None pixel_semantics = SourceImagePixelSemantics() @@ -360,6 +439,8 @@ def _read_source_metadata(cls, path: Path) -> ImageFileSourceMetadata: source_dtype=dtype, intensity_scale=(declared_scale or image_intensity_scale_for_dtype(dtype)), pixel_semantics=pixel_semantics, + image_shape_yx=image_shape_yx, + source_frame_shape=frame_shape, ) @classmethod diff --git a/openhcs/core/source_projection.py b/openhcs/core/source_projection.py index 38bf869dcd..8711e50c62 100644 --- a/openhcs/core/source_projection.py +++ b/openhcs/core/source_projection.py @@ -707,6 +707,11 @@ def matches_binding(self, binding: NamedSourceBinding) -> bool: and self.artifact_kind is binding.artifact_kind ) + def belongs_to_execution_axis(self, axis_id: str) -> bool: + """Retain an unscoped source or its exact declared execution axis.""" + + return self.execution_scope is None or self.execution_scope.axis_id == axis_id + def declared_optional_payload_field(cls, payload_type: type) -> str: """Derive the one declared field carrying ``payload_type | None``. diff --git a/openhcs/core/source_workspace_projection.py b/openhcs/core/source_workspace_projection.py index 8caef9e15f..0b3216e025 100644 --- a/openhcs/core/source_workspace_projection.py +++ b/openhcs/core/source_workspace_projection.py @@ -21,6 +21,7 @@ from openhcs.core.runtime_array_values import RuntimeArrayData from openhcs.core.source_bindings import ( SOURCE_BINDING_ALIAS_METADATA_FIELD, + NamedSourceBinding, SourceProjectionRole, ) from openhcs.core.source_metadata import ( @@ -370,6 +371,27 @@ def files_for_projection_role( is role ) + def source_occurrences_for_binding( + self, + binding: NamedSourceBinding, + *, + axis_id: str, + ) -> tuple[tuple[str, SourceProjection], ...]: + """Select exact declared occurrences without merging physical resources.""" + + occurrences = [] + for path in self.files_for_projection_role( + binding.projection_role, axis_id=axis_id + ): + projection = self.require_source_projection_for( + VirtualWorkspacePathLookup.from_paths(path, path) + ) + if projection.matches_binding( + binding + ) and projection.belongs_to_execution_axis(axis_id): + occurrences.append((path, projection)) + return tuple(occurrences) + def validate_runtime_metadata_projection( self, *, diff --git a/openhcs/interop/cellprofiler/analyst_export.py b/openhcs/interop/cellprofiler/analyst_export.py index 4d1df75243..8aeacc0359 100644 --- a/openhcs/interop/cellprofiler/analyst_export.py +++ b/openhcs/interop/cellprofiler/analyst_export.py @@ -30,6 +30,7 @@ ) from openhcs.core.component_group_scope import RuntimeExecutionAxisScope from openhcs.core.context.processing_context import ProcessingContext +from openhcs.core.image_file_serialization import ImageFileFormat from openhcs.core.measurement_row_materialization import ( WideMeasurementRowAccumulator, ) @@ -185,10 +186,13 @@ def _source_image_projection_values( source_path: Path, source_image_name: str, dialect: CellProfilerDatabaseColumnDialect, + *, + source_axis_indices: tuple[int, ...] = (), ) -> Mapping[str, Any]: - with Image.open(source_path) as source_image: - width, height = source_image.size - source_dtype = np.asarray(source_image).dtype + header = ImageFileFormat.require_path(source_path).require_source_metadata( + source_path + ) + source_dtype, height, width = header.require_image_geometry() if np.issubdtype(source_dtype, np.integer): scaling = float(np.iinfo(source_dtype).max) elif np.issubdtype(source_dtype, np.bool_): @@ -196,7 +200,9 @@ def _source_image_projection_values( else: scaling = 1.0 values_by_field = { - CellProfilerSourceImageProjectionField.FRAME: 0, + CellProfilerSourceImageProjectionField.FRAME: header.frame_for_source_indices( + source_axis_indices + ), CellProfilerSourceImageProjectionField.HEIGHT: height, CellProfilerSourceImageProjectionField.MD5_DIGEST: md5( source_path.read_bytes(), @@ -877,6 +883,7 @@ def collect_image_provenance( ], thumbnail_field: FieldSpec | None = None, auto_scale_thumbnail_intensities: bool = True, + source_axis_indices: tuple[int, ...] = (), ) -> None: """Fold typed source provenance directly into projected image rows.""" @@ -899,11 +906,13 @@ def collect_image_provenance( source_path, source_image_name, self.dialect, + source_axis_indices=source_axis_indices, ) ) if thumbnail_field is not None: - with Image.open(source_path) as source_image: - pixels = np.asarray(source_image) + pixels = ImageFileFormat.require_path(source_path).read( + source_path + ) values[thumbnail_field.name] = _thumbnail_png_base64( pixels, auto_scale=auto_scale_thumbnail_intensities, @@ -939,6 +948,97 @@ def collect_image_provenance( ) source_metadata_by_image_number[image_number].append(metadata_items) + def collect_recorded_image_provenance( + self, + *, + records: Sequence[StoredRuntimeValue], + image_channels: Sequence[CPAImageChannelSpec], + image_rows_by_number: dict[int, dict[str, Any]], + source_metadata_by_image_number: dict[ + int, + list[Mapping[str, SourceMetadataScalar]], + ], + ) -> None: + for channel in image_channels: + channel_records = tuple( + record for record in records if record.key.name == channel.alias + ) + for record in channel_records: + provenance = image_payload_metadata(record.value.data).source_provenance + self.collect_image_provenance( + provenance, + scope=record.key.scope, + source_image_name=channel.alias, + image_rows_by_number=image_rows_by_number, + source_metadata_by_image_number=source_metadata_by_image_number, + ) + + def collect_source_bound_image_provenance( + self, + *, + source_binding_plan: CompiledSourceBindingPlan, + image_channels: Sequence[CPAImageChannelSpec], + axis_id: str, + image_rows_by_number: dict[int, dict[str, Any]], + source_metadata_by_image_number: dict[ + int, list[Mapping[str, SourceMetadataScalar]] + ], + ) -> None: + """Project declared source occurrences for image sets actually exported.""" + + if self.context is None: + return + workspace = VirtualWorkspaceSourceProjectionAuthority.from_context( + self.context, + cache=self.context.runtime_source_workspace_projection_cache, + ).projection_if_available() + if workspace is None: + return + channel_aliases = frozenset(channel.alias for channel in image_channels) + scope = RuntimeExecutionAxisScope(axis_id) + for binding in source_binding_plan.binding_declarations: + if ( + binding.alias not in channel_aliases + or binding.artifact_kind is not ImageArtifactType + ): + continue + for path, projection in workspace.source_occurrences_for_binding( + binding, axis_id=axis_id + ): + lookup = VirtualWorkspacePathLookup.from_paths(path, path) + provenance = SourceImageProvenance( + source_path=path, + source_component_metadata=workspace.source_metadata_for(lookup), + source_image_names=(binding.alias,), + ) + if ( + self.image_set_numbering.existing_number_for_source_slice( + scope=scope, + provenance=provenance, + slice_index=0, + owner=binding.alias, + ) + is None + ): + continue + selection = binding.explicit_source + if selection is not None and ( + selection.series not in (None, "0") + or selection.index not in (None, "0") + ): + raise ValueError( + f"CPA source image {binding.alias!r} requires format-owned " + "Frame/Series projection for its embedded source selection." + ) + self.collect_image_provenance( + provenance, + scope=scope, + source_image_name=binding.alias, + image_rows_by_number=image_rows_by_number, + source_metadata_by_image_number=source_metadata_by_image_number, + source_axis_indices=projection.ref.source_axis_indices, + ) + def image_numbers_for_provenance( self, provenance: SourceImageProvenance, @@ -1128,11 +1228,16 @@ def build( image_rows_by_number=image_rows_by_number, source_metadata_by_image_number=source_metadata_by_image_number, ) - self._collect_image_provenance( + row_projection.collect_recorded_image_provenance( records=image_records[axis_id], image_channels=image_channels, - settings=settings, - row_projection=row_projection, + image_rows_by_number=image_rows_by_number, + source_metadata_by_image_number=source_metadata_by_image_number, + ) + row_projection.collect_source_bound_image_provenance( + source_binding_plan=self.source_binding_plan, + image_channels=image_channels, + axis_id=axis_id, image_rows_by_number=image_rows_by_number, source_metadata_by_image_number=source_metadata_by_image_number, ) @@ -1444,33 +1549,6 @@ def _collect_relationships( rows = row_projection.relationship_rows(record, relationship) relationship_rows_by_name[relationship.name].extend(rows) - @staticmethod - def _collect_image_provenance( - *, - records: Sequence[StoredRuntimeValue], - image_channels: Sequence[CPAImageChannelSpec], - settings: CellProfilerDatabaseExportSettings, - row_projection: CPATableRowProjection, - image_rows_by_number: dict[int, dict[str, Any]], - source_metadata_by_image_number: dict[ - int, - list[Mapping[str, SourceMetadataScalar]], - ], - ) -> None: - for channel in image_channels: - channel_records = tuple( - record for record in records if record.key.name == channel.alias - ) - for record in channel_records: - provenance = image_payload_metadata(record.value.data).source_provenance - row_projection.collect_image_provenance( - provenance, - scope=record.key.scope, - source_image_name=channel.alias, - image_rows_by_number=image_rows_by_number, - source_metadata_by_image_number=source_metadata_by_image_number, - ) - @staticmethod def _collect_measurement_provenance( *, diff --git a/openhcs/interop/cellprofiler/image_set_numbering.py b/openhcs/interop/cellprofiler/image_set_numbering.py index ac119612ef..f3c9a8f3c1 100644 --- a/openhcs/interop/cellprofiler/image_set_numbering.py +++ b/openhcs/interop/cellprofiler/image_set_numbering.py @@ -43,19 +43,39 @@ def for_source_slices( return { slice_index: self._numbers.setdefault( - ( - scope.axis_id, - self._source_identity( - provenance, - slice_index, - owner=owner, - ), - ), + self._source_key(scope, provenance, slice_index, owner=owner), len(self._numbers) + 1, ) for slice_index in slice_indices } + def existing_number_for_source_slice( + self, + *, + scope: RuntimeExecutionAxisScope, + provenance: SourceImageProvenance, + slice_index: int, + owner: str, + ) -> int | None: + """Find an executed image set without admitting an unused source occurrence.""" + + return self._numbers.get( + self._source_key(scope, provenance, slice_index, owner=owner) + ) + + def _source_key( + self, + scope: RuntimeExecutionAxisScope, + provenance: SourceImageProvenance, + slice_index: int, + *, + owner: str, + ) -> tuple[str, SourceImageSetIdentity]: + return ( + scope.axis_id, + self._source_identity(provenance, slice_index, owner=owner), + ) + def for_source_slice( self, *, diff --git a/tests/unit/test_cellprofiler_analyst_export.py b/tests/unit/test_cellprofiler_analyst_export.py index 89390f3370..7c2ee83497 100644 --- a/tests/unit/test_cellprofiler_analyst_export.py +++ b/tests/unit/test_cellprofiler_analyst_export.py @@ -67,6 +67,15 @@ SourceImageIdentity, SourceImageProvenanceContributor, ) +from openhcs.core.source_projection import ( + OpenHCSPlaneAddress, + SourceArtifactProjection, + SourcePlaneProjection, + SourceProjectionMetadataSerializer, + SourceProjectionSet, +) +from openhcs.microscopes.source_schema import SourceSchemaFilenameParser +from polystore.virtual_workspace import SourcePixelRef from openhcs.interop.cellprofiler.analyst_export import ( CPAImageChannelSpec, CPAPropertiesRenderer, @@ -682,6 +691,228 @@ def test_projection_renders_source_builtins_and_thumbnail_from_declared_image( } <= {field_spec.name for field_spec in projection.image_table.columns} +def _borrowed_source_export_fixture( + tmp_path: Path, + *, + embedded_tiff: bool = False, + calibration_scope=None, +): + """Real source workspace occurrences and measurements, without image records.""" + calibration = tmp_path / ("IllumDNA.tif" if embedded_tiff else "IllumDNA.npy") + if embedded_tiff: + import tifffile + + tifffile.imwrite( + calibration, + np.ones((3, 5, 7), dtype=np.uint16), + photometric="minisblack", + metadata={"axes": "ZYX"}, + ) + else: + np.save(calibration, np.ones((5, 7), dtype=np.float32)) + sources = [] + source_metadata = [] + for site in (1, 2, 3): + original = tmp_path / f"dna-{site}.tif" + Image.fromarray(np.zeros((5, 7), dtype=np.uint16)).save(original) + metadata = {"well": "A01", "site": str(site), "z_index": "1", "timepoint": "1"} + source_metadata.append(metadata) + sources.extend( + ( + SourcePlaneProjection( + address=OpenHCSPlaneAddress.from_values("A01", site, 1, 1, 1), + ref=SourcePixelRef("disk", str(original)), + source_alias="DNA", + source_metadata=metadata, + ), + SourceArtifactProjection( + address=OpenHCSPlaneAddress.from_values("A01", site, 2, 1, 1), + ref=SourcePixelRef( + "disk", str(calibration), (site - 1,) if embedded_tiff else () + ), + source_alias="IllumDNA", + artifact_kind=ImageArtifactType, + source_metadata=metadata, + execution_scope=calibration_scope, + ), + ) + ) + serializer = SourceProjectionMetadataSerializer(SourceSchemaFilenameParser()) + document = { + "subdirectories": { + "source": serializer.metadata_dict( + SourceProjectionSet(tuple(sources)), + microscope_handler_name="openhcs", + source_filename_parser_name="source_schema", + grid_dimensions=[1, 1], + pixel_size=1.0, + ) + } + } + context = _export_context() + context.plate_path = tmp_path + context.microscope_handler = SimpleNamespace( + metadata_handler=SimpleNamespace( + source_workspace_metadata_document=lambda _plate_path: document, + ) + ) + context.filemanager = SimpleNamespace( + exists=lambda *_args: False, + source_path=lambda address, backend, *, base_path: address, + ) + plan = CompiledSourceBindingPlan( + bindings=( + NamedSourceBinding(alias="DNA"), + NamedSourceBinding( + alias="IllumDNA", projection_role=SourceProjectionRole.SOURCE_ARTIFACT + ), + ) + ) + table = MeasurementTable( + name="ImageMeasurements", + rows=MeasurementSparseColumnarRows.from_rows( + tuple( + {RUNTIME_IMAGE_FIELD: index, "Count_Nuclei": index + 1} + for index in range(2) + ), + fields=( + FieldSpec(RUNTIME_IMAGE_FIELD, int), + FieldSpec("Count_Nuclei", int), + ), + ), + subject=MeasurementSubject(MeasurementScope.IMAGE, "Image"), + source_image_provenance_planes=SourceImageProvenancePlanes.from_components( + component_metadata=tuple(source_metadata[:2]), + ), + ) + store = RuntimeValueStore() + output = ArtifactOutputPlan( + name=table.name, + path="/memory/measurements.pkl", + artifact_type=MeasurementsArtifactType, + ) + store.record( + RuntimeValue.normalize(output, table, axis_id="A01"), + path=output.path, + backend="memory", + ) + batch = RuntimeArtifactBatch( + input_specs=( + ArtifactSpec.input("DNA", ImageArtifactType), + ArtifactSpec.input("IllumDNA", ImageArtifactType), + ArtifactSpec.input(table.name, MeasurementsArtifactType), + ), + records_by_axis={"A01": store.values()}, + source_image_set_identity_policy=SourceImageSetIdentityPolicy( + frozenset((AllComponents.CHANNEL,)) + ), + source_binding_plan=plan, + ) + builder = CellProfilerAnalystProjectionBuilder(plan, context=context) + channels = CPAImageChannelSpec.defaults_for_artifacts( + batch.specs_of_type(ImageArtifactType), source_binding_plan=plan + ) + return builder, batch, channels, calibration, document + + +def test_source_only_calibration_occurrences_export_each_executed_site( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + builder, batch, channels, calibration, _document = _borrowed_source_export_fixture( + tmp_path + ) + assert batch.records_of_type(ImageArtifactType)["A01"] == () + monkeypatch.setattr( + np, + "asarray", + lambda *_args, **_kwargs: pytest.fail( + "Source headers must not decode pixel arrays" + ), + ) + + projection = builder.build(batch, _settings(), channels) + + rows = _external_rows(projection.image_table) + assert len(rows) == 2 # The third declared source site was never executed. + for image_number, row in enumerate(rows, start=1): + assert row["ImageNumber"] == image_number + assert row["Image_FileName_IllumDNA"] == calibration.name + assert row["Image_PathName_IllumDNA"] == str(tmp_path) + assert row["Image_URL_IllumDNA"] == calibration.as_uri() + assert row["Image_Height_IllumDNA"] == 5 + assert row["Image_Width_IllumDNA"] == 7 + assert row["Image_Frame_IllumDNA"] == row["Image_Series_IllumDNA"] == 0 + assert row["Image_Scaling_IllumDNA"] == 1.0 + assert len(row["Image_MD5Digest_IllumDNA"]) == 32 + assert row["Image_FileName_DNA"] == f"dna-{image_number}.tif" + assert row["Image_Scaling_DNA"] == 65535.0 + + +def test_source_bound_export_refuses_conflicting_recorded_alias(tmp_path: Path) -> None: + builder, batch, channels, _calibration, _document = _borrowed_source_export_fixture( + tmp_path + ) + other = tmp_path / "different.npy" + np.save(other, np.ones((5, 7), dtype=np.float32)) + store = RuntimeValueStore() + output = ArtifactOutputPlan( + name="IllumDNA", path="/memory/conflict.pkl", artifact_type=ImageArtifactType + ) + payload = ImagePayloadMetadata( + source_path=str(other), + source_component_metadata={ + "well": "A01", + "site": "1", + "z_index": "1", + "timepoint": "1", + }, + ).attach_to(np.ones((5, 7), dtype=np.float32)) + store.record( + RuntimeValue.normalize(output, payload, axis_id="A01"), + path=output.path, + backend="memory", + ) + batch = replace( + batch, records_by_axis={"A01": (*batch.records_by_axis["A01"], *store.values())} + ) + with pytest.raises(ValueError, match="conflicting values.*Image_FileName_IllumDNA"): + builder.build(batch, _settings(), channels) + + +def test_source_bound_export_retains_exact_embedded_tiff_frame_selection( + tmp_path: Path, +) -> None: + builder, batch, channels, calibration, _document = _borrowed_source_export_fixture( + tmp_path, embedded_tiff=True + ) + rows = _external_rows(builder.build(batch, _settings(), channels).image_table) + assert tuple(row["Image_Frame_IllumDNA"] for row in rows) == (0, 1) + assert ( + tuple(row["Image_FileName_IllumDNA"] for row in rows) == (calibration.name,) * 2 + ) + assert ( + tuple( + (row["Image_Height_IllumDNA"], row["Image_Width_IllumDNA"]) for row in rows + ) + == ((5, 7),) * 2 + ) + + +def test_source_bound_export_does_not_cross_declared_execution_scope( + tmp_path: Path, +) -> None: + from openhcs.core.component_group_scope import RuntimeExecutionAxisScope + + builder, batch, channels, _calibration, _document = _borrowed_source_export_fixture( + tmp_path, calibration_scope=RuntimeExecutionAxisScope("B01") + ) + rows = _external_rows(builder.build(batch, _settings(), channels).image_table) + assert len(rows) == 2 + assert all(row.get("Image_FileName_IllumDNA") is None for row in rows) + assert all(row["Image_FileName_DNA"] for row in rows) + + def test_measurement_provenance_projects_exact_named_contributors_by_site( tmp_path: Path, ) -> None: diff --git a/tests/unit/test_image_file_serialization.py b/tests/unit/test_image_file_serialization.py index b9dcee0ffe..37aa545030 100644 --- a/tests/unit/test_image_file_serialization.py +++ b/tests/unit/test_image_file_serialization.py @@ -7,6 +7,7 @@ from openhcs.core.image_file_serialization import ( ImageFileFormat, + ImageFileSourceMetadata, NumpyImageFileFormat, PngImageFileFormat, TiffImageFileFormat, @@ -124,6 +125,7 @@ def test_tiff_source_metadata_reads_dtype_and_declared_scale_without_imageio( assert metadata.source_dtype == np.dtype(np.uint16) assert metadata.intensity_scale == 4095.0 + assert metadata.image_shape_yx == (1, 2) assert metadata.pixel_semantics.channel_axis is None assert metadata.pixel_semantics.channel_count is None @@ -150,6 +152,7 @@ def test_tiff_source_metadata_reads_rgb_semantics_without_generic_reopen( assert metadata.intensity_scale == 255.0 assert metadata.pixel_semantics.channel_axis == -1 assert metadata.pixel_semantics.channel_count == 3 + assert metadata.image_shape_yx == (4, 5) assert metadata.pixel_semantics.validated_channel_axis(tifffile.imread(path)) == -1 @@ -177,6 +180,71 @@ def test_required_source_metadata_does_not_reuse_replaced_header( assert require_image_file_source_metadata(path).pixel_semantics.channel_axis is None assert image_file_source_metadata(path).pixel_semantics.channel_axis is None + assert require_image_file_source_metadata(path).image_shape_yx == (4, 5) + + +def test_numpy_source_header_owns_shape_dtype_and_revision_without_pixel_decode( + tmp_path, monkeypatch +) -> None: + path = tmp_path / "illum.npy" + np.save(path, np.zeros((5, 7), dtype=np.float32)) + original_load = np.load + calls = [] + + def load_header(file, *, mmap_mode, allow_pickle): + assert mmap_mode == "r" + assert allow_pickle is False + calls.append(file) + return original_load(file, mmap_mode=mmap_mode, allow_pickle=allow_pickle) + + monkeypatch.setattr(np, "load", load_header) + metadata = require_image_file_source_metadata(path) + assert metadata.image_shape_yx == (5, 7) + assert metadata.source_dtype == np.dtype(np.float32) + assert require_image_file_source_metadata(path) is metadata + assert len(calls) == 1 + np.save(path, np.zeros((3, 4), dtype=np.uint16)) + replaced = require_image_file_source_metadata(path) + assert replaced.image_shape_yx == (3, 4) + assert replaced.source_dtype == np.dtype(np.uint16) + assert len(calls) == 2 + + +@pytest.mark.parametrize( + "shape", ((1,), (1, 2, 3), (0, 2), (-1, 2), (True, 2), (1.0, 2)) +) +def test_source_header_refuses_invalid_declared_yx_dimensions(shape) -> None: + with pytest.raises(ValueError, match="two positive integer dimensions"): + ImageFileSourceMetadata(image_shape_yx=shape) + + +def test_numpy_non_image_header_does_not_infer_spatial_axes(tmp_path) -> None: + for name, shape in (("scalar", ()), ("volume", (2, 5, 7)), ("color", (5, 7, 3))): + path = tmp_path / f"{name}.npy" + np.save(path, np.zeros(shape, dtype=np.float32)) + metadata = require_image_file_source_metadata(path) + assert metadata.image_shape_yx is None + assert metadata.source_frame_shape is None + with pytest.raises(ValueError, match="container-frame mapping"): + metadata.frame_for_source_indices((0,)) + + +def test_tiff_header_owns_selected_frame_grid_and_bounds(tmp_path) -> None: + path = tmp_path / "frames.tif" + tifffile.imwrite( + path, + np.zeros((2, 3, 5, 7), dtype=np.uint16), + photometric="minisblack", + metadata={"axes": "TZYX"}, + ) + metadata = require_image_file_source_metadata(path) + assert metadata.image_shape_yx == (5, 7) + assert metadata.source_frame_shape == (2, 3) + assert metadata.frame_for_source_indices((1, 2)) == 5 + with pytest.raises(ValueError, match="complete container-frame mapping"): + metadata.frame_for_source_indices((1,)) + with pytest.raises(ValueError, match="outside dimension"): + metadata.frame_for_source_indices((2, 0)) def test_tiff_source_metadata_uses_declared_planar_sample_axis(tmp_path) -> None: From c729c7fb9379321f3b7f71a1a4afdb9930377097 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 06:50:12 -0400 Subject: [PATCH 058/235] refactor: derive header image layout from pixel semantics owner Share declared channel-axis validation between loaded pixels and header geometry, preserving the no-axis early return and refusing invalid negative axes instead of wrapping them. Remove the foreign optional-field probe from the generic image format reader. Refs #444. --- openhcs/core/image_file_serialization.py | 27 +++++++++--- tests/unit/test_image_file_serialization.py | 48 +++++++++++++++++++++ 2 files changed, 70 insertions(+), 5 deletions(-) diff --git a/openhcs/core/image_file_serialization.py b/openhcs/core/image_file_serialization.py index 4ab05b2b94..0b2593fa72 100644 --- a/openhcs/core/image_file_serialization.py +++ b/openhcs/core/image_file_serialization.py @@ -47,6 +47,26 @@ def validated_channel_axis(self, payload: Any) -> int | None: if axis is None: return None shape = tuple(int(value) for value in np.shape(image_payload_data(payload))) + return self._validated_channel_axis_for_shape(axis, shape) + + def image_shape_yx_for_shape( + self, + shape: tuple[int, ...], + ) -> tuple[int, int] | None: + """Project format-declared channel bands without inferring image axes.""" + + axis = self.channel_axis + if axis is not None: + self._validated_channel_axis_for_shape(axis, shape) + normalized = axis if axis >= 0 else len(shape) + axis + shape = shape[:normalized] + shape[normalized + 1 :] + return shape if len(shape) == 2 else None + + def _validated_channel_axis_for_shape( + self, + axis: int, + shape: tuple[int, ...], + ) -> int: normalized = axis if axis >= 0 else len(shape) + axis if normalized < 0 or normalized >= len(shape): raise ValueError( @@ -279,9 +299,6 @@ def _read_source_metadata(cls, path: Path) -> ImageFileSourceMetadata: dtype = properties.dtype pixel_semantics = cls.require_pixel_semantics(path) shape = tuple(properties.shape) - if pixel_semantics.channel_axis is not None: - axis = pixel_semantics.channel_axis % len(shape) - shape = shape[:axis] + shape[axis + 1 :] return ImageFileSourceMetadata( source_dtype=dtype, intensity_scale=( @@ -289,8 +306,8 @@ def _read_source_metadata(cls, path: Path) -> ImageFileSourceMetadata: or image_intensity_scale_for_dtype(dtype) ), pixel_semantics=pixel_semantics, - image_shape_yx=( - tuple(int(value) for value in shape) if len(shape) == 2 else None + image_shape_yx=pixel_semantics.image_shape_yx_for_shape( + tuple(int(value) for value in shape) ), ) diff --git a/tests/unit/test_image_file_serialization.py b/tests/unit/test_image_file_serialization.py index 37aa545030..5646baefa8 100644 --- a/tests/unit/test_image_file_serialization.py +++ b/tests/unit/test_image_file_serialization.py @@ -10,6 +10,7 @@ ImageFileSourceMetadata, NumpyImageFileFormat, PngImageFileFormat, + SourceImagePixelSemantics, TiffImageFileFormat, image_file_source_metadata, prepare_disk_image_payloads, @@ -218,6 +219,53 @@ def test_source_header_refuses_invalid_declared_yx_dimensions(shape) -> None: ImageFileSourceMetadata(image_shape_yx=shape) +@pytest.mark.parametrize( + "axis,shape,expected", + ( + (-1, (5, 7, 3), (5, 7)), + (0, (3, 5, 7), (5, 7)), + (None, (5, 7), (5, 7)), + (None, (5, 7, 3), None), + ), +) +def test_format_declared_image_shape_shares_loaded_channel_validation( + axis, shape, expected +) -> None: + semantics = SourceImagePixelSemantics(axis, 3 if axis is not None else None) + assert semantics.image_shape_yx_for_shape(shape) == expected + assert semantics.validated_channel_axis(np.zeros(shape, dtype=np.uint8)) == axis + + +@pytest.mark.parametrize("shape", ((5, 7, 4), (5, 7), ())) +def test_source_header_and_loaded_pixels_refuse_same_invalid_channel_layout( + shape, +) -> None: + semantics = SourceImagePixelSemantics(2, 3) + with pytest.raises(ValueError): + semantics.image_shape_yx_for_shape(shape) + with pytest.raises(ValueError): + semantics.validated_channel_axis(np.zeros(shape, dtype=np.uint8)) + + +def test_no_channel_declaration_does_not_inspect_payload(monkeypatch) -> None: + from openhcs.core import image_file_serialization + + monkeypatch.setattr( + image_file_serialization, + "image_payload_data", + lambda _payload: pytest.fail("No-axis guard must precede payload inspection"), + ) + assert SourceImagePixelSemantics().validated_channel_axis(object()) is None + + +def test_declared_negative_channel_axis_is_not_wrapped_into_valid_range() -> None: + semantics = SourceImagePixelSemantics(-4, 3) + with pytest.raises(ValueError, match="axis -4 is invalid"): + semantics.image_shape_yx_for_shape((5, 7, 3)) + with pytest.raises(ValueError, match="axis -4 is invalid"): + semantics.validated_channel_axis(np.zeros((5, 7, 3))) + + def test_numpy_non_image_header_does_not_infer_spatial_axes(tmp_path) -> None: for name, shape in (("scalar", ()), ("volume", (2, 5, 7)), ("color", (5, 7, 3))): path = tmp_path / f"{name}.npy" From d4e800a7552623c8d5a55522aadea42728ecea6e Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 07:06:42 -0400 Subject: [PATCH 059/235] fix: honor declared composition for unannotated raw ABI --- openhcs/core/callable_contract.py | 5 ++ tests/unit/test_callable_abi_preparation.py | 67 +++++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/openhcs/core/callable_contract.py b/openhcs/core/callable_contract.py index ad74e85f5e..18e75a7f7f 100644 --- a/openhcs/core/callable_contract.py +++ b/openhcs/core/callable_contract.py @@ -445,6 +445,11 @@ def raw_main_flow_call_argument(self, source_payload: Any, signature: inspect.Si primary_name = self.primary_input_name(signature.parameters) annotation = None if primary_name is None else signature.parameters[primary_name].annotation + if ( + annotation is inspect.Parameter.empty + and self.image_payload_consumption is ImagePayloadConsumption.COMPOSED + ): + return source_payload return source_payload if any( isinstance(source_payload, argument_type) for argument_type in self._nominal_argument_types(annotation) diff --git a/tests/unit/test_callable_abi_preparation.py b/tests/unit/test_callable_abi_preparation.py index 8b050ad204..dd3f1aa8e9 100644 --- a/tests/unit/test_callable_abi_preparation.py +++ b/tests/unit/test_callable_abi_preparation.py @@ -6,6 +6,7 @@ from dataclasses import replace from functools import wraps from unittest.mock import Mock +from typing import Any import cloudpickle import numpy as np @@ -22,6 +23,8 @@ from openhcs.core.function_reference import ImportableFunctionReference from openhcs.core.processing_preparation import CallablePreparation, PreparationCacheBatch from openhcs.core.runtime_image_values import ImagePayloadMetadata, image_payload_data +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 ( Pure2DSliceBatchExecutor, RuntimeBatchExecutionDomain, @@ -37,6 +40,70 @@ def raw_numeric(image: np.ndarray, scale: float = 2.0) -> np.ndarray: return image * scale +@composed_image_payload +def unannotated_composed_echo(image): + return image + + +@pytest.mark.parametrize( + ("composed", "annotation", "retains_payload"), + ((True, inspect.Parameter.empty, True), + (False, inspect.Parameter.empty, False), + (True, np.ndarray, False), + (True, Any, False), + (True, ArrayPayload, True)), +) +def test_composition_default_respects_explicit_raw_carrier_boundary( + composed, annotation, retains_payload, fake_preparation, +): + received = [] + def raw(image): + received.append(image) + return image + if annotation is not inspect.Parameter.empty: + raw.__annotations__["image"] = annotation + if composed: + composed_image_payload(raw) + contract = CallableContract.from_prepared_callable(raw) + pixels = np.arange(20, dtype=np.float32).reshape(1, 4, 5) + mask = np.ones((1, 4, 5), dtype=bool) + mask[0, 0, 0] = False + source = ImagePayloadMetadata( + plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + source_image_names=("FITC",), + ).payload_with(pixels, mask) + expected = source if retains_payload else pixels + + result = RuntimeCallablePolicy().contract_invocation(contract, raw, source, {}).call() + + assert result is expected + assert received[0] is expected + assert source.metadata.plane_axis is RuntimePlaneAxis.SOURCE_BINDING + np.testing.assert_array_equal(source.mask, mask) + + +@pytest.mark.parametrize("serializer", (pickle, cloudpickle)) +def test_prepared_composition_default_survives_transport_without_introspection( + serializer, fake_preparation, monkeypatch, +): + contract = CallableContract.from_prepared_callable(unannotated_composed_echo) + reference = ImportableFunctionReference( + import_identity=CallableImportIdentity.from_callable(unannotated_composed_echo), + composite_key="test:unannotated_composed_echo", metadata=contract.metadata, + ) + restored = CallableContract.from_callable(serializer.loads(serializer.dumps(reference))) + def forbidden(*args, **kwargs): + pytest.fail("Prepared composed ABI must not inspect live callable declarations") + monkeypatch.setattr(contract_module, "get_type_hints", forbidden) + monkeypatch.setattr(inspect, "signature", forbidden) + payload = ImagePayloadMetadata( + plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + source_image_names=("FITC",), + ).payload_with(np.ones((1, 4, 5)), np.ones((1, 4, 5), dtype=bool)) + for _ in range(40): + assert restored.raw_main_flow_call_argument(payload) is payload + + @pytest.fixture def fake_preparation(monkeypatch): prepare = Mock() From 424bd2029bc21af0626d74356d06cfe075d25363 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 07:41:29 -0400 Subject: [PATCH 060/235] docs: record thread-owned attribution and actual geometry ABI loss (#384, #450) --- .../shared_runtime_plumbing_384_20261001.rst | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/docs/validation/shared_runtime_plumbing_384_20261001.rst b/docs/validation/shared_runtime_plumbing_384_20261001.rst index 29acca111b..5941ad53c8 100644 --- a/docs/validation/shared_runtime_plumbing_384_20261001.rst +++ b/docs/validation/shared_runtime_plumbing_384_20261001.rst @@ -945,3 +945,76 @@ recording/export ceiling of 2.4189 seconds; a two-second saving requires about Receipts are ``openhcs-measurement-export-declared-state-profile-20261002`` and ``openhcs-measurement-export-declared-fields-alias-control-20261002.json`` under ``/var/tmp``. The original four-snapshot and raw-heap failures remain retained. + + +Current-main integration and recording attribution (2026-10-02) +--------------------------------------------------------------- + +Main was normally merged through 3cb701770 at 6097ec4ff. Independent PR447 +merged the source-only CPA metadata repair and closed issue444. Its own +220 controls, original architecture check, public Advanced run and independent +scientific/source-identity comparison are documented in +``cpa-secondary-source-metadata-444.rst``. The larger branch still has open +acceptance gates and remains draft. + +The main58ee singleton controls exposed a real prepared-ABI integration bug: +unannotated COMPOSED functions lost their nominal carrier. d4e800a75 derives +that default from the existing consumption declaration while preserving +explicit ndarray/Any/nominal ABI precedence. 136 isolated and249 integrated +controls pass; the original scoped architecture check has zero increases. + +A fresh frozen746 Advanced run completed with compilation1.7983s, +execution9.4002s and total12.3852s, default observers and startup excluded. +Its scientific database comparison has zero differences. All90 previously +NULL calibration fields are present;70 match literally and20 native staging +PathName/URL strings are proven to identify the same source files using +same-inode, resolved-source and original native input-inventory SHA checks. +The raw path-string comparison remains separatelyRED. This is one observation, +not a performance benefit: colocalization and one primary identification step +were slower than frozen919, while database export changed1.8378 to1.7538s. + +The first nine-recorder cProfile diagnostic is scopeRED. It omitted the +existing monitoring callback thread filter and attributed background sleeps +to arbitrary numeric callers. A separate companion-thread experiment +reproduced the error and confirmed the existing worker-profiling policy +removes it after every profiler enable. No production workaround or duplicate +issue for the already-fixed profiler infrastructure was introduced. + +The corrected frozen919 diagnostic has zero foreign sleep/server/poller +events, exact original IFC CSV bytes, and zero scientific differences against +native CP for1800rows. Its disjoint recording attribution is diagnostic only: + +=============================== ============ +Region Profile time +=============================== ============ +Centroid calculation 0.6766s +Sparse relationship row assembly 0.4404s +Repeated axis-domain scans 0.2159s +Other table assembly 0.3644s +Ownership validation 0.000027s +Measurement storage 0.0123s +=============================== ============ + +Centroid-to-IJV conversion is nested within centroid calculation and must not +be added to it. Storage/ownership are immaterial targets for these recording +calls; centroid-only cannot deliver the multi-second goal. Recording plus +nonserialization export remains an optimistic2.4189s ceiling, not a measured +counterfactual improvement. + +A real source-born3D diagnostic on isolated8dec failed mask alignment at +step18. It observed300 source births and18 completed producer groups with +no observation errors. RescaledDNA starts with60 correlated physical Z rows; +Resize preserves60planes and changes onlyXY but drops the source domain. +Resize after ImageMath repeats the loss. Source audit identifies bare ndarray +annotations on the public Resize entries despite their shared implementation +requiring metadata/masks. Direct unwrapped tests bypassed this boundary. +Issue450 is formally linked to PR394; truthful RuntimeArrayData declarations, +prepared-contract tests and a fresh production rerun are required. No source +facts are injected into legacy captures, and no3D speedup/parity claim is made. + +Retained thread-scope/science receipts: +``/var/tmp/openhcs-nine-recorders-profile-thread-scope-control-20261002.json``, +``/var/tmp/openhcs-nine-recorders-profile-thread-owned-science-20261002.json`` +and ``/var/tmp/openhcs-nine-recorders-thread-owned-disjoint-analysis-20261002.json``. +Actual failed3D proof: +``/var/tmp/openhcs-spatial-plane-sourceborn-diagnostic-v2-20261002/observations.json``. From bae7f072bf88f36f2811c004560d8d6c23ba37ae Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 08:03:41 -0400 Subject: [PATCH 061/235] docs: record current 3D evidence and reject unused spatial prototype (#450) --- .../shared_runtime_plumbing_384_20261001.rst | 68 ++++++++++++++++--- 1 file changed, 58 insertions(+), 10 deletions(-) diff --git a/docs/validation/shared_runtime_plumbing_384_20261001.rst b/docs/validation/shared_runtime_plumbing_384_20261001.rst index 5941ad53c8..8d2ab12902 100644 --- a/docs/validation/shared_runtime_plumbing_384_20261001.rst +++ b/docs/validation/shared_runtime_plumbing_384_20261001.rst @@ -984,16 +984,16 @@ The corrected frozen919 diagnostic has zero foreign sleep/server/poller events, exact original IFC CSV bytes, and zero scientific differences against native CP for1800rows. Its disjoint recording attribution is diagnostic only: -=============================== ============ -Region Profile time -=============================== ============ -Centroid calculation 0.6766s -Sparse relationship row assembly 0.4404s -Repeated axis-domain scans 0.2159s -Other table assembly 0.3644s -Ownership validation 0.000027s -Measurement storage 0.0123s -=============================== ============ +================================ ============ +Region Profile time +================================ ============ +Centroid calculation 0.6766s +Sparse relationship row assembly 0.4404s +Repeated axis-domain scans 0.2159s +Other table assembly 0.3644s +Ownership validation 0.000027s +Measurement storage 0.0123s +================================ ============ Centroid-to-IJV conversion is nested within centroid calculation and must not be added to it. Storage/ownership are immaterial targets for these recording @@ -1018,3 +1018,51 @@ Retained thread-scope/science receipts: and ``/var/tmp/openhcs-nine-recorders-thread-owned-disjoint-analysis-20261002.json``. Actual failed3D proof: ``/var/tmp/openhcs-spatial-plane-sourceborn-diagnostic-v2-20261002/observations.json``. + + +Actual 3D production completion and remaining qualification (2026-10-02) +---------------------------------------------------------------------- + +The isolated physical-plane prototype at aba4bfaf3 normally incorporates this +branch through424bd2029. Its truthful Resize ABI declarations and prepared +FULL_STACK controls pass438 tests. The actual new production run completes +with no observer errors,780 source-plane births and32 producer groups. Resize +now retains the runtime stack axis and all60 correlated physical rows; +MonolayerMask and the primary input consequently use the existing declared +stack-binding path. The diagnostic controller had required the different +opaque-image fallback's hook. Its original observation gate remains RED; +zero calls to that fallback is not a production failure or evidence of lost +metadata. The actual route is separately adjudicated from retained producer +observations and the existing binding declaration. + +Independent unchanged saved-output comparison passes all6 CSVs and120 TIFFs +exactly against the specified retained ordinary reference: headers/cells and +image dtype/shape/pixels. Receipt: +``/var/tmp/openhcs-spatial-plane-aba4-independent-historical-science-20261002.json``. +Diagnostic clocks are compilation2.1216s, execution9.7020s and total12.7136s; +they exclude server startup but do not establish a new performance benefit +or a current native CP ratio. Original R1 passes within its unchanged budget; +original R0 retains the quoted-Iterable finding. Independent actual RGB-volume +controls show identical pixels/masks but incorrect XY metadata in the +prototype: (3,3) instead of the declared channel-aware (2,3). Baseline passes. +The separate 2D RGB mask failure also occurs on baseline and is not claimed +as a new regression. + +The decisive route reassessment rejects promotion of the broader physical-Z +prototype. Preserving Resize's runtime stack carrier makes the existing typed +binding sufficient here; the new opaque physical-plane binding has no actual +production consumer in this run. Synthetic controls and successful saved +science do not establish its runtime payoff. Further prototype implementation +is stopped; the next counterfactual applies only truthful carrier declarations +and existing prepared metadata/mask/stack controls to current PR394. Actual +production execution must identify any remaining necessary repair. The +original failed observer gate and the RGB counterexample remain retained; +no source facts are injected and no gate is retroactively called PASS. + +PR394 normally incorporates latest main4754fbe2b at2aa930eb2. The added Napari +integration controls pass183 cases and fail the existing offscreen window +width assertion (1260 versus0). The exact isolated assertion also fails on +clean main4754fbe2b with the same values; it remains a separately recorded +inherited gate failure, not a passing full suite. Logs: +``/var/tmp/openhcs-pr394-latest-main4754-napari-controls-20261002.log`` and +``/var/tmp/openhcs-main4754-napari-layout-isolated-20261002.log``. From fecf3114aeec77931718e7da4ea885c9aa7cdfe4 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 08:08:30 -0400 Subject: [PATCH 062/235] Preserve typed geometry carriers at prepared call boundaries (#450) Declare the actual RuntimeArrayData inputs and returns of Resize and ResizeVolumetric, and the actual MaskImage return family. Preserve existing masks, source provenance and runtime/opaque domains through the prepared FULL_STACK executor without changing numerical or contextualization algorithms. --- .../backends/cellprofiler/image_geometry.py | 10 +- ...lprofiler_geometry_prepared_payload_abi.py | 127 ++++++++++++++++++ 2 files changed, 132 insertions(+), 5 deletions(-) create mode 100644 tests/unit/test_cellprofiler_geometry_prepared_payload_abi.py diff --git a/openhcs/processing/backends/cellprofiler/image_geometry.py b/openhcs/processing/backends/cellprofiler/image_geometry.py index 37b24b3ce3..f51560c8a8 100644 --- a/openhcs/processing/backends/cellprofiler/image_geometry.py +++ b/openhcs/processing/backends/cellprofiler/image_geometry.py @@ -1098,7 +1098,7 @@ def mask_image( mask_source: MaskSource = MaskSource.IMAGE, invert_mask: bool = False, binary_threshold: float = 0.5, -) -> np.ndarray: +) -> RuntimeArrayData: """Mask an image using CellProfiler image/object mask semantics. Args: @@ -1276,14 +1276,14 @@ def tile( @numpy(contract=ProcessingContract.PURE_2D) def resize( - image: np.ndarray, + image: RuntimeArrayData, resize_method: ResizeMethod = ResizeMethod.BY_FACTOR, resizing_factor_x: float = 0.25, resizing_factor_y: float = 0.25, specific_width: int = 100, specific_height: int = 100, interpolation: InterpolationMethod = InterpolationMethod.NEAREST_NEIGHBOR, -) -> np.ndarray: +) -> RuntimeArrayData: """Resize a CellProfiler image plane by factor or explicit dimensions.""" pixels = image_payload_data(image) geometry = ResizeGeometry.from_parameters( @@ -1298,7 +1298,7 @@ def resize( @numpy(contract=ProcessingContract.PURE_3D) def resize_volumetric( - image: np.ndarray, + image: RuntimeArrayData, resize_method: ResizeMethod = ResizeMethod.BY_FACTOR, resizing_factor_x: float = 0.25, resizing_factor_y: float = 0.25, @@ -1307,7 +1307,7 @@ def resize_volumetric( specific_height: int = 100, specific_planes: int = 10, interpolation: InterpolationMethod = InterpolationMethod.NEAREST_NEIGHBOR, -) -> np.ndarray: +) -> RuntimeArrayData: """Resize a CellProfiler ZYX image volume by factor or explicit dimensions.""" pixels = image_payload_data(image) geometry = ResizeGeometry.from_trailing_spatial_parameters( diff --git a/tests/unit/test_cellprofiler_geometry_prepared_payload_abi.py b/tests/unit/test_cellprofiler_geometry_prepared_payload_abi.py new file mode 100644 index 0000000000..933738e65e --- /dev/null +++ b/tests/unit/test_cellprofiler_geometry_prepared_payload_abi.py @@ -0,0 +1,127 @@ +"""Geometry declarations preserve the payload their raw implementations consume.""" + +from dataclasses import replace +from typing import get_type_hints + +import numpy as np +import pytest + +from openhcs.core.aligned_image_payload import ImagePayloadExecutionMode +from openhcs.core.callable_contract import CallableContract +from openhcs.core.runtime_array_values import RuntimeArrayData +from openhcs.core.runtime_image_values import ( + ImagePayloadMetadata, + image_payload_data, + image_payload_mask, + image_payload_metadata, +) +from openhcs.core.runtime_plane_projection import RuntimePlaneAxis +from openhcs.core.source_spatial_domain import SourceSpatialDomain +from openhcs.interop.cellprofiler.runtime.adapter import CellProfilerRuntimeAdapter +from openhcs.interop.cellprofiler.runtime.function_contract_execution import ( + CellProfilerFunctionContractExecutor, +) +from openhcs.processing.backends.cellprofiler.image_geometry import ( + mask_image, + resize, + resize_volumetric, +) + + +def _prepared_contract(func): + contract = CallableContract.from_callable(func).with_prepared_signature() + return replace( + contract, + metadata=replace( + contract.metadata, + runtime_adapter=CellProfilerRuntimeAdapter.runtime_adapter_spec(), + ), + ) + + +@pytest.mark.parametrize("func", (resize, resize_volumetric)) +def test_prepared_resize_preserves_typed_primary_and_bare_array(func): + pixels = np.arange(24, dtype=np.float32).reshape(4, 6) + source = ImagePayloadMetadata( + source_path="/images/input.tif", source_image_names=("Original",), + ).payload_with(pixels, pixels % 2 == 0) + contract = _prepared_contract(func) + + assert contract.raw_main_flow_call_argument(source) is source + assert contract.raw_main_flow_call_argument(pixels) is pixels + + +@pytest.mark.parametrize("axis", (None, RuntimePlaneAxis.RUNTIME_SLICE)) +@pytest.mark.parametrize("z_factor", (1.0, 2.0)) +def test_prepared_full_stack_resize_preserves_existing_metadata_and_mask(axis, z_factor): + planes = tuple( + ImagePayloadMetadata( + source_path=f"/images/z{index}.tif", + source_image_names=("Original",), + source_component_metadata={"z_index": index, "timepoint": 1, "well": "W001"}, + source_spatial_domain=SourceSpatialDomain( + origin_yx=(2, 3), source_shape_yx=(8, 10), + ), + ).payload_with(np.full((4, 6), index, dtype=np.float32), None) + for index in (3, 1, 2) + ) + metadata = ImagePayloadMetadata.compose(planes).replace_fields(plane_axis=axis) + pixels = np.stack(tuple(image_payload_data(plane) for plane in planes)) + mask = np.ones(pixels.shape, dtype=bool) + mask[:, 0, 0] = False + source = metadata.payload_with(pixels, mask) + contract = _prepared_contract(resize_volumetric) + canonical = contract.resolve_canonical_raw_callable() + kwargs = dict( + resizing_factor_x=0.5, resizing_factor_y=0.5, resizing_factor_z=z_factor, + ) + expected = canonical(source, **kwargs) + + result = CellProfilerFunctionContractExecutor().execute( + contract, canonical, source, kwargs, + execution_mode=ImagePayloadExecutionMode.FULL_STACK, + ) + + np.testing.assert_array_equal(image_payload_data(result), image_payload_data(expected)) + np.testing.assert_array_equal(image_payload_mask(result), image_payload_mask(expected)) + assert image_payload_metadata(result) == image_payload_metadata(expected) + assert image_payload_metadata(result).plane_axis is axis + assert image_payload_metadata(result).source_image_paths == metadata.source_image_paths + assert image_payload_metadata(result).source_provenance == metadata.source_provenance + assert image_payload_metadata(result).source_spatial_domain.source_shape_yx == (2, 3) + assert image_payload_metadata(source) is metadata + np.testing.assert_array_equal(image_payload_data(source), pixels) + np.testing.assert_array_equal(image_payload_mask(source), mask) + + +@pytest.mark.parametrize("func", (resize, resize_volumetric)) +def test_prepared_resize_bare_pixels_preserve_canonical_return_contract(func): + pixels = np.arange(24, dtype=np.float32).reshape(4, 6) + contract = _prepared_contract(func) + canonical = contract.resolve_canonical_raw_callable() + kwargs = dict(resizing_factor_x=0.5, resizing_factor_y=0.5) + if func is resize_volumetric: + kwargs["resizing_factor_z"] = 1.0 + expected = canonical(pixels, **kwargs) + + result = CellProfilerFunctionContractExecutor().execute( + contract, canonical, pixels, kwargs, + execution_mode=ImagePayloadExecutionMode.FULL_STACK, + ) + + assert isinstance(result, RuntimeArrayData) + assert type(result) is type(expected) + np.testing.assert_array_equal(image_payload_data(result), image_payload_data(expected)) + np.testing.assert_array_equal(image_payload_mask(result), image_payload_mask(expected)) + assert image_payload_metadata(result) == image_payload_metadata(expected) + + +def test_mask_image_declares_its_actual_nominal_return_family(): + contract = _prepared_contract(mask_image) + canonical = contract.resolve_canonical_raw_callable() + assert get_type_hints(canonical)["return"] == RuntimeArrayData + pixels = np.arange(12, dtype=np.float32).reshape(3, 4) + source = ImagePayloadMetadata(source_path="/images/input.tif").payload_with(pixels) + result = canonical(source, pixels > 3) + assert isinstance(result, RuntimeArrayData) + assert image_payload_metadata(result).source_image_paths == ("/images/input.tif",) From 3cd113f1d57064a7911a6bf747425c45fcb9a1ba Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 08:19:16 -0400 Subject: [PATCH 063/235] docs: qualify narrow geometry fix and fresh native 3D comparison (#450) --- .../shared_runtime_plumbing_384_20261001.rst | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/docs/validation/shared_runtime_plumbing_384_20261001.rst b/docs/validation/shared_runtime_plumbing_384_20261001.rst index 8d2ab12902..6ab20bfa48 100644 --- a/docs/validation/shared_runtime_plumbing_384_20261001.rst +++ b/docs/validation/shared_runtime_plumbing_384_20261001.rst @@ -1066,3 +1066,60 @@ clean main4754fbe2b with the same values; it remains a separately recorded inherited gate failure, not a passing full suite. Logs: ``/var/tmp/openhcs-pr394-latest-main4754-napari-controls-20261002.log`` and ``/var/tmp/openhcs-main4754-napari-layout-isolated-20261002.log``. + + +Qualified narrow carrier repair and fresh native 3D (2026-10-02) +--------------------------------------------------------------- + +The rejected physical-plane prototype is absent from PR394. Narrow commit +b7640ad61 on isolated main-synchronized2aa930eb2 changes only five geometry +annotations; no numerical body, domain, HoleRemoval or renderer changes. +Nine prepared/FULL_STACK controls establish7 failures before and9 passes +after, preserving typed masks, runtime/opaque domains and source provenance. +The related suite passes148 tests; unchanged original R0/R1 both pass within +their original budgets. Integration fecf3114a has exactly the same production +and test Git trees as the qualified isolated source; only validation prose +differs. The fix is formally carried by PR394 for issue450. + +The actual uninstrumented default3D pipeline completes, with exact6CSV and +120TIFF saved science against the same retained reference. All180 actual +source-plane references belong to exactly3 physical TIFFs with indices0..59 +and unchanged source-file SHA values. Both fresh native CP4.2.8.1 measured +repeats pass the existing strict6table/150key/2825fact comparison, exact two +logical uint16(60,256,256) label volumes and complete physical-file coverage. +Native comparison to the prototype plus exact prototype/reference/narrow +comparisons establishes transitive narrow parity; no direct narrow/native +comparison is claimed. The original comparison-controller API failure +(tuple subtraction from frozenset) remains RED; a separate recipe supplies +the required frozenset input views without changing any scientific gate. + +Fresh current main4754fbe2b also matches the same saved reference exactly. +All clocks below use CPU5 and one thread, with startup/warmup excluded. +OpenHCS includes default OUTCOMES and memory observation; native invocation +includes prepare_run, module execution, post_run and Measurements.close, +and excludes imports/JVM/pipeline loading plus its separate warmup. + +===================== ============ ============ ============ +Observation Compilation Execution Total +===================== ============ ============ ============ +Main4754 ordinary 1.6754s 10.0712s 12.4766s +Narrowb764 ordinary 1.6190s 8.3008s 10.7417s +Native measured0 excluded 14.3544s invocation +Native measured1 excluded 14.4375s invocation +===================== ============ ============ ============ + +The observed main/candidate difference is1.7704s execution and1.7349s total +for the full PR394 plus annotation repair. It is not evidence that the five +annotations alone save that time. One ordinary observation per source does +not establish repeatability or statistical significance. Native's two-run +mean is14.39595s, giving a descriptive1.7343x candidate execution ratio and +1.3402x whole-pipeline ratio under the explicitly different clock scopes. +The target gap remains: reaching2x requires about1.10s further execution +reduction, or3.54s whole-pipeline reduction. No goal completion is claimed. + +Receipts: +``/var/tmp/openhcs-geometry-narrow-original-guards-20261002.json``, +``/var/tmp/openhcs-geometry-narrow-b764-3d-ordinary-20261002/observations.json``, +``/var/tmp/openhcs-geometry-narrow-b764-independent-science-and-inputs-20261002.json``, +``/var/tmp/openhcs-main4754-independent-historical-3d-science-20261002.json`` and +``/var/tmp/openhcs-native-3d-aba4-qualification-20261002/fresh-native-scientific-comparison-supplemental.json``. From c5e15c84a33e77144f187e94f97b38e40755c33f Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 08:58:34 -0400 Subject: [PATCH 064/235] docs: record four-run 3D timings and dominant plumbing frontier (#384) --- .../shared_runtime_plumbing_384_20261001.rst | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/docs/validation/shared_runtime_plumbing_384_20261001.rst b/docs/validation/shared_runtime_plumbing_384_20261001.rst index 6ab20bfa48..643f91f193 100644 --- a/docs/validation/shared_runtime_plumbing_384_20261001.rst +++ b/docs/validation/shared_runtime_plumbing_384_20261001.rst @@ -1123,3 +1123,81 @@ Receipts: ``/var/tmp/openhcs-geometry-narrow-b764-independent-science-and-inputs-20261002.json``, ``/var/tmp/openhcs-main4754-independent-historical-3d-science-20261002.json`` and ``/var/tmp/openhcs-native-3d-aba4-qualification-20261002/fresh-native-scientific-comparison-supplemental.json``. + + +Four-run 3D comparison and dominant runtime frontier (2026-10-02) +---------------------------------------------------------------- + +The subsequent A1/B1/B2/A2 ordinary comparison supersedes the single-pair +ratios above. A is frozen clean main4754fbe2b; B is frozen narrowb7640ad61, +including the full PR394 runtime change and five truthful annotations. Each +run uses CPU5, one worker/thread, the unchanged public driver, default OUTCOMES +and memory observation. Mandatory server/library/kernel startup and shutdown +remain outside the pipeline clocks. All four observations preserve exact six +CSV tables and120 TIFFs against the same retained reference, unchanged source +and dependency freezes, and all180 source-plane references to the same three +physical volumes used by fresh native CP. + +======================= ============ ============ ============ +Observation Compilation Execution Total +======================= ============ ============ ============ +Main A1 1.6754s 10.0712s 12.4766s +Candidate B1 1.6190s 8.3008s 10.7417s +Candidate B2 1.7936s 8.7713s 11.3281s +Main A2 1.6213s 10.0910s 12.4374s +Main mean 1.6484s 10.0811s 12.4570s +Candidate mean 1.7063s 8.5360s 11.0349s +======================= ============ ============ ============ + +The descriptive mean reductions are1.5451s execution and1.4221s total. +Native warm invocation mean14.39595s gives1.6865x execution and1.3046x +total ratios under the previously stated clock scopes. Two observations per +source do not establish statistical significance or annotation-only causality. +The remaining2x gap is1.3381s execution and3.8369s total, larger than the +first-pair estimate. The complete independent receipt is +``/var/tmp/openhcs-3d-b764-abba-science-clocks-20261002.json``. + +A separate source-pinned diagnostic partitions8.3448s of execution into +4.0497s inside the callable boundary and4.2951s outside it. Callable time +includes decorated metadata/control work, not only numerical kernels. The +exclusive outer terms include stack loading0.5991s, unstacking0.2126s, +saving0.3487s, output identity0.2070s across1680 calls, image recording0.2333s, +CP image requests0.2012s, SaveImages invocation preparation0.3463s, metadata +finalization0.1112s, publication0.4795s and reconciliation0.2812s. These are +diagnostic spans, not accepted speedup measurements; required copying, +validation and I/O remain within them. + +The SaveImages preparation span stores memory artifacts and contextualizes +their values; it is not TIFF encoding. Actual artifact materialization has a +separate0.0831s envelope. Saved metadata contains120 SourceArtifactProjection +entries from the two SaveImages outputs. The generic main-flow metadata/VFS +reload branch is not the observed publication route. The actual joint route +is artifact invocation/store, successful materialization metadata, source +artifact projection, serialization, locked publication and reconciliation. + +Publication/finalization/identity alone has an optimistic1.0789s ceiling and +is rejected as insufficient to close the current execution gap. The broader +cohort/recording/invocation/publication envelope is3.0201s and requires at +least44.3% collapse before accounting for mandatory work. Existing whole-stack +cache lookup/storage is already below1ms; dictionary tuning or assuming every +input is restacked cannot support the target. The next admission requires an +actual saved joint leaf replay preserving metadata mutation, source correlation, +custom normalization, error ordering and pixel/mask isolation. No new mutable +identity cache or production optimization has yet been admitted by this ceiling. + +Diagnostic science and freeze validation: +``/var/tmp/openhcs-narrow450-phase-science-and-freeze-20261002.json``. +Global authority/consumer inventory and corrected route: +``/var/tmp/openhcs-b764-publication-authority-consumer-inventory-20261002.json`` +and ``/var/tmp/openhcs-b764-coupled-publication-source-frontier-v2-20261002.json``. + +Latest mainb68029c0c is normally merged atc0d78b62e. The initial MCP/agent +integration run passes131 cases and fails both MRO variants of a new cold +feedback fixture. Clean mainb680 reproduces both failures: its reporting thread +can observe published progress before the fake compiler sets its subsequent +``emitted`` marker. This identifies a fixture synchronization race, not a +demonstrated production race. The original candidate and clean-main failures +remain retained. Baseline causal receipt: +``/var/tmp/openhcs-main-b680-cold-feedback-race-baseline-20261002.json``. +Benchmark timings above remain pinned to their actual main4754/narrowb764 +sources; the newer MCP merge is not silently included in their evidence. From 3317bf9c2631324fefd1c92e63f74a13b0540f29 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 08:59:47 -0400 Subject: [PATCH 065/235] Synchronize cold progress fixture before publishing (#436) The SDK progress relay can run before emit_progress returns. Mark entry into held compilation before publishing, preserving the real progress release and both cooperative MRO assertions. --- tests/unit/agent/test_cold_inspection_feedback.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/unit/agent/test_cold_inspection_feedback.py b/tests/unit/agent/test_cold_inspection_feedback.py index 57db9b0f62..32d6a7d586 100644 --- a/tests/unit/agent/test_cold_inspection_feedback.py +++ b/tests/unit/agent/test_cold_inspection_feedback.py @@ -63,7 +63,7 @@ def test_new_gateway_hook_cooperates_in_both_mros_before_terminal_result(tmp_pat """Independent audit capability needs no edits to service or MCP consumers.""" identity = ContextVar("cold-feedback-request", default="outside") calls, messages = [], [] - emitted = threading.Event() + compile_entered = threading.Event() release = threading.Event() error = ValueError("original declared compiler error") @@ -77,8 +77,8 @@ def compile(self, request): class DeclaredGateway(_FakeCompileInspectionGateway): def _compile(self, request): assert threading.current_thread() is threading.main_thread() + compile_entered.set() self.emit_progress(request) - emitted.set() assert release.wait(3), "Actual stage relay was not delivered during held work" if fails: raise error @@ -97,7 +97,7 @@ async def report_progress(self, progress, total=None, message=None): assert threading.current_thread() is not threading.main_thread() messages.append(message) if "compile: running: A01: compilation" in message: - assert emitted.is_set() + assert compile_entered.is_set() release.set() executor = McpTransportExecutor() @@ -105,7 +105,7 @@ async def report_progress(self, progress, total=None, message=None): try: for gateway_type in (Before, After): release.clear() - emitted.clear() + compile_entered.clear() gateway = gateway_type() service = ExecutionSessionService( path_policy=AgentPathPolicy.with_roots( From 7a835b48a0931e61bd6c0c6a00db6c1967b5b715 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 09:04:09 -0400 Subject: [PATCH 066/235] docs: qualify latest-main cold progress integration controls (#384) --- .../shared_runtime_plumbing_384_20261001.rst | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/docs/validation/shared_runtime_plumbing_384_20261001.rst b/docs/validation/shared_runtime_plumbing_384_20261001.rst index 643f91f193..3c6c7ed82e 100644 --- a/docs/validation/shared_runtime_plumbing_384_20261001.rst +++ b/docs/validation/shared_runtime_plumbing_384_20261001.rst @@ -1201,3 +1201,26 @@ remain retained. Baseline causal receipt: ``/var/tmp/openhcs-main-b680-cold-feedback-race-baseline-20261002.json``. Benchmark timings above remain pinned to their actual main4754/narrowb764 sources; the newer MCP merge is not silently included in their evidence. + +Test-only integration3317bf9c2 sets the compiler-entry marker before publishing +progress. The actual reporting callback still releases held work; the original +three-second timeout, both cooperative MROs, ContextVar restoration, terminal +ordering and original compiler-error assertions are unchanged. No production +race workaround, retry or longer timeout is introduced. The clean-main repair +passes124 cold-feedback/agent-service controls; integrated PR394 then passes +all133 controls including the narrow prepared geometry ABI tests in13.72s. +Both initial failed runs remain retained. Integrated command, CPU3: + +.. code-block:: bash + + env PYTHONDONTWRITEBYTECODE=1 \ + PYTHONPATH=/home/ts/code/projects/openhcs-shared-runtime-plumbing \ + OPENBLAS_NUM_THREADS=1 OMP_NUM_THREADS=1 MKL_NUM_THREADS=1 \ + NUMBA_NUM_THREADS=1 CUDA_VISIBLE_DEVICES='' taskset -c 3 \ + /home/ts/code/projects/openhcs/.venv/bin/python -m pytest -q \ + -p no:cacheprovider tests/unit/agent/test_cold_inspection_feedback.py \ + tests/unit/agent/test_agent_services.py \ + tests/unit/test_cellprofiler_geometry_prepared_payload_abi.py + +Pass log: +``/var/tmp/openhcs-pr394-main-b680-repaired-integration-controls-20261002.log``. From 789e3c3043685ef77bb7cc087dec202f3e565ce1 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 09:53:42 -0400 Subject: [PATCH 067/235] test(mcp): scope resource fixtures and activate checkout imports (#457) --- .../test_mcp_process_resource_lifetime.py | 111 +++++++++--------- 1 file changed, 56 insertions(+), 55 deletions(-) diff --git a/tests/unit/agent/test_mcp_process_resource_lifetime.py b/tests/unit/agent/test_mcp_process_resource_lifetime.py index d64c219894..f497a95889 100644 --- a/tests/unit/agent/test_mcp_process_resource_lifetime.py +++ b/tests/unit/agent/test_mcp_process_resource_lifetime.py @@ -18,62 +18,63 @@ @pytest.mark.parametrize("failure", (None, ValueError("original resource failure"))) def test_original_resource_declaration_and_cooperative_close(monkeypatch, failure): - calls = [] - - class Gateway: - def dispose(self): - assert threading.current_thread() is threading.main_thread() - calls.append("gateway disposed") - - class Java: - def shutdown_jvm(self): + with monkeypatch.context() as resource_patch: + calls = [] + + class Gateway: + def dispose(self): + assert threading.current_thread() is threading.main_thread() + calls.append("gateway disposed") + + class Java: + def shutdown_jvm(self): + assert threading.current_thread() is threading.main_thread() + calls.append("JVM stopped") + + context = BioFormatsJavaContext(imagej_module=None, scyjava_module=Java()) + context.ij = Gateway() + resource_patch.setattr(BioFormatsJavaContext, "_instance", context) + resource_patch.setattr(backend_registry, "_cleanup_callbacks", []) + backend_registry.register_cleanup_callback(BioFormatsJavaContext.shutdown_instance) + + class AuditCloseCapability: + def close(self): + calls.append("cooperative enter") + super().close() + calls.append("cooperative exit") + + class AuditedExecutor(AuditCloseCapability, McpTransportExecutor): + pass + + executor = AuditedExecutor() + + def independent_resource(): + assert executor._closed + assert executor.active_futures() == () assert threading.current_thread() is threading.main_thread() - calls.append("JVM stopped") - - context = BioFormatsJavaContext(imagej_module=None, scyjava_module=Java()) - context.ij = Gateway() - monkeypatch.setattr(BioFormatsJavaContext, "_instance", context) - monkeypatch.setattr(backend_registry, "_cleanup_callbacks", []) - backend_registry.register_cleanup_callback(BioFormatsJavaContext.shutdown_instance) - - class AuditCloseCapability: - def close(self): - calls.append("cooperative enter") - super().close() - calls.append("cooperative exit") - - class AuditedExecutor(AuditCloseCapability, McpTransportExecutor): - pass - - executor = AuditedExecutor() - - def independent_resource(): - assert executor._closed - assert executor.active_futures() == () - assert threading.current_thread() is threading.main_thread() - calls.append("independent resource") - if failure is not None: - raise failure - - # A new resource needs only its original declaration, not an MCP edit. - backend_registry.register_cleanup_callback(independent_resource) - backend_registry.register_cleanup_callback(independent_resource) - if failure is None: - executor.close() - else: - with pytest.raises(ExceptionGroup) as caught: + calls.append("independent resource") + if failure is not None: + raise failure + + # A new resource needs only its original declaration, not an MCP edit. + backend_registry.register_cleanup_callback(independent_resource) + backend_registry.register_cleanup_callback(independent_resource) + if failure is None: executor.close() - assert caught.value.exceptions == (failure,) - assert BioFormatsJavaContext._instance is None - assert context.ij is None - assert calls[:4] == [ - "cooperative enter", "gateway disposed", "JVM stopped", "independent resource" - ] - assert calls.count("cooperative exit") == int(failure is None) - executor.close() - assert calls.count("gateway disposed") == 1 - assert calls.count("JVM stopped") == 1 - assert calls.count("independent resource") == 1 + else: + with pytest.raises(ExceptionGroup) as caught: + executor.close() + assert caught.value.exceptions == (failure,) + assert BioFormatsJavaContext._instance is None + assert context.ij is None + assert calls[:4] == [ + "cooperative enter", "gateway disposed", "JVM stopped", "independent resource" + ] + assert calls.count("cooperative exit") == int(failure is None) + executor.close() + assert calls.count("gateway disposed") == 1 + assert calls.count("JVM stopped") == 1 + assert calls.count("independent resource") == 1 def test_off_main_close_rejected_before_resource_release(monkeypatch): @@ -165,9 +166,9 @@ def test_real_sdk_stdio_declared_resource_closes_before_child_exit(tmp_path, fai script.write_text( f"import sys\nsys.path.insert(0, {str(source_root)!r})\n" "from pathlib import Path\nimport threading,inspect\n" + "from openhcs.mcp.stdio import McpStdioTransport\n" "from mcp.server.fastmcp import FastMCP\n" "from polystore import register_cleanup_callback\n" - "from openhcs.mcp.stdio import McpStdioTransport\n" f"assert Path(inspect.getfile(McpStdioTransport)).resolve().is_relative_to(Path({str(source_root)!r}))\n" "server = FastMCP('declared-resource-lifetime')\n" "@server.tool()\ndef health() -> dict[str, bool]:\n return {'ok': True}\n" From 5770763371820a74ce0b29dadc4fa16a7e98565a Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 09:58:34 -0400 Subject: [PATCH 068/235] docs: retain rejected leaf routes and whole-worker evidence (#384) --- .../shared_runtime_plumbing_384_20261001.rst | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/docs/validation/shared_runtime_plumbing_384_20261001.rst b/docs/validation/shared_runtime_plumbing_384_20261001.rst index 3c6c7ed82e..9e9509716f 100644 --- a/docs/validation/shared_runtime_plumbing_384_20261001.rst +++ b/docs/validation/shared_runtime_plumbing_384_20261001.rst @@ -1224,3 +1224,71 @@ Both initial failed runs remain retained. Integrated command, CPU3: Pass log: ``/var/tmp/openhcs-pr394-main-b680-repaired-integration-controls-20261002.log``. + +Saved-leaf rejection and whole-worker diagnostic, 2026-10-02 +---------------------------------------------------------- + +A second source-b764 diagnostic captures the actual contextualization, +normalization scopes, identity-cache preimages, serializer inputs and five +locked publication preimages. All39 admitted snapshots retain their original +metadata, source correlations and alias relations. Exact six CSV/120 TIFF +comparison against the same-head ordinary run and retained historical reference, +all180 physical input references and source/dependency/native freezes pass. +Injected capture work is separately timed; these are not accepted pipeline clocks. +Receipt: +``/var/tmp/openhcs-narrow450-joint-leaf-v3-science-and-freeze-20261002.json``. + +An exploratory metadata projection fusion passes the11 captured valid-input +leaf comparisons but saves only4.82ms across the five actual context calls; +normalization has no measured gain. Separately, malformed self spatial-domain +metadata combined with malformed source fallback spacing changes the first +exception. The original reports the spatial-domain failure; the candidate +reports invalid spacing. This is a real correctness failure, independent of a +historical test that observes normalization phase counts. The candidate is +rejected for both inadequate payoff and error-order drift, and is not included +in this branch. Original/candidate replay receipts: +``/var/tmp/openhcs-narrow450-joint-leaf-v3-original-replay-20261002.json`` and +``/var/tmp/openhcs-narrow450-joint-leaf-v3-fusion-replay-20261002.json``. +Malformed-input witness: +``/var/tmp/repro_narrow450_fusion_domain_spacing_order_20261002.py``. + +All five original locked publication transactions and both serializer calls +replay with exact ordered JSON results and argument after-state. Original replay +transaction medians sum to0.4646s; the observed callback sum is0.3760s and +lock/read/JSON-write residual0.0872s. Replacing three final transactions with one +would remove only about0.04s of this replay's I/O baseline. Standalone batching +is rejected as insufficient. These are saved-input replay clocks, not an +end-to-end speedup; original on-disk whitespace was not captured. Receipt: +``/var/tmp/openhcs-original-publication-v3-replay-20261002/receipt.json``. + +One further actual public-driver diagnostic uses the existing thread-owned +worker profiling policy, with no source edits or diagnostic monkeypatches. +The worker records26,443,742 calls, including5,280 slice-context projections, +25,304 source-metadata merges,55,173 source identity constructions and248 NumPy +stack calls. The next investigation follows repeated whole-image/plane +derivations across loading, invocation, saving and publication. It does not +assume every cache hit copies or every saved plane has independent pixels: +the observed SaveImages named values, VFS planes and cache share pixels, while +the observed Resize output has independent buffers. Profiling increases the +execution clock substantially; profile times must not be scaled into ordinary +costs or reported as benchmark speedups. Exact six CSV/120 TIFF and frozen source, +dependency, native and physical-input comparisons pass. Receipts: +``/var/tmp/openhcs-narrow450-existing-worker-profile-20261002/observations.json`` +and +``/var/tmp/openhcs-narrow450-existing-worker-profile-science-and-freeze-20261002.json``. + +Main17c630606 is normally merged ata6abc4418. Its new lifetime controls initially +produce140 passes, two SDK failures and one teardown error in the integrated +repository suite. The generated child imports PolyStore before activating the +selected OpenHCS checkout; the intentionally failing synthetic callback also +outlives its test into autouse cleanup. Test-only789e3c304 activates OpenHCS first +and restores the callback owner in a lexical monkeypatch context. Original +failure identity, cooperative close, main-thread checks and repeated-close +assertions remain unchanged. All142 lifetime/cold-feedback/agent-service/prepared +geometry controls then pass in25.24s. This is tracked separately by issue457; +closed issue455's installed production shutdown acceptance is not reopened. +Original RED and repaired logs: +``/var/tmp/openhcs-pr394-main-17c630606-integration-controls-20261002.log`` and +``/var/tmp/openhcs-pr394-main17-lifetime-fixture-repair-controls-20261002.log``. +AST/owner receipt: +``/var/tmp/openhcs-pr394-main17-lifetime-fixture-repair-20261002.json``. From 349c04d7484d1e89e872c3f757b428824ff636d0 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 10:12:55 -0400 Subject: [PATCH 069/235] docs: link merged fixture repair and canonical owner census (#384) --- .../shared_runtime_plumbing_384_20261001.rst | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/validation/shared_runtime_plumbing_384_20261001.rst b/docs/validation/shared_runtime_plumbing_384_20261001.rst index 9e9509716f..c744eac2fc 100644 --- a/docs/validation/shared_runtime_plumbing_384_20261001.rst +++ b/docs/validation/shared_runtime_plumbing_384_20261001.rst @@ -1292,3 +1292,21 @@ Original RED and repaired logs: ``/var/tmp/openhcs-pr394-main17-lifetime-fixture-repair-controls-20261002.log``. AST/owner receipt: ``/var/tmp/openhcs-pr394-main17-lifetime-fixture-repair-20261002.json``. + +The test-only repair ships independently in merged PR460, commitb495dbb13; +issue457 is formally linked and closed. Its exact clean-main branch passes21 +lifetime/stdio/dev-client controls through normal repository conftest collection +in13.21s. Main is normally merged back into this branch at01875a772; the merge +introduces no additional production diff. Clean-main qualification receipt: +``/var/tmp/openhcs-mcp-lifetime-fixture-457-main-qualification-20261002.json``. + +The next ownership census uses NRA's ModuleSyntaxIndex and canonical +CompactClassFamilyIndex on all703 OpenHCS production modules and the actual +63 PolyStore,12 python-introspect and6 metaclass-registry modules. All5506 +original class declarations remain represented;5493 join canonical family +declarations and13 remain unprojected OPEN. There are no parse errors. +Conditional/function-local binding, omitted numerical dependencies and native +runtime effects remain explicit proof limits. This is source discovery, not +an admitted architecture or equivalence proof. NRA revision0844525ec is pinned. +Receipt: +``/var/tmp/openhcs-b764-shared-derivation-nra-class-census-20261002.json``. From 51533665ead8ab415db19f13c41215cc3bda9c5c Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 10:50:56 -0400 Subject: [PATCH 070/235] docs: record reproduced public publication failure for #435 and current gates --- .../shared_runtime_plumbing_384_20261001.rst | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/docs/validation/shared_runtime_plumbing_384_20261001.rst b/docs/validation/shared_runtime_plumbing_384_20261001.rst index c744eac2fc..9b2f801d5d 100644 --- a/docs/validation/shared_runtime_plumbing_384_20261001.rst +++ b/docs/validation/shared_runtime_plumbing_384_20261001.rst @@ -1310,3 +1310,59 @@ runtime effects remain explicit proof limits. This is source discovery, not an admitted architecture or equivalence proof. NRA revision0844525ec is pinned. Receipt: ``/var/tmp/openhcs-b764-shared-derivation-nra-class-census-20261002.json``. + +Current-main integration and acceptance findings, 2026-10-02 +----------------------------------------------------------- + +Main cc9fcdfd4 is normally merged at 133da74ec. The affected native +presentation/MCP, agent-service, resource-lifetime and prepared-geometry suite +passes 150 cases in 17.75s. A separate current-source owner gate passes 73 +metadata-owner, live-mutation, provenance-constructor and resolution-snapshot +cases in 1.34s. These scoped gates do not erase the retained broader offscreen +window failure or qualify a new benchmark source. Logs: +``/var/tmp/openhcs-pr394-main-cc9fcdfd4-integration-controls-20261002.log`` and +``/var/tmp/openhcs-pr394-current-maincc9-source-owner-controls-20261002.log``. + +The original, unchanged whole-branch R0 is rerun against cc9fcdfd4 and +133da74ec with tool revision 3b03785f4. It exits 1. Its only positive measures +remain source_metadata's one TypeSwitch and three TypeSwitchArms; the removed +virtual_workspace_metadata classifier contributes minus one switch and minus +four arms. The ownership decision retains scalar admission on +SourceMetadataFields and durable rejection on DurableSourceMetadata: the +grammar belongs to the field owner, while canonical runtime spelling and +literal durable spelling remain substitutable policies. Reintroducing the +decoder's duplicate validator or moving the grammar to an unrelated utility +would contradict that ownership. This adjudicates the relocation's architecture; +it does not declare the per-file ratchet passed or alter its implementation, +budget, exclusions or observations. Primitive/subclass admission, reported-class +read ordering, scalar/container precedence, exact errors and mutable lifetime +remain separate behavior obligations covered by the owner controls. Raw report: +``/var/tmp/openhcs-pr394-current-maincc9-original-r0-20261002.log``. + +The metadata-only whole-query prototype is rejected immediately after saved +replay: its five captured context calls still take about 55ms, with no matched +speedup demonstrated. Valid saved leaf gates pass, but constructor/subclass, +malformed-input and live-mutation obligations remain unresolved. Neither its +callback-based query abstraction nor its manually replayed constructor effects +are promoted. A separate source trace falsifies wholesale stack-rebuild removal: +23 of 32 loads already hit the whole-stack cache and memory saves retain typed +payload references. The entire load/unstack/save/identity envelope is only +1.3674s before mandatory work, insufficient to close the 1.3381s execution gap. +The next route must span repeated source derivation across existing request, +recording, stack and publication owners. Receipts: +``/var/tmp/openhcs-whole-source-query-exploratory-REJECTED-20261002.json`` and +``/var/tmp/openhcs-b764-whole-cohort-cycle-dominant-route-review-20261002.json``. + +Issue 435's fresh public MCP synthetic acceptance is RED on frozen 133da74ec. +The original five processing steps are unchanged; only owned output paths and +the unused required DAPI binding differ. The first attempt stops before runtime +creation because native TCP lock paths do not follow XDG storage. A fresh, +separately retained attempt admits only its declared private data/control lock +paths, completes mandatory preparation and compilation, then fails the fifth +step at the unchanged duplicate source-projection guard. Its exact owned runtime +is closed through the public API and all SDK children are terminal. This is a +real workflow failure that the existing same-occurrence unit fixture did not +cover; installed acceptance remains open. No original biological replay or +performance claim follows from this synthetic check. Source/input/dependency, +native, request/reply and failure evidence: +``/var/tmp/openhcs-derived-role-435-synthetic-acceptance-v2-20261002``. From 0b6c665603cee6f9be89accf293b6adeb70cc189 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 11:22:05 -0400 Subject: [PATCH 071/235] fix: retain distinct declared image roles during automatic materialization Automatic source-identity image retention omitted the declared output qualifier, so two roles overwrote one unqualified file and failed strict publication. Share the existing filename identity projection across scalar and projected image outputs while preserving explicit export contracts. Refs #435. --- openhcs/core/artifacts.py | 24 ++ .../function_artifact_materialization.py | 3 + openhcs/processing/materialization/core.py | 66 ++++-- .../processing/materialization/persistence.py | 12 + ...utomatic_image_role_materialization_435.py | 216 ++++++++++++++++++ 5 files changed, 297 insertions(+), 24 deletions(-) create mode 100644 tests/unit/test_automatic_image_role_materialization_435.py diff --git a/openhcs/core/artifacts.py b/openhcs/core/artifacts.py index 7078339c22..394adf3046 100644 --- a/openhcs/core/artifacts.py +++ b/openhcs/core/artifacts.py @@ -23,6 +23,7 @@ from openhcs.core.component_set import ComponentSet if TYPE_CHECKING: + from openhcs.core.steps.function_output_identity import FunctionOutputIdentity from openhcs.core.artifact_key_selection import AdapterRecordedArtifactOutputPolicy from openhcs.core.runtime_artifact_values import ( RuntimeValue, @@ -212,6 +213,11 @@ def materialization_base_path( del source_filename, uses_source_identity_filename, image_output_dir return Path(analysis_output_dir) / descriptor_filename + @classmethod + def retained_filename_qualifier(cls, artifact_name: str) -> str | None: + """Retain families whose existing paths already own artifact identity.""" + return None + @classmethod def normalize_runtime_payload( cls, @@ -382,6 +388,11 @@ class ImageArtifactType(ArtifactType): participates_in_main_flow_output = True carries_source_image_context = True + @classmethod + def retained_filename_qualifier(cls, artifact_name: str) -> str: + """Distinguish named retained images sharing one physical source plane.""" + return artifact_name + @classmethod def projected_materialization_base_path( cls, @@ -1051,6 +1062,19 @@ def participates_in_persistent_materialization(self) -> bool: def uses_source_identity_filename(self) -> bool: """Return whether this materialization names files by source identity.""" + @abstractmethod + def filename_qualifier(self, output_plan: ArtifactOutputPlan | None) -> str | None: + """Derive the output role required by this materialization purpose.""" + + def filename_identity_for_output( + self, + identity: FunctionOutputIdentity, + output_plan: ArtifactOutputPlan | None, + ) -> FunctionOutputIdentity: + """Apply a declared role through the shared filename identity algorithm.""" + qualifier = self.filename_qualifier(output_plan) + return identity if qualifier is None else identity.with_filename_qualifier(qualifier) + def _coerce_artifact_plan_type( plan_type: type["ArtifactPlan"], diff --git a/openhcs/core/steps/function_artifact_materialization.py b/openhcs/core/steps/function_artifact_materialization.py index 35effbb957..e91198c959 100644 --- a/openhcs/core/steps/function_artifact_materialization.py +++ b/openhcs/core/steps/function_artifact_materialization.py @@ -821,6 +821,9 @@ def record_source_descriptor( variable_components=plan.variable_components, ) if identity is not None: + identity = materialization_spec.filename_identity_for_output( + identity, output_plan, + ) try: filename = Path( FunctionOutputPathAuthority.filename_for_identity( diff --git a/openhcs/processing/materialization/core.py b/openhcs/processing/materialization/core.py index 61340a6503..c881284823 100644 --- a/openhcs/processing/materialization/core.py +++ b/openhcs/processing/materialization/core.py @@ -79,6 +79,11 @@ StreamImagePayloadMetadataProjector, StreamViewerComponentMetadataProjector, ) +from openhcs.core.steps.function_output_identity import ( + FunctionOutputIdentityAuthority, + FunctionOutputParserContext, + FunctionOutputPathAuthority, +) from openhcs.processing.materialization.constants import ( MaterializationFormat, WriteMode, @@ -2508,32 +2513,41 @@ def write_image_file( options.filename_identity is MaterializedFilenameIdentity.ARTIFACT_NAME or paths.source_identity_base_path(options) is not None ) - outputs = tuple( - ( - ( - _image_relative_output_path( - item.value, - options, - context, - sequence_index=sequence_index, + outputs = [] + for sequence_index, item in enumerate(projected_items, start=1): + if options.relative_path_template is not None: + path = _image_relative_output_path( + item.value, options, context, sequence_index=sequence_index, + ) + elif preserves_planned_path: + path = paths.primary_output_path(options) + else: + output_plan = context.output_plan + qualifier = ( + output_plan.materialization.filename_qualifier(output_plan) + if output_plan is not None and output_plan.materialization is not None + else None + ) + if qualifier is None: + filename = ( + source_stem_authority.required_source_stem(item.metadata) + + options.primary_output_suffix ) - if options.relative_path_template is not None - else ( - paths.primary_output_path(options) - if preserves_planned_path - else str( - paths.parent - / ( - source_stem_authority.required_source_stem(item.metadata) - + options.primary_output_suffix - ) - ) + else: + parser = FunctionOutputParserContext.from_processing_context( + context.context, + ).parser + identity = FunctionOutputIdentityAuthority.filename_identity_from_metadata( + parser, item.metadata, ) - ), - item, - ) - for sequence_index, item in enumerate(projected_items, start=1) - ) + if identity is None: + raise ValueError("Retained image output has no addressable source filename identity.") + identity = output_plan.materialization.filename_identity_for_output( + replace(identity, extension=options.primary_output_suffix), output_plan, + ) + filename = FunctionOutputPathAuthority.filename_for_identity(parser, identity) + path = str(paths.parent / filename) + outputs.append((path, item)) output_paths = tuple(path for path, _item in outputs) if len(set(output_paths)) != len(output_paths): raise ValueError( @@ -4007,6 +4021,10 @@ def participates_in_runtime_export_observation(self) -> bool: """Explicit materialization specs are externally observed exports.""" return True + def filename_qualifier(self, output_plan: ArtifactOutputPlan | None) -> str | None: + """Preserve the source filenames authored by an explicit export.""" + return None + def participates_in_persistent_materialization(self) -> bool: """Explicit materialization specs write to configured persistent targets.""" return True diff --git a/openhcs/processing/materialization/persistence.py b/openhcs/processing/materialization/persistence.py index 7d976d1d09..4fd675fc20 100644 --- a/openhcs/processing/materialization/persistence.py +++ b/openhcs/processing/materialization/persistence.py @@ -1,5 +1,11 @@ """Shared persistence and export-observation policies for materialized artifacts.""" +from __future__ import annotations +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from openhcs.core.artifacts import ArtifactOutputPlan + from .core import MaterializationSpec @@ -9,6 +15,12 @@ class TerminalMaterializationSpec(MaterializationSpec): def participates_in_runtime_export_observation(self) -> bool: return False + def filename_qualifier(self, output_plan: ArtifactOutputPlan | None) -> str | None: + """Retain the compiled image role without changing its source address.""" + if output_plan is None: + return None + return output_plan.artifact_type.retained_filename_qualifier(output_plan.name) + class StreamingOnlyMaterializationSpec(MaterializationSpec): """Prepare viewer outputs without declaring persistent or external exports.""" diff --git a/tests/unit/test_automatic_image_role_materialization_435.py b/tests/unit/test_automatic_image_role_materialization_435.py new file mode 100644 index 0000000000..f89a1a7eac --- /dev/null +++ b/tests/unit/test_automatic_image_role_materialization_435.py @@ -0,0 +1,216 @@ +"""Default retained image artifacts keep their declared output role identity.""" + +from dataclasses import replace +from pathlib import Path +import json + +import pytest +import numpy as np +import tifffile +from polystore.disk import DiskStorageBackend +from polystore.filemanager import FileManager +from polystore.memory import MemoryStorageBackend + +from test_function_outputs import context_stub, function_step_plan, record_output_path +from openhcs.constants.constants import Backend, Microscope, VariableComponents +from openhcs.core.aligned_image_payload import AlignedImageSliceContext, ImagePayloadBundleContext +from openhcs.core.artifacts import ArtifactOutputPlan, ArtifactSpec, ImageArtifactType +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_stores import RuntimeValueStore +from openhcs.core.source_metadata import SourceVoxelSpacing +from openhcs.core.source_workspace_projection import VirtualWorkspaceSourceProjection +from openhcs.core.steps.function_output_identity import ( + FunctionOutputIdentityAuthority, FunctionOutputPathAuthority, +) +from openhcs.core.steps.function_outputs import ( + PrimaryImageMetadataTarget, RuntimeArtifactMaterializationAuthority, +) +from openhcs.microscopes.source_schema import SourceSchemaFilenameParser + + +@pytest.mark.parametrize("plane_count", (1, 2)) +def test_automatic_two_role_images_publish_their_actual_saved_occurrences(tmp_path, plane_count): + filemanager = FileManager({ + Backend.DISK.value: DiskStorageBackend(), + Backend.MEMORY.value: MemoryStorageBackend(), + }) + context = context_stub(filemanager, parser=SourceSchemaFilenameParser()) + context.microscope_handler.microscope_type = Microscope.OPENHCS.value + context.runtime_value_store = RuntimeValueStore() + context.metadata_cache = {} + context.tiff_config = None + plan = function_step_plan( + "Declared raw and capped roles", + variable_components=((VariableComponents.Z_INDEX,) if plane_count > 1 else ()), + ) + plan.streaming_configs = {} + plan.write_backend = Backend.DISK.value + plan.output_plate_root = str(tmp_path) + plan.output_dir = tmp_path / "images" + plan.sub_dir = "images" + plan.analysis_results_dir = str(plan.output_dir) + plan.runtime_artifact_materialization = RuntimeArtifactMaterializationPlan( + persistent_enabled=True, persistent_backend=Backend.DISK.value, + ) + context.step_plans = {plan.step_index: plan} + plans = tuple( + ArtifactOutputPlan( + name=name, path=f"/memory/{name}.pkl", artifact_type=ImageArtifactType, + producer_step_index=plan.step_index, + producer_step_scope_id=plan.step_scope_id, + producer_step_name=plan.step_name, + materialization=ArtifactOutputMaterializationPlanner.materialization_for( + ArtifactSpec(name=name, artifact_type=ImageArtifactType, plan_type=ArtifactOutputPlan), (), + ), + ) + for name in ("raw_body_processing_role", "capped_outgrowth_processing_role") + ) + plan.artifact_outputs = {output.ref(): output for output in plans} + contexts = AlignedImageSliceContext.main_flow_for_output_plans(plans) + expected = {} + for output, output_context, factor in zip(plans, contexts, (1.0, 0.5), strict=True): + payloads = tuple( + ImageMetadataPayload( + (np.arange(20, dtype=np.float32).reshape(4, 5) + index) * factor, + ImagePayloadMetadata( + source_component_metadata={ + "well": "A01", "site": "1", "channel": "2", "z_index": str(index), + "timepoint": "1", "extension": ".tif", + }, + source_image_names=(output.name,), + source_voxel_spacing=SourceVoxelSpacing((1.3556, 1.3556)), + ), + ) + for index in range(1, plane_count + 1) + ) + value = ( + payloads[0] if plane_count == 1 else ImagePayloadBundleContext.from_payloads( + payloads, metadata_mode=ImagePayloadMetadataCompositionMode.STACK, + ).compose() + ) + context.runtime_value_store.record( + RuntimeValue.normalize(output, value, axis_id="A01"), + path=output.path, backend=Backend.MEMORY.value, + ) + expected[output.name] = [] + for payload in payloads: + identity = FunctionOutputIdentityAuthority.filename_identity_from_metadata( + context.microscope_handler.parser, payload.metadata, + ).with_filename_qualifier(output_context.output_key) + destination = plan.output_dir / FunctionOutputPathAuthority.filename_for_identity( + context.microscope_handler.parser, identity, + ) + filemanager.ensure_directory(plan.output_dir, Backend.MEMORY.value) + filemanager.ensure_directory(plan.output_dir, Backend.DISK.value) + filemanager.save(payload, str(destination), Backend.MEMORY.value) + filemanager.save(payload.data, str(destination), Backend.DISK.value) + record_output_path( + context, plan, str(destination), output_context=output_context, + image_metadata=payload.metadata, identity=identity, + ) + expected[output.name].append((destination, payload.data)) + saved = RuntimeArtifactMaterializationAuthority.materialize(context, plan) + target = replace(PrimaryImageMetadataTarget.from_plan(plan), artifact_materializations=saved) + # The original implementation raises its real duplicate projection guard + # here: both automatic materializations used the same unqualified path. + structured = target.produced_projection_metadata(context, plan) + assert {item["source_alias"] for item in structured["source_projection"]} == set(expected) + assert len(structured["source_projection"]) == 2 * plane_count + for materialized in saved: + outputs = materialized.outputs_for_backend(Backend.DISK.value) + assert len(outputs) == plane_count + for actual, (path, pixels) in zip(outputs, expected[materialized.materialization.output_plan.name], strict=True): + assert actual.path == str(path) + np.testing.assert_array_equal(tifffile.imread(path), pixels) + target.write(context, produced_plan=plan) + document = json.loads((tmp_path / "openhcs_metadata.json").read_text()) + reopened = VirtualWorkspaceSourceProjection.from_openhcs_metadata(tmp_path, document) + for name, expected_planes in expected.items(): + for path, pixels in expected_planes: + projection = reopened.source_projections_by_virtual_path[str(path.relative_to(tmp_path))] + assert projection.source_alias == name + assert projection.address.as_component_metadata()["channel"] == "2" + assert projection.image_metadata.source_voxel_spacing == SourceVoxelSpacing((1.3556, 1.3556)) + np.testing.assert_array_equal(tifffile.imread(tmp_path / projection.ref.backend_address), pixels) + + +@pytest.mark.parametrize("purpose", ( + "explicit_source", "explicit_template", "terminal_template", + "explicit_artifact", "terminal_artifact", "unnamed_terminal", +)) +def test_retained_role_policy_preserves_authored_and_unnamed_image_paths(tmp_path, purpose): + from openhcs.processing.materialization import ( + ImageFileOptions, MaterializationSpec, MaterializedFilenameIdentity, + TerminalMaterializationSpec, materialization_outputs, + ) + + options = ImageFileOptions( + filename_suffix=".tif", + relative_path_template=("authored/plane_{index}.tif" if purpose.endswith("template") else None), + filename_identity=( + MaterializedFilenameIdentity.ARTIFACT_NAME if purpose.endswith("artifact") + else MaterializedFilenameIdentity.SOURCE_IDENTITY + ), + ) + spec = ( + TerminalMaterializationSpec(options) if purpose.startswith("terminal") or purpose == "unnamed_terminal" + else MaterializationSpec(options) + ) + output_plan = None if purpose == "unnamed_terminal" else ArtifactOutputPlan( + name="DeclaredRole", path="/memory/DeclaredRole.pkl", + artifact_type=ImageArtifactType, materialization=spec, + ) + payloads = tuple( + ImageMetadataPayload(np.ones((4, 5), dtype=np.float32) * index, ImagePayloadMetadata( + 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() + filemanager = FileManager({Backend.MEMORY.value: MemoryStorageBackend()}) + context = context_stub(filemanager, parser=SourceSchemaFilenameParser()) + outputs = materialization_outputs( + spec, value, str(tmp_path / "AuthoredBase"), filemanager, + context=context, output_plan=output_plan, + variable_components=(VariableComponents.Z_INDEX,), + ) + expected_paths = ( + ("authored/plane_1.tif", "authored/plane_2.tif") if purpose.endswith("template") + else ("AuthoredBase.tif",) if purpose.endswith("artifact") + else ("A01_s001_w2_z001_t001.tif", "A01_s001_w2_z002_t001.tif") + ) + assert tuple(Path(output.path).relative_to(tmp_path).as_posix() for output in outputs) == expected_paths + if len(outputs) == 2: + for actual, expected in zip(outputs, payloads, strict=True): + np.testing.assert_array_equal(actual.content, expected.data) + else: + np.testing.assert_array_equal(outputs[0].content, value.data) + + +def test_authored_duplicate_template_still_fails_before_any_save(tmp_path): + from openhcs.processing.materialization import ImageFileOptions, TerminalMaterializationSpec, materialization_outputs + + spec = TerminalMaterializationSpec(ImageFileOptions( + filename_suffix=".tif", relative_path_template="same.tif", + )) + output_plan = ArtifactOutputPlan(name="NamedRole", path="/memory/role.pkl", artifact_type=ImageArtifactType, materialization=spec) + payloads = tuple( + ImageMetadataPayload(np.ones((4, 5), dtype=np.float32), ImagePayloadMetadata( + 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() + manager = FileManager({Backend.MEMORY.value: MemoryStorageBackend()}) + with pytest.raises(ValueError, match="produced duplicate paths"): + materialization_outputs(spec, value, str(tmp_path / "role"), manager, + context=context_stub(manager, parser=SourceSchemaFilenameParser()), + output_plan=output_plan) + assert not (tmp_path / "same.tif").exists() From 659aca3f3c5bf0645300bf2da50e4df1bb680ac3 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 11:27:19 -0400 Subject: [PATCH 072/235] docs: record latest main controls and physical collision cause for #435 --- .../shared_runtime_plumbing_384_20261001.rst | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/docs/validation/shared_runtime_plumbing_384_20261001.rst b/docs/validation/shared_runtime_plumbing_384_20261001.rst index 9b2f801d5d..8912f5518a 100644 --- a/docs/validation/shared_runtime_plumbing_384_20261001.rst +++ b/docs/validation/shared_runtime_plumbing_384_20261001.rst @@ -1366,3 +1366,43 @@ cover; installed acceptance remains open. No original biological replay or performance claim follows from this synthetic check. Source/input/dependency, native, request/reply and failure evidence: ``/var/tmp/openhcs-derived-role-435-synthetic-acceptance-v2-20261002``. + +Current main integration and publication diagnosis, 2026-10-02 +----------------------------------------------------------- + +Main 283119c42 is normally merged after the declared independent-plane NLM +implementation and its documentation land. The combined affected NLM, +cold-feedback, agent-service, prepared geometry, native presentation and MCP +resource-lifetime controls pass 168 cases in 25.84s. Log: +``/var/tmp/openhcs-pr394-main-283119c42-integration-controls-20261002.log``. +The qualified benchmark sources and clocks above remain unchanged. + +The separately retained issue 435 public diagnostic reproduces the original +failure. Four actual projections show two correctly qualified main-flow images +but both retained artifacts address the same unqualified TIFF. The final raw +artifact overwrites the capped artifact. The same-occurrence ownership comparator +is never called: destination matching fails before that comparison. The existing +duplicate guard therefore detects a physical filename collision, not a guard +that should be relaxed. Both observer hooks call their original implementation +once; no capture errors are reported. The exact owned runtime is closed through +the public API and all SDK children are terminal. Receipt: +``/var/tmp/openhcs-derived-role-435-collision-facts-v3-20261002.json``. +An isolated repair on existing materialization-purpose and filename owners is +under review; its local test results do not yet establish public acceptance. + +The owned source-field composition prototype is also rejected: the eleven +captured valid leaves and additional mutation/constructor controls agree, but +the five context, three normalization and three identity calls save only 9.24ms +warm or 5.96ms cold. No candidate pipeline qualification or PR is opened for it. +Receipt: +``/var/tmp/openhcs-owned-source-composition-exploratory-REJECTED-20261002.json``. + +SaveImages reconstructs two source stacks solely for materialization metadata, +whose downstream consumer reads provenance. Its earlier 0.346s envelope includes +four contextualization children already measured elsewhere. After separating +those children in the retained diagnostics, the residual envelope is only about +0.10--0.114s. This is rejected as a standalone performance route; it cannot be +counted as an independent 0.346s saving. The complete source-query preimage was +not captured, so resulting metadata leaves are not a source-lookup replay. +Read-only audit: +``/var/tmp/openhcs-b764-save-images-artifact-subtree-readonly-audit-20261002.json``. From 2d756646b301382e4142a2de783432263f8ed337 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 11:37:26 -0400 Subject: [PATCH 073/235] fix: bind image filename policy to the actual rendering spec The compiled plan can declare a different materialization purpose from the spec supplied to the public render call. Bind that actual spec on MaterializationContext once and preserve the original plan, source inputs, and writer protocol. Add mismatched-purpose, projected suffix, parser-free, context-reuse, direct-writer and normalized-alias collision controls. Refs #435. --- openhcs/processing/materialization/core.py | 43 ++--- ...utomatic_image_role_materialization_435.py | 168 +++++++++++++++++- 2 files changed, 187 insertions(+), 24 deletions(-) diff --git a/openhcs/processing/materialization/core.py b/openhcs/processing/materialization/core.py index c881284823..21dd73d80c 100644 --- a/openhcs/processing/materialization/core.py +++ b/openhcs/processing/materialization/core.py @@ -81,7 +81,6 @@ ) from openhcs.core.steps.function_output_identity import ( FunctionOutputIdentityAuthority, - FunctionOutputParserContext, FunctionOutputPathAuthority, ) from openhcs.processing.materialization.constants import ( @@ -1734,6 +1733,25 @@ class MaterializationContext: source_paths: tuple[str, ...] = () pipeline_position: int | None = None output_plan: ArtifactOutputPlan | None = None + materialization_spec: MaterializationSpec | None = None + + def named_source_filename( + self, metadata: ImagePayloadMetadata, extension: str, + ) -> str | None: + """Name a retained image from the actual rendering purpose and role.""" + if self.materialization_spec is None: + return None + qualifier = self.materialization_spec.filename_qualifier(self.output_plan) + if qualifier is None: + return None + parser = SourceStemAuthoritySelection.from_processing_context(self.context).required_parser() + identity = FunctionOutputIdentityAuthority.filename_identity_from_metadata(parser, metadata) + if identity is None: + raise ValueError("Retained image output has no addressable source filename identity.") + identity = self.materialization_spec.filename_identity_for_output( + replace(identity, extension=extension), self.output_plan, + ) + return FunctionOutputPathAuthority.filename_for_identity(parser, identity) def paths(self, options: FileOutputOptions) -> PathHelper: return PathHelper(self.base_path, options) @@ -2522,30 +2540,12 @@ def write_image_file( elif preserves_planned_path: path = paths.primary_output_path(options) else: - output_plan = context.output_plan - qualifier = ( - output_plan.materialization.filename_qualifier(output_plan) - if output_plan is not None and output_plan.materialization is not None - else None - ) - if qualifier is None: + filename = context.named_source_filename(item.metadata, options.primary_output_suffix) + if filename is None: filename = ( source_stem_authority.required_source_stem(item.metadata) + options.primary_output_suffix ) - else: - parser = FunctionOutputParserContext.from_processing_context( - context.context, - ).parser - identity = FunctionOutputIdentityAuthority.filename_identity_from_metadata( - parser, item.metadata, - ) - if identity is None: - raise ValueError("Retained image output has no addressable source filename identity.") - identity = output_plan.materialization.filename_identity_for_output( - replace(identity, extension=options.primary_output_suffix), output_plan, - ) - filename = FunctionOutputPathAuthority.filename_for_identity(parser, identity) path = str(paths.parent / filename) outputs.append((path, item)) output_paths = tuple(path for path, _item in outputs) @@ -4186,6 +4186,7 @@ def render( *, output_path_filter: Callable[[Path], bool] | None = None, ) -> MaterializationBatch: + context = replace(context, materialization_spec=spec) groups = tuple( ( writer, diff --git a/tests/unit/test_automatic_image_role_materialization_435.py b/tests/unit/test_automatic_image_role_materialization_435.py index f89a1a7eac..58f6253db9 100644 --- a/tests/unit/test_automatic_image_role_materialization_435.py +++ b/tests/unit/test_automatic_image_role_materialization_435.py @@ -139,7 +139,8 @@ def test_automatic_two_role_images_publish_their_actual_saved_occurrences(tmp_pa @pytest.mark.parametrize("purpose", ( "explicit_source", "explicit_template", "terminal_template", - "explicit_artifact", "terminal_artifact", "unnamed_terminal", + "explicit_artifact", "terminal_artifact", "unnamed_terminal", "unnamed_explicit", + "explicit_source_declared_terminal", "terminal_source_declared_explicit", )) def test_retained_role_policy_preserves_authored_and_unnamed_image_paths(tmp_path, purpose): from openhcs.processing.materialization import ( @@ -159,9 +160,14 @@ def test_retained_role_policy_preserves_authored_and_unnamed_image_paths(tmp_pat TerminalMaterializationSpec(options) if purpose.startswith("terminal") or purpose == "unnamed_terminal" else MaterializationSpec(options) ) - output_plan = None if purpose == "unnamed_terminal" else ArtifactOutputPlan( + declared_spec = ( + TerminalMaterializationSpec(options) if purpose == "explicit_source_declared_terminal" + else MaterializationSpec(options) if purpose == "terminal_source_declared_explicit" + else spec + ) + output_plan = None if purpose in ("unnamed_terminal", "unnamed_explicit") else ArtifactOutputPlan( name="DeclaredRole", path="/memory/DeclaredRole.pkl", - artifact_type=ImageArtifactType, materialization=spec, + artifact_type=ImageArtifactType, materialization=declared_spec, ) payloads = tuple( ImageMetadataPayload(np.ones((4, 5), dtype=np.float32) * index, ImagePayloadMetadata( @@ -185,6 +191,7 @@ def test_retained_role_policy_preserves_authored_and_unnamed_image_paths(tmp_pat expected_paths = ( ("authored/plane_1.tif", "authored/plane_2.tif") if purpose.endswith("template") else ("AuthoredBase.tif",) if purpose.endswith("artifact") + else ("A01_s001_w2_z001_t001_DeclaredRole.tif", "A01_s001_w2_z002_t001_DeclaredRole.tif") if purpose == "terminal_source_declared_explicit" else ("A01_s001_w2_z001_t001.tif", "A01_s001_w2_z002_t001.tif") ) assert tuple(Path(output.path).relative_to(tmp_path).as_posix() for output in outputs) == expected_paths @@ -214,3 +221,158 @@ def test_authored_duplicate_template_still_fails_before_any_save(tmp_path): context=context_stub(manager, parser=SourceSchemaFilenameParser()), output_plan=output_plan) assert not (tmp_path / "same.tif").exists() + + +@pytest.mark.parametrize("suffix", (".tif", ".png", ".ome.tif", ".labels.tif")) +@pytest.mark.parametrize("plane_count", (1, 2)) +def test_retained_image_qualifier_respects_complete_writer_suffix(tmp_path, suffix, plane_count): + from openhcs.core.steps.function_artifact_materialization import RuntimeArtifactMaterialization + from openhcs.processing.materialization import ImageFileOptions, TerminalMaterializationSpec + + manager = FileManager({Backend.MEMORY.value: MemoryStorageBackend()}) + context = context_stub(manager, parser=SourceSchemaFilenameParser()) + plan = function_step_plan("Retained format role", variable_components=(VariableComponents.Z_INDEX,) if plane_count > 1 else ()) + plan.output_dir = tmp_path / "images" + plan.analysis_results_dir = str(plan.output_dir) + output_plan = ArtifactOutputPlan( + name="Role Name", path="/memory/role.pkl", artifact_type=ImageArtifactType, + materialization=TerminalMaterializationSpec(ImageFileOptions(filename_suffix=suffix)), + ) + payloads = tuple( + ImageMetadataPayload(np.ones((4, 5), dtype=np.uint8) * index, ImagePayloadMetadata( + source_component_metadata={"well": "A01", "site": "1", "channel": "2", "z_index": str(index), "timepoint": "1", "extension": ".tif"}, + )) for index in range(1, plane_count + 1) + ) + value = payloads[0] if plane_count == 1 else ImagePayloadBundleContext.from_payloads( + payloads, metadata_mode=ImagePayloadMetadataCompositionMode.STACK, + ).compose() + store = RuntimeValueStore() + record = store.record(RuntimeValue.normalize(output_plan, value, axis_id="A01"), path=output_plan.path, backend=Backend.MEMORY.value) + outputs = RuntimeArtifactMaterialization.from_record( + output_plan=output_plan, record=record, plan=plan, context=context, + ).outputs(plan, context) + assert len(outputs) == plane_count + for index, (output, payload) in enumerate(zip(outputs, payloads, strict=True), 1): + identity = FunctionOutputIdentityAuthority.filename_identity_from_metadata( + context.microscope_handler.parser, payload.metadata, + ).with_filename_qualifier(output_plan.name) + expected = FunctionOutputPathAuthority.filename_for_identity( + context.microscope_handler.parser, replace(identity, extension=suffix), + ) + assert Path(output.path).name == expected + assert Path(output.path).name.endswith("_Role_Name" + suffix) + assert output.metadata.source_component_metadata["channel"] == "2" + assert output.metadata.source_component_metadata["z_index"] == str(index) + np.testing.assert_array_equal(output.content, payload.data) + + +def test_distinct_aliases_that_normalize_to_one_filename_still_conflict(tmp_path): + from openhcs.core.steps.function_artifact_materialization import RuntimeArtifactMaterialization + from openhcs.processing.materialization import ImageFileOptions, TerminalMaterializationSpec + + manager = FileManager({Backend.MEMORY.value: MemoryStorageBackend()}) + context = context_stub(manager, parser=SourceSchemaFilenameParser()) + plan = function_step_plan("Ambiguous role spelling") + plan.output_dir = tmp_path / "images" + plan.analysis_results_dir = str(plan.output_dir) + value = ImageMetadataPayload(np.ones((4, 5), dtype=np.float32), ImagePayloadMetadata( + source_component_metadata={"well": "A01", "site": "1", "channel": "2", "z_index": "1", "timepoint": "1", "extension": ".tif"}, + )) + paths = [] + store = RuntimeValueStore() + for name in ("a/b", "a b"): + output_plan = ArtifactOutputPlan(name=name, path=f"/memory/{name}.pkl", artifact_type=ImageArtifactType, + materialization=TerminalMaterializationSpec(ImageFileOptions(filename_suffix=".tif"))) + record = store.record(RuntimeValue.normalize(output_plan, value, axis_id="A01"), path=output_plan.path, backend=Backend.MEMORY.value) + outputs = RuntimeArtifactMaterialization.from_record(output_plan=output_plan, record=record, plan=plan, context=context).outputs(plan, context) + paths.append(outputs[0].path) + assert paths[0] == paths[1] + # Existing main-flow destination authority rejects this declared naming + # collision instead of choosing a new suffix or inventing a channel. + from openhcs.core.steps.function_runtime import OutputPathBatchUniqueness + with pytest.raises(ValueError, match="duplicate"): + OutputPathBatchUniqueness(output_paths=paths, input_paths=[], step_name=plan.step_name, pattern_repr="declared roles").validate() + + +def test_batch_binds_actual_purpose_once_without_rewriting_plan_or_source(monkeypatch, tmp_path): + from openhcs.processing.materialization import ImageFileOptions, MaterializationSpec, TerminalMaterializationSpec + from openhcs.processing.materialization.constants import MaterializationFormat + from openhcs.processing.materialization.core import MaterializationBatch, MaterializationContext, Output, WriterSpec, _WRITERS_BY_OPTIONS + + options = ImageFileOptions(filename_suffix=".tif") + declared = TerminalMaterializationSpec(options) + explicit = MaterializationSpec(options) + plan = ArtifactOutputPlan(name="Role", path="/memory/role", artifact_type=ImageArtifactType, materialization=declared) + manager = FileManager({Backend.MEMORY.value: MemoryStorageBackend()}) + source = {"physical_source": "original"} + context = MaterializationContext( + base_path=str(tmp_path / "role"), backends=[], backend_kwargs={}, + filemanager=manager, extra_inputs=source, output_plan=plan, + materialization_spec=declared, + ) + seen = [] + def custom_writer(data, actual_options, actual_context): + assert actual_options is options + assert actual_context.output_plan is plan + assert actual_context.extra_inputs is source + seen.append(actual_context) + return [Output(path=str(tmp_path / "custom.tif"), content=data)] + monkeypatch.setitem(_WRITERS_BY_OPTIONS, ImageFileOptions, WriterSpec( + format=MaterializationFormat.IMAGE_FILE, options_type=ImageFileOptions, + write=custom_writer, primary_path=lambda outputs: outputs[0].path, + candidate_paths=lambda _options, base: (base,), + )) + pixels = np.ones((4, 5), dtype=np.float32) + first = MaterializationBatch.render(explicit, pixels, context) + second = MaterializationBatch.render(declared, pixels, first.context) + assert seen == [first.context, second.context] + assert seen[0] is first.context and seen[1] is second.context + assert first.context.materialization_spec is explicit + assert second.context.materialization_spec is declared + assert context.materialization_spec is declared + assert plan.materialization is declared + assert source == {"physical_source": "original"} + + +def test_direct_image_writer_without_purpose_binding_retains_legacy_source_path(tmp_path): + from openhcs.processing.materialization import ImageFileOptions, TerminalMaterializationSpec + from openhcs.processing.materialization.core import MaterializationContext, write_image_file + + options = ImageFileOptions(filename_suffix=".tif") + plan = ArtifactOutputPlan(name="Role", path="/memory/role", artifact_type=ImageArtifactType, + materialization=TerminalMaterializationSpec(options)) + manager = FileManager({Backend.MEMORY.value: MemoryStorageBackend()}) + value = ImageMetadataPayload(np.ones((4, 5), dtype=np.float32), ImagePayloadMetadata( + source_component_metadata={"well": "A01", "site": "1", "channel": "2", "z_index": "1", "timepoint": "1", "extension": ".tif"}, + )) + context = MaterializationContext(base_path=str(tmp_path / "role"), backends=[], backend_kwargs={}, + filemanager=manager, extra_inputs={}, output_plan=plan, + context=context_stub(manager, parser=SourceSchemaFilenameParser())) + outputs = write_image_file(value, options, context) + assert Path(outputs[0].path).name == "A01_s001_w2_z001_t001.tif" + assert context.materialization_spec is None + assert context.output_plan is plan + np.testing.assert_array_equal(outputs[0].content, value.data) + + +@pytest.mark.parametrize("purpose", ("named_terminal", "explicit", "unnamed_terminal")) +def test_parser_free_render_requires_parser_only_for_automatic_named_role(tmp_path, purpose): + from openhcs.processing.materialization import ImageFileOptions, MaterializationSpec, TerminalMaterializationSpec, materialization_outputs + + options = ImageFileOptions(filename_suffix=".tif") + spec = MaterializationSpec(options) if purpose == "explicit" else TerminalMaterializationSpec(options) + plan = None if purpose == "unnamed_terminal" else ArtifactOutputPlan( + name="Role", path="/memory/role", artifact_type=ImageArtifactType, materialization=spec, + ) + manager = FileManager({Backend.MEMORY.value: MemoryStorageBackend()}) + value = ImageMetadataPayload(np.ones((4, 5), dtype=np.float32), ImagePayloadMetadata( + source_path="/physical/OriginalSource.tif", + )) + if purpose == "named_terminal": + with pytest.raises(ValueError, match="Parser-backed source-stem authority requires a parser"): + materialization_outputs(spec, value, str(tmp_path / "Role"), manager, output_plan=plan) + else: + outputs = materialization_outputs(spec, value, str(tmp_path / "Role"), manager, output_plan=plan) + assert Path(outputs[0].path).name == "OriginalSource.tif" + np.testing.assert_array_equal(outputs[0].content, value.data) + assert value.metadata.source_path == "/physical/OriginalSource.tif" From df9466c9154650385aadad587dac790fc9e4b2ce Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 11:48:38 -0400 Subject: [PATCH 074/235] docs: record #435 actual-purpose repair and public execution gates --- .../shared_runtime_plumbing_384_20261001.rst | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/docs/validation/shared_runtime_plumbing_384_20261001.rst b/docs/validation/shared_runtime_plumbing_384_20261001.rst index 8912f5518a..6c9f01662f 100644 --- a/docs/validation/shared_runtime_plumbing_384_20261001.rst +++ b/docs/validation/shared_runtime_plumbing_384_20261001.rst @@ -1406,3 +1406,56 @@ counted as an independent 0.346s saving. The complete source-query preimage was not captured, so resulting metadata leaves are not a source-lookup replay. Read-only audit: ``/var/tmp/openhcs-b764-save-images-artifact-subtree-readonly-audit-20261002.json``. + +Automatic image-role repair and public execution, 2026-10-02 +---------------------------------------------------------- + +Commits 0b6c66560 and 2d756646b repair the physical issue 435 collision on +existing owners. Artifact type and materialization purpose supply the retained +role; shared filename identity supplies parser-backed names. MaterializationBatch +binds the original rendering spec onto its existing context, because a public +caller may render a spec different from the compiled plan's default. The original +output plan and source references remain intact. Explicit exports keep their +authored paths. Both scalar and projected retained images preserve physical +coordinates and complete filename suffixes. Missing parsers use the existing +required-parser error policy. Strict projection and path-conflict guards remain +unchanged. + +The initial candidate's two actual-purpose mismatch controls fail before this +binding repair. The initial scoped R0 also reports a foreign optional-plan probe; +the corrected owner eliminates that probe without changing the guard. Original +scoped R0 and R1 pass for the complete repair versus 51533665e. Both original +failures are retained. Related controls pass 171 cases; after normal main +283119c42 integration at 053c9d445, 183 affected controls pass. An attempted +unrelated Torch NLM control stops at collection because Torch is absent; that +failure remains separate and no dependency changes follow. Handoff: +``/var/tmp/openhcs-automatic-image-role-435-reviewed-handoff-20261002.json``. + +The clean frozen 053c9d445 source completes public MCP startup preparation, +compilation and all five original processing steps. The output contains exactly +the two distinct qualified role TIFFs and two matching source-projection rows. +The fourth journey then stops at its inspection assertion: the API reports +PARTIAL with the sole missing-grid warning, because the synthetic declaration +does not supply plate grid dimensions. This original whole-journey RED is +retained; it is not a pixel-comparison failure or a completed fresh-process +readback acceptance. The runtime and all SDK children are terminal. Receipt: +``/var/tmp/openhcs-derived-role-435-synthetic-acceptance-v4-20261002/journey.json``. +Supplemental public inventory, complete pixel and fresh-process checks remain +pending on these unchanged outputs. + +The reviewed source is normally integrated into PR394 at fa694eb56. Its affected +current NLM, cold-feedback, agent-service, prepared geometry, native presentation, +MCP lifetime and new role controls pass 194 cases in 20.43s. Log: +``/var/tmp/openhcs-pr394-automatic-role-435-current-integration-controls-20261002.log``. +Neither this bug repair nor its public synthetic run establishes a performance +gain. Accepted benchmark clocks remain pinned to main4754 and candidateb764. + +The global runtime reassessment finds no sufficient measured optimization route +yet. Removing 485 provenance births and 485 identity births in captured context +leaves already saved only 4.82ms and changed malformed-input ordering. The next +candidate must remove whole repeated projection/adapter transactions, rather +than another constructor or field-query leaf. The strongest remaining joint +frontier is 3.0201s with incomplete whole-lane replay coverage; counts from the +instrumented profile cannot establish its reducible fraction. Audit and smallest +missing capture recipe: +``/var/tmp/openhcs-b764-whole-lifecycle-architecture-reassessment-20261002.json``. From 185efca6267117b0ea5966e51dcfeb2c5edbb0e3 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 11:53:05 -0400 Subject: [PATCH 075/235] docs: qualify #435 role pixels and fresh public readback --- .../shared_runtime_plumbing_384_20261001.rst | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/validation/shared_runtime_plumbing_384_20261001.rst b/docs/validation/shared_runtime_plumbing_384_20261001.rst index 6c9f01662f..5c9091bcda 100644 --- a/docs/validation/shared_runtime_plumbing_384_20261001.rst +++ b/docs/validation/shared_runtime_plumbing_384_20261001.rst @@ -1459,3 +1459,20 @@ frontier is 3.0201s with incomplete whole-lane replay coverage; counts from the instrumented profile cannot establish its reducible fraction. Audit and smallest missing capture recipe: ``/var/tmp/openhcs-b764-whole-lifecycle-architecture-reassessment-20261002.json``. + +Issue 435 supplemental public readback passes on the unchanged V4 output. Two +fresh SDK processes expose identical complete inventory, sample and projection +facts. Both 1024-by-1024 float32 role images pass the unchanged existing CP pixel +comparison with zero out-of-tolerance pixels: raw maximum difference is zero; +capped maximum difference is 1.1920928955078125e-7 under atol=rtol=1e-6. Both +roles retain physical C2 and calibration within the existing inspection spacing +tolerance of 1e-12. The original controller, journey, inputs, source, dependencies, +native binaries and all physical output hashes remain unchanged, and both reader +children exit zero. The original V4 journey remains RED. The supplemental +inspection check follows the existing status policy, retaining PARTIAL for the +sole undeclared-grid warning without fabricating grid metadata; spacing uses the +existing inspection tolerance rather than literal equality after TIFF formatting. +This is synthetic source qualification, not original biological or installed-root +acceptance. Supplemental receipt: +``/var/tmp/openhcs-derived-role-435-v4-readback-supplement-20261002/receipt.json`` +(SHA256 4f0f0550e50aaff297d134ee5accf2b2da01297305394a155fe79e827b844fdf). From 6f257f10e9047f1bf4d3e3cdc52f5f90c03ee1ff Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 12:35:23 -0400 Subject: [PATCH 076/235] docs: record current runtime qualification and workload priority --- .../shared_runtime_plumbing_384_20261001.rst | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/docs/validation/shared_runtime_plumbing_384_20261001.rst b/docs/validation/shared_runtime_plumbing_384_20261001.rst index 5c9091bcda..effb31e7ac 100644 --- a/docs/validation/shared_runtime_plumbing_384_20261001.rst +++ b/docs/validation/shared_runtime_plumbing_384_20261001.rst @@ -1476,3 +1476,44 @@ This is synthetic source qualification, not original biological or installed-roo acceptance. Supplemental receipt: ``/var/tmp/openhcs-derived-role-435-v4-readback-supplement-20261002/receipt.json`` (SHA256 4f0f0550e50aaff297d134ee5accf2b2da01297305394a155fe79e827b844fdf). + +Current source qualification and workload priority, 2026-10-02 +------------------------------------------------------------ + +Main 142196857 (callable documentation ownership) is normally merged at +9aa3918d2. The affected declared-documentation, agent-service, cold-feedback, +automatic-image-role and plane-NLM integration controls pass 170 cases in +14.77s. Log: +``/var/tmp/openhcs-pr394-main142-documentation-role-integration-controls-20261002.log``. + +A clean frozen 9aa3918d2 ordinary public-driver observation completes one CPU5 +inline worker/thread with compilation 2.103262s, execution 7.870635s and total +10.781240s. Server/library/kernel startup and shutdown remain outside pipeline +clocks. All six CSVs, 120 TIFFs, 180 disk projection references and three source +volume hashes agree exactly with the retained qualified reference and b764 +output; source, dependencies, interpreter and native binaries remain unchanged. +Native science is transitive through that exact reference. This single current +observation does not replace the earlier repeated ABBA comparison or establish +a new optimization gain. Receipt: +``/var/tmp/openhcs-pr394-role-qualified-3d-science-20261002.json``. + +The descriptive comparison against the two retained warm native 3D observations +(mean 14.395949s) is 1.829x execution and 1.335x total. 3D is weakest among the +three qualified representatives (3D, Advanced and IFC), whose OpenHCS revisions +differ. The global weakest case remains unknown: all30 cached native clocks +include subprocess startup and the older multicore CP scaling baseline is +projected. Historical WoundHealing, TrackObjects and worm cases require current +matched warm-native qualification before ranking the whole catalog. Receipt: +``/var/tmp/openhcs-current-native-workload-priority-revalidation-20261002.json``. + +The next whole-transaction diagnostic is rejected before launch when its saved +representative identity pairs predict 2,719,141,800 bytes for three 60-request +cohorts alone, exceeding the transport's 1GiB root budget. No pipeline is run +for V4 and none of its frozen files are changed. This prediction is not an +observed complete production roster. A saved grouped-graph experiment reduces +those three representative graphs to 81,450,818 bytes while preserving actual +saved payloads; cache subsets contain no arrays. A separate V5 preparation +must capture actual ordered requests and detached pre/post cache states before +its replay or a performance candidate can be admitted. Receipts: +``/var/tmp/openhcs-whole-cohort-v4-budget-preflight-REJECTED-20261002.json`` and +``/var/tmp/openhcs-grouped-identity-budget-saved-experiment-v3-20261002/receipt.json``. From d99b2eb4c96bf2b36c206ae256c873433b62c0a0 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 13:33:45 -0400 Subject: [PATCH 077/235] docs: qualify warm native slow cases and redirect runtime priority --- .../shared_runtime_plumbing_384_20261001.rst | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/docs/validation/shared_runtime_plumbing_384_20261001.rst b/docs/validation/shared_runtime_plumbing_384_20261001.rst index effb31e7ac..d8a3c6bfb9 100644 --- a/docs/validation/shared_runtime_plumbing_384_20261001.rst +++ b/docs/validation/shared_runtime_plumbing_384_20261001.rst @@ -1517,3 +1517,103 @@ must capture actual ordered requests and detached pre/post cache states before its replay or a performance candidate can be admitted. Receipts: ``/var/tmp/openhcs-whole-cohort-v4-budget-preflight-REJECTED-20261002.json`` and ``/var/tmp/openhcs-grouped-identity-budget-saved-experiment-v3-20261002/receipt.json``. + +Fresh native qualification changes the primary target +--------------------------------------------------- + +Four additional ordinary current9aa cases now have matched source inventories +and actual warm native runs. Each native case excludes one warmup and retains +both measured repetitions. OpenHCS uses one CPU5 inline worker/thread, mandatory +READY warmup before compilation, the default OUTCOMES observer and memory policy. +Server startup and shutdown are excluded. The following ratios are descriptive: +one OpenHCS observation against the mean of two native observations, not a +repeated A/B optimization claim. Native time includes preparation, modules, +post-run and measurement closure, excluding imports, JVM startup and pipeline +loading; OpenHCS total includes compilation. + +.. list-table:: Current single-worker observations, seconds + :header-rows: 1 + + * - Case + - OpenHCS execution + - OpenHCS total + - Native mean + - Execution speedup + - Total speedup + * - WoundHealing + - 3.571737 + - 4.096850 + - 3.779188 + - 1.058x + - 0.922x + * - TrackObjects + - 5.036489 + - 6.400329 + - 8.276148 + - 1.643x + - 1.293x + * - UntangleWorms + - 2.194352 + - 3.206172 + - 2.974680 + - 1.356x + - 0.928x + * - UntangleWormsBrightField + - 3.064656 + - 4.264850 + - 6.152021 + - 2.007x + - 1.443x + +All eight comparisons pass the existing measurement/schema/discrete gate and +unchanged numeric tolerances. WoundHealing and BrightField also pass complete +saved-output science. TrackObjects and UntangleWorms fail saved-image content +comparison in both repetitions; their ratios therefore have measurement-only +qualification. Physical output inventories match. All original RED receipts +remain unchanged. TrackObjects saves 21 three-panel PNGs: original-image and +outline panels agree exactly, while the tracked-object panels are gray instead +of the native colored labels and white IDs. The current callable deletes the +requested saved-image options and returns its unchanged input image. Worm +images have matching backgrounds and colors but 96 differing outline pixels +across two outputs. Sparse-overlap rendering is a candidate cause requiring +actual producer inputs, not a proved segmentation diagnosis. + +WoundHealing is weakest among seven retained representative measurement-science +scopes, replacing 3D as the primary investigation. This is not a full-catalog +ranking. WoundHealing needs about 1.682142s execution reduction and 2.207256s +total reduction to reach twice its native throughput. Its native comparison +uses exactly two selected 2304-by-1536 RGB JPGs; TrackObjects uses all 21 selected +sequence frames, and both worm cases retain their actual source bindings and +training inputs. Source, dependencies, interpreter, native binaries and all +original inputs are frozen before and after. Evidence: +``/var/tmp/openhcs-current-slowcase-priority-and-png-audit-20261002.json`` +(SHA256 c84d5c0f39bf2a246e00eb7cbb42a7457849efb6a974a6d78fee0e68c6c270ef). + +The existing worker-owned profile on current9aa WoundHealing preserves exact +ordinary CSV outputs. It attributes 0.604904s to 22 runtime stack operations, +0.551025s to 16 image normalizations, 0.288266s to two centroid queries and +0.464524s to two color reductions. These profile durations are diagnostic costs, +not measured savings. Metadata composition itself is only 0.016512s. Source +recording's 0.251105s overlaps normalization and must not be added to it. Even +eliminating stacking, normalization and centroid costs entirely gives an +optimistic 1.444195s, below the execution gap before mandatory copies. Another +small metadata leaf is rejected as a sufficient route. Receipt: +``/var/tmp/openhcs-current9aa-wound-profile-dominant-frontier-20261002.json``. + +The bounded actual-input normalization capture completes successfully: all 16 +calls are recorded, with two unchanged V6 before/after graph pairs for the +large integer arrivals. Both consume the same two-frame uint8 RGB stack, +shape (2, 1536, 2304, 3), scale 255 and float32 target. Original input pixels +and public metadata remain unchanged; source/environment/native freezes pass. +Capture clocks are not performance evidence. Independent whole-output science +and original-leaf replay remain separate gates. Receipt: +``/var/tmp/openhcs-wound-intensity-leaf-capture-v1-20261002/observations.json``. +The generic consumer investigation must preserve exact compiled artifact +selection, source projection and stack-broadcast laws; raw integer pixels cannot +be relabeled as normalized unit-interval pixels to avoid a conversion. + +The former 3D grouped-cohort V5 diagnostic remains prepared and unlaunched after +this priority change. No rejected route is promoted because its preparation is +already complete. The tracking renderer is being investigated in an independent +main142 worktree with actual 21-frame producer inputs and exact native panel +replay; no saved-image repair is included or claimed here yet. From e8dd23fdbf2de8532569f58bef98c0aadbd11647 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 15:23:30 -0400 Subject: [PATCH 078/235] perf: share dense object coordinate reduction across storage and CP backends Dense int32 storage reduces coordinates directly instead of materializing sparse pixels. Tracking and relationships use the same numerical reducer with their existing 2D ABI, and declared preparation readies mutable and read-only storage signatures before execution. Refs #384. Exact saved Wound and Track numerical/ownership replay passes; this is a local coordinate-lane gain pending ordinary pipeline qualification. --- openhcs/core/runtime_object_labels.py | 87 ++++++++++ .../backends/cellprofiler/_backend.py | 2 + .../backends/cellprofiler/_preparation.py | 2 + .../backends/cellprofiler/relationships.py | 32 +--- .../backends/cellprofiler/tracking.py | 30 +--- .../test_dense_object_coordinate_reduction.py | 163 ++++++++++++++++++ 6 files changed, 263 insertions(+), 53 deletions(-) create mode 100644 tests/unit/test_dense_object_coordinate_reduction.py diff --git a/openhcs/core/runtime_object_labels.py b/openhcs/core/runtime_object_labels.py index 056dc95c89..fac841b9bb 100644 --- a/openhcs/core/runtime_object_labels.py +++ b/openhcs/core/runtime_object_labels.py @@ -14,6 +14,7 @@ import numpy as np from metaclass_registry import AutoRegisterMeta +from numba import njit from openhcs.core import ( runtime_array_values, @@ -1198,6 +1199,38 @@ def dense_data( def sparse_ijv_rows(self, labels: object) -> SparseIJVLabelRows: return SparseIJVLabelRows.from_dense_stack(cast(np.ndarray, labels)) + @classmethod + def prepare_coordinates(cls) -> None: + """Prepare both mutability signatures of the dense coordinate reducer.""" + labels = np.array([[0, 1]], dtype=np.int32) + for writeable in (True, False): + labels.flags.writeable = writeable + _dense_label_coordinate_centers_numba(labels.ravel(), 1, 2, 1, 2) + dense_label_centers_2d_numba(labels, 1) + + def axis_centers( + self, labels: object, *, domain: Sequence[int] + ) -> tuple[tuple[np.ndarray, ...], np.ndarray]: + if ( + type(labels) is not np.ndarray + or labels.dtype != np.dtype(np.int32) + or labels.ndim not in (2, 3) + or any(size > np.iinfo(np.int32).max for size in labels.shape) + or type(domain) is not tuple + or any(type(value) is not int for value in domain) + or max(domain, default=0) >= np.iinfo(np.intp).max + ): + return super().axis_centers(labels, domain=domain) + maximum_label = max(int(labels.max(initial=0)), max(domain, default=0)) + centers, counts = _dense_label_coordinate_centers_numba( + labels.ravel(order="C"), + labels.shape[-2], labels.shape[-1], maximum_label, labels.ndim, + ) + return ( + tuple(centers[:, axis].copy() for axis in range(labels.ndim)), + counts.copy(), + ) + def stack_planes( self, labels: Sequence[object], @@ -1399,6 +1432,14 @@ def sparse_ijv_rows(self, labels: object) -> SparseIJVLabelRows: label_data ) + def axis_centers( + self, labels: object, *, domain: Sequence[int] + ) -> tuple[tuple[np.ndarray, ...], np.ndarray]: + label_data = self.label_data(labels) + return ObjectLabelStorageStrategy.for_value(label_data).axis_centers( + label_data, domain=domain + ) + def stack_planes( self, labels: Sequence[object], @@ -1551,6 +1592,52 @@ def object_label_axis_centers( ) +@njit(cache=True) +def _dense_label_coordinate_centers_numba( + flat_labels: np.ndarray, + height: int, + width: int, + maximum_label: int, + coordinate_count: int, +) -> tuple[np.ndarray, np.ndarray]: + """Reduce dense positive labels once in their declared row-major geometry.""" + sums = np.zeros((maximum_label + 1, 3), dtype=np.float64) + counts = np.zeros(maximum_label + 1, dtype=np.int64) + plane_size = height * width + plane_count = 0 if plane_size == 0 else flat_labels.size // plane_size + offset = 0 + for plane in range(plane_count): + for y in range(height): + for x in range(width): + label_id = int(flat_labels[offset]) + offset += 1 + if label_id > 0 and label_id <= maximum_label: + sums[label_id, 0] += plane + sums[label_id, 1] += y + sums[label_id, 2] += x + counts[label_id] += 1 + centers = np.empty((maximum_label + 1, coordinate_count), dtype=np.float64) + for label_id in range(maximum_label + 1): + for axis in range(coordinate_count): + centers[label_id, axis] = ( + np.nan if counts[label_id] == 0 + else sums[label_id, axis + 3 - coordinate_count] / counts[label_id] + ) + return centers, counts + + +@njit(cache=True) +def dense_label_centers_2d_numba( + labels: np.ndarray, label_count: int +) -> np.ndarray: + """Validate compiled two-dimensional geometry and return its y/x centers.""" + height, width = labels.shape + centers, _counts = _dense_label_coordinate_centers_numba( + labels.ravel(), height, width, label_count, 2 + ) + return centers + + def object_label_storage_is_sparse_ijv(payload: object) -> bool: """Return whether the nominal storage authority owns sparse-IJV rows.""" authority = ObjectLabelStorageStrategy.for_value(payload) diff --git a/openhcs/processing/backends/cellprofiler/_backend.py b/openhcs/processing/backends/cellprofiler/_backend.py index e37742d5b1..811a6deebc 100644 --- a/openhcs/processing/backends/cellprofiler/_backend.py +++ b/openhcs/processing/backends/cellprofiler/_backend.py @@ -14,6 +14,7 @@ from openhcs.constants.constants import MemoryType from openhcs.core.callable_contract import CallableContract from openhcs.core.processing_preparation import PersistentNumbaKernelPreparation +from openhcs.core.runtime_object_labels import DenseArrayObjectLabelStorageStrategy from openhcs.core.runtime_plane_projection import RuntimeSliceInvariantValue @@ -306,6 +307,7 @@ def prepare_registered_family(cls) -> None: """Prepare every registered backend implementation for compiler warmup.""" if cls is CellProfilerBackendStrategyMixin: return + DenseArrayObjectLabelStorageStrategy.prepare_coordinates() snapshot = CellProfilerBackendRegistrySnapshot.for_family( cls, MemoryType.NUMPY, diff --git a/openhcs/processing/backends/cellprofiler/_preparation.py b/openhcs/processing/backends/cellprofiler/_preparation.py index 933a8a3795..cea2850ff1 100644 --- a/openhcs/processing/backends/cellprofiler/_preparation.py +++ b/openhcs/processing/backends/cellprofiler/_preparation.py @@ -3,6 +3,7 @@ from __future__ import annotations from openhcs.core.processing_preparation import RegisteredNumbaKernelPreparation +from openhcs.core.runtime_object_labels import DenseArrayObjectLabelStorageStrategy from openhcs.processing.backends.cellprofiler.perf_fixtures import capture_enabled @@ -20,4 +21,5 @@ def can_prepare_in_child(cls) -> bool: def prepare_registered_family(cls) -> None: if capture_enabled(): return + DenseArrayObjectLabelStorageStrategy.prepare_coordinates() super().prepare_registered_family() diff --git a/openhcs/processing/backends/cellprofiler/relationships.py b/openhcs/processing/backends/cellprofiler/relationships.py index a775ecd7c2..b69c31d2f6 100644 --- a/openhcs/processing/backends/cellprofiler/relationships.py +++ b/openhcs/processing/backends/cellprofiler/relationships.py @@ -865,6 +865,7 @@ def aggregate_child_feature_name( ) from openhcs.core.runtime_tabular_values import ColumnarRows from openhcs.core.runtime_object_labels import ( + dense_label_centers_2d_numba, ObjectLabelValue, object_label_value_with_dense_labels, ) @@ -1854,7 +1855,7 @@ def label_centers(self, labels: np.ndarray) -> np.ndarray: label_count = int(labels.max()) if label_count == 0: return np.empty((0, 2), dtype=np.float64) - centroids = _label_centroids_numba(np.ascontiguousarray(labels), label_count) + centroids = dense_label_centers_2d_numba(np.ascontiguousarray(labels), label_count) return centroids[1:] @@ -2193,29 +2194,6 @@ def _parents_of_from_overlap_counts_numba( return parents_of -@njit(cache=True) -def _label_centroids_numba(labels: np.ndarray, label_count: int) -> np.ndarray: - sums = np.zeros((label_count + 1, 2), dtype=np.float64) - counts = np.zeros(label_count + 1, dtype=np.int64) - height, width = labels.shape - for row in range(height): - for col in range(width): - label_id = int(labels[row, col]) - if label_id > 0 and label_id <= label_count: - sums[label_id, 0] += row - sums[label_id, 1] += col - counts[label_id] += 1 - centroids = np.empty((label_count + 1, 2), dtype=np.float64) - for label_id in range(label_count + 1): - if counts[label_id] == 0: - centroids[label_id, 0] = np.nan - centroids[label_id, 1] = np.nan - else: - centroids[label_id, 0] = sums[label_id, 0] / counts[label_id] - centroids[label_id, 1] = sums[label_id, 1] / counts[label_id] - return centroids - - @njit(cache=True) def _calculate_centroid_distances_numba( parent_labels: np.ndarray, @@ -2229,8 +2207,8 @@ def _calculate_centroid_distances_numba( distances[child_idx] = np.nan if child_count == 0 or parent_count == 0: return distances - parent_centroids = _label_centroids_numba(parent_labels, parent_count) - child_centroids = _label_centroids_numba(child_labels, child_count) + parent_centroids = dense_label_centers_2d_numba(parent_labels, parent_count) + child_centroids = dense_label_centers_2d_numba(child_labels, child_count) for child_idx in range(child_count): parent_id = int(parents_of[child_idx]) child_id = child_idx + 1 @@ -2282,7 +2260,7 @@ def _calculate_minimum_distances_numba( distances[child_idx] = np.nan if child_count == 0 or parent_count == 0: return distances - child_centroids = _label_centroids_numba(child_labels, child_count) + child_centroids = dense_label_centers_2d_numba(child_labels, child_count) height, width = parent_labels.shape counts = np.zeros(parent_count + 1, dtype=np.int64) for row in range(height): diff --git a/openhcs/processing/backends/cellprofiler/tracking.py b/openhcs/processing/backends/cellprofiler/tracking.py index 3a36e51e4e..e591ba0d42 100644 --- a/openhcs/processing/backends/cellprofiler/tracking.py +++ b/openhcs/processing/backends/cellprofiler/tracking.py @@ -39,6 +39,7 @@ from openhcs.core.registry_strategies import EnumKeyedStrategyMixin from openhcs.core.runtime_object_labels import ( ObjectLabelValue, + dense_label_centers_2d_numba, object_label_dense_array, ) from openhcs.core.runtime_relationships import ObjectRelationshipDeclaration @@ -830,7 +831,7 @@ def label_centers(self, labels: np.ndarray) -> tuple[np.ndarray, np.ndarray]: label_count = int(labels_array.max()) if labels_array.size else 0 if label_count == 0: return (np.array([], dtype=np.float64), np.array([], dtype=np.float64)) - centers = _label_centers_numba(np.ascontiguousarray(labels_array), label_count) + centers = dense_label_centers_2d_numba(np.ascontiguousarray(labels_array), label_count) return (centers[1:, 0], centers[1:, 1]) def track_by_overlap( @@ -1461,29 +1462,6 @@ def _apply_final_age_measurements(frame_results: TrackingFrameResults) -> None: final_age_records[slice_index, object_label].final_age = int(lifetime) -@njit(cache=True) -def _label_centers_numba(labels: np.ndarray, label_count: int) -> np.ndarray: - sums = np.zeros((label_count + 1, 2), dtype=np.float64) - counts = np.zeros(label_count + 1, dtype=np.int64) - height, width = labels.shape - for y in range(height): - for x in range(width): - label_id = int(labels[y, x]) - if label_id > 0 and label_id <= label_count: - sums[label_id, 0] += y - sums[label_id, 1] += x - counts[label_id] += 1 - centers = np.empty((label_count + 1, 2), dtype=np.float64) - for label_id in range(label_count + 1): - if counts[label_id] == 0: - centers[label_id, 0] = np.nan - centers[label_id, 1] = np.nan - else: - centers[label_id, 0] = sums[label_id, 0] / counts[label_id] - centers[label_id, 1] = sums[label_id, 1] / counts[label_id] - return centers - - @njit(cache=True) def _track_by_overlap_numba( frame: TrackingKernelFrame, @@ -1528,8 +1506,8 @@ def _track_by_overlap_numba( def _track_by_distance_numba( frame: TrackingKernelFrame, pixel_radius: int ) -> tuple[np.ndarray, np.ndarray, np.ndarray, int]: - current_centers = _label_centers_numba(frame.current_labels, frame.current_count) - old_centers = _label_centers_numba(frame.old_labels, frame.old_count) + current_centers = dense_label_centers_2d_numba(frame.current_labels, frame.current_count) + old_centers = dense_label_centers_2d_numba(frame.old_labels, frame.old_count) new_labels = np.zeros(frame.current_count, dtype=np.int64) parent_object_numbers = np.zeros(frame.current_count, dtype=np.int64) parent_image_numbers = np.zeros(frame.current_count, dtype=np.int64) diff --git a/tests/unit/test_dense_object_coordinate_reduction.py b/tests/unit/test_dense_object_coordinate_reduction.py new file mode 100644 index 0000000000..b62bd89290 --- /dev/null +++ b/tests/unit/test_dense_object_coordinate_reduction.py @@ -0,0 +1,163 @@ +"""Storage reduction preserves its generic oracle and compiled backend domains.""" + +import os +import subprocess +import sys +import textwrap +import warnings +from pathlib import Path + +import numpy as np +import pytest + +from openhcs.core.runtime_object_labels import ( + ObjectLabelPayload, + ObjectLabelStorageStrategy, + ObjectLabelVariantData, + object_label_axis_centers, +) +from openhcs.core.runtime_sparse_labels import SparseIJVLabelRows + + +def generic_centers(labels, domain): + return ObjectLabelStorageStrategy.axis_centers( + ObjectLabelStorageStrategy.for_value(labels), labels, domain=domain + ) + + +def assert_centers(actual, expected): + for actual_array, expected_array in zip( + (*actual[0], actual[1]), (*expected[0], expected[1]), strict=True + ): + np.testing.assert_array_equal(actual_array, expected_array) + assert actual_array.tobytes() == expected_array.tobytes() + assert actual_array.dtype == expected_array.dtype + assert actual_array.strides == expected_array.strides + assert actual_array.flags.owndata == expected_array.flags.owndata + assert actual_array.base is None + assert not np.shares_memory(*actual[0][:2]) + + +@pytest.mark.parametrize("wrapped", (False, True)) +@pytest.mark.parametrize("shape", ((4, 5), (3, 4, 5))) +def test_dense_storage_reduces_without_sparse_materialization(monkeypatch, wrapped, shape): + labels = np.zeros(shape, dtype=np.int32) + labels[..., 1:3, 2:4] = 1 + labels[..., 3, 0] = 4 + domain = (1, 2, 4, 7) + expected = generic_centers(labels, domain) + value = ( + ObjectLabelPayload(variant_data=ObjectLabelVariantData(labels=labels)) + if wrapped else labels + ) + before = labels.copy() + + def reject_materialization(_cls, _labels): + raise AssertionError("Coordinate reduction materialized sparse pixels") + + monkeypatch.setattr(SparseIJVLabelRows, "from_dense_stack", classmethod(reject_materialization)) + assert_centers(object_label_axis_centers(value, domain=domain), expected) + np.testing.assert_array_equal(labels, before) + + +@pytest.mark.parametrize("layout", ("c", "f", "view", "readonly", "empty")) +def test_layout_and_mutable_pixels_preserve_generic_result(layout): + labels = np.arange(30, dtype=np.int32).reshape(5, 6) % 5 + if layout == "f": + labels = np.asfortranarray(labels) + elif layout == "view": + labels = labels[::-1, ::2] + elif layout == "readonly": + labels.flags.writeable = False + elif layout == "empty": + labels = np.zeros((2, 0, 3), dtype=np.int32) + domain = (1, 2, 4, 9) + assert_centers(object_label_axis_centers(labels, domain=domain), generic_centers(labels, domain)) + if labels.size and labels.flags.writeable: + labels.flat[0] = 4 + assert_centers(object_label_axis_centers(labels, domain=domain), generic_centers(labels, domain)) + + +@pytest.mark.parametrize("dtype", (np.bool_, np.float64, np.int64, np.uint64)) +def test_other_dtype_conversion_and_warning_policy_stays_generic(dtype): + labels = np.array([[0, 1], [2, 3]], dtype=dtype) + if dtype is np.float64: + labels[0] = (0.5, np.nan) + elif dtype is np.uint64: + labels[0, 1] = 2**32 + 1 + outcomes = [] + for call in (generic_centers, lambda x, domain: object_label_axis_centers(x, domain=domain)): + with warnings.catch_warnings(record=True) as observed: + warnings.simplefilter("always") + try: + result = call(labels, (1, 2, 3, 8)) + except ValueError as error: + result = (type(error), str(error)) + outcomes.append((result, [(type(w.message), str(w.message)) for w in observed])) + if dtype is np.uint64: + assert outcomes[0][0] == outcomes[1][0] + else: + assert_centers(outcomes[0][0], outcomes[1][0]) + assert outcomes[0][1] == outcomes[1][1] + + +def test_sparse_overlap_and_subtype_keep_existing_representation(): + sparse = SparseIJVLabelRows(np.array([[0, 0, 1], [0, 0, 2], [2, 3, 1]], dtype=np.int32)) + assert_centers(object_label_axis_centers(sparse, domain=(1, 2, 4)), generic_centers(sparse, (1, 2, 4))) + + class DenseSubtype(np.ndarray): + @property + def dtype(self): + raise AssertionError("Subtype properties must not be probed before generic coercion") + + subtype = np.array([[1, 0], [2, 2]], dtype=np.int32).view(DenseSubtype) + assert_centers(object_label_axis_centers(subtype, domain=(1, 2, 4)), generic_centers(subtype, (1, 2, 4))) + + +@pytest.mark.parametrize("shape", ((), (4,), (2, 3, 4, 5))) +def test_invalid_shape_keeps_original_error(shape): + labels = np.zeros(shape, dtype=np.int32) + with pytest.raises(ValueError) as original: + generic_centers(labels, (1,)) + with pytest.raises(type(original.value), match="SparseIJVLabelRows.from_dense_stack") as current: + object_label_axis_centers(labels, domain=(1,)) + assert str(current.value) == str(original.value) + + +def test_primary_objects_preparation_readies_all_admitted_storage_signatures(tmp_path): + script = textwrap.dedent(""" + from unittest.mock import patch + import numpy as np + from openhcs.core.callable_contract import prepare_processing_callable + from openhcs.core.runtime_object_labels import ( + _dense_label_coordinate_centers_numba, dense_label_centers_2d_numba, + object_label_axis_centers, + ) + from openhcs.processing.backends.cellprofiler.primary_objects import identify_primary_objects + assert not _dense_label_coordinate_centers_numba.signatures + prepare_processing_callable(identify_primary_objects) + kernels = (_dense_label_coordinate_centers_numba, dense_label_centers_2d_numba) + signatures = tuple(tuple(kernel.signatures) for kernel in kernels) + assert all(signatures) + + def reject_compilation(signature): + raise AssertionError(('Late coordinate compilation', str(signature))) + + with patch.object(_dense_label_coordinate_centers_numba, 'compile', + side_effect=reject_compilation), patch.object( + dense_label_centers_2d_numba, 'compile', side_effect=reject_compilation): + for shape in ((4, 5), (3, 4, 5)): + base = np.arange(np.prod(shape), dtype=np.int32).reshape(shape) % 4 + for labels in (base, np.asfortranarray(base), base[..., ::-1], base.copy()): + object_label_axis_centers(labels, domain=(1, 2, 3, 6)) + labels.flags.writeable = False + object_label_axis_centers(labels, domain=(1, 2, 3, 6)) + assert tuple(tuple(kernel.signatures) for kernel in kernels) == signatures + """) + environment = os.environ.copy() + environment.update(OPENHCS_CPU_ONLY="true", NUMBA_CACHE_DIR=str(tmp_path / "kernels")) + result = subprocess.run( + (sys.executable, "-c", script), cwd=Path(__file__).parents[2], + env=environment, capture_output=True, text=True, timeout=120, + ) + assert result.returncode == 0, result.stdout + result.stderr From 49205ac42694387f9de078aeba89c67a0f53b14d Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 15:23:51 -0400 Subject: [PATCH 079/235] fix: prewarm masked polynomial illumination kernels Prepare masked and unmasked polynomial signatures on the existing callable preparation hook, including readonly canonical inputs. Readiness controls refuse any subsequent dispatcher compilation or cache load while validating exact fitting behavior, input isolation and geometry errors. Refs #384. Intended for PR #394. --- .../backends/cellprofiler/illumination.py | 10 +++ .../test_cellprofiler_kernel_preparation.py | 80 +++++++++++++++++++ 2 files changed, 90 insertions(+) diff --git a/openhcs/processing/backends/cellprofiler/illumination.py b/openhcs/processing/backends/cellprofiler/illumination.py index d86e17def8..1dab00b4e5 100644 --- a/openhcs/processing/backends/cellprofiler/illumination.py +++ b/openhcs/processing/backends/cellprofiler/illumination.py @@ -1003,6 +1003,16 @@ def _prepare_correct_illumination_calculate() -> None: filter_size_method=FilterSizeMethod.AUTOMATIC, rescale_option=RescaleOption.YES, ) + # Polynomial fitting canonicalizes dtype/layout, but retains read-only + # contiguous arrays. Prepare masked and unmasked mutability signatures. + polynomial_image = np.ascontiguousarray(image, dtype=np.float64) + polynomial_mask = np.ones(image.shape, dtype=np.bool_) + for image_writeable in (True, False): + polynomial_image.flags.writeable = image_writeable + fit_polynomial_surface(polynomial_image, None) + for mask_writeable in (True, False): + polynomial_mask.flags.writeable = mask_writeable + fit_polynomial_surface(polynomial_image, polynomial_mask) convex_hull_image = np.linspace(0.0, 1.0, 32 * 32, dtype=np.float32).reshape( (32, 32) ) diff --git a/tests/unit/test_cellprofiler_kernel_preparation.py b/tests/unit/test_cellprofiler_kernel_preparation.py index ba9f6173dd..9295d7bcf9 100644 --- a/tests/unit/test_cellprofiler_kernel_preparation.py +++ b/tests/unit/test_cellprofiler_kernel_preparation.py @@ -148,3 +148,83 @@ def record_compile(dispatcher, signature): timeout=120, ) assert result.returncode == 0, result.stdout + result.stderr + + +def test_illumination_preparation_covers_masked_polynomial_and_array_views(tmp_path): + """READY must include every canonical pixel/mask mutability signature.""" + script = textwrap.dedent(""" + from contextlib import ExitStack + from unittest.mock import patch + import numpy as np + from openhcs.core.callable_contract import prepare_processing_callable + from openhcs.processing.backends.cellprofiler import illumination + + dispatchers = ( + illumination._fit_polynomial_unmasked_gram_numba, + illumination._fit_polynomial_unmasked_rhs_numba, + illumination._fit_polynomial_normal_equations_numba, + illumination._evaluate_polynomial_surface_numba, + ) + assert all(not dispatcher.signatures for dispatcher in dispatchers) + prepare_processing_callable(illumination.correct_illumination_calculate) + signatures = tuple(tuple(dispatcher.signatures) for dispatcher in dispatchers) + assert all(signatures), "A reached masked polynomial kernel was not prepared" + + def reject_late_compilation(signature): + raise AssertionError("Polynomial kernel compiled or loaded after READY: " + str(signature)) + + # The polynomial spans the exact solver feature basis, so the expected + # answer is independent of production warmup and implementation details. + shape = (11, 13) + y, x = np.indices(shape, dtype=np.float64) + expected = 1.0 + x / shape[1] + y / shape[0] + (x / shape[1]) ** 2 + mask = np.ones(shape, dtype=bool) + mask[2:4, 3:5] = False + with ExitStack() as stack: + for dispatcher in dispatchers: + stack.enter_context(patch.object(dispatcher, "compile", reject_late_compilation)) + for dtype in (np.float32, np.float64): + for order in ("C", "F", "reversed"): + for readonly_image in (False, True): + image = np.array(expected, dtype=dtype, order="F" if order == "F" else "C") + selected_mask = mask.copy(order="F" if order == "F" else "C") + if order == "reversed": + image = image[::-1, ::-1] + selected_mask = selected_mask[::-1, ::-1] + if readonly_image: + image.flags.writeable = False + for mask_mode in ("absent", "writable", "readonly"): + active_mask = None if mask_mode == "absent" else selected_mask.copy(order="K") + if mask_mode == "readonly": + active_mask.flags.writeable = False + before_image = image.copy() + before_mask = None if active_mask is None else active_mask.copy() + result = illumination.fit_polynomial_surface(image, active_mask) + oracle = expected[::-1, ::-1] if order == "reversed" else expected + np.testing.assert_allclose(result, oracle, atol=1e-6, rtol=1e-6) + np.testing.assert_array_equal(image, before_image) + if active_mask is not None: + np.testing.assert_array_equal(active_mask, before_mask) + assert not np.shares_memory(result, image) + if active_mask is not None: + assert not np.shares_memory(result, active_mask) + with np.testing.assert_raises_regex(ValueError, "mask must match"): + illumination.fit_polynomial_surface(expected, np.ones((3, 4), dtype=bool)) + with np.testing.assert_raises_regex(NotImplementedError, "2-D"): + illumination.fit_polynomial_surface(expected[None, ...], None) + assert tuple(tuple(dispatcher.signatures) for dispatcher in dispatchers) == signatures + """) + environment = os.environ.copy() + environment.update( + {"OPENHCS_CPU_ONLY": "true", "NUMBA_CACHE_DIR": str(tmp_path / "polynomial-kernels")} + ) + result = subprocess.run( + (sys.executable, "-c", script), + cwd=Path(__file__).parents[2], + env=environment, + check=False, + capture_output=True, + text=True, + timeout=90, + ) + assert result.returncode == 0, result.stdout + result.stderr From 66b485034217f5ab86410d67aafa881facb8e138 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 15:40:57 -0400 Subject: [PATCH 080/235] refactor: snapshot dense moments before public domain effects Keep the original Sequence contract and NumPy allocation/error order without exact-type admission probes. The storage parent owns shared center allocation and division; dense storage supplies the snapshot before effectful domain evaluation. Refs #384. Eleven original-parent callback and mutation controls, 29 focused controls and exact saved Wound/Track coordinate ABI replay pass. The original rejected guard implementation and R0 failure remain retained externally. --- openhcs/core/runtime_object_labels.py | 95 ++++++++++++------- .../test_dense_object_coordinate_reduction.py | 82 +++++++++++++++- 2 files changed, 139 insertions(+), 38 deletions(-) diff --git a/openhcs/core/runtime_object_labels.py b/openhcs/core/runtime_object_labels.py index fac841b9bb..a503a78b14 100644 --- a/openhcs/core/runtime_object_labels.py +++ b/openhcs/core/runtime_object_labels.py @@ -6,6 +6,7 @@ from collections.abc import ( Callable, Hashable, + Iterable, MutableMapping, Sequence, ) @@ -1112,14 +1113,30 @@ def axis_centers( if sparse_labels.has_slice_index else (sparse_labels.y_column, sparse_labels.x_column) ) + return self.coordinate_centers( + counts, + ( + np.bincount( + object_ids, + weights=array[:, coordinate_column], + minlength=max_label + 1, + ) + for coordinate_column in coordinate_columns + ), + maximum_label=max_label, + ) + + @staticmethod + def coordinate_centers( + counts: np.ndarray, + coordinate_sums: Iterable[np.ndarray], + *, + maximum_label: int, + ) -> tuple[tuple[np.ndarray, ...], np.ndarray]: + """Allocate and divide each coordinate sum in its declared ID domain.""" axis_centers: list[np.ndarray] = [] - for coordinate_column in coordinate_columns: - sums = np.bincount( - object_ids, - weights=array[:, coordinate_column], - minlength=max_label + 1, - ) - centers = np.full(max_label + 1, np.nan, dtype=np.float64) + for sums in coordinate_sums: + centers = np.full(maximum_label + 1, np.nan, dtype=np.float64) np.divide(sums, counts, out=centers, where=counts > 0) axis_centers.append(centers) return tuple(axis_centers), counts @@ -1205,30 +1222,41 @@ def prepare_coordinates(cls) -> None: labels = np.array([[0, 1]], dtype=np.int32) for writeable in (True, False): labels.flags.writeable = writeable - _dense_label_coordinate_centers_numba(labels.ravel(), 1, 2, 1, 2) + _dense_label_coordinate_moments_numba(labels.ravel(), 1, 2, 1) dense_label_centers_2d_numba(labels, 1) def axis_centers( self, labels: object, *, domain: Sequence[int] ) -> tuple[tuple[np.ndarray, ...], np.ndarray]: + array = np.asarray(labels) if ( - type(labels) is not np.ndarray - or labels.dtype != np.dtype(np.int32) - or labels.ndim not in (2, 3) - or any(size > np.iinfo(np.int32).max for size in labels.shape) - or type(domain) is not tuple - or any(type(value) is not int for value in domain) - or max(domain, default=0) >= np.iinfo(np.intp).max + array.dtype != np.dtype(np.int32) + or array.ndim not in (2, 3) + or any(size > np.iinfo(np.int32).max for size in array.shape) ): return super().axis_centers(labels, domain=domain) - maximum_label = max(int(labels.max(initial=0)), max(domain, default=0)) - centers, counts = _dense_label_coordinate_centers_numba( - labels.ravel(order="C"), - labels.shape[-2], labels.shape[-1], maximum_label, labels.ndim, + coordinate_columns = tuple(range(3 - array.ndim, 3)) + sums, pixel_counts = _dense_label_coordinate_moments_numba( + array.ravel(order="C"), + array.shape[-2], array.shape[-1], int(array.max(initial=0)), ) - return ( - tuple(centers[:, axis].copy() for axis in range(labels.ndim)), - counts.copy(), + # Like sparse conversion, the moments snapshot precedes domain callbacks. + object_ids = np.flatnonzero(pixel_counts) + max_domain_label = max(domain, default=0) + maximum_label = max(int(object_ids.max(initial=0)), max_domain_label) + counts = np.bincount(object_ids, minlength=maximum_label + 1) + counts[object_ids] = pixel_counts[object_ids] + return self.coordinate_centers( + counts, + ( + np.bincount( + object_ids, + weights=sums[object_ids, coordinate_column], + minlength=maximum_label + 1, + ) + for coordinate_column in coordinate_columns + ), + maximum_label=maximum_label, ) def stack_planes( @@ -1593,12 +1621,11 @@ def object_label_axis_centers( @njit(cache=True) -def _dense_label_coordinate_centers_numba( +def _dense_label_coordinate_moments_numba( flat_labels: np.ndarray, height: int, width: int, maximum_label: int, - coordinate_count: int, ) -> tuple[np.ndarray, np.ndarray]: """Reduce dense positive labels once in their declared row-major geometry.""" sums = np.zeros((maximum_label + 1, 3), dtype=np.float64) @@ -1616,14 +1643,7 @@ def _dense_label_coordinate_centers_numba( sums[label_id, 1] += y sums[label_id, 2] += x counts[label_id] += 1 - centers = np.empty((maximum_label + 1, coordinate_count), dtype=np.float64) - for label_id in range(maximum_label + 1): - for axis in range(coordinate_count): - centers[label_id, axis] = ( - np.nan if counts[label_id] == 0 - else sums[label_id, axis + 3 - coordinate_count] / counts[label_id] - ) - return centers, counts + return sums, counts @njit(cache=True) @@ -1632,9 +1652,16 @@ def dense_label_centers_2d_numba( ) -> np.ndarray: """Validate compiled two-dimensional geometry and return its y/x centers.""" height, width = labels.shape - centers, _counts = _dense_label_coordinate_centers_numba( - labels.ravel(), height, width, label_count, 2 + sums, counts = _dense_label_coordinate_moments_numba( + labels.ravel(), height, width, label_count ) + centers = np.empty((label_count + 1, 2), dtype=np.float64) + for label_id in range(label_count + 1): + for axis in range(2): + centers[label_id, axis] = ( + np.nan if counts[label_id] == 0 + else sums[label_id, axis + 1] / counts[label_id] + ) return centers diff --git a/tests/unit/test_dense_object_coordinate_reduction.py b/tests/unit/test_dense_object_coordinate_reduction.py index b62bd89290..f3cc02442e 100644 --- a/tests/unit/test_dense_object_coordinate_reduction.py +++ b/tests/unit/test_dense_object_coordinate_reduction.py @@ -124,26 +124,100 @@ def test_invalid_shape_keeps_original_error(shape): assert str(current.value) == str(original.value) +@pytest.mark.parametrize("failure", (None, "iterate", 1, 2, 3, 4, 5)) +def test_domain_callbacks_follow_pixel_snapshot_and_original_allocation_order(failure): + outcomes = [] + for call in (generic_centers, lambda labels, domain: object_label_axis_centers(labels, domain=domain)): + labels = np.array([[1, 1], [2, 0]], dtype=np.int32) + effects = [] + + class DomainInteger(int): + def __gt__(self, other): + effects.append("compare") + return super().__gt__(other) + + def __add__(self, other): + effects.append("add") + result = super().__add__(other) + return float(result) if effects.count("add") == failure else result + + class MutatingDomain: + def __iter__(self): + effects.append("iterate") + labels[:] = 3 + labels.shape = (1, 2, 2) + if failure == "iterate": + raise RuntimeError("Domain iteration failed after changing pixels") + yield DomainInteger(4) + + try: + result = call(labels, MutatingDomain()) + except (RuntimeError, TypeError) as error: + outcome = (type(error), str(error)) + else: + outcome = result + outcomes.append((outcome, effects, labels.copy())) + + if failure is None: + assert_centers(outcomes[0][0], outcomes[1][0]) + np.testing.assert_array_equal(outcomes[1][0][1], (0, 2, 1, 0, 0)) + assert len(outcomes[1][0][0]) == 2 + else: + assert outcomes[0][0] == outcomes[1][0] + assert outcomes[0][1] == outcomes[1][1] + np.testing.assert_array_equal(outcomes[0][2], outcomes[1][2]) + + +@pytest.mark.parametrize("domain", ((4.0,), (np.int64(np.iinfo(np.int64).max),), (2**63,))) +def test_domain_minlength_errors_and_warnings_remain_numpy_owned(domain): + outcomes = [] + for call in (generic_centers, lambda labels, domain: object_label_axis_centers(labels, domain=domain)): + with warnings.catch_warnings(record=True) as observed: + warnings.simplefilter("always") + try: + call(np.array([[1, 0], [2, 1]], dtype=np.int32), domain) + except (TypeError, ValueError, OverflowError) as error: + outcome = (type(error), str(error)) + else: + raise AssertionError("Invalid minlength unexpectedly succeeded") + outcomes.append((outcome, [(type(w.message), str(w.message)) for w in observed])) + assert outcomes[0] == outcomes[1] + + +def test_invalid_geometry_precedes_domain_iteration(): + for call in (generic_centers, lambda labels, domain: object_label_axis_centers(labels, domain=domain)): + effects = [] + + class Domain: + def __iter__(self): + effects.append("iterate") + yield 1 + + with pytest.raises(ValueError, match="SparseIJVLabelRows.from_dense_stack"): + call(np.zeros((2, 3, 4, 5), dtype=np.int32), Domain()) + assert not effects + + def test_primary_objects_preparation_readies_all_admitted_storage_signatures(tmp_path): script = textwrap.dedent(""" from unittest.mock import patch import numpy as np from openhcs.core.callable_contract import prepare_processing_callable from openhcs.core.runtime_object_labels import ( - _dense_label_coordinate_centers_numba, dense_label_centers_2d_numba, + _dense_label_coordinate_moments_numba, dense_label_centers_2d_numba, object_label_axis_centers, ) from openhcs.processing.backends.cellprofiler.primary_objects import identify_primary_objects - assert not _dense_label_coordinate_centers_numba.signatures + assert not _dense_label_coordinate_moments_numba.signatures prepare_processing_callable(identify_primary_objects) - kernels = (_dense_label_coordinate_centers_numba, dense_label_centers_2d_numba) + kernels = (_dense_label_coordinate_moments_numba, dense_label_centers_2d_numba) signatures = tuple(tuple(kernel.signatures) for kernel in kernels) assert all(signatures) def reject_compilation(signature): raise AssertionError(('Late coordinate compilation', str(signature))) - with patch.object(_dense_label_coordinate_centers_numba, 'compile', + with patch.object(_dense_label_coordinate_moments_numba, 'compile', side_effect=reject_compilation), patch.object( dense_label_centers_2d_numba, 'compile', side_effect=reject_compilation): for shape in ((4, 5), (3, 4, 5)): From eb2f23efdf6c9f7d4da0e5692567b3535f23f257 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 15:47:49 -0400 Subject: [PATCH 081/235] docs: qualify current native frontier and merged readiness repair Refs #384, #419, #433, #435 and #450. Preserve timing scopes and original failed gates. --- .../shared_runtime_plumbing_384_20261001.rst | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/docs/validation/shared_runtime_plumbing_384_20261001.rst b/docs/validation/shared_runtime_plumbing_384_20261001.rst index d8a3c6bfb9..16cf3492e0 100644 --- a/docs/validation/shared_runtime_plumbing_384_20261001.rst +++ b/docs/validation/shared_runtime_plumbing_384_20261001.rst @@ -1617,3 +1617,128 @@ this priority change. No rejected route is promoted because its preparation is already complete. The tracking renderer is being investigated in an independent main142 worktree with actual 21-frame producer inputs and exact native panel replay; no saved-image repair is included or claimed here yet. + +Library readiness repair and latest qualification, 2026-10-02 +------------------------------------------------------------ + +Main 3f955d3fb (MCP sampling) and cf5a83f89 (fixture CLI repair) are normally +merged. The MCP/prepared-geometry/automatic-role integration gate records 354 +passes and nine failures. All nine fail identically on clean main3f955 with the +same readonly native-binary bindings: eight fixture declarations omit the +required server, and one expected stream-argument mapping omits display_config. +The first clean-main attempt refused a missing native extension at collection; +that original failure is retained before the exact-bound native supplement. +These are existing fixture failures, not a passing full suite. Numerical and +dependency sources are unchanged by the fixture-CLI merge. + +The actual Illumination3 first run reached an unprepared masked polynomial +specialization. Its Numba artifacts were written inside the measured polynomial +step: 0.526s versus 0.016s when already cached. The existing callable preparation +hook now prepares masked/unmasked and readonly canonical image/mask signatures +before READY. A fresh-cache subprocess forbids all later compilation and cache +loads across 36 dtype/layout/mutability/mask combinations and verifies an +independent polynomial oracle, input isolation and geometry errors. Twenty +preparation/fitted-illumination controls pass, with two existing skips. The fix +ships independently in merged PR474, formally closing issue473; original R0 +passes all three roots and original R1 passes its unchanged 160-second budget. +Main a6c18f054 is normally merged back into this branch. Its full receipt is +``docs/validation/polynomial-library-readiness-473-20261002.rst``. + +The clean frozen integrated source54708367b has fresh ordinary CPU5 one-worker, +one-thread observations using the existing cache, default OUTCOMES observer and +default memory policy: + +.. list-table:: Fresh post-repair observations, seconds + :header-rows: 1 + + * - Case + - Compilation + - Execution + - Pipeline total + * - Illumination3 + - 0.962373 + - 0.647611 + - 1.961545 + * - WoundHealing + - 0.346341 + - 3.916717 + - 4.575604 + +Startup/library/kernel preparation and shutdown remain outside pipeline clocks. +These are single observations, with no matched repair speedup or statistical +regression claim. Wound is slower than its retained earlier single observation; +that result is preserved rather than substituting the better sample. Both +authored Illumination3 NPY images and Wound's complete two-row Image.csv agree +byte-for-byte with the qualified unchanged references. Existing complete saved +inventory and CP-tolerance comparison gates pass. Native science is transitive +through those fully qualified references. The first validator incorrectly +expected two Wound tables rather than two rows; its failure remains retained, +and the V2 supplement checks the actual authored inventory without changing any +production comparison policy. Source, dependencies, interpreter, native binaries +and all inputs remain frozen. Receipt: +``/var/tmp/openhcs-pr394-masked-prewarm-ordinary-science-v2-20261002.json`` +(SHA256 21231dffa14dbe17bf824a4258f677e7d656122036da81e0b3d8c9c8bc2f9c02). + +Additional native qualification and remaining execution frontier +--------------------------------------------------------------- + +Current9aa Vitra uses one selected joined source set containing all four physical +resources, rather than two channel-derived image sets. Its two warm native +repetitions average 3.044868s; OpenHCS is 1.615775s execution and 2.908514s total. +Complete saved science passes for 2,233 table rows and its authored image in both +native repetitions. Earlier source-set controller refusals remain retained. +Receipt: +``/var/tmp/openhcs-current-vitra-native-timing-science-v3-20261002.json``. + +Illumination3's two warm native repetitions average 0.603076s. Its authored +pipeline saves two NPY images and no CSV. Complete existing inventory and image +science pass in both repetitions. The retained first OpenHCS observation is +1.309110s execution and 2.603767s total, containing the late polynomial +specialization described above; the fresh post-repair observation is recorded +separately. Cache-state differences prevent treating these as a matched repair +speedup. The global weakest full-catalog case remains unknown. Receipt: +``/var/tmp/openhcs-current-illumination3-native-timing-science-v1-20261002.json``. + +Wound's actual worker profile partitions 3.387111s into 1.536850s of disjoint raw +processing and 1.850261s of other runtime/recording work. The 22 stack operations +include 0.4036s in 16 aggregate image stacks, 0.1481s in object variants, 0.0538s +in main-flow/cache and 0.0023s in initial loading. Ten source-record +normalizations account for 0.2436s of the 0.5510s normalization envelope. These +are profiled costs and overlap the earlier source-query attribution; none are +accepted saved seconds. + +Actual Wound ON_DEMAND QA images are not forced by terminal OUTCOMES publication. +However, raw QA planes borrow object-variant pixels and shared validity masks; +their current aggregates establish independent mutable artifact buffers. +Deferral with equivalent eager per-plane copies cannot remove those copy bytes, +and zero-copy adoption without an ownership/lifetime proof is unsound. Existing +ArrayBridge geometry validation also forces concrete arrays at payload +construction. No lazy wrapper or second mutable identity cache is promoted. +The global AST owner/lifetime audit retains a bounded actual alias-capture plan: +``/var/tmp/openhcs-current9aa-wound-copy-lifetime-reassessment-20261002.json``. + +An external shared-geometry convex prototype preserves both actual saved input +graphs exactly. The outer 256-level call's warm local median changes from +0.260906s to 0.023542s, saving 0.237364s; the nested 16-level call overlaps it +and is never added as independent work. This is useful local evidence, not a +production pipeline gain. The nominal owner audit identifies existing shared +column-envelope and Bresenham authorities and a disconnected 15-kernel legacy +component; production migration and end-to-end qualification remain separate. +The shared dense-coordinate prototype also has exact actual Wound/Track input +replays and passing scoped R0/R1 after removing duplicated admission logic. +Its pre-domain allocation behavior for very sparse large label IDs remains +under review. Neither prototype is integrated here or counted as a speedup. + +Independent tracking PR468 is synced to main and passes 19 focused tests plus +unchanged scoped R0/R1. It remains draft: native saved-image differences shrink +from 77,726 to 3,340 text pixels, but full native PNG parity is still RED. It +formally links issue467. No parity gate, tolerance, scientific output or renderer +environment is weakened, and no tracking repair is included in this branch. + +Remaining acceptance still includes installed-root issue419 IPO/Shape and +Morph/DISTANCE consumers; issue433's original 13.36GB OOM allocation owner and +repeated READY behavior; installed-root issue435; and issue450's installed +prepared-Resize consumer. The original whole-branch scalar-classifier R0 RED +remains separately retained. These draft closing references do not establish +completion. Full native scaling, all30 benchmark reruns and fresh figures remain +outstanding, and the long-running performance goal stays active. From b4700288f6762674e5d79f95730e20cf6d348f16 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 15:57:03 -0400 Subject: [PATCH 082/235] perf: share exact CP geometry in incremental quantized hulls Reuse the label column-envelope walk and Bresenham segment rasterizer across illumination, label hulls, and worms. Keep original float quantization and recursive level semantics, preserve owned public line arrays, and prepare every reached geometry signature before execution. Remove the disconnected legacy diamond-hull component. Refs #394. --- .../backends/cellprofiler/illumination.py | 718 +++--------------- .../backends/cellprofiler/label_geometry.py | 90 ++- .../backends/cellprofiler/worm_geometry.py | 78 +- .../test_cellprofiler_kernel_preparation.py | 78 ++ tests/unit/test_cellprofiler_worm_geometry.py | 42 + 5 files changed, 324 insertions(+), 682 deletions(-) diff --git a/openhcs/processing/backends/cellprofiler/illumination.py b/openhcs/processing/backends/cellprofiler/illumination.py index 1dab00b4e5..3c53edf1f8 100644 --- a/openhcs/processing/backends/cellprofiler/illumination.py +++ b/openhcs/processing/backends/cellprofiler/illumination.py @@ -71,6 +71,7 @@ from openhcs.core.runtime_profile import RuntimeProfiler from openhcs.processing.backends.cellprofiler.label_geometry import ( CellProfilerLabelHull, + _cellprofiler_column_envelope_vertices_numba, ) from openhcs.processing.backends.cellprofiler.morphology import ( MorphologyBackendStrategy, @@ -84,6 +85,7 @@ ) from openhcs.processing.backends.cellprofiler.worm_geometry import ( _cellprofiler_line_points, + _fill_cellprofiler_line_points_numba, ) from openhcs.processing.backends.lib_registry.unified_registry import ProcessingContract @@ -1804,6 +1806,12 @@ def prepare_backend(self) -> None: self.smooth_background_plane( image, mask=mask, filter_size=3, morphology=morphology ) + _cellprofiler_convex_hull_transform(np.asfortranarray(image), mask) + endpoints = np.array([0, 3], dtype=int) + _cellprofiler_line_points(endpoints, endpoints, endpoints[::-1], endpoints) + CellProfilerLabelHull.from_labels( + (image > 0.5).astype(np.int32), np.array([1], dtype=np.int32) + ).vertices() def smooth_background_plane( self, @@ -1906,44 +1914,101 @@ def _cellprofiler_convex_hull_transform( ) scaled = np.maximum(scaled, rough) scaled = scaled.astype(np.int32) - unique_levels = np.unique(scaled) - output_levels = np.full( - image_array.shape, - int(unique_levels[0]), - dtype=np.int32, - ) + output_levels = _incremental_quantized_hulls_numba(scaled) + return scale[output_levels] - for level in unique_levels[1:]: - level_mask = scaled >= int(level) - label_hull = CellProfilerLabelHull.from_labels( - level_mask.astype(np.int32), - np.array([1], dtype=np.int32), - ) - hull, counts = label_hull.vertices() - if counts[0] == 0: + +@njit(cache=True) +def _paint_column_envelope_level_numba( + vertices: np.ndarray, + vertex_count: int, + output: np.ndarray, + assigned: np.ndarray, + level: int, + line_rows: np.ndarray, + line_columns: np.ndarray, +) -> None: + """Fill each column enclosed by the canonical CP rasterized hull edges.""" + width = output.shape[1] + low = np.full(width, np.iinfo(np.int64).max, np.int64) + high = np.full(width, np.iinfo(np.int64).min, np.int64) + for segment in range(vertex_count): + following = (segment + 1) % vertex_count + count = _fill_cellprofiler_line_points_numba( + vertices[segment, 0], + vertices[segment, 1], + vertices[following, 0], + vertices[following, 1], + line_rows, + line_columns, + 0, + ) + for position in range(count): + column = line_columns[position] + row = line_rows[position] + low[column] = min(low[column], row) + high[column] = max(high[column], row) + for column in range(width): + if low[column] == np.iinfo(np.int64).max: continue - vertices = hull[:, 1:] - next_vertices = np.roll(vertices, -1, axis=0) - _indexes, _counts, rows, columns = _cellprofiler_line_points( - vertices[:, 0], - vertices[:, 1], - next_vertices[:, 0], - next_vertices[:, 1], - ) - row_minimum = np.full( - image_array.shape[1], np.iinfo(np.int64).max, dtype=np.int64 - ) - row_maximum = np.full( - image_array.shape[1], np.iinfo(np.int64).min, dtype=np.int64 - ) - np.minimum.at(row_minimum, columns, rows) - np.maximum.at(row_maximum, columns, rows) - for column in np.unique(columns): - output_levels[ - int(row_minimum[column]) : int(row_maximum[column]) + 1, - int(column), - ] = int(level) - return scale[output_levels] + for row in range(low[column], high[column] + 1): + if not assigned[row, column]: + output[row, column] = level + assigned[row, column] = True + + +@njit(cache=True) +def _incremental_quantized_hulls_numba(scaled: np.ndarray) -> np.ndarray: + """Reuse cumulative column extrema for descending quantized level sets.""" + height, width = scaled.shape + flat = scaled.ravel() + levels = np.unique(flat) + # Index the observed codes, rather than allocating across a possibly sparse + # or invalid int32 range produced by the original floating-point cast. + counts = np.zeros(levels.size, np.int64) + for value in flat: + counts[np.searchsorted(levels, value)] += 1 + offsets = np.zeros(levels.size + 1, np.int64) + for index in range(levels.size): + offsets[index + 1] = offsets[index] + counts[index] + cursors = offsets.copy() + positions = np.empty(flat.size, np.int64) + for position in range(flat.size): + index = np.searchsorted(levels, flat[position]) + positions[cursors[index]] = position + cursors[index] += 1 + row_minimum = np.full(width, np.iinfo(np.int64).max, np.int64) + row_maximum = np.full(width, np.iinfo(np.int64).min, np.int64) + vertices = np.empty((width * 2, 2), np.int64) + line_rows = np.empty(max(height, width), np.int64) + line_columns = np.empty(line_rows.size, np.int64) + output = np.full((height, width), levels[0], np.int32) + assigned = np.zeros((height, width), np.bool_) + for level_index in range(levels.size - 1, 0, -1): + changed = False + for cursor in range(offsets[level_index], offsets[level_index + 1]): + position = positions[cursor] + row, column = position // width, position % width + if row < row_minimum[column]: + row_minimum[column] = row + changed = True + if row > row_maximum[column]: + row_maximum[column] = row + changed = True + if changed: + vertex_count = _cellprofiler_column_envelope_vertices_numba( + row_minimum, row_maximum, 0, vertices + ) + _paint_column_envelope_level_numba( + vertices, + vertex_count, + output, + assigned, + levels[level_index], + line_rows, + line_columns, + ) + return output @dataclass(frozen=True, slots=True) @@ -2001,587 +2066,6 @@ def _restore_masked_pixels( return result -@njit(cache=True) -def _exact_level_set_convex_hull_smoothing_numba( - image: np.ndarray, valid_mask: np.ndarray, thresholds: np.ndarray -) -> np.ndarray: - height, width = image.shape - minimum = np.float32(0.0) - maximum = np.float32(0.0) - found_valid = False - valid_pixel_count = 0 - for y in range(height): - for x in range(width): - if not valid_mask[y, x]: - continue - valid_pixel_count += 1 - value = image[y, x] - if not found_valid: - minimum = value - maximum = value - found_valid = True - else: - if value < minimum: - minimum = value - if value > maximum: - maximum = value - output = np.empty((height, width), dtype=np.float32) - for y in range(height): - for x in range(width): - output[y, x] = minimum if valid_mask[y, x] else np.float32(0.0) - if not found_valid or maximum <= minimum: - return output - row_count2 = height * 2 + 1 - min_col_by_row = np.empty(row_count2, dtype=np.int64) - max_col_by_row = np.empty(row_count2, dtype=np.int64) - point_capacity = max(2, row_count2 * 2) - point_x = np.empty(point_capacity, dtype=np.int64) - point_y = np.empty(point_capacity, dtype=np.int64) - hull_x = np.empty(point_capacity * 2, dtype=np.int64) - hull_y = np.empty(point_capacity * 2, dtype=np.int64) - bucket_counts = np.zeros(thresholds.size, dtype=np.int64) - active_pixel_count = _count_convex_hull_threshold_buckets( - image, valid_mask, thresholds, bucket_counts - ) - if active_pixel_count == 0: - return output - bucket_offsets = np.empty(thresholds.size + 1, dtype=np.int64) - offset = 0 - for bucket_index in range(thresholds.size): - bucket_offsets[bucket_index] = offset - offset += bucket_counts[bucket_index] - bucket_counts[bucket_index] = 0 - bucket_offsets[thresholds.size] = offset - bucket_rows = np.empty(active_pixel_count, dtype=np.int64) - bucket_cols = np.empty(active_pixel_count, dtype=np.int64) - _fill_convex_hull_threshold_buckets( - image, - valid_mask, - thresholds, - bucket_offsets, - bucket_counts, - bucket_rows, - bucket_cols, - ) - assigned = np.zeros((height, width), dtype=np.bool_) - for row_index in range(row_count2): - min_col_by_row[row_index] = 9223372036854775807 - max_col_by_row[row_index] = -9223372036854775807 - assigned_count = 0 - for level_index in range(thresholds.size - 1, -1, -1): - start = bucket_offsets[level_index] - end = bucket_offsets[level_index] + bucket_counts[level_index] - if start == end: - continue - changed_extrema = False - for bucket_position in range(start, end): - y = bucket_rows[bucket_position] - x = bucket_cols[bucket_position] - if _add_diamond_vertex(min_col_by_row, max_col_by_row, 2 * y - 1, 2 * x): - changed_extrema = True - if _add_diamond_vertex(min_col_by_row, max_col_by_row, 2 * y + 1, 2 * x): - changed_extrema = True - if _add_diamond_vertex(min_col_by_row, max_col_by_row, 2 * y, 2 * x - 1): - changed_extrema = True - if _add_diamond_vertex(min_col_by_row, max_col_by_row, 2 * y, 2 * x + 1): - changed_extrema = True - if not changed_extrema: - continue - point_count = _emit_diamond_extreme_points( - min_col_by_row, max_col_by_row, point_x, point_y - ) - if point_count == 0: - continue - hull_count = _monotone_chain_hull(point_x, point_y, point_count, hull_x, hull_y) - assigned_count += _paint_convex_hull( - output, - assigned, - False, - valid_mask, - thresholds[level_index], - hull_x, - hull_y, - hull_count, - ) - if assigned_count >= valid_pixel_count: - break - return output - - -@njit(cache=True) -def _count_convex_hull_threshold_buckets( - image: np.ndarray, - valid_mask: np.ndarray, - thresholds: np.ndarray, - bucket_counts: np.ndarray, -) -> int: - height, width = image.shape - active_pixel_count = 0 - for y in range(height): - for x in range(width): - if not valid_mask[y, x]: - continue - bucket_index = _last_threshold_index_not_greater_than( - thresholds, image[y, x] - ) - if bucket_index < 0: - continue - bucket_counts[bucket_index] += 1 - active_pixel_count += 1 - return active_pixel_count - - -@njit(cache=True) -def _fill_convex_hull_threshold_buckets( - image: np.ndarray, - valid_mask: np.ndarray, - thresholds: np.ndarray, - bucket_offsets: np.ndarray, - bucket_counts: np.ndarray, - bucket_rows: np.ndarray, - bucket_cols: np.ndarray, -) -> None: - height, width = image.shape - for y in range(height): - for x in range(width): - if not valid_mask[y, x]: - continue - bucket_index = _last_threshold_index_not_greater_than( - thresholds, image[y, x] - ) - if bucket_index < 0: - continue - bucket_position = bucket_offsets[bucket_index] + bucket_counts[bucket_index] - bucket_rows[bucket_position] = y - bucket_cols[bucket_position] = x - bucket_counts[bucket_index] += 1 - - -@njit(cache=True) -def _last_threshold_index_not_greater_than( - thresholds: np.ndarray, value: np.float32 -) -> int: - low = 0 - high = thresholds.size - while low < high: - middle = (low + high) // 2 - if thresholds[middle] <= value: - low = middle + 1 - else: - high = middle - return low - 1 - - -@njit(cache=True) -def _emit_diamond_extreme_points( - min_col_by_row: np.ndarray, - max_col_by_row: np.ndarray, - point_x: np.ndarray, - point_y: np.ndarray, -) -> int: - point_count = 0 - for row_index in range(max_col_by_row.size): - max_col = max_col_by_row[row_index] - if max_col < -9223372036854775800: - continue - row2 = row_index - 1 - min_col = min_col_by_row[row_index] - point_x[point_count] = row2 - point_y[point_count] = min_col - point_count += 1 - if max_col != min_col: - point_x[point_count] = row2 - point_y[point_count] = max_col - point_count += 1 - return point_count - - -@njit(cache=True) -def _collect_diamond_extreme_points( - image: np.ndarray, - valid_mask: np.ndarray, - threshold: np.float32, - min_col_by_row: np.ndarray, - max_col_by_row: np.ndarray, - point_x: np.ndarray, - point_y: np.ndarray, -) -> int: - height, width = image.shape - row_count2 = height * 2 + 1 - for row_index in range(row_count2): - min_col_by_row[row_index] = 9223372036854775807 - max_col_by_row[row_index] = -9223372036854775807 - for y in range(height): - for x in range(width): - if valid_mask[y, x] and image[y, x] >= threshold: - _add_diamond_vertex(min_col_by_row, max_col_by_row, 2 * y - 1, 2 * x) - _add_diamond_vertex(min_col_by_row, max_col_by_row, 2 * y + 1, 2 * x) - _add_diamond_vertex(min_col_by_row, max_col_by_row, 2 * y, 2 * x - 1) - _add_diamond_vertex(min_col_by_row, max_col_by_row, 2 * y, 2 * x + 1) - return _emit_diamond_extreme_points( - min_col_by_row, max_col_by_row, point_x, point_y - ) - - -@njit(cache=True) -def _exact_level_set_convex_hull_smoothing_reference_numba( - image: np.ndarray, valid_mask: np.ndarray, thresholds: np.ndarray -) -> np.ndarray: - height, width = image.shape - minimum = np.float32(0.0) - maximum = np.float32(0.0) - found_valid = False - for y in range(height): - for x in range(width): - if not valid_mask[y, x]: - continue - value = image[y, x] - if not found_valid: - minimum = value - maximum = value - found_valid = True - else: - if value < minimum: - minimum = value - if value > maximum: - maximum = value - output = np.empty((height, width), dtype=np.float32) - for y in range(height): - for x in range(width): - output[y, x] = minimum if valid_mask[y, x] else np.float32(0.0) - if not found_valid or maximum <= minimum: - return output - row_count2 = height * 2 + 1 - min_col_by_row = np.empty(row_count2, dtype=np.int64) - max_col_by_row = np.empty(row_count2, dtype=np.int64) - point_capacity = max(2, row_count2 * 2) - point_x = np.empty(point_capacity, dtype=np.int64) - point_y = np.empty(point_capacity, dtype=np.int64) - hull_x = np.empty(point_capacity * 2, dtype=np.int64) - hull_y = np.empty(point_capacity * 2, dtype=np.int64) - assigned = np.ones((height, width), dtype=np.bool_) - for level_index in range(thresholds.size): - threshold = thresholds[level_index] - point_count = _collect_diamond_extreme_points( - image, - valid_mask, - threshold, - min_col_by_row, - max_col_by_row, - point_x, - point_y, - ) - if point_count == 0: - continue - hull_count = _monotone_chain_hull(point_x, point_y, point_count, hull_x, hull_y) - _paint_convex_hull( - output, assigned, True, valid_mask, threshold, hull_x, hull_y, hull_count - ) - return output - - -@njit(cache=True) -def _add_diamond_vertex( - min_col_by_row: np.ndarray, max_col_by_row: np.ndarray, row2: int, col2: int -) -> bool: - row_index = row2 + 1 - changed = False - if col2 < min_col_by_row[row_index]: - min_col_by_row[row_index] = col2 - changed = True - if col2 > max_col_by_row[row_index]: - max_col_by_row[row_index] = col2 - changed = True - return changed - - -@njit(cache=True) -def _cross_points(ax: int, ay: int, bx: int, by: int, cx: int, cy: int) -> int: - return (bx - ax) * (cy - ay) - (by - ay) * (cx - ax) - - -@njit(cache=True) -def _monotone_chain_hull( - point_x: np.ndarray, - point_y: np.ndarray, - point_count: int, - hull_x: np.ndarray, - hull_y: np.ndarray, -) -> int: - if point_count <= 1: - if point_count == 1: - hull_x[0] = point_x[0] - hull_y[0] = point_y[0] - return point_count - hull_count = 0 - for index in range(point_count): - px = point_x[index] - py = point_y[index] - while ( - hull_count >= 2 - and _cross_points( - hull_x[hull_count - 2], - hull_y[hull_count - 2], - hull_x[hull_count - 1], - hull_y[hull_count - 1], - px, - py, - ) - <= 0 - ): - hull_count -= 1 - hull_x[hull_count] = px - hull_y[hull_count] = py - hull_count += 1 - lower_count = hull_count - for index in range(point_count - 2, -1, -1): - px = point_x[index] - py = point_y[index] - while ( - hull_count > lower_count - and _cross_points( - hull_x[hull_count - 2], - hull_y[hull_count - 2], - hull_x[hull_count - 1], - hull_y[hull_count - 1], - px, - py, - ) - <= 0 - ): - hull_count -= 1 - hull_x[hull_count] = px - hull_y[hull_count] = py - hull_count += 1 - if hull_count > 1: - hull_count -= 1 - return hull_count - - -@njit(cache=True) -def _paint_convex_hull( - output: np.ndarray, - assigned: np.ndarray, - overwrite_assigned: bool, - valid_mask: np.ndarray, - threshold: np.float32, - hull_x: np.ndarray, - hull_y: np.ndarray, - hull_count: int, -) -> int: - if hull_count <= 0: - return 0 - if hull_count == 1: - if hull_x[0] % 2 != 0 or hull_y[0] % 2 != 0: - return 0 - y = hull_x[0] // 2 - x = hull_y[0] // 2 - if ( - y >= 0 - and y < valid_mask.shape[0] - and (x >= 0) - and (x < valid_mask.shape[1]) - and valid_mask[y, x] - and (overwrite_assigned or not assigned[y, x]) - ): - output[y, x] = threshold - was_unassigned = not assigned[y, x] - assigned[y, x] = True - return 1 if was_unassigned else 0 - return 0 - min_row2 = hull_x[0] - max_row2 = hull_x[0] - min_col2 = hull_y[0] - max_col2 = hull_y[0] - for index in range(1, hull_count): - row2 = hull_x[index] - col2 = hull_y[index] - if row2 < min_row2: - min_row2 = row2 - if row2 > max_row2: - max_row2 = row2 - if col2 < min_col2: - min_col2 = col2 - if col2 > max_col2: - max_col2 = col2 - if hull_count == 2: - return _paint_line_hull( - output, - assigned, - overwrite_assigned, - valid_mask, - threshold, - hull_x[0], - hull_y[0], - hull_x[1], - hull_y[1], - min_row2, - max_row2, - min_col2, - max_col2, - ) - image_height, image_width = output.shape - min_y = max(0, _ceil_div2(min_row2)) - max_y = min(image_height - 1, _floor_div2(max_row2)) - min_x = max(0, _ceil_div2(min_col2)) - max_x = min(image_width - 1, _floor_div2(max_col2)) - return _paint_polygon_hull_scanlines( - output, - assigned, - overwrite_assigned, - valid_mask, - threshold, - hull_x, - hull_y, - hull_count, - min_y, - max_y, - min_x, - max_x, - ) - - -@njit(cache=True) -def _paint_polygon_hull_scanlines( - output: np.ndarray, - assigned: np.ndarray, - overwrite_assigned: bool, - valid_mask: np.ndarray, - threshold: np.float32, - hull_x: np.ndarray, - hull_y: np.ndarray, - hull_count: int, - min_y: int, - max_y: int, - min_x: int, - max_x: int, -) -> int: - assigned_delta = 0 - for y in range(min_y, max_y + 1): - query_row2 = y * 2 - left_col2 = 0.0 - right_col2 = 0.0 - found_intersection = False - for index in range(hull_count): - next_index = index + 1 - if next_index == hull_count: - next_index = 0 - row0 = hull_x[index] - col0 = hull_y[index] - row1 = hull_x[next_index] - col1 = hull_y[next_index] - if row0 == row1: - if query_row2 != row0: - continue - edge_left = float(min(col0, col1)) - edge_right = float(max(col0, col1)) - if not found_intersection: - left_col2 = edge_left - right_col2 = edge_right - found_intersection = True - else: - if edge_left < left_col2: - left_col2 = edge_left - if edge_right > right_col2: - right_col2 = edge_right - continue - row_min = min(row0, row1) - row_max = max(row0, row1) - if query_row2 < row_min or query_row2 > row_max: - continue - row_fraction = (query_row2 - row0) / (row1 - row0) - intersection_col2 = col0 + row_fraction * (col1 - col0) - if not found_intersection: - left_col2 = intersection_col2 - right_col2 = intersection_col2 - found_intersection = True - else: - if intersection_col2 < left_col2: - left_col2 = intersection_col2 - if intersection_col2 > right_col2: - right_col2 = intersection_col2 - if not found_intersection: - continue - scan_min_x = max(min_x, int(np.ceil(left_col2 / 2.0 - 1e-09))) - scan_max_x = min(max_x, int(np.floor(right_col2 / 2.0 + 1e-09))) - for x in range(scan_min_x, scan_max_x + 1): - if not valid_mask[y, x]: - continue - if not overwrite_assigned and assigned[y, x]: - continue - output[y, x] = threshold - if not assigned[y, x]: - assigned_delta += 1 - assigned[y, x] = True - return assigned_delta - - -@njit(cache=True) -def _ceil_div2(value: int) -> int: - if value >= 0: - return (value + 1) // 2 - return value // 2 - - -@njit(cache=True) -def _floor_div2(value: int) -> int: - if value >= 0: - return value // 2 - return -((-value + 1) // 2) - - -@njit(cache=True) -def _paint_line_hull( - output: np.ndarray, - assigned: np.ndarray, - overwrite_assigned: bool, - valid_mask: np.ndarray, - threshold: np.float32, - x0: int, - y0: int, - x1: int, - y1: int, - min_row2: int, - max_row2: int, - min_col2: int, - max_col2: int, -) -> int: - dx = x1 - x0 - dy = y1 - y0 - length2 = dx * dx + dy * dy - if length2 == 0: - if valid_mask[y0, x0] and (overwrite_assigned or not assigned[y0, x0]): - output[y0, x0] = threshold - was_unassigned = not assigned[y0, x0] - assigned[y0, x0] = True - return 1 if was_unassigned else 0 - return 0 - image_height, image_width = output.shape - min_y = max(0, _ceil_div2(min_row2)) - max_y = min(image_height - 1, _floor_div2(max_row2)) - min_x = max(0, _ceil_div2(min_col2)) - max_x = min(image_width - 1, _floor_div2(max_col2)) - assigned_delta = 0 - for y in range(min_y, max_y + 1): - query_row2 = y * 2 - for x in range(min_x, max_x + 1): - if not valid_mask[y, x]: - continue - if not overwrite_assigned and assigned[y, x]: - continue - query_col2 = x * 2 - dot = (query_row2 - x0) * dx + (query_col2 - y0) * dy - if dot < 0 or dot > length2: - continue - cross = dx * (query_col2 - y0) - dy * (query_row2 - x0) - if cross == 0: - output[y, x] = threshold - if not assigned[y, x]: - assigned_delta += 1 - assigned[y, x] = True - return assigned_delta - - @njit(cache=True) def _rank_median_global_minimum_is_majority_everywhere_numba( image: np.ndarray, diff --git a/openhcs/processing/backends/cellprofiler/label_geometry.py b/openhcs/processing/backends/cellprofiler/label_geometry.py index 61fef5884c..9dc88da3af 100644 --- a/openhcs/processing/backends/cellprofiler/label_geometry.py +++ b/openhcs/processing/backends/cellprofiler/label_geometry.py @@ -356,6 +356,53 @@ def _outline_points_at_positions_numba( return counts, offsets, ys, xs +@njit(cache=True) +def _cellprofiler_column_envelope_vertices_numba( + row_minimum: np.ndarray, + row_maximum: np.ndarray, + column_offset: int, + vertices: np.ndarray, +) -> int: + """Fill caller-owned vertices using CP's exact ordered envelope walk.""" + columns = np.flatnonzero(row_minimum != np.iinfo(np.int64).max) + vertex_count = 0 + for k in range(2 * columns.size): + if k < columns.size: + col = columns[k] + y = row_minimum[col] + else: + col = columns[2 * columns.size - 1 - k] + y = row_maximum[col] + x = col + column_offset + if ( + vertex_count + and vertices[vertex_count - 1, 0] == y + and vertices[vertex_count - 1, 1] == x + ): + continue + while vertex_count >= 2: + previous_y = vertices[vertex_count - 2, 0] + previous_x = vertices[vertex_count - 2, 1] + middle_y = vertices[vertex_count - 1, 0] + middle_x = vertices[vertex_count - 1, 1] + cross = (middle_x - previous_x) * (y - middle_y) - (x - middle_x) * ( + middle_y - previous_y + ) + if cross > 0 or (cross == 0 and middle_x > previous_x and middle_x > x): + break + vertex_count -= 1 + vertices[vertex_count, 0] = y + vertices[vertex_count, 1] = x + vertex_count += 1 + if ( + vertex_count > 1 + and vertices[vertex_count - 1, 0] == vertices[0, 0] + and vertices[vertex_count - 1, 1] == vertices[0, 1] + ): + vertex_count -= 1 + return vertex_count + + @njit(cache=True) def _cellprofiler_hull_vertices_numba( indexes: np.ndarray, @@ -385,43 +432,11 @@ def _cellprofiler_hull_vertices_numba( y = point_y[p] row_minimum[col] = min(row_minimum[col], y) row_maximum[col] = max(row_maximum[col], y) - columns = np.flatnonzero(row_minimum != np.iinfo(np.int64).max) - vertices = np.empty((2 * columns.size, 2), np.int64) - vertex_count = 0 - for k in range(2 * columns.size): - if k < columns.size: - col = columns[k] - y = row_minimum[col] - else: - col = columns[2 * columns.size - 1 - k] - y = row_maximum[col] - x = col + first - if ( - vertex_count - and vertices[vertex_count - 1, 0] == y - and vertices[vertex_count - 1, 1] == x - ): - continue - while vertex_count >= 2: - previous_y = vertices[vertex_count - 2, 0] - previous_x = vertices[vertex_count - 2, 1] - middle_y = vertices[vertex_count - 1, 0] - middle_x = vertices[vertex_count - 1, 1] - cross = (middle_x - previous_x) * (y - middle_y) - (x - middle_x) * ( - middle_y - previous_y - ) - if cross > 0 or (cross == 0 and middle_x > previous_x and middle_x > x): - break - vertex_count -= 1 - vertices[vertex_count, 0] = y - vertices[vertex_count, 1] = x - vertex_count += 1 - if ( - vertex_count > 1 - and vertices[vertex_count - 1, 0] == vertices[0, 0] - and vertices[vertex_count - 1, 1] == vertices[0, 1] - ): - vertex_count -= 1 + column_count = np.count_nonzero(row_minimum != np.iinfo(np.int64).max) + vertices = np.empty((2 * column_count, 2), np.int64) + vertex_count = _cellprofiler_column_envelope_vertices_numba( + row_minimum, row_maximum, first, vertices + ) hull_counts[object_index] = vertex_count for p in range(vertex_count): output[output_count, 0] = indexes[object_index] @@ -621,6 +636,7 @@ def _numpy124_partition_indices_numba( current_depth = stack_depth[stack_size] return indices + def _numpy124_aquicksort_indices(values: np.ndarray) -> np.ndarray: return _numpy124_partition_indices_numba(np.asarray(values)) diff --git a/openhcs/processing/backends/cellprofiler/worm_geometry.py b/openhcs/processing/backends/cellprofiler/worm_geometry.py index c9af20347d..54a8d6102d 100644 --- a/openhcs/processing/backends/cellprofiler/worm_geometry.py +++ b/openhcs/processing/backends/cellprofiler/worm_geometry.py @@ -371,6 +371,47 @@ def sample_control_points( return np.vstack((path_coords[:1, :], sampled, path_coords[-1:, :])) +@njit(cache=True) +def _fill_cellprofiler_line_points_numba( + row: int, + column: int, + end_row: int, + end_column: int, + rows: np.ndarray, + columns: np.ndarray, + output_index: int, +) -> int: + """Fill one inclusive segment with CP's Bresenham endpoint and tie rules.""" + row_delta = abs(row - end_row) + column_delta = abs(column - end_column) + count = max(row_delta, column_delta) + 1 + rows[output_index] = row + columns[output_index] = column + step_row = 1 if end_row > row else -1 + step_column = 1 if end_column > column else -1 + if row_delta >= column_delta: + remainder = 2 * column_delta - row_delta + for offset in range(1, count): + if remainder >= 0: + column += step_column + remainder -= 2 * row_delta + row += step_row + remainder += 2 * column_delta + rows[output_index + offset] = row + columns[output_index + offset] = column + else: + remainder = 2 * row_delta - column_delta + for offset in range(1, count): + if remainder >= 0: + row += step_row + remainder -= 2 * column_delta + column += step_column + remainder += 2 * row_delta + rows[output_index + offset] = row + columns[output_index + offset] = column + return count + + def _cellprofiler_line_points( start_rows: np.ndarray, start_columns: np.ndarray, @@ -393,34 +434,15 @@ def _cellprofiler_line_points( columns = np.empty(rows.size, dtype=int) for segment in range(len(count)): - output_index = int(index[segment]) - row = int(row0[segment]) - column = int(column0[segment]) - rows[output_index] = row - columns[output_index] = column - step_row = 1 if row1[segment] > row0[segment] else -1 - step_column = 1 if column1[segment] > column0[segment] else -1 - - if row_delta[segment] >= column_delta[segment]: - remainder = 2 * int(column_delta[segment]) - int(row_delta[segment]) - for offset in range(1, int(count[segment])): - if remainder >= 0: - column += step_column - remainder -= 2 * int(row_delta[segment]) - row += step_row - remainder += 2 * int(column_delta[segment]) - rows[output_index + offset] = row - columns[output_index + offset] = column - else: - remainder = 2 * int(row_delta[segment]) - int(column_delta[segment]) - for offset in range(1, int(count[segment])): - if remainder >= 0: - row += step_row - remainder -= 2 * int(column_delta[segment]) - column += step_column - remainder += 2 * int(row_delta[segment]) - rows[output_index + offset] = row - columns[output_index + offset] = column + _fill_cellprofiler_line_points_numba( + int(row0[segment]), + int(column0[segment]), + int(row1[segment]), + int(column1[segment]), + rows, + columns, + int(index[segment]), + ) return index, count, rows, columns diff --git a/tests/unit/test_cellprofiler_kernel_preparation.py b/tests/unit/test_cellprofiler_kernel_preparation.py index 9295d7bcf9..c438e686fb 100644 --- a/tests/unit/test_cellprofiler_kernel_preparation.py +++ b/tests/unit/test_cellprofiler_kernel_preparation.py @@ -228,3 +228,81 @@ def reject_late_compilation(signature): timeout=90, ) assert result.returncode == 0, result.stdout + result.stderr + + +def test_illumination_preparation_covers_shared_hull_and_line_geometry(tmp_path): + """Prepared geometry must not compile or load kernels during execution.""" + script = textwrap.dedent(""" + from contextlib import ExitStack + from unittest.mock import patch + import numpy as np + import centrosome.filter + import centrosome.cpmorphology + from openhcs.core.callable_contract import prepare_processing_callable + from openhcs.processing.backends.cellprofiler import illumination, label_geometry, worm_geometry + + dispatchers = ( + illumination._incremental_quantized_hulls_numba, + illumination._paint_column_envelope_level_numba, + label_geometry._cellprofiler_column_envelope_vertices_numba, + worm_geometry._fill_cellprofiler_line_points_numba, + ) + assert all(not dispatcher.signatures for dispatcher in dispatchers) + prepare_processing_callable(illumination.correct_illumination_calculate) + before = tuple(tuple(dispatcher.signatures) for dispatcher in dispatchers) + assert all(before) + layouts = {signature[0].layout for signature in dispatchers[0].signatures} + assert layouts == {"C", "F"}, layouts + + def reject_late_compilation(signature): + raise AssertionError("Shared geometry compiled or loaded after READY: " + str(signature)) + + rng = np.random.default_rng(419) + source = rng.random((13, 17)) + partial_mask = rng.random(source.shape) > 0.2 + with ExitStack() as stack: + for dispatcher in dispatchers: + stack.enter_context(patch.object(dispatcher, "compile", reject_late_compilation)) + for dtype in (np.float32, np.float64): + for view in (source.astype(dtype), np.asfortranarray(source, dtype=dtype), source[::-1, ::-1].astype(dtype)): + for writeable in (True, False): + view.flags.writeable = writeable + pixels = view.copy() + for mask in (None, partial_mask): + mask_before = None if mask is None else mask.copy() + for levels in (16, 256): + expected = centrosome.filter.convex_hull_transform(view, mask=mask, levels=levels) + actual = illumination._cellprofiler_convex_hull_transform(view, mask, levels=levels) + np.testing.assert_array_equal(actual, expected) + np.testing.assert_array_equal(view, pixels) + if mask is not None: + np.testing.assert_array_equal(mask, mask_before) + assert not np.shares_memory(actual, view) + labels = np.zeros((11, 13), dtype=np.int32) + labels[2:8, 4:10] = 1 + indexes = np.array([1], dtype=np.int32) + expected_hull = centrosome.cpmorphology.convex_hull(labels, indexes) + actual_hull = label_geometry.CellProfilerLabelHull.from_labels(labels, indexes).vertices() + for actual, expected in zip(actual_hull, expected_hull): + np.testing.assert_array_equal(actual, expected) + endpoints = (np.array([0, 4]), np.array([0, -2]), np.array([4, 0]), np.array([2, 0])) + expected_lines = centrosome.cpmorphology.get_line_pts(*endpoints) + for actual, expected in zip(worm_geometry._cellprofiler_line_points(*endpoints), expected_lines): + np.testing.assert_array_equal(actual, expected) + assert actual.flags.owndata and actual.flags.writeable + assert tuple(tuple(dispatcher.signatures) for dispatcher in dispatchers) == before + """) + environment = os.environ.copy() + environment.update( + {"OPENHCS_CPU_ONLY": "true", "NUMBA_CACHE_DIR": str(tmp_path / "kernels")} + ) + result = subprocess.run( + (sys.executable, "-c", script), + cwd=Path(__file__).parents[2], + env=environment, + check=False, + capture_output=True, + text=True, + timeout=120, + ) + assert result.returncode == 0, result.stdout + result.stderr diff --git a/tests/unit/test_cellprofiler_worm_geometry.py b/tests/unit/test_cellprofiler_worm_geometry.py index fae7fb7029..07d6742c60 100644 --- a/tests/unit/test_cellprofiler_worm_geometry.py +++ b/tests/unit/test_cellprofiler_worm_geometry.py @@ -76,3 +76,45 @@ def test_skeletonize_preserves_centrosome_top_border_behavior() -> None: skeletonize_worm_mask(np.ones((2, 2), dtype=bool)), np.array([[True, True], [False, False]]), ) + + +def test_line_points_match_centrosome_all_octants_and_keep_owned_outputs() -> None: + import centrosome.cpmorphology + + grid = np.arange(-3, 4) + end_rows, end_columns = np.meshgrid(grid, grid) + end_rows = end_rows.ravel() + end_columns = end_columns.ravel() + start_rows = np.zeros_like(end_rows) + start_columns = np.zeros_like(end_columns) + for endpoints in ( + (start_rows, start_columns, end_rows, end_columns), + (end_rows, end_columns, start_rows, start_columns), + tuple(np.empty(0, dtype=int) for _ in range(4)), + ): + expected = centrosome.cpmorphology.get_line_pts(*endpoints) + actual = _cellprofiler_line_points(*endpoints) + for result, oracle in zip(actual, expected): + np.testing.assert_array_equal(result, oracle) + assert result.dtype == np.dtype(int) + assert result.flags.owndata and result.flags.writeable + assert all(not np.shares_memory(result, source) for source in endpoints) + assert all( + not np.shares_memory(left, right) + for index, left in enumerate(actual) + for right in actual[index + 1 :] + ) + + +def test_line_points_reject_unequal_endpoint_lengths() -> None: + import pytest + + with pytest.raises( + ValueError, match="Line endpoint arrays must have equal lengths" + ): + _cellprofiler_line_points( + np.array([0, 1]), + np.array([0]), + np.array([2]), + np.array([2]), + ) From f70390176a1774115792e1721d9909b0badb4817 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 16:07:26 -0400 Subject: [PATCH 083/235] perf: bound dense moments and scan original strided geometry Use the existing dense ID-domain law on positive pixels before moment allocation, preserving sparse high-ID fallback and original domain callback order. Scan a shared strided 3D view rather than flatten-copying mutable label buffers; prepare all admitted layouts and mutability signatures before execution. Refs #384. --- openhcs/core/runtime_object_labels.py | 43 ++++++----- .../test_dense_object_coordinate_reduction.py | 73 +++++++++++++++++-- 2 files changed, 93 insertions(+), 23 deletions(-) diff --git a/openhcs/core/runtime_object_labels.py b/openhcs/core/runtime_object_labels.py index a503a78b14..396e677ae6 100644 --- a/openhcs/core/runtime_object_labels.py +++ b/openhcs/core/runtime_object_labels.py @@ -30,6 +30,7 @@ NominalTypeStrategyFamilyMixin, ) from openhcs.core.runtime_object_label_domains import ( + DenseIntegerObjectLabelIdDomain, ObjectLabelDomain, ObjectLabelDomainDeclaration, ObjectLabelDomainMetadata, @@ -1218,12 +1219,17 @@ def sparse_ijv_rows(self, labels: object) -> SparseIJVLabelRows: @classmethod def prepare_coordinates(cls) -> None: - """Prepare both mutability signatures of the dense coordinate reducer.""" - labels = np.array([[0, 1]], dtype=np.int32) - for writeable in (True, False): - labels.flags.writeable = writeable - _dense_label_coordinate_moments_numba(labels.ravel(), 1, 2, 1) - dense_label_centers_2d_numba(labels, 1) + """Prepare C/F/strided signatures with both input mutability policies.""" + plane = np.array([[0, 1], [1, 0]], dtype=np.int32) + volume = np.stack((plane, plane)) + for labels in (volume, np.asfortranarray(volume), volume.copy()[..., ::-1]): + for writeable in (True, False): + labels.flags.writeable = writeable + _dense_label_coordinate_moments_numba(labels, 1) + for labels in (plane, np.asfortranarray(plane), plane.copy()[:, ::-1]): + for writeable in (True, False): + labels.flags.writeable = writeable + dense_label_centers_2d_numba(labels, 1) def axis_centers( self, labels: object, *, domain: Sequence[int] @@ -1235,11 +1241,19 @@ def axis_centers( or any(size > np.iinfo(np.int32).max for size in array.shape) ): return super().axis_centers(labels, domain=domain) + coordinate_planes = array[None, ...] if array.ndim == 2 else array + positive_parts = tuple(plane[plane > 0] for plane in coordinate_planes) + coordinate_domain = DenseIntegerObjectLabelIdDomain.from_array( + np.concatenate(positive_parts or (np.empty(0, dtype=np.int32),)) + ) + del positive_parts + if coordinate_domain is None: + return super().axis_centers(labels, domain=domain) coordinate_columns = tuple(range(3 - array.ndim, 3)) sums, pixel_counts = _dense_label_coordinate_moments_numba( - array.ravel(order="C"), - array.shape[-2], array.shape[-1], int(array.max(initial=0)), + coordinate_planes, coordinate_domain.max_label, ) + del coordinate_domain # Like sparse conversion, the moments snapshot precedes domain callbacks. object_ids = np.flatnonzero(pixel_counts) max_domain_label = max(domain, default=0) @@ -1622,22 +1636,17 @@ def object_label_axis_centers( @njit(cache=True) def _dense_label_coordinate_moments_numba( - flat_labels: np.ndarray, - height: int, - width: int, + labels: np.ndarray, maximum_label: int, ) -> tuple[np.ndarray, np.ndarray]: """Reduce dense positive labels once in their declared row-major geometry.""" sums = np.zeros((maximum_label + 1, 3), dtype=np.float64) counts = np.zeros(maximum_label + 1, dtype=np.int64) - plane_size = height * width - plane_count = 0 if plane_size == 0 else flat_labels.size // plane_size - offset = 0 + plane_count, height, width = labels.shape for plane in range(plane_count): for y in range(height): for x in range(width): - label_id = int(flat_labels[offset]) - offset += 1 + label_id = int(labels[plane, y, x]) if label_id > 0 and label_id <= maximum_label: sums[label_id, 0] += plane sums[label_id, 1] += y @@ -1653,7 +1662,7 @@ def dense_label_centers_2d_numba( """Validate compiled two-dimensional geometry and return its y/x centers.""" height, width = labels.shape sums, counts = _dense_label_coordinate_moments_numba( - labels.ravel(), height, width, label_count + labels[None, :height, :width], label_count ) centers = np.empty((label_count + 1, 2), dtype=np.float64) for label_id in range(label_count + 1): diff --git a/tests/unit/test_dense_object_coordinate_reduction.py b/tests/unit/test_dense_object_coordinate_reduction.py index f3cc02442e..a136d5828c 100644 --- a/tests/unit/test_dense_object_coordinate_reduction.py +++ b/tests/unit/test_dense_object_coordinate_reduction.py @@ -198,6 +198,64 @@ def __iter__(self): assert not effects +@pytest.mark.parametrize("shape,label_id", (((1, 1), 2**31 - 1), ((64, 64), 4096))) +def test_sparse_high_ids_reach_domain_error_before_moment_allocation(monkeypatch, shape, label_id): + from openhcs.core import runtime_object_labels + + def reject_moment_allocation(*_args): + raise AssertionError("Sparse ID span must not allocate a dense moment table") + + monkeypatch.setattr(runtime_object_labels, "_dense_label_coordinate_moments_numba", reject_moment_allocation) + for call in (generic_centers, lambda labels, domain: object_label_axis_centers(labels, domain=domain)): + labels = np.zeros(shape, dtype=np.int32) + labels.flat[0] = label_id + effects = [] + + class Domain: + def __iter__(self): + effects.append("iterate") + labels.flat[0] = 1 + raise RuntimeError("Domain rejected sparse high IDs") + + with pytest.raises(RuntimeError, match="Domain rejected sparse high IDs"): + call(labels, Domain()) + assert effects == ["iterate"] + assert labels.flat[0] == 1 + + +@pytest.mark.parametrize("dimensions", (2, 3)) +@pytest.mark.parametrize("layout", ("fortran", "reversed", "broadcast")) +def test_coordinate_scan_borrows_strided_input_without_flatten_copy(monkeypatch, dimensions, layout): + from openhcs.core import runtime_object_labels + + labels = np.array([[0, 1, 1], [2, 0, 2]], dtype=np.int32) + if dimensions == 3: + labels = np.stack((labels, labels)) + if layout == "fortran": + labels = np.asfortranarray(labels) + elif layout == "reversed": + labels = labels[..., ::-1] + else: + labels = np.broadcast_to(labels[..., :1, :], (*labels.shape[:-2], 4, 3)) + expected = generic_centers(labels, (1, 2, 4)) + before = labels.copy() + original = runtime_object_labels._dense_label_coordinate_moments_numba + requests = [] + + def borrowed_scan(coordinate_planes, maximum_label): + assert coordinate_planes.ndim == 3 + assert np.shares_memory(coordinate_planes, labels) + assert coordinate_planes.flags.writeable == labels.flags.writeable + assert coordinate_planes.shape == ((1, *labels.shape) if dimensions == 2 else labels.shape) + requests.append(coordinate_planes) + return original(coordinate_planes, maximum_label) + + monkeypatch.setattr(runtime_object_labels, "_dense_label_coordinate_moments_numba", borrowed_scan) + assert_centers(object_label_axis_centers(labels, domain=(1, 2, 4)), expected) + assert len(requests) == 1 + np.testing.assert_array_equal(labels, before) + + def test_primary_objects_preparation_readies_all_admitted_storage_signatures(tmp_path): script = textwrap.dedent(""" from unittest.mock import patch @@ -214,15 +272,18 @@ def test_primary_objects_preparation_readies_all_admitted_storage_signatures(tmp signatures = tuple(tuple(kernel.signatures) for kernel in kernels) assert all(signatures) - def reject_compilation(signature): - raise AssertionError(('Late coordinate compilation', str(signature))) + def reject_compilation(*arguments): + raise AssertionError(('Late coordinate preparation', tuple(map(str, arguments)))) - with patch.object(_dense_label_coordinate_moments_numba, 'compile', - side_effect=reject_compilation), patch.object( - dense_label_centers_2d_numba, 'compile', side_effect=reject_compilation): + from contextlib import ExitStack + with ExitStack() as guards: + for kernel in kernels: + guards.enter_context(patch.object(kernel, 'compile', side_effect=reject_compilation)) + guards.enter_context(patch.object(kernel._cache, 'load_overload', side_effect=reject_compilation)) for shape in ((4, 5), (3, 4, 5)): base = np.arange(np.prod(shape), dtype=np.int32).reshape(shape) % 4 - for labels in (base, np.asfortranarray(base), base[..., ::-1], base.copy()): + for labels in (base, np.asfortranarray(base), base[..., ::-1], + np.broadcast_to(base[..., :1, :], shape), base.copy()): object_label_axis_centers(labels, domain=(1, 2, 3, 6)) labels.flags.writeable = False object_label_axis_centers(labels, domain=(1, 2, 3, 6)) From 94738b63d088444cfc99feb964f957bbc94d2e8e Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 16:26:51 -0400 Subject: [PATCH 084/235] docs: qualify shared geometry and coordinate runtime gains (refs #384) --- ...shared-dense-coordinate-owner-20261002.rst | 111 ++++++++++++++++++ .../shared_runtime_plumbing_384_20261001.rst | 37 ++++++ 2 files changed, 148 insertions(+) create mode 100644 docs/validation/shared-dense-coordinate-owner-20261002.rst diff --git a/docs/validation/shared-dense-coordinate-owner-20261002.rst b/docs/validation/shared-dense-coordinate-owner-20261002.rst new file mode 100644 index 0000000000..4d865af45f --- /dev/null +++ b/docs/validation/shared-dense-coordinate-owner-20261002.rst @@ -0,0 +1,111 @@ +Shared dense coordinate reduction +================================ + +The shared reducer is integrated into PR394 for issue #384. The isolated +qualification source was ``d5441f960fb4cafe5a1f56740cf8c52de7a7d69d``, normally +merged with qualified PR394 ``eb2f23efd`` and main ``a6c18f054``. Integrated +controls use clean ``6e69b85122d3c1a44857835ba9b3fe77e743240d``. + +``ObjectLabelStorageStrategy`` retains the genuine sparse-IJV algorithm and +owns shared sum allocation/division. The existing dense leaf specializes int32 +2D/3D arrays; ``ObjectLabelValueStorageStrategy`` delegates actual storage. +Tracking and relationships consume one core numerical primitive through their +existing ABI adapters. Their two duplicate kernels are deleted, with all five +compiled consumers migrated. No new class, registry, dispatch table or cache +was introduced. + +Before a public domain iterator can mutate pixels or raise, the dense leaf +captures coordinate moments from a borrowing strided 3D view. The existing +``DenseIntegerObjectLabelIdDomain`` bounds moment allocation by the number of +positive pixels. Empty or large sparse ID spans use the original sparse path; +a lone ID ``2**31 - 1`` therefore reaches the original domain error without +allocating a max-ID-sized table. Unsupported dtypes/ranks retain original +conversion, warnings and errors. Readonly, reversed, Fortran and zero-stride +inputs avoid flatten copies. This establishes an O(nnz) allocation bound, not +identical heap thresholds under OOM or a resolution of issue #433. + +Existing provider/callable preparation warms C/F/A writable and readonly +signatures before execution. An isolated PrimaryObjects preparation control +refuses both compilation and cache loading during subsequent admitted calls. +Controls preserve effectful Sequence iteration, NumPy allocation-phase errors, +NaNs/counts, independent result buffers and input mutation/alias semantics. +Eleven effect controls also run against the actual unchanged Git parent. + +Qualification +------------- + +* Isolated synchronized controls: 100 passed, 2 existing skips. +* Integrated controls at ``6e69b85122``: 101 passed, 2 existing skips in + 194.57 seconds; tracked source hashes remain unchanged. +* Original scoped R0: every metric delta zero; original R1: no increases, + unchanged 160-second budget. Both compare ``eb2f23efd`` to ``d5441f960``. +* Global NRA census: 5,526 original classes, 5,513 canonical declarations, + 13 explicitly OPEN syntax rows, no parse errors. +* Saved real Wound pair: exact coordinates/counts and physical allocation/ + alias gates; original median 0.160669 s, candidate 0.019144 s, local saving + 0.141524 s across paired replay. Preparation/JIT precede timing. +* Twenty-one saved Track frames and old-Git tracking/relationship numerical + ABI comparisons pass. This is not a full relationship transaction proof. + +Initial admission, readiness and high-ID negative evidence remains retained. +These local results alone do not establish ordinary pipeline performance. +Root-owned ordinary qualification is recorded separately below. Illumination +has no centroid calls. + +Evidence: ``/var/tmp/openhcs-shared-dense-coordinate-owner-closure-v2-20261002.json``; +``/var/tmp/openhcs-shared-dense-production-saved-replay-v4-20261002/receipt.json``; +``/var/tmp/openhcs-pr394-integrated-dense-coordinate-controls-20261002.log``. + +Ordinary integrated comparison +------------------------------ + +Four uninstrumented public sweeps in ABBA order compare frozen b4700288f +(shared geometry baseline) with integrated 6e69b8512 (shared coordinate owner). +Each case has one well, inline worker and thread on CPU5, default OUTCOMES +and memory observer, and a reused READY server. Server/library/kernel startup +and shutdown are excluded; compilation remains in total. No samples are removed. + +Two-sample descriptive means (seconds; not statistical significance): + +* ExampleWoundHealing: execution 3.287030339 -> 2.984141707; + compilation 0.337689161 -> 0.340739846; + total 3.866369809 -> 3.562642806. +* ExampleTrackObjects: execution 5.338271976 -> 5.052027345; + compilation 0.929434419 -> 1.252314091; + total 6.629491309 -> 6.684317434. + +Track total regresses by 0.054826125 seconds in this comparison. Its compilation +increases by 0.322879672 seconds; no causal late-JIT attribution is established. +Wound total decreases by 0.303727004 seconds. Neither result satisfies the +remaining whole-pipeline objective or replaces the generic-plumbing frontier. + +Every observation, in case/compile/execution/total seconds: + +* 0-A ExampleTrackObjects: 0.950359821, + 5.625208855, 6.914708113. +* 0-A ExampleWoundHealing: 0.343463182, + 3.276733875, 3.827193568. +* 1-B ExampleTrackObjects: 1.421878338, + 5.020748138, 6.829343345. +* 1-B ExampleWoundHealing: 0.342035055, + 3.044292212, 3.623388787. +* 2-B ExampleTrackObjects: 1.082749844, + 5.083306551, 6.539291524. +* 2-B ExampleWoundHealing: 0.339444637, + 2.923991203, 3.501896825. +* 3-A ExampleTrackObjects: 0.908509016, + 5.051335096, 6.344274506. +* 3-A ExampleWoundHealing: 0.331915140, + 3.297326803, 3.905546051. + +All eight complete authored-output inventories, strict table/image comparisons, +scientific file bytes and source/environment/native/input freezes pass. Wound +retains full native qualification through the exact qualified output reference. +Track preserves all original OH bytes and its original 77,726-pixel native RED; +this does not include the separate draft renderer repair in PR468. No parity +tolerance, exclusion or output policy changes. + +Receipt: ``/var/tmp/openhcs-pr394-shared-dense-ordinary-abba-science-20261002.json`` +SHA256 ``affb93c5955585be6963936552a1f40da051989688e7b0c58f406f398e894400``. +Commands: ``run_pr394_shared_dense_ordinary_abba_20261002.py`` and +``validate_pr394_shared_dense_ordinary_abba_science_20261002.py`` under /var/tmp. diff --git a/docs/validation/shared_runtime_plumbing_384_20261001.rst b/docs/validation/shared_runtime_plumbing_384_20261001.rst index 16cf3492e0..b32bf532b1 100644 --- a/docs/validation/shared_runtime_plumbing_384_20261001.rst +++ b/docs/validation/shared_runtime_plumbing_384_20261001.rst @@ -1742,3 +1742,40 @@ prepared-Resize consumer. The original whole-branch scalar-classifier R0 RED remains separately retained. These draft closing references do not establish completion. Full native scaling, all30 benchmark reruns and fresh figures remain outstanding, and the long-running performance goal stays active. + +Shared geometry release and coordinate-owner checkpoint +------------------------------------------------------ + +Main ``5b7f3c48d89da4415de001375d0e06de881153c9`` is normally merged. +Independent PR476 ships the shared quantized hull/Bresenham geometry and +officially closes issue475. Earlier hull issues158/268 and PR161 remain +completed. The full-source PR394 ABBA comparison pins eb2 versus b470: +BrightField total4.635292249 ->3.294145465s, Untangle3.234328632 ->2.311149861s, +Illumination1.988957128 ->1.892348221s. All16 complete saved outputs pass exact +science and source/env/native/input freezes. Wound is a variance control, not +an attributed hull saving. See the committed shared-quantized-hull report for +every sample, readiness/source guards and retained native-image RED. + +The existing object-label storage family now owns the single shared dense +coordinate reducer. Tracking and relationships inherit/use its primitive; +two duplicate kernels are removed. Resource/domain-effect and C/F/A readiness +controls pass. Isolated100 and integrated101 tests pass, both with2 existing +skips; original scoped R0/R1 pass unchanged against eb2. Frozen b470 versus +6e69 ordinary ABBA Wound total3.866369809 ->3.562642806s; Track execution +5.338271976 ->5.052027345s, but total6.629491309 ->6.684317434s regresses +with higher compilation. All8 full saved outputs/freeze gates pass. The separate +shared-dense-coordinate report retains all samples, no causal late-JIT claim +and the original tracking image RED. No reduction resolves issue433's original +allocation owner or the whole-branch scalar-classifier R0 RED. + +Owner WT remains /home/ts/code/projects/openhcs-shared-runtime-plumbing. All +production edits are committed here. Agent-owned implementation branches are +integrated; no additional unpublished production files are claimed. Frozen +benchmark WTs remain unchanged. Compared with receiving checkpoint eb2, new +production edits are precisely three CP geometry/illumination modules plus +core/runtime_object_labels.py and four CP backend/preparation/tracking/ +relationships modules. These are indirect numerical/coordinate and preparation +changes beyond the receiving review's original four-file eb2/wheel delta. +Installed receiving obligations419/433/435/450, whole-branch R0, full native +image parity, full-catalog/scaling and figures remain open. A first-wave fresh +four-case ordinary/native qualification is in progress, not a completed gate. From b25120d5a91f6d4f40e9742982ab143667e91398 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 16:43:27 -0400 Subject: [PATCH 085/235] Project match-image labels on declared scalar singleton invocations (refs #477) --- .../cellprofiler/runtime/module_execution.py | 19 +++ ...test_singleton_rgb_label_invocation_477.py | 131 ++++++++++++++++++ 2 files changed, 150 insertions(+) create mode 100644 tests/unit/test_singleton_rgb_label_invocation_477.py diff --git a/openhcs/interop/cellprofiler/runtime/module_execution.py b/openhcs/interop/cellprofiler/runtime/module_execution.py index 4507007dd4..180527d4bc 100644 --- a/openhcs/interop/cellprofiler/runtime/module_execution.py +++ b/openhcs/interop/cellprofiler/runtime/module_execution.py @@ -36,6 +36,7 @@ MeasurementSparseColumnarRows, ) from openhcs.core.pipeline.function_contracts import ( + ObjectLabelInputExecutionMode, object_label_input_execution_mode_from_callable, ) from openhcs.core.runtime_adapters import ( @@ -1494,6 +1495,24 @@ def _invocation_request( kwargs=runtime_kwargs, variable_components=adapter.request.variable_components, ) + # A scalar image can result from composing channels on a declared + # singleton runtime root. Match-image labels consume that root only + # after the module has chosen its final image domain and execution mode. + if ( + object_label_input_execution_mode_from_callable(self.raw_func) + is ObjectLabelInputExecutionMode.MATCH_IMAGE_STACK + and execution_mode is ImagePayloadExecutionMode.NATURAL + and image_request.plane_projection is None + and runtime_projection is not None + and runtime_projection.axis_size == 1 + ): + runtime_kwargs = cast( + dict[str, RuntimeCallableArgument], + RuntimeSliceProjection.kwargs_for_slice( + runtime_kwargs, + runtime_projection.selected_plane(0), + ), + ) if profile_enabled: CellProfilerRuntimeProfileLogger.log_module_profile( "cp_invocation_execution_mode_policy", diff --git a/tests/unit/test_singleton_rgb_label_invocation_477.py b/tests/unit/test_singleton_rgb_label_invocation_477.py new file mode 100644 index 0000000000..9b9354338f --- /dev/null +++ b/tests/unit/test_singleton_rgb_label_invocation_477.py @@ -0,0 +1,131 @@ +"""Scalar RGB invocations consume only an explicitly declared singleton root.""" +import numpy as np +import pytest + +from openhcs.core.artifacts import ArtifactSpec, ImageArtifactType, ObjectLabelsArtifactType +from openhcs.core.aligned_image_payload import ImagePayloadExecutionMode +from openhcs.core.function_contract_metadata import FunctionContractAttribute +from openhcs.core.pipeline.function_contracts import ObjectLabelInputExecutionMode +from openhcs.core.runtime_image_values import ImageMetadataPayload, ImagePayloadMetadata +from openhcs.core.runtime_object_label_domains import ObjectLabelDomain, ObjectLabelDomainScope +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_slice_projection import RuntimeSliceProjectionDeclarationError +from openhcs.interop.cellprofiler.runtime.function_contract_execution import CellProfilerFunctionContractExecutor +from openhcs.processing.backends.cellprofiler.outlines import OverlayOutlinesModule +from tests.unit.test_cellprofiler_module_execution import ( + _FakeCellProfilerRuntime, + _artifact_input_edge_for_test, + _compiled_callable_contract, + _module_executor, +) + + +def _invocation_case(*, root_count=1, label_count=1): + image_spec = ArtifactSpec.input('GFPandDNA', ImageArtifactType) + labels_spec = ArtifactSpec.input('Nuclei', ObjectLabelsArtifactType) + raw = OverlayOutlinesModule.require_callable() + contract = _compiled_callable_contract(raw, artifact_inputs=(image_spec, labels_spec)) + data = np.linspace(0, 1, 8 * 9 * 3, dtype=np.float32).reshape(8, 9, 3) + image = ImageMetadataPayload(data, ImagePayloadMetadata(source_channel_axis=2, source_image_names=('GFPandDNA',))) + label_data = np.zeros((label_count, 8, 9), dtype=np.int32) + 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), + ) + adapter = _FakeCellProfilerRuntime( + {'GFPandDNA': image}, objects={'Nuclei': labels}, callable_contract=contract, + artifact_input_edges=tuple(_artifact_input_edge_for_test(spec) for spec in (image_spec, labels_spec)), + plane_projection=RuntimePlaneProjection.stack(root_count), + ) + executor = _module_executor(contract) + request = executor._image_request(image, adapter, module_type=OverlayOutlinesModule, active_input_specs=(image_spec, labels_spec)) + return raw, contract, image, labels, adapter, executor, request + + +def _bind(case): + raw, contract, image, labels, adapter, executor, request = case + return executor._invocation_request( + image_request=request, adapter=adapter, current_image=image, + kwargs={'outline_colors': ('#21FFFF',)}, module_type=OverlayOutlinesModule, + ) + + +def _execute(case, invocation): + raw, contract, *_ = case + return CellProfilerFunctionContractExecutor().execute( + contract, raw, invocation.image, invocation.kwargs, + execution_mode=invocation.execution_mode, plane_projection=invocation.plane_projection, + ) + + +def test_match_image_labels_consume_declared_singleton_root_for_scalar_rgb(): + case = _invocation_case() + raw, contract, image, labels, *_ = case + image_before = image.data.copy() + labels_before = object_label_dense_array(labels).copy() + invocation = _bind(case) + selected = invocation.kwargs['object_labels'][0] + assert invocation.execution_mode is ImagePayloadExecutionMode.NATURAL + assert invocation.plane_projection is None + assert invocation.image.metadata.source_channel_axis == 2 + assert selected.plane_axis is None + 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)) + 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 + + +@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 + with pytest.raises(RuntimeSliceProjectionDeclarationError, match='Kwargs cannot create image-axis execution semantics'): + _execute(case, invocation) + + +def test_explicit_full_stack_label_contract_preserves_scalar_boundary_refusal(monkeypatch): + raw = OverlayOutlinesModule.require_callable() + monkeypatch.setattr(raw, FunctionContractAttribute.object_label_input_execution_mode, ObjectLabelInputExecutionMode.FULL_STACK) + case = _invocation_case() + invocation = _bind(case) + assert invocation.kwargs['object_labels'][0].plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + with pytest.raises(RuntimeSliceProjectionDeclarationError, match='Kwargs cannot create image-axis execution semantics'): + _execute(case, invocation) + + +def test_declared_singleton_root_refuses_mismatched_label_cardinality(): + case = _invocation_case(label_count=2) + with pytest.raises(ValueError, match='Object-label runtime plane-axis cardinality mismatch'): + _bind(case) + + +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 object_label_dense_array(kwargs['object_labels'][0]).shape == (1, 8, 9) + return ImagePayloadExecutionMode.FULL_STACK + monkeypatch.setattr(OverlayOutlinesModule, 'execution_mode', classmethod(mode)) + invocation = _bind(case) + assert invocation.execution_mode is ImagePayloadExecutionMode.FULL_STACK + assert invocation.kwargs['object_labels'][0].plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + + +def test_module_mode_failure_precedes_projection_guard(monkeypatch): + case = _invocation_case(label_count=2) + error = RuntimeError('module image-domain selection failed') + def mode(cls, default, *, image, kwargs, variable_components): + raise error + monkeypatch.setattr(OverlayOutlinesModule, 'execution_mode', classmethod(mode)) + with pytest.raises(RuntimeError) as caught: + _bind(case) + assert caught.value is error From ccaa4cba6a1eca818cd544438941f1c1cedb3e32 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 16:47:18 -0400 Subject: [PATCH 086/235] Own singleton label projection on the declared input policy (refs #477) --- openhcs/core/pipeline/function_contracts.py | 32 +++++++++++++- .../cellprofiler/runtime/module_execution.py | 43 ++++++------------- 2 files changed, 43 insertions(+), 32 deletions(-) diff --git a/openhcs/core/pipeline/function_contracts.py b/openhcs/core/pipeline/function_contracts.py index 413f266de8..e0d343fc5f 100644 --- a/openhcs/core/pipeline/function_contracts.py +++ b/openhcs/core/pipeline/function_contracts.py @@ -1,6 +1,6 @@ """Function-level artifact contract decorators for the pipeline compiler.""" -from collections.abc import Sequence +from collections.abc import Mapping, Sequence from dataclasses import replace from enum import Enum import inspect @@ -33,6 +33,8 @@ ImagePayloadConsumption, ) from openhcs.core.function_contract_metadata import FunctionContractAttribute +from openhcs.core.image_payload_execution_mode import ImagePayloadExecutionMode +from openhcs.core.runtime_plane_projection import RuntimePlaneAxisValueProjection from openhcs.core.variable_component_stack_requirement import ( AlwaysRequiresVariableComponentStack, VariableComponentStackRequirement, @@ -136,6 +138,34 @@ def preserves_full_stack(self, *, image_stack_required: bool) -> bool: self is self.MATCH_IMAGE_STACK and image_stack_required ) + def invocation_kwargs( + self, + kwargs: Mapping[str, Any], + *, + execution_mode: ImagePayloadExecutionMode, + image_projection: RuntimePlaneAxisValueProjection | None, + runtime_projection: RuntimePlaneAxisValueProjection | None, + semantic_controls: Mapping[str, Any], + ) -> dict[str, Any]: + """Match scalar images to their declared singleton runtime root. + + Explicit full-stack label consumers keep their domain. Matching labels + can consume a singleton root only after the image's final execution + mode and retained plane projection have been resolved. + """ + if ( + self is self.MATCH_IMAGE_STACK + and execution_mode is ImagePayloadExecutionMode.NATURAL + and image_projection is None + ): + if runtime_projection is not None and runtime_projection.axis_size == 1: + from openhcs.core.runtime_slice_projection import RuntimeSliceProjection + + kwargs = RuntimeSliceProjection.kwargs_for_slice( + kwargs, runtime_projection.selected_plane(0) + ) + return {**kwargs, **semantic_controls} + def _artifact_spec_from_output_declaration( spec: str | ArtifactSpec | tuple[str, MaterializationSpec | None], diff --git a/openhcs/interop/cellprofiler/runtime/module_execution.py b/openhcs/interop/cellprofiler/runtime/module_execution.py index 180527d4bc..170b24ac37 100644 --- a/openhcs/interop/cellprofiler/runtime/module_execution.py +++ b/openhcs/interop/cellprofiler/runtime/module_execution.py @@ -36,7 +36,6 @@ MeasurementSparseColumnarRows, ) from openhcs.core.pipeline.function_contracts import ( - ObjectLabelInputExecutionMode, object_label_input_execution_mode_from_callable, ) from openhcs.core.runtime_adapters import ( @@ -1484,50 +1483,32 @@ def _invocation_request( image_request=image_request, runtime_kwargs=runtime_kwargs, ) - invocation_image = image_request.payload - default_execution_mode = ( - self.callable_contract.runtime_image_execution_mode - or image_request.execution_mode - ) execution_mode = module_type.execution_mode( - default_execution_mode, - image=invocation_image, + self.callable_contract.runtime_image_execution_mode + or image_request.execution_mode, + image=image_request.payload, kwargs=runtime_kwargs, variable_components=adapter.request.variable_components, ) - # A scalar image can result from composing channels on a declared - # singleton runtime root. Match-image labels consume that root only - # after the module has chosen its final image domain and execution mode. - if ( - object_label_input_execution_mode_from_callable(self.raw_func) - is ObjectLabelInputExecutionMode.MATCH_IMAGE_STACK - and execution_mode is ImagePayloadExecutionMode.NATURAL - and image_request.plane_projection is None - and runtime_projection is not None - and runtime_projection.axis_size == 1 - ): - runtime_kwargs = cast( - dict[str, RuntimeCallableArgument], - RuntimeSliceProjection.kwargs_for_slice( - runtime_kwargs, - runtime_projection.selected_plane(0), - ), - ) if profile_enabled: CellProfilerRuntimeProfileLogger.log_module_profile( "cp_invocation_execution_mode_policy", time.perf_counter() - phase_started_at, module=module_name, ) - invocation_kwargs = { - **runtime_kwargs, - **_execution_mode_semantic_control_kwargs( + invocation_kwargs = object_label_input_execution_mode_from_callable( + self.raw_func + ).invocation_kwargs( + runtime_kwargs, execution_mode=execution_mode, + image_projection=image_request.plane_projection, + runtime_projection=runtime_projection, + semantic_controls=_execution_mode_semantic_control_kwargs( self.callable_contract.require_processing_contract(), execution_mode, ), - } + ) return RuntimeFunctionInvocationRequest( - image=invocation_image, + image=image_request.payload, kwargs=invocation_kwargs, source_image_name=image_request.source_image_name, image_count=image_request.image_count, From 619468b998482b6e0cbcf212f8f8e1e02a9fbef6 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 17:08:03 -0400 Subject: [PATCH 087/235] docs: qualify generic singleton label invocation repair (refs #477) --- ...ingleton-label-invocation-477-20261002.rst | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 docs/validation/singleton-label-invocation-477-20261002.rst diff --git a/docs/validation/singleton-label-invocation-477-20261002.rst b/docs/validation/singleton-label-invocation-477-20261002.rst new file mode 100644 index 0000000000..faad1c18a4 --- /dev/null +++ b/docs/validation/singleton-label-invocation-477-20261002.rst @@ -0,0 +1,92 @@ +Singleton label projection at the final invocation boundary (#477) +================================================================= + +The ordinary translocation benchmark failed on frozen ``6e69b851`` before +producing a successful well. Its OverlayOutlines invocation paired a scalar +640-by-640 RGB image with object labels on a declared singleton runtime root. +The image had a channel axis but no image-plane projection. Treating the RGB +channels as image planes would violate the existing strict axis contract. + +The input binding request initially derived label shape with the callable's +FLEXIBLE default. The module executor resolved the final NATURAL mode later, +after binding, so its semantic controls and the bound labels could disagree. + +Existing nominal ownership +------------------------- + +``ObjectLabelInputExecutionMode.invocation_kwargs`` now owns this final +projection. MATCH_IMAGE_STACK with a NATURAL scalar image may project an +already-bound label argument only when the original runtime declaration proves +exactly one plane. It delegates to the existing ``RuntimeSliceProjection`` and +merges the resolved semantic controls once. The module executor delegates to +that owner after resolving execution mode. + +Explicit FULL_STACK label declarations and FULL_STACK execution remain intact. +Multiple or unproved runtime axes and retained image-plane projections keep +their original strict guards. The fix neither invents an image axis nor reloads +artifacts. Redundant intermediate image/default-mode locals are removed. No +new policy class, cache, per-module exception, or color-processing change is +introduced. + +Source and local validation +--------------------------- + +The final isolated source is ``ccaa4cba6a1eca818cd544438941f1c1cedb3e32``, +integrated into PR #394 by ``1763ef07eba959dbefa7643cb90c8230fdf12518``. +The initial inline implementation and its original R0 failure are retained in +history; that route added GodClass, boolean-chain and foreign-absence metrics. +The final existing-owner implementation passes unchanged original R0 metrics +and original R1 with its original 160-second budget. + +The original reproducer recorded two expected failures and five passing +controls. The repaired module-consumer gate passes 402 controls. The global +source census parsed 703 modules with no errors. All five registered +MATCH_IMAGE_STACK consumers currently declare FLEXIBLE processing contracts. +External custom MATCH consumers with a non-FLEXIBLE stack-only contract have +not been qualified; this report makes no blanket callback-equivalence claim. + +The source-qualified receipt is +``/var/tmp/issue477-policy-owner-qualified-receipt-20261002.json`` with SHA256 +``7ebcd89739bbe58cf7630c093f02b92ecf41732a196073641ec373e4d9b019ca``. + +Actual public pipeline consumer +------------------------------- + +The frozen integrated source at +``/var/tmp/openhcs-pr394-singleton-label-qualified-20261002`` remains immutable. +Its ordinary public run completed both translocation and WoundHealing with one +successful well each, a reused READY server, one inline worker and one thread +on CPU 5. All input, native-binary, environment, dependency and tracked-source +freezes pass. Server/library/kernel preparation and shutdown are outside the +pipeline clocks; compilation and OUTCOMES closure remain inside total. + +The single observations are: + +================= ============== ============== ============== +Case Compilation(s) Execution(s) Total(s) +================= ============== ============== ============== +Translocation 0.527004 1.570796 2.539227 +WoundHealing 1.296431 2.897727 4.421997 +================= ============== ============== ============== + +These are correctness observations, not matched optimization evidence. +Wound's higher compilation observation is retained and its cause is unknown. +The original failed translocation clocks remain excluded from speedup claims. +The observation receipt is +``/var/tmp/openhcs-pr394-singleton-label-ordinary-v1-20261002/observations.json``; +its source-freeze SHA256 is +``ea4969dbfe48e7a42ff039b07a32eb0d2464374362b89588682483fa33039128``. + +Wound's complete authored output inventory, table comparison and exact +scientific bytes pass against the qualified ``9aa3918d`` reference. Its +two-row Image.csv SHA256 remains +``939735fa17a067137fea2adc77d57f9ce1df24bfcb6588c49023de1cc3998677``. +The read-only scientific receipt is +``/var/tmp/openhcs-pr394-singleton-label-wound-science-20261002.json`` with SHA256 +``7a8c3cafa33e54394d9a24f7744c6a6b641e48c6f712f1baab7c632dcace0655``. + +Translocation still requires fresh warm-native execution and complete authored +table, image, discrete-value and inventory comparison before issue acceptance. +The first native-controller preparation correctly rejected an auxiliary CSV +misclassified as an image input; its failure is retained. No tolerance change, +scientific-output exclusion or issue-closure claim is made here. From a73f48f0d3308b3a2763bb0d8d8312caf2c99d6e Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 17:28:15 -0400 Subject: [PATCH 088/235] Remove identical singleton invocation method introduced by main merge --- openhcs/core/pipeline/function_contracts.py | 29 --------------------- 1 file changed, 29 deletions(-) diff --git a/openhcs/core/pipeline/function_contracts.py b/openhcs/core/pipeline/function_contracts.py index 50fc409f65..e0d343fc5f 100644 --- a/openhcs/core/pipeline/function_contracts.py +++ b/openhcs/core/pipeline/function_contracts.py @@ -167,35 +167,6 @@ def invocation_kwargs( return {**kwargs, **semantic_controls} - def invocation_kwargs( - self, - kwargs: Mapping[str, Any], - *, - execution_mode: ImagePayloadExecutionMode, - image_projection: RuntimePlaneAxisValueProjection | None, - runtime_projection: RuntimePlaneAxisValueProjection | None, - semantic_controls: Mapping[str, Any], - ) -> dict[str, Any]: - """Match scalar images to their declared singleton runtime root. - - Explicit full-stack label consumers keep their domain. Matching labels - can consume a singleton root only after the image's final execution - mode and retained plane projection have been resolved. - """ - if ( - self is self.MATCH_IMAGE_STACK - and execution_mode is ImagePayloadExecutionMode.NATURAL - and image_projection is None - ): - if runtime_projection is not None and runtime_projection.axis_size == 1: - from openhcs.core.runtime_slice_projection import RuntimeSliceProjection - - kwargs = RuntimeSliceProjection.kwargs_for_slice( - kwargs, runtime_projection.selected_plane(0) - ) - return {**kwargs, **semantic_controls} - - def _artifact_spec_from_output_declaration( spec: str | ArtifactSpec | tuple[str, MaterializationSpec | None], ) -> ArtifactSpec: From a8953b97ee8b61f736962f8729cdf9ea299f0e6b Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 17:30:56 -0400 Subject: [PATCH 089/235] fix(equivalence): preserve saved relationship correlations (refs #479) --- benchmark/matched_cellprofiler_batch.py | 48 +- openhcs/core/equivalence/relationships.py | 463 +++++++++++++++++- openhcs/core/runtime_equivalence.py | 156 ++++-- openhcs/core/runtime_relationships.py | 159 +++++- .../cellprofiler/measurement_lookup.py | 29 +- .../runtime/relationship_measurement_rows.py | 48 +- .../backends/cellprofiler/relationships.py | 4 +- .../test_cellprofiler_module_execution.py | 6 +- ...est_cellprofiler_relationship_contracts.py | 8 +- .../test_exported_relationship_equivalence.py | 393 +++++++++++++++ 10 files changed, 1187 insertions(+), 127 deletions(-) create mode 100644 tests/unit/test_exported_relationship_equivalence.py diff --git a/benchmark/matched_cellprofiler_batch.py b/benchmark/matched_cellprofiler_batch.py index 53441d6777..77845e4437 100644 --- a/benchmark/matched_cellprofiler_batch.py +++ b/benchmark/matched_cellprofiler_batch.py @@ -179,14 +179,51 @@ def _require_compared_output_inventory( candidate_managed_files: frozenset[Path] = frozenset(), ) -> None: """Reject unqualified output formats and missing scientific output files.""" + # Validate all explicit edges against their object rows, then compare the + # cross-output correlations before using redundant scalar table inventory. + from openhcs.core.equivalence.relationships import ( + ExportedRelationshipMeasurementSemantics, + ) + from openhcs.core.runtime_equivalence import RuntimeMeasurementSnapshot + + measurements = tuple( + RuntimeMeasurementSnapshot.from_output_snapshot(snapshot) + for snapshot in (reference_snapshot, candidate_snapshot) + ) + if any( + measurement.correlated_relationships is None for measurement in measurements + ): + raise RuntimeError( + "Matched saved output inventory requires known relationship correlations." + ) + if measurements[0].relationship_differences( + measurements[1].correlated_relationships + ): + raise RuntimeError("Matched saved output relationship correlations differ.") + comparable_tables = tuple( + tuple( + table + for table in snapshot.tables + if table.participates_in_comparison + and not ExportedRelationshipMeasurementSemantics.supports_table(table) + ) + for snapshot in (reference_snapshot, candidate_snapshot) + ) counts = [] - for files, exports, snapshot, managed_files in ( - (reference_files, reference_exports, reference_snapshot, frozenset()), + for files, exports, snapshot, managed_files, tables in ( + ( + reference_files, + reference_exports, + reference_snapshot, + frozenset(), + comparable_tables[0], + ), ( candidate_files, candidate_exports, candidate_snapshot, candidate_managed_files, + comparable_tables[1], ), ): files = files - managed_files @@ -210,7 +247,7 @@ def _require_compared_output_inventory( - len(snapshot.tables) - len(exports.image_outputs) + len(snapshot.images) - + sum(table.participates_in_comparison for table in snapshot.tables) + + len(tables) ) if count < 1: raise RuntimeError("Matched batch has no compared scientific output files.") @@ -232,10 +269,9 @@ def _require_compared_output_inventory( ), len(table.rows), ) - for table in snapshot.tables - if table.participates_in_comparison + for table in tables ) - for snapshot in (reference_snapshot, candidate_snapshot) + for tables in comparable_tables ) if table_shapes[0] != table_shapes[1]: raise RuntimeError( diff --git a/openhcs/core/equivalence/relationships.py b/openhcs/core/equivalence/relationships.py index 1f03d975c8..6b77b7717f 100644 --- a/openhcs/core/equivalence/relationships.py +++ b/openhcs/core/equivalence/relationships.py @@ -6,7 +6,7 @@ from abc import ABC, abstractmethod from collections.abc import Mapping from dataclasses import dataclass, field -from typing import ClassVar +from typing import ClassVar, Self from metaclass_registry import RegistryFamily, RegistryKeyAttribute import numpy as np @@ -54,7 +54,7 @@ RuntimeMeasurementDialect, normalize_runtime_identifier, ) -from openhcs.core.equivalence.tables import measurement_table_padding_group +from openhcs.core.equivalence.tables import RuntimeTableSnapshot, measurement_table_padding_group from openhcs.core.registry_strategies import MostDerivedContextStrategyMixin from openhcs.core.measurement_row_materialization import ( columnar_row_values, @@ -66,9 +66,15 @@ MeasurementScope, MeasurementStatistic, ObjectCoreMeasurementFeature, + MeasurementScalarLiteral, ) from openhcs.core.runtime_relationships import ( ObjectInstanceKey, + ObjectRelationshipDeclaration, + DirectedObjectRelationshipPayload, + DirectParentReferenceFeatureDeclaration, + ChildCountFeatureDeclaration, + DirectParentReferenceMeasurementFeature, ObjectInstanceRelationship, ) from openhcs.core.runtime_tabular_values import ( @@ -1047,3 +1053,456 @@ def mean_child_value( if not values: return float("nan") return float(sum(values) / len(values)) + + +@dataclass(frozen=True, slots=True) +class ExportedRelationshipMeasurementSemantics(RelationshipMeasurementSemantics): + """Restore directed edge meaning using the saved object-row domains.""" + + object_tables: tuple[RuntimeScopedMeasurementTable, ...] + image_number_offset: RuntimeImageNumberOffset + + @classmethod + def supports_table(cls, table: RuntimeTableSnapshot) -> bool: + fields = frozenset(normalize_runtime_identifier(name) for name in table.header) + declaration_fields = frozenset( + ObjectRelationshipDeclaration.exported_field_names + ) + if not fields & declaration_fields: + return False + if not declaration_fields <= fields: + raise ValueError( + f"Relationship table {table.path} has an incomplete declaration schema." + ) + return True + + @classmethod + def validated_output_tables( + cls, + tables: tuple[RuntimeTableSnapshot, ...], + policy: RuntimeEquivalencePolicy, + ) -> tuple[RuntimeTableSnapshot, ...]: + """Consume every edge table before exposing its redundant scalar projection.""" + ordinary = tuple(table for table in tables if not cls.supports_table(table)) + object_tables = tuple( + RuntimeScopedMeasurementTable(measurement) + for table in ordinary + for measurement in table.measurement_tables(policy.measurement_dialect) + ) + image_offset = RuntimeImageNumberOffset.from_runtime_rows( + row + for table in object_tables + for row in table.table.rows.iter_row_mappings() + ) + relationships = tuple( + relationship + for table in tables + if cls.supports_table(table) + for relationship in cls.from_table( + table, object_tables, policy, image_offset + ) + ) + for relationship in relationships: + relationship.validate_recorded_measurements() + relationship.validate_reciprocal_declarations(relationships) + return ordinary + + @classmethod + def from_table( + cls, + table: RuntimeTableSnapshot, + object_tables: tuple[RuntimeScopedMeasurementTable, ...], + policy: RuntimeEquivalencePolicy, + image_number_offset: RuntimeImageNumberOffset, + ) -> tuple[Self, ...]: + if not table.rows: + raise ValueError("Relationship table has no determining declaration rows.") + normalized_header = tuple( + normalize_runtime_identifier(name) for name in table.header + ) + if len(set(normalized_header)) != len(normalized_header): + raise ValueError("Relationship table has duplicate normalized columns.") + groups: dict[ + ObjectRelationshipDeclaration, list[RuntimeMeasurementRowMapping] + ] = {} + for values in table.rows: + row = RuntimeMeasurementRowMapping( + dict(zip(table.header, values, strict=True)) + ) + declaration = ObjectRelationshipDeclaration.from_exported_values( + *( + row.first_value((field,)) + for field in ObjectRelationshipDeclaration.exported_field_names + ) + ) + groups.setdefault(declaration, []).append(row) + results = [] + for declaration, rows in groups.items(): + source_ids = [] + target_ids = [] + slice_indices = [] + counts = set() + for row in rows: + source_id = MeasurementScalarLiteral( + row.first_value((declaration.source_id_field,)) + ).integer_value + target_id = MeasurementScalarLiteral( + row.first_value((declaration.target_id_field,)) + ).integer_value + if ( + source_id is None + or target_id is None + or min(source_id, target_id) < 0 + ): + raise ValueError( + "Relationship endpoint IDs must be nonnegative integers." + ) + slice_index = image_number_offset.object_instance_key( + row.row, 1 + ).slice_index + if slice_index is None: + raise ValueError( + "Relationship rows require an explicit image or slice identity." + ) + raw_count = row.first_value(("slice_count",)) + count = MeasurementScalarLiteral(raw_count).integer_value + if raw_count is not None and count is None: + raise ValueError( + "Relationship slice count must be an integer when declared." + ) + counts.add(count) + source_ids.append(source_id) + target_ids.append(target_id) + slice_indices.append(slice_index) + if len(counts) != 1: + raise ValueError( + "Relationship rows have conflicting declared slice counts." + ) + relationship = ObjectRelationship( + name=declaration.artifact_name(), + declaration=declaration, + payload=DirectedObjectRelationshipPayload( + source_ids=tuple(source_ids), + target_ids=tuple(target_ids), + slice_indices=tuple(slice_indices), + slice_count=counts.pop(), + ), + ) + results.append( + cls( + relationship, + policy.measurement_dialect, + object_tables, + image_number_offset, + ) + ) + return tuple(results) + + @property + def source_name(self) -> str: + declaration = self.relationship.declaration + ref = ( + declaration.source + if declaration.projects_parent_child_measurements() + else declaration.target + ) + return normalize_runtime_identifier(ref.name) + + @property + def target_name(self) -> str: + declaration = self.relationship.declaration + ref = ( + declaration.target + if declaration.projects_parent_child_measurements() + else declaration.source + ) + return normalize_runtime_identifier(ref.name) + + @property + def instance_relationship(self) -> ObjectInstanceRelationship: + instances = super( + ExportedRelationshipMeasurementSemantics, self + ).instance_relationship + return ( + instances + if self.relationship.declaration.projects_parent_child_measurements() + else instances.reverse_endpoints() + ) + + def validate_reciprocal_declarations(self, relationships: tuple[Self, ...]) -> None: + """Require an observed reverse declaration to retain producer and pairs.""" + declaration = self.relationship.declaration + reverse = tuple( + other + for other in relationships + if other.source_subject == self.source_subject + and other.target_subject == self.target_subject + and other.relationship.declaration.source_role != declaration.source_role + ) + if reverse and not any( + other.relationship.declaration.producer_module_number + == declaration.producer_module_number + and other.instance_relationship.slice_count + == self.instance_relationship.slice_count + and frozenset( + zip( + other.instance_relationship.source_keys, + other.instance_relationship.target_keys, + strict=True, + ) + ) + == frozenset( + zip( + self.instance_relationship.source_keys, + self.instance_relationship.target_keys, + strict=True, + ) + ) + for other in reverse + ): + raise ValueError( + "Relationship reverse declarations disagree on producer or directed pairs." + ) + + @classmethod + def object_rows( + cls, + object_tables: tuple[RuntimeScopedMeasurementTable, ...], + subject: RuntimeMeasurementSubjectKey, + image_number_offset: RuntimeImageNumberOffset, + ) -> dict[ObjectInstanceKey, RuntimeMeasurementRowMapping]: + rows: dict[ObjectInstanceKey, RuntimeMeasurementRowMapping] = {} + for scoped_table in object_tables: + table = scoped_table.table + if ( + RuntimeMeasurementSubjectKey.from_table_subject(table.subject) + != subject + ): + continue + for values in table.rows.iter_row_mappings(): + row = RuntimeMeasurementRowMapping(measurement_row_mapping(values)) + object_id = row.object_label() + if object_id is None: + raise ValueError( + f"Relationship endpoint {subject.name!r} lacks an object identity." + ) + key = image_number_offset.object_instance_key(row.row, object_id) + if key.slice_index is None: + raise ValueError( + "Relationship object rows require explicit image identities." + ) + if key in rows and rows[key].row != row.row: + raise ValueError( + f"Relationship endpoint {subject.name!r} has conflicting object rows for {key!r}." + ) + rows[key] = row + return rows + + def source_domain( + self, object_label_catalog: RuntimeObjectLabelInstanceCatalog + ) -> tuple[ObjectInstanceKey, ...]: + del object_label_catalog + return tuple( + self.object_rows( + self.object_tables, self.source_subject, self.image_number_offset + ) + ) + + def target_domain( + self, object_label_catalog: RuntimeObjectLabelInstanceCatalog + ) -> tuple[ObjectInstanceKey, ...]: + del object_label_catalog + return tuple( + self.object_rows( + self.object_tables, self.target_subject, self.image_number_offset + ) + ) + + def child_keys_by_parent( + self, object_label_catalog: RuntimeObjectLabelInstanceCatalog + ) -> dict[ObjectInstanceKey, tuple[ObjectInstanceKey, ...]]: + return self.instance_relationship.child_keys_by_parent( + declared_source_keys=self.source_domain(object_label_catalog) + ) + + def validate_recorded_measurements(self) -> None: + """Check all edges and zero-child rows before suppressing redundant facts.""" + parents = self.object_rows( + self.object_tables, self.source_subject, self.image_number_offset + ) + children = self.object_rows( + self.object_tables, self.target_subject, self.image_number_offset + ) + original = self.relationship.payload + for source_id, target_id, slice_index in zip( + original.source_ids, + original.target_ids, + original.slice_indices, + strict=True, + ): + parent_id, child_id = ( + (source_id, target_id) + if self.relationship.declaration.projects_parent_child_measurements() + else (target_id, source_id) + ) + if ( + child_id <= 0 + or ObjectInstanceKey(child_id, slice_index) not in children + ): + raise ValueError( + "Relationship table refers to an absent child endpoint." + ) + if ( + parent_id > 0 + and ObjectInstanceKey(parent_id, slice_index) not in parents + ): + raise ValueError( + "Relationship table refers to an absent parent endpoint." + ) + instances = self.instance_relationship + pairs = tuple(zip(instances.source_keys, instances.target_keys, strict=True)) + if len(set(pairs)) != len(pairs): + raise ValueError("Relationship table contains duplicate directed edges.") + parent_by_child = instances.parent_key_by_child() + if len(parent_by_child) != len(pairs): + raise ValueError( + "Relationship table assigns contradictory parents to one child." + ) + if ( + not set(instances.source_keys) <= parents.keys() + or not set(instances.target_keys) <= children.keys() + ): + raise ValueError("Relationship table refers to an absent object endpoint.") + parent_feature = DirectParentReferenceFeatureDeclaration.feature_name( + DirectParentReferenceMeasurementFeature( + self.relationship.declaration.source.name + if self.relationship.declaration.projects_parent_child_measurements() + else self.relationship.declaration.target.name + ) + ) + for child, row in children.items(): + observed = row.first_value((normalize_runtime_identifier(parent_feature),)) + if observed is None: + raise ValueError( + f"Relationship child {child!r} lacks its explicit parent measurement." + ) + expected = ( + parent_by_child[child].object_id if child in parent_by_child else 0 + ) + if MeasurementScalarLiteral(observed).integer_value != expected: + raise ValueError( + f"Relationship edges disagree with {parent_feature!r} at child {child!r}." + ) + counts = instances.child_keys_by_parent(declared_source_keys=parents) + count_feature = ChildCountFeatureDeclaration.feature_name( + self.relationship.declaration.target.name + if self.relationship.declaration.projects_parent_child_measurements() + else self.relationship.declaration.source.name + ) + for parent, row in parents.items(): + observed = row.first_value((normalize_runtime_identifier(count_feature),)) + if observed is None or MeasurementScalarLiteral( + observed + ).integer_value != len(counts[parent]): + raise ValueError( + f"Relationship edges disagree with {count_feature!r} at parent {parent!r}." + ) + + @classmethod + def correlated_object_relationships( + cls, + object_tables: tuple[RuntimeScopedMeasurementTable, ...], + image_number_offset: RuntimeImageNumberOffset, + ) -> dict[RuntimeMeasurementFeatureKey, ObjectInstanceRelationship]: + """Retain exact native parent references, independently of value counters.""" + pairs: dict[ + RuntimeMeasurementFeatureKey, + list[tuple[ObjectInstanceKey, ObjectInstanceKey]], + ] = {} + for scoped_table in object_tables: + table = scoped_table.table + if table.subject.scope is not MeasurementScope.OBJECT: + continue + child_name = normalize_runtime_identifier(table.subject.name) + for field in table.rows.fields: + identity = DirectParentReferenceFeatureDeclaration.from_feature_name( + field.name + ) + if identity is None: + continue + key = RuntimeMeasurementFeatureKey( + RuntimeMeasurementSubjectKey( + MeasurementScope.RELATIONSHIP, child_name + ), + normalize_runtime_identifier(identity.parent_object_name), + ) + values = pairs.setdefault(key, []) + for raw_row in table.rows.iter_row_mappings(): + row = RuntimeMeasurementRowMapping(measurement_row_mapping(raw_row)) + child_id = row.object_label() + parent_id = MeasurementScalarLiteral( + row.row[field.name] + ).integer_value + if child_id is None or parent_id is None or parent_id < 0: + raise ValueError( + "Relationship measurements require integral endpoint identities." + ) + child_key = image_number_offset.object_instance_key( + row.row, child_id + ) + if child_key.slice_index is None: + raise ValueError( + "Relationship measurements require explicit image identities." + ) + if parent_id > 0: + values.append( + ( + ObjectInstanceKey(parent_id, child_key.slice_index), + child_key, + ) + ) + result = {} + for key, values in pairs.items(): + ordered = tuple( + sorted( + values, + key=lambda pair: ( + pair[0].slice_index, + pair[0].object_id, + pair[1].object_id, + ), + ) + ) + graph = ObjectInstanceRelationship( + tuple(pair[0] for pair in ordered), tuple(pair[1] for pair in ordered) + ) + if len(set(graph.target_keys)) != len(graph.target_keys): + raise ValueError( + "Relationship measurements assign duplicate or contradictory child identities." + ) + parent_subject = RuntimeMeasurementSubjectKey( + MeasurementScope.OBJECT, key.feature_name + ) + parents = cls.object_rows( + object_tables, parent_subject, image_number_offset + ) + if not set(graph.source_keys) <= parents.keys(): + raise ValueError( + "Relationship measurements refer to an absent parent endpoint." + ) + children_by_parent = graph.child_keys_by_parent( + declared_source_keys=parents + ) + count_field = normalize_runtime_identifier( + ChildCountFeatureDeclaration.feature_name(key.subject.name) + ) + for parent, row in parents.items(): + observed = row.first_value((count_field,)) + if observed is not None and MeasurementScalarLiteral( + observed + ).integer_value != len(children_by_parent[parent]): + raise ValueError( + f"Relationship measurements disagree with {count_field!r} at parent {parent!r}." + ) + result[key] = graph + return result diff --git a/openhcs/core/runtime_equivalence.py b/openhcs/core/runtime_equivalence.py index 244f9edde5..f83d3f4251 100644 --- a/openhcs/core/runtime_equivalence.py +++ b/openhcs/core/runtime_equivalence.py @@ -69,6 +69,8 @@ MeasurementTable, ) from openhcs.core.runtime_relationships import ( + ObjectInstanceKey, + ObjectInstanceRelationship, ObjectRelationship, ) from openhcs.core.equivalence.policy import ( @@ -141,6 +143,7 @@ from openhcs.core.equivalence.relationships import ( RelationshipAggregateFeatureSemantics, RelationshipMeasurementSemantics, + ExportedRelationshipMeasurementSemantics, RuntimeObjectRelationshipIdentity, RuntimeScopedMeasurementTable, object_measurement_values_by_label, @@ -1708,7 +1711,7 @@ def project_measurement_fact_counts(self) -> RuntimeMeasurementFactCounterMap: str | None, ] RuntimeCellSignatureCachePayload = tuple[str, str] -RuntimeMeasurementSnapshotCachePayload = tuple[ +RuntimeMeasurementFactCachePayload = tuple[ tuple[ RuntimeMeasurementFeatureCachePayload, tuple[tuple[RuntimeCellSignatureCachePayload, int], ...], @@ -1717,11 +1720,24 @@ def project_measurement_fact_counts(self) -> RuntimeMeasurementFactCounterMap: ] +RuntimeRelationshipCorrelationCachePayload = tuple[ + tuple[RuntimeMeasurementFeatureCachePayload, tuple[tuple[tuple[int, int | None], tuple[int, int | None]], ...], int | None], + ..., +] +RuntimeMeasurementSnapshotCachePayload = tuple[ + RuntimeMeasurementFactCachePayload, + RuntimeRelationshipCorrelationCachePayload | None, +] + + @dataclass(slots=True) class RuntimeMeasurementSnapshot: """Semantic measurement facts independent of table layout.""" measurement_fact_counts: RuntimeMeasurementFactCounterMapping + correlated_relationships: ( + Mapping[RuntimeMeasurementFeatureKey, ObjectInstanceRelationship] | None + ) = None @classmethod def from_output_snapshot( @@ -1736,18 +1752,29 @@ def from_output_snapshot( policy=policy, known_source_names=known_source_names, ) - state.record_measurement_tables( - tuple( - RuntimeScopedMeasurementTable(measurement_table) - for table in snapshot.tables - for measurement_table in table.measurement_tables( - policy.measurement_dialect - ) - ), - None, + tables = tuple( + RuntimeScopedMeasurementTable(measurement_table) + for table in ExportedRelationshipMeasurementSemantics.validated_output_tables( + snapshot.tables, policy + ) + for measurement_table in table.measurement_tables( + policy.measurement_dialect + ) + ) + image_offset = RuntimeImageNumberOffset.from_runtime_rows( + row for table in tables for row in table.table.rows.iter_row_mappings() ) + correlations = ( + ExportedRelationshipMeasurementSemantics.correlated_object_relationships( + tables, image_offset + ) + ) + state.record_measurement_tables(tables, None) state.required_measurement_keys = frozenset(state.explicit_measurement_keys) - return cls(measurement_fact_counts=state.project_measurement_fact_counts()) + return cls( + measurement_fact_counts=state.project_measurement_fact_counts(), + correlated_relationships=correlations, + ) @classmethod def from_artifact_execution_observation( @@ -1779,48 +1806,100 @@ def __post_init__(self) -> None: } ) + if self.correlated_relationships is not None: + self.correlated_relationships = MappingProxyType( + dict(self.correlated_relationships) + ) + @property def is_empty(self) -> bool: return not self.measurement_fact_counts - def to_cache_payload( + def relationship_differences( self, - ) -> RuntimeMeasurementSnapshotCachePayload: - """Return a stable semantic cache payload for repeated equivalence checks.""" - return tuple( + candidate: ( + Mapping[RuntimeMeasurementFeatureKey, ObjectInstanceRelationship] | None + ), + ) -> tuple[RuntimeEquivalenceDifference, ...]: + """Compare saved correlations; None denotes the typed value-only scope.""" + if self.correlated_relationships is None or candidate is None: + return () + if self.correlated_relationships == candidate: + return () + return ( + RuntimeEquivalenceDifference( + RuntimeEquivalenceDifferenceKind.MEASUREMENT_CONTENT, + "directed relationship endpoint/image correlations differ", + ), + ) + + def to_cache_payload(self) -> RuntimeMeasurementSnapshotCachePayload: + """Transport numeric facts and known/unknown relationship evidence together.""" + facts = tuple( ( feature.to_cache_payload(), tuple( (value.to_cache_payload(), int(count)) for value, count in sorted( - values.items(), - key=lambda item: item[0].sort_key, + values.items(), key=lambda item: item[0].sort_key ) ), ) for feature, values in sorted( - self.measurement_fact_counts.items(), - key=lambda item: item[0].sort_key, + self.measurement_fact_counts.items(), key=lambda item: item[0].sort_key ) ) + relationships = None + if self.correlated_relationships is not None: + relationships = tuple( + ( + key.to_cache_payload(), + tuple( + ( + (source.object_id, source.slice_index), + (target.object_id, target.slice_index), + ) + for source, target in zip( + value.source_keys, value.target_keys, strict=True + ) + ), + value.slice_count, + ) + for key, value in sorted( + self.correlated_relationships.items(), + key=lambda item: item[0].sort_key, + ) + ) + return facts, relationships @classmethod def from_cache_payload( - cls, - payload: RuntimeMeasurementSnapshotCachePayload, + cls, payload: RuntimeMeasurementSnapshotCachePayload ) -> "RuntimeMeasurementSnapshot": - """Rebuild a semantic measurement snapshot from cache payload data.""" - measurement_fact_counts: RuntimeMeasurementFactCounterMap = {} - for feature_payload, values_payload in payload: # type: ignore[union-attr] - counter: Counter[RuntimeCellSignature] = Counter() - for value_payload, count in values_payload: - counter[RuntimeCellSignature.from_cache_payload(value_payload)] = int( - count + """Restore the same evidence scope, retaining every directed endpoint pair.""" + fact_payload, relationship_payload = payload + facts = { + RuntimeMeasurementFeatureKey.from_cache_payload(feature): Counter( + { + RuntimeCellSignature.from_cache_payload(value): count + for value, count in values + } + ) + for feature, values in fact_payload + } + relationships = None + if relationship_payload is not None: + relationships = { + RuntimeMeasurementFeatureKey.from_cache_payload( + key + ): ObjectInstanceRelationship( + tuple(ObjectInstanceKey(*source) for source, _target in pairs), + tuple(ObjectInstanceKey(*target) for _source, target in pairs), + slice_count, ) - measurement_fact_counts[ - RuntimeMeasurementFeatureKey.from_cache_payload(feature_payload) - ] = counter - return cls(measurement_fact_counts=measurement_fact_counts) + for key, pairs, slice_count in relationship_payload + } + return cls(facts, relationships) @dataclass(slots=True) @@ -1881,9 +1960,18 @@ def runtime_measurement_equivalence( *, policy: RuntimeEquivalencePolicy = RuntimeEquivalencePolicy(), ) -> RuntimeEquivalenceReport: - """Compare precomputed semantic measurement snapshots.""" + """Compare measurement facts and any jointly supplied saved edge correlations. + + Typed artifact snapshots retain their value-only scope when global saved + image numbering is unavailable. Full saved qualification must use the + known correlation evidence produced by ``from_output_snapshot`` on both + sides; an UNKNOWN typed projection is not such a qualification. + """ return RuntimeEquivalenceReport( - differences=_measurement_differences(reference, candidate, policy) + differences=( + *_measurement_differences(reference, candidate, policy), + *reference.relationship_differences(candidate.correlated_relationships), + ) ) diff --git a/openhcs/core/runtime_relationships.py b/openhcs/core/runtime_relationships.py index 1d542b67e1..ea3b158552 100644 --- a/openhcs/core/runtime_relationships.py +++ b/openhcs/core/runtime_relationships.py @@ -27,7 +27,12 @@ from abc import abstractmethod from collections.abc import Iterable from openhcs.core.registry_strategies import MostDerivedContextStrategyMixin -from openhcs.core.runtime_measurements import MeasurementRowAxisField +from openhcs.core.runtime_measurements import ( + MeasurementRowAxisField, + ObjectReferenceFeatureMarker, + RuntimeMeasurementFeatureDeclaration, + MeasurementScalarLiteral, +) from openhcs.core.source_spatial_domain import SourceSpatialDomainAdapter from openhcs.core.runtime_object_label_domains import ObjectLabelDomainMetadataStrategy from openhcs.core.runtime_object_label_domains import ObjectLabelDomainScope @@ -41,10 +46,106 @@ from openhcs.core.runtime_sparse_labels import SparseIJVLabelRows +class DirectParentReferenceFeatureMarker(ObjectReferenceFeatureMarker): + """Semantic marker for a child's direct parent-object reference.""" + + +@dataclass(frozen=True, slots=True) +class DirectParentReferenceMeasurementFeature: + """Nominal identity encoded by a ``Parent_`` measurement name.""" + + parent_object_name: str + + def __post_init__(self) -> None: + if not isinstance(self.parent_object_name, str) or not self.parent_object_name: + raise ValueError("Direct parent-reference object name cannot be empty.") + + +class DirectParentReferenceFeatureDeclaration(RuntimeMeasurementFeatureDeclaration): + """Parse and render direct parent references at their row-production owner.""" + + declaration_key = "direct_parent_reference" + semantic_marker_types = (DirectParentReferenceFeatureMarker,) + prefix = "Parent_" + + @classmethod + def from_feature_name( + cls, + feature_name: str, + ) -> DirectParentReferenceMeasurementFeature | None: + if not feature_name.startswith(cls.prefix): + return None + parent_object_name = feature_name[len(cls.prefix) :] + if not parent_object_name: + return None + return DirectParentReferenceMeasurementFeature(parent_object_name) + + @classmethod + def feature_name(cls, identity: object) -> str: + if not isinstance(identity, DirectParentReferenceMeasurementFeature): + raise TypeError( + f"{cls.__name__}.feature_name requires " + "DirectParentReferenceMeasurementFeature." + ) + return f"{cls.prefix}{identity.parent_object_name}" + + +class ChildCountFeatureDeclaration(RuntimeMeasurementFeatureDeclaration): + """Own the child-name grammar shared by export readers and CP producers.""" + + declaration_key = "child_count_reference" + prefix = "Children_" + suffix = "_Count" + + @classmethod + def from_feature_name(cls, feature_name: str) -> str | None: + if not feature_name.startswith(cls.prefix) or not feature_name.endswith( + cls.suffix + ): + return None + name = feature_name[len(cls.prefix) : -len(cls.suffix)].strip() + return name or None + + @classmethod + def feature_name(cls, identity: str) -> str: + name = identity.strip() + if not name: + raise ValueError( + "Child-count feature requires a non-empty child object name." + ) + return f"{cls.prefix}{name}{cls.suffix}" + + @dataclass(frozen=True) class ObjectRelationshipDeclaration(ArtifactSpecRelation): """Authoritative directed relationship semantics for one artifact output.""" + exported_field_names: ClassVar[tuple[str, ...]] = ( + "relationship_type", + "source_role", + "target_role", + "source_object", + "target_object", + "producer_module_number", + ) + + def exported_columns(self) -> dict[str, object]: + """Render the declaration owned by every exported edge row.""" + return dict( + zip( + self.exported_field_names, + ( + self.relationship_type, + self.source_role, + self.target_role, + self.source.name, + self.target.name, + self.producer_module_number, + ), + strict=True, + ) + ) + relation_key = "object_relationship_declaration" target_plan_type: ClassVar[type[ArtifactPlan] | None] = None target: ArtifactSpecRef @@ -122,6 +223,45 @@ def parent_child( target_runtime_slice_offset=target_runtime_slice_offset, ) + @classmethod + def from_exported_values( + cls, + relationship_type: str, + source_role: str, + target_role: str, + source_object: str, + target_object: str, + producer_module_number: object, + ) -> Self: + """Restore the complete declared parent/child edge grammar from CSV.""" + source = ArtifactSpec.output(source_object, ObjectLabelsArtifactType).ref() + target = ArtifactSpec.output(target_object, ObjectLabelsArtifactType).ref() + module = MeasurementScalarLiteral(producer_module_number).integer_value + if module is None: + raise ValueError("Relationship producer module number is required.") + canonical = cls.parent_child( + source=source, target=target, producer_module_number=module + ) + roles = (source_role, target_role) + canonical_roles = (canonical.source_role, canonical.target_role) + fields = (canonical.source_id_field, canonical.target_id_field) + if roles == canonical_roles: + source_field, target_field = fields + elif roles == canonical_roles[::-1]: + target_field, source_field = fields + else: + raise ValueError(f"Unsupported exported relationship roles {roles!r}.") + return cls( + source=source, + target=target, + relationship_type=relationship_type, + source_role=roles[0], + target_role=roles[1], + source_id_field=source_field, + target_id_field=target_field, + producer_module_number=module, + ) + def require_target_spec(self, spec: ArtifactSpec) -> None: super().require_target_spec(spec) if not issubclass(spec.artifact_type, ObjectLineageArtifactType): @@ -244,16 +384,9 @@ def project_runtime_slice(self, slice_index: int) -> "ObjectRelationship": def relationship_columns(self) -> dict[str, Any]: """Return semantic relationship columns without payload provenance.""" - columns = { - "relationship_type": self.declaration.relationship_type, - "source_role": self.declaration.source_role, - "target_role": self.declaration.target_role, - "source_object": self.declaration.source.name, - "target_object": self.declaration.target.name, - "producer_module_number": self.declaration.producer_module_number, - self.declaration.source_id_field: self.payload.source_ids, - self.declaration.target_id_field: self.payload.target_ids, - } + columns = self.declaration.exported_columns() + columns[self.declaration.source_id_field] = self.payload.source_ids + columns[self.declaration.target_id_field] = self.payload.target_ids if self.payload.slice_indices: columns["slice_index"] = self.payload.slice_indices if self.payload.slice_count is not None: @@ -951,6 +1084,10 @@ def child_keys_by_parent( ) } + def reverse_endpoints(self) -> Self: + """Transpose the same directed instance pairs without losing their axes.""" + return type(self)(self.target_keys, self.source_keys, self.slice_count) + def parent_key_by_child(self) -> dict[ObjectInstanceKey, ObjectInstanceKey]: """Return source identity for each target identity.""" return dict(zip(self.target_keys, self.source_keys, strict=True)) diff --git a/openhcs/interop/cellprofiler/measurement_lookup.py b/openhcs/interop/cellprofiler/measurement_lookup.py index 8d285990a6..120db857d4 100644 --- a/openhcs/interop/cellprofiler/measurement_lookup.py +++ b/openhcs/interop/cellprofiler/measurement_lookup.py @@ -9,6 +9,7 @@ from metaclass_registry import AutoRegisterMeta +from openhcs.core.runtime_relationships import ChildCountFeatureDeclaration from openhcs.core.public_api import declared_public_names from openhcs.core.measurement_feature_queries import measurement_values_for_feature @@ -151,19 +152,10 @@ class CellProfilerChildCountFeatureParser(CellProfilerMeasurementFeatureParser): kind = CellProfilerMeasurementFeatureKind.CHILD_COUNT kind_key = kind.value - prefix = "Children_" - suffix = "_Count" - def parse_feature( - self, - feature_name: str, - ) -> CellProfilerMeasurementFeature | None: - if not feature_name.startswith(self.prefix): - return None - if not feature_name.endswith(self.suffix): - return None - object_name = feature_name[len(self.prefix) : -len(self.suffix)].strip() - if not object_name: + def parse_feature(self, feature_name: str) -> CellProfilerMeasurementFeature | None: + object_name = ChildCountFeatureDeclaration.from_feature_name(feature_name) + if object_name is None: return None return CellProfilerMeasurementFeature( name=feature_name, @@ -172,18 +164,13 @@ def parse_feature( ) def feature_from_object_name( - self, - object_name: str, + self, object_name: str ) -> CellProfilerMeasurementFeature: - normalized = object_name.strip() - if not normalized: - raise ValueError( - "Child-count feature requires a non-empty child object name." - ) + feature_name = ChildCountFeatureDeclaration.feature_name(object_name) return CellProfilerMeasurementFeature( - name=f"{self.prefix}{normalized}{self.suffix}", + name=feature_name, kind=CellProfilerMeasurementFeatureKind.CHILD_COUNT, - object_name=normalized, + object_name=object_name.strip(), ) diff --git a/openhcs/interop/cellprofiler/runtime/relationship_measurement_rows.py b/openhcs/interop/cellprofiler/runtime/relationship_measurement_rows.py index f34f814108..5849251569 100644 --- a/openhcs/interop/cellprofiler/runtime/relationship_measurement_rows.py +++ b/openhcs/interop/cellprofiler/runtime/relationship_measurement_rows.py @@ -17,8 +17,6 @@ ) from openhcs.core.runtime_measurements import ( MeasurementRowAxisField, - ObjectReferenceFeatureMarker, - RuntimeMeasurementFeatureDeclaration, ) from openhcs.core.runtime_object_label_domains import ( ObjectLabelDomainScope, @@ -35,6 +33,8 @@ from openhcs.core.runtime_relationships import ( ObjectRelationship, ObjectRelationshipDeclaration, + DirectParentReferenceFeatureDeclaration, + DirectParentReferenceMeasurementFeature, ) from openhcs.core.runtime_image_values import ( image_payload_metadata, @@ -53,50 +53,6 @@ ) -class DirectParentReferenceFeatureMarker(ObjectReferenceFeatureMarker): - """Semantic marker for a child's direct parent-object reference.""" - - -@dataclass(frozen=True, slots=True) -class DirectParentReferenceMeasurementFeature: - """Nominal identity encoded by a ``Parent_`` measurement name.""" - - parent_object_name: str - - def __post_init__(self) -> None: - if not isinstance(self.parent_object_name, str) or not self.parent_object_name: - raise ValueError("Direct parent-reference object name cannot be empty.") - - -class DirectParentReferenceFeatureDeclaration(RuntimeMeasurementFeatureDeclaration): - """Parse and render direct parent references at their row-production owner.""" - - declaration_key = "direct_parent_reference" - semantic_marker_types = (DirectParentReferenceFeatureMarker,) - prefix = "Parent_" - - @classmethod - def from_feature_name( - cls, - feature_name: str, - ) -> DirectParentReferenceMeasurementFeature | None: - if not feature_name.startswith(cls.prefix): - return None - parent_object_name = feature_name[len(cls.prefix) :] - if not parent_object_name: - return None - return DirectParentReferenceMeasurementFeature(parent_object_name) - - @classmethod - def feature_name(cls, identity: object) -> str: - if not isinstance(identity, DirectParentReferenceMeasurementFeature): - raise TypeError( - f"{cls.__name__}.feature_name requires " - "DirectParentReferenceMeasurementFeature." - ) - return f"{cls.prefix}{identity.parent_object_name}" - - @dataclass(frozen=True, slots=True) class RelationshipMeasurementRows: """Project parent-child relationship payloads into CP object measurement rows.""" diff --git a/openhcs/processing/backends/cellprofiler/relationships.py b/openhcs/processing/backends/cellprofiler/relationships.py index b69c31d2f6..0f6f2d0380 100644 --- a/openhcs/processing/backends/cellprofiler/relationships.py +++ b/openhcs/processing/backends/cellprofiler/relationships.py @@ -5,6 +5,9 @@ from dataclasses import dataclass, replace from enum import Enum from typing import TYPE_CHECKING, Annotated +from openhcs.core.runtime_relationships import ( + DirectParentReferenceFeatureMarker, +) from openhcs.core.alias_property import AliasProperty from openhcs.core.artifacts import ( ArtifactInputPlan, @@ -71,7 +74,6 @@ ObjectLocationMeasurementRows, ) from openhcs.interop.cellprofiler.runtime.relationship_measurement_rows import ( - DirectParentReferenceFeatureMarker, RelationshipMeasurementRows, ) diff --git a/tests/unit/test_cellprofiler_module_execution.py b/tests/unit/test_cellprofiler_module_execution.py index 90f612d3e4..43240f69c8 100644 --- a/tests/unit/test_cellprofiler_module_execution.py +++ b/tests/unit/test_cellprofiler_module_execution.py @@ -10,6 +10,10 @@ import openhcs.processing.backends.cellprofiler.secondary as iso import openhcs.processing.backends.cellprofiler.secondary as ito +from openhcs.core.runtime_relationships import ( + DirectParentReferenceFeatureDeclaration, + DirectParentReferenceMeasurementFeature, +) from openhcs.constants.constants import ( AllComponents, Backend, @@ -252,8 +256,6 @@ CellProfilerOutputRecorder, ) from openhcs.interop.cellprofiler.runtime.relationship_measurement_rows import ( - DirectParentReferenceFeatureDeclaration, - DirectParentReferenceMeasurementFeature, RelationshipMeasurementRows, ) from openhcs.processing.backends.cellprofiler.alignment import ( diff --git a/tests/unit/test_cellprofiler_relationship_contracts.py b/tests/unit/test_cellprofiler_relationship_contracts.py index 74ee1d575a..64a352ebb2 100644 --- a/tests/unit/test_cellprofiler_relationship_contracts.py +++ b/tests/unit/test_cellprofiler_relationship_contracts.py @@ -6,6 +6,10 @@ import numpy as np +from openhcs.core.runtime_relationships import ( + DirectParentReferenceFeatureDeclaration, + DirectParentReferenceMeasurementFeature, +) from openhcs.core.artifacts import ( ArtifactInputPlan, ArtifactOutputPlan, @@ -49,10 +53,6 @@ ) from openhcs.interop.cellprofiler.pipeline_import import import_cellprofiler_pipeline from openhcs.interop.cellprofiler.setting_names import setting_values -from openhcs.interop.cellprofiler.runtime.relationship_measurement_rows import ( - DirectParentReferenceFeatureDeclaration, - DirectParentReferenceMeasurementFeature, -) from openhcs.processing.backends.cellprofiler.neighbors import ( MeasureObjectNeighborsModule, ) diff --git a/tests/unit/test_exported_relationship_equivalence.py b/tests/unit/test_exported_relationship_equivalence.py new file mode 100644 index 0000000000..60273f2fac --- /dev/null +++ b/tests/unit/test_exported_relationship_equivalence.py @@ -0,0 +1,393 @@ +"""Saved directed edges must retain the native object-row correlations.""" + +from pathlib import Path + +import pytest + +from openhcs.core.equivalence.outputs import RuntimeOutputSnapshot +from openhcs.core.runtime_equivalence import ( + RuntimeMeasurementSnapshot, + runtime_measurement_equivalence, +) + +_HEADER = ( + "relationship_type,source_role,target_role,source_object,target_object," + "producer_module_number,parent_id,child_id,image_number,slice_count\n" +) + + +def _saved_outputs( + root: Path, *, edges: bool, swapped: bool = False +) -> RuntimeOutputSnapshot: + root.mkdir() + (root / "Cells.csv").write_text( + "ImageNumber,ObjectNumber,Children_Nuclei_Count\n" + "1,1,1\n1,2,1\n1,3,0\n2,1,1\n2,2,1\n2,3,0\n" + ) + parents = (2, 1) if swapped else (1, 2) + (root / "Nuclei.csv").write_text( + "ImageNumber,ObjectNumber,Parent_Cells\n" + + "".join( + f"{image},{child},{parents[child - 1]}\n" + for image in (1, 2) + for child in (1, 2) + ) + ) + if edges: + (root / "Relationships.csv").write_text( + _HEADER + + "".join( + f"{kind},{source_role},{target_role},{source},{target},11," + f"{parents[child - 1]},{child},{image},2\n" + for kind, source_role, target_role, source, target in ( + ("Parent", "parent", "child", "Cells", "Nuclei"), + ("Child", "child", "parent", "Nuclei", "Cells"), + ) + for image in (1, 2) + for child in (1, 2) + ) + ) + return RuntimeOutputSnapshot.from_output_root(root) + + +def test_saved_relationship_edges_join_native_parent_and_zero_child_counts( + tmp_path: Path, +) -> None: + reference = RuntimeMeasurementSnapshot.from_output_snapshot( + _saved_outputs(tmp_path / "native", edges=False) + ) + candidate = RuntimeMeasurementSnapshot.from_output_snapshot( + _saved_outputs(tmp_path / "openhcs", edges=True) + ) + assert runtime_measurement_equivalence(reference, candidate).is_equivalent + + +def test_relationship_endpoint_permutation_cannot_hide_in_equal_value_histograms( + tmp_path: Path, +) -> None: + reference = RuntimeMeasurementSnapshot.from_output_snapshot( + _saved_outputs(tmp_path / "native", edges=False) + ) + candidate = RuntimeMeasurementSnapshot.from_output_snapshot( + _saved_outputs(tmp_path / "openhcs", edges=True, swapped=True) + ) + assert not runtime_measurement_equivalence(reference, candidate).is_equivalent + + +def test_relationship_rows_must_agree_with_explicit_parent_measurements( + tmp_path: Path, +) -> None: + snapshot = _saved_outputs(tmp_path / "openhcs", edges=True) + table = next( + table for table in snapshot.tables if table.path.stem == "Relationships" + ) + table.rows = tuple( + tuple( + "2" if index == 6 and row[7] == "1" and row[8] == "1" else value + for index, value in enumerate(row) + ) + for row in table.rows + ) + with pytest.raises(ValueError, match="relationship|Relationship"): + RuntimeMeasurementSnapshot.from_output_snapshot(snapshot) + + +def test_ordinary_image_row_conflicts_remain_rejected(tmp_path: Path) -> None: + root = tmp_path / "ordinary" + root.mkdir() + (root / "Image.csv").write_text("ImageNumber,Count_Nuclei\n1,2\n1,3\n") + with pytest.raises(ValueError, match="conflicting observed values"): + RuntimeMeasurementSnapshot.from_output_snapshot( + RuntimeOutputSnapshot.from_output_root(root) + ) + + +@pytest.mark.parametrize( + "mutation, message", + [ + ( + lambda header, rows: (header, rows[:-1]), + "Relationship edges disagree|reverse declarations", + ), + (lambda header, rows: (header, (*rows, rows[0])), "duplicate directed edges"), + ( + lambda header, rows: ( + header, + tuple( + tuple( + "12" if i == 5 and row[0] == "Child" else value + for i, value in enumerate(row) + ) + for row in rows + ), + ), + "reverse declarations", + ), + ( + lambda header, rows: ( + header, + tuple( + tuple( + "neighbor_source" if i == 1 else value + for i, value in enumerate(row) + ) + for row in rows + ), + ), + "Unsupported exported relationship roles", + ), + ( + lambda header, rows: ( + header, + tuple( + tuple("999" if i == 7 else value for i, value in enumerate(row)) + for row in rows + ), + ), + "absent child", + ), + ( + lambda header, rows: ( + header, + tuple( + tuple("2.5" if i == 6 else value for i, value in enumerate(row)) + for row in rows + ), + ), + "nonnegative integers", + ), + ( + lambda header, rows: ( + header, + tuple( + tuple("2.5" if i == 9 else value for i, value in enumerate(row)) + for row in rows + ), + ), + "slice count must be an integer", + ), + ( + lambda header, rows: (header[1:], tuple(row[1:] for row in rows)), + "incomplete declaration", + ), + (lambda header, rows: (header, ()), "no determining declaration"), + ], +) +def test_saved_edge_schema_and_coverage_rejects_malformed_inputs( + tmp_path: Path, mutation, message: str +) -> None: + snapshot = _saved_outputs(tmp_path / "openhcs", edges=True) + table = next( + table for table in snapshot.tables if table.path.stem == "Relationships" + ) + table.header, table.rows = mutation(table.header, table.rows) + with pytest.raises((ValueError, TypeError), match=message): + RuntimeMeasurementSnapshot.from_output_snapshot(snapshot) + + +def test_saved_relationship_counts_include_noncontiguous_zero_child_parents( + tmp_path: Path, +) -> None: + native = _saved_outputs(tmp_path / "native", edges=False) + candidate = _saved_outputs(tmp_path / "openhcs", edges=True) + for snapshot in (native, candidate): + for table in snapshot.tables: + table.rows = tuple( + tuple( + ( + "30" + if table.path.stem == "Cells" and i == 1 and value == "3" + else value + ) + for i, value in enumerate(row) + ) + for row in table.rows + ) + a = RuntimeMeasurementSnapshot.from_output_snapshot(native) + b = RuntimeMeasurementSnapshot.from_output_snapshot(candidate) + assert ( + a.correlated_relationships is not None + and b.correlated_relationships is not None + ) + assert runtime_measurement_equivalence(a, b).is_equivalent + + +def test_snapshot_cache_retains_known_correlations_and_unknown_typed_scope( + tmp_path: Path, +) -> None: + known = RuntimeMeasurementSnapshot.from_output_snapshot( + _saved_outputs(tmp_path / "saved", edges=True) + ) + unknown = RuntimeMeasurementSnapshot(known.measurement_fact_counts) + for snapshot in (known, unknown): + restored = RuntimeMeasurementSnapshot.from_cache_payload( + snapshot.to_cache_payload() + ) + assert restored.measurement_fact_counts == snapshot.measurement_fact_counts + assert restored.correlated_relationships == snapshot.correlated_relationships + assert unknown.correlated_relationships is None + assert known.correlated_relationships is not None + + +def test_cached_relationship_preserves_declared_slice_count_and_endpoint_axes() -> None: + from openhcs.core.equivalence.keys import ( + RuntimeMeasurementFeatureKey, + RuntimeMeasurementSubjectKey, + ) + from openhcs.core.runtime_measurements import MeasurementScope + from openhcs.core.runtime_relationships import ( + ObjectInstanceKey, + ObjectInstanceRelationship, + ) + + key = RuntimeMeasurementFeatureKey( + RuntimeMeasurementSubjectKey(MeasurementScope.RELATIONSHIP, "child"), "parent" + ) + graph = ObjectInstanceRelationship( + (ObjectInstanceKey(2, 0),), (ObjectInstanceKey(5, 1),), 2 + ) + snapshot = RuntimeMeasurementSnapshot({}, {key: graph}) + assert ( + RuntimeMeasurementSnapshot.from_cache_payload( + snapshot.to_cache_payload() + ).correlated_relationships + == snapshot.correlated_relationships + ) + + +def test_saved_relationship_omitted_slice_count_preserves_optional_payload_contract( + tmp_path: Path, +) -> None: + snapshot = _saved_outputs(tmp_path / "saved", edges=True) + table = next( + table for table in snapshot.tables if table.path.stem == "Relationships" + ) + table.header = table.header[:-1] + table.rows = tuple(row[:-1] for row in table.rows) + result = RuntimeMeasurementSnapshot.from_output_snapshot(snapshot) + assert result.correlated_relationships is not None + + +def test_saved_relationship_row_order_does_not_change_directed_correlation( + tmp_path: Path, +) -> None: + native = RuntimeMeasurementSnapshot.from_output_snapshot( + _saved_outputs(tmp_path / "native", edges=False) + ) + snapshot = _saved_outputs(tmp_path / "saved", edges=True) + table = next( + table for table in snapshot.tables if table.path.stem == "Relationships" + ) + table.rows = table.rows[:4] + table.rows[4:][::-1] + result = RuntimeMeasurementSnapshot.from_output_snapshot(snapshot) + assert runtime_measurement_equivalence(native, result).is_equivalent + + +def test_unparented_zero_edges_still_validate_actual_child_domain( + tmp_path: Path, +) -> None: + snapshot = _saved_outputs(tmp_path / "saved", edges=True) + for table in snapshot.tables: + if table.path.stem == "Cells": + table.rows = tuple((*row[:2], "0") for row in table.rows) + elif table.path.stem == "Nuclei": + table.rows = tuple((*row[:2], "0") for row in table.rows) + else: + table.rows = tuple((*row[:6], "0", *row[7:]) for row in table.rows) + result = RuntimeMeasurementSnapshot.from_output_snapshot(snapshot) + assert result.correlated_relationships is not None + assert all( + not graph.source_keys for graph in result.correlated_relationships.values() + ) + table = next( + table for table in snapshot.tables if table.path.stem == "Relationships" + ) + table.rows = tuple((*row[:7], "999", *row[8:]) for row in table.rows) + with pytest.raises(ValueError, match="absent child"): + RuntimeMeasurementSnapshot.from_output_snapshot(snapshot) + + +@pytest.mark.parametrize( + "feature, identity", + [ + ("Children_ Nuclei _Count", "Nuclei"), + ("Children__Count", None), + ("Children_Nuclei", None), + (" children_Nuclei_Count", None), + ], +) +def test_child_count_parser_and_core_declaration_share_original_grammar( + feature: str, identity: str | None +) -> None: + from openhcs.core.runtime_relationships import ChildCountFeatureDeclaration + from openhcs.interop.cellprofiler.measurement_lookup import ( + CellProfilerChildCountFeatureParser, + ) + + assert ChildCountFeatureDeclaration.from_feature_name(feature) == identity + parsed = CellProfilerChildCountFeatureParser().parse_feature(feature) + assert (None if parsed is None else parsed.object_name) == identity + + +@pytest.mark.parametrize("swapped", (False, True)) +def test_physical_inventory_consumes_redundant_edges_and_compares_exact_correlations( + tmp_path: Path, swapped: bool +) -> None: + from benchmark.matched_cellprofiler_batch import _require_compared_output_inventory + from openhcs.core.runtime_exports import RuntimeExportObservation + + reference = _saved_outputs(tmp_path / "native", edges=False) + candidate = _saved_outputs(tmp_path / "saved", edges=True, swapped=swapped) + reference_exports = RuntimeExportObservation.from_output_root(tmp_path / "native") + candidate_exports = RuntimeExportObservation.from_output_root(tmp_path / "saved") + kwargs = dict( + reference_files=frozenset((tmp_path / "native").iterdir()), + candidate_files=frozenset((tmp_path / "saved").iterdir()), + reference_exports=reference_exports, + candidate_exports=candidate_exports, + reference_snapshot=reference, + candidate_snapshot=candidate, + ) + if swapped: + with pytest.raises(RuntimeError, match="correlations differ"): + _require_compared_output_inventory(**kwargs) + else: + _require_compared_output_inventory(**kwargs) + + +@pytest.mark.parametrize( + "parent_value, message", + [("999", "absent parent endpoint"), ("2.5", "integral endpoint")], +) +def test_native_parent_rows_require_real_integral_parent_domain( + tmp_path: Path, parent_value: str, message: str +) -> None: + snapshot = _saved_outputs(tmp_path / "native", edges=False) + table = next(table for table in snapshot.tables if table.path.stem == "Nuclei") + table.rows = tuple((*row[:2], parent_value) for row in table.rows) + with pytest.raises(ValueError, match=message): + RuntimeMeasurementSnapshot.from_output_snapshot(snapshot) + + +def test_native_child_count_permutation_cannot_hide_in_equal_histograms( + tmp_path: Path, +) -> None: + snapshot = _saved_outputs(tmp_path / "native", edges=False) + table = next(table for table in snapshot.tables if table.path.stem == "Cells") + table.rows = tuple((*row[:2], "0" if row[1] == "1" else "1") for row in table.rows) + with pytest.raises(ValueError, match="disagree with.*children_nuclei_count"): + RuntimeMeasurementSnapshot.from_output_snapshot(snapshot) + + +def test_snapshot_json_cache_retains_correlation_scope(tmp_path: Path) -> None: + import json + + known = RuntimeMeasurementSnapshot.from_output_snapshot( + _saved_outputs(tmp_path / "saved", edges=True) + ) + for snapshot in (known, RuntimeMeasurementSnapshot(known.measurement_fact_counts)): + restored = RuntimeMeasurementSnapshot.from_cache_payload( + json.loads(json.dumps(snapshot.to_cache_payload())) + ) + assert restored.correlated_relationships == snapshot.correlated_relationships + assert restored.measurement_fact_counts == snapshot.measurement_fact_counts From acb26de631be9d451f6421e23e31ace28da5e232 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 17:34:02 -0400 Subject: [PATCH 090/235] Render authored CPA workspace panels and compare saved semantics (refs #480) --- benchmark/cellprofiler_export_equivalence.py | 33 ++ benchmark/matched_cellprofiler_batch.py | 2 +- docs/development/cpa_workspace_export_480.md | 13 + .../interop/cellprofiler/analyst_export.py | 4 + .../interop/cellprofiler/workspace_export.py | 359 ++++++++++++++++++ .../cellprofiler/export_to_database.py | 89 ++--- .../quality_control_4281.workspace | 71 ++++ .../unit/test_cellprofiler_analyst_export.py | 33 +- .../test_cellprofiler_export_to_database.py | 1 - .../test_cellprofiler_workspace_export.py | 239 ++++++++++++ 10 files changed, 777 insertions(+), 67 deletions(-) create mode 100644 docs/development/cpa_workspace_export_480.md create mode 100644 openhcs/interop/cellprofiler/workspace_export.py create mode 100644 tests/fixtures/cellprofiler/quality_control_4281.workspace create mode 100644 tests/unit/test_cellprofiler_workspace_export.py diff --git a/benchmark/cellprofiler_export_equivalence.py b/benchmark/cellprofiler_export_equivalence.py index ded38131a3..a07723be6e 100644 --- a/benchmark/cellprofiler_export_equivalence.py +++ b/benchmark/cellprofiler_export_equivalence.py @@ -70,6 +70,10 @@ def cellprofiler_database_export_equivalence( _declared_sqlite_table_subjects(candidate_properties), policy, ), + *_workspace_export_differences( + tuple(sorted(reference_root.rglob("*.workspace"))), + _outputs_with_suffix(candidate_exports, ".workspace"), + ), *_properties_export_differences( reference_properties, candidate_properties, @@ -157,6 +161,12 @@ def cellprofiler_native_shard_equivalence( differences.extend( _properties_export_differences(reference_properties, shard_properties) ) + differences.extend( + _workspace_export_differences( + tuple(sorted(reference_root.rglob("*.workspace"))), + tuple(sorted(root.rglob("*.workspace"))), + ) + ) shard_subjects = _declared_sqlite_table_subjects(shard_properties) if shard_subjects != reference_subjects: differences.append( @@ -686,3 +696,26 @@ def _unique_paths_by_name( ) by_name[path.name] = path return by_name + + +def _workspace_export_differences(reference_paths, candidate_paths): + from openhcs.interop.cellprofiler.workspace_export import CPAWorkspacePanel + + differences, reference_by_name, candidate_by_name = _named_output_differences( + reference_paths, candidate_paths, output_label="CPA workspace" + ) + for name in reference_by_name.keys() & candidate_by_name.keys(): + reference = CPAWorkspacePanel.parse_workspace( + reference_by_name[name].read_text() + ) + candidate = CPAWorkspacePanel.parse_workspace( + candidate_by_name[name].read_text() + ) + if reference != candidate: + differences.append( + RuntimeEquivalenceDifference( + RuntimeEquivalenceDifferenceKind.TABLE_CONTENT, + f"CPA workspace {name!r} panel declarations differ.", + ) + ) + return tuple(differences) diff --git a/benchmark/matched_cellprofiler_batch.py b/benchmark/matched_cellprofiler_batch.py index 53441d6777..ff7652e785 100644 --- a/benchmark/matched_cellprofiler_batch.py +++ b/benchmark/matched_cellprofiler_batch.py @@ -196,7 +196,7 @@ def _require_compared_output_inventory( | frozenset( path for path in exports.output_files - if path.suffix.lower() in {".db", ".properties"} + if path.suffix.lower() in {".db", ".properties", ".workspace"} ) ) if files != compared_files: diff --git a/docs/development/cpa_workspace_export_480.md b/docs/development/cpa_workspace_export_480.md new file mode 100644 index 0000000000..44e67d55fd --- /dev/null +++ b/docs/development/cpa_workspace_export_480.md @@ -0,0 +1,13 @@ +# Authored CPA workspace export (#480) + +ExportToDatabase accepted and validated workspace panel settings, then discarded them. The public export wrote SQLite and CPA properties while omitting the requested workspace. Both fresh native QC repetitions retained twelve workspace panels; the original complete-output comparison stayed red. + +The existing export settings now retain the requested workspace and original ordered panel declarations. `CPAWorkspacePanel` owns the external panel grammar, repeated setting binding and authoring transport. Its Scatter/Density leaves share two-axis rendering; Histogram/Box/PlateViewer use the single-axis grammar, with PlateViewer's native `measurement` field. `CPAWorkspaceAxis` derives its Image/Object/Index implementations from their declarations. Image/object measurements use the existing `MeasurementSubject` and database dialect; there is no parallel tool enum or handwritten dispatch roster. + +The public exporter merges workspace text into the existing file bundle and retains its duplicate-file rejection. The shared database-export comparison parses ordered workspace semantics, rejects invalid layout or unknown fields/tools, and reports missing or changed panels. The strict matched inventory now admits `.workspace` because it has an actual comparison consumer. Database and properties rendering remain unchanged. + +Native 4.2.8.1 emits raw subject/feature names in workspace axes, and Object axes always name `Per_Object`, even when separate object tables are selected. These native quirks are preserved, not silently translated to different scientific columns. The near-term QC fixture has only Image/Index axes: both retained native workspace texts match exactly, and all 22 axis references resolve to actual native SQLite columns. This does not establish arbitrary native object-workspace references or shortened column names are valid. The reader validates workflow format version 1 and the numeric producer-version header; the CP producer version is descriptive rather than compared panel meaning. + +The retained native fixture comes from source `6e69b85122d3c1a44857835ba9b3fe77e743240d`, exact matched ten physical images/two image sets and native CP 4.2.8.1. Its SHA256 is `884f14365ed643c8dd7050ea77e416a1b7253dca83ff11d2e01aa54a8e07bdc6`. Source-backed installed native renderer: `cellprofiler/modules/exporttodatabase.py:4799`. The original native inventory red and interrupted test shard remain in `/var/tmp`; the current production pipeline still needs a fresh complete-output gate before #480 closes. + +Ownership evidence reuses the complete NRA class census from the shared derivation audit and the finite original CPA class inventory at `/var/tmp/openhcs-qc-workspace-owner-inventory-20261002.json`. The bounded migration removes the old validate-and-discard workspace algorithm from `ExportToDatabaseModule`; authored setting names remain authoritative there, and the panel owner consumes those exact declarations. These are feature and boundary-format changes authored as exact patches; no automatic codemod or formal scientific equivalence is claimed. diff --git a/openhcs/interop/cellprofiler/analyst_export.py b/openhcs/interop/cellprofiler/analyst_export.py index 8aeacc0359..9ceba032f7 100644 --- a/openhcs/interop/cellprofiler/analyst_export.py +++ b/openhcs/interop/cellprofiler/analyst_export.py @@ -6,6 +6,8 @@ from __future__ import annotations +from openhcs.interop.cellprofiler.workspace_export import CPAWorkspacePanel + from base64 import b64encode from collections import defaultdict from collections.abc import Mapping, Sequence @@ -283,6 +285,8 @@ class CellProfilerDatabaseExportSettings: selected_objects: tuple[str, ...] | None wants_properties_file: bool wants_relationship_tables: bool + wants_workspace_file: bool = False + workspace_panels: tuple[CPAWorkspacePanel, ...] = () maximum_column_name_length: int = 64 location_object: str | None = None plate_type: str | None = None diff --git a/openhcs/interop/cellprofiler/workspace_export.py b/openhcs/interop/cellprofiler/workspace_export.py new file mode 100644 index 0000000000..c32a9a4670 --- /dev/null +++ b/openhcs/interop/cellprofiler/workspace_export.py @@ -0,0 +1,359 @@ +"""Authored CellProfiler Analyst workspace panels and their external grammar.""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from dataclasses import dataclass +from pathlib import Path +from typing import ClassVar, TYPE_CHECKING + +from metaclass_registry import AutoRegisterMeta + +from openhcs.core.runtime_measurements import MeasurementSubject, MeasurementScope +from openhcs.core.runtime_tabular_values import FieldSpec + +from openhcs.interop.cellprofiler.database_column_dialect import ( + CellProfilerDatabaseColumnDialect, +) + +from openhcs.interop.cellprofiler.parser import ModuleBlock, ModuleSetting +from openhcs.interop.cellprofiler.cellprofiler_literals import ( + cellprofiler_setting_literal, +) +from openhcs.interop.cellprofiler.setting_names import optional_setting_value +from openhcs.interop.cellprofiler.settings_binder import parse_cellprofiler_bool + +if TYPE_CHECKING: + from openhcs.interop.cellprofiler.analyst_export import ( + CellProfilerDatabaseExportSettings, + ) + from openhcs.processing.backends.cellprofiler.export_to_database import ( + ExportToDatabaseModule, + ) + + +@dataclass(frozen=True) +class CPAWorkspaceAxis(ABC, metaclass=AutoRegisterMeta): + """One authored axis; irreducible Image/Object/Index projection on leaves.""" + + __registry_key__ = "measurement_type" + __skip_if_no_key__ = True + measurement_type: ClassVar[str | None] = None + object_name: str + measurement: str + index: str + + @classmethod + def from_settings( + cls, kind: str, object_name: str, measurement: str, index: str + ) -> CPAWorkspaceAxis: + axis_type = cls.__registry__.get(kind) + if axis_type is None: + raise ValueError(f"Unsupported CPA workspace measurement type {kind!r}.") + return axis_type(object_name, measurement, index) + + def projected(self, dialect: CellProfilerDatabaseColumnDialect) -> tuple[str, str]: + field, table = self.field_name(dialect), self.table_name(dialect) + if not field or any(c in field for c in "\n\r\t"): + raise ValueError("CPA workspace axis must have a single nonempty field.") + return field, table + + @abstractmethod + def field_name(self, dialect: CellProfilerDatabaseColumnDialect) -> str: + """Project the authored field through the existing measurement dialect.""" + + @abstractmethod + def table_name(self, dialect: CellProfilerDatabaseColumnDialect) -> str: + """Return the original native table role for this axis.""" + + +class CPAImageWorkspaceAxis(CPAWorkspaceAxis): + measurement_type = "Image" + + def field_name(self, dialect): + return dialect.measurement_field( + MeasurementSubject(MeasurementScope.IMAGE, "Image"), + FieldSpec(self.measurement, None), + ).name + + def table_name(self, dialect): + return dialect.image_table_name() + + +class CPAObjectWorkspaceAxis(CPAWorkspaceAxis): + measurement_type = "Object" + + def field_name(self, dialect): + return dialect.measurement_field( + MeasurementSubject(MeasurementScope.OBJECT, self.object_name), + FieldSpec(self.measurement, None), + ).name + + def table_name(self, dialect): + # CP4281 workspace references Per_Object even for per-object exports. + return dialect.combined_object_table_name() + + +class CPAIndexWorkspaceAxis(CPAWorkspaceAxis): + measurement_type = "Index" + + def field_name(self, dialect): + if self.index not in ("ImageNumber", "Group_Index"): + raise ValueError(f"Unsupported CPA workspace index {self.index!r}.") + return self.index + + def table_name(self, dialect): + return dialect.image_table_name() + + +@dataclass(frozen=True) +class CPAWorkspacePanel(ABC, metaclass=AutoRegisterMeta): + """One substitutable workspace tool; shared ordered external rendering.""" + + __registry_key__ = "setting_name" + __skip_if_no_key__ = True + setting_name: ClassVar[str | None] = None + display_name: ClassVar[str] + x_axis_label: ClassVar[str] = "x-axis" + x_table_label: ClassVar[str] = "table" + x: CPAWorkspaceAxis + y: CPAWorkspaceAxis + + @classmethod + def bound_settings( + cls, module: ModuleBlock, declaration: type[ExportToDatabaseModule] + ) -> dict[str, object]: + count_value = optional_setting_value( + module, declaration.workspace_measurement_count_setting + ) + count = 0 if count_value is None else int(count_value) + names = ( + declaration.workspace_display_tool_setting, + declaration.workspace_x_type_setting, + declaration.workspace_x_measurement_setting, + declaration.workspace_x_index_setting, + declaration.workspace_y_type_setting, + declaration.workspace_y_measurement_setting, + declaration.workspace_y_index_setting, + ) + values = tuple(module.get_setting_values(name.canonical) for name in names) + for name, rows in zip(names, values, strict=True): + declaration._require_record_count(count, name, rows) + objects = module.get_setting_values( + declaration.workspace_object_name_setting.canonical + ) + declaration._require_record_count( + count * 2, declaration.workspace_object_name_setting, objects + ) + wants_value = optional_setting_value( + module, declaration.wants_workspace_file_setting + ) + wants = wants_value is not None and parse_cellprofiler_bool(wants_value) + ( + tools, + x_types, + x_measurements, + x_indices, + y_types, + y_measurements, + y_indices, + ) = values + panels = ( + tuple( + cls.from_settings( + tool, + CPAWorkspaceAxis.from_settings( + x_types[i], objects[2 * i], x_measurements[i], x_indices[i] + ), + CPAWorkspaceAxis.from_settings( + y_types[i], objects[2 * i + 1], y_measurements[i], y_indices[i] + ), + ) + for i, tool in enumerate(tools) + ) + if wants + else () + ) + return ( + {"wants_workspace_file": True, "workspace_panels": panels} if wants else {} + ) + + @classmethod + def setting_records( + cls, + declaration: type[ExportToDatabaseModule], + wants: bool, + panels: tuple[CPAWorkspacePanel, ...], + ) -> tuple[ModuleSetting, ...]: + records = [ + ModuleSetting( + declaration.workspace_measurement_count_setting.canonical, + str(len(panels)), + ), + ModuleSetting( + declaration.wants_workspace_file_setting.canonical, + cellprofiler_setting_literal(wants), + ), + ] + for panel in panels: + values = ( + (declaration.workspace_display_tool_setting, panel.setting_name), + (declaration.workspace_x_type_setting, panel.x.measurement_type), + (declaration.workspace_object_name_setting, panel.x.object_name), + (declaration.workspace_x_measurement_setting, panel.x.measurement), + (declaration.workspace_x_index_setting, panel.x.index), + (declaration.workspace_y_type_setting, panel.y.measurement_type), + (declaration.workspace_object_name_setting, panel.y.object_name), + (declaration.workspace_y_measurement_setting, panel.y.measurement), + (declaration.workspace_y_index_setting, panel.y.index), + ) + records.extend( + ModuleSetting(name.canonical, value) for name, value in values + ) + return tuple(records) + + @classmethod + def from_settings( + cls, tool: str, x: CPAWorkspaceAxis, y: CPAWorkspaceAxis + ) -> CPAWorkspacePanel: + panel_type = cls.__registry__.get(tool) + if panel_type is None: + raise ValueError(f"Unsupported CPA workspace display tool {tool!r}.") + panel = panel_type(x, y) + panel.rows(CellProfilerDatabaseColumnDialect()) + return panel + + def rows( + self, dialect: CellProfilerDatabaseColumnDialect + ) -> tuple[tuple[str, str], ...]: + field, table = self.x.projected(dialect) + return ( + (self.x_axis_label, field), + (self.x_table_label, table), + *self.additional_rows(dialect), + ) + + @abstractmethod + def additional_rows( + self, dialect: CellProfilerDatabaseColumnDialect + ) -> tuple[tuple[str, str], ...]: + """Irreducible tool axes beyond the common X declaration.""" + + @classmethod + def parse_workspace( + cls, text: str + ) -> tuple[tuple[str, tuple[tuple[str, str], ...]], ...]: + """Read ordered panel meaning; reject unknown tools, fields and layout.""" + lines = text.splitlines() + if len(lines) < 3 or lines[:2] != [ + "CellProfiler Analyst workflow", + "version: 1", + ]: + raise ValueError("Invalid CPA workspace header.") + if not lines[2].startswith("CP version : ") or not lines[2][13:].isdigit(): + raise ValueError("Invalid CPA workspace CellProfiler version.") + tools = {leaf.display_name: leaf for leaf in cls.__registry__.values()} + panels = [] + current = None + rows = [] + for line in (*lines[3:], ""): + if not line: + if current is not None: + expected = current.external_fields() + if tuple(key for key, value in rows) != expected or any( + not value for key, value in rows + ): + raise ValueError( + f"Invalid CPA workspace {current.display_name} fields." + ) + panels.append((current.display_name, tuple(rows))) + current, rows = None, [] + continue + if current is None: + if line not in tools: + raise ValueError(f"Unknown CPA workspace tool {line!r}.") + current = tools[line] + else: + if not line.startswith("\t") or ": " not in line: + raise ValueError("Invalid CPA workspace panel field layout.") + key, value = line[1:].split(": ", 1) + rows.append((key, value)) + return tuple(panels) + + @classmethod + def external_fields(cls) -> tuple[str, ...]: + return (cls.x_axis_label, cls.x_table_label) + + +class CPASingleAxisWorkspacePanel(CPAWorkspacePanel, ABC): + def additional_rows(self, dialect): + return () + + +class CPATwoAxisWorkspacePanel(CPAWorkspacePanel, ABC): + x_table_label = "x-table" + + def additional_rows(self, dialect): + field, table = self.y.projected(dialect) + return (("y-axis", field), ("y-table", table)) + + @classmethod + def external_fields(cls): + return (*super().external_fields(), "y-axis", "y-table") + + +class CPAScatterWorkspacePanel(CPATwoAxisWorkspacePanel): + setting_name = "ScatterPlot" + display_name = "Scatter" + + +class CPADensityWorkspacePanel(CPATwoAxisWorkspacePanel): + setting_name = "DensityPlot" + display_name = "Density" + + +class CPAHistogramWorkspacePanel(CPASingleAxisWorkspacePanel): + setting_name = "Histogram" + display_name = "Histogram" + + +class CPAPlateViewerWorkspacePanel(CPASingleAxisWorkspacePanel): + setting_name = "PlateViewer" + display_name = "PlateViewer" + x_axis_label = "measurement" + + +class CPABoxWorkspacePanel(CPASingleAxisWorkspacePanel): + setting_name = "BoxPlot" + display_name = "BoxPlot" + + +@dataclass(frozen=True, slots=True) +class CPAWorkspaceRenderer: + """Render the requested workspace through the export's actual table dialect.""" + + dialect: CellProfilerDatabaseColumnDialect + + def render(self, settings: CellProfilerDatabaseExportSettings) -> dict[str, str]: + if not settings.wants_workspace_file: + return {} + if self.dialect.table_prefix != settings.table_prefix: + raise ValueError( + "CPA workspace dialect disagrees with export table prefix." + ) + name = str(Path(settings.sqlite_file).with_suffix("")) + prefix = settings.table_prefix.removesuffix("_") + if prefix: + name = f"{name}_{prefix}" + sections = tuple( + "\n" + + panel.display_name + + "\n" + + "\n".join(f"\t{key}: {value}" for key, value in panel.rows(self.dialect)) + + "\n" + for panel in settings.workspace_panels + ) + return { + f"{name}.workspace": "CellProfiler Analyst workflow\nversion: 1\nCP version : 4281\n" + + "".join(sections) + } diff --git a/openhcs/processing/backends/cellprofiler/export_to_database.py b/openhcs/processing/backends/cellprofiler/export_to_database.py index 2234aaa77f..9a650ab9c9 100644 --- a/openhcs/processing/backends/cellprofiler/export_to_database.py +++ b/openhcs/processing/backends/cellprofiler/export_to_database.py @@ -32,6 +32,10 @@ CellProfilerDatabaseExportSettings, CellProfilerObjectTableMode, ) +from openhcs.interop.cellprofiler.workspace_export import ( + CPAWorkspacePanel, + CPAWorkspaceRenderer, +) from openhcs.interop.cellprofiler.cellprofiler_literals import ( cellprofiler_setting_literal, ) @@ -404,7 +408,7 @@ def postprocess_bound_settings( overwrite_value = optional_setting_value(module, cls.overwrite_mode_setting) if overwrite_value is not None: _parse_overwrite_mode(overwrite_value) - cls._validate_workspace_settings(module) + workspace_settings = CPAWorkspacePanel.bound_settings(module, cls) location_value = optional_setting_value(module, cls.location_object_setting) filter_count_value = optional_setting_value( module, @@ -454,7 +458,7 @@ def postprocess_bound_settings( bound = bound.with_consumed_settings( cls.property_filter_count_setting, ) - return bound.with_consumed_settings( + return bound.with_kwargs(workspace_settings).with_consumed_settings( cls.aggregate_well_mean_setting, cls.aggregate_well_median_setting, cls.aggregate_well_standard_deviation_setting, @@ -522,10 +526,10 @@ def finalize_module_blocks_for_invocation( cellprofiler_setting_literal(False), ), ModuleSetting(cls.overwrite_mode_setting.canonical, "Never"), - ModuleSetting(cls.workspace_measurement_count_setting.canonical, "0"), - ModuleSetting( - cls.wants_workspace_file_setting.canonical, - cellprofiler_setting_literal(False), + *CPAWorkspacePanel.setting_records( + cls, + arguments.arguments["wants_workspace_file"], + arguments.arguments["workspace_panels"], ), *cls._selected_object_setting_records(selected_objects), *cls._property_image_setting_records(image_channels), @@ -721,54 +725,6 @@ def _group_fields(cls, module: ModuleBlock) -> tuple[tuple[str, str], ...]: for name, statement in zip(names, columns, strict=True) ) - @classmethod - def _validate_workspace_settings( - cls, - module: ModuleBlock, - ) -> None: - count_value = optional_setting_value( - module, - cls.workspace_measurement_count_setting, - ) - declared_count = 0 if count_value is None else int(count_value) - display_tools = module.get_setting_values( - cls.workspace_display_tool_setting.canonical - ) - x_types = module.get_setting_values(cls.workspace_x_type_setting.canonical) - object_names = module.get_setting_values( - cls.workspace_object_name_setting.canonical - ) - x_measurements = module.get_setting_values( - cls.workspace_x_measurement_setting.canonical - ) - x_indices = module.get_setting_values(cls.workspace_x_index_setting.canonical) - y_types = module.get_setting_values(cls.workspace_y_type_setting.canonical) - y_measurements = module.get_setting_values( - cls.workspace_y_measurement_setting.canonical - ) - y_indices = module.get_setting_values(cls.workspace_y_index_setting.canonical) - for setting_name, values in ( - (cls.workspace_display_tool_setting, display_tools), - (cls.workspace_x_type_setting, x_types), - (cls.workspace_x_measurement_setting, x_measurements), - (cls.workspace_x_index_setting, x_indices), - (cls.workspace_y_type_setting, y_types), - (cls.workspace_y_measurement_setting, y_measurements), - (cls.workspace_y_index_setting, y_indices), - ): - cls._require_record_count(declared_count, setting_name, values) - cls._require_record_count( - declared_count * 2, - cls.workspace_object_name_setting, - object_names, - ) - wants_workspace_value = optional_setting_value( - module, - cls.wants_workspace_file_setting, - ) - if wants_workspace_value is not None: - parse_cellprofiler_bool(wants_workspace_value) - @classmethod def _require_record_count( cls, @@ -898,6 +854,8 @@ def export_to_database( object_table_mode: CellProfilerObjectTableMode = CellProfilerObjectTableMode.PER_OBJECT, selected_objects: tuple[str, ...] | None = None, wants_properties_file: bool = True, + wants_workspace_file: bool = False, + workspace_panels: tuple[CPAWorkspacePanel, ...] = (), wants_relationship_tables: bool = False, include_all_images: bool = True, image_channels: tuple[CPAImageChannelSpec, ...] = (), @@ -941,6 +899,8 @@ def export_to_database( object_table_mode=object_table_mode, selected_objects=selected_objects, wants_properties_file=wants_properties_file, + wants_workspace_file=wants_workspace_file, + workspace_panels=workspace_panels, wants_relationship_tables=wants_relationship_tables, maximum_column_name_length=maximum_column_name_length, location_object=location_object, @@ -981,14 +941,17 @@ def export_to_database( bundle: dict[str, bytes | str] = { settings.sqlite_file: CPASQLiteRenderer(dialect).render(projection, settings) } - for properties_file in CPAPropertiesRenderer(dialect).render( - settings, - resolved_channels, - projection, - ): - if properties_file.file_name in bundle: - raise ValueError( - f"ExportToDatabase emits duplicate file {properties_file.file_name!r}." + text_files = ( + *( + (file.file_name, file.text) + for file in CPAPropertiesRenderer(dialect).render( + settings, resolved_channels, projection ) - bundle[properties_file.file_name] = properties_file.text + ), + *CPAWorkspaceRenderer(dialect).render(settings).items(), + ) + for file_name, text in text_files: + if file_name in bundle: + raise ValueError(f"ExportToDatabase emits duplicate file {file_name!r}.") + bundle[file_name] = text return bundle diff --git a/tests/fixtures/cellprofiler/quality_control_4281.workspace b/tests/fixtures/cellprofiler/quality_control_4281.workspace new file mode 100644 index 0000000000..e51c9326f4 --- /dev/null +++ b/tests/fixtures/cellprofiler/quality_control_4281.workspace @@ -0,0 +1,71 @@ +CellProfiler Analyst workflow +version: 1 +CP version : 4281 + +Scatter + x-axis: ImageNumber + x-table: BBBC022QC_Per_Image + y-axis: Image_ImageQuality_PowerLogLogSlope_OrigER + y-table: BBBC022QC_Per_Image + +Scatter + x-axis: ImageNumber + x-table: BBBC022QC_Per_Image + y-axis: Image_ImageQuality_PowerLogLogSlope_OrigHoechst + y-table: BBBC022QC_Per_Image + +Scatter + x-axis: ImageNumber + x-table: BBBC022QC_Per_Image + y-axis: Image_ImageQuality_PowerLogLogSlope_OrigMito + y-table: BBBC022QC_Per_Image + +Scatter + x-axis: ImageNumber + x-table: BBBC022QC_Per_Image + y-axis: Image_ImageQuality_PowerLogLogSlope_OrigPh_golgi + y-table: BBBC022QC_Per_Image + +Scatter + x-axis: ImageNumber + x-table: BBBC022QC_Per_Image + y-axis: Image_ImageQuality_PowerLogLogSlope_OrigSyto + y-table: BBBC022QC_Per_Image + +Scatter + x-axis: Image_ImageQuality_StdIntensity_OrigSyto + x-table: BBBC022QC_Per_Image + y-axis: Image_ImageQuality_PercentMaximal_OrigSyto + y-table: BBBC022QC_Per_Image + +Scatter + x-axis: Image_ImageQuality_StdIntensity_OrigPh_golgi + x-table: BBBC022QC_Per_Image + y-axis: Image_ImageQuality_PercentMaximal_OrigPh_golgi + y-table: BBBC022QC_Per_Image + +Scatter + x-axis: Image_ImageQuality_StdIntensity_OrigMito + x-table: BBBC022QC_Per_Image + y-axis: Image_ImageQuality_PercentMaximal_OrigMito + y-table: BBBC022QC_Per_Image + +Scatter + x-axis: Image_ImageQuality_StdIntensity_OrigHoechst + x-table: BBBC022QC_Per_Image + y-axis: Image_ImageQuality_PercentMaximal_OrigHoechst + y-table: BBBC022QC_Per_Image + +Scatter + x-axis: Image_ImageQuality_StdIntensity_OrigER + x-table: BBBC022QC_Per_Image + y-axis: Image_ImageQuality_PercentMaximal_OrigER + y-table: BBBC022QC_Per_Image + +Histogram + x-axis: Image_ImageQuality_ThresholdOtsu_OrigHoechst_2W + table: BBBC022QC_Per_Image + +Histogram + x-axis: Image_ImageQuality_ThresholdOtsu_OrigPh_golgi_3FW + table: BBBC022QC_Per_Image diff --git a/tests/unit/test_cellprofiler_analyst_export.py b/tests/unit/test_cellprofiler_analyst_export.py index 7c2ee83497..b1cd83e42e 100644 --- a/tests/unit/test_cellprofiler_analyst_export.py +++ b/tests/unit/test_cellprofiler_analyst_export.py @@ -1536,7 +1536,10 @@ def test_module_contract_selects_ordered_tables_and_cpa_images_and_declares_bund ) -def test_raw_callable_uses_batch_source_plan_with_sibling_plate_step() -> None: +@pytest.mark.parametrize("workspace", [False, True]) +def test_raw_callable_uses_batch_source_plan_with_sibling_plate_step( + workspace: bool, +) -> None: store = RuntimeValueStore() measurements = _record_measurements( store, @@ -1575,6 +1578,22 @@ def test_raw_callable_uses_batch_source_plan_with_sibling_plate_step() -> None: ), ) + from openhcs.interop.cellprofiler.workspace_export import ( + CPAWorkspacePanel, + CPAWorkspaceAxis, + ) + + panels = ( + ( + CPAWorkspacePanel.from_settings( + "Histogram", + CPAWorkspaceAxis.from_settings("Image", "None", "Count", "ImageNumber"), + CPAWorkspaceAxis.from_settings("Image", "None", "None", "ImageNumber"), + ), + ) + if workspace + else () + ) bundle = export_to_database( artifact_batch=batch, context=context, @@ -1582,9 +1601,19 @@ def test_raw_callable_uses_batch_source_plan_with_sibling_plate_step() -> None: experiment_name="Example", add_table_prefix=True, table_prefix="CPA_", + wants_workspace_file=workspace, + workspace_panels=panels, ) - assert tuple(bundle) == ("analysis.sqlite", "analysis_CPA.properties") + assert tuple(bundle) == ( + ("analysis.sqlite", "analysis_CPA.properties", "analysis_CPA.workspace") + if workspace + else ("analysis.sqlite", "analysis_CPA.properties") + ) + if workspace: + assert CPAWorkspacePanel.parse_workspace(bundle["analysis_CPA.workspace"]) == ( + ("Histogram", (("x-axis", "Image_Count"), ("table", "CPA_Per_Image"))), + ) assert isinstance(bundle["analysis.sqlite"], bytes) assert str(bundle["analysis_CPA.properties"]).startswith("db_type = sqlite\n") diff --git a/tests/unit/test_cellprofiler_export_to_database.py b/tests/unit/test_cellprofiler_export_to_database.py index 18a8a28398..5823725774 100644 --- a/tests/unit/test_cellprofiler_export_to_database.py +++ b/tests/unit/test_cellprofiler_export_to_database.py @@ -142,7 +142,6 @@ "wants_filter_fields", "create_plate_filters", "overwrite_mode", - "wants_workspace_file", "workspace_measurements", } diff --git a/tests/unit/test_cellprofiler_workspace_export.py b/tests/unit/test_cellprofiler_workspace_export.py new file mode 100644 index 0000000000..0e91895cce --- /dev/null +++ b/tests/unit/test_cellprofiler_workspace_export.py @@ -0,0 +1,239 @@ +from dataclasses import replace +from pathlib import Path + +import pytest + +from benchmark.cellprofiler_export_equivalence import ( + cellprofiler_database_export_equivalence, +) +from openhcs.core.equivalence.policy import RuntimeEquivalencePolicy +from openhcs.core.runtime_exports import RuntimeExportObservation +from openhcs.interop.cellprofiler.analyst_export import ( + CellProfilerDatabaseExportSettings, + CellProfilerObjectTableMode, +) +from openhcs.interop.cellprofiler.database_column_dialect import ( + CellProfilerDatabaseColumnDialect, +) +from openhcs.interop.cellprofiler.workspace_export import ( + CPAWorkspaceAxis, + CPAWorkspacePanel, + CPAWorkspaceRenderer, +) +from openhcs.processing.backends.cellprofiler.export_to_database import ( + ExportToDatabaseModule, +) +from openhcs.interop.cellprofiler.parser import ModuleBlock + + +def settings(**kwargs): + return replace( + CellProfilerDatabaseExportSettings( + sqlite_file="Database.db", + experiment_name="Experiment", + table_prefix="Prefix_", + object_table_mode=CellProfilerObjectTableMode.PER_OBJECT, + selected_objects=None, + wants_properties_file=False, + wants_relationship_tables=False, + ), + **kwargs, + ) + + +def axis(kind="Image", name="Intensity_MeanIntensity_DNA", index="ImageNumber"): + return CPAWorkspaceAxis.from_settings(kind, "Nuclei", name, index) + + +@pytest.mark.parametrize( + "tool,display,fields", + [ + ("ScatterPlot", "Scatter", ("x-axis", "x-table", "y-axis", "y-table")), + ("DensityPlot", "Density", ("x-axis", "x-table", "y-axis", "y-table")), + ("Histogram", "Histogram", ("x-axis", "table")), + ("BoxPlot", "BoxPlot", ("x-axis", "table")), + ("PlateViewer", "PlateViewer", ("measurement", "table")), + ], +) +def test_all_native_tools_preserve_axis_roles(tool, display, fields): + panel = CPAWorkspacePanel.from_settings(tool, axis("Index"), axis("Object")) + declaration = settings(wants_workspace_file=True, workspace_panels=(panel,)) + ((name, text),) = ( + CPAWorkspaceRenderer(CellProfilerDatabaseColumnDialect("Prefix_")) + .render(declaration) + .items() + ) + assert name == "Database_Prefix.workspace" + ((tool_name, rows),) = CPAWorkspacePanel.parse_workspace(text) + assert tool_name == display + assert tuple(key for key, value in rows) == fields + assert rows[:2] == ((fields[0], "ImageNumber"), (fields[1], "Prefix_Per_Image")) + if tool in ("ScatterPlot", "DensityPlot"): + assert rows[2:] == ( + ("y-axis", "Nuclei_Intensity_MeanIntensity_DNA"), + ("y-table", "Prefix_Per_Object"), + ) + + +def test_single_axis_tools_do_not_validate_inactive_y(): + panel = CPAWorkspacePanel.from_settings( + "Histogram", axis(), axis(name="invalid\nfield") + ) + assert ( + panel.rows(CellProfilerDatabaseColumnDialect())[0][1] + == "Image_Intensity_MeanIntensity_DNA" + ) + with pytest.raises(ValueError, match="single nonempty field"): + CPAWorkspacePanel.from_settings( + "ScatterPlot", axis(), axis(name="invalid\nfield") + ) + + +def test_disabled_workspace_emits_nothing(): + assert ( + CPAWorkspaceRenderer(CellProfilerDatabaseColumnDialect()).render(settings()) + == {} + ) + + +@pytest.mark.parametrize("kind,index", [("Bogus", "ImageNumber"), ("Index", "Bogus")]) +def test_malformed_active_axes_reject(kind, index): + with pytest.raises(ValueError): + CPAWorkspacePanel.from_settings("Histogram", axis(kind, index=index), axis()) + + +def test_workspace_module_records_round_trip_without_reordering(): + panels = ( + CPAWorkspacePanel.from_settings( + "ScatterPlot", axis("Index", index="Group_Index"), axis("Object") + ), + CPAWorkspacePanel.from_settings( + "PlateViewer", axis(), axis(name="invalid\nfield") + ), + ) + records = CPAWorkspacePanel.setting_records(ExportToDatabaseModule, True, panels) + block = ModuleBlock( + name="ExportToDatabase", + module_num=1, + metadata={"variable_revision_number": 28}, + enabled=True, + setting_records=list(records), + ) + assert CPAWorkspacePanel.bound_settings(block, ExportToDatabaseModule) == { + "wants_workspace_file": True, + "workspace_panels": panels, + } + with pytest.raises(ValueError): + CPAWorkspacePanel.bound_settings( + replace(block, setting_records=list(records[:-1])), ExportToDatabaseModule + ) + + +@pytest.mark.parametrize( + "replacement", + [ + "Unknown", + "Scatter\n\tx-axis: X", + "Scatter\n\tx-axis: X\n\tx-axis: Y\n\ty-axis: Z\n\ty-table: T", + ], +) +def test_workspace_reader_rejects_unknown_missing_duplicate_fields(replacement): + with pytest.raises(ValueError): + CPAWorkspacePanel.parse_workspace( + "CellProfiler Analyst workflow\nversion: 1\nCP version : 4281\n\n" + + replacement + + "\n" + ) + + +def test_workspace_comparison_covers_order_values_and_missing_files(tmp_path): + native = ( + Path(__file__).parents[1] + / "fixtures/cellprofiler/quality_control_4281.workspace" + ).read_text() + reference = tmp_path / "reference" + reference.mkdir() + candidate = tmp_path / "candidate" + candidate.mkdir() + (reference / "QC.workspace").write_text(native) + (candidate / "QC.workspace").write_text(native) + policy = RuntimeEquivalencePolicy() + compare = lambda: cellprofiler_database_export_equivalence( + reference, + RuntimeExportObservation.from_output_roots((candidate,)), + policy=policy, + ) + assert compare().is_equivalent + (candidate / "QC.workspace").write_text( + native.replace("ImageNumber", "Group_Index", 1) + ) + assert not compare().is_equivalent + (candidate / "QC.workspace").unlink() + assert not compare().is_equivalent + + +def test_native_quality_control_fixture_contains_twelve_ordered_panels(): + native = ( + Path(__file__).parents[1] + / "fixtures/cellprofiler/quality_control_4281.workspace" + ).read_text() + panels = CPAWorkspacePanel.parse_workspace(native) + assert ( + tuple(tool for tool, rows in panels) == ("Scatter",) * 10 + ("Histogram",) * 2 + ) + assert panels[0][1][2][1] == "Image_ImageQuality_PowerLogLogSlope_OrigER" + assert panels[-1][1][0][1] == "Image_ImageQuality_ThresholdOtsu_OrigPh_golgi_3FW" + + +def test_workspace_panels_survive_source_transport(): + import openhcs.serialization.pycodify_formatters + from pycodify import Assignment, generate_python_source + + panels = ( + CPAWorkspacePanel.from_settings("ScatterPlot", axis("Index"), axis("Object")), + ) + source = generate_python_source(Assignment("panels", panels), clean_mode=True) + namespace = {} + exec(source, namespace) + assert namespace["panels"] == panels + + +def test_workspace_dialect_must_match_its_export_request(): + panel = CPAWorkspacePanel.from_settings("Histogram", axis(), axis()) + with pytest.raises(ValueError, match="table prefix"): + CPAWorkspaceRenderer(CellProfilerDatabaseColumnDialect("Other_")).render( + settings(wants_workspace_file=True, workspace_panels=(panel,)) + ) + + +def test_workspace_filename_preserves_native_relative_directory_and_single_suffix(): + panel = CPAWorkspacePanel.from_settings("Histogram", axis(), axis()) + declaration = settings( + sqlite_file="nested/Database.sqlite", + table_prefix="Prefix__", + wants_workspace_file=True, + workspace_panels=(panel,), + ) + assert tuple( + CPAWorkspaceRenderer(CellProfilerDatabaseColumnDialect("Prefix__")).render( + declaration + ) + ) == ("nested/Database_Prefix_.workspace",) + + +def test_workspace_reader_rejects_malformed_version_and_reordered_fields(): + native = ( + Path(__file__).parents[1] + / "fixtures/cellprofiler/quality_control_4281.workspace" + ).read_text() + for malformed in ( + native.replace("version: 1", "version: 2"), + native.replace("CP version : 4281", "CP version : unknown"), + native.replace( + "\tx-axis: ImageNumber\n\tx-table: BBBC022QC_Per_Image", + "\tx-table: BBBC022QC_Per_Image\n\tx-axis: ImageNumber", + 1, + ), + ): + with pytest.raises(ValueError): + CPAWorkspacePanel.parse_workspace(malformed) From 2a5b691ab1e8c0bc2dd202d089b403645112d88f Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 17:41:10 -0400 Subject: [PATCH 091/235] refactor(equivalence): own saved correlation admission (refs #479) --- benchmark/matched_cellprofiler_batch.py | 13 ++++------- openhcs/core/equivalence/relationships.py | 22 +++++-------------- openhcs/core/equivalence/tables.py | 6 +++++ openhcs/core/runtime_equivalence.py | 10 +++++++++ openhcs/core/runtime_relationships.py | 6 +++++ .../test_exported_relationship_equivalence.py | 20 +++++++++++++++++ 6 files changed, 51 insertions(+), 26 deletions(-) diff --git a/benchmark/matched_cellprofiler_batch.py b/benchmark/matched_cellprofiler_batch.py index 77845e4437..f4891b5d07 100644 --- a/benchmark/matched_cellprofiler_batch.py +++ b/benchmark/matched_cellprofiler_batch.py @@ -190,15 +190,10 @@ def _require_compared_output_inventory( RuntimeMeasurementSnapshot.from_output_snapshot(snapshot) for snapshot in (reference_snapshot, candidate_snapshot) ) - if any( - measurement.correlated_relationships is None for measurement in measurements - ): - raise RuntimeError( - "Matched saved output inventory requires known relationship correlations." - ) - if measurements[0].relationship_differences( - measurements[1].correlated_relationships - ): + correlations = tuple( + measurement.required_relationship_correlations() for measurement in measurements + ) + if correlations[0] != correlations[1]: raise RuntimeError("Matched saved output relationship correlations differ.") comparable_tables = tuple( tuple( diff --git a/openhcs/core/equivalence/relationships.py b/openhcs/core/equivalence/relationships.py index 6b77b7717f..4dbff95e2b 100644 --- a/openhcs/core/equivalence/relationships.py +++ b/openhcs/core/equivalence/relationships.py @@ -1115,8 +1115,6 @@ def from_table( policy: RuntimeEquivalencePolicy, image_number_offset: RuntimeImageNumberOffset, ) -> tuple[Self, ...]: - if not table.rows: - raise ValueError("Relationship table has no determining declaration rows.") normalized_header = tuple( normalize_runtime_identifier(name) for name in table.header ) @@ -1125,7 +1123,7 @@ def from_table( groups: dict[ ObjectRelationshipDeclaration, list[RuntimeMeasurementRowMapping] ] = {} - for values in table.rows: + for values in table.required_rows(): row = RuntimeMeasurementRowMapping( dict(zip(table.header, values, strict=True)) ) @@ -1159,11 +1157,7 @@ def from_table( ) slice_index = image_number_offset.object_instance_key( row.row, 1 - ).slice_index - if slice_index is None: - raise ValueError( - "Relationship rows require an explicit image or slice identity." - ) + ).required_slice_index() raw_count = row.first_value(("slice_count",)) count = MeasurementScalarLiteral(raw_count).integer_value if raw_count is not None and count is None: @@ -1287,10 +1281,7 @@ def object_rows( f"Relationship endpoint {subject.name!r} lacks an object identity." ) key = image_number_offset.object_instance_key(row.row, object_id) - if key.slice_index is None: - raise ValueError( - "Relationship object rows require explicit image identities." - ) + key.required_slice_index() if key in rows and rows[key].row != row.row: raise ValueError( f"Relationship endpoint {subject.name!r} has conflicting object rows for {key!r}." @@ -1450,14 +1441,11 @@ def correlated_object_relationships( child_key = image_number_offset.object_instance_key( row.row, child_id ) - if child_key.slice_index is None: - raise ValueError( - "Relationship measurements require explicit image identities." - ) + slice_index = child_key.required_slice_index() if parent_id > 0: values.append( ( - ObjectInstanceKey(parent_id, child_key.slice_index), + ObjectInstanceKey(parent_id, slice_index), child_key, ) ) diff --git a/openhcs/core/equivalence/tables.py b/openhcs/core/equivalence/tables.py index 05962edc89..4f947d719e 100644 --- a/openhcs/core/equivalence/tables.py +++ b/openhcs/core/equivalence/tables.py @@ -78,6 +78,12 @@ class RuntimeTableSnapshot: rows: tuple[tuple[str, ...], ...] column_context: tuple[str | None, ...] = () + def required_rows(self) -> tuple[tuple[str, ...], ...]: + """Admit actual rows when a consumer needs a determining declaration.""" + if not self.rows: + raise ValueError(f"Runtime table {self.path} has no determining declaration rows.") + return self.rows + @property def participates_in_comparison(self) -> bool: """Compare output tables except the engine's Experiment key/value receipt.""" diff --git a/openhcs/core/runtime_equivalence.py b/openhcs/core/runtime_equivalence.py index f83d3f4251..4fbbbe3d2f 100644 --- a/openhcs/core/runtime_equivalence.py +++ b/openhcs/core/runtime_equivalence.py @@ -1815,6 +1815,16 @@ def __post_init__(self) -> None: def is_empty(self) -> bool: return not self.measurement_fact_counts + def required_relationship_correlations( + self, + ) -> Mapping[RuntimeMeasurementFeatureKey, ObjectInstanceRelationship]: + """Admit this snapshot for a full saved comparison, not value-only scope.""" + if self.correlated_relationships is None: + raise RuntimeError( + "Matched saved output inventory requires known relationship correlations." + ) + return self.correlated_relationships + def relationship_differences( self, candidate: ( diff --git a/openhcs/core/runtime_relationships.py b/openhcs/core/runtime_relationships.py index ea3b158552..dfadd0035c 100644 --- a/openhcs/core/runtime_relationships.py +++ b/openhcs/core/runtime_relationships.py @@ -951,6 +951,12 @@ def __post_init__(self) -> None: object.__setattr__(self, "object_id", object_id) object.__setattr__(self, "slice_index", slice_index) + def required_slice_index(self) -> int: + """Admit this object identity for a comparison requiring an explicit plane.""" + if self.slice_index is None: + raise ValueError("Object instance requires an explicit image or slice identity.") + return self.slice_index + @classmethod def from_measurement_row( cls, diff --git a/tests/unit/test_exported_relationship_equivalence.py b/tests/unit/test_exported_relationship_equivalence.py index 60273f2fac..6eeba55b94 100644 --- a/tests/unit/test_exported_relationship_equivalence.py +++ b/tests/unit/test_exported_relationship_equivalence.py @@ -391,3 +391,23 @@ def test_snapshot_json_cache_retains_correlation_scope(tmp_path: Path) -> None: ) assert restored.correlated_relationships == snapshot.correlated_relationships assert restored.measurement_fact_counts == snapshot.measurement_fact_counts + + +@pytest.mark.parametrize("slice_index", (None, 0, 2)) +def test_object_identity_owns_required_plane_admission(slice_index: int | None) -> None: + from openhcs.core.runtime_relationships import ObjectInstanceKey + + key = ObjectInstanceKey(1, slice_index) + if slice_index is None: + with pytest.raises(ValueError, match="explicit image or slice identity"): + key.required_slice_index() + else: + assert key.required_slice_index() == slice_index + + +def test_full_saved_comparison_cannot_admit_unknown_relationship_scope() -> None: + unknown = RuntimeMeasurementSnapshot({}) + with pytest.raises(RuntimeError, match="requires known relationship correlations"): + unknown.required_relationship_correlations() + known = RuntimeMeasurementSnapshot({}, {}) + assert known.required_relationship_correlations() is known.correlated_relationships From f03a7025295485508e7e7e701960baf1b9903b9d Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 17:41:17 -0400 Subject: [PATCH 092/235] Keep workspace file selection on the existing export request (refs #480) --- docs/development/cpa_workspace_export_480.md | 2 +- .../interop/cellprofiler/analyst_export.py | 25 +++++++++++- .../interop/cellprofiler/workspace_export.py | 39 +++++++------------ .../cellprofiler/export_to_database.py | 9 +---- .../test_cellprofiler_workspace_export.py | 26 +++++-------- 5 files changed, 52 insertions(+), 49 deletions(-) diff --git a/docs/development/cpa_workspace_export_480.md b/docs/development/cpa_workspace_export_480.md index 44e67d55fd..360b8e5af7 100644 --- a/docs/development/cpa_workspace_export_480.md +++ b/docs/development/cpa_workspace_export_480.md @@ -4,7 +4,7 @@ ExportToDatabase accepted and validated workspace panel settings, then discarded The existing export settings now retain the requested workspace and original ordered panel declarations. `CPAWorkspacePanel` owns the external panel grammar, repeated setting binding and authoring transport. Its Scatter/Density leaves share two-axis rendering; Histogram/Box/PlateViewer use the single-axis grammar, with PlateViewer's native `measurement` field. `CPAWorkspaceAxis` derives its Image/Object/Index implementations from their declarations. Image/object measurements use the existing `MeasurementSubject` and database dialect; there is no parallel tool enum or handwritten dispatch roster. -The public exporter merges workspace text into the existing file bundle and retains its duplicate-file rejection. The shared database-export comparison parses ordered workspace semantics, rejects invalid layout or unknown fields/tools, and reports missing or changed panels. The strict matched inventory now admits `.workspace` because it has an actual comparison consumer. Database and properties rendering remain unchanged. +The existing export settings own enabled-workspace selection, exact filenames and matching table-prefix admission. The format renderer consumes only the resulting ordered panels. Authoring projection binds the already-validated public call keywords at the panel owner, retaining unrelated export settings without copying two raw key lookups. The public exporter merges workspace text into the existing file bundle and retains its duplicate-file rejection. The shared database-export comparison parses ordered workspace semantics, rejects invalid layout or unknown fields/tools, and reports missing or changed panels. The strict matched inventory now admits `.workspace` because it has an actual comparison consumer. Database and properties rendering remain unchanged. Native 4.2.8.1 emits raw subject/feature names in workspace axes, and Object axes always name `Per_Object`, even when separate object tables are selected. These native quirks are preserved, not silently translated to different scientific columns. The near-term QC fixture has only Image/Index axes: both retained native workspace texts match exactly, and all 22 axis references resolve to actual native SQLite columns. This does not establish arbitrary native object-workspace references or shortened column names are valid. The reader validates workflow format version 1 and the numeric producer-version header; the CP producer version is descriptive rather than compared panel meaning. diff --git a/openhcs/interop/cellprofiler/analyst_export.py b/openhcs/interop/cellprofiler/analyst_export.py index 9ceba032f7..db6c084f78 100644 --- a/openhcs/interop/cellprofiler/analyst_export.py +++ b/openhcs/interop/cellprofiler/analyst_export.py @@ -6,7 +6,10 @@ from __future__ import annotations -from openhcs.interop.cellprofiler.workspace_export import CPAWorkspacePanel +from openhcs.interop.cellprofiler.workspace_export import ( + CPAWorkspacePanel, + CPAWorkspaceRenderer, +) from base64 import b64encode from collections import defaultdict @@ -396,6 +399,26 @@ def __post_init__(self) -> None: ) object.__setattr__(self, "thumbnail_image_names", thumbnail_names) + def workspace_files( + self, dialect: CellProfilerDatabaseColumnDialect + ) -> dict[str, str]: + """Resolve enabled workspace filenames from this exact export request.""" + if not self.wants_workspace_file: + return {} + if dialect.table_prefix != self.table_prefix: + raise ValueError( + "CPA workspace dialect disagrees with export table prefix." + ) + name = str(Path(self.sqlite_file).with_suffix("")) + prefix = self.table_prefix.removesuffix("_") + if prefix: + name = f"{name}_{prefix}" + return { + f"{name}.workspace": CPAWorkspaceRenderer(dialect).render( + self.workspace_panels + ) + } + def exports_object(self, object_name: str) -> bool: """Return whether this export includes one declared object subject.""" diff --git a/openhcs/interop/cellprofiler/workspace_export.py b/openhcs/interop/cellprofiler/workspace_export.py index c32a9a4670..3de9d0804a 100644 --- a/openhcs/interop/cellprofiler/workspace_export.py +++ b/openhcs/interop/cellprofiler/workspace_export.py @@ -4,7 +4,6 @@ from abc import ABC, abstractmethod from dataclasses import dataclass -from pathlib import Path from typing import ClassVar, TYPE_CHECKING from metaclass_registry import AutoRegisterMeta @@ -24,9 +23,6 @@ from openhcs.interop.cellprofiler.settings_binder import parse_cellprofiler_bool if TYPE_CHECKING: - from openhcs.interop.cellprofiler.analyst_export import ( - CellProfilerDatabaseExportSettings, - ) from openhcs.processing.backends.cellprofiler.export_to_database import ( ExportToDatabaseModule, ) @@ -182,20 +178,24 @@ def bound_settings( def setting_records( cls, declaration: type[ExportToDatabaseModule], - wants: bool, - panels: tuple[CPAWorkspacePanel, ...], + *, + wants_workspace_file: bool, + workspace_panels: tuple[CPAWorkspacePanel, ...], + **other_export_parameters: object, ) -> tuple[ModuleSetting, ...]: + """Project workspace records from the already-bound public export call.""" + del other_export_parameters records = [ ModuleSetting( declaration.workspace_measurement_count_setting.canonical, - str(len(panels)), + str(len(workspace_panels)), ), ModuleSetting( declaration.wants_workspace_file_setting.canonical, - cellprofiler_setting_literal(wants), + cellprofiler_setting_literal(wants_workspace_file), ), ] - for panel in panels: + for panel in workspace_panels: values = ( (declaration.workspace_display_tool_setting, panel.setting_name), (declaration.workspace_x_type_setting, panel.x.measurement_type), @@ -334,26 +334,17 @@ class CPAWorkspaceRenderer: dialect: CellProfilerDatabaseColumnDialect - def render(self, settings: CellProfilerDatabaseExportSettings) -> dict[str, str]: - if not settings.wants_workspace_file: - return {} - if self.dialect.table_prefix != settings.table_prefix: - raise ValueError( - "CPA workspace dialect disagrees with export table prefix." - ) - name = str(Path(settings.sqlite_file).with_suffix("")) - prefix = settings.table_prefix.removesuffix("_") - if prefix: - name = f"{name}_{prefix}" + def render(self, panels: tuple[CPAWorkspacePanel, ...]) -> str: + """Encode panel meaning without deciding whether an export requested it.""" sections = tuple( "\n" + panel.display_name + "\n" + "\n".join(f"\t{key}: {value}" for key, value in panel.rows(self.dialect)) + "\n" - for panel in settings.workspace_panels + for panel in panels ) - return { - f"{name}.workspace": "CellProfiler Analyst workflow\nversion: 1\nCP version : 4281\n" + return ( + "CellProfiler Analyst workflow\nversion: 1\nCP version : 4281\n" + "".join(sections) - } + ) diff --git a/openhcs/processing/backends/cellprofiler/export_to_database.py b/openhcs/processing/backends/cellprofiler/export_to_database.py index 9a650ab9c9..9b144f20d4 100644 --- a/openhcs/processing/backends/cellprofiler/export_to_database.py +++ b/openhcs/processing/backends/cellprofiler/export_to_database.py @@ -34,7 +34,6 @@ ) from openhcs.interop.cellprofiler.workspace_export import ( CPAWorkspacePanel, - CPAWorkspaceRenderer, ) from openhcs.interop.cellprofiler.cellprofiler_literals import ( cellprofiler_setting_literal, @@ -526,11 +525,7 @@ def finalize_module_blocks_for_invocation( cellprofiler_setting_literal(False), ), ModuleSetting(cls.overwrite_mode_setting.canonical, "Never"), - *CPAWorkspacePanel.setting_records( - cls, - arguments.arguments["wants_workspace_file"], - arguments.arguments["workspace_panels"], - ), + *CPAWorkspacePanel.setting_records(cls, **arguments.arguments), *cls._selected_object_setting_records(selected_objects), *cls._property_image_setting_records(image_channels), *cls._group_setting_records(group_fields), @@ -948,7 +943,7 @@ def export_to_database( settings, resolved_channels, projection ) ), - *CPAWorkspaceRenderer(dialect).render(settings).items(), + *settings.workspace_files(dialect).items(), ) for file_name, text in text_files: if file_name in bundle: diff --git a/tests/unit/test_cellprofiler_workspace_export.py b/tests/unit/test_cellprofiler_workspace_export.py index 0e91895cce..4f5616a23f 100644 --- a/tests/unit/test_cellprofiler_workspace_export.py +++ b/tests/unit/test_cellprofiler_workspace_export.py @@ -18,7 +18,6 @@ from openhcs.interop.cellprofiler.workspace_export import ( CPAWorkspaceAxis, CPAWorkspacePanel, - CPAWorkspaceRenderer, ) from openhcs.processing.backends.cellprofiler.export_to_database import ( ExportToDatabaseModule, @@ -58,11 +57,9 @@ def axis(kind="Image", name="Intensity_MeanIntensity_DNA", index="ImageNumber"): def test_all_native_tools_preserve_axis_roles(tool, display, fields): panel = CPAWorkspacePanel.from_settings(tool, axis("Index"), axis("Object")) declaration = settings(wants_workspace_file=True, workspace_panels=(panel,)) - ((name, text),) = ( - CPAWorkspaceRenderer(CellProfilerDatabaseColumnDialect("Prefix_")) - .render(declaration) - .items() - ) + ((name, text),) = declaration.workspace_files( + CellProfilerDatabaseColumnDialect("Prefix_") + ).items() assert name == "Database_Prefix.workspace" ((tool_name, rows),) = CPAWorkspacePanel.parse_workspace(text) assert tool_name == display @@ -90,10 +87,7 @@ def test_single_axis_tools_do_not_validate_inactive_y(): def test_disabled_workspace_emits_nothing(): - assert ( - CPAWorkspaceRenderer(CellProfilerDatabaseColumnDialect()).render(settings()) - == {} - ) + assert settings().workspace_files(CellProfilerDatabaseColumnDialect()) == {} @pytest.mark.parametrize("kind,index", [("Bogus", "ImageNumber"), ("Index", "Bogus")]) @@ -111,7 +105,9 @@ def test_workspace_module_records_round_trip_without_reordering(): "PlateViewer", axis(), axis(name="invalid\nfield") ), ) - records = CPAWorkspacePanel.setting_records(ExportToDatabaseModule, True, panels) + records = CPAWorkspacePanel.setting_records( + ExportToDatabaseModule, wants_workspace_file=True, workspace_panels=panels + ) block = ModuleBlock( name="ExportToDatabase", module_num=1, @@ -201,8 +197,8 @@ def test_workspace_panels_survive_source_transport(): def test_workspace_dialect_must_match_its_export_request(): panel = CPAWorkspacePanel.from_settings("Histogram", axis(), axis()) with pytest.raises(ValueError, match="table prefix"): - CPAWorkspaceRenderer(CellProfilerDatabaseColumnDialect("Other_")).render( - settings(wants_workspace_file=True, workspace_panels=(panel,)) + settings(wants_workspace_file=True, workspace_panels=(panel,)).workspace_files( + CellProfilerDatabaseColumnDialect("Other_") ) @@ -215,9 +211,7 @@ def test_workspace_filename_preserves_native_relative_directory_and_single_suffi workspace_panels=(panel,), ) assert tuple( - CPAWorkspaceRenderer(CellProfilerDatabaseColumnDialect("Prefix__")).render( - declaration - ) + declaration.workspace_files(CellProfilerDatabaseColumnDialect("Prefix__")) ) == ("nested/Database_Prefix_.workspace",) From 23cc8cf262bfd043acafd3154b390e5ba0c00c44 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 17:55:40 -0400 Subject: [PATCH 093/235] Derive strict CPA inventory coverage from actual comparison readers (refs #480) --- benchmark/adapters/openhcs.py | 4 +- benchmark/cellprofiler_export_equivalence.py | 69 +++++----- benchmark/matched_cellprofiler_batch.py | 9 +- docs/development/cpa_workspace_export_480.md | 4 +- openhcs/core/equivalence/report.py | 9 ++ .../test_cellprofiler_export_equivalence.py | 3 + .../test_cellprofiler_workspace_export.py | 123 ++++++++++++++++++ 7 files changed, 183 insertions(+), 38 deletions(-) diff --git a/benchmark/adapters/openhcs.py b/benchmark/adapters/openhcs.py index c5cd8be0ef..3653bc3f26 100644 --- a/benchmark/adapters/openhcs.py +++ b/benchmark/adapters/openhcs.py @@ -524,7 +524,9 @@ def _run_converted_cppipe_pipeline( ( *equivalence_report.differences, *database_export_report.differences, - ) + ), + equivalence_report.compared_output_files + | database_export_report.compared_output_files, ) if not equivalence_report.is_equivalent: equivalence_failure_message = ( diff --git a/benchmark/cellprofiler_export_equivalence.py b/benchmark/cellprofiler_export_equivalence.py index a07723be6e..139423df14 100644 --- a/benchmark/cellprofiler_export_equivalence.py +++ b/benchmark/cellprofiler_export_equivalence.py @@ -56,30 +56,29 @@ def cellprofiler_database_export_equivalence( reference_root = Path(reference_output_root) reference_properties = tuple(sorted(reference_root.rglob("*.properties"))) candidate_properties = _outputs_with_suffix(candidate_exports, ".properties") - differences = [ - *_sqlite_export_differences( - _declared_sqlite_paths( - tuple(sorted(reference_root.rglob("*.db"))), - reference_properties, - ), - _declared_sqlite_paths( - _outputs_with_suffix(candidate_exports, ".db"), - candidate_properties, - ), + reference_databases = _declared_sqlite_paths( + tuple(sorted(reference_root.rglob("*.db"))), reference_properties + ) + candidate_databases = _declared_sqlite_paths( + _outputs_with_suffix(candidate_exports, ".db"), candidate_properties + ) + reference_workspaces = tuple(sorted(reference_root.rglob("*.workspace"))) + candidate_workspaces = _outputs_with_suffix(candidate_exports, ".workspace") + reports = ( + _sqlite_export_equivalence( + reference_databases, + candidate_databases, _declared_sqlite_table_subjects(reference_properties), _declared_sqlite_table_subjects(candidate_properties), policy, ), - *_workspace_export_differences( - tuple(sorted(reference_root.rglob("*.workspace"))), - _outputs_with_suffix(candidate_exports, ".workspace"), - ), - *_properties_export_differences( - reference_properties, - candidate_properties, - ), - ] - return RuntimeEquivalenceReport(tuple(differences)) + _workspace_export_equivalence(reference_workspaces, candidate_workspaces), + _properties_export_equivalence(reference_properties, candidate_properties), + ) + return RuntimeEquivalenceReport( + tuple(difference for report in reports for difference in report.differences), + frozenset(path for report in reports for path in report.compared_output_files), + ) def cellprofiler_native_shard_equivalence( @@ -159,13 +158,15 @@ def cellprofiler_native_shard_equivalence( for root in shard_roots: shard_properties = tuple(sorted(root.rglob("*.properties"))) differences.extend( - _properties_export_differences(reference_properties, shard_properties) + _properties_export_equivalence( + reference_properties, shard_properties + ).differences ) differences.extend( - _workspace_export_differences( + _workspace_export_equivalence( tuple(sorted(reference_root.rglob("*.workspace"))), tuple(sorted(root.rglob("*.workspace"))), - ) + ).differences ) shard_subjects = _declared_sqlite_table_subjects(shard_properties) if shard_subjects != reference_subjects: @@ -282,18 +283,19 @@ def _declared_sqlite_paths( return tuple(path for path in database_paths if path.name in declared_names) -def _sqlite_export_differences( +def _sqlite_export_equivalence( reference_paths: Sequence[Path], candidate_paths: Sequence[Path], reference_subjects: Mapping[str, Mapping[str, MeasurementSubject]], candidate_subjects: Mapping[str, Mapping[str, MeasurementSubject]], policy: RuntimeEquivalencePolicy, -) -> tuple[RuntimeEquivalenceDifference, ...]: +) -> RuntimeEquivalenceReport: differences, reference_by_name, candidate_by_name = _named_output_differences( reference_paths, candidate_paths, output_label="SQLite database", ) + compared_paths: set[Path] = set() for name in sorted(reference_by_name.keys() & candidate_by_name.keys()): differences.extend( _sqlite_database_differences( @@ -304,7 +306,8 @@ def _sqlite_export_differences( policy, ) ) - return tuple(differences) + compared_paths.update((reference_by_name[name], candidate_by_name[name])) + return RuntimeEquivalenceReport(tuple(differences), frozenset(compared_paths)) def _sqlite_database_differences( @@ -597,15 +600,16 @@ def _normalized_sqlite_row( return tuple(values) -def _properties_export_differences( +def _properties_export_equivalence( reference_paths: Sequence[Path], candidate_paths: Sequence[Path], -) -> tuple[RuntimeEquivalenceDifference, ...]: +) -> RuntimeEquivalenceReport: differences, reference_by_name, candidate_by_name = _named_output_differences( reference_paths, candidate_paths, output_label="CPA properties file", ) + compared_paths: set[Path] = set() for name in sorted(reference_by_name.keys() & candidate_by_name.keys()): reference_properties = _read_cpa_properties(reference_by_name[name]) candidate_properties = _read_cpa_properties(candidate_by_name[name]) @@ -634,7 +638,8 @@ def _properties_export_differences( f"{mismatched_values!r}", ) ) - return tuple(differences) + compared_paths.update((reference_by_name[name], candidate_by_name[name])) + return RuntimeEquivalenceReport(tuple(differences), frozenset(compared_paths)) def _read_cpa_properties(path: Path) -> dict[str, str]: @@ -698,12 +703,13 @@ def _unique_paths_by_name( return by_name -def _workspace_export_differences(reference_paths, candidate_paths): +def _workspace_export_equivalence(reference_paths, candidate_paths): from openhcs.interop.cellprofiler.workspace_export import CPAWorkspacePanel differences, reference_by_name, candidate_by_name = _named_output_differences( reference_paths, candidate_paths, output_label="CPA workspace" ) + compared_paths: set[Path] = set() for name in reference_by_name.keys() & candidate_by_name.keys(): reference = CPAWorkspacePanel.parse_workspace( reference_by_name[name].read_text() @@ -718,4 +724,5 @@ def _workspace_export_differences(reference_paths, candidate_paths): f"CPA workspace {name!r} panel declarations differ.", ) ) - return tuple(differences) + compared_paths.update((reference_by_name[name], candidate_by_name[name])) + return RuntimeEquivalenceReport(tuple(differences), frozenset(compared_paths)) diff --git a/benchmark/matched_cellprofiler_batch.py b/benchmark/matched_cellprofiler_batch.py index ff7652e785..017edca9bb 100644 --- a/benchmark/matched_cellprofiler_batch.py +++ b/benchmark/matched_cellprofiler_batch.py @@ -62,6 +62,7 @@ ) from openhcs.core.equivalence.comparison import runtime_image_differences from openhcs.core.equivalence.outputs import RuntimeOutputSnapshot +from openhcs.core.equivalence.report import RuntimeEquivalenceReport from openhcs.core.source_matching import SourceImageSetIdentityPolicy from openhcs.core.equivalence.policy import normalize_runtime_identifier from openhcs.core.input_workspace import InputWorkspacePreparationRequest @@ -177,6 +178,7 @@ def _require_compared_output_inventory( reference_snapshot: RuntimeOutputSnapshot, candidate_snapshot: RuntimeOutputSnapshot, candidate_managed_files: frozenset[Path] = frozenset(), + compared_file_report: RuntimeEquivalenceReport = RuntimeEquivalenceReport(()), ) -> None: """Reject unqualified output formats and missing scientific output files.""" counts = [] @@ -193,11 +195,7 @@ def _require_compared_output_inventory( compared_files = ( frozenset(exports.table_outputs) | frozenset(exports.image_outputs) - | frozenset( - path - for path in exports.output_files - if path.suffix.lower() in {".db", ".properties", ".workspace"} - ) + | (files & compared_file_report.compared_output_files) ) if files != compared_files: raise RuntimeError( @@ -836,6 +834,7 @@ def capture_progress_event(event: Mapping[str, Any]) -> None: reference_snapshot=native_snapshot, candidate_snapshot=candidate_snapshot, candidate_managed_files=managed_output_files, + compared_file_report=database_report, ) result = { "repetition": repetition, diff --git a/docs/development/cpa_workspace_export_480.md b/docs/development/cpa_workspace_export_480.md index 360b8e5af7..924a2493d0 100644 --- a/docs/development/cpa_workspace_export_480.md +++ b/docs/development/cpa_workspace_export_480.md @@ -4,10 +4,12 @@ ExportToDatabase accepted and validated workspace panel settings, then discarded The existing export settings now retain the requested workspace and original ordered panel declarations. `CPAWorkspacePanel` owns the external panel grammar, repeated setting binding and authoring transport. Its Scatter/Density leaves share two-axis rendering; Histogram/Box/PlateViewer use the single-axis grammar, with PlateViewer's native `measurement` field. `CPAWorkspaceAxis` derives its Image/Object/Index implementations from their declarations. Image/object measurements use the existing `MeasurementSubject` and database dialect; there is no parallel tool enum or handwritten dispatch roster. -The existing export settings own enabled-workspace selection, exact filenames and matching table-prefix admission. The format renderer consumes only the resulting ordered panels. Authoring projection binds the already-validated public call keywords at the panel owner, retaining unrelated export settings without copying two raw key lookups. The public exporter merges workspace text into the existing file bundle and retains its duplicate-file rejection. The shared database-export comparison parses ordered workspace semantics, rejects invalid layout or unknown fields/tools, and reports missing or changed panels. The strict matched inventory now admits `.workspace` because it has an actual comparison consumer. Database and properties rendering remain unchanged. +The existing export settings own enabled-workspace selection, exact filenames and matching table-prefix admission. The format renderer consumes only the resulting ordered panels. Authoring projection binds the already-validated public call keywords at the panel owner, retaining unrelated export settings without copying two raw key lookups. The public exporter merges workspace text into the existing file bundle and retains its duplicate-file rejection. The shared database-export comparison parses ordered workspace semantics, rejects invalid layout or unknown fields/tools, and reports missing or changed panels. The existing equivalence report records physical inputs only after each matched SQLite, properties or workspace reader completes. The strict matched inventory consumes those exact paths instead of admitting a filename suffix roster. Unknown, unmatched and unread files remain uncovered; file consumption does not erase scientific differences. Existing CSV/image callers retain their separate declared output coverage. Database and properties rendering remain unchanged. Native 4.2.8.1 emits raw subject/feature names in workspace axes, and Object axes always name `Per_Object`, even when separate object tables are selected. These native quirks are preserved, not silently translated to different scientific columns. The near-term QC fixture has only Image/Index axes: both retained native workspace texts match exactly, and all 22 axis references resolve to actual native SQLite columns. This does not establish arbitrary native object-workspace references or shortened column names are valid. The reader validates workflow format version 1 and the numeric producer-version header; the CP producer version is descriptive rather than compared panel meaning. The retained native fixture comes from source `6e69b85122d3c1a44857835ba9b3fe77e743240d`, exact matched ten physical images/two image sets and native CP 4.2.8.1. Its SHA256 is `884f14365ed643c8dd7050ea77e416a1b7253dca83ff11d2e01aa54a8e07bdc6`. Source-backed installed native renderer: `cellprofiler/modules/exporttodatabase.py:4799`. The original native inventory red and interrupted test shard remain in `/var/tmp`; the current production pipeline still needs a fresh complete-output gate before #480 closes. Ownership evidence reuses the complete NRA class census from the shared derivation audit and the finite original CPA class inventory at `/var/tmp/openhcs-qc-workspace-owner-inventory-20261002.json`. The bounded migration removes the old validate-and-discard workspace algorithm from `ExportToDatabaseModule`; authored setting names remain authoritative there, and the panel owner consumes those exact declarations. These are feature and boundary-format changes authored as exact patches; no automatic codemod or formal scientific equivalence is claimed. + +The report consumer inventory parses 1,620 original Python modules across production, benchmark, scripts and tests; its finite AST receipt is `/var/tmp/openhcs-cpa-workspace-480-report-consumer-ast-20261002.json`. Runtime aliases and third-party serialization are not claimed closed. Existing adapter aggregation preserves consumed-path unions. Missing files, malformed readers, changed scientific panels, unknown copies and ambiguous physical aliases have separate controls; the original guard failures and malformed command invocation logs remain retained. diff --git a/openhcs/core/equivalence/report.py b/openhcs/core/equivalence/report.py index fadbc4e2d6..1122e8865f 100644 --- a/openhcs/core/equivalence/report.py +++ b/openhcs/core/equivalence/report.py @@ -4,6 +4,7 @@ from dataclasses import dataclass from enum import Enum +from pathlib import Path class RuntimeEquivalenceDifferenceKind(str, Enum): @@ -43,6 +44,14 @@ class RuntimeEquivalenceReport: """Semantic equivalence result for two runtime outputs.""" differences: tuple[RuntimeEquivalenceDifference, ...] + compared_output_files: frozenset[Path] = frozenset() + + def __post_init__(self) -> None: + object.__setattr__( + self, + "compared_output_files", + frozenset(Path(path) for path in self.compared_output_files), + ) @property def is_equivalent(self) -> bool: diff --git a/tests/unit/test_cellprofiler_export_equivalence.py b/tests/unit/test_cellprofiler_export_equivalence.py index 6d29760c66..6417a741b8 100644 --- a/tests/unit/test_cellprofiler_export_equivalence.py +++ b/tests/unit/test_cellprofiler_export_equivalence.py @@ -352,6 +352,9 @@ def test_database_export_equivalence_compares_sqlite_and_semantic_properties( ) assert report.is_equivalent + assert report.compared_output_files == frozenset( + (*reference.iterdir(), *candidate.iterdir()) + ) def test_database_export_equivalence_rejects_unequal_row_count_before_value_projection( diff --git a/tests/unit/test_cellprofiler_workspace_export.py b/tests/unit/test_cellprofiler_workspace_export.py index 4f5616a23f..f6a0a3e13a 100644 --- a/tests/unit/test_cellprofiler_workspace_export.py +++ b/tests/unit/test_cellprofiler_workspace_export.py @@ -231,3 +231,126 @@ def test_workspace_reader_rejects_malformed_version_and_reordered_fields(): ): with pytest.raises(ValueError): CPAWorkspacePanel.parse_workspace(malformed) + + +def test_workspace_comparison_coverage_is_physical_and_separate_from_science(tmp_path): + from benchmark.matched_cellprofiler_batch import _require_compared_output_inventory + from openhcs.core.equivalence.outputs import RuntimeOutputSnapshot + + reference, candidate = tmp_path / "reference", tmp_path / "candidate" + reference.mkdir() + candidate.mkdir() + fixture = ( + Path(__file__).parents[1] + / "fixtures/cellprofiler/quality_control_4281.workspace" + ) + paths = tuple(root / "QC.workspace" for root in (reference, candidate)) + for path in paths: + path.write_bytes(fixture.read_bytes()) + exports = tuple( + RuntimeExportObservation.from_output_root(root) + for root in (reference, candidate) + ) + report = cellprofiler_database_export_equivalence( + reference, exports[1], policy=RuntimeEquivalencePolicy() + ) + assert report.is_equivalent + assert report.compared_output_files == frozenset(paths) + kwargs = dict( + reference_files=frozenset(reference.iterdir()), + candidate_files=frozenset(candidate.iterdir()), + reference_exports=exports[0], + candidate_exports=exports[1], + reference_snapshot=RuntimeOutputSnapshot(), + candidate_snapshot=RuntimeOutputSnapshot(), + ) + with pytest.raises(RuntimeError, match="without a value comparison"): + _require_compared_output_inventory(**kwargs) + _require_compared_output_inventory(**kwargs, compared_file_report=report) + paths[1].write_text(paths[1].read_text().replace("Image_", "Changed_", 1)) + changed = cellprofiler_database_export_equivalence( + reference, exports[1], policy=RuntimeEquivalencePolicy() + ) + assert not changed.is_equivalent + assert changed.compared_output_files == frozenset(paths) + _require_compared_output_inventory(**kwargs, compared_file_report=changed) + unknown = candidate / "copied-export.unknown" + unknown.write_bytes(paths[1].read_bytes()) + with pytest.raises(RuntimeError, match="without a value comparison"): + _require_compared_output_inventory( + **{**kwargs, "candidate_files": frozenset(candidate.iterdir())}, + compared_file_report=changed, + ) + + +def test_unmatched_workspace_and_reader_failures_never_claim_coverage( + tmp_path, monkeypatch +): + reference, candidate = tmp_path / "reference", tmp_path / "candidate" + reference.mkdir() + candidate.mkdir() + fixture = ( + Path(__file__).parents[1] + / "fixtures/cellprofiler/quality_control_4281.workspace" + ) + (reference / "QC.workspace").write_bytes(fixture.read_bytes()) + extra = candidate / "Unmatched.workspace" + extra.write_bytes(fixture.read_bytes()) + exports = RuntimeExportObservation.from_output_root(candidate) + report = cellprofiler_database_export_equivalence( + reference, exports, policy=RuntimeEquivalencePolicy() + ) + assert not report.is_equivalent + assert not report.compared_output_files + extra.rename(candidate / "QC.workspace") + extra = candidate / "QC.workspace" + exports = RuntimeExportObservation.from_output_root(candidate) + extra.write_text("not a CPA workspace") + with pytest.raises(ValueError): + cellprofiler_database_export_equivalence( + reference, exports, policy=RuntimeEquivalencePolicy() + ) + extra.write_bytes(fixture.read_bytes()) + original_read = Path.read_text + failure = OSError("actual reader failed") + + def fail_read(path, *args, **kwargs): + if path == extra: + raise failure + return original_read(path, *args, **kwargs) + + monkeypatch.setattr(Path, "read_text", fail_read) + with pytest.raises(OSError) as raised: + cellprofiler_database_export_equivalence( + reference, exports, policy=RuntimeEquivalencePolicy() + ) + assert raised.value is failure + + +def test_workspace_aliases_are_compared_by_each_actual_path(tmp_path): + reference, candidate = tmp_path / "reference", tmp_path / "candidate" + reference.mkdir() + candidate.mkdir() + fixture = ( + Path(__file__).parents[1] + / "fixtures/cellprofiler/quality_control_4281.workspace" + ) + first = reference / "QC.workspace" + first.write_bytes(fixture.read_bytes()) + second = candidate / "QC.workspace" + second.symlink_to(first) + report = cellprofiler_database_export_equivalence( + reference, + RuntimeExportObservation.from_output_root(candidate), + policy=RuntimeEquivalencePolicy(), + ) + assert report.is_equivalent + assert report.compared_output_files == frozenset((first, second)) + (candidate / "other").mkdir() + (candidate / "other/QC.workspace").symlink_to(first) + with pytest.raises(ValueError, match="ambiguous"): + cellprofiler_database_export_equivalence( + reference, + RuntimeExportObservation.from_output_root(candidate), + policy=RuntimeEquivalencePolicy(), + ) From 4904884e330126916ffcc7e18b9884ab35fdce63 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 17:58:58 -0400 Subject: [PATCH 094/235] Record merged readiness fixes and strict saved-reader evidence --- .../shared_runtime_plumbing_384_20261001.rst | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/docs/validation/shared_runtime_plumbing_384_20261001.rst b/docs/validation/shared_runtime_plumbing_384_20261001.rst index b32bf532b1..fa42e9d337 100644 --- a/docs/validation/shared_runtime_plumbing_384_20261001.rst +++ b/docs/validation/shared_runtime_plumbing_384_20261001.rst @@ -1779,3 +1779,63 @@ changes beyond the receiving review's original four-file eb2/wheel delta. Installed receiving obligations419/433/435/450, whole-branch R0, full native image parity, full-catalog/scaling and figures remain open. A first-wave fresh four-case ordinary/native qualification is in progress, not a completed gate. + +Library readiness and saved-reader checkpoint, 2026-10-02 +------------------------------------------------------- + +Main ``b4405ff3541add6b91004002b7d6f389f1208636`` is normally merged. +Independent PR482 closes issue477 and PR486 closes issue481. Main's singleton +invocation fix passes 401 controls and original scoped R0/R1; its actual public +translocation observation is 1.679128s execution / 3.094840s total / 1.008657s +compilation. Both saved warm native repetitions pass full database, schema, +discrete and numerical measurements, properties, exact image and physical +inventory gates. This single main-based observation is not a measured gain. +Receipt: /var/tmp/openhcs-singleton-label-main-477-saved-native-science-20261002.json +(SHA256 5c5c4c497df1ee4b115aba42650941f451fac3ae3f784cfcfe3144e0ae94d6ac). + +Issue481 prepares canonical masked float32/float64 declumping signatures through +the existing morphology backend library lifecycle. Numerical bodies are +unchanged. Eight isolated controls and original scoped R0/R1 pass; cold and +disk-cache witnesses reject subsequent compilation or cache loading. Frozen +integrated ``adef79b4ff3825b35a41ef96b60f3bc421a5ddd5`` Speckles execution is +1.915174s, total 3.212164s, compilation 0.959121s. No kernel-cache writes occur +inside execution. All three scientific CSVs match the original fully native +qualified output exactly, with complete inventory and freezes. This necessary +readiness repair does not establish a speedup. Receipt: +/var/tmp/openhcs-pr394-morphology-ready-speckles-science-20261002.json +(SHA256 29526b6c5c37bbd840c706393b9290695894bc80a2247aedc7c718cb58ee5477). + +Issue479's saved relationship reader is integrated from ``2de4e021f``. It +validates directed edge declarations, object/image domains, reverse pairs and +redundant Parent/Children fields before using scalar projections. Saved +snapshots and cache transport retain endpoint correlations; typed value-only +snapshots explicitly retain UNKNOWN evidence. Both-root scoped R0 passes, +but original R1 remains RED for an unchanged validator's relocation (+1 core, +-1 interop, zero global type-check change). This is not all-guards admission. +The merged-parent controls pass 380; root integration passes 235. + +Corrected saved Beginner replay uses the actual complete output root. Both +native repetitions have exact two-image agreement, complete physical-file +coverage and five matching correlation keys / 2,093 directed pairs. Full +measurement parity remains RED: 80 extra Costes features and 63 missing Nuclei +child-mean features. The earlier helper's incorrect results-directory root and +missing-image report are rejected, retained harness evidence. Reader receipt: +/var/tmp/openhcs-relationship-479-full-saved-replay-v2-20261002/receipt.json. + +Issue487 isolates the first loss for unwanted Costes work: existing settings +postprocessing treats legacy Run all metrics=Accurate as true and overwrites +the five authored flags. Native CellProfiler uses this aggregate setting only +for UI visibility, including when Yes; individual choices remain authoritative. +The correct existing owner is ModuleOnlySettingBinding, with the obsolete +runtime override removed. Public-importer RED and native/global AST census are +retained. Qualification is in progress. Colocalization's ordinary 2.514475s +inclusive step cost inside 6.424050s execution is an upper bound, not measured +Costes-exclusive work or a promised saving. The separate 63 missing means +still require first-loss evidence. No numerical tolerance or parity gate changes. + +Compiler profiling confirms repeated effective-configuration construction and +cold schema analysis, but its timings overlap and are diagnostic only. A +private prototype is not admitted: default-factory/callback and mutable-state +laws remain unproved. This route is deferred while the larger unauthorized +metric execution is qualified. Full scaling and fresh figures remain pending; +the long-running optimization goal remains active. From 907087bb9b4c1da1656b41baea7aed65bf0c7866 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 18:15:12 -0400 Subject: [PATCH 095/235] Record complete requested CPA workspace production qualification --- .../shared_runtime_plumbing_384_20261001.rst | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/docs/validation/shared_runtime_plumbing_384_20261001.rst b/docs/validation/shared_runtime_plumbing_384_20261001.rst index fa42e9d337..29d47c301a 100644 --- a/docs/validation/shared_runtime_plumbing_384_20261001.rst +++ b/docs/validation/shared_runtime_plumbing_384_20261001.rst @@ -1839,3 +1839,43 @@ private prototype is not admitted: default-factory/callback and mutable-state laws remain unproved. This route is deferred while the larger unauthorized metric execution is qualified. Full scaling and fresh figures remain pending; the long-running optimization goal remains active. + +Requested CPA workspace production qualification +------------------------------------------------ + +Issue480 is integrated at ``38d02e20a795ad6f8c6457459c380995a798d0fe``. +The existing export settings retain authored workspace requests and panels; +nominal tool/axis leaves own the irreducible external grammar and derive +columns from the existing CPA dialect. The discarded settings validator is +removed. Publication uses the same existing export bundle/materialization +path. Full physical-file admission now consumes paths actually read by the +existing CPA equivalence report, instead of a manually synchronized suffix +roster. Unmatched files and failed readers cannot claim coverage. + +Original scoped R0 across all three roots and original R1 pass against2560; +138 agent controls and 81 root integration controls pass. An actual ordinary +QualityControl public run on clean, sealed38d observes 0.652361870s execution, +2.120112958s total and 1.088341713s compilation. Startup/readiness and shutdown +are excluded. No diagnostic hooks are installed. Both immutable saved native +repetitions pass database/schema/discrete/numerical measurements, nonempty +declared two-row Image table, requested workspace meaning, every physical +output and all source/input/dependency/native/controller freezes. This is a +correctness qualification, not a paired performance gain. The original V4 +missing-workspace inventory RED is retained unchanged. + +Complete receipt: +/var/tmp/openhcs-cpa-workspace-480-integrated-saved-native-v1-20261002.json +SHA256 72e22c3859081de0494ad24440d36a54b306adbaa509d8afcb9db87d671ece5d. +Ordinary source-freeze SHA256: +317de6735208ed9c99149d81296bf40dd3c1e70823e198da69b7cb7575d47e5d. +Standalone main isolation and its independent source checks are in progress; +the integrated result does not claim a main-only benchmark. + +The actual before-fix Beginner observation on frozen4904884 is execution +6.981277943s, total9.296563831s, compilation1.451889992s. It intentionally +preserves the reproduced issue487 metric bug and missing63parentmeans; no +parity exemption or speedup is claimed. Controller/source/environment/native +and complete actual input freezes pass. Source-freeze SHA256: +dc8a40519a10b7dd13ba9809a95bd6e2070622a326afa66ee2684adbe892a28d. +Issue487's source fix passes156 controls and original scoped R0/R1, but actual +candidate pipeline science and paired ordinary timing remain outstanding. From 7b8461bd1790b10cba90d6be83cb2013ed4b73ab Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 18:49:50 -0400 Subject: [PATCH 096/235] docs(perf): record cached metric gains and remaining contract gaps --- .../shared_runtime_plumbing_384_20261001.rst | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/docs/validation/shared_runtime_plumbing_384_20261001.rst b/docs/validation/shared_runtime_plumbing_384_20261001.rst index 29d47c301a..ef134f39ad 100644 --- a/docs/validation/shared_runtime_plumbing_384_20261001.rst +++ b/docs/validation/shared_runtime_plumbing_384_20261001.rst @@ -1879,3 +1879,76 @@ and complete actual input freezes pass. Source-freeze SHA256: dc8a40519a10b7dd13ba9809a95bd6e2070622a326afa66ee2684adbe892a28d. Issue487's source fix passes156 controls and original scoped R0/R1, but actual candidate pipeline science and paired ordinary timing remain outstanding. + +Metric policy, saved parity and readiness follow-up, 2026-10-02 +------------------------------------------------------------- + +Main3705c071b13c29d354417d85dccf78c765b4a580 is normally merged at05ea538a. +Independent PR488 closes480 and PR489 closes487. Their production changes +are already integrated; subsequent main synchronization changes documentation +only. PR489 passes156 controls and original scoped R0/R1. Existing metric +declarations now derive enabled output columns; disabled Costes computations +and columns are omitted without changing enabled numerical implementations. + +Frozen baseline4904884 and sealed candidate d567d4d7 each have two cached +ordinary observations. Baseline execution5.527808428/5.327760458s and total +7.723725582/7.677667536s compare with candidate execution4.583703041/ +4.648376942s and total6.814110309/6.839743058s. Means improve execution +5.427784443 to4.616039992s (0.811744451s) and total7.700696559 to6.826926684s +(0.873769875s). Startup/readiness and shutdown are excluded; one worker +executes inline, with ordinary OUTCOMES and memory observation enabled. +This is a full-branch cached comparison, not an exclusive Costes attribution, +ABBA experiment, statistical claim or fully prewarmed qualification. + +The original baseline6.981277943/9.296563831s is excluded from that comparison: +three kernel overloads were written inside its pipeline timing window. The +later runs have no cache writes, which does not prove absence of cache loads. +Existing public library preparation omits object correlation and prepares +float64 threshold/RWC arrays; canonical runtime stages require float32 arrays. +A fresh private-cache public-preparation probe refuses later compilation and +cache loading: base passes; correlation, threshold and RWC fail. Requested +argument types exactly match retained production cache signatures. This is +readiness evidence using lawful synthetic values, not actual-input science +or performance evidence. Issue491 tracks canonical-stage preparation. + +All seven common CSV domains and two images are unchanged;100 unauthorized +Costes columns disappear (80 canonical and20 derived parent columns). +Candidate repeat scientific bytes are exact. Complete saved native replay +still fails solely for63 missing parent-mean features in both repetitions, +with no Costes, image, other numeric or physical-inventory differences. All +five relationship correlation keys and2093 pairs agree. Numerical tolerances +and required outputs are unchanged; full-case native parity remains RED. + +Issue492 identifies its first loss: all16 object-intensity tables exist, and +the producer has channel groups1/2/5/3, but compilation selects only child +channel3 for the prior-measurement input. Other channels never reach runtime +scope filtering. Repair belongs to the existing artifact relation/projection +contracts and must preserve child subject and image-set correlation. It adds +required scientific work; no speedup is claimed for this correctness repair. + +The bounded joint diagnostic pipeline succeeds with exact scientific bytes, +but capture admission is RED: no batch arrays are captured, and one final +store metadata event exceeds the4MiB cap. Earlier complete selection events +establish the bounded first-loss fact. The worker profile reaches28 serial +measurement invocations and zero batch calls; the earlier assumed batch +capture route is invalidated. A narrower serial-owner capture and nominal +batch-executor transport audit are next, not a claimed admitted replay. + +Cached comparison receipt: +/var/tmp/openhcs-beginner-metric-487-ordinary-cached-comparison-v1-20261002.json +SHA25636883d131c224f20b44139b02078c6ddba8b78a2e598901651910e12a92e4c2b. +Strict saved native receipt: +/var/tmp/openhcs-beginner-metric-487-strict-native-science-v1-20261002/receipt.json. +Fresh READY probe: +/var/tmp/openhcs-colocalization-public-ready-signatures-v2-20261002/observations.json. +Parent-mean first-loss receipt: +/var/tmp/openhcs-beginner-parent-means-first-loss-v2-20261002.json +SHA256c127ff3c79d82ddad4df38bb197606a25ee44a934e1909759922fc62b560f135. +Joint diagnostic admission/science receipt: +/var/tmp/issue487-joint-v2-original-admission-and-science-20261002.json +SHA256b29d5df0cd203e40a6e7f018e59b42d91e815a21af3e623811bc6b87a00ae53e. + +Whole-branch scalar-classifier R0 and issue479 validator-relocation R1 remain +RED. Installed419/433/435/450, original native image differences, full30-case +and scaling reruns and fresh figures remain open. The optimization goal stays +active; these measured gains do not redefine or complete the target. From e742defc2e57d576362a2c9115e1ca6429a3847f Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 19:34:29 -0400 Subject: [PATCH 097/235] docs(perf): record current generic-query frontier and acceptance limits --- .../shared_runtime_plumbing_384_20261001.rst | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/docs/validation/shared_runtime_plumbing_384_20261001.rst b/docs/validation/shared_runtime_plumbing_384_20261001.rst index ef134f39ad..043b0a6d65 100644 --- a/docs/validation/shared_runtime_plumbing_384_20261001.rst +++ b/docs/validation/shared_runtime_plumbing_384_20261001.rst @@ -1952,3 +1952,80 @@ Whole-branch scalar-classifier R0 and issue479 validator-relocation R1 remain RED. Installed419/433/435/450, original native image differences, full30-case and scaling reruns and fresh figures remain open. The optimization goal stays active; these measured gains do not redefine or complete the target. + +Canonical READY and current generic-query frontier +------------------------------------------------- + +PR493 is merged on mainb85f1e4add6075a71f4fd4e5ba7e2512e48990e5 and +closes491. Root normally integrates it atb015526c7c491abf80ea0958a08eb8723131c152. +Canonical image/object stages replace the duplicate kernel list and threshold +arithmetic.23 focused controls and original three-root R0/R1 pass; the actual +root fresh-cache integration control passes1test/9.86s. Compilation and disk +cache loading are refused after public preparation across IMAGE/OBJECT/BOTH +and threshold/Costes option states. This establishes readiness, not a speedup. + +One source-pinned Beginner diagnostic on clean b015 completes with exactly one +existing worker profile, nonempty existing runtime events,108 actual CP +executor calls,133 declared callable calls and no diagnostic errors. Profile +and event files occupy1.19MiB. Ten prelaunch controls preserve original calls, +errors, alias/mutation, descriptor/loader and thread partition behavior. The +instrumented12.9997s execution/15.2588s total is not an ordinary benchmark or +a regression. Profiling overhead is substantial and nonuniform. + +The same-thread orchestrator region partitions exactly12.988882s. This region +omits prior source/config decode; it is not the complete server-job clock. +Disjoint instrumented exclusive costs include module-output recording4.918576s, +declared callable invocations2.300491s, stack loading1.007764s, per-object output +recording0.920284s and worker residual0.861094s. cProfile costs overlap these +regions and must not be added or scaled to ordinary clocks. + +The determining generic query path makes312 full-column queries directly from +upstream parent-mean derivation:803488 structural-missing checks and195808 +qualification calls within those queries. Whole-worker structural checks are +1187866. Qualification precedes feature/source/row/object selection. Issue496 +tracks an operation-local schema/index fold on the existing nominal owners. +Opaque callback order, errors and mutation must remain observable; explicit +NaN propagation differs from default finite-value qualification. No new cache, +callback-identity dispatch or per-Relate numerical optimization is admitted. + +Strict replay of this diagnostic retains exactly63 missing parent means in both +saved native repetitions, with zero other numeric, image or inventory failures. +All seven CSVs/two TIFFs are byte-identical to ordinary candidated567; full +physical inventories agree and five correlation keys/2093pairs match. Source, +outputs, inputs, native and dependencies remain unchanged through replay. +Final all-zero science assertion correctly fails: no63-feature waiver exists. +Receipt /var/tmp/openhcs-current-generic-job-profile-strict-native-science-v1-20261002/receipt.json +SHA256bbc135e6de97628ddbabe8d350381a76a8af3000b4fba28f19e1879c27a220a8. + +Saved CSV readers cannot reconstruct preexport long rows, structural padding, +explicit missing cells, ownership or query order. That route is rejected; +metadata/relationship reader-domain errors are harness counterevidence, not +science failures. A bounded post492 actual logical-column/query capture must +demonstrate material absolute payoff before production optimization. The prior +ordinary Relate span near0.8s is only a whole-operation ceiling; the corrected +four-channel frontier and transfer remain unmeasured. + +Independent client residual evidence is0.818/0.804s for cached Beginner and +0.379s for QC. Two submissions occupy0.367/0.368s and0.120s respectively; +waiting beyond server clocks occupies0.03-0.08s. Four pipeline renders and +three config renders are observed in source, but their exact elapsed and byte +equality remain unproved. Blind memoization is rejected because shallow-frozen +documents contain mutable steps. This client-only route cannot remove server +execution work or independently close the seconds-wide target gap. + +Issue492's prepared four-channel/two-site repair passes its original controls +and guards. New prerequisite495 proves complete dynamic queries previously +ignored compiled producer path/backend, allowing foreign locations or an +ambiguous retained producer after legal rebinding. Existing planned query and +location authorities own the repair and point-in-time address law. Combined +qualification and actual full native63-field acceptance remain pending. + +The shared /home filesystem temporarily blocked Git commits. Only the owned +mutable worktree's website projection was made sparse through standard Git; +all75 website files were first preserved byte-exact at +/var/tmp/openhcs-owned-runtime-website-sparse-preserved-20261002/website with +their manifest receipt. Git objects retain the same files. This freed27MiB; +canonical ROOT, source code, benchmark inputs/outputs, dependencies and shared +caches were untouched. Earlier b015 source freezes completed before this +projection change; future freezes must explicitly record the sparse website. +All remaining acceptance and full-catalog/scaling/figure obligations stay open. From 5d50a0bb46ab4ccdd17bb9924ce698893f5ed85b Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 20:10:15 -0400 Subject: [PATCH 098/235] docs(perf): qualify corrected producer domain and native parity --- .../shared_runtime_plumbing_384_20261001.rst | 97 ++++++++++++++++++- 1 file changed, 96 insertions(+), 1 deletion(-) diff --git a/docs/validation/shared_runtime_plumbing_384_20261001.rst b/docs/validation/shared_runtime_plumbing_384_20261001.rst index 043b0a6d65..f928de0fba 100644 --- a/docs/validation/shared_runtime_plumbing_384_20261001.rst +++ b/docs/validation/shared_runtime_plumbing_384_20261001.rst @@ -1985,7 +1985,7 @@ qualification calls within those queries. Whole-worker structural checks are 1187866. Qualification precedes feature/source/row/object selection. Issue496 tracks an operation-local schema/index fold on the existing nominal owners. Opaque callback order, errors and mutation must remain observable; explicit -NaN propagation differs from default finite-value qualification. No new cache, +NaN propagation differs from default measurement-presence qualification. No new cache, callback-identity dispatch or per-Relate numerical optimization is admitted. Strict replay of this diagnostic retains exactly63 missing parent means in both @@ -2029,3 +2029,98 @@ canonical ROOT, source code, benchmark inputs/outputs, dependencies and shared caches were untouched. Earlier b015 source freezes completed before this projection change; future freezes must explicitly record the sparse website. All remaining acceptance and full-catalog/scaling/figure obligations stay open. + +Corrected producer domain, native parity and residual measurement frontier +------------------------------------------------------------------------ + +PR497 merges on main49f2fd3639363b7cc5e716eb3ba3a7358aa85429 and formally +closes492 and495. Its independently qualified production commit iseeed09233; +final6da19179 adds tracked qualification only. Original unchanged R0/R1 pass +against normally merged main6ce527a29 with no increased finding and unchanged +R1 budget.211 related controls pass;212 integrated controls pass11.78s with +two unrelated plugin configuration warnings. Global supplemental ownership +census covers704 modules/5228 original classes with no AST errors. + +InputObjectMeasurementSourceRelation derives complete measurement-producer +consumption through the existing input projection owner. The old planner and +RelateObjects selection algorithms are removed. Ordinary input and PLATE +selection remain intact. Dynamic queries resolve the actual compiled producer +path/backend and snapshot mutable group-address maps at query birth. The +meaningful old store-pickle control passed before planned lookup but failed +after caching a mapping-proxy address. Both lookup domains now use the existing +bounded store-owned cache lifetime; obsolete flat cache fields, initialization, +invalidation and transport paths are deleted. Ordered records, empty results, +replacement revision, eviction, primary/current/observation state and aliases +are covered. Arbitrary external plan/mapping callbacks and query-target callback +counts under bounded eviction remain outside the concrete production audit. + +Root integrates production at5ed0d13978db1cda92a490614d326525ad552af7. A fresh +uninstrumented ordinary Beginner1w1t completes with execution4.650205374s, +compilation1.603918076s and total7.070084621s. Mandatory READY, registry/kernel +warmup, process startup and shutdown are excluded. Execution is inline with one +thread; no measured fork/spawn startup. Default OUTCOMES and ordinary memory +observation are retained. The tracked75-file website-only sparse projection is +explicitly inventoried; no production source/input/dependency is omitted. +Source, Python, installed dependencies, native binaries and exact image/CPPipe +hashes are fixed before and after. This single correctness observation is not +causal optimization or statistical evidence. + +The unchanged strict reader consumes all seven CSVs and two TIFFs against BOTH +retained native repetitions: ZERO numeric/measurement differences, ZERO image +differences, complete physical inventory, and five correlation keys/2093pairs +on both sides. The63 missing parent means are restored. Only MyExpt_Nuclei.csv +changes scientifically versus the previous ordinary result. Reader/source and +output hashes remain unchanged through comparison. This execution uses the +integrated394/479 reader: it qualifies the producer repair's outputs, not a +standalone main-only execution or whole394 architecture gate. +Receipt /var/tmp/openhcs-child-measurements-492-integrated-strict-native-science-v1-20261002/receipt.json +SHA25637930ee4622c5fc1e067fc853bef1ae627ebe74546c27c3cc65d9fb476343ba0. +Tracked independent qualification: +docs/validation/child-measurement-producer-selection-492-495-20261002/QUALIFICATION.rst. + +The retained native warm-process invocations are15.270141648s and14.634740414s, +mean14.952441031s. A descriptive comparison to this single corrected execution +is3.215x execution and2.115x including OpenHCS compilation/client completion; +these are different sample counts, not a causal change or statistical claim. +Native invocation includes prepare/modules/post-run/close, with import/JVM, +CPPipe load and its warmup excluded. Startup is not added to either pipeline. +Other cases and full-catalog/scaling figures are not qualified by this case. + +The next actual logical-query capture executes438 queries over seven tables, +not four times the previous312. Its processor succeeds and all nine scientific +files are byte-identical to the qualified5ed ordinary output; complete physical +inventories match. Diagnostic admission correctly fails before writing any +logical graph: unchanged V6 refuses the150000-node unique-instance inventory. +No partial graph, increased limit, saved-CSV substitution or performance claim +is admitted. The profiled/captured14.1409s execution is diagnostic overhead, +not an ordinary regression. Preserve the original failure and source freeze. +Artifact /var/tmp/openhcs-measurement-query-logical-capture-v1-20261002/observations.json. +Scientific-byte receipt: +/var/tmp/openhcs-measurement-query-logical-capture-v1-scientific-byte-comparison-20261002.json. + +A revised diagnostic representation may pack numeric answer-oracle leaves while +preserving every actual query and the complete determining input/schema/alias +graph. Node/category counts and original replay must establish admission before +any matched candidate timing or production change. Existing150000-node, +32MiB graph,4MiB ledger and512MiB free-space limits remain unchanged. Pure +callback declarations alone do not license skipping arbitrary scalar equality +or string effects; original opaque callback/error/mutation order remains a +separate production wiring gate. + +The output-recording ownership audit also rejects a misleading second frontier: +coarse recorder-exclusive4.9186s contains the query496 subtree. In cProfile, +record_module_outputs self time is0.000638s, and measurement-table construction +contains Relate augmentation4.1412s/upstream query3.3457s. Those diagnostic +times cannot be added. Independent100-call object recording is dominated by +split_scoped_rows0.5620s diagnostic. Its18735-row domain has80 colocalization, +16 concatenated and4 shape batches. Existing row owners could remove dictionary +expansion, but no credible multi-second ordinary payoff is established; the +standalone route is rejected pending a necessary shared-query dependency. +Audit /var/tmp/openhcs-b015-output-recording-ownership-audit-20261002.json +SHA256b0bf2c90f50c1da5022f6aa2a657779f0bdd9390c0adfb022a86b5db21b10779. + +Root normally merges current main after all5ed jobs complete, at899aaddce; +that merge changes documentation/evidence only relative to5ed production. +Whole394 scalar-classifier R0 and479 validator-relocation R1 remain RED. +Installed419/433/435/450, outstanding native image differences, full30/scaling +reruns and fresh figures remain open. The unlimited optimization goal is ACTIVE. From 2d3d75e80309472f6b6b26af067da6e69aa32ab6 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 20:25:20 -0400 Subject: [PATCH 099/235] docs(perf): separate plate export from callable timing --- .../shared_runtime_plumbing_384_20261001.rst | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/docs/validation/shared_runtime_plumbing_384_20261001.rst b/docs/validation/shared_runtime_plumbing_384_20261001.rst index f928de0fba..ef92751224 100644 --- a/docs/validation/shared_runtime_plumbing_384_20261001.rst +++ b/docs/validation/shared_runtime_plumbing_384_20261001.rst @@ -2124,3 +2124,55 @@ that merge changes documentation/evidence only relative to5ed production. Whole394 scalar-classifier R0 and479 validator-relocation R1 remain RED. Installed419/433/435/450, outstanding native image differences, full30/scaling reruns and fresh figures remain open. The unlimited optimization goal is ACTIVE. + +Profiler-free exclusive ledger and plate export correction +--------------------------------------------------------- + +The source-pinned5d50a0bb46ab4ccdd17bb9924ce698893f5ed85b diagnostic completes +with BOTH cProfile/runtime profiler disabled,24 coarse nominal boundaries and +14 meaningful prelaunch controls. Source/environment/native/input and explicit +website-only sparse witnesses remain fixed before/after. It retains32,223bytes +of JSON, no profile/capture artifacts and no diagnostic errors. All nine +scientific output bytes and the complete11-file inventory are identical to the +strict-both-native-qualified5ed ordinary result. Managed metadata differences +are recorded separately. No new native execution or tolerance waiver is used. + +Public execution4.703251600s/compilation1.568818331s/total7.089778207s are +diagnostic, not a causal gain or an estimate of wrapper overhead. The execution +region4.692535908s partitions into23 nonzero owner groups:133 hooked callable +invocations1.923475937s and2.769059971s outside that boundary. The invocation +boundary includes metadata/filtering and does NOT cover all registered calls. +In particular, execute_plate_scoped_steps directly resolves/calls PLATE exports. +Actual ExportToSpreadsheet progress is0.707153320s; it lies inside the +server-exclusive0.734300637s bucket. These intervals overlap and cannot be +added or subtracted as exact disjoint method costs. Server reconstruction is +therefore not established as the dominant0.734s term. Source/config decoding +also precedes this region. Compiler-exclusive1.009028861s includes genuine +fresh compilation, not a second artifact-reuse compile. + +Current measured exclusive query cost is0.481040613s/438calls. Nonquery module +recording is0.405474477s (inclusive0.886515090s), object recording0.156714182s, +stack loading0.184871211s, CP contract execution0.212007483s, worker remainder +0.140013289s, CP module remainder0.130000117s and image request0.086237226s. +Saving/publication/unstacking individually are smaller. Recording remainder +contains actual table creation, parent means and other required processing; it +is not isolated provenance. All-query/all-module/all-object recording has an +impossible elimination ceiling1.043229273s before retaining mandatory work. + +This invalidates the single-query route as the main multi-second fix. Its +18-control compact numeric-oracle capture preparation is retained but will not +be frozen/launched/polished without a necessary broader dependency. Existing +V6 and original limits remain unchanged. The new investigation maps common +columnar table/schema derivation across queries, recorders and the actual +spreadsheet publication path, retaining opaque scalar/callback semantics. +The exporter needs its own authority/phase analysis before a payoff claim. + +Ledger /var/tmp/openhcs-current-generic-runtime-ledger-v2-20261002/observations.json. +Corrected analysis /var/tmp/openhcs-current-generic-runtime-ledger-v2-analysis-plate-corrected-20261002.json +SHA2568b74fb088fae2527563fb42d38f3dedf5ab5ed936c5de805cd394343e3d0529c. +Byte gate /var/tmp/openhcs-current-generic-runtime-ledger-v2-scientific-byte-gate-20261002.json. +Original first-classification and correction receipts are both retained. +PR394's current description is rewritten around the remaining implementation; +merged fixes are described as dependencies and closed issue clauses removed. +Open persisted-publication134 now has an explicit closing link to draft394, +alongside384/419/433/435/450/479/176/496. None is claimed complete merely linked. From 396379b06769a3f5afe92413b7d9d6a21dd3f66f Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 21:45:57 -0400 Subject: [PATCH 100/235] docs: record current plumbing frontier and rejected performance routes Refs #496, #394. Preserve separate scientific, diagnostic and performance gates; record current ordinary Speckles and complete 3D plumbing breakdown. --- .../runtime_plumbing_frontier_20261003.rst | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 docs/plans/runtime_plumbing_frontier_20261003.rst diff --git a/docs/plans/runtime_plumbing_frontier_20261003.rst b/docs/plans/runtime_plumbing_frontier_20261003.rst new file mode 100644 index 0000000000..e34009a5cb --- /dev/null +++ b/docs/plans/runtime_plumbing_frontier_20261003.rst @@ -0,0 +1,134 @@ +Runtime plumbing frontier: current measurements and rejected routes +================================================================= + +This checkpoint supports PR #394 and issue #496. It records measurements and +remaining investigation, not a new production optimization. Main #498 was +normally merged and pushed at ``093c2f42d1235b5e218800f57937b6a52b4f5255``. +That transition changed documentation only; production sources remain identical +to the earlier native-qualified integrated source. + +Timing boundaries +----------------- + +Every run uses the original public benchmark driver, one worker and one CPU +thread, default OUTCOMES completion and the ordinary memory observer. Mandatory +registry/library/kernel preparation completes before server READY. Server +startup, preparation and shutdown are outside pipeline clocks. Execution excludes +compilation; total includes compilation and completion. Single-worker execution +is inline. The retained native invocation clock excludes import/JVM/pipeline +loading and includes prepare, modules, post-run and closure. + +Diagnostic nested exclusive clocks partition their original execution root. +They include observer overhead and required work. A callable boundary includes +its filtering, conversion and metadata behavior; it is not a pure-kernel clock. +Inclusive descendants cannot be added to their ancestor. Correct output bytes +do not establish performance improvement, input immutability or callback purity. + +Fresh ordinary Speckles +----------------------- + +Two independent uninstrumented invocations on the current source produced +execution times of 1.199828148s and 1.159908533s. Their means are: + +* Execution: 1.179868340s. +* Compilation: 1.067295671s. +* Total: 2.514483608s. + +Both complete five-file inventories, including all three scientific CSVs, +are byte identical to the qualified baseline. Physical images, pipeline, +configuration, server environment, dependencies and native evidence are joined +to the original receipts. Compared descriptively with the retained native +invocation mean of 1.915068373s, execution is 1.6231x faster and total is 0.7616x +as fast. The execution gap to 2x is 0.222334154s. Native was not rerun for this +checkpoint; two observations do not establish statistical significance or a +causal source improvement. + +The earlier and current pipelines both invoke IdentifyPrimaryObjects twice. +Their relevant source bodies and settings are identical. The prior slower +timing therefore has no established code-change explanation. The current +ordinary progress spans put about 0.820924s in those two steps, approximately +70 percent of execution. + +Actual declumping inputs and boolean output oracles were separately captured +under unchanged storage limits. The original two stage bodies took 0.070122s +and 0.001791s, totaling 0.071913s. Capture overhead was excluded from these +stage clocks. Even eliminating that stage cannot bridge the measured gap, so +the seed optimization route is stopped before a candidate or replay is built. +The original diagnostic wrapper import failure is retained separately; the +successful harness passed controls for all five actual installed hooks. + +Current 3D plumbing +------------------- + +An unchanged 24-boundary diagnostic on the current source completed with both +profilers disabled. Its execution root is 8.958889063s, partitioned into +4.163085915s at 211 callable boundaries and 4.795803148s outside them. The latter +is approximately 53.5 percent of execution. Selected disjoint exclusive terms: + +* Stack loading: 0.965303496s across 32 calls. +* Output saving: 0.608366542s across 28 calls. +* Worker-lane residual: 0.619469857s. +* Image-request construction: 0.390939402s across 30 calls. +* Module-output recording: 0.413856133s across 26 calls. +* Metadata publication: 0.493661132s across five calls. +* Artifact metadata reconciliation: 0.288939269s. +* Validation and unstacking: 0.189130950s across 28 calls. + +There are zero feature-query calls. The complete six-CSV/120-TIFF scientific +bytes match the qualified ordinary baseline, all 128 inventory paths match, +and three physical input volumes, the pipeline and retained native logical +volume/plane witnesses are verified. Only the managed metadata JSON differs, +with both hashes explicitly reported. This is a diagnostic breakdown, not an +accepted ordinary runtime gain. + +The prior ordinary 3D execution of 8.536s versus native 14.396s leaves roughly +1.34s to reach 2x execution. Publication and reconciliation together have only +a 0.7826s diagnostic ceiling before mandatory work. A publication-only change +is therefore insufficient. The investigation must span shared producer/source +derivation across loading, image requests, recording, saving and publication. +The existing nominal authorities are the intended owners; no extra mirror, +global cache, per-function shortcut or delayed per-step visibility is admitted. + +Measurement frontier and architectural constraints +-------------------------------------------------- + +The separate 42-boundary Beginner diagnostic measures a disjoint table +assembly/query/pivot envelope of 1.462782s. Native CSV rendering is another +0.163899s. All nine scientific output files and complete physical inventory +match the native-qualified baseline. Its occurrence graph reaches the original +4,096-event limit and remains RED independently of scientific correctness. +The complete Speckles graph demonstrates repeated access to one table and +object-label vector, but that entire measurement lane is only about 0.038s. +Neither query-only nor the 16-call intensity projection route can close the +dominant gaps, and the measurement fold is not a 3D optimization: that workload +does not perform these queries. + +Existing serialization views have distinct semantics. Raw projection source +metadata and enriched SOURCE_METADATA cannot share one output dictionary. +The transaction overlays raw path records before decoding surviving entries; +decoding overwritten malformed entries earlier would change first errors. +Serializer callbacks, independently mutable outputs, current per-step +publication visibility and concurrent-axis final pruning remain obligations. +A frozen projection declaration does not establish deep immutability of its +image metadata. Ownership proposals remain provisional until those laws and +representative replay show a sufficient end-to-end payoff. + +Retained evidence +----------------- + +The following local receipts contain exact source/controller/environment/input +pins, original commands, file inventories and hashes: + +* ``/var/tmp/openhcs-current-speckles-ordinary-pair-v1-20261003/observations.json``: + SHA-256 ``d08f35061285e8a4d543377c1de7993623654844eddcaea806d269672a0bf4a1``. +* ``/var/tmp/openhcs-speckles-seed-capture-v4-scientific-byte-gate-20261003.json``: + SHA-256 ``4da188db100b9526f18a6a02ce98872dad6e7ee5dca60c1b85e7052e1287e301``. +* ``/var/tmp/openhcs-current-3d-runtime-ledger-v1-scientific-byte-gate-20261003.json``: + SHA-256 ``36a4f2cdcc14baaa38f027a6dc759facd1229cc1f5960ddb063cbe689efda0cf``. +* ``/var/tmp/openhcs-measurement-occurrence-ledger-v1-scientific-byte-gate-v2-20261003.json``: + SHA-256 ``4352fa24e8984e0f4c67587ae674ff28085ed134531dea28fe7d5b8517671f99``. + +PR #394 remains draft. Whole-branch original R0 and the #479 per-file R1 +failures remain explicit and unwaived; installed consumer acceptance, +full-catalog/scaling measurements and fresh figures remain unfinished. +The performance goal remains active. From fdad1567f15948527ab4d08e2868579219763431 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 22:52:24 -0400 Subject: [PATCH 101/235] docs: record current 3D GC attribution and producer ownership frontier Refs #384, #496. Keep scientific acceptance separate from diagnostic costs and document the existing producer semantics before the next generic runtime candidate. --- .../runtime_plumbing_frontier_20261003.rst | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/docs/plans/runtime_plumbing_frontier_20261003.rst b/docs/plans/runtime_plumbing_frontier_20261003.rst index e34009a5cb..9335fe603f 100644 --- a/docs/plans/runtime_plumbing_frontier_20261003.rst +++ b/docs/plans/runtime_plumbing_frontier_20261003.rst @@ -132,3 +132,79 @@ PR #394 remains draft. Whole-branch original R0 and the #479 per-file R1 failures remain explicit and unwaived; installed consumer acceptance, full-catalog/scaling measurements and fresh figures remain unfinished. The performance goal remains active. + +Deeper current 3D attribution and producer ownership +--------------------------------------------------- + +Two further diagnostics use clean source ``396379b06769a3f5afe92413b7d9d6a21dd3f66f``. +Its production, test and benchmark driver bytes are unchanged from the preceding +``093c2f42`` source. Both preserve mandatory READY warmup, inline 1w_1t execution, +OUTCOMES export, the normal memory observer and both disabled profilers. Neither +is a new ordinary performance comparison. + +The 36-hook run separates metadata normalization and provenance operations from +their actual nested callers. It observes 29,701 normalization calls outside raw +callable boundaries, totaling 0.317682s exclusive. Ordinary normalization creates +a fresh scalar identity and reuses the cached plane collection; it does not copy +the complete 60-plane lineage. Whole-plane merges and derived naming do rebuild +plane/contributor snapshots. Those operations retain live-mutation and fresh +snapshot obligations. Their observed costs do not admit a standalone fix large +enough to close the 1.338s qualified execution gap. + +Several large isolated stalls prompted a separate 25-hook run with passive GC +callbacks. It does not disable GC, freeze objects, change thresholds, force a +collection or replace existing callbacks. Startup has no active pipeline root. +Its execution root is 8.462928s: 3.803686s within the 211 declared callable +boundaries and 4.659242s outside them. The public diagnostic clocks are execution +8.490154s, compilation 1.824986s and total 11.210223s. GC consumes 0.285505s, +including one 0.196025s generation-two collection during saving. Collection +durations overlap the owner clocks; adding them to plumbing would double count. +GC alone cannot explain or close the remaining gap, so a GC-policy optimization +is rejected. Different diagnostic clocks on identical production bytes are not +evidence of a production gain or regression. + +Both diagnostics match all six CSVs and 120 TIFFs byte for byte against the same +native-qualified baseline, with complete 128-file inventories and physical +input joins. No fresh native repetition is claimed. The 36-hook scientific +output is retained in a lossless, individually hash-verified archive at +``/var/tmp/openhcs-current-3d-owner-ledger-v2-20261003/scientific-output.tar.gz``; +its custody receipt records every original path, mode, size and hash. Later +readers must use or restore that archive rather than assume those extracted +files remain present. Previous baselines and frozen failures are unchanged. + +Source and consumer audits reject two tempting shortcuts. Artifact-only groups +already return ``NoMainFlowOutput`` and skip unstacking and saving. Canonical CP +publication already reuses the stored runtime artifact payload. Downstream CP +inputs deliberately distinguish stored secondary artifacts from relation-owned +current main flow. The latter has an independent mutable whole buffer and final +filename/source-component context. Repeated cache hits share that buffer. A +direct stored-artifact substitution would change these relationships and, in a +saved determining case, 62 metadata facts. The final named output bundle can +also retain earlier sibling outputs, so the last canonical return roster cannot +stand in for the complete main-flow cohort. + +The remaining structural route belongs to existing ``PatternGroupOutputData``, +``AlignedImageStack``, manifest and runtime-input owners: derive physical plane +views and final context from the exact correlated producer occurrence, preserving +the independent main-flow buffer, rather than repeatedly project and recompose +the whole cohort. The seven marked load/request/record/unstack/save/publication/ +reconciliation spans total 3.432617s in the GC diagnostic. This is an upper +envelope containing mandatory work, not a claimed removable cost. Before a +candidate, representative replay must retain the stored occurrence, actual +merged bundle, projected leaves, final records and cache, filename/path prestates, +and next consumer edge in one graph. No fake processing context, new cache, +canonical-roster shortcut or zero-copy assumption is admitted. + +Additional retained receipts: + +* ``/var/tmp/openhcs-current-3d-owner-ledger-v2-scientific-byte-gate-20261003.json``: + SHA-256 ``df4f6548f4459c6a55ea5dc182ac52a5472eaacace572ae60740a9409377ca89``. +* ``/var/tmp/openhcs-current-3d-gc-ledger-v1-scientific-byte-gate-20261003.json``: + SHA-256 ``d0b8d6af1b8a1777b03624cf6435d67b04746e9477a5cefbc7a962feaa2d667d``. +* ``/var/tmp/openhcs-current3d-canonical-mainflow-CP-owner-review-v1-20261003.json``: + SHA-256 ``9c264eaa2a735388f5694a305dcbf8ff61258ed959adb4819ffacfcb39235bbf``. +* ``/var/tmp/openhcs-current3d-canonical-mainflow-macro-owner-audit-v4-20261003.json``: + SHA-256 ``c589df6f3c28d6d3a3dc668adc696089bc4aeb68aa2d762a0128592e8d67b891``. + +This checkpoint changes documentation only. No optimization, merge, original +R0/R1 waiver, installed acceptance or full-catalog/scaling completion is claimed. From 75cffb0aa093bc018bbe2e71082019877cb4063a Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 2 Oct 2026 22:55:01 -0400 Subject: [PATCH 102/235] docs: record custody of the completed GC diagnostic outputs Refs #384. All scientific bytes remain in individually verified lossless archives; no source or benchmark behavior changed. --- docs/plans/runtime_plumbing_frontier_20261003.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/plans/runtime_plumbing_frontier_20261003.rst b/docs/plans/runtime_plumbing_frontier_20261003.rst index 9335fe603f..7d5eb76dbe 100644 --- a/docs/plans/runtime_plumbing_frontier_20261003.rst +++ b/docs/plans/runtime_plumbing_frontier_20261003.rst @@ -170,7 +170,9 @@ output is retained in a lossless, individually hash-verified archive at ``/var/tmp/openhcs-current-3d-owner-ledger-v2-20261003/scientific-output.tar.gz``; its custody receipt records every original path, mode, size and hash. Later readers must use or restore that archive rather than assume those extracted -files remain present. Previous baselines and frozen failures are unchanged. +files remain present. The 25-hook output uses the same verified custody protocol +at ``/var/tmp/openhcs-current-3d-gc-ledger-v1-20261003/scientific-output.tar.gz``. +Previous baselines and frozen failures are unchanged. Source and consumer audits reject two tempting shortcuts. Artifact-only groups already return ``NoMainFlowOutput`` and skip unstacking and saving. Canonical CP From 4eaacf17e5de3dba59ec217f133fa6e1821c8641 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 00:23:35 -0400 Subject: [PATCH 103/235] docs: pin current runtime replay and compilation frontiers (refs #384, #496) --- .../runtime_plumbing_frontier_20261003.rst | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/docs/plans/runtime_plumbing_frontier_20261003.rst b/docs/plans/runtime_plumbing_frontier_20261003.rst index 7d5eb76dbe..a41e346ec9 100644 --- a/docs/plans/runtime_plumbing_frontier_20261003.rst +++ b/docs/plans/runtime_plumbing_frontier_20261003.rst @@ -210,3 +210,79 @@ Additional retained receipts: This checkpoint changes documentation only. No optimization, merge, original R0/R1 waiver, installed acceptance or full-catalog/scaling completion is claimed. + +Current producer/consumer evidence and compilation frontier +---------------------------------------------------------- + +Fresh diagnostics at ``bb9c49c2ad70f1cee49fa0cde47592681edb2be3`` preserve +main ``87d9a99a9``, shared dependency/native/input pins, normal READY warmup, +inline 1w_1t execution, OUTCOMES completion and the normal memory observer. +Their observation clocks are not ordinary performance improvements. + +The bounded V7 capture retains four actual step-13 ImageMath producer and +step-14 Resize load states, totaling 856,419,294 bytes under the unchanged +384 MiB per-item and 1 GiB total limits. Its full six-CSV/120-TIFF parity, +complete scientific inventory, all 180 ordered physical source references, +and three native input-volume witnesses pass. Original step-13 replay matches +all 60 records, named/VFS/cache outputs and the complete 123-array identity, +sharing, dtype, stride and writeability graph. + +Original step-14 replay is RED before cache lookup: the actual producer +manifest lives in module ``_STEP_OUTPUT_MANIFESTS``, a WeakKeyDictionary outside +ProcessingContext transport. Reconstructing it from the separate producer +oracle would fabricate a receiver state and is rejected. Independently, +RuntimeSourceBindingContextCache deliberately drops four derived caches through +its reducer. The actual after-load graph proves the result is the retained +whole image for its exact 60-path cache key, but cold restored clocks cannot +represent warm pipeline work. A future replay must carry actual manifest/cache +owners in the same graph and rekey identity indexes only from real owner +references, or obtain live production confirmation. Original failed artifacts +remain preserved; schema completeness is not complete transaction state. + +A six-group ephemeral layout experiment validates the proposed existing-class +projection ancestor layout, constructor signatures, default factories, frozen +assignment, cooperative projection order, errors and fresh public provenance. +This proves layout feasibility only. Lazy read algorithms, custom subclass +behavior, combined transaction acceptance and sufficient elapsed savings +remain unproved. No new wrapper or cache authority is implemented. + +Illumination remains the weakest known total comparison. Its current V3 +diagnostic records compilation 1.125192s, execution job 0.377852s and total +1.844212s, with both authored NPY files, full inventory and actual physical +TIFF/pipeline/native witnesses matching the retained qualified reference. +The compilation partition places 0.302983s exclusive in dataclass analysis +and 0.285258s in nested documentation extraction: 0.588241s together. Four +client pipeline renders plus three config renders take 0.235231s. The joint +0.823472s envelope contains required work; it is not a saving estimate. +Ten effective-config getters include the schema/state subtree and cannot be +added to it. Completion alone is too small to close the remaining gap. + +The separate V4 diagnostic also passes full science and distinguishes actual +cache topology: compilation has 40 successful first-use misses and 338 hits +across 44 class identities; execution has 94 hits. There are no uncached empty +results. This rejects repeated failing analysis or fresh proxy churn as the +mechanism. Exact runtime class names were not retained because the diagnostic +read a type-owned attribute from the class dictionary; that metadata omission +is explicit. A separate declaration inspection finds 22 registered public/proxy +pairs and 95 differing fields, including Optional annotations and None defaults. +Canonical-class ParameterInfo substitution would change real reset and +reconstruction consumers, so it is rejected. The remaining candidate must +separate reusable declaration work from correctly timed value/default-factory +evaluation using the existing schema and preparation owners. + +Local source-qualified receipts: + +* ``/home/ts/.local/state/openhcs-maintenance/20261003/current-cohort-capture-v7-science-verification-v2.json`` + (SHA-256 ``99f2f830861eb1ecd1492c50ff6bbb2a66172674211e0a925f9f6a85359a1be0``). +* ``/home/ts/.local/state/openhcs-maintenance/20261003/current-cohort-v7-original-replay-v3.json``. +* ``/home/ts/.local/state/openhcs-maintenance/20261003/current-cohort-v7-load-owned-role-review.json`` + (SHA-256 ``bd08002bbb535911ba189b1b97bf8cbceb03c938b89c5ec8d0c061e4743735a7``). +* ``/home/ts/.local/state/openhcs-maintenance/20261003/projection_ancestor_feasibility_bb9c_v3.json`` + (SHA-256 ``b5ff353e33c70d4066de5315c0bef2046e1e8680056125404a7c393eb2e531ae``). +* ``/home/ts/.local/state/openhcs-maintenance/20261003/illumination-compiler-ledger-v3-science.json`` + (SHA-256 ``e042e25aa0fd9e087459c77613150f0db6447e91d0fc943acf2ea638312cc850``). +* ``/home/ts/.local/state/openhcs-maintenance/20261003/illumination-schema-cache-topology-v4-20261003.json`` + (SHA-256 ``134244ea39004ccfc86d20fa14d4fdc49e78aa72f4a92349092c6f420c1b89e3``). + +No production optimization, new native repetition, original R0/R1 waiver, +full-catalog/scaling completion or accepted ordinary speedup is claimed here. From 7258826d7198f2bb7010763276dba02320d62142 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 00:54:05 -0400 Subject: [PATCH 104/235] Warm registered configuration source before execution readiness --- external/ObjectState | 2 +- external/python-introspect | 2 +- .../backends/lib_registry/registry_service.py | 11 +++ tests/unit/test_callable_abi_preparation.py | 69 ++++++++++++++++++- 4 files changed, 81 insertions(+), 3 deletions(-) diff --git a/external/ObjectState b/external/ObjectState index b0004daf32..3831b561ca 160000 --- a/external/ObjectState +++ b/external/ObjectState @@ -1 +1 @@ -Subproject commit b0004daf3223326ac78689ba7b2e68abc18ae298 +Subproject commit 3831b561cafc55a74d97ebde76cf528cf57b055f diff --git a/external/python-introspect b/external/python-introspect index 4e8f8bde5f..d34ad8fd25 160000 --- a/external/python-introspect +++ b/external/python-introspect @@ -1 +1 @@ -Subproject commit 4e8f8bde5f750d9a50eacac80acd3806123addd8 +Subproject commit d34ad8fd251a7fd191847a7b5a920eaded0d98ed diff --git a/openhcs/processing/backends/lib_registry/registry_service.py b/openhcs/processing/backends/lib_registry/registry_service.py index b730021e07..dd327c12a5 100644 --- a/openhcs/processing/backends/lib_registry/registry_service.py +++ b/openhcs/processing/backends/lib_registry/registry_service.py @@ -14,9 +14,12 @@ import threading from collections.abc import Callable from concurrent.futures import CancelledError +from dataclasses import is_dataclass from typing import TYPE_CHECKING, Dict, Optional from arraybridge import MemoryType +from objectstate import LazyDataclassFactory +from python_introspect import SignatureAnalyzer from pyqt_reactive.process_launch import BackgroundProcessLaunchPolicy from zmqruntime import OperationCancellation from zmqruntime.client import endpoint_process @@ -164,6 +167,14 @@ def prepare_in_current_process( emit_status("Resolving prepared callable signatures") for func in callables: CallableProjection.from_callable(func).warm_canonical_signature() + emit_status("Preparing registered configuration source declarations") + for declaration in dict.fromkeys( + declaration + for pair in LazyDataclassFactory.registered_type_pairs() + for declaration in pair + if is_dataclass(declaration) + ): + SignatureAnalyzer.prepare_dataclass_declaration(declaration) emit_status(f"Registered kernels ready ({len(callables)} callables)") emit_status(f"Function catalog ready ({len(cls._metadata_cache)} callables)") return cls._metadata_cache diff --git a/tests/unit/test_callable_abi_preparation.py b/tests/unit/test_callable_abi_preparation.py index dd3f1aa8e9..d7fe5ecddc 100644 --- a/tests/unit/test_callable_abi_preparation.py +++ b/tests/unit/test_callable_abi_preparation.py @@ -3,7 +3,7 @@ import inspect import pickle from types import SimpleNamespace -from dataclasses import replace +from dataclasses import dataclass, field, replace from functools import wraps from unittest.mock import Mock from typing import Any @@ -12,6 +12,8 @@ import numpy as np import pytest from arraybridge import ArrayPayload +import objectstate.lazy_factory as lazy_factory +from python_introspect import SignatureAnalyzer import openhcs.core.callable_contract as contract_module from openhcs.core.callable_contract import ( @@ -165,6 +167,71 @@ def raw(image: np.ndarray): assert second.canonical_parameter_annotations["image"] is ArrayPayload +def test_registry_prepares_current_configuration_declarations_before_ready(monkeypatch): + events = [] + factories = [] + + def default_value(): + factories.append("evaluated") + return 3 + + @dataclass + class PublicConfig: + value: int = field(default_factory=default_value) + + @dataclass + class LazyConfig: + value: int | None = None + + @dataclass + class LaterPublicConfig: + value: int = field(default_factory=default_value) + + @dataclass + class LaterLazyConfig: + value: int | None = None + + def raw(image: np.ndarray): + return image + + metadata = {"raw": SimpleNamespace(func=raw)} + monkeypatch.setattr(RegistryService, "_metadata_cache", metadata) + monkeypatch.setattr(lazy_factory, "_lazy_type_registry", {}) + monkeypatch.setattr(lazy_factory, "_base_to_lazy_registry", {}) + monkeypatch.setattr(PreparationCacheBatch, "populate_child_caches", lambda *a, **k: None) + pairs = iter(((LazyConfig, PublicConfig), (LaterLazyConfig, LaterPublicConfig))) + + def prepare(owner): + events.append("hook") + lazy_factory.register_lazy_type_mapping(*next(pairs)) + + original = SignatureAnalyzer.prepare_dataclass_declaration + + def declaration(config_type): + assert "hook" in events + events.append(config_type) + original(config_type) + + monkeypatch.setattr(CallablePreparation, "prepare", prepare) + monkeypatch.setattr(SignatureAnalyzer, "prepare_dataclass_declaration", declaration) + # The registry admits types that are not dataclasses; readiness filters them. + lazy_factory.register_lazy_type_mapping(dict, list) + assert RegistryService.prepare_in_current_process(status_callback=events.append) is metadata + assert factories == [] + first_ready = next(i for i, item in enumerate(events) if isinstance(item, str) and "kernels ready" in item) + assert events.index(LazyConfig) < first_ready + assert events.index(PublicConfig) < first_ready + assert LaterPublicConfig not in events and dict not in events and list not in events + events.clear() + # An already loaded callable catalog must not freeze the configuration roster. + RegistryService.prepare_in_current_process(status_callback=events.append) + second_ready = next(i for i, item in enumerate(events) if isinstance(item, str) and "kernels ready" in item) + for config_type in (LazyConfig, PublicConfig, LaterLazyConfig, LaterPublicConfig): + assert events.count(config_type) == 1 + assert events.index(config_type) < second_ready + assert factories == [] + + def test_authored_compilation_prepares_before_signature_and_refreshes_each_compilation(monkeypatch): def raw(image: np.ndarray, scale=2): return image From 7070bcef493b546e7ad522e452f36f339dd3eacf Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 01:00:59 -0400 Subject: [PATCH 105/235] Publish ordinary schema-readiness gains and sync merged dependencies --- .../README.rst | 25 ++++ .../recipes/controller.py | 136 ++++++++++++++++++ .../recipes/validate_science.py | 88 ++++++++++++ .../runtime_plumbing_frontier_20261003.rst | 69 ++++++++- external/ObjectState | 2 +- external/python-introspect | 2 +- 6 files changed, 318 insertions(+), 4 deletions(-) create mode 100644 benchmark/results/perf_schema_source_readiness_20261003/README.rst create mode 100644 benchmark/results/perf_schema_source_readiness_20261003/recipes/controller.py create mode 100644 benchmark/results/perf_schema_source_readiness_20261003/recipes/validate_science.py diff --git a/benchmark/results/perf_schema_source_readiness_20261003/README.rst b/benchmark/results/perf_schema_source_readiness_20261003/README.rst new file mode 100644 index 0000000000..17b2e5305c --- /dev/null +++ b/benchmark/results/perf_schema_source_readiness_20261003/README.rst @@ -0,0 +1,25 @@ +Registered source readiness: ordinary Illumination ABBA +==================================================== + +``receipt.json`` contains public observations, the exact variant hashes and full +science-qualification links. The committed recipes preserve the actual local +experiment; they are evidence, not a turnkey portable benchmark command. Their +paths name the owned checkout and immutable maintenance directory, and the +ordinary helper/bootstrap and physical dataset/native references must already +exist. Do not rerun into a completed output directory. + +Only three source files vary. Their baseline bytes are derived from the recorded +Git revisions in ``variants.json``. Candidate registry bytes are available at +OpenHCS commit ``7258826d7198f2bb7010763276dba02320d62142``; candidate dependency +bytes are available at python-introspect merge +``6f5ac0b79d6cef163df6a8389e72a889a91ed548`` and ObjectState merge +``55e71df39a05aa08d00bc1bb06c2be174ad2c522``. The original maintenance directory +retains all six variant files, source freezes and four immutable output trees. +No dependency source, benchmark images or environments are duplicated here. + +Mean compilation is 0.962152 → 0.587605 seconds; total is +1.693765 → 1.363694 seconds, excluding mandatory server READY/startup and +shutdown. Execution has no demonstrated improvement. All four complete authored +image inventories pass exact byte parity and the retained native physical input +qualification; no new native CP timing is claimed. Other workloads and scaling +remain unmeasured for this change. diff --git a/benchmark/results/perf_schema_source_readiness_20261003/recipes/controller.py b/benchmark/results/perf_schema_source_readiness_20261003/recipes/controller.py new file mode 100644 index 0000000000..effe202bcd --- /dev/null +++ b/benchmark/results/perf_schema_source_readiness_20261003/recipes/controller.py @@ -0,0 +1,136 @@ +"""Ordinary public ABBA; only three owned source files vary between terminated runs.""" +import csv +import fcntl +import hashlib +import importlib.util +import json +import os +from pathlib import Path +import shutil +import subprocess +import time + +HERE = Path(__file__).parent +SOURCE = Path('/home/ts/code/projects/openhcs-shared-runtime-plumbing') +MAIN = Path('/home/ts/code/projects/openhcs') +PYTHON = MAIN / '.venv/bin/python' +CASE = 'ExampleIlluminationCorrection_Example3' +VARIANTS = json.loads((HERE / 'variants.json').read_text()) + +def load(name, path): + spec = importlib.util.spec_from_file_location(name, path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + +ordinary = load('ordinary_schema_source', '/tmp/run_integrated_fields_ordinary_abba_20261002.py') +base = load('schema_source_inputs', '/var/tmp/run_narrow450_runtime_phase_probe_20261002.py') +base.SOURCE = SOURCE +base.CASES = (CASE,) +sparse = load('schema_source_sparse', '/var/tmp/openhcs_measurement_sparse_inventory_v2_20261003.py') + +def sha(path): + return hashlib.sha256(Path(path).read_bytes()).hexdigest() + +def git(repo, *args): + return subprocess.check_output(['git', '-C', str(repo), *args], text=True).strip() + +def environment(): + env = ordinary.environment(SOURCE) + for key in tuple(env): + if key.startswith(('OPENHCS_DIAGNOSTIC_', 'OPENHCS_CAPTURE_CELLPROFILER_FIXTURES_', 'OPENHCS_PROFILE_FUNCTION_RUNTIME')) or key == 'OPENHCS_WORKER_PROFILE_DIR': + del env[key] + env.update(PYTHONDONTWRITEBYTECODE='1', TMPDIR=str(HERE / 'tmp')) + return env + +def sources(): + dependencies = {} + for line in git(SOURCE, 'config', '--file', '.gitmodules', '--get-regexp', 'path$').splitlines(): + _, relative = line.split(maxsplit=1) + repo = SOURCE / relative + dependencies[relative] = {'head': git(repo, 'rev-parse', 'HEAD'), 'status': git(repo, 'status', '--porcelain'), 'files': base.tracked_source_hashes(repo)} + return {'head': git(SOURCE, 'rev-parse', 'HEAD'), 'status': git(SOURCE, 'status', '--porcelain'), 'files': base.tracked_source_hashes(SOURCE), 'dependencies': dependencies, + 'native_binaries': {r: sha(SOURCE/r) for r in ordinary.NATIVE}} + +def select(label): + # This checkout is owned, and the source files are not being edited by agents. + # No live server may remain while its source is changed. + for entry in VARIANTS: + target = Path(entry['path']) + assert target.is_relative_to(SOURCE) + current = sha(target) + assert current in {sha(entry['A']), sha(entry['B'])}, 'Unexpected source edit' + target.write_bytes(Path(entry[label]).read_bytes()) + +def assert_endpoint_terminal(receipt): + import psutil + endpoint = receipt['endpoint_provenance'] + pid = endpoint['endpoint_pid'] + try: + process = psutil.Process(pid) + assert abs(process.create_time() - endpoint['endpoint_create_time_epoch_seconds']) > 0.1 or not process.is_running() or process.status() == psutil.STATUS_ZOMBIE, 'Measured endpoint remains live' + except psutil.NoSuchProcess: + pass + +def main(): + assert shutil.disk_usage('/var/tmp').free >= 512 * 1024**2 + assert shutil.disk_usage('/home').free >= 2 * 1024**3 + assert not (HERE / 'freeze.json').exists() + (HERE / 'tmp').mkdir() + env = environment() + original = sources() + assert not original['status'], 'Commit source before measurement' + physical = base.physical_input_hashes() + sparse_state = sparse.sparse_source_inventory(SOURCE) + installed = subprocess.check_output([str(PYTHON), '-I', '-m', 'pip', 'freeze', '--all'], text=True) + imports = subprocess.check_output([str(PYTHON), '-c', 'import openhcs,objectstate,python_introspect,json;print(json.dumps({m.__name__:m.__file__ for m in (openhcs,objectstate,python_introspect)}))'], env=env, cwd=str(HERE/'tmp'), text=True) + for path in json.loads(imports).values(): + assert Path(path).is_relative_to(SOURCE), path + freeze = {'candidate': original, 'physical_inputs': physical, 'sparse_inventory': sparse_state, 'installed_freeze': installed, 'python_sha256': sha(PYTHON.resolve()), 'controller_sha256': sha(__file__), 'environment': env, 'imports': json.loads(imports), + 'variants': [{**r, 'A_sha256': sha(r['A']), 'B_sha256': sha(r['B'])} for r in VARIANTS], + 'scope': 'Four ordinary uninstrumented public Illumination runs ABBA, CPU5 1w_1t INLINE, default OUTCOMES and memory observer. Server READY preparation/startup and shutdown excluded. A is exact prior source bytes for three files; dependency Git HEADs identify containing checkout, file hashes identify actual baseline contents. Only source lookup, existing registered type projection and registry readiness differ. No scientific processing or numerical changes. No new native CP execution.'} + (HERE/'freeze.json').write_text(json.dumps(freeze, indent=2)+'\n') + rows = [] + lock = Path('/tmp/openhcs-benchmark-xdg-cache/openhcs/official30-runtime.lock') + lock.parent.mkdir(parents=True, exist_ok=True) + try: + with lock.open('a+') as lease: + fcntl.flock(lease, fcntl.LOCK_EX | fcntl.LOCK_NB) + for index, label in enumerate(('A', 'B', 'B', 'A')): + select(label) + before = sources() + dest = HERE / f'{index}-{label}' + assert not dest.exists() + command = ['/usr/bin/taskset', '-c', '5', str(PYTHON), '-c', ordinary.BOOTSTRAP, str(SOURCE/'scripts/benchmark_cppipe_well_throughput.py'), '--manifest', str(SOURCE/'benchmark/manifests/official30_portable_axis1.json'), '--mode', '1w_1t', '--case', CASE, '--output-dir', str(dest/'public')] + record = {'index': index, 'label': label, 'source': before, 'command': command} + (HERE/f'{index}-{label}-source.json').write_text(json.dumps(record, indent=2)+'\n') + print(json.dumps({'status': 'RUNNING', 'index': index, 'label': label}), flush=True) + started = time.monotonic() + with (HERE/f'{index}-{label}.log').open('x') as stream: + result = subprocess.run(command, cwd=str(HERE/'tmp'), env=env, stdout=stream, stderr=subprocess.STDOUT, timeout=900) + assert sources() == before, 'Source/dependency/native bytes changed during run' + assert base.physical_input_hashes() == physical + assert subprocess.check_output([str(PYTHON), '-I', '-m', 'pip', 'freeze', '--all'], text=True) == installed + assert sha(__file__) == freeze['controller_sha256'] + measured = list(csv.DictReader((dest/'public/well_throughput.csv').open())) + assert result.returncode == 0 and len(measured) == 1 and measured[0]['status'] == 'success' and measured[0]['successful_wells'] == '1' and measured[0]['execution_route'] == 'ordinary-zmq-outcomes-v1' + receipt_path, = (dest/'public'/CASE/'wells_1/workers_1/ordinary_run_evidence').glob('*/measured_pipeline_receipt.json') + receipt = json.loads(receipt_path.read_text()) + assert receipt['observation_export_scope'] == 'outcomes' and receipt['observed_axis_count'] == receipt['expected_axis_count'] == 1 + assert_endpoint_terminal(receipt) + assert sum(p.stat().st_size for p in dest.rglob('*') if p.is_file()) < 128 * 1024**2, 'Output cap' + rows.append({'label': label, 'directory': str(dest), 'seconds_including_startup_for_audit_only': time.monotonic()-started, 'public_rows': measured, 'receipt': str(receipt_path), 'receipt_sha256': sha(receipt_path), 'source_freeze_sha256': sha(HERE/f'{index}-{label}-source.json')}) + (HERE/'observations.json').write_text(json.dumps({'rows': rows, 'science': 'PENDING', 'scope': freeze['scope']}, indent=2)+'\n') + print(json.dumps({'status': 'RUN_COMPLETE', 'label': label, 'timings': {k: measured[0][k] for k in ('compile_seconds', 'execute_seconds', 'total_seconds')}}), flush=True) + finally: + select('B') + assert sources() == original + sparse.validate_sparse_source_inventory(SOURCE, sparse_state) + from statistics import mean + means = {label: {key: mean(float(row[key]) for r in rows if r['label']==label for row in r['public_rows']) for key in ('compile_seconds', 'execute_seconds', 'total_seconds')} for label in ('A', 'B')} + result = {'means': means, 'saved_seconds': {key: means['A'][key]-means['B'][key] for key in means['A']}, 'rows': rows, 'science': 'PENDING', 'scope': freeze['scope']} + (HERE/'observations.json').write_text(json.dumps(result, indent=2)+'\n') + print(json.dumps({'status': 'ABBA_MEASUREMENT_COMPLETE_SCIENCE_PENDING', 'means': means, 'saved_seconds': result['saved_seconds']}), flush=True) + +if __name__ == '__main__': + main() diff --git a/benchmark/results/perf_schema_source_readiness_20261003/recipes/validate_science.py b/benchmark/results/perf_schema_source_readiness_20261003/recipes/validate_science.py new file mode 100644 index 0000000000..fcbe65f2a6 --- /dev/null +++ b/benchmark/results/perf_schema_source_readiness_20261003/recipes/validate_science.py @@ -0,0 +1,88 @@ +"""Strict complete authored image inventory and retained native input qualification.""" +import openhcs +from pathlib import Path +import hashlib +import json + +from benchmark.adapters.openhcs import _strict_cellprofiler_runtime_equivalence_policy +from benchmark.matched_cellprofiler_batch import _require_compared_output_inventory +from openhcs.core.equivalence.comparison import runtime_image_differences, runtime_table_differences +from openhcs.core.equivalence.outputs import RuntimeOutputSnapshot +from openhcs.core.runtime_exports import RuntimeExportObservation +from openhcs.core.virtual_workspace_metadata import METADATA_CONFIG, VirtualWorkspaceSourceProjectionEntries +from openhcs.core.source_projection import SourceProjectionSet + +HERE = Path(__file__).parent +REFERENCE = HERE.parent/'illumination-compiler-ledger-v4' +REFERENCE_SCIENCE = HERE.parent/'illumination-compiler-ledger-v4-science.json' +NATIVE = Path('/var/tmp/openhcs-current-illumination3-native-timing-science-v1-20261002.json') +CASE = 'ExampleIlluminationCorrection_Example3' + +def sha(path): + return hashlib.sha256(Path(path).read_bytes()).hexdigest() + +def read(directory): + receipt_path, = (directory/'public'/CASE/'wells_1/workers_1/ordinary_run_evidence').glob('*/measured_pipeline_receipt.json') + receipt = json.loads(receipt_path.read_text()) + assert receipt['observation_export_scope'] == 'outcomes' + assert receipt['observed_axis_count'] == receipt['expected_axis_count'] == 1 + assert sha(receipt['observation_export_path']) == receipt['observation_export_sha256'] + assert sha(receipt['results_summary_path']) == receipt['results_summary_sha256'] + root, = map(Path, receipt['output_roots']) + assert root.resolve().is_relative_to(directory.resolve()) + files = frozenset(p for p in root.rglob('*') if p.is_file()) + exports = RuntimeExportObservation.from_output_roots((root,)) + snapshot = RuntimeOutputSnapshot.from_export_observation(exports, source_workspaces=(root,)) + return receipt_path, receipt, root, files, exports, snapshot + +def main(): + assert not (HERE/'science.json').exists() + obs = json.loads((HERE/'observations.json').read_text()) + assert len(obs['rows']) == 4 and [r['label'] for r in obs['rows']] == ['A','B','B','A'] + qualification = json.loads(REFERENCE_SCIENCE.read_text()) + assert qualification['status'] == 'CURRENT_BB9_DIAGNOSTIC_FULL_AUTHORED_IMAGE_INVENTORY_BYTE_NATIVE_INPUT_TRANSITIVE_PASS' + native = json.loads(NATIVE.read_text()) + assert native['status'] == 'PASS_AUTHORED_NONEMPTY_SAVED_IMAGE_SCIENCE_AND_MATCHED_FRESH_NATIVE_TIMING' + for role in ('native_report','source_environment_input_witness','saved_image_science_receipt'): + assert sha(native[role]) == native[role+'_sha256'] + ref = read(REFERENCE) + assert sha(ref[0]) == qualification['current_receipt_sha256'] + assert {str(p.relative_to(ref[2])): sha(p) for p in ref[3] if p.suffix == '.npy'} == qualification['scientific_npy_bytes'] + before = {str(p): sha(p) for p in ref[3]} + policy = _strict_cellprofiler_runtime_equivalence_policy() + results = [] + freeze = json.loads((HERE/'freeze.json').read_text()) + cpp = freeze['physical_inputs']['cppipe_path'] + assert cpp['path'] == native['source_cppipe'] and list(cpp['files'].values()) == [native['source_cppipe_sha256']] + environments = [] + for run in obs['rows']: + actual = read(Path(run['directory'])) + assert sha(actual[0]) == run['receipt_sha256'] + before.update({str(p): sha(p) for p in actual[3]}) + td = runtime_table_differences(ref[5].tables, actual[5].tables, policy) + im = runtime_image_differences(ref[5].images, actual[5].images, policy) + assert not td and not im, (td, im) + assert not actual[5].tables and len(actual[5].images) == len(ref[5].images) == 2 + _require_compared_output_inventory(reference_files=ref[3]-frozenset(METADATA_CONFIG.managed_paths(ref[2])), candidate_files=actual[3], reference_exports=ref[4], candidate_exports=actual[4], reference_snapshot=ref[5], candidate_snapshot=actual[5], candidate_managed_files=frozenset(METADATA_CONFIG.managed_paths(actual[2]))) + images = {str(p.relative_to(actual[2])): sha(p) for p in actual[3] if p.suffix == '.npy'} + assert images == qualification['scientific_npy_bytes'] and len(images) == 2 + workspace = Path(actual[1]['execution_plate_id']) + subdirs = json.loads((workspace/'openhcs_metadata.json').read_text())['subdirectories'] + entries = tuple(entry for s in subdirs.values() for entry in VirtualWorkspaceSourceProjectionEntries.from_subdirectory(s).entries.values()) + projections = SourceProjectionSet(entries) + assert len(projections.plane_projections) == 1 and not projections.artifact_projections + assert all(not p.ref.source_axis_indices for p in projections.plane_projections) + selected = {str(Path(p.ref.backend_address).resolve(strict=True)): sha(p.ref.backend_address) for p in projections.plane_projections} + assert selected == native['physical_source_sha256'] + assert actual[1]['global_config_source_sha256'] == ref[1]['global_config_source_sha256'] + assert actual[1]['endpoint_provenance']['client_openhcs_file'] == str(Path(freeze['imports']['openhcs'])) + environments.append(actual[1]['server_environment']) + results.append({'label': run['label'], 'directory':run['directory'], 'receipt_sha256':sha(actual[0]), 'scientific_npy_bytes':images, 'physical_selected_inputs':selected, 'image_differences':im, 'table_differences':td, 'full_physical_inventory':'PASS'}) + assert all(e == environments[0] for e in environments) + assert all(sha(p) == digest for p,digest in before.items()) + result = {'status':'ALL4_ORDINARY_AUTHORED_IMAGES_EXACT_AND_RETAINED_NATIVE_INPUT_PARITY_PASS', 'runs':results, 'means':obs['means'], 'saved_seconds':obs['saved_seconds'], 'all_before_after_output_sha256':before, 'server_environment':environments[0], 'freeze_sha256':sha(HERE/'freeze.json'), 'reference_science_sha256':sha(REFERENCE_SCIENCE), 'native_receipt_sha256':sha(NATIVE), 'validator_sha256':sha(__file__), 'limits':['Two observations per variant; no execution speedup is claimed.', 'Authored workload exports two images and no tables.', 'Native comparison is retained matched CP4.2.8.1 evidence, not a fresh native run.', 'Ordinary pipeline total excludes mandatory server startup and shutdown.']} + (HERE/'science.json').write_text(json.dumps(result, indent=2)+'\n') + print(json.dumps({'status':result['status'], 'means':obs['means'], 'saved_seconds':obs['saved_seconds'], 'science_sha256':sha(HERE/'science.json')})) + +if __name__ == '__main__': + main() diff --git a/docs/plans/runtime_plumbing_frontier_20261003.rst b/docs/plans/runtime_plumbing_frontier_20261003.rst index a41e346ec9..6476dd2d31 100644 --- a/docs/plans/runtime_plumbing_frontier_20261003.rst +++ b/docs/plans/runtime_plumbing_frontier_20261003.rst @@ -284,5 +284,70 @@ Local source-qualified receipts: * ``/home/ts/.local/state/openhcs-maintenance/20261003/illumination-schema-cache-topology-v4-20261003.json`` (SHA-256 ``134244ea39004ccfc86d20fa14d4fdc49e78aa72f4a92349092c6f420c1b89e3``). -No production optimization, new native repetition, original R0/R1 waiver, -full-catalog/scaling completion or accepted ordinary speedup is claimed here. +The preceding V3/V4 diagnostics are attribution only. They do not establish a +production saving, new native repetition, original R0/R1 waiver or +full-catalog/scaling completion. + +Registered configuration source readiness +----------------------------------------- + +The decisive source-only prototype found repeated Python 3.10--3.12 class +source lookup walking the complete config module. The existing +``SignatureAnalyzer`` now derives bounded module AST and qualified-class source +views from current immutable source contents. Python 3.13 and newer retain the +native locator. Live annotations, factories, mutable field/inherited docs, +loader invalidation and the original completed value-analysis cache remain at +their existing observation points. Earlier class-identity snapshots failed +real mutation controls and were rejected. + +``LazyDataclassFactory.registered_type_pairs()`` projects the existing registry +without a second roster or constructing values. ``RegistryService`` prepares +both public and proxy declarations after callable hooks and before READY, +including declarations registered after the catalog was cached. Default +factories are not evaluated during source preparation. + +At OpenHCS ``7258826d7198f2bb7010763276dba02320d62142``, four ordinary public +Illumination runs in ABBA order measured: + +.. list-table:: Ordinary 1w_1t mean seconds; two observations per variant + :header-rows: 1 + + * - Phase + - Prior source + - Prepared source + * - Compilation + - 0.962152 + - 0.587605 + * - Execution job + - 0.382655 + - 0.400589 + * - Total pipeline + - 1.693765 + - 1.363694 + +Compilation saves 0.374547s (38.9%); total saves 0.330070s (19.5%). No execution +gain is claimed. All four runs use CPU5, INLINE one-worker execution, normal +OUTCOMES and the default memory observer, without hooks or profilers. Mandatory +server startup/preparation and shutdown are outside total pipeline timing. +Exactly three owned source files vary; immutable before/after hashes identify +actual baseline contents separately from containing dependency Git HEADs. +All source, dependencies, native binaries, environment and physical inputs were +checked before/after each run; the clean candidate was restored afterward. + +All four complete authored output inventories retain both NPY artifacts exactly +and match the qualified physical TIFF/pipeline/native evidence. The workload +authors no tables. Native CellProfiler was not rerun in this experiment; broad +scaling or new native speedup claims are not made. Science receipt SHA-256: +``918c36612ec20738ad2d2760da85c9e9be07e81d4cd4bf0f60ec91dfcf906488``. + +The 136 upstream, 25 existing lazy-factory and 119 OpenHCS readiness/catalog/ +startup tests pass. python-introspect PR #6 and ObjectState PR #9 are merged and +formally close their issues #5 and #8. Their merge commits change no measured +source bytes. OpenHCS wiring is pushed in draft #394; a small main-only readiness +change can be isolated without merging the unresolved broad branch. +Reviewable measurement receipt and recipes are under +``benchmark/results/perf_schema_source_readiness_20261003``; original immutable +outputs/freeze remain under the corresponding maintenance ABBA directory. +Runtime plumbing, full-catalog/scaling measurements, fresh native repetitions +and figures remain outstanding. Original whole-branch R0/R1 failures remain +explicit and unwaived. diff --git a/external/ObjectState b/external/ObjectState index 3831b561ca..55e71df39a 160000 --- a/external/ObjectState +++ b/external/ObjectState @@ -1 +1 @@ -Subproject commit 3831b561cafc55a74d97ebde76cf528cf57b055f +Subproject commit 55e71df39a05aa08d00bc1bb06c2be174ad2c522 diff --git a/external/python-introspect b/external/python-introspect index d34ad8fd25..6f5ac0b79d 160000 --- a/external/python-introspect +++ b/external/python-introspect @@ -1 +1 @@ -Subproject commit d34ad8fd251a7fd191847a7b5a920eaded0d98ed +Subproject commit 6f5ac0b79d6cef163df6a8389e72a889a91ed548 From 986f6a917ecfed38f121158201ea902e091d2992 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 01:01:15 -0400 Subject: [PATCH 106/235] Retain source-readiness measurement receipt for review --- .../receipt.json | 261 ++++++++++++++++++ .../recipes/variants.json | 26 ++ 2 files changed, 287 insertions(+) create mode 100644 benchmark/results/perf_schema_source_readiness_20261003/receipt.json create mode 100644 benchmark/results/perf_schema_source_readiness_20261003/recipes/variants.json diff --git a/benchmark/results/perf_schema_source_readiness_20261003/receipt.json b/benchmark/results/perf_schema_source_readiness_20261003/receipt.json new file mode 100644 index 0000000000..817e85e994 --- /dev/null +++ b/benchmark/results/perf_schema_source_readiness_20261003/receipt.json @@ -0,0 +1,261 @@ +{ + "status": "ALL4_ORDINARY_AUTHORED_IMAGES_EXACT_AND_RETAINED_NATIVE_INPUT_PARITY_PASS", + "case": "ExampleIlluminationCorrection_Example3", + "mode": "1w_1t", + "cpu": 5, + "candidate_openhcs_commit": "7258826d7198f2bb7010763276dba02320d62142", + "variants": [ + { + "path": "/home/ts/code/projects/openhcs-shared-runtime-plumbing/openhcs/processing/backends/lib_registry/registry_service.py", + "baseline_revision": "4eaacf17e5de3dba59ec217f133fa6e1821c8641", + "repository": "/home/ts/code/projects/openhcs-shared-runtime-plumbing", + "relative": "openhcs/processing/backends/lib_registry/registry_service.py", + "A": "/home/ts/.local/state/openhcs-maintenance/20261003/schema-source-readiness-ordinary-abba-v1/0-A.source", + "B": "/home/ts/.local/state/openhcs-maintenance/20261003/schema-source-readiness-ordinary-abba-v1/0-B.source", + "A_sha256": "73e0d41f98d7ea5bb3fe076b97b97d9980b621c54f2d23b808550e00bae481d4", + "B_sha256": "d9e6e30b627fae7b01e08872c58ad26ee060919b6b6bd9fd9174223f2ec5a95b" + }, + { + "path": "/home/ts/code/projects/openhcs-shared-runtime-plumbing/external/python-introspect/src/python_introspect/signature_analyzer.py", + "baseline_revision": "13f6ff3b43aaa430f5e771848f08d99faf941158", + "repository": "/home/ts/code/projects/openhcs-shared-runtime-plumbing/external/python-introspect", + "relative": "src/python_introspect/signature_analyzer.py", + "A": "/home/ts/.local/state/openhcs-maintenance/20261003/schema-source-readiness-ordinary-abba-v1/1-A.source", + "B": "/home/ts/.local/state/openhcs-maintenance/20261003/schema-source-readiness-ordinary-abba-v1/1-B.source", + "A_sha256": "51f4729861062ab857153c57d64b01bd7b0f47daa6f18fc4443819fb75c77b45", + "B_sha256": "7e25a0355159c86d361a21a7afc449ec758cbf8a32e9b72d06de66a924f04159" + }, + { + "path": "/home/ts/code/projects/openhcs-shared-runtime-plumbing/external/ObjectState/src/objectstate/lazy_factory.py", + "baseline_revision": "b0004daf3223326ac78689ba7b2e68abc18ae298", + "repository": "/home/ts/code/projects/openhcs-shared-runtime-plumbing/external/ObjectState", + "relative": "src/objectstate/lazy_factory.py", + "A": "/home/ts/.local/state/openhcs-maintenance/20261003/schema-source-readiness-ordinary-abba-v1/2-A.source", + "B": "/home/ts/.local/state/openhcs-maintenance/20261003/schema-source-readiness-ordinary-abba-v1/2-B.source", + "A_sha256": "f81ce722695dbab98fd02658f2015e5cb93c1e1e69547d24249e62d896e2a828", + "B_sha256": "90637bdad398bae1348168e62533d69618ec965341b1b27057292e709c2a1f85" + } + ], + "means": { + "A": { + "compile_seconds": 0.9621515274047852, + "execute_seconds": 0.38265538215637207, + "total_seconds": 1.693764767376706 + }, + "B": { + "compile_seconds": 0.5876049995422363, + "execute_seconds": 0.40058863162994385, + "total_seconds": 1.3636944827158004 + } + }, + "saved_seconds": { + "compile_seconds": 0.37454652786254883, + "execute_seconds": -0.017933249473571777, + "total_seconds": 0.3300702846609056 + }, + "runs": [ + { + "label": "A", + "public_rows": [ + { + "case_name": "ExampleIlluminationCorrection_Example3", + "mode_name": "1w_1t", + "worker_count": "1", + "well_count": "1", + "compile_seconds": "0.9799041748046875", + "prepare_seconds": "0.0", + "execute_seconds": "0.38725757598876953", + "total_seconds": "1.7173671890050173", + "wells_per_second": "2.5822606502835725", + "successful_wells": "1", + "native_single_sample_execution_seconds": "", + "projected_native_execution_seconds": "", + "projected_execution_speedup": "", + "peak_memory_mb": "1011.84765625", + "status": "success", + "memory_limit_mb": "", + "error_message": "", + "execution_route": "ordinary-zmq-outcomes-v1", + "server_lifecycle": "reused-per-sweep", + "run_input_sha256": "a37695e774a71b04c3e2a390df76245c973458380fca25a9aac513dbf5e8be8e" + } + ], + "receipt_sha256": "6805ee89e2872aea6199e959e53b956ef144e2ff82516fbf6ac68042450d6fd2", + "source_freeze_sha256": "d71dcaef08a9eb2ac12a00b5e004aedfcd9b9606d860e6a8bca5da73baa9900b" + }, + { + "label": "B", + "public_rows": [ + { + "case_name": "ExampleIlluminationCorrection_Example3", + "mode_name": "1w_1t", + "worker_count": "1", + "well_count": "1", + "compile_seconds": "0.593247652053833", + "prepare_seconds": "0.0", + "execute_seconds": "0.42087531089782715", + "total_seconds": "1.4185294113121927", + "wells_per_second": "2.3760006208650304", + "successful_wells": "1", + "native_single_sample_execution_seconds": "", + "projected_native_execution_seconds": "", + "projected_execution_speedup": "", + "peak_memory_mb": "1043.54296875", + "status": "success", + "memory_limit_mb": "", + "error_message": "", + "execution_route": "ordinary-zmq-outcomes-v1", + "server_lifecycle": "reused-per-sweep", + "run_input_sha256": "3c135c9fa72e1a8ec100dca796e96c7f6721cccfab82316b3836236b6b4e75b0" + } + ], + "receipt_sha256": "c8f76ce4b6acaa60f5bbfe13c5a5ca924e299d5c8834e249f757dd53adc434c7", + "source_freeze_sha256": "ac0a8d0ad5938927dbddf01b937ef15387a15c6701d87d292ed486fbcbec7870" + }, + { + "label": "B", + "public_rows": [ + { + "case_name": "ExampleIlluminationCorrection_Example3", + "mode_name": "1w_1t", + "worker_count": "1", + "well_count": "1", + "compile_seconds": "0.5819623470306396", + "prepare_seconds": "0.0", + "execute_seconds": "0.38030195236206055", + "total_seconds": "1.3088595541194081", + "wells_per_second": "2.629489524807818", + "successful_wells": "1", + "native_single_sample_execution_seconds": "", + "projected_native_execution_seconds": "", + "projected_execution_speedup": "", + "peak_memory_mb": "1043.19140625", + "status": "success", + "memory_limit_mb": "", + "error_message": "", + "execution_route": "ordinary-zmq-outcomes-v1", + "server_lifecycle": "reused-per-sweep", + "run_input_sha256": "3c135c9fa72e1a8ec100dca796e96c7f6721cccfab82316b3836236b6b4e75b0" + } + ], + "receipt_sha256": "8a7f0ecb3274b47312e8ee1431b4bd75f9aaba713fd76d3e9ec28ab038aa93da", + "source_freeze_sha256": "d6608b629bb37ecb89d30797258d674d816857671e77fcf580bec3d62910a7a2" + }, + { + "label": "A", + "public_rows": [ + { + "case_name": "ExampleIlluminationCorrection_Example3", + "mode_name": "1w_1t", + "worker_count": "1", + "well_count": "1", + "compile_seconds": "0.9443988800048828", + "prepare_seconds": "0.0", + "execute_seconds": "0.3780531883239746", + "total_seconds": "1.6701623457483947", + "wells_per_second": "2.6451304495891326", + "successful_wells": "1", + "native_single_sample_execution_seconds": "", + "projected_native_execution_seconds": "", + "projected_execution_speedup": "", + "peak_memory_mb": "1012.59375", + "status": "success", + "memory_limit_mb": "", + "error_message": "", + "execution_route": "ordinary-zmq-outcomes-v1", + "server_lifecycle": "reused-per-sweep", + "run_input_sha256": "a37695e774a71b04c3e2a390df76245c973458380fca25a9aac513dbf5e8be8e" + } + ], + "receipt_sha256": "4a804982debefcd6cd075ac16520e85036281b3eddd78cabaf86f41f2841b4c6", + "source_freeze_sha256": "6b40a27bf359bb67febc0af967221829a7719cac0d965704609c0d36991f608e" + } + ], + "tests": { + "python_introspect": 136, + "objectstate_lazy_factory": 25, + "openhcs_readiness": 119 + }, + "upstream_prs": [ + "https://github.com/OpenHCSDev/python-introspect/pull/6", + "https://github.com/OpenHCSDev/ObjectState/pull/9" + ], + "science_runs": [ + { + "label": "A", + "directory": "/home/ts/.local/state/openhcs-maintenance/20261003/schema-source-readiness-ordinary-abba-v1/0-A", + "receipt_sha256": "6805ee89e2872aea6199e959e53b956ef144e2ff82516fbf6ac68042450d6fd2", + "scientific_npy_bytes": { + "results/reference_image__PolynomialCorrected.npy": "8875da63a072c8f2de4f5cb707d779a064b663bf69aa0f304fe6fa74d4ff57a6", + "results/reference_image__ConvexHullCorrWorm.npy": "9ba1259e4ca007c06f14be6946e32a86307ec2dfb9952b73fa2cc253ccae8b3a" + }, + "physical_selected_inputs": { + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/ADSAStaphInfection2_A01_w2247376DD-6ADD-442D-AE47-F54A05F3EA94.tif": "5a57b7557bb0da5e241b48851b82d936c7ec36ede7aaca5e77b28ac4bfebf502" + }, + "image_differences": [], + "table_differences": [], + "full_physical_inventory": "PASS" + }, + { + "label": "B", + "directory": "/home/ts/.local/state/openhcs-maintenance/20261003/schema-source-readiness-ordinary-abba-v1/1-B", + "receipt_sha256": "c8f76ce4b6acaa60f5bbfe13c5a5ca924e299d5c8834e249f757dd53adc434c7", + "scientific_npy_bytes": { + "results/reference_image__ConvexHullCorrWorm.npy": "9ba1259e4ca007c06f14be6946e32a86307ec2dfb9952b73fa2cc253ccae8b3a", + "results/reference_image__PolynomialCorrected.npy": "8875da63a072c8f2de4f5cb707d779a064b663bf69aa0f304fe6fa74d4ff57a6" + }, + "physical_selected_inputs": { + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/ADSAStaphInfection2_A01_w2247376DD-6ADD-442D-AE47-F54A05F3EA94.tif": "5a57b7557bb0da5e241b48851b82d936c7ec36ede7aaca5e77b28ac4bfebf502" + }, + "image_differences": [], + "table_differences": [], + "full_physical_inventory": "PASS" + }, + { + "label": "B", + "directory": "/home/ts/.local/state/openhcs-maintenance/20261003/schema-source-readiness-ordinary-abba-v1/2-B", + "receipt_sha256": "8a7f0ecb3274b47312e8ee1431b4bd75f9aaba713fd76d3e9ec28ab038aa93da", + "scientific_npy_bytes": { + "results/reference_image__PolynomialCorrected.npy": "8875da63a072c8f2de4f5cb707d779a064b663bf69aa0f304fe6fa74d4ff57a6", + "results/reference_image__ConvexHullCorrWorm.npy": "9ba1259e4ca007c06f14be6946e32a86307ec2dfb9952b73fa2cc253ccae8b3a" + }, + "physical_selected_inputs": { + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/ADSAStaphInfection2_A01_w2247376DD-6ADD-442D-AE47-F54A05F3EA94.tif": "5a57b7557bb0da5e241b48851b82d936c7ec36ede7aaca5e77b28ac4bfebf502" + }, + "image_differences": [], + "table_differences": [], + "full_physical_inventory": "PASS" + }, + { + "label": "A", + "directory": "/home/ts/.local/state/openhcs-maintenance/20261003/schema-source-readiness-ordinary-abba-v1/3-A", + "receipt_sha256": "4a804982debefcd6cd075ac16520e85036281b3eddd78cabaf86f41f2841b4c6", + "scientific_npy_bytes": { + "results/reference_image__ConvexHullCorrWorm.npy": "9ba1259e4ca007c06f14be6946e32a86307ec2dfb9952b73fa2cc253ccae8b3a", + "results/reference_image__PolynomialCorrected.npy": "8875da63a072c8f2de4f5cb707d779a064b663bf69aa0f304fe6fa74d4ff57a6" + }, + "physical_selected_inputs": { + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/ADSAStaphInfection2_A01_w2247376DD-6ADD-442D-AE47-F54A05F3EA94.tif": "5a57b7557bb0da5e241b48851b82d936c7ec36ede7aaca5e77b28ac4bfebf502" + }, + "image_differences": [], + "table_differences": [], + "full_physical_inventory": "PASS" + } + ], + "scope": "Four ordinary uninstrumented public Illumination runs ABBA, CPU5 1w_1t INLINE, default OUTCOMES and memory observer. Server READY preparation/startup and shutdown excluded. A is exact prior source bytes for three files; dependency Git HEADs identify containing checkout, file hashes identify actual baseline contents. Only source lookup, existing registered type projection and registry readiness differ. No scientific processing or numerical changes. No new native CP execution.", + "limits": [ + "Two observations per variant; no execution speedup is claimed.", + "Authored workload exports two images and no tables.", + "Native comparison is retained matched CP4.2.8.1 evidence, not a fresh native run.", + "Ordinary pipeline total excludes mandatory server startup and shutdown." + ], + "artifact_hashes": { + "freeze.json": "2c5d9272f1693e1da821ad45c6db3190cd046f12f446142f9c263611f8a81da8", + "observations.json": "aa72dc885608831f3bbc21efc62f2739d86ec74d64f5ae56d0ee27cd129792fd", + "science.json": "918c36612ec20738ad2d2760da85c9e9be07e81d4cd4bf0f60ec91dfcf906488", + "controller.py": "6fcf1cb1008ac32ee3ceaff8fd61f6db1fdba87544e1ffee1e4b3d484ce30b90", + "validate_science.py": "86fd34f0ca552309a453c5c169a97b949304aa37fb642a66972f4d12f9940684" + }, + "authoritative_artifacts": "/home/ts/.local/state/openhcs-maintenance/20261003/schema-source-readiness-ordinary-abba-v1", + "remaining_work": "No execution speedup claimed. Runtime plumbing, other workloads, fresh native/scaling sweeps and figures remain outstanding." +} diff --git a/benchmark/results/perf_schema_source_readiness_20261003/recipes/variants.json b/benchmark/results/perf_schema_source_readiness_20261003/recipes/variants.json new file mode 100644 index 0000000000..3cbd1f0ed4 --- /dev/null +++ b/benchmark/results/perf_schema_source_readiness_20261003/recipes/variants.json @@ -0,0 +1,26 @@ +[ + { + "path": "/home/ts/code/projects/openhcs-shared-runtime-plumbing/openhcs/processing/backends/lib_registry/registry_service.py", + "baseline_revision": "4eaacf17e5de3dba59ec217f133fa6e1821c8641", + "repository": "/home/ts/code/projects/openhcs-shared-runtime-plumbing", + "relative": "openhcs/processing/backends/lib_registry/registry_service.py", + "A": "/home/ts/.local/state/openhcs-maintenance/20261003/schema-source-readiness-ordinary-abba-v1/0-A.source", + "B": "/home/ts/.local/state/openhcs-maintenance/20261003/schema-source-readiness-ordinary-abba-v1/0-B.source" + }, + { + "path": "/home/ts/code/projects/openhcs-shared-runtime-plumbing/external/python-introspect/src/python_introspect/signature_analyzer.py", + "baseline_revision": "13f6ff3b43aaa430f5e771848f08d99faf941158", + "repository": "/home/ts/code/projects/openhcs-shared-runtime-plumbing/external/python-introspect", + "relative": "src/python_introspect/signature_analyzer.py", + "A": "/home/ts/.local/state/openhcs-maintenance/20261003/schema-source-readiness-ordinary-abba-v1/1-A.source", + "B": "/home/ts/.local/state/openhcs-maintenance/20261003/schema-source-readiness-ordinary-abba-v1/1-B.source" + }, + { + "path": "/home/ts/code/projects/openhcs-shared-runtime-plumbing/external/ObjectState/src/objectstate/lazy_factory.py", + "baseline_revision": "b0004daf3223326ac78689ba7b2e68abc18ae298", + "repository": "/home/ts/code/projects/openhcs-shared-runtime-plumbing/external/ObjectState", + "relative": "src/objectstate/lazy_factory.py", + "A": "/home/ts/.local/state/openhcs-maintenance/20261003/schema-source-readiness-ordinary-abba-v1/2-A.source", + "B": "/home/ts/.local/state/openhcs-maintenance/20261003/schema-source-readiness-ordinary-abba-v1/2-B.source" + } +] From 8e06a382e2d4b6d9d9b8ceb93c229717c328d538 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 01:03:41 -0400 Subject: [PATCH 107/235] Sync ObjectState readiness annotation correction from main --- external/ObjectState | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/ObjectState b/external/ObjectState index 55e71df39a..9c31e4b07b 160000 --- a/external/ObjectState +++ b/external/ObjectState @@ -1 +1 @@ -Subproject commit 55e71df39a05aa08d00bc1bb06c2be174ad2c522 +Subproject commit 9c31e4b07b5537c0ed2307c5a63034c708b86bbb From ed4d8cf7b2fb21b3bd16c68b68b453e1da198936 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 01:21:01 -0400 Subject: [PATCH 108/235] Require published configuration source readiness APIs Raise installed dependency floors for current registered declaration and pure source preparation APIs; preserve existing breaking-series bounds. Refs #517 --- external/ObjectState | 2 +- external/python-introspect | 2 +- pyproject.toml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/external/ObjectState b/external/ObjectState index 9c31e4b07b..90951d09dc 160000 --- a/external/ObjectState +++ b/external/ObjectState @@ -1 +1 @@ -Subproject commit 9c31e4b07b5537c0ed2307c5a63034c708b86bbb +Subproject commit 90951d09dcbd0633065aa44dc54405e2f13cf2a3 diff --git a/external/python-introspect b/external/python-introspect index 6f5ac0b79d..117c2452b6 160000 --- a/external/python-introspect +++ b/external/python-introspect @@ -1 +1 @@ -Subproject commit 6f5ac0b79d6cef163df6a8389e72a889a91ed548 +Subproject commit 117c2452b679962e0d243a07ebb547dc6028cb7f diff --git a/pyproject.toml b/pyproject.toml index ff8fd15e1f..744446d885 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -101,8 +101,8 @@ dependencies = [ # External dependencies (PyPI versions for production builds) "zmqruntime>=0.3.0,<0.4", "pycodify>=0.1.3,<0.2", - "objectstate>=1.1.8,<2", - "python-introspect>=0.1.14,<0.2", + "objectstate>=1.1.9,<2", + "python-introspect>=0.1.15,<0.2", "metaclass-registry>=0.2.2,<0.3", "arraybridge>=0.3.6,<0.4", "polystore>=0.3.0,<0.4", From b312148855b6ed486442d15e9395a09e453d77c6 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 01:29:48 -0400 Subject: [PATCH 109/235] Derive provenance fingerprints from canonical metadata fields --- .../runtime_plumbing_frontier_20261003.rst | 34 +++++++++++++ openhcs/core/source_metadata.py | 4 +- tests/unit/test_source_metadata_owner.py | 50 +++++++++++++++++++ 3 files changed, 86 insertions(+), 2 deletions(-) diff --git a/docs/plans/runtime_plumbing_frontier_20261003.rst b/docs/plans/runtime_plumbing_frontier_20261003.rst index 6476dd2d31..3cbe1622bd 100644 --- a/docs/plans/runtime_plumbing_frontier_20261003.rst +++ b/docs/plans/runtime_plumbing_frontier_20261003.rst @@ -351,3 +351,37 @@ outputs/freeze remain under the corresponding maintenance ABBA directory. Runtime plumbing, full-catalog/scaling measurements, fresh native repetitions and figures remain outstanding. Original whole-branch R0/R1 failures remain explicit and unwaived. + +Canonical provenance identity integration +------------------------------------------ + +Main readiness #514 is merged into this branch. The overlapping readiness +regression was removed from the callable ABI suite; the dedicated main test +remains authoritative. Callable canonical-signature preparation still precedes +registered declaration preparation and kernel readiness. + +The existing ``SourceMetadataFields.provenance_identity_items`` now derives its +representation from ``identity_items``. Equal nested field mappings have equal +provenance fingerprints independent of insertion order. Raw wire order, real +value differences, ordered planes and lifetime-selected views remain intact. +Existing birth fingerprints are preserved by pickle/cloudpickle; they are not +recomputed from subsequently mutated public fields. Historical nested-mapping +fingerprints can therefore differ from newly canonical fingerprints. A fresh +same-source capture is required before candidate fingerprint replay. + +The integrated metadata/transport/readiness controls pass 86 tests in 2.40s. +The separate exact PR #494 source qualification passes its unchanged 31 writer +controls plus seven identity controls. Those writer controls cannot collect on +this branch because its independent ``PointROIOutput`` producer is absent; no +installed, native or live viewer acceptance is inferred. The integrated receipt +is ``~/.local/state/openhcs-maintenance/20261003/pr494-identity-owner-scratch/ +integrated-owned-qualification-v1.json``. + +V8's original producer and downstream loader replay now pass complete value, +metadata, manifest and array-alias gates, and the full six-CSV/120-TIFF science +gate passes. Serialization still drops nested metadata views and source-context +derived caches. These scoped clocks are not complete production-warm performance +evidence. The saved downstream cache hit constructs no new provenance objects; +it does not establish the payoff of a broader loading/recording/publication fix. +The next capture must retain actual nested caches in the same graph, under the +existing admission bounds, without rewarming or inventing state. diff --git a/openhcs/core/source_metadata.py b/openhcs/core/source_metadata.py index c8ab6df14b..06fc651d4a 100644 --- a/openhcs/core/source_metadata.py +++ b/openhcs/core/source_metadata.py @@ -205,11 +205,11 @@ def project() -> SourceMetadataIdentityItems: def provenance_identity_items( cls, metadata: SourceMetadataMapping ) -> tuple[tuple[str, str], ...]: - """Preserve the exact stored repr, including nested mapping insertion order.""" + """Derive stable provenance identity from the owner's canonical field view.""" return cls._view( metadata, "provenance_identity_items", - lambda: tuple(sorted((str(k), repr(v)) for k, v in metadata.items())), + lambda: tuple((key, repr(value)) for key, value in cls.identity_items(metadata)), ) @classmethod diff --git a/tests/unit/test_source_metadata_owner.py b/tests/unit/test_source_metadata_owner.py index 4a492b864d..f204c09c61 100644 --- a/tests/unit/test_source_metadata_owner.py +++ b/tests/unit/test_source_metadata_owner.py @@ -24,6 +24,8 @@ from openhcs.core.source_image_provenance import ( SourceImageIdentity, SourceImageProvenance, + SourceImageProvenancePlanes, + SourceImageProvenance, SourceImageProvenancePlaneRecord, SourcePlaneIndexedMetadata, source_component_metadata_consensus, @@ -271,6 +273,54 @@ def __getattribute__(self, name): assert left != right and left.identity == captured +def test_nested_mapping_order_does_not_change_provenance_identity_or_wire_order(): + raw = {"well": "A01", ORIGINAL_SOURCE_METADATA_FIELD: {"site": "001", "channel": "1"}} + reordered = {ORIGINAL_SOURCE_METADATA_FIELD: {"channel": "1", "site": "001"}, "well": "A01"} + owned = DurableSourceMetadata.from_mapping(reordered) + before = to_jsonable(owned) + original = SourceImageProvenance(source_path="source.tif", source_component_metadata=raw) + for metadata in (reordered, owned): + candidate = SourceImageProvenance(source_path="source.tif", source_component_metadata=metadata) + assert candidate == original + assert candidate.equality_identity == original.equality_identity + assert SourceMetadataFields.provenance_identity_items(raw) == SourceMetadataFields.provenance_identity_items(owned) + assert to_jsonable(owned) == before + assert tuple(reordered) == (ORIGINAL_SOURCE_METADATA_FIELD, "well") + assert tuple(reordered[ORIGINAL_SOURCE_METADATA_FIELD]) == ("channel", "site") + reordered[ORIGINAL_SOURCE_METADATA_FIELD]["site"] = "002" + changed = SourceImageProvenance(source_path="source.tif", source_component_metadata=reordered) + assert changed.equality_identity != original.equality_identity + + +def test_canonical_metadata_identity_preserves_ordered_provenance_planes(): + paths = ("a.tif", "b.tif") + metadata = ({"site": "1"}, {"site": "2"}) + forward = SourceImageProvenancePlanes.from_components(paths=paths, component_metadata=metadata) + reverse = SourceImageProvenancePlanes.from_components( + paths=tuple(reversed(paths)), component_metadata=tuple(reversed(metadata)), + ) + assert SourceImageProvenance(source_image_provenance_planes=forward).equality_identity != SourceImageProvenance( + source_image_provenance_planes=reverse, + ).equality_identity + + +@pytest.mark.parametrize("serializer", (pickle, cloudpickle)) +def test_identity_transport_preserves_birth_fingerprint_after_current_metadata_changes(serializer): + from openhcs.core.orchestrator.execution_result import RuntimeExecutionTransportSerialization + + RuntimeExecutionTransportSerialization.register() + nested = {"site": "001", "channel": "1"} + identity = SourceImageIdentity("original.tif", {ORIGINAL_SOURCE_METADATA_FIELD: nested}) + birth = identity.identity + nested["site"] = "002" + identity.path = "current.tif" + restored = serializer.loads(serializer.dumps(identity)) + assert restored.path == "current.tif" + assert restored.component_metadata[ORIGINAL_SOURCE_METADATA_FIELD]["site"] == "002" + assert restored.identity == birth + assert SourceImageIdentity(restored.path, restored.component_metadata).identity != birth + + def test_raw_composition_snapshots_once_before_consensus_reads(): class ObservableMapping(Mapping): def __init__(self, values): From 675f7eddb4aff51f7e4fb07352e44e8e667633b0 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 01:42:27 -0400 Subject: [PATCH 110/235] Remove redundant local readiness recipes; retain immutable evidence --- .../README.rst | 40 +++++- .../recipes/controller.py | 136 ------------------ .../recipes/validate_science.py | 88 ------------ .../recipes/variants.json | 26 ---- .../runtime_plumbing_frontier_20261003.rst | 6 +- 5 files changed, 38 insertions(+), 258 deletions(-) delete mode 100644 benchmark/results/perf_schema_source_readiness_20261003/recipes/controller.py delete mode 100644 benchmark/results/perf_schema_source_readiness_20261003/recipes/validate_science.py delete mode 100644 benchmark/results/perf_schema_source_readiness_20261003/recipes/variants.json diff --git a/benchmark/results/perf_schema_source_readiness_20261003/README.rst b/benchmark/results/perf_schema_source_readiness_20261003/README.rst index 17b2e5305c..e450fc5dc8 100644 --- a/benchmark/results/perf_schema_source_readiness_20261003/README.rst +++ b/benchmark/results/perf_schema_source_readiness_20261003/README.rst @@ -2,14 +2,16 @@ Registered source readiness: ordinary Illumination ABBA ==================================================== ``receipt.json`` contains public observations, the exact variant hashes and full -science-qualification links. The committed recipes preserve the actual local -experiment; they are evidence, not a turnkey portable benchmark command. Their -paths name the owned checkout and immutable maintenance directory, and the -ordinary helper/bootstrap and physical dataset/native references must already -exist. Do not rerun into a completed output directory. +science-qualification links. The original local experiment scripts remain in +Git history and the maintenance directory named below. Their redundant tracked +copies were removed because they require absolute local helper, source-variant, +freeze and physical dataset/native paths. They are historical evidence, not +turnkey portable benchmark commands. The public benchmark driver and strict +scientific comparison authorities remain unchanged. Do not rerun into a +completed output directory. Only three source files vary. Their baseline bytes are derived from the recorded -Git revisions in ``variants.json``. Candidate registry bytes are available at +Git revisions in ``receipt.json``. Candidate registry bytes are available at OpenHCS commit ``7258826d7198f2bb7010763276dba02320d62142``; candidate dependency bytes are available at python-introspect merge ``6f5ac0b79d6cef163df6a8389e72a889a91ed548`` and ObjectState merge @@ -17,6 +19,32 @@ bytes are available at python-introspect merge retains all six variant files, source freezes and four immutable output trees. No dependency source, benchmark images or environments are duplicated here. +Immutable recipe custody +----------------------- + +The byte-identical originals remain under +``/home/ts/.local/state/openhcs-maintenance/20261003/schema-source-readiness-ordinary-abba-v1``. +All six referenced A/B source files there match the hashes in ``receipt.json``; +the source freezes and four completed output trees remain intact. The following +links identify the removed copies at immutable commit +``986f6a917ecfed38f121158201ea902e091d2992``: + +* `controller.py `_ + (maintenance basename ``controller.py``). + SHA-256: ``6fcf1cb1008ac32ee3ceaff8fd61f6db1fdba87544e1ffee1e4b3d484ce30b90``. + Git blob: ``effe202bcd36777cc6af76bd66376a15e8a0ed24``. +* `validate_science.py `_ + (maintenance basename ``validate_science.py``). + SHA-256: ``86fd34f0ca552309a453c5c169a97b949304aa37fb642a66972f4d12f9940684``. + Git blob: ``fcbe65f2a69c22cba0c057adc67d18d53a0da45b``. +* `variants.json `_ + (maintenance basename ``variants.json``). + SHA-256: ``d65bf31aacafae16045118722bd68b0cc55c454f9ebd50df7713ade3a7858fcf``. + Git blob: ``3cbd1f0ed47ac2840e0297d381c1ea7f93bff8ff``. + +Measured scope +-------------- + Mean compilation is 0.962152 → 0.587605 seconds; total is 1.693765 → 1.363694 seconds, excluding mandatory server READY/startup and shutdown. Execution has no demonstrated improvement. All four complete authored diff --git a/benchmark/results/perf_schema_source_readiness_20261003/recipes/controller.py b/benchmark/results/perf_schema_source_readiness_20261003/recipes/controller.py deleted file mode 100644 index effe202bcd..0000000000 --- a/benchmark/results/perf_schema_source_readiness_20261003/recipes/controller.py +++ /dev/null @@ -1,136 +0,0 @@ -"""Ordinary public ABBA; only three owned source files vary between terminated runs.""" -import csv -import fcntl -import hashlib -import importlib.util -import json -import os -from pathlib import Path -import shutil -import subprocess -import time - -HERE = Path(__file__).parent -SOURCE = Path('/home/ts/code/projects/openhcs-shared-runtime-plumbing') -MAIN = Path('/home/ts/code/projects/openhcs') -PYTHON = MAIN / '.venv/bin/python' -CASE = 'ExampleIlluminationCorrection_Example3' -VARIANTS = json.loads((HERE / 'variants.json').read_text()) - -def load(name, path): - spec = importlib.util.spec_from_file_location(name, path) - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - return module - -ordinary = load('ordinary_schema_source', '/tmp/run_integrated_fields_ordinary_abba_20261002.py') -base = load('schema_source_inputs', '/var/tmp/run_narrow450_runtime_phase_probe_20261002.py') -base.SOURCE = SOURCE -base.CASES = (CASE,) -sparse = load('schema_source_sparse', '/var/tmp/openhcs_measurement_sparse_inventory_v2_20261003.py') - -def sha(path): - return hashlib.sha256(Path(path).read_bytes()).hexdigest() - -def git(repo, *args): - return subprocess.check_output(['git', '-C', str(repo), *args], text=True).strip() - -def environment(): - env = ordinary.environment(SOURCE) - for key in tuple(env): - if key.startswith(('OPENHCS_DIAGNOSTIC_', 'OPENHCS_CAPTURE_CELLPROFILER_FIXTURES_', 'OPENHCS_PROFILE_FUNCTION_RUNTIME')) or key == 'OPENHCS_WORKER_PROFILE_DIR': - del env[key] - env.update(PYTHONDONTWRITEBYTECODE='1', TMPDIR=str(HERE / 'tmp')) - return env - -def sources(): - dependencies = {} - for line in git(SOURCE, 'config', '--file', '.gitmodules', '--get-regexp', 'path$').splitlines(): - _, relative = line.split(maxsplit=1) - repo = SOURCE / relative - dependencies[relative] = {'head': git(repo, 'rev-parse', 'HEAD'), 'status': git(repo, 'status', '--porcelain'), 'files': base.tracked_source_hashes(repo)} - return {'head': git(SOURCE, 'rev-parse', 'HEAD'), 'status': git(SOURCE, 'status', '--porcelain'), 'files': base.tracked_source_hashes(SOURCE), 'dependencies': dependencies, - 'native_binaries': {r: sha(SOURCE/r) for r in ordinary.NATIVE}} - -def select(label): - # This checkout is owned, and the source files are not being edited by agents. - # No live server may remain while its source is changed. - for entry in VARIANTS: - target = Path(entry['path']) - assert target.is_relative_to(SOURCE) - current = sha(target) - assert current in {sha(entry['A']), sha(entry['B'])}, 'Unexpected source edit' - target.write_bytes(Path(entry[label]).read_bytes()) - -def assert_endpoint_terminal(receipt): - import psutil - endpoint = receipt['endpoint_provenance'] - pid = endpoint['endpoint_pid'] - try: - process = psutil.Process(pid) - assert abs(process.create_time() - endpoint['endpoint_create_time_epoch_seconds']) > 0.1 or not process.is_running() or process.status() == psutil.STATUS_ZOMBIE, 'Measured endpoint remains live' - except psutil.NoSuchProcess: - pass - -def main(): - assert shutil.disk_usage('/var/tmp').free >= 512 * 1024**2 - assert shutil.disk_usage('/home').free >= 2 * 1024**3 - assert not (HERE / 'freeze.json').exists() - (HERE / 'tmp').mkdir() - env = environment() - original = sources() - assert not original['status'], 'Commit source before measurement' - physical = base.physical_input_hashes() - sparse_state = sparse.sparse_source_inventory(SOURCE) - installed = subprocess.check_output([str(PYTHON), '-I', '-m', 'pip', 'freeze', '--all'], text=True) - imports = subprocess.check_output([str(PYTHON), '-c', 'import openhcs,objectstate,python_introspect,json;print(json.dumps({m.__name__:m.__file__ for m in (openhcs,objectstate,python_introspect)}))'], env=env, cwd=str(HERE/'tmp'), text=True) - for path in json.loads(imports).values(): - assert Path(path).is_relative_to(SOURCE), path - freeze = {'candidate': original, 'physical_inputs': physical, 'sparse_inventory': sparse_state, 'installed_freeze': installed, 'python_sha256': sha(PYTHON.resolve()), 'controller_sha256': sha(__file__), 'environment': env, 'imports': json.loads(imports), - 'variants': [{**r, 'A_sha256': sha(r['A']), 'B_sha256': sha(r['B'])} for r in VARIANTS], - 'scope': 'Four ordinary uninstrumented public Illumination runs ABBA, CPU5 1w_1t INLINE, default OUTCOMES and memory observer. Server READY preparation/startup and shutdown excluded. A is exact prior source bytes for three files; dependency Git HEADs identify containing checkout, file hashes identify actual baseline contents. Only source lookup, existing registered type projection and registry readiness differ. No scientific processing or numerical changes. No new native CP execution.'} - (HERE/'freeze.json').write_text(json.dumps(freeze, indent=2)+'\n') - rows = [] - lock = Path('/tmp/openhcs-benchmark-xdg-cache/openhcs/official30-runtime.lock') - lock.parent.mkdir(parents=True, exist_ok=True) - try: - with lock.open('a+') as lease: - fcntl.flock(lease, fcntl.LOCK_EX | fcntl.LOCK_NB) - for index, label in enumerate(('A', 'B', 'B', 'A')): - select(label) - before = sources() - dest = HERE / f'{index}-{label}' - assert not dest.exists() - command = ['/usr/bin/taskset', '-c', '5', str(PYTHON), '-c', ordinary.BOOTSTRAP, str(SOURCE/'scripts/benchmark_cppipe_well_throughput.py'), '--manifest', str(SOURCE/'benchmark/manifests/official30_portable_axis1.json'), '--mode', '1w_1t', '--case', CASE, '--output-dir', str(dest/'public')] - record = {'index': index, 'label': label, 'source': before, 'command': command} - (HERE/f'{index}-{label}-source.json').write_text(json.dumps(record, indent=2)+'\n') - print(json.dumps({'status': 'RUNNING', 'index': index, 'label': label}), flush=True) - started = time.monotonic() - with (HERE/f'{index}-{label}.log').open('x') as stream: - result = subprocess.run(command, cwd=str(HERE/'tmp'), env=env, stdout=stream, stderr=subprocess.STDOUT, timeout=900) - assert sources() == before, 'Source/dependency/native bytes changed during run' - assert base.physical_input_hashes() == physical - assert subprocess.check_output([str(PYTHON), '-I', '-m', 'pip', 'freeze', '--all'], text=True) == installed - assert sha(__file__) == freeze['controller_sha256'] - measured = list(csv.DictReader((dest/'public/well_throughput.csv').open())) - assert result.returncode == 0 and len(measured) == 1 and measured[0]['status'] == 'success' and measured[0]['successful_wells'] == '1' and measured[0]['execution_route'] == 'ordinary-zmq-outcomes-v1' - receipt_path, = (dest/'public'/CASE/'wells_1/workers_1/ordinary_run_evidence').glob('*/measured_pipeline_receipt.json') - receipt = json.loads(receipt_path.read_text()) - assert receipt['observation_export_scope'] == 'outcomes' and receipt['observed_axis_count'] == receipt['expected_axis_count'] == 1 - assert_endpoint_terminal(receipt) - assert sum(p.stat().st_size for p in dest.rglob('*') if p.is_file()) < 128 * 1024**2, 'Output cap' - rows.append({'label': label, 'directory': str(dest), 'seconds_including_startup_for_audit_only': time.monotonic()-started, 'public_rows': measured, 'receipt': str(receipt_path), 'receipt_sha256': sha(receipt_path), 'source_freeze_sha256': sha(HERE/f'{index}-{label}-source.json')}) - (HERE/'observations.json').write_text(json.dumps({'rows': rows, 'science': 'PENDING', 'scope': freeze['scope']}, indent=2)+'\n') - print(json.dumps({'status': 'RUN_COMPLETE', 'label': label, 'timings': {k: measured[0][k] for k in ('compile_seconds', 'execute_seconds', 'total_seconds')}}), flush=True) - finally: - select('B') - assert sources() == original - sparse.validate_sparse_source_inventory(SOURCE, sparse_state) - from statistics import mean - means = {label: {key: mean(float(row[key]) for r in rows if r['label']==label for row in r['public_rows']) for key in ('compile_seconds', 'execute_seconds', 'total_seconds')} for label in ('A', 'B')} - result = {'means': means, 'saved_seconds': {key: means['A'][key]-means['B'][key] for key in means['A']}, 'rows': rows, 'science': 'PENDING', 'scope': freeze['scope']} - (HERE/'observations.json').write_text(json.dumps(result, indent=2)+'\n') - print(json.dumps({'status': 'ABBA_MEASUREMENT_COMPLETE_SCIENCE_PENDING', 'means': means, 'saved_seconds': result['saved_seconds']}), flush=True) - -if __name__ == '__main__': - main() diff --git a/benchmark/results/perf_schema_source_readiness_20261003/recipes/validate_science.py b/benchmark/results/perf_schema_source_readiness_20261003/recipes/validate_science.py deleted file mode 100644 index fcbe65f2a6..0000000000 --- a/benchmark/results/perf_schema_source_readiness_20261003/recipes/validate_science.py +++ /dev/null @@ -1,88 +0,0 @@ -"""Strict complete authored image inventory and retained native input qualification.""" -import openhcs -from pathlib import Path -import hashlib -import json - -from benchmark.adapters.openhcs import _strict_cellprofiler_runtime_equivalence_policy -from benchmark.matched_cellprofiler_batch import _require_compared_output_inventory -from openhcs.core.equivalence.comparison import runtime_image_differences, runtime_table_differences -from openhcs.core.equivalence.outputs import RuntimeOutputSnapshot -from openhcs.core.runtime_exports import RuntimeExportObservation -from openhcs.core.virtual_workspace_metadata import METADATA_CONFIG, VirtualWorkspaceSourceProjectionEntries -from openhcs.core.source_projection import SourceProjectionSet - -HERE = Path(__file__).parent -REFERENCE = HERE.parent/'illumination-compiler-ledger-v4' -REFERENCE_SCIENCE = HERE.parent/'illumination-compiler-ledger-v4-science.json' -NATIVE = Path('/var/tmp/openhcs-current-illumination3-native-timing-science-v1-20261002.json') -CASE = 'ExampleIlluminationCorrection_Example3' - -def sha(path): - return hashlib.sha256(Path(path).read_bytes()).hexdigest() - -def read(directory): - receipt_path, = (directory/'public'/CASE/'wells_1/workers_1/ordinary_run_evidence').glob('*/measured_pipeline_receipt.json') - receipt = json.loads(receipt_path.read_text()) - assert receipt['observation_export_scope'] == 'outcomes' - assert receipt['observed_axis_count'] == receipt['expected_axis_count'] == 1 - assert sha(receipt['observation_export_path']) == receipt['observation_export_sha256'] - assert sha(receipt['results_summary_path']) == receipt['results_summary_sha256'] - root, = map(Path, receipt['output_roots']) - assert root.resolve().is_relative_to(directory.resolve()) - files = frozenset(p for p in root.rglob('*') if p.is_file()) - exports = RuntimeExportObservation.from_output_roots((root,)) - snapshot = RuntimeOutputSnapshot.from_export_observation(exports, source_workspaces=(root,)) - return receipt_path, receipt, root, files, exports, snapshot - -def main(): - assert not (HERE/'science.json').exists() - obs = json.loads((HERE/'observations.json').read_text()) - assert len(obs['rows']) == 4 and [r['label'] for r in obs['rows']] == ['A','B','B','A'] - qualification = json.loads(REFERENCE_SCIENCE.read_text()) - assert qualification['status'] == 'CURRENT_BB9_DIAGNOSTIC_FULL_AUTHORED_IMAGE_INVENTORY_BYTE_NATIVE_INPUT_TRANSITIVE_PASS' - native = json.loads(NATIVE.read_text()) - assert native['status'] == 'PASS_AUTHORED_NONEMPTY_SAVED_IMAGE_SCIENCE_AND_MATCHED_FRESH_NATIVE_TIMING' - for role in ('native_report','source_environment_input_witness','saved_image_science_receipt'): - assert sha(native[role]) == native[role+'_sha256'] - ref = read(REFERENCE) - assert sha(ref[0]) == qualification['current_receipt_sha256'] - assert {str(p.relative_to(ref[2])): sha(p) for p in ref[3] if p.suffix == '.npy'} == qualification['scientific_npy_bytes'] - before = {str(p): sha(p) for p in ref[3]} - policy = _strict_cellprofiler_runtime_equivalence_policy() - results = [] - freeze = json.loads((HERE/'freeze.json').read_text()) - cpp = freeze['physical_inputs']['cppipe_path'] - assert cpp['path'] == native['source_cppipe'] and list(cpp['files'].values()) == [native['source_cppipe_sha256']] - environments = [] - for run in obs['rows']: - actual = read(Path(run['directory'])) - assert sha(actual[0]) == run['receipt_sha256'] - before.update({str(p): sha(p) for p in actual[3]}) - td = runtime_table_differences(ref[5].tables, actual[5].tables, policy) - im = runtime_image_differences(ref[5].images, actual[5].images, policy) - assert not td and not im, (td, im) - assert not actual[5].tables and len(actual[5].images) == len(ref[5].images) == 2 - _require_compared_output_inventory(reference_files=ref[3]-frozenset(METADATA_CONFIG.managed_paths(ref[2])), candidate_files=actual[3], reference_exports=ref[4], candidate_exports=actual[4], reference_snapshot=ref[5], candidate_snapshot=actual[5], candidate_managed_files=frozenset(METADATA_CONFIG.managed_paths(actual[2]))) - images = {str(p.relative_to(actual[2])): sha(p) for p in actual[3] if p.suffix == '.npy'} - assert images == qualification['scientific_npy_bytes'] and len(images) == 2 - workspace = Path(actual[1]['execution_plate_id']) - subdirs = json.loads((workspace/'openhcs_metadata.json').read_text())['subdirectories'] - entries = tuple(entry for s in subdirs.values() for entry in VirtualWorkspaceSourceProjectionEntries.from_subdirectory(s).entries.values()) - projections = SourceProjectionSet(entries) - assert len(projections.plane_projections) == 1 and not projections.artifact_projections - assert all(not p.ref.source_axis_indices for p in projections.plane_projections) - selected = {str(Path(p.ref.backend_address).resolve(strict=True)): sha(p.ref.backend_address) for p in projections.plane_projections} - assert selected == native['physical_source_sha256'] - assert actual[1]['global_config_source_sha256'] == ref[1]['global_config_source_sha256'] - assert actual[1]['endpoint_provenance']['client_openhcs_file'] == str(Path(freeze['imports']['openhcs'])) - environments.append(actual[1]['server_environment']) - results.append({'label': run['label'], 'directory':run['directory'], 'receipt_sha256':sha(actual[0]), 'scientific_npy_bytes':images, 'physical_selected_inputs':selected, 'image_differences':im, 'table_differences':td, 'full_physical_inventory':'PASS'}) - assert all(e == environments[0] for e in environments) - assert all(sha(p) == digest for p,digest in before.items()) - result = {'status':'ALL4_ORDINARY_AUTHORED_IMAGES_EXACT_AND_RETAINED_NATIVE_INPUT_PARITY_PASS', 'runs':results, 'means':obs['means'], 'saved_seconds':obs['saved_seconds'], 'all_before_after_output_sha256':before, 'server_environment':environments[0], 'freeze_sha256':sha(HERE/'freeze.json'), 'reference_science_sha256':sha(REFERENCE_SCIENCE), 'native_receipt_sha256':sha(NATIVE), 'validator_sha256':sha(__file__), 'limits':['Two observations per variant; no execution speedup is claimed.', 'Authored workload exports two images and no tables.', 'Native comparison is retained matched CP4.2.8.1 evidence, not a fresh native run.', 'Ordinary pipeline total excludes mandatory server startup and shutdown.']} - (HERE/'science.json').write_text(json.dumps(result, indent=2)+'\n') - print(json.dumps({'status':result['status'], 'means':obs['means'], 'saved_seconds':obs['saved_seconds'], 'science_sha256':sha(HERE/'science.json')})) - -if __name__ == '__main__': - main() diff --git a/benchmark/results/perf_schema_source_readiness_20261003/recipes/variants.json b/benchmark/results/perf_schema_source_readiness_20261003/recipes/variants.json deleted file mode 100644 index 3cbd1f0ed4..0000000000 --- a/benchmark/results/perf_schema_source_readiness_20261003/recipes/variants.json +++ /dev/null @@ -1,26 +0,0 @@ -[ - { - "path": "/home/ts/code/projects/openhcs-shared-runtime-plumbing/openhcs/processing/backends/lib_registry/registry_service.py", - "baseline_revision": "4eaacf17e5de3dba59ec217f133fa6e1821c8641", - "repository": "/home/ts/code/projects/openhcs-shared-runtime-plumbing", - "relative": "openhcs/processing/backends/lib_registry/registry_service.py", - "A": "/home/ts/.local/state/openhcs-maintenance/20261003/schema-source-readiness-ordinary-abba-v1/0-A.source", - "B": "/home/ts/.local/state/openhcs-maintenance/20261003/schema-source-readiness-ordinary-abba-v1/0-B.source" - }, - { - "path": "/home/ts/code/projects/openhcs-shared-runtime-plumbing/external/python-introspect/src/python_introspect/signature_analyzer.py", - "baseline_revision": "13f6ff3b43aaa430f5e771848f08d99faf941158", - "repository": "/home/ts/code/projects/openhcs-shared-runtime-plumbing/external/python-introspect", - "relative": "src/python_introspect/signature_analyzer.py", - "A": "/home/ts/.local/state/openhcs-maintenance/20261003/schema-source-readiness-ordinary-abba-v1/1-A.source", - "B": "/home/ts/.local/state/openhcs-maintenance/20261003/schema-source-readiness-ordinary-abba-v1/1-B.source" - }, - { - "path": "/home/ts/code/projects/openhcs-shared-runtime-plumbing/external/ObjectState/src/objectstate/lazy_factory.py", - "baseline_revision": "b0004daf3223326ac78689ba7b2e68abc18ae298", - "repository": "/home/ts/code/projects/openhcs-shared-runtime-plumbing/external/ObjectState", - "relative": "src/objectstate/lazy_factory.py", - "A": "/home/ts/.local/state/openhcs-maintenance/20261003/schema-source-readiness-ordinary-abba-v1/2-A.source", - "B": "/home/ts/.local/state/openhcs-maintenance/20261003/schema-source-readiness-ordinary-abba-v1/2-B.source" - } -] diff --git a/docs/plans/runtime_plumbing_frontier_20261003.rst b/docs/plans/runtime_plumbing_frontier_20261003.rst index 3cbe1622bd..e675b955c4 100644 --- a/docs/plans/runtime_plumbing_frontier_20261003.rst +++ b/docs/plans/runtime_plumbing_frontier_20261003.rst @@ -345,9 +345,11 @@ startup tests pass. python-introspect PR #6 and ObjectState PR #9 are merged and formally close their issues #5 and #8. Their merge commits change no measured source bytes. OpenHCS wiring is pushed in draft #394; a small main-only readiness change can be isolated without merging the unresolved broad branch. -Reviewable measurement receipt and recipes are under -``benchmark/results/perf_schema_source_readiness_20261003``; original immutable +The reviewable measurement receipt and immutable recipe links/hashes are under +``benchmark/results/perf_schema_source_readiness_20261003``. Redundant tracked +local recipe copies were removed; the originals, source variants and immutable outputs/freeze remain under the corresponding maintenance ABBA directory. +Those recipes depend on local paths and are not turnkey portable commands. Runtime plumbing, full-catalog/scaling measurements, fresh native repetitions and figures remain outstanding. Original whole-branch R0/R1 failures remain explicit and unwaived. From 0fe0482d2784bd6ab1d3a7b3fbb6e5ddfb473e55 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 02:29:21 -0400 Subject: [PATCH 111/235] Record complete bounded warm-cohort replay qualification for #510 --- .../qualification.json | 198 ++++++++++++++++++ .../runtime_plumbing_frontier_20261003.rst | 50 +++++ 2 files changed, 248 insertions(+) create mode 100644 benchmark/results/perf_current_cohort_v10_20261003/qualification.json diff --git a/benchmark/results/perf_current_cohort_v10_20261003/qualification.json b/benchmark/results/perf_current_cohort_v10_20261003/qualification.json new file mode 100644 index 0000000000..7d5ec179f9 --- /dev/null +++ b/benchmark/results/perf_current_cohort_v10_20261003/qualification.json @@ -0,0 +1,198 @@ +{ + "status": "BOUNDED_CURRENT_ORIGINAL_WARM_PRODUCER_AND_LOADER_REPLAY_FULL_GATES_PASS", + "issue": 510, + "source_revision": "4c636a281de4da7341f4f9f6bb8f718c75644a1d", + "protocol": { + "hash_seed": 0, + "actual_outer_cwd": "/home/ts/code/projects/openhcs", + "outer_taskset_cpu": 4, + "public_child_cpu": 5, + "public_child_cwd": "/home/ts/.local/state/openhcs-maintenance/20261003/current-cohort-capture-v10/output/tmp", + "actual_command": "PYTHONDONTWRITEBYTECODE=1 taskset -c 4 /home/ts/code/projects/openhcs/.venv/bin/python /home/ts/.local/state/openhcs-maintenance/20261003/current-cohort-capture-v10/controller.py --run --revision 4c636a281de4da7341f4f9f6bb8f718c75644a1d", + "actual_public_handle": 16238, + "actual_public_terminal": 0, + "failed_preflight_handle": 93559, + "failed_preflight_started_driver": false, + "effective_environment_sha256": "15a56d8163d7c2b38db7398210ec6e1c700da84184031a6460333193909fd375", + "no_environment_field_exclusions_or_reseal": true + }, + "capture": { + "graphs": 4, + "bytes": 1027681056, + "remaining_combined_cap_bytes": 46060768, + "per_item_cap": 402653184, + "total_cap": 1073741824, + "node_cap": 150000, + "errors": [], + "graphs_scope": [ + { + "key": "13-PatternGroupRuntime-_validate_and_unstack-before", + "bytes": 272201079, + "sha256": "3c49c2ebc3050c4bd9e62052d7a6e9acd6c095f23849f80f86a115041c544519", + "sidecar_sha256": "3ca071d05e7516eda02f3d0cbb6f6f77d6d3defee63991af3f147247eef5bd0b", + "owners": 1054, + "nonempty_views": 677, + "view_entries": 4286 + }, + { + "key": "13-save-oracle", + "bytes": 194819278, + "sha256": "c9ed3ce36c322a5d32aeb03f65b1d8d2697775e61231a6d433dae66e7d290542", + "sidecar_sha256": "dec76667df2aa673785cc176e23d309ca3fc6a552adbdd67ce957d4e0194e8c0", + "owners": 1112, + "nonempty_views": 735, + "view_entries": 4400 + }, + { + "key": "14-PatternGroupRuntime-_load_input_stack-before", + "bytes": 280297985, + "sha256": "2a48c08fdf3eecfdd5af13b0052ce985a21469dbbe5e5c4f97be69682a933780", + "sidecar_sha256": "8b6f277dc8d1765e3d6b68f816a91b0b762df72f3128b3294de0f5b513455d26", + "owners": 1115, + "nonempty_views": 738, + "view_entries": 4408 + }, + { + "key": "14-load-oracle", + "bytes": 280362714, + "sha256": "20c4d2721789d5c9dfdce4e6ff789cbcfb59d49e87ba53e042deb75be5c3bde7", + "sidecar_sha256": "67f9015dde0043870494880620dabbb8e5ea17feb08a1da1ad21398b032228b6", + "owners": 1116, + "nonempty_views": 738, + "view_entries": 4408 + } + ] + }, + "original_producer": { + "status": "PASS", + "results": [ + { + "kind": "producer13", + "status": "PASS", + "original_call_started": true, + "original_nested_owner_count": 1054, + "original_nonempty_views": 677, + "nested_exact_capture_refs_restored": true, + "captured_external_manifest_available": true, + "input_fixture": "/home/ts/.local/state/openhcs-maintenance/20261003/current-cohort-capture-v10/output/saved-inputs/root-813175-139641010583232-0002/cohort-13-PatternGroupRuntime-_validate_and_unstack-before.pkl", + "transport": { + "root_type": null, + "array_count": 1184, + "strict_snapshot_restore": true + }, + "original_declaring_method_counts": {}, + "records": 60, + "unstack_seconds": 0.007623694837093353, + "save_seconds": 0.021458042785525322, + "full_outputdata_records_planes_named_whole_equal": true, + "complete_array_alias_graph": true, + "array_roles": 123 + } + ], + "scope": "V10 original saved receiver, fresh graph each repetition; snapshots remain immutable. Exact gates retained. Diagnostic only: graph-owned captured warm fields restored by declared exact references; globalLRU/privateepoch not guessed. No end-to-end performance claim." + }, + "original_loader": { + "status": "PASS", + "results": [ + { + "kind": "loader14", + "status": "PASS", + "original_call_started": true, + "original_nested_owner_count": 1115, + "original_nonempty_views": 738, + "nested_exact_capture_refs_restored": true, + "captured_external_manifest_available": true, + "input_fixture": "/home/ts/.local/state/openhcs-maintenance/20261003/current-cohort-capture-v10/output/saved-inputs/root-813175-139641010583232-0002/cohort-14-PatternGroupRuntime-_load_input_stack-before.pkl", + "transport": { + "root_type": null, + "array_count": 1244, + "strict_snapshot_restore": true + }, + "original_declaring_method_counts": {}, + "load_seconds": 0.009833849966526031, + "full_loaded_metadata_store_cache_equal": true, + "complete_array_alias_graph": true, + "array_roles": 104 + } + ], + "scope": "V10 original saved receiver, fresh graph each repetition; snapshots remain immutable. Exact gates retained. Diagnostic only: graph-owned captured warm fields restored by declared exact references; globalLRU/privateepoch not guessed. No end-to-end performance claim." + }, + "immediate_producer_effects": { + "status": "IMMEDIATE_RETAINED_OWNER_EFFECTS_PASS", + "kind": "producer13", + "before_owners": 1054, + "before_nonempty_views": 677, + "before_view_entries": 4286, + "immediate_after_owners": 1115, + "after_nonempty_views": 738, + "after_view_entries": 4408, + "original_shared_owners_exactly_unchanged": 1050, + "original_outside_after_scope": 4, + "replay_changed_before_owners": 0, + "replay_new_reachable_owners": 61, + "outside_original_after_observation_changes": [], + "snapshot_sha256": "3c49c2ebc3050c4bd9e62052d7a6e9acd6c095f23849f80f86a115041c544519", + "scope": "Observed immediately after original calls, before afteroracle/equality/property checks. Mapping is original before witness path->captured samegraph owner ref->actual restored owner; original after IDs only select observed retained sourceowners, never identify objects across independent processes. Newly reached owners are separate. No candidate/globalLRU/performanceclaim.", + "script_sha256": "66848b5c5f5dcf0ccf7552420754aaab2005dc1a2c563961c6b6bc8d49748728" + }, + "immediate_loader_effects": { + "status": "IMMEDIATE_RETAINED_OWNER_EFFECTS_PASS", + "kind": "loader14", + "before_owners": 1115, + "before_nonempty_views": 738, + "before_view_entries": 4408, + "immediate_after_owners": 1116, + "after_nonempty_views": 738, + "after_view_entries": 4408, + "original_shared_owners_exactly_unchanged": 1115, + "original_outside_after_scope": 0, + "replay_changed_before_owners": 0, + "replay_new_reachable_owners": 1, + "outside_original_after_observation_changes": [], + "snapshot_sha256": "2a48c08fdf3eecfdd5af13b0052ce985a21469dbbe5e5c4f97be69682a933780", + "scope": "Observed immediately after original calls, before afteroracle/equality/property checks. Mapping is original before witness path->captured samegraph owner ref->actual restored owner; original after IDs only select observed retained sourceowners, never identify objects across independent processes. Newly reached owners are separate. No candidate/globalLRU/performanceclaim.", + "script_sha256": "66848b5c5f5dcf0ccf7552420754aaab2005dc1a2c563961c6b6bc8d49748728" + }, + "full_nested_after_oracles": "Corrected actual save oracle restores all1112anchored owners after nominal parser/FileManager/backend alias registration; full loader after-oracle restores all1116owners through original captured receiver/context. No disconnected-owner exclusion or graph union. Exact rows/reference/presence/order checks from retained adapters complete before output equality.", + "science": { + "status": "FULL6CSV120TIFF_AND180_PHYSICAL_SOURCE_REFS_PASS", + "csv": 6, + "tiff": 120, + "ordered_source_refs": 180, + "physical_volumes": 3, + "native_scope": "TRANSITIVE ONLY: exact current saved science equals retained reference and B764-qualified bytes with actual same3physicalvolume inputs/180orderedrefs. Retained2native repetitions verified; native was NOT rerun for current source/environment. Original native compare API RED retained separately; no current native timing ratio or performance claim." + }, + "all_handles_terminal": true, + "large_graphs_released_and_temp_encodings_disposed": true, + "source_clean": true, + "limits": [ + "Four current original occurrences only: producer13 and loader14, independent actual before/after graphs. Not whole-pipeline heap replay.", + "All captured graph-owned declared cache fields and exact presence/absence retained; process-global functools LRU caches explicitly outside scope.", + "Diagnostic constructor/call durations are not ordinary pipeline timings or optimization gains.", + "Original V7/V8/V9 failures and incomplete original save oracle remain immutable; V10 does not reinterpret their source pins or silently repair them." + ], + "evidence_hashes": { + "controller.py": "4d96ff2362e93d8d0bbe7fe66929d4b88e8791ae2710bbfd3636713a384a358e", + "original_replay_v10.py": "7001af6479a05e0dd1afc52a1509a44fed45ce1d0c1151dee3b832dc7a96a74a", + "immediate_original_effects_v10.py": "66848b5c5f5dcf0ccf7552420754aaab2005dc1a2c563961c6b6bc8d49748728", + "science_validator.py": "3483bec5d1f3f0d12e5a09b63e233db9fed1b11f9d201f8cc37d5ae391a0c599", + "output/source-freeze.json": "6d4f4a59e62acbebbd6e21fcff66fa5806f45d99778264e7f5399b666bb3171f", + "output/observations.json": "1559ae44e06fed622816b1518132fd738f56e0355faadf9e15cdd2159052f491", + "science.json": "6f1a8078dfaa74ea840f398b5e5f5b4c0b7d370a436fc1829e4ad5d90f597b6a", + "launch.log": "7d0d52ffcd64337698061f0c4654c66f17a6f6b5b8a8931102a0003f9f6ac9a1", + "launch-v2.log": "8e4a5f55bea98fc6a40613ff182af4172800ec8a009ba5bdb3b7ef746994a2d8", + "launch-environment-diagnosis-v1.json": "d9d74e533a776d8729b9a97941dc94c1a74edc513e12c750fcc8d0dc336d6219", + "launch-environment-diagnosis-v2.json": "032936f8dc7aa279cb468e703ffa04906cfd4b4cda1fb1e44531e697a4c7a071", + "producer-original-replay-v10.log": "74091052623a193539dab4a1f11b21640099824a173abfa33ab99fb444853c53", + "loader-original-replay-v10.log": "34a4c1394fe4435ccb4d21208fd3385623e9905e965c79055e4ac564fdee46a5", + "immediate-original-effects-producer13-v1.json": "d1358af67a0163b223663811dc56f0c5ba5cdf2144d82909b578ae3c943f52a4", + "immediate-original-effects-loader14-v1.json": "fee1fb6458bcc5427fb51b748db16202957205d6f25b1a07626e38a3172c6091", + "site/nested_cache_retention.py": "7e6d9ca3e7dce0da18a603610576cc9eea2d8038ad936fe2599e5b87d94bf3b1", + "site/nested_cache_witness.py": "f8c23b439ea301b761c7329a7273ff66ff2d9a865ff7d377d82d5766432e1055", + "site/openhcs_cohort_ownership_v8_20261003.py": "89abcd4587135989c4fb5ff9a7990f6983ad428c9c403390c770c3b9330ee0ee", + "site/openhcs_cohort_snapshot_transport_v6_20261002.py": "a8b11cf6401aa0f80617e6734ae67bbbffcecc74ad4f7a59041523e42b850763", + "site/openhcs_current_cohort_capture_v10_20261003.py": "ec9af29df043c9f465ba410c7317b6668d903fca3d3308f70716a350e096c98a", + "site/ordered_array_role_oracle.py": "cf6b799a8ade0dbf82185fdc2a80f242111b784e1e643f4e91ea3456a6c5718b", + "site/sitecustomize.py": "60b213d69117b57d7c5340071db6499ff4b923d6e1baf68b05d8d1f69d8fca59" + } +} diff --git a/docs/plans/runtime_plumbing_frontier_20261003.rst b/docs/plans/runtime_plumbing_frontier_20261003.rst index e675b955c4..e7a4d4c50d 100644 --- a/docs/plans/runtime_plumbing_frontier_20261003.rst +++ b/docs/plans/runtime_plumbing_frontier_20261003.rst @@ -387,3 +387,53 @@ evidence. The saved downstream cache hit constructs no new provenance objects; it does not establish the payoff of a broader loading/recording/publication fix. The next capture must retain actual nested caches in the same graph, under the existing admission bounds, without rewarming or inventing state. + +Current graph-owned replay qualification (#510) +------------------------------------------------ + +The fresh V10 capture at ``4c636a281de4da7341f4f9f6bb8f718c75644a1d`` completes +that bounded qualification. Its four independent producer-13/loader-14 graphs +total 1,027,681,056 bytes, below the unchanged 1 GiB combined limit; each remains +below 384 MiB and 150,000 nodes. Original processing succeeds, with no capture +errors. The complete six-CSV/120-TIFF inventory and bytes, 180 ordered source +references and three physical input-volume hashes pass the original science +gate. Native agreement uses the retained qualified two-repeat witness; native +CellProfiler was not rerun by this diagnostic. + +Before serialization, the adapters retain actual manifest membership, source +cache dictionaries, process-ID key owners and physically stored nested fields +in each same graph. Restoration validates anchors and declarations before +reconnecting exact references; process-ID keys are rekeyed from retained owners. +Stored field absence, ordered metadata views, birth fingerprints and array +aliases remain intact. Fresh interpreters preserve the original hash seed zero. + +The smaller save oracle must also retain its actual memory backend. Its existing +parser must point to the receiver's exact FileManager; restoration reconnects +that backend through ``register_backend`` before restoring nested fields. This +closes V9's nine detached-owner failures without reconstructing missing state, +adding a receiver, unioning graphs, excluding owners or increasing bounds. + +Original producer replay restores all 1,112 after-oracle owners and matches all +60 records, output data, named values, VFS planes and whole-stack cache, including +123 complete array roles and their object/shared-memory relationships. Original +loader replay restores all 1,116 after-oracle owners and matches the complete +loaded result, runtime store, observations and stack cache, including 104 array +roles. Immediately after the original calls, before equality or property queries, +all 1,050 producer and 1,115 loader retained owners have unchanged stored fields +and cached entries; newly reachable owners are reported separately. + +The determining receipt is +``benchmark/results/perf_current_cohort_v10_20261003/qualification.json``; +its SHA-256 is +``17bb47f609d95ca6044f92810dbbf1c5f31e066ef330955aab264a5d8d7e6273``. +It identifies every immutable helper, snapshot, sidecar, log and consumer gate +under ``~/.local/state/openhcs-maintenance/20261003/current-cohort-capture-v10``. +The authoritative outer preparation/launch directory is the canonical OpenHCS +checkout; the public child still uses the owned temporary directory and CPU5. +The failed outer-environment preflight started no pipeline and remains retained. + +This completes the replay friction in #510. It qualifies only these actual +graph-owned warm occurrences, not process-global LRU state or a whole-pipeline +heap. Diagnostic durations are not ordinary performance gains. V7/V8/V9 failures +remain immutable, and the broad branch's architecture gates, fresh native runs, +full-catalog scaling and figures remain outstanding. From 30ebacf907273ee0d0767f392ac21281915f5a0e Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 02:58:22 -0400 Subject: [PATCH 112/235] Record fresh matched native benchmarks and current gap (#496) --- .../README.md | 20 + .../matched_runtime.png | Bin 0 -> 103150 bytes .../matched_runtime.svg | 1615 ++++++++++++++++ .../matched_speedup.png | Bin 0 -> 134500 bytes .../matched_speedup.svg | 1683 +++++++++++++++++ .../metrics.csv | 7 + .../native-3d.json | 61 + .../native-speckles.json | 61 + .../ordinary-0.csv | 3 + .../ordinary-1.csv | 3 + .../qualification.json | 640 +++++++ .../strict-science.json | 795 ++++++++ .../timing_summary.json | 73 + .../shared-environment-qualification.json | 836 ++++++++ .../runtime_plumbing_frontier_20261003.rst | 39 + 15 files changed, 5836 insertions(+) create mode 100644 benchmark/results/perf_current_native_paired_20261003/README.md create mode 100644 benchmark/results/perf_current_native_paired_20261003/matched_runtime.png create mode 100644 benchmark/results/perf_current_native_paired_20261003/matched_runtime.svg create mode 100644 benchmark/results/perf_current_native_paired_20261003/matched_speedup.png create mode 100644 benchmark/results/perf_current_native_paired_20261003/matched_speedup.svg create mode 100644 benchmark/results/perf_current_native_paired_20261003/metrics.csv create mode 100644 benchmark/results/perf_current_native_paired_20261003/native-3d.json create mode 100644 benchmark/results/perf_current_native_paired_20261003/native-speckles.json create mode 100644 benchmark/results/perf_current_native_paired_20261003/ordinary-0.csv create mode 100644 benchmark/results/perf_current_native_paired_20261003/ordinary-1.csv create mode 100644 benchmark/results/perf_current_native_paired_20261003/qualification.json create mode 100644 benchmark/results/perf_current_native_paired_20261003/strict-science.json create mode 100644 benchmark/results/perf_current_native_paired_20261003/timing_summary.json create mode 100644 benchmark/results/perf_dependency_source_readiness_20261003/shared-environment-qualification.json diff --git a/benchmark/results/perf_current_native_paired_20261003/README.md b/benchmark/results/perf_current_native_paired_20261003/README.md new file mode 100644 index 0000000000..1f52a1f9ea --- /dev/null +++ b/benchmark/results/perf_current_native_paired_20261003/README.md @@ -0,0 +1,20 @@ +# Current paired native comparison + +The ordinary public driver and unchanged warm native worker were run on clean source `60b2ac9b4d4fd91558f68aac85ae39b55e575fc8`, including main `f7de9efad` and the qualified ObjectState 1.1.9 / python-introspect 0.1.15 source update. CPU 5, one worker inline, one native thread, default OUTCOMES and process-tree RSS. All startup, mandatory registry/library/kernel preparation and shutdown precede/follow pipeline clocks. Native imports/JVM/pipeline loading and complete warmup -1 are outside its measured invocation. + +| Case | OH compile mean | OH execute mean | OH total mean | Native invocation mean | Execution speedup | Total speedup | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | +| 3D monolayer | 1.212512 s | 9.127215 s | 11.153507 s | 14.422177 s | 1.58013x | 1.29306x | +| Speckles | 0.663492 s | 1.231350 s | 2.246087 s | 1.954230 s | 1.58706x | 0.87006x | + +Both observations are retained; ranges and exact ratios are in `qualification.json`. These descriptive two-observation means are not a causal optimization experiment or statistical significance. Reaching 2x execution still requires 1.916127 s for 3D and 0.254235 s for Speckles. The source/environment differs from historical evidence, so no source change is blamed for the observed spread. + +Every OH repetition passed against both fresh native repetitions: eight complete strict comparisons, measurements within the existing 1e-6 policy, exact 3D uint16 image values, known relationship correlations, and complete physical output inventories. OH 3D has six authored CSVs and 120 TIFFs (128 total physical files); native has two logical label volumes. Speckles has three CSVs (five total files). The existing comparator owns the native-only experiment table and managed metadata rules; no new exclusions were introduced. The selected source domains are exactly 180 refs / one volumetric image set and two refs / one image set. Root independently verified all 11 linked qualification artifacts and all 290 unique scientific physical output paths. + +The figures use the existing `benchmark.reports.cppipe_figures` owner, means from all four ordinary rows and all four native measured observations, and the 2x reference. They are a two-case preview; full30 and scaling figures remain unfinished. Figure generator evidence and raw sample custody are in `timing_summary.json`; `metrics.csv` retains the declared figure rows. + +The external immutable controllers remain at `/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-preparation-v2/controller.py` (SHA256 `b772d231196eaf23d9a150ffa491f95426cb6e074470ac3febd508afd1d3ad18`) and `/home/ts/.local/state/openhcs-maintenance/20261003/plot_current_paired_timings_v2.py`; their recipes use existing public execution and scientific comparison owners. These are local custody references, not a portable launcher. Original sparse-preparation failure and the exact-current-Git supplement are retained separately. + +Qualification SHA256: `3db0f60d3ef04aa0ce846eaef70e746031442422cc693ba943bf78ac8db1ab32`. Science SHA256: `77ab1a1e88d52502c633b646efe0d032ad87f117fc403298cb8b5742a0510190`. + +This evidence does not admit the whole PR394 R0/R1 guards, finish full-catalog parity, or establish a new performance improvement. The generic runtime plumbing goal remains active. diff --git a/benchmark/results/perf_current_native_paired_20261003/matched_runtime.png b/benchmark/results/perf_current_native_paired_20261003/matched_runtime.png new file mode 100644 index 0000000000000000000000000000000000000000..34bd139c921b75a3bf65fc755528d1cd775a9fd3 GIT binary patch literal 103150 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�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%0D6H_RZV5&(SeiNz3Hf8qbb`}lH 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 OBSGJXeaG1w`}4ocD-4|g literal 0 HcmV?d00001 diff --git a/benchmark/results/perf_current_native_paired_20261003/matched_runtime.svg b/benchmark/results/perf_current_native_paired_20261003/matched_runtime.svg new file mode 100644 index 0000000000..24bf1032e3 --- /dev/null +++ b/benchmark/results/perf_current_native_paired_20261003/matched_runtime.svg @@ -0,0 +1,1615 @@ + + + + + + + + 2026-10-03T02:56:54.133761 + image/svg+xml + + + Matplotlib v3.11.2, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/benchmark/results/perf_current_native_paired_20261003/matched_speedup.png b/benchmark/results/perf_current_native_paired_20261003/matched_speedup.png new file mode 100644 index 0000000000000000000000000000000000000000..684d7d7f68d079d32584ed14ce215817d1369dd3 GIT binary patch literal 134500 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�bmN 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 jezy^V(fQ00())4e5Aj + + + + + + + 2026-10-03T02:56:54.463573 + image/svg+xml + + + Matplotlib v3.11.2, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/benchmark/results/perf_current_native_paired_20261003/metrics.csv b/benchmark/results/perf_current_native_paired_20261003/metrics.csv new file mode 100644 index 0000000000..596cf18e87 --- /dev/null +++ b/benchmark/results/perf_current_native_paired_20261003/metrics.csv @@ -0,0 +1,7 @@ +pipeline_name,method,assay_category,module_category,accuracy_fraction,raw_seconds,speedup,peak_memory_mb +ExampleSpeckles,CellProfiler,,,1.0,1.9542299311142415,1.0, +ExampleSpeckles,OH execution,,,1.0,1.2313504219055176,1.5870623799276133, +ExampleSpeckles,OH total,,,1.0,2.2460867043118924,0.870059881198102, +cp_tutorial_3d_monolayer,CellProfiler,,,1.0,14.422176547115669,1.0, +cp_tutorial_3d_monolayer,OH execution,,,1.0,9.127214908599854,1.580128954071936, +cp_tutorial_3d_monolayer,OH total,,,1.0,11.153506982140243,1.2930620449881318, diff --git a/benchmark/results/perf_current_native_paired_20261003/native-3d.json b/benchmark/results/perf_current_native_paired_20261003/native-3d.json new file mode 100644 index 0000000000..01f5ea843a --- /dev/null +++ b/benchmark/results/perf_current_native_paired_20261003/native-3d.json @@ -0,0 +1,61 @@ +{ + "startup_seconds": 0.9434431060217321, + "environment": { + "python_executable": "/home/ts/code/projects/openhcs/.venv-cellprofiler39/bin/python", + "python_version": "3.9.25", + "cellprofiler_version": "4.2.8.1", + "cellprofiler_core_version": "4.2.8.1", + "numpy_version": "1.24.4", + "scipy_version": "1.9.0", + "temporary_root": "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/native_tmp" + }, + "request": { + "pipeline_path": "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/preparation/native_cellprofiler_headless/3d_monolayer_final.cppipe", + "input_dir": "/home/ts/.cache/openhcs/benchmark_datasets/CellProfiler_tutorials/data/3d_monolayer/images", + "output_root": "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs", + "expected_image_sets": 1, + "repetitions": 2, + "file_list_path": "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/preparation/native_cellprofiler_file_list.txt", + "first_image_set": 1, + "last_image_set": null, + "report_path": "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/native_report.json", + "start_barrier_root": null, + "start_barrier_job_count": 1, + "start_barrier_job_index": 0 + }, + "observations": [ + { + "repetition": -1, + "output_root": "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/-1", + "image_set_count": 1, + "invocation_seconds": 15.945333322975785, + "pre_first_module_seconds": 0.4420607606880367, + "first_module_through_post_run_seconds": 15.503272562287748, + "invocation_started_monotonic_seconds": 3804735.20616804, + "first_module_started_monotonic_seconds": 3804735.648228801, + "completed_monotonic_seconds": 3804751.151501363 + }, + { + "repetition": 0, + "output_root": "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/0", + "image_set_count": 1, + "invocation_seconds": 14.286907745990902, + "pre_first_module_seconds": 0.1336168795824051, + "first_module_through_post_run_seconds": 14.153290866408497, + "invocation_started_monotonic_seconds": 3804751.151514428, + "first_module_started_monotonic_seconds": 3804751.285131308, + "completed_monotonic_seconds": 3804765.438422174 + }, + { + "repetition": 1, + "output_root": "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/1", + "image_set_count": 1, + "invocation_seconds": 14.557445348240435, + "pre_first_module_seconds": 0.13210490392521024, + "first_module_through_post_run_seconds": 14.425340444315225, + "invocation_started_monotonic_seconds": 3804765.438434918, + "first_module_started_monotonic_seconds": 3804765.570539822, + "completed_monotonic_seconds": 3804779.995880266 + } + ] +} \ No newline at end of file diff --git a/benchmark/results/perf_current_native_paired_20261003/native-speckles.json b/benchmark/results/perf_current_native_paired_20261003/native-speckles.json new file mode 100644 index 0000000000..132c17ac31 --- /dev/null +++ b/benchmark/results/perf_current_native_paired_20261003/native-speckles.json @@ -0,0 +1,61 @@ +{ + "startup_seconds": 0.807538575027138, + "environment": { + "python_executable": "/home/ts/code/projects/openhcs/.venv-cellprofiler39/bin/python", + "python_version": "3.9.25", + "cellprofiler_version": "4.2.8.1", + "cellprofiler_core_version": "4.2.8.1", + "numpy_version": "1.24.4", + "scipy_version": "1.9.0", + "temporary_root": "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/ExampleSpeckles/native_tmp" + }, + "request": { + "pipeline_path": "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/ExampleSpeckles/preparation/native_cellprofiler_headless/ExampleSpeckles.cppipe", + "input_dir": "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleSpeckles/images", + "output_root": "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/ExampleSpeckles/outputs", + "expected_image_sets": 1, + "repetitions": 2, + "file_list_path": "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/ExampleSpeckles/preparation/native_cellprofiler_file_list.txt", + "first_image_set": 1, + "last_image_set": null, + "report_path": "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/ExampleSpeckles/native_report.json", + "start_barrier_root": null, + "start_barrier_job_count": 1, + "start_barrier_job_index": 0 + }, + "observations": [ + { + "repetition": -1, + "output_root": "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/ExampleSpeckles/outputs/-1", + "image_set_count": 1, + "invocation_seconds": 3.0011698277667165, + "pre_first_module_seconds": 0.301135023124516, + "first_module_through_post_run_seconds": 2.7000348046422005, + "invocation_started_monotonic_seconds": 3804788.163755448, + "first_module_started_monotonic_seconds": 3804788.464890471, + "completed_monotonic_seconds": 3804791.164925276 + }, + { + "repetition": 0, + "output_root": "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/ExampleSpeckles/outputs/0", + "image_set_count": 1, + "invocation_seconds": 1.9962371489964426, + "pre_first_module_seconds": 0.10309637477621436, + "first_module_through_post_run_seconds": 1.8931407742202282, + "invocation_started_monotonic_seconds": 3804791.164935265, + "first_module_started_monotonic_seconds": 3804791.26803164, + "completed_monotonic_seconds": 3804793.161172414 + }, + { + "repetition": 1, + "output_root": "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/ExampleSpeckles/outputs/1", + "image_set_count": 1, + "invocation_seconds": 1.9122227132320404, + "pre_first_module_seconds": 0.07363771507516503, + "first_module_through_post_run_seconds": 1.8385849981568754, + "invocation_started_monotonic_seconds": 3804793.161182433, + "first_module_started_monotonic_seconds": 3804793.234820148, + "completed_monotonic_seconds": 3804795.073405146 + } + ] +} \ No newline at end of file diff --git a/benchmark/results/perf_current_native_paired_20261003/ordinary-0.csv b/benchmark/results/perf_current_native_paired_20261003/ordinary-0.csv new file mode 100644 index 0000000000..bb3efe6808 --- /dev/null +++ b/benchmark/results/perf_current_native_paired_20261003/ordinary-0.csv @@ -0,0 +1,3 @@ +case_name,mode_name,worker_count,well_count,compile_seconds,prepare_seconds,execute_seconds,total_seconds,wells_per_second,successful_wells,native_single_sample_execution_seconds,projected_native_execution_seconds,projected_execution_speedup,peak_memory_mb,status,memory_limit_mb,error_message,execution_route,server_lifecycle,run_input_sha256 +ExampleSpeckles,1w_1t,1,1,0.7714827060699463,0.0,1.0937919616699219,2.189194314647466,0.91425063910076,1,,,,1225.88671875,success,,,ordinary-zmq-outcomes-v1,reused-per-sweep,1a86dbcc3b37bc80fe8cc7f2605b43d0cef1fdc7d075a3b7172fe4c67ce94eb4 +cp_tutorial_3d_monolayer,1w_1t,1,1,1.1917080879211426,0.0,8.670458555221558,10.651790381409228,0.11533415373951313,1,,,,1891.54296875,success,,,ordinary-zmq-outcomes-v1,reused-per-sweep,1a86dbcc3b37bc80fe8cc7f2605b43d0cef1fdc7d075a3b7172fe4c67ce94eb4 diff --git a/benchmark/results/perf_current_native_paired_20261003/ordinary-1.csv b/benchmark/results/perf_current_native_paired_20261003/ordinary-1.csv new file mode 100644 index 0000000000..94bf2fd3ac --- /dev/null +++ b/benchmark/results/perf_current_native_paired_20261003/ordinary-1.csv @@ -0,0 +1,3 @@ +case_name,mode_name,worker_count,well_count,compile_seconds,prepare_seconds,execute_seconds,total_seconds,wells_per_second,successful_wells,native_single_sample_execution_seconds,projected_native_execution_seconds,projected_execution_speedup,peak_memory_mb,status,memory_limit_mb,error_message,execution_route,server_lifecycle,run_input_sha256 +ExampleSpeckles,1w_1t,1,1,0.5555016994476318,0.0,1.3689088821411133,2.302979093976319,0.7305088111020931,1,,,,1176.95703125,success,,,ordinary-zmq-outcomes-v1,reused-per-sweep,1a86dbcc3b37bc80fe8cc7f2605b43d0cef1fdc7d075a3b7172fe4c67ce94eb4 +cp_tutorial_3d_monolayer,1w_1t,1,1,1.23331618309021,0.0,9.58397126197815,11.655223582871258,0.10434088048314934,1,,,,1783.82421875,success,,,ordinary-zmq-outcomes-v1,reused-per-sweep,1a86dbcc3b37bc80fe8cc7f2605b43d0cef1fdc7d075a3b7172fe4c67ce94eb4 diff --git a/benchmark/results/perf_current_native_paired_20261003/qualification.json b/benchmark/results/perf_current_native_paired_20261003/qualification.json new file mode 100644 index 0000000000..251ebad0a1 --- /dev/null +++ b/benchmark/results/perf_current_native_paired_20261003/qualification.json @@ -0,0 +1,640 @@ +{ + "status": "TWO_CASE_FRESH_PAIRED_SCIENCE_PASS", + "source_head": "60b2ac9b4d4fd91558f68aac85ae39b55e575fc8", + "cases": { + "cp_tutorial_3d_monolayer": { + "ordinary_execution_seconds": [ + 8.670458555221558, + 9.58397126197815 + ], + "ordinary_total_seconds": [ + 10.651790381409228, + 11.655223582871258 + ], + "native_invocation_seconds": [ + 14.286907745990902, + 14.557445348240435 + ], + "means_seconds": { + "openhcs_execute": 9.127214908599854, + "openhcs_total": 11.153506982140243, + "openhcs_compile": 1.2125121355056763, + "native_invocation": 14.422176547115669 + }, + "descriptive_native_over_openhcs_execute": 1.580128954071936, + "descriptive_native_over_openhcs_total": 1.2930620449881318, + "remaining_seconds_to_2x_execution": 1.9161266350420192, + "remaining_seconds_to_2x_total": 3.9424187085824087, + "actual_source_refs": 180, + "actual_native_image_sets": 1, + "native_environment": { + "python_executable": "/home/ts/code/projects/openhcs/.venv-cellprofiler39/bin/python", + "python_version": "3.9.25", + "cellprofiler_version": "4.2.8.1", + "cellprofiler_core_version": "4.2.8.1", + "numpy_version": "1.24.4", + "scipy_version": "1.9.0", + "temporary_root": "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/native_tmp" + }, + "science_comparisons": [ + { + "ordinary_repeat": 0, + "native_repeat": 0, + "tables": [ + 7, + 6 + ], + "physical_images": [ + 2, + 120 + ], + "logical_images": [ + 2, + 2 + ], + "known_relationship_keys": [ + 3, + 3 + ] + }, + { + "ordinary_repeat": 0, + "native_repeat": 1, + "tables": [ + 7, + 6 + ], + "physical_images": [ + 2, + 120 + ], + "logical_images": [ + 2, + 2 + ], + "known_relationship_keys": [ + 3, + 3 + ] + }, + { + "ordinary_repeat": 1, + "native_repeat": 0, + "tables": [ + 7, + 6 + ], + "physical_images": [ + 2, + 120 + ], + "logical_images": [ + 2, + 2 + ], + "known_relationship_keys": [ + 3, + 3 + ] + }, + { + "ordinary_repeat": 1, + "native_repeat": 1, + "tables": [ + 7, + 6 + ], + "physical_images": [ + 2, + 120 + ], + "logical_images": [ + 2, + 2 + ], + "known_relationship_keys": [ + 3, + 3 + ] + } + ], + "raw_range_seconds": { + "native": [ + 14.286907745990902, + 14.557445348240435 + ], + "openhcs_execute": [ + 8.670458555221558, + 9.58397126197815 + ], + "openhcs_total": [ + 10.651790381409228, + 11.655223582871258 + ] + }, + "descriptive_ratio_range": { + "native_over_execute": [ + 1.490708533598217, + 1.6789706398485225 + ], + "native_over_total": [ + 1.2257943954835167, + 1.3666665252489216 + ] + }, + "complete_ordinary_physical_inventories": [ + { + "repeat": 0, + "output_roots": [ + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput" + ], + "file_count": 128, + "file_sha256": { + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/openhcs_metadata.json": "df996ea7347162652903028922a9ac2206dd2d6dbecbf9fb6da05a7e65ece370", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/openhcs_metadata.json.lock": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_erodedResizedNuclei.csv": "cca23f86a85bb3ee7a5c3a1d174c34b5f026ca7f399be98458e1a8c6e12d5c77", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_Cells.csv": "6f6e7431171ed5ea73a01784105147eca0e43a6e3e41f8720df93106353c9c79", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_downsizedNuclei.csv": "a7b998c5a524e11747fc5452fd763fc3d9abe16f46e961f62e09e9393b6a8adf", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_Nuclei.csv": "17500f5d4321bbf255df31f7a3503339029c2b077045a6c10e4d4f6f0db9ad1e", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_Image.csv": "75ac32ff5ef57bc285ec4eae1c9b501c27a35b486bf35435af5d499d856d34a4", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_erodedDownsizedNuclei.csv": "ee507fb9c2387f86fc768184baf2596ebe1f8e40f7fc5eb24f252345fee63ae6", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z052_t001_NucleiLabels.tiff": "0daf51526658e23b7c9ab8872e992ebe9322399286f95741d0ccdbd357e5d631", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z008_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z060_t001_NucleiLabels.tiff": "7cc3c6c6a22f558e62194277090bdea2deb79475dd91fe7d0e49eac3c0187388", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z015_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z057_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z021_t001_CellsLabels.tiff": "daa31cb1e537e5c32b1936ab7647c5690741d2bc1d0e15c60b59ba1310a8c0ce", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z047_t001_CellsLabels.tiff": "b926aa64e1fa8aa52279a69d91dc23cf14bf0ae76107f9b360c7ae77bdf53719", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z022_t001_NucleiLabels.tiff": "50aef20e28a2f9e18ca2e28eabf6d87ec2ad20c79898e56f8d63a959ba5bb507", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z027_t001_CellsLabels.tiff": "ee9056587a19e581f5feb6078b41fa1004d923a5ddf1725aefc4b8b8c5201ad6", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z029_t001_NucleiLabels.tiff": "34bce0f06f674e20667554eed0075fde8d9d33c9c0eaeffd88a1c558499cc432", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z007_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z018_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z047_t001_NucleiLabels.tiff": "995d5ce77cbf61a9be8590f34987ef3f1dd698ae6ed393cac4d0960bc9e7a66a", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z046_t001_CellsLabels.tiff": "dc081df050be6cd19b5d5bbfe21c96c350eecf9bc6f10f608dc361a36056632a", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z042_t001_CellsLabels.tiff": "88d2e9964455459499656c4f0a762cd86376c678756635aaa6b6c8d98be01852", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z024_t001_NucleiLabels.tiff": "9e96a05ed9860cac9c57c618db2f4fe5e4b769a9d235aac6fd1d628486317df0", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z035_t001_NucleiLabels.tiff": "13e69afc324152bb91329fa758d131d849127e8fd346078d94c6adc25582b3a6", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z055_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z050_t001_CellsLabels.tiff": "ea7654a6fe1f92ad274a54935420923eec4bec5ad030187afc0ac7af02690af5", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z014_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z031_t001_NucleiLabels.tiff": "247490c261fecc26c7379ab2eebaa01e4bea523218b3cc5491d2fe2b9aadfe11", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z020_t001_NucleiLabels.tiff": "b0cd031b448d8bd2075e65710bd76c064161b25242f85ee150da7cd139fcc1a8", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z051_t001_CellsLabels.tiff": "73483bd10a0f37990084013ad054768086d6d3c929da6bc7e74f4aee37e9dbf2", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z028_t001_CellsLabels.tiff": "fef31040e0c54501c3bf965eb98f055cdb1d42c96f91cf1c239033d0a9a06534", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z022_t001_CellsLabels.tiff": "7d7ec170ed3652efa99e43d8db546c51b696821dca7727d0ff4d2f2ead7fdbaa", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z033_t001_CellsLabels.tiff": "a320b99ee129d97b99f9fac66aabb89fc1652930b3bb8b2b3fbba7b57e994f85", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z038_t001_CellsLabels.tiff": "6ac8fd9c95acc1f261e96a8fb9332e5d6a403548842f9b85ecb977e6213b4c9b", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z051_t001_NucleiLabels.tiff": "f0f36ccaf744d42da036948682cfd226e6dd4a3cfb94aed38729b2d25ede5dff", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z023_t001_CellsLabels.tiff": "2f06989618050934c73473f9342c2fe3410fd834a0ddb976cf602fe6198db7a6", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z010_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z004_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z046_t001_NucleiLabels.tiff": "c1250e060d32f2cfc07fc7e1594fdb1d6cbb4d0483fa94a8113efa695397963c", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z003_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z026_t001_NucleiLabels.tiff": "ad22809ff3f43d15c838f697061c263070dbf1da1031da39900681cdfbf9d54c", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z054_t001_CellsLabels.tiff": "25ff2ec586a568e42286f73713d64aab1208c38c016099299211f04368ac49c8", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z039_t001_CellsLabels.tiff": "22fa79a76e64013f10373be2ca2af98a2173275c5d43c9cad33135acdc376d14", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z057_t001_NucleiLabels.tiff": "695517641e9fe4b8421d4a7db56b24c755b6957c98ce32bb24ad5b25f52661eb", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z020_t001_CellsLabels.tiff": "e8105a4f04198ac75de9831247030084d6f65bf98691d7444ead7e178072fa22", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z008_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z037_t001_NucleiLabels.tiff": "927d52441c57cb3a4014effaca3e3c23c76275fb264c26e1942a9c58b85ee6d2", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z011_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z043_t001_NucleiLabels.tiff": "81a705d05b0a5bfa8783df28990c1d70634c1f4aa2bca23e02cfa8a4bc861b86", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z015_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z041_t001_NucleiLabels.tiff": "744eab7cd170d5af8d2f72ced3893dd939c4e1b0f415afab799a37ded5d046c7", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z060_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z055_t001_NucleiLabels.tiff": "44e02416cc94a70528165a4b6c42b9e19b205f9c91528076796272cb1a8504ea", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z033_t001_NucleiLabels.tiff": "512a3a0ed8025d14b0556a72ebfc13b173d14f7046817ca09876242ab63b9d6b", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z050_t001_NucleiLabels.tiff": "565b4f08f7503086c1f2f8e631d19063129e02a5767d2f979c0cec8fdec23601", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z009_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z036_t001_CellsLabels.tiff": "28b68d1300cf7b08e21dbcd4858447862520dded5c27f2d84cf223513a119eed", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z049_t001_NucleiLabels.tiff": "e92e493078672a0e0d90e31409612e215bf8486300d5c765d0e0504ff8888e7a", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z040_t001_NucleiLabels.tiff": "e0bd66b166e9f92120dbee183973849eda0afbbe6fed9d03fc4cfd2c56a96280", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z016_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z012_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z001_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z053_t001_NucleiLabels.tiff": "b8613192175a4615b58aed607474657668429cb6ac635cd012569b19667f2d3d", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z017_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z006_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z032_t001_CellsLabels.tiff": "8a2c5774044326ae722e41aae517266197fbebb0e4dd5eab4b22414fbfea309f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z032_t001_NucleiLabels.tiff": "97b09fe5444ad439c3c2007d75971dce2766d94c93268eccfee9ae49b8a04aa1", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z058_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z048_t001_NucleiLabels.tiff": "325478230529e94ba372f38f0bf512292f131d337b6ac09ce5a91b0266724e02", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z006_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z038_t001_NucleiLabels.tiff": "e2852ea83c62ab30d77681b4e39f88d3cee74ae052f1c948675ae3581c9fc28a", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z052_t001_CellsLabels.tiff": "7bb98f0587062e01b965e486fc687872651d7d90d271a2fdf0d35d67b464b233", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z041_t001_CellsLabels.tiff": "be7762756eb65f08cb337904165c6f43b5351f39405461f65ad28669abfe3899", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z005_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z011_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z044_t001_NucleiLabels.tiff": "a78264f2f5e245a3913a69f89105e94df30048b1aaef4eb80fc5c3235e78b196", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z029_t001_CellsLabels.tiff": "b3bdde5f751b8f5d41ca8b5219a3427e450cb1b8795401d0ec638940d1ad7bd9", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z059_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z056_t001_NucleiLabels.tiff": "ca0723c8b9764d5b77034719bb75a2940be35da2e04119af4c0e6d3552c08718", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z019_t001_CellsLabels.tiff": "ec6ef1d319f2732c47b06f5cf02775c8cd94540f2f30a6c002f0aace1d317955", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z002_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z036_t001_NucleiLabels.tiff": "4fa967c78709aa400246a50528a36b24ca366e825dd0e64561643369e814906b", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z039_t001_NucleiLabels.tiff": "f93e8293a37886a36844b8748083a1f8e717efab092652755263e797f12f0364", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z044_t001_CellsLabels.tiff": "e03946d94c789ee787cc29a3c8f5e5d9c56b37a0044b297ba8f403abc98eb55e", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z025_t001_CellsLabels.tiff": "03aabe5528198cfd6ec23a058dab540fb8be0870207c6dc94a62d9b621d1f2f9", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z045_t001_NucleiLabels.tiff": "94bd6a65795ad6395cc18a4fd63dc591354bef612ee49fffd2d9f63e4ed9db84", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z026_t001_CellsLabels.tiff": "5b433d931fa6637fc793b23880199c01000ef6d9e610903bb38bb8cea5574563", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z016_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z027_t001_NucleiLabels.tiff": "8c8f402d3a7c65c04e7f1575dd34de0427eeeb433ff2956d20e07d06eb48fb8d", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z018_t001_NucleiLabels.tiff": "03ba7a4d6ae53e6c84aab5926ae1e2592b5f8ca24a839a256c82f08544df4270", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z010_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z040_t001_CellsLabels.tiff": "1635ea20dd80ce99f0e86494666825a52a66645837d598fd3a3fa91f0bb77d53", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z013_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z003_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z024_t001_CellsLabels.tiff": "39899adabca74b10b9950d1f97e8b0b3bbaf136d75e7a46ef4a6e86896e3119f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z014_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z007_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z002_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z056_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z042_t001_NucleiLabels.tiff": "e07252ca1a55c5195c99666d8d3f0d70db636b4ef4b99e1a0049ca22b182d87a", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z017_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z025_t001_NucleiLabels.tiff": "9cc33676a0177130609ab1331feab1722bbde8d55b2f49031730fd5a2a40029c", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z031_t001_CellsLabels.tiff": "1b14aab71bf3623458ea9373852b0baf28be9727877b32f894407927ecfb6d65", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z035_t001_CellsLabels.tiff": "eedeb489b46abf1eafbd9c81491cb3f85ce1cb3ce877844ad22e72547fc3c438", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z021_t001_NucleiLabels.tiff": "71a10b0539f3fc4cba600b275b7e948258c6a435ba33ef522faa0869ac085518", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z019_t001_NucleiLabels.tiff": "250cb56a8054c28cea894bc3d9edaddcdce01f5151a05da78da20157c3c3be9f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z043_t001_CellsLabels.tiff": "bd0df007524ea4e555e4d2fdbebe6a274caa03bc360a1cf90d601659155984b2", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z001_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z059_t001_NucleiLabels.tiff": "76c785cf0f481c73ad3cf34ccb576eaa19dd1bdd9c74f9eaf92adfbde5d44b97", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z048_t001_CellsLabels.tiff": "133a58f6fac8b07f79bfed9634867ccb5428b83e63682031a1def16c6a694568", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z005_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z058_t001_NucleiLabels.tiff": "a37c63c6052e40d73db918a014e6f2a18be4d0cbfbeeef65a9c26096a38a4c09", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z013_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z037_t001_CellsLabels.tiff": "aa1f449f1769b27cd9f1076e70f49f0242e2549f288a77984ca159cb07cf6a40", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z004_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z030_t001_CellsLabels.tiff": "b71a2e05d270ae0cd237d6c0d97ea82a63e5ad3633723e7255c35d34e585ec80", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z054_t001_NucleiLabels.tiff": "b841c5d901c65a6138d9ff2b0ec4aa04f00765b1dd21bcd6fc9f8dc974c6035d", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z023_t001_NucleiLabels.tiff": "ecb7f78c8ca0bad2b9d0495659609b9023781b6a936d6afb0a04653eac176555", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z053_t001_CellsLabels.tiff": "ff17f538baaf179752412026a250bdbee016e3101037bf4e0dbb81199fc4c8ed", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z034_t001_CellsLabels.tiff": "3f6c1d8cdbce20aac8f70e7315bea70b147b7b6a034432845b27eeb6cfba083c", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z045_t001_CellsLabels.tiff": "c678f2100c7431e5a43c391af86a79e3436d4cf13f050f98c25f62682e4632fe", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z034_t001_NucleiLabels.tiff": "08af61857cfb97fd95fcb08d1ce18139ce7087b192044c63452bee533c5b1aa4", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z009_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z028_t001_NucleiLabels.tiff": "1676f21bd3b7e105601c83f206890507c32ba2447988be2a75d0e8153d069b65", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z012_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z030_t001_NucleiLabels.tiff": "c54c8b98a5c38a47e0cf9391a1afab09b4c8c58e06294cdb650a15add4a32b58", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z049_t001_CellsLabels.tiff": "28d83dfbf5bbf7e4da9c3e4806353e9fd0a89680dcdab4b43ca2fd19b80f669c" + }, + "receipt": "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/ordinary_run_evidence/cp_tutorial_3d_monolayer-1w_1t-1791010195055995834/measured_pipeline_receipt.json", + "receipt_sha256": "4fd3a5d08663ac900fef96a286b12329bf8eac5eb10c0bc1d015cf1b3feb390b" + }, + { + "repeat": 1, + "output_roots": [ + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput" + ], + "file_count": 128, + "file_sha256": { + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/openhcs_metadata.json": "2b1c063d1583e1eacc2c087e956acb68f69b4554cd8d04c0d3e4dd4d641053e4", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/openhcs_metadata.json.lock": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_erodedResizedNuclei.csv": "cca23f86a85bb3ee7a5c3a1d174c34b5f026ca7f399be98458e1a8c6e12d5c77", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_Cells.csv": "6f6e7431171ed5ea73a01784105147eca0e43a6e3e41f8720df93106353c9c79", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_downsizedNuclei.csv": "a7b998c5a524e11747fc5452fd763fc3d9abe16f46e961f62e09e9393b6a8adf", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_Nuclei.csv": "17500f5d4321bbf255df31f7a3503339029c2b077045a6c10e4d4f6f0db9ad1e", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_Image.csv": "75ac32ff5ef57bc285ec4eae1c9b501c27a35b486bf35435af5d499d856d34a4", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_erodedDownsizedNuclei.csv": "ee507fb9c2387f86fc768184baf2596ebe1f8e40f7fc5eb24f252345fee63ae6", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z052_t001_NucleiLabels.tiff": "0daf51526658e23b7c9ab8872e992ebe9322399286f95741d0ccdbd357e5d631", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z008_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z060_t001_NucleiLabels.tiff": "7cc3c6c6a22f558e62194277090bdea2deb79475dd91fe7d0e49eac3c0187388", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z015_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z057_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z021_t001_CellsLabels.tiff": "daa31cb1e537e5c32b1936ab7647c5690741d2bc1d0e15c60b59ba1310a8c0ce", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z047_t001_CellsLabels.tiff": "b926aa64e1fa8aa52279a69d91dc23cf14bf0ae76107f9b360c7ae77bdf53719", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z022_t001_NucleiLabels.tiff": "50aef20e28a2f9e18ca2e28eabf6d87ec2ad20c79898e56f8d63a959ba5bb507", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z027_t001_CellsLabels.tiff": "ee9056587a19e581f5feb6078b41fa1004d923a5ddf1725aefc4b8b8c5201ad6", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z029_t001_NucleiLabels.tiff": "34bce0f06f674e20667554eed0075fde8d9d33c9c0eaeffd88a1c558499cc432", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z007_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z018_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z047_t001_NucleiLabels.tiff": "995d5ce77cbf61a9be8590f34987ef3f1dd698ae6ed393cac4d0960bc9e7a66a", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z046_t001_CellsLabels.tiff": "dc081df050be6cd19b5d5bbfe21c96c350eecf9bc6f10f608dc361a36056632a", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z042_t001_CellsLabels.tiff": "88d2e9964455459499656c4f0a762cd86376c678756635aaa6b6c8d98be01852", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z024_t001_NucleiLabels.tiff": "9e96a05ed9860cac9c57c618db2f4fe5e4b769a9d235aac6fd1d628486317df0", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z035_t001_NucleiLabels.tiff": "13e69afc324152bb91329fa758d131d849127e8fd346078d94c6adc25582b3a6", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z055_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z050_t001_CellsLabels.tiff": "ea7654a6fe1f92ad274a54935420923eec4bec5ad030187afc0ac7af02690af5", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z014_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z031_t001_NucleiLabels.tiff": "247490c261fecc26c7379ab2eebaa01e4bea523218b3cc5491d2fe2b9aadfe11", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z020_t001_NucleiLabels.tiff": "b0cd031b448d8bd2075e65710bd76c064161b25242f85ee150da7cd139fcc1a8", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z051_t001_CellsLabels.tiff": "73483bd10a0f37990084013ad054768086d6d3c929da6bc7e74f4aee37e9dbf2", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z028_t001_CellsLabels.tiff": "fef31040e0c54501c3bf965eb98f055cdb1d42c96f91cf1c239033d0a9a06534", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z022_t001_CellsLabels.tiff": "7d7ec170ed3652efa99e43d8db546c51b696821dca7727d0ff4d2f2ead7fdbaa", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z033_t001_CellsLabels.tiff": "a320b99ee129d97b99f9fac66aabb89fc1652930b3bb8b2b3fbba7b57e994f85", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z038_t001_CellsLabels.tiff": "6ac8fd9c95acc1f261e96a8fb9332e5d6a403548842f9b85ecb977e6213b4c9b", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z051_t001_NucleiLabels.tiff": "f0f36ccaf744d42da036948682cfd226e6dd4a3cfb94aed38729b2d25ede5dff", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z023_t001_CellsLabels.tiff": "2f06989618050934c73473f9342c2fe3410fd834a0ddb976cf602fe6198db7a6", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z010_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z004_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z046_t001_NucleiLabels.tiff": "c1250e060d32f2cfc07fc7e1594fdb1d6cbb4d0483fa94a8113efa695397963c", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z003_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z026_t001_NucleiLabels.tiff": "ad22809ff3f43d15c838f697061c263070dbf1da1031da39900681cdfbf9d54c", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z054_t001_CellsLabels.tiff": "25ff2ec586a568e42286f73713d64aab1208c38c016099299211f04368ac49c8", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z039_t001_CellsLabels.tiff": "22fa79a76e64013f10373be2ca2af98a2173275c5d43c9cad33135acdc376d14", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z057_t001_NucleiLabels.tiff": "695517641e9fe4b8421d4a7db56b24c755b6957c98ce32bb24ad5b25f52661eb", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z020_t001_CellsLabels.tiff": "e8105a4f04198ac75de9831247030084d6f65bf98691d7444ead7e178072fa22", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z008_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z037_t001_NucleiLabels.tiff": "927d52441c57cb3a4014effaca3e3c23c76275fb264c26e1942a9c58b85ee6d2", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z011_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z043_t001_NucleiLabels.tiff": "81a705d05b0a5bfa8783df28990c1d70634c1f4aa2bca23e02cfa8a4bc861b86", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z015_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z041_t001_NucleiLabels.tiff": "744eab7cd170d5af8d2f72ced3893dd939c4e1b0f415afab799a37ded5d046c7", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z060_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z055_t001_NucleiLabels.tiff": "44e02416cc94a70528165a4b6c42b9e19b205f9c91528076796272cb1a8504ea", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z033_t001_NucleiLabels.tiff": "512a3a0ed8025d14b0556a72ebfc13b173d14f7046817ca09876242ab63b9d6b", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z050_t001_NucleiLabels.tiff": "565b4f08f7503086c1f2f8e631d19063129e02a5767d2f979c0cec8fdec23601", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z009_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z036_t001_CellsLabels.tiff": "28b68d1300cf7b08e21dbcd4858447862520dded5c27f2d84cf223513a119eed", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z049_t001_NucleiLabels.tiff": "e92e493078672a0e0d90e31409612e215bf8486300d5c765d0e0504ff8888e7a", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z040_t001_NucleiLabels.tiff": "e0bd66b166e9f92120dbee183973849eda0afbbe6fed9d03fc4cfd2c56a96280", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z016_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z012_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z001_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z053_t001_NucleiLabels.tiff": "b8613192175a4615b58aed607474657668429cb6ac635cd012569b19667f2d3d", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z017_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z006_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z032_t001_CellsLabels.tiff": "8a2c5774044326ae722e41aae517266197fbebb0e4dd5eab4b22414fbfea309f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z032_t001_NucleiLabels.tiff": "97b09fe5444ad439c3c2007d75971dce2766d94c93268eccfee9ae49b8a04aa1", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z058_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z048_t001_NucleiLabels.tiff": "325478230529e94ba372f38f0bf512292f131d337b6ac09ce5a91b0266724e02", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z006_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z038_t001_NucleiLabels.tiff": "e2852ea83c62ab30d77681b4e39f88d3cee74ae052f1c948675ae3581c9fc28a", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z052_t001_CellsLabels.tiff": "7bb98f0587062e01b965e486fc687872651d7d90d271a2fdf0d35d67b464b233", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z041_t001_CellsLabels.tiff": "be7762756eb65f08cb337904165c6f43b5351f39405461f65ad28669abfe3899", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z005_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z011_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z044_t001_NucleiLabels.tiff": "a78264f2f5e245a3913a69f89105e94df30048b1aaef4eb80fc5c3235e78b196", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z029_t001_CellsLabels.tiff": "b3bdde5f751b8f5d41ca8b5219a3427e450cb1b8795401d0ec638940d1ad7bd9", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z059_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z056_t001_NucleiLabels.tiff": "ca0723c8b9764d5b77034719bb75a2940be35da2e04119af4c0e6d3552c08718", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z019_t001_CellsLabels.tiff": "ec6ef1d319f2732c47b06f5cf02775c8cd94540f2f30a6c002f0aace1d317955", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z002_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z036_t001_NucleiLabels.tiff": "4fa967c78709aa400246a50528a36b24ca366e825dd0e64561643369e814906b", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z039_t001_NucleiLabels.tiff": "f93e8293a37886a36844b8748083a1f8e717efab092652755263e797f12f0364", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z044_t001_CellsLabels.tiff": "e03946d94c789ee787cc29a3c8f5e5d9c56b37a0044b297ba8f403abc98eb55e", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z025_t001_CellsLabels.tiff": "03aabe5528198cfd6ec23a058dab540fb8be0870207c6dc94a62d9b621d1f2f9", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z045_t001_NucleiLabels.tiff": "94bd6a65795ad6395cc18a4fd63dc591354bef612ee49fffd2d9f63e4ed9db84", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z026_t001_CellsLabels.tiff": "5b433d931fa6637fc793b23880199c01000ef6d9e610903bb38bb8cea5574563", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z016_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z027_t001_NucleiLabels.tiff": "8c8f402d3a7c65c04e7f1575dd34de0427eeeb433ff2956d20e07d06eb48fb8d", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z018_t001_NucleiLabels.tiff": "03ba7a4d6ae53e6c84aab5926ae1e2592b5f8ca24a839a256c82f08544df4270", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z010_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z040_t001_CellsLabels.tiff": "1635ea20dd80ce99f0e86494666825a52a66645837d598fd3a3fa91f0bb77d53", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z013_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z003_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z024_t001_CellsLabels.tiff": "39899adabca74b10b9950d1f97e8b0b3bbaf136d75e7a46ef4a6e86896e3119f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z014_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z007_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z002_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z056_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z042_t001_NucleiLabels.tiff": "e07252ca1a55c5195c99666d8d3f0d70db636b4ef4b99e1a0049ca22b182d87a", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z017_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z025_t001_NucleiLabels.tiff": "9cc33676a0177130609ab1331feab1722bbde8d55b2f49031730fd5a2a40029c", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z031_t001_CellsLabels.tiff": "1b14aab71bf3623458ea9373852b0baf28be9727877b32f894407927ecfb6d65", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z035_t001_CellsLabels.tiff": "eedeb489b46abf1eafbd9c81491cb3f85ce1cb3ce877844ad22e72547fc3c438", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z021_t001_NucleiLabels.tiff": "71a10b0539f3fc4cba600b275b7e948258c6a435ba33ef522faa0869ac085518", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z019_t001_NucleiLabels.tiff": "250cb56a8054c28cea894bc3d9edaddcdce01f5151a05da78da20157c3c3be9f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z043_t001_CellsLabels.tiff": "bd0df007524ea4e555e4d2fdbebe6a274caa03bc360a1cf90d601659155984b2", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z001_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z059_t001_NucleiLabels.tiff": "76c785cf0f481c73ad3cf34ccb576eaa19dd1bdd9c74f9eaf92adfbde5d44b97", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z048_t001_CellsLabels.tiff": "133a58f6fac8b07f79bfed9634867ccb5428b83e63682031a1def16c6a694568", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z005_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z058_t001_NucleiLabels.tiff": "a37c63c6052e40d73db918a014e6f2a18be4d0cbfbeeef65a9c26096a38a4c09", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z013_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z037_t001_CellsLabels.tiff": "aa1f449f1769b27cd9f1076e70f49f0242e2549f288a77984ca159cb07cf6a40", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z004_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z030_t001_CellsLabels.tiff": "b71a2e05d270ae0cd237d6c0d97ea82a63e5ad3633723e7255c35d34e585ec80", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z054_t001_NucleiLabels.tiff": "b841c5d901c65a6138d9ff2b0ec4aa04f00765b1dd21bcd6fc9f8dc974c6035d", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z023_t001_NucleiLabels.tiff": "ecb7f78c8ca0bad2b9d0495659609b9023781b6a936d6afb0a04653eac176555", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z053_t001_CellsLabels.tiff": "ff17f538baaf179752412026a250bdbee016e3101037bf4e0dbb81199fc4c8ed", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z034_t001_CellsLabels.tiff": "3f6c1d8cdbce20aac8f70e7315bea70b147b7b6a034432845b27eeb6cfba083c", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z045_t001_CellsLabels.tiff": "c678f2100c7431e5a43c391af86a79e3436d4cf13f050f98c25f62682e4632fe", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z034_t001_NucleiLabels.tiff": "08af61857cfb97fd95fcb08d1ce18139ce7087b192044c63452bee533c5b1aa4", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z009_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z028_t001_NucleiLabels.tiff": "1676f21bd3b7e105601c83f206890507c32ba2447988be2a75d0e8153d069b65", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z012_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z030_t001_NucleiLabels.tiff": "c54c8b98a5c38a47e0cf9391a1afab09b4c8c58e06294cdb650a15add4a32b58", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z049_t001_CellsLabels.tiff": "28d83dfbf5bbf7e4da9c3e4806353e9fd0a89680dcdab4b43ca2fd19b80f669c" + }, + "receipt": "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/ordinary_run_evidence/cp_tutorial_3d_monolayer-1w_1t-1791010233174646417/measured_pipeline_receipt.json", + "receipt_sha256": "f2f35969c4e153c03e7b921da2503929020c1ecce2d64bbaa49c04033fe1494a" + } + ] + }, + "ExampleSpeckles": { + "ordinary_execution_seconds": [ + 1.0937919616699219, + 1.3689088821411133 + ], + "ordinary_total_seconds": [ + 2.189194314647466, + 2.302979093976319 + ], + "native_invocation_seconds": [ + 1.9962371489964426, + 1.9122227132320404 + ], + "means_seconds": { + "openhcs_execute": 1.2313504219055176, + "openhcs_total": 2.2460867043118924, + "openhcs_compile": 0.6634922027587891, + "native_invocation": 1.9542299311142415 + }, + "descriptive_native_over_openhcs_execute": 1.5870623799276133, + "descriptive_native_over_openhcs_total": 0.870059881198102, + "remaining_seconds_to_2x_execution": 0.25423545634839684, + "remaining_seconds_to_2x_total": 1.2689717387547716, + "actual_source_refs": 2, + "actual_native_image_sets": 1, + "native_environment": { + "python_executable": "/home/ts/code/projects/openhcs/.venv-cellprofiler39/bin/python", + "python_version": "3.9.25", + "cellprofiler_version": "4.2.8.1", + "cellprofiler_core_version": "4.2.8.1", + "numpy_version": "1.24.4", + "scipy_version": "1.9.0", + "temporary_root": "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/ExampleSpeckles/native_tmp" + }, + "science_comparisons": [ + { + "ordinary_repeat": 0, + "native_repeat": 0, + "tables": [ + 3, + 3 + ], + "physical_images": [ + 0, + 0 + ], + "logical_images": [ + 0, + 0 + ], + "known_relationship_keys": [ + 1, + 1 + ] + }, + { + "ordinary_repeat": 0, + "native_repeat": 1, + "tables": [ + 3, + 3 + ], + "physical_images": [ + 0, + 0 + ], + "logical_images": [ + 0, + 0 + ], + "known_relationship_keys": [ + 1, + 1 + ] + }, + { + "ordinary_repeat": 1, + "native_repeat": 0, + "tables": [ + 3, + 3 + ], + "physical_images": [ + 0, + 0 + ], + "logical_images": [ + 0, + 0 + ], + "known_relationship_keys": [ + 1, + 1 + ] + }, + { + "ordinary_repeat": 1, + "native_repeat": 1, + "tables": [ + 3, + 3 + ], + "physical_images": [ + 0, + 0 + ], + "logical_images": [ + 0, + 0 + ], + "known_relationship_keys": [ + 1, + 1 + ] + } + ], + "raw_range_seconds": { + "native": [ + 1.9122227132320404, + 1.9962371489964426 + ], + "openhcs_execute": [ + 1.0937919616699219, + 1.3689088821411133 + ], + "openhcs_total": [ + 2.189194314647466, + 2.302979093976319 + ] + }, + "descriptive_ratio_range": { + "native_over_execute": [ + 1.3968955408055566, + 1.8250610892666765 + ], + "native_over_total": [ + 0.8303256934609861, + 0.9118592788406287 + ] + }, + "complete_ordinary_physical_inventories": [ + { + "repeat": 0, + "output_roots": [ + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/ExampleSpeckles/wells_1/workers_1/images_ExampleSpeckles_source_workspace_well_throughput" + ], + "file_count": 5, + "file_sha256": { + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/ExampleSpeckles/wells_1/workers_1/images_ExampleSpeckles_source_workspace_well_throughput/openhcs_metadata.json": "71582c62cabb2571ebf8a0de57f92fdf82c55a7ece3dac0bf2a6eca95cad84c9", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/ExampleSpeckles/wells_1/workers_1/images_ExampleSpeckles_source_workspace_well_throughput/openhcs_metadata.json.lock": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/ExampleSpeckles/wells_1/workers_1/images_ExampleSpeckles_source_workspace_well_throughput/results/Nuclei.csv": "e50cb9f8c23f65e9ce8db25132263cf4a4448ed65fc1088d782536abf74c5670", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/ExampleSpeckles/wells_1/workers_1/images_ExampleSpeckles_source_workspace_well_throughput/results/h2ax.csv": "eb453565f3b79ac932eeb5cde8605888b72e58c80f42edbbf0b1ba37f34fd6f8", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/ExampleSpeckles/wells_1/workers_1/images_ExampleSpeckles_source_workspace_well_throughput/results/Image.csv": "85ef16f24e75f03dc7055215cf2e6cb196b1dc0a0d73713a4c5999f698d4f96e" + }, + "receipt": "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/ExampleSpeckles/wells_1/workers_1/ordinary_run_evidence/ExampleSpeckles-1w_1t-1791010191010518862/measured_pipeline_receipt.json", + "receipt_sha256": "cda421a4fe86662f7be512585425e834739b510b6e0b31e8396f28ed2e123c16" + }, + { + "repeat": 1, + "output_roots": [ + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/ExampleSpeckles/wells_1/workers_1/images_ExampleSpeckles_source_workspace_well_throughput" + ], + "file_count": 5, + "file_sha256": { + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/ExampleSpeckles/wells_1/workers_1/images_ExampleSpeckles_source_workspace_well_throughput/openhcs_metadata.json": "71582c62cabb2571ebf8a0de57f92fdf82c55a7ece3dac0bf2a6eca95cad84c9", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/ExampleSpeckles/wells_1/workers_1/images_ExampleSpeckles_source_workspace_well_throughput/openhcs_metadata.json.lock": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/ExampleSpeckles/wells_1/workers_1/images_ExampleSpeckles_source_workspace_well_throughput/results/Nuclei.csv": "e50cb9f8c23f65e9ce8db25132263cf4a4448ed65fc1088d782536abf74c5670", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/ExampleSpeckles/wells_1/workers_1/images_ExampleSpeckles_source_workspace_well_throughput/results/h2ax.csv": "eb453565f3b79ac932eeb5cde8605888b72e58c80f42edbbf0b1ba37f34fd6f8", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/ExampleSpeckles/wells_1/workers_1/images_ExampleSpeckles_source_workspace_well_throughput/results/Image.csv": "85ef16f24e75f03dc7055215cf2e6cb196b1dc0a0d73713a4c5999f698d4f96e" + }, + "receipt": "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/ExampleSpeckles/wells_1/workers_1/ordinary_run_evidence/ExampleSpeckles-1w_1t-1791010228935841467/measured_pipeline_receipt.json", + "receipt_sha256": "52bfa7d40c01a6f5405d57bb6d3d6652c4c592746a50b35677cdec241045e868" + } + ] + } + }, + "source_env_native_input_sparse_freeze": "PASS before and after all original actions; original V1 missing-new-sparse-doc failure retained. V2 exact new Git-blob supplement with unchanged inventory validator.", + "science": "8 complete comparisons; strict1e-6 measurement/float policy, exact3Duint16 image policy, known cross-output relationship correlations and full scientific physical inventory; no exclusions, aliases or fallback.", + "clocks": "Existing ordinary public driver/defaultOUTCOMES/defaultRSS; READY startup and shutdown outside OH clocks. Unchanged native warm batch excludes interpreter/import/JVM/load and warmup-1, invocation includes prepare/modules/post_run/close. CPU5 native thread1; prepare/readersCPU3.", + "limits": "Two observations per workload are descriptive, not statistical significance or a causal code gain. Current branch/shared dependencies differ from earlier evidence; no attribution of changes. This is not full30 acceptance or broad R0/R1 waiver.", + "generated_files_bytes": 37827386, + "generated_budget_bytes": 157286400, + "artifact_sha256": { + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/source-freeze.json": "72e1b4f14c8c7bf03517c8d99329a1a49182fefa4b128d87326abd5b3ed829b8", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/strict-science.json": "77ab1a1e88d52502c633b646efe0d032ad87f117fc403298cb8b5742a0510190", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-preparation-v2/controller.py": "b772d231196eaf23d9a150ffa491f95426cb6e074470ac3febd508afd1d3ad18", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-preparation-v2/prepare.log": "33eba2651dcf37602dc55b42146f8beff10526831b86583b96b9ddf26d4f67d3", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-preparation-v2/ordinary.log": "20e43f8c2295b7d987c18fefee0d628817bfc47828f643148dc659d99dbeeed3", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-preparation-v2/native.log": "db94eaf7177b9367684a661ccfc6a801348054bfa3505a20ee01b22c030108d7", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-preparation-v2/compare.log": "36f725536f8eae4e5c286d5763031fbafdf2bf880b73557248cfb03692c98b97", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/well_throughput.csv": "43680d83b0df5de1391448655370b1bd013a5b70581741689bc2b3d6cac9db3f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/well_throughput.csv": "54d24ecc9904bafc59405692862769ccbc11b663eae8420d24e2e77886d0bac3", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/native_report.json": "f1f5804eaef6af43b73f0608071ff57c62691bbd9172b33fa45ca633a4593654", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/ExampleSpeckles/native_report.json": "25f17f873f96555efb451ecbd39ce03545fb722aaca66a27b64a199fc8e67beb" + }, + "handles": { + "prepare12999": "TERMINAL0", + "ordinary24312": "TERMINAL0", + "native96389": "TERMINAL0", + "compare44367": "TERMINAL0" + }, + "source_state_unchanged": true, + "home_free_bytes": 75883118592, + "var_tmp_free_bytes": 537546752, + "source_freeze_sha256": "72e1b4f14c8c7bf03517c8d99329a1a49182fefa4b128d87326abd5b3ed829b8", + "strict_science_sha256": "77ab1a1e88d52502c633b646efe0d032ad87f117fc403298cb8b5742a0510190", + "parent_custody_paths": { + "source_env_inputs_helpers": "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/source-freeze.json", + "all_compared_output_hashes": "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/strict-science.json", + "original_sparse_prepare_failure": "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-preparation-v1/prepare.log", + "new_current_docs_exact_Git_custody": "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-preparation-v2/sparse-supplement/preservation.json" + }, + "release": "All four owned action handles terminal0; no resident graph or benchmark/reader process remains. Source/environment/helper freeze released now. All generated scientific outputs retained immutable; no further benchmark/capture authorized by this completed protocol.", + "figures": "Separate parent-owned existing generator preview pending; no figure hashes yet. Full30/scaling qualification unfinished." +} diff --git a/benchmark/results/perf_current_native_paired_20261003/strict-science.json b/benchmark/results/perf_current_native_paired_20261003/strict-science.json new file mode 100644 index 0000000000..49c5db3d62 --- /dev/null +++ b/benchmark/results/perf_current_native_paired_20261003/strict-science.json @@ -0,0 +1,795 @@ +{ + "status": "PASS", + "comparisons": [ + { + "case": "cp_tutorial_3d_monolayer", + "ordinary_repeat": 0, + "native_repeat": 0, + "measurement_differences": 0, + "image_differences": 0, + "tables": [ + 7, + 6 + ], + "physical_images": [ + 2, + 120 + ], + "logical_images": [ + 2, + 2 + ], + "known_relationship_keys": [ + 3, + 3 + ], + "ordinary_receipt_sha256": "4fd3a5d08663ac900fef96a286b12329bf8eac5eb10c0bc1d015cf1b3feb390b", + "output_hashes": { + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_xy1_ch2_NucleiLabels.tiff": "340bbd795541e4031749d1fff767fa4fcdcac10f5b97879056b25dac9fd7674b", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_erodedResizedNuclei.csv": "66a58f78cab0cfdbe907468e8ba383fc59fab49025b84b589855bdfa04854609", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_Experiment.csv": "c05f0a11f43a5bda943e9f739ff257216f8ab3f65d8988a0d3d0c612c1db6997", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_downsizedNuclei.csv": "2c66bc7b17de52147d5a875403f9a9746019ac61371c08157979044c6ed818dd", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_Cells.csv": "cc34681db8a66c38ca77bc47db496b26703f21e4d7c7628dd74e30c33250859f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_Nuclei.csv": "440c33edc7e8cb5611910a835454db62390301a7af617a542349f60aece57561", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_erodedDownsizedNuclei.csv": "527a0cce497d2a618478be84d1a0bf174049f58aa2a666e37626a990fdca0116", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_Image.csv": "1f3dec653b6909c8a81ac109ce1faf91380886ac9a18e56fafc91102d419904e", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_xy1_ch0_CellsLabels.tiff": "aaed9ed23179a162ee7f5a7e8d1d88f1fa342ca55172b7fccd4cbf67f7841d81", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z015_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z048_t001_NucleiLabels.tiff": "325478230529e94ba372f38f0bf512292f131d337b6ac09ce5a91b0266724e02", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z011_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z042_t001_NucleiLabels.tiff": "e07252ca1a55c5195c99666d8d3f0d70db636b4ef4b99e1a0049ca22b182d87a", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_Nuclei.csv": "17500f5d4321bbf255df31f7a3503339029c2b077045a6c10e4d4f6f0db9ad1e", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z048_t001_CellsLabels.tiff": "133a58f6fac8b07f79bfed9634867ccb5428b83e63682031a1def16c6a694568", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z046_t001_NucleiLabels.tiff": "c1250e060d32f2cfc07fc7e1594fdb1d6cbb4d0483fa94a8113efa695397963c", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z029_t001_CellsLabels.tiff": "b3bdde5f751b8f5d41ca8b5219a3427e450cb1b8795401d0ec638940d1ad7bd9", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z017_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z010_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z003_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z037_t001_NucleiLabels.tiff": "927d52441c57cb3a4014effaca3e3c23c76275fb264c26e1942a9c58b85ee6d2", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z031_t001_NucleiLabels.tiff": "247490c261fecc26c7379ab2eebaa01e4bea523218b3cc5491d2fe2b9aadfe11", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z049_t001_NucleiLabels.tiff": "e92e493078672a0e0d90e31409612e215bf8486300d5c765d0e0504ff8888e7a", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z052_t001_NucleiLabels.tiff": "0daf51526658e23b7c9ab8872e992ebe9322399286f95741d0ccdbd357e5d631", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z038_t001_NucleiLabels.tiff": "e2852ea83c62ab30d77681b4e39f88d3cee74ae052f1c948675ae3581c9fc28a", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z047_t001_NucleiLabels.tiff": "995d5ce77cbf61a9be8590f34987ef3f1dd698ae6ed393cac4d0960bc9e7a66a", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z021_t001_CellsLabels.tiff": "daa31cb1e537e5c32b1936ab7647c5690741d2bc1d0e15c60b59ba1310a8c0ce", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z022_t001_NucleiLabels.tiff": "50aef20e28a2f9e18ca2e28eabf6d87ec2ad20c79898e56f8d63a959ba5bb507", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z012_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z005_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z059_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z028_t001_CellsLabels.tiff": "fef31040e0c54501c3bf965eb98f055cdb1d42c96f91cf1c239033d0a9a06534", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z008_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z009_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z019_t001_CellsLabels.tiff": "ec6ef1d319f2732c47b06f5cf02775c8cd94540f2f30a6c002f0aace1d317955", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z054_t001_NucleiLabels.tiff": "b841c5d901c65a6138d9ff2b0ec4aa04f00765b1dd21bcd6fc9f8dc974c6035d", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z035_t001_CellsLabels.tiff": "eedeb489b46abf1eafbd9c81491cb3f85ce1cb3ce877844ad22e72547fc3c438", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z024_t001_NucleiLabels.tiff": "9e96a05ed9860cac9c57c618db2f4fe5e4b769a9d235aac6fd1d628486317df0", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z026_t001_NucleiLabels.tiff": "ad22809ff3f43d15c838f697061c263070dbf1da1031da39900681cdfbf9d54c", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z050_t001_NucleiLabels.tiff": "565b4f08f7503086c1f2f8e631d19063129e02a5767d2f979c0cec8fdec23601", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z045_t001_CellsLabels.tiff": "c678f2100c7431e5a43c391af86a79e3436d4cf13f050f98c25f62682e4632fe", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z012_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z040_t001_CellsLabels.tiff": "1635ea20dd80ce99f0e86494666825a52a66645837d598fd3a3fa91f0bb77d53", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z023_t001_NucleiLabels.tiff": "ecb7f78c8ca0bad2b9d0495659609b9023781b6a936d6afb0a04653eac176555", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z035_t001_NucleiLabels.tiff": "13e69afc324152bb91329fa758d131d849127e8fd346078d94c6adc25582b3a6", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z011_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z002_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z018_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z007_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z034_t001_CellsLabels.tiff": "3f6c1d8cdbce20aac8f70e7315bea70b147b7b6a034432845b27eeb6cfba083c", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z039_t001_CellsLabels.tiff": "22fa79a76e64013f10373be2ca2af98a2173275c5d43c9cad33135acdc376d14", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z059_t001_NucleiLabels.tiff": "76c785cf0f481c73ad3cf34ccb576eaa19dd1bdd9c74f9eaf92adfbde5d44b97", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z029_t001_NucleiLabels.tiff": "34bce0f06f674e20667554eed0075fde8d9d33c9c0eaeffd88a1c558499cc432", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z044_t001_NucleiLabels.tiff": "a78264f2f5e245a3913a69f89105e94df30048b1aaef4eb80fc5c3235e78b196", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z046_t001_CellsLabels.tiff": "dc081df050be6cd19b5d5bbfe21c96c350eecf9bc6f10f608dc361a36056632a", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z034_t001_NucleiLabels.tiff": "08af61857cfb97fd95fcb08d1ce18139ce7087b192044c63452bee533c5b1aa4", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_Image.csv": "75ac32ff5ef57bc285ec4eae1c9b501c27a35b486bf35435af5d499d856d34a4", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z015_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/openhcs_metadata.json": "df996ea7347162652903028922a9ac2206dd2d6dbecbf9fb6da05a7e65ece370", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z057_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z045_t001_NucleiLabels.tiff": "94bd6a65795ad6395cc18a4fd63dc591354bef612ee49fffd2d9f63e4ed9db84", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z055_t001_NucleiLabels.tiff": "44e02416cc94a70528165a4b6c42b9e19b205f9c91528076796272cb1a8504ea", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_erodedResizedNuclei.csv": "cca23f86a85bb3ee7a5c3a1d174c34b5f026ca7f399be98458e1a8c6e12d5c77", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z005_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z028_t001_NucleiLabels.tiff": "1676f21bd3b7e105601c83f206890507c32ba2447988be2a75d0e8153d069b65", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z051_t001_CellsLabels.tiff": "73483bd10a0f37990084013ad054768086d6d3c929da6bc7e74f4aee37e9dbf2", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z043_t001_NucleiLabels.tiff": "81a705d05b0a5bfa8783df28990c1d70634c1f4aa2bca23e02cfa8a4bc861b86", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z027_t001_NucleiLabels.tiff": "8c8f402d3a7c65c04e7f1575dd34de0427eeeb433ff2956d20e07d06eb48fb8d", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z042_t001_CellsLabels.tiff": "88d2e9964455459499656c4f0a762cd86376c678756635aaa6b6c8d98be01852", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z003_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z025_t001_NucleiLabels.tiff": "9cc33676a0177130609ab1331feab1722bbde8d55b2f49031730fd5a2a40029c", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/openhcs_metadata.json.lock": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z033_t001_CellsLabels.tiff": "a320b99ee129d97b99f9fac66aabb89fc1652930b3bb8b2b3fbba7b57e994f85", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z057_t001_NucleiLabels.tiff": "695517641e9fe4b8421d4a7db56b24c755b6957c98ce32bb24ad5b25f52661eb", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z006_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z038_t001_CellsLabels.tiff": "6ac8fd9c95acc1f261e96a8fb9332e5d6a403548842f9b85ecb977e6213b4c9b", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z044_t001_CellsLabels.tiff": "e03946d94c789ee787cc29a3c8f5e5d9c56b37a0044b297ba8f403abc98eb55e", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z014_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z041_t001_NucleiLabels.tiff": "744eab7cd170d5af8d2f72ced3893dd939c4e1b0f415afab799a37ded5d046c7", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z007_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z054_t001_CellsLabels.tiff": "25ff2ec586a568e42286f73713d64aab1208c38c016099299211f04368ac49c8", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z013_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z053_t001_NucleiLabels.tiff": "b8613192175a4615b58aed607474657668429cb6ac635cd012569b19667f2d3d", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z051_t001_NucleiLabels.tiff": "f0f36ccaf744d42da036948682cfd226e6dd4a3cfb94aed38729b2d25ede5dff", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z036_t001_NucleiLabels.tiff": "4fa967c78709aa400246a50528a36b24ca366e825dd0e64561643369e814906b", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z016_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z055_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z027_t001_CellsLabels.tiff": "ee9056587a19e581f5feb6078b41fa1004d923a5ddf1725aefc4b8b8c5201ad6", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z024_t001_CellsLabels.tiff": "39899adabca74b10b9950d1f97e8b0b3bbaf136d75e7a46ef4a6e86896e3119f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z013_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z039_t001_NucleiLabels.tiff": "f93e8293a37886a36844b8748083a1f8e717efab092652755263e797f12f0364", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_downsizedNuclei.csv": "a7b998c5a524e11747fc5452fd763fc3d9abe16f46e961f62e09e9393b6a8adf", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z010_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_erodedDownsizedNuclei.csv": "ee507fb9c2387f86fc768184baf2596ebe1f8e40f7fc5eb24f252345fee63ae6", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z004_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z040_t001_NucleiLabels.tiff": "e0bd66b166e9f92120dbee183973849eda0afbbe6fed9d03fc4cfd2c56a96280", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z056_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z058_t001_NucleiLabels.tiff": "a37c63c6052e40d73db918a014e6f2a18be4d0cbfbeeef65a9c26096a38a4c09", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z004_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z018_t001_NucleiLabels.tiff": "03ba7a4d6ae53e6c84aab5926ae1e2592b5f8ca24a839a256c82f08544df4270", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z001_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z049_t001_CellsLabels.tiff": "28d83dfbf5bbf7e4da9c3e4806353e9fd0a89680dcdab4b43ca2fd19b80f669c", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z043_t001_CellsLabels.tiff": "bd0df007524ea4e555e4d2fdbebe6a274caa03bc360a1cf90d601659155984b2", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z031_t001_CellsLabels.tiff": "1b14aab71bf3623458ea9373852b0baf28be9727877b32f894407927ecfb6d65", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z033_t001_NucleiLabels.tiff": "512a3a0ed8025d14b0556a72ebfc13b173d14f7046817ca09876242ab63b9d6b", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z052_t001_CellsLabels.tiff": "7bb98f0587062e01b965e486fc687872651d7d90d271a2fdf0d35d67b464b233", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z016_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z022_t001_CellsLabels.tiff": "7d7ec170ed3652efa99e43d8db546c51b696821dca7727d0ff4d2f2ead7fdbaa", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z041_t001_CellsLabels.tiff": "be7762756eb65f08cb337904165c6f43b5351f39405461f65ad28669abfe3899", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z056_t001_NucleiLabels.tiff": "ca0723c8b9764d5b77034719bb75a2940be35da2e04119af4c0e6d3552c08718", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z032_t001_NucleiLabels.tiff": "97b09fe5444ad439c3c2007d75971dce2766d94c93268eccfee9ae49b8a04aa1", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z014_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z060_t001_NucleiLabels.tiff": "7cc3c6c6a22f558e62194277090bdea2deb79475dd91fe7d0e49eac3c0187388", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z037_t001_CellsLabels.tiff": "aa1f449f1769b27cd9f1076e70f49f0242e2549f288a77984ca159cb07cf6a40", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z001_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_Cells.csv": "6f6e7431171ed5ea73a01784105147eca0e43a6e3e41f8720df93106353c9c79", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z047_t001_CellsLabels.tiff": "b926aa64e1fa8aa52279a69d91dc23cf14bf0ae76107f9b360c7ae77bdf53719", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z025_t001_CellsLabels.tiff": "03aabe5528198cfd6ec23a058dab540fb8be0870207c6dc94a62d9b621d1f2f9", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z026_t001_CellsLabels.tiff": "5b433d931fa6637fc793b23880199c01000ef6d9e610903bb38bb8cea5574563", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z032_t001_CellsLabels.tiff": "8a2c5774044326ae722e41aae517266197fbebb0e4dd5eab4b22414fbfea309f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z053_t001_CellsLabels.tiff": "ff17f538baaf179752412026a250bdbee016e3101037bf4e0dbb81199fc4c8ed", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z020_t001_NucleiLabels.tiff": "b0cd031b448d8bd2075e65710bd76c064161b25242f85ee150da7cd139fcc1a8", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z050_t001_CellsLabels.tiff": "ea7654a6fe1f92ad274a54935420923eec4bec5ad030187afc0ac7af02690af5", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z036_t001_CellsLabels.tiff": "28b68d1300cf7b08e21dbcd4858447862520dded5c27f2d84cf223513a119eed", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z020_t001_CellsLabels.tiff": "e8105a4f04198ac75de9831247030084d6f65bf98691d7444ead7e178072fa22", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z030_t001_NucleiLabels.tiff": "c54c8b98a5c38a47e0cf9391a1afab09b4c8c58e06294cdb650a15add4a32b58", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z060_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z023_t001_CellsLabels.tiff": "2f06989618050934c73473f9342c2fe3410fd834a0ddb976cf602fe6198db7a6", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z030_t001_CellsLabels.tiff": "b71a2e05d270ae0cd237d6c0d97ea82a63e5ad3633723e7255c35d34e585ec80", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z009_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z002_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z006_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z019_t001_NucleiLabels.tiff": "250cb56a8054c28cea894bc3d9edaddcdce01f5151a05da78da20157c3c3be9f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z021_t001_NucleiLabels.tiff": "71a10b0539f3fc4cba600b275b7e948258c6a435ba33ef522faa0869ac085518", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z008_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z058_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z017_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f" + } + }, + { + "case": "cp_tutorial_3d_monolayer", + "ordinary_repeat": 0, + "native_repeat": 1, + "measurement_differences": 0, + "image_differences": 0, + "tables": [ + 7, + 6 + ], + "physical_images": [ + 2, + 120 + ], + "logical_images": [ + 2, + 2 + ], + "known_relationship_keys": [ + 3, + 3 + ], + "ordinary_receipt_sha256": "4fd3a5d08663ac900fef96a286b12329bf8eac5eb10c0bc1d015cf1b3feb390b", + "output_hashes": { + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_xy1_ch2_NucleiLabels.tiff": "340bbd795541e4031749d1fff767fa4fcdcac10f5b97879056b25dac9fd7674b", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_Cells.csv": "cc34681db8a66c38ca77bc47db496b26703f21e4d7c7628dd74e30c33250859f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_erodedDownsizedNuclei.csv": "527a0cce497d2a618478be84d1a0bf174049f58aa2a666e37626a990fdca0116", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_Image.csv": "4cde74a38ef73b892dd9012708a3ac7d71fab206a7903f525b39e84493dcfe17", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_Experiment.csv": "65f1f124e99f13ddc73b71d01732262cd353f24f0bd4697a776b274a39eaecc5", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_Nuclei.csv": "440c33edc7e8cb5611910a835454db62390301a7af617a542349f60aece57561", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_erodedResizedNuclei.csv": "66a58f78cab0cfdbe907468e8ba383fc59fab49025b84b589855bdfa04854609", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_xy1_ch0_CellsLabels.tiff": "aaed9ed23179a162ee7f5a7e8d1d88f1fa342ca55172b7fccd4cbf67f7841d81", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_downsizedNuclei.csv": "2c66bc7b17de52147d5a875403f9a9746019ac61371c08157979044c6ed818dd", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z015_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z048_t001_NucleiLabels.tiff": "325478230529e94ba372f38f0bf512292f131d337b6ac09ce5a91b0266724e02", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z011_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z042_t001_NucleiLabels.tiff": "e07252ca1a55c5195c99666d8d3f0d70db636b4ef4b99e1a0049ca22b182d87a", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_Nuclei.csv": "17500f5d4321bbf255df31f7a3503339029c2b077045a6c10e4d4f6f0db9ad1e", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z048_t001_CellsLabels.tiff": "133a58f6fac8b07f79bfed9634867ccb5428b83e63682031a1def16c6a694568", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z046_t001_NucleiLabels.tiff": "c1250e060d32f2cfc07fc7e1594fdb1d6cbb4d0483fa94a8113efa695397963c", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z029_t001_CellsLabels.tiff": "b3bdde5f751b8f5d41ca8b5219a3427e450cb1b8795401d0ec638940d1ad7bd9", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z017_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z010_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z003_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z037_t001_NucleiLabels.tiff": "927d52441c57cb3a4014effaca3e3c23c76275fb264c26e1942a9c58b85ee6d2", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z031_t001_NucleiLabels.tiff": "247490c261fecc26c7379ab2eebaa01e4bea523218b3cc5491d2fe2b9aadfe11", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z049_t001_NucleiLabels.tiff": "e92e493078672a0e0d90e31409612e215bf8486300d5c765d0e0504ff8888e7a", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z052_t001_NucleiLabels.tiff": "0daf51526658e23b7c9ab8872e992ebe9322399286f95741d0ccdbd357e5d631", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z038_t001_NucleiLabels.tiff": "e2852ea83c62ab30d77681b4e39f88d3cee74ae052f1c948675ae3581c9fc28a", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z047_t001_NucleiLabels.tiff": "995d5ce77cbf61a9be8590f34987ef3f1dd698ae6ed393cac4d0960bc9e7a66a", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z021_t001_CellsLabels.tiff": "daa31cb1e537e5c32b1936ab7647c5690741d2bc1d0e15c60b59ba1310a8c0ce", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z022_t001_NucleiLabels.tiff": "50aef20e28a2f9e18ca2e28eabf6d87ec2ad20c79898e56f8d63a959ba5bb507", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z012_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z005_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z059_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z028_t001_CellsLabels.tiff": "fef31040e0c54501c3bf965eb98f055cdb1d42c96f91cf1c239033d0a9a06534", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z008_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z009_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z019_t001_CellsLabels.tiff": "ec6ef1d319f2732c47b06f5cf02775c8cd94540f2f30a6c002f0aace1d317955", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z054_t001_NucleiLabels.tiff": "b841c5d901c65a6138d9ff2b0ec4aa04f00765b1dd21bcd6fc9f8dc974c6035d", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z035_t001_CellsLabels.tiff": "eedeb489b46abf1eafbd9c81491cb3f85ce1cb3ce877844ad22e72547fc3c438", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z024_t001_NucleiLabels.tiff": "9e96a05ed9860cac9c57c618db2f4fe5e4b769a9d235aac6fd1d628486317df0", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z026_t001_NucleiLabels.tiff": "ad22809ff3f43d15c838f697061c263070dbf1da1031da39900681cdfbf9d54c", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z050_t001_NucleiLabels.tiff": "565b4f08f7503086c1f2f8e631d19063129e02a5767d2f979c0cec8fdec23601", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z045_t001_CellsLabels.tiff": "c678f2100c7431e5a43c391af86a79e3436d4cf13f050f98c25f62682e4632fe", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z012_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z040_t001_CellsLabels.tiff": "1635ea20dd80ce99f0e86494666825a52a66645837d598fd3a3fa91f0bb77d53", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z023_t001_NucleiLabels.tiff": "ecb7f78c8ca0bad2b9d0495659609b9023781b6a936d6afb0a04653eac176555", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z035_t001_NucleiLabels.tiff": "13e69afc324152bb91329fa758d131d849127e8fd346078d94c6adc25582b3a6", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z011_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z002_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z018_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z007_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z034_t001_CellsLabels.tiff": "3f6c1d8cdbce20aac8f70e7315bea70b147b7b6a034432845b27eeb6cfba083c", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z039_t001_CellsLabels.tiff": "22fa79a76e64013f10373be2ca2af98a2173275c5d43c9cad33135acdc376d14", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z059_t001_NucleiLabels.tiff": "76c785cf0f481c73ad3cf34ccb576eaa19dd1bdd9c74f9eaf92adfbde5d44b97", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z029_t001_NucleiLabels.tiff": "34bce0f06f674e20667554eed0075fde8d9d33c9c0eaeffd88a1c558499cc432", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z044_t001_NucleiLabels.tiff": "a78264f2f5e245a3913a69f89105e94df30048b1aaef4eb80fc5c3235e78b196", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z046_t001_CellsLabels.tiff": "dc081df050be6cd19b5d5bbfe21c96c350eecf9bc6f10f608dc361a36056632a", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z034_t001_NucleiLabels.tiff": "08af61857cfb97fd95fcb08d1ce18139ce7087b192044c63452bee533c5b1aa4", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_Image.csv": "75ac32ff5ef57bc285ec4eae1c9b501c27a35b486bf35435af5d499d856d34a4", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z015_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/openhcs_metadata.json": "df996ea7347162652903028922a9ac2206dd2d6dbecbf9fb6da05a7e65ece370", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z057_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z045_t001_NucleiLabels.tiff": "94bd6a65795ad6395cc18a4fd63dc591354bef612ee49fffd2d9f63e4ed9db84", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z055_t001_NucleiLabels.tiff": "44e02416cc94a70528165a4b6c42b9e19b205f9c91528076796272cb1a8504ea", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_erodedResizedNuclei.csv": "cca23f86a85bb3ee7a5c3a1d174c34b5f026ca7f399be98458e1a8c6e12d5c77", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z005_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z028_t001_NucleiLabels.tiff": "1676f21bd3b7e105601c83f206890507c32ba2447988be2a75d0e8153d069b65", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z051_t001_CellsLabels.tiff": "73483bd10a0f37990084013ad054768086d6d3c929da6bc7e74f4aee37e9dbf2", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z043_t001_NucleiLabels.tiff": "81a705d05b0a5bfa8783df28990c1d70634c1f4aa2bca23e02cfa8a4bc861b86", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z027_t001_NucleiLabels.tiff": "8c8f402d3a7c65c04e7f1575dd34de0427eeeb433ff2956d20e07d06eb48fb8d", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z042_t001_CellsLabels.tiff": "88d2e9964455459499656c4f0a762cd86376c678756635aaa6b6c8d98be01852", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z003_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z025_t001_NucleiLabels.tiff": "9cc33676a0177130609ab1331feab1722bbde8d55b2f49031730fd5a2a40029c", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/openhcs_metadata.json.lock": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z033_t001_CellsLabels.tiff": "a320b99ee129d97b99f9fac66aabb89fc1652930b3bb8b2b3fbba7b57e994f85", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z057_t001_NucleiLabels.tiff": "695517641e9fe4b8421d4a7db56b24c755b6957c98ce32bb24ad5b25f52661eb", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z006_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z038_t001_CellsLabels.tiff": "6ac8fd9c95acc1f261e96a8fb9332e5d6a403548842f9b85ecb977e6213b4c9b", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z044_t001_CellsLabels.tiff": "e03946d94c789ee787cc29a3c8f5e5d9c56b37a0044b297ba8f403abc98eb55e", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z014_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z041_t001_NucleiLabels.tiff": "744eab7cd170d5af8d2f72ced3893dd939c4e1b0f415afab799a37ded5d046c7", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z007_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z054_t001_CellsLabels.tiff": "25ff2ec586a568e42286f73713d64aab1208c38c016099299211f04368ac49c8", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z013_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z053_t001_NucleiLabels.tiff": "b8613192175a4615b58aed607474657668429cb6ac635cd012569b19667f2d3d", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z051_t001_NucleiLabels.tiff": "f0f36ccaf744d42da036948682cfd226e6dd4a3cfb94aed38729b2d25ede5dff", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z036_t001_NucleiLabels.tiff": "4fa967c78709aa400246a50528a36b24ca366e825dd0e64561643369e814906b", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z016_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z055_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z027_t001_CellsLabels.tiff": "ee9056587a19e581f5feb6078b41fa1004d923a5ddf1725aefc4b8b8c5201ad6", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z024_t001_CellsLabels.tiff": "39899adabca74b10b9950d1f97e8b0b3bbaf136d75e7a46ef4a6e86896e3119f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z013_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z039_t001_NucleiLabels.tiff": "f93e8293a37886a36844b8748083a1f8e717efab092652755263e797f12f0364", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_downsizedNuclei.csv": "a7b998c5a524e11747fc5452fd763fc3d9abe16f46e961f62e09e9393b6a8adf", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z010_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_erodedDownsizedNuclei.csv": "ee507fb9c2387f86fc768184baf2596ebe1f8e40f7fc5eb24f252345fee63ae6", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z004_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z040_t001_NucleiLabels.tiff": "e0bd66b166e9f92120dbee183973849eda0afbbe6fed9d03fc4cfd2c56a96280", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z056_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z058_t001_NucleiLabels.tiff": "a37c63c6052e40d73db918a014e6f2a18be4d0cbfbeeef65a9c26096a38a4c09", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z004_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z018_t001_NucleiLabels.tiff": "03ba7a4d6ae53e6c84aab5926ae1e2592b5f8ca24a839a256c82f08544df4270", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z001_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z049_t001_CellsLabels.tiff": "28d83dfbf5bbf7e4da9c3e4806353e9fd0a89680dcdab4b43ca2fd19b80f669c", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z043_t001_CellsLabels.tiff": "bd0df007524ea4e555e4d2fdbebe6a274caa03bc360a1cf90d601659155984b2", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z031_t001_CellsLabels.tiff": "1b14aab71bf3623458ea9373852b0baf28be9727877b32f894407927ecfb6d65", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z033_t001_NucleiLabels.tiff": "512a3a0ed8025d14b0556a72ebfc13b173d14f7046817ca09876242ab63b9d6b", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z052_t001_CellsLabels.tiff": "7bb98f0587062e01b965e486fc687872651d7d90d271a2fdf0d35d67b464b233", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z016_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z022_t001_CellsLabels.tiff": "7d7ec170ed3652efa99e43d8db546c51b696821dca7727d0ff4d2f2ead7fdbaa", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z041_t001_CellsLabels.tiff": "be7762756eb65f08cb337904165c6f43b5351f39405461f65ad28669abfe3899", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z056_t001_NucleiLabels.tiff": "ca0723c8b9764d5b77034719bb75a2940be35da2e04119af4c0e6d3552c08718", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z032_t001_NucleiLabels.tiff": "97b09fe5444ad439c3c2007d75971dce2766d94c93268eccfee9ae49b8a04aa1", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z014_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z060_t001_NucleiLabels.tiff": "7cc3c6c6a22f558e62194277090bdea2deb79475dd91fe7d0e49eac3c0187388", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z037_t001_CellsLabels.tiff": "aa1f449f1769b27cd9f1076e70f49f0242e2549f288a77984ca159cb07cf6a40", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z001_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_Cells.csv": "6f6e7431171ed5ea73a01784105147eca0e43a6e3e41f8720df93106353c9c79", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z047_t001_CellsLabels.tiff": "b926aa64e1fa8aa52279a69d91dc23cf14bf0ae76107f9b360c7ae77bdf53719", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z025_t001_CellsLabels.tiff": "03aabe5528198cfd6ec23a058dab540fb8be0870207c6dc94a62d9b621d1f2f9", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z026_t001_CellsLabels.tiff": "5b433d931fa6637fc793b23880199c01000ef6d9e610903bb38bb8cea5574563", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z032_t001_CellsLabels.tiff": "8a2c5774044326ae722e41aae517266197fbebb0e4dd5eab4b22414fbfea309f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z053_t001_CellsLabels.tiff": "ff17f538baaf179752412026a250bdbee016e3101037bf4e0dbb81199fc4c8ed", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z020_t001_NucleiLabels.tiff": "b0cd031b448d8bd2075e65710bd76c064161b25242f85ee150da7cd139fcc1a8", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z050_t001_CellsLabels.tiff": "ea7654a6fe1f92ad274a54935420923eec4bec5ad030187afc0ac7af02690af5", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z036_t001_CellsLabels.tiff": "28b68d1300cf7b08e21dbcd4858447862520dded5c27f2d84cf223513a119eed", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z020_t001_CellsLabels.tiff": "e8105a4f04198ac75de9831247030084d6f65bf98691d7444ead7e178072fa22", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z030_t001_NucleiLabels.tiff": "c54c8b98a5c38a47e0cf9391a1afab09b4c8c58e06294cdb650a15add4a32b58", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z060_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z023_t001_CellsLabels.tiff": "2f06989618050934c73473f9342c2fe3410fd834a0ddb976cf602fe6198db7a6", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z030_t001_CellsLabels.tiff": "b71a2e05d270ae0cd237d6c0d97ea82a63e5ad3633723e7255c35d34e585ec80", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z009_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z002_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z006_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z019_t001_NucleiLabels.tiff": "250cb56a8054c28cea894bc3d9edaddcdce01f5151a05da78da20157c3c3be9f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z021_t001_NucleiLabels.tiff": "71a10b0539f3fc4cba600b275b7e948258c6a435ba33ef522faa0869ac085518", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z008_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z058_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z017_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f" + } + }, + { + "case": "ExampleSpeckles", + "ordinary_repeat": 0, + "native_repeat": 0, + "measurement_differences": 0, + "image_differences": 0, + "tables": [ + 3, + 3 + ], + "physical_images": [ + 0, + 0 + ], + "logical_images": [ + 0, + 0 + ], + "known_relationship_keys": [ + 1, + 1 + ], + "ordinary_receipt_sha256": "cda421a4fe86662f7be512585425e834739b510b6e0b31e8396f28ed2e123c16", + "output_hashes": { + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/ExampleSpeckles/outputs/0/Image.csv": "55cdc9117d38d5a9a96b603052b9ca323824897be4962f285934973af03d73bb", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/ExampleSpeckles/outputs/0/h2ax.csv": "0f54fc72ccdeb7e1a68bab9dc3a8d9d49788cdf81dbbc109ed119e589c09538a", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/ExampleSpeckles/outputs/0/Nuclei.csv": "15af7cac08d651c19b3e3f409b2b0d7fc4f96011c9b53394aa9a35969a8936a6", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/ExampleSpeckles/wells_1/workers_1/images_ExampleSpeckles_source_workspace_well_throughput/results/Image.csv": "85ef16f24e75f03dc7055215cf2e6cb196b1dc0a0d73713a4c5999f698d4f96e", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/ExampleSpeckles/wells_1/workers_1/images_ExampleSpeckles_source_workspace_well_throughput/results/h2ax.csv": "eb453565f3b79ac932eeb5cde8605888b72e58c80f42edbbf0b1ba37f34fd6f8", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/ExampleSpeckles/wells_1/workers_1/images_ExampleSpeckles_source_workspace_well_throughput/openhcs_metadata.json.lock": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/ExampleSpeckles/wells_1/workers_1/images_ExampleSpeckles_source_workspace_well_throughput/openhcs_metadata.json": "71582c62cabb2571ebf8a0de57f92fdf82c55a7ece3dac0bf2a6eca95cad84c9", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/ExampleSpeckles/wells_1/workers_1/images_ExampleSpeckles_source_workspace_well_throughput/results/Nuclei.csv": "e50cb9f8c23f65e9ce8db25132263cf4a4448ed65fc1088d782536abf74c5670" + } + }, + { + "case": "ExampleSpeckles", + "ordinary_repeat": 0, + "native_repeat": 1, + "measurement_differences": 0, + "image_differences": 0, + "tables": [ + 3, + 3 + ], + "physical_images": [ + 0, + 0 + ], + "logical_images": [ + 0, + 0 + ], + "known_relationship_keys": [ + 1, + 1 + ], + "ordinary_receipt_sha256": "cda421a4fe86662f7be512585425e834739b510b6e0b31e8396f28ed2e123c16", + "output_hashes": { + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/ExampleSpeckles/outputs/1/Nuclei.csv": "15af7cac08d651c19b3e3f409b2b0d7fc4f96011c9b53394aa9a35969a8936a6", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/ExampleSpeckles/outputs/1/h2ax.csv": "0f54fc72ccdeb7e1a68bab9dc3a8d9d49788cdf81dbbc109ed119e589c09538a", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/ExampleSpeckles/outputs/1/Image.csv": "1d2f3cbff7ee2608efb670186878d4588a8a438ef74aded60e7052f4d2f243ac", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/ExampleSpeckles/wells_1/workers_1/images_ExampleSpeckles_source_workspace_well_throughput/results/Image.csv": "85ef16f24e75f03dc7055215cf2e6cb196b1dc0a0d73713a4c5999f698d4f96e", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/ExampleSpeckles/wells_1/workers_1/images_ExampleSpeckles_source_workspace_well_throughput/results/h2ax.csv": "eb453565f3b79ac932eeb5cde8605888b72e58c80f42edbbf0b1ba37f34fd6f8", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/ExampleSpeckles/wells_1/workers_1/images_ExampleSpeckles_source_workspace_well_throughput/openhcs_metadata.json.lock": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/ExampleSpeckles/wells_1/workers_1/images_ExampleSpeckles_source_workspace_well_throughput/openhcs_metadata.json": "71582c62cabb2571ebf8a0de57f92fdf82c55a7ece3dac0bf2a6eca95cad84c9", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-0/ExampleSpeckles/wells_1/workers_1/images_ExampleSpeckles_source_workspace_well_throughput/results/Nuclei.csv": "e50cb9f8c23f65e9ce8db25132263cf4a4448ed65fc1088d782536abf74c5670" + } + }, + { + "case": "cp_tutorial_3d_monolayer", + "ordinary_repeat": 1, + "native_repeat": 0, + "measurement_differences": 0, + "image_differences": 0, + "tables": [ + 7, + 6 + ], + "physical_images": [ + 2, + 120 + ], + "logical_images": [ + 2, + 2 + ], + "known_relationship_keys": [ + 3, + 3 + ], + "ordinary_receipt_sha256": "f2f35969c4e153c03e7b921da2503929020c1ecce2d64bbaa49c04033fe1494a", + "output_hashes": { + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_xy1_ch2_NucleiLabels.tiff": "340bbd795541e4031749d1fff767fa4fcdcac10f5b97879056b25dac9fd7674b", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_erodedResizedNuclei.csv": "66a58f78cab0cfdbe907468e8ba383fc59fab49025b84b589855bdfa04854609", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_Experiment.csv": "c05f0a11f43a5bda943e9f739ff257216f8ab3f65d8988a0d3d0c612c1db6997", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_downsizedNuclei.csv": "2c66bc7b17de52147d5a875403f9a9746019ac61371c08157979044c6ed818dd", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_Cells.csv": "cc34681db8a66c38ca77bc47db496b26703f21e4d7c7628dd74e30c33250859f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_Nuclei.csv": "440c33edc7e8cb5611910a835454db62390301a7af617a542349f60aece57561", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_erodedDownsizedNuclei.csv": "527a0cce497d2a618478be84d1a0bf174049f58aa2a666e37626a990fdca0116", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_Image.csv": "1f3dec653b6909c8a81ac109ce1faf91380886ac9a18e56fafc91102d419904e", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_xy1_ch0_CellsLabels.tiff": "aaed9ed23179a162ee7f5a7e8d1d88f1fa342ca55172b7fccd4cbf67f7841d81", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z010_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z044_t001_NucleiLabels.tiff": "a78264f2f5e245a3913a69f89105e94df30048b1aaef4eb80fc5c3235e78b196", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z012_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z020_t001_CellsLabels.tiff": "e8105a4f04198ac75de9831247030084d6f65bf98691d7444ead7e178072fa22", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z006_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z016_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z024_t001_NucleiLabels.tiff": "9e96a05ed9860cac9c57c618db2f4fe5e4b769a9d235aac6fd1d628486317df0", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z032_t001_CellsLabels.tiff": "8a2c5774044326ae722e41aae517266197fbebb0e4dd5eab4b22414fbfea309f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z014_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z021_t001_NucleiLabels.tiff": "71a10b0539f3fc4cba600b275b7e948258c6a435ba33ef522faa0869ac085518", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z034_t001_CellsLabels.tiff": "3f6c1d8cdbce20aac8f70e7315bea70b147b7b6a034432845b27eeb6cfba083c", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z018_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z007_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z052_t001_CellsLabels.tiff": "7bb98f0587062e01b965e486fc687872651d7d90d271a2fdf0d35d67b464b233", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z042_t001_CellsLabels.tiff": "88d2e9964455459499656c4f0a762cd86376c678756635aaa6b6c8d98be01852", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/openhcs_metadata.json": "2b1c063d1583e1eacc2c087e956acb68f69b4554cd8d04c0d3e4dd4d641053e4", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z043_t001_NucleiLabels.tiff": "81a705d05b0a5bfa8783df28990c1d70634c1f4aa2bca23e02cfa8a4bc861b86", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z040_t001_NucleiLabels.tiff": "e0bd66b166e9f92120dbee183973849eda0afbbe6fed9d03fc4cfd2c56a96280", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_erodedResizedNuclei.csv": "cca23f86a85bb3ee7a5c3a1d174c34b5f026ca7f399be98458e1a8c6e12d5c77", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z016_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z048_t001_NucleiLabels.tiff": "325478230529e94ba372f38f0bf512292f131d337b6ac09ce5a91b0266724e02", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z054_t001_NucleiLabels.tiff": "b841c5d901c65a6138d9ff2b0ec4aa04f00765b1dd21bcd6fc9f8dc974c6035d", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z050_t001_NucleiLabels.tiff": "565b4f08f7503086c1f2f8e631d19063129e02a5767d2f979c0cec8fdec23601", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z058_t001_NucleiLabels.tiff": "a37c63c6052e40d73db918a014e6f2a18be4d0cbfbeeef65a9c26096a38a4c09", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z054_t001_CellsLabels.tiff": "25ff2ec586a568e42286f73713d64aab1208c38c016099299211f04368ac49c8", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z002_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z031_t001_CellsLabels.tiff": "1b14aab71bf3623458ea9373852b0baf28be9727877b32f894407927ecfb6d65", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z005_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z055_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z028_t001_CellsLabels.tiff": "fef31040e0c54501c3bf965eb98f055cdb1d42c96f91cf1c239033d0a9a06534", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z006_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/openhcs_metadata.json.lock": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z050_t001_CellsLabels.tiff": "ea7654a6fe1f92ad274a54935420923eec4bec5ad030187afc0ac7af02690af5", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_Image.csv": "75ac32ff5ef57bc285ec4eae1c9b501c27a35b486bf35435af5d499d856d34a4", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z019_t001_CellsLabels.tiff": "ec6ef1d319f2732c47b06f5cf02775c8cd94540f2f30a6c002f0aace1d317955", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z023_t001_CellsLabels.tiff": "2f06989618050934c73473f9342c2fe3410fd834a0ddb976cf602fe6198db7a6", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z017_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z014_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z039_t001_CellsLabels.tiff": "22fa79a76e64013f10373be2ca2af98a2173275c5d43c9cad33135acdc376d14", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z043_t001_CellsLabels.tiff": "bd0df007524ea4e555e4d2fdbebe6a274caa03bc360a1cf90d601659155984b2", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z038_t001_CellsLabels.tiff": "6ac8fd9c95acc1f261e96a8fb9332e5d6a403548842f9b85ecb977e6213b4c9b", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z002_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z009_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z011_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z036_t001_NucleiLabels.tiff": "4fa967c78709aa400246a50528a36b24ca366e825dd0e64561643369e814906b", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z060_t001_NucleiLabels.tiff": "7cc3c6c6a22f558e62194277090bdea2deb79475dd91fe7d0e49eac3c0187388", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z029_t001_CellsLabels.tiff": "b3bdde5f751b8f5d41ca8b5219a3427e450cb1b8795401d0ec638940d1ad7bd9", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z013_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z004_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z033_t001_NucleiLabels.tiff": "512a3a0ed8025d14b0556a72ebfc13b173d14f7046817ca09876242ab63b9d6b", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z046_t001_CellsLabels.tiff": "dc081df050be6cd19b5d5bbfe21c96c350eecf9bc6f10f608dc361a36056632a", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z051_t001_NucleiLabels.tiff": "f0f36ccaf744d42da036948682cfd226e6dd4a3cfb94aed38729b2d25ede5dff", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_Cells.csv": "6f6e7431171ed5ea73a01784105147eca0e43a6e3e41f8720df93106353c9c79", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z049_t001_NucleiLabels.tiff": "e92e493078672a0e0d90e31409612e215bf8486300d5c765d0e0504ff8888e7a", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z007_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z052_t001_NucleiLabels.tiff": "0daf51526658e23b7c9ab8872e992ebe9322399286f95741d0ccdbd357e5d631", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z009_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z049_t001_CellsLabels.tiff": "28d83dfbf5bbf7e4da9c3e4806353e9fd0a89680dcdab4b43ca2fd19b80f669c", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z022_t001_CellsLabels.tiff": "7d7ec170ed3652efa99e43d8db546c51b696821dca7727d0ff4d2f2ead7fdbaa", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z057_t001_NucleiLabels.tiff": "695517641e9fe4b8421d4a7db56b24c755b6957c98ce32bb24ad5b25f52661eb", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z027_t001_CellsLabels.tiff": "ee9056587a19e581f5feb6078b41fa1004d923a5ddf1725aefc4b8b8c5201ad6", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z013_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z041_t001_CellsLabels.tiff": "be7762756eb65f08cb337904165c6f43b5351f39405461f65ad28669abfe3899", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z015_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_downsizedNuclei.csv": "a7b998c5a524e11747fc5452fd763fc3d9abe16f46e961f62e09e9393b6a8adf", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z041_t001_NucleiLabels.tiff": "744eab7cd170d5af8d2f72ced3893dd939c4e1b0f415afab799a37ded5d046c7", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z036_t001_CellsLabels.tiff": "28b68d1300cf7b08e21dbcd4858447862520dded5c27f2d84cf223513a119eed", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z042_t001_NucleiLabels.tiff": "e07252ca1a55c5195c99666d8d3f0d70db636b4ef4b99e1a0049ca22b182d87a", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z037_t001_NucleiLabels.tiff": "927d52441c57cb3a4014effaca3e3c23c76275fb264c26e1942a9c58b85ee6d2", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z029_t001_NucleiLabels.tiff": "34bce0f06f674e20667554eed0075fde8d9d33c9c0eaeffd88a1c558499cc432", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z047_t001_NucleiLabels.tiff": "995d5ce77cbf61a9be8590f34987ef3f1dd698ae6ed393cac4d0960bc9e7a66a", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z020_t001_NucleiLabels.tiff": "b0cd031b448d8bd2075e65710bd76c064161b25242f85ee150da7cd139fcc1a8", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z055_t001_NucleiLabels.tiff": "44e02416cc94a70528165a4b6c42b9e19b205f9c91528076796272cb1a8504ea", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z001_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z051_t001_CellsLabels.tiff": "73483bd10a0f37990084013ad054768086d6d3c929da6bc7e74f4aee37e9dbf2", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z056_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z011_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z008_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z003_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z030_t001_CellsLabels.tiff": "b71a2e05d270ae0cd237d6c0d97ea82a63e5ad3633723e7255c35d34e585ec80", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z004_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z048_t001_CellsLabels.tiff": "133a58f6fac8b07f79bfed9634867ccb5428b83e63682031a1def16c6a694568", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z031_t001_NucleiLabels.tiff": "247490c261fecc26c7379ab2eebaa01e4bea523218b3cc5491d2fe2b9aadfe11", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z003_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z008_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z059_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z018_t001_NucleiLabels.tiff": "03ba7a4d6ae53e6c84aab5926ae1e2592b5f8ca24a839a256c82f08544df4270", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z047_t001_CellsLabels.tiff": "b926aa64e1fa8aa52279a69d91dc23cf14bf0ae76107f9b360c7ae77bdf53719", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z026_t001_CellsLabels.tiff": "5b433d931fa6637fc793b23880199c01000ef6d9e610903bb38bb8cea5574563", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z035_t001_NucleiLabels.tiff": "13e69afc324152bb91329fa758d131d849127e8fd346078d94c6adc25582b3a6", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z026_t001_NucleiLabels.tiff": "ad22809ff3f43d15c838f697061c263070dbf1da1031da39900681cdfbf9d54c", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z039_t001_NucleiLabels.tiff": "f93e8293a37886a36844b8748083a1f8e717efab092652755263e797f12f0364", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z012_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z056_t001_NucleiLabels.tiff": "ca0723c8b9764d5b77034719bb75a2940be35da2e04119af4c0e6d3552c08718", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z024_t001_CellsLabels.tiff": "39899adabca74b10b9950d1f97e8b0b3bbaf136d75e7a46ef4a6e86896e3119f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z059_t001_NucleiLabels.tiff": "76c785cf0f481c73ad3cf34ccb576eaa19dd1bdd9c74f9eaf92adfbde5d44b97", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z028_t001_NucleiLabels.tiff": "1676f21bd3b7e105601c83f206890507c32ba2447988be2a75d0e8153d069b65", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z045_t001_NucleiLabels.tiff": "94bd6a65795ad6395cc18a4fd63dc591354bef612ee49fffd2d9f63e4ed9db84", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z019_t001_NucleiLabels.tiff": "250cb56a8054c28cea894bc3d9edaddcdce01f5151a05da78da20157c3c3be9f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z057_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z060_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z017_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z025_t001_NucleiLabels.tiff": "9cc33676a0177130609ab1331feab1722bbde8d55b2f49031730fd5a2a40029c", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z021_t001_CellsLabels.tiff": "daa31cb1e537e5c32b1936ab7647c5690741d2bc1d0e15c60b59ba1310a8c0ce", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z040_t001_CellsLabels.tiff": "1635ea20dd80ce99f0e86494666825a52a66645837d598fd3a3fa91f0bb77d53", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z053_t001_CellsLabels.tiff": "ff17f538baaf179752412026a250bdbee016e3101037bf4e0dbb81199fc4c8ed", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z034_t001_NucleiLabels.tiff": "08af61857cfb97fd95fcb08d1ce18139ce7087b192044c63452bee533c5b1aa4", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_Nuclei.csv": "17500f5d4321bbf255df31f7a3503339029c2b077045a6c10e4d4f6f0db9ad1e", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z027_t001_NucleiLabels.tiff": "8c8f402d3a7c65c04e7f1575dd34de0427eeeb433ff2956d20e07d06eb48fb8d", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z005_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z038_t001_NucleiLabels.tiff": "e2852ea83c62ab30d77681b4e39f88d3cee74ae052f1c948675ae3581c9fc28a", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z025_t001_CellsLabels.tiff": "03aabe5528198cfd6ec23a058dab540fb8be0870207c6dc94a62d9b621d1f2f9", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z023_t001_NucleiLabels.tiff": "ecb7f78c8ca0bad2b9d0495659609b9023781b6a936d6afb0a04653eac176555", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z058_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z035_t001_CellsLabels.tiff": "eedeb489b46abf1eafbd9c81491cb3f85ce1cb3ce877844ad22e72547fc3c438", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z033_t001_CellsLabels.tiff": "a320b99ee129d97b99f9fac66aabb89fc1652930b3bb8b2b3fbba7b57e994f85", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z030_t001_NucleiLabels.tiff": "c54c8b98a5c38a47e0cf9391a1afab09b4c8c58e06294cdb650a15add4a32b58", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z010_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z032_t001_NucleiLabels.tiff": "97b09fe5444ad439c3c2007d75971dce2766d94c93268eccfee9ae49b8a04aa1", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_erodedDownsizedNuclei.csv": "ee507fb9c2387f86fc768184baf2596ebe1f8e40f7fc5eb24f252345fee63ae6", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z044_t001_CellsLabels.tiff": "e03946d94c789ee787cc29a3c8f5e5d9c56b37a0044b297ba8f403abc98eb55e", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z037_t001_CellsLabels.tiff": "aa1f449f1769b27cd9f1076e70f49f0242e2549f288a77984ca159cb07cf6a40", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z001_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z046_t001_NucleiLabels.tiff": "c1250e060d32f2cfc07fc7e1594fdb1d6cbb4d0483fa94a8113efa695397963c", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z015_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z045_t001_CellsLabels.tiff": "c678f2100c7431e5a43c391af86a79e3436d4cf13f050f98c25f62682e4632fe", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z022_t001_NucleiLabels.tiff": "50aef20e28a2f9e18ca2e28eabf6d87ec2ad20c79898e56f8d63a959ba5bb507", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z053_t001_NucleiLabels.tiff": "b8613192175a4615b58aed607474657668429cb6ac635cd012569b19667f2d3d" + } + }, + { + "case": "cp_tutorial_3d_monolayer", + "ordinary_repeat": 1, + "native_repeat": 1, + "measurement_differences": 0, + "image_differences": 0, + "tables": [ + 7, + 6 + ], + "physical_images": [ + 2, + 120 + ], + "logical_images": [ + 2, + 2 + ], + "known_relationship_keys": [ + 3, + 3 + ], + "ordinary_receipt_sha256": "f2f35969c4e153c03e7b921da2503929020c1ecce2d64bbaa49c04033fe1494a", + "output_hashes": { + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_xy1_ch2_NucleiLabels.tiff": "340bbd795541e4031749d1fff767fa4fcdcac10f5b97879056b25dac9fd7674b", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_Cells.csv": "cc34681db8a66c38ca77bc47db496b26703f21e4d7c7628dd74e30c33250859f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_erodedDownsizedNuclei.csv": "527a0cce497d2a618478be84d1a0bf174049f58aa2a666e37626a990fdca0116", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_Image.csv": "4cde74a38ef73b892dd9012708a3ac7d71fab206a7903f525b39e84493dcfe17", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_Experiment.csv": "65f1f124e99f13ddc73b71d01732262cd353f24f0bd4697a776b274a39eaecc5", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_Nuclei.csv": "440c33edc7e8cb5611910a835454db62390301a7af617a542349f60aece57561", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_erodedResizedNuclei.csv": "66a58f78cab0cfdbe907468e8ba383fc59fab49025b84b589855bdfa04854609", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_xy1_ch0_CellsLabels.tiff": "aaed9ed23179a162ee7f5a7e8d1d88f1fa342ca55172b7fccd4cbf67f7841d81", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_downsizedNuclei.csv": "2c66bc7b17de52147d5a875403f9a9746019ac61371c08157979044c6ed818dd", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z010_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z044_t001_NucleiLabels.tiff": "a78264f2f5e245a3913a69f89105e94df30048b1aaef4eb80fc5c3235e78b196", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z012_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z020_t001_CellsLabels.tiff": "e8105a4f04198ac75de9831247030084d6f65bf98691d7444ead7e178072fa22", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z006_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z016_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z024_t001_NucleiLabels.tiff": "9e96a05ed9860cac9c57c618db2f4fe5e4b769a9d235aac6fd1d628486317df0", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z032_t001_CellsLabels.tiff": "8a2c5774044326ae722e41aae517266197fbebb0e4dd5eab4b22414fbfea309f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z014_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z021_t001_NucleiLabels.tiff": "71a10b0539f3fc4cba600b275b7e948258c6a435ba33ef522faa0869ac085518", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z034_t001_CellsLabels.tiff": "3f6c1d8cdbce20aac8f70e7315bea70b147b7b6a034432845b27eeb6cfba083c", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z018_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z007_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z052_t001_CellsLabels.tiff": "7bb98f0587062e01b965e486fc687872651d7d90d271a2fdf0d35d67b464b233", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z042_t001_CellsLabels.tiff": "88d2e9964455459499656c4f0a762cd86376c678756635aaa6b6c8d98be01852", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/openhcs_metadata.json": "2b1c063d1583e1eacc2c087e956acb68f69b4554cd8d04c0d3e4dd4d641053e4", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z043_t001_NucleiLabels.tiff": "81a705d05b0a5bfa8783df28990c1d70634c1f4aa2bca23e02cfa8a4bc861b86", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z040_t001_NucleiLabels.tiff": "e0bd66b166e9f92120dbee183973849eda0afbbe6fed9d03fc4cfd2c56a96280", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_erodedResizedNuclei.csv": "cca23f86a85bb3ee7a5c3a1d174c34b5f026ca7f399be98458e1a8c6e12d5c77", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z016_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z048_t001_NucleiLabels.tiff": "325478230529e94ba372f38f0bf512292f131d337b6ac09ce5a91b0266724e02", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z054_t001_NucleiLabels.tiff": "b841c5d901c65a6138d9ff2b0ec4aa04f00765b1dd21bcd6fc9f8dc974c6035d", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z050_t001_NucleiLabels.tiff": "565b4f08f7503086c1f2f8e631d19063129e02a5767d2f979c0cec8fdec23601", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z058_t001_NucleiLabels.tiff": "a37c63c6052e40d73db918a014e6f2a18be4d0cbfbeeef65a9c26096a38a4c09", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z054_t001_CellsLabels.tiff": "25ff2ec586a568e42286f73713d64aab1208c38c016099299211f04368ac49c8", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z002_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z031_t001_CellsLabels.tiff": "1b14aab71bf3623458ea9373852b0baf28be9727877b32f894407927ecfb6d65", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z005_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z055_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z028_t001_CellsLabels.tiff": "fef31040e0c54501c3bf965eb98f055cdb1d42c96f91cf1c239033d0a9a06534", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z006_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/openhcs_metadata.json.lock": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z050_t001_CellsLabels.tiff": "ea7654a6fe1f92ad274a54935420923eec4bec5ad030187afc0ac7af02690af5", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_Image.csv": "75ac32ff5ef57bc285ec4eae1c9b501c27a35b486bf35435af5d499d856d34a4", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z019_t001_CellsLabels.tiff": "ec6ef1d319f2732c47b06f5cf02775c8cd94540f2f30a6c002f0aace1d317955", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z023_t001_CellsLabels.tiff": "2f06989618050934c73473f9342c2fe3410fd834a0ddb976cf602fe6198db7a6", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z017_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z014_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z039_t001_CellsLabels.tiff": "22fa79a76e64013f10373be2ca2af98a2173275c5d43c9cad33135acdc376d14", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z043_t001_CellsLabels.tiff": "bd0df007524ea4e555e4d2fdbebe6a274caa03bc360a1cf90d601659155984b2", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z038_t001_CellsLabels.tiff": "6ac8fd9c95acc1f261e96a8fb9332e5d6a403548842f9b85ecb977e6213b4c9b", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z002_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z009_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z011_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z036_t001_NucleiLabels.tiff": "4fa967c78709aa400246a50528a36b24ca366e825dd0e64561643369e814906b", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z060_t001_NucleiLabels.tiff": "7cc3c6c6a22f558e62194277090bdea2deb79475dd91fe7d0e49eac3c0187388", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z029_t001_CellsLabels.tiff": "b3bdde5f751b8f5d41ca8b5219a3427e450cb1b8795401d0ec638940d1ad7bd9", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z013_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z004_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z033_t001_NucleiLabels.tiff": "512a3a0ed8025d14b0556a72ebfc13b173d14f7046817ca09876242ab63b9d6b", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z046_t001_CellsLabels.tiff": "dc081df050be6cd19b5d5bbfe21c96c350eecf9bc6f10f608dc361a36056632a", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z051_t001_NucleiLabels.tiff": "f0f36ccaf744d42da036948682cfd226e6dd4a3cfb94aed38729b2d25ede5dff", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_Cells.csv": "6f6e7431171ed5ea73a01784105147eca0e43a6e3e41f8720df93106353c9c79", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z049_t001_NucleiLabels.tiff": "e92e493078672a0e0d90e31409612e215bf8486300d5c765d0e0504ff8888e7a", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z007_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z052_t001_NucleiLabels.tiff": "0daf51526658e23b7c9ab8872e992ebe9322399286f95741d0ccdbd357e5d631", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z009_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z049_t001_CellsLabels.tiff": "28d83dfbf5bbf7e4da9c3e4806353e9fd0a89680dcdab4b43ca2fd19b80f669c", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z022_t001_CellsLabels.tiff": "7d7ec170ed3652efa99e43d8db546c51b696821dca7727d0ff4d2f2ead7fdbaa", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z057_t001_NucleiLabels.tiff": "695517641e9fe4b8421d4a7db56b24c755b6957c98ce32bb24ad5b25f52661eb", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z027_t001_CellsLabels.tiff": "ee9056587a19e581f5feb6078b41fa1004d923a5ddf1725aefc4b8b8c5201ad6", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z013_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z041_t001_CellsLabels.tiff": "be7762756eb65f08cb337904165c6f43b5351f39405461f65ad28669abfe3899", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z015_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_downsizedNuclei.csv": "a7b998c5a524e11747fc5452fd763fc3d9abe16f46e961f62e09e9393b6a8adf", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z041_t001_NucleiLabels.tiff": "744eab7cd170d5af8d2f72ced3893dd939c4e1b0f415afab799a37ded5d046c7", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z036_t001_CellsLabels.tiff": "28b68d1300cf7b08e21dbcd4858447862520dded5c27f2d84cf223513a119eed", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z042_t001_NucleiLabels.tiff": "e07252ca1a55c5195c99666d8d3f0d70db636b4ef4b99e1a0049ca22b182d87a", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z037_t001_NucleiLabels.tiff": "927d52441c57cb3a4014effaca3e3c23c76275fb264c26e1942a9c58b85ee6d2", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z029_t001_NucleiLabels.tiff": "34bce0f06f674e20667554eed0075fde8d9d33c9c0eaeffd88a1c558499cc432", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z047_t001_NucleiLabels.tiff": "995d5ce77cbf61a9be8590f34987ef3f1dd698ae6ed393cac4d0960bc9e7a66a", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z020_t001_NucleiLabels.tiff": "b0cd031b448d8bd2075e65710bd76c064161b25242f85ee150da7cd139fcc1a8", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z055_t001_NucleiLabels.tiff": "44e02416cc94a70528165a4b6c42b9e19b205f9c91528076796272cb1a8504ea", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z001_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z051_t001_CellsLabels.tiff": "73483bd10a0f37990084013ad054768086d6d3c929da6bc7e74f4aee37e9dbf2", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z056_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z011_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z008_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z003_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z030_t001_CellsLabels.tiff": "b71a2e05d270ae0cd237d6c0d97ea82a63e5ad3633723e7255c35d34e585ec80", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z004_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z048_t001_CellsLabels.tiff": "133a58f6fac8b07f79bfed9634867ccb5428b83e63682031a1def16c6a694568", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z031_t001_NucleiLabels.tiff": "247490c261fecc26c7379ab2eebaa01e4bea523218b3cc5491d2fe2b9aadfe11", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z003_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z008_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z059_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z018_t001_NucleiLabels.tiff": "03ba7a4d6ae53e6c84aab5926ae1e2592b5f8ca24a839a256c82f08544df4270", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z047_t001_CellsLabels.tiff": "b926aa64e1fa8aa52279a69d91dc23cf14bf0ae76107f9b360c7ae77bdf53719", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z026_t001_CellsLabels.tiff": "5b433d931fa6637fc793b23880199c01000ef6d9e610903bb38bb8cea5574563", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z035_t001_NucleiLabels.tiff": "13e69afc324152bb91329fa758d131d849127e8fd346078d94c6adc25582b3a6", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z026_t001_NucleiLabels.tiff": "ad22809ff3f43d15c838f697061c263070dbf1da1031da39900681cdfbf9d54c", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z039_t001_NucleiLabels.tiff": "f93e8293a37886a36844b8748083a1f8e717efab092652755263e797f12f0364", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z012_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z056_t001_NucleiLabels.tiff": "ca0723c8b9764d5b77034719bb75a2940be35da2e04119af4c0e6d3552c08718", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z024_t001_CellsLabels.tiff": "39899adabca74b10b9950d1f97e8b0b3bbaf136d75e7a46ef4a6e86896e3119f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z059_t001_NucleiLabels.tiff": "76c785cf0f481c73ad3cf34ccb576eaa19dd1bdd9c74f9eaf92adfbde5d44b97", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z028_t001_NucleiLabels.tiff": "1676f21bd3b7e105601c83f206890507c32ba2447988be2a75d0e8153d069b65", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z045_t001_NucleiLabels.tiff": "94bd6a65795ad6395cc18a4fd63dc591354bef612ee49fffd2d9f63e4ed9db84", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z019_t001_NucleiLabels.tiff": "250cb56a8054c28cea894bc3d9edaddcdce01f5151a05da78da20157c3c3be9f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z057_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z060_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z017_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z025_t001_NucleiLabels.tiff": "9cc33676a0177130609ab1331feab1722bbde8d55b2f49031730fd5a2a40029c", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z021_t001_CellsLabels.tiff": "daa31cb1e537e5c32b1936ab7647c5690741d2bc1d0e15c60b59ba1310a8c0ce", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z040_t001_CellsLabels.tiff": "1635ea20dd80ce99f0e86494666825a52a66645837d598fd3a3fa91f0bb77d53", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z053_t001_CellsLabels.tiff": "ff17f538baaf179752412026a250bdbee016e3101037bf4e0dbb81199fc4c8ed", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z034_t001_NucleiLabels.tiff": "08af61857cfb97fd95fcb08d1ce18139ce7087b192044c63452bee533c5b1aa4", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_Nuclei.csv": "17500f5d4321bbf255df31f7a3503339029c2b077045a6c10e4d4f6f0db9ad1e", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z027_t001_NucleiLabels.tiff": "8c8f402d3a7c65c04e7f1575dd34de0427eeeb433ff2956d20e07d06eb48fb8d", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z005_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z038_t001_NucleiLabels.tiff": "e2852ea83c62ab30d77681b4e39f88d3cee74ae052f1c948675ae3581c9fc28a", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z025_t001_CellsLabels.tiff": "03aabe5528198cfd6ec23a058dab540fb8be0870207c6dc94a62d9b621d1f2f9", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z023_t001_NucleiLabels.tiff": "ecb7f78c8ca0bad2b9d0495659609b9023781b6a936d6afb0a04653eac176555", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z058_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z035_t001_CellsLabels.tiff": "eedeb489b46abf1eafbd9c81491cb3f85ce1cb3ce877844ad22e72547fc3c438", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z033_t001_CellsLabels.tiff": "a320b99ee129d97b99f9fac66aabb89fc1652930b3bb8b2b3fbba7b57e994f85", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z030_t001_NucleiLabels.tiff": "c54c8b98a5c38a47e0cf9391a1afab09b4c8c58e06294cdb650a15add4a32b58", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z010_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z032_t001_NucleiLabels.tiff": "97b09fe5444ad439c3c2007d75971dce2766d94c93268eccfee9ae49b8a04aa1", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_erodedDownsizedNuclei.csv": "ee507fb9c2387f86fc768184baf2596ebe1f8e40f7fc5eb24f252345fee63ae6", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z044_t001_CellsLabels.tiff": "e03946d94c789ee787cc29a3c8f5e5d9c56b37a0044b297ba8f403abc98eb55e", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z037_t001_CellsLabels.tiff": "aa1f449f1769b27cd9f1076e70f49f0242e2549f288a77984ca159cb07cf6a40", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z001_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z046_t001_NucleiLabels.tiff": "c1250e060d32f2cfc07fc7e1594fdb1d6cbb4d0483fa94a8113efa695397963c", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z015_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z045_t001_CellsLabels.tiff": "c678f2100c7431e5a43c391af86a79e3436d4cf13f050f98c25f62682e4632fe", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z022_t001_NucleiLabels.tiff": "50aef20e28a2f9e18ca2e28eabf6d87ec2ad20c79898e56f8d63a959ba5bb507", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z053_t001_NucleiLabels.tiff": "b8613192175a4615b58aed607474657668429cb6ac635cd012569b19667f2d3d" + } + }, + { + "case": "ExampleSpeckles", + "ordinary_repeat": 1, + "native_repeat": 0, + "measurement_differences": 0, + "image_differences": 0, + "tables": [ + 3, + 3 + ], + "physical_images": [ + 0, + 0 + ], + "logical_images": [ + 0, + 0 + ], + "known_relationship_keys": [ + 1, + 1 + ], + "ordinary_receipt_sha256": "52bfa7d40c01a6f5405d57bb6d3d6652c4c592746a50b35677cdec241045e868", + "output_hashes": { + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/ExampleSpeckles/outputs/0/Image.csv": "55cdc9117d38d5a9a96b603052b9ca323824897be4962f285934973af03d73bb", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/ExampleSpeckles/outputs/0/h2ax.csv": "0f54fc72ccdeb7e1a68bab9dc3a8d9d49788cdf81dbbc109ed119e589c09538a", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/ExampleSpeckles/outputs/0/Nuclei.csv": "15af7cac08d651c19b3e3f409b2b0d7fc4f96011c9b53394aa9a35969a8936a6", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/ExampleSpeckles/wells_1/workers_1/images_ExampleSpeckles_source_workspace_well_throughput/results/h2ax.csv": "eb453565f3b79ac932eeb5cde8605888b72e58c80f42edbbf0b1ba37f34fd6f8", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/ExampleSpeckles/wells_1/workers_1/images_ExampleSpeckles_source_workspace_well_throughput/openhcs_metadata.json.lock": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/ExampleSpeckles/wells_1/workers_1/images_ExampleSpeckles_source_workspace_well_throughput/openhcs_metadata.json": "71582c62cabb2571ebf8a0de57f92fdf82c55a7ece3dac0bf2a6eca95cad84c9", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/ExampleSpeckles/wells_1/workers_1/images_ExampleSpeckles_source_workspace_well_throughput/results/Image.csv": "85ef16f24e75f03dc7055215cf2e6cb196b1dc0a0d73713a4c5999f698d4f96e", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/ExampleSpeckles/wells_1/workers_1/images_ExampleSpeckles_source_workspace_well_throughput/results/Nuclei.csv": "e50cb9f8c23f65e9ce8db25132263cf4a4448ed65fc1088d782536abf74c5670" + } + }, + { + "case": "ExampleSpeckles", + "ordinary_repeat": 1, + "native_repeat": 1, + "measurement_differences": 0, + "image_differences": 0, + "tables": [ + 3, + 3 + ], + "physical_images": [ + 0, + 0 + ], + "logical_images": [ + 0, + 0 + ], + "known_relationship_keys": [ + 1, + 1 + ], + "ordinary_receipt_sha256": "52bfa7d40c01a6f5405d57bb6d3d6652c4c592746a50b35677cdec241045e868", + "output_hashes": { + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/ExampleSpeckles/outputs/1/Nuclei.csv": "15af7cac08d651c19b3e3f409b2b0d7fc4f96011c9b53394aa9a35969a8936a6", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/ExampleSpeckles/outputs/1/h2ax.csv": "0f54fc72ccdeb7e1a68bab9dc3a8d9d49788cdf81dbbc109ed119e589c09538a", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/ExampleSpeckles/outputs/1/Image.csv": "1d2f3cbff7ee2608efb670186878d4588a8a438ef74aded60e7052f4d2f243ac", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/ExampleSpeckles/wells_1/workers_1/images_ExampleSpeckles_source_workspace_well_throughput/results/h2ax.csv": "eb453565f3b79ac932eeb5cde8605888b72e58c80f42edbbf0b1ba37f34fd6f8", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/ExampleSpeckles/wells_1/workers_1/images_ExampleSpeckles_source_workspace_well_throughput/openhcs_metadata.json.lock": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/ExampleSpeckles/wells_1/workers_1/images_ExampleSpeckles_source_workspace_well_throughput/openhcs_metadata.json": "71582c62cabb2571ebf8a0de57f92fdf82c55a7ece3dac0bf2a6eca95cad84c9", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/ExampleSpeckles/wells_1/workers_1/images_ExampleSpeckles_source_workspace_well_throughput/results/Image.csv": "85ef16f24e75f03dc7055215cf2e6cb196b1dc0a0d73713a4c5999f698d4f96e", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/ordinary-1/ExampleSpeckles/wells_1/workers_1/images_ExampleSpeckles_source_workspace_well_throughput/results/Nuclei.csv": "e50cb9f8c23f65e9ce8db25132263cf4a4448ed65fc1088d782536abf74c5670" + } + } + ], + "source_head": "60b2ac9b4d4fd91558f68aac85ae39b55e575fc8", + "scope": "Both ordinary observations versus both native measured observations; full strict files/values/correlation policy; descriptive two-repeat clocks, not full30 acceptance." +} diff --git a/benchmark/results/perf_current_native_paired_20261003/timing_summary.json b/benchmark/results/perf_current_native_paired_20261003/timing_summary.json new file mode 100644 index 0000000000..348ffb1661 --- /dev/null +++ b/benchmark/results/perf_current_native_paired_20261003/timing_summary.json @@ -0,0 +1,73 @@ +{ + "source_head": "60b2ac9b4d4fd91558f68aac85ae39b55e575fc8", + "cases": { + "ExampleSpeckles": { + "native_invocation_seconds": [ + 1.9962371489964426, + 1.9122227132320404 + ], + "native_mean_seconds": 1.9542299311142415, + "ordinary_samples": { + "compile_seconds": [ + 0.7714827060699463, + 0.5555016994476318 + ], + "execute_seconds": [ + 1.0937919616699219, + 1.3689088821411133 + ], + "total_seconds": [ + 2.189194314647466, + 2.302979093976319 + ] + }, + "ordinary_means": { + "compile_seconds": 0.6634922027587891, + "execute_seconds": 1.2313504219055176, + "total_seconds": 2.2460867043118924 + }, + "other_total_seconds_mean": 0.35124407964758575, + "execution_speedup_ratio_of_means": 1.5870623799276133, + "total_speedup_ratio_of_means": 0.870059881198102 + }, + "cp_tutorial_3d_monolayer": { + "native_invocation_seconds": [ + 14.286907745990902, + 14.557445348240435 + ], + "native_mean_seconds": 14.422176547115669, + "ordinary_samples": { + "compile_seconds": [ + 1.1917080879211426, + 1.23331618309021 + ], + "execute_seconds": [ + 8.670458555221558, + 9.58397126197815 + ], + "total_seconds": [ + 10.651790381409228, + 11.655223582871258 + ] + }, + "ordinary_means": { + "compile_seconds": 1.2125121355056763, + "execute_seconds": 9.127214908599854, + "total_seconds": 11.153506982140243 + }, + "other_total_seconds_mean": 0.8137799380347133, + "execution_speedup_ratio_of_means": 1.580128954071936, + "total_speedup_ratio_of_means": 1.2930620449881318 + } + }, + "scope": "Two current cases only; means of both observations, not a causal before/after comparison, confidence interval, or full30/scaling result. Server READY startup/shutdown and native imports/JVM/load/full warmup are excluded. Ordinary total includes compilation and default OUTCOMES/RSS completion.", + "science_sha256": "77ab1a1e88d52502c633b646efe0d032ad87f117fc403298cb8b5742a0510190", + "plot_script_sha256": "a85cbed7174f3e9cd00577e7ec2031d4b11e014042d2246bf4ad4b6927826c40", + "artifacts": { + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/figures-v2/metrics.csv": "906273de1e1fe146a7cf53a41892d203aa80fa96da9e7288ec619e6c828ad999", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/figures-v2/matched_runtime.png": "867a94f34c5cbe1b6d4fe86917a4e42ab335e486d8c321dd745a353c95dad72d", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/figures-v2/matched_runtime.svg": "9a5377c6ad8609f5a59ae2e9757ec5089d7d5dc77615839ac756fdbbb654e9f7", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/figures-v2/matched_speedup.png": "c8a789edb4dda3ae82fbb708a0d2c9a459f0ee0d7ea7732c266703622839e1ee", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/figures-v2/matched_speedup.svg": "d2e121af82f0a08e23f47cfa00b306ac60f8b580e72dad63cde72f6809ca8a2c" + } +} diff --git a/benchmark/results/perf_dependency_source_readiness_20261003/shared-environment-qualification.json b/benchmark/results/perf_dependency_source_readiness_20261003/shared-environment-qualification.json new file mode 100644 index 0000000000..d75a360aa3 --- /dev/null +++ b/benchmark/results/perf_dependency_source_readiness_20261003/shared-environment-qualification.json @@ -0,0 +1,836 @@ +{ + "status": "SELECTIVE_SHARED_EDITABLE_DEPENDENCY_UPDATE_QUALIFIED_FOR_OWNED_SOURCE", + "actual_OpenHCS_head": "60b2ac9b4d4fd91558f68aac85ae39b55e575fc8", + "actual_OpenHCS_status": "", + "installed_distribution_delta": { + "status": "ONLY_TWO_AUTHORIZED_DISTRIBUTIONS_CHANGED", + "changes": { + "objectstate": { + "before": "1.1.8", + "after": "1.1.9" + }, + "python-introspect": { + "before": "0.1.14", + "after": "0.1.15" + } + } + }, + "direct_isolated_import": { + "status": "LATEST_OWNED_IMPORT_METADATA_APIS_PASS", + "mode": "isolated-direct", + "rows": { + "objectstate": { + "metadata_version": "1.1.9", + "runtime_version": "1.1.9", + "module_file": "/home/ts/code/projects/openhcs-shared-runtime-plumbing/external/ObjectState/src/objectstate/__init__.py", + "direct_url": { + "dir_info": { + "editable": true + }, + "url": "file:///home/ts/code/projects/openhcs-shared-runtime-plumbing/external/ObjectState" + }, + "metadata_path": "/home/ts/code/projects/openhcs/.venv/lib/python3.12/site-packages/objectstate-1.1.9.dist-info", + "default_discovery_metadata_path": "/home/ts/code/projects/openhcs/.venv/lib/python3.12/site-packages/objectstate-1.1.9.dist-info", + "default_discovery_version": "1.1.9" + }, + "python-introspect": { + "metadata_version": "0.1.15", + "runtime_version": "0.1.15", + "module_file": "/home/ts/code/projects/openhcs-shared-runtime-plumbing/external/python-introspect/src/python_introspect/__init__.py", + "direct_url": { + "dir_info": { + "editable": true + }, + "url": "file:///home/ts/code/projects/openhcs-shared-runtime-plumbing/external/python-introspect" + }, + "metadata_path": "/home/ts/code/projects/openhcs/.venv/lib/python3.12/site-packages/python_introspect-0.1.15.dist-info", + "default_discovery_metadata_path": "/home/ts/code/projects/openhcs/.venv/lib/python3.12/site-packages/python_introspect-0.1.15.dist-info", + "default_discovery_version": "0.1.15" + } + }, + "factories_or_registry_ready_not_called": true + }, + "owned_source_first_import": { + "status": "LATEST_OWNED_IMPORT_METADATA_APIS_PASS", + "mode": "source-first", + "rows": { + "objectstate": { + "metadata_version": "1.1.9", + "runtime_version": "1.1.9", + "module_file": "/home/ts/code/projects/openhcs-shared-runtime-plumbing/external/ObjectState/src/objectstate/__init__.py", + "direct_url": { + "dir_info": { + "editable": true + }, + "url": "file:///home/ts/code/projects/openhcs-shared-runtime-plumbing/external/ObjectState" + }, + "metadata_path": "/home/ts/code/projects/openhcs/.venv/lib/python3.12/site-packages/objectstate-1.1.9.dist-info", + "default_discovery_metadata_path": "/home/ts/code/projects/openhcs/.venv/lib/python3.12/site-packages/objectstate-1.1.9.dist-info", + "default_discovery_version": "1.1.9" + }, + "python-introspect": { + "metadata_version": "0.1.15", + "runtime_version": "0.1.15", + "module_file": "/home/ts/code/projects/openhcs-shared-runtime-plumbing/external/python-introspect/src/python_introspect/__init__.py", + "direct_url": { + "dir_info": { + "editable": true + }, + "url": "file:///home/ts/code/projects/openhcs-shared-runtime-plumbing/external/python-introspect" + }, + "metadata_path": "/home/ts/code/projects/openhcs/.venv/lib/python3.12/site-packages/python_introspect-0.1.15.dist-info", + "default_discovery_metadata_path": "/home/ts/code/projects/openhcs-shared-runtime-plumbing/external/python-introspect/src/python_introspect.egg-info", + "default_discovery_version": "0.1.15" + } + }, + "factories_or_registry_ready_not_called": true + }, + "all8_actual_owned_import_roots_and_source_hashes": { + "ObjectState": { + "root": "/home/ts/code/projects/openhcs-shared-runtime-plumbing/external/ObjectState", + "head": "a01f8939b9bf4659502a33fe3febe62cab0f3c9a", + "status": "", + "tracked_code_metadata_hashes": { + "docs/conf.py": "bc49d86e265031fd39550513524727428685c836d73d5434a43031db4490301b", + "pyproject.toml": "a0d2835c65fe7c65b5f40259cc1d9527095bf4a3b87d1f70030df6ef9e7ef16a", + "scripts/check_ruff_added_lines.py": "e88118d5e1fb5db477cc11fb80f594ba213482614c565f3ed6383d992995c81d", + "scripts/release.py": "52abee369fee146ee8cf83fa266b5f1dbaacc31bb5934e967265054dbc7561ba", + "scripts/update_and_release.py": "80896e49f856d6a193bb1dc3917f55d2878acf1476c8240333eda16b92e8e61a", + "scripts/update_version.py": "63d0085d6b72ff34bf9560f60e4a91351ba21416b4eccd9b48aa30d40c49022a", + "scripts/verify_release_ready.py": "0a8d8fec9af999c3bba6281f2cafdadd06365090536580c8b168f43c96674dd2", + "src/objectstate/__init__.py": "b972d45dccc8143cf141b31582fc50d2a9a1f992f3fa374dac2b07e1183ccd73", + "src/objectstate/collection_containers.py": "a939d07f5dc391b7131f7ffaa7d1390b0449b4931d54ed5b09b27f59056b6e9b", + "src/objectstate/config.py": "6db2d9de077872009f1d6defe351cf8f40e441a0de31ff574b02741cefb4ffe3", + "src/objectstate/construction_binding.py": "9cfed94cad1cc9d5cf2cf0f7e8f534f72192168bdf3ba90b2b8d7ee57f4950d3", + "src/objectstate/context_manager.py": "a01b0fc8aeb9b5b0888346f725f92cbfd96b90963bf20001b5414a5e6cd6c0e6", + "src/objectstate/dual_axis_resolver.py": "17fcda62912050bdcdd018bee2fca5f81c2f4940c5f27f2af3dc19b7691e1fb0", + "src/objectstate/edit_session.py": "003cfaff8d3687b0fa115e051c3cf833a3cbb21ad9d9becfe16c59359d656350", + "src/objectstate/field_access.py": "77f483322a504c392c0ca41cc3ee6b691d258396f70f00a61b98d9f4bc03d43a", + "src/objectstate/global_config.py": "a8187811f87806579640a8147f287114e7127401530178dd37e8d28ed788a6ae", + "src/objectstate/history_migration.py": "1c24097040e38c159ea7e809424c0b94ec6c34efa3dc84cd664ff6a2f790815f", + "src/objectstate/lazy_factory.py": "64ae924c176757863a84eef19b9df0eff516e32443c2661134ec2ae72f234bf9", + "src/objectstate/live_context_resolver.py": "bd169b0bf2e9eea833ffc1d28331d320b2386193652fca041387f32bf018d71e", + "src/objectstate/object_state.py": "735ce860ab650b281534e24e3c7354f37b64bb06832cf186d6b1f626f87108fe", + "src/objectstate/object_state_metadata.py": "4c5be42e313e3f896f2db0d3f89ca18ca6ab09b9d25f202a7b2af9e59a099ddb", + "src/objectstate/object_state_registry.py": "f848746a9c2a023a616c37531691073ea7bdb8d28d7db3bca5c8a3dcb176107a", + "src/objectstate/parameter_owner.py": "908606ec2da70f223a93071e92ca24affb3f172f7df14c64dfd27974f1fbb85c", + "src/objectstate/parameter_structure.py": "00a786af17bca57b4e819d39c76a6523af3bec116d119c323dea8c2953c7be3d", + "src/objectstate/parametric_axes.py": "7229b518f8d5cca522a3020361c16a601f3084bc5a7d34ef92a2fef11619e40c", + "src/objectstate/placeholder.py": "4e057cf3225dd38ef3859f20fcd1305526829f044af45ef616381ea734df8f67", + "src/objectstate/reified_generics.py": "39ad0897ec45ba929c0fae694dd870df987c06e2b005644189212fe224eceab2", + "src/objectstate/snapshot_model.py": "001cc83d354b040763b2da9aad0014619fdee22c82dce622dbd93ecfba780681", + "src/objectstate/subfield_semantics.py": "97aa6a7f2659260b1950c068f47d9bccec15087ed1852f1dd182272c8230c14a", + "src/objectstate/time_travel_profile.py": "c59d157f9cb85ee29aa64e18393d916ded656c6ce88bb46782f44e2463fcd007", + "src/objectstate/token_cache.py": "d6b7cdb71c15e13e8bab7271d70e0b96ccbe0825004c45c4e88851ee30a9435f", + "src/objectstate/transaction_checkpoint.py": "4c105f2078bf23c9312c600543623b9456de256c7093b9cb8a9b753b49b67530", + "src/objectstate/ui_visibility.py": "be0c60b66b86ebb696583ee394c0d45b05fd1f68a609a6aa0c29aa1098dc2592", + "src/objectstate/value_semantics.py": "4b8a43ca8667c592b74f0d46b8507ef8331402e376230dd7cac217870b2ae2f9", + "tests/__init__.py": "b0f08e026c34c8ec04203081ad79f7ed1c6cbabd784cf475333965f279ea33ae", + "tests/conftest.py": "0fa840dddea79e86afe717e146ba7ac7ee713b032e0e398b0a9f56842e4a1d62", + "tests/test_callable_baseline.py": "638929e6eb97a7c899443272e307beaa62a12922debdc7594a45ec0a85da3356", + "tests/test_config.py": "973b08f470fd54a4990cf39c7f176b9f5d02ed0b808dbf8ea2549a01d7b30d77", + "tests/test_construction_history.py": "88469c8157d4010b9877f76b7a605bb7222222d614e1ea22aeec0c39870d1609", + "tests/test_context_manager.py": "c14d3ce760520544cda14273e76df66606e47ae1bba410f83746b216b4a6418c", + "tests/test_field_access.py": "7484f7b0127e1be332ac013ea5f5c61ec5da9f2cd7fef9fcde8a8e8ab1384c54", + "tests/test_global_config.py": "f4624ffd22cf4d09bc05466dac91cf952e81033b34ab475bd1442c3d96887833", + "tests/test_history_persistence.py": "547d0cba078c2caec8b6217ec0b8713aac1ac2efd69d143e6549b023e5cd4d33", + "tests/test_integration.py": "df082bcca0ee4d6cf1c3899b4e43ccd2a004891446a05480bcdb1930ed467c24", + "tests/test_lazy_factory.py": "b3f1602d85a6e91a1b4e42490dd5253d2d0c43371a193d7369fe149bc839ceb4", + "tests/test_object_state_metadata_contracts.py": "dbd555d1659a1dd79c8c93c1d5168282a8cfaa3d727ac3fcc4d7d913eea00360", + "tests/test_object_state_restore.py": "3319b7244e0be237896902955f4c49829d4135c057116885935791bd8f125a81", + "tests/test_object_state_update_instance.py": "aef1aa303a2f3d68246f87a1a6871c0e0e48cc12d5574ef105be5aa3202a7526", + "tests/test_parameter_owner.py": "b28675a55ab72d8fdf011f972e3c5154fc683fc8206a7195342ac0a774bd873d", + "tests/test_parametric_axes.py": "8717ac074d559294e00a3d33afaac6415230efcce78f38b1e296a11c9596dcce", + "tests/test_placeholder.py": "1b9d20e8931234622ef1cf726d12e8e4fa00328c0b1cb33e9eb0fdf7acc1631d", + "tests/test_registry_subscription.py": "c634b5e416a7ea3ca04abcab65679c2852595c595b6e23cde6424ba9f6c7643d", + "tests/test_reified_generics.py": "548f5b6a282fc26b73cb5211fc8a9eb92f37fe9cc57e37cf964597293fe98866", + "tests/test_resolved_change_notifications.py": "d0addec005be985da187845efba11e1dd03367b5b9557cc1fcc54a119e42c66a", + "tests/test_subfield_semantics.py": "f156d9666a909abe9c9dbb8f69823b1e88edab310248687c4e012ab97e5a43ec", + "tests/test_timeline_projection.py": "e02529d96f61f3601c91fe30e133c4f78107d3ea6f65c864198b232b8ad76780", + "tests/test_transaction_checkpoint.py": "531c4df6b3fa25a28ac17d020532179877d4f53d66f57af1bff7beb07d08f89d", + "tests/test_ui_visible_field_projection.py": "27e016e8755374e9322460b83344836f9a3f1f0e54e9303c4b16e6abf775baea", + "tests/test_undo_redo_cascade.py": "5f8cd61f70b966ac506b5a2ba4460d58c789137f7f992dd7537d572efdcefc67", + "tests/test_value_semantics.py": "fb98e8637abe45a981faae3b61fcc9ddc14ef84b4ef5e41b95fa5736bdae2968" + } + }, + "PolyStore": { + "root": "/home/ts/code/projects/openhcs-shared-runtime-plumbing/external/PolyStore", + "head": "84f322e46871de5ed47e7fd20976ad03e533c1c4", + "status": "", + "tracked_code_metadata_hashes": { + "docs/source/conf.py": "7cb4e271ad57f025b60ede742e8df94a095dea71e32aa44cc7527180d8d90bb7", + "pyproject.toml": "2f3edfdb1307f3300e3db056b2828814017270a9726f4d7bdac52caf84f96f8c", + "scripts/release.py": "25ef09845453b73a29ea84c303c6c77a79f291775bdba97347f296f35e8629d1", + "scripts/update_and_release.py": "80896e49f856d6a193bb1dc3917f55d2878acf1476c8240333eda16b92e8e61a", + "scripts/update_version.py": "e96bc7111806eb9a15b5d739329a6a41730fc0591e31f7975e52b17e67e1f721", + "scripts/verify_release_ready.py": "1c32aae8b7ab774eb0fb99d0a51ce6535ed7ee5020f44ca9dc8f6d3ae50af01a", + "src/polystore/__init__.py": "15dabedec6acd92b025d4a0b4e8151227a4496e8b7d5e87f28713f7799919b5a", + "src/polystore/array_payload.py": "3eb7e2cf8eb01a2b10b860778a8fd4ea47e7557fdbe28ab8adf31045088e145e", + "src/polystore/async_init.py": "21d9b8af54ff21247a2012d1ae759550ae0147cf75f667a73b4a5a8387bf3d0f", + "src/polystore/atomic.py": "5e412948fb919ce27f09971b0a5fdf0851499812e4c0ae8bd456abbd8ca86b90", + "src/polystore/backend_registry.py": "a4c4e73a79aa4363301f76727b35db2c8b55fa14107f8866a05f74da4fefb64c", + "src/polystore/base.py": "ae641affae21b61448816a14bd68b7f2f3fc0082f0f1fbe51079ed32520450b1", + "src/polystore/bioformats_java.py": "e7ee3fc888dfeebaab48681ac3e9dd2463cca693a0ff0d77ce274c72135666ff", + "src/polystore/bioformats_storage.py": "0f57d0b35619de4aa6fdad0839ae689ae6f91b0be099fd0b28742c28795f3b71", + "src/polystore/config.py": "62c8acad3da3abed8ccf7f1166fc6a0d094be1c3e842cec6446780541df292a0", + "src/polystore/constants.py": "246da078ce8a3aae6d1fdb76183f1ff724795014cbca4d670b80174f74084635", + "src/polystore/disk.py": "cf089b8c79f86099a7cea1b6cbe43ceaec6331196a44b43183f2e64b7746c219", + "src/polystore/exceptions.py": "0ecbd94af34ae3d1a37eab29e1f72be3e68c651c9a93bf52264a783a8fc65ab3", + "src/polystore/fiji_stream.py": "62a60a36f1da5668f60bc0b4e3a5d0e65008042b2361787f4909224dd9b51db9", + "src/polystore/filemanager.py": "877a2b10731b12904b1784a5b42c9e39688e3a84f7f86018c1c2beee96ea2296", + "src/polystore/formats.py": "6f7ec984549b45a31bb32b9051672e996a67d9caab3ce2030ddc7d50e17101c5", + "src/polystore/imagej_distribution.py": "29000b7f7d8067a8a1667fc8ee603d3c2f33fd87f0a318a21cf0093473df49b4", + "src/polystore/imagej_runtime.py": "150983b2da981b31f974ad384a0fbadf5804003fd59c085993701b876384276a", + "src/polystore/memory.py": "4ac155187a80c5b6f096b2170d6851e6ce119938222beef4bd99440cbccba9e8", + "src/polystore/metadata_migration.py": "4390dc79fd1f9dd3fa405b961ea15573037bc19c6db2b31b36a5bcb7170abc5e", + "src/polystore/metadata_writer.py": "987b8ff3cfb45b170656fb7dd405abc68c460381871ca26cc26cf82e0550e256", + "src/polystore/napari_stream.py": "29286154e37d7eeb2c75d9a48cd12fa696f40c02d5ac35865360b7673801f905", + "src/polystore/ome_zarr_metadata.py": "ed0e4a3de24a6f590404d98578e19b76c9c76b27e854639e68b521f49924fbbc", + "src/polystore/ome_zarr_storage.py": "005bdd6ce615ff115050e1853aab0c56c50c65d1361c31ef6219bb0d6c4852b7", + "src/polystore/omero_address.py": "8a3ae8789fceba0f099eef2ee983d858a8f83b764f19b70dfa7bb88f521c0c92", + "src/polystore/omero_local.py": "35d39564317510234c2a3ded00bb06a09417d1b6f7359e52e74c7bd0a086f642", + "src/polystore/omero_tables.py": "910aaa6bea75f40bae82df71dd414415278b1387b80c7f1bc11e8d681874451e", + "src/polystore/omero_text.py": "9f51daef3d96cf191994d8a9c7aacf6182941ec2b0ada3f5ba61e0cc4f9412f1", + "src/polystore/roi.py": "fa945e9a3f7272e72f3ea022c7a3ae6cd30520ed7924591dd3f3cad5d0e71fc0", + "src/polystore/roi_converters.py": "50c37e5b070db7cbe418a5a64576595ee3ea7e5bbcf014492f38958f4debbf07", + "src/polystore/source_tile_geometry.py": "ebe4a422ec76972f2318ba56910ba6ccaef28734764ed3b88a3ba478ed69d322", + "src/polystore/streaming/__init__.py": "e401faf53b6fb9a08e237535313e5b99d7e371372875182b76132921233e5adc", + "src/polystore/streaming/_streaming_backend.py": "7caf2252677a894a5a761b60c473b92184b83745608c2b50a75eb1733c77cd10", + "src/polystore/streaming/base.py": "353e87dc92ad8df2ccec0136ab8ef7de7729896522cf8b16c0ec79bde455c176", + "src/polystore/streaming/handlers/__init__.py": "78daf5078f1914dc5b646ebeeb79bea336036b3b4737e122865be41ab7b24f03", + "src/polystore/streaming/handlers/fiji_images.py": "b5f06390cfeaef6c6a10c06bf9af00eab29f90ce1bb2899794d2e516cf86c7d0", + "src/polystore/streaming/handlers/fiji_rois.py": "5edcdc8d7344c5793730beb5a1c3baea89e96b13e62b746cc1cbeded3217a978", + "src/polystore/streaming/identity.py": "a8e48d76c8fd222515121e2ebc879dccc820e0ebf5b576a8728c1a7ffd35dc4e", + "src/polystore/streaming/receivers/__init__.py": "07953a0ecae9f307b9a2706062c1f452648267887e8cd03fbdb87d4b301b4877", + "src/polystore/streaming/receivers/batching/__init__.py": "c045df607fc2c07983659caff8b191660dd2ce5e73aeceb89894be3a457926ec", + "src/polystore/streaming/receivers/core/__init__.py": "b61b9545e4e373556d5beb87a23c3f6475238361d0c3a4cf18a3a5e197dedcba", + "src/polystore/streaming/receivers/core/batch_engine.py": "36d1dd9a7a2beae134c6f149ca148dd4cc2921fa2feb37cbcb88c42312743055", + "src/polystore/streaming/receivers/core/contracts.py": "c89f3f2dfb3e23482112a6b98ae3238d1768b2f4a01b4b2d605b698bd307f561", + "src/polystore/streaming/receivers/core/image_presentation.py": "dbe1395afce5a280fc1ab0c66bf302e324577ff15c6dff7a1698f50f1fc72a73", + "src/polystore/streaming/receivers/core/layer_inspection.py": "e93f707de62516eb0d48d9fdb0f71862b4265389723ca92a05145fce99a28802", + "src/polystore/streaming/receivers/core/metadata_presentation.py": "5e021ff3659039856616a75b4e19918b709121ee4b8902d39430f110b1adc5e8", + "src/polystore/streaming/receivers/core/viewport_presentation.py": "936378683122ad28deba10d71e9561abfc904017582e345316bf1c760d2abfb0", + "src/polystore/streaming/receivers/core/window_projection.py": "e95d0ccda66aa866082c5afc5bf9908259c727ca33270d3f069384651e1783e3", + "src/polystore/streaming/receivers/fiji/__init__.py": "a86e999752d8212da5bd58f1a3db43ffa7ce867fb0ae1209bf0f148bfe5b4b50", + "src/polystore/streaming/receivers/fiji/fiji_batch_processor.py": "165919f2e36c37e221bdd6f17e4398795745a95da9ddd7bba0ab93d74b9bdab2", + "src/polystore/streaming/receivers/napari/__init__.py": "ab4ba3ba76e98365e67abf6f9d79c0be098a774bf9e17e8523fc430a0babb9f1", + "src/polystore/streaming/receivers/napari/image_presentation.py": "1abb2d4c33ea073deb415cf9a242baa64d0b9d73985b7920d72508f2a5998a8e", + "src/polystore/streaming/receivers/napari/layer_inspection.py": "6bb86202480b5aad55d6641e3a6891105c5284d4408ff1cb7f8cbb36929888cc", + "src/polystore/streaming/receivers/napari/layer_key.py": "650eba665a58130fd67c38b164f97f677d805e07e53c2c7f367f850ae6db4a8f", + "src/polystore/streaming/receivers/napari/napari_batch_processor.py": "c3ed3f9fb4d89f82cc887a4d7775db911c96fc7e67d437cdfe8c4b78e62a16e3", + "src/polystore/streaming/receivers/napari/viewport_presentation.py": "aef743171c8d27ac56b53217f8fb2352584d447b33791ee2ce3e1c8fa4ade356", + "src/polystore/streaming/viewer_transport.py": "3b883e3078ded6b8e9c80e135a2eab876f9399fb7813dcd1dc8dd8f6049a3537", + "src/polystore/streaming_constants.py": "645e1685a1cf23167b2f6f06d4807c76444c339fbde66f3b12de9bba3adbe170", + "src/polystore/tiff_header.py": "714c6ddd0ff3928df37b2e65775c2987c285538b2375ee17254ae0dee868fa6b", + "src/polystore/utils.py": "1dd12cb9742f58f9537f1e23832f44f90deaf4332de8ae9b97e8df711ed261c7", + "src/polystore/virtual_workspace.py": "b19fcda85369d812e70bfc1a0efc5d8861ae256f69e94372e2e9515c591d36af", + "src/polystore/zarr.py": "b9404235a3a7882dd6f64383c11aeb6073c8f3a6485bec74264ad6434a1ee35f", + "src/polystore/zarr_batch.py": "8ed86ac525b561e6557a8ac55c6cb0d3954311a31d3ae65ea84af196246d60d9", + "src/polystore/zmq_config.py": "14123ce6fe232144f8a4fa086cad164a0b29e3b014ab809810d622e39ac92854", + "tests/conftest.py": "5ca35c45b774ffda10030f950823eedb160628aad1076b805e401e277bf624f4", + "tests/imagej_cache_process_fixture.py": "bf04cebf8be7fb4e911ec1df75cc3e4fc311745981975620b2946b308c461d93", + "tests/integration/test_imagej_runtime_live.py": "bc1c16a755e12682b5b94b81b902068fb4a2e4b8f8b708a43c814b28ecd78bf5", + "tests/metadata_namespace_worker.py": "5db68ec70ba017d1b81cad2302788dbcf0a1cce5be019358259d3aec5ffd7ef5", + "tests/test_array_payload.py": "95a02bc11cd21f57da986ab8f90196a7343c70513d034e69dec581c413ee802c", + "tests/test_atomic_lock.py": "0ee06b9b53964df6777f72cd0ddf0faaa63d9316ec7edfdedb6e1b6b9087f736", + "tests/test_backend_registry.py": "6af08f5ca00bebde1a4190bb40cc2df99e025aa3465521770a22da0cc51b5651", + "tests/test_basic.py": "803e5643003e4bea60fb48c89cc84d732c77b2a3a68bccbdc6be3747ca053aba", + "tests/test_bioformats_java.py": "bf8b626c3081c26de73902822129ece5128549ec6544b604285dc9b1ac2ddc6d", + "tests/test_bioformats_storage.py": "e97642deacceab3348e8a337b34e79da235c1e1283fccfb844c0142f8632a35a", + "tests/test_ci_action_pinning.py": "a636dd4d70e231d06c5de2802afc89c389f678b3b49a40ce8463eac5b0c308dd", + "tests/test_disk_backend.py": "423c0bb9389cb002ad9c5384add0e08d4c8f99c4279ffa7d1d6621ff90ce9747", + "tests/test_disk_coverage.py": "8f0486f4611c3cfbc449e5cc7276a3cbd72f50de683eeea9647e68a0d2533be7", + "tests/test_disk_more.py": "78ae7241ab3a1d031bc733fa8f20368ba24b225c59cee47ab00624da8b77aab4", + "tests/test_filemanager_backends.py": "f506fde0e7aa4fac67c9682fcb7ed5c850c0a9ac069dd3a6ed7732427ab5578e", + "tests/test_filemanager_extended.py": "3b20c0038ba749f3d4a0f787d2e854bcd7c4e65ea58305231193f415fa734a4c", + "tests/test_imagej_cache_environment.py": "0c3b854542257e09ec13806048ceb5cc3deadf1a6302c621f44c3bc4e6efa9c7", + "tests/test_imagej_distribution.py": "fa7740c10975dfbc2e654ac3b7fc0df0f2f08599a98966e51c2c046f8cae01e1", + "tests/test_imagej_runtime.py": "fb5ce7c8d53354eb46756644d3d0c135bbeae10a21a76718f67e458f046bf480", + "tests/test_lazy_package_exports.py": "ee015f6ea3c2dc198f522db1f494f886b892540750ca3289f7f39909c4700460", + "tests/test_memory_backend.py": "ac68a5bb7c0cc4bf6a74bf3d54cc3a75df2986b9f73e92af5316d878f678b16b", + "tests/test_memory_coverage.py": "a53b737d7899f52ccc0f683ae0312094760e532be709c17a5accba7249a60542", + "tests/test_memory_type_authority.py": "ec326f948fa957b0cc8358629199442db397b9676c884d6e730e23048468cdd0", + "tests/test_metadata_namespace.py": "5a4809a6024b6eec30130ec1bc98cdd6a2541779496cea717f8a89a98d5ca5c9", + "tests/test_metadata_namespace_ownership.py": "175737cbf844b620132bea2c5e5827ffaacac01bf53de136ea49ab32727428f9", + "tests/test_napari_batch_processor.py": "a62c8875fdde0bac50059aebc3b54dfb736246ba7e1980301ebb9e7124592cd5", + "tests/test_ome_zarr_metadata.py": "7444723fcc0bd0cdedd6d8b9454be57ce929caa61698539da55c782f0b7fb420", + "tests/test_ome_zarr_storage.py": "b70a93d160cb0ba0acfd603c75ade965fbf1f95158d7c0cfc680e6a8ce907deb", + "tests/test_omero_address.py": "15d81321402ccf015f5d687b70c27e64d58ba97e719bb090efeea74821e7ffd0", + "tests/test_omero_image_plane_batch.py": "ea857b139476e127a0ee026c20cf5ca918a65f45a70bc1fe27787313d6e830b8", + "tests/test_omero_tables.py": "45ab6ce56b95dfcf9b2c202e2caa587a67bd0de4769e77b36a9986b482cea159", + "tests/test_omero_text.py": "ddf16ee95766440f5ec4d3b0e60e159cc70b2f7694e7136ec13a72c85117ca88", + "tests/test_png_format.py": "f74821025f5d962a45287d88ede69793601725f6f158fdd9cb4c9635ad75cc62", + "tests/test_receiver_layer_inspection.py": "b1b153e86978aa31e933a26072be6e3a19f4881408d10df9044fe351ad0483a8", + "tests/test_roi.py": "a2f256e55483b45ce54264fd667452230ec1024ddb7f57432711b6be7278912b", + "tests/test_roi_region_properties.py": "5cac0122893d4df5289b2e7a14018625333103a713c87ae8c559872958ba0743", + "tests/test_streaming_cleanup.py": "2edca13d06e25936076c624fd9ff6db3b66d0997c08c41082c822a0b7e8fe8ab", + "tests/test_streaming_identity.py": "7d0256ce52d48d944961bad8dff436197c45dd0ac93c8946bc354aec0cfcabff", + "tests/test_streaming_metadata.py": "806cf4998459d90ed532307a48d0f793bfada277c69537ec031fd4e28babcb46", + "tests/test_streaming_receiver_core.py": "c79e5a32d46c39b6974d421bef2ec90099e67d6b6e13d4baa5eaa48588781548", + "tests/test_streaming_return_route.py": "e20baf9443d034ab18bfff0bfc38d60af7d7245e94ec0b7e01f80d8ce403c120", + "tests/test_streaming_shared_memory.py": "c5ecc8e9c027f5fa14783ce9e568b07eb3f39ddd07254e5fa2f9847d1a86aff2", + "tests/test_viewer_transport.py": "35f4e6ffd0bb7de1c5ebb1163e2d46d71f8dcbd0544fe3494e197c2d1ac610be", + "tests/test_zarr_backend.py": "7896159c33ee7fefb89976291e35db507acf282e92361c1abd86eca54160e2fa", + "tests/test_zarr_batch.py": "883da1109c08c53ddb5e03397e5e43bdb0f878c2da8ec4c4158ee9a70520bb17" + } + }, + "arraybridge": { + "root": "/home/ts/code/projects/openhcs-shared-runtime-plumbing/external/arraybridge", + "head": "1e53d03d9f468322a1c085c8be29485fb139caf2", + "status": "", + "tracked_code_metadata_hashes": { + "docs/source/conf.py": "1b43c862dc93e51e93c0ef0435a9e5cf208665d84a2d0ed1c2477d5e4dbf3832", + "pyproject.toml": "8134c94077df2defe89328963c250e6defaa2ec2b5f49c190f0966e9e575661b", + "scripts/release.py": "3e9caa24fbfde769e347f52542a557610bb02249b81aaa71649bd579d081b69d", + "scripts/update_version.py": "9d57e7324469c34691c4561466cc4d3330485812ce1c14418d8034d30bd85a3c", + "scripts/verify_release_ready.py": "bff7cb1232eeceb6d6d235309cfc75866c7b6c1f44c48f5b2ab65f856830477e", + "src/arraybridge/__init__.py": "ae1b2dd87b91b191356e9e1fdb74a32ba7131cda6341402c4a7aa087d47a4140", + "src/arraybridge/array_geometry.py": "0a8a5f1a61552b057997e94e4c89a19b5615875f307949e0d11784179f2a94f2", + "src/arraybridge/array_operations.py": "7e9ebda3d058702cdf8662dc5ab1faf1254649f3cbde76fa0adcbe02677fa103", + "src/arraybridge/array_payload.py": "ebf8c9c0f4726b4307ad435203d9173e54c84a92dad06ef8aea0d56a51972b1e", + "src/arraybridge/converters.py": "ab8e04e8f835ac9cfccaae964083cb3993efd65aaa7147041b81a1e03e4cd7fa", + "src/arraybridge/converters_registry.py": "fe11ed6ddfb748c20a530f1fbcdbb7b703d6258eea1f237243f361569dde4874", + "src/arraybridge/decorators.py": "28f9bf4770bd4cc1ef34045fdef28065063ede029296f62b8a6c4140cc88e40e", + "src/arraybridge/dtype_scaling.py": "283818c82bf182c4172c9265fdbb399c7e3acd08a6b00edcd4e96feb1276cf5a", + "src/arraybridge/exceptions.py": "7e17a3e594b7f50701156ccd78d623a2a78b5673a0c30a7e7162267b6e63caea", + "src/arraybridge/framework_config.py": "e724d3b7d356730cca0cc5db73e7ea09d88597ea6228eb774949f19915d70909", + "src/arraybridge/framework_ops.py": "eac427f155ed3e7e10ae0f9cc603a9a17d7c3f54b30db339e23f33873c94fff4", + "src/arraybridge/gpu_cleanup.py": "003238c0c2e4f4620ef6b27a049b8d937b9923200da00a89ed51539cb273adb1", + "src/arraybridge/oom_recovery.py": "846bceb891d54a8a6222a6e5ce830596a5bcfe82ba66500617f978a5f1258de2", + "src/arraybridge/slice_processing.py": "e51d743a61d6f0149b245c13d5445dd92cf73f5e68e4485a236567554508be44", + "src/arraybridge/stack_utils.py": "1558a43745c6f97af2c3ed69f4c6924544d97adee42dccb48131044528dcf6be", + "src/arraybridge/types.py": "7b94760d57e54008d7adf4e4d37dca45d8a2c288a9dc8400be00c26148cf7624", + "src/arraybridge/utils.py": "46a4e1b5849c3c4384406e3dd5d03c6ff56f5138e07058702db8747ab42ee4db", + "tests/__init__.py": "a0ac3b43f372047f4b85b516a7e4471f98744cf549dbeec484ff3f79728f0257", + "tests/conftest.py": "6a2fe5d7206ac30e37e544ba8ff939ffa4576565b9c7559fd660f0e41db6f24c", + "tests/test_array_geometry.py": "f9c0e91d6275a940399418f2c0de810ca90c781c30219d720b745f1cf912f0a3", + "tests/test_callable_dtype_default.py": "52f343317d9d943bee46edf4edfa7e1dfd9afbe2e2ced2a297b9bc764bacedc7", + "tests/test_converters.py": "390edeb089747cc13a6378d715996f238ece1610f7aa4e9e0c3a6c932a23e315", + "tests/test_converters_registry.py": "f40974f61e55cc49fc31dd53af0dda1790301edc2218d5ec6dbcbda0c712365f", + "tests/test_decorators.py": "0b841b293e82b59133c895159c9773552dc4e7a508bbe7976da45aaa1e5d894d", + "tests/test_dtype_scaling.py": "7fcea155eede82781b9566166079a86e403afa075c5335118696a390c1ed531b", + "tests/test_durable_decorator_context.py": "53e4bfb9a2491b6d5a238ca4aaf24ffce5853346d46621e25ebf92adfdf70642", + "tests/test_exceptions.py": "441f10606ee9175814b01a243f8aea572f341bb53517c31c2c719ce41a36fe27", + "tests/test_framework_config.py": "4995b8649bef8ff4bfbf80dae282a6c8e008b0120f1a905f118903fa903e8d8c", + "tests/test_gpu_cleanup.py": "bcd6ef4f4f56850e1f12eaf9fc237d756b06841bd1c2d093ebbd81cf1a04a0bf", + "tests/test_integration.py": "e8d87ba83d8fec2ea7a9841c413277516bf665aa08f688b29da819a4c545edd2", + "tests/test_oom_recovery.py": "773b96e901e85663b6418d4deefbb28bb3ac9298b7b20589373cfa0de05e2bf4", + "tests/test_registry_integration.py": "49b88b4f692af569b66c298c8751a78b0e28a08f4db41034ef05684352df5418", + "tests/test_slice_processing.py": "b7f996dd6c38382148c1b48c5428d33775c3067f91562f082de9ec0ce20ebb5f", + "tests/test_stack_utils.py": "918e015e452b31adef93ceb829e949043390d22888daff9a295317940a58a6bd", + "tests/test_types.py": "ae1701b48c68c6f07bb23d83800ef3ca049545c254840ad5a7863db4213b3c16", + "tests/test_utils.py": "15a78a8269843d90b8dee4a2b4a50cd1b34bc791fcd1588260d45cae3956aab5" + } + }, + "metaclass-registry": { + "root": "/home/ts/code/projects/openhcs-shared-runtime-plumbing/external/metaclass-registry", + "head": "393a7e03003cdc56df9013f932ed4f26e632d77a", + "status": "", + "tracked_code_metadata_hashes": { + "docs/source/conf.py": "1d1618299bdaed818c3518ccc11dcd99f7277e4574b78a1db345c85fa2d949dd", + "pyproject.toml": "2bc5a1a364bb17b03a4220cc3595d7889bfad3f731de3fe78453b16908b291c2", + "scripts/release.py": "19344fc3616a3d63e035df687ac82e8a040d0f7b69d5fb934c6e801df0ed5947", + "scripts/update_and_release.py": "80896e49f856d6a193bb1dc3917f55d2878acf1476c8240333eda16b92e8e61a", + "scripts/update_version.py": "f15d84a33dce9188e36576b21a84b7423dc868fc5e7eea22f8528dca83c3a5ba", + "scripts/verify_release_ready.py": "8747b1de6233c106025abe5fc4cfbbdf2a8d07641566445f4151b6fe4b5a9e1f", + "src/metaclass_registry/__init__.py": "b7dd8d905bcac00f490a6155df09e68d148f3e7b8238fed2854f2e1b5de06567", + "src/metaclass_registry/_home.py": "6949afb54c145a8c58f680c61177e3aa7a04e9b432b73552f5beb4b1b92169e0", + "src/metaclass_registry/cache.py": "10ba153bbbd6a45388a47aa28f5b18ab77847a33acec4820d31cf909153fb6ca", + "src/metaclass_registry/core.py": "5fbfc22dee24ac7cf653209ca06ca511ddedfb2c9e96a5354d7d4d4600828493", + "src/metaclass_registry/discovery.py": "757958272d11633e7b854c7104c07f0d90845c37fee6ab0c3646fb14c2ed8daa", + "src/metaclass_registry/exceptions.py": "333ae9a5b87cb884213a1833fde35c178734f5e684a8f772d6c0e35a090ec987", + "tests/__init__.py": "dadf7fb5f53a6c573dffaecd51460f3d36c83fd4ed903a5ed7ed21da331dd9d3", + "tests/conftest.py": "a2ad838c1e90bcfa8c368c51f07f473a85cbe4a95d27134349f94bfb1a57f767", + "tests/test_cache.py": "c0d077f47be112b257abf7ad574ca2bb4dbb7e7f3fbbe0c8044b69b69bf5c7cb", + "tests/test_cache_path_projection.py": "8da2d89cb7e88bc838b0a0d59068afe4a6ffc0010e3d4fcc196f55519d730ced", + "tests/test_core.py": "26446d33fcdd012ecf12ff7e541ce434aa38fb8f1fb110ee4ce4c74cbf0bea00", + "tests/test_discovery.py": "f28535b526c4fe95d68a33386c4378237468fb160867dd9b07a55de920939d3f", + "tests/test_exceptions.py": "11dd2d86ec4baea6834b28d9f368bb71c90dd91f23a12dcf795fb821a29ebf1b", + "tests/test_selected_discovery.py": "b20e2ff2441a37fc2700223e18cde1d64d1eba314521a656d8be2376da2c0b0a" + } + }, + "pycodify": { + "root": "/home/ts/code/projects/openhcs-shared-runtime-plumbing/external/pycodify", + "head": "792b03413331fdf107e369c924c257a2f0b16126", + "status": "", + "tracked_code_metadata_hashes": { + "docs/conf.py": "3cacb5b7a11daa491ff739fe6df58494ed38e50fde40d6bd6fb7c9b4d4eb782c", + "pyproject.toml": "1103178d6661d94d66f6b43039d0961557e6d07861e46bbf8df18b6d40a0b1b5", + "scripts/release.py": "9027086b71a742b688bc984dd2fe50c4af2ad6e5ba1b1c564cf531a414e19f62", + "scripts/update_and_release.py": "80896e49f856d6a193bb1dc3917f55d2878acf1476c8240333eda16b92e8e61a", + "scripts/update_version.py": "f93a6a1ab4aa714f699edab98d36d3f610c7b7e14ff5748a22fc656214958d8d", + "scripts/verify_release_ready.py": "aa51d925335037158e0d34d0b198039e4d3833df564f01622ae4005bc7e9d6eb", + "src/pycodify/__init__.py": "b839bc2a7cd2afdfd8756eb39f22b791cb4f7aa55c0fc9c11099b7219c9c663b", + "src/pycodify/core.py": "b32dea495ceb8745c1b363f2e42aa62eaa28a1d54198a5d6541e0b1546facfb4", + "src/pycodify/formatters.py": "08791a7f3cb779889ef24a5bfb395a1acfb4d3320fe9a7cf037986c98a9d7faa", + "src/pycodify/imports.py": "17fc080d60ef168a9c211f27111b04da1d527d2bf87e2188c674e4de9d25bef1", + "tests/__init__.py": "efd53630f4e3c520366b8aa444fe2d4d33c07c7caa0e3841fbb43a04cfdd9183", + "tests/test_core.py": "164dd47f93e4adfd8fe9de851f5db74744509179f6eaaae877be879f22689e66", + "tests/test_imports.py": "ce63bc44d8fc24a02e3439a7b1504b4dacf21e64ac51f1581e60f0099664c3bb" + } + }, + "pyqt-reactive": { + "root": "/home/ts/code/projects/openhcs-shared-runtime-plumbing/external/pyqt-reactive", + "head": "ad4948775ab81180a354d4b793d17ee5ddff3972", + "status": "", + "tracked_code_metadata_hashes": { + "docs/conf.py": "2ef313507c9b6ce6941cbe82863a0373b818ea5627c9dec2824953ffc7f312d3", + "examples/tear_off_tabs_example.py": "5506bb39a1953d99d1a8b730851a53233dddd79aa0a4cc738f422fad10b2d640", + "pyproject.toml": "524865db6ac36de136ee2b8dd43b1c741152f5ade3f8195f637650d252dbf8c2", + "scripts/benchmark_form_construction.py": "a2597d87f99c5ad0859e66732f7f7e43a73b4c896b3733d2ee9d501227d1a3d9", + "scripts/check_ruff_added_lines.py": "67ed9e6064204caca990b7b7054cd0774f59b235e241a4fa46db28ad48213ad7", + "scripts/release.py": "09fa8c9c7ccd7f4936aa9a9994b6c47726ce27e8bf99e0cfdcf06d528ff4565f", + "scripts/update_and_release.py": "80896e49f856d6a193bb1dc3917f55d2878acf1476c8240333eda16b92e8e61a", + "scripts/update_version.py": "3f771c59e65b5f6674faef3dafa3b660f152b58ea61114ae0a37f840a9ce4f04", + "scripts/verify_release_ready.py": "7a023ed885a132d922f1fd3942dd56b66158958192f7e5371e838c537cd5d2b5", + "src/pyqt_reactive/__init__.py": "5b37a90f246987a98087428e80d2cbdcc030b80ae962c347482f21719054f68c", + "src/pyqt_reactive/animation/__init__.py": "f449133ede280b74aec90dc85e88a9a2800e38c45b239996d9abde51d0cee6d9", + "src/pyqt_reactive/animation/flash_config.py": "efc6859a2de5115775a940be0a9e1c5e58f7e60e068872f11a256060b045fdeb", + "src/pyqt_reactive/animation/flash_mixin.py": "37cd2b32047109fc16255a3f9a9938ae553d5f8bd5987cd10460c7ba500f1486", + "src/pyqt_reactive/animation/flash_overlay_opengl.py": "f6bc6452e56cae0a17b07244547b55820c091fa20615de3ab1a8159f638f1dd0", + "src/pyqt_reactive/animation/flash_trace.py": "9a2e71467be16a31e0abd1f7999253e4842447289400d15cd3ff995fd06af9a2", + "src/pyqt_reactive/core/__init__.py": "c111f382ba9945651f0b385131896ea2ae522141f7a91f7a1e1eae9657e97122", + "src/pyqt_reactive/core/background_task.py": "4afa0d07107479f028557d23f15a6c5146cdb9f33e253b81f856f9092824c2aa", + "src/pyqt_reactive/core/collapsible_splitter_helper.py": "8d959845294513e0240c994c1bd9142a968f682be994cb2e3fd02f38f0403c3b", + "src/pyqt_reactive/core/debounce_timer.py": "7c3bcef0d7dc0107df0d413f6188ece136e3be5cfea473e3e74b69496af95763", + "src/pyqt_reactive/core/deferred_callback.py": "b52167f36cef2e58bc0f6b38c15359cbfb28a12a71c3a41ad9ac62155f357803", + "src/pyqt_reactive/core/future_completion.py": "7ba6ef203bae81319004ccc4ddfe4226e0230b177df663cfb0d02c364e1dbc47", + "src/pyqt_reactive/core/log_utils.py": "8a2f67434497cf5bc3d637b945b75590c735ff8d97b8db56f9efe5bbaff5e351", + "src/pyqt_reactive/core/path_cache.py": "dfca58e2ae9deb036d4df4038ce8f78125cb5ddeb2e691293bb4d54ca673b586", + "src/pyqt_reactive/core/performance_monitor.py": "09299865cc6641b87bdc195442a297d982ee3909b35c8d2063859933180834eb", + "src/pyqt_reactive/core/reorderable_list_widget.py": "edbba72d7aa88362860b923c0b86715a8349dfec9a7d85660f12cf33d859429e", + "src/pyqt_reactive/core/rich_text_appender.py": "fce3d7be3654c988f6eb16117231e6032718b28608f99f625054ff60269768a2", + "src/pyqt_reactive/core/sort_utils.py": "ac9cb5bd209113980dc398197f461e19e6c9716a5110a889bfdd7bf7b85f3d81", + "src/pyqt_reactive/dialogs/__init__.py": "a9ab9bde099f50c796b1c29c77482f0bf771591fbb12514f1b9ab8dfbddeb4a9", + "src/pyqt_reactive/dialogs/group_by_selector_dialog.py": "a62fdc652c3ff9fe2ac3babce17968a5786877e38ff5b6c2a10ea0678d5a5e67", + "src/pyqt_reactive/flash_trace.py": "c0110553bed7908221c10d1a33e48f4324366b047eda75f5b2561b68f5855063", + "src/pyqt_reactive/forms/__init__.py": "5c9c1af2df09c479f1cfc50fb8d8ee7ff3e157a940d137fcde8af95c95d035a5", + "src/pyqt_reactive/forms/form_init_service.py": "4d34e5f2494a075f9aac68e40c07b07cdb06600d832bd5b510ee971db980b82d", + "src/pyqt_reactive/forms/form_value_validation.py": "ec3ca9d48168b651e8fe0b0ef6f90a67ad32f145332a8798f1cfad220c9fd597", + "src/pyqt_reactive/forms/inline_dataclass_chrome.py": "5d224ea256bffbb840f380d9ad75f9f98b7c3ed18fc4b1ba4db0b64f92a428a1", + "src/pyqt_reactive/forms/inline_dataclass_context.py": "c0d7a19651dc1b88e66b656a031b39a7aae67ff8998e50cc4e5962a184f4fb10", + "src/pyqt_reactive/forms/layout_constants.py": "a16ebe77ea6fb5d6c26d658bc586dd8357d18506c0935cb6426edbffc906ff98", + "src/pyqt_reactive/forms/object_form_document_renderer.py": "c47fada4032d685b6bf7dca4cb9f31e4e9240a25bd1039b165b045d5de541f1f", + "src/pyqt_reactive/forms/parameter_form_base.py": "f35bb8ddd6c630a47cc5095df93ffee608a384abadf755b2f0cf12bcc963b36b", + "src/pyqt_reactive/forms/parameter_form_chrome_sync.py": "94fda5faae9adaddb1cddd1b7a4d3b0452906828d94c582f6024ff72d89ca4a3", + "src/pyqt_reactive/forms/parameter_form_constants.py": "4d4ae05b91d525108342a10e9b2da7860b5a52b0028612f244339d4531d02097", + "src/pyqt_reactive/forms/parameter_form_manager.py": "a561e62e29af42630fa107b0929fc87b74007f7ed14ec8f0a6881c49070dc923", + "src/pyqt_reactive/forms/parameter_form_service.py": "0cbf305c99b0d7ab0c558162ba5d86dcd18f81eb8cce0ad271fbd4b3d15ebc89", + "src/pyqt_reactive/forms/parameter_form_tree_index.py": "e343c3ae5535bbd46dff964844225df13fad5431253eb3924b86f8c8d1dad273", + "src/pyqt_reactive/forms/parameter_info_types.py": "3f87562ecc9b1a4cbf541767ae39d53e1b2dcfe71d9a5e211456d8b34a0a75f8", + "src/pyqt_reactive/forms/parameter_type_utils.py": "c7fd2f17faa1e3d5c3027179135de51b859d204c4a2cb9f9be10837652530ea6", + "src/pyqt_reactive/forms/parameter_value_contracts.py": "109b82548f6a90e1d495d825eeb81309b555b6122db67867a2c91769e9c54f78", + "src/pyqt_reactive/forms/ui_utils.py": "96521f8a8b75f501ca9f2dd34f377025a7d5fdf543dcbed84ec17939875ca0ec", + "src/pyqt_reactive/forms/widget_creation_config.py": "01f32efb0dadc4d5e1a42344f43e7704c47468844b4a05bd13936e490ad4489a", + "src/pyqt_reactive/forms/widget_creation_types.py": "3b1c08a0a94334facb76afa273fef0d247a93f748ce86c3fb9e7026d46ef844f", + "src/pyqt_reactive/forms/widget_dispatcher.py": "5b13da9b2aee79344c343ddcbc296f125429c9780696ed35f996e982c3ee1562", + "src/pyqt_reactive/forms/widget_operations.py": "708bf6dfd51c01154e02210abe8840eeb496af51db57cfda425d51a365080b72", + "src/pyqt_reactive/forms/widget_registry.py": "6529d5879667827e7c6adf36129e6dfdfa18239c4e0ae32bec27eab7b03805c0", + "src/pyqt_reactive/forms/widget_strategies.py": "6eb7fc78526a247dc30c39493f36f54f2df5560b0af63d743d41b30b4f469384", + "src/pyqt_reactive/pattern_metadata.py": "7c2aa3f2660d530503acdbf8e122711cf7b3d1c0aae2d05bf6addb7091dcb4cd", + "src/pyqt_reactive/process_launch.py": "c324a6bcc3b5002bd4e6e5db184d4fd80bdcdeb7b130565ca52eb37bbaa08f9e", + "src/pyqt_reactive/process_signals.py": "8b4c44c98d6979fab6935485ea94d64bdfec6eb0143855078628451d4cd9ce82", + "src/pyqt_reactive/protocols/__init__.py": "2f59ae586807161ac8d019d904646684be0cc458b8b4bcef66f3d233002fd294", + "src/pyqt_reactive/protocols/codegen_provider.py": "98c030fbf5fc99ac8ce53bba0305f0591e0b1a12403b1cd22dd282d1df476fb3", + "src/pyqt_reactive/protocols/component_selection.py": "a68d8ca325a0972c7e911901a47521e44882699729f98bd4cfde57cda7b4d1fb", + "src/pyqt_reactive/protocols/form_config.py": "cfee9524e2e2c92fe1317d9cf1229580df1e1c04a45d9583c4b97fde1b3543e2", + "src/pyqt_reactive/protocols/log_providers.py": "c6602cec12dfa687a6f4e122aa62ebaf3ba7c5472a0d63b29ab0af9db3c18514", + "src/pyqt_reactive/protocols/preview_formatter.py": "05ed6999327cf4483a79d3bb5c9d1f1bc231fd3c0a33026e1e7069f1e8d3771c", + "src/pyqt_reactive/protocols/widget_adapters.py": "1ae6653bf3f2786eafa9f53f246e145ebfa34b81c190aaa8f41c19a1463a42f9", + "src/pyqt_reactive/protocols/widget_protocols.py": "4877e65746459f556954eb718d276748fd5211d244e15f1e92973e5ee01c24d4", + "src/pyqt_reactive/qt_types.py": "cca393ec5c0126b296c2462b414a84acd0cd1b5f63de9a2b6112a0b6593921b7", + "src/pyqt_reactive/services/__init__.py": "9ec1b77dc39cd6e0f550e7ca014c4e6a560ac0fa46f071c0f6ea47a6a34fe46a", + "src/pyqt_reactive/services/async_operation_executor.py": "f1252a0244f816b2187881bee484776ebd41529d1a17df08a1360967d4e03516", + "src/pyqt_reactive/services/coalesced_scan_requests.py": "5a565f9ed1bf34871f2802400fbe6b9ae7623cc612f07dbde44df10e92a1fe21", + "src/pyqt_reactive/services/enabled_field_styling_service.py": "0a16f22878c4ff68a3a75c937ad50314a0acf14014ceb39bf2420673a75e01ac", + "src/pyqt_reactive/services/enum_dispatch_service.py": "20b18b0ef51d7ecdab99e3fe0fb20fc38c605ce80995945f5e1033e24bb4bee4", + "src/pyqt_reactive/services/executable_action.py": "6942ff087473fc102c5395184aa74c8b6c8088cab5d7fa394bc5e103e41c5d28", + "src/pyqt_reactive/services/field_change_dispatcher.py": "0875f01a405b95de12cb0d4fc396d959a758d67a39c211d3056c512352d0f1c5", + "src/pyqt_reactive/services/flag_context_manager.py": "88b634b8aa899f82f6e9525101ade1f60b0be11763a33908ed3ad23ad3bfe147", + "src/pyqt_reactive/services/function_list_editor_actions.py": "68a0fe00e8b80cf1154aaa1545725b48b0c4b75e338afbcd23fed159d6d100d3", + "src/pyqt_reactive/services/function_navigation.py": "47664c983d2f515f2c4e6599eb4fbf269817e69f9abe68101872e6380ee60e85", + "src/pyqt_reactive/services/function_pattern_code_document.py": "bdf41b58bff009890e6e696cfc4f18313adfb1669566b2566f9e7b93d863c071", + "src/pyqt_reactive/services/help_document.py": "c0c1973e7ab02823c204dfd7003ac2a2b44acea36b895a82604ac31d0f4db1e3", + "src/pyqt_reactive/services/interval_snapshot_poller.py": "77fcd5b147bcf66782ebb1bbd6a9e9a8379c9e38672a47fd14fd2432eeeed11f", + "src/pyqt_reactive/services/manager_host_abc.py": "a9a3ffc9b2cc2c5d6f6b1224c89838f74bc9d41d2c8de8da39bf33f98483b90a", + "src/pyqt_reactive/services/parameter_help_service.py": "422f9e7b818f855f28979400512b4943524606827a74bf807bbf26afef1a589c", + "src/pyqt_reactive/services/parameter_ops_service.py": "8814b7c13bc56a2b0d3f69fbfb2a6153b4e2b5d0433779db6ffce5ba480890e7", + "src/pyqt_reactive/services/parameter_service_abc.py": "c5818d83e39444cbd88e135e2a075e45872d49f8c83c34e1d0e925e7de38d387", + "src/pyqt_reactive/services/pattern_data_manager.py": "ddcf9659dab6ee50ea3c155f57e1a4111a97e414ecf74b58151da1ef6557ac88", + "src/pyqt_reactive/services/persistent_system_monitor.py": "2041aaa91a3e242bd43e1686a65feed89078d18f8f7626bff00225895bde0007", + "src/pyqt_reactive/services/process_tracker.py": "5850b647c4c75ab2ced0ea91be34fe99b8e1a004cf3b2df597caa7769f7bc899", + "src/pyqt_reactive/services/scope_color_service.py": "b9d958c27205ca5a9eed16dd729a354642d90bcca96a519b01a0b19d94b1c822", + "src/pyqt_reactive/services/scope_token_service.py": "f84056a6b542d1d9a7df9e05c2ec344d6acb7cafd3301054158e89f04eb32b17", + "src/pyqt_reactive/services/scope_window_factory.py": "fc2a4606f89c85d44014ea6d08018e34921b8898816d8a09ca8febf80a448338", + "src/pyqt_reactive/services/scope_window_navigation.py": "39a0b4e95ce6e9a478c5882b91aa0822f53b683c9f2a706727bbf88922679331", + "src/pyqt_reactive/services/search_service.py": "e72ef88fdaf28d069bc80c2eec722ffa6921015613815929006fa35fd7de2054", + "src/pyqt_reactive/services/service_registry.py": "8f6dff4b7e6db9eb4ba2a0944d18cfa3c03b75e9cef85594b1e8bc7c5bd03e58", + "src/pyqt_reactive/services/signal_service.py": "04680bff5098b52edb1f4998356c1ef9543bcca96b3f37a7377ce4f98c1f18b1", + "src/pyqt_reactive/services/system_metrics_sampler.py": "7f0df0d9425a9998e371c86f55a27cc492fb71b36817ae07b6280f73e25b8ad6", + "src/pyqt_reactive/services/system_monitor_actions.py": "2aa205be430ff8b0429279a3ec44e5bc0c672b6da7a246a208f1ed40b79aded3", + "src/pyqt_reactive/services/system_monitor_config.py": "2f7259cafbdfd28f19d2a291afcd2deeef4e5eba2402bfb0e303a0aa643d6dd4", + "src/pyqt_reactive/services/system_monitor_core.py": "1712ed50c76e6303dcb96f65c8294922084bf2e3f90d21be56b766abca0dd39a", + "src/pyqt_reactive/services/tab_identity.py": "48274c530cd0792b3565c1becf1cef9c90d2f48b4e6ccec26cdd8807c51715ff", + "src/pyqt_reactive/services/tree_item_key.py": "e908f21bd6cb1621cd8356611f1ea0d7daa59d19171b211f3c3038598b8a5aef", + "src/pyqt_reactive/services/ui_thread_dispatch.py": "f2dbe7e8ce8ac0ad3116001d7dfcd91affd516a34615775ce4ce9921480e40bd", + "src/pyqt_reactive/services/value_collection_service.py": "33562f9b50e1080a26c28467ec31c3915e5f734bdc237cc04f4802db2b36b911", + "src/pyqt_reactive/services/widget_service.py": "cf3248353552db77b3257cbcf982987848fb16acd38ab2245dbef1fcf5e25223", + "src/pyqt_reactive/services/widget_tree_projection.py": "05d5221d64e74ff14483f203975f17543381d959c30ee5918c200fa8e9957547", + "src/pyqt_reactive/services/widget_tree_projection_config.py": "8f616aa3f2fe1dc88077dc612cb7c527970363692c6a8ace15b92ba56a806e4b", + "src/pyqt_reactive/services/window_code_document.py": "d0ab321af0eb2b14fac3ab2577c8c61672763d2f13a70a7ce8a35ee7bfab7c75", + "src/pyqt_reactive/services/window_manager.py": "84fff75ff74e59c5d7080f103ad1a4baa2c884aff3d2f87891a0afdae9a5c332", + "src/pyqt_reactive/services/window_navigation.py": "88da77ed07ea220b7c6874e71029b5bd50805fb2c3048701f9df773fe17257f2", + "src/pyqt_reactive/services/window_snapshot.py": "bced886e6bbfd69ae9bdebf950027b809e72c23eddadcaa5fcf7151adf7ae913", + "src/pyqt_reactive/services/zmq_server_info.py": "1050f0e9ada59580eba9a6c4a703678f0945f4a2cd83df4fb32251733911b81d", + "src/pyqt_reactive/services/zmq_server_scan_service.py": "ae12092dfd8b85a3354b87a708f3c32aef787c2f9fd0402158d32d14b654c7de", + "src/pyqt_reactive/strategies/__init__.py": "cab5cb18e271d822d459ab8b7f6427f03b8beee5ac9a4d04b4b73a0914d58e9b", + "src/pyqt_reactive/strategies/preview_formatting.py": "dc1a423246ca40f13633847d5d3ffde3916e53c1a8a8a92de893d3e80ad8fda7", + "src/pyqt_reactive/strategies/status_presentation.py": "435d97f7804e2db714cf60b236074c7a56fc87542b05a4d7a4b1722a340a7893", + "src/pyqt_reactive/strategies/tree_aggregation.py": "d28be44b8472fe1db805fb706485f02bae2c48d99e41f422dc534d24e0084c5f", + "src/pyqt_reactive/theming/__init__.py": "ddbd3f379805b6ebc01a86c86ba41999617b46498b4436563e5a427afea3866d", + "src/pyqt_reactive/theming/accent_chrome.py": "2ae2fd7cc5b3d2b60a99942792ed0621040339e117338107be01fc6128b36f86", + "src/pyqt_reactive/theming/color_scheme.py": "45f621f809accb5dd3fab03c1bbe95b43d680e6b4e03680e89ac2fe6400f16e4", + "src/pyqt_reactive/theming/color_scheme_resolution.py": "e8c9a7501268979d0fbb3e02f693b31a63e08688e80c3cb23abe0a856e1de09a", + "src/pyqt_reactive/theming/palette_manager.py": "00b81b76f39b561eb7a4bf37d0547bf2db6e7b63efa18ef752f614005f56be90", + "src/pyqt_reactive/theming/style_generator.py": "7433368f14efdefa05a401501122704c08f061412a343531fba5400a5807c885", + "src/pyqt_reactive/theming/themed_checkbox.py": "93426143261ca2c1da431264cec69e89ee76a35d7bf85d9e54db27e72c058ea3", + "src/pyqt_reactive/utils/__init__.py": "6b8f0030c2822411d7efb3433a2276320b985423ade229529196cf09fce17e3f", + "src/pyqt_reactive/utils/log_highlight_client.py": "db8e2fd014eedc85af1d0805250931f74257a402651edad2bedbc4588cc82f7b", + "src/pyqt_reactive/utils/log_highlighter.py": "e38237cfea9d757910a7b4ac84a8095068ea1c4531abd56c6fd15228b40e4703", + "src/pyqt_reactive/utils/log_loader.py": "c0cc1d6e4c7ac286562e2659b327e738f3cbed32686c09292b0d957ea61d159d", + "src/pyqt_reactive/utils/log_streamer.py": "ac195bf633e690695d61c1ea4cdfa7bcb45a4ecc20dc237f35bc220a87a08669", + "src/pyqt_reactive/utils/preview_formatters.py": "76a383c498300e2672d6b67ee0261e0dd90df0ed2fd64fc2ffd3513cdf097838", + "src/pyqt_reactive/utils/scroll_filter.py": "cb38375d28d5613f162dc020e2b8735824ceea767ae5cc1156c7086240e22fa8", + "src/pyqt_reactive/utils/styling_utils.py": "22d32d0e01fca8f575c15fa4a61b27cde2e46616cb86088733e8d15f312a1da6", + "src/pyqt_reactive/utils/window_utils.py": "11b222927102bc79d8a5f6a957bd99ab1e828bfb6586e1d647aebd564b99afb2", + "src/pyqt_reactive/weak_events.py": "446b5ea3e71eef459a2afd3663aac3e05a4242c6e9e21379f79fadf9563202c8", + "src/pyqt_reactive/widgets/__init__.py": "ddcf678b0dc5c03aaf11d2b6f12bb9cda681a29f0105925c51a5b9263d751716", + "src/pyqt_reactive/widgets/editors/__init__.py": "6fdb2746dd03af27487402eddb929f4f10664f6dab82726dba5894dee8d33878", + "src/pyqt_reactive/widgets/editors/simple_code_editor.py": "b84df51fd669c550dddfc5484e178f585d7b05f9ca3b230cab6bb82b3d0ecdab", + "src/pyqt_reactive/widgets/enhanced_path_widget.py": "5c72029a13fcf4e58a99ece7bb898afebab083f20c104f9255082b5f9ab1b232", + "src/pyqt_reactive/widgets/function_list_editor.py": "3a71a7af92cd6a3e304a030f7e821710af53cefb63c9e13cfb331c61b86d48b6", + "src/pyqt_reactive/widgets/function_pane.py": "da0f781f1948df80c856751fb6ca5f552583395d0f5b2d2beaa83d03738e1abf", + "src/pyqt_reactive/widgets/help_document_browser.py": "f8f0bb2e454ef458f2fd4f686f4d6814a717e8f1da0de1790ddb6f8a96795e8a", + "src/pyqt_reactive/widgets/log_viewer.py": "e68eda879c7cd3f5360dd1b4a08a481298cabf85cd72bdfda66b0efbd54d56f9", + "src/pyqt_reactive/widgets/mixins/__init__.py": "fc5176659b1c3239954ce3257c3615c5053c65e233ff350c869da5a4a356bfb5", + "src/pyqt_reactive/widgets/mixins/cross_window_preview_mixin.py": "663d644a603e6da4a07eb06faabc7f4eedc21ebab392094b3c76fa2f0abb3117", + "src/pyqt_reactive/widgets/mixins/selection_preservation_mixin.py": "94fc92d98854f4bbc611fe616033abccea19ea401ef9a3e9c53ea1017a77bd66", + "src/pyqt_reactive/widgets/no_scroll_spinbox.py": "f6be116d4976fc8a83ff576ef6ca0968ed95312b6333631a7705727dede10cfd", + "src/pyqt_reactive/widgets/shared/__init__.py": "e7525c16a67b93240ce3597d7f6e6b7b9bc9841d6e4b2de1084d10fd6aea2c4b", + "src/pyqt_reactive/widgets/shared/abstract_manager_widget.py": "92f8ede35a48392112c6c1aa17e959c3cc4a9450e69f5b1447cbc4ef5498dd5b", + "src/pyqt_reactive/widgets/shared/abstract_table_browser.py": "478feab4d742bfbc4170df25e5e3cc3bc0b409506192290817a064250dd9ff5d", + "src/pyqt_reactive/widgets/shared/action_tabbed_window_body.py": "246128cd317deb382bf830a67fc6468f768e6e5547ff0e17896b16e304371ecf", + "src/pyqt_reactive/widgets/shared/base_form_dialog.py": "9b5280ef06b1c8421b3889c1c477fb311dca3b5f960e8ecfb58a0a7812551369", + "src/pyqt_reactive/widgets/shared/base_managed_window.py": "3fda59f66502bfb9ce14565981f2c8738d074c78a08ae3afc9c8e00e93c882fe", + "src/pyqt_reactive/widgets/shared/button_factory.py": "78b69f821ad925773ca049d0c08715375a4d38eaf7ed075701417401b461a14c", + "src/pyqt_reactive/widgets/shared/button_panel.py": "280f904f101681461a8899b0dae53fd10e0f616e54511ee09c3324c2a589ee43", + "src/pyqt_reactive/widgets/shared/checkbox_group_widget.py": "593b9a2c1358556c710a9a3b406042e6e335cee5c46aff1e1ef42e5025c36a4f", + "src/pyqt_reactive/widgets/shared/clickable_help_components.py": "c480f45796f893e8f9431198a0d08f5acfee3ead9034764005587aae88902e4d", + "src/pyqt_reactive/widgets/shared/column_filter_widget.py": "7e492ba7cd8d2d91ead001be293bdd69dc6a98332102e30ed45d0bb6968be589", + "src/pyqt_reactive/widgets/shared/config_hierarchy_tree.py": "150062486b1e4d092ce703cee83a3d400097267dba3670d9a0120378f0045b34", + "src/pyqt_reactive/widgets/shared/config_tree_contracts.py": "8e6b4b0047219c2f7c8297de1baf7437b2571016bd2d82e1f2e1c520bc4a35e0", + "src/pyqt_reactive/widgets/shared/detachable_action_bar.py": "37157c0874e5495d3fc5c4b33042d6334b439a57d912e354ff48529e1d434ef1", + "src/pyqt_reactive/widgets/shared/dirty_window_presenter.py": "d0171ce0b9009780675f8eee0b528fe3fd2446c8b800934a476778e1c554a147", + "src/pyqt_reactive/widgets/shared/form_window_action_header.py": "18725af2a223d9c00b2496f298f745af1628c5e0cd07a2cbfc8e39b684b574c2", + "src/pyqt_reactive/widgets/shared/function_table_browser.py": "654c394929b744ca3fd6293fadfced3966d64626ddcf5f8b74f46e63cf026340", + "src/pyqt_reactive/widgets/shared/image_table_browser.py": "3a9b45805225021f6f17174e06985ab19f75b473bc0dfbf3ecfa7a5eef9931e2", + "src/pyqt_reactive/widgets/shared/list_item_delegate.py": "282c8fe0c88f9bfd51df7eb95c739a8d428d2e9d99478796bb073c698f5907a7", + "src/pyqt_reactive/widgets/shared/list_item_text_rendering.py": "0d997923c78dcb363748daf685cdc4e93ef5d6a4ee1f7d74100cd5266cdc5220", + "src/pyqt_reactive/widgets/shared/manager_action_controller.py": "e21b2407ec7cf6f891a7a67b2ae554e741f4a16aa35537b31e3b9992c009b317", + "src/pyqt_reactive/widgets/shared/manager_item_access.py": "21540ce53728a72028cc3c9af770bbca5210aa7c18bd4f697cad42bf5d27b276", + "src/pyqt_reactive/widgets/shared/manager_item_display_builder.py": "fe1d01ba449c69a052ed796e9c6901b080e2982b565f3ae0e0d73ad6599da45f", + "src/pyqt_reactive/widgets/shared/manager_item_hooks.py": "3dbb99e17114c9869db7a4cdf9284574921f4865e10f90a2161877c41da6c222", + "src/pyqt_reactive/widgets/shared/manager_list_updater.py": "c7036bb3f37198b06f5af6859c7bc6d9a0a85520da126286d31f00750a93b50b", + "src/pyqt_reactive/widgets/shared/manager_list_visual_state.py": "ba8a54d4b682d7bf61c2286439894d59eeddebf160102f4367038866cb54a2bd", + "src/pyqt_reactive/widgets/shared/manager_preview_formatting.py": "b9057f97f87f757272027baf402740db99da945fb04a4985ad0df5dc11f29b24", + "src/pyqt_reactive/widgets/shared/manager_reorder_controller.py": "64c829c64193e6cd1b604fc883fc0406fc43ecc329adc5066fa93196e2d67551", + "src/pyqt_reactive/widgets/shared/manager_selection_controller.py": "900fcec4c0a8e61b60130cc7a53ef8b0fd157d906182f96aa85fe57e5f008d46", + "src/pyqt_reactive/widgets/shared/manager_state_binding.py": "5fc9598ab1319cdf7048ab4e20c36c9d4c64d6ba7da584efc039e6c333cf888b", + "src/pyqt_reactive/widgets/shared/manager_status_controller.py": "b0e9fb4010775206b9b66889115be864d22f8e060b67bba2ef30dc0cccca4f9c", + "src/pyqt_reactive/widgets/shared/manager_time_travel_binding.py": "4cfeeef4badfe945c8ce2bd0e86254251fe79c5884261ac2d3f07c649ac3bda3", + "src/pyqt_reactive/widgets/shared/manager_ui_scaffold.py": "1332a8b69c8c562fd49146ea29a6b9aa5e156c90db06a00731e7716788e84589", + "src/pyqt_reactive/widgets/shared/manager_workflows.py": "e9e107b3129aa6f8725031abf35b2a67a52840643efdd120554ec78b13ce157a", + "src/pyqt_reactive/widgets/shared/reflowing_vertical_scroll_area.py": "7b04fd0dcd92ae470f9b6e7005bbae811ad3a614ddac86592381d840269c4387", + "src/pyqt_reactive/widgets/shared/responsive_groupbox_title.py": "a195a584afe51c456b35038a55d63c9e8cef708ca559df01d6dbbe42e399db0e", + "src/pyqt_reactive/widgets/shared/responsive_layout_widgets.py": "4adf600b914cf0769ca37c490523f744ac5fc2e5809028a6a24f21661e05359c", + "src/pyqt_reactive/widgets/shared/scope_border_renderer.py": "ab5a8246f3ca1d88bcd935f76fe59a33782e8ccb6944aff943386fe06fc46268", + "src/pyqt_reactive/widgets/shared/scope_color_receiver.py": "8b75714a16d6cf90d0f55c68f2d7d0935b0a1fa906ac2fd659c3409406568905", + "src/pyqt_reactive/widgets/shared/scope_color_strategy.py": "aa53231e9830e66721ef637177ae1470403cc9a13519015babdec8c824febffe", + "src/pyqt_reactive/widgets/shared/scope_color_utils.py": "d4c317df9c3d1391c3f8768b2bbd041435196cc876f16be2f929b63cab8aae10", + "src/pyqt_reactive/widgets/shared/scope_style_applier.py": "cd649a820631ceb367444c38ef3722489308147f9857f451cb95d5373d9ee763", + "src/pyqt_reactive/widgets/shared/scope_visual_config.py": "51286f8aa2f960e9d656f11f5ee57a11f1b2e1213511d0d17b46ad7be3b6fa4a", + "src/pyqt_reactive/widgets/shared/scoped_border_mixin.py": "f291c293302adf863574b6abf398605687d0325a8e3af16c37abb929d3fdbfad", + "src/pyqt_reactive/widgets/shared/scoped_table_widget.py": "cf81e6a8075ae89b10a46dc0e262de9f465cf5a0cd25a915dadb91dc0601a2b0", + "src/pyqt_reactive/widgets/shared/scrollable_form_body.py": "5a00352eda3b8e7932a878c18ef6ceae68a58358d15987dd8155702b70afd20c", + "src/pyqt_reactive/widgets/shared/scrollable_form_mixin.py": "e1ac397789a515097f464fb72b9378ba91e5dfbc6bfb6c316054049004fdcffe", + "src/pyqt_reactive/widgets/shared/styled_text_layout.py": "f174770e600b58e9d7b22f10db7a7e176d11207d534d090926e747af2ab594bd", + "src/pyqt_reactive/widgets/shared/tabbed_form_widget.py": "f2914db1586963f3dfe3cda7d6940d13b2bbb7ec1ffb4bb894c3844443b34a4d", + "src/pyqt_reactive/widgets/shared/tear_off_registry.py": "b44b1d391588d2290ff50b7f73379a875698e61de926e2ea153470744399091d", + "src/pyqt_reactive/widgets/shared/tear_off_tab_widget.py": "6b4fa8aebd7134cb2451135662f6b9aaee39a688c7b3ad8b37639985c7447fc7", + "src/pyqt_reactive/widgets/shared/tree_rebuild_coordinator.py": "deb28fea82fc963f999876dc54b37a4d0e310995bb16a1892befebdef79eccaa", + "src/pyqt_reactive/widgets/shared/tree_state_adapter.py": "ba4877d3431f4cfe344c530493b934b6d181254492070ca210fdcf2ed07f148c", + "src/pyqt_reactive/widgets/shared/tree_sync_adapter.py": "0031145c849a7ab6f2c4787bdc44692306073f08b2c17bea9b12932c45698f07", + "src/pyqt_reactive/widgets/shared/zmq_server_browser_widget.py": "35a7bc0ce56461336967a2a3f2e357edf8ccae122beb179d9dd438c86df105ae", + "src/pyqt_reactive/widgets/status_bar.py": "7acd1e194c1bdb350e2dd288adc9c55a37b1027fed97df0c5fb3e77ed794c940", + "src/pyqt_reactive/widgets/status_indicator.py": "96cd497e82336c02feec80dcc7c1bbf2e81336055e5b933f1cd1b96b240a0489", + "src/pyqt_reactive/widgets/structural_table.py": "4a757027ced7529f7b4f7737c09dbfa2d9dda48e7944b5375566299d3315f346", + "src/pyqt_reactive/widgets/system_monitor.py": "620f54ccd75eeeb0e370db4d461cfbf82f3625b898d343bb7c214de1cb5d671b", + "src/pyqt_reactive/windows/help_window_manager.py": "3946e3c1f7a99e75f86c87f44fab0af1282454dbf58dc95db14bb5a3fd66c7d2", + "tests/__init__.py": "db6cc46f8f6d69c35c72a39f4829527f661a63d906f091bc59a2055943a43281", + "tests/conftest.py": "911c65976a80aa48405b9c6bf0b18830599a2185c05434cbef8b7c25c4a48b39", + "tests/test_accent_chrome.py": "0862933afb738e2866611167a7c70e412d15e72ea46f9712a0b641587cadb709", + "tests/test_action_tabbed_window_body_lazy.py": "6f5b16efb53510e2a2806e1e412d89438c5e93c4efa5a9568439daf5a9daf9e8", + "tests/test_async_operation_executor.py": "2c8bc81bff7a3fd8879b6b79d2e9e09a3a22610fbb3785d970400e5e55dcd84e", + "tests/test_ci_action_pinning.py": "a636dd4d70e231d06c5de2802afc89c389f678b3b49a40ce8463eac5b0c308dd", + "tests/test_column_presentation.py": "aa83341b1443c131637e5b2b498b42cfed858927fa3f7ded00885e03f38e688a", + "tests/test_compact_dataclass_projection.py": "b765e0b8ced31a5cc351125bb2d86670e30cc973779c0b6931c75b026a14b271", + "tests/test_core.py": "a2aba8923fd176a75a9e94f5c3b1fa0a6d18f41b28935b0c4d39ed75892894fb", + "tests/test_deferred_callback.py": "1c59913d1b19a9725e5735bbd342dc115fdf4b555863427169b5001324e83b1c", + "tests/test_enabled_field_styling_ownership.py": "d831ab09cb4a0b92c3c33e4f2c368874a454c85eb8943d9acce48388ea961263", + "tests/test_enabled_title_row_ownership.py": "01052fea145b31698ec29319f521179adda5945ce324f3d976bdcc144aeb5be4", + "tests/test_explicit_function_pattern_overrides.py": "69179658cd4d89cdbf8bd469166e819f3421c39a6fcb6a38cc330dbbcb592a33", + "tests/test_field_mutation_authorization.py": "cab1faf400063e32df23249457738b9b28f59c760a6fe4d3cfcbba34884800ea", + "tests/test_flash_destruction_lifetime.py": "a567e8b978182773d316e9a7dc7934b80da2708e2804bd8e84898b4b1a030b37", + "tests/test_flash_mask_rendering.py": "9ca43be6bb9bb98a6950e9025688a6f40d8c7d050dec2bbe5721c215c47122f8", + "tests/test_flash_playback.py": "51438a64b08edec35a54447dbf3acd8aebd77a8dde92dfd7ccb3b0755bc196df", + "tests/test_flash_scroll_ownership.py": "f660469075d1c96e1fdce5bfaa0bd94857001d5e4f9ccf33fbae9ce9b2a51072", + "tests/test_form_build_transaction.py": "fc9860a115de764ee14d2a246fb5dd2d66bb60b159ae7f5853050d1e28442417", + "tests/test_function_list_editor_tokens.py": "5eea6d8a08f76c22fe81f4fca16b80d82ab8bb2ccc6f21428e5eed739065741b", + "tests/test_function_table_browser.py": "e0289caa47564647e1952a8592cda03840a58b11fb10fac6e6db9b35b1045541", + "tests/test_future_completion.py": "fddee2a4c4b210e43c137a24f5730c0e8303a38ecc67afec7a4ac5f39d2ad860", + "tests/test_help_document.py": "b28d3fb53b253372cdd4e5fa542faf5ef9a2cd7f6b42cd3a3694c2713f4f5768", + "tests/test_help_document_browser.py": "b04eca203a5d7be904ef86f490370bb71b598bc9f9d8f2ec441d118c4baa5d71", + "tests/test_host_provider_contracts.py": "ccc75dee1cd12eb05b29debe8da84a9b98a4714607bebdb9ad75c901f24f846e", + "tests/test_inline_dataclass_chrome.py": "e2c62d17ff58ad6dbe3dc2ec7e8caa1b8eeabc7e26f92c27a1a4489d1b12656b", + "tests/test_key_sequence_adapter_lifecycle.py": "bb4e57ee2548822f06252bdf07a9e3f36d569a01165f8707c2687150e4364824", + "tests/test_list_preview_wrapping.py": "60bfe5b16e9f17f3fed1f88213e69644ad2bcf278d7012b446689fbc75e181a2", + "tests/test_log_viewer_lifecycle.py": "4c7372276b7e65d84568c01489a544df7b9b2df791910c21cba11ad9c0eaff75", + "tests/test_manager_ui_scaffold.py": "cef43797060e94628d04fc581268ccd7e054cd5810b37105a68ce7930594eba6", + "tests/test_nominal_contract_architecture.py": "d02bb10514bab0bb58ad9585f986e208d88365838597eef520186f066685867a", + "tests/test_numeric_roundtrip.py": "5975b0ce8bd7bfcf1b21bf03593b72882c775a7982d834894081b6012c556566", + "tests/test_parameter_form_service.py": "30bb0cbb28b6edd5c144a58cc30a4e9fc068f9e56ee2dbe19e39f2b64fd1982d", + "tests/test_parameter_form_tree_index.py": "d28a29f77a68a22b3fce087de2c2679f2a291dd6a66cecf94f68ac700c1fded5", + "tests/test_parameter_help_service.py": "d41f813fbefd005003ab90a6e83d44727cc1adf925a9d521095068cbed2355b7", + "tests/test_parameter_ops_placeholder_capability.py": "8e4006e9b04df1efbcc4d878a68505a7057b83d6ac44e5b47dc1ea0bdd9dad6f", + "tests/test_path_name_roles.py": "86f5ef6cc8677dd92faa349d6e3254bbf6edc734e31fe0e13f8436dc4283c34a", + "tests/test_preview_configuration_imports.py": "7bec54d0815d88d2dce8dea9b73febbf23216b32c06666eab34e1487f0ec142b", + "tests/test_preview_formatter_registry.py": "9b62974f3e42be630f8f38a72d894f937affd6a694fe6f13c71629b5a38104ee", + "tests/test_preview_formatters.py": "4d6bbd9ada4ced6e6d2b49ff8c582d30b6341dd3403ee0c458a6bc4f0e595e49", + "tests/test_process_launch.py": "49c14b825f7fd3f7f7aa51f727c436c529cd2aaabefa3bee13e9f9e9d955e489", + "tests/test_process_signals.py": "de4d134edc1345bfe53df7f8b4ed362a24e4b3da35cfbc2132c5f6fb5107a662", + "tests/test_protocols.py": "3843eb80885957639b40d1cd260c87d0680df3a63d6ac448a084a25e8a37d8f2", + "tests/test_qt_types.py": "c24a5242ec6395b8c72723540e4b100e41f573d071444fc8c27eaac9a42de911", + "tests/test_reset_flash_changes.py": "3b901bd26e339f4585d90f25a70957fd949c7bdecbcfd56c0100c5a80f006d4b", + "tests/test_scoped_border_mixin.py": "9914504211aeaeb0cc713b9aa2c11619a00bfc53dc8dc6aa68d2d5ce3bd2f538", + "tests/test_scoped_table_widget.py": "764ed376f2e284682ddab7ab8379071d19ee703c391ea8196562b8f7b5b9a2fb", + "tests/test_scroll_area_content.py": "4b6f4a9e1fed02a267e2c707b1618ad3de8c451fe5e44b2f404360ef00e579ca", + "tests/test_scrollable_form_viewport_context.py": "d445d86f08bee9cc364868d4f50c976ee95bd85827b02790353c9f7439c9df7b", + "tests/test_status_indicator.py": "78f0e0b01548b8d676fd6ed75c79a800a4399826dcc5d2770ff3bb49a7e2a85e", + "tests/test_system_metrics_sampler.py": "524776fcf817233d461402b0f096623763f119a5d690a738434b390c6c752b07", + "tests/test_system_monitor_config.py": "2cc61be642ee7c9e26dab9e13497800bd3f85490a5253e5634a1374d4fe8287a", + "tests/test_theming.py": "bb811e5a73ee0a4fb95a0b87a40165442c39e3afdfa6c6ca6c0061590dae781c", + "tests/test_tree_state_adapter.py": "76d12b348ed6a015cb9b8f5bf3173968ca1b17a219df21aae82df23b12341907", + "tests/test_ui_thread_dispatch.py": "3478d122a45ee394edd3ab191f46636c62fe3b1ba1705d80e48c9e764b31e8d7", + "tests/test_weak_events.py": "d752129493c8cdd1d427024c64c7f02cd37d0e7a87bbf854b1ed332d174fa710", + "tests/test_widget_projection_safety.py": "f124cf36ca48fe51bae0aedad633f420bccd469ea8d9059d82d64dff3ca841e2", + "tests/test_widget_tree_projection.py": "3d6a1224caf0913ca194b13f4cf791cfcb3ce9e9c38b88281fe2dac4a0e222ef", + "tests/test_widgets.py": "f24b7bace49d7c6ce5357d38e9b5362ba0625b9f4421222788ab661ae278ed19", + "tests/test_window_code_document.py": "5c546e6151f19a8bcebfaa6a332a301b888416236baa982e2ae761e015326712", + "tests/test_window_navigation.py": "0f2bd021ca9d547f2b3d64e42cba1d13d0ffb1ed52e86bd07df836d5d3f649e3", + "tests/test_window_snapshot.py": "33fa1d5509217b0c16e926f7b4643ae1d5176ac77434f16099acd9b7cd52b8e2", + "tests/test_window_snapshot_bindings.py": "44af6189d6bf64bcfcad9e87150d2f01620e2a2c0b4877d581f64428176decf9", + "tests/test_zmq_server_browser_lifecycle.py": "2789e1a2cd5c81bddd37b62eb5fbe507228972e9f0dac0d1ea06b87dda066579" + } + }, + "python-introspect": { + "root": "/home/ts/code/projects/openhcs-shared-runtime-plumbing/external/python-introspect", + "head": "ed1e4eb15f7a0bf8315274d3e47858fa2b4372bb", + "status": "", + "tracked_code_metadata_hashes": { + ".github/actions/validate-python-release/validate_python_release.py": "96275b643ee03adac9a13996006cca803ec07a0f0d0835d47f559677af016fbc", + "docs/source/conf.py": "534c6205463a628d4d7d58b2793d95cffea83a6ec2c0085b012a29b046c398ea", + "pyproject.toml": "cd108ffbf3b756dbc3b8a1d85530cdeab0d2d0f452751c131db489a31178fff4", + "scripts/release.py": "f5ba54de5d2ef89ec7c75e536b63817989b8f9f17e3b2c10bb981b5bf8d08837", + "scripts/update_and_release.py": "80896e49f856d6a193bb1dc3917f55d2878acf1476c8240333eda16b92e8e61a", + "scripts/update_version.py": "303a09dca1b7374ed1d8e4aa26d76ec1bc76cfb6a769e002f2088927d51a6aa5", + "scripts/verify_release_ready.py": "5fe0aebbcaee7f60540f50221a1f8da1beab2b8b59d9b97f3b4a240f0e72211f", + "src/python_introspect/__init__.py": "d4e29e9b857a785f8ea1a1e26ea5317a39958a527df053677c335057671e4a94", + "src/python_introspect/annotation_types.py": "dc126879a8a581cea0758eec4df18ec20c47ca4b1e135abde23699b65b790bde", + "src/python_introspect/callable_declaration.py": "3bb8b474624fe6b2964a5c1f9a6aa540b2703500bfa124e7808995cb789d6951", + "src/python_introspect/dataclass_projection.py": "82662ff8eaa4f2aeb3b33edd61abae57020a7f139c3aa396d82dcecf8b801160", + "src/python_introspect/docstring_annotations.py": "444e8064f833f1a044208bd4a15107a6b688b597e36ab78ac58bee6996b51fc9", + "src/python_introspect/enableable.py": "025eadd64a6a6f1648bc36fbd1724f6049355a0b9b1a259443248d70170b8eb6", + "src/python_introspect/environment_projection.py": "8ab7d1e01e9a4529ea0b77d8996a5feeb94bdc2e905dd6263afb28d11bc217a0", + "src/python_introspect/exceptions.py": "4c6f15a3625d02b05b2fc8651e67e6b77d4bb442ad180b85121d91874ec3407b", + "src/python_introspect/runtime_parameter.py": "59fed2d467e751686447322f8e19a8633d964741d3d8afccb850dc504a619897", + "src/python_introspect/signature_analyzer.py": "7e25a0355159c86d361a21a7afc449ec758cbf8a32e9b72d06de66a924f04159", + "src/python_introspect/unified_parameter_analyzer.py": "ac52ab997fb385425920614451e8cc754fece0176bab4cc9b132eae8c930ab15", + "src/python_introspect/validation.py": "46fba78bd068c75c89b91baa4a5cbe8cdca9692e75a285eaf76311183df77b64", + "tests/__init__.py": "c752ad382f759406b1c9c102142aac7cc10a2693b8e5848536e44315567a034f", + "tests/test_annotation_types.py": "ae38706899ad48445224b09babff76496006576e7f188af7e27261521ea34ad9", + "tests/test_callable_declaration.py": "b8486dcca0c85805a14975c00987c7c3247c2c8abcb75545f89e04a01f738c32", + "tests/test_dataclass_projection.py": "6f662d0adcdd4531d8cbd94c2ffed55ced3a53c3ec481a9dce67979ac8489990", + "tests/test_dataclass_source_preparation.py": "c7438b14578eb59d53df2499c4db2004ff1b22d5e6d8350b0aa6ed2eb262a535", + "tests/test_environment_projection.py": "e11e2c180c605e761acd48103fcba10dada614a326e40a55d4777abcf0853899", + "tests/test_exceptions.py": "41f4a70df08a7718b4c9daa05569e6c491c0148abf975f0aeae78688e99d8744", + "tests/test_init.py": "7e9ca130ed8254034e32b991f8abd4624f6c1bfcdba65f911dc28c12d9cdfeca", + "tests/test_runtime_parameter.py": "b728307c8e6c7e50ca9b0befd1aaf154ad293eb18f332be544719d7ae9dd3fb4", + "tests/test_signature_analyzer.py": "0708fdcb848fe874eb4dae025c1c9fd9b0929e983cb1c1c53cb268af9fb37c2e", + "tests/test_unified_parameter_analyzer.py": "deb55bac9e31465c8368c6101a93e3c951e139c5c7ebd442a5f8cdc98842f1cd", + "tests/test_validate_python_release_action.py": "65fc7bdff5d20229e74e3abd44f936d7cfcaf9fa5881a7e9e65c59bbd65c6c12", + "tests/test_validation.py": "ce9cab31e33a001df0d40454a8d7de9e7a2ab6dc6296399adb234d06f7dfae39" + } + }, + "zmqruntime": { + "root": "/home/ts/code/projects/openhcs-shared-runtime-plumbing/external/zmqruntime", + "head": "668edafcb5ee530a163377c936431ae1e8334e85", + "status": "", + "tracked_code_metadata_hashes": { + "docs/source/conf.py": "e2224f5e41f473e9a9a213dfff036859c0834eee3561a9d6ce5415bc4487d126", + "pyproject.toml": "e2b56ba5b94418c99ee04142edf0fc4f14a06860fbecad7170d4a2e1e4be70c5", + "scripts/release.py": "109327ab88e002e4fdd56e6a50a8b5e265fda3723fb3c0ccc184c9d1b0539c2e", + "scripts/update_and_release.py": "80896e49f856d6a193bb1dc3917f55d2878acf1476c8240333eda16b92e8e61a", + "scripts/update_version.py": "d70feec0d50ca0a8d90f0105be3e85bf5a5b17a7bb0ca63a6178ad443f0ad60f", + "scripts/verify_release_ready.py": "e8190c4f79b1fae85af698b704b339564e77090f8adb7393f0079cf0d13bd844", + "src/zmqruntime/__init__.py": "37f3e14f58b4c1dda3d338a3a2fb075aefea215f6f25b69f44091891341755ef", + "src/zmqruntime/ack_listener.py": "da5aa8d1031f971c2621071a6da40b41f10dedf12839060db13d10153050cd6d", + "src/zmqruntime/client.py": "00757150b06b837b918153bc3c2752dbdba396569ebb4f4ed91cfbff7c23d25d", + "src/zmqruntime/config.py": "79840f3b72593042b6a56e3a703a1064e0d1faa19b12446d99e0903af494f2a3", + "src/zmqruntime/execution/__init__.py": "7a5d9f1636946d5fe6edcaaef181e8740e17d5b348d81e04b50a9fccf6fb16f9", + "src/zmqruntime/execution/batch_submit_wait.py": "c447b2ccbc372a5336f5a3b32844b896dc5d58566cb3de31640ae3be2379edee", + "src/zmqruntime/execution/client.py": "1b9453ea2be540b29849770cc3fe82f284f501e0e140797dd48c2c257ec36f12", + "src/zmqruntime/execution/lifecycle.py": "23596c31b9efe812f1ab66b4300ee1bb19125ea9bef6e51ddcd8acaafb0f4ed6", + "src/zmqruntime/execution/logs.py": "c7d1816835620a2b3e82a889bd36f2a97b80aad7c0f639a07c5c9d426c6ae803", + "src/zmqruntime/execution/progress_stream.py": "13b39fb4d256cd835302f7a8949e9e56bfe0b045e3e6fc1d197ce8ae40bc57a4", + "src/zmqruntime/execution/responses.py": "43ebbaf9451c34a5cb5a0c3182cb4de4c3745e13db8482ba1373becbfc8a2a03", + "src/zmqruntime/execution/server.py": "13663ba20e62326171ff8b43c2474419940729e90b2c0b25b04ea3514734d599", + "src/zmqruntime/execution/status_poller.py": "da0c7c304f293ba4f52aaefc4c722442b5a2859d1fcb806e8fccdcd3b4f6cdc6", + "src/zmqruntime/execution/wait_policy.py": "df4a9cad5a2fb74aa0c030def22dd8cc5b851983c3f04c26d479b607075ef922", + "src/zmqruntime/messages.py": "76018d6f6ac6795e076eabed9c6dd018b8c2fb9640e194a19086a013c0a50785", + "src/zmqruntime/progress/__init__.py": "1767a50989e4a19e4ae8a7ed4343b6be564740236cdcb4c5c56c06f55c21f68f", + "src/zmqruntime/progress/projection.py": "72b22f765dabbcf6983c619c8cc5404bc52518186ef72d85a6f6c6ab65cb426f", + "src/zmqruntime/progress/registry.py": "2943b28d1490688bddcba914db5aadd445cc8e6c9b6381230f0451366e98a0c2", + "src/zmqruntime/queue_tracker.py": "b2e380a93699f85e608d1ae7c03b5a20ee62d8beb82cc93272b050084156610f", + "src/zmqruntime/runner.py": "9f52b69f23b4626adb4e1394e0ed476624d2a38d9cd2c8ec656ae3b2e6816df9", + "src/zmqruntime/server.py": "a38ed80dfbbf9d738f4a96b4bcff8ca667f592b84413c33731550513dcc924d0", + "src/zmqruntime/shutdown.py": "67acebb3ac7a2c419320005931f0de44e28f5ab0b393e6bfbdc3f11207a2073f", + "src/zmqruntime/startup.py": "55118bc44633926434f0e0b057c167dc2948f2a632a902b5bdb60dcb92731d69", + "src/zmqruntime/streaming/__init__.py": "911cac6c1f747d77ec6614dd0961ebb799a7dbaa276e4259e1f38087e3b7320b", + "src/zmqruntime/streaming/process_manager.py": "8229c322c309969f89e2a51a3e416ea38a4990248776844dbcbb01ad9796b3e5", + "src/zmqruntime/streaming/server.py": "6a587842676bdd2f414d8df8f277e5e671a5f41a7b3c4615bf1ea2a5da182834", + "src/zmqruntime/subscription.py": "4a393bd94d8a3611ff7d7423ec4ea681df6f26cf906a77e2682c76cc0e762696", + "src/zmqruntime/timeouts.py": "bb5121a03070f0f2a0ed76170a2324512ed4a4d64536ecc7ba8bdc6a4b8961d0", + "src/zmqruntime/transport.py": "e486a4215d4b977553030e897589bccac637dfe4e9abb28cfb7594139b2196c2", + "src/zmqruntime/transport_modes.py": "12bec1c13d87bb8f05b049dc76d3cb7c130414c9e69d6220228ceeb0087f9ad8", + "src/zmqruntime/viewer_protocol.py": "5580efffddca2f723b11a652f482fbdb52ebae657620fad95217b313997b9fc9", + "src/zmqruntime/viewer_state.py": "f3dd0b35c3c75b0537bf8e50624b8f05a6dce02b40f1700a878189c3312ae30c", + "tests/diagnostics/check_ack_startup_failure.py": "08856fa0ca60e4b00c8577ce2ed3bb82e6e3fbbe16eb2876b69a03500ff130ad", + "tests/test_ack_listener.py": "a616052e042aa145d4eb2b9bca6b153633662bbd54588038730c9d8a2551f70d", + "tests/test_ack_return_route.py": "4519ccf21b0d6bc892d43405541308326a8a7ae885e0ec8942e3596eda3078bd", + "tests/test_ci_action_pinning.py": "a636dd4d70e231d06c5de2802afc89c389f678b3b49a40ce8463eac5b0c308dd", + "tests/test_cold_start_activity.py": "3e0486f8e520e3bb2a767f1aca49df0ffc269476cea0f29594cb08bfce1d7896", + "tests/test_config.py": "f9a16ae6cdb8c9d1dce74bcbc95ff59e853245b219ce848d8aca9704eadfb95a", + "tests/test_endpoint_ownership.py": "e37ddca59beb48b4c1ccca4d1feee1793728cfc54b2aef69870e120bf17098e5", + "tests/test_execution.py": "7b547a68e912b012144519216097bde868264fcf7bb0ad20b3223862701bea0c", + "tests/test_execution_logs.py": "3136e1cf250b02d89e9da9be06b90ecab59fe34ba58e2e22145fd1b0e9fe2a90", + "tests/test_messages.py": "8223e0a5b461e7dd3fc6392c9f652cd74180c8de1b07fef9d28305ffffb2f229", + "tests/test_owned_close.py": "92c3149704b88278ce6771c0c2c064aa897948bd7303b8100f24f99e97d9ac84", + "tests/test_owned_startup.py": "95d3997eaf5d627925108b85dbbe5937abe448c666f26caa43f2bad5586bf73a", + "tests/test_process_manager.py": "b21d1eedd0f98549a7b0c235cca43fbea35afb3c041df81b362d94f3fde9439d", + "tests/test_progress_annotation.py": "27b237877b660d6699402a091dc7843201a4fc0636e4b9f0fc1e25d16cddee75", + "tests/test_progress_projection.py": "e56b49ba046b7c459cee386076201b252d3c831a3d8eca535eafbe0ff6affced", + "tests/test_progress_registry.py": "bf80de2da039272700a366884e5aee4163f04b156fe5c8f58912d4c7f42e7ef4", + "tests/test_progress_stream.py": "27d056146596e7e7c4f2a15ae7e7f7e01e9b4882f3258fa48108d799ecb19698", + "tests/test_queue_tracker.py": "85257cf345a02532441f188242134a2fcb4b60f793eead2f21a6bda360529a29", + "tests/test_shutdown.py": "ffff454475abed18dc520b8629a2799d5a30d0e83abd0292c5acb916d3e63eee", + "tests/test_startup.py": "3e5441886d22921b255de1016bedefd74e87e57f7099bc9e72f8021d1b115148", + "tests/test_startup_callback_scope.py": "06c9dfe205fef9aa293ca0a5dfbaaa8d5083da02fb7c2e06bc9895dea607760b", + "tests/test_timeouts.py": "8c4c95ce4c2d02d5210ef4ccfd4d0cbd1850535c8d5793b87380e995c126babb", + "tests/test_transport.py": "02072b46ca87c082eeedccb02f4b85a1ba02bc51a4c24ebfc6930a6b8b2a21f9", + "tests/test_viewer_process_ownership.py": "972bf0988a9b3b49371ddbd8e1ba7ce965c9fa9a37f04ece70fa93905335b348", + "tests/test_viewer_protocol.py": "26adaa73cc3a39cefc4c797880fb5b2e52082adaa980e479e318fa3805e7fe28", + "tests/test_viewer_state.py": "2f7b0dabbed227db35fbac5321f85b84352aafe19e33c879a50b3aee0fc9d8ea" + } + } + }, + "protected_canonical_source_first_boundary": { + "status": "CANONICAL_SOURCE_BOOTSTRAP_POLICY_OBSERVATION_NOT_LATEST_API_QUALIFICATION", + "openhcs": "/home/ts/code/projects/openhcs/openhcs/__init__.py", + "objectstate": { + "module_file": "/home/ts/code/projects/openhcs/external/ObjectState/src/objectstate/__init__.py", + "runtime_version": "1.1.8", + "default_metadata_version": "1.1.9", + "installed_metadata_version": "1.1.9", + "registered_type_pairs_available": false + }, + "python-introspect": { + "module_file": "/home/ts/code/projects/openhcs/external/python-introspect/src/python_introspect/__init__.py", + "runtime_version": "0.1.12", + "default_metadata_version": "0.1.14", + "installed_metadata_version": "0.1.15", + "prepare_dataclass_declaration_available": false + }, + "scope": "Existing canonical OpenHCS bootstrap selects canonical external gitlinks before shared editables. No path bypass or protected canonical mutation. Current owned source-first qualification is separate." + }, + "primary_tests": { + "python-introspect source preparation": { + "passed": 9, + "log": "python-introspect-readiness-tests.log" + }, + "ObjectState lazy factory": { + "passed": 25, + "log": "objectstate-readiness-tests.log" + }, + "main514 registered source readiness": { + "passed": 1, + "log": "openhcs-main514-readiness-tests.log" + } + }, + "primary_total_passed": 35, + "additional_started_before_selector_clarification": { + "ObjectState_lazy_global_context": { + "passed": 40, + "log": "objectstate-required40-controls.log" + }, + "python_introspect_source_release": { + "passed": 13, + "log": "python-introspect-required13-controls.log" + }, + "scope": "Supplementary historical-selector equivalent checks; not substituted for primary actual35 or claimed additionalunique total." + }, + "pip_check": "PASS", + "only_two_authorized_owned_git_checkouts_changed": true, + "protected_canonical_two_and_other_six_source_roots_heads_status_unchanged": true, + "no_other_installed_distribution_versions_changed": true, + "temporary_build_prefixes_disposed": true, + "all_command_handles_terminal": true, + "no_new_venv_or_public_jobs_or_tags_or_PyPI_publish": true, + "parent_OpenHCS_gitlinks_and_floors": "Owned by ROOT/PR518; this agent did not edit parent worktree/index or refs.", + "retained_failures": [ + "source-first-import-probe-v1-RED.json: metadata discovery selected valid generated source .egg-info, not installed .dist-info; corrected diagnostic reports both.", + "focused-readiness-tests.log: combined repository conftest namespace collision beforetests; corrected by authoritative separate repository processes, no test/import policy alteration." + ], + "scope_limit": "Shared default direct dependency imports and the owned benchmark source-first environment use releasedversions/currentAPIs. Canonical OpenHCS-first source bootstrap intentionally selects protected oldcanonical gitlinks, so canonical application latest-dependency readiness is NOT claimed. No path bypass/canonicalmutation applied.", + "evidence_sha256": { + "import_readiness_probe.py": "b8efb1e905340134b84ba4419da18b015083359ecfded16bff9b0dbbeda9dde7", + "source-first-import-probe.json": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "source-first-import-probe-v1-RED.json": "5dc1783943f57940ee0e4a482c96cacff9a87a061cb0f6fd54a55f4f5e8d5def", + "objectstate-required40-controls.log": "272ee4e3e3a343e21d77a7423ecab8c1d3fdc4d033eb16315e2edfdf698a5edb", + "canonical-openhcs-first-order-policy.json": "0263509945dd322084d3cdb2ec6f8404c7219b2e1fff006fdcd7cf30887eefa0", + "openhcs-main514-readiness-tests.log": "fb9f4833ec108937050465e52b6621ab8e602a363b84195e69739696eeb7595e", + "installed-distributions-after.json": "4adc2f61f621c5a648e486c12aaba2e97d5c89512a50235b05deb0c2bdd49b10", + "objectstate-readiness-tests.log": "c4aafe1937f5b1840f2797549da35341a66d98c4dae55daa3c3f284ed5dec096", + "all8-source-first-imports.json": "a0cd99d45ddccb08c9a125ff14a1bdd113a91ea12e1546b76a74a1d1a392d8ab", + "direct-import-probe.json": "570c855676a911125a5b4e6ea2ddcbf4493b13a7682e0182f2a1e5b701d9b917", + "pip-install.log": "66fec4e76d20efb1a34400b16aa6c20cbf0b46849b5c598e45330365a7c2ed23", + "python-introspect-readiness-tests.log": "d938a7e650a474cb5ffb80088da3912024873000d2f9feb357c9cb6ca67d85e6", + "installed-distributions-before.json": "4301dd81c9d1b48801751497a7812d3e79a1df71593bc875f8f9b60f602054fe", + "focused-readiness-tests.log": "c9d0387d883174c4c81e5f70d91b232effd9cff1f113a6737f1d5d4d7115030d", + "installed-distribution-delta.json": "8f90c46e3c520a576ba42d478df290b63a795420677357a29e19f0601c4157a9", + "source-first-import-probe-v2.json": "aad43dd8758c41b060fe734b66e04d08c1e0fc95728c770850b8964f17c23fc3", + "pip-check.log": "9261363b733079a641c2e4cc9bc46ffa1d8336945a87f807b6cf68847dbc9b09", + "direct-import-probe-v2.json": "74b1826b4053200898401781051e882b3c414764bea38d37a660df8a78c8a8f3", + "python-introspect-required13-controls.log": "72928e7c8f0d61d67b0bf821e3eaa5c47f1a9a3ee8d00ac0294fdfe54b94bc30", + "import_readiness_probe_v2.py": "2b9060564efe6dcbc49a083488351e28f1f47cb58842295b8d25748dc648c069" + } +} diff --git a/docs/plans/runtime_plumbing_frontier_20261003.rst b/docs/plans/runtime_plumbing_frontier_20261003.rst index e7a4d4c50d..3b6fcd6b47 100644 --- a/docs/plans/runtime_plumbing_frontier_20261003.rst +++ b/docs/plans/runtime_plumbing_frontier_20261003.rst @@ -437,3 +437,42 @@ graph-owned warm occurrences, not process-global LRU state or a whole-pipeline heap. Diagnostic durations are not ordinary performance gains. V7/V8/V9 failures remain immutable, and the broad branch's architecture gates, fresh native runs, full-catalog scaling and figures remain outstanding. + +Fresh current native comparison after main and dependency sync +------------------------------------------------------------- + +Clean source ``60b2ac9b4`` includes main ``f7de9efad`` and actual merged +ObjectState/python-introspect source heads at versions 1.1.9/0.1.15. The +shared environment qualification passes 35 primary controls and the supplementary +40/13 historical-selector controls; all eight imported source roots are recorded. +Protected canonical-first imports still select older canonical vendored sources. +This qualification applies to the owned benchmark checkout, not canonical startup. + +Two fresh ordinary sweeps and two fresh native measured repetitions per case pass +all eight full scientific comparisons. Startup/READY warmup/shutdown remain +excluded; native invocation includes preparation/modules/post-run/closure. Both +ordinary observations are retained with default OUTCOMES/RSS and inline 1w_1t: + +* 3D: compile mean 1.212512s; execute 9.127215s; total 11.153507s; + native 14.422177s. Execution speedup 1.58013x; total 1.29306x. +* Speckles: compile mean 0.663492s; execute 1.231350s; total 2.246087s; + native 1.954230s. Execution speedup 1.58706x; total 0.87006x. + +The remaining 2x execution gaps are now 1.916127s and 0.254235s; earlier +1.338s/0.222s targets refer only to their historical source-qualified samples. +These two observations are descriptive; source/environment synchronization and +run-to-run spread do not establish a causal improvement or regression. No new +callable/plumbing attribution is inferred from these uninstrumented clocks. + +All selected source occurrences and physical hashes match: 180 refs/one 3D +volumetric image set and two refs/one Speckles image set. Full 3D six-CSV/120-TIFF +and Speckles three-CSV controls, exact 3D labels, existing float tolerances, +relationship correlations and complete physical inventories pass. Root verified +all 11 linked receipt artifacts and all 290 unique compared output paths. + +Current receipts, raw rows, fresh native reports and two-case figure previews are +in ``benchmark/results/perf_current_native_paired_20261003``. Shared environment +qualification is in ``benchmark/results/perf_dependency_source_readiness_20261003``. +The unchanged original R0/R1 failures, full30/scaling work and other workloads' +unresolved native image differences remain open. No whole-goal completion is +claimed. From 3a9e3a3e831d32acb8d1c2ada1a374f329fb7a8d Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 03:41:13 -0400 Subject: [PATCH 113/235] fix: project aggregate materialization into declared viewer layers (#515) --- .../core/steps/stream_component_semantics.py | 48 +++ openhcs/processing/materialization/core.py | 54 ++- ...est_aggregate_layer_materialization_515.py | 389 ++++++++++++++++++ 3 files changed, 488 insertions(+), 3 deletions(-) create mode 100644 tests/unit/test_aggregate_layer_materialization_515.py diff --git a/openhcs/core/steps/stream_component_semantics.py b/openhcs/core/steps/stream_component_semantics.py index 39998938e8..82b3b487a9 100644 --- a/openhcs/core/steps/stream_component_semantics.py +++ b/openhcs/core/steps/stream_component_semantics.py @@ -29,8 +29,16 @@ ) from openhcs.constants.constants import AllComponents, get_multiprocessing_axis +from openhcs.core.config import FijiDimensionMode, NapariDimensionMode from openhcs.core.context.processing_context import ProcessingContext from openhcs.core.runtime_image_values import ImagePayloadMetadata +from openhcs.core.runtime_plane_projection import RuntimePlaneAxisValueProjection +from openhcs.core.runtime_slice_projection import ( + RuntimeProjectionData, + RuntimeProjectedPayloadItem, + RuntimeProjectionSourceIdentityRequest, + RuntimeProjectionSourceIdentityRequirement, +) from openhcs.core.source_image_provenance import ( SourceComponentMetadata, @@ -62,6 +70,46 @@ class StreamImagePayloadMetadataProjector: """Project image-axis declarations into viewer batch-item fields.""" + @classmethod + def payload_items_for_display( + cls, + payload: RuntimeProjectionData, + *, + metadata: ImagePayloadMetadata, + plane_components: tuple[AllComponents, ...], + component_modes: Mapping[str, str], + source_description: str, + ) -> tuple[RuntimeProjectedPayloadItem, ...]: + """Project declared planes when their display requires scalar identities.""" + item_fields = cls.item_fields_for_plane_components(metadata, plane_components) + component_values = item_fields.get( + ViewerWireField.PLANE_COMPONENT_VALUES.value, {} + ) + scalar_modes = ( + NapariDimensionMode.LAYER.value, + FijiDimensionMode.WINDOW.value, + ) + if not any( + component_modes[component] in scalar_modes + for component in component_values + ): + return (RuntimeProjectedPayloadItem(payload, source_description),) + projection = RuntimePlaneAxisValueProjection.preserve( + axis=metadata.plane_axis, + axis_size=metadata.source_provenance.source_plane_count, + ) + source_identity = ( + RuntimeProjectionSourceIdentityRequirement.REQUIRED_COMPONENT_METADATA + ) + return source_identity.project_payload_items( + RuntimeProjectionSourceIdentityRequest( + value=metadata.attach_to(payload), + source_description=source_description, + variable_components=plane_components, + plane_projection=projection, + ) + ) + @classmethod def partition_indices( cls, diff --git a/openhcs/processing/materialization/core.py b/openhcs/processing/materialization/core.py index 21dd73d80c..64f315f734 100644 --- a/openhcs/processing/materialization/core.py +++ b/openhcs/processing/materialization/core.py @@ -1532,7 +1532,10 @@ def to_filemanager_kwargs( self, output: Output, ) -> dict: - output_fields = self._output_fields(output) + projected_outputs = self.projected_outputs((output,)) + if len(projected_outputs) != 1 or projected_outputs[0] is not output: + raise ValueError("Projected viewer materialization requires batch saving.") + output_fields = self._output_fields(projected_outputs[0]) if output_fields is None: return self.values.to_kwargs() component_metadata, item_fields = output_fields @@ -1553,7 +1556,7 @@ def filemanager_batches( ] ] = [] unprojected_outputs: list[Output] = [] - for output in outputs: + for output in self.projected_outputs(outputs): output_fields = self._output_fields(output) if output_fields is None: unprojected_outputs.append(output) @@ -1602,6 +1605,49 @@ def filemanager_batches( ) return tuple(batches) + def projected_outputs(self, outputs: Sequence[Output]) -> tuple[Output, ...]: + """Derive stream items without changing persisted aggregate outputs.""" + result: list[Output] = [] + display_semantics = self.values.stream_request.display_semantics + parser = self.values.stream_request.source.identity.microscope_handler.parser + for output in outputs: + if output.metadata is None: + result.append(output) + continue + items = StreamImagePayloadMetadataProjector.payload_items_for_display( + output.content, + metadata=output.metadata, + plane_components=output.variable_components, + component_modes=display_semantics.component_modes, + source_description=output.path, + ) + if len(items) == 1 and items[0].value is output.content: + result.append(output) + continue + output_path = Path(output.path) + for item in items: + identity = FunctionOutputIdentityAuthority.identity_from_metadata( + parser, + item.metadata, + fallback_identity_path=output.path, + ) + if identity is None: + raise ValueError("Projected viewer output requires a filename identity.") + identity = replace(identity, extension=output_path.suffix) + filename = FunctionOutputPathAuthority.filename_for_identity( + parser, + identity.with_filename_qualifier(output_path.stem), + ) + result.append( + replace( + output, + path=str(output_path.parent / filename), + content=item.data, + metadata=item.metadata, + ) + ) + return tuple(result) + def _output_fields( self, output: Output, @@ -1688,6 +1734,7 @@ def save_all( kwargs_payload = self.backend_kwargs[backend] else: kwargs_payload = EMPTY_BACKEND_CALL_KWARGS + written_outputs: list[Output] = [] for batch_outputs, kwargs in kwargs_payload.filemanager_batches( supported_outputs ): @@ -1697,7 +1744,8 @@ def save_all( backend, **kwargs, ) - saved_outputs[backend] = supported_outputs + written_outputs.extend(batch_outputs) + saved_outputs[backend] = tuple(written_outputs) return saved_outputs def _prepare_path(self, backend: str, backend_instance, path: str) -> None: diff --git a/tests/unit/test_aggregate_layer_materialization_515.py b/tests/unit/test_aggregate_layer_materialization_515.py new file mode 100644 index 0000000000..11ce4b5b2e --- /dev/null +++ b/tests/unit/test_aggregate_layer_materialization_515.py @@ -0,0 +1,389 @@ +"""Issue #515: automatic label TIFFs must honor scalar LAYER routing. + +The renderer, disk TIFF writer and wire builder are real. Only the final viewer +transport is intercepted; no viewer or native process is started. +""" + +from dataclasses import replace + +import numpy as np +import pytest + +from openhcs.constants.constants import AllComponents, VariableComponents +from openhcs.core.artifacts import ArtifactOutputPlan, ObjectLabelsArtifactType +from openhcs.core.pipeline.artifact_planning import ( + AutomaticObjectLabelsArtifactOutputMaterializationStrategy, +) +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_labels import ObjectLabelPayload, ObjectLabelVariantData +from openhcs.core.runtime_plane_projection import RuntimePlaneAxis, RuntimePlaneAxisValueProjection +from openhcs.core.runtime_slice_projection import ( + RuntimeProjectionSourceIdentityRequest, + RuntimeProjectionSourceIdentityRequirement, + RuntimeSliceProjectionDeclarationError, +) +from openhcs.core.source_image_provenance import SourceImageProvenancePlanes +from openhcs.core.source_metadata import SourceVoxelSpacing +from openhcs.core.source_spatial_domain import SourceSpatialDomain +from openhcs.runtime.napari_streaming_handlers import ( + NapariAggregateAxisBindingAuthority, + NapariStreamLayerAddress, + NapariStreamLayerItem, +) +from openhcs.runtime.viewer_component_system import ( + ViewerComponentAxisSemanticsAuthority, + ViewerComponentValueDomainPayload, + ViewerMappingDisplayConfigInput, +) +from openhcs.core.steps.function_artifact_materialization import ( + PersistentArtifactMaterializationTargetPlan, + materialize_artifact_outputs, +) +from polystore.napari_stream import NapariStreamingBackend +from polystore.base import ensure_storage_registry, storage_registry +from polystore.filemanager import FileManager +from polystore.streaming import StreamingBatchMessageBuilder, StreamingBatchMessageRequest +from polystore.streaming.receivers.napari.layer_key import build_route_key, normalize_component_layout +from polystore.streaming.viewer_transport import ViewerStreamKwarg +from polystore.streaming.viewer_transport import ViewerStreamBackendKwargs +from polystore.streaming_constants import StreamingDataType +from openhcs.processing.materialization.core import Output, ViewerStreamBackendCallKwargs +from polystore.streaming.identity import StreamProducerIdentity + +from tests.unit.test_function_artifact_materialization import ( + StreamingConfigStub, + _context, + _plan, +) + + +class ComponentDisplayConfig(StreamingConfigStub): + def __init__(self, component, mode): + super().__init__(5555) + self.component = component + self.mode = mode + + def component_modes(self): + return {**super().component_modes(), self.component.value: self.mode} + + +def _source_bound_labels(component): + pixels = np.zeros((2, 4, 5), dtype=np.int32) + pixels[0, 1:3, 1:4] = 1 + pixels[1, 1:3, 2:5] = 2 + source_fields = tuple( + {"well": "A01", "site": 1, "channel": 1, "z_index": 1, + "timepoint": 1, component.value: value} + for value in (1, 2) + ) + paths = tuple( + f"/input/A01_s{fields['site']:03d}_w{fields['channel']}_z001_t001.tif" + for fields in source_fields + ) + labels = ObjectLabelPayload( + variant_data=ObjectLabelVariantData(labels=pixels), + plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + domain=ObjectLabelDomain( + declared_object_id_domains=((1,), (2,)), scope=ObjectLabelDomainScope.PLANE, + ), + source_image_provenance_planes=SourceImageProvenancePlanes.from_components( + paths=paths, component_metadata=source_fields, + ), + source_component_metadata={ + key: value for key, value in source_fields[0].items() + if all(fields[key] == value for fields in source_fields) + }, + source_spatial_domain=SourceSpatialDomain(source_shape_yx=(4, 5)), + parent_image_source_voxel_spacing=SourceVoxelSpacing((0.75, 0.75)), + ) + return labels, pixels.copy() + + +def _automatic_label_tiff(component, mode, return_route, monkeypatch, tmp_path): + labels, pixels = _source_bound_labels(component) + output_plan = ArtifactOutputPlan( + name="Nuclei", path="/memory/Nuclei.pkl", artifact_type=ObjectLabelsArtifactType, + variable_components=(VariableComponents(component.value),), + materialization=AutomaticObjectLabelsArtifactOutputMaterializationStrategy().materialization(), + ) + ensure_storage_registry() + filemanager = FileManager(dict(storage_registry)) + stream_saves = [] + original_save_batch = filemanager.save_batch + + def save_batch(contents, paths, backend, **kwargs): + if backend == "napari_stream": + stream_saves.extend( + (content, path, backend, kwargs) + for content, path in zip(contents, paths, strict=True) + ) + return + return original_save_batch(contents, paths, backend, **kwargs) + + monkeypatch.setattr(filemanager, "save_batch", save_batch) + context = _context(filemanager) + context.runtime_value_store.record( + RuntimeValue.normalize(output_plan, labels, axis_id="A01"), + path=output_plan.path, backend="memory", + ) + plan = replace(_plan( + output_plan, + streaming_configs={"napari_stream": ComponentDisplayConfig(component, mode)}, + variable_components=(VariableComponents(component.value),), + ), analysis_results_dir=str(tmp_path / "analysis"), output_dir=tmp_path / "images") + materialized = materialize_artifact_outputs( + filemanager, plan, PersistentArtifactMaterializationTargetPlan("disk"), context, + ) + disk_outputs = [output for result in materialized + for output in result.outputs_by_backend["disk"] + if output.path.endswith(".labels.tif")] + assert len(disk_outputs) == 1 + import tifffile + + persisted_pixels = tifffile.imread(disk_outputs[0].path) + 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.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.value] + for index in range(2)] == [1, 2] + np.testing.assert_array_equal(labels.labels, pixels) + tiff_saves = [saved for saved in stream_saves if saved[1].endswith(".labels.tif")] + assert tiff_saves, "Automatic object-label materialization did not reach the TIFF backend." + backend = NapariStreamingBackend() + wire_batches = [] + try: + for content, path, backend_name, kwargs in tiff_saves: + assert backend_name == "napari_stream" + assert content.dtype == pixels.dtype + request = kwargs[ViewerStreamKwarg.STREAM_REQUEST.value] + wire_batches.append(StreamingBatchMessageBuilder.build( + backend, + StreamingBatchMessageRequest( + return_route=return_route, data_list=[content], file_paths=[path], + stream_request=request, + component_names_request=backend.component_names_request(request), + display_payload_extra=backend.display_payload_extra(request), + ), + )) + return labels, pixels, tiff_saves, wire_batches + finally: + backend.cleanup() + + +def _route_keys(batches): + return [ + build_route_key( + item["producer_identity"], item["metadata"], + normalize_component_layout(batch.message["display_config"]), + StreamingDataType(item["data_type"]), + ) + for batch in batches for item in batch.batch_images + ] + + +def _received_item(batch, pixels): + wire = batch.batch_images[0] + item = NapariStreamLayerItem( + data=pixels, + producer=StreamProducerIdentity.from_payload(wire["producer_identity"]), + address=NapariStreamLayerAddress( + components=wire["metadata"], path=wire["path"], + stream_layer_data_type=StreamingDataType(wire["data_type"]), + ), + image_metadata=ImagePayloadMetadata.from_viewer_image_metadata(wire["image_metadata"]), + plane_component_domain=ViewerComponentValueDomainPayload.from_ordered_wire_mapping( + wire["plane_component_values"], context="actual automatic TIFF wire domain", + ), + ) + semantics = ViewerComponentAxisSemanticsAuthority.from_display_config( + ViewerMappingDisplayConfigInput(batch.message["display_config"]), + ViewerComponentValueDomainPayload.from_wire_mapping( + batch.message["component_value_domain"], context="actual automatic TIFF viewer domain", + ), + ) + return item, semantics + + +@pytest.mark.parametrize("component", (AllComponents.CHANNEL, AllComponents.SITE)) +def test_automatic_aggregate_label_tiff_honors_separate_layers(component, viewer_ack_return_route, monkeypatch, tmp_path): + labels, pixels, saves, batches = _automatic_label_tiff(component, "layer", viewer_ack_return_route, monkeypatch, tmp_path) + # This is the original strict receiver, not a test-side default or projection. + keys = _route_keys(batches) + items = [item for batch in batches for item in batch.batch_images] + assert [item["metadata"][component.value] for item in items] == [1, 2] + assert len(set(keys)) == 2 + assert [tuple(item["shape"]) for item in items] == [(4, 5), (4, 5)] + for index, (content, _path, _backend, _kwargs) in enumerate(saves): + np.testing.assert_array_equal(content, pixels[index]) + np.testing.assert_array_equal(labels.labels, pixels) + + +@pytest.mark.parametrize("component", (AllComponents.CHANNEL, AllComponents.SITE)) +def test_automatic_aggregate_label_tiff_preserves_stack(component, viewer_ack_return_route, monkeypatch, tmp_path): + labels, pixels, saves, batches = _automatic_label_tiff(component, "stack", viewer_ack_return_route, monkeypatch, tmp_path) + assert len(saves) == len(batches) == 1 + np.testing.assert_array_equal(saves[0][0], pixels) + 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_component_values"] == {component.value: ["1", "2"]} + decoded = ImagePayloadMetadata.from_viewer_image_metadata(item["image_metadata"]) + assert decoded.source_voxel_spacing.values_zyx == (0.75, 0.75) + assert decoded.source_spatial_domain.source_shape_yx == (4, 5) + assert component.value not in item["metadata"] + assert len(_route_keys(batches)) == 1 + received, semantics = _received_item(batches[0], saves[0][0]) + bindings = NapariAggregateAxisBindingAuthority.bindings((received,), semantics) + assert len(bindings.bindings) == 1 + assert bindings.bindings[0].component == component.value + assert bindings.bindings[0].values == (1, 2) + np.testing.assert_array_equal(labels.labels, pixels) + + +def test_strict_receiver_still_rejects_aggregate_without_scalar_layer(viewer_ack_return_route, monkeypatch, tmp_path): + _labels, _pixels, _saves, batches = _automatic_label_tiff( + AllComponents.CHANNEL, "stack", viewer_ack_return_route, monkeypatch, tmp_path, + ) + batch = batches[0] + layout = normalize_component_layout(batch.message["display_config"]) + layout = replace(layout, component_modes={**layout.component_modes, "channel": "layer"}) + item = batch.batch_images[0] + with pytest.raises(ValueError, match="missing layer component 'channel'"): + build_route_key(item["producer_identity"], item["metadata"], layout, StreamingDataType.IMAGE) + + +@pytest.mark.parametrize("plane_count", (1, 2)) +def test_single_output_kwargs_cannot_hide_projected_pixels( + plane_count, viewer_ack_return_route, monkeypatch, tmp_path, +): + labels, pixels, saves, _batches = _automatic_label_tiff( + AllComponents.CHANNEL, "stack", viewer_ack_return_route, monkeypatch, tmp_path, + ) + metadata = labels.metadata.replace_fields( + source_image_provenance_planes=SourceImageProvenancePlanes( + labels.source_image_provenance_planes.planes[:plane_count] + ), + ) + request = replace( + saves[0][3][ViewerStreamKwarg.STREAM_REQUEST.value], + display_config=ComponentDisplayConfig(AllComponents.CHANNEL, "layer"), + ) + kwargs = ViewerStreamBackendCallKwargs(ViewerStreamBackendKwargs(request)) + output = Output( + path=saves[0][1], content=pixels[:plane_count], metadata=metadata, + variable_components=(AllComponents.CHANNEL,), + ) + with pytest.raises(ValueError, match="requires batch saving"): + kwargs.to_filemanager_kwargs(output) + batches = kwargs.filemanager_batches((output,)) + projected = [item for items, _kwargs in batches for item in items] + assert len(projected) == plane_count + assert len({item.path for item in projected}) == plane_count + 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.source_component_metadata["channel"] == index + 1 + assert metadata.plane_axis is RuntimePlaneAxis.SOURCE_BINDING + np.testing.assert_array_equal(output.content, pixels[:plane_count]) + + +@pytest.mark.parametrize( + ("domain", "error"), + (({"channel": [1]}, "cardinality mismatch"), + ({"channel": [1, 99]}, "outside the viewer domain"), + ({"site": [1, 2], "channel": [1, 2]}, "more component axes")), +) +def test_strict_aggregate_receiver_rejects_malformed_tiff_domains( + domain, error, viewer_ack_return_route, monkeypatch, tmp_path, +): + _labels, _pixels, saves, batches = _automatic_label_tiff( + AllComponents.CHANNEL, "stack", viewer_ack_return_route, monkeypatch, tmp_path, + ) + received, semantics = _received_item(batches[0], saves[0][0]) + received = replace(received, plane_component_domain=ViewerComponentValueDomainPayload.from_ordered_wire_mapping( + domain, context="deliberately malformed incoming TIFF domain", + )) + with pytest.raises(ValueError, match=error): + NapariAggregateAxisBindingAuthority.bindings((received,), semantics) + + +def test_strict_receiver_rejects_duplicate_plane_coordinates(viewer_ack_return_route, monkeypatch, tmp_path): + _labels, _pixels, _saves, batches = _automatic_label_tiff( + AllComponents.CHANNEL, "stack", viewer_ack_return_route, monkeypatch, tmp_path, + ) + wire = batches[0].batch_images[0] + domain = {**wire["plane_component_values"], "channel": ["1", "1"]} + with pytest.raises(ValueError, match="coordinates must be unique"): + ViewerComponentValueDomainPayload.from_ordered_wire_mapping( + domain, context="duplicate incoming pixel-plane coordinate", + ) + + +def test_strict_receiver_rejects_inconsistent_item_plane_domains(viewer_ack_return_route, monkeypatch, tmp_path): + _labels, _pixels, saves, batches = _automatic_label_tiff( + AllComponents.CHANNEL, "stack", viewer_ack_return_route, monkeypatch, tmp_path, + ) + received, semantics = _received_item(batches[0], saves[0][0]) + reversed_item = replace( + received, plane_component_domain=ViewerComponentValueDomainPayload.from_ordered_wire_mapping( + {"channel": [2, 1]}, context="contradictory second item pixel-plane order", + ), + ) + with pytest.raises(ValueError, match="inconsistent plane component domains"): + NapariAggregateAxisBindingAuthority.bindings((received, reversed_item), semantics) + + +@pytest.mark.parametrize("component", (AllComponents.CHANNEL, AllComponents.SITE)) +def test_existing_projection_owns_source_binding_label_planes(component): + labels, pixels = _source_bound_labels(component) + payload = labels.metadata.attach_to(labels.image_data()) + items = RuntimeProjectionSourceIdentityRequirement.REQUIRED_COMPONENT_METADATA.project_payload_items( + RuntimeProjectionSourceIdentityRequest( + value=payload, source_description="automatic label TIFF", + variable_components=(VariableComponents(component.value),), + plane_projection=RuntimePlaneAxisValueProjection.preserve( + axis=RuntimePlaneAxis.SOURCE_BINDING, 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.require_source_component_metadata()[component.value] == index + 1 + assert item.metadata.source_voxel_spacing.values_zyx == (0.75, 0.75) + assert np.shares_memory(item.data, labels.image_data()) + + +def test_existing_projection_does_not_infer_runtime_axis_from_label_array(): + labels, _pixels = _source_bound_labels(AllComponents.CHANNEL) + with pytest.raises(RuntimeSliceProjectionDeclarationError, match="requires a nominal payload"): + RuntimeProjectionSourceIdentityRequirement.REQUIRED_COMPONENT_METADATA.project_payload_items( + RuntimeProjectionSourceIdentityRequest( + value=labels.metadata.attach_to(labels.image_data()), + source_description="automatic label TIFF", + variable_components=(VariableComponents.CHANNEL,), + ) + ) + + +def test_existing_projection_rejects_label_source_cardinality_mismatch(): + labels, _pixels = _source_bound_labels(AllComponents.CHANNEL) + with pytest.raises(ValueError, match="metadata cardinality mismatch"): + RuntimeProjectionSourceIdentityRequirement.REQUIRED_COMPONENT_METADATA.project_payload_items( + RuntimeProjectionSourceIdentityRequest( + value=labels.metadata.attach_to(labels.image_data()), + source_description="automatic label TIFF", + variable_components=(VariableComponents.CHANNEL,), + plane_projection=RuntimePlaneAxisValueProjection.preserve( + axis=RuntimePlaneAxis.SOURCE_BINDING, axis_size=3, + ), + ) + ) From 293e04acb53156a0dd9ae71476703e24f66b196c Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 03:45:58 -0400 Subject: [PATCH 114/235] refactor: retain one output identity import after layer-fix synchronization --- openhcs/processing/materialization/core.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/openhcs/processing/materialization/core.py b/openhcs/processing/materialization/core.py index 74148e316d..7a6f9aee00 100644 --- a/openhcs/processing/materialization/core.py +++ b/openhcs/processing/materialization/core.py @@ -83,10 +83,6 @@ StreamImagePayloadMetadataProjector, StreamViewerComponentMetadataProjector, ) -from openhcs.core.steps.function_output_identity import ( - FunctionOutputIdentityAuthority, - FunctionOutputPathAuthority, -) from openhcs.processing.materialization.constants import ( MaterializationFormat, WriteMode, From 918ceda6cbab28f6cf48968ffb06ac9120a14726 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 04:06:12 -0400 Subject: [PATCH 115/235] benchmark: retain fresh native-paired Wound and Illum3 evidence Record all eight scientific comparisons and both timing observations per side, source-qualified to 293e04acb. Preserve ordinary full pipeline clocks and separate native execution/invocation; Illum3 total remains below native performance. Related to #450 and #479. --- .../README.md | 18 + .../freeze-release.json | 15 + .../matched_runtime.png | Bin 0 -> 128778 bytes .../matched_runtime.svg | 1614 ++++ .../matched_speedup.png | Bin 0 -> 145723 bytes .../matched_speedup.svg | 1586 +++ .../metrics.csv | 7 + .../native-illum3.json | 61 + .../native-wound.json | 61 + .../ordinary-0.csv | 3 + .../ordinary-1.csv | 3 + .../qualification.json | 231 + .../raw-observations.csv | 9 + .../source-freeze.json | 8607 +++++++++++++++++ .../strict-science.json | 491 + .../timing_summary.json | 145 + 16 files changed, 12851 insertions(+) create mode 100644 benchmark/results/perf_current_wound_illum3_paired_20261003/README.md create mode 100644 benchmark/results/perf_current_wound_illum3_paired_20261003/freeze-release.json create mode 100644 benchmark/results/perf_current_wound_illum3_paired_20261003/matched_runtime.png create mode 100644 benchmark/results/perf_current_wound_illum3_paired_20261003/matched_runtime.svg create mode 100644 benchmark/results/perf_current_wound_illum3_paired_20261003/matched_speedup.png create mode 100644 benchmark/results/perf_current_wound_illum3_paired_20261003/matched_speedup.svg create mode 100644 benchmark/results/perf_current_wound_illum3_paired_20261003/metrics.csv create mode 100644 benchmark/results/perf_current_wound_illum3_paired_20261003/native-illum3.json create mode 100644 benchmark/results/perf_current_wound_illum3_paired_20261003/native-wound.json create mode 100644 benchmark/results/perf_current_wound_illum3_paired_20261003/ordinary-0.csv create mode 100644 benchmark/results/perf_current_wound_illum3_paired_20261003/ordinary-1.csv create mode 100644 benchmark/results/perf_current_wound_illum3_paired_20261003/qualification.json create mode 100644 benchmark/results/perf_current_wound_illum3_paired_20261003/raw-observations.csv create mode 100644 benchmark/results/perf_current_wound_illum3_paired_20261003/source-freeze.json create mode 100644 benchmark/results/perf_current_wound_illum3_paired_20261003/strict-science.json create mode 100644 benchmark/results/perf_current_wound_illum3_paired_20261003/timing_summary.json diff --git a/benchmark/results/perf_current_wound_illum3_paired_20261003/README.md b/benchmark/results/perf_current_wound_illum3_paired_20261003/README.md new file mode 100644 index 0000000000..d768c954f1 --- /dev/null +++ b/benchmark/results/perf_current_wound_illum3_paired_20261003/README.md @@ -0,0 +1,18 @@ +# Fresh paired WoundHealing and Illumination Example 3 comparison + +The ordinary public driver ran on clean source `293e04acb53156a0dd9ae71476703e24f66b196c`, containing main `f7de9efad393bce83a3756a271f0cd4383783ea9` and qualified ObjectState 1.1.9 / python-introspect 0.1.15 sources. This is baseline qualification; it does not measure a new performance improvement. CPU 5, one worker executing inline, one native thread, ordinary OUTCOMES export and process-tree RSS. Mandatory server/library/registry/kernel preparation is before pipeline clocks; startup and shutdown are excluded. Native imports, JVM, loading and complete warmup -1 are also outside the measured clocks. + +| Case | OH compilation | OH execution | OH total | Native execution | Native invocation | Execution speedup | Total speedup | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| Illumination Example 3 | 0.594438 s | 0.535986 s | 1.539587 s | 0.626957 s | 0.695522 s | 1.16973x | 0.407224x | +| WoundHealing | 0.345184 s | 3.000829 s | 3.621918 s | 4.253330 s | 4.332074 s | 1.41738x | 1.17433x | + +Speedup means native execution divided by the corresponding OH clock, matching the existing figure policy. Native execution runs from the first module through post_run/close; invocation is recorded separately. Wound covers both selected image sets on each side: its native observation is not divided by two. Both ordinary and native observations are retained with ranges in the raw files. These two-observation means are descriptive, not statistical or causal evidence. Illum OH execution spans 0.385–0.687 s; that dispersion has not been attributed. + +Illum is the weakest total-time case in this new pair. Compilation plus work outside the server compilation/execution clocks consumes 1.003600 s of its 1.539587 s total. Its 2x-native total target leaves a 1.226108 s gap; execution-only work cannot bridge that total gap. Wound's execution and total 2x gaps are 0.874164 s and 1.495253 s. The unassigned completion remainder is not assumed to be entirely avoidable runtime work. + +Every ordinary observation passed against both fresh native observations: eight unchanged full scientific comparisons, declared output inventories and source projections. Illum has two float32 [463,461] image outputs, 213443 pixels per declared comparison; PolynomialCorrected has maximum absolute difference 2.9802322387695312e-08 and no pixels outside the existing 1e-6 policy, while ConvexHullCorrWorm pixels match exactly. Wound has one Image CSV with two rows and no authored image exports. All input/helper/native binary hashes and saved physical output hashes were independently verified. No new comparator exclusions or tolerances were introduced. + +Figures are generated by the existing `benchmark.reports.cppipe_figures` owner and retain the 2x reference. They complement the separate 3D/Speckles qualification; full30, scaling, and broader PR394 acceptance remain unfinished. The qualification source pin is immutable; it does not assert that subsequent live source/environment remains at that pin. + +Raw scientific files and the bounded controller remain in `/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1` and `current-wound-illum3-paired-preparation-v1/controller.py` (SHA256 `0b7abed65ecc3d61761533a3a9a69e9b9a8acb220049175ff46fb5f004ade4aa`). These are local custody locations, not a portable launcher. The compact committed pack retains every timing observation, exact qualification and full comparison receipt without copying scientific pixels. diff --git a/benchmark/results/perf_current_wound_illum3_paired_20261003/freeze-release.json b/benchmark/results/perf_current_wound_illum3_paired_20261003/freeze-release.json new file mode 100644 index 0000000000..7e4153fce3 --- /dev/null +++ b/benchmark/results/perf_current_wound_illum3_paired_20261003/freeze-release.json @@ -0,0 +1,15 @@ +{ + "status": "RELEASED_AFTER_ALL_ACTIONS_TERMINAL", + "source_head": "293e04acb53156a0dd9ae71476703e24f66b196c", + "strict_science_sha256": "3d89dc4daaceb34535ceaab05100b80b88487e32e67180a99595035b66c67236", + "action_handles": { + "prepare": 65780, + "ordinary": 31694, + "native": 10409, + "compare": 44398 + }, + "all_action_exit_codes": 0, + "source_clean_at_release": true, + "main_based_fix_pr": 523, + "output_bytes": 10031981 +} diff --git a/benchmark/results/perf_current_wound_illum3_paired_20261003/matched_runtime.png b/benchmark/results/perf_current_wound_illum3_paired_20261003/matched_runtime.png new file mode 100644 index 0000000000000000000000000000000000000000..19ca99020a84d103e6a2f0b0ed3e972ed8d8525e GIT binary patch literal 128778 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 tU&CGyhzb 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 literal 0 HcmV?d00001 diff --git a/benchmark/results/perf_current_wound_illum3_paired_20261003/matched_runtime.svg b/benchmark/results/perf_current_wound_illum3_paired_20261003/matched_runtime.svg new file mode 100644 index 0000000000..8f16558dde --- /dev/null +++ b/benchmark/results/perf_current_wound_illum3_paired_20261003/matched_runtime.svg @@ -0,0 +1,1614 @@ + + + + + + + + 2026-10-03T03:59:18.091894 + image/svg+xml + + + Matplotlib v3.11.2, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/benchmark/results/perf_current_wound_illum3_paired_20261003/matched_speedup.png b/benchmark/results/perf_current_wound_illum3_paired_20261003/matched_speedup.png new file mode 100644 index 0000000000000000000000000000000000000000..629f47cbd89ad553a0225b9e9eeee57c5ed674a4 GIT binary patch literal 145723 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

Ikb`v$_%17CJn0&` zgPkYZBFh)oWl#BiM6atr;?f$2|NhkE_)2*qDZKkrB5fIx%#}THM)v~79>MUWJ-?S@1ZXK32iR!d?c9uNzH`=uPB&Vy?@Efckb-&3npn~6{NZ^PVMJjH3n#BPVBddO^ zJpYPnRj?+x(;SUU8Kt-4j&eWi*IYu=p;42IS&=y`b)U+d<|(;ML&IFy^NJZi`;Pz4 zfDhIn4~*V4TFNFqhBh(&Cg->aToXt!+Q^%tTzdQh-{AuqA?n+I?phnM@6H{LppMv$ zWHeql{OrKLek9tc1;V8(Z=Wg4F}G92fM_Z)uj;?Dd2W3K!%~~q9a@Y6-JWbVJWa#8 zyKnav!tQ_HzK{3PsMNGaxX>aH4g(9%zI&R@P6w^9zW$N*;O0H1H2#;P!8!P5J+D)w zdaDkV?lEQEZ@9Y~XSo|JQN^HZ+52xV9#iQ{4Ns2u87s(U`_D|QTuNkUhyMMBW(l>j zr3za+LE@jwiQVpc5=?^$2mi0pCr92GXEa5Ga!M_g#N zzw(+)0@FkN;@tGsi!!I0BqKh5)7v-NpdTU?CngB;{sVIYi*y0P?Qr9CG2fyjAnqad zJ<)W6^jOvk!gR&;jTWFBl!znm+C;pwK1}xX^i*cN=2|E6C<$$XHKsl}Jf|i?O_}3D z2SOo(*bqn9YEA|0T(T9cRZ=3V0T@~?(>6DEdKphA^>sA4_^@q^$YJa=CahBFGu|Q7 z@+RlWS3ft9tppnx3{`!Cu@a)Nxax{ZOrDjbj|{*JF@)@|9~AMQ^6}l+yg@6=bMDG# zj+u7HSWImN0r~ZO4)G8~Sa5%6;?ey1ZLn{iRvhba{Y6E`Q9<73^<|}s(2z{$;E+tP zjKPY15TE1Nu4qmhh*)@FrQFGvn_WJz{8y9Ueb5+S*#79TBdE&JnH7MQ!}V3<{D;FE zBqJVl(PW~Bx9~3~d2f@{VKf10_p}_ha%ufw1i!UJkw#c;=3N4+w8 zI#5*Ha4bZ*LraSkd2U2I2Zw3QD9^ATKlB`H?Sai>bAv&*0g!MV+%K_Buy5Z6RW9lwFMKU99s<@q6Nb3&t><%QfZ3#g^ zW(9o}yy2Q%xQ=;Rs90qcQQU$I@9h2~n+}VEu5m_0WTiE|WF7bj4dZXmEdi^<&R{Fe z|N8aoT8RIa!3YB<5orjO+u}O{cw8?L0acVlNfWUGal^e>Y4QfF9xtAA+pIlzD8(W^ z{?DqJ9&)Utq!+4Wx{n%YTwWl%Lj$o|N;9m{mZFgB)}O`cbQ#-Dzx3$QZukn}G0u7; zVRLFq9ptaefHqqLB*u4;8=Y+^&HQ}VPhM?bn;XD70@n!F(rM_j5wty& z9GX+nbSN>Wb}N~7Z!d?p&@as92Oh}d#^0cY68eTe@E#aboe`TSepWLZK3=UI6o-qP zt3|ys3R1M95!s<0c!q;&wnecmSwH00_xq#J{Bz;znG==+w}b2#>(N-~26HK7a`b*W zkK4tocow#!&EczyUt0fq>noY-C8;YdJU1fee$Yppz`fxwv8jegudzwoIO;Rl+7tcC zj6XGhG5xtM>5ZS!G!wdK*89yC^$Jb1rw+Q=a-r%(#!MTTNAPA@16 zKVi!)vpw7z+JrqiZNYBj`I>pME3cc^l~z2wU1Xez(#wWkc&s!(%sN>e7X|su*O&Vo z;Z?TSf{(I$8dKbQ5BEsmMfBrO=5w}vpbo^!SA)83Co&+|(TeTLb&U1Nc5HUKqbBcS zw2RF+8{{27(3PQoduG~trNY@5kq>#BFU7=k94)praLAr%3vpi}(8}thGXx#mbAlc` zcyaVmvUCG;_K{rKFcY;#z|Dn;-gj6=*JWhFemivUq_>#BtxQ^{_pW8xd$;XvdjGUK z-KlZLt*-y)Ho`o`kyvK3DZ(hlFXY$V9$A*v7h>9TQhZJqGPeHO*LERWbcDJW1)W@3 zKhuNOOKz*9!iBcGX@rOGI0W3PpE{c&NT8W52Ix1?yRN(3Z7|bz=(ZLEEi!_s7A?30 zc#w{nD84UFl6dmDJm{JYkoqe_MIyb-F!f|kt5Dc>X@fi&+n80~B&dS~HC^&|Y!P5$ z=V&an#6{*RCc0p+nG}>p;wWiedHikMF8SQ#U3t&U*0AYfZDpnXG3I=ZmXkhn4y% z?M*G=c5-vnEkF$pm(t&x>Xdcxbvnr45u9R`jO}a8pvx(iVmX_{l0HV)O2#@5Gn@Y0#a*Zu z?nMAUhb%{4uSQ;nw`tnzSeS)e-FN3fU!;YEIBjOc!oCBBS3=z$MrN@`RXQ^a!*fa8aW$hww+}Lg0%A1ikdz5Y117p?YHq-qt%i3P$b%&Uz=ciq^hLwa76K~LS6wb)K$Z2ZfQn`KPjW$g}H zvA(zR;*zk%1FDVpX-5w4HnuY+NaIjjpK}>$NK(UfEU;$Gb^2exgbWOGyFDJQ-8PV> z8#W1b$%s`V{7c*5xC+g0S(f##24r4xC!Viyc``u{MC(_P>_<4kYF$gcU83OGEW?R{hqR^e_!`BqG5uK*!QPOs8naTw_>zlx9|l*elpN@y}E^A~mfU5Y$&UXJ(nXGb907 zo0(}$lrhOBaF^=7nV?+|MG-p*B&`2h%9VKd?!4p-LH)&HI463EJ#v&NAB7Zn%R|M= z{T?X;^TQZ{$7lXW^+i&YW4pcE9jS$gHnD4jo01+3ij+2;51I?uu0lKwk3+f$_)1Qr zKInE||H*@Y#{x1sP80<|WMa`EVepV5I?_3M`!)Jl6}4`qnTv{Y1-LRtrsL^c zrvp1CKP9fBHqCgh_;Xvea4n6iH4TcdwJn8*@q_4hcCv}Y2OC}6dc6r36?@8)K>lS0 zIIK-7`Cmv?@6Xg&TL)RbdwTxs4~N38npgx+jP%$5LHXyOA9V52Ox%=yBZ^&^#W(-7 z`{yqO0vsf+SyE#FAkdI+&n_LxE^cxaPnuO)8AlFH z?Gol6%&;IdiQLvJ&c8~{>TA)Tzqyq%XR)wYPt3^3Utz#MUT`8Rb^4mS)o0L6vGTK+ z$%&y@y4V6JRk;(ZPOJ?%~3nPUVKQ`LT>F z?2%Wy6gB3nE~|j=%L-^fZe*OKe5l>koHYaD9>P~XQ!01@H^OnL{0|i{`Pvv+;p-kc zl3%$dE0BS_K%U}*2fQ~p`<;IwKI+M-Qx?z021s+jdpoH%P)+L*aKIkfZXw8g$&hUYeYPXnR?v7hpvWKs5uG_rP+Gf;|7QGqj`%ZrUtO5-Wb zV8QJh4y7~RB>3}h0upi*ZqfV-LZgt>bl3q~#g}kSo$exH%a75HhGsP6m*`QdvuKdA zEoV!haDmo=`=yN^B^7qUx>gc$0~zEIQme-Nhweny*HcbBTXB?qrqSM8sy8jKH!`== zl&w4F%_~__-$WrX*iMjFRl#|%mpFAgmZ1AfVniopLOtAyZ=HcSVJZ7mO2&=cP6P&F z;71>6CT<`o3TLyaJ4P;w^AJ3)@6DZXY-q#vX&A^JefU!vrJ+Z_jm;>tgUuD_4IPAC zpBZ31B3n#dzAQWCS0&0gk;=M}sbh|o8p^M1QADpyO(^bJ_cWl4Q(PdCxTpfWy zMZ>7|z0GL3?N|~y@CGMEHdO&>Qi`hsbmks7IniSgj76lB?+o-~$Ji%kV9r1;0|zI6 zvIR6r;yP6aAMTpuyl4j}P%19?yVZWzkG6wIEYd+8aW#x3qr@lg!_x-N46{^t zto9jua00t>`3Iw^EtyR@_D0nCnQ#EF7Z)5F$@0(bXoL60Dx!XrK6cckPaTCEmu3d1 ztme-rqV)eP=;YS2;xqG#C9u^SDmYA)D<2<#S6_wO(wWyH*fbaP+ zs;B$Pxh0nw?p^3r>6-E{sI-4TiWKibG^8lPIwcld{4vcmxRv&Az;)rlSN8SBbLC6;r-yw#FtD#6O zc$1|r&0RA~;Z_6C?$);FXxu1`~+rXC27?x>UEI+_Jlk3 zf~ar6dTF*!#gx+6t_EwG70xJK_{x0eW^KyoNWG7#PmBRvo>jZxH^&9t?Mre!K&Ra@ucp!3w<8VG7z&( zjo!0SI7^8X5|)$nc##_u{T`w;xL)T-8^n35&-`-Rzq?9s(N`tgEym*Yr3f`DEqIf8 z%hmh9oNy>%8`isut7yFY|2LTrL`%#--Z^SqDinKQkOMOZ?Clen!%PR}T-GR*{=V;A-yEx#{2=iN>7 zC~_YFUVv@6rzQ;H_rGt;H8I;Z8#rf{udo{+4Jx(FOiXSy&F}|Gb9M(s%f1c0XaXgZ zHjNzYTGLnO76CyO9oyEywR2d6^YQ)r#`_!?xWT+QweY~5gT!b#?;HL_1{ji^KGw)W z&47lIm|fpA@wg=pr+3mQ`+V&%K^=6Pw**c;`%b97$!97WoKtqQsjOoO<-=~s;=|e< ziyTQ#o}4|J25{onXA%ce7YWzm7^J+ii9ozX1G&1lC3qjBXLaePoly!(Bf9j5H{tJ! z_DJIM8vQi+cc1X)1xRy%DPSL)XJ$F@G-wO>d;5ABu5c3{x@VuRm+FX)MwuU0m^Kzk z@!tnq3v1#r3YcQA?|LrU1PMvSc%H!q)~LeXh*rd07VU=vY6HQfUs88#l*aU46KbzV zlYh)F(ngrDpgm8w?0RD#?>oF#Z0iYIF_rgMVExJa)b0^3QbU8ckeoLPuSA$0Y2jbc zF+#9k0L%pjCn=k&NjhRc9j#+$>%JXgD|MCKl*>4rAp;{?3D469@c@(=1%L>da|C-sWHJM!m;~Z_492^D3(#;Q+bjq@@1e#V}lR_F0(?1hPPhP5I-^ z#BNgkBzs^x_Q3f4lj}adj1?teOF0p!S9TvwYd8gIS02eiEWuq1?dVA5B~bdQ*6yF( zp4|r8&^X+NMf?oqsbsm@%j17g8*GRmB)e8@0P^noiS&Y&r+#Pvz*mCyxG!#^+|+?KGH8BcCdQsH8z1yW~Jw0SZ zHvcCO-Rv1XA(>Xp+CMK==DRQ>ecZ=?-DQ>YMrGUC#tlaxkVKI8y&`{}1}xl55}iU0 zi&F-Q+%oW!!jHnB`JUy|Kf<~%^ksw~9&Yw5MWx7z&a45KxTD}$LIE)lYIT+k#MF1b z-r8V?7;@;7RT$7MD@%fIA(aD_7bz-i(B+P6@=!auLWoqi9O5wNv)QB}NO5toNPNxg zF}<8U7>^f&cGXF7fr z&Tx6l38rz(x9&Vr$(CgK0c38OaDd8Y`;1<_Xc?}jm_9UtN4Xqw*c1<*p3;Em5{=R0 zRP+DGj|kai7vzFv#4Uyg@-tx;cDE9cfpqNr>*=Hb-L`TNQyKlTd~SA_GPas8ztI%A z0mQW681VrHDn~!2sS0EfOu?d#D5-egO`$52EjQJC+z*9Bt+=mf%wv)X+*m;SfI z|6%XFqoO?fbzu^B!cJ^aqbMlEiWO7@6e%h;q}h?KA{{|NK|#pnzGUEr8B|QRE2$2zqAUt2d{vjUNTp zVU3DxcW$l*1T|V8h^e>|+Na7M=7^7&p~}I7j$=$as2K2+Zc&mWJ~HjHC@<%Ds3`qgk|e_du^=6X?fl zqj~}8A7jQ*l0R5MqR7?^4XY|^g~BOJYF+{T{0R*3J>lS$k7x3si_q&?mBh8gJHw!q zYFOg8iaNm@CxS3bWnmok076pIMAk9)iFh%F_GyGI0OIFNV+N9?9L8GYmoR~`Je^Y- zHlq*=aDH=cU77sdOGaFjNh2RnCIumXNM`i%y*3TUSXrYJy;r-+)?S<%lk_)AxZ_~l z{{Em0(6$=Et9D;;e_z6G)*XO3b{^D7DkL=JrKiWf02Eo3TthE2UKSDV?s13^Ln4sG`pSFE1Gj0VZkL_De(~bv zsF=yB2}isov(%RFU6KdenvWG2&EE-u)%kJD-Dmxi{y}jiESslVbM4<$ZV#~hn=NX#{VxQ22RaHnq zYG&@gcF3C;?e~ueSMn|YoW^gGdoTPVP-ZjM86lx8lnuRmxggwb+j`~Hq~Yi|RuxCn z9q6rHw20`bPEDxM4>>#g-@8X>!_s4K<=Pff#!gA^?t7lw1XfGlbNPkta_qsL<=dZ8 zo)qiwo!UTo9Fbw&yKK*%J=Q(sT)jjy_^I4^YEXDF6U5vlS@z6GTk2fV^mVcmU2Q%) z^bTX$fkO?pAxYEj$E4J4wrMafKr-0421X{Ux%8L!xh0G$)?uWtPWDp(6WDV(Z3bW1 zaD|`aJxGyE7Zmg4Jb{TOsKio*S@vBOblFI z?CaEYt6nD>LxcT<<4|`vM(3_X-1hF1o{TEaAVs$l0X(hsw+jpNXcT9aKDn{C zP;Xx}pW6w-gq-I~f1&_n8BM+60zVWqH+(Fne9@O@4W0YfeBm77PQ1*A>Yq>1Ub zKt&~8@9Nb+2Q=%~JBVz(T&6T19)#|~J)oqWnYNCKL8@<56WjP>urlBgpdTF?^dA<5 z+uSyoHOr72C|4A%<07njynT@WVIWmk)k^_=w3sTIYnuKuZ!jS zsPCd8r>lFH98-L9v*(TYe>_qPyPWaH#U3Z8c%PXgvCS|Ha*~Lb02^SnVV24R%5y_r6G_~DZq?*@?Q|FFAtSf4U6C9s2 zT^ZfWn6mZYDjk$z7pd|~skl{ZIk1n0)OXIvsKAu@G107DI{|irp((s2q*yCZHcsUPbM74xvfHR4HTWy> zIF2F-D5-8iEJOl|Ai8;pH0h-z8)%)H3q>H?Mr&2JGgoFAy$`Vy3TAEB$69!at?=dJ z#Ibs>pxyOzX6eSwyOgU_BVR-g7)E*+T3U&*ygBg&cZFvPZ4;w0lNr>hq2~Ovn#`!G zCe*2Ok=xQg0g$t}*LJ!;mNbC^8MC$H8%qf&2uaLJ=dlZeZ>a;WIx-Gnp@!~)z4>g; z)RC1=BPWczy;bl4s+E9u$6C1`a)r2Yf9+|5;xQ?x76hL|uc?vxLL0=J%dMm^TbDH6 z%sEsuH9p+tB7AsPA$xXaB{|NNlccj_cPg_0-?F~Q&pyM*%KNoDO2%o zmdyo1PRALE!h%-kUg%`d56_1!XZZ$ClQxF(A9qoC%(TewQonbSz}jbQc|s_c9{Tbc z+ppOEPW`3JmTu_TV@0n{D@ZWB|KnV>%=kr!0KghoT~|F?XG+^BB4S>?2h5}JgY1@H ze^9pP!ntiz!=%dC1Zf8;U%}@ZSS*Xtf2L>?tU~uky~giLP7CQ3xWStc?kEcc?2Vq# z;`=OuR(zm`%@ZTs9GFN0aPPzY_P2j_$Ifw zFjaRD(ny7E#@;)3Nsf?P6>{^OQ`G<(_rK7kZfomLX?~?ccrx(ek%*}p2d9(N{AQwn zu4XEwj;;j<1v|nPIjtRj^40G4N4Na1-C-z0k>o_&8;$VyB}~TF+50HFf`m-QrY|*h za=@Xrszyh*I~z$3e=vI}Q@3C*V@mWtpx|6XR)J#8z4>qG&-S|z)MwUD1%i`pXrd2{ z)0bEiCW^O6V1-n3r1bQsF9)*_tQ^D?^TU<1r0tbn48SqxaOdo0pLG;YsPbY`ZWPIw z`mnpZ{gm2@G96DsMMJtvt)kpnNLizNDy#q?h!w9YIhIUDHcJOm(cw|jYG{xVJ*tbB zsZK2ANyjb2we;YEyG!83G&2)QzhuGFGp|ekKpg=k9yXXg#Bo_9yujZFTxl&tmcB4N zv{}}Hu?h!s6_Y?&kw6%C2QDy;sgk*~((=m1ekh>a|M0<&xuhyPZUZE}(np-z1u+?1ASf+&z|xN<6?K$Wr>$TIB?ZC=4^NBfe7 z&8sNz;}d22*&*E6fkxG0)4MAzj}rytDBXj(NGSLV1Z@vwP7QTw@OBF;jcs(kR=ERc zXx$Cfl&ht+ByYE;adlPXX8Nk)7jsVy4t4EtNk(};Z#|#xa^kedtjpr0iee2N??vfu z`!CqxbhX_DlXBMix7B(*(jCkC2OQggxUSpuDfcmfJt=5HaG z$@KfZw*H&Q24a9%0m{h>Dt~ zd6rCkb(9)ytRRcxE}|_Z8NtdIuy`6F>?|F>pUt1M`tHh=_)#}vtc~n-&6?IWcj0_A zFH*~`V3;7U<n-@OI#lW=h*1nVknvxP{V>P-SZJ%E+@x`U z{f9zhf3CjUnuJ{;>`qOK0_f#M_98Qe`gE_rO4#$Aqhv7`GoOl1S9}g~Ysp>m&|MY{ zDo^U969OdbP=~p|er`hSPnukK$KoG<FOhBox? zh?`VT`s>&7grp?JQa=mC(8Z}#$yS(Exq`C1RhHeC=(EGg9>8ID4|FlW;O0VagB&w|lYwRT5gMeWjG}4=*PyTy3!j(!* z5L_LTxTGA+YPlm2l1#xR7F!#4ze^gO{TL65f9Svg`CNJE_HE;S>Xi#SpEka^T>sAx z@h!7CS{WLuFFsIGSorD`gS2$8fkcrK_le`jkN@u~xq!)_k~=*qw2st?v+e}W?z=oi z0EVN88v7)o*@=+_tT!6gi;>5SDr;KIe(YHX`Pjp~13nQ)9$c3KELAO460^A4iP+u& zGOHRrA9TS+UanwEBL>gKfRFSHstE$8dDy|KKW9@jdV0>m>&iM11Dny8@OP9rs^qS& zu6l?XT$1IqtuBLqG0E(KD$-KPhU=%KnUxhTOq7yblWR1hnz$*WF{=3L+BKz9-Kqqz zQbqJ$11x*G2S2~Q`+}$xIH4n-xSRbTI3LHW=Q-8TlQ0<$55KI#^`O>ANr|hcJk(0d zxi)mLw^{2aY$b~ZDf|1k#ExkbQabUT&BF+}4Go35Sczo^%~U#Ekf!>4j00Spn)U8D z3z*oSvg}9AJes+7$E1}gQq6cnamECkh6UEJ&VvEb%PvKNSJ5r!<7yjxh2Sk%%nrkq$(9B z!+Xfwob6mrIu*9f)`kHnSO2X>c+yw!7j0;V1g3U}30$m8io1w95A3oA30j2|Fxq^{ z9{mQj4s&=ahl;UZU6pc0N?*AJF7D(kQmg)Nl{E@*RQt zR&+J7iEWaBMeO62zx+TLq(vwZC>O>9hi1SH;`qJwH2a$VXB43SOQh`oCqGNtf|UT} zug5-Od*b9yIg(dJ#Z+&C0%J0qaSBI_W`||p7H{#@{qE4(N1Np#QvUoTW8H16Oe=a7 z5aiQW-`?cSHP0CExXC3+%9HR*NEXP6seK7sT73jR=Q}5`67InjA#DeD2k|Ib#C}t4*%XnKH=TZ_xul8Hm%)%Fu~Swy(c)mi z$q3B5Wk8Rpw-&eM+%14r`%*N;-DJ5KMEeq_JVtqi9G4?aw7VFCp5@>d`JC3*Id?XE zi`(q$;fx>FpBcd}(w z>dm-6T!9Z`FpgHjW8O5dR5XLr8)sPgddci(e#rv!-(h+R;AW`5MO2KFQJhI!aM$sm zH6|6$X0JT2ca}ht5rAFVTln;xCdg^?GiKL4PkFfdous0oEhMecnZMU` zG)L4pE<$+ry8HBuytrW$H_TwsF#FH{4Y`+OwF1yW(Lyb{s+*|D2{f!-bCH1RFn(3z zT;#fFlzqsy# zCj7G_h z*2(DZ)Z_*--jFInB(r7bj2)>ePCMvX zxH&TrwLXo|9cM+wLoxF(l^evfhj1SfX>#tIDSFWia26V))5?}Cq^JZ*ceaMq$JC&4 z@T7rj+9k~^5eZ=fVZC@JTfMuW-@k7fD#2GnhiC7fCt4{HZByWG4rb**_Q{^}aPKdaoJhf4{E}H%U z5muId&UXUzMYI7En!lH8b$VcQU>kk;1rT9-aQ|38dMF#ZBqPGCe$53e`{fmWYwt2X z;U1V)#40d!C4LSeFPu}ZM*o+~imX|!-LQ^VBS9og50K(0%_ z*TD!kX0YzKOrUCET0RMHV>?~Oe97nwT)V^DA@YM!&JbJv)m`>J$8lW$Ndr2l{E!z> zh-TJDHs6=zx_QPgMkqD_q;Ah-)1=xRTOv6&;`_ z=O**g=y^B|K5b+<;S{?1;g#VGXNR4;#NFbb(;g}Jt>ru0ALVM0K-6YJwKa{dd$d;@ zJ8i~Vfm z70}G8NWR*4RTv0(-St6)wI@RDOL^>HZ>GyNqso*B30j#*hf8Wvv;(e}Zz3Y;xry)# zl7&puIBBQ1KAW@f9$_|DLv;(_0Zn6?aYgoh6jrteIZWSP4j3cWaNL#VP;af3IB`$Z zIdefB%j|d66XXDCE-FiE3jzHiN_ZYcG89nz2*>^5y?fXz&RJyX5SUdS&}VRnATWll zK5}x$I%)2r)Bt|8`ZYTOspHJJi|-%Zs+iWCU5YoVpF@OZ?HVFR6}r`CQ(5ka}(>SWASMl<6fspeidHhY85 zl1;Cdr@1g8q+_1meTT58i9`qrR!ltCZr0*WOm(%us?E?3E_t5Q{5ynTJj`hz^iV#Tojnt0+q%9RE2=z&K5|-zj;#woy zv;ivcu0lH9g;L;OA!>vYG<(ng9YhWDhNoK$Sv6@XlOkln!#qI4YLwico@nCQ*?fNq z3GA324(8-i1+Q;E4BZl4CvlmRqZeS^^MbiHrrttCf_cmZ7k9N>gTvp33c!)Bu1XPb(i&&mw1V*K zH~5eL#>;TWelBs0VfL4?G(*mKBRy^UjsOuaNSEz<{k-Tq4~h|bcMrG3Wp0fhrBRx6 z+2Y-1bnfA5Kvny(yX?g7eAxQU6pbh8a+)1!tG=IiFVMCwK3!ys=Fcon?@iBu_Y1hE zLCNDsHp}b)Y+ej$e1}{Vh;*V7@2-;Ab%Hul{ywS1bJIUR-5q+u#UAQ@G0a+I<(p7) zvX;^Rzz50Jr%5GVKm7d*UPJ*SOiAI+y_C9i-5479wX%d>d9k0w&jm&+TDYG?>j%{5 z5$*{0phQC89a79sm)hLCyKArMJ|tXulDf1Po@xaj8D?(Fc17sEp>nMcjaXFWpV@fL zUvCQtW2uanjF{cAieEic(P8#M`RFNt)77L};UAN7A4_`2f<5h@yHF5{aWEME%R3%k z-@kQ-UVe>BIwTH3EbQ~ucaZP`W!4sL(Sro%nTfi%7IQL6uPn&n4+KVaq@l!L?nh#E z|JL}|*Dkye*}65_%KgQQcHeS+dkFwqrSj~1nfK9FgC5UEOPb=b&H{pf5e~N00K7Hf zgLCaPS0BTeOI5;ChhqpnZ92t#;OH-ujfNBOBAO@uT7L%qo55_UjzdW`l&z?>AfmkB-U_bcc5MCILaL>`=r+|B;P9-S51k20*GA!T*+wX;&NLP3? zbM4Qy+oGr;+CX#(2VFbn2yY(0U8Bq7veM=Ah$?PS&{R(76ocu>MSHpWhNy6cvGu*{64S)GieN!MII<5VWbN%DZA?dlde|8ISK&C6AB z=|O0k5me5#2leOk4&}K_WYjiSr9szdJq=k{&1*3+n}!Q;iUMlRw4t7#dyA;uG#q>{ zHhj_Ap!?n=ezbhSBk=W2w{e^3~yjMXbSqDhb2Fu=lmt>r1 z`Emnn2V~P}`@zlFJ-&kJBBW||oO-SH3h6h(o=6XI5p?f$dj)~=4vIhf4Yh^|#93U~2PIiH(_??*Cf%v<-()JD8`5+h zA7hK2(gi^S2g>PiLAXdezZgLI+1PiRHUuZUBk*l7NsVqhXy$NjP`~U2qIVKux^g8k z;sf&C;SV%3_A6j7AaW`Y`xwjIWg5kZa}HRVrcTcJ58~Re8N7;xs=~bGWpbB?LvunD zg4gxHXsBkaCQdw)Ycowah59UHQJzNdF5vfh(?qnyX6RlR4>Gz*OpAN_9X3$t6*Oth z-bB>*m0mzqq#w7tdz)2g(fboG7c-$Jn^n%<#^&1P9|eMz0lkzz%Iutl#EB#{bZ$i* zHG6dBu8pV^X=($fD4aZDKSIQ}A0H7?j)stkvvF|TYU0ZPIFi;y@N;a{yM4W;xL>ah zUa!a|-{1(>AMtyXmUovnPTt-BS&3AL(ZP3LY^tU;o&l(Ehwj95kuKu_g67Cr2WHO- z-c{G%Zf1Z3Mk5N?RTRXkY2|DZE`T_EW>mX+WQAC3rY?I!{2Nj;h1o(zxs@`T>*CKO z@L<~KKCM0hR&Uiz!CfVbP74nH&+%o5o)Au_&;N8!MW*9v*Qa!iz+!)#ZmJviDEC)R~cQOcSI^ zF}xw}CzzZbm-d&FGZ@{7xWoY|7=>}(y&>J>zNCtWxMl2R>w8Kh`u)AKyOT(pP$)Kg zvq<__If+vdYmrP30wggb4f}al{p8IaEe~711C@7j!ygMv>vNn<1I3mVKyi` zPCzTV7SnNLMa4@LIEqC#xp<5Z!+$s8^+qY1tq}4&0eH+MIZO7`o!SM zVN3{>3ys?y5LO_vrad#gAmU0k!K_axWh0(#CHgZUm`laWUjO18KdNCWe!VHb8WCYv zeTqfhWRp`(AXBOau(qzIEKQ;=X&e+`!{5ubb?m!LFq{)o0K2=*R9Qf#L0TVPa4Sa; zZT=4$tZ^XPiNgm!PJXqEGM>lspt2S8<}cDhtG7@Ih8=kuIf&bcc0OtSwsQVer=~TS zk&5;%0UNnuk_iAlwS;cqD6@8hYS79h!piq|ywr6`X>xYB{d@qnNWVZ7r*<8ci_T)S zXNJs0YFdNMzG7`~TU3lz4VGOy4fyEU>zxrVoijgV6a3#wyWAFIUoO0_k@gJGI1n)1 zt0lqHKY_tFf4~!6H>xx%Aa(4;-16-!xUwXEeW#;)TRGVg?5?#7VNtT+enZ0qJA@D& z2C{ogcaOI}DVJWw^@ZPI|D-E$wL)2v)FAR+19+DT8ai;@SQanx#R&Al3a?Kio9EN(D9gay!qL1XdO!OgW+2>-A%K}_J z(pS4S=Py5?45c>C@w)&dLRP343Z8%d*|&v|X8-)3JpW*#5>-{vQW;SKEddz*FFb*L z$zWtqnbq}IK@rc5&6xkdRdoC^@lo*=-h1gjfqwX>_wBS9@Ys@GH!wb#1%&VYoW|KN^UU`IIP%^-s5kF|iO{>{ko+Tv&3FRffqa15 zB#~{?vMRCOaivV>YA4NEP}FVZQdA`_zjK)#>>FMppuU%{2I?E@rzxHWu{8+mfF`!K zmGChk`<3~QjgsadA)s}5A1S~tyo^`y%05CO!72}T5#|bCHT<++v4x$dpH9Q*A!(rSKP!nLVvQy{aY4B}wR z2<$X-r@?6gJ&i9C*?Z*ohXc%Ytnm!hy^*#2XXBdQGnTr=yv4Vn^VKSRh& z4~Qcd_GqIp4)+zR(mU4NksG|E2G-qn=4ug@1uDWE!C8}IK+DlQ( z*q996@NI;Syux7Ry_UhZb2y|xE{zR-rwkW546MU+unsQ+&oEf8DC)A~<&iuhUiYTi zJVVKyt1EFS?d+=Z=_Uf)vB+kgRhqbTGK||WIzbEl#7iHryxIzdkL0!iun*4QB}nc3 z`RCbfqcf}f*DHGEM&P})xi5ry8D%Z>Le8dBk!FFy7iE9w&rkF0o0EM1KZfoKsoVql z{~m#w0pfNxjY3quh==%SvqdwQ=O+K+>N&!JI1ecE(txEzSdFYdS&rk-W|l347>T9< zshXaH$co?j1G5);wtj-r*J%6Hvua%8v%{SWUCPm);%%RtJMnTud`g5ubi}=D@sMiV ze`sGH?85wVn-u%Bx5m4Ed;glxD7(dbxaj?Hnd0qNv`y2h?M5?L&cUp8rg_C_G}+

BFEk_?1Z3?*i(rz$|m{urXx*WUT+B^YZW%M|A@H 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 literal 0 HcmV?d00001 diff --git a/benchmark/results/perf_current_wound_illum3_paired_20261003/matched_speedup.svg b/benchmark/results/perf_current_wound_illum3_paired_20261003/matched_speedup.svg new file mode 100644 index 0000000000..3f4b7b878c --- /dev/null +++ b/benchmark/results/perf_current_wound_illum3_paired_20261003/matched_speedup.svg @@ -0,0 +1,1586 @@ + + + + + + + + 2026-10-03T03:59:18.419344 + image/svg+xml + + + Matplotlib v3.11.2, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/benchmark/results/perf_current_wound_illum3_paired_20261003/metrics.csv b/benchmark/results/perf_current_wound_illum3_paired_20261003/metrics.csv new file mode 100644 index 0000000000..d8d026b071 --- /dev/null +++ b/benchmark/results/perf_current_wound_illum3_paired_20261003/metrics.csv @@ -0,0 +1,7 @@ +pipeline_name,method,assay_category,module_category,accuracy_fraction,raw_seconds,speedup,peak_memory_mb +ExampleIlluminationCorrection_Example3,Native CP execution,,,1.0,0.6269565154798329,1.0, +ExampleIlluminationCorrection_Example3,OH execution,,,1.0,0.5359861850738525,1.1697251401982418, +ExampleIlluminationCorrection_Example3,OH total,,,1.0,1.5395865004975349,0.4072239625881526, +ExampleWoundHealing,Native CP execution,,,1.0,4.253329693106934,1.0, +ExampleWoundHealing,OH execution,,,1.0,3.0008292198181152,1.4173847898497653, +ExampleWoundHealing,OH total,,,1.0,3.6219175045844167,1.174330913866291, diff --git a/benchmark/results/perf_current_wound_illum3_paired_20261003/native-illum3.json b/benchmark/results/perf_current_wound_illum3_paired_20261003/native-illum3.json new file mode 100644 index 0000000000..b77b0d762b --- /dev/null +++ b/benchmark/results/perf_current_wound_illum3_paired_20261003/native-illum3.json @@ -0,0 +1,61 @@ +{ + "startup_seconds": 0.9386949227191508, + "environment": { + "python_executable": "/home/ts/code/projects/openhcs/.venv-cellprofiler39/bin/python", + "python_version": "3.9.25", + "cellprofiler_version": "4.2.8.1", + "cellprofiler_core_version": "4.2.8.1", + "numpy_version": "1.24.4", + "scipy_version": "1.9.0", + "temporary_root": "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleIlluminationCorrection_Example3/native_tmp" + }, + "request": { + "pipeline_path": "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleIlluminationCorrection_Example3/preparation/native_cellprofiler_headless/ExampleIlluminationCorrection_Example3.cppipe", + "input_dir": "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images", + "output_root": "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleIlluminationCorrection_Example3/outputs", + "expected_image_sets": 1, + "repetitions": 2, + "file_list_path": "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleIlluminationCorrection_Example3/preparation/native_cellprofiler_file_list.txt", + "first_image_set": 1, + "last_image_set": null, + "report_path": "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleIlluminationCorrection_Example3/native_report.json", + "start_barrier_root": null, + "start_barrier_job_count": 1, + "start_barrier_job_index": 0 + }, + "observations": [ + { + "repetition": -1, + "output_root": "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleIlluminationCorrection_Example3/outputs/-1", + "image_set_count": 1, + "invocation_seconds": 1.3667851807549596, + "pre_first_module_seconds": 0.2952348357066512, + "first_module_through_post_run_seconds": 1.0715503450483084, + "invocation_started_monotonic_seconds": 3808405.732912145, + "first_module_started_monotonic_seconds": 3808406.028146981, + "completed_monotonic_seconds": 3808407.099697326 + }, + { + "repetition": 0, + "output_root": "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleIlluminationCorrection_Example3/outputs/0", + "image_set_count": 1, + "invocation_seconds": 0.6704615103080869, + "pre_first_module_seconds": 0.07343892008066177, + "first_module_through_post_run_seconds": 0.5970225902274251, + "invocation_started_monotonic_seconds": 3808407.099707816, + "first_module_started_monotonic_seconds": 3808407.173146736, + "completed_monotonic_seconds": 3808407.770169326 + }, + { + "repetition": 1, + "output_root": "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleIlluminationCorrection_Example3/outputs/1", + "image_set_count": 1, + "invocation_seconds": 0.7205826421268284, + "pre_first_module_seconds": 0.06369220139458776, + "first_module_through_post_run_seconds": 0.6568904407322407, + "invocation_started_monotonic_seconds": 3808407.770178504, + "first_module_started_monotonic_seconds": 3808407.833870705, + "completed_monotonic_seconds": 3808408.490761146 + } + ] +} \ No newline at end of file diff --git a/benchmark/results/perf_current_wound_illum3_paired_20261003/native-wound.json b/benchmark/results/perf_current_wound_illum3_paired_20261003/native-wound.json new file mode 100644 index 0000000000..13b0a47072 --- /dev/null +++ b/benchmark/results/perf_current_wound_illum3_paired_20261003/native-wound.json @@ -0,0 +1,61 @@ +{ + "startup_seconds": 0.8074899800121784, + "environment": { + "python_executable": "/home/ts/code/projects/openhcs/.venv-cellprofiler39/bin/python", + "python_version": "3.9.25", + "cellprofiler_version": "4.2.8.1", + "cellprofiler_core_version": "4.2.8.1", + "numpy_version": "1.24.4", + "scipy_version": "1.9.0", + "temporary_root": "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleWoundHealing/native_tmp" + }, + "request": { + "pipeline_path": "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleWoundHealing/preparation/native_cellprofiler_headless/ExampleWoundHealing.cppipe", + "input_dir": "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleWoundHealing/images", + "output_root": "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleWoundHealing/outputs", + "expected_image_sets": 2, + "repetitions": 2, + "file_list_path": "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleWoundHealing/preparation/native_cellprofiler_file_list.txt", + "first_image_set": 1, + "last_image_set": null, + "report_path": "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleWoundHealing/native_report.json", + "start_barrier_root": null, + "start_barrier_job_count": 1, + "start_barrier_job_index": 0 + }, + "observations": [ + { + "repetition": -1, + "output_root": "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleWoundHealing/outputs/-1", + "image_set_count": 2, + "invocation_seconds": 5.988651636987925, + "pre_first_module_seconds": 0.3156425580382347, + "first_module_through_post_run_seconds": 5.67300907894969, + "invocation_started_monotonic_seconds": 3808416.643831191, + "first_module_started_monotonic_seconds": 3808416.959473749, + "completed_monotonic_seconds": 3808422.632482828 + }, + { + "repetition": 0, + "output_root": "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleWoundHealing/outputs/0", + "image_set_count": 2, + "invocation_seconds": 4.462228200864047, + "pre_first_module_seconds": 0.08501017466187477, + "first_module_through_post_run_seconds": 4.377218026202172, + "invocation_started_monotonic_seconds": 3808422.632492666, + "first_module_started_monotonic_seconds": 3808422.717502841, + "completed_monotonic_seconds": 3808427.094720867 + }, + { + "repetition": 1, + "output_root": "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleWoundHealing/outputs/1", + "image_set_count": 2, + "invocation_seconds": 4.201918839942664, + "pre_first_module_seconds": 0.07247747993096709, + "first_module_through_post_run_seconds": 4.129441360011697, + "invocation_started_monotonic_seconds": 3808427.094730606, + "first_module_started_monotonic_seconds": 3808427.167208086, + "completed_monotonic_seconds": 3808431.296649446 + } + ] +} \ No newline at end of file diff --git a/benchmark/results/perf_current_wound_illum3_paired_20261003/ordinary-0.csv b/benchmark/results/perf_current_wound_illum3_paired_20261003/ordinary-0.csv new file mode 100644 index 0000000000..84f26f5306 --- /dev/null +++ b/benchmark/results/perf_current_wound_illum3_paired_20261003/ordinary-0.csv @@ -0,0 +1,3 @@ +case_name,mode_name,worker_count,well_count,compile_seconds,prepare_seconds,execute_seconds,total_seconds,wells_per_second,successful_wells,native_single_sample_execution_seconds,projected_native_execution_seconds,projected_execution_speedup,peak_memory_mb,status,memory_limit_mb,error_message,execution_route,server_lifecycle,run_input_sha256 +ExampleIlluminationCorrection_Example3,1w_1t,1,1,0.5973958969116211,0.0,0.6867749691009521,1.7173079238273203,1.4560810236125619,1,,,,1266.88671875,success,,,ordinary-zmq-outcomes-v1,reused-per-sweep,7b9e90e243383cc67d48359b943511e18cedb23e386c92cd2bea0d2257f35d50 +ExampleWoundHealing,1w_1t,1,1,0.34695887565612793,0.0,2.856407642364502,3.471247054170817,0.3500900869919993,1,,,,1893.171875,success,,,ordinary-zmq-outcomes-v1,reused-per-sweep,7b9e90e243383cc67d48359b943511e18cedb23e386c92cd2bea0d2257f35d50 diff --git a/benchmark/results/perf_current_wound_illum3_paired_20261003/ordinary-1.csv b/benchmark/results/perf_current_wound_illum3_paired_20261003/ordinary-1.csv new file mode 100644 index 0000000000..9cc7a096b9 --- /dev/null +++ b/benchmark/results/perf_current_wound_illum3_paired_20261003/ordinary-1.csv @@ -0,0 +1,3 @@ +case_name,mode_name,worker_count,well_count,compile_seconds,prepare_seconds,execute_seconds,total_seconds,wells_per_second,successful_wells,native_single_sample_execution_seconds,projected_native_execution_seconds,projected_execution_speedup,peak_memory_mb,status,memory_limit_mb,error_message,execution_route,server_lifecycle,run_input_sha256 +ExampleIlluminationCorrection_Example3,1w_1t,1,1,0.5914797782897949,0.0,0.38519740104675293,1.3618650771677494,2.596071513677285,1,,,,1025.0546875,success,,,ordinary-zmq-outcomes-v1,reused-per-sweep,7b9e90e243383cc67d48359b943511e18cedb23e386c92cd2bea0d2257f35d50 +ExampleWoundHealing,1w_1t,1,1,0.34340977668762207,0.0,3.1452507972717285,3.7725879549980164,0.3179396698245576,1,,,,1664.71484375,success,,,ordinary-zmq-outcomes-v1,reused-per-sweep,7b9e90e243383cc67d48359b943511e18cedb23e386c92cd2bea0d2257f35d50 diff --git a/benchmark/results/perf_current_wound_illum3_paired_20261003/qualification.json b/benchmark/results/perf_current_wound_illum3_paired_20261003/qualification.json new file mode 100644 index 0000000000..b80759f54b --- /dev/null +++ b/benchmark/results/perf_current_wound_illum3_paired_20261003/qualification.json @@ -0,0 +1,231 @@ +{ + "status": "SAVED_HASHES_AND_NONVACUITY_VERIFIED_FULL_SCIENCE_JOIN_PASS", + "source_head": "293e04acb53156a0dd9ae71476703e24f66b196c", + "source_freeze_sha256": "7719484a567283a9427cd9a3742b8ca4f25b26c0678f2d8858073d5666adb284", + "source_environment_scope": "Authoritative before/after controller freeze and terminal release joined; live checkout/env after release not asserted identical.", + "environment_sha256": "18b33cab3a61c45a940312d5ea4e1e108bc5d57261a520f3d9c5058db3ef3776", + "source_input_hashes": { + "/home/ts/code/projects/openhcs/benchmark/reference_exports/official30_value_completion_20260914/ExampleIlluminationCorrection_Example3.cppipe": "f7ec9eb561730cc567986e6a50504a69f23a3ca78a1a033faba0b2fea52759fb", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/--W00001--P00001--Z00000--T00000--cherry.tif": "f073bc8f8102f05e6443226fcb986e7d30b3f9f0d7cd77d25c7c9f82ee11b782", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/ADSAStaphInfection2_A01_w2247376DD-6ADD-442D-AE47-F54A05F3EA94.tif": "5a57b7557bb0da5e241b48851b82d936c7ec36ede7aaca5e77b28ac4bfebf502", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O01f00d2.TIF": "5bfdeeb9e1dba87dc55220f71168af3d9949f38b8d59ba741b546619c2a49fd3", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O01f01d2.TIF": "8ddf231a2727fe0dd5e80d1747a968ecc67438def175dd2b4a5850bf54699c2b", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O01f02d2.TIF": "5a7bf7ff3504baaf787f004e2137059ea2ca890a37842df5360a035b78e49940", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O02f00d2.TIF": "57f0a36092341c746441c837c78b103f35ef6460a71df77f5ae9ee74cd82ad4e", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O02f01d2.TIF": "d1b6057f138d897c3dba0d055e90e17ca5239523264c138c863bd2c4c7e453fa", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O02f02d2.TIF": "4b52dc59a87b5230d2e8701971d42df4f0e96966cf1308ce6eb7dc4865c1000e", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O03f00d2.TIF": "269e5ab417a616ef55679321f56ee99e870abe4e519c5c24c911b39b1527e7fd", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O03f01d2.TIF": "0e95bb9ab0feeab2d0ddd1a5cc877475b5c8001ae4873b9ebcaaa44107acec78", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O03f02d2.TIF": "a26dbf0381c2404bb0f495e9c68d7f2ec8fa87eae6edc7c95b5a3fc55f548a97", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O04f00d2.TIF": "fce63090cdf6281b51ac538d7af3840610f2e275af45d1cf8dd59ee2bf92377a", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O04f01d2.TIF": "e27fd485ee359490f13efa73bf7bbaef2c6743ecadba05d21a22d156ed9b892e", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O04f02d2.TIF": "fa078664928f35b7b0d1a3a971e39602bbb775f78e9001c3dcd81e392c570bf5", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O05f00d2.TIF": "cb80c657237e9cbdadba93d49d9a10cf35531ea2cb7246c5cf15d6cac1a30651", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O05f01d2.TIF": "6035bce54914bc2ce236dd1c37eda3dbeb83752ad6c3c3746e57ae13041ec868", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O05f02d2.TIF": "64e64f69f7b6abd5ed2db49b1a39dcf4036706a2a7cebef48a5188c41e0598e2", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O06f00d2.TIF": "076a52b3f4e257d18f45b01a61bfeeac112531fb5036890f2f2aa44f2ea36fdf", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O06f01d2.TIF": "72e69058831189fb1e799209c6d8bfd90d52e2736d96352ecca08735238c8384", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O06f02d2.TIF": "3c1a864005429908707a07a7f26debc838b9fff9328953a2838b7b629b305d85", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O07f00d2.TIF": "e4adc0b64624c1e83deca015415473d77f323ed7d6cad0322472874cb32b2aa3", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O07f01d2.TIF": "ce3310b92a870a24abc9e81319d319fef07a6fe6a99961c174b1bc06bb65fc47", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O07f02d2.TIF": "0ada0de31dec60745bece90d3d67c5ca5dbec6fa0532d61afe621602eef95543", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O08f00d2.TIF": "ff903c458dbfdbdd076952533aa1f3b17aa71d6b2add8cb79ead0f1649d8a189", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O08f01d2.TIF": "0f3c0748ed5ec06b7e95e411a7744571e3338e3c37ff5b0336d707db997e939c", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O08f02d2.TIF": "97f926715e7ee9743e7b382808486de24a0b386db6712cf9b38a012fcb5b3e0e", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O09f00d2.TIF": "c60dd32928613a0905306b3697279138828520bc8a9420e662dbe79799cba89c", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O09f01d2.TIF": "917e37b5cd8d60c9e2caa89472246fb94d1c51f1a350554556cf826fae276437", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O09f02d2.TIF": "7f3750dd2ea324983f4ab1fcac59183a410267d1f8221f2963f7e74d19646450", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O10f00d2.TIF": "fe52a74223e5a46d3d2b434d7dacc3d2517f2c5b10f0d04b7a3cbf7fbf41e63d", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O10f01d2.TIF": "fc325b8baff83f25402f8398760df8b2bb72425a98077ede74fa05446b22ac88", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O10f02d2.TIF": "b763c7dc7a8a539f2eaa52517c60c39b0673b9777c4642555ba86b5774d6b0fd", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O11f00d2.TIF": "3f05a9e77c8cdf6c5c29c0af26a147bcc63c110bf0bc438abc27497501cfeda6", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O11f01d2.TIF": "93e08b9408c31d4317b2ef2db997c4df3efa7ae8d8d32738cb0ec0c5c9ec1d1f", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O11f02d2.TIF": "25304455c0b64544bcf09aed892c61cd37c3f6535db2447741f0fd0f08eea27c", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O12f00d2.TIF": "6876b895933a84d9dbe7d94e0db1862fdc19ae006a9be1ed76c0e1ec390a95cb", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O12f01d2.TIF": "b29ea362fcf4f1276977f5e09804b3caeca1dba4c030fa285ce69fb7f26b6044", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O12f02d2.TIF": "e086c6b414fa5345577bb9d1cbcfcfdbd183e639e9039b50f1c08b5b67a00f36", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O13f00d2.TIF": "cae644c4b99556fccddbab42098f8dbdaa723996c521557f3b59caf3430b82d7", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O13f01d2.TIF": "17e88f342e33075b08338cb191df919c85eb3a5fa8efb3ddfcae57e69e776be3", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O13f02d2.TIF": "7bfe66d604eeb2b996e368f6a0e36891f2dd0d6ba5d819b4104614d0f0236bd2", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O14f00d2.TIF": "844889a709a3326137fd67171c0b8ec018b397fe1ede836a9b7e5809e075f58d", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O14f01d2.TIF": "968ea530f2a3aab735664fff7936ebc4e406a7112fd84b89d92431ba27178c96", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O14f02d2.TIF": "5193e9ba72e56eaaff791c214c6be34766522b6aaab383ad8e9893f0734e60c3", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O15f00d2.TIF": "488149e5f50630d794c6c57340df6271a693035bbfa46142ed6422cbfa64c8ae", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O15f01d2.TIF": "3278f9402a8110bb622b2a8c62b8087d19b35736ae9a9e6361e675af67ecc13f", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O15f02d2.TIF": "25a40d29b3e2525c976b2288013bdbeb0a2eee7026f172d100586ff9a51ce348", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O16f00d2.TIF": "16fe3c9674e55150497d1d573ccc262419314ae62acb1c76dca858c40f72f744", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O16f01d2.TIF": "69133d455b367c919d5f2bcde19ab2c6223ef57b06d2c3011393e8c9f1a709c9", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O16f02d2.TIF": "b0c7dbcf259ad76e9fccb6de614b5d15f542317bb95c97a85233fbdcb73c9de3", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O17f00d2.TIF": "d049efceda2aea79923d4b8df69de9144ba104c77a06bf168184830cc30f30d4", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O17f01d2.TIF": "bae6ec7694d0868c74c079f5cd24534542f2835d4cab41ed8d82586c662ce4d1", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O17f02d2.TIF": "c991d219a21e6f1274b01bb6f370c4fb44addb122c1e292c36acd33def0787e2", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O18f00d2.TIF": "15e3dde0bdcd792bde186c6576cbb45d64723fb50b5d4c1841ad9ce3c6386f2d", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O18f01d2.TIF": "cf8e231352f5bf3f0b63aa29cf72a21c90a15ad2233e8495068a778399a056d0", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O18f02d2.TIF": "f67d7dba8a06dea16da609d350a98647d181c7b175109b940c74627e4c7fc4fd", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O19f00d2.TIF": "c370003080a53449570d1d780b9dd84626bd3cfa49c80c6db0ccdb493cbf6a63", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O19f01d2.TIF": "1a5b7ad836b7f86e187c52eb4e9f76dbc71b2057bf6ab8ff57d676314811a930", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O19f02d2.TIF": "e8e3774e2ad39eeb9dcfb25cb92d7c75c022f86de5fab772a36e60a332cc7cf1", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O20f00d2.TIF": "99cf977fccdc4cbdb1ba8a1a2992dbf4d1c26b7f6d822a18d0db323e842ace8e", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O20f01d2.TIF": "6847c82a3ea55a29c1f55c7dcf251e5b8a776eb207107f41aaabf3978a855c58", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O20f02d2.TIF": "2e0059c518aa222960520be791721a52837a948d22bf1d2fb4000b37ec89e881", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O21f00d2.TIF": "26fb12cc1228982b372da290eabe06897521db386800aca9518535fead716870", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O21f01d2.TIF": "1c59f7447309b5854ebcac27ed3cd10c564894b4cdb6510865b447bca4029c95", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O21f02d2.TIF": "9c0d1062e13d193427eb146b882e19c54ccbbba531df786d797e7d13756198b0", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O22f00d2.TIF": "fafa8607d475c46e645433036df14b58ec9ba7c850ef5ff37b8253bd8ea67f05", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O22f01d2.TIF": "2183fc92523891136905acf96fc2bdad04e00657f8070911ac3d4c1950ebf822", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O22f02d2.TIF": "5684cec26114f2813f41d4c42ba2bbe982b01bdcad78d80d73cfb78e07f9c790", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O23f00d2.TIF": "7e2420883575733a3d759752d633df10eec665b9d0575173063365366320a8b7", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O23f01d2.TIF": "f61467aadaf8e4c92cab7daff2cf5cf99b87783d5a055f69d9043c8b9b958554", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O23f02d2.TIF": "0a0839f901f76e8f11e85c94af74e0c8f1df302cc7fd88cd7429e83e694329c7", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O24f00d2.TIF": "ecd17866fa382365023790c9c874255e9248ce77fa374abfaec73a62f7cb898d", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O24f01d2.TIF": "8ac212dcc8b92fb953bd4a17f2b6a49a9b824c521b5c0fb62bfafacadd03a44d", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O24f02d2.TIF": "adf56aa932a3e04f19e5eb2b0f5d85a15b52fa2ffa0a0e57b6754c6de818eee0", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleWoundHealing/ExampleWoundHealing.cppipe": "61da2bdd29daec73813645fef978fe237ecd92229433106d80e9bb0c65a3c8e6", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleWoundHealing/images/DMSO_B5_t0.JPG": "1d91c175dd7aa433e63d218b9d352e6065c5c5c888631366f6d0716fa384f642", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleWoundHealing/images/DMSO_B5_t24.JPG": "fc83c19de66538f1619728cd52930b867fb87bbcfa39d85ab9838eae7e097198" + }, + "helpers": { + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-preparation-v1/controller.py": "0b7abed65ecc3d61761533a3a9a69e9b9a8acb220049175ff46fb5f004ade4aa", + "/var/tmp/run_narrow450_runtime_phase_probe_20261002.py": "93a4b1720d3d2c64e77b722c91597be42c7c9968bad42a4503e5588fc7fc4a55", + "/var/tmp/openhcs_measurement_sparse_inventory_v2_20261003.py": "481d6ee99045090e6dac0e1d287a70cd33698575e06375780c123c66ed0bf52b", + "/tmp/run_integrated_fields_ordinary_abba_20261002.py": "8a41db448a722d231777a90a3ae23091617c8ac1ba72cefcb91f72b2c7975554" + }, + "native_binaries": { + "openhcs/core/_tabular_native.abi3.so": "2ce348417de70ad3f614962c82aed9c4977c32b3ba00e13531ecafb8c10d8d1d", + "openhcs/processing/backends/cellprofiler/_granularity_native.abi3.so": "6e45987a50d5c0588a8fb0b09a2232a63e72c122d6a3d583ae4d62830e3b4096" + }, + "strict_science_sha256": "3d89dc4daaceb34535ceaab05100b80b88487e32e67180a99595035b66c67236", + "release_sha256": "718c3806e7d77bc7762268033e1fae6e5491db177f43bc29ff0e314b5b8e50f5", + "all_handles_terminal": { + "prepare": 65780, + "ordinary": 31694, + "native": 10409, + "compare": 44398 + }, + "retained_scientific_comparisons": 8, + "nonvacuity": [ + { + "case": "ExampleIlluminationCorrection_Example3", + "ordinary_repeat": 0, + "native_repeat": 0, + "declared_image_count": 2, + "pixels_per_declared_comparison": [ + 213443, + 213443 + ] + }, + { + "case": "ExampleIlluminationCorrection_Example3", + "ordinary_repeat": 0, + "native_repeat": 1, + "declared_image_count": 2, + "pixels_per_declared_comparison": [ + 213443, + 213443 + ] + }, + { + "case": "ExampleWoundHealing", + "ordinary_repeat": 0, + "native_repeat": 0, + "participating_tables_per_side": 1 + }, + { + "case": "ExampleWoundHealing", + "ordinary_repeat": 0, + "native_repeat": 1, + "participating_tables_per_side": 1 + }, + { + "case": "ExampleIlluminationCorrection_Example3", + "ordinary_repeat": 1, + "native_repeat": 0, + "declared_image_count": 2, + "pixels_per_declared_comparison": [ + 213443, + 213443 + ] + }, + { + "case": "ExampleIlluminationCorrection_Example3", + "ordinary_repeat": 1, + "native_repeat": 1, + "declared_image_count": 2, + "pixels_per_declared_comparison": [ + 213443, + 213443 + ] + }, + { + "case": "ExampleWoundHealing", + "ordinary_repeat": 1, + "native_repeat": 0, + "participating_tables_per_side": 1 + }, + { + "case": "ExampleWoundHealing", + "ordinary_repeat": 1, + "native_repeat": 1, + "participating_tables_per_side": 1 + } + ], + "output_role_hashes": { + "ExampleIlluminationCorrection_Example3/ordinary0": { + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/ordinary-0/ExampleIlluminationCorrection_Example3/wells_1/workers_1/images_ExampleIlluminationCorrection_Example3_source_workspace_well_throughput/openhcs_metadata.json": "579ac084b396dff5f7fbaa844139bf952c2576b6cb0242772c925ba9839c2267", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/ordinary-0/ExampleIlluminationCorrection_Example3/wells_1/workers_1/images_ExampleIlluminationCorrection_Example3_source_workspace_well_throughput/openhcs_metadata.json.lock": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/ordinary-0/ExampleIlluminationCorrection_Example3/wells_1/workers_1/images_ExampleIlluminationCorrection_Example3_source_workspace_well_throughput/results/reference_image__ConvexHullCorrWorm.npy": "9ba1259e4ca007c06f14be6946e32a86307ec2dfb9952b73fa2cc253ccae8b3a", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/ordinary-0/ExampleIlluminationCorrection_Example3/wells_1/workers_1/images_ExampleIlluminationCorrection_Example3_source_workspace_well_throughput/results/reference_image__PolynomialCorrected.npy": "8875da63a072c8f2de4f5cb707d779a064b663bf69aa0f304fe6fa74d4ff57a6" + }, + "ExampleIlluminationCorrection_Example3/ordinary1": { + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/ordinary-1/ExampleIlluminationCorrection_Example3/wells_1/workers_1/images_ExampleIlluminationCorrection_Example3_source_workspace_well_throughput/openhcs_metadata.json": "579ac084b396dff5f7fbaa844139bf952c2576b6cb0242772c925ba9839c2267", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/ordinary-1/ExampleIlluminationCorrection_Example3/wells_1/workers_1/images_ExampleIlluminationCorrection_Example3_source_workspace_well_throughput/openhcs_metadata.json.lock": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/ordinary-1/ExampleIlluminationCorrection_Example3/wells_1/workers_1/images_ExampleIlluminationCorrection_Example3_source_workspace_well_throughput/results/reference_image__ConvexHullCorrWorm.npy": "9ba1259e4ca007c06f14be6946e32a86307ec2dfb9952b73fa2cc253ccae8b3a", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/ordinary-1/ExampleIlluminationCorrection_Example3/wells_1/workers_1/images_ExampleIlluminationCorrection_Example3_source_workspace_well_throughput/results/reference_image__PolynomialCorrected.npy": "8875da63a072c8f2de4f5cb707d779a064b663bf69aa0f304fe6fa74d4ff57a6" + }, + "ExampleIlluminationCorrection_Example3/native-1": { + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleIlluminationCorrection_Example3/outputs/-1/reference_image__ConvexHullCorrWorm.npy": "9ba1259e4ca007c06f14be6946e32a86307ec2dfb9952b73fa2cc253ccae8b3a", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleIlluminationCorrection_Example3/outputs/-1/reference_image__PolynomialCorrected.npy": "588e6f9951aa7ad220678dbb0053e98fb6d324e71aab089d1d828d891ee32f54" + }, + "ExampleIlluminationCorrection_Example3/native0": { + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleIlluminationCorrection_Example3/outputs/0/reference_image__ConvexHullCorrWorm.npy": "9ba1259e4ca007c06f14be6946e32a86307ec2dfb9952b73fa2cc253ccae8b3a", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleIlluminationCorrection_Example3/outputs/0/reference_image__PolynomialCorrected.npy": "588e6f9951aa7ad220678dbb0053e98fb6d324e71aab089d1d828d891ee32f54" + }, + "ExampleIlluminationCorrection_Example3/native1": { + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleIlluminationCorrection_Example3/outputs/1/reference_image__ConvexHullCorrWorm.npy": "9ba1259e4ca007c06f14be6946e32a86307ec2dfb9952b73fa2cc253ccae8b3a", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleIlluminationCorrection_Example3/outputs/1/reference_image__PolynomialCorrected.npy": "588e6f9951aa7ad220678dbb0053e98fb6d324e71aab089d1d828d891ee32f54" + }, + "ExampleWoundHealing/ordinary0": { + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/ordinary-0/ExampleWoundHealing/wells_1/workers_1/images_ExampleWoundHealing_source_workspace_well_throughput/openhcs_metadata.json": "71582c62cabb2571ebf8a0de57f92fdf82c55a7ece3dac0bf2a6eca95cad84c9", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/ordinary-0/ExampleWoundHealing/wells_1/workers_1/images_ExampleWoundHealing_source_workspace_well_throughput/openhcs_metadata.json.lock": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/ordinary-0/ExampleWoundHealing/wells_1/workers_1/images_ExampleWoundHealing_source_workspace_well_throughput/results/Image.csv": "939735fa17a067137fea2adc77d57f9ce1df24bfcb6588c49023de1cc3998677" + }, + "ExampleWoundHealing/ordinary1": { + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/ordinary-1/ExampleWoundHealing/wells_1/workers_1/images_ExampleWoundHealing_source_workspace_well_throughput/openhcs_metadata.json": "71582c62cabb2571ebf8a0de57f92fdf82c55a7ece3dac0bf2a6eca95cad84c9", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/ordinary-1/ExampleWoundHealing/wells_1/workers_1/images_ExampleWoundHealing_source_workspace_well_throughput/openhcs_metadata.json.lock": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/ordinary-1/ExampleWoundHealing/wells_1/workers_1/images_ExampleWoundHealing_source_workspace_well_throughput/results/Image.csv": "939735fa17a067137fea2adc77d57f9ce1df24bfcb6588c49023de1cc3998677" + }, + "ExampleWoundHealing/native-1": { + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleWoundHealing/outputs/-1/Image.csv": "368b4c00672ff296d0a72ee7b2dc6ff4da56f35da3bed14ec62afba78f9c6c9b" + }, + "ExampleWoundHealing/native0": { + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleWoundHealing/outputs/0/Image.csv": "497700eb62ed0a557b902968c0fdb444eca8b748a772d8234f241c84f5dfad87" + }, + "ExampleWoundHealing/native1": { + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleWoundHealing/outputs/1/Image.csv": "373f02427a4758051bfdd80efd7071774d8adcae60a2928c6d3bb67ad993155c" + } + }, + "original_matched_output_hash_count": 20, + "figure_owner_sha256": "bf4132cabafca6f7e1d277d73ac8cbfd4f30141dcb1b6981bc5fc451da43725e", + "plot_script_sha256": "7972986db6f16f48094d078be56b5fc89e0b020b2bda9b65a14bb267f9045b2b", + "figure_artifacts_sha256": { + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/figures-v1/README.md": "c6eff275dab63534b4b555904f82098ea1e343486f7f5bc422b9743e0898b204", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/figures-v1/matched_speedup.png": "73b72fa7e2aebec57029f421d081a47261fd7eadf18f6f40705aa4668e136d90", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/figures-v1/metrics.csv": "df153bc46af1b2e9c1c8e56e18a8a6f48032eecda90000413536eab92e0e99b5", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/figures-v1/matched_speedup.svg": "6fe3c9f639515d615ff2feb0acf4bd184f54911e273c2cd68ce5f2c34d23d426", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/figures-v1/matched_runtime.svg": "a65fe6ba36b2b120e5dd919ce9e154226469171a85ea156b95eae78448c932cc", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/figures-v1/timing_summary.json": "0cc2fa4e4331e97026d46217cc7fa470773cae20784169355a0459398d81662e", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/figures-v1/matched_runtime.png": "2289b2d27c7c044da4057afc0cd15297377be5526e83e539603847262321ea91", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/figures-v1/raw-observations.csv": "04f92f2af7a45ab8838868d1460b1d2332bcb325b8f1d2c7b3ac46bdb34db889" + }, + "scope": "Independent saved file/input/helper/native hash and nonvacuity verification; original numerical/full-inventory/correlation comparison results retained unchanged, not a new comparator run. Warmup files hash-preserved but not counted as measured science comparisons. No pipeline/source/env edits. No Wound per-image division." +} diff --git a/benchmark/results/perf_current_wound_illum3_paired_20261003/raw-observations.csv b/benchmark/results/perf_current_wound_illum3_paired_20261003/raw-observations.csv new file mode 100644 index 0000000000..30788dcb9d --- /dev/null +++ b/benchmark/results/perf_current_wound_illum3_paired_20261003/raw-observations.csv @@ -0,0 +1,9 @@ +case,method,repeat,compile_seconds,execute_seconds,total_seconds,native_invocation_seconds,image_sets +ExampleIlluminationCorrection_Example3,OpenHCS,0,0.5973958969116211,0.6867749691009521,1.7173079238273203,,1 +ExampleIlluminationCorrection_Example3,OpenHCS,1,0.5914797782897949,0.38519740104675293,1.3618650771677494,,1 +ExampleIlluminationCorrection_Example3,Native CP,0,,0.5970225902274251,,0.6704615103080869,1 +ExampleIlluminationCorrection_Example3,Native CP,1,,0.6568904407322407,,0.7205826421268284,1 +ExampleWoundHealing,OpenHCS,0,0.34695887565612793,2.856407642364502,3.471247054170817,,2 +ExampleWoundHealing,OpenHCS,1,0.34340977668762207,3.1452507972717285,3.7725879549980164,,2 +ExampleWoundHealing,Native CP,0,,4.377218026202172,,4.462228200864047,2 +ExampleWoundHealing,Native CP,1,,4.129441360011697,,4.201918839942664,2 diff --git a/benchmark/results/perf_current_wound_illum3_paired_20261003/source-freeze.json b/benchmark/results/perf_current_wound_illum3_paired_20261003/source-freeze.json new file mode 100644 index 0000000000..fea7d563c6 --- /dev/null +++ b/benchmark/results/perf_current_wound_illum3_paired_20261003/source-freeze.json @@ -0,0 +1,8607 @@ +{ + "source_state": { + "head": "293e04acb53156a0dd9ae71476703e24f66b196c", + "status": "", + "origin_main": "f7de9efad393bce83a3756a271f0cd4383783ea9", + "tracked": { + ".coveragerc": "5c9dbf297df8f44b2a4750224108ca116f67fc0ce1d6fa6ca51674b4928a3dd4", + ".github/dependabot.yml": "34790e08d06d30f3f06cf2f071fcb2430df398c0a70bf1d99b5cca71d69080ef", + ".github/pull_request_template.md": "fe05b94ba8fa1d566e4463a32fe938c0af6a20c50c373bf77e93fca8a77b905a", + ".github/tests/test_refactor_guardrails.py": "7d9b663b1801754fe934b76ae5c740c5355b34ec7bfa8ebac55762b236ee118e", + ".github/workflows/coverage-pages.yml": "1bc15d59c10cd3dc40c9d803facf81a4ac6c5bc9de2634a94f0ceef97d3314a9", + ".github/workflows/docs.yml": "b36d0a763034b127e99ba1bdd3061e833e933b861e2c81ec34098db0a17c19c7", + ".github/workflows/integration-tests.yml": "a85b32502b58df220e5f7c42cf6e876b6d970c2c5beb419b8d88bb3977d45b6d", + ".github/workflows/publish.yml": "03cda04e49ecc3ab34014e4c440e3f376b13df4b53e6952887fa05b7d69d705b", + ".github/workflows/published-release-canary.yml": "b5b377bf774fd8c19045e7708279d062b3e8da1738b809a8adb79c79a33c1cf0", + ".github/workflows/refactor-guardrails.yml": "47f3eabe448382c8529fb3920a06b07ec6cec90e485a8bb1c29c1f99ee87a3d2", + ".github/workflows/website-pages.yml": "94fd5634ace8d7f138446a120acb49de27d0ea7230b09b1e958ef8b8fcd85b37", + ".gitignore": "13c945f83cd74fbb6205745658ccba4c3bd559c080795a45ffb73851e02f6f75", + ".gitmodules": "b70034bf0b9661f2528f002e08c03042d6cece29de79bd96c11d2d5a943684bd", + ".readthedocs.yaml": "bd5346a882747235c8202c2de4ecfc8c9902accc1cf1534a55d1d1c7a93c373c", + ".vscode/settings.json": "3e7bf4823a1a681cc4162d6f78c91e0d36b2472ddbcda3417750c29b0023a853", + "CHANGELOG.md": "855d7bc59dcd8ef0cb352c33c88710f03c12c833af10867411a14c6ea45d571e", + "LICENSE": "1fe81c33f0cdfbcea8ce2fa4bf5cc98604be54a954ff7c59cea0ff02e3c15741", + "MANIFEST.in": "f54ae206d1d86e914964233d7d3ae1535a4740861257ca9413c24615d4cc5fa2", + "Makefile": "a5b8e41d8bc34948a87649ea12bb1f89c683fafb53a109da6a3f6394d77c7155", + "README.md": "1a600010deb7ddc3fe141b5efe3528bbe4a4e119846157e935e56784628445ed", + "THIRD_PARTY_LICENSES/human-eval-bia-MIT.txt": "d9835c14c2464401664c33f9458254b23fe3ee650e6a54b954338466518b8708", + "THIRD_PARTY_LICENSES/napari-roi-manager-LICENSE": "b6f986100ba22febd16871abdc9af7d0e7b3ae730131bb14fb9589c2acc9caa6", + "benchmark/__init__.py": "802082be754468008c44f86a8a4df7de65eb44280919daace363d04519e45f93", + "benchmark/adapters/__init__.py": "aa93075a5770d135a44c207807cc75c4309e630101bc3d13ef7a75a915151157", + "benchmark/adapters/cellprofiler.py": "6bcbbf744efa539bce2323d484085282fbfb823878ed7e47e7f1250d0d110c01", + "benchmark/adapters/cellprofiler_installation.py": "36ea8b73a88666621430f02dcabd38a502ab202062b667c9c70dd4a2baa1860a", + "benchmark/adapters/cppipe_source.py": "59a317cf302d859445fecb63c39fe5ef5d0ffbac6f849127759967c264ce9316", + "benchmark/adapters/openhcs.py": "21a49c342047a8d8afe7e6937c7dafb97a42fe3038b207a9e67e4459e688a2cc", + "benchmark/agent_validation/OPERATOR_GUIDE.md": "2b2ef7d0dd070e03f8a33bfd8bc0257087e3090c5de02477deef2ebee8d2401f", + "benchmark/agent_validation/__init__.py": "835f7c1cd162b17c5c0095fefb52ff0f7dc696f6d4cd0978b7b08b03e9768db0", + "benchmark/agent_validation/__main__.py": "00e80d3634fe279798b64066c486e9bad165f235c10d2b45646e9e29b32f8d5a", + "benchmark/agent_validation/challenge_functions.py": "3ad0e8ae25f569889e2978738716de50f6cc37b812b0f186c97de62ef7c15540", + "benchmark/agent_validation/contracts.py": "2fb3fa8af1a5ee3f5869791980bb63bbe3e811c721bd85227bfd04f5fbd122c9", + "benchmark/agent_validation/corpus.py": "40695857621b64985221408d93a260bd2c2adbd51233d673138faddc9c1f97ba", + "benchmark/agent_validation/declarations.py": "005a26a7315ae16d1b0dce4983b06377cf351043089bef3ff43b9d1da6034b1b", + "benchmark/agent_validation/journal.py": "486c8f85c86864f5e3d36acf5b5f5c70f752942b16d395ec51a87613e8671300", + "benchmark/agent_validation/mcp_attempt_recorder.py": "b34d8b5da45d3bce7d2144295cd3dfe176a2f626f19f083c10bf2484d85567da", + "benchmark/agent_validation/perturbations.py": "f092af99eff3f40a87c92ca99fb52e690c83839a41c649fad6a5b7af919150d3", + "benchmark/agent_validation/provenance.py": "9925b8ca8d51f191c0c9f75ef50c4f129173d7a89d00efa3afa44bf6376a5d6a", + "benchmark/agent_validation/scoring.py": "5fc0aecd11283f921c0511346c15d5f7a1fb9e892ef1562045e8d6eca87e4baa", + "benchmark/agent_validation/tasks.py": "7579bc6a4e775d1041e331143854dce4f8f8aa76780d9cd25bf3c30fc8a5a921", + "benchmark/annotated_validation.py": "1db72c805e35c6a1328cf1b890bc2af55cc39c159524961187b7f8d81d6820cc", + "benchmark/annotated_validation_20260915.md": "7e7c44be5477f8356b9b93b677270d32277150ec47490803a5d035015616595b", + "benchmark/bioformats_hcs_validation.py": "5114788070a955ae099a26d81127f3f637ac50a1724008e84a1701bfcba8838f", + "benchmark/cellprofiler_benchmark_cli.py": "39272e9d32cf758dfb6e2183f899562d6fab9af3a4d8f5fbc81a434d21f464a1", + "benchmark/cellprofiler_comparison.py": "5ca39ae6db8d43c7c293cd4c5a83508cde59b06e210d1633eaf7dc79aae7f3b2", + "benchmark/cellprofiler_export_equivalence.py": "21bbb55fae8f6ddcf8fdbacba54619b79b410cbf1afda05312949797f548b31d", + "benchmark/cellprofiler_pipelines/BBBC021_analysis.cppipe": "fdb0b710daa900db417b0032b00a18aabf664aedf95a939fc2ac60aaa9097062", + "benchmark/cellprofiler_pipelines/BBBC021_illum.cppipe": "09b73e9a7eca196d0d5056a1e9a39ea21c6b5dca1e2781f767e4b45aec3b45c2", + "benchmark/cellprofiler_pipelines/ExampleFly.cppipe": "8793ac307cda12e66b1d31928894ae2f82b94477b88ba7530d786f35e43bac17", + "benchmark/cellprofiler_pipelines/ExampleHuman.cppipe": "3fc7ace9921e5591f6455ee04ba4cff3754ea70a9f8035ce5cd8eeffe74bce3f", + "benchmark/cellprofiler_reference_exports.py": "604f9a95f94eeb10365437b875a0ff3d558dbb2fda13e7e951242d417a69b49c", + "benchmark/cellprofiler_semantics/__init__.py": "a201adc7d67fd72225be4b5ef9a662380b84d6bbf222a703e41d6b7b589f478c", + "benchmark/contracts/comparison_manifest.py": "17cd75bd2d4c39bd168bed65e94b705b1899e05c606a7e660e10148f7fd18cfd", + "benchmark/contracts/control.py": "0bce24c29ea9dfb7b50c88937b616118078505db6f497312fff87766397e5ce9", + "benchmark/contracts/dataset.py": "99b3bc0456918befd92d9ac65a61fe5315fbda8b688bbe5162b67aabf298bf6f", + "benchmark/contracts/manifest_acquisition.py": "8a09c47497c234fabe526114ba7ed5ea672e6e3dde3c2d8c3911b8a29656446e", + "benchmark/contracts/measured_run_receipt.py": "67c71873d050e05c0e4a1e231c46b700a765c12cade51269553cc7a5cb96989e", + "benchmark/contracts/metric.py": "cb7f9e7afc0b8c9eed8b9770266c4554e21b8f8bffcf9a612ba23c3dd2447cc0", + "benchmark/contracts/pipeline.py": "54857ee11404bdc510478e53873b359d05f4a1a103d915626c27acfb4884946c", + "benchmark/contracts/run_artifacts.py": "e11f0d5cf699529bf2356d43628e06394cdf702119a4d303640aaae783c86f32", + "benchmark/contracts/run_receipt.py": "1704e1ab47411d25d382b95871c96c1e60c94db14b8ffd55f4dc6c8a917758b9", + "benchmark/contracts/tool_adapter.py": "6ff5923c8f7ba171e525ad56029cb28daf989dd87006d01d1b5b5b2ee46a5a58", + "benchmark/contracts/validation.py": "f23637d184c8d9722a61dd4bd8c34d2d11dbabfd3227cf02b9636e29240cc286", + "benchmark/contracts/values.py": "5bd8863dc06d1f7220412cf8db46a8d3f5a15c6d71e33d37b30f6fcecafb3969", + "benchmark/control.py": "4706078a8fbeb516a52bc51600751ae86c39c3ef7e397a2f8cf13d49204db941", + "benchmark/control_service.py": "bf2b39696a49d8c87cf74d39770d63013972b9c8cf884863c467ccdf46eb12d3", + "benchmark/converter/__init__.py": "8f439af8eced6b234ff5501b22f7633ac781cfe427518df5c108846c8834f10b", + "benchmark/converter/compatibility_matrix.py": "ba1f09a2e44e38763a6e316de80f9bda4f82a488b88343505dd9dc3e4b327361", + "benchmark/converter/convert.py": "db93f95345a3d1519415c8016df6b16975872ffb91bc52fef5e2e70a8fe6d2f0", + "benchmark/converter/cppipe_corpus.py": "aa0ffd01ca4980117c3ee18e6a7d36c7e8bc6c88f9e19a53921aaa2278c3e2e1", + "benchmark/datasets/__init__.py": "e8fa7845da316edf641ad8cda3356c0ec8619f20161a7185d932a82c45fbea56", + "benchmark/datasets/acquire.py": "d1d7f66bd78a0a5b6dd5891e47e8a324121b064be77cdbd07a93b5cbf39f6632", + "benchmark/datasets/bioformats_hcs.py": "2d45347a8f84f6908539e8fde85f1890d9fdd3c1264d868c4f6f04741aab5963", + "benchmark/datasets/cache.py": "6c5b12280dcd7cfb41c54ac5f385c6b9936628ec0e59e7c169230a0903bf6bbc", + "benchmark/datasets/cppipe_case_catalog.py": "83bd92a621a386a4e3ead9b9649a74746d3cb90678dc8a9081e9fc7303577c12", + "benchmark/datasets/manifest.py": "f3ab9ca4152d54bb29a7659a96ecf5762c08844b0f0caaf0f491807c2d924802", + "benchmark/datasets/registry.py": "b8ce320322c5d15d210ed677b9ad622fa3bb3acc01924b30937fce36ef692778", + "benchmark/datasets/visible_source.py": "c412d522c4e2a4e820497952013d246ebde73167916175bce4d77e46bf541d2c", + "benchmark/demos/__init__.py": "572160cfa8694cc155ad375b2a3a593ad9b28dbf7c88e235426dd5417ac938dd", + "benchmark/demos/official30_lab_meeting.py": "45b9391e6e0d8561770d6e7fd3ff3e85db11f24dd5bedf643875d09a8d4bacfd", + "benchmark/file_digest.py": "c0628cda351b085ab2487eec761e42fa3e38e32169d1e412a3578e68958d14a8", + "benchmark/fragmented_roi_profile.py": "556fafcab934dded426c1a85427345c0478e9d1fdcb16e086e10d3d786bd8525", + "benchmark/manifests/README.md": "c9db6e2d207001d9a6315689ae874c7c0624077b570ca2b8179876f7ce6b9482", + "benchmark/manifests/annotated_validation_20260915_preparation.json": "5a52361e4856e52aa7f634ec6a0b26d0d835949c829f3b235efaa080ae659d9e", + "benchmark/manifests/cp_tutorial_translocation_start_axis1.json": "270a20636ce740c37f5cdbb27209774bc195cf479dd57b7016aa9ee2e4199f1c", + "benchmark/manifests/extra_provided_not_in_23_axis1.json": "97faf7af8082279c733ed5f77235356f9aaf2188c5dae9ab13179fb47aed3004", + "benchmark/manifests/official23_plus_extra6_axis1.json": "5affa65ff1ec582f7bda22f35c480fb9fcb8d75cd433796874281c5433181f34", + "benchmark/manifests/official23_plus_extra7_axis1.json": "971e1ccf2ffdbffee1c6d3ffd095aa456bf76a77a859f974bb5c5f8470778928", + "benchmark/manifests/official23_plus_monolayer.json": "19af816b2736ee6d4bc9a2650a461ade1db995b057e62062548056805b0c5a03", + "benchmark/manifests/official30_existing_image_refresh_20260914.json": "938167a89704e8a195bd8cbd69e23beb5e643894cad762cd7627e70c2bce4e61", + "benchmark/manifests/official30_portable_axis1.json": "4c302e136b427cb9968eef82d7ede4a6c5b65cbcb4f8e5490fd5125c2e7ab2a8", + "benchmark/manifests/official30_value_completion_20260914.json": "39dd742855c97e49eb653ef8d410f97fb421b993b5df6911585daca7fa39af1c", + "benchmark/matched_cellprofiler_batch.py": "101114f4be7e93bbcdac86f88f39a17ab5b39533a082cebb3f5759c0dd03f3b1", + "benchmark/mcp_extension.py": "14a03b06491b011eb4a965284267cf2b825f82bb3917510348d9d402d90f542b", + "benchmark/metrics/__init__.py": "3eaba4b2c099085ded379020a6a919e75fa25774982aeb0044fa897a3f3ec952", + "benchmark/metrics/memory.py": "bc049ff424564b876b82d79f5a34f49a3d5dc7f374804b23d53eed1dd729e25e", + "benchmark/metrics/time.py": "fb1a4facd5f7a93cefd43219bc7a884d9b8cdb334c4208f8f56f71c323bd38c6", + "benchmark/napari_roi_streaming.py": "8d94caaa494e5ab33b9acf7d653183c392c772d5a57bb7158bf5c7b39c8e8ba8", + "benchmark/native_batch_barrier.py": "74ac18df75dae90c702b0c3f8f0baa1703de20d2b257b01db8507f250ec40c4e", + "benchmark/native_cellprofiler_batch_worker.py": "b377740417fbb9b78839ec792b634246a168de16736224a5991e8fb1ae70be76", + "benchmark/native_refs/README.md": "00c40407653d3a00b47f9746c13dfc7859a06768c619ce5494648ef2359f8062", + "benchmark/native_refs/official30_existing_image_refresh_20260914/CellProfiler_tutorials_cp_tutorial_translocation_final_wells_include_first1/TranslocationData_cp_tutorial_translocation_final_native_cellprofiler/.cellprofiler_benchmark_reference.json": "930c22f3c57c3ea5d2c9e1e4bf37c33217fbb74f53b4e53809aefd58e83bd2de", + "benchmark/native_refs/official30_existing_image_refresh_20260914/CellProfiler_tutorials_cp_tutorial_translocation_final_wells_include_first1/TranslocationData_cp_tutorial_translocation_final_native_cellprofiler/BBBC013_A01_s1_w1_Overlay.tiff": "a8dd29f39f592308c65f5dd20e27fdcf56292fda62a0b09ed6b4a253f3a7429e", + "benchmark/native_refs/official30_existing_image_refresh_20260914/CellProfiler_tutorials_cp_tutorial_translocation_final_wells_include_first1/TranslocationData_cp_tutorial_translocation_final_native_cellprofiler/DefaultDB.db": "ad7882394007e7413189d8e659d1f69548befad832161d58574981f6208f3573", + "benchmark/native_refs/official30_existing_image_refresh_20260914/CellProfiler_tutorials_cp_tutorial_translocation_final_wells_include_first1/TranslocationData_cp_tutorial_translocation_final_native_cellprofiler/DefaultDB.properties": "817353550c5588decd2c07942a187463ef5a802643d7d3e42c45c59fb68cb299", + "benchmark/native_refs/official30_existing_image_refresh_20260914/ExampleIlluminationCorrection_ExampleIlluminationCorrection_Example1_AllMethod_wells_include_first1/images_a015597fb7a8_ExampleIlluminationCorrection_Example1_AllMethod_native_cellprofiler/.cellprofiler_benchmark_reference.json": "4b59286575280b4fb6d7d7da4aff827a29cbecf810c9d5480795f5bfe7b69517", + "benchmark/native_refs/official30_existing_image_refresh_20260914/ExampleIlluminationCorrection_ExampleIlluminationCorrection_Example1_AllMethod_wells_include_first1/images_a015597fb7a8_ExampleIlluminationCorrection_Example1_AllMethod_native_cellprofiler/Illum.npy": "2f8490c4a6a7301d569797a80e3f9f26581fc83cc267aa436e44dc2f06cc346a", + "benchmark/native_refs/official30_scoped_rows/CellProfiler4_benchmark_supplement_cp4_supplement_combine_objects_wells_include_first1/CombineObjects_f4072628fb1a_cp4_supplement_combine_objects_native_cellprofiler/.cellprofiler_benchmark_reference.json": "5adccc651a8610352acb88288c56f2d5de7845be1e0d9813cea1c76b6e87b03a", + "benchmark/native_refs/official30_scoped_rows/CellProfiler4_benchmark_supplement_cp4_supplement_combine_objects_wells_include_first1/CombineObjects_f4072628fb1a_cp4_supplement_combine_objects_native_cellprofiler/reference_labels__CombinedObjects.tiff": "f5a96f1e11f1c9a0a3a86388f100705d99765856bc668aba675c935563e32197", + "benchmark/native_refs/official30_scoped_rows/CellProfiler4_benchmark_supplement_cp4_supplement_combine_objects_wells_include_first1/native_cellprofiler_headless/CombineObjectsDemo.cppipe": "50e67bacad558da3652c27599bbaf2c35a52c974ea9e546b36ac07b014530503", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_3d_monolayer_samples_first2wells/images_cp_tutorial_3d_monolayer_native_cellprofiler/3d_monolayer_Cells.csv": "cc34681db8a66c38ca77bc47db496b26703f21e4d7c7628dd74e30c33250859f", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_3d_monolayer_samples_first2wells/images_cp_tutorial_3d_monolayer_native_cellprofiler/3d_monolayer_Experiment.csv": "06c1d8caf3de21a47d79da63fe835857555131e067f5db2d6986d7e0c577362e", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_3d_monolayer_samples_first2wells/images_cp_tutorial_3d_monolayer_native_cellprofiler/3d_monolayer_Image.csv": "4036d5e7a2699ce804b8bf5edcc5695dc8e79f5ac6c9c12818aa42be0ef6c24b", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_3d_monolayer_samples_first2wells/images_cp_tutorial_3d_monolayer_native_cellprofiler/3d_monolayer_Nuclei.csv": "440c33edc7e8cb5611910a835454db62390301a7af617a542349f60aece57561", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_3d_monolayer_samples_first2wells/images_cp_tutorial_3d_monolayer_native_cellprofiler/3d_monolayer_downsizedNuclei.csv": "2c66bc7b17de52147d5a875403f9a9746019ac61371c08157979044c6ed818dd", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_3d_monolayer_samples_first2wells/images_cp_tutorial_3d_monolayer_native_cellprofiler/3d_monolayer_erodedDownsizedNuclei.csv": "527a0cce497d2a618478be84d1a0bf174049f58aa2a666e37626a990fdca0116", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_3d_monolayer_samples_first2wells/images_cp_tutorial_3d_monolayer_native_cellprofiler/3d_monolayer_erodedResizedNuclei.csv": "66a58f78cab0cfdbe907468e8ba383fc59fab49025b84b589855bdfa04854609", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_3d_monolayer_samples_first2wells/images_cp_tutorial_3d_monolayer_native_cellprofiler/3d_monolayer_xy1_ch0_CellsLabels.tiff": "aaed9ed23179a162ee7f5a7e8d1d88f1fa342ca55172b7fccd4cbf67f7841d81", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_3d_monolayer_samples_first2wells/images_cp_tutorial_3d_monolayer_native_cellprofiler/3d_monolayer_xy1_ch2_NucleiLabels.tiff": "340bbd795541e4031749d1fff767fa4fcdcac10f5b97879056b25dac9fd7674b", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_3d_monolayer_samples_first2wells/native_cellprofiler_file_list.txt": "556169d5baf25f95b770d2b03ec431cc4217bf3f92008d4c0000b13154d3de5e", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_3d_monolayer_samples_first2wells/native_cellprofiler_headless/3d_monolayer_final.cppipe": "e31b37a86255d6d62c59d18573b6329513fe194d024421897819e63979c24e09", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_3d_monolayer_wells_include_first1/images_7a5a13509180_cp_tutorial_3d_monolayer_native_cellprofiler/.cellprofiler_benchmark_reference.json": "4b59286575280b4fb6d7d7da4aff827a29cbecf810c9d5480795f5bfe7b69517", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_3d_monolayer_wells_include_first1/images_7a5a13509180_cp_tutorial_3d_monolayer_native_cellprofiler/3d_monolayer_Cells.csv": "cc34681db8a66c38ca77bc47db496b26703f21e4d7c7628dd74e30c33250859f", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_3d_monolayer_wells_include_first1/images_7a5a13509180_cp_tutorial_3d_monolayer_native_cellprofiler/3d_monolayer_Experiment.csv": "1ea3d9b2a556509a56700516857787925f53ad8d51750da1178c15f42cd9a71e", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_3d_monolayer_wells_include_first1/images_7a5a13509180_cp_tutorial_3d_monolayer_native_cellprofiler/3d_monolayer_Image.csv": "9775dc08d51f1045b42becd671506138c0801db3528b65d250ce306e44e136bd", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_3d_monolayer_wells_include_first1/images_7a5a13509180_cp_tutorial_3d_monolayer_native_cellprofiler/3d_monolayer_Nuclei.csv": "440c33edc7e8cb5611910a835454db62390301a7af617a542349f60aece57561", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_3d_monolayer_wells_include_first1/images_7a5a13509180_cp_tutorial_3d_monolayer_native_cellprofiler/3d_monolayer_downsizedNuclei.csv": "2c66bc7b17de52147d5a875403f9a9746019ac61371c08157979044c6ed818dd", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_3d_monolayer_wells_include_first1/images_7a5a13509180_cp_tutorial_3d_monolayer_native_cellprofiler/3d_monolayer_erodedDownsizedNuclei.csv": "527a0cce497d2a618478be84d1a0bf174049f58aa2a666e37626a990fdca0116", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_3d_monolayer_wells_include_first1/images_7a5a13509180_cp_tutorial_3d_monolayer_native_cellprofiler/3d_monolayer_erodedResizedNuclei.csv": "66a58f78cab0cfdbe907468e8ba383fc59fab49025b84b589855bdfa04854609", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_3d_monolayer_wells_include_first1/images_7a5a13509180_cp_tutorial_3d_monolayer_native_cellprofiler/3d_monolayer_xy1_ch0_CellsLabels.tiff": "aaed9ed23179a162ee7f5a7e8d1d88f1fa342ca55172b7fccd4cbf67f7841d81", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_3d_monolayer_wells_include_first1/images_7a5a13509180_cp_tutorial_3d_monolayer_native_cellprofiler/3d_monolayer_xy1_ch2_NucleiLabels.tiff": "340bbd795541e4031749d1fff767fa4fcdcac10f5b97879056b25dac9fd7674b", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_3d_monolayer_wells_include_first1/native_cellprofiler_file_list.txt": "556169d5baf25f95b770d2b03ec431cc4217bf3f92008d4c0000b13154d3de5e", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_3d_monolayer_wells_include_first1/native_cellprofiler_headless/3d_monolayer_final.cppipe": "e31b37a86255d6d62c59d18573b6329513fe194d024421897819e63979c24e09", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_advanced_segmentation_final_wells_include_first1/BBBC022_20585_AE_cp_tutorial_advanced_segmentation_final_native_cellprofiler/.cellprofiler_benchmark_reference.json": "4b59286575280b4fb6d7d7da4aff827a29cbecf810c9d5480795f5bfe7b69517", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_advanced_segmentation_final_wells_include_first1/BBBC022_20585_AE_cp_tutorial_advanced_segmentation_final_native_cellprofiler/BBBC022.db": "471dd4621683a2b1bda1f712b4721f7d9a14dbd2ca94fbfd88222733faf58222", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_advanced_segmentation_final_wells_include_first1/BBBC022_20585_AE_cp_tutorial_advanced_segmentation_final_native_cellprofiler/BBBC022_Cells.properties": "64f308b9481641a9d23b63f8a7420dc8cd5cb260ffb784803c9501531339472b", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_advanced_segmentation_final_wells_include_first1/BBBC022_20585_AE_cp_tutorial_advanced_segmentation_final_native_cellprofiler/BBBC022_Cytoplasm.properties": "37eab4186b49c760c024559a630d7f0533be381f7bc26252482958d6983bd173", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_advanced_segmentation_final_wells_include_first1/BBBC022_20585_AE_cp_tutorial_advanced_segmentation_final_native_cellprofiler/BBBC022_Mitochondria.properties": "060c93a8c50e60516083186dfe4cdb2201e355bbd8799a1af55c74db4323533e", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_advanced_segmentation_final_wells_include_first1/BBBC022_20585_AE_cp_tutorial_advanced_segmentation_final_native_cellprofiler/BBBC022_MitochondriaChildObjects.properties": "3fafb9595c11405691680c4b645270f0d92cc9c5dc471d8a6a7fb7c064866ddb", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_advanced_segmentation_final_wells_include_first1/BBBC022_20585_AE_cp_tutorial_advanced_segmentation_final_native_cellprofiler/BBBC022_Nuclei.properties": "c53d1e71b1ba9a74730595a8d621cfc43b2f08c90912940b2e301ac336c08d11", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_advanced_segmentation_final_wells_include_first1/BBBC022_20585_AE_cp_tutorial_advanced_segmentation_final_native_cellprofiler/BBBC022_Nucleoli.properties": "634db019116ec9b9170962c8a09626eefa1b78f91d903267fc277a5e09999a08", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_advanced_segmentation_final_wells_include_first1/BBBC022_20585_AE_cp_tutorial_advanced_segmentation_final_native_cellprofiler/BBBC022_NucleoliChildObjects.properties": "96a5cff7af576f59d6f140a066928897552a3fbe7854f5a2bac9097a82f12c0c", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_advanced_segmentation_final_wells_include_first1/native_cellprofiler_headless/BBBC022_Analysis_Final.cppipe": "335b3fa5179860bfd898f09a42ed80388f8bd4bdc4b5bafa30ad769de734c35d", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_advanced_segmentation_final_wells_include_first1/native_cellprofiler_selected_source_workspace/_source/IllumER/20585_IllumER.npy": "45eda877b52af53e4ff71595751d5e35362ca01f930c0408174ab8337977686d", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_advanced_segmentation_final_wells_include_first1/native_cellprofiler_selected_source_workspace/_source/IllumHoechst/20585_IllumHoechst.npy": "3bb225213f036c701a8ad04e5233a11ae4acb2eae1f676fcd4391ce945d00200", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_advanced_segmentation_final_wells_include_first1/native_cellprofiler_selected_source_workspace/_source/IllumMito/20585_IllumMito.npy": "95803eb745543c0f9cd3291afbaca1ffa7d78d0e2a793d5efaac212b737bb918", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_advanced_segmentation_final_wells_include_first1/native_cellprofiler_selected_source_workspace/_source/IllumPh_golgi/20585_IllumPh_golgi.npy": "a6ee7d393822172428d686353e6c90779f1b3dbe4d32629666d6d1bf735459a1", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_advanced_segmentation_final_wells_include_first1/native_cellprofiler_selected_source_workspace/_source/IllumSyto/20585_IllumSyto.npy": "3450bf6a25718d45d163138c6774299ef8974e7c84b59dd1e7e6dcdddf7095b5", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_beginner_segmentation_final_wells_include_first1/images_Illum-corrected_8f3a144edf6a_cp_tutorial_beginner_segmentation_final_native_cellprofiler/.cellprofiler_benchmark_reference.json": "4b59286575280b4fb6d7d7da4aff827a29cbecf810c9d5480795f5bfe7b69517", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_beginner_segmentation_final_wells_include_first1/images_Illum-corrected_8f3a144edf6a_cp_tutorial_beginner_segmentation_final_native_cellprofiler/MyExpt_Cells.csv": "990c806a68d7f44addfc5294acd50bd42ff6cfcab23fb11b5c39e5f45f9be505", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_beginner_segmentation_final_wells_include_first1/images_Illum-corrected_8f3a144edf6a_cp_tutorial_beginner_segmentation_final_native_cellprofiler/MyExpt_Cytoplasm.csv": "f918da3280f8e88f70b048249ed10f9815ef04f5381fe0fe016cbc0ed0b19c2f", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_beginner_segmentation_final_wells_include_first1/images_Illum-corrected_8f3a144edf6a_cp_tutorial_beginner_segmentation_final_native_cellprofiler/MyExpt_Experiment.csv": "1bbfd0c3e6d91c8561fa13d02d1e9e1b09d4c0f366c8ed8b03456af9b01e7fad", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_beginner_segmentation_final_wells_include_first1/images_Illum-corrected_8f3a144edf6a_cp_tutorial_beginner_segmentation_final_native_cellprofiler/MyExpt_Image.csv": "4ca079358ad8bc84d8bf185be404ecb15f6c3395dbe99ead6e83e4a54206c8a9", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_beginner_segmentation_final_wells_include_first1/images_Illum-corrected_8f3a144edf6a_cp_tutorial_beginner_segmentation_final_native_cellprofiler/MyExpt_Nuclei.csv": "80496e920f2098aa181c8d6f7efd435c9b9da62cf28780d61e898fa899b62304", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_beginner_segmentation_final_wells_include_first1/images_Illum-corrected_8f3a144edf6a_cp_tutorial_beginner_segmentation_final_native_cellprofiler/MyExpt_Nucleoli.csv": "6c7ae52b3b3d3dbfa7cbbb8e088cf9cb2aa4d9c14c608867b8a18b29eb783f2a", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_beginner_segmentation_final_wells_include_first1/images_Illum-corrected_8f3a144edf6a_cp_tutorial_beginner_segmentation_final_native_cellprofiler/MyExpt_NucleoliChildObjects.csv": "bd75ae8a5ae880fa090956729f7826a6242164ab763172084309db29b01b5cb6", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_beginner_segmentation_final_wells_include_first1/images_Illum-corrected_8f3a144edf6a_cp_tutorial_beginner_segmentation_final_native_cellprofiler/overlay_images/plate1_A14_site1_Ch3_overlay.tiff": "05a85674095f244ab4d85aad6644e04420ac2e8394d01d781ec968ecb5ff59f1", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_beginner_segmentation_final_wells_include_first1/images_Illum-corrected_8f3a144edf6a_cp_tutorial_beginner_segmentation_final_native_cellprofiler/overlay_images/plate1_A14_site2_Ch3_overlay.tiff": "89888a6d23423bb99118578d423fb5efa13641c1346ddb09547792fd294d6981", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_beginner_segmentation_final_wells_include_first1/native_cellprofiler_file_list.txt": "8133ba17de0fd35b9a83ba859b9751904b4b58825c137d8575043cd14c16647b", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_beginner_segmentation_final_wells_include_first1/native_cellprofiler_headless/segmentation_final.cppipe": "f309b3e3065ac3dffe48b2fd4cbd5da7a1436297f9708c70383a9926748b1e89", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_pixel_based_classification_wells_include_first1/images_d8212f79a2a8_cp_tutorial_pixel_based_classification_native_cellprofiler/.cellprofiler_benchmark_reference.json": "4b59286575280b4fb6d7d7da4aff827a29cbecf810c9d5480795f5bfe7b69517", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_pixel_based_classification_wells_include_first1/images_d8212f79a2a8_cp_tutorial_pixel_based_classification_native_cellprofiler/cho01_outlines.png": "f79d304a78ee7d881c9e85eca53003e47812d4ce886f4014bfc7c1e7361ce96a", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_pixel_based_classification_wells_include_first1/images_d8212f79a2a8_cp_tutorial_pixel_based_classification_native_cellprofiler/cho_Cells.csv": "0d04a1adfec9141705ec49660f0e2ec07ae447be28fe86402b5f7c5861883709", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_pixel_based_classification_wells_include_first1/images_d8212f79a2a8_cp_tutorial_pixel_based_classification_native_cellprofiler/cho_Experiment.csv": "5165adae2b7ead2af3d78bb69ff056159a412dc39ea06ff28bbb56eafbab9e93", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_pixel_based_classification_wells_include_first1/images_d8212f79a2a8_cp_tutorial_pixel_based_classification_native_cellprofiler/cho_Image.csv": "eabd9cc9c661888ef7e6093ccb9a05cb936454dd0428fb5c95ead99371f8f4f1", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_pixel_based_classification_wells_include_first1/native_cellprofiler_file_list.txt": "e34bd9281ebffbcff1008132d11d6576cf13901ee324287ed988ba2752fc3f68", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_pixel_based_classification_wells_include_first1/native_cellprofiler_headless/pixel_based_classification_cho.cppipe": "acea8b966b03cba696d887f37cd28b1b5c4cec267937ef3a49892b0ab5aeead3", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_quality_control_wells_include_first1/BBBC022_20585_AE_9fb2a83e083c_cp_tutorial_quality_control_native_cellprofiler/.cellprofiler_benchmark_reference.json": "4b59286575280b4fb6d7d7da4aff827a29cbecf810c9d5480795f5bfe7b69517", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_quality_control_wells_include_first1/BBBC022_20585_AE_9fb2a83e083c_cp_tutorial_quality_control_native_cellprofiler/BBBC022QC.db": "644858dc9862358514b612ba557c9a00234681c910ab4c56f1760630d6bb7f9d", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_quality_control_wells_include_first1/BBBC022_20585_AE_9fb2a83e083c_cp_tutorial_quality_control_native_cellprofiler/BBBC022QC_BBBC022QC.properties": "5f86bbb4cc181ad933097a5c83abdf2afcd12988ac4b035c57dd39438a37bb1a", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_quality_control_wells_include_first1/BBBC022_20585_AE_9fb2a83e083c_cp_tutorial_quality_control_native_cellprofiler/BBBC022QC_BBBC022QC.workspace": "884f14365ed643c8dd7050ea77e416a1b7253dca83ff11d2e01aa54a8e07bdc6", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_quality_control_wells_include_first1/native_cellprofiler_file_list.txt": "d83754368f8b7ae51c8bd367d4714086c398cd5fda8150bfd6de34e73f8e81cc", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_quality_control_wells_include_first1/native_cellprofiler_headless/BBBC022_QC.cppipe": "8b931f7c0fa2c149af539174bf909477f2c33a06ee1c418bea7d13c729f3ff59", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_translocation_final_wells_include_first1/TranslocationData_1593ea523b1e_cp_tutorial_translocation_final_native_cellprofiler/.cellprofiler_benchmark_reference.json": "4b59286575280b4fb6d7d7da4aff827a29cbecf810c9d5480795f5bfe7b69517", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_translocation_final_wells_include_first1/TranslocationData_1593ea523b1e_cp_tutorial_translocation_final_native_cellprofiler/BBBC013_A01_s1_w1_Overlay.tiff": "a8dd29f39f592308c65f5dd20e27fdcf56292fda62a0b09ed6b4a253f3a7429e", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_translocation_final_wells_include_first1/TranslocationData_1593ea523b1e_cp_tutorial_translocation_final_native_cellprofiler/DefaultDB.db": "6495268ebcd4f7daaaccc34d6707e32578766220f81e9925397a2c3c5ffe1a74", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_translocation_final_wells_include_first1/TranslocationData_1593ea523b1e_cp_tutorial_translocation_final_native_cellprofiler/DefaultDB.properties": "6ac2716fbd152e7837e0690ab9cc2341ad537c6e09af136ddcab60e2114e0161", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_translocation_final_wells_include_first1/native_cellprofiler_headless/Translocation_final.cppipe": "6d647905484274fab9aeb8bdcc082dbca46a715bc8a45e91e7bfc7bd0177b163", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_translocation_start_wells_include_first1/TranslocationData_cp_tutorial_translocation_start_native_cellprofiler/.cellprofiler_benchmark_reference.json": "e5ea45cd8328a430dc4e515c23242dbd2d0970a74493271aacca076278238c8b", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_translocation_start_wells_include_first1/TranslocationData_cp_tutorial_translocation_start_native_cellprofiler/reference_labels__Nuclei.tiff": "eaa520c6b780dcf61899b0018ae4f1e610b3cdcd9048cd3ec1202669c03ee6e2", + "benchmark/native_refs/official30_scoped_rows/CellProfiler_tutorials_cp_tutorial_translocation_start_wells_include_first1/native_cellprofiler_headless/Translocation_start.cppipe": "19025441307b218f0c2e7023d9cf34ff7016c4ca1cd3356b09586b69034ad2e3", + "benchmark/native_refs/official30_scoped_rows/ExampleColocalization_ExampleColocalization_wells_include_first1/images_48d92a72301c_ExampleColocalization_native_cellprofiler/.cellprofiler_benchmark_reference.json": "4b59286575280b4fb6d7d7da4aff827a29cbecf810c9d5480795f5bfe7b69517", + "benchmark/native_refs/official30_scoped_rows/ExampleColocalization_ExampleColocalization_wells_include_first1/images_48d92a72301c_ExampleColocalization_native_cellprofiler/Image.csv": "5ed9d8130a78f5a03b3823a2e207c74187152c699c83c2dc19ded58d03bfc874", + "benchmark/native_refs/official30_scoped_rows/ExampleColocalization_ExampleColocalization_wells_include_first1/native_cellprofiler_file_list.txt": "22db458de69d10ac4b82c148fd44b96ea72ece88e05e378fe588f0178f708196", + "benchmark/native_refs/official30_scoped_rows/ExampleColocalization_ExampleColocalization_wells_include_first1/native_cellprofiler_headless/ExampleColocalization.cppipe": "839bb2b5efb5b5035c69231ab49be696772017eb2f629a6545d0c7eb9fd38b28", + "benchmark/native_refs/official30_scoped_rows/ExampleCometAssay_ExampleCometAssay_wells_include_first1/images_3112abca6fed_ExampleCometAssay_native_cellprofiler/.cellprofiler_benchmark_reference.json": "4b59286575280b4fb6d7d7da4aff827a29cbecf810c9d5480795f5bfe7b69517", + "benchmark/native_refs/official30_scoped_rows/ExampleCometAssay_ExampleCometAssay_wells_include_first1/images_3112abca6fed_ExampleCometAssay_native_cellprofiler/Comet.csv": "bf3d9b0365d9157e60dfed4c1473b021f69810f4c7592d9dfe7a8b2bd00add6b", + "benchmark/native_refs/official30_scoped_rows/ExampleCometAssay_ExampleCometAssay_wells_include_first1/images_3112abca6fed_ExampleCometAssay_native_cellprofiler/CometHead.csv": "a3505fe5b510a9ad92eb5c58e9ea8a71b17c2f122f272c8649167afcd47ea9a5", + "benchmark/native_refs/official30_scoped_rows/ExampleCometAssay_ExampleCometAssay_wells_include_first1/images_3112abca6fed_ExampleCometAssay_native_cellprofiler/CometTail.csv": "fa3e46fbd2d4272932e236e30a331420443a92368bda32ed00682af6d86cac2c", + "benchmark/native_refs/official30_scoped_rows/ExampleCometAssay_ExampleCometAssay_wells_include_first1/images_3112abca6fed_ExampleCometAssay_native_cellprofiler/CometTails_CometHeadOutline.png": "6c37dc5412968ba29393ab57de0f390a933c0e7cb9df9ec930338de2b34c3684", + "benchmark/native_refs/official30_scoped_rows/ExampleCometAssay_ExampleCometAssay_wells_include_first1/images_3112abca6fed_ExampleCometAssay_native_cellprofiler/Experiment.csv": "9dede520618c58c6b21a2748572ba4f3b230d7ef033535f190fca23f3a95e39a", + "benchmark/native_refs/official30_scoped_rows/ExampleCometAssay_ExampleCometAssay_wells_include_first1/images_3112abca6fed_ExampleCometAssay_native_cellprofiler/Image.csv": "2b9f99f81365c7cc4e7d0b7a702af9370dc4a705a7487054f665936079a11358", + "benchmark/native_refs/official30_scoped_rows/ExampleCometAssay_ExampleCometAssay_wells_include_first1/images_3112abca6fed_ExampleCometAssay_native_cellprofiler/NoTails_CometHeadOutline.png": "c5bd0b0a6bddd22be27240f9d85a6243cc28dc204a7a900d3d698bc89219f03d", + "benchmark/native_refs/official30_scoped_rows/ExampleCometAssay_ExampleCometAssay_wells_include_first1/native_cellprofiler_file_list.txt": "5af4f1dfa994676acef7c7375e7e4db4608cc7ba0cb33434d6a5cd966100a47f", + "benchmark/native_refs/official30_scoped_rows/ExampleCometAssay_ExampleCometAssay_wells_include_first1/native_cellprofiler_headless/ExampleCometAssay.cppipe": "daec62eb90d9457a240bd28dcb3a1002b480db366a5d6aa849a8813f1073f8b6", + "benchmark/native_refs/official30_scoped_rows/ExampleFly_ExampleFlyURL_wells_include_first1/images_2cc1c937754b_ExampleFlyURL_native_cellprofiler/.cellprofiler_benchmark_reference.json": "4b59286575280b4fb6d7d7da4aff827a29cbecf810c9d5480795f5bfe7b69517", + "benchmark/native_refs/official30_scoped_rows/ExampleFly_ExampleFlyURL_wells_include_first1/images_2cc1c937754b_ExampleFlyURL_native_cellprofiler/Cells.csv": "17807e7bd065a66116c99ed9503ec2106c8efdb79ade8a343c6eeb1d4e2419f7", + "benchmark/native_refs/official30_scoped_rows/ExampleFly_ExampleFlyURL_wells_include_first1/images_2cc1c937754b_ExampleFlyURL_native_cellprofiler/CroppedFlyImage0001.tiff": "0f3ae4de1252127bc433873df236cab6dba2b1e39cd24621feb27a61b1d1f257", + "benchmark/native_refs/official30_scoped_rows/ExampleFly_ExampleFlyURL_wells_include_first1/images_2cc1c937754b_ExampleFlyURL_native_cellprofiler/CroppedFlyImage0002.tiff": "0bc3a70d3ec2911d5f6f2517f30856a82c48830c03c2362d5f16df13333c277e", + "benchmark/native_refs/official30_scoped_rows/ExampleFly_ExampleFlyURL_wells_include_first1/images_2cc1c937754b_ExampleFlyURL_native_cellprofiler/CroppedFlyImage0003.tiff": "a52eaa99f86447e92b5c3351fe00af600888c82d137962430d55e741047a1e42", + "benchmark/native_refs/official30_scoped_rows/ExampleFly_ExampleFlyURL_wells_include_first1/images_2cc1c937754b_ExampleFlyURL_native_cellprofiler/Cytoplasm.csv": "421ec8602ceb55446d3360bd61aa272af8954327f0b660cca15512a747018d80", + "benchmark/native_refs/official30_scoped_rows/ExampleFly_ExampleFlyURL_wells_include_first1/images_2cc1c937754b_ExampleFlyURL_native_cellprofiler/Image.csv": "abf8cd393bf12fa90174bc6715fb6b759143bd48a9f7247e3f78dd7878982999", + "benchmark/native_refs/official30_scoped_rows/ExampleFly_ExampleFlyURL_wells_include_first1/images_2cc1c937754b_ExampleFlyURL_native_cellprofiler/Nuclei.csv": "da0c217235d10ff3a28314041a70349c2ab56606f3b34fff276ff67d76d7598f", + "benchmark/native_refs/official30_scoped_rows/ExampleFly_ExampleFlyURL_wells_include_first1/native_cellprofiler_headless/ExampleFlyURL.cppipe": "cc5485857e4392c91736c9f38b81f1dd04295f8d676d524062fa65136a54a2d6", + "benchmark/native_refs/official30_scoped_rows/ExampleFly_ExampleFly_wells_include_first1/images_2cc1c937754b_ExampleFly_native_cellprofiler/.cellprofiler_benchmark_reference.json": "4b59286575280b4fb6d7d7da4aff827a29cbecf810c9d5480795f5bfe7b69517", + "benchmark/native_refs/official30_scoped_rows/ExampleFly_ExampleFly_wells_include_first1/images_2cc1c937754b_ExampleFly_native_cellprofiler/01_POS002_DRGB.tiff": "0f3ae4de1252127bc433873df236cab6dba2b1e39cd24621feb27a61b1d1f257", + "benchmark/native_refs/official30_scoped_rows/ExampleFly_ExampleFly_wells_include_first1/images_2cc1c937754b_ExampleFly_native_cellprofiler/01_POS076_DRGB.tiff": "0bc3a70d3ec2911d5f6f2517f30856a82c48830c03c2362d5f16df13333c277e", + "benchmark/native_refs/official30_scoped_rows/ExampleFly_ExampleFly_wells_include_first1/images_2cc1c937754b_ExampleFly_native_cellprofiler/01_POS218_DRGB.tiff": "a52eaa99f86447e92b5c3351fe00af600888c82d137962430d55e741047a1e42", + "benchmark/native_refs/official30_scoped_rows/ExampleFly_ExampleFly_wells_include_first1/images_2cc1c937754b_ExampleFly_native_cellprofiler/Cells.csv": "a3e229c264bc943aa298de966c101983d4ca3d7e75c9df947311d6656f38021e", + "benchmark/native_refs/official30_scoped_rows/ExampleFly_ExampleFly_wells_include_first1/images_2cc1c937754b_ExampleFly_native_cellprofiler/Cytoplasm.csv": "71120635ce6af4b40f7629a28ee18dfb605b453b57ba24c50af1b659dd160ee9", + "benchmark/native_refs/official30_scoped_rows/ExampleFly_ExampleFly_wells_include_first1/images_2cc1c937754b_ExampleFly_native_cellprofiler/Image.csv": "719bba516b4ec6dc19c97f022d51aecc36dd1f8c1d6df9368577bbb39c950190", + "benchmark/native_refs/official30_scoped_rows/ExampleFly_ExampleFly_wells_include_first1/images_2cc1c937754b_ExampleFly_native_cellprofiler/Nuclei.csv": "51ab042002e91f581ce238fd50f43f8bfa737bb6373c9c5661a67964da919848", + "benchmark/native_refs/official30_scoped_rows/ExampleFly_ExampleFly_wells_include_first1/native_cellprofiler_file_list.txt": "4e4293727a6ee67d4efca00cd5b456727d8faa7b5048c06ba9fff4c82173c1e3", + "benchmark/native_refs/official30_scoped_rows/ExampleFly_ExampleFly_wells_include_first1/native_cellprofiler_headless/ExampleFly.cppipe": "85c799ccbf2a6dd92185b8902da24229a0ca896c51dc7a83fcb124aae47c644a", + "benchmark/native_refs/official30_scoped_rows/ExampleHuman_ExampleHuman_wells_include_first1/images_95e1f0d3695a_ExampleHuman_native_cellprofiler/.cellprofiler_benchmark_reference.json": "c0f6eed26fd477ab9f98af2f19604f1927ce44a40985bdb79bb53b454936fbbc", + "benchmark/native_refs/official30_scoped_rows/ExampleHuman_ExampleHuman_wells_include_first1/images_95e1f0d3695a_ExampleHuman_native_cellprofiler/AS_09125_050116030001_D03f00d0_Overlay.png": "7ab7c2ec53beca87eda3a911213884c5002bf8b456e770304ee88b3f146ba5d0", + "benchmark/native_refs/official30_scoped_rows/ExampleHuman_ExampleHuman_wells_include_first1/images_95e1f0d3695a_ExampleHuman_native_cellprofiler/Cells.csv": "878e54e6c3759b1ce1c12e655d1482eae964d73f5d84b30d870b26d8724495c7", + "benchmark/native_refs/official30_scoped_rows/ExampleHuman_ExampleHuman_wells_include_first1/images_95e1f0d3695a_ExampleHuman_native_cellprofiler/Cytoplasm.csv": "4698593b4ff357656a74385d9f9ffc636bbf0754dc7e5c0ddda44f6d6b3d4ae6", + "benchmark/native_refs/official30_scoped_rows/ExampleHuman_ExampleHuman_wells_include_first1/images_95e1f0d3695a_ExampleHuman_native_cellprofiler/Experiment.csv": "0ef8436b70da872826c39e47c162b490a12d2a9ff0ca1f081361cc38d3107e99", + "benchmark/native_refs/official30_scoped_rows/ExampleHuman_ExampleHuman_wells_include_first1/images_95e1f0d3695a_ExampleHuman_native_cellprofiler/Image.csv": "1be263c6192d6a191884f27a60340a6d578e8ae39d5f740c999fedbcc6820073", + "benchmark/native_refs/official30_scoped_rows/ExampleHuman_ExampleHuman_wells_include_first1/images_95e1f0d3695a_ExampleHuman_native_cellprofiler/Nuclei.csv": "be557c8bedbe19c5d59e083e92f81b526d9ca58098a0568af02950495dc9288c", + "benchmark/native_refs/official30_scoped_rows/ExampleHuman_ExampleHuman_wells_include_first1/images_95e1f0d3695a_ExampleHuman_native_cellprofiler/PH3.csv": "7112907105b44e061918a6581238b0f3199dfabe0faf38e313b45e0b6379ec78", + "benchmark/native_refs/official30_scoped_rows/ExampleHuman_ExampleHuman_wells_include_first1/native_cellprofiler_file_list.txt": "dfeb6768ac6f3608707a401a8644c268d14ea9cf2e565d1743d69643c24853a2", + "benchmark/native_refs/official30_scoped_rows/ExampleHuman_ExampleHuman_wells_include_first1/native_cellprofiler_headless/ExampleHuman.cppipe": "e3c567abdab72455674521234f9183a769c8365b1bbb969312d95cf7482c73bd", + "benchmark/native_refs/official30_scoped_rows/ExampleIlluminationCorrection_ExampleIlluminationCorrection_Example1_AllMethod_wells_include_first1/images_a015597fb7a8_ExampleIlluminationCorrection_Example1_AllMethod_native_cellprofiler/.cellprofiler_benchmark_reference.json": "4b59286575280b4fb6d7d7da4aff827a29cbecf810c9d5480795f5bfe7b69517", + "benchmark/native_refs/official30_scoped_rows/ExampleIlluminationCorrection_ExampleIlluminationCorrection_Example1_AllMethod_wells_include_first1/images_a015597fb7a8_ExampleIlluminationCorrection_Example1_AllMethod_native_cellprofiler/Illum.npy": "2f8490c4a6a7301d569797a80e3f9f26581fc83cc267aa436e44dc2f06cc346a", + "benchmark/native_refs/official30_scoped_rows/ExampleIlluminationCorrection_ExampleIlluminationCorrection_Example1_AllMethod_wells_include_first1/native_cellprofiler_file_list.txt": "9793c1df10fad958514cc6604e0edde27dfb152f8654e9414e3729bd19af61a1", + "benchmark/native_refs/official30_scoped_rows/ExampleIlluminationCorrection_ExampleIlluminationCorrection_Example1_AllMethod_wells_include_first1/native_cellprofiler_headless/ExampleIlluminationCorrection_Example1_AllMethod.cppipe": "e76125ee2402879bf0610135ea02c1bc065120c074d404b6d955fa12523fbb02", + "benchmark/native_refs/official30_scoped_rows/ExampleIlluminationCorrection_ExampleIlluminationCorrection_Example1_EachMethod_wells_include_first1/images_ExampleIlluminationCorrection_Example1_EachMethod_native_cellprofiler/.cellprofiler_benchmark_reference.json": "331429c61ec577b7baec0beb6cc913cd4b6575b323a02a64aa00693918941194", + "benchmark/native_refs/official30_scoped_rows/ExampleIlluminationCorrection_ExampleIlluminationCorrection_Example1_EachMethod_wells_include_first1/images_ExampleIlluminationCorrection_Example1_EachMethod_native_cellprofiler/reference_image__CorrGreen.npy": "3c17148f0fad16738a23af55ac4e41692e6c2906808a7d83823331702b1b4f46", + "benchmark/native_refs/official30_scoped_rows/ExampleIlluminationCorrection_ExampleIlluminationCorrection_Example1_EachMethod_wells_include_first1/native_cellprofiler_file_list.txt": "730871fddd9fd941ae38a6c34109f9818c219179599b65d686c92f7963d06d0f", + "benchmark/native_refs/official30_scoped_rows/ExampleIlluminationCorrection_ExampleIlluminationCorrection_Example1_EachMethod_wells_include_first1/native_cellprofiler_headless/ExampleIlluminationCorrection_Example1_EachMethod.cppipe": "ae7ae773b1479b18b08606c9cd7a5106c5ebba78f58f3ccbd27bc28f684d65b2", + "benchmark/native_refs/official30_scoped_rows/ExampleIlluminationCorrection_ExampleIlluminationCorrection_Example2_wells_include_first1/images_ExampleIlluminationCorrection_Example2_native_cellprofiler/.cellprofiler_benchmark_reference.json": "dd475748fcbfd645efef9e29f6a856e1b68096344ab4614dae2ff597523e95ca", + "benchmark/native_refs/official30_scoped_rows/ExampleIlluminationCorrection_ExampleIlluminationCorrection_Example2_wells_include_first1/images_ExampleIlluminationCorrection_Example2_native_cellprofiler/reference_labels__LargeBlockCorrectedNuclei.tiff": "f1bbed3348ce75d8d4158cad37c08ae4ab30d9e0a5dfbb9e259b906311811e45", + "benchmark/native_refs/official30_scoped_rows/ExampleIlluminationCorrection_ExampleIlluminationCorrection_Example2_wells_include_first1/images_ExampleIlluminationCorrection_Example2_native_cellprofiler/reference_labels__SmallBlockCorrectedNuclei.tiff": "d31aa4bc3e9782e646820b5f3bef6f91f4fbf78bf8ee46b5260cde78e558f700", + "benchmark/native_refs/official30_scoped_rows/ExampleIlluminationCorrection_ExampleIlluminationCorrection_Example2_wells_include_first1/images_ExampleIlluminationCorrection_Example2_native_cellprofiler/reference_labels__UncorrectedNuclei.tiff": "2f54cba8f1482ab54e8a21f71fb2a912ea8a260f8dae2bb5bccfb94994da5a84", + "benchmark/native_refs/official30_scoped_rows/ExampleIlluminationCorrection_ExampleIlluminationCorrection_Example2_wells_include_first1/native_cellprofiler_file_list.txt": "6cdd305e931eab9d08f3020e7417c974c58a57b13128e515a7ee03096bb835aa", + "benchmark/native_refs/official30_scoped_rows/ExampleIlluminationCorrection_ExampleIlluminationCorrection_Example2_wells_include_first1/native_cellprofiler_headless/ExampleIlluminationCorrection_Example2.cppipe": "b6170bcb31668b082deb2fb9be285d0019ba7137a691ce80dc34a141744b9e97", + "benchmark/native_refs/official30_scoped_rows/ExampleIlluminationCorrection_ExampleIlluminationCorrection_Example3_wells_include_first1/images_ExampleIlluminationCorrection_Example3_native_cellprofiler/.cellprofiler_benchmark_reference.json": "37e74838f03ca2fcb0b941ad62f2bcf6ebc4082651dd04c577d7e0b760a03a79", + "benchmark/native_refs/official30_scoped_rows/ExampleIlluminationCorrection_ExampleIlluminationCorrection_Example3_wells_include_first1/images_ExampleIlluminationCorrection_Example3_native_cellprofiler/reference_image__ConvexHullCorrWorm.npy": "9ba1259e4ca007c06f14be6946e32a86307ec2dfb9952b73fa2cc253ccae8b3a", + "benchmark/native_refs/official30_scoped_rows/ExampleIlluminationCorrection_ExampleIlluminationCorrection_Example3_wells_include_first1/images_ExampleIlluminationCorrection_Example3_native_cellprofiler/reference_image__PolynomialCorrected.npy": "588e6f9951aa7ad220678dbb0053e98fb6d324e71aab089d1d828d891ee32f54", + "benchmark/native_refs/official30_scoped_rows/ExampleIlluminationCorrection_ExampleIlluminationCorrection_Example3_wells_include_first1/native_cellprofiler_file_list.txt": "84cdf25570fc4ed34acee7d8b83a456d2dbe7ba5f87d8c2d59f52501e2ff8e1b", + "benchmark/native_refs/official30_scoped_rows/ExampleIlluminationCorrection_ExampleIlluminationCorrection_Example3_wells_include_first1/native_cellprofiler_headless/ExampleIlluminationCorrection_Example3.cppipe": "64c5ad5f41c0c843506c288ffc61b2aed9391e3bc914f770633e57ee5bbada12", + "benchmark/native_refs/official30_scoped_rows/ExampleImagingFlowCytometryObjectsInGrid_ExampleImagingFlowCytometryObjectsInGrid_wells_include_first1/images_febbfcc5c842_ExampleImagingFlowCytometryObjectsInGrid_native_cellprofiler/.cellprofiler_benchmark_reference.json": "c0f6eed26fd477ab9f98af2f19604f1927ce44a40985bdb79bb53b454936fbbc", + "benchmark/native_refs/official30_scoped_rows/ExampleImagingFlowCytometryObjectsInGrid_ExampleImagingFlowCytometryObjectsInGrid_wells_include_first1/images_febbfcc5c842_ExampleImagingFlowCytometryObjectsInGrid_native_cellprofiler/BF_cells_on_grid.csv": "23ca5a36a568fc87405888b130328aeb45e9ecf2853516d35493f8b3b4d769d1", + "benchmark/native_refs/official30_scoped_rows/ExampleImagingFlowCytometryObjectsInGrid_ExampleImagingFlowCytometryObjectsInGrid_wells_include_first1/native_cellprofiler_file_list.txt": "0413a52c486a36ca8534e50a246952c35b44b640c722b00189bd60f2fa575541", + "benchmark/native_refs/official30_scoped_rows/ExampleImagingFlowCytometryObjectsInGrid_ExampleImagingFlowCytometryObjectsInGrid_wells_include_first1/native_cellprofiler_headless/ExampleImagingFlowCytometryObjectsInGrid.cppipe": "391c846497690c51db29ce728383fdf7327aecef9922e9069414ab28c7b9577c", + "benchmark/native_refs/official30_scoped_rows/ExampleNeighbors_ExampleNeighbors_wells_include_first1/images_752e3d1a4a72_ExampleNeighbors_native_cellprofiler/.cellprofiler_benchmark_reference.json": "4b59286575280b4fb6d7d7da4aff827a29cbecf810c9d5480795f5bfe7b69517", + "benchmark/native_refs/official30_scoped_rows/ExampleNeighbors_ExampleNeighbors_wells_include_first1/images_752e3d1a4a72_ExampleNeighbors_native_cellprofiler/Cells.csv": "3ac4cbfaaf7d4cdbe4ea9f0624707ca1776b2b0a65b8c3dfd30cfb3e3a1c908f", + "benchmark/native_refs/official30_scoped_rows/ExampleNeighbors_ExampleNeighbors_wells_include_first1/images_752e3d1a4a72_ExampleNeighbors_native_cellprofiler/Clones1_ColorNeighbors.png": "9c930b44559a4c8b343652b52c42602348b8c9b48d0608f715fce3285198d18b", + "benchmark/native_refs/official30_scoped_rows/ExampleNeighbors_ExampleNeighbors_wells_include_first1/images_752e3d1a4a72_ExampleNeighbors_native_cellprofiler/Clones1_InvertedRed.png": "81efde4802d5cfa71292ffdf78567535b8761ed9dffd10d849b95c9cb2b37b72", + "benchmark/native_refs/official30_scoped_rows/ExampleNeighbors_ExampleNeighbors_wells_include_first1/images_752e3d1a4a72_ExampleNeighbors_native_cellprofiler/Image.csv": "74dacf6d115a893193a0cb890ce679944355519813800295a4662d5a47f01f14", + "benchmark/native_refs/official30_scoped_rows/ExampleNeighbors_ExampleNeighbors_wells_include_first1/native_cellprofiler_file_list.txt": "2a80e5949bc283999e14c575276ffe817b03dab061abf3cf99be28fe5953a7b6", + "benchmark/native_refs/official30_scoped_rows/ExampleNeighbors_ExampleNeighbors_wells_include_first1/native_cellprofiler_headless/ExampleNeighbors.cppipe": "53bd447b6bb96fb63588590838e0b74d9b3514615773b88c0e00437d15f82c64", + "benchmark/native_refs/official30_scoped_rows/ExamplePercentPositive_ExamplePercentPositive_wells_include_first1/images_386eec9eae5f_ExamplePercentPositive_native_cellprofiler/.cellprofiler_benchmark_reference.json": "4b59286575280b4fb6d7d7da4aff827a29cbecf810c9d5480795f5bfe7b69517", + "benchmark/native_refs/official30_scoped_rows/ExamplePercentPositive_ExamplePercentPositive_wells_include_first1/images_386eec9eae5f_ExamplePercentPositive_native_cellprofiler/Experiment.csv": "2a88f16ce5fe41011936191d59c52b23833d3167882abf78209919737eccafac", + "benchmark/native_refs/official30_scoped_rows/ExamplePercentPositive_ExamplePercentPositive_wells_include_first1/images_386eec9eae5f_ExamplePercentPositive_native_cellprofiler/Image.csv": "61318dfb76b44a9665af2a9bc9b7f12c5f7065d04f4dc8f1f769e03b38ce9f41", + "benchmark/native_refs/official30_scoped_rows/ExamplePercentPositive_ExamplePercentPositive_wells_include_first1/images_386eec9eae5f_ExamplePercentPositive_native_cellprofiler/Nuclei.csv": "8784edd1a232864fe18a71bd78eca1214e63fa8ca4b8789926605e0e75cbd7c7", + "benchmark/native_refs/official30_scoped_rows/ExamplePercentPositive_ExamplePercentPositive_wells_include_first1/images_386eec9eae5f_ExamplePercentPositive_native_cellprofiler/PH3.csv": "2396591af9c4bdd277060be915bf096797eb0820887817645cdc60aadbe6bd21", + "benchmark/native_refs/official30_scoped_rows/ExamplePercentPositive_ExamplePercentPositive_wells_include_first1/images_386eec9eae5f_ExamplePercentPositive_native_cellprofiler/PH3PosNuclei.csv": "edb4ffb53dbbb581dacc22bc36eaa1c8da55005972d1d4c923960f6db56754f7", + "benchmark/native_refs/official30_scoped_rows/ExamplePercentPositive_ExamplePercentPositive_wells_include_first1/native_cellprofiler_file_list.txt": "50e6dda932f1f8c22e9ff02529d94df54cb3a254e869fbb781bb2311b4507320", + "benchmark/native_refs/official30_scoped_rows/ExamplePercentPositive_ExamplePercentPositive_wells_include_first1/native_cellprofiler_headless/ExamplePercentPositive.cppipe": "17c5308d3f2ddbb26e3418c9b1f9937dab9b14f4da330d7dcdce08e0aad1c600", + "benchmark/native_refs/official30_scoped_rows/ExampleSpeckles_ExampleSpeckles_wells_include_first1/images_9683227414f2_ExampleSpeckles_native_cellprofiler/.cellprofiler_benchmark_reference.json": "4b59286575280b4fb6d7d7da4aff827a29cbecf810c9d5480795f5bfe7b69517", + "benchmark/native_refs/official30_scoped_rows/ExampleSpeckles_ExampleSpeckles_wells_include_first1/images_9683227414f2_ExampleSpeckles_native_cellprofiler/Image.csv": "8f12b7360d23ca72e8b1aca6ddb23ebde86543421971ce77f36d2ff9734ca847", + "benchmark/native_refs/official30_scoped_rows/ExampleSpeckles_ExampleSpeckles_wells_include_first1/images_9683227414f2_ExampleSpeckles_native_cellprofiler/Nuclei.csv": "15af7cac08d651c19b3e3f409b2b0d7fc4f96011c9b53394aa9a35969a8936a6", + "benchmark/native_refs/official30_scoped_rows/ExampleSpeckles_ExampleSpeckles_wells_include_first1/images_9683227414f2_ExampleSpeckles_native_cellprofiler/h2ax.csv": "0f54fc72ccdeb7e1a68bab9dc3a8d9d49788cdf81dbbc109ed119e589c09538a", + "benchmark/native_refs/official30_scoped_rows/ExampleSpeckles_ExampleSpeckles_wells_include_first1/native_cellprofiler_file_list.txt": "1adc0f16f42239d43fb944773e6b813c3c5596031b0f5977ec6ba241700506f5", + "benchmark/native_refs/official30_scoped_rows/ExampleSpeckles_ExampleSpeckles_wells_include_first1/native_cellprofiler_headless/ExampleSpeckles.cppipe": "2dbe55a93a6356f495c954e4492e2d436d96f11f3b3074d5ed9f8df12f3c240a", + "benchmark/native_refs/official30_scoped_rows/ExampleStraightenWorms_ExampleUntangleAndStraightenWorms_wells_include_first1/images_b2548cae1315_ExampleUntangleAndStraightenWorms_native_cellprofiler/.cellprofiler_benchmark_reference.json": "4b59286575280b4fb6d7d7da4aff827a29cbecf810c9d5480795f5bfe7b69517", + "benchmark/native_refs/official30_scoped_rows/ExampleStraightenWorms_ExampleUntangleAndStraightenWorms_wells_include_first1/images_b2548cae1315_ExampleUntangleAndStraightenWorms_native_cellprofiler/StraightenedWorms.csv": "f5f6b1cf5bc764e7a4779489847508a9945490ef597b2a3178182355d78ab750", + "benchmark/native_refs/official30_scoped_rows/ExampleStraightenWorms_ExampleUntangleAndStraightenWorms_wells_include_first1/native_cellprofiler_file_list.txt": "0427f41525037dcd997d181aaad3025ad96d6feea44f5ce0de4a2f42bea9faba", + "benchmark/native_refs/official30_scoped_rows/ExampleStraightenWorms_ExampleUntangleAndStraightenWorms_wells_include_first1/native_cellprofiler_headless/ExampleUntangleAndStraightenWorms.cppipe": "bf1076e58498cedcec86edcc2154067dc6e84fbdc8cbb3f7622efe301a10a742", + "benchmark/native_refs/official30_scoped_rows/ExampleTrackObjects_ExampleTrackObjects_wells_include_first1/images_45c4bf7d7bb1_ExampleTrackObjects_native_cellprofiler/.cellprofiler_benchmark_reference.json": "4b59286575280b4fb6d7d7da4aff827a29cbecf810c9d5480795f5bfe7b69517", + "benchmark/native_refs/official30_scoped_rows/ExampleTrackObjects_ExampleTrackObjects_wells_include_first1/images_45c4bf7d7bb1_ExampleTrackObjects_native_cellprofiler/Sequence1/DrosophilaEmbryo-0000.png": "ccb27d92a6837e0f2f6f59f0bd67dd6dc776c3779f636a89b82d89fa861de91e", + "benchmark/native_refs/official30_scoped_rows/ExampleTrackObjects_ExampleTrackObjects_wells_include_first1/images_45c4bf7d7bb1_ExampleTrackObjects_native_cellprofiler/Sequence1/DrosophilaEmbryo-0001.png": "df0fbeb62e76d4959516178835bc88a3f6632d32f440fb954d3d7c219e9edcfc", + "benchmark/native_refs/official30_scoped_rows/ExampleTrackObjects_ExampleTrackObjects_wells_include_first1/images_45c4bf7d7bb1_ExampleTrackObjects_native_cellprofiler/Sequence1/DrosophilaEmbryo-0002.png": "e3172e0b3f6c97693e4992c349e499c0392d6c5f15c6fbab1641a5abc0eec34b", + "benchmark/native_refs/official30_scoped_rows/ExampleTrackObjects_ExampleTrackObjects_wells_include_first1/images_45c4bf7d7bb1_ExampleTrackObjects_native_cellprofiler/Sequence1/DrosophilaEmbryo-0003.png": "e3172e0b3f6c97693e4992c349e499c0392d6c5f15c6fbab1641a5abc0eec34b", + "benchmark/native_refs/official30_scoped_rows/ExampleTrackObjects_ExampleTrackObjects_wells_include_first1/images_45c4bf7d7bb1_ExampleTrackObjects_native_cellprofiler/Sequence1/DrosophilaEmbryo-0004.png": "2b4575e3eac35bd306845582f82714f9390f2c5331462bb9e37a62d99e5119d8", + "benchmark/native_refs/official30_scoped_rows/ExampleTrackObjects_ExampleTrackObjects_wells_include_first1/images_45c4bf7d7bb1_ExampleTrackObjects_native_cellprofiler/Sequence1/DrosophilaEmbryo-0005.png": "4392bfaacdf926e1d961bc3945cf7dba716ec338b4ab8579a4082a715e10379a", + "benchmark/native_refs/official30_scoped_rows/ExampleTrackObjects_ExampleTrackObjects_wells_include_first1/images_45c4bf7d7bb1_ExampleTrackObjects_native_cellprofiler/Sequence1/DrosophilaEmbryo-0006.png": "4392bfaacdf926e1d961bc3945cf7dba716ec338b4ab8579a4082a715e10379a", + "benchmark/native_refs/official30_scoped_rows/ExampleTrackObjects_ExampleTrackObjects_wells_include_first1/images_45c4bf7d7bb1_ExampleTrackObjects_native_cellprofiler/Sequence1/DrosophilaEmbryo-0007.png": "42c4b6f5a3f339051c5bce755e9ba61d577013cb21d0d4f9e0428f06e61d8d41", + "benchmark/native_refs/official30_scoped_rows/ExampleTrackObjects_ExampleTrackObjects_wells_include_first1/images_45c4bf7d7bb1_ExampleTrackObjects_native_cellprofiler/Sequence1/DrosophilaEmbryo-0008.png": "a6e057f7ddd09cf602ea442c754190509cbafdcd422ceb735634949928a39cfc", + "benchmark/native_refs/official30_scoped_rows/ExampleTrackObjects_ExampleTrackObjects_wells_include_first1/images_45c4bf7d7bb1_ExampleTrackObjects_native_cellprofiler/Sequence1/DrosophilaEmbryo-0009.png": "a6e057f7ddd09cf602ea442c754190509cbafdcd422ceb735634949928a39cfc", + "benchmark/native_refs/official30_scoped_rows/ExampleTrackObjects_ExampleTrackObjects_wells_include_first1/images_45c4bf7d7bb1_ExampleTrackObjects_native_cellprofiler/Sequence1/DrosophilaEmbryo-0010.png": "09a253d116ff74f9b8a223031a1e01f25367b78aa4bfb7ee8aeee8a865543603", + "benchmark/native_refs/official30_scoped_rows/ExampleTrackObjects_ExampleTrackObjects_wells_include_first1/images_45c4bf7d7bb1_ExampleTrackObjects_native_cellprofiler/Sequence1/DrosophilaEmbryo-0011.png": "1246ed67396f5107d4532bc0d67f5bbebadce77034d451ba7003b39ec458834b", + "benchmark/native_refs/official30_scoped_rows/ExampleTrackObjects_ExampleTrackObjects_wells_include_first1/images_45c4bf7d7bb1_ExampleTrackObjects_native_cellprofiler/Sequence1/DrosophilaEmbryo-0012.png": "1246ed67396f5107d4532bc0d67f5bbebadce77034d451ba7003b39ec458834b", + "benchmark/native_refs/official30_scoped_rows/ExampleTrackObjects_ExampleTrackObjects_wells_include_first1/images_45c4bf7d7bb1_ExampleTrackObjects_native_cellprofiler/Sequence1/DrosophilaEmbryo-0013.png": "fef7b5d3a9064f5a6cdeee821f46a83c1d1cfd02f4dcedf237ef69156965e0c3", + "benchmark/native_refs/official30_scoped_rows/ExampleTrackObjects_ExampleTrackObjects_wells_include_first1/images_45c4bf7d7bb1_ExampleTrackObjects_native_cellprofiler/Sequence1/DrosophilaEmbryo-0014.png": "00c51409c195bd9a22033bb65df6a590088b5b13b54d2cfc7b2f5c249b65020b", + "benchmark/native_refs/official30_scoped_rows/ExampleTrackObjects_ExampleTrackObjects_wells_include_first1/images_45c4bf7d7bb1_ExampleTrackObjects_native_cellprofiler/Sequence1/DrosophilaEmbryo-0015.png": "00c51409c195bd9a22033bb65df6a590088b5b13b54d2cfc7b2f5c249b65020b", + "benchmark/native_refs/official30_scoped_rows/ExampleTrackObjects_ExampleTrackObjects_wells_include_first1/images_45c4bf7d7bb1_ExampleTrackObjects_native_cellprofiler/Sequence1/DrosophilaEmbryo-0016.png": "e0f9c89fdf96d28aa0b4c53d98eac408f4d4f4b02c5e29e847e24b935bf8d636", + "benchmark/native_refs/official30_scoped_rows/ExampleTrackObjects_ExampleTrackObjects_wells_include_first1/images_45c4bf7d7bb1_ExampleTrackObjects_native_cellprofiler/Sequence1/DrosophilaEmbryo-0017.png": "936196086157848b06d961d30d9ff0fcba09731885e9209f13194cf1039bf25d", + "benchmark/native_refs/official30_scoped_rows/ExampleTrackObjects_ExampleTrackObjects_wells_include_first1/images_45c4bf7d7bb1_ExampleTrackObjects_native_cellprofiler/Sequence1/DrosophilaEmbryo-0018.png": "936196086157848b06d961d30d9ff0fcba09731885e9209f13194cf1039bf25d", + "benchmark/native_refs/official30_scoped_rows/ExampleTrackObjects_ExampleTrackObjects_wells_include_first1/images_45c4bf7d7bb1_ExampleTrackObjects_native_cellprofiler/Sequence1/DrosophilaEmbryo-0019.png": "a068c00d5245cae6cabd119892586ccbe32341a31e71857b76d6107a6f9d05c9", + "benchmark/native_refs/official30_scoped_rows/ExampleTrackObjects_ExampleTrackObjects_wells_include_first1/images_45c4bf7d7bb1_ExampleTrackObjects_native_cellprofiler/Sequence1/DrosophilaEmbryo-0020.png": "3f4a3c0d670e75260df0e15bfce911954b285dd3df6c70a9bae62182bd8c106e", + "benchmark/native_refs/official30_scoped_rows/ExampleTrackObjects_ExampleTrackObjects_wells_include_first1/images_45c4bf7d7bb1_ExampleTrackObjects_native_cellprofiler/Sequence1/Embryos.csv": "0d6927a942947e220e91385cd90082c0349391fdeeaa70ee2a95fd4aae8038b8", + "benchmark/native_refs/official30_scoped_rows/ExampleTrackObjects_ExampleTrackObjects_wells_include_first1/images_45c4bf7d7bb1_ExampleTrackObjects_native_cellprofiler/Sequence1/Image.csv": "a1f0c73eaa61e08247160e36b0cb281f1cc1da4afa1b6862c2cec4cac1b5f88d", + "benchmark/native_refs/official30_scoped_rows/ExampleTrackObjects_ExampleTrackObjects_wells_include_first1/native_cellprofiler_file_list.txt": "49759b614a98bdd8000adc32d20b6bf7dbeb17a360b4e6bdae51fb378ba601f6", + "benchmark/native_refs/official30_scoped_rows/ExampleTrackObjects_ExampleTrackObjects_wells_include_first1/native_cellprofiler_headless/ExampleTrackObjects.cppipe": "c3f91a36bb7fef8b078a60e3bfe73fe2f4ea25bc97048c44f8e6d57ecfac0098", + "benchmark/native_refs/official30_scoped_rows/ExampleTumor_ExampleTumor_wells_include_first1/images_9401bbd08d77_ExampleTumor_native_cellprofiler/.cellprofiler_benchmark_reference.json": "c0f6eed26fd477ab9f98af2f19604f1927ce44a40985bdb79bb53b454936fbbc", + "benchmark/native_refs/official30_scoped_rows/ExampleTumor_ExampleTumor_wells_include_first1/images_9401bbd08d77_ExampleTumor_native_cellprofiler/30-2A1f_Tumors.png": "eaa154d347361f8347db3c444d74a0708bd4f77964ec6d60a529438f047d5d6c", + "benchmark/native_refs/official30_scoped_rows/ExampleTumor_ExampleTumor_wells_include_first1/images_9401bbd08d77_ExampleTumor_native_cellprofiler/Experiment.csv": "73ffe748dcfa18b8dd683fab5e7ea53f41a5529f47bfe96ac8479c44c8342df3", + "benchmark/native_refs/official30_scoped_rows/ExampleTumor_ExampleTumor_wells_include_first1/images_9401bbd08d77_ExampleTumor_native_cellprofiler/Image.csv": "0c23c2a135ce9b1d83c739e076ef0b50e3532fed66812b523317926018bd0a1a", + "benchmark/native_refs/official30_scoped_rows/ExampleTumor_ExampleTumor_wells_include_first1/images_9401bbd08d77_ExampleTumor_native_cellprofiler/tumor.csv": "85f033d06783f917ae543cf3ed332e184c0a3b69c7ecd66c1593f06e5390eb0c", + "benchmark/native_refs/official30_scoped_rows/ExampleTumor_ExampleTumor_wells_include_first1/native_cellprofiler_file_list.txt": "234cb63fa27f4086a95a6a1474edcb7fc2fb86c934be683afd753bb79066dd5b", + "benchmark/native_refs/official30_scoped_rows/ExampleTumor_ExampleTumor_wells_include_first1/native_cellprofiler_headless/ExampleTumor.cppipe": "f45ae0a3969cb8f0ab7e1d346c1f7df76ddcda110b081d98835996a6a7c41997", + "benchmark/native_refs/official30_scoped_rows/ExampleUntangleWormsBrightField_ExampleUntangleWormsBrightField_wells_include_first1/images_a61f79afad70_ExampleUntangleWormsBrightField_native_cellprofiler/.cellprofiler_benchmark_reference.json": "4b59286575280b4fb6d7d7da4aff827a29cbecf810c9d5480795f5bfe7b69517", + "benchmark/native_refs/official30_scoped_rows/ExampleUntangleWormsBrightField_ExampleUntangleWormsBrightField_wells_include_first1/images_a61f79afad70_ExampleUntangleWormsBrightField_native_cellprofiler/NonOverlappingWorms.csv": "9ade92229325f64cf9d2f2d404ac8654a04199721b054922f47e05be5028ad2e", + "benchmark/native_refs/official30_scoped_rows/ExampleUntangleWormsBrightField_ExampleUntangleWormsBrightField_wells_include_first1/native_cellprofiler_file_list.txt": "699d439b828ca3e0706a789521015dfc79ae6fd8192e4c37ae9c147b39dd2f85", + "benchmark/native_refs/official30_scoped_rows/ExampleUntangleWormsBrightField_ExampleUntangleWormsBrightField_wells_include_first1/native_cellprofiler_headless/ExampleUntangleWormsBrightField.cppipe": "cffb98c4f763a7656fc149d06edd2533f436416ab84810f8138fd9640fccc6bb", + "benchmark/native_refs/official30_scoped_rows/ExampleUntangleWorms_ExampleUntangleWorms_wells_include_first1/images_886125f8f337_ExampleUntangleWorms_native_cellprofiler/.cellprofiler_benchmark_reference.json": "4b59286575280b4fb6d7d7da4aff827a29cbecf810c9d5480795f5bfe7b69517", + "benchmark/native_refs/official30_scoped_rows/ExampleUntangleWorms_ExampleUntangleWorms_wells_include_first1/images_886125f8f337_ExampleUntangleWorms_native_cellprofiler/1649_1109_0003_Amp5-1_B_20070424_C01_w2_CB2F18CD-EDF0-4BCD-98CF-3A07E5A582FFoutlines.png": "3af795fe333ba75a120324267162f6e647dd48ba938d7f68af9e2a951aaa9cbd", + "benchmark/native_refs/official30_scoped_rows/ExampleUntangleWorms_ExampleUntangleWorms_wells_include_first1/images_886125f8f337_ExampleUntangleWorms_native_cellprofiler/1649_1109_0003_Amp5-1_B_20070424_C20_w2_5F64C597-735E-435D-B49C-2A07B9D6DFC3outlines.png": "5fb364ee092cbb112b0a790067d15814595f8f31e81889f5feaa5f58702556a0", + "benchmark/native_refs/official30_scoped_rows/ExampleUntangleWorms_ExampleUntangleWorms_wells_include_first1/images_886125f8f337_ExampleUntangleWorms_native_cellprofiler/Image.csv": "666590d051a3266922dbb8623b63b8e65efc4b9a87e8337c7628af4ec64376c1", + "benchmark/native_refs/official30_scoped_rows/ExampleUntangleWorms_ExampleUntangleWorms_wells_include_first1/images_886125f8f337_ExampleUntangleWorms_native_cellprofiler/OverlappingWorms.csv": "b3d754b7df1cf265875977067e251ea209e505ee0dcbcf4752fab500ffaf8ed0", + "benchmark/native_refs/official30_scoped_rows/ExampleUntangleWorms_ExampleUntangleWorms_wells_include_first1/native_cellprofiler_file_list.txt": "f9f2a939f212b570addf2e4217842844c5bbe4b646fcb76707705d8bc78d9b02", + "benchmark/native_refs/official30_scoped_rows/ExampleUntangleWorms_ExampleUntangleWorms_wells_include_first1/native_cellprofiler_headless/ExampleUntangleWorms.cppipe": "9fc8c49624dbb6796afa37794a957eb95177f0632891dc48f2ab12d7d2381c17", + "benchmark/native_refs/official30_scoped_rows/ExampleVitraImages_ExampleVitra_wells_include_first1/images_571b7d96aae0_ExampleVitra_native_cellprofiler/.cellprofiler_benchmark_reference.json": "4b59286575280b4fb6d7d7da4aff827a29cbecf810c9d5480795f5bfe7b69517", + "benchmark/native_refs/official30_scoped_rows/ExampleVitraImages_ExampleVitra_wells_include_first1/images_571b7d96aae0_ExampleVitra_native_cellprofiler/Cells.csv": "2ac1308e6bd6c3299ce34217916f3eb1846e1992215bc658658e947ab19d46fc", + "benchmark/native_refs/official30_scoped_rows/ExampleVitraImages_ExampleVitra_wells_include_first1/images_571b7d96aae0_ExampleVitra_native_cellprofiler/Channel 1-01-A-01-00_Outlined.tiff": "4c9caab6edec2642f60376a9b5257ab60d08adac602952b1528b868d0e6ae711", + "benchmark/native_refs/official30_scoped_rows/ExampleVitraImages_ExampleVitra_wells_include_first1/images_571b7d96aae0_ExampleVitra_native_cellprofiler/Cytoplasm.csv": "7c8c1139cea980114efe72dac52cfb2364a4dcc26904c3ef4c68e3297731e5df", + "benchmark/native_refs/official30_scoped_rows/ExampleVitraImages_ExampleVitra_wells_include_first1/images_571b7d96aae0_ExampleVitra_native_cellprofiler/Image.csv": "be1933d1d3df78ef586154cb4a24641b560f9d85755942fc1f905f160bfc9213", + "benchmark/native_refs/official30_scoped_rows/ExampleVitraImages_ExampleVitra_wells_include_first1/images_571b7d96aae0_ExampleVitra_native_cellprofiler/Nuclei.csv": "2201239ff8cf2042dc36ed4812b61a00cdee8311bcbed804b2cd0537ce82666b", + "benchmark/native_refs/official30_scoped_rows/ExampleVitraImages_ExampleVitra_wells_include_first1/native_cellprofiler_selected_source_workspace/_source/IllumDNA/VitraChannel2ILLUM.npy": "f95bcd9ea006a731eac68070808c225584b85834aea703356d61d471a318a075", + "benchmark/native_refs/official30_scoped_rows/ExampleVitraImages_ExampleVitra_wells_include_first1/native_cellprofiler_selected_source_workspace/_source/IllumProtein/VitraChannel1ILLUM.npy": "047d756eb3ccc55c4c850a85c0b9883ac683cc4cc415c0c9f9b41712ad9d650e", + "benchmark/native_refs/official30_scoped_rows/ExampleWoundHealing_ExampleWoundHealing_wells_include_first1/images_ea0a23a93a14_ExampleWoundHealing_native_cellprofiler/.cellprofiler_benchmark_reference.json": "4b59286575280b4fb6d7d7da4aff827a29cbecf810c9d5480795f5bfe7b69517", + "benchmark/native_refs/official30_scoped_rows/ExampleWoundHealing_ExampleWoundHealing_wells_include_first1/images_ea0a23a93a14_ExampleWoundHealing_native_cellprofiler/Image.csv": "5092109ba5104d090b3d6d95683d7fee23ae609de3ea874583d0ee5d59916849", + "benchmark/native_refs/official30_scoped_rows/ExampleWoundHealing_ExampleWoundHealing_wells_include_first1/native_cellprofiler_file_list.txt": "f00beb9e827b95803f59725b3dd7b98817fdb815bd4c0361300daeb8b5e3f2ab", + "benchmark/native_refs/official30_scoped_rows/ExampleWoundHealing_ExampleWoundHealing_wells_include_first1/native_cellprofiler_headless/ExampleWoundHealing.cppipe": "492f8be03ef821ebf56ac43023a956f368fd89f03abfdf9e5ed5cefe3ac790fb", + "benchmark/native_refs/official30_scoped_rows/ExampleYeastColonies_ExampleYeastColonies_wells_include_first1/images_d4157ddaf410_ExampleYeastColonies_native_cellprofiler/.cellprofiler_benchmark_reference.json": "c0f6eed26fd477ab9f98af2f19604f1927ce44a40985bdb79bb53b454936fbbc", + "benchmark/native_refs/official30_scoped_rows/ExampleYeastColonies_ExampleYeastColonies_wells_include_first1/images_d4157ddaf410_ExampleYeastColonies_native_cellprofiler/6-1_outlines.png": "359ecfa9aed57495df2b3896bcaa6d499762314ad8a2525b1ca14a5d7d0d340e", + "benchmark/native_refs/official30_scoped_rows/ExampleYeastColonies_ExampleYeastColonies_wells_include_first1/images_d4157ddaf410_ExampleYeastColonies_native_cellprofiler/Colonies.csv": "0efb44e46b42674c383ffd808cf83f5111826217ddadf5510e3c07ae29bcf9cd", + "benchmark/native_refs/official30_scoped_rows/ExampleYeastColonies_ExampleYeastColonies_wells_include_first1/images_d4157ddaf410_ExampleYeastColonies_native_cellprofiler/Image.csv": "0c65352424f6d5928b130d37b8d69f494faf55224c20ba6f17a98944c9a5b7fa", + "benchmark/native_refs/official30_scoped_rows/ExampleYeastColonies_ExampleYeastColonies_wells_include_first1/native_cellprofiler_file_list.txt": "efc7e3daaa355ae68efebf397620be8f51725929a50f3aed6fb94512cfcf1ef9", + "benchmark/native_refs/official30_scoped_rows/ExampleYeastColonies_ExampleYeastColonies_wells_include_first1/native_cellprofiler_headless/ExampleYeastColonies.cppipe": "dcd88ecaa4109da46752b15596f1caa3f41db96e3f6baa960c693d96c9fae436", + "benchmark/native_refs/official30_scoped_rows/ExampleYeastPatches_ExampleYeastPatches_wells_include_first1/images_5dcdc9c27278_ExampleYeastPatches_native_cellprofiler/.cellprofiler_benchmark_reference.json": "4b59286575280b4fb6d7d7da4aff827a29cbecf810c9d5480795f5bfe7b69517", + "benchmark/native_refs/official30_scoped_rows/ExampleYeastPatches_ExampleYeastPatches_wells_include_first1/images_5dcdc9c27278_ExampleYeastPatches_native_cellprofiler/1832-48hours-gal-1_ForcedOutline.png": "c03031031d98b5e391c77bc8345f07148543d25b60843ad3ce1206bba1e9f8fe", + "benchmark/native_refs/official30_scoped_rows/ExampleYeastPatches_ExampleYeastPatches_wells_include_first1/images_5dcdc9c27278_ExampleYeastPatches_native_cellprofiler/1832-48hours-gal-1_NaturalOutline.png": "d581dc4d03a11a5535ee237459c14603e80ebbbdf22a82abdb6e469769445954", + "benchmark/native_refs/official30_scoped_rows/ExampleYeastPatches_ExampleYeastPatches_wells_include_first1/images_5dcdc9c27278_ExampleYeastPatches_native_cellprofiler/Experiment.csv": "9010befceef019ce48bf8d6a0165269b8a497d7344d2bf21da3f541a191b3d58", + "benchmark/native_refs/official30_scoped_rows/ExampleYeastPatches_ExampleYeastPatches_wells_include_first1/images_5dcdc9c27278_ExampleYeastPatches_native_cellprofiler/FilterObjects.csv": "c096a4758c1816a807f15e5a97e1437953445f5f0637887200f95f6c5d73d09c", + "benchmark/native_refs/official30_scoped_rows/ExampleYeastPatches_ExampleYeastPatches_wells_include_first1/images_5dcdc9c27278_ExampleYeastPatches_native_cellprofiler/ForcedSpots.csv": "ae682b931aac32f5ea6c899953ccd72c4ee826e711153dad97b053a4d0ec3b65", + "benchmark/native_refs/official30_scoped_rows/ExampleYeastPatches_ExampleYeastPatches_wells_include_first1/images_5dcdc9c27278_ExampleYeastPatches_native_cellprofiler/Image.csv": "20e979345f3e0c2c52ce691430b84643070902f1416940c6b36a04cb1bb123ca", + "benchmark/native_refs/official30_scoped_rows/ExampleYeastPatches_ExampleYeastPatches_wells_include_first1/images_5dcdc9c27278_ExampleYeastPatches_native_cellprofiler/NaturalSpots.csv": "87da0ddc6b311e95e9ab11d162d62e85fec437a33eb07a6f01fd2f7cfc2b47d1", + "benchmark/native_refs/official30_scoped_rows/ExampleYeastPatches_ExampleYeastPatches_wells_include_first1/images_5dcdc9c27278_ExampleYeastPatches_native_cellprofiler/Prespots.csv": "d3500afcfb50d95a18eb391371e79897456bbf39489657f2ffa1551ecc7d6c3d", + "benchmark/native_refs/official30_scoped_rows/ExampleYeastPatches_ExampleYeastPatches_wells_include_first1/native_cellprofiler_file_list.txt": "b08566b61f65bfb22c329500259b205fc82e58249f94b27262aca9f079208882", + "benchmark/native_refs/official30_scoped_rows/ExampleYeastPatches_ExampleYeastPatches_wells_include_first1/native_cellprofiler_headless/ExampleYeastPatches.cppipe": "0a44f253afd1ab73ec99c77f3b91cdb09bf5d41b1fe22465ee4b1e46ce720066", + "benchmark/native_refs/official30_value_completion_20260914/CellProfiler4_benchmark_supplement_cp4_supplement_combine_objects_wells_include_first1/CombineObjects_f4072628fb1a_cp4_supplement_combine_objects_native_cellprofiler/.cellprofiler_benchmark_reference.json": "5adccc651a8610352acb88288c56f2d5de7845be1e0d9813cea1c76b6e87b03a", + "benchmark/native_refs/official30_value_completion_20260914/CellProfiler4_benchmark_supplement_cp4_supplement_combine_objects_wells_include_first1/CombineObjects_f4072628fb1a_cp4_supplement_combine_objects_native_cellprofiler/reference_labels__CombinedObjects.tiff": "f5a96f1e11f1c9a0a3a86388f100705d99765856bc668aba675c935563e32197", + "benchmark/native_refs/official30_value_completion_20260914/CellProfiler_tutorials_cp_tutorial_translocation_start_wells_include_first1/TranslocationData_cp_tutorial_translocation_start_native_cellprofiler/.cellprofiler_benchmark_reference.json": "e5ea45cd8328a430dc4e515c23242dbd2d0970a74493271aacca076278238c8b", + "benchmark/native_refs/official30_value_completion_20260914/CellProfiler_tutorials_cp_tutorial_translocation_start_wells_include_first1/TranslocationData_cp_tutorial_translocation_start_native_cellprofiler/reference_labels__Nuclei.tiff": "eaa520c6b780dcf61899b0018ae4f1e610b3cdcd9048cd3ec1202669c03ee6e2", + "benchmark/native_refs/official30_value_completion_20260914/ExampleIlluminationCorrection_ExampleIlluminationCorrection_Example1_EachMethod_wells_include_first1/images_ExampleIlluminationCorrection_Example1_EachMethod_native_cellprofiler/.cellprofiler_benchmark_reference.json": "331429c61ec577b7baec0beb6cc913cd4b6575b323a02a64aa00693918941194", + "benchmark/native_refs/official30_value_completion_20260914/ExampleIlluminationCorrection_ExampleIlluminationCorrection_Example1_EachMethod_wells_include_first1/images_ExampleIlluminationCorrection_Example1_EachMethod_native_cellprofiler/reference_image__CorrGreen.npy": "3c17148f0fad16738a23af55ac4e41692e6c2906808a7d83823331702b1b4f46", + "benchmark/native_refs/official30_value_completion_20260914/ExampleIlluminationCorrection_ExampleIlluminationCorrection_Example2_wells_include_first1/images_ExampleIlluminationCorrection_Example2_native_cellprofiler/.cellprofiler_benchmark_reference.json": "dd475748fcbfd645efef9e29f6a856e1b68096344ab4614dae2ff597523e95ca", + "benchmark/native_refs/official30_value_completion_20260914/ExampleIlluminationCorrection_ExampleIlluminationCorrection_Example2_wells_include_first1/images_ExampleIlluminationCorrection_Example2_native_cellprofiler/reference_labels__LargeBlockCorrectedNuclei.tiff": "f1bbed3348ce75d8d4158cad37c08ae4ab30d9e0a5dfbb9e259b906311811e45", + "benchmark/native_refs/official30_value_completion_20260914/ExampleIlluminationCorrection_ExampleIlluminationCorrection_Example2_wells_include_first1/images_ExampleIlluminationCorrection_Example2_native_cellprofiler/reference_labels__SmallBlockCorrectedNuclei.tiff": "d31aa4bc3e9782e646820b5f3bef6f91f4fbf78bf8ee46b5260cde78e558f700", + "benchmark/native_refs/official30_value_completion_20260914/ExampleIlluminationCorrection_ExampleIlluminationCorrection_Example2_wells_include_first1/images_ExampleIlluminationCorrection_Example2_native_cellprofiler/reference_labels__UncorrectedNuclei.tiff": "2f54cba8f1482ab54e8a21f71fb2a912ea8a260f8dae2bb5bccfb94994da5a84", + "benchmark/native_refs/official30_value_completion_20260914/ExampleIlluminationCorrection_ExampleIlluminationCorrection_Example3_wells_include_first1/images_ExampleIlluminationCorrection_Example3_native_cellprofiler/.cellprofiler_benchmark_reference.json": "37e74838f03ca2fcb0b941ad62f2bcf6ebc4082651dd04c577d7e0b760a03a79", + "benchmark/native_refs/official30_value_completion_20260914/ExampleIlluminationCorrection_ExampleIlluminationCorrection_Example3_wells_include_first1/images_ExampleIlluminationCorrection_Example3_native_cellprofiler/reference_image__ConvexHullCorrWorm.npy": "9ba1259e4ca007c06f14be6946e32a86307ec2dfb9952b73fa2cc253ccae8b3a", + "benchmark/native_refs/official30_value_completion_20260914/ExampleIlluminationCorrection_ExampleIlluminationCorrection_Example3_wells_include_first1/images_ExampleIlluminationCorrection_Example3_native_cellprofiler/reference_image__PolynomialCorrected.npy": "588e6f9951aa7ad220678dbb0053e98fb6d324e71aab089d1d828d891ee32f54", + "benchmark/native_synthetic_well_scaling.py": "de987613fe1ee2c1f8e29be505846595ec8f829f1d2148ca4b3d51e2304b2954", + "benchmark/openhcs_measured_run.py": "1e0cccc518fb6bec9c92ea8057dd7adfc7305b2d90a6da8f152c1b97601878ab", + "benchmark/own_neurite_blind_20260915_preparation.md": "9c4e9a9e1dca543c7a22649693045d94b7cdef41873e8f815de70637ea826df5", + "benchmark/pipelines/__init__.py": "a95462dd83b89a8b11e46ca9c1c0b9eee8d0934773dc55b2bc3ca6125768f8ef", + "benchmark/pipelines/bbbc021_nuclei_segmentation.py": "56166f2dadfdd8d7f1c7fe509129f15ad9ac086bd48ebe5860b3432d66304ea4", + "benchmark/pipelines/bbbc022_nuclei_segmentation.py": "c442e7586fbadc8cc975048fcad9403957d096ce0cfb8b8183ff0486db5498e8", + "benchmark/pipelines/cellprofiler_nuclei.py": "d7bbc254c82381edc2c82f22e0583c783574d36d9ed52819445f64d7d65925fc", + "benchmark/pipelines/cellprofiler_nuclei_cupy.py": "f92d88239172074f3b84f63b4c1faa02f5c10945cee89797214c20c74f727a9d", + "benchmark/pipelines/cellprofiler_nuclei_gpu.py": "780db7199ad37ac957d8688583c86d8cca75aa69ab219fc1b467e96470b065ae", + "benchmark/pipelines/cellprofiler_preprocess.py": "30179b93bd69cf0c8a3fc9b0751b577ec306b776ccee03daac6b229be2a6eef2", + "benchmark/pipelines/registry.py": "d6bb037e43aee45ad6041bb71710066bd6ce9b7e3d5d23d79e5186d9b08a10e0", + "benchmark/prepare_blind_neurite.py": "341587cc14bec627555cb70ddcbd858cce2f209f9bd611aa4045b460fe8e01ea", + "benchmark/progress.py": "d5ffd622416e9d954095a58d8aee816b5875473d87b75c785f91b65ed2cac3a8", + "benchmark/reference_exports/official30_value_completion_20260914/ExampleIlluminationCorrection_Example1_EachMethod.cppipe": "3879f7d442034a3603f4dabe44ba4a542d831156494fff0d4e37b64cbf2161ad", + "benchmark/reference_exports/official30_value_completion_20260914/ExampleIlluminationCorrection_Example1_EachMethod.reference_exports.json": "858fc86b72c13e0e641b971f2b8877a5a07ae4078df2671eff44725ec79b34c5", + "benchmark/reference_exports/official30_value_completion_20260914/ExampleIlluminationCorrection_Example2.cppipe": "fa2966b0cd36932e1fa5a853e3a5f4d272173f20c97b81636cdfeb31d79419a2", + "benchmark/reference_exports/official30_value_completion_20260914/ExampleIlluminationCorrection_Example2.reference_exports.json": "2b6c192627491f6b532e062624b7dcc5832eca2768c3ff10f7dfc5a1f25d3099", + "benchmark/reference_exports/official30_value_completion_20260914/ExampleIlluminationCorrection_Example3.cppipe": "f7ec9eb561730cc567986e6a50504a69f23a3ca78a1a033faba0b2fea52759fb", + "benchmark/reference_exports/official30_value_completion_20260914/ExampleIlluminationCorrection_Example3.reference_exports.json": "23509b73afd617981e8c06a107760dc0233dd5049aae028717de649ae929e80a", + "benchmark/reference_exports/official30_value_completion_20260914/cp4_supplement_combine_objects.cppipe": "25d76709b04f6eaeae234545948e9cb304b5d88a4988631fea0bc60113a71fe3", + "benchmark/reference_exports/official30_value_completion_20260914/cp4_supplement_combine_objects.reference_exports.json": "e2e7e2efd24a396c5bbb6fe5ec3d3a00fc74997d9c84b316459fe00c7bc7f5ac", + "benchmark/reference_exports/official30_value_completion_20260914/cp_tutorial_translocation_start.cppipe": "15d95afa1821526e5b58cc76fc33916ee0107ba5a3123cba33fa91ac319e5708", + "benchmark/reference_exports/official30_value_completion_20260914/cp_tutorial_translocation_start.reference_exports.json": "bae2691f4f71f2297ae85eecd9fe4c209047bd3b8c1b888c2b81a80e4ab456a4", + "benchmark/reports/__init__.py": "5257727a4b7318de6de26d52564a414fa02a93d1f14439b58ed6945344283547", + "benchmark/reports/autonomous_image_analysis_validation_tranche_20260917.md": "2a9ed4eb0417dcf403c7593ece8f217bcb086e44c09d5c460ba2a48071e71d54", + "benchmark/reports/cppipe_figures.py": "bf4132cabafca6f7e1d277d73ac8cbfd4f30141dcb1b6981bc5fc451da43725e", + "benchmark/reports/cppipe_scaling_figures.py": "047ed45f4a73a0cd90afc8a2b221895dec56e104b130b881b57025e995796276", + "benchmark/reports/independent_validation_preparation_20260917.md": "1042a818ee1216550553b5da8ebfed6e2a115027a135a3dc67e048a251764be0", + "benchmark/results/labmeeting_20260505/README.md": "51680197c044a5d5daecbd3ed7e489e1e46058caa08c019ee0bead32cdf5e4e3", + "benchmark/results/labmeeting_20260505/cppipe_cp_vs_openhcs_summary.csv": "25bf28d0bf7587ffa6b59883ae836197370a946974181d26866e2f932c4fba19", + "benchmark/results/labmeeting_20260505/cppipe_figures_20260505_v7/cppipe_accuracy.png": "7b3565fb46c6f7f3da79b45aa868e9b1b38524d8212dea1e20d873dee0f7597b", + "benchmark/results/labmeeting_20260505/cppipe_figures_20260505_v7/cppipe_accuracy.svg": "9ecd4d9c5be0907ec051af8e8beb5e26df1ce674e9ac2bbcef01f4e03b14ccb6", + "benchmark/results/labmeeting_20260505/cppipe_figures_20260505_v7/cppipe_accuracy_zoom.png": "15f2e309e6d3a81b5715a02484d41530f60190e77979054650ccd0c301066108", + "benchmark/results/labmeeting_20260505/cppipe_figures_20260505_v7/cppipe_accuracy_zoom.svg": "661aa6143bbef3966b5f0b5cbe5c3ae89dffccf8a529608174af291c149613cc", + "benchmark/results/labmeeting_20260505/cppipe_figures_20260505_v7/cppipe_comparison_metrics_long.csv": "e298d34a3a8e7f0fdf124059bdf784f13cb7e040a5fb43e9798cb6a2053145b4", + "benchmark/results/labmeeting_20260505/cppipe_figures_20260505_v7/cppipe_raw_seconds.png": "419a52a00adf37f2ad5c1960e12d463cfdc3fe202015f2326f291cfc58537603", + "benchmark/results/labmeeting_20260505/cppipe_figures_20260505_v7/cppipe_raw_seconds.svg": "e3f3184ad6dc2d7ba1965e864ca810403d19edd2861e60c7118383068f99b361", + "benchmark/results/labmeeting_20260505/cppipe_figures_20260505_v7/cppipe_raw_seconds_log.png": "214f215d3d0eb9e5fbc7e62bd99d56c811bf7f0c16f0f54a5b4ed71f13d2a485", + "benchmark/results/labmeeting_20260505/cppipe_figures_20260505_v7/cppipe_raw_seconds_log.svg": "dafdffc0e3c1c29520b01c5bca533a73ce322a8a7e320fa094880e21ff15cd8b", + "benchmark/results/labmeeting_20260505/cppipe_figures_20260505_v7/cppipe_speedup.png": "430ad689c9083ae857f847d7ad98341e7fa3ba834d21a5daa6b40a00365eb86a", + "benchmark/results/labmeeting_20260505/cppipe_figures_20260505_v7/cppipe_speedup.svg": "2f764837cfdef11d7a2a5f66ec65f55a42f12843dc6c6820194bab24b52cd05e", + "benchmark/results/labmeeting_20260505/cppipe_figures_20260505_v7/cppipe_speedup_log.png": "126bde3a80ead1931f2a84e892534cdbe11a1e964ea244eb59fc825fb7696a58", + "benchmark/results/labmeeting_20260505/cppipe_figures_20260505_v7/cppipe_speedup_log.svg": "5c428999f91c6c15c0c56c45b32dad601f6e92d09d34652b7c64b257c357d6a3", + "benchmark/results/labmeeting_20260505/throughput_2v16_samples_1v2_jobs_partial/throughput_batches.csv": "5af6b59ba253f193ac19a7babd5e0bbd9a4f8924e2fb2c954d22318f38173376", + "benchmark/results/labmeeting_20260505/throughput_2v16_samples_1v2_jobs_partial/throughput_jobs.csv": "9836b7b7c06045ae9a7b84808af61ecbcd95efc287b0e164c636793d53664d8a", + "benchmark/results/labmeeting_20260505/throughput_2v16_samples_1v2_jobs_partial/throughput_summary.csv": "fb5309e9a5ce28ca0a53d5c70cfb8e68b73360a0ebc2e0b8752bb55ce84f3898", + "benchmark/results/labmeeting_20260505/well_throughput_native_coloc_24w_1v2v3/ExampleColocalization/wells_24/workers_1/well_throughput_progress_events.csv": "6b2af2df6325a6b7e1238d6979a7cbf040855aab758d0976561b10b12f0c76c6", + "benchmark/results/labmeeting_20260505/well_throughput_native_coloc_24w_1v2v3/ExampleColocalization/wells_24/workers_1/well_throughput_step_timings.csv": "82178ee1eced32201c85ae89ddc75fd0331871da768e5507d45e971c0fa605aa", + "benchmark/results/labmeeting_20260505/well_throughput_native_coloc_24w_1v2v3/ExampleColocalization/wells_24/workers_1/well_throughput_worker_lanes.csv": "b619e13ecafbb80c12018cb18b8b866aeab304c3d6ef1dcb88a2946eebb85357", + "benchmark/results/labmeeting_20260505/well_throughput_native_coloc_24w_1v2v3/ExampleColocalization/wells_24/workers_2/well_throughput_progress_events.csv": "f3219e1e9ccc95f72e82ebc47fd17b47d52add4d315de294c32fd1866938dc58", + "benchmark/results/labmeeting_20260505/well_throughput_native_coloc_24w_1v2v3/ExampleColocalization/wells_24/workers_2/well_throughput_step_timings.csv": "7cd51b935d6aed535e4399a5eec0f8bc6ba0525dd7bc901b723231b274603411", + "benchmark/results/labmeeting_20260505/well_throughput_native_coloc_24w_1v2v3/ExampleColocalization/wells_24/workers_2/well_throughput_worker_lanes.csv": "83208040a6b48cef0296d63bd56e3fe6c06fd96d8ba4ccae072cca4652e612b0", + "benchmark/results/labmeeting_20260505/well_throughput_native_coloc_24w_1v2v3/ExampleColocalization/wells_24/workers_3/well_throughput_progress_events.csv": "e5a6a6a3737c1a4efa6e21f88242a8de8e8d71f07784029943309ff5de77a796", + "benchmark/results/labmeeting_20260505/well_throughput_native_coloc_24w_1v2v3/ExampleColocalization/wells_24/workers_3/well_throughput_step_timings.csv": "37da778817614f39665e1dee26be02b63a32b765f68c89b73baa9a56dcae8596", + "benchmark/results/labmeeting_20260505/well_throughput_native_coloc_24w_1v2v3/ExampleColocalization/wells_24/workers_3/well_throughput_worker_lanes.csv": "ac0d424450cc4b369d36739373e9be33dc9fb654adeeb938c84b7d794959bf85", + "benchmark/results/labmeeting_20260505/well_throughput_native_coloc_24w_1v2v3/well_throughput.csv": "35aeef7992b64abb3ad5aa7b4f2d22a3e135c9a7ce95df07ff5cc6f36c74416c", + "benchmark/results/labmeeting_20260505/well_throughput_native_coloc_2v16/well_throughput.csv": "0854ac349626a7828e87454bcd44248ef395215829b8c6f1a52ebbcb8a22804d", + "benchmark/results/labmeeting_20260513/README.md": "e507c90f0cbc65ef3ad8b33099b770e64fdcba424cba7c55b797c1fd627ac434", + "benchmark/results/labmeeting_20260513/official30_well_throughput/data/core_scaling_well_throughput.csv": "3b799feec3a7c024bf878b2c08c3848b8d5320e50d87387a999eeb34a429d491", + "benchmark/results/labmeeting_20260513/official30_well_throughput/data/single_process_summary.csv": "38747ee2a7a0a8fcd361cc5269f76aaf3150396a6b2ff7988edd11aa12345488", + "benchmark/results/labmeeting_20260513/official30_well_throughput/data/wells_per_core_2c3c4c.csv": "1d6db1948a510855e24d52bb316e36c704fe0176d8623eb99dc1346fa396b6de", + "benchmark/results/labmeeting_20260513/official30_well_throughput/data/wells_per_core_4c_6wpc_8wpc.csv": "5938eefdba5aefa50b55f320120253f1096502e2ba74973ae38d05ecb3f5ab2f", + "benchmark/results/labmeeting_20260513/official30_well_throughput/figures/01_parity_by_pipeline.csv": "2ac591b1865756ac6f54066ad3f978e6ab047bdaf4676c5fbc37811291366969", + "benchmark/results/labmeeting_20260513/official30_well_throughput/figures/01_parity_by_pipeline.png": "c1465fdab74fe6c98beedad84b845e81d39a344c54e2bb5a56b64455263a7fd9", + "benchmark/results/labmeeting_20260513/official30_well_throughput/figures/01_parity_by_pipeline.svg": "605558d1ab275cdd39806f193312807b00a085d75560e45b9a6f91892f483a1d", + "benchmark/results/labmeeting_20260513/official30_well_throughput/figures/02_core_scaling_by_pipeline_plus_average_long.csv": "dac4de8bf11d427030ed1e5069b6bb47d4f8d489fee7f4224ab2d86fca8caf58", + "benchmark/results/labmeeting_20260513/official30_well_throughput/figures/02_core_scaling_by_pipeline_plus_average_speedup.png": "88b4a869f837282280b79ca95d929a556112f276612764adc9f3b0f85b1fba28", + "benchmark/results/labmeeting_20260513/official30_well_throughput/figures/02_core_scaling_by_pipeline_plus_average_speedup.svg": "dc266b7aa72bd230609f09621a5d2dab11c7798e4e53576c7e1942efaa586763", + "benchmark/results/labmeeting_20260513/official30_well_throughput/figures/02_core_scaling_by_pipeline_plus_average_speedup_log.png": "a8f5d3dc4b5f03ce0b5c78f3d9c4a916fee346bf1df809c1bcf506bb2965d8ed", + "benchmark/results/labmeeting_20260513/official30_well_throughput/figures/02_core_scaling_by_pipeline_plus_average_speedup_log.svg": "a55897c712c9ce9fec5eaaa5e2f30290d5e052e10612d855d5024777b2fe1e97", + "benchmark/results/labmeeting_20260513/official30_well_throughput/figures/03_core_scaling_average_with_pipeline_points_speedup.png": "933ea666c623db0b43f1b32505a258fe12ec07c3042797bf2ec5c08fc6fbc7ff", + "benchmark/results/labmeeting_20260513/official30_well_throughput/figures/03_core_scaling_average_with_pipeline_points_speedup.svg": "32837549844ad1422bcdf0b5d700a4c51ac4a330b4615295e2e3474448908650", + "benchmark/results/labmeeting_20260513/official30_well_throughput/figures/03_core_scaling_average_with_pipeline_points_speedup_log.png": "5aff106f3814bdcb4764588819a93efbe856a8262f159c4b814c015fbddb5e58", + "benchmark/results/labmeeting_20260513/official30_well_throughput/figures/03_core_scaling_average_with_pipeline_points_speedup_log.svg": "d987f7a000fb5448c151b90585bb0915827a90550f398352ea37eae59731213d", + "benchmark/results/labmeeting_20260513/official30_well_throughput/figures/04_core_scaling_average_with_pipeline_points_ram.png": "048d50ecb36a6731e65b48fe2c1d9a68a1481dfc37606223874e22b99b6af6e0", + "benchmark/results/labmeeting_20260513/official30_well_throughput/figures/04_core_scaling_average_with_pipeline_points_ram.svg": "433b722561af91dde339705f2a0da040db22e670e38ef9f0c27a9374772984e4", + "benchmark/results/labmeeting_20260513/official30_well_throughput/figures/04_core_scaling_by_pipeline_plus_average_ram.png": "703da7f0d220c0068e4356399c67d19f240129a5a11b23a23c4d451e9f864924", + "benchmark/results/labmeeting_20260513/official30_well_throughput/figures/04_core_scaling_by_pipeline_plus_average_ram.svg": "9f7f74db3afc9e96c7579c3d8e1c40a7327a3f3345ec64abe0a1c79aff3926ad", + "benchmark/results/labmeeting_20260513/official30_well_throughput/figures/04_core_scaling_by_pipeline_plus_average_ram_log.png": "b07a3eb5528229351f20c1a5655888cd78b32c620de1a73590902302eb084426", + "benchmark/results/labmeeting_20260513/official30_well_throughput/figures/04_core_scaling_by_pipeline_plus_average_ram_log.svg": "54604f5c04f69ce0b2fe3093373b2deb8ed26cc2dcaa0cdacbbd2373ea391182", + "benchmark/results/labmeeting_20260513/official30_well_throughput/figures/05_speedup_by_core_and_wells_per_core_summary.csv": "63473a7219d7a42b2f48f0f872dde98c943c1416ab1a9403852fe39ab7a7cef3", + "benchmark/results/labmeeting_20260513/official30_well_throughput/figures/05_speedup_by_core_and_wells_per_core_summary.md": "027d5b2ee710b087a415e353b63e43a13dc345f932d1493985482988e1d41118", + "benchmark/results/labmeeting_20260513/official30_well_throughput/figures/05_speedup_summary_by_core_and_wells_per_core.png": "338c1ec72020fd12809289fe997e30e9bac41edf4aa120a076fb27c68a57457d", + "benchmark/results/labmeeting_20260513/official30_well_throughput/figures/05_speedup_summary_by_core_and_wells_per_core.svg": "f37d086ca32e7aa13b16ed72e088bfdb21fe2a295f543215c4f788db6d5ddf84", + "benchmark/results/labmeeting_20260513/official30_well_throughput/figures/05_speedup_summary_by_core_and_wells_per_core_log.png": "141da994ec06ec12b44dbf1f8d0b44b8efc7d6842443c4c75682c7d05e0e7e52", + "benchmark/results/labmeeting_20260513/official30_well_throughput/figures/05_speedup_summary_by_core_and_wells_per_core_log.svg": "e4c2993bf24d292717b6ba2e7feaa1f27b592e5bcbafc90b499a0cf854aef6f6", + "benchmark/results/labmeeting_20260513/official30_well_throughput/figures/06_module_coverage_by_abstraction.png": "9300f12f6317d99d0e8fb144550a9a6a5575869f68ad1570f8f660323fc7b875", + "benchmark/results/labmeeting_20260513/official30_well_throughput/figures/06_module_coverage_by_abstraction.svg": "f46e1b5876b1eca9398359f8d1354f2126db507c5ad525f71a44d3b02bce24be", + "benchmark/results/labmeeting_20260513/official30_well_throughput/figures/06_module_coverage_by_abstraction_modules.csv": "9091dbd94ff3b2233085a1644e0db87bb105c1896d8c266ee2e6fec55505abbe", + "benchmark/results/labmeeting_20260513/official30_well_throughput/figures/06_module_coverage_by_abstraction_modules.md": "b35f2a1f1ba2dc7c7bd8e49ebb2e08283c8e977e80566abb45e13a9840ed93c2", + "benchmark/results/labmeeting_20260513/official30_well_throughput/figures/module_coverage_absorbed_modules.csv": "fbc5be2c4e93fa853739b4f8b312735a52e70d2e7064c07807070f1a8861f7a6", + "benchmark/results/labmeeting_20260513/official30_well_throughput/figures/module_coverage_cppipe_modules.csv": "2b0e620a3a040af285b4947e3e16b0afea90443f5c4fef15c51c61a7a6601c6d", + "benchmark/results/labmeeting_20260513/official30_well_throughput/figures/module_coverage_cppipe_settings.csv": "12fa844a0682baee44cc16b5a87a192a5f9c2173e54fa02c823d18c30283f087", + "benchmark/results/labmeeting_20260513/official30_well_throughput/figures/module_coverage_semantic_families.csv": "5b65b12926b490571339ef72df7270a179c72f5c9821a644958856d43be78b0c", + "benchmark/results/labmeeting_20260513/official30_well_throughput/figures/presentation_figure_index.md": "6c285b83ca84ab132bec1622f43a93db7653541a072a16562230f5cd97f62959", + "benchmark/results/matched_batch_concurrency_20260923/README.md": "60b8fe63b52a2d10de8a87cbf4d0d9cbe49212e747b0a6814240181f2cd67ae6", + "benchmark/results/matched_batch_concurrency_20260923/candidate_evidence/-1/measured_pipeline_receipt.json": "9866c2429dc84a9bf6eac52d1ef7f442ba2498f1d5c08bb2cc0d30cf759dae2f", + "benchmark/results/matched_batch_concurrency_20260923/candidate_evidence/-1/submitted_global_config.py.txt": "bc1af8635d4850ed123463438c15c7eb9b0c44cdc031f81e3932c9ebc6759006", + "benchmark/results/matched_batch_concurrency_20260923/candidate_evidence/-1/submitted_pipeline.py.txt": "c2eaa0a5de13dc877d51d9f6c1ad39199f4dfb1a658ae3013b8bb832393e75af", + "benchmark/results/matched_batch_concurrency_20260923/candidate_evidence/-1/zmq_results_summary.json": "c63b27185b82823f97e24b91f9dfecf20c1e0a803b5a39a5ed78c38e863af1ae", + "benchmark/results/matched_batch_concurrency_20260923/candidate_evidence/0/measured_pipeline_receipt.json": "2401ce5a473af0746c9f9c74c39153d8fa5be8071f3dd51d44d301b4fbb6011e", + "benchmark/results/matched_batch_concurrency_20260923/candidate_evidence/0/observation.pkl": "5a9bcb31f2fd33a5c91611af7da99c161317aba890442383831f2cde9e0c8f97", + "benchmark/results/matched_batch_concurrency_20260923/candidate_evidence/0/submitted_global_config.py.txt": "3e25715a0ff1de90782d8006eea3c3e90b6abcf2b7481b363b1ed722eb69e0f6", + "benchmark/results/matched_batch_concurrency_20260923/candidate_evidence/0/submitted_pipeline.py.txt": "adce89e9fab8e4267da3b242ccd33085d45d7c63630fe898a3966fcb015e3626", + "benchmark/results/matched_batch_concurrency_20260923/candidate_evidence/0/zmq_results_summary.json": "d0fe961c00a9f1d666159d8d96ac5eae6f9f36b6e308833281cab96232273955", + "benchmark/results/matched_batch_concurrency_20260923/candidate_evidence/1/measured_pipeline_receipt.json": "6af567d895f43ea2ff27712d9ade92db08b51b638f7eb1c05dd5713e71f03d8a", + "benchmark/results/matched_batch_concurrency_20260923/candidate_evidence/1/observation.pkl": "c863f9586ac64a0e582da98eb9e39cf16bfcafc081c7c85eed5a0abcdf2f8ba8", + "benchmark/results/matched_batch_concurrency_20260923/candidate_evidence/1/submitted_global_config.py.txt": "cb907e38b4a1a4ef993c1039603d30446bc30cb6e34da91ccf5d9c44628f1cf5", + "benchmark/results/matched_batch_concurrency_20260923/candidate_evidence/1/submitted_pipeline.py.txt": "bc5d590f7cb47967d8ac5ca80eedd164a4e915685ba027b8fed279f36112aabe", + "benchmark/results/matched_batch_concurrency_20260923/candidate_evidence/1/zmq_results_summary.json": "9ed64039ba39df1c66689b556a472a157bbc8096e8fd6c276d721532488ec3b3", + "benchmark/results/matched_batch_concurrency_20260923/candidate_evidence/2/measured_pipeline_receipt.json": "e65ff346a549f15d45c19544bb5e0d69f9070bd338609c300068853b89d341cd", + "benchmark/results/matched_batch_concurrency_20260923/candidate_evidence/2/observation.pkl": "0dc5d4edd08bc2c6c85fba1b30ba9f4c47da8c4f423cf8cda0e25b59c3f4ee62", + "benchmark/results/matched_batch_concurrency_20260923/candidate_evidence/2/submitted_global_config.py.txt": "e19f897158809ec122e39cac05398fba1b65ba328a8ff48f47bd77795f383e31", + "benchmark/results/matched_batch_concurrency_20260923/candidate_evidence/2/submitted_pipeline.py.txt": "56ef5e650382b307d1e695b3db41e7827050751b895cd91fba55834355725837", + "benchmark/results/matched_batch_concurrency_20260923/candidate_evidence/2/zmq_results_summary.json": "542822f3bd0fd93411fe42edfa382a11e72ffa9b81a365c168ae9524f09a5955", + "benchmark/results/matched_batch_concurrency_20260923/candidate_report.json": "ee971d5c0b16d518a7b5019689235542df9cdf0a11037be51e341934f8ad4d54", + "benchmark/results/matched_batch_concurrency_20260923/equivalence_policy.json": "00a8c59aa435ef235aee9e5a05cbfc32f03f0ca9f6fd0aff89085d91118b49bf", + "benchmark/results/matched_batch_concurrency_20260923/imported_pipeline.py.txt": "5f3129bfa97e5f542e6d56912e965f04ba9f96ca6ea0707800a62606d095f412", + "benchmark/results/matched_batch_concurrency_20260923/input_inventory.json": "76d136a7abca37e1543b521e15e31f2ba8ebf5964608f65e1c470cdaa7c76f83", + "benchmark/results/matched_batch_concurrency_20260923/native_preparation/native_cellprofiler_headless/Translocation_final.cppipe": "3992ce36c9d5b6c42c7b6b6d5e1fcef59563d9e244d38613b257c848a4e10090", + "benchmark/results/matched_batch_concurrency_20260923/native_report.json": "5fd212d18255a77ff0263d77cae015ada81845b835ff6b7bc6fdb38ddd93ffd9", + "benchmark/results/matched_batch_concurrency_20260923/native_request.json": "7ba2ee85e6732acb1cf15d06d83e3e4131cd21b8971377a25aa2bc3b58cb1d51", + "benchmark/results/matched_batch_concurrency_20260923/native_shards/0_stderr.log": "1bfef94ea190e1c68ffc1d9320e86bcdec847795d6b9cd408097422784d581fc", + "benchmark/results/matched_batch_concurrency_20260923/native_shards/1_stderr.log": "0f18fce288e4880a1051025ce1b28631b0f68befe8c4d1300bae5f9c988dfab6", + "benchmark/results/matched_batch_concurrency_20260923/native_shards/equivalence.json": "e47c16d8ec7ea8f1376d729e775c37f4b2c06645064a3a340ea20970dd226c78", + "benchmark/results/matched_batch_concurrency_20260923/native_shards/reports.json": "32a2c15221059d81ca23dfd86b7d8443aefa8210b4334fdb28b830cfbf255bac", + "benchmark/results/matched_batch_concurrency_20260923/native_shards/request_0.json": "178e897c0b5dcd68d7d70e0aeff0f475b35c85034646e8b54fab27f61ecfb1ac", + "benchmark/results/matched_batch_concurrency_20260923/native_shards/request_1.json": "8d02d9255123c21aa0ec40b9f8fde70ff2a7f16bd95d1828286b8c60e0d470c0", + "benchmark/results/matched_batch_concurrency_20260923/native_stderr.log": "1bfef94ea190e1c68ffc1d9320e86bcdec847795d6b9cd408097422784d581fc", + "benchmark/results/matched_batch_concurrency_20260923/pilot_provenance.json": "895cb585b20dddbe9f6448fc022c01c309dbc98d0fdd0b208a929197ae89d08b", + "benchmark/results/matched_batch_concurrency_20260923/report.json": "5da28c0be5a78866488411333f0cd06bfd9d1152a822a23e04ce2c1949c9fb8e", + "benchmark/results/matched_batch_concurrency_fork_20260923/README.md": "bb846d0cff0071644dde0978285005d4acbaedf3d9ccbd86c5fa0fe47aa9ed42", + "benchmark/results/matched_batch_concurrency_fork_20260923/candidate_evidence/-1/measured_pipeline_receipt.json": "0083fac025224c4a82a8606e0cb9bf8c8b6eb479898ddc9aee3b183f99fac451", + "benchmark/results/matched_batch_concurrency_fork_20260923/candidate_evidence/-1/submitted_global_config.py.txt": "dcdc73cbfed64fd31f961b9a4fdc92ee821aca10ad4cdaacc3e8e07e901a740b", + "benchmark/results/matched_batch_concurrency_fork_20260923/candidate_evidence/-1/submitted_pipeline.py.txt": "cc3158f25ee58d716faa1c10c67ef49d2a7afd8b9e36b6bd657aa4dc15d5504d", + "benchmark/results/matched_batch_concurrency_fork_20260923/candidate_evidence/-1/well_throughput_progress_events.csv": "fd6d55f4b93b648a57948cd629ef53e0783a173ac5ae8ba78f1c021ba0fc2807", + "benchmark/results/matched_batch_concurrency_fork_20260923/candidate_evidence/-1/well_throughput_step_timings.csv": "544336b987f2c64bdcec61d88dff40f1fbfb628a9ac07d93391ce07a3a1ebc2e", + "benchmark/results/matched_batch_concurrency_fork_20260923/candidate_evidence/-1/well_throughput_worker_lanes.csv": "26ef335eba0a99c12c0e3e5951be4ce18d3a2d551c6b698203d1cb9653f72dd9", + "benchmark/results/matched_batch_concurrency_fork_20260923/candidate_evidence/-1/zmq_results_summary.json": "76a9512d8e528d325c426e719651684cdf9c71a571524d6fecd82a897552b95e", + "benchmark/results/matched_batch_concurrency_fork_20260923/candidate_evidence/0/measured_pipeline_receipt.json": "7bd5af04aa29740ef6af81474e8b00009c61abb0c1540e53b2c942783ee6bc58", + "benchmark/results/matched_batch_concurrency_fork_20260923/candidate_evidence/0/runtime_outcomes.pkl.gz": "4fcebaa56d982fb0462e82a237dc2a08771343ac2f3706dc637948f7b0cf2c61", + "benchmark/results/matched_batch_concurrency_fork_20260923/candidate_evidence/0/submitted_global_config.py.txt": "a23c1ad757b04d053a7e93672431bad46eeab38cac4cd0b8ce4fabec1ce9ff53", + "benchmark/results/matched_batch_concurrency_fork_20260923/candidate_evidence/0/submitted_pipeline.py.txt": "c9308c39f21ec04ab7687264cf22d962691cd888298813658f48277c9e5b2fa3", + "benchmark/results/matched_batch_concurrency_fork_20260923/candidate_evidence/0/well_throughput_progress_events.csv": "ee3e23732629f8287ba47a9dd74b6e67f8919c5ce70f4fbb08610092614e2954", + "benchmark/results/matched_batch_concurrency_fork_20260923/candidate_evidence/0/well_throughput_step_timings.csv": "904007268550dd212f560e856c7766751048a601d784766f8691341a1d31cdc0", + "benchmark/results/matched_batch_concurrency_fork_20260923/candidate_evidence/0/well_throughput_worker_lanes.csv": "6461af40c8430e4bbbc6aee48635a6752914908a9d12cffd4d08d494bc6c22cd", + "benchmark/results/matched_batch_concurrency_fork_20260923/candidate_evidence/0/zmq_results_summary.json": "8334e761e1f3913f02051d86f39174225ab8825910d3170a138b9052e7519255", + "benchmark/results/matched_batch_concurrency_fork_20260923/candidate_evidence/1/measured_pipeline_receipt.json": "4470b9200832aa5a681f5269a299fb3af4772afefee0e49c8e6ab988a0daaa7a", + "benchmark/results/matched_batch_concurrency_fork_20260923/candidate_evidence/1/runtime_outcomes.pkl.gz": "4c21fc7629036cf7e5e24f5b77a32ae2a07426b32beb01cf7d1c25fee57f9699", + "benchmark/results/matched_batch_concurrency_fork_20260923/candidate_evidence/1/submitted_global_config.py.txt": "1c855b0e7d1e26ae42d6fa0fb85cdb073f5a86359b6a224dbd5d25701acc1910", + "benchmark/results/matched_batch_concurrency_fork_20260923/candidate_evidence/1/submitted_pipeline.py.txt": "e896b861d9fb3b90485f8d66e7a3f27cac9610e35c96ef78567e04b707710a23", + "benchmark/results/matched_batch_concurrency_fork_20260923/candidate_evidence/1/well_throughput_progress_events.csv": "55b111df9d55a1007eb5e119a5c1eba89119c21b4c77e17b7618bb3013e7d1f1", + "benchmark/results/matched_batch_concurrency_fork_20260923/candidate_evidence/1/well_throughput_step_timings.csv": "8a50bb8297975d2724808670d8982230a672c0f6608c741ba3179dc28580ff9a", + "benchmark/results/matched_batch_concurrency_fork_20260923/candidate_evidence/1/well_throughput_worker_lanes.csv": "9c5f7fe1c61a52526f2e523dd844d39a9d9fe25035cf6743ac3aefe6d497877c", + "benchmark/results/matched_batch_concurrency_fork_20260923/candidate_evidence/1/zmq_results_summary.json": "f8cf711bad9840c4239394c640f53aeb9a810e998d954e4e43d5be06518b5b06", + "benchmark/results/matched_batch_concurrency_fork_20260923/candidate_evidence/2/measured_pipeline_receipt.json": "8d3f002dbc32484d64e970d8656455396bba49384f16a1d8813f1e0a8f3bce19", + "benchmark/results/matched_batch_concurrency_fork_20260923/candidate_evidence/2/runtime_outcomes.pkl.gz": "df29c72f4d4a012aa8beb25f4af1c06fd34b64dd0ec3cd88777b7f6ea549fefc", + "benchmark/results/matched_batch_concurrency_fork_20260923/candidate_evidence/2/submitted_global_config.py.txt": "8e3ec609e6d94a7c3c30922b1a65a50acb25f34428bcd2cff78135560ed78f86", + "benchmark/results/matched_batch_concurrency_fork_20260923/candidate_evidence/2/submitted_pipeline.py.txt": "62de8041a654ffd823020783c47124e58b16753e45c2959f7046dda9cd6a2847", + "benchmark/results/matched_batch_concurrency_fork_20260923/candidate_evidence/2/well_throughput_progress_events.csv": "5ea8e78db6513305d9d8e91e122224385ae321b74b8215bd68acae02e54bd55a", + "benchmark/results/matched_batch_concurrency_fork_20260923/candidate_evidence/2/well_throughput_step_timings.csv": "001cb532bc19c5ada6ffb94a922d5ad3beab71cb1871beb203bf43cd8cf4ee1e", + "benchmark/results/matched_batch_concurrency_fork_20260923/candidate_evidence/2/well_throughput_worker_lanes.csv": "34e98ec0bd5dc30a1b09d4c1bb1aeba85191f4b2c85270ff9669329d1baf9b0b", + "benchmark/results/matched_batch_concurrency_fork_20260923/candidate_evidence/2/zmq_results_summary.json": "6e3aa985ddbd3c70687559cd927772f100acd6e93da4b453da5d674634a32ac0", + "benchmark/results/matched_batch_concurrency_fork_20260923/candidate_report.json": "5c47c4d0c8b81b5d548b49ec474c22e585f44ef0781424ab508850d7b425e04e", + "benchmark/results/matched_batch_concurrency_fork_20260923/equivalence_policy.json": "00a8c59aa435ef235aee9e5a05cbfc32f03f0ca9f6fd0aff89085d91118b49bf", + "benchmark/results/matched_batch_concurrency_fork_20260923/imported_pipeline.py.txt": "5f3129bfa97e5f542e6d56912e965f04ba9f96ca6ea0707800a62606d095f412", + "benchmark/results/matched_batch_concurrency_fork_20260923/native_execution.cppipe": "3992ce36c9d5b6c42c7b6b6d5e1fcef59563d9e244d38613b257c848a4e10090", + "benchmark/results/matched_batch_concurrency_fork_20260923/native_report.json": "f51d1ba3f09a308c8548347178cc3e5b9641327d21d74aaf7b4a0defa08ab6bf", + "benchmark/results/matched_batch_concurrency_fork_20260923/native_request.json": "c08455531b719b407b976417fbfa2b9ebdfb686ee888c9e8d17fd847f56652ec", + "benchmark/results/matched_batch_concurrency_fork_20260923/native_shards/0_stderr.log": "1bfef94ea190e1c68ffc1d9320e86bcdec847795d6b9cd408097422784d581fc", + "benchmark/results/matched_batch_concurrency_fork_20260923/native_shards/1_stderr.log": "0f18fce288e4880a1051025ce1b28631b0f68befe8c4d1300bae5f9c988dfab6", + "benchmark/results/matched_batch_concurrency_fork_20260923/native_shards/equivalence.json": "852ec06a3d52c267733cfc94b5f9915ea18730a2ba0dae9d8731c146a342d9ba", + "benchmark/results/matched_batch_concurrency_fork_20260923/native_shards/reports.json": "fd3a3e44d14295352b4c2877d53d6addc1b6ad97d847bdd36acf26b301fafdf1", + "benchmark/results/matched_batch_concurrency_fork_20260923/native_shards/request_0.json": "f891535796d897329a337d43a249cdba513642fc12d3b1f163f9a96716062b3b", + "benchmark/results/matched_batch_concurrency_fork_20260923/native_shards/request_1.json": "f63a333884c76ffcbd241fadd19826c49b816b1a68b50cb092b549cebf72c826", + "benchmark/results/matched_batch_concurrency_fork_20260923/native_stderr.log": "1bfef94ea190e1c68ffc1d9320e86bcdec847795d6b9cd408097422784d581fc", + "benchmark/results/matched_batch_concurrency_fork_20260923/pilot_provenance.json": "4ad80909a00a32dd67bdb919293800a02c015d1437aec2bc4c5bd81c419b6583", + "benchmark/results/matched_batch_concurrency_fork_20260923/report.json": "6dc6375fe8284b8171cab9af7dba3c65637ebef64f30056acd7610a752e3b172", + "benchmark/results/matched_batch_concurrency_fork_20260923/source_workspace_metadata.json": "aff9aa0b2d88404a3a6c38c1c1949ba0d1504753bf2b7a93d14f88142602b9fb", + "benchmark/results/matched_batch_pilot_20260915/README.md": "a166608b61e1809b6cd3e80a3dc0f5bd1f7eac87e65fc66ea1cb6c8d544e472f", + "benchmark/results/matched_batch_pilot_20260915/candidate_probe.json": "b8876f6f986f15a3e727b08b134a6545b7fb4bfadab98b231c6edde7bb21141f", + "benchmark/results/matched_batch_pilot_20260915/design.md": "9aa0ae40df73c80c3af94af43ad3f13568d4d56313b04e17057142405974b75f", + "benchmark/results/matched_batch_pilot_20260915/native_report.json": "bd6bacda979f7c1502565471d4fbcc449dfd27294d57b660ee5d77d357a93c14", + "benchmark/results/matched_batch_pilot_20260915/native_request.json": "134ed45e59f38120e3811cadfce6edbb2e24cd17c07a7e12dcf5cc6b8bc44c56", + "benchmark/results/matched_batch_pilot_20260915/pilot_driver.py": "3e96ef4275a25e72fee905b73fcd91c859ec878e26d3d9f51fcde72ca93028f6", + "benchmark/results/matched_batch_pilot_20260915/receipt.json": "e92e3ecbc1c0ea0db2a393d7ce1e5c2141df82ebd96b75916b842686439a00d9", + "benchmark/results/matched_batch_pilot_v2_20260923/README.md": "27e71b9cf67eca46790ad262d016f6d523d3d885d320f7518702ddf5e2629c00", + "benchmark/results/matched_batch_pilot_v2_20260923/candidate_evidence/-1/measured_pipeline_receipt.json": "c86ba66f43f194472ff28f49ce97559ce5182fe26061d72c4898ad90d92784b2", + "benchmark/results/matched_batch_pilot_v2_20260923/candidate_evidence/-1/submitted_global_config.py.txt": "c36e49f1fbf34cbbe4b4cfbb6ccd50b93d785c37b01c4a2c0aea91adb2179e51", + "benchmark/results/matched_batch_pilot_v2_20260923/candidate_evidence/-1/submitted_pipeline.py.txt": "580f0bb142048d758eee1bb88fdb18ea45a45e36328cf36253dce8e5c077006b", + "benchmark/results/matched_batch_pilot_v2_20260923/candidate_evidence/-1/zmq_results_summary.json": "59b17135e44da0a99c73680f5f0d495308441c641f609018401cbdc73f82b2d5", + "benchmark/results/matched_batch_pilot_v2_20260923/candidate_evidence/0/measured_pipeline_receipt.json": "789af27f2c388f6a4104655ce933ac6ee23892fcaa05fd8d5ca269a5a8e3ce18", + "benchmark/results/matched_batch_pilot_v2_20260923/candidate_evidence/0/submitted_global_config.py.txt": "82448464297c2ef2ca6701aeb032daaf29a5e0a73bcf98bcc23b5c63bb333b3a", + "benchmark/results/matched_batch_pilot_v2_20260923/candidate_evidence/0/submitted_pipeline.py.txt": "5417d3ef4ceb4dd0be9d1b6d6a051bf3449dbeff59549fb302f38e627b0fd81b", + "benchmark/results/matched_batch_pilot_v2_20260923/candidate_evidence/0/zmq_results_summary.json": "36b533a4e4145f93b7d037fd5f5863d82ce0fc40c1ce5996efe49bc67442ac9d", + "benchmark/results/matched_batch_pilot_v2_20260923/candidate_evidence/1/measured_pipeline_receipt.json": "5fa320171d0373bb188340f0768a3559bb711f7b792f993bd50e31f3d52b9853", + "benchmark/results/matched_batch_pilot_v2_20260923/candidate_evidence/1/submitted_global_config.py.txt": "36a7537d372cbfd7e50f11563379dc58fbcf5b35f9dbed3b1ed66be9e6758bfd", + "benchmark/results/matched_batch_pilot_v2_20260923/candidate_evidence/1/submitted_pipeline.py.txt": "69831b8ed1dc5921a0c3045e7e62ab676807e3ccf663a9113134189e14b1e09f", + "benchmark/results/matched_batch_pilot_v2_20260923/candidate_evidence/1/zmq_results_summary.json": "ca4f221cbcf2323271f5a6c73e489d4db34f740d4cceca79aa5bb6cfc003ab05", + "benchmark/results/matched_batch_pilot_v2_20260923/candidate_evidence/2/measured_pipeline_receipt.json": "d76e3a25787775b48f759232a530083026edc5191f48e2e49814d80c68505c90", + "benchmark/results/matched_batch_pilot_v2_20260923/candidate_evidence/2/submitted_global_config.py.txt": "83036fe3f0210e750d891202897bbf235e65c8b3db0599f7f83cc4d8303cb632", + "benchmark/results/matched_batch_pilot_v2_20260923/candidate_evidence/2/submitted_pipeline.py.txt": "daa78c85f51b44204a9ff04b6f2fe17a19f72c724e36c4bf79026eb56e49007b", + "benchmark/results/matched_batch_pilot_v2_20260923/candidate_evidence/2/zmq_results_summary.json": "8a96218334240be93d4ec7772eab9d50bda1c32c30e6bd7686fa27f77a5f39ee", + "benchmark/results/matched_batch_pilot_v2_20260923/candidate_report.json": "22fdb17e9d60f0a905f858fa9aac3c053b859a09acd141259fa7a592c4702796", + "benchmark/results/matched_batch_pilot_v2_20260923/native_pipeline.cppipe": "3992ce36c9d5b6c42c7b6b6d5e1fcef59563d9e244d38613b257c848a4e10090", + "benchmark/results/matched_batch_pilot_v2_20260923/native_report.json": "bfbfac3c0a07816baa507336c7795195737eb668ec0c966ba39f19ca53d2d8a3", + "benchmark/results/matched_batch_pilot_v2_20260923/native_request.json": "2f1f8fa389ab1b8a9f45a95b6decce809dbf31b57353d16d9a97b2f0d5862914", + "benchmark/results/matched_batch_pilot_v2_20260923/pilot_provenance.json": "edc1fc18ea0a58b852c0ae0969fc27cae27f75f618afef8deccbb26b4a79026b", + "benchmark/results/matched_batch_pilot_v2_20260923/report.json": "fbd7c078690dd0a0db12ca21b68fe724e90cd0a881c8a27ef2b0c507e2080416", + "benchmark/results/matched_batch_pilot_v2_20260923/strict_confirmation/candidate_evidence/-1/measured_pipeline_receipt.json": "ed4cc36936157639c5372f7db3970612fd81af9163df907eb8d53143ea00ff9b", + "benchmark/results/matched_batch_pilot_v2_20260923/strict_confirmation/candidate_evidence/-1/submitted_global_config.py.txt": "4e8121fddf7db72432b7715ef8e205406b02f86ad0a827da87b9c7f62039ef2f", + "benchmark/results/matched_batch_pilot_v2_20260923/strict_confirmation/candidate_evidence/-1/submitted_pipeline.py.txt": "8196fb569bf5bf67545ac4495eb49ae9307689f2ad75544f14b8ade84869ed86", + "benchmark/results/matched_batch_pilot_v2_20260923/strict_confirmation/candidate_evidence/-1/zmq_results_summary.json": "f8957c2160eca3f40d7a42f73c3985647e921c8f070a169686eb25c65011831c", + "benchmark/results/matched_batch_pilot_v2_20260923/strict_confirmation/candidate_evidence/0/measured_pipeline_receipt.json": "1d38731b138dd0fb27055a0a5eeebf8fb1c27b78bc2f3bc04ab23d57ee9cca56", + "benchmark/results/matched_batch_pilot_v2_20260923/strict_confirmation/candidate_evidence/0/submitted_global_config.py.txt": "d9f27b5a6c2413e0f4ad3eda849a7713d42bf1de6d9b63fcc65ba48abcf6509a", + "benchmark/results/matched_batch_pilot_v2_20260923/strict_confirmation/candidate_evidence/0/submitted_pipeline.py.txt": "51de99b51ff19707ef852264b37ebd7520db77e1620fdeb930de9eefe1a65784", + "benchmark/results/matched_batch_pilot_v2_20260923/strict_confirmation/candidate_evidence/0/zmq_results_summary.json": "4d56fd2b5591e14fca632b199f0ff65b95927c92b4b8b3487cf2930ec8c5ea8d", + "benchmark/results/matched_batch_pilot_v2_20260923/strict_confirmation/candidate_report.json": "7e2bad5c1d555b8f552ea2e408b05c7f7d4fd190ad560e1d09d97bd8a9d1dd04", + "benchmark/results/matched_batch_pilot_v2_20260923/strict_confirmation/equivalence_policy.json": "00a8c59aa435ef235aee9e5a05cbfc32f03f0ca9f6fd0aff89085d91118b49bf", + "benchmark/results/matched_batch_pilot_v2_20260923/strict_confirmation/native_report.json": "b48874f78a4d1efe8135a3106ddef8325a9bf5fcc9ed67a87faf26c3014dd07d", + "benchmark/results/matched_batch_pilot_v2_20260923/strict_confirmation/native_request.json": "df651776f933dd547a9cfdfeb389889161527450732da06f89f1bf74c018e932", + "benchmark/results/matched_batch_pilot_v2_20260923/strict_confirmation/pilot_provenance.json": "563ea0c75d2437b7e9af622f67a86e36425f0cc9f2a0dfe2bed099b6cf47fa99", + "benchmark/results/matched_batch_pilot_v2_20260923/strict_confirmation/report.json": "e83e3001d29b87e36188154aabffd3a697a0b7a0c10b142d2932d07a76452ae4", + "benchmark/results/matched_bbbc022_20260923_rc5/README.md": "507c79f3ed2b1aec29d283df51b95bdf847b762111ef09ab5105d5baf9f42a28", + "benchmark/results/matched_bbbc022_20260923_rc5/candidate_evidence/-1/measured_pipeline_receipt.json": "e1e5a944b9d82cc6073755945abfd8b80233b227b745c07c3cdad7d7a25602aa", + "benchmark/results/matched_bbbc022_20260923_rc5/candidate_evidence/-1/observation.pkl": "90ca7dde3cbf832403fa86d8e2020439e041f03402bf8f82365dfae5a453474a", + "benchmark/results/matched_bbbc022_20260923_rc5/candidate_evidence/-1/submitted_global_config.py.txt": "b139a9640a8a2be14d9782a21b14ef71fc8762375c4a5fe0afd3185bbdccbb7a", + "benchmark/results/matched_bbbc022_20260923_rc5/candidate_evidence/-1/submitted_pipeline.py.txt": "5df03db5eb47c5f714b1163c1d325fa2a62a38cb396ddc8779200dd2db8a9eb2", + "benchmark/results/matched_bbbc022_20260923_rc5/candidate_evidence/-1/well_throughput_progress_events.csv": "e770d6fc773589f897db479a1299ef93c1f1e070834d509b0af62eb77a034ce2", + "benchmark/results/matched_bbbc022_20260923_rc5/candidate_evidence/-1/well_throughput_step_timings.csv": "10fe559789de7d288f580ce82aceb73c2402a204279c12228e6f9ecc64768bfe", + "benchmark/results/matched_bbbc022_20260923_rc5/candidate_evidence/-1/well_throughput_worker_lanes.csv": "0812d86c57c5d70e779f3e2192ba7105fefe0d2fc2d6b826dbc95b3eb51608ec", + "benchmark/results/matched_bbbc022_20260923_rc5/candidate_evidence/-1/zmq_results_summary.json": "92b21d78830abb6e82acd72f3c906d7b2af64c2b15501381e63e7f531886c778", + "benchmark/results/matched_bbbc022_20260923_rc5/candidate_evidence/0/measured_pipeline_receipt.json": "f4017ece68a2289232deafc4fd30d6c6b32e43ba681fb9812e6ed069ce5bf8ec", + "benchmark/results/matched_bbbc022_20260923_rc5/candidate_evidence/0/observation.pkl": "68c10b08756f0038c705e1f71e0b6c402144794209ff01602bb471b610f6c199", + "benchmark/results/matched_bbbc022_20260923_rc5/candidate_evidence/0/submitted_global_config.py.txt": "2487e2067157ae5087f31adffed7a11233f95ae1428ba37a64b2ee596729f6df", + "benchmark/results/matched_bbbc022_20260923_rc5/candidate_evidence/0/submitted_pipeline.py.txt": "cbbeaaacf8460718694703346ec04572ee998da456997a5fcf3cab413bc857b5", + "benchmark/results/matched_bbbc022_20260923_rc5/candidate_evidence/0/well_throughput_progress_events.csv": "e68e5101956a7f25bb9b080b5193f1a5f82435051500a8a208e0db260bb29c22", + "benchmark/results/matched_bbbc022_20260923_rc5/candidate_evidence/0/well_throughput_step_timings.csv": "bf42e9fe69d20b7b768d2256488f624ce496d1af72566902066d2c1c4114b308", + "benchmark/results/matched_bbbc022_20260923_rc5/candidate_evidence/0/well_throughput_worker_lanes.csv": "5e7a191aaab21f15bb38d00ce3e32951c71e62838cc420db08d25d4006cd8352", + "benchmark/results/matched_bbbc022_20260923_rc5/candidate_evidence/0/zmq_results_summary.json": "2adf72fe2b7866af28aa1bc1f16f958040f30f26bedf4778883795ea3be15e44", + "benchmark/results/matched_bbbc022_20260923_rc5/candidate_report.json": "1761878cf8f03b7e27abd968f4216b40a9ffb5aef4cf35f3cbdd39623f5befc1", + "benchmark/results/matched_bbbc022_20260923_rc5/equivalence_policy.json": "00a8c59aa435ef235aee9e5a05cbfc32f03f0ca9f6fd0aff89085d91118b49bf", + "benchmark/results/matched_bbbc022_20260923_rc5/imported_pipeline.py.txt": "0e4c0714de41ce665725a63bf47385ce71f135b61b9c8503b40db5256862fad4", + "benchmark/results/matched_bbbc022_20260923_rc5/native_preparation/native_cellprofiler_headless/BBBC022_Analysis_Final.cppipe": "cf77de35d244d264a64e87b37bd5da3871c64b227c8f297b2be39d87d315f582", + "benchmark/results/matched_bbbc022_20260923_rc5/native_report.json": "b1bfa2bf2e24d6ffbd8801626392fd31bf609ebdf9e6d83209c4b872de5ec3a1", + "benchmark/results/matched_bbbc022_20260923_rc5/native_request.json": "4266132b87bfed663c84aecb9744e43543a3f6b534b909c36db46a5613c4174f", + "benchmark/results/matched_bbbc022_20260923_rc5/native_shards/0_stderr.log": "1ff3a3b82ae3762e019bd3f796a6b3374850e4dea9dd4f31503df16204f6da5f", + "benchmark/results/matched_bbbc022_20260923_rc5/native_shards/0_stdout.log": "a3565fdcda5edfcc323bef428d5352d535a184f23b9c66c24763c53a879385f4", + "benchmark/results/matched_bbbc022_20260923_rc5/native_shards/1_stderr.log": "622dfab87d0b78fbb8d6ae056e550b141713c4fd3ede992b6489abb0deb04ae5", + "benchmark/results/matched_bbbc022_20260923_rc5/native_shards/1_stdout.log": "33de2d6b0d07eabc330206edbc19c5b5f1069708be2e3908f993765c03b94e62", + "benchmark/results/matched_bbbc022_20260923_rc5/native_shards/equivalence.json": "d68e9727efe1662a7a8048728e80cae706ce14bb85b820ae666fe9633f8112c1", + "benchmark/results/matched_bbbc022_20260923_rc5/native_shards/reports.json": "cf2049d58327afb9a3abc92e1021d54d64ce9c85ee1df0e1f7e995cabf79fd35", + "benchmark/results/matched_bbbc022_20260923_rc5/native_shards/request_0.json": "619c39740e931dfeeb2d5c6690484f74dde442e78ce6fb59177e28cb7e7df099", + "benchmark/results/matched_bbbc022_20260923_rc5/native_shards/request_1.json": "d63562c5134ff475c63d0c5adcf87f6cf202516a2d843d4b5047359da584948a", + "benchmark/results/matched_bbbc022_20260923_rc5/native_stderr.log": "1ff3a3b82ae3762e019bd3f796a6b3374850e4dea9dd4f31503df16204f6da5f", + "benchmark/results/matched_bbbc022_20260923_rc5/native_stdout.log": "2ed77a8b1bdd5a30e52e480afb048e534cb7568d1620a271204e022773ed2c2f", + "benchmark/results/matched_bbbc022_20260923_rc5/pilot_provenance.json": "1ca278b4992268fa139fc092b18a8763a766faede15d49a736daa8739dbe292f", + "benchmark/results/matched_bbbc022_20260923_rc5/report.json": "1ee3f66fe77009574b6faadf4dfb98232b41fdfd83a2714bcfafb9d7a9170958", + "benchmark/results/official30_existing_image_refresh_20260914/current_endpoint_run/candidate_outputs/ExampleIlluminationCorrection_Example1_AllMethod/Illum.npy": "1e3255fc4af5a6fa45b4efbd656ae5187b8ec8c55c3a71c99766ebd351d6bd4f", + "benchmark/results/official30_existing_image_refresh_20260914/current_endpoint_run/candidate_outputs/cp_tutorial_translocation_final/A01_s001_w1_z001_t001_Overlay.tiff": "4bdc8e3891c6b1212388ec6b16bd2e132079c6237ed1b19498708f43e1447b1f", + "benchmark/results/official30_existing_image_refresh_20260914/current_endpoint_run/endpoint_logs/openhcs_zmq_server_port_23844_1789406886652996.log": "b2eb329a7596a5ac246d81fe4ea79ea6a6b70ba151ee98e9acf3244709cd6bd8", + "benchmark/results/official30_existing_image_refresh_20260914/current_endpoint_run/endpoint_logs/openhcs_zmq_server_port_23844_1789406920299344.log": "3ce30abd164706b959a1f3985dc033a9f93f603de092527453ffee5ef2b712d8", + "benchmark/results/official30_existing_image_refresh_20260914/current_endpoint_run/image_comparisons.csv": "3de4a7119f16164d2a951aa086d2bd5bb3a28df8eb459bc4d3c5e4bb92708956", + "benchmark/results/official30_existing_image_refresh_20260914/current_endpoint_run/observations.csv": "22f1b74a3351d0ae5401421c4f9bf9cdcb0345e02e48467f8c7bba85d43152c9", + "benchmark/results/official30_existing_image_refresh_20260914/current_endpoint_run/observations.jsonl": "41c395c5efc6c36665251add6a5fd1d8928b273313d6694e31dddfae887bfa5b", + "benchmark/results/official30_existing_image_refresh_20260914/current_endpoint_run/phase_timing.csv": "5f66956c197b3042aaf0d6324d1db43ec15695054a89bca7bd9687761b39a0c8", + "benchmark/results/official30_existing_image_refresh_20260914/current_endpoint_run/run_environment.json": "8f46ea996b6d17edad8ee593ff2a52b73ae1bc5cd79487a6b2af1bb64ebb244e", + "benchmark/results/official30_existing_image_refresh_20260914/current_endpoint_run/suite_metadata.json": "08602c90d5e4feae9686bdb5b763af8702e4ca7e35167572a990b468a9c161ef", + "benchmark/results/official30_existing_image_refresh_20260914/current_endpoint_run/summary.csv": "f3253c93ffb565eaf39e0471743187cbc2df15881f0816d873fe4292109d4554", + "benchmark/results/official30_existing_image_refresh_20260914/native_markers/cp_tutorial_advanced_segmentation_final.cellprofiler_benchmark_reference.json": "064541e52c452470cdec69dbac694aacad0aa9f6fbe1c43e5d815a792fb8405e", + "benchmark/results/official30_existing_image_refresh_20260914/native_markers/cp_tutorial_translocation_final.cellprofiler_benchmark_reference.json": "930c22f3c57c3ea5d2c9e1e4bf37c33217fbb74f53b4e53809aefd58e83bd2de", + "benchmark/results/official30_existing_image_refresh_20260914/stale_endpoint_run/fresh_pair/observations.csv": "270d5cf592b653465e1279e0045f5fa0eb0d5fee8b630cb990f9ec874deb076f", + "benchmark/results/official30_existing_image_refresh_20260914/stale_endpoint_run/fresh_pair/observations.jsonl": "c182b0b2a81fd5677ea46c63ec7421b688ad5dd40516c200ba7be86ea3b4fce9", + "benchmark/results/official30_existing_image_refresh_20260914/stale_endpoint_run/fresh_pair/phase_timing.csv": "216efe6ef84d275944949a047646653eb3f23b0cd0ef1cd54bb90847affe73d1", + "benchmark/results/official30_existing_image_refresh_20260914/stale_endpoint_run/fresh_pair/suite_metadata.json": "b3e26ab55c44b76a163f4c28a79cb7d16dc3ba2e2f5a8267dd165bb64d76e96c", + "benchmark/results/official30_existing_image_refresh_20260914/stale_endpoint_run/fresh_pair/summary.csv": "6bd827ac8f0a1ebbb75f42f0976f23fe929bb2452a46fbb23f24114235bd85b4", + "benchmark/results/official30_existing_image_refresh_20260914/stale_endpoint_run/image_comparisons.csv": "4902d565323bc979fb3a7391b57c7db7722e682ab52100774e8bfe737d909e2e", + "benchmark/results/official30_existing_image_refresh_20260914/stale_endpoint_run/retained_allmethod/observations.csv": "b12340ded2ea449b9ab4490f3af3cc20d876820891aa2fd50841836593c82e07", + "benchmark/results/official30_existing_image_refresh_20260914/stale_endpoint_run/retained_allmethod/observations.jsonl": "f160d2a571d0aaca792f2c926a4e4c79f3713104f485d2cf4a7d160fb1df9d15", + "benchmark/results/official30_existing_image_refresh_20260914/stale_endpoint_run/retained_allmethod/phase_timing.csv": "ba42b5806c07cceb3bd29974bd461e946a958dd32dbb81183337c2913a3f8c00", + "benchmark/results/official30_existing_image_refresh_20260914/stale_endpoint_run/retained_allmethod/suite_metadata.json": "6b32d1ca251e8276901f5dcabfc27fa24496d424b70fe8bcb590b4bc04f7969b", + "benchmark/results/official30_existing_image_refresh_20260914/stale_endpoint_run/retained_allmethod/summary.csv": "79f391269dc8b02c43d64334a463bfc3978ca2dd18eff2b50d256daba5503d6a", + "benchmark/results/official30_grid_shrink_regression_20260914/ExampleImagingFlowCytometryObjectsInGrid_openhcs.py.txt": "ba0a589e4eccff9a5fd9bd6de634890584921feb18720a0a4e2fbc66f25101cf", + "benchmark/results/official30_grid_shrink_regression_20260914/candidate_output/BF_cells_on_grid.csv": "9017cea6750607d456f73aa733024c91347502f27fc19cb5041455b0eaec174f", + "benchmark/results/official30_grid_shrink_regression_20260914/endpoint_logs/openhcs_zmq_server_port_23951_1789412266964266.log": "21d16c5557208c4d8a1881c04690ca3caaeb555fd42837eed41977786f4018ce", + "benchmark/results/official30_grid_shrink_regression_20260914/observations.csv": "e9241b436259a2d9a505fa1da592348caa51062ebddcff08d7e1f00012a3af40", + "benchmark/results/official30_grid_shrink_regression_20260914/observations.jsonl": "a0937dc5a01f22bb43dfee67944fc3ff47e505acf1004f524c74a28650b01c38", + "benchmark/results/official30_grid_shrink_regression_20260914/phase_timing.csv": "cf782533e09b73e75dbef961234a4775ea24cda8bebf8327f80508f1329344c5", + "benchmark/results/official30_grid_shrink_regression_20260914/run_environment.json": "02126009a3a96d6e975dcb01fb5eb3fe7a3c697608d714fd52381c63a2e3c745", + "benchmark/results/official30_grid_shrink_regression_20260914/suite_metadata.json": "e2ad92513dae6e44ffac91257ef23bac70ab5ba8f01e00aaab863afafaaf1a1b", + "benchmark/results/official30_grid_shrink_regression_20260914/summary.csv": "e504a8498d89f2ea88b1e955a992e630436352b7bc33b9324acf99d427a9b6be", + "benchmark/results/official30_reference_root_audit_20260914/reference_inventory.csv": "d17005e72abf61e803d0045e10448d743fd33c16e7dd1dfba7d0524326a2c68f", + "benchmark/results/official30_unified_value_comparison_20260916/README.md": "833ef86fc39e6adbb5ef7ea53217e735ae4154fc01ef96a6832ddc65f9fe27d8", + "benchmark/results/official30_unified_value_comparison_20260916/observations.csv": "b09fa5fd6926afa093e7eef47b26950458eab8afc31b6e029246a61ec6a41677", + "benchmark/results/official30_unified_value_comparison_20260916/observations.jsonl": "ee7f6862e1713ee55630d62226343d1162285b39975d98a68608b73d5e50e451", + "benchmark/results/official30_unified_value_comparison_20260916/phase_timing.csv": "bd2cec4749ac63e1bfd0a046ff9166ee2ca8e79c659dcadf15aaf19645c52f66", + "benchmark/results/official30_unified_value_comparison_20260916/reference_inventory.csv": "42c4001b156733d62a5936142263c9b1db169b30a2064e1e0bebfa9236f581ac", + "benchmark/results/official30_unified_value_comparison_20260916/summary.csv": "8858fd821172a13ff3914f3ee78451f32451472ba2c185df54712cc62ab33226", + "benchmark/results/official30_value_completion_20260914/committed_fixed_endpoint_run/artifact_comparisons.csv": "a2673a9edbe6193455aa760a90c6220b8c5ec50314e13a536bca99dc1f359dd4", + "benchmark/results/official30_value_completion_20260914/committed_fixed_endpoint_run/candidate_outputs/ExampleIlluminationCorrection_Example1_EachMethod/reference_image__CorrGreen.npy": "3c17148f0fad16738a23af55ac4e41692e6c2906808a7d83823331702b1b4f46", + "benchmark/results/official30_value_completion_20260914/committed_fixed_endpoint_run/candidate_outputs/ExampleIlluminationCorrection_Example2/reference_labels__LargeBlockCorrectedNuclei.tiff": "48e0ff1c313ce3e7154453d5b8ec53db9fcb6fb3ec8321f6eb5958c77e4b201f", + "benchmark/results/official30_value_completion_20260914/committed_fixed_endpoint_run/candidate_outputs/ExampleIlluminationCorrection_Example2/reference_labels__SmallBlockCorrectedNuclei.tiff": "0b3270a092b0882d2c34effdd28d62edb9cd0f594dda592fe0e05845c5abba09", + "benchmark/results/official30_value_completion_20260914/committed_fixed_endpoint_run/candidate_outputs/ExampleIlluminationCorrection_Example2/reference_labels__UncorrectedNuclei.tiff": "4543f3afec804f201126e640bae070fd4e44700ac7b0de99dec13e038b42377a", + "benchmark/results/official30_value_completion_20260914/committed_fixed_endpoint_run/candidate_outputs/ExampleIlluminationCorrection_Example3/reference_image__ConvexHullCorrWorm.npy": "9ba1259e4ca007c06f14be6946e32a86307ec2dfb9952b73fa2cc253ccae8b3a", + "benchmark/results/official30_value_completion_20260914/committed_fixed_endpoint_run/candidate_outputs/ExampleIlluminationCorrection_Example3/reference_image__PolynomialCorrected.npy": "8875da63a072c8f2de4f5cb707d779a064b663bf69aa0f304fe6fa74d4ff57a6", + "benchmark/results/official30_value_completion_20260914/committed_fixed_endpoint_run/candidate_outputs/cp4_supplement_combine_objects/reference_labels__CombinedObjects.tiff": "2d1994f6d02100c4f5e77fc3c077d14d873bad8b5a48023966bdbb6a06bb83e4", + "benchmark/results/official30_value_completion_20260914/committed_fixed_endpoint_run/candidate_outputs/cp_tutorial_translocation_start/reference_labels__Nuclei.tiff": "7cbac0da81e073728ee86f7265b960d1d90b75ece750b92c1bdfcbad99ffbdfd", + "benchmark/results/official30_value_completion_20260914/committed_fixed_endpoint_run/endpoint_logs/openhcs_zmq_server_port_23850_1789409446631210.log": "d0bd0975b748ed28d245db48d50150b83930b75f20347333671f7308cdc7fd8e", + "benchmark/results/official30_value_completion_20260914/committed_fixed_endpoint_run/endpoint_logs/openhcs_zmq_server_port_23850_1789409479132474.log": "2f7c6cd5eeb193fe97b7a067687896f2e0172cb9f41ebf8674f4a4e0137d366f", + "benchmark/results/official30_value_completion_20260914/committed_fixed_endpoint_run/endpoint_logs/openhcs_zmq_server_port_23850_1789409505323215.log": "61ee37213ec0a08fd21e4daabc9d171f9fd63ae1475b0e0305360c0788d693b7", + "benchmark/results/official30_value_completion_20260914/committed_fixed_endpoint_run/endpoint_logs/openhcs_zmq_server_port_23850_1789409525315168.log": "34a387c0d0fd5eba460ae8500a1623ea9c5b16a73b170034e301a88ea86c3dfc", + "benchmark/results/official30_value_completion_20260914/committed_fixed_endpoint_run/endpoint_logs/openhcs_zmq_server_port_23850_1789409543626042.log": "70d5337711f93a6e153fba48dabd46d92f356508627cf02bb6f15f716bdeb59a", + "benchmark/results/official30_value_completion_20260914/committed_fixed_endpoint_run/module_coverage_absorbed_modules.csv": "2acf6a4e7385137fceafad7a3591e93d3302ebc85fec14057d5be8af334e9f1e", + "benchmark/results/official30_value_completion_20260914/committed_fixed_endpoint_run/module_coverage_cppipe_modules.csv": "9294a9c694d85f1f2676cb33f41431d66a4986cfb7f07432d3a2015423521c1e", + "benchmark/results/official30_value_completion_20260914/committed_fixed_endpoint_run/module_coverage_cppipe_settings.csv": "02764a949ea6b3bb7037a65091a3dff2d715adbeffe998dcda43f78ac226c05f", + "benchmark/results/official30_value_completion_20260914/committed_fixed_endpoint_run/module_coverage_summary.json": "acdc4005dfc26d8a51b943a8bb43a6378cc667d68ebf60c7124c2d23dbca24b6", + "benchmark/results/official30_value_completion_20260914/committed_fixed_endpoint_run/observations.csv": "f6d6d0781c3afd330fb0bb9b4590f4ac4f3d3db2ef6a14b727f06c580e8cd8a3", + "benchmark/results/official30_value_completion_20260914/committed_fixed_endpoint_run/observations.jsonl": "7c3cbaac6e201995ef124a175e77538061a486b73810f3172ae1ec7fbb6b1179", + "benchmark/results/official30_value_completion_20260914/committed_fixed_endpoint_run/phase_timing.csv": "b2b4c193576e46200c238c0943c1343b67c740a51aa78f08205a7da8e5243325", + "benchmark/results/official30_value_completion_20260914/committed_fixed_endpoint_run/run_environment.json": "d361b9ee4345447ded8ad908cd7766351ff1ce61a5203e00e15e53dab8622541", + "benchmark/results/official30_value_completion_20260914/committed_fixed_endpoint_run/suite_metadata.json": "26b1bddf9a3e9c61b00784e81e55f7d3d55536d300eeb8c3ffa74bf91dc79e56", + "benchmark/results/official30_value_completion_20260914/committed_fixed_endpoint_run/summary.csv": "3b5f444d8f830e38effbe204d648039defea05dfffca8e83e300c396b460ce36", + "benchmark/results/official30_value_completion_20260914/current_endpoint_run/artifact_comparisons.csv": "85cc6483ceae8e7b7ff950209f298d33de9320eabe05a7e128764beb5fbb1568", + "benchmark/results/official30_value_completion_20260914/current_endpoint_run/candidate_outputs/ExampleIlluminationCorrection_Example1_EachMethod/reference_image__CorrGreen.npy": "3c17148f0fad16738a23af55ac4e41692e6c2906808a7d83823331702b1b4f46", + "benchmark/results/official30_value_completion_20260914/current_endpoint_run/candidate_outputs/ExampleIlluminationCorrection_Example2/reference_labels__LargeBlockCorrectedNuclei.tiff": "48e0ff1c313ce3e7154453d5b8ec53db9fcb6fb3ec8321f6eb5958c77e4b201f", + "benchmark/results/official30_value_completion_20260914/current_endpoint_run/candidate_outputs/ExampleIlluminationCorrection_Example2/reference_labels__SmallBlockCorrectedNuclei.tiff": "0b3270a092b0882d2c34effdd28d62edb9cd0f594dda592fe0e05845c5abba09", + "benchmark/results/official30_value_completion_20260914/current_endpoint_run/candidate_outputs/ExampleIlluminationCorrection_Example2/reference_labels__UncorrectedNuclei.tiff": "ca7d7941ef6beb2695c941c2274412650e393f380141fca70dfb242f5e4109e6", + "benchmark/results/official30_value_completion_20260914/current_endpoint_run/candidate_outputs/ExampleIlluminationCorrection_Example3/reference_image__ConvexHullCorrWorm.npy": "fb4a60bdedfb1164a12a7fcaab6a8d951bd6be35b8816f5cabb35d372df80ee7", + "benchmark/results/official30_value_completion_20260914/current_endpoint_run/candidate_outputs/ExampleIlluminationCorrection_Example3/reference_image__PolynomialCorrected.npy": "35f818f06250b6bd4b747027526cf8cd6b96eb56ae3df8e89471294843caa61a", + "benchmark/results/official30_value_completion_20260914/current_endpoint_run/candidate_outputs/cp4_supplement_combine_objects/reference_labels__CombinedObjects.tiff": "a42fdaebb6358e22d1089869c8ca19933acd0f5288d8f7643b0cefdd3683570b", + "benchmark/results/official30_value_completion_20260914/current_endpoint_run/candidate_outputs/cp_tutorial_translocation_start/reference_labels__Nuclei.tiff": "357890970564fc2750467ddece6ab9e7ef45977cc0b10ced3033624313681e75", + "benchmark/results/official30_value_completion_20260914/current_endpoint_run/endpoint_logs/openhcs_zmq_server_port_23843_1789406516482568.log": "86fd2e0ce55635c12f33ebc85f4d492aa2001c93d6adcb16e48345554820ac8c", + "benchmark/results/official30_value_completion_20260914/current_endpoint_run/endpoint_logs/openhcs_zmq_server_port_23843_1789406538957050.log": "5ce8af41470538bb4cdc8add5ef3788672ac8e75b0bd0c28e2281840bb21fe0e", + "benchmark/results/official30_value_completion_20260914/current_endpoint_run/endpoint_logs/openhcs_zmq_server_port_23843_1789406563469953.log": "d211d41c61fa4db6d9dab768537b91dbe628fe00e4448e54a9d20442704f55d8", + "benchmark/results/official30_value_completion_20260914/current_endpoint_run/endpoint_logs/openhcs_zmq_server_port_23843_1789406582510098.log": "e496703016e3a09513855b550caa3244d574a3724291f49f8a5d54dc504900e3", + "benchmark/results/official30_value_completion_20260914/current_endpoint_run/endpoint_logs/openhcs_zmq_server_port_23843_1789406600832518.log": "b8e1ae6c47158f6d76ae19733a1e6110debba60a5f0a53ce1ee22f36491774f6", + "benchmark/results/official30_value_completion_20260914/current_endpoint_run/observations.csv": "0cb87c3b785c01f0e1d9f11b5053269db2b2d570e2e20f987e7f5e065d5acdcf", + "benchmark/results/official30_value_completion_20260914/current_endpoint_run/observations.jsonl": "ba798d976b0031a46d18f2948240d553e7134cec355ff11450c9565bbcc1af24", + "benchmark/results/official30_value_completion_20260914/current_endpoint_run/phase_timing.csv": "00d1fd2f4c471f3ff3d05a2a03fffc72f98cbad47940320619c449fc6d53cbaf", + "benchmark/results/official30_value_completion_20260914/current_endpoint_run/run_environment.json": "1802aba67ee4820c548f050745d368b297652e182a8ecd05bec46949a9096385", + "benchmark/results/official30_value_completion_20260914/current_endpoint_run/suite_metadata.json": "45e5680f1d7033cc0e865f286cfffa4b0b3c43de9c51b0e6bf479ea8642df39b", + "benchmark/results/official30_value_completion_20260914/current_endpoint_run/summary.csv": "26a9200f34b97f78cd2b3ef82feab3f3c86eab8403b0d289b36463347f8a424f", + "benchmark/results/official30_value_completion_20260914/example3_intermediate_diagnostic/artifact_comparisons.csv": "b52085641deb680bda7daea3e3d5513224d8fb66e2556dfc8e12f210ea17cebc", + "benchmark/results/official30_value_completion_20260914/example3_intermediate_diagnostic/candidate_outputs/reference_image__ConvexHullCorrWorm.npy": "9ba1259e4ca007c06f14be6946e32a86307ec2dfb9952b73fa2cc253ccae8b3a", + "benchmark/results/official30_value_completion_20260914/example3_intermediate_diagnostic/candidate_outputs/reference_image__ConvexHullIllumWorm.npy": "1890f573e1ca6ea4f16c6a533db1a429f86090f5ea52851dab4a151ca01c8614", + "benchmark/results/official30_value_completion_20260914/example3_intermediate_diagnostic/candidate_outputs/reference_image__InvertedWorms.npy": "0c35c2ceb36b064a8f9e9879ce0339f545bed0637b66a6cc36c5bde0d2734f31", + "benchmark/results/official30_value_completion_20260914/example3_intermediate_diagnostic/candidate_outputs/reference_image__MaskedInvertedWorms.npy": "45c1ae2ca8acf6a4b45931f670d0f5ff0db96b35de49ee054e407f04f6436f83", + "benchmark/results/official30_value_completion_20260914/example3_intermediate_diagnostic/candidate_outputs/reference_image__MaskedOrigWorms.npy": "1e45f21c32f98b5969705e54c4a2d5c5261cb1de1bb9abfb544ec1e57a9948d7", + "benchmark/results/official30_value_completion_20260914/example3_intermediate_diagnostic/candidate_outputs/reference_image__PolynomialCorrected.npy": "8875da63a072c8f2de4f5cb707d779a064b663bf69aa0f304fe6fa74d4ff57a6", + "benchmark/results/official30_value_completion_20260914/example3_intermediate_diagnostic/candidate_outputs/reference_image__PolynomialIllum.npy": "86f02928457324b3dc9b59dae1d858c8e10376a6cde16f1dc4e46bf7b8fae456", + "benchmark/results/official30_value_completion_20260914/example3_intermediate_diagnostic/candidate_outputs/reference_labels__ShrunkenWell.tiff": "debdfd00e7ce8d5c14f0de9c95c16ad26526f2e9fec2fd46e99b566077f614d1", + "benchmark/results/official30_value_completion_20260914/example3_intermediate_diagnostic/candidate_outputs/reference_labels__Well.tiff": "9f059bedc38992b8c83908028a213f232722d582667fbd7f4d9c3fc373830915", + "benchmark/results/official30_value_completion_20260914/example3_intermediate_diagnostic/endpoint_logs/openhcs_zmq_server_port_23848_1789408615215817.log": "f618dc60b84bb708781c8b2df087f4277d0f3da41fb448c7fda8e4e726ccd4ad", + "benchmark/results/official30_value_completion_20260914/example3_intermediate_diagnostic/native_outputs/.cellprofiler_benchmark_reference.json": "8b7661079f0da1c0f1a64e2471baf8afa22e4bb4725a8b7d622e8cb12352a341", + "benchmark/results/official30_value_completion_20260914/example3_intermediate_diagnostic/native_outputs/reference_image__ConvexHullCorrWorm.npy": "9ba1259e4ca007c06f14be6946e32a86307ec2dfb9952b73fa2cc253ccae8b3a", + "benchmark/results/official30_value_completion_20260914/example3_intermediate_diagnostic/native_outputs/reference_image__ConvexHullIllumWorm.npy": "1890f573e1ca6ea4f16c6a533db1a429f86090f5ea52851dab4a151ca01c8614", + "benchmark/results/official30_value_completion_20260914/example3_intermediate_diagnostic/native_outputs/reference_image__InvertedWorms.npy": "0c35c2ceb36b064a8f9e9879ce0339f545bed0637b66a6cc36c5bde0d2734f31", + "benchmark/results/official30_value_completion_20260914/example3_intermediate_diagnostic/native_outputs/reference_image__MaskedInvertedWorms.npy": "45c1ae2ca8acf6a4b45931f670d0f5ff0db96b35de49ee054e407f04f6436f83", + "benchmark/results/official30_value_completion_20260914/example3_intermediate_diagnostic/native_outputs/reference_image__MaskedOrigWorms.npy": "1e45f21c32f98b5969705e54c4a2d5c5261cb1de1bb9abfb544ec1e57a9948d7", + "benchmark/results/official30_value_completion_20260914/example3_intermediate_diagnostic/native_outputs/reference_image__PolynomialCorrected.npy": "588e6f9951aa7ad220678dbb0053e98fb6d324e71aab089d1d828d891ee32f54", + "benchmark/results/official30_value_completion_20260914/example3_intermediate_diagnostic/native_outputs/reference_image__PolynomialIllum.npy": "c3a44c530753120d1224d5c4abcd21225508fd63bc19c7dd3bd2a49eac0d31c7", + "benchmark/results/official30_value_completion_20260914/example3_intermediate_diagnostic/native_outputs/reference_labels__ShrunkenWell.tiff": "b1fcb0e9d83d8c2bcfcbe618aac6b2f153cf5971fe2b812229aa1c865dc05653", + "benchmark/results/official30_value_completion_20260914/example3_intermediate_diagnostic/native_outputs/reference_labels__Well.tiff": "31ba2ed9039c7b7fc37310a2b1e0f784a4c5aca5d09bd1fcaa606bc40548a060", + "benchmark/results/official30_value_completion_20260914/example3_intermediate_diagnostic/observations.csv": "1945f4cc68fa327fe9638d31bcf995bba342dca6a4f3dc8d68f58bea88709c99", + "benchmark/results/official30_value_completion_20260914/example3_intermediate_diagnostic/observations.jsonl": "002d7cf28a1dae4a864798c563cfca957872e24695facc22d173c7c33ac70e9f", + "benchmark/results/official30_value_completion_20260914/example3_intermediate_diagnostic/phase_timing.csv": "e38aaa8f5e16698c7b3a2d1669d66c43112977d9cd6ff3d9783e141a352b156c", + "benchmark/results/official30_value_completion_20260914/example3_intermediate_diagnostic/pipeline/ExampleIlluminationCorrection_Example3.cppipe": "8e93c5dca4aa4abd722b69c036e05c4ccd1c337e386e3cc7716def363d9df792", + "benchmark/results/official30_value_completion_20260914/example3_intermediate_diagnostic/pipeline/ExampleIlluminationCorrection_Example3.reference_exports.json": "725a929078405268ae450679e2061eae0332a2b9428f950a1ace504e161bc913", + "benchmark/results/official30_value_completion_20260914/example3_intermediate_diagnostic/pipeline/manifest.json": "8b288b7b155bbe8b54259eb0791d04da0add142bd3315de5cf628843637fdaec", + "benchmark/results/official30_value_completion_20260914/example3_intermediate_diagnostic/run_environment.json": "ac9f2201e7ebcf85d4fcea749045c692f4f8a5340a129304e7a84bd8b8d05144", + "benchmark/results/official30_value_completion_20260914/example3_intermediate_diagnostic/suite_metadata.json": "d170891869d30b1eaf90b6d2ee96796a3b0e269914c3dc12faa110226d03b547", + "benchmark/results/official30_value_completion_20260914/example3_intermediate_diagnostic/summary.csv": "ed34ecdbbc9eec25ac0eb19bd634b50a139f4ea0e65d67e3af2ca6b0617d2022", + "benchmark/results/official30_value_completion_20260914/invalid_suppressed_image_run/observations.csv": "53c0b56a7cc156e6a443d02954b308e9ee0c7407ea2ce3c0ba4dcca23756ea5c", + "benchmark/results/official30_value_completion_20260914/invalid_suppressed_image_run/observations.jsonl": "adce8264063f7182a53e76d30fe8256d73143b5ef93ed577a82f88013ef3b822", + "benchmark/results/official30_value_completion_20260914/invalid_suppressed_image_run/suite_metadata.json": "395d4bfdd7641b74458a756f43240acefed0af46baa1a034bb7200cae07cc090", + "benchmark/results/official30_value_completion_20260914/invalid_suppressed_image_run/summary.csv": "ba32869b4aa83f7d17d998fb48b7cb6d53568ede82ff3a8d0839d924136b6cd2", + "benchmark/results/official30_value_completion_20260914/label_aware_exact_commit_run/artifact_comparisons.csv": "6769bb47a485e4cc942a535265d6f93cad530d7672ea6d0303c7c5b20101f5bb", + "benchmark/results/official30_value_completion_20260914/label_aware_exact_commit_run/candidate_outputs/ExampleIlluminationCorrection_Example1_EachMethod/reference_image__CorrGreen.npy": "3c17148f0fad16738a23af55ac4e41692e6c2906808a7d83823331702b1b4f46", + "benchmark/results/official30_value_completion_20260914/label_aware_exact_commit_run/candidate_outputs/ExampleIlluminationCorrection_Example2/reference_labels__LargeBlockCorrectedNuclei.tiff": "48e0ff1c313ce3e7154453d5b8ec53db9fcb6fb3ec8321f6eb5958c77e4b201f", + "benchmark/results/official30_value_completion_20260914/label_aware_exact_commit_run/candidate_outputs/ExampleIlluminationCorrection_Example2/reference_labels__SmallBlockCorrectedNuclei.tiff": "0b3270a092b0882d2c34effdd28d62edb9cd0f594dda592fe0e05845c5abba09", + "benchmark/results/official30_value_completion_20260914/label_aware_exact_commit_run/candidate_outputs/ExampleIlluminationCorrection_Example2/reference_labels__UncorrectedNuclei.tiff": "4543f3afec804f201126e640bae070fd4e44700ac7b0de99dec13e038b42377a", + "benchmark/results/official30_value_completion_20260914/label_aware_exact_commit_run/candidate_outputs/ExampleIlluminationCorrection_Example3/reference_image__ConvexHullCorrWorm.npy": "9ba1259e4ca007c06f14be6946e32a86307ec2dfb9952b73fa2cc253ccae8b3a", + "benchmark/results/official30_value_completion_20260914/label_aware_exact_commit_run/candidate_outputs/ExampleIlluminationCorrection_Example3/reference_image__PolynomialCorrected.npy": "8875da63a072c8f2de4f5cb707d779a064b663bf69aa0f304fe6fa74d4ff57a6", + "benchmark/results/official30_value_completion_20260914/label_aware_exact_commit_run/candidate_outputs/cp4_supplement_combine_objects/reference_labels__CombinedObjects.tiff": "2d1994f6d02100c4f5e77fc3c077d14d873bad8b5a48023966bdbb6a06bb83e4", + "benchmark/results/official30_value_completion_20260914/label_aware_exact_commit_run/candidate_outputs/cp_tutorial_translocation_start/reference_labels__Nuclei.tiff": "7cbac0da81e073728ee86f7265b960d1d90b75ece750b92c1bdfcbad99ffbdfd", + "benchmark/results/official30_value_completion_20260914/label_aware_exact_commit_run/endpoint_logs/openhcs_zmq_server_port_23952_1789412557483684.log": "21468a74bd0bf30c9ff06c434fc6673479ee6250a2cedf17c50f3f2b975e065d", + "benchmark/results/official30_value_completion_20260914/label_aware_exact_commit_run/endpoint_logs/openhcs_zmq_server_port_23952_1789412579144257.log": "9c138abe3ae085f701fc1d4dd28778686c138732b7eb8597964cc72b72f9e04d", + "benchmark/results/official30_value_completion_20260914/label_aware_exact_commit_run/endpoint_logs/openhcs_zmq_server_port_23952_1789412604000694.log": "b7ab91fd34917b424ab5ac363f268cf84de6e2f7c390d0bbc208db4f657e0cce", + "benchmark/results/official30_value_completion_20260914/label_aware_exact_commit_run/endpoint_logs/openhcs_zmq_server_port_23952_1789412623395869.log": "1abfc359c124f6d6e0a838f2764f59075a559231f3ea5c0674c42b60c2fd228c", + "benchmark/results/official30_value_completion_20260914/label_aware_exact_commit_run/endpoint_logs/openhcs_zmq_server_port_23952_1789412642098045.log": "94d8e09536f0066d85dc6cf23422a77f97fefc2d3d91a663e4afc8c53c904a55", + "benchmark/results/official30_value_completion_20260914/label_aware_exact_commit_run/module_coverage_absorbed_modules.csv": "2acf6a4e7385137fceafad7a3591e93d3302ebc85fec14057d5be8af334e9f1e", + "benchmark/results/official30_value_completion_20260914/label_aware_exact_commit_run/module_coverage_cppipe_modules.csv": "9294a9c694d85f1f2676cb33f41431d66a4986cfb7f07432d3a2015423521c1e", + "benchmark/results/official30_value_completion_20260914/label_aware_exact_commit_run/module_coverage_cppipe_settings.csv": "02764a949ea6b3bb7037a65091a3dff2d715adbeffe998dcda43f78ac226c05f", + "benchmark/results/official30_value_completion_20260914/label_aware_exact_commit_run/module_coverage_summary.json": "acdc4005dfc26d8a51b943a8bb43a6378cc667d68ebf60c7124c2d23dbca24b6", + "benchmark/results/official30_value_completion_20260914/label_aware_exact_commit_run/observations.csv": "a1f84b3baef6c0a41c37f1c6ce613bd8a7b7a64402a4b26067add4366a731d9f", + "benchmark/results/official30_value_completion_20260914/label_aware_exact_commit_run/observations.jsonl": "4b27671215b174655fcfe0878b0be1e82a91c45e25cbd8977c1affc7db04ea76", + "benchmark/results/official30_value_completion_20260914/label_aware_exact_commit_run/phase_timing.csv": "9cf38c07b4fa8b02a02a4d68c7fcd9028e2e466b97ff835e3a3c7a2d0f9ff4ff", + "benchmark/results/official30_value_completion_20260914/label_aware_exact_commit_run/run_environment.json": "1657aa024b80da323b37cd62ea4a585bfeae50b1774649d6f9a9733c36d5d6f6", + "benchmark/results/official30_value_completion_20260914/label_aware_exact_commit_run/suite_metadata.json": "27173378623b3f7fb9753dc453b839a7d8a4484dbc7ddbee65a34efec137ed10", + "benchmark/results/official30_value_completion_20260914/label_aware_exact_commit_run/summary.csv": "699f4238d29a277d33749959f1f23f4e2b4bdd2b5cade914420f2bc238b2d347", + "benchmark/results/official30_value_completion_20260914/stale_endpoint_run/artifact_comparisons.csv": "64b5ed21e64b2ea62d4a92de290e17fc5f51f7b608df77ad89d33e0aff107ade", + "benchmark/results/official30_value_completion_20260914/stale_endpoint_run/observations.csv": "6575e5497a75f2341269980182749314b9fc207397a350ef9236b85f8cfeba46", + "benchmark/results/official30_value_completion_20260914/stale_endpoint_run/observations.jsonl": "4ab94c30460ce9d6da5df5a72600ca2f0c65179abd4a10d0fc9b4d39a54dc89d", + "benchmark/results/official30_value_completion_20260914/stale_endpoint_run/phase_timing.csv": "c68b045a8d00c63da60849aad9ea1d10ca1a7b89fa1b234eb0935d4ce22d77d3", + "benchmark/results/official30_value_completion_20260914/stale_endpoint_run/run_environment.json": "4a6cdbf676791877e6e8cc1a6f13c1a1365b092f1a2c125022b741cfa6d0c78c", + "benchmark/results/official30_value_completion_20260914/stale_endpoint_run/suite_metadata.json": "b403d0dac8413a58671431e42eb2b4915dd451d3cf0d8f4bfb3d44cbe0a3b391", + "benchmark/results/official30_value_completion_20260914/stale_endpoint_run/summary.csv": "fe1ecdecede51b92181b85ba661330534f6e3cfd7186379a7708e532b699facf", + "benchmark/results/paper_config_new_api_probe_20260923/8w_1c.csv": "9830d4add0020e24995190e5e603b7bfdba2c4799010ff0da67a553ba9d7f381", + "benchmark/results/paper_config_new_api_probe_20260923/8w_1c_outcomes.pkl.gz": "ff6daf5d45b817ac616e9af41dd53cff1b1ae5c41d2d12f974f2ea797acc6e34", + "benchmark/results/paper_config_new_api_probe_20260923/8w_1c_receipt.json": "3027c94773d6e55bfb2f65d18b7083a0e7924093ee5ebfea64ed94feff4864a3", + "benchmark/results/paper_config_new_api_probe_20260923/8w_2c.csv": "638dcfd5fed0e2ff2377e36c4a0c0d66e9768ac2c8741d97a23f2c7196ee05b3", + "benchmark/results/paper_config_new_api_probe_20260923/8w_2c_declared_outputs.csv": "e6ccd858434c95bababfcc28c04b3684ee270a992f34b27d18bbf87b81ca69dd", + "benchmark/results/paper_config_new_api_probe_20260923/8w_2c_declared_outputs_outcomes.pkl.gz": "b29ff7bb0c747604f993ad392f7c3368de8453f199f48c5ed27da0cad120b53e", + "benchmark/results/paper_config_new_api_probe_20260923/8w_2c_declared_outputs_receipt.json": "deb11a85bf2e101d5836cfc962d1aa2b1d8322320cc9b01fe7e528a8116c3ade", + "benchmark/results/paper_config_new_api_probe_20260923/8w_2c_outcomes.pkl.gz": "859be309417596501880a8a4d43118d6cfdbea4fce70c73ab8924e546de329c1", + "benchmark/results/paper_config_new_api_probe_20260923/8w_2c_receipt.json": "032c248f67bd279574f4ec4c67e1fac796206e68fcfbaaa31f146e44f23592ab", + "benchmark/results/paper_config_new_api_probe_20260923/8w_2c_verified_2pid/measured_pipeline_receipt.json": "b8fd0422d90e248ac5bb70840a142414caf9e9dad007dce39ffdfb907e11724c", + "benchmark/results/paper_config_new_api_probe_20260923/8w_2c_verified_2pid/runtime_outcomes.pkl.gz": "bdcf6a36243367658625132f87bd60b85a939cc4df1c8cf3acf10e6c3d1fa3a3", + "benchmark/results/paper_config_new_api_probe_20260923/8w_2c_verified_2pid/submitted_global_config.py.txt": "11e089b9f329088028498a66fc39a51833624cd8eb29562002c5e807512c5740", + "benchmark/results/paper_config_new_api_probe_20260923/8w_2c_verified_2pid/submitted_pipeline.py.txt": "e6ebb8608d2a22a5ea5ee40059f0dbf7583876bd6245387c24cf0ed6ab7345bd", + "benchmark/results/paper_config_new_api_probe_20260923/8w_2c_verified_2pid/well_throughput.csv": "08c44e894b2ee7130cb7005bb1fe486ad79dc406302179707c35c950fd0b5bab", + "benchmark/results/paper_config_new_api_probe_20260923/8w_2c_verified_2pid/well_throughput_progress_events.csv": "705fd02774f61bc2f905a21f0362a5cc49a80452cbc6bab3ac3c4be58b8a186e", + "benchmark/results/paper_config_new_api_probe_20260923/8w_2c_verified_2pid/well_throughput_step_timings.csv": "1b30e7b61e160e8d97231b29af988095e7ff1f441185b3c9868929ab89abf32a", + "benchmark/results/paper_config_new_api_probe_20260923/8w_2c_verified_2pid/well_throughput_worker_lanes.csv": "e688af596ca7a4e54bd71df0221bed792f98f0bf6e70f7a8d738df740778080f", + "benchmark/results/paper_config_new_api_probe_20260923/8w_2c_verified_2pid/zmq_results_summary.json": "22a3ceacbbbae85e3e4f193cf9c247ac2d9fbe09ae8b1a17be339695353ca8a4", + "benchmark/results/paper_config_new_api_probe_20260923/README.md": "f74483f8e75346851ff9c0a9751200c4c3ed5f5115d7d8bbaa4521ab065d78fd", + "benchmark/results/paper_config_new_api_probe_20260923/translocation_four_modes_fork/12w_3c/measured_pipeline_receipt.json": "ee1fee7401b8b8dcfe17dc053824d35bfe66f439783626be44ddfd3838083f37", + "benchmark/results/paper_config_new_api_probe_20260923/translocation_four_modes_fork/12w_3c/progress_events.csv": "32aa3f571d0cc1c36cd9b9ededd72979b88760062155c2cd8902c22c8b168d12", + "benchmark/results/paper_config_new_api_probe_20260923/translocation_four_modes_fork/12w_3c/runtime_outcomes.pkl.gz": "adc1dd9c98eb04c85ea5c2e5b098da43de6b3d082848947ba728a645c172cbbb", + "benchmark/results/paper_config_new_api_probe_20260923/translocation_four_modes_fork/12w_3c/step_timings.csv": "7dc918c5dd8378cfdd5a245d0828e0b242181359c581d80c3dc3504851c30805", + "benchmark/results/paper_config_new_api_probe_20260923/translocation_four_modes_fork/12w_3c/submitted_global_config.py.txt": "5b02ba65228c46b1eb52b79d6f6d7e4d87c1b8feb4b72d916a29064ab5b071be", + "benchmark/results/paper_config_new_api_probe_20260923/translocation_four_modes_fork/12w_3c/submitted_pipeline.py.txt": "f6176b7c207fdc44a89729ed336225c3b56d7c0c7bc96cbb5a4e73aab2320377", + "benchmark/results/paper_config_new_api_probe_20260923/translocation_four_modes_fork/12w_3c/worker_lanes.csv": "2ac06c1beec3c379fbf9b13a22674e303c68414a459c75a9da8e6b4783652451", + "benchmark/results/paper_config_new_api_probe_20260923/translocation_four_modes_fork/12w_3c/zmq_results_summary.json": "9aa7300757cd96bfcba6775adfe811fab88d0415b9aa1db053283de743ee0df7", + "benchmark/results/paper_config_new_api_probe_20260923/translocation_four_modes_fork/16w_4c/measured_pipeline_receipt.json": "e34bcc76ccf26058ba36fd18ec7f690dcc26a4c6a17759381a7b4392ec5217fe", + "benchmark/results/paper_config_new_api_probe_20260923/translocation_four_modes_fork/16w_4c/progress_events.csv": "bb5d4d68a48d6dc850b884482513d309dbc62a121f3aee2588afc74c4dcbcad2", + "benchmark/results/paper_config_new_api_probe_20260923/translocation_four_modes_fork/16w_4c/runtime_outcomes.pkl.gz": "dba0a261f50186cf097b1c6df874186d5e6063b1a2aa74a1b7ffd406f6ea2d7d", + "benchmark/results/paper_config_new_api_probe_20260923/translocation_four_modes_fork/16w_4c/step_timings.csv": "16f04f753226989c6f4ca66e949e8a9f637034f5a476a54f8a1bf66b91053bcf", + "benchmark/results/paper_config_new_api_probe_20260923/translocation_four_modes_fork/16w_4c/submitted_global_config.py.txt": "80a34af100ced928482f27c647571274798d2748e66ed1d244f70ebe5d573097", + "benchmark/results/paper_config_new_api_probe_20260923/translocation_four_modes_fork/16w_4c/submitted_pipeline.py.txt": "cd1ce5cd2e75d10ce50bba191f3d946ec383888686a4185a1199a997d72ba2c4", + "benchmark/results/paper_config_new_api_probe_20260923/translocation_four_modes_fork/16w_4c/worker_lanes.csv": "5a70fe4c631d40b79e982d6364887d4417f7a803f335438477993b2e24d3d3b1", + "benchmark/results/paper_config_new_api_probe_20260923/translocation_four_modes_fork/16w_4c/zmq_results_summary.json": "3ec94ab3c15f17c015a2e8d3bf8c8d31532c400cccce505d8d053ee5587fd208", + "benchmark/results/paper_config_new_api_probe_20260923/translocation_four_modes_fork/1w_1t/measured_pipeline_receipt.json": "a09cc0bcef1c8c6bee3521505d1d84bad5ac5eca26a265d998da84c79e1c9575", + "benchmark/results/paper_config_new_api_probe_20260923/translocation_four_modes_fork/1w_1t/progress_events.csv": "a1dca6aae9555f9dc97ab031f5208024497f702110fa6970d52419844381f9f3", + "benchmark/results/paper_config_new_api_probe_20260923/translocation_four_modes_fork/1w_1t/runtime_outcomes.pkl.gz": "0dc6e0f703cd192e73898546dac5730e44a9e5494f0c385fdc75fc760056973d", + "benchmark/results/paper_config_new_api_probe_20260923/translocation_four_modes_fork/1w_1t/step_timings.csv": "b03de853c21350719a48a7f2cd33ce4f4b210c513c568ab3715a79d5a0eca5a8", + "benchmark/results/paper_config_new_api_probe_20260923/translocation_four_modes_fork/1w_1t/submitted_global_config.py.txt": "2e2cf95d87a9465d20ecbb8c88629ce615b64de6e5843319f9b51599f343c1c3", + "benchmark/results/paper_config_new_api_probe_20260923/translocation_four_modes_fork/1w_1t/submitted_pipeline.py.txt": "48c6b13776fc8312baf76308a1016c9faca1c61db154434344a869cd183530c3", + "benchmark/results/paper_config_new_api_probe_20260923/translocation_four_modes_fork/1w_1t/worker_lanes.csv": "aa76fbc3919a68a5fe6fce8b158e8d94a695bbb5267bfdc1865a60a1c1349037", + "benchmark/results/paper_config_new_api_probe_20260923/translocation_four_modes_fork/1w_1t/zmq_results_summary.json": "527db04cd5ab7ac04b23eedb5db8113bfb0073445431403a1327c2a32ddc361a", + "benchmark/results/paper_config_new_api_probe_20260923/translocation_four_modes_fork/8w_2c/measured_pipeline_receipt.json": "edc8c152c3ab6f662af7d50ae9cdc96e9d9d10168e2cd30ad5be27d520d5b043", + "benchmark/results/paper_config_new_api_probe_20260923/translocation_four_modes_fork/8w_2c/progress_events.csv": "b3433d039d3ee84ba05185b59e4b0b594ebd1892c734344e2ae4f39bf756be95", + "benchmark/results/paper_config_new_api_probe_20260923/translocation_four_modes_fork/8w_2c/runtime_outcomes.pkl.gz": "04b1b1b87f74d04692c0df45a505bd6c7242d8227ffc7ad68e96a05765576f94", + "benchmark/results/paper_config_new_api_probe_20260923/translocation_four_modes_fork/8w_2c/step_timings.csv": "9bac75d5f6550c3a299707651a26f8120281b958d821d98526e8b0877507a693", + "benchmark/results/paper_config_new_api_probe_20260923/translocation_four_modes_fork/8w_2c/submitted_global_config.py.txt": "11e089b9f329088028498a66fc39a51833624cd8eb29562002c5e807512c5740", + "benchmark/results/paper_config_new_api_probe_20260923/translocation_four_modes_fork/8w_2c/submitted_pipeline.py.txt": "ab20fcab6f0b07a3a7029b68eb3b20ac4cd95efef78e47ac82ffbde141cd6475", + "benchmark/results/paper_config_new_api_probe_20260923/translocation_four_modes_fork/8w_2c/worker_lanes.csv": "9398bab30364fa71e31276030f470b84e89cec863ec5e244a8bb14898b8eb96b", + "benchmark/results/paper_config_new_api_probe_20260923/translocation_four_modes_fork/8w_2c/zmq_results_summary.json": "116db1296535c341894ccb67a3b1ca1bed0eecfc4a2e8d82b2dd386bd6e09c7c", + "benchmark/results/paper_config_new_api_probe_20260923/translocation_four_modes_fork/README.md": "353cb4ce359532168d2467cb0196f4cb15ec1398dc54371a7265a04303e29c71", + "benchmark/results/paper_config_new_api_probe_20260923/translocation_four_modes_fork/well_throughput.csv": "c92b7cfb06e17a7a312bb3abb93d6a06b39d1bf7983f266409c964059f1fa263", + "benchmark/results/perf_coloc_step_cache_20260929/README.md": "aa888a93071515ae296a2a5e615d57ba043708a2676d4faf81442c14f963056f", + "benchmark/results/perf_coloc_step_cache_20260929/abba_1_control.csv": "ecbaae1ddb7c931cc7cd76e2b68fee261426330ae9b7ac853ff85ee1aec4a2b5", + "benchmark/results/perf_coloc_step_cache_20260929/abba_2_candidate.csv": "a49b0f06696f192073bb9aa4786c148d6969e6e9c22b17c1d924331a41faf38d", + "benchmark/results/perf_coloc_step_cache_20260929/abba_3_candidate.csv": "cffcc08fb7c3acbf673f12db48ffd838a6ed646e574ba85aeb063760d20a56f1", + "benchmark/results/perf_coloc_step_cache_20260929/abba_4_control.csv": "765727623360319a134023bccdc1fb10a25697981c046eb25818384327c91326", + "benchmark/results/perf_coloc_step_cache_20260929/affected_case_execution.png": "f90ff514a2191b0a42b9a75d867744be6d10abce8455366e05b5a37496e7b9d3", + "benchmark/results/perf_coloc_step_cache_20260929/full_first_candidate.csv": "406e67b3d1b9a5f2b257caebfdfe211bcc4744363d2a1c75c7c0e95b25efc7bf", + "benchmark/results/perf_coloc_step_cache_20260929/full_first_control.csv": "1dceecbae5cb6858914b0a241cab4fa362da784ca699eaccca46b0c9e37ad775", + "benchmark/results/perf_coloc_step_cache_20260929/full_suite_execution_delta.png": "d48e224d6fe6cfa570cb6512693db00a8d4bae4ca82f1987d51308ca10eb0cc4", + "benchmark/results/perf_coloc_step_cache_20260929/full_warm_candidate.csv": "77ad4113fdc086c9412ea0aab24e19a8db93cf960e749b2fb4a2ac92394455dd", + "benchmark/results/perf_coloc_step_cache_20260929/full_warm_control.csv": "f7effe7ac56c5547fa0a3769dbaac4062744baac3cc0d42eec98c400217d8a24", + "benchmark/results/perf_coloc_step_cache_20260929/plot.py": "c96b55e25f5e145a0531d88bf77d09026f1a66e3caab110db609eb4d8767ff60", + "benchmark/results/perf_compiled_radial_20260929/README.md": "4de88c4978800932097c9e9f89382a05de1acbbdec1b9e5a17c4c3d33288050f", + "benchmark/results/perf_compiled_radial_20260929/analysis.json": "ba90d0586f8ac9252dfe0f4a68da0fb2d13b8d386a0b911f9e616d61745d75d5", + "benchmark/results/perf_compiled_radial_20260929/analyze.py": "8d5af51b4768f1448de23b1cfd128195dc0892710642502776c237a57de14f11", + "benchmark/results/perf_compiled_radial_20260929/architecture.md": "7193e21aedf6e49089f5a0a082040c6cf2e6ed19125c958c65f9da58ebdf1b75", + "benchmark/results/perf_compiled_radial_20260929/census_summary.json": "8efc5b9337c9fdc85139fdbec4050735b659c0fb2d3ed3a8f0e763cfa33c80e2", + "benchmark/results/perf_compiled_radial_20260929/check_export_parity.py": "5b73605782f75598c76925966fe31d363c28c474de6f3f3735cc5fedea10c525", + "benchmark/results/perf_compiled_radial_20260929/consumer_tests.log": "9559c2c2107217a19bae92dfe8a0a66c26dd60d0d90e9ca0e09443a9d0d16c20", + "benchmark/results/perf_compiled_radial_20260929/export_parity_16w.json": "1972a155234a4921d2466f905085a11cb09ffeed09a47a6d4ac7dbddd5d69712", + "benchmark/results/perf_compiled_radial_20260929/export_parity_1w.json": "7baf12caa4ef5a280383d19c93842b3c1385ed06552455188d3d3c164eec91e8", + "benchmark/results/perf_compiled_radial_20260929/focused_tests.log": "51c5f4493731fc4516821d0446564ad5d8fb45d83f46e1e675e59c5b39d28154", + "benchmark/results/perf_compiled_radial_20260929/input_digest_checks.json": "73ba5709841fd20f52f0bc34254fa251044648b54bedc0fd66f9770f03a199c1", + "benchmark/results/perf_compiled_radial_20260929/measured_radial_scaling.png": "fb3f31b15e45b9bf9ab55f54ad2228335466a785a2fcee13412d2b2d258e94c2", + "benchmark/results/perf_compiled_radial_20260929/measured_radial_scaling.svg": "1ebdfe437f1e677e8514cccaa7d8d456df6350a5e12a27524d75fcb1fc1ae45f", + "benchmark/results/perf_compiled_radial_20260929/observations_16w.jsonl": "06307852309a4ab5ad8fdbcf426ae228c66d3c7ed89a6917e1c4ff4c51c2c641", + "benchmark/results/perf_compiled_radial_20260929/observations_1w.jsonl": "69a82bbb6bf4e3a3c0c4a309a17274dd0a05aced8af2fe65d97a69e8a41dd09f", + "benchmark/results/perf_compiled_radial_20260929/plot.py": "abe70858d4debec9fef7d9e2eb83db20e56313a74879950fe9eca003276645b6", + "benchmark/results/perf_compiled_radial_20260929/provenance.json": "1c7fb6b859ed5f9ffe7063e7653460689dd86015820da57496ca1b6d55905652", + "benchmark/results/perf_compiled_radial_20260929/radial_probe_observations.json": "0a73c10c81e96f2cd66f89f15efe5a79bd8e0df9e5bc248fff522b4b4d351e56", + "benchmark/results/perf_compiled_radial_20260929/recipes/01_select_compiled_default.py": "61521b77d0e5cb7a3986d134b17d821502b293006652ad7f11afa52235ae8339", + "benchmark/results/perf_compiled_radial_20260929/recipes/02_share_request_boundary.py": "f07bb95b6bbcc39e09cd090a2c0d8c3069fbe685d2e76f3d9eb5040a2cc7d39b", + "benchmark/results/perf_compiled_radial_20260929/recipes/03_share_compiled_normalization.py": "976f78897800d801538d0d1a72415df3b248677b6966f4ab05bc2fffca5bfe94", + "benchmark/results/perf_compiled_radial_20260929/recipes/perf-compiled-radial-nra-projected-20260929.diff": "4340b6d4998dac35a2a527127be79d28d6de0f1e9eb00b6d7cabeb8dcc7d7e73", + "benchmark/results/perf_compiled_radial_20260929/recipes/perf-radial-normalization-nra-projected-20260929.diff": "26d23401861d9509560247e943cd7699d9be03dc55675670da1e5603d75a19bf", + "benchmark/results/perf_compiled_radial_20260929/recipes/perf-radial-request-boundary-nra-projected-20260929.diff": "13f23a3810750714f751796fb6f32efb2ce3ed24c71a9e4cdeb56e0e5f137b9c", + "benchmark/results/perf_compiled_radial_20260929/replay_saved_requests.py": "dd97cb2afba7ea2da38378c83a120e0a51e9c5a285a6a45295668ab64c395896", + "benchmark/results/perf_compiled_radial_20260929/run_pairs.py": "b6e9da11b530c855759e95084582c2e5be91a44e91d23492937774bc6433a85f", + "benchmark/results/perf_compiled_radial_20260929/runs/16w/candidate_1/lanes.csv": "6b26eb412a9e61dda5fbc0e4a29403f86a772bb22392b9f781235e0be597c4df", + "benchmark/results/perf_compiled_radial_20260929/runs/16w/candidate_1/observation.csv": "4b4b5249ec0c7ad7da4b3418c0025773c5e84649b8dfe539e0d9634e103559d9", + "benchmark/results/perf_compiled_radial_20260929/runs/16w/candidate_1/receipt.json": "4313e6d7db3534e8776137926d95d7955407ffbf74fcf4b8b0ff2b9c21c54be9", + "benchmark/results/perf_compiled_radial_20260929/runs/16w/candidate_1/steps.csv": "433aa409929a6cfb0650c28c393fb0ef9ca5451069c071ced3b798b800ac2b0b", + "benchmark/results/perf_compiled_radial_20260929/runs/16w/candidate_1/warm.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "benchmark/results/perf_compiled_radial_20260929/runs/16w/candidate_2/lanes.csv": "06e34d1ff26f4f00d32044f8a1219fadfac6ce48364fb0fc93dbe583e61b54ff", + "benchmark/results/perf_compiled_radial_20260929/runs/16w/candidate_2/observation.csv": "e523b5e543cee1eaf0071c564225e6f52dae130fd726eeb0c39c51d24e8cd292", + "benchmark/results/perf_compiled_radial_20260929/runs/16w/candidate_2/receipt.json": "fa5da6355824e2056e2bfb2eed97f02d367abaf7a53d0f30c6b6d580ae0b0bf2", + "benchmark/results/perf_compiled_radial_20260929/runs/16w/candidate_2/steps.csv": "af8b2b8d50405f276805a6175c750ab5ae973dc50cbd6f21d3981366ba7addb5", + "benchmark/results/perf_compiled_radial_20260929/runs/16w/candidate_2/warm.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "benchmark/results/perf_compiled_radial_20260929/runs/16w/control_1/lanes.csv": "f8dc5f00bf00eca1848aac546d288e835d390aea0bc89db7ba77ed9bc9afec47", + "benchmark/results/perf_compiled_radial_20260929/runs/16w/control_1/observation.csv": "6e33a531e8576ccfc726550021ebf466bae26cc917f500170b997a8ebdf21002", + "benchmark/results/perf_compiled_radial_20260929/runs/16w/control_1/receipt.json": "ad55269d27c9e544b0350b764c61326fb9126da8492291fb73aefcd504ac50f6", + "benchmark/results/perf_compiled_radial_20260929/runs/16w/control_1/steps.csv": "b7e8de743c24e9ae6b77ce29823d7226ac7d670196e46a4f0aa1810a39f46457", + "benchmark/results/perf_compiled_radial_20260929/runs/16w/control_1/warm.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "benchmark/results/perf_compiled_radial_20260929/runs/16w/control_2/lanes.csv": "2dc771537fff91b16f682b6e76e5270a2c33d2f6a72837983839715b5ad7bb40", + "benchmark/results/perf_compiled_radial_20260929/runs/16w/control_2/observation.csv": "17a37ad58ef2a7834379e4859b8fba25de8860dce3ca5f9917dbb773a64b2a0d", + "benchmark/results/perf_compiled_radial_20260929/runs/16w/control_2/receipt.json": "c51ba471fc40524c41c9e74da089c15ea550faa09ad28c2398ae8b06fcdd0ee5", + "benchmark/results/perf_compiled_radial_20260929/runs/16w/control_2/steps.csv": "495f7e6274b1e630428058c8c408904a0b34c6b14b5603b2a0b486cac8f321ca", + "benchmark/results/perf_compiled_radial_20260929/runs/16w/control_2/warm.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "benchmark/results/perf_compiled_radial_20260929/runs/1w/candidate_1/lanes.csv": "7a826c444e88849408b94523d01ac04ee59ddcade186a523766dc06b81c7fb28", + "benchmark/results/perf_compiled_radial_20260929/runs/1w/candidate_1/observation.csv": "e21d6605d0e670c0aba253502edf6ff56f3bf7e038a9cc6bd61f8819cf47ee0b", + "benchmark/results/perf_compiled_radial_20260929/runs/1w/candidate_1/receipt.json": "b72dc79054ed1228332651ea35b638a86c85184d4c5d38b67931151d5126364c", + "benchmark/results/perf_compiled_radial_20260929/runs/1w/candidate_1/steps.csv": "1d8fbffb3b5bb8179211cdffdc28fb566e0f57fae49543d24f357964a6668a36", + "benchmark/results/perf_compiled_radial_20260929/runs/1w/candidate_1/warm.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "benchmark/results/perf_compiled_radial_20260929/runs/1w/candidate_2/lanes.csv": "fc52a4f6af56dc266abf76c5e7edb0819f8aa98e2b3fdc299e06a4a7bc79145b", + "benchmark/results/perf_compiled_radial_20260929/runs/1w/candidate_2/observation.csv": "9dce42b0c959dd5e53e725ddd07bb88d350b1bb572135addbaaa749ac524183b", + "benchmark/results/perf_compiled_radial_20260929/runs/1w/candidate_2/receipt.json": "d54090a75a601224778bf1f6a3ed7aae3d2dc588403af439d39aab84daca1fc4", + "benchmark/results/perf_compiled_radial_20260929/runs/1w/candidate_2/steps.csv": "c8cb71b1e10c39d9683dc48f73faafe8d86908321e4efe7752a076901eb1d322", + "benchmark/results/perf_compiled_radial_20260929/runs/1w/candidate_2/warm.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "benchmark/results/perf_compiled_radial_20260929/runs/1w/control_1/lanes.csv": "6cafb7ad213c7c9d13afa340cd2064ea4d7318ed100f060dfb851236e214ee01", + "benchmark/results/perf_compiled_radial_20260929/runs/1w/control_1/observation.csv": "2f843f6da5b2dc56beb567fe686c2cb41378a695d74b1aef83a97365b71491e1", + "benchmark/results/perf_compiled_radial_20260929/runs/1w/control_1/receipt.json": "194cff54fc1159fee7dd0f2b6eee7d9f11d7d3d32c5c9409010daf37d0b2726a", + "benchmark/results/perf_compiled_radial_20260929/runs/1w/control_1/steps.csv": "7a7265a6a85fe4d4411b476d1951818ca8d48050b1fe4b089a05570346e6b1f4", + "benchmark/results/perf_compiled_radial_20260929/runs/1w/control_1/warm.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "benchmark/results/perf_compiled_radial_20260929/runs/1w/control_2/lanes.csv": "144f10255b0e693ee56c7a4aeb0bd804c2a4fb3570e7ddcf7597042d7f327b3a", + "benchmark/results/perf_compiled_radial_20260929/runs/1w/control_2/observation.csv": "7bc6f2c3262dd5354a02b5c6240076d576a60f0758c444294a7e30396cc79ff9", + "benchmark/results/perf_compiled_radial_20260929/runs/1w/control_2/receipt.json": "d35e75707f248910835abf1f006ab859ec0e854d5352f2cf98bae1a1399f2f59", + "benchmark/results/perf_compiled_radial_20260929/runs/1w/control_2/steps.csv": "eb9e85450dd20eb1cb530fb4936b9ce882dd96516283d035746842a7dae6a4d6", + "benchmark/results/perf_compiled_radial_20260929/runs/1w/control_2/warm.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "benchmark/results/perf_compiled_radial_20260929/saved_request_replay.json": "425db6a6ba66e31d64356d4e7e107eebb78499f14220d0682209b9fc06ac811d", + "benchmark/results/perf_compiled_radial_20260929/scope_16w.json": "ed2a5ac206df30462f524e4c7b78ef88bb6738b827b2703049819f7f9b625d90", + "benchmark/results/perf_compiled_radial_20260929/scope_1w.json": "ed2a5ac206df30462f524e4c7b78ef88bb6738b827b2703049819f7f9b625d90", + "benchmark/results/perf_compiled_radial_20260929/spawn_cache/populate.log": "0397ac9f4204f8eae9072d741a6b28fb49e5943bb78893ae113deea6a3f07d4a", + "benchmark/results/perf_compiled_radial_20260929/spawn_cache/spawn_0.log": "49499ae034f18cbc70d39dfec1fbed98cf807de2922f36f240b6d03ddc032727", + "benchmark/results/perf_compiled_radial_20260929/spawn_cache/spawn_1.log": "49499ae034f18cbc70d39dfec1fbed98cf807de2922f36f240b6d03ddc032727", + "benchmark/results/perf_compiled_radial_20260929/spawn_cache_summary.json": "46a49bc1bec06c6017bb6e4d05429e01958955d8b7855460c5f057e56740e611", + "benchmark/results/perf_current_cohort_v10_20261003/qualification.json": "17bb47f609d95ca6044f92810dbbf1c5f31e066ef330955aab264a5d8d7e6273", + "benchmark/results/perf_current_native_paired_20261003/README.md": "41990bd207602a7b72a486dff057c7903f66fae43e7355501468c0b3ffaabac5", + "benchmark/results/perf_current_native_paired_20261003/matched_runtime.png": "867a94f34c5cbe1b6d4fe86917a4e42ab335e486d8c321dd745a353c95dad72d", + "benchmark/results/perf_current_native_paired_20261003/matched_runtime.svg": "9a5377c6ad8609f5a59ae2e9757ec5089d7d5dc77615839ac756fdbbb654e9f7", + "benchmark/results/perf_current_native_paired_20261003/matched_speedup.png": "c8a789edb4dda3ae82fbb708a0d2c9a459f0ee0d7ea7732c266703622839e1ee", + "benchmark/results/perf_current_native_paired_20261003/matched_speedup.svg": "d2e121af82f0a08e23f47cfa00b306ac60f8b580e72dad63cde72f6809ca8a2c", + "benchmark/results/perf_current_native_paired_20261003/metrics.csv": "906273de1e1fe146a7cf53a41892d203aa80fa96da9e7288ec619e6c828ad999", + "benchmark/results/perf_current_native_paired_20261003/native-3d.json": "f1f5804eaef6af43b73f0608071ff57c62691bbd9172b33fa45ca633a4593654", + "benchmark/results/perf_current_native_paired_20261003/native-speckles.json": "25f17f873f96555efb451ecbd39ce03545fb722aaca66a27b64a199fc8e67beb", + "benchmark/results/perf_current_native_paired_20261003/ordinary-0.csv": "43680d83b0df5de1391448655370b1bd013a5b70581741689bc2b3d6cac9db3f", + "benchmark/results/perf_current_native_paired_20261003/ordinary-1.csv": "54d24ecc9904bafc59405692862769ccbc11b663eae8420d24e2e77886d0bac3", + "benchmark/results/perf_current_native_paired_20261003/qualification.json": "3db0f60d3ef04aa0ce846eaef70e746031442422cc693ba943bf78ac8db1ab32", + "benchmark/results/perf_current_native_paired_20261003/strict-science.json": "77ab1a1e88d52502c633b646efe0d032ad87f117fc403298cb8b5742a0510190", + "benchmark/results/perf_current_native_paired_20261003/timing_summary.json": "c3a2afbb04f59b1c7de4d001d9ce42577fe778d15d46bae81362f0d0ec73f24a", + "benchmark/results/perf_declared_callable_kernels_20260930/README.md": "3577c991ddfa0d412fc242c38e284fbfae92692866dcf180c702ec885a6a3d84", + "benchmark/results/perf_declared_callable_kernels_20260930/analysis.json": "a745a031ec8f0b179c454cebaf92241dda5e890c0655f4f168cf0f181f64a27f", + "benchmark/results/perf_declared_callable_kernels_20260930/architecture.md": "ca63c13968ecdb26bfa970f39fc65bc9e3bc54c5433e120520debec12cc566d2", + "benchmark/results/perf_declared_callable_kernels_20260930/cold_observations.jsonl": "5b4351dc6e84221cd91a6f1c31b732ff65457770afa5d7811eeb6cdeb35861db", + "benchmark/results/perf_declared_callable_kernels_20260930/cold_scope.json": "c2748dfc40a186a4ef21218ff114dada404d5b62b05b05c52b9198b45c76019a", + "benchmark/results/perf_declared_callable_kernels_20260930/input_digest_checks.json": "51c2835d516f08e6292595393dbeac3493c2fcdb90e1d748fc4beeb22c54f7b7", + "benchmark/results/perf_declared_callable_kernels_20260930/installed_scope.json": "bfd11e13721d0e21c763da80cf7e6944bdd2c710b8421a31757dcfa68d5f8441", + "benchmark/results/perf_declared_callable_kernels_20260930/installed_throughput.csv": "c40c49bee53d694c5350a3b8890a9b1cd22c33ee041a47d415867474e4e912c8", + "benchmark/results/perf_declared_callable_kernels_20260930/measured_kernel_preparation_scaling.png": "a468bc28adce19c28b50af534cd5ccd345dbff6ad09da4af9b7e79a49806a92d", + "benchmark/results/perf_declared_callable_kernels_20260930/measured_kernel_preparation_scaling.svg": "2bb4930d358a60723adb8f080b38cae3ccbb8df7a90b25d038c0b4d2e29d4741", + "benchmark/results/perf_declared_callable_kernels_20260930/runs/cold/candidate_1/steps.csv": "a2504974ba0b0e0146357f9fca97dfcea6fcd93334d96ae2e6bdce92e1754bb1", + "benchmark/results/perf_declared_callable_kernels_20260930/runs/cold/candidate_1/throughput.csv": "88e3aa8d61048342a1827db268d9cdf785c70c9ecf3b8f0520d0644afbf7a657", + "benchmark/results/perf_declared_callable_kernels_20260930/runs/cold/candidate_1/worker_lanes.csv": "2549c53d3150bec98c378157edf583d1ab5525ce4c63c3f7e42b30017a3bc458", + "benchmark/results/perf_declared_callable_kernels_20260930/runs/cold/candidate_2/steps.csv": "fb5420342770c3ebff3d5aceaa848b15daa6b5b84bb000bac1579b36725ef9d1", + "benchmark/results/perf_declared_callable_kernels_20260930/runs/cold/candidate_2/throughput.csv": "a0c64337b88770b4f92ae891e854301c9d4aec9874983a8ac284ec98b1cf504a", + "benchmark/results/perf_declared_callable_kernels_20260930/runs/cold/candidate_2/worker_lanes.csv": "0dcb070d4d97fb44f955ac13eaa4db562ba81d01f9c36b10fb6c07062766c8c6", + "benchmark/results/perf_declared_callable_kernels_20260930/runs/cold/control_1/steps.csv": "b9ece8ba640d5c3ddbba092d232eb29dea6d634dc64088871c76da4287509202", + "benchmark/results/perf_declared_callable_kernels_20260930/runs/cold/control_1/throughput.csv": "b12dc7a024cff6c5e5045883575fbedd9dfb1b20d0a2e49962feceb1a5421dc7", + "benchmark/results/perf_declared_callable_kernels_20260930/runs/cold/control_1/worker_lanes.csv": "1a4b144a84767fc63cf67a63c36af29e343647c2bf778764a06bcff460e13287", + "benchmark/results/perf_declared_callable_kernels_20260930/runs/cold/control_2/steps.csv": "3cc1861af1d82b40e94a87da0f9fecc77d93df0a7f5bcd60cd585d7b881e6c91", + "benchmark/results/perf_declared_callable_kernels_20260930/runs/cold/control_2/throughput.csv": "3582178c5c102e40c160c01374fcead2f8c832b8be823cda5f435307a681137c", + "benchmark/results/perf_declared_callable_kernels_20260930/runs/cold/control_2/worker_lanes.csv": "78ad314a4c6d3c3f9a0d3d52f40eb97ab82a87ff225b119bafa9414d06ecaccc", + "benchmark/results/perf_declared_callable_kernels_20260930/runs/warm-16w/candidate_1/steps.csv": "d7d7ba3480b8a0fcac8c09527f60601c3bb7e3736835a3f43451e482f67f4e80", + "benchmark/results/perf_declared_callable_kernels_20260930/runs/warm-16w/candidate_1/throughput.csv": "d366dcdd3cac7ca6842311403fecc9c70970a7d4c96e0ecb077c4dab8608c6b5", + "benchmark/results/perf_declared_callable_kernels_20260930/runs/warm-16w/candidate_1/worker_lanes.csv": "148ee1bf9957576adbe2bd3f2427d2242c0b91ff307e266d9beeffe37516472b", + "benchmark/results/perf_declared_callable_kernels_20260930/runs/warm-16w/control_1/steps.csv": "ea341d3daf794c519763fdb6607eb2a8e87f359e6c59efcfe59e00768b086184", + "benchmark/results/perf_declared_callable_kernels_20260930/runs/warm-16w/control_1/throughput.csv": "d5b45e0ffef673d95041faeaaabd5f88f41e9a3ccfefd5be30e420259cf1e20c", + "benchmark/results/perf_declared_callable_kernels_20260930/runs/warm-16w/control_1/worker_lanes.csv": "091ed4bb6af0d2a3d0c1f306817facfdf22bda98b8ec4292f40600f32124ebc5", + "benchmark/results/perf_declared_callable_kernels_20260930/runs/warm-1w/candidate_1/steps.csv": "5ce7beec847c7a168c0b1f631c67f2f96eff78a3755a21dac43cc696ed020cbd", + "benchmark/results/perf_declared_callable_kernels_20260930/runs/warm-1w/candidate_1/throughput.csv": "3f1d5cd5efcef7d785bfc4db1008f9b216dd874e12ba57eea45880d9031c9027", + "benchmark/results/perf_declared_callable_kernels_20260930/runs/warm-1w/candidate_1/worker_lanes.csv": "8987e467e3c0be3478ad9cd8f267f83eaf1275646b4b959d96dd37dfbbe59b1a", + "benchmark/results/perf_declared_callable_kernels_20260930/runs/warm-1w/candidate_2/steps.csv": "c9726b7074d6b156c860f530cb2843346be8c9152b3d4207238c7f0c52756b4f", + "benchmark/results/perf_declared_callable_kernels_20260930/runs/warm-1w/candidate_2/throughput.csv": "1e80b0042c48df962741e15939a78ebd847be5f1ffdffa0f3f58ecd5a1966de0", + "benchmark/results/perf_declared_callable_kernels_20260930/runs/warm-1w/candidate_2/worker_lanes.csv": "22d22dfdaa9a2d3e94492be83368684f0bb520e9c1a0a83e2e0ddd429ddae7ab", + "benchmark/results/perf_declared_callable_kernels_20260930/runs/warm-1w/control_1/steps.csv": "1bdc5f3cd5ee7025d036860d335c84b66a949fb580c342debc796cbcd287415a", + "benchmark/results/perf_declared_callable_kernels_20260930/runs/warm-1w/control_1/throughput.csv": "8e5923e1ff827aad55af177cbc277d18bbdf29766eb470c2e08bd2a9764a540b", + "benchmark/results/perf_declared_callable_kernels_20260930/runs/warm-1w/control_1/worker_lanes.csv": "0d38295275d9bfa8bf47fd27c9da052cab626348ca7af2ef9b3dd797d08c1565", + "benchmark/results/perf_declared_callable_kernels_20260930/runs/warm-1w/control_2/steps.csv": "28e55cfe9ebef2d2d9d7c1360bdf42687e2280b47107345f7ea6115beaf67f7d", + "benchmark/results/perf_declared_callable_kernels_20260930/runs/warm-1w/control_2/throughput.csv": "58d0e53aa0c5afc7ca6d26b4797efe3c6bbc76333528351514f6089f2c92df88", + "benchmark/results/perf_declared_callable_kernels_20260930/runs/warm-1w/control_2/worker_lanes.csv": "c58b38fccc6e83c7141d4917d60b531743e0c1f0b3f91b014755a7b86e5d2186", + "benchmark/results/perf_declared_callable_kernels_20260930/validation/census_after_full.json.gz": "f075c3731639cc5ebaa5f323eb5935d91a4d870dbb2e6ea96db70b6d778b00c4", + "benchmark/results/perf_declared_callable_kernels_20260930/validation/census_before_full.json.gz": "5f3e07267d88d49c0c88e01384604b594ef97922c4bb4e5697208296cc7bcfc4", + "benchmark/results/perf_declared_callable_kernels_20260930/validation/perf-declared-kernel-capture-order-original-regression-20260930.log": "12702fd1022a4d4db576b983c658e19115ff108e0db5ebfdfcb76946745d893b", + "benchmark/results/perf_declared_callable_kernels_20260930/validation/perf-declared-kernel-inherited-lock-candidate-20260930.log": "3275522a65c60210c9aee3615ac7c007be3d0dd63b9902e872ac3dd10094cc3d", + "benchmark/results/perf_declared_callable_kernels_20260930/validation/perf-declared-kernel-inherited-lock-original-regression-20260930.log": "d1b5f1fef92a34bc053c743801d0728c55774fcee2e98b3b7849b0a13db7271b", + "benchmark/results/perf_declared_callable_kernels_20260930/validation/perf-declared-kernel-integrated-consumer-tests-20260930.log": "62425fa689161103b9b8581498903c11b95a69174fd98534a8a1bdc3de9b0222", + "benchmark/results/perf_declared_callable_kernels_20260930/validation/perf-declared-kernel-integrated-dependency-install-20260930.log": "6a43b60d9aa63d7980745c647c86eca240f1d2abc0245ddbc779ca6f6b042959", + "benchmark/results/perf_declared_callable_kernels_20260930/validation/perf-declared-kernel-integrated-installed-provenance-20260930.json": "edbf9a3458ee6a64f3c57932b331ef78697cbcbe93872f34047918e4fbd76217", + "benchmark/results/perf_declared_callable_kernels_20260930/validation/perf-declared-kernel-integrated-r1-20260930.log": "2b2816ec41fb06232920106b03b9dabbd8f044d8e7e7ed0864489b7d9c298c11", + "benchmark/results/perf_declared_callable_kernels_20260930/validation/perf-declared-kernel-integrated-wheel-build-20260930.log": "6553775a2fe6c5e80dcc3fb402de9cfaf3fbe4de7aab40ddab00fc369e4561a4", + "benchmark/results/perf_declared_callable_kernels_20260930/validation/perf-declared-kernel-integrated-wheel-install-20260930.log": "f265f60af03f420f3549488e30f2eb7783fdba01b42777877a66980b69d0b153", + "benchmark/results/perf_declared_callable_kernels_20260930/validation/perf-declared-kernel-nra-admission-order-20260930.diff": "dc242266c89b1d501e68bea9bf4f30fbc3b527bfa5afa94d7402e37d87800908", + "benchmark/results/perf_declared_callable_kernels_20260930/validation/perf-declared-kernel-nra-cache-projection-20260930.diff": "ab986425bc54dcf18ab1229569732c15eedb7cfd6fd3831a2d4fd6eff089f6be", + "benchmark/results/perf_declared_callable_kernels_20260930/validation/perf-declared-kernel-nra-capture-admission-20260930.diff": "6ccb606252a410c366df37fed4068cdc5a8a1f6cb335d93f641fb8edd608012f", + "benchmark/results/perf_declared_callable_kernels_20260930/validation/perf-declared-kernel-nra-hook-phase-20260930.diff": "09a62d9cc814bd90382f96891f6c3c555e35d14b3bcba8eabb0d5fe93fecd016", + "benchmark/results/perf_declared_callable_kernels_20260930/validation/perf-declared-kernel-nra-source-transaction-20260930.diff": "1eab40f216c91868545ccedeee0792293866d293e9b4d9786e782dfd9e9f2de1", + "benchmark/results/perf_declared_callable_kernels_20260930/validation/ratchet_benchmark_full.json.gz": "3c44f2b26488ffac4c4aaab2ffc91549bab5280586e8ac370b9c815a7c84e549", + "benchmark/results/perf_declared_callable_kernels_20260930/validation/ratchet_openhcs_full.json.gz": "a1f8b10203a6d192a003ae6872197ec985dfe7efa77af14940572599f6546565", + "benchmark/results/perf_declared_callable_kernels_20260930/validation/ratchet_scripts_full.json.gz": "b9c7d20235e8d1cb736e637422a20de5a9d6d7b1e6e7257d8ea3c9aef377ab8a", + "benchmark/results/perf_declared_callable_kernels_20260930/warm-16w_observations.jsonl": "23dc4c804613368e9fc6510241836f5db5e0cf645861dd4a40cfb00370e08262", + "benchmark/results/perf_declared_callable_kernels_20260930/warm-16w_scope.json": "4ca55b6593152f4e32693daa0a0c5e5f68116df069ab6cf6989b18f18bde9326", + "benchmark/results/perf_declared_callable_kernels_20260930/warm-1w_observations.jsonl": "c3e1db27a443061c62a26fd2fb28f740305d50465e268b5b87adc4844c41a322", + "benchmark/results/perf_declared_callable_kernels_20260930/warm-1w_scope.json": "c2748dfc40a186a4ef21218ff114dada404d5b62b05b05c52b9198b45c76019a", + "benchmark/results/perf_dependency_source_readiness_20261003/shared-environment-qualification.json": "8715be9d5e5bcbf1fa1334f67433b22c50e2e2f4d8140d127bddc912db344c48", + "benchmark/results/perf_direct_docstring_20260929/README.md": "bc96703dee346baa7971d5d1ab35cc9f475a75d34514396ed74f0ac8356b4559", + "benchmark/results/perf_direct_docstring_20260929/control_fresh.csv": "65d9a015f07256c5b07e6c4baaf3c14ddbc05cd3bc0c9206f3eabf703cd38cd8", + "benchmark/results/perf_direct_docstring_20260929/direct_fresh.csv": "baeaa02de3efa4c944a435ead533bb963fba62649c1ea0eb5a2381281c804956", + "benchmark/results/perf_direct_docstring_20260929/direct_fresh_native_relative.png": "c0449c5128451e07d2a44d329b1196fc59a68df50852d9ad62994f0af6a9e7c3", + "benchmark/results/perf_direct_docstring_20260929/direct_reused.csv": "b522f2b104075ff815b301e78eae3b5b52b6133ceeba83dc17373b16f01d8973", + "benchmark/results/perf_direct_docstring_20260929/direct_reused_native_relative.png": "ed398949fc43f8cdb4b4b36952d1518096629b2cc8a0db036161a6b2231831b2", + "benchmark/results/perf_direct_docstring_20260929/native_cp_summary.csv": "a14d75cfc762140506f703731d78e99310f72f4eae5374345cb8ec008222558d", + "benchmark/results/perf_fast_poll_20260929/README.md": "6a0f668f0579750259855b4b5138bf1c5752589f6f4a05426ee5248918c6c5f2", + "benchmark/results/perf_fast_poll_20260929/fast_poll_well_throughput.csv": "5043444a0a49f963c95d5efd0b29d5e7712b599a511fd7e9da80ab30369f2b9f", + "benchmark/results/perf_fast_poll_20260929/plot.py": "c730cac645b7684ccf31ff3df04ace779c0c8b443247290145a987aaafe280b7", + "benchmark/results/perf_fast_poll_20260929/polling_total_and_slack.png": "64704ceda22ab313afa9510b7016f4af7792c118c59ac98423b36d902f3a0c5e", + "benchmark/results/perf_fast_poll_20260929/wait_breakdown.csv": "71633d5f6c9521c68b8e7a7c91b841ba1a215046392685e5d4c9b0dc19c5b0fc", + "benchmark/results/perf_fork_compiler_prewarm_20260929/README.md": "d41090fd6c37fbb1253955f0cb79573607f6f1296eb4a96cc021ee7b6d2bbaed", + "benchmark/results/perf_fork_compiler_prewarm_20260929/measured_phases.png": "858610eb6d10a9f6fedcca6981436a3f516056d8fc86861e85ae8cf12a0d9d34", + "benchmark/results/perf_fork_compiler_prewarm_20260929/measured_phases.svg": "979030a49c9a62d9d7f17a6d30af2c1e8f237288e75fd1c14776d1c44362f0ad", + "benchmark/results/perf_fork_compiler_prewarm_20260929/method_probe/fork-cold.json": "4829bc68e88e147c4be1be12f7db0426657c0c1dc416ce5279bdc7aac887ccda", + "benchmark/results/perf_fork_compiler_prewarm_20260929/method_probe/fork-warm.json": "422dd55cb471e417a4a31dd1e836d8a9877a96ddab5a5aa676c0939a3e30a780", + "benchmark/results/perf_fork_compiler_prewarm_20260929/method_probe/serial-cold.json": "b18e0f438fdd8db56718141bd014869fb91e172f1c4f44c0d8552353290c6a4e", + "benchmark/results/perf_fork_compiler_prewarm_20260929/method_probe/serial-warm.json": "7ec6997a89b5878d277eed644e25aa41e089b317cbccaee47e9098dc59905ab9", + "benchmark/results/perf_fork_compiler_prewarm_20260929/observations.csv": "0b4900371140a957121146b55f9014ed353baaa0dc56e9988eb871a577b57277", + "benchmark/results/perf_fork_compiler_prewarm_20260929/output_parity.csv": "d4cceb88faee5a6fb0a2c85feb4f643bb5a5e00d957cba18bfaed015c60b6ded", + "benchmark/results/perf_fork_compiler_prewarm_20260929/plot.py": "e41deef66523bc3e89c4f1a27a73efb2e2473675bbecd7bbbdf710cb4d7ca34b", + "benchmark/results/perf_fork_compiler_prewarm_20260929/provenance.json": "ad70f31ef686909f49ff393c05932fb3a0194cc232586639067bed8eb5707831", + "benchmark/results/perf_fused_haralick_20260929/README.md": "7e76ad808f3956e15618329727a0ebe8f2999338fddcf2783256a525fac1d3ef", + "benchmark/results/perf_fused_haralick_20260929/apply_fused_haralick_20260929.py": "7d729dd9fbc183ae68da8b5e3c75964002c50be3249a46c6b8233bdbcb985a0c", + "benchmark/results/perf_fused_haralick_20260929/check_haralick_export_parity_20260929.py": "92a938d62445a26f20c91fea4f32165ea5e6e93a30048cf8eb190997ff379d14", + "benchmark/results/perf_fused_haralick_20260929/class_roles_after.json": "bc630b2cdddca8a1ec18279d6f86949eb508cb03510d8f8f0ac512985f8e2c59", + "benchmark/results/perf_fused_haralick_20260929/class_roles_before.json": "dde2ffc4555e31cdbe3d877e3a57fc60ba218d91614ab5c28ecaddc01fb61d55", + "benchmark/results/perf_fused_haralick_20260929/main_dependency_sync_tests.log": "983deaad161b346d90a4e59daf3686b5b4fe8aa576f5df8621c56ac3f6ed7762", + "benchmark/results/perf_fused_haralick_20260929/measured_phases.png": "6fa05f0f2f3dd4b2fd7c3806a96aa40207003b9bbbcb9b5a72e85f433bfffaab", + "benchmark/results/perf_fused_haralick_20260929/measured_phases.svg": "7d9107d9f15f825ed9af7ca1347cfb6862d058bf9bd10c9553a870e3ed3f146e", + "benchmark/results/perf_fused_haralick_20260929/medians.json": "df7af7ee2b7622469ba974f66ca6b55b19313b5e66b3f25c9cb29007a5cdbb58", + "benchmark/results/perf_fused_haralick_20260929/observations.csv": "331e30099f88d9033cbed7ddb2329c822e84b2faee4aec9d18167d9b985cd483", + "benchmark/results/perf_fused_haralick_20260929/ownership.md": "2e4ea25eeac128ba73bf3bbf00f517bd191f62309cd6d0a08c31ceb19a3dcf0c", + "benchmark/results/perf_fused_haralick_20260929/paired_prewarmed_haralick_runs_20260929.py": "07f207d22ec54b360c7a63bde26ccccc8b9bab3980c9d4253f0302009895b2b4", + "benchmark/results/perf_fused_haralick_20260929/perf-fused-haralick-nra-projected-20260929.diff": "cfcd7011b8abd4d27efb04595c98d3a2036fcce0778f7122694b91a965500d80", + "benchmark/results/perf_fused_haralick_20260929/perf-fused-haralick-ruff-parity-20260929.json": "103a9e3ac59b9c2fafa1ddc9f198e08ee7f9e8678d785d8aa5532a2428a06150", + "benchmark/results/perf_fused_haralick_20260929/perf-fused-haralick-tests-20260929.log": "e6e523bb5c1c1054c9c2c2352e64bd1d54fb885204c325b8b17784ec82f188d2", + "benchmark/results/perf_fused_haralick_20260929/perf-fused-haralick-tests-fixed-20260929.log": "50074f61feb1f320694ee94760c93e04f68768128b92dc4dd318423deb4ef741", + "benchmark/results/perf_fused_haralick_20260929/perf-haralick-export-parity-20260929.json": "c34424a59ce61c8380ebfccd9a3916fa6870af8d980f97f99053c32437a0a29e", + "benchmark/results/perf_fused_haralick_20260929/perf-haralick-prewarmed-pairs-20260929.log": "e127f456b8b26329323095f6100e3fa9d5fd7cae483175e04fd4e3d6025a5cd0", + "benchmark/results/perf_fused_haralick_20260929/plot.py": "e41ce367959d5eb736ac968b08831d3bcf0366bed8b2599c655666635a0f7e7c", + "benchmark/results/perf_fused_haralick_20260929/replay_production_fused_haralick_20260929.py": "af61aadb42df3ba5a63bcb185be4aa472cd01b71cbd330fbec8b5ee26dc0c21b", + "benchmark/results/perf_fused_haralick_20260929/saved_frontier_replay.json": "db39a89bb2ef2c52fc049a52d849271021e9268b6e830953b23f2e7a98652b02", + "benchmark/results/perf_fused_haralick_20260929/steps.csv": "0495f48c3d61c8b7a82699c3b39976ef7f215905b839eace1277d59bee7f32ff", + "benchmark/results/perf_fused_haralick_scaling_20260929/README.md": "d08dcbf2dbe4ba48c9e01b82d66e799751d3135da6ab54ba3929916ac44b956d", + "benchmark/results/perf_fused_haralick_scaling_20260929/input_digest_checks.json": "73ba5709841fd20f52f0bc34254fa251044648b54bedc0fd66f9770f03a199c1", + "benchmark/results/perf_fused_haralick_scaling_20260929/measured_native_scaling.png": "dffdc3ea829e541384eb111aef4da28943134791d002a69ad9d6c2cf02919658", + "benchmark/results/perf_fused_haralick_scaling_20260929/measured_native_scaling.svg": "5ee1d4c2a69fa7b1678535749d33e77ad9b7b4e3613f113b6a6c1824cc441975", + "benchmark/results/perf_fused_haralick_scaling_20260929/measured_ratios.json": "cf0a3644cf5789ce7f15e28862c832ebb26bcdb3070e574582c9792ca01f6d32", + "benchmark/results/perf_fused_haralick_scaling_20260929/native_16w_provenance.json": "462f5fe8862808815cd1f3a0b5ac9783023ef3a3f962ad6c1b15f5e7a16396f8", + "benchmark/results/perf_fused_haralick_scaling_20260929/native_16w_summary.json": "bb75252582f04b94bb7a32aa56e1dd67c9d036a0ee17aa6707a62fdc1b0b3058", + "benchmark/results/perf_fused_haralick_scaling_20260929/native_1w_provenance.json": "ab996e3a02d6b83992c223a6a2886a9a2d184ca3e7ab5870f4bd7c48340271c1", + "benchmark/results/perf_fused_haralick_scaling_20260929/native_1w_summary.json": "e91e1264b72e4ab06454b8746d2b4d1902c88df48690b79427856f2d83182bf8", + "benchmark/results/perf_fused_haralick_scaling_20260929/openhcs_16w_lanes.csv": "0c77c84e969f26c842d77093adbdac5cdda74b81a7381fe0e2cb28d3c28f305b", + "benchmark/results/perf_fused_haralick_scaling_20260929/openhcs_16w_receipt.json": "64bc0294f0e10c59723efe0764078a73d28a083af271a2a8c68b8f611473021a", + "benchmark/results/perf_fused_haralick_scaling_20260929/openhcs_16w_steps.csv": "66f44fcfad3a5f2ac38c07086c144bc49c554d53449537f302bc50d703efc700", + "benchmark/results/perf_fused_haralick_scaling_20260929/openhcs_1w_lanes.csv": "4cdaedb8f3615d48ac4abc5085f604163cd0b25a8bfb75cb8400a8da84685870", + "benchmark/results/perf_fused_haralick_scaling_20260929/openhcs_1w_receipt.json": "61c71a06d6a84bd33cbd597cd95f77cb2321b5f640f2830eac8793ebec43d751", + "benchmark/results/perf_fused_haralick_scaling_20260929/openhcs_1w_steps.csv": "c872b6a597f12a94545d20aa4dd77e760476e0ea58893e2a2526e502b6d25286", + "benchmark/results/perf_fused_haralick_scaling_20260929/openhcs_observations.csv": "8966df254b818a295778cd3d6cb7ae22a462727e173e599c8ed7012d892a6b5d", + "benchmark/results/perf_fused_haralick_scaling_20260929/output_prefix_check.json": "c25e63b2f86280be762759c7d8dbb08d4c0d0b19be97d71ed1f53fc9104cf457", + "benchmark/results/perf_fused_haralick_scaling_20260929/plot.py": "e0b563e48ec793f1f90c73a571b51f32ce80b5b12c74fb36ebf55aa65e92f6aa", + "benchmark/results/perf_fused_haralick_scaling_20260929/provenance.json": "f4d8294e14b6f248d921b672f2194b3c987477a3b8b3d20cedde2885fd5b7f05", + "benchmark/results/perf_fused_haralick_scaling_20260929/recovery_capture.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "benchmark/results/perf_fused_haralick_scaling_20260929/recovery_capture_status.json": "20db60eac2659f3592e99d22549f2903134c4c60d7dc8ad1ba57051d5e56b0c3", + "benchmark/results/perf_granularity_native_sampling_20260930/README.md": "1152f62f7018b2373ad0f889ba58b527b17f0396924dabcab3d151f695742ca7", + "benchmark/results/perf_granularity_native_sampling_20260930/analysis.json": "deb2f60fbf6ef2eedf73815eb7e1d72e11057aeafeb3e3f66b1604fb0eb371b1", + "benchmark/results/perf_granularity_native_sampling_20260930/analyze.py": "a4e35ce573ed5fa0a7f4bb960bf0fe072cf94ae9d9b86cb4fe79286f240192fa", + "benchmark/results/perf_granularity_native_sampling_20260930/architecture.md": "bab2d8d5f853ccc6a639d2b3fbd01368379b58ef77a29d6392e2b01739453e4c", + "benchmark/results/perf_granularity_native_sampling_20260930/census_summary.json": "cd494090302120e5cafddee7d0321f583cfd0fe4039951b76846f99f42551940", + "benchmark/results/perf_granularity_native_sampling_20260930/check_export_parity.py": "5b73605782f75598c76925966fe31d363c28c474de6f3f3735cc5fedea10c525", + "benchmark/results/perf_granularity_native_sampling_20260930/cpp_class_census_after.json": "407f99faf394974eb9477c231f4b10c04b563ee7c3d05c9f089e8d7539075123", + "benchmark/results/perf_granularity_native_sampling_20260930/cpp_class_census_before.json": "e5fab502796c82b18d7c4b68bdff81cc97865ba3fdcca20968795be05b63ac45", + "benchmark/results/perf_granularity_native_sampling_20260930/dtype_replay.json": "1a84c800f307e6cdb8ad90333ee21ed47591310e6d72704798bfc5c1cf77d7fe", + "benchmark/results/perf_granularity_native_sampling_20260930/earlier_observations/perf-granularity-native-paired-1w-20260930/observations.jsonl": "620ff1fe5dd7ce94e220c35e548db7e17119c578f38cefbdb006e9c60381af48", + "benchmark/results/perf_granularity_native_sampling_20260930/earlier_observations/perf-granularity-native-paired-1w-20260930/scope.json": "da112f2162f1517ce4115033d7ec39fac5d669590e8a2432621dc0445b733411", + "benchmark/results/perf_granularity_native_sampling_20260930/earlier_observations/perf-granularity-native-paired-steady-1w-20260930/observations.jsonl": "71746efe6578d8d1ead766c760b6b432eb308fc049e91448a2f08dad60d16105", + "benchmark/results/perf_granularity_native_sampling_20260930/earlier_observations/perf-granularity-native-paired-steady-1w-20260930/scope.json": "da112f2162f1517ce4115033d7ec39fac5d669590e8a2432621dc0445b733411", + "benchmark/results/perf_granularity_native_sampling_20260930/export_parity_16w.json": "7bdc09386d2048f28c5a24de245d89c03c5a57c258685ae5556c01652eee1a61", + "benchmark/results/perf_granularity_native_sampling_20260930/export_parity_1w.json": "690fc2abb40ca286775b54cacf182b89685ee2d07095194b8c01ff67c751e226", + "benchmark/results/perf_granularity_native_sampling_20260930/input_digest_checks.json": "73ba5709841fd20f52f0bc34254fa251044648b54bedc0fd66f9770f03a199c1", + "benchmark/results/perf_granularity_native_sampling_20260930/measured_granularity_scaling.png": "e65054ae2f4cee0278a56ba062d4876b8efa5048af6673d0634054947cc7c071", + "benchmark/results/perf_granularity_native_sampling_20260930/measured_granularity_scaling.svg": "c31994311b1b2c0b0e87c992e8223429927c7be1dd7931c3fc71a8fbcb8daa6f", + "benchmark/results/perf_granularity_native_sampling_20260930/observations_16w.jsonl": "9ee767da07b97e9bafb7182091a0dfde2e6e1b5755916c6a7605a128a8617027", + "benchmark/results/perf_granularity_native_sampling_20260930/observations_1w.jsonl": "245ee0039af090258c4cbf4e0a9b7ab46b3a040115ac0b620cafe790632700bb", + "benchmark/results/perf_granularity_native_sampling_20260930/plot.py": "06ce5513e65947b237fa77d3b287214867da4c5386a9078169e313852f8f7c56", + "benchmark/results/perf_granularity_native_sampling_20260930/recipes/01_own_sampling_grid.py": "a0c0e6b91b58525fe9bd7fe9969f8887858f84dc31b1319c0b1b178b70aacd90", + "benchmark/results/perf_granularity_native_sampling_20260930/recipes/02_migrate_sparse_consumer.py": "4908418793212e2a6d8ae3bf6eac2f23aa8c3c02398c76f02d99aeda0101df00", + "benchmark/results/perf_granularity_native_sampling_20260930/recipes/perf-granularity-grid-nra-projected-20260930.diff": "62971066a48920e339c32b0d6ada7f6865c324d15870bd49b8bed0a288791817", + "benchmark/results/perf_granularity_native_sampling_20260930/recipes/perf-granularity-sparse-grid-nra-projected-20260930.diff": "d69dfa553f880c8e1ad4cb30b58c0907af534b7afb2955a0e0a9eed21ce6f999", + "benchmark/results/perf_granularity_native_sampling_20260930/replay_saved_inputs.py": "b93163d027d07c59def5f959a168b766c035e1a3f0741daee8294fef0543d6eb", + "benchmark/results/perf_granularity_native_sampling_20260930/run_pairs.py": "5d44d85055dfc568b8418e7014db55d32bd6fa3d5bfd97ac5d13241b7a6c8a75", + "benchmark/results/perf_granularity_native_sampling_20260930/runs/16w/candidate_1/lanes.csv": "68fea9b269da9b6043688cf21451bdbae79c19040bb66d8c478ac80aca2da786", + "benchmark/results/perf_granularity_native_sampling_20260930/runs/16w/candidate_1/observation.csv": "a29e960b190d06e3052f32771096bccd9bc1b6941822ba28bbf826470c327250", + "benchmark/results/perf_granularity_native_sampling_20260930/runs/16w/candidate_1/receipt.json": "0edc169e4ca223f7e9159123c472262dceb5adbe9c181e1bf972947c4feb0ccb", + "benchmark/results/perf_granularity_native_sampling_20260930/runs/16w/candidate_1/steps.csv": "91da0c0f94fa8dfa7db52e21d8a8930c14444994d1ea4df0d17590d751deea36", + "benchmark/results/perf_granularity_native_sampling_20260930/runs/16w/candidate_1/warm.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "benchmark/results/perf_granularity_native_sampling_20260930/runs/16w/candidate_2/lanes.csv": "6d19d8541b3f1e5536f975b1cf1487ebf7a768f6e537ef6e4d39de1283e71ecb", + "benchmark/results/perf_granularity_native_sampling_20260930/runs/16w/candidate_2/observation.csv": "d2e49cb7c87f9cf5448bea18d2ad2bda42d5849407a1a96c960242f7f5d9b194", + "benchmark/results/perf_granularity_native_sampling_20260930/runs/16w/candidate_2/receipt.json": "1a17c4e7da48c46c45e48bb6f4e43c3d4a4c6e40f9e8acfa420dc2ce1601853c", + "benchmark/results/perf_granularity_native_sampling_20260930/runs/16w/candidate_2/steps.csv": "9157fb7b32d155a5f81706cd781a33153f1fef439221230076fe584f20f02e8e", + "benchmark/results/perf_granularity_native_sampling_20260930/runs/16w/candidate_2/warm.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "benchmark/results/perf_granularity_native_sampling_20260930/runs/16w/control_1/lanes.csv": "2de4f611388135bb764f3b54ac5117cd3680a70ae432b2e980627d670bacd04d", + "benchmark/results/perf_granularity_native_sampling_20260930/runs/16w/control_1/observation.csv": "638db8e59ee88885dacc7b17abd8af3942dcfb3fb048ec5b031ff85c0da02f8a", + "benchmark/results/perf_granularity_native_sampling_20260930/runs/16w/control_1/receipt.json": "5743cc94191e009e5357f6c4c41556640549b870870213599e2b72d39e71f9b5", + "benchmark/results/perf_granularity_native_sampling_20260930/runs/16w/control_1/steps.csv": "101563be3b6b0a60d7016c19739ffa68583ff9f3f61f2e08d958c35a8aafde7c", + "benchmark/results/perf_granularity_native_sampling_20260930/runs/16w/control_1/warm.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "benchmark/results/perf_granularity_native_sampling_20260930/runs/16w/control_2/lanes.csv": "90f4704a2214ae71623e15f2bb0e91eee9566b7a0ebb4295c115c1eccf6e21c4", + "benchmark/results/perf_granularity_native_sampling_20260930/runs/16w/control_2/observation.csv": "6375cc07b07f03e5b214a183142f74fc9c2e50fd426f73e1271dca8a3832482b", + "benchmark/results/perf_granularity_native_sampling_20260930/runs/16w/control_2/receipt.json": "90a12637dab99a321013e3f4e10dca97b96eaf7b9505b936f38af012a2da4f03", + "benchmark/results/perf_granularity_native_sampling_20260930/runs/16w/control_2/steps.csv": "bae568bf6030c1ffe8ce98fcd9b932cf775cb42c1d029283b2bb1dbf465d65e4", + "benchmark/results/perf_granularity_native_sampling_20260930/runs/16w/control_2/warm.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "benchmark/results/perf_granularity_native_sampling_20260930/runs/1w/candidate_1/lanes.csv": "3119508f57e8692012b64074d4efe0cceb4f1a4e2a5f7391c3fec3b3909920aa", + "benchmark/results/perf_granularity_native_sampling_20260930/runs/1w/candidate_1/observation.csv": "aefd9df5b670b4fbf7b0d43eaa4740289d0de17bb9c5c94788fd97233b0638d1", + "benchmark/results/perf_granularity_native_sampling_20260930/runs/1w/candidate_1/receipt.json": "4df5d233714528b408c2f0c200d48f57b1b51f996a036ce1b72617c4194308a8", + "benchmark/results/perf_granularity_native_sampling_20260930/runs/1w/candidate_1/steps.csv": "93c5fd981444a55c373bcb6b5d86c54b623dc132061ed37a6fc0b727ba8f3a76", + "benchmark/results/perf_granularity_native_sampling_20260930/runs/1w/candidate_1/warm.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "benchmark/results/perf_granularity_native_sampling_20260930/runs/1w/candidate_2/lanes.csv": "51d665398e0caced3ff6011cf84688c5284bc122d5d343330f340c4ebc7bdcb8", + "benchmark/results/perf_granularity_native_sampling_20260930/runs/1w/candidate_2/observation.csv": "ebb76efb20b4981255ed3afa62a5ce27e10f71c352415da154314be1821ea103", + "benchmark/results/perf_granularity_native_sampling_20260930/runs/1w/candidate_2/receipt.json": "c5167be0a785cc0ac05cd5e583769a592104178aec995148cb7af4fb0090497e", + "benchmark/results/perf_granularity_native_sampling_20260930/runs/1w/candidate_2/steps.csv": "f02fd2969726f3081401c97ae39881972701bf67ff94d442c64b151a87592ca1", + "benchmark/results/perf_granularity_native_sampling_20260930/runs/1w/candidate_2/warm.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "benchmark/results/perf_granularity_native_sampling_20260930/runs/1w/control_1/lanes.csv": "c54a086f9a5dfca4c108f1f27d4cb1647fbd434758de6486a3fb2226befce6d1", + "benchmark/results/perf_granularity_native_sampling_20260930/runs/1w/control_1/observation.csv": "a8a6e73d1477493ec7c979ee8a7b6d0df30dab71664649661e5ace31693133ac", + "benchmark/results/perf_granularity_native_sampling_20260930/runs/1w/control_1/receipt.json": "84038f3397c0da6f4f2291e0748f3a38777f60275091805d1d6b785512dd4f88", + "benchmark/results/perf_granularity_native_sampling_20260930/runs/1w/control_1/steps.csv": "80552d2035d0a15666f06955c2e180840926a2225b113eb93ca766e475a9f675", + "benchmark/results/perf_granularity_native_sampling_20260930/runs/1w/control_1/warm.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "benchmark/results/perf_granularity_native_sampling_20260930/runs/1w/control_2/lanes.csv": "adab77881db78c11b02c443bca27095885b62893cfece3e0661b71939d008dfe", + "benchmark/results/perf_granularity_native_sampling_20260930/runs/1w/control_2/observation.csv": "a057337107afca793e6426f30bec8b3d4c911219a34ed9a712c1a324d23410a9", + "benchmark/results/perf_granularity_native_sampling_20260930/runs/1w/control_2/receipt.json": "3b77cddd993412eb442c3f39acff7b126e85770bf92633b20fb79392ec9c956a", + "benchmark/results/perf_granularity_native_sampling_20260930/runs/1w/control_2/steps.csv": "a1a6708170695b214dccdaa3c52d9130c9bc6ae70f671cb4f23a1cba379f882e", + "benchmark/results/perf_granularity_native_sampling_20260930/runs/1w/control_2/warm.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "benchmark/results/perf_granularity_native_sampling_20260930/saved_input_replay.json": "8736f32e04929928f08e7de507036e80f4c82c9e03771fdfbe8aaf37bcaaeea8", + "benchmark/results/perf_granularity_native_sampling_20260930/scope_16w.json": "dc9c90ba6a6e90e50e763cf4a35ce9e4069e68305bf3c241b0b0c9f612bcbd12", + "benchmark/results/perf_granularity_native_sampling_20260930/scope_1w.json": "2810bbd9100bf34ffd717ef8748e603a3e83670914b2b706d5d3c8766529525a", + "benchmark/results/perf_granularity_native_sampling_20260930/validation/perf-granularity-latest-main-integration-tests-20260930.log": "251044be6f7d4a933a4457fa8a41568d33a37206499e45b72f4654624ff87a73", + "benchmark/results/perf_granularity_native_sampling_20260930/validation/perf-granularity-main-integration-tests-20260930.log": "84afadedbd349b06243f8a97c34d7454f25b13e3faaa60e46018e183bd7a6e89", + "benchmark/results/perf_granularity_native_sampling_20260930/validation/perf-granularity-native-build-20260930.log": "817b0dde474117531a1dfad095033631f976ac15f83cb1cbc65f4134dd7f52e7", + "benchmark/results/perf_granularity_native_sampling_20260930/validation/perf-granularity-native-candidate-1w-r1-20260930.log": "dd3cb00ed5fce5b5de43d07c24d459c2bd99532cb29b60edc50e6dc124f52f06", + "benchmark/results/perf_granularity_native_sampling_20260930/validation/perf-granularity-native-candidate-1w-r2-20260930.log": "306c705d985addf6b7bd8a472b9fa1bbc19605e696bc55dd9bb45ef5b9603da4", + "benchmark/results/perf_granularity_native_sampling_20260930/validation/perf-granularity-native-candidate-main-16w-r1-20260930.log": "e1b58481bbefc3823889c21190bf591f1010ee43001c07a78d4d784a08376a2f", + "benchmark/results/perf_granularity_native_sampling_20260930/validation/perf-granularity-native-candidate-main-16w-r2-20260930.log": "6ba6853435ef66126d38702a49e905518e4576616fb5302405c47691bd433c0c", + "benchmark/results/perf_granularity_native_sampling_20260930/validation/perf-granularity-native-candidate-main-1w-r1-20260930.log": "2194f7f736cacc6aeaf109e596825a19f2687f1e142b492805f69ba98fe48ab9", + "benchmark/results/perf_granularity_native_sampling_20260930/validation/perf-granularity-native-candidate-main-1w-r2-20260930.log": "a3a87051d4fdd1b832204d602ee90ef41528076158a1960f6f0cda504076378d", + "benchmark/results/perf_granularity_native_sampling_20260930/validation/perf-granularity-native-candidate-steady-1w-r1-20260930.log": "11f3ea9c45d4f439b36d9cb09bc44ae3db7d20f4b7cc7a790511fd228c0ad22d", + "benchmark/results/perf_granularity_native_sampling_20260930/validation/perf-granularity-native-candidate-steady-1w-r2-20260930.log": "99a08919e6746613714d874e6fe5dd222768b4bf729156bdcffb0fa305f8c56f", + "benchmark/results/perf_granularity_native_sampling_20260930/validation/perf-granularity-native-control-1w-r1-20260930.log": "aec47865920cbfe92f775aed275c35604a9d8f7d41d490fe03166f89d822bcdb", + "benchmark/results/perf_granularity_native_sampling_20260930/validation/perf-granularity-native-control-1w-r2-20260930.log": "aa9816c08cdf89e12c760a0de974347b84b66c6e7ddf254be13153e3e0016b63", + "benchmark/results/perf_granularity_native_sampling_20260930/validation/perf-granularity-native-control-main-16w-r1-20260930.log": "275dfe2f0f47e4a83dd68415f06786106ced00ce0aebbfd7469aef6b9a5cb683", + "benchmark/results/perf_granularity_native_sampling_20260930/validation/perf-granularity-native-control-main-16w-r2-20260930.log": "52eb6c7cec6da570e47c45173b3f7e99e5d7ee013f63864c42bbed8e22a26155", + "benchmark/results/perf_granularity_native_sampling_20260930/validation/perf-granularity-native-control-main-1w-r1-20260930.log": "87c77149c096d426fb4a4b4263d5e38f580446bbc10e5e17d5e490fa0c66d65c", + "benchmark/results/perf_granularity_native_sampling_20260930/validation/perf-granularity-native-control-main-1w-r2-20260930.log": "2839fa4c7b0174d81059b80b52c386cd962285dfe1f4d39cdc47fa4bc03e656e", + "benchmark/results/perf_granularity_native_sampling_20260930/validation/perf-granularity-native-control-steady-1w-r1-20260930.log": "a3e539f1f97afe81931b3a7edfd3e413f928c314b62630a2a67298bb8c870e8f", + "benchmark/results/perf_granularity_native_sampling_20260930/validation/perf-granularity-native-control-steady-1w-r2-20260930.log": "da98ff540cb8690036024feb0432ddab850ae00c4278c24b568bbb109cfeb6ec", + "benchmark/results/perf_granularity_native_sampling_20260930/validation/perf-granularity-native-edge-tests-20260930.log": "70030fc54b05e8e948122cc7080b8587f3d26dfdbfcd156f2db85382d2449f0e", + "benchmark/results/perf_granularity_native_sampling_20260930/validation/perf-granularity-native-final-production-replays-20260930.log": "8736f32e04929928f08e7de507036e80f4c82c9e03771fdfbe8aaf37bcaaeea8", + "benchmark/results/perf_granularity_native_sampling_20260930/validation/perf-granularity-native-final-tests-20260930.log": "e0a7213e40a57df421c4d50092ef9fa4d8ba933ec2420404125752eb7f757629", + "benchmark/results/perf_granularity_native_sampling_20260930/validation/perf-granularity-native-final-wheel-build-20260930.log": "e584d1dde05604676bdb173cdfa716c574c07c49385e8287cc433c0955e6363f", + "benchmark/results/perf_granularity_native_sampling_20260930/validation/perf-granularity-native-first-tests-20260930.log": "0084d084ab04e8c2df76092e6ffd291a6bac141ad5a57682e4954a4d79fae6db", + "benchmark/results/perf_granularity_native_sampling_20260930/validation/perf-granularity-native-production-replays-20260930.log": "13a7334bcd744f19d7bd229df5d69c804f0b788476f141c253c066915c2a7e6d", + "benchmark/results/perf_granularity_native_sampling_20260930/validation/perf-granularity-native-strict-boundary-tests-20260930.log": "d3cfaa0082aaabf780fa23a4e6749de937f399bf9c2781dd80f42812115786e8", + "benchmark/results/perf_granularity_native_sampling_20260930/validation/perf-granularity-native-wheel-build-20260930.log": "7fe2a48d3508e397da00bf66c4edf570e34a93a1ab3d0491043109f53efc623d", + "benchmark/results/perf_granularity_native_sampling_20260930/validation_manifest.json": "ba0f6f052de666947fb34ac39365620589f7a811a204fcdddb4bb3f4f657d7ee", + "benchmark/results/perf_granularity_native_sampling_20260930/wheel_smoke.json": "8dad3fd78351c72b18e976a85a05447e7a068336eda51f4222b9f1deffa9b088", + "benchmark/results/perf_lazy_catalog_20260929/README.md": "a0383ead1f0009d7c0eb00e228fe3126188f526038f33100731060df76583df6", + "benchmark/results/perf_lazy_catalog_20260929/control_fresh.csv": "baeaa02de3efa4c944a435ead533bb963fba62649c1ea0eb5a2381281c804956", + "benchmark/results/perf_lazy_catalog_20260929/fresh_total_breakdown.png": "7fc23391c879535fca84188c431bc266a4bd59d3268e2f82631a2d89a99cd8e8", + "benchmark/results/perf_lazy_catalog_20260929/fresh_total_breakdown.svg": "f92b21dd7e5505606f04455b33f6c763afe23cdfb514ec878350a10644162995", + "benchmark/results/perf_lazy_catalog_20260929/lazy_fresh.csv": "0a2e7686d78cce4f21be8c51251519d678f5365d296a9d122743661e83281a50", + "benchmark/results/perf_lazy_catalog_20260929/lazy_native_execution_cdf.png": "602339ed2c680e288a11d7f23c0f7b1a1b6d9502459a14cbd150d0c2746931a2", + "benchmark/results/perf_lazy_catalog_20260929/lazy_native_execution_speedup.png": "c6fe70a732afb9b307150ae4b2f1cc0f60bd1c80057f28b92a14cad959d61730", + "benchmark/results/perf_lazy_catalog_20260929/native_cp_summary.csv": "a14d75cfc762140506f703731d78e99310f72f4eae5374345cb8ec008222558d", + "benchmark/results/perf_lazy_cp_registry_20260929/README.md": "17c1d91cfe8716bc1190a936006f8d32c52ead8ba013c28689a9defb2220e10d", + "benchmark/results/perf_lazy_cp_registry_20260929/abba.csv": "7d92d6e50da1037e10ffdb6854f40afa8b54dc56257ec610cc82fa0e515dd5b7", + "benchmark/results/perf_lazy_cp_registry_20260929/candidate.csv": "010c60ea28ac3b831d79eb2708c78debfa98fbccd26094b5cdd5d9fc827e00c7", + "benchmark/results/perf_lazy_cp_registry_20260929/control.csv": "c8a08fc6e9f00a9ce72ef4c60a62ef805ba07ffe168d381930e43de68ba9a344", + "benchmark/results/perf_lazy_cp_registry_20260929/fresh_server_total_breakdown.png": "1c25a11a741a5ed49fb6f7d117528ebdc5a00ac706d79effcc0619dd37499d76", + "benchmark/results/perf_lazy_cp_registry_20260929/lookup_only.csv": "9852b479dd349ecabc95e666e99a351786eba886b748e2135ba35ca17b4d371c", + "benchmark/results/perf_lazy_cp_registry_20260929/per_case_phase_deltas.png": "ded04cda8aeaed7485d5a73732f12544e05e0b960cfb5c7d61c025c8c04002c8", + "benchmark/results/perf_lazy_cp_registry_20260929/plot.py": "2b9927a8159da3887ae6265a2eead3fb85fde53871f2daf0138a25999a67a608", + "benchmark/results/perf_lazy_cp_registry_20260929/scoped.csv": "b7589b2a7dd3cc85b8b4732216f2cdde53d27269beb0b14364099418d26e596e", + "benchmark/results/perf_lazy_ome_zarr_20260929/README.md": "c4f1fd610b6b974cc5a581a1900782e321484f777f57f00caf5dd23359903acb", + "benchmark/results/perf_lazy_ome_zarr_20260929/candidate.csv": "2be45166bf843144544aecbddd0424f06158228025431c659c5b0762943ec50b", + "benchmark/results/perf_lazy_ome_zarr_20260929/control.csv": "77ad4113fdc086c9412ea0aab24e19a8db93cf960e749b2fb4a2ac92394455dd", + "benchmark/results/perf_lazy_ome_zarr_20260929/fresh_server_total_breakdown.png": "557d6ac7c7ec89d5a16a2d49781223573e4626a4cb6e0d01c8ba16e43f29aec1", + "benchmark/results/perf_lazy_ome_zarr_20260929/per_case_nonjob_delta.png": "a36ce9b826d711decfd2ad34f15ae8c4afa9e99910993e9b54cfaeb352debab8", + "benchmark/results/perf_lazy_ome_zarr_20260929/plot.py": "a555b4da51c6597d1b54608ae841cd38d3dcf167d8f3073ba6dfecbd42b8d1a2", + "benchmark/results/perf_long_form_export_20260929/README.md": "7305c2f43d9234eb01e7dd1d8250db11d43de375f91e495890ae7da2e012d1d9", + "benchmark/results/perf_long_form_export_20260929/imagingflow_scaling_vs_native.png": "c4cdc890b3917552efb8f0485e01347de22d99573e12bc049d30029699093e18", + "benchmark/results/perf_long_form_export_20260929/observations.csv": "56bbe1d3eea2e300a972b0e86659ed78defaf1f0d2c40ca0545a0945b1c812ed", + "benchmark/results/perf_long_form_export_20260929/official30_totals.csv": "c92665a1013b0ad22b9e0d476c37a91e7e2ca866d9bb7e6170385a64551a7c2f", + "benchmark/results/perf_long_form_export_20260929/plot.py": "13e0065694fc0b64ff12ec03e4bb34ea472e2dea62e3d565b0ead2ff408a81ea", + "benchmark/results/perf_native_batch_durability_20260929/README.md": "68e7414346907334ae197e388cc349232053a59000b87e2b71200f66766d9bde", + "benchmark/results/perf_native_batch_durability_20260929/authored_nra_transaction.py": "60342b74f0e62d69c90cefdf4676dc3b73edb3e4df26d12a65c05b8b6cddf4c1", + "benchmark/results/perf_native_batch_durability_20260929/class_roles_after.json": "3eba64ae4564c918f79dbc8619ebabf9b154e5a0998ecd409c495126ba4d7bfa", + "benchmark/results/perf_native_batch_durability_20260929/class_roles_before.json": "2425998e665b877af726f7f0e2c819bad1a892fb5d97a14dbb686af79203e5b9", + "benchmark/results/perf_native_batch_durability_20260929/perf-native-batch-durability-nra-projected-20260929.diff": "d59f34de3c253daf9b9dd77fbeb74b31b2006dd6f6f9f6090dbf368149d6709f", + "benchmark/results/perf_native_batch_durability_20260929/perf-native-batch-durability-ruff-parity-20260929.json": "0d9503776ce5fe2a238428d520007feb9699bd33606d72f7a4acb7d8cd24cbe6", + "benchmark/results/perf_native_batch_durability_20260929/perf-native-batch-durability-tests-20260929.log": "91df7ae57684662b410cb4ee37f2f63f6f0a2db89eb8b469c8db1e06a781aa9e", + "benchmark/results/perf_native_synthetic_scaling_20260929/ExampleColocalization_16w_provenance.json": "c199d7b4658e96981e33403e70c194573e47cbc76b5382d0fcad49a310214309", + "benchmark/results/perf_native_synthetic_scaling_20260929/ExampleColocalization_16w_summary.json": "04e596323cb05b7fac40e688a48d18d0daa9c0cd02dc23bc5859dfa0b4d4238c", + "benchmark/results/perf_native_synthetic_scaling_20260929/ExampleColocalization_1w_provenance.json": "344d44603b3b2f7d8e7bbd6127902298a1dea4a8a7a95e9cb1a7f54b375c92d9", + "benchmark/results/perf_native_synthetic_scaling_20260929/ExampleColocalization_1w_summary.json": "6ecda793fe883c638cfe3a876e8ca3cdf7c5eadc51e96e0566496863539cfb9a", + "benchmark/results/perf_native_synthetic_scaling_20260929/ExampleImagingFlowCytometryObjectsInGrid_16w_provenance.json": "4d2d29f71bb837907a484d98347e53c6935d65076c09e5b17dc6627beaf39793", + "benchmark/results/perf_native_synthetic_scaling_20260929/ExampleImagingFlowCytometryObjectsInGrid_16w_summary.json": "2091f9f1ec5763986b3f288f4e4cc0b906787430a12f5e09459b496dba285359", + "benchmark/results/perf_native_synthetic_scaling_20260929/ExampleImagingFlowCytometryObjectsInGrid_1w_provenance.json": "85b19062484e5b2791760e1269a831cd68a2fb9c008972692c4b3195d1ef9bbb", + "benchmark/results/perf_native_synthetic_scaling_20260929/ExampleImagingFlowCytometryObjectsInGrid_1w_summary.json": "4353c55fa9320cda777eb9e220a0e8ef1f0ce1585e12a3d225eb4d3633a1be0b", + "benchmark/results/perf_native_synthetic_scaling_20260929/README.md": "5d9d394d5c8630de888cb11c644c93eab5bc528b48c7f9c169cb883b747018ba", + "benchmark/results/perf_native_synthetic_scaling_20260929/cache_probe_observations.csv": "4d8bb5c4f25b844b3930c17682f699aff99e3c69e41450f9cb534345389830a0", + "benchmark/results/perf_native_synthetic_scaling_20260929/cache_probe_steps.csv": "88de53af149104bbccfe0f7de5d06208ece15dce78262d95a7c6319fe8fd5c65", + "benchmark/results/perf_native_synthetic_scaling_20260929/measured_native_scaling.png": "05562fdc06828c2d5f67e52543eb139a9d27b7762ea0625aef68c96590cbcddc", + "benchmark/results/perf_native_synthetic_scaling_20260929/openhcs_observations.csv": "7f3bc948e162c2a835710be3e42c12c88b42a523cf5c65e6a12b704810b313df", + "benchmark/results/perf_native_synthetic_scaling_20260929/plot.py": "d2127deac41ad1bd91e1794ea541171fbc2e66dc08536b111c39e62f23b4abfa", + "benchmark/results/perf_native_tabular_20260929/README.md": "18e208d5df7cd4cd9e17af6a51c7974b4f5accf9bd3f6a3b94d77b46a963e8e8", + "benchmark/results/perf_native_tabular_20260929/abi_audit.json": "f278609477391c5cf8815372b488b1ba6287f88458efbd74e35be9dd97cedc09", + "benchmark/results/perf_native_tabular_20260929/broad_tests.log": "40f1f288eb85faeaaa394105980a5632ba1fbcbe48603e286067a8ba8cb86e4f", + "benchmark/results/perf_native_tabular_20260929/focused_tests.log": "adf6e9cc742286bb53bb33508c9dc4123a7ffcdd459e678ac7721b844b5464fc", + "benchmark/results/perf_native_tabular_20260929/installed_wheel.json": "3e87d0df974ee052d57375363ea8419eb12d3712818c39d4d498a32bca6d002d", + "benchmark/results/perf_native_tabular_20260929/measured_pipeline_receipt.json": "54bf5ee3ad39545cb474f78f86004adf1a30a216b0a4636735e884fd1b5860bf", + "benchmark/results/perf_native_tabular_20260929/nominal_build_classes.csv": "a87fcc5b0d4b64c62bb372612d906bc574f9deffba401210e5e25ad204cc07cf", + "benchmark/results/perf_native_tabular_20260929/ordinary_run.csv": "c0a1583d7cf56bd34af55e4fb989d8a16efa088c71c5c1eb698090ff4770376a", + "benchmark/results/perf_native_tabular_20260929/ownership.md": "79862819e186a04f5de14ae0cb8c5fe9e0054b926c2c8ff568b50f2dbc96a77b", + "benchmark/results/perf_native_tabular_20260929/provenance.json": "f4360d39eb19357d0ff6acf0cfc4da22631643605eddfcf7b009060edd05ea87", + "benchmark/results/perf_native_tabular_20260929/ruff_baseline.json": "b7c5f1c4c036131be8fae5743ac4046dbe3205e1368c9c2d07a8b1af51810238", + "benchmark/results/perf_native_tabular_20260929/saved_input_replays.json": "4783207b8c40f2727db73cd2c058f01ff2da1d2e92ac2cb52565a8205f6104dd", + "benchmark/results/perf_native_tabular_20260929/step_timings.csv": "dde363d4f0c1c91aad22db7f933024f9c9780ce82365b7dddf439deda0f2b51f", + "benchmark/results/perf_nominal_preparation_20260929/README.md": "4559e5a21d21d3a6792f64fe0e95d8573a5b43b755f8ca341ec0bcc758330c3c", + "benchmark/results/perf_nominal_preparation_20260929/observations.csv": "c06c14948fbf011025820016a0ebdc7248b8ce4c39e24c80799f230253513d01", + "benchmark/results/perf_nominal_preparation_20260929/output_parity.csv": "6d7a4a17ac6720e3844c7f37c6fb49dc9e58a5f026f047714652da4de50f94bd", + "benchmark/results/perf_nominal_preparation_20260929/ownership_decision.md": "010031ebee7614ff83bf6fac9ef38cb35d46f6b7e01534575c0e2bb9dbb8209f", + "benchmark/results/perf_nominal_preparation_20260929/probe_spawn_cache.py": "9ff0b53fa0e4b6665d1ebb21cefccfebc7773de5302c8c308de00bc64ec7e098", + "benchmark/results/perf_nominal_preparation_20260929/provenance.json": "94d6a06c7742a6e6a89697695338ca08a58c5b9b1b328105599ea8a60d081c8b", + "benchmark/results/perf_nominal_preparation_20260929/shared_environment.json": "ceef376ab654d753103c155acdb8437f0e1441f32127131a56111a2411a07149", + "benchmark/results/perf_nominal_preparation_20260929/shared_mcp_health.json": "60bcb5a20f06309ca889a6362c833343d10418648c8b9a719e47eaba345c8ff0", + "benchmark/results/perf_nominal_preparation_20260929/shared_requirements.txt": "fe17c29ad9c9a782fda6dc8c2adcc0134da259435837b4f2621cbc5d42b11c2f", + "benchmark/results/perf_nominal_preparation_20260929/spawn_cache_hits.json": "40235c72fe6121dec137919c9d993e46a7562a2091524cb913e3b12abcd60bdf", + "benchmark/results/perf_owned_label_hulls_20260930/README.md": "926e851555d8d4054b006dc4887e0cb9a94648bf98db0b0e7c71b6f6dd7bb321", + "benchmark/results/perf_owned_label_hulls_20260930/analysis.json": "74381e4d1b7c6af6c828f5f467c0e1afca5ee190017395ad66b9bbe516f0883a", + "benchmark/results/perf_owned_label_hulls_20260930/architecture.md": "42699e2b9798cef4efac48ddb3e065f7b9fc6577067307e98eb7f0dba738bbc9", + "benchmark/results/perf_owned_label_hulls_20260930/architecture_transactions/perf-owned-hull-common-outline-nra-projected-20260930.diff": "366eca170026ef8722ad64d6c78e7e93e30b4d3980a8c4bf0e63c9ec4713e67d", + "benchmark/results/perf_owned_label_hulls_20260930/architecture_transactions/perf-owned-hull-leaf-contracts-nra-projected-20260930.diff": "f64d1fe84dc9a353928bc28c7ba80db001034eeac8ec89d1e255b3e8b546b0af", + "benchmark/results/perf_owned_label_hulls_20260930/architecture_transactions/perf-owned-label-hull-nra-projected-20260930.diff": "e41e5acb98e55395d6b4527e60f272d8a143afd8b90c7c336534a7b90289e493", + "benchmark/results/perf_owned_label_hulls_20260930/captured_circle_frontier.json": "d555f121c808764f521d6a876e54367817dc29695867d50396a0cad53875a9d2", + "benchmark/results/perf_owned_label_hulls_20260930/export_hashes.json": "fc0abbb1e6305f54eb626a9961477d4ad0cbf1928666ea599563ee04c993017c", + "benchmark/results/perf_owned_label_hulls_20260930/input_digest_checks.json": "73ba5709841fd20f52f0bc34254fa251044648b54bedc0fd66f9770f03a199c1", + "benchmark/results/perf_owned_label_hulls_20260930/measured_hull_scaling.png": "c1ce6d371f765ef36f4011052e8b954a668f0e634a5fe62b850f3c0096d20685", + "benchmark/results/perf_owned_label_hulls_20260930/measured_hull_scaling.svg": "a35e6a2992241d14ad15a57e8bcc0b21c493fe66c1a20a886f1842ed0ee85a8e", + "benchmark/results/perf_owned_label_hulls_20260930/observations_16w.jsonl": "cc3e6ff6865888e77693dedf84b2a29cece87323becbd624350beeb33b761255", + "benchmark/results/perf_owned_label_hulls_20260930/observations_1w.jsonl": "2e901b4d20bd7cd7fba55211d31e916e2dc209d13f7a3c8920a2f324a54bcd58", + "benchmark/results/perf_owned_label_hulls_20260930/parity/16w_1.json": "91ce60d66ce48d09137e06d1ae6d4d8bbb951ae7c91879f34fb0a0caa82135c9", + "benchmark/results/perf_owned_label_hulls_20260930/parity/16w_2.json": "91ce60d66ce48d09137e06d1ae6d4d8bbb951ae7c91879f34fb0a0caa82135c9", + "benchmark/results/perf_owned_label_hulls_20260930/parity/1w_1.json": "6db2f7a6b3bc0dc94b6c266ab8c9e8dcf640f28b4d17647d3453cdeaab57afe4", + "benchmark/results/perf_owned_label_hulls_20260930/parity/1w_2.json": "6db2f7a6b3bc0dc94b6c266ab8c9e8dcf640f28b4d17647d3453cdeaab57afe4", + "benchmark/results/perf_owned_label_hulls_20260930/runs/16w/candidate_1/preparation.log": "126e37216097eae16adba6d9b10734088f9aec8869055037ca4cf9cec5cc7e5e", + "benchmark/results/perf_owned_label_hulls_20260930/runs/16w/candidate_1/steps.csv": "77fc2150cf2fc04385c182ec87b3918ae8b47a2b64a008c15cafaa25052825a9", + "benchmark/results/perf_owned_label_hulls_20260930/runs/16w/candidate_1/throughput.csv": "41e95dbf6aae43808e11c45ff4bbc03d416aaf05f93213e1f19c0440a0ffae06", + "benchmark/results/perf_owned_label_hulls_20260930/runs/16w/candidate_1/worker_lanes.csv": "276ffad7083054a9b4ec3ef30edce3132aad70f806a1cd32f9db2f21ac951535", + "benchmark/results/perf_owned_label_hulls_20260930/runs/16w/candidate_2/preparation.log": "126e37216097eae16adba6d9b10734088f9aec8869055037ca4cf9cec5cc7e5e", + "benchmark/results/perf_owned_label_hulls_20260930/runs/16w/candidate_2/steps.csv": "f9a808fe8a66abdf054b0c2af6ba4f25aea836988d8f6751bb42f042ababab91", + "benchmark/results/perf_owned_label_hulls_20260930/runs/16w/candidate_2/throughput.csv": "6e1b1c6ece5753722b07b6461f6da2e3c0151f48113472189e80673fc00e191d", + "benchmark/results/perf_owned_label_hulls_20260930/runs/16w/candidate_2/worker_lanes.csv": "ef6594640d1c822a563aa221a1f240117df469e028f47456777600d02a8f8975", + "benchmark/results/perf_owned_label_hulls_20260930/runs/16w/control_1/preparation.log": "126e37216097eae16adba6d9b10734088f9aec8869055037ca4cf9cec5cc7e5e", + "benchmark/results/perf_owned_label_hulls_20260930/runs/16w/control_1/steps.csv": "169810bf1f2bc5c17bf9f3e780b240de639224cc6b074b7d9e99b6eade848999", + "benchmark/results/perf_owned_label_hulls_20260930/runs/16w/control_1/throughput.csv": "431441321f944a7e28770f05ad0d1cf1e8e98e3b2026ff5c8ee5f7b3bb6ce85f", + "benchmark/results/perf_owned_label_hulls_20260930/runs/16w/control_1/worker_lanes.csv": "cbac9c083d8d1732b8fc8882efe5a04964ec612c5ef7b6205b6659044093d8de", + "benchmark/results/perf_owned_label_hulls_20260930/runs/16w/control_2/preparation.log": "126e37216097eae16adba6d9b10734088f9aec8869055037ca4cf9cec5cc7e5e", + "benchmark/results/perf_owned_label_hulls_20260930/runs/16w/control_2/steps.csv": "c7b3e60b3cf071803d7e4613cf56079336dbfc84f627a96cd0c0493148d5be26", + "benchmark/results/perf_owned_label_hulls_20260930/runs/16w/control_2/throughput.csv": "5a2fdbcb1b4750ddec7ea8bac77ab34012819c735cbd43285bcf04cf71f444c7", + "benchmark/results/perf_owned_label_hulls_20260930/runs/16w/control_2/worker_lanes.csv": "8d50d0606168c00302f13a909a76be1495544336d72eaf29d8f7229eb0cace18", + "benchmark/results/perf_owned_label_hulls_20260930/runs/1w/candidate_1/preparation.log": "126e37216097eae16adba6d9b10734088f9aec8869055037ca4cf9cec5cc7e5e", + "benchmark/results/perf_owned_label_hulls_20260930/runs/1w/candidate_1/steps.csv": "6d32bd6347174ed34e095e223e4412ec2deb26fb773fa188d9fc58e4c17f933c", + "benchmark/results/perf_owned_label_hulls_20260930/runs/1w/candidate_1/throughput.csv": "e91c20c07d487a5b6035b86d2bbe2caafed1face61a2513a861b6d73bace9e09", + "benchmark/results/perf_owned_label_hulls_20260930/runs/1w/candidate_1/worker_lanes.csv": "b11748e48b8f85c93936ede3d5ce5ad35b4d475e96f8c0a15779783227536c65", + "benchmark/results/perf_owned_label_hulls_20260930/runs/1w/candidate_2/preparation.log": "126e37216097eae16adba6d9b10734088f9aec8869055037ca4cf9cec5cc7e5e", + "benchmark/results/perf_owned_label_hulls_20260930/runs/1w/candidate_2/steps.csv": "539b4d3cf19b5ffa71c3a91d36adca014f847c768ec38119e9100eec69ac8180", + "benchmark/results/perf_owned_label_hulls_20260930/runs/1w/candidate_2/throughput.csv": "ad16d0b6fe2f648f33e6adf64d8bfb9ed65b9a9e1d41ce7e97a7049421ff90b9", + "benchmark/results/perf_owned_label_hulls_20260930/runs/1w/candidate_2/worker_lanes.csv": "fba3cc04a96f857f37ec1a87f3dd83a8d5d3feb8c71be54320a8d7f0e9fee49d", + "benchmark/results/perf_owned_label_hulls_20260930/runs/1w/control_1/preparation.log": "126e37216097eae16adba6d9b10734088f9aec8869055037ca4cf9cec5cc7e5e", + "benchmark/results/perf_owned_label_hulls_20260930/runs/1w/control_1/steps.csv": "376cfc51e6bb7ec4ddc754f666a5536dd2f2bddd73e1f44876f209e358325800", + "benchmark/results/perf_owned_label_hulls_20260930/runs/1w/control_1/throughput.csv": "82c9d6d92250a81400dae6533a5bbd6fc6b16e09c037171dd083d5267432c4ca", + "benchmark/results/perf_owned_label_hulls_20260930/runs/1w/control_1/worker_lanes.csv": "0738afe9aaa5e78c60a8b2345bc5212bd583be0bb107370fec8905e76591584d", + "benchmark/results/perf_owned_label_hulls_20260930/runs/1w/control_2/preparation.log": "126e37216097eae16adba6d9b10734088f9aec8869055037ca4cf9cec5cc7e5e", + "benchmark/results/perf_owned_label_hulls_20260930/runs/1w/control_2/steps.csv": "82e29922ddede1697ce200a3de7fca73b7316673bd0e5d27af57adb531bc722c", + "benchmark/results/perf_owned_label_hulls_20260930/runs/1w/control_2/throughput.csv": "c76bf33b3e0857ab1eadd749faf09c075c5335b411b373e0af1aa5084b6ab7bd", + "benchmark/results/perf_owned_label_hulls_20260930/runs/1w/control_2/worker_lanes.csv": "80bd59f9435e34247689d8a60939e929fd19814b0818192ff8d1ceefc3f10dac", + "benchmark/results/perf_owned_label_hulls_20260930/saved_input_replay.json": "a076cce556a2ea924cc56cad62df2a524099c1d6c171a3b64d534a1af0f81f63", + "benchmark/results/perf_owned_label_hulls_20260930/scope_16w.json": "816105a904edb0bbe45c9bb4db23bef8f264f8fa506610fc6a56a2dace2b3111", + "benchmark/results/perf_owned_label_hulls_20260930/scope_1w.json": "fdcee02c23a72b6fa6a8a5346abc001fee7954b5d55f657104595196dd171f79", + "benchmark/results/perf_owned_label_hulls_20260930/validation/global_class_census_after.json.gz": "6791de7cc3cbd45d1d3e7c6b295f02479fc71ab227f663acb842dab4534c4c86", + "benchmark/results/perf_owned_label_hulls_20260930/validation/global_class_census_before.json.gz": "9fcfe58a0a746f1672fe9c26c8d4d19c7e2a682a4de0d4e9e11f5e233bf3c827", + "benchmark/results/perf_owned_label_hulls_20260930/validation/perf-final-production-owned-hull-replay-20260930.log": "612cd322cdf003adc5990b2f61edd4e2fb1000b24785ebd945903cbcdd7ef14b", + "benchmark/results/perf_owned_label_hulls_20260930/validation/perf-owned-hull-baseline-consumer-failure-20260930.log": "66ba009e61dd089a58cdcc2067f84d3a012d304673221908766d5216f59b28dc", + "benchmark/results/perf_owned_label_hulls_20260930/validation/perf-owned-hull-final-consumer-tests-20260930.log": "d416ee8ec5ed87bfc639586cef4020b948ebabd21579c549c6ff9852f8594ded", + "benchmark/results/perf_owned_label_hulls_20260930/validation/perf-owned-hull-installed-acceptance-20260930.json": "f89801fac0cb4571ef748d9938ecda1753117a3aa6f3006986b3744ca85f6b4c", + "benchmark/results/perf_owned_label_hulls_20260930/validation/perf-owned-hull-installed-acceptance-20260930.log": "4045f9e254d63b066fd79dfd76201b58257655ae6bbd48c412e6d509703966cd", + "benchmark/results/perf_owned_label_hulls_20260930/validation/perf-owned-hull-r1-consolidated-20260930.json": "24dc0d3ac06b837e9a64260a3f999ed010a765551b4bfdf69feef9df02c17abe", + "benchmark/results/perf_owned_label_hulls_20260930/validation/perf-owned-hull-r1-consolidated-20260930.log": "a6bf08f1fad7fee274dd0a192d9a5e9b34be7dbc3398a64e7d6a294a5be8d429", + "benchmark/results/perf_owned_label_hulls_20260930/validation/perf-requested-hull-nra-census-after-20260930.log": "09a6d48a9f65847afe1baefc9dcd0b9060b7b4cfe79d36b5e087105ec00e77e5", + "benchmark/results/perf_owned_label_hulls_20260930/validation/perf-requested-hull-nra-census-before-20260930.log": "4e7cf25b2071a6935c7a1b429f801411ca090ccd69edf2c2b59d41ffff070517", + "benchmark/results/perf_owned_label_hulls_20260930/validation/ratchet_benchmark_full.json.gz": "3229c499774ef6f608beab2872b0484ac8acb0ebcbc9e8622f6881ec101a330a", + "benchmark/results/perf_owned_label_hulls_20260930/validation/ratchet_benchmark_summary.json": "933b8b80ce77a470c4f913ab33a4b2e737d204b60f0bfc74bad42b1db3fef25b", + "benchmark/results/perf_owned_label_hulls_20260930/validation/ratchet_openhcs_full.json.gz": "19595554e5f1b811e5796f88da51d5f2a746182f719c311fd1e72891f6e2ced6", + "benchmark/results/perf_owned_label_hulls_20260930/validation/ratchet_openhcs_summary.json": "c97be412e4a76a9bfa37c1665bbf6af204f86d838321eccfb671293f118365eb", + "benchmark/results/perf_owned_label_hulls_20260930/validation/ratchet_scripts_full.json.gz": "c7f1065131bf0ec6be42d3a74abf6c4cb432aa50f3a904bd0651d27378b321e0", + "benchmark/results/perf_owned_label_hulls_20260930/validation/ratchet_scripts_summary.json": "22025f61d889d9b9712334cf9be16caed39378b3ff69e4c984bae58cc626f0f3", + "benchmark/results/perf_owned_label_hulls_20260930/validation_manifest.json": "96de72753b691e5636a2460f5070755d9995b30948ecba94d2ed39d666f09f69", + "benchmark/results/perf_payload_slice_projection_owner_20260930/README.md": "6dbbd8710f8ebfb21361a402672d4fcf1dc21d1ff5226f9ded1de2cc9237cbc5", + "benchmark/results/perf_payload_slice_projection_owner_20260930/recipes/apply_payload_slice_projection_owner_20260930.py": "5a46ad52c50de3534c94163ea049634502dde90de195210d01e398574cdc0bef", + "benchmark/results/perf_payload_slice_projection_owner_20260930/recipes/refine_payload_slice_projection_owner_20260930.py": "5222711472d6adfe5b59ce2c8dd761a09a5a19321acfcc58aa991a127232a6bf", + "benchmark/results/perf_payload_slice_projection_owner_20260930/recipes/replay_payload_slice_projection_owner_20260930.py": "3cf47beef6e1f9d6ff75b3d0d43cc50b9ff55082da59d01a72e5c44a4e9fcba8", + "benchmark/results/perf_payload_slice_projection_owner_20260930/recipes/reproduce_profile_thread_contamination_20260930.py": "4fbbcaa832378b2aa8aabfc9f444d169852db22f1e60014c26fd7bb96a725900", + "benchmark/results/perf_payload_slice_projection_owner_20260930/recipes/retire_unreachable_mask_axis_guard_20260930.py": "e48a42abb553e0e303987a52d737daf50aa8c156e1f0d408326f68f0fe9e44c9", + "benchmark/results/perf_payload_slice_projection_owner_20260930/recipes/run_payload_slice_projection_acceptance_20260930.py": "6d0b09cb8bd3b407d2f044e1b719670557bebb6a1972456b4b364e2516c6b5cb", + "benchmark/results/perf_payload_slice_projection_owner_20260930/recipes/run_runtime_owner_timer_20260930.py": "ad1f3d2aa488a4d22f647355bce8e908ea71cf67a8860a8da0d5ccb8c1226bf6", + "benchmark/results/perf_payload_slice_projection_owner_20260930/recipes/sitecustomize.py": "213412e0a1f845a21261de620c6d67384a266c56d0c9cb9f67f93d2e395f95b8", + "benchmark/results/perf_payload_slice_projection_owner_20260930/recipes/validate_payload_slice_projection_outputs_20260930.py": "4a645c74eadfd0504efd32a2358596c2d01c2050695542900b04fe0c955eb7aa", + "benchmark/results/perf_payload_slice_projection_owner_20260930/validation/perf-payload-slice-projection-consumer-tests-20260930.xml": "8e124e5a78f2bb126f5ff0430f6923aff85e468dbf6bf0c20c3479cf55019111", + "benchmark/results/perf_payload_slice_projection_owner_20260930/validation/perf-payload-slice-projection-diagnostic-parity-20260930.json": "17498c56d8fb5a71d5b1c829a6267d53b9b657741c60ba891c920435440aeb4e", + "benchmark/results/perf_payload_slice_projection_owner_20260930/validation/perf-payload-slice-projection-final-consumer-tests-20260930.xml": "86858a73afe49519d202710808a528970e07ff876dd7acbfa37e24250dcd5597", + "benchmark/results/perf_payload_slice_projection_owner_20260930/validation/perf-payload-slice-projection-integrated-consumer-tests-20260930.xml": "834abbba0957cd1b4f4673d8538345552d31b96f7955dcc0543df08f97e6bb6c", + "benchmark/results/perf_payload_slice_projection_owner_20260930/validation/perf-payload-slice-projection-nra-transaction-20260930.diff": "e0cd07e7373a69ac0cb90e73c09890ff6767969aa1e9907144e839d4caf0f3b1", + "benchmark/results/perf_payload_slice_projection_owner_20260930/validation/perf-payload-slice-projection-owner-refinement-nra-transaction-20260930.diff": "84bda389525389e79c8a934cf4cd0419fe5ec6a57b0096278af5f8bd939192ea", + "benchmark/results/perf_payload_slice_projection_owner_20260930/validation/perf-payload-slice-projection-ownership-20260930.json": "90496956740d56c96f61cd6eb2cfcf587ed5be232f6f62ad3f7ff24812ab1f41", + "benchmark/results/perf_payload_slice_projection_owner_20260930/validation/perf-payload-slice-projection-public-abba-observations-20260930.json": "0ddcde813dae4ec88cc52652236fa811caa5c46f8aa401e3406bc349f9187b5d", + "benchmark/results/perf_payload_slice_projection_owner_20260930/validation/perf-payload-slice-projection-public-comparison-20260930.json": "fd8d4bafdd83b1c2c073e50a49c2cf27c7f38cbbf7649498ac155eaf065df767", + "benchmark/results/perf_payload_slice_projection_owner_20260930/validation/perf-payload-slice-projection-replay-0-main-20260930.json": "268cc484fba7531c44788521d56c7277e45c6a58b35873e65696e7688c041ad1", + "benchmark/results/perf_payload_slice_projection_owner_20260930/validation/perf-payload-slice-projection-replay-1-candidate-20260930.json": "1b07d5f8ebc9c7ad92c85fae783f8518b57b5bd19d5f612797eb3cc102b6e1e7", + "benchmark/results/perf_payload_slice_projection_owner_20260930/validation/perf-payload-slice-projection-replay-2-candidate-20260930.json": "4a1ed61a76b3a3467e64d8a3948c82b785b80ac1718938526abf9baa5b1c9558", + "benchmark/results/perf_payload_slice_projection_owner_20260930/validation/perf-payload-slice-projection-replay-3-main-20260930.json": "fcd4a03e1a903240834f300a54a2d601722c75833b431e6da320657baf00803d", + "benchmark/results/perf_payload_slice_projection_owner_20260930/validation/perf-payload-slice-projection-retire-guard-nra-transaction-20260930.diff": "0619c594bafdee0300a63027c6b401bc7b75ffd35b089b61fc6874f9ce838df6", + "benchmark/results/perf_payload_slice_projection_owner_20260930/validation/perf-profile-thread-contamination-reproduction-20260930.json": "84cc5e7db3e23d38e0b1f28159f12e1b4b5c87eb7d502697e4e13c2a91daa862", + "benchmark/results/perf_payload_slice_projection_owner_20260930/validation/perf-runtime-owner-timer-summary-20260930.json": "5adbf2dc7fdcd01214168a9645f4c10740af2646e0caaae431f1f0bf0353171f", + "benchmark/results/perf_payload_slice_projection_owner_20260930/validation/r0_initial.json": "092714814183bcf602853444bbb2436ae3892978abfe9f550d0e97a50ff339fe", + "benchmark/results/perf_payload_slice_projection_owner_20260930/validation/r1_final.json": "3b09e698b62f9b18d552d481a7ee3206a51ffe54dbfc6b04e3aad70db47827be", + "benchmark/results/perf_payload_slice_projection_owner_20260930/validation/r1_initial.json": "cb501bd75160d68faa662d5882a42ad63f8810ffcb02c86470da38982763304d", + "benchmark/results/perf_payload_slice_projection_owner_20260930/validation/structural_checks.json": "950dba66c69763010f6339512813a2079e0cb2a9d7bd859e6c077874dbfa0566", + "benchmark/results/perf_plate_input_transport_20260929/README.md": "bd1e5be9b4eaed2c839772e362f3ca9c695269bca43c45222a36461081f5d965", + "benchmark/results/perf_plate_input_transport_20260929/multiwell.csv": "0be811e1843c96d083f2745feed9f4410e532ffb962557409d87af44d3dde8ef", + "benchmark/results/perf_plate_input_transport_20260929/multiwell_execution_and_transfer.png": "f4e66b34a7a3ee807f17f4688e6d0792b8efc7e625a988f18a3fdbbe429f49ff", + "benchmark/results/perf_plate_input_transport_20260929/official30_candidate.csv": "12ba219820c4cb8c2a31a7cbb02f1825c7f3e6092952e1baeb78d0aad8fdc41a", + "benchmark/results/perf_plate_input_transport_20260929/official30_control.csv": "8fc6f717372ab664822cb316fdff63189f68fd65ccbb651bea9b04b338c077c2", + "benchmark/results/perf_plate_input_transport_20260929/official30_phase_totals.png": "03714a464b8571f3c7e77a8fce920bebd91ceba65d83e7105388b61ec417ece1", + "benchmark/results/perf_plate_input_transport_20260929/plot.py": "e23738e0650d4f954d2923e223e976d8532b747a75bdbbbf9215f017e4c61135", + "benchmark/results/perf_plate_input_transport_20260929/transport_payload.csv": "36830ae656be98e2e17d9f42167e2948bcd0cc3b7ad0d16c16b23cc68128d20a", + "benchmark/results/perf_radial_geometry_20260929/README.md": "db12abb21784105bbda6078c71b4796e0296650c58af3cdd05773edb732feb04", + "benchmark/results/perf_radial_geometry_20260929/full30_output_parity.csv": "32decced1ac7d073957967e322ed283ad434487ca3f4877d91d4c11518e22b67", + "benchmark/results/perf_radial_geometry_20260929/imagingflow_1w_fresh_runs.csv": "22a018aabd087141e312430d17d18353648ec75814ec11b3e3e16405a1988d31", + "benchmark/results/perf_radial_geometry_20260929/review_base_1w_fresh_runs.csv": "26341010def31263fb47b665b7295cb9d3c6937abf77566fddd465054202b058", + "benchmark/results/perf_refill_preparation_20260930/README.md": "e5c55f80da24f7f07f3b4fcea1ab8cc79d524bf2efd44c272570105802ca3a01", + "benchmark/results/perf_refill_preparation_20260930/analysis.json": "245e4f609b7788bec91533bcd20c2cc7acd5ae1adb2edd6aa16e9d6587f8103e", + "benchmark/results/perf_refill_preparation_20260930/architecture.md": "e1e92f0b4cadc7a82f56dbcc7b9d7c3e9342d7640b5e48f9062a54f0023b7891", + "benchmark/results/perf_refill_preparation_20260930/input_digest_checks.json": "51c2835d516f08e6292595393dbeac3493c2fcdb90e1d748fc4beeb22c54f7b7", + "benchmark/results/perf_refill_preparation_20260930/measured_preparation_scaling.png": "b4b80beec8187317a91790b05fe86730ed5025615d8ece2da83c4f8fd6d7c44c", + "benchmark/results/perf_refill_preparation_20260930/measured_preparation_scaling.svg": "a6cef795e50f0024df5716fbabdcb202723097727f1d90fe5b22557ea3c8a884", + "benchmark/results/perf_refill_preparation_20260930/runs/true_cold/candidate_1/throughput.csv": "afc29e71eadd4ca99e3a34117a096c970363ee69bd77b1305bd7b5da30378758", + "benchmark/results/perf_refill_preparation_20260930/runs/true_cold/candidate_2/throughput.csv": "f6f59e870757f07711c94b75680eecfed4e9b1f9fca3e2c81bec31df0c64e726", + "benchmark/results/perf_refill_preparation_20260930/runs/true_cold/control_1/throughput.csv": "0fb43fc1fff84e2827b22eea5e6b0e93bed8063fdad1e5eb57eec1a990281c34", + "benchmark/results/perf_refill_preparation_20260930/runs/true_cold/control_2/throughput.csv": "3e453ce9789b81991756507a317ac55b352a0869eaedc8f4b94e7a9efe042203", + "benchmark/results/perf_refill_preparation_20260930/runs/warm/16w/candidate_1/steps.csv": "79b3e07d764556e2afa77c6d87d7e7e914e1530937cbac4ecf8249013fdc1144", + "benchmark/results/perf_refill_preparation_20260930/runs/warm/16w/candidate_1/throughput.csv": "f2dac38233d7aa5d3f7d18d252121252134250d5f7aad2db92005210e23f1af6", + "benchmark/results/perf_refill_preparation_20260930/runs/warm/16w/candidate_1/worker_lanes.csv": "8168e486b681f20627e09bfa4a4f126a162d65dd74eac5aa1081ad9071aecc36", + "benchmark/results/perf_refill_preparation_20260930/runs/warm/16w/control_1/steps.csv": "5d1fe480854a5323a8093b061d4b345dc8c3d371646fe291bc4aec77cc6adebe", + "benchmark/results/perf_refill_preparation_20260930/runs/warm/16w/control_1/throughput.csv": "a726ea21123439ce5fe931ae7657fa867b50f7157e7833ec6226d33aa0128669", + "benchmark/results/perf_refill_preparation_20260930/runs/warm/16w/control_1/worker_lanes.csv": "4bae87dd031c0fa7ad743e935144b68226efa8c3e2cf6441c3d77f54402cbe6f", + "benchmark/results/perf_refill_preparation_20260930/runs/warm/1w/candidate_1/steps.csv": "3bf27d6d9a0ea3ac52e7af4787983bebe4ed17766723acb6ba8b9f1fea37f469", + "benchmark/results/perf_refill_preparation_20260930/runs/warm/1w/candidate_1/throughput.csv": "11655fe69222dba206ac80d9e085814ebf642b86f6e0107fc3ab958d60e62006", + "benchmark/results/perf_refill_preparation_20260930/runs/warm/1w/candidate_1/worker_lanes.csv": "e52966c61592070de3524e006b2b63ead2d982fb819941d746bfa276b9dc6704", + "benchmark/results/perf_refill_preparation_20260930/runs/warm/1w/candidate_2/steps.csv": "c9303e2de93e7e58fc6903f3c45313abed0760953bb0cbab9aaffad9f3029b20", + "benchmark/results/perf_refill_preparation_20260930/runs/warm/1w/candidate_2/throughput.csv": "9258ecba3d1bae70ad897575516a781d7e1fd422f1363acd98ae846e5036a84f", + "benchmark/results/perf_refill_preparation_20260930/runs/warm/1w/candidate_2/worker_lanes.csv": "c750fb5c98bfbbd44d3032abe6de6b630d932efdaf63c619d5216ce6fcd6a758", + "benchmark/results/perf_refill_preparation_20260930/runs/warm/1w/control_1/steps.csv": "5f1b0b6075efa27613b102a8e55da6da2a058083ca551e380ff5fdc3638e4742", + "benchmark/results/perf_refill_preparation_20260930/runs/warm/1w/control_1/throughput.csv": "a8c5791a6c4922e3a45b4685b67a5930024d0d17487d2b99cc01c0cc2a07d7dc", + "benchmark/results/perf_refill_preparation_20260930/runs/warm/1w/control_1/worker_lanes.csv": "12fdf31f2cd447b4e6968e266f3831c52383601a09578942baec008defb19260", + "benchmark/results/perf_refill_preparation_20260930/runs/warm/1w/control_2/steps.csv": "af716c8df70bf103e14d264d7a51cba2712c14e20b35f4a22a439618cd40cef3", + "benchmark/results/perf_refill_preparation_20260930/runs/warm/1w/control_2/throughput.csv": "d059cfac08acf27a741aed64d056b7fae11157e504dc564b96554ba1c57e00ad", + "benchmark/results/perf_refill_preparation_20260930/runs/warm/1w/control_2/worker_lanes.csv": "ba9332f75958016e6c4055f9a52d667c41c424004d0add892ec1eedccb843548", + "benchmark/results/perf_refill_preparation_20260930/true_cold_observations.jsonl": "69c038b58f814c1fab2beeb462932a447c9ae5f6afcaba3e7bd5fe1e176abc99", + "benchmark/results/perf_refill_preparation_20260930/validation/class_census_after.json.gz": "b99cc7360b64a39d0894823474656e6a9ce92907b9079d7cadf2f87755e433b1", + "benchmark/results/perf_refill_preparation_20260930/validation/class_census_before.json.gz": "4982a27089ff5a26ce45ae329c31d2cdca8648b57827adc0c1dfc18d878a4dab", + "benchmark/results/perf_refill_preparation_20260930/validation/perf-refill-preparation-completion-loop-tests-20260930.log": "beee2f9bda7fe4ea7187ed0439cc52439b17fcc77f3e926a54b47f043d3f5c97", + "benchmark/results/perf_refill_preparation_20260930/validation/perf-refill-preparation-consumer-tests-20260930.log": "cf85e332a2efdfdfcdd5a6025e96fabe5b7b0acd63b302186ea1254802efaf1f", + "benchmark/results/perf_refill_preparation_20260930/validation/perf-refill-preparation-installed-provenance-20260930.json": "41848c9ab7d1e7ce684dab85c429524dac7e4eece93a0eedb7186c66ecec9cc4", + "benchmark/results/perf_refill_preparation_20260930/validation/perf-refill-preparation-nra-completion-loop-20260930.diff": "6fdf65ecdff8f341e9c8056abc7fa6e924612f4f8a1207340d8be71277a4cb24", + "benchmark/results/perf_refill_preparation_20260930/validation/perf-refill-preparation-nra-source-transaction-20260930.diff": "5739370bd682c72dbec12c4588a591ad8d62141671449ea7b41f36359443164b", + "benchmark/results/perf_refill_preparation_20260930/validation/perf-refill-preparation-original-regression-20260930.log": "2fa14273872348f2f8f071aeef7403075aaaeee820b944959b22266d0ae677ab", + "benchmark/results/perf_refill_preparation_20260930/validation/perf-refill-preparation-r1-20260930.log": "3e69ed79d64187b00a87c8f178f33429990264af8a8002835adc2df052400221", + "benchmark/results/perf_refill_preparation_20260930/validation/perf-refill-preparation-saved-frontier-proxy-20260930.json": "e390e4040085be3f81e167ef5876144a1cae6b46253aa718150e680776fe9030", + "benchmark/results/perf_refill_preparation_20260930/validation/perf-refill-preparation-synced-main-r1-20260930.log": "c5ef13bdd1fb3f780c4c9d37d10cfd259dbb6a5769cd61aaa2f2dcbb5c333d78", + "benchmark/results/perf_refill_preparation_20260930/validation/perf-refill-preparation-synced-main-tests-20260930.log": "147c91f707d5663dd44698ff9111ba9c76da784b45d6d509d39e0f43f041b974", + "benchmark/results/perf_refill_preparation_20260930/validation/ratchet_benchmark_full.json.gz": "7b2a4fdc79de6990151ac06b4d27ef940e3f1dbfa94cdaa541e55e112e57a4ee", + "benchmark/results/perf_refill_preparation_20260930/validation/ratchet_openhcs_full.json.gz": "666b57ef7c6d977606fd5cb02ab8ea0c2b042553e5bf4955f921b032bbf4cabf", + "benchmark/results/perf_refill_preparation_20260930/validation/ratchet_scripts_full.json.gz": "2540f3be53c1ff0acb06043257b93da3cad7aaffc73868f5830f34a5ba7b2c7e", + "benchmark/results/perf_refill_preparation_20260930/warm_observations_16w.jsonl": "305f4b08c7a4f68940244c3771859b53da72f2b0ad910bb26583dea1798de470", + "benchmark/results/perf_refill_preparation_20260930/warm_observations_1w.jsonl": "b276b8c4a85637e5e7e260a6a601f8335c4f78f574c4a11e4122e5581dae8730", + "benchmark/results/perf_refill_preparation_20260930/warm_scope_16w.json": "1c4ad4416e931f7b7f9a4b8b00dc84f3dbd3d566dc340800cfca9764b4d633ed", + "benchmark/results/perf_refill_preparation_20260930/warm_scope_1w.json": "5d35f3e0a919a3bb34358fb7c5c4543a9d0f9e842de7ed5dc15b038dbe51a909", + "benchmark/results/perf_registry_kernel_warming_20260929/README.md": "b0c8e085e8b163c0759230244552737ed95b778e19e201ab128a9491d395cb2a", + "benchmark/results/perf_registry_kernel_warming_20260929/measured_pipeline_receipt.json": "7327fa807c1bf41baca4830a48194615540b5e9ca7a0ec9d674e5c983434a4c0", + "benchmark/results/perf_registry_kernel_warming_20260929/nra_classes.csv": "0f69daf80b9fce4af81db0e4744cb7a3ab2228b32581031cc1ffef97cfe363fb", + "benchmark/results/perf_registry_kernel_warming_20260929/nra_scope.json": "acc0434b98b7455d8fb965b392dca28efdbd341fbf74160ba9cfd3eb66825eb6", + "benchmark/results/perf_registry_kernel_warming_20260929/ordinary_observation.csv": "cb5878c0502da25098b8999db4af9b5578d0b13bf49cb91ea9028e5ffb6ee72d", + "benchmark/results/perf_registry_kernel_warming_20260929/ownership_decision.md": "b2559947546d7acbeea199a5849504ea28ae681a5a8804c3d2fa6e5d77acc516", + "benchmark/results/perf_registry_kernel_warming_20260929/perf-registry-kernel-warming-backend-tests-20260929.log": "15568e162438c46e739b08dbee8d68a0ed08996c174a3db242ae806fb1b8e04e", + "benchmark/results/perf_registry_kernel_warming_20260929/perf-registry-kernel-warming-compiler-tests-20260929.log": "b6eeac8913019addb99b987c35ad33ea56f2fdcbb3ea7640c07db3f68fe726f5", + "benchmark/results/perf_registry_kernel_warming_20260929/perf-registry-kernel-warming-tests-20260929.log": "8cd94ea08d5da414d8b36a236ccdfd0d03a99769bb4bb8d1eb0ade4127bb1c18", + "benchmark/results/perf_registry_kernel_warming_20260929/probe_catalog_warmup.py": "2b9903c5b5508752b7c69f048435de6b6961c19f1a2f3799252e0e7fc945607d", + "benchmark/results/perf_registry_kernel_warming_20260929/probe_spawn_cache.py": "351e6b076ef49e561d88f1350ba53a90e8d2d41ce08900e9964569087d9e9389", + "benchmark/results/perf_registry_kernel_warming_20260929/production_warmup.log": "030312e1ea82edc6919f51806090b76a7947ab25915d8c5906c3830c7e6092c9", + "benchmark/results/perf_registry_kernel_warming_20260929/provenance.json": "90f6c1ee7a94b100497c7faa61a07d4398da5c492f03adf813ebade7a8689dc2", + "benchmark/results/perf_registry_kernel_warming_20260929/spawn_cache_hits.json": "6a553f581b6f54fc82c5df5e993cfbf15242ebe36dbc52b18643d4775a7bf421", + "benchmark/results/perf_reused_server_current_20260929/README.md": "92428e24c53b75fd7e92c70981b5571f713534b8d1514bb9226338763562a23d", + "benchmark/results/perf_reused_server_current_20260929/fresh.csv": "75919572a19f5255def07e7086e818c91f8e0d5f423aa6d8a2858aba8e6086c3", + "benchmark/results/perf_reused_server_current_20260929/fresh_vs_reused_phase_sums.png": "e7f48d279bb2f6448765279705fd47bbda6844e9b546e9be21a993fc5992b7b8", + "benchmark/results/perf_reused_server_current_20260929/plot.py": "1966762d70d1027d632d3a6d06f12fd0a41e98015759d0b8448a1323a450a926", + "benchmark/results/perf_reused_server_current_20260929/reused.csv": "e14be1164d8cb478ccbb84960d65b93d136ad0c061792148706d3d0e18c8a388", + "benchmark/results/perf_reused_server_current_20260929/wall_times.csv": "3da1b9ff342b7496e98226f743ee9eacfe5b0ea8e03074a2e0a8c8d6b8d01019", + "benchmark/results/perf_runtime_image_metadata_admission_20260930/README.md": "4ab32b8d71bfb457d14789d385055ecc33cf44c9b0ec3a49c3dbe322b60a18c4", + "benchmark/results/perf_runtime_image_metadata_admission_20260930/architecture.md": "7f50f88eb9f1f1e75c82dd9532453a1f954fb6d519e423b1b673a530576e7f6d", + "benchmark/results/perf_runtime_image_metadata_admission_20260930/observations/failed_main_profile-clock.json": "c56e5e8360943cd9f568b6656fdc63ed0d30182ef4962d3f6edd6922bf5abc51", + "benchmark/results/perf_runtime_image_metadata_admission_20260930/observations/failed_main_profile.csv": "3ca32d9648fe03307349a8ecf8596d4b827739c7288221d4502d4570f352089c", + "benchmark/results/perf_runtime_image_metadata_admission_20260930/observations/failed_main_traceback.log": "7d92e1d053bf055507d94b3ec4a6bbe63c19276e55caf5a1e30571aed3ffd25a", + "benchmark/results/perf_runtime_image_metadata_admission_20260930/observations/installed_acceptance-clock.json": "9dbb65c4a83878c95d03bc803db4ebb362118603d1482dff036e88498c498a80", + "benchmark/results/perf_runtime_image_metadata_admission_20260930/observations/installed_acceptance.csv": "c8c2d3f417ae0b86f3e2a9486f5e1759b32ed3cca3a65010668ca8989aab6374", + "benchmark/results/perf_runtime_image_metadata_admission_20260930/validation/imagingflow_parity.json": "d4cfe0a97690d8a330b49e6413ff28ef49b437c1265ac93e7938292846be50a6", + "benchmark/results/perf_runtime_image_metadata_admission_20260930/validation/perf-runtime-image-target-consumers-20260930.log": "43234bbdd6bd7fa279d0eba2fd2814fe8dc0edc604d757b465fe0811c09f5d56", + "benchmark/results/perf_runtime_image_metadata_admission_20260930/validation/perf-runtime-image-target-installed-provenance-20260930.json": "7eb6b03ac6b27458e4282a0413b5b69ce0f967f193ae3a00d3485176500397ff", + "benchmark/results/perf_runtime_image_metadata_admission_20260930/validation/perf-runtime-image-target-installed-scientific-parity-20260930.json": "0677ccd33cb9b6a3401f1ae2b0fe0a2e8fef4c091d451512e662455205d0c141", + "benchmark/results/perf_runtime_image_metadata_admission_20260930/validation/perf-runtime-image-target-nra-source-transaction-20260930.diff": "a3598b2a58e89a14f6c1fda90499d1b74087f3e85a14d23fbbe71e8753b0d898", + "benchmark/results/perf_runtime_image_metadata_admission_20260930/validation/perf-runtime-image-target-r1-20260930.log": "c6731798cacf77882a36bcfb0548236c5c35af1ef28a6422c21d0170886d8071", + "benchmark/results/perf_runtime_image_metadata_admission_20260930/validation/perf-runtime-image-target-tests-20260930.log": "f89dd6b69f813d4bcf2fd4342cc9e07821f71e0bc02a9c57b410506284845763", + "benchmark/results/perf_runtime_image_metadata_admission_20260930/validation/perf-runtime-image-target-unchanged-main-regression-tests-20260930.log": "ab3874e8e7407b52c999cc2d3007fee50f9709550ffb42640e8b33a0598c4f43", + "benchmark/results/perf_runtime_image_metadata_admission_20260930/validation/perf-runtime-image-target-wheel-build-20260930.log": "f67ed7fb9b06083b5a700248cacda563b9360f8359adbf81ab395c90ee1e4d1f", + "benchmark/results/perf_runtime_image_metadata_admission_20260930/validation/perf-runtime-image-target-wheel-install-20260930.log": "207bac2efdc91844b03743c8abf48913a0d08403e8400dd342422211894fb33f", + "benchmark/results/perf_runtime_image_metadata_admission_20260930/validation/structural_checks.json": "46670ae71423fce031766c3e63d3d32c5fb21bf1e7f3539e016782cda100a718", + "benchmark/results/perf_runtime_producer_path_index_20260930/README.md": "fb59803d86a785e71200c70588864d08a3238bb5d78373f0845ef5ecf7605688", + "benchmark/results/perf_runtime_producer_path_index_20260930/architecture.md": "c62665137d81719fa243f39c95e6ba26c1403aa1227c2788a51de73eb27755a2", + "benchmark/results/perf_runtime_producer_path_index_20260930/observations/perf-runtime-context-abba-1-main-20260930-clock.json": "6576cc42fe33cf8f7dd9e167e0f75416683e51525488875cf96ff6e04b259c30", + "benchmark/results/perf_runtime_producer_path_index_20260930/observations/perf-runtime-context-abba-1-main-20260930.csv": "a4008a9b335a3ac69a65d7604f4cb45213a80ae3f606be5fe80d40f5f3e6f4dc", + "benchmark/results/perf_runtime_producer_path_index_20260930/observations/perf-runtime-context-abba-2-candidate-20260930-clock.json": "d073aabb15a21ecf016796cd4e076cc8a1e0aa3dc6ed8bbf874f2296c41d40e4", + "benchmark/results/perf_runtime_producer_path_index_20260930/observations/perf-runtime-context-abba-2-candidate-20260930.csv": "9eb645ecff6def59a0778813432ebca7ee9261f147705138a22d5974f17318bf", + "benchmark/results/perf_runtime_producer_path_index_20260930/observations/perf-runtime-context-abba-3-candidate-20260930-clock.json": "cd42645c5513c118603cc0517936133c5296d4c1882a9a3556fbc8cff8ae91ae", + "benchmark/results/perf_runtime_producer_path_index_20260930/observations/perf-runtime-context-abba-3-candidate-20260930.csv": "2cc63812939984df6ccfd984340ea897779b53f6df1852c9f7e843544fe5c215", + "benchmark/results/perf_runtime_producer_path_index_20260930/observations/perf-runtime-context-abba-4-main-20260930-clock.json": "ea551daad24293d88a9a36ef39bae7015d17b4802bcd0b3dc4544f232bd6f323", + "benchmark/results/perf_runtime_producer_path_index_20260930/observations/perf-runtime-context-abba-4-main-20260930.csv": "156f133f799312b00b9a9b0b500937465d9b5588292e1724a13d22ec9b642e2c", + "benchmark/results/perf_runtime_producer_path_index_20260930/observations/perf-runtime-context-abba-observations-20260930.json": "c45a1af67365b92ec68ef620912e32c22f898811e8f4afc2cd08bc0e27907805", + "benchmark/results/perf_runtime_producer_path_index_20260930/observations/perf-runtime-context-fixed-cpu-abba-1-main-20260930-clock.json": "55ac11260fb9f421e640b07accac7ff4240af515d8ae409a14207f3628b4ef67", + "benchmark/results/perf_runtime_producer_path_index_20260930/observations/perf-runtime-context-fixed-cpu-abba-1-main-20260930.csv": "e5effa2f42bd87edde291e6b64bbe9a97edcae1b10c70277935842f424b62cb4", + "benchmark/results/perf_runtime_producer_path_index_20260930/observations/perf-runtime-context-fixed-cpu-abba-2-candidate-20260930-clock.json": "37c5485e89ecd7b7371d3d449a9221f470b3318935924a148b939093e82e6526", + "benchmark/results/perf_runtime_producer_path_index_20260930/observations/perf-runtime-context-fixed-cpu-abba-2-candidate-20260930.csv": "ef421fa28189e15bf738faa8a67ac13097f1486d7405fdd26b0752e91721a64b", + "benchmark/results/perf_runtime_producer_path_index_20260930/observations/perf-runtime-context-fixed-cpu-abba-3-candidate-20260930-clock.json": "e70cf143b3386749f8745519f216efb3f8e8715daa2663f7b555382fa671c95c", + "benchmark/results/perf_runtime_producer_path_index_20260930/observations/perf-runtime-context-fixed-cpu-abba-3-candidate-20260930.csv": "8ff36bb5b2dd019c2b9351e104764c016ca10f34c534926c26d6e6ad9dd426a5", + "benchmark/results/perf_runtime_producer_path_index_20260930/observations/perf-runtime-context-fixed-cpu-abba-4-main-20260930-clock.json": "7b34d8a6a6f4e75ad474ee32e459da449764f9b301bd70e970495bc2228dc995", + "benchmark/results/perf_runtime_producer_path_index_20260930/observations/perf-runtime-context-fixed-cpu-abba-4-main-20260930.csv": "4bc5676c7d5aba251de71c63532e042e07f4c47f44c6b2af596fc3692e890ff9", + "benchmark/results/perf_runtime_producer_path_index_20260930/observations/perf-runtime-context-fixed-cpu-abba-observations-20260930.json": "e10cc39965533356406cc710b1b0385b78afc7f689eb7bea7c429a7a5ff6a0a7", + "benchmark/results/perf_runtime_producer_path_index_20260930/pipeline_comparison.png": "4c815131a00c45bbe9ddbf9e375a38d24efdebe892df7c85c6cc7553b59b00af", + "benchmark/results/perf_runtime_producer_path_index_20260930/pipeline_comparison.svg": "e5c101551f90c8247a0c36670f9b6728c25e398d126db27200f55a8e50631529", + "benchmark/results/perf_runtime_producer_path_index_20260930/timing_summary.json": "ad8b3ab948b28132dd476c1733959508066a4603f932342ab965e693db4810c9", + "benchmark/results/perf_runtime_producer_path_index_20260930/validation/perf-runtime-context-consumer-tests-20260930.log": "bca5c2f553174924417df158b21c7208820afa0d5319c139e7fad639f6b88500", + "benchmark/results/perf_runtime_producer_path_index_20260930/validation/perf-runtime-context-independent-main-baseline-failures-20260930.log": "052defd30a2ec61feb024e01b3b0e4ba1b739effb89e10493cc750635331ea77", + "benchmark/results/perf_runtime_producer_path_index_20260930/validation/perf-runtime-context-installed-provenance-20260930.json": "819a628d56147abe1982a039f5db031aaa300158c82a1a7ae0ddcc14b9b7c8e6", + "benchmark/results/perf_runtime_producer_path_index_20260930/validation/perf-runtime-context-known-baseline-failures-20260930.log": "5b08d68dd2e717cf1cee4538703e3163253ddde538b0afe5beb27045159441e3", + "benchmark/results/perf_runtime_producer_path_index_20260930/validation/perf-runtime-context-nra-source-transaction-20260930.diff": "506f63b47d1e4e621ce8f53432382e178177e1641c0ceac7a7581bbdaa6e056e", + "benchmark/results/perf_runtime_producer_path_index_20260930/validation/perf-runtime-context-r1-20260930.log": "fb8896bd2f4d0a5131002abe782e89f9671e57380e80f072e16f08ad698674b1", + "benchmark/results/perf_runtime_producer_path_index_20260930/validation/perf-runtime-context-representative-replay-20260930.json": "342dc106beda75f7bee10982013013e7067982cc93af194f32af6aed7a0a4009", + "benchmark/results/perf_runtime_producer_path_index_20260930/validation/perf-runtime-context-scientific-parity-20260930.json": "5741c88809d28a4dfb26d0eb8b1d6aec6b7701f5ec7c81c110440281df942b5b", + "benchmark/results/perf_runtime_producer_path_index_20260930/validation/perf-runtime-context-selector-nra-source-transaction-20260930.diff": "611960e442dc4961b2ef237a36875d39024030841a38d8842583667cab3a88d5", + "benchmark/results/perf_runtime_producer_path_index_20260930/validation/perf-runtime-context-wheel-build-20260930.log": "dc5fc22c21b78a5faf1d185aa0606f69136d89fd5bea6d42624ce8129eb44340", + "benchmark/results/perf_runtime_producer_path_index_20260930/validation/perf-runtime-context-wheel-install-corrected-20260930.log": "9348905423857b4f5095fcd91fd41c7bdd3a4415546f8f35a34e5b8ba4af514b", + "benchmark/results/perf_runtime_producer_path_index_20260930/validation/structural_checks.json": "372b657d8a694b8d5f59840117efd9627c9f6d98bfd84a58332b0e1ecfbddb83", + "benchmark/results/perf_runtime_projection_platform_20260930/README.md": "8d98c60202d94db0e2bbffb9ebe3b67acf3cefa5cb4cc8fa14f3c41586b8227c", + "benchmark/results/perf_runtime_projection_platform_20260930/comparison.png": "72f8fc2e077c04c66f33a1579b503eed525684083814482a5a03d9d393208437", + "benchmark/results/perf_runtime_projection_platform_20260930/comparison.svg": "b8491c13e64983c83e398f2cc993bbccfb17292ee33874e70c8965c667eb2036", + "benchmark/results/perf_runtime_projection_platform_20260930/recipes/perf-runtime-projection-fixture-repair-nra-transaction-20260930.diff": "536e6a7c7b047317b082dde827666273f686b25ac41c691fd2c045117d30deb9", + "benchmark/results/perf_runtime_projection_platform_20260930/recipes/perf-runtime-projection-owner-refinement-nra-transaction-20260930.diff": "dbf24baf1bef10376c8c7707a1ffa2ac9ead2a40b7aacd8776c2e565d8daebc9", + "benchmark/results/perf_runtime_projection_platform_20260930/recipes/perf-runtime-projection-ownership-nra-transaction-20260930.diff": "8d57892458162042b4bc682a80831e4403f139e07d4ba65361c5eed2458fe8f0", + "benchmark/results/perf_runtime_projection_platform_20260930/recipes/replay_runtime_output_projection_production_20260930.py": "6fc66b2dbfdfd6124fa22b8d11d9d833f9074555ff7fe2bc59751e42879cf4f2", + "benchmark/results/perf_runtime_projection_platform_20260930/recipes/run_runtime_projection_platform_abba_20260930.py": "570733b0130e6c7bbce1893b8d63c710c65f2292e7c1e0b5d0c00474f92f393e", + "benchmark/results/perf_runtime_projection_platform_20260930/recipes/run_runtime_projection_platform_profile_20260930.py": "0c6df2834cb423bc1c97dba4032bff9664b981a463720af4d343b1d68eea0cfb", + "benchmark/results/perf_runtime_projection_platform_20260930/recipes/sitecustomize.py": "62f575353e857f55682b7efff4797da1117d3bc8be6999ae81cd9c360961facf", + "benchmark/results/perf_runtime_projection_platform_20260930/recipes/validate_runtime_projection_platform_20260930.py": "48528ce69f0fa40ea3cf05a5b28b2a9e08e708d8308c1e8c78af0e768a8d3f9c", + "benchmark/results/perf_runtime_projection_platform_20260930/validation/perf-runtime-output-projection-production-audit-overlap-limits-20260930.json": "7edd9bf09049da12b887dff16b631a743f926e850edb1b8541d61d6c999e6345", + "benchmark/results/perf_runtime_projection_platform_20260930/validation/perf-runtime-output-projection-production-candidate-20260930-audit-overlap.json": "180c639d49f81a63cc128ca5750c81fecb0a403f7ffaaae0f96b6b9e42a9d026", + "benchmark/results/perf_runtime_projection_platform_20260930/validation/perf-runtime-output-projection-production-candidate-20260930.json": "441a07fc3432a9e29a7210a2e8b14b13f9f4d159f0427daa1c634526b476c862", + "benchmark/results/perf_runtime_projection_platform_20260930/validation/perf-runtime-output-projection-production-main-20260930-audit-overlap.json": "0908272e990b373f5a10fd0bd6d08a0ab3f0434ef15a9e55f2c1a1ae059728e5", + "benchmark/results/perf_runtime_projection_platform_20260930/validation/perf-runtime-output-projection-production-main-20260930.json": "29bba554c3400392083f6b28b9795155b036934145c5192e4d5dff6ad18dbf8e", + "benchmark/results/perf_runtime_projection_platform_20260930/validation/perf-runtime-output-projection-prototype-20260930.json": "64777817f1e78d42aa65ca8602b83d720656f88fd8e2583b66fb6cb51ff9b621", + "benchmark/results/perf_runtime_projection_platform_20260930/validation/perf-runtime-projection-current-main-consumer-tests-20260930.log": "c081bb7ff28fa58012474f3370c69f28fa6283fe17335d78ce1fc022a1ed9112", + "benchmark/results/perf_runtime_projection_platform_20260930/validation/perf-runtime-projection-current-main-r1-20260930.log": "b2d2e9dddfb01989907e42c9ede6e44de083324656551fdc4c0dccbd78e883bb", + "benchmark/results/perf_runtime_projection_platform_20260930/validation/perf-runtime-projection-global-cache-declaration-inventory-20260930.json": "198da819fc4ed938f64b6f9996c2c0ff34abbf1c7b30533c5b29d5ff77d7f87d", + "benchmark/results/perf_runtime_projection_platform_20260930/validation/perf-runtime-projection-ownership-consumer-tests-20260930.log": "55ebbc9bb02996c7d203f42345867fde715e103942c3368fc5b729e8c7ade0ff", + "benchmark/results/perf_runtime_projection_platform_20260930/validation/perf-runtime-projection-ownership-decision-20260930.json": "97e45e54cc19254599f7ed5cf4f3a4ac31e19a9597bb9f082bf4830472d371fc", + "benchmark/results/perf_runtime_projection_platform_20260930/validation/perf-runtime-projection-ownership-fixture-repair-tests-20260930.log": "e5402f834a2951686d74ca6279cb56b90782026bcc598d97da26e6b553bc7b5a", + "benchmark/results/perf_runtime_projection_platform_20260930/validation/perf-runtime-projection-ownership-main-fixture-counterexample-20260930.log": "901de316db2fb5975583aa967cef5848719949653478127688fbf19fe0796ce6", + "benchmark/results/perf_runtime_projection_platform_20260930/validation/perf-runtime-projection-platform-abba-1-main-20260930-clock.json": "4550b1c4ca091fc8720a6ef8ef1cee1f9a69e515288fe45d621b7ecf8879e324", + "benchmark/results/perf_runtime_projection_platform_20260930/validation/perf-runtime-projection-platform-abba-2-candidate-20260930-clock.json": "6e18b53ad6b495df3e995b2e1b35f99d46817c1809d3093a6b5d62864bad53dc", + "benchmark/results/perf_runtime_projection_platform_20260930/validation/perf-runtime-projection-platform-abba-3-candidate-20260930-clock.json": "3ec7649fb89915d2a6a883eae989403c1b0575a522e8d6c576426b97531c7fb0", + "benchmark/results/perf_runtime_projection_platform_20260930/validation/perf-runtime-projection-platform-abba-4-main-20260930-clock.json": "9000d11f572610e7c7bd728055479142676a6a179580a94484fd905afce3f2ab", + "benchmark/results/perf_runtime_projection_platform_20260930/validation/perf-runtime-projection-platform-abba-observations-20260930.json": "cef93ac8b59182b5c4769ecc50f88d1125712f132f383c4cd5262513563b1111", + "benchmark/results/perf_runtime_projection_platform_20260930/validation/perf-runtime-projection-platform-comparison-20260930.json": "38a349648c6a363eeacb22efece8373bf03f5b08fe2072b966daae4156ccb376", + "benchmark/results/perf_runtime_projection_platform_20260930/validation/perf-runtime-projection-platform-current-provenance-20260930.json": "e04050a1cb9ab7086120e63af4bb31571844fc2748a31f35c2dbf2211e859a26", + "benchmark/results/perf_runtime_projection_platform_20260930/validation/perf-runtime-projection-platform-phase-comparison-20260930.json": "2358f8296c1f6eb19fb7907fd6288206137b35a440d93781532c9136f7e72aa8", + "benchmark/results/perf_runtime_projection_platform_20260930/validation/perf-runtime-projection-platform-profile-parity-20260930.json": "f23561230710904b03c5a03be677f00dcd46e2eba337e27e3930c4a0dd72468a", + "benchmark/results/perf_runtime_projection_platform_20260930/validation/structural_checks.json": "f89aac49b61d5b8dd50ba4cb2029a4cc066e7a1fbb257ce22649bf5205dadaa1", + "benchmark/results/perf_saveimages_artifact_binding_20260930/README.md": "9ff06c77092d7ea9fd3cd8b9a7322bab6347abba5facece2a0ee75b4dd5dde6c", + "benchmark/results/perf_saveimages_artifact_binding_20260930/analysis.json": "666b030e26de9308507dffedf802a3f2c5503dfafba8e32229b0381e88951699", + "benchmark/results/perf_saveimages_artifact_binding_20260930/architecture.md": "8b923e761b15f79ebc8eecc760e3cbb1594fcf45d0d7068fe684c60cb9f76c14", + "benchmark/results/perf_saveimages_artifact_binding_20260930/observations/ExampleImagingFlowCytometryObjectsInGrid_receipt.json": "dab23bdea8f4ab29e586b4b6ed58d3de453a0c797979424cea28ae529ebd737f", + "benchmark/results/perf_saveimages_artifact_binding_20260930/observations/cp_tutorial_3d_monolayer_receipt.json": "96de5ff58478c3da0b69a95282c634a858470519ff8fee19dfca7567b5cd2103", + "benchmark/results/perf_saveimages_artifact_binding_20260930/observations/installed_frontier.csv": "cc22553adaea736c58e3727aedb1e6250d766916605dc686ba127c39f9978a5b", + "benchmark/results/perf_saveimages_artifact_binding_20260930/observations/installed_frontier_ExampleImagingFlowCytometryObjectsInGrid_steps.csv": "1061b5076abe2623b965c113efdd0332a12516bc4ed779f6ad29c404c9e03b18", + "benchmark/results/perf_saveimages_artifact_binding_20260930/observations/installed_frontier_cp_tutorial_3d_monolayer_steps.csv": "a6704a1dfeb9bfee71b0431fe944932c32c229267b235f2f18209cbbbc706e6a", + "benchmark/results/perf_saveimages_artifact_binding_20260930/observations/main_3d_failure.csv": "fff90aaef255e2122203f535eab2734c8bd37968c086d436b6cfe52ef5c30dd0", + "benchmark/results/perf_saveimages_artifact_binding_20260930/observations/main_3d_failure_cp_tutorial_3d_monolayer_steps.csv": "02cf0d0ba4c92ee65a5b58c7aec0643e4d1123c1482ab27bb852cb1ce182d8c0", + "benchmark/results/perf_saveimages_artifact_binding_20260930/observations/source_prototype.csv": "3e7b4361add2c7ed4ea03d82dba8063e5014b05b3edb33c9fb9d185ed593e7b0", + "benchmark/results/perf_saveimages_artifact_binding_20260930/observations/source_prototype_cp_tutorial_3d_monolayer_steps.csv": "34e2bd8f54c563b23eb5c75a171f26b80ab8c4281057db94801bea567aa2e2ba", + "benchmark/results/perf_saveimages_artifact_binding_20260930/refreshed_single_well_execution.png": "5157714f2cb3ac35531b09357565bedfdbeacb16f6d08c7bab8dcb767f347a03", + "benchmark/results/perf_saveimages_artifact_binding_20260930/refreshed_single_well_execution.svg": "26b5b96f3f2bb9f2107508d710d983a256b06ebdc32765540a31c13f28e00b94", + "benchmark/results/perf_saveimages_artifact_binding_20260930/validation/perf-saveimages-artifact-binding-nra-migration-closure-20260930.diff": "ae8d44f35766a3fde00a1d2de5cabee0ae9076081e389f1f4b083c3337a7d270", + "benchmark/results/perf_saveimages_artifact_binding_20260930/validation/perf-saveimages-artifact-binding-nra-source-transaction-20260930.diff": "414fd983ce456da9eb9a1a478fe32b00024463abc3fa3b3685c7ea3edb38fed3", + "benchmark/results/perf_saveimages_artifact_binding_20260930/validation/perf-saveimages-baseline-fixture-failures-20260930.log": "08a5b57fb10891aaae099ec15812197e429751fe4c890fb1cf415881247a0421", + "benchmark/results/perf_saveimages_artifact_binding_20260930/validation/perf-saveimages-binding-final-consumer-r2-tests-20260930.log": "ad7963af8e4bb55b122cabb5f9af58d9ef09755d8d0977b04691c2b01e5b5701", + "benchmark/results/perf_saveimages_artifact_binding_20260930/validation/perf-saveimages-binding-final-r1-20260930.log": "a0172400e380dc58bd7a9ea6a1a3a9b8d433a292f8ab16ce968268e279efa94b", + "benchmark/results/perf_saveimages_artifact_binding_20260930/validation/perf-saveimages-binding-installed-provenance-20260930.json": "2da33a2f9eca1d68e370cea855c07539c46616be86cdae27560f7d532705e96a", + "benchmark/results/perf_saveimages_artifact_binding_20260930/validation/perf-saveimages-binding-policy-nra-source-transaction-20260930.diff": "2c3219976c127f92007ff4c9aa6f307991e762409138897cdb93b5233b302d7e", + "benchmark/results/perf_saveimages_artifact_binding_20260930/validation/perf-saveimages-binding-wheel-build-20260930.log": "8afb31890af4b5d4925c62786010b2ed90fc83168e202cdae0cd67e3f4b8a973", + "benchmark/results/perf_saveimages_artifact_binding_20260930/validation/perf-saveimages-binding-wheel-install-20260930.log": "3f6771a8343db1c2b7794d15a631926af945952e0ca757ea16c34cb15b2d086a", + "benchmark/results/perf_saveimages_artifact_binding_20260930/validation/perf-saveimages-input-value-nra-source-transaction-20260930.diff": "2f133544c1a47be96a8d7761b142b93a870a87726dceb07b4a1b7c5fe37e5ca0", + "benchmark/results/perf_saveimages_artifact_binding_20260930/validation/perf-saveimages-installed-scientific-parity-20260930.json": "0b2f7575d1f21a43de81a96cce6b3c72a900f73761fdf9e44248966ea148caac", + "benchmark/results/perf_saveimages_artifact_binding_20260930/validation/perf-saveimages-known-failing-supplemental-final-tests-20260930.log": "1b59d7fb22075a84bd5e7e2c7d7a221037c3bf20261a31e4207dc43f2558dbda", + "benchmark/results/perf_saveimages_artifact_binding_20260930/validation/perf-saveimages-runtime-values-baseline-fixture-failures-20260930.log": "6a06c0b66959f29a1fec4b2e8450c3814b1a19d44628e4f4d9be113746a8c7d9", + "benchmark/results/perf_saveimages_artifact_binding_20260930/validation/structural_checks.json": "356441b5b96105110f902ebb7d617fb4031da899e71c836d33a8b1d4b7f6964f", + "benchmark/results/perf_scaling_rise_investigation_20260929/README.md": "1bf288d522c32c756008cba5582f8cef92a388294befb70868d95c7ca10a2e10", + "benchmark/results/perf_scaling_rise_investigation_20260929/analysis.json": "a4d1fd5a7f891135f3abd5e8aff507bbf230905b3cced55dde56c7c72c162342", + "benchmark/results/perf_scaling_rise_investigation_20260929/analyze.py": "f01332b37a27dae1257432ac3fdddc8259b35a6d3fd76111c49ead14d34734c1", + "benchmark/results/perf_scaling_rise_investigation_20260929/audit_overlap_lanes.csv": "477c56ef60a83b3bf4b5775342fbd1eb000cc71e374f27ca64a2e7ceba9cae05", + "benchmark/results/perf_scaling_rise_investigation_20260929/audit_overlap_observation.json": "28bc8a37103c5d461cfa0ed4edd56c5c1709da2e25b0eab499c8ac545521f744", + "benchmark/results/perf_scaling_rise_investigation_20260929/audit_overlap_pressure.jsonl": "42ee04a92068b5fd9059294ccb2f4972bed9c468400d281ad389987a1be5a763", + "benchmark/results/perf_scaling_rise_investigation_20260929/audit_overlap_receipt.json": "113b5bcd1e39935efe3f47dda6eb3c8683d55f1c8c7d0b17f3bb17e5939aca5b", + "benchmark/results/perf_scaling_rise_investigation_20260929/audit_overlap_resources.jsonl": "68570f53f3d97cfd7428f9cd3482b214b9e39bbccca892655360b651529a4f6e", + "benchmark/results/perf_scaling_rise_investigation_20260929/audit_overlap_steps.csv": "7248bf88f04f109cf862c28c4cdf29b92b6894ed51401c8bf06d078669f0632b", + "benchmark/results/perf_scaling_rise_investigation_20260929/audit_processes.jsonl": "491a34cb38232b27af00ffd0c75e3194b44feb1f14b95edafeaecb6c28edea94", + "benchmark/results/perf_scaling_rise_investigation_20260929/check_export_parity.py": "4620186c3a049fb677b9f6c0799fdc3a151f539e9a56885de3668372cb8b4de5", + "benchmark/results/perf_scaling_rise_investigation_20260929/clean_comparison.json": "29cad99db5d0d7f4aacac119c4a6b078cb835f1cd64ca9de6d0bb3caf4aa9e5a", + "benchmark/results/perf_scaling_rise_investigation_20260929/control_1_lanes.csv": "e8c810f898ab57af09e1bd890025e9c2c55178dc91b7618a1bf1e298224b2696", + "benchmark/results/perf_scaling_rise_investigation_20260929/control_1_observation.json": "61c422f74198a6970b6c1038a5e912f814b3b3b6b8d4f0bf7f89471bbb5d8f90", + "benchmark/results/perf_scaling_rise_investigation_20260929/control_1_receipt.json": "0324e4586c3a0d0b990d3b208fec27f7b970d3364e68d6963de78e1c9f96be5c", + "benchmark/results/perf_scaling_rise_investigation_20260929/control_1_resources.jsonl": "8f9556f4c18ac305bc804d9b6c2597b5a85792f1dc3d7766e3ad19eac6f76205", + "benchmark/results/perf_scaling_rise_investigation_20260929/control_1_steps.csv": "32a013d5e18c4ddf47bae5b1f972ad6824d6d4a829938f999bd61d38216fe8e7", + "benchmark/results/perf_scaling_rise_investigation_20260929/control_2_lanes.csv": "3a9a6911b3ad605d2bd6c312cd1e3b15df3e1637e7aef0fd136907cb1dc56045", + "benchmark/results/perf_scaling_rise_investigation_20260929/control_2_observation.json": "ba389668fb78ef25fae78527976e7a02fd4428db769e741fdf37124758c9cbf9", + "benchmark/results/perf_scaling_rise_investigation_20260929/control_2_receipt.json": "862d753d3d98012252cdb231a62cf13fc4a90210522497823c63e9241d36d599", + "benchmark/results/perf_scaling_rise_investigation_20260929/control_2_resources.jsonl": "a48ec2c55507f89120c41033e94e553692fd8bed9f38833fd3c07486f7b89e77", + "benchmark/results/perf_scaling_rise_investigation_20260929/control_2_steps.csv": "a5c6a4439a78e2df6555407a0b118a8c589233ee1be7c7d502bdbcf61caf45ab", + "benchmark/results/perf_scaling_rise_investigation_20260929/export_parity.json": "2604ecab15341d4959d9bd8f2f262c55ec8809251cc572af96a4621d93b90dff", + "benchmark/results/perf_scaling_rise_investigation_20260929/fused_1_lanes.csv": "20da5aa68bf5f6c7352efa5c2b047ac0954333febc7355250dff8416f50cc52f", + "benchmark/results/perf_scaling_rise_investigation_20260929/fused_1_observation.json": "841c92ebee8b468ff2a1a15aa04f46b31c7c50b0c1c6c4654f1b8a3b1e06c4b6", + "benchmark/results/perf_scaling_rise_investigation_20260929/fused_1_receipt.json": "f19e82e711b6baf06c405ef8ff82eb4a9f5b68c99481e2449807d9785e2ab9b6", + "benchmark/results/perf_scaling_rise_investigation_20260929/fused_1_resources.jsonl": "10366f04634d250df292eea320e469ff343823fcd5a0b09a6bb3f87efb8613f3", + "benchmark/results/perf_scaling_rise_investigation_20260929/fused_1_steps.csv": "cda853cc8f06bcc27f603a69161d89252dca4c9ccb209da4ed39b5ccef9e0d9c", + "benchmark/results/perf_scaling_rise_investigation_20260929/fused_2_lanes.csv": "afa016f89bdc5c5de31dd7bad24f6da87c7863d9ccc3953265e1705c49d31d96", + "benchmark/results/perf_scaling_rise_investigation_20260929/fused_2_observation.json": "62a412a1d60684805229850a9d6bd72ceea2b149ff6b43c55bee0dfc846d3e92", + "benchmark/results/perf_scaling_rise_investigation_20260929/fused_2_receipt.json": "cb8e180c5bbfb77b97b591b1584179f3e6aada3b78d7319d93f7b27219a83398", + "benchmark/results/perf_scaling_rise_investigation_20260929/fused_2_resources.jsonl": "aaec729d62128f354d07706f3bfb4670a1dd0a45e4a77889c6ff8cf1b035092f", + "benchmark/results/perf_scaling_rise_investigation_20260929/fused_2_steps.csv": "32bff7c63bdb18e41b5ca01f1abc978caf69d85c500442523539365e1d920bfc", + "benchmark/results/perf_scaling_rise_investigation_20260929/historical_control_lanes.csv": "9c0cea11096b1fa6df8f92bca8721bfc3778c33c452cbc20e312fe080c4fa37e", + "benchmark/results/perf_scaling_rise_investigation_20260929/historical_control_observation.json": "5b30461c7976dca68f937251c1402c753270540fb095ae8e5439d1bbb8ec46be", + "benchmark/results/perf_scaling_rise_investigation_20260929/historical_control_steps.csv": "24029c49adb574e68f8753a38138e93f64d9cb3c928a6813479053e564f43eea", + "benchmark/results/perf_scaling_rise_investigation_20260929/historical_fused_lanes.csv": "0c77c84e969f26c842d77093adbdac5cdda74b81a7381fe0e2cb28d3c28f305b", + "benchmark/results/perf_scaling_rise_investigation_20260929/historical_fused_observation.json": "14a76ef34d2f0e7d5d1711875358c255cc3c750d6be756ac9e9862ca46af2a2a", + "benchmark/results/perf_scaling_rise_investigation_20260929/historical_fused_steps.csv": "66f44fcfad3a5f2ac38c07086c144bc49c554d53449537f302bc50d703efc700", + "benchmark/results/perf_scaling_rise_investigation_20260929/measured_native_scaling.png": "5451ef08907f83ea0580baf23b56154b24c4aeb313561ff120d37623ad58f67f", + "benchmark/results/perf_scaling_rise_investigation_20260929/measured_native_scaling.svg": "897b542be439bd4af2b46888d93d517df41d27a7cffba7bcf2e648207214f526", + "benchmark/results/perf_scaling_rise_investigation_20260929/measured_ratios.json": "2afe22e357217037e8e8eeb03e3d7926f41bf8723070a793df0c89d6ea1e152d", + "benchmark/results/perf_scaling_rise_investigation_20260929/observations.jsonl": "3eb0c48208eeaf7ce9915f8d9aade45f041979018443cca7b9bc0b18c9cadb55", + "benchmark/results/perf_scaling_rise_investigation_20260929/original_audit_completion.json": "05f51d788a3f6a8483c388e625f3ce87fe395a320799381d9ec426cc4e4f7e9e", + "benchmark/results/perf_scaling_rise_investigation_20260929/plot.py": "b374f19611462b2d32c7c3b2f0a31a1a1771254f7e982d01c56d1365e3d0bc00", + "benchmark/results/perf_scaling_rise_investigation_20260929/reproduce_audit_overlap.py": "6d903e96b1617e96527ed52c456c2b7e8fae29d23203711692536323e6ae05e1", + "benchmark/results/perf_scaling_rise_investigation_20260929/reproduced_audit_after.log": "13874b229972cd645f8d848ecc44ba6ae39d7714bff6dc1b58c5f9b9a11a10a3", + "benchmark/results/perf_scaling_rise_investigation_20260929/reproduced_audit_after.py": "7692c8ae0ac9c82e32f5984ad080cfe609436451e3115979b192d67d30596c22", + "benchmark/results/perf_scaling_rise_investigation_20260929/reproduced_audit_before_expanded.log": "b3d881d0ed506f021b19dce00b3b9b89226747847c897ee15d706755d9c1f6b6", + "benchmark/results/perf_scaling_rise_investigation_20260929/reproduced_audit_before_expanded.py": "2d3f39587d0c7a050d54e0bf5ea2652e9da074f0ef4307fdc8e9c02d83ec9695", + "benchmark/results/perf_scaling_rise_investigation_20260929/resource_summary.json": "e4fc06980257cf315ab5f9a8cd158c7ac9747e7ad67411f269faa6e1f920ed30", + "benchmark/results/perf_scaling_rise_investigation_20260929/run_clean_pairs.py": "05e873c327c5c8fb03d875e0967db0a2e3b16051a4d8f1ce6ba9925a50d98ba7", + "benchmark/results/perf_scaling_rise_investigation_20260929/scope.json": "9a2279a99cbd1ac41c8b7ab49a7bff50af0ed2b04febeb0a2dbc8826b71d66f2", + "benchmark/results/perf_schema_source_readiness_20261003/README.rst": "9c21aa47a6824050dbae03d15050a8bf9385d33079f6182096d1d63ebab6382c", + "benchmark/results/perf_schema_source_readiness_20261003/receipt.json": "9baa4230ef299fd3514ab91fc107c5ab1fc241910f5a30cadd40b56c7fd15c79", + "benchmark/results/perf_server_startup_preparation_20260930/README.md": "327c6ae38a8b22943b6b85e2cd9da6c25a8822747ff5282ad0449c2efa0aef7a", + "benchmark/results/perf_server_startup_preparation_20260930/analysis.json": "2acf3132377fced68aa981ec1b70b759a6bbebece273c1c2f56e548399d37c0e", + "benchmark/results/perf_server_startup_preparation_20260930/architecture.md": "25e910fdcaef52a6763c5c587c08493b0db65a9baee079e344710bb4d9687cc9", + "benchmark/results/perf_server_startup_preparation_20260930/observations/cold_server_diagnostic.csv": "3227764653e8041c25148980f5bfc3aed587b716f31b7821776c19122c321f2f", + "benchmark/results/perf_server_startup_preparation_20260930/observations/cold_server_diagnostic_clock.json": "38d84791c399cb705b588a7f93cca3c769eb41ba056c744931bdf02981a148a8", + "benchmark/results/perf_server_startup_preparation_20260930/observations/imagingflow_pipeline_receipt.json": "3ab7c4071e94167a4522a5dfd38b5a7b429f65a2243cd8e72f33bfa5104afe4e", + "benchmark/results/perf_server_startup_preparation_20260930/observations/initial_startup_inactivity_failure.csv": "4b4093e6438e2641c44aefade6cbadd496b605ede4c7024c7e6c94e077e97d56", + "benchmark/results/perf_server_startup_preparation_20260930/observations/main_3d_failure.csv": "fff90aaef255e2122203f535eab2734c8bd37968c086d436b6cfe52ef5c30dd0", + "benchmark/results/perf_server_startup_preparation_20260930/observations/main_3d_failure_clock.json": "fc3a9b94bc0141e4bc346666a5a7fef15f9f05d0732d9ea0f33810f848ddfe80", + "benchmark/results/perf_server_startup_preparation_20260930/observations/ready_pipeline_installed.csv": "4117a32599a08705eecc2dc8e12bff755a7a6854765233c476b1f6437b14a952", + "benchmark/results/perf_server_startup_preparation_20260930/observations/ready_pipeline_installed_clock.json": "b0fcccad7ea0bcf268c789be81ded4e51302cae11f22677f14a24457b9dc55e6", + "benchmark/results/perf_server_startup_preparation_20260930/validation/perf-server-startup-benchmark-boundary-tests-20260930.log": "e62fc5cb412b4aee73d7faa9e997c91974752113442fa892bccac14c5039d602", + "benchmark/results/perf_server_startup_preparation_20260930/validation/perf-server-startup-final-r1-20260930.log": "6d4b85b3fe7af5d48ba1d634b7210e5a43f8a830c450307fc5e9934083bbee43", + "benchmark/results/perf_server_startup_preparation_20260930/validation/perf-server-startup-final-wheel-build-20260930.log": "4990d961752c288dd2eefeb590f06548c40c6bd62bf08e50179966106a2faffb", + "benchmark/results/perf_server_startup_preparation_20260930/validation/perf-server-startup-final-wheel-install-20260930.log": "d84a606fb441a22e113f3548f763c47268795a44f3fe696fbd8a21e55184d6cc", + "benchmark/results/perf_server_startup_preparation_20260930/validation/perf-server-startup-installed-provenance-20260930.json": "60d1776a3fcb534bd0bb640e620c33584c5544af459533541c9b38c272d91838", + "benchmark/results/perf_server_startup_preparation_20260930/validation/perf-server-startup-latest-main-consumer-tests-20260930.log": "383ec7187143430f958308198f1e46ca74fe6e0a79f4c6485b853a448b4094c6", + "benchmark/results/perf_server_startup_preparation_20260930/validation/perf-server-startup-nra-source-transaction-20260930.diff": "f9962b866fc9c7f8e44a6fe6ca2aa4e3854a387e1cc0028fc1c27077f6b3b5e1", + "benchmark/results/perf_server_startup_preparation_20260930/validation/perf-server-startup-progress-nra-source-transaction-20260930.diff": "457503e409cac051aae12f7eb59b407ed955786d0c6ee317b5bdb41c80952b73", + "benchmark/results/perf_server_startup_preparation_20260930/validation/structural_checks.json": "88dac376aa48035531253941ad5b018416c222d87c2357b14e76ac613c8f59eb", + "benchmark/results/perf_single_axis_session_20260929/README.md": "02bdcf8c806adce5d33a76d2a2aa42be7d909b4039760f89d2f7934af8451fdc", + "benchmark/results/perf_single_axis_session_20260929/candidate.csv": "ff56ad8a251369fc73a17e2fff9da7b5a48bca44190a99cc6a324298ec191580", + "benchmark/results/perf_single_axis_session_20260929/control.csv": "b7589b2a7dd3cc85b8b4732216f2cdde53d27269beb0b14364099418d26e596e", + "benchmark/results/perf_single_axis_session_20260929/fresh_server_total_breakdown.png": "5c73efe68bd3524528accdeffdc0a8fffb31bee8256a200db9668d615b01b353", + "benchmark/results/perf_single_axis_session_20260929/multiwell_candidate.csv": "6a377170e3b4678d1dfebc2da0cd2ddd3a701ae65d48dd45e2f70d4bb60f3937", + "benchmark/results/perf_single_axis_session_20260929/multiwell_control.csv": "d9807519e2235be4b5eb7d22a7f47cc86a1f64e790a1951553fdbb374bac5dbc", + "benchmark/results/perf_single_axis_session_20260929/multiwell_control_initial.csv": "4a8320b8eb6e896ac21d0e39dbb03723ebba8041037e1358a7bfd46940022b0c", + "benchmark/results/perf_single_axis_session_20260929/per_case_phase_deltas.png": "a1759dba131493c381f8540314e79cf2ce6147dcb6c94ba3bab2c536ee3b3e29", + "benchmark/results/perf_single_axis_session_20260929/plot.py": "4fa0997a6ce1b859e0afa69b109936d2f68f7d3485b33047ba60bd3c1706c0c9", + "benchmark/results/perf_source_header_reuse_20260930/README.md": "d573cda3f18feacf2606bb4effcb2f22b016b779d9fba7b68d262cf18495093a", + "benchmark/results/perf_source_header_reuse_20260930/comparison.png": "d0b3b41dbc91ba0d90a86b5775f7b7a8f4882178161f0cd81374991d64e1b819", + "benchmark/results/perf_source_header_reuse_20260930/comparison.svg": "bfa2d081246d88c3197043c5c6e371e47822fbe2bcf71d3b33859f34d0a3c32c", + "benchmark/results/perf_source_header_reuse_20260930/observations/current-main-primary-abba-1-main-cli-clock.json": "0b953ae6c5761824617dda39094e69b5d4d23c499177dc8c0040ab41be344dc8", + "benchmark/results/perf_source_header_reuse_20260930/observations/current-main-primary-abba-1-main.csv": "fd63b3bc178bd28c178c3148d0f4160e6649c7654bb0c8a6b3c0c42059b9992a", + "benchmark/results/perf_source_header_reuse_20260930/observations/current-main-primary-abba-2-candidate-cli-clock.json": "4b50df94f0533637a2fb202333a4aa3bcd71206ec2cc19120af8cac27337e2bd", + "benchmark/results/perf_source_header_reuse_20260930/observations/current-main-primary-abba-2-candidate.csv": "58e6e7f741eddb464c156c62e5fd52e669bae3fba90876e09cef0e71ca87abab", + "benchmark/results/perf_source_header_reuse_20260930/observations/current-main-primary-abba-3-candidate-cli-clock.json": "bb08b80b51aae0c7bc95b3aad183df936aea2b5c7da34cae34629372a136c4b8", + "benchmark/results/perf_source_header_reuse_20260930/observations/current-main-primary-abba-3-candidate.csv": "b987377e78c8d1e77068b68ac4b5b77748ab44601a746a8b2ab3c0d890e21d1e", + "benchmark/results/perf_source_header_reuse_20260930/observations/current-main-primary-abba-4-main-cli-clock.json": "c120a272df3a190d89a27fba53016df6c48ad27100d97aad185a999edbb3e3b2", + "benchmark/results/perf_source_header_reuse_20260930/observations/current-main-primary-abba-4-main.csv": "bbe73574f3f153612cc5ffb922be4ce384744a5a3537169c6a93480ddbd0b5ec", + "benchmark/results/perf_source_header_reuse_20260930/observations/fixed-cpu-abba-1-main-cli-clock.json": "c12e2ab163df069033e5615ce95102a2afb7a2d97856234418a282d636bde30b", + "benchmark/results/perf_source_header_reuse_20260930/observations/fixed-cpu-abba-1-main.csv": "63922bfca62a8da3f6a1609909fd909bb3e034ba3fff63d32a80e2eb04e29724", + "benchmark/results/perf_source_header_reuse_20260930/observations/fixed-cpu-abba-2-candidate-cli-clock.json": "5d3bde2043f6d4882229234371d28313d3c38bcf639efd52390d58db1b2f2bd7", + "benchmark/results/perf_source_header_reuse_20260930/observations/fixed-cpu-abba-2-candidate.csv": "77c2194e590099238394aba9644bc9bc5fe942724214f4b4b613210ec31acc98", + "benchmark/results/perf_source_header_reuse_20260930/observations/fixed-cpu-abba-3-candidate-cli-clock.json": "8257bbc798b2e65311c205c6f60277cb36420fbc76bd56ff5ad4cf393f7158f8", + "benchmark/results/perf_source_header_reuse_20260930/observations/fixed-cpu-abba-3-candidate.csv": "31eaa3c2a385d03bc1e628701c4ec7ca307050191b32e3efc051a8343bc2306e", + "benchmark/results/perf_source_header_reuse_20260930/observations/fixed-cpu-abba-4-main-cli-clock.json": "afb99489c8071e9c9171f3043c445539a7e3e72d2c0a7e7472335f42d9af38d7", + "benchmark/results/perf_source_header_reuse_20260930/observations/fixed-cpu-abba-4-main.csv": "2613a0de75f5dfc7fc315dc4f36770e1e0b85a03feda425ed9de82f1618d97b3", + "benchmark/results/perf_source_header_reuse_20260930/observations/perf-source-header-current-main-primary-abba-observations-20260930.json": "0c2bc28205a5e1ca1e8f4ca9ba78f486856b98643b6c4a8b4ea390bb1a1969ca", + "benchmark/results/perf_source_header_reuse_20260930/observations/perf-source-header-fixed-cpu-abba-observations-20260930.json": "9e1d2795b715ee86bb02310150f1d9ecae854704ce4efd91ccb0528d4cbde4d2", + "benchmark/results/perf_source_header_reuse_20260930/observations/perf-source-header-primary-only-abba-observations-20260930.json": "1a07ca770f4b89ad30fc3ec3d7db9d05967d83441b20ce1b49c4e3831032e5dc", + "benchmark/results/perf_source_header_reuse_20260930/observations/primary-only-abba-1-main-cli-clock.json": "4a5ca518ecc32a7e611f3a3325b8d9a86ee9c7b60372864ff8c45520adc51e93", + "benchmark/results/perf_source_header_reuse_20260930/observations/primary-only-abba-1-main.csv": "d6f602114ad0ed3a0bd99e82c1a83e60e634beb2a31fed8c58b637bd90a51aee", + "benchmark/results/perf_source_header_reuse_20260930/observations/primary-only-abba-2-candidate-cli-clock.json": "1e789a7a798e58428496939054fcfd0aec5b75f5ba91010d395bc5ade812acac", + "benchmark/results/perf_source_header_reuse_20260930/observations/primary-only-abba-2-candidate.csv": "3a928779b10f3ab68258da537bd3664bdec8071d1131a1c999f0a79c5b094ebb", + "benchmark/results/perf_source_header_reuse_20260930/observations/primary-only-abba-3-candidate-cli-clock.json": "5fd5e0c9d3c6e584996b53aac9e96f6a7266f16918a8151c5f51f1c447de2f58", + "benchmark/results/perf_source_header_reuse_20260930/observations/primary-only-abba-3-candidate.csv": "9b422231278c42c5f11a3c049b9a0cfc534449976b7843251d9182b9612a1ca7", + "benchmark/results/perf_source_header_reuse_20260930/observations/primary-only-abba-4-main-cli-clock.json": "12500377ab449aff7ff5f8354ee4117d92db768d5a8505f73c1769351ee55e17", + "benchmark/results/perf_source_header_reuse_20260930/observations/primary-only-abba-4-main.csv": "ae0d4ce65a9d02a5fbbd866ad5af4ab417cb9938a79057461b5c81eab88ca4e8", + "benchmark/results/perf_source_header_reuse_20260930/recipes/analyze_source_header_phases_20260930.py": "a56040710dde0fe391263a4b5d231e4374e8222a8807f896c2a225c884bb711b", + "benchmark/results/perf_source_header_reuse_20260930/recipes/replay_production_source_headers_20260930.py": "ca0d6c7d3afe7989ed1c7c3b2b3206fc916b5456ab9c727dc73f6d75b282ea81", + "benchmark/results/perf_source_header_reuse_20260930/recipes/run_source_header_current_main_abba_20260930.py": "21f221d367f41ed16dba7377795e58cc9ae91ff8e6b4789ee7de7b99dce171f5", + "benchmark/results/perf_source_header_reuse_20260930/recipes/run_source_header_preparation_timing_20260930.py": "4bc27ee0c8559dd830d1a58c8476eecf90ee69c0ef1b0a89e92a6d0934698f4f", + "benchmark/results/perf_source_header_reuse_20260930/recipes/validate_source_header_current_main_20260930.py": "5fcc2c2d7e96717fbb0068ffee2c82fdcd21d96b3a4a80cf04511c39fdb9d481", + "benchmark/results/perf_source_header_reuse_20260930/validation/perf-source-header-comparison-20260930.json": "ff41380dad71d23b242a69cfea22eb4515517ba61ce3066af8b3c3af62ad0534", + "benchmark/results/perf_source_header_reuse_20260930/validation/perf-source-header-current-main-comparison-20260930.json": "beea5157067f67b05594d3479dc97967b35ef532e28dcbb5d166dbd546477a20", + "benchmark/results/perf_source_header_reuse_20260930/validation/perf-source-header-current-main-provenance-20260930.json": "9d8bbba5cb8adb80d7474ef1007ccdac890c56f8438601195774b120499ad842", + "benchmark/results/perf_source_header_reuse_20260930/validation/perf-source-header-latest-main-consumer-tests-20260930.log": "cd716188cc66c2cffbdcf62f21534893a8aa4e071587a1760dc59a31df7646cb", + "benchmark/results/perf_source_header_reuse_20260930/validation/perf-source-header-latest-main-r1-20260930.log": "523f8f5bcb9c890938a709831b3f4f9b1452afd2e1e68c4591868f749ce48056", + "benchmark/results/perf_source_header_reuse_20260930/validation/perf-source-header-nra-transaction-20260930.diff": "edfc5f6b197c1de8805ee286322d8032709793cf4097075c47fd8e8332e36f06", + "benchmark/results/perf_source_header_reuse_20260930/validation/perf-source-header-ownership-decision-20260930.json": "3b897eb635cd6bfea106bc847fdbb742a486c89476977350c84b708c3958af37", + "benchmark/results/perf_source_header_reuse_20260930/validation/perf-source-header-phase-comparison-20260930.json": "e0bb44f1cbe9e73103f4db48b0603401f6c5e94c59d104c3cb46fcbb93b56217", + "benchmark/results/perf_source_header_reuse_20260930/validation/perf-source-header-production-replay-candidate-20260930.json": "2ae3fe9f025f5b302e23524ae9c6897e86554bc1978213f690be8a683ee666be", + "benchmark/results/perf_source_header_reuse_20260930/validation/perf-source-header-production-replay-main-20260930.json": "6dab79713afad5e2919b93f35bb38e87308565b843826579a0a259b8676c14a4", + "benchmark/results/perf_source_header_reuse_20260930/validation/perf-source-header-scientific-parity-20260930.json": "a790f1b9f3998f58667b481f9489484cad3ef794ed7c55f381bc6e5c1ca550fb", + "benchmark/results/perf_source_header_reuse_20260930/validation/structural_checks.json": "123a0fb9b9901b6b6fef2d33b1ccf85c11d3b7b4f81dedc08edd0e7edec88f07", + "benchmark/results/perf_source_identity_resolution_20260930/README.md": "6097286b97d21659d43868a7b9089c2e6112ca263c579797fb6a7dc11872a154", + "benchmark/results/perf_source_identity_resolution_20260930/comparison.png": "10e99e9a42d251a2b37b9539d1a11595bc8407fa3bf1cd84448eb25754e42857", + "benchmark/results/perf_source_identity_resolution_20260930/comparison.svg": "9b0ffba4cc2a22eb278d4a36720c3cb1c845fe03d5ec7bc2766e60b3216141e5", + "benchmark/results/perf_source_identity_resolution_20260930/observations/perf-source-identity-comparison-20260930.json": "f8ca190b2c0326700be187c14e466e8997ca9581aeb179e0c35b208fd8086cf8", + "benchmark/results/perf_source_identity_resolution_20260930/observations/perf-source-identity-fixed-cpu-abba-observations-20260930.json": "8a4b8af2e3a0f6ce4fa51d80e58d45b83a801f8eb449a40980407131b6efb5e2", + "benchmark/results/perf_source_identity_resolution_20260930/observations/run-1-main-clock.json": "9c6cc151dca167a9f38930c7fbea2acf35dcef076dd7db7b72556bfc808b30cc", + "benchmark/results/perf_source_identity_resolution_20260930/observations/run-1-main.csv": "180039c73ebd424228568bbba2f5eff049ddcec153605079d31ead81a3016b77", + "benchmark/results/perf_source_identity_resolution_20260930/observations/run-2-candidate-clock.json": "8f25c40ce27f40d382d567547791b053e2a043733fbc9cce365edf46222934df", + "benchmark/results/perf_source_identity_resolution_20260930/observations/run-2-candidate.csv": "b0987e7ee5177ce42e42a88f093acb998ecf30cdc57962c8a65740a427db1eed", + "benchmark/results/perf_source_identity_resolution_20260930/observations/run-3-candidate-clock.json": "3f8c0083f1b98d09e7d8ee62f7fd99e912279fdc54e79bd0ecd42f8ee7311a32", + "benchmark/results/perf_source_identity_resolution_20260930/observations/run-3-candidate.csv": "3e23fa00ce05a00652b53992b25bde86783a1abc05a343a2e0848b4584679c27", + "benchmark/results/perf_source_identity_resolution_20260930/observations/run-4-main-clock.json": "f4b86256ca4fa029bd40b44d653b993c3ce2c6739411b291c4dd11841f239b32", + "benchmark/results/perf_source_identity_resolution_20260930/observations/run-4-main.csv": "2f08ef2f5cd3c9338468ede91afae1088017f609c5ab6efa5b98d421fa73de06", + "benchmark/results/perf_source_identity_resolution_20260930/observations/source-preparation-candidate.jsonl": "794f0411506d014f664a05ebef3b77b21260bc628189989d8771661c4c61d5c9", + "benchmark/results/perf_source_identity_resolution_20260930/observations/source-preparation-main.jsonl": "8b7cd0aa0f8e472f41104fce5f24dee53e85f07fddeff7d9729201804031799a", + "benchmark/results/perf_source_identity_resolution_20260930/validation/perf-source-identity-index-ownership-decision-20260930.json": "38cc2bcd99573a6e0f963ca47f1b6420b9d7b1390dd9f605e4c30873c938b96a", + "benchmark/results/perf_source_identity_resolution_20260930/validation/perf-source-identity-scientific-parity-20260930.json": "b97ba3cbf3f33ddd50c773724d1903f969de92896fe5ae9eb8d10a8626de72a1", + "benchmark/results/perf_source_identity_resolution_20260930/validation/perf-source-member-production-replay-candidate-20260930.json": "c11b3e9e847604145c82dd7ff9cc0f6ea94447dee4712c197bf30f5aea1a556b", + "benchmark/results/perf_source_identity_resolution_20260930/validation/perf-source-member-production-replay-main-20260930.json": "ec042f87dc615e5a39cbcb54b10e1595194c07b1d6a773a3d0dcfa727e61f810", + "benchmark/results/perf_source_identity_resolution_20260930/validation/structural_checks.json": "9ac2fe388d786c9d50e9ab0b44525a3c993fcdf419a1232dcf8d53942ac23337", + "benchmark/results/perf_wide_intensity_distribution_20260929/README.md": "b63a1f555235f93772537dfafca20cf01a9e86ac7009a6f396a37d3b346e6f2b", + "benchmark/results/perf_wide_intensity_distribution_20260929/measured_scaling.png": "b980f672c3f66478b1cd25cd996d2d592c52d5ed936cfaca5db810e6eaa674c7", + "benchmark/results/perf_wide_intensity_distribution_20260929/measured_scaling.svg": "e13b8f55d96b8436ffdb338df7b93d03fcb378ac0cc2a2928751ea457c152630", + "benchmark/results/perf_wide_intensity_distribution_20260929/observations.csv": "9f94923f61028d6b9816ebae75226516171b8e2f2254653edf5d47cc5bbfb675", + "benchmark/results/perf_wide_intensity_distribution_20260929/output_parity.csv": "74435f24bfbe5b0d41b48d2e57cf690b05b20bececb901e737fb1446eb1821b7", + "benchmark/results/perf_wide_intensity_distribution_20260929/plot.py": "a51838fc0ad2804d8974bb41a3e152dd526026703a6465bd176be1fdcdb3c57b", + "benchmark/results/perf_wide_intensity_distribution_20260929/provenance.json": "8bd7f8d239a95c75089e1b441ea5ed92bfd5d60ba74fe845e5a26bca9b975abc", + "benchmark/results/perf_wide_intensity_distribution_20260929/replay_saved_batch.py": "e1edd3ca70d48d2c489e4a038d2150ed6e57c4f2836a29bc334c350744603bdc", + "benchmark/results/perf_wide_intensity_distribution_20260929/saved_batch_probe.json": "d759679a546b5d88ecb784494e5fae0f39e6fc91663cd9cd564439d074b9f64f", + "benchmark/results/perf_wide_intensity_distribution_20260929/step_timings.csv": "165dbac3bee4b7cd76862a082dbeec5a3d4b2b1c1ab59d62eed88845746af2b2", + "benchmark/results/perf_worker_profiling_20260929/README.md": "ff7bd0e2b525d94e9d140c3a7e5badbc60d2dae30198b350b6b00f9113a712a4", + "benchmark/results/perf_worker_profiling_20260929/authored_nra_transaction.py": "ea4a4cd873585a4463968179e5d3927b4fa4c946d59f66db7bdb8167447a565f", + "benchmark/results/perf_worker_profiling_20260929/class_roles_after.json": "c29cbf078dfb11698cab777fe0b491ec2daeb163ff9e9a17628984ee12aa1e91", + "benchmark/results/perf_worker_profiling_20260929/class_roles_before.json": "29bf5cb828343e28579f4982e1b4339e32fcaab9d531d1e8e0bb8a775b617d4b", + "benchmark/results/perf_worker_profiling_20260929/main_sync_tests.log": "b54ba3ec209c2a1a4938cfe31e3778e025657bd349e858ed6ef51935ce963c8b", + "benchmark/results/perf_worker_profiling_20260929/ownership.md": "ab37026e795ade8f356e650c3ecea83f4246673963eb93798766a750b595d9bf", + "benchmark/results/perf_worker_profiling_20260929/perf-worker-profile-baseline-regression-20260929.log": "a16a0e89391babd64abd1878a5fcb49df055e7388652a231e14323d73069392b", + "benchmark/results/perf_worker_profiling_20260929/perf-worker-profile-corrected-top-20260929.log": "57b36807c0f5a0c6923f3acc46620180635772b308e5568ef9791982103bd5ea", + "benchmark/results/perf_worker_profiling_20260929/perf-worker-profile-integration-tests-20260929.log": "ecfea1a16e246ff55aaf411843b17c866c59d8a5ec4c6b7b6e05ebabf08c72e7", + "benchmark/results/perf_worker_profiling_20260929/perf-worker-profile-nra-projected-20260929.diff": "edf4fe82ec7edbc66cab4b33018a1111c60fdee54d3bf88f955047d5ac0188bf", + "benchmark/results/perf_worker_profiling_20260929/perf-worker-profile-production-check-20260929.json": "4438eacd5a28a14ee48343911c8925fd8661cb9b698196667d9984f18c29ce3e", + "benchmark/runner.py": "089f0c3a31496dcf46519aa2c1daea435066a77e82a4e0897f040a39641c9031", + "benchmark/runtime_env.py": "c5661239230be69ebd86559a661940efb8156901aaa1616b7cdcfefa1537439d", + "benchmark/throughput_scaling.py": "0df1d940e84b6ffa246ae795ecaa3d66f5f1cb3fda6523ec7282db8ea9369af2", + "benchmark/timing.py": "b908f648d8e0ef0091da96a33a65fad9203962f17e9a2e6e75695c12d0404ada", + "benchmark/validation/README.md": "e188b7e57b42aca79f2dd1267e18af8986f73d8ce80456febf17b4665d449f36", + "benchmark/validation/__init__.py": "d88b40e492c5aed0f461522dad63bbd4ce1cc1c1f65498adfadf1c64576c316d", + "benchmark/validation/__main__.py": "d093e023493bc368c7ff37d8323028fb40dc17f0c4f09633ed7645e84f5058e0", + "benchmark/validation/corpus.py": "c122842a39b9dc2af7db688519bddb3470339904c347e6e5a039d77cf723c0ae", + "benchmark/validation/layouts.py": "be14d9802251778d5bbf07167ce571651115ce55d7b55a4d2c0ee8ae8a4d563d", + "benchmark/validation/references.py": "d3257c514ae6d4f64f46d513d63dabacfe04cc36bed05373b7a3055438928c90", + "benchmark/validation/scoring.py": "6e9c7a4be22be553f02642ea0375604a890047cf40cea5429b7720804aa75e93", + "benchmark/well_throughput_scaling.py": "bcb58517c9c699be96d87027725a1fe299082874002cf0dc1032c6b267283e35", + "diagnostics/.gitignore": "b26aaf3fa669581280dc2d73f5c65aa4e7f5cdf675415309f7835a2537721095", + "diagnostics/ACQUISITION_SHA256SUMS": "01d1ee74593192823a32051d8ea9aaa76589c97787fc31ad522dc5265ea8b42e", + "diagnostics/NOMINAL_REVIEW_SHA256SUMS": "920243a783c70c5142e83a7bdd4ff4bebcba85159b62b573aefb9d1d66a772d1", + "diagnostics/README.rst": "939b5cfa8c32eb2db38cee2606ffa755be3eaf96fbe3640674f038ced125f5fc", + "diagnostics/SHA256SUMS": "bcfce1e1789f68ed4774f62db87e077d7a409344e6e9a741aba20b81388c5865", + "diagnostics/run_source_tests.py": "2dd04e5005b9cde8b1b9b86f0d17e3b9c45120795e9be0ed9b556787b33ffdf2", + "diagnostics/test_344_engineering_document.py": "9599cb9152d642e834b4056d820856c3dba9593647ea7554c36f0589c72b3798", + "diagnostics/test_344_source_binding_ingress.py": "f398b5db2435208a1317f0e97bf347e7e837e4b64f91ac0887e2e571d6da913c", + "docs/__init__.py": "f2ea2f1e16a5c2f29a8d6f869618ac9b5e70e72368d5ff60fffcf348dbccdaf4", + "docs/announcements/image_sc_openhcs_beta.md": "00488fcd6312b59f9408c69f32173d8b8af057f3080e90587536e428e5e7835e", + "docs/archive/README.md": "930de929bf83de77ec77a2bd992661f56517a8d26ed55027b703a69250d68d67", + "docs/archive/architecture/abstract_manager_widget.rst": "b779175704d10f906a4c1dfd9cb700639deb0e41492cf4ea69c5f8f48c24901d", + "docs/archive/architecture/abstract_table_browser.rst": "74a6dd42c6abe836aa7e58a4c80814a3adba9f8b2396d0aea9f0592342ebb346", + "docs/archive/architecture/cellprofiler_boundary_audit.md": "99e1518dc0aef8f97934af4c320e98a4e6fc39537c019e02bdc3f1395fd6fe38", + "docs/archive/architecture/cellprofiler_openhcs_runtime_unification_plan.md": "7cead27eeffa05cb1e27e35bea920963c843478e5ac707b494430113aac7300e", + "docs/archive/architecture/cellprofiler_runtime_execution_import_dispatch_consolidation_plan.md": "09b50a5f3afc7c743baa1d8ac97c051ad2a6644ece2b776d1fa7d9181a42e53a", + "docs/archive/architecture/cellprofiler_runtime_unification_uncertainty_audit.md": "0b2470532ef7684eb028ff19d13d8da165ace999fb68c634c829cafc60b80e4c", + "docs/archive/architecture/compilation_service.rst": "21e975265889add3ec4c896db0049a8d93f1e7b6c703dc0e3fd8c2c79a59e2c9", + "docs/archive/architecture/compilation_system_detailed.rst": "e5e74c8eededb2c1963acfb6deb84500f7014791f2beb7870083cec52b69e2c7", + "docs/archive/architecture/component_configuration_framework.rst": "f90bd3d76218d7a5a7055653b8abb1c2299cbd7ada32f803fef31e6788a0b726", + "docs/archive/architecture/component_processor_metaprogramming.rst": "fa52a3d58e15c141ba0e9546e3502fbc6340ad07dc31fd81de4f2a237d69d807", + "docs/archive/architecture/configuration_framework.rst": "1b37707e0d88d7f4542c74ddb729123b6a1ae3d128ad9891fb55c3f9af6be310", + "docs/archive/architecture/context_system.rst": "086c59e3331851427d02a0ed1df22eb27ad5ba8bcde4aadd05e72acc25bc0cc9", + "docs/archive/architecture/cross_window_update_optimization.rst": "8169bb105e1090877aa35f5612c6148f2d25d966f0db2f32aede4a455f4f296f", + "docs/archive/architecture/custom_function_registration_system.rst": "7f982f824c9ab6b72a20eb3a887852a01bc1c1cfa89c2b74599fe17b933212a2", + "docs/archive/architecture/declarative_window_system.rst": "d9e9b6cdca53c057fd907d41d80f6280a74016841ca978245ba60fabf1eaccb8", + "docs/archive/architecture/dict_pattern_case_study.rst": "17d3910ab6872065a5617df801473fadea5a1d1fdc842c7ccb9de350f5797420", + "docs/archive/architecture/dynamic_dataclass_factory.rst": "d22d1d24d18e2ac897a12974811ce4bf199cd8c4280350a0082aac04b6aea4eb", + "docs/archive/architecture/ezstitcher_to_openhcs_evolution.rst": "e73fdd71a23282180898eeacc99dc1ff205731981aa16af4b41dfe433e2dbf28", + "docs/archive/architecture/field_change_dispatcher.rst": "16f4ce723be215965b342f7f1a3d1b9895a5042b3b20be41e4d7621f30d1a819", + "docs/archive/architecture/fiji_streaming_system.rst": "4b5640cd5ab59d5978d62ab595e8cae1eaf8b1fdb7ad4d0e9eb5d53e90a394c2", + "docs/archive/architecture/flash_animation_system.rst": "a9021663f826034d0945caafba5acdde5d22a1727609689a94784b30dc578ee3", + "docs/archive/architecture/function_pattern_system.rst": "1511efd73879b31054dafcbdf029193bf597e586ee1b0ea4666e766b7beb2e7d", + "docs/archive/architecture/function_reference_pattern.rst": "f627165e8463192fdfea3f592028edade4123abbd70feb78e769ef1bf76286ea", + "docs/archive/architecture/function_registry_system.rst": "76b426d5ede8d808b22436b32f2f3ae8d1e24e1215bb83c721ee42dc8b49dbf3", + "docs/archive/architecture/gui_performance_patterns.rst": "e56dc6076c5f72d78fb53e402357dbb56c82ea0333dd93e5261b7b7b4300082d", + "docs/archive/architecture/list_item_preview_system.rst": "5f2b4b8a4c7e839ac25dffe2b3aaee50ef5357921d65dce7be9940b1fbeff6b2", + "docs/archive/architecture/memory_type_system.rst": "0d6c338f15792e55392ddefec582539ac2bd7a59292bcd2bbffe6b45fcd38027", + "docs/archive/architecture/napari_integration_architecture.rst": "3d37439e1abf621f4b49411ea6780b41f8ab09672f9bfa8479f348f31ad8927c", + "docs/archive/architecture/napari_streaming_system.rst": "0cabea8dea2d1377557fc3ebec24f259bec8fce57328dccd32507ab914a233c8", + "docs/archive/architecture/omero_backend_system.rst": "e55fc574916a152455105fdf7cecf8888fd76ff417febfe15a51bea994bbd421", + "docs/archive/architecture/parameter_form_lifecycle.rst": "93f59f392b13d333efbc2fc8653c140b6013158a8d1f50c7ae981780d2547339", + "docs/archive/architecture/parameter_form_service_architecture.rst": "1ac8b9651df9d4fbb611511027debb5ed22049f5af0978c71571cb82cae80e12", + "docs/archive/architecture/parametric_widget_creation.rst": "77699972cc6495a84d70214ae92060050426dda0dd7bee53b6ebbe041d0eeaa8", + "docs/archive/architecture/parser_metaprogramming_system.rst": "fd454b4111c68c20aaca5021a5e24d096e652c3d2a1f87f3fa6ccc0c6d20956c", + "docs/archive/architecture/pattern_detection_system.rst": "8428c68070fb999996ed22b25e30872f7f5384efc58537a1172356cbc8ef1e47", + "docs/archive/architecture/plugin_registry_advanced.rst": "95dd6f9be02fd9dbf139bc1da99a2fab22960b9a223ece052abe846d5d1cb040", + "docs/archive/architecture/plugin_registry_system.rst": "0c08d4ba2bb2eb0f070fcacc8240b75396d499ce8ab7519b146076950488d215", + "docs/archive/architecture/quick_start.rst": "bfd16fc9170f9e10a4927cbef9a3c9a43f9b17a10b87f8b441f4c8044554869d", + "docs/archive/architecture/roi_system.rst": "9a1c3f0a082bdece753b6713f409f1d15cd38090ec1267bba4436a6490d0aeec", + "docs/archive/architecture/scope_visual_feedback_system.rst": "3aa092abe420fbe990c880f1f0c17f32390b928ebc2eae1a62de75cc61d2afae", + "docs/archive/architecture/scope_window_factory_system.rst": "101ee2b845448cf7cd55a9fcc93c9e88a7353a0ad292ebbd28fa765eb94a3397", + "docs/archive/architecture/service-layer-architecture.rst": "4a9401ccd609bb0855f8ff63dac60513b8ff5a752f4e658f097e7bce392cd810", + "docs/archive/architecture/service_registry_integration.rst": "dcfe7f19815dc2468fa228bf38e1cccfb06aa0f43ce5ee52fc07b626e37341d4", + "docs/archive/architecture/step-editor-generalization.rst": "01590287d526f179be82e8fc4b14741f63057636016c93c2e3d7a8a46ccc4496", + "docs/archive/architecture/storage_and_memory_system.rst": "47724cfd7fe6408f4981a55677507dd7b7d15ed57b4fe242be27d3f12c28d00b", + "docs/archive/architecture/system_integration.rst": "5803ffb3614d61df3b0dfa9498932bacd1a06fdcab43597b16579323512e8fcd", + "docs/archive/architecture/tui_system.rst": "3ede276228c97bf216cae3f5e9d0e73a53ff9c38a8a6a6cd2cfbaf71b5a4c27d", + "docs/archive/architecture/ui_services_architecture.rst": "29f0b6327889053dad3dd603e52b736be886ee84cfe2cd85e0db6b96163eea52", + "docs/archive/architecture/widget_protocol_system.rst": "662e0caddb44b7cc85851aba9e456021bb7264e7811881a881b95aa7a3adf808", + "docs/archive/architecture/zmq_execution_service_extracted.rst": "74b82a586a657e38c8ec52c494616830dde54a74b9b815181367e0dbb4b6d395", + "docs/archive/development/architectural_refactoring_patterns.rst": "48c16c9ddcdac58eb14dbf8c290ec987d96d3cbe1a1eb5341b07dbcd6cb0ba4c", + "docs/archive/development/ast_refactoring_workflow.rst": "3a1a384e96e03ea0f93b60404cade294c2637b238c822f2e992adba81c724e92", + "docs/archive/development/cellprofiler_semantic_refactor_audit_20260507.md": "b91e8efcdf4ed148294095d960d8314b97444878d5a0ac76d4de5932ed829dfb", + "docs/archive/development/compact_window_patterns.rst": "b649f8ae20629ace06ae6dd7f11c3a4c11e97b9ab21e79a3cc05440591556ed2", + "docs/archive/development/compositional_commit_message_generation.rst": "44c6fe8f7fc325b16979fdd57e7e9817f4301136e0b76208b89798f550675b35", + "docs/archive/development/dry_run_methodology.md": "e716752082ccad04069bce1fb0caa68070f688d2d73ae4f1d4be38cfd12aaa0c", + "docs/archive/development/gui_test_recording.rst": "c32d42f86fbc4c8c92605ea153c4d4c7b5618f6592369ea1c67db177fc765559", + "docs/archive/development/lazy_dataclass_utils.rst": "b29b2f4bb9bb144bd96015496aaedb2920da36ecce832489b65231562bb9ae59", + "docs/archive/development/literal_includes_audit_methodology.rst": "5d9ff1fd3bf453f31142ab53f47982ca3dcdf41eefc5ba5bb39df0dce3fcfd23", + "docs/archive/development/parameter_analysis_audit.rst": "a1b1561e4a62940f72454261bc8c8dde598b049fcb52da8d2fd6d707eaab541f", + "docs/archive/development/placeholder_inheritance_debugging.rst": "3b1deb2b5edd91f2d0ad7ea12428774bd082dfe2fcf84905cc1e6e4bf90a8ea7", + "docs/archive/development/placeholder_refresh_threading.rst": "75097d76ad7c31dd6a9c8d69fbfeb5a656bd1d89b0d3969973eed846029cb23b", + "docs/archive/development/refactoring_principles.rst": "6b04521e7a6cd55b82d64930b3544e4d26782198005cced7a6e2a74717c8150f", + "docs/archive/development/scope_hierarchy_live_context.rst": "544658f0b897bdf99c8f833a8dd691eb720c6a83a87bef79395ce4513e02ed67", + "docs/archive/development/source_inspection_methodology.md": "365ad1d3903219363fbb83d03ec08927fbd3c2c41d989a16ae78e18765d7b4fd", + "docs/archive/development/systematic_refactoring_framework.rst": "0c117c5b780e5499e257d3e08682c0da01d411242fbfa23aa79fa9add70100e0", + "docs/archive/development/ui-patterns.rst": "c19cd470c6fe3f82ed61f467686f670863fe8749109426efc6ab51dc143ab301", + "docs/archive/development/unified_parameter_analyzer_migration.rst": "c08e68fe28309e9533815ace3410330b2a87d0ac73de791f2424848e77d7eaaa", + "docs/archive/development/window_manager_usage.rst": "7c1dab597e236e663f0395da526fe8e712962642f2e1dec5e38efcc4b1da0e45", + "docs/archive/user/research_impact.rst": "3c2303a420fd1e34418e1e23011917928f2e7e8183fd58d744377c3ca2adeaba", + "docs/archive/user_guide/production_examples.rst": "528884f6bf9c1dfc91ff906d11d3c6d3f5d92f412b0919a0431501eb74bb9fbe", + "docs/audits/README.md": "aeb08ec4e4a0e8cc7dd6e0be9d0de9c1dc159793f5643e91aa6c543cac145c4b", + "docs/audits/architecture_a_m.json": "7b8348bff72685427a8bcdde00dd6968cc2cf68125b6d954a653a26646033479", + "docs/audits/architecture_n_z_development.json": "f7bedd2b90bbd95721968e3dedf4d05090bea42df7c68972883683e82c4fecd5", + "docs/audits/authoring_preview_review_20260910.md": "0d48687e89bc855c33ec2fdd3029c8dd969ad2363b05fd6cba9135285ea79e01", + "docs/audits/automatic_materialization_policy_review_20260923.md": "3d5e28add58d44c0d70c35034c407503d2c4ed62eb460a10a10b6d2a07984cc4", + "docs/audits/benchmark_cli_interrupt_review_20260923.md": "96de3efd5b19689627eaa843900991785656ce9d669c91b11739a5c2990e283e", + "docs/audits/benchmark_outcome_scope_review_20260922.md": "225f364c2141c50f5c9f0cf5e6edc43372745816039d3f3f27e38c3f31a44867", + "docs/audits/benchmark_report_and_run_claim_review_20260923.md": "a4f810c130630e7509a2f75acc2d9a4704acb8bdab3aa69931f207caa899c78d", + "docs/audits/benchmark_wrapper_review_20260922.md": "3406437d32bf5cc0187fc6ec308cf6e717022e0fc1369b27467d0b060b255d82", + "docs/audits/gallery_connection_owner_review_20260924.md": "9e34feff8c8c84b78243e682d397b9263b34663b252a2c022e89a8586420b017", + "docs/audits/headless_observation_export_review_20260922.md": "7ce818c3c095b623bda793f00e184e1f90542c68a2355cfcf1f3c70a4ff69de7", + "docs/audits/hosted_ci_repair_20260914.md": "fea7faf803353f42d38f95ee786fbf923e1ef202b276a2151e7bcceef57d2e3e", + "docs/audits/measured_run_inspection_review_20260922.md": "d8bc2a5379196646fef482450af98e959c0b4d756cede8c4ddafd2769c87d072", + "docs/audits/native_volume_comparison_boundary_361_20261002.md": "86c9e06122d5ac8aa1df38b04eca2c76a470ec1965153a1c56b48a27591aea39", + "docs/audits/official30_reference_output_audit.md": "77258d0a4db14d3f4c37d8d8f88e3d5972d52d6693780e4fb177473ee471770e", + "docs/audits/official30_value_completion_20260914.md": "b97c66dfc108770a3172af706f676f500b006dcc2ca2f6573f75fc2d9ca3782c", + "docs/audits/pr126_authority_delta_review_20260922.md": "b8405f5c4462966939d680632301ea0aa3587b42b549a12bad0dd142a540787c", + "docs/audits/pr129_tiff_compression_review_20260924.md": "821a06dec8355362946f0ed91cfc81e42287b1153935d205218ef4e140098961", + "docs/audits/project_surface.json": "5267d115faa41528aaa31ce5cdfd40b4c6abfbeaebf62b79fc6ffcc77333ab86", + "docs/audits/runtime_axis_ownership_review_20260923.md": "7cb53ae20886d412a426113c2fff7a648b7ece95f409d2feb4c5115ad697429c", + "docs/audits/source_object_domain_boundary_428_20261002.md": "5f1dd0abfdaf629e8029ee50a9bc55e8409f619b4ef0625c61461f5cf7356564", + "docs/audits/user_docs.json": "719d0f96842efde3d66042b4962afc98ac566b5159087ff249a9640afacde98b", + "docs/audits/well_throughput_cli_review_20260923.md": "aa659e13bdc3e09c0e16d656d43ff57f89e98e14e58d848c4703bd3589268a27", + "docs/audits/windows_source_path_review_20260923.md": "e3e97bc877cdb0122e98368756bb77e4a15896f9a15483e6cc241e071c57804c", + "docs/cellprofiler_backend_optimization_tracker.md": "2c76b17dd668f4d1993fb6e9edf1adfa7dc3b57d0f4f27671323d7a7bff847be", + "docs/cellprofiler_cppipe_parity_tracker.md": "abd1345c76a0fcb966dac1dc4e249bb9dd219739fe95dd27ae70bba4af52376b", + "docs/cellprofiler_dataset_acquisition.md": "4ff4f204bdbe0a60272b0f4df32e1fbc8eb524f85a930c50aa8771b51ae3647e", + "docs/code_complexity_analysis.md": "c47bb7cc241d25ce5543707da851d97d33ac46c5781d24a05674e3f33bcbe6a5", + "docs/development/CANONICAL-CUSTOM-SOURCE-379-20261001.rst": "908af78872abb85ad1ed2c8928ff0b185f00f54d4c5fad7c5cad1ab3d3a52772", + "docs/development/colocalization_metric_selection_487.md": "c58aa4cb4f902efb50ae5d695511df37ec9bc19d8b17dc9452090eb339f409ab", + "docs/development/colocalization_preparation_491.md": "4de9ff456384c75e44b89f644c362faac41708bd29f8e8fa4e555a3978b095f1", + "docs/development/cpa_workspace_export_480.md": "aa4bc6b49151b9eb200459cf32f9391b076a2fed5314308aab0091d15e9f59ca", + "docs/development_setup.md": "3b524c15a49a9c0f5e5816061a9298def00f72e4a2783a14457ff140e55baf9e", + "docs/lab_meeting_cellprofiler_openhcs_slides.md": "2b56514e08d80a1f6c00eb5de173ead90029673427ed69ce64d55b6399a5eb0c", + "docs/openhcs_cellprofiler_lab_meeting.pptx": "e7db5a7375a708f458e141e26e161fcc4de7dbeb2b70d175665d9b25ca9b7c9c", + "docs/pics/cellprofilershuffle2_1.png": "22766756ee8fda75caa8e485e05e04170338a2a7a95c350b4e14ef84347fe852", + "docs/pics/cp_logo.png": "b4a2c73f83d00e37ee772215168258a7b4a45af54d5b15e69291b30d1a800f1f", + "docs/pics/cp_logo_text.png": "089e089ac4200c57cc06b90f1231a467ae0f51467e414ece8d923b08df0d2acf", + "docs/pics/examples/clones1colorneighbors_1.jpg": "fd9ae59612601b82c8b4d03b203010df0cb5ad8aa399a4aa8f46298f95694b04", + "docs/pics/examples/examplecolocalization_masked_1.png": "e9f1081df7c90a982e62f003b85d81d2cc03114892d6829d6d8f6e0afcbbb652", + "docs/pics/examples/examplecolocalization_orig_1.png": "e9bb26162e3e33c08f88c38561071861764a5f20eed159d44e7979f4276009fa", + "docs/pics/examples/examplecometid_4.png": "de009c6800c298d29056e3dd270dbcd316ff554e866372c3ae8941b7959b2c99", + "docs/pics/examples/examplecometimg_1.png": "f3b429e907a38ef30d4b85473fe02342044cf500b51fa5eb077d6f8e4c75bbee", + "docs/pics/examples/exampleilluminationcorrection_corr_1.png": "e3db3f2847fbbcdfed52b994bcbce8f599ac6b89aaf3a45b3119bf512e6395b1", + "docs/pics/examples/exampleilluminationcorrection_orig_1.png": "f4b63bb13ec6dee8e9f3c46f2aae5a0334d932152a1e7c7ff6545cecd00e31a4", + "docs/pics/examples/examplepercentposid_1.png": "e7c80215d6e63fa52f898dff31be31379d6e29a2608e5df4a01a5fd372ad9d6a", + "docs/pics/examples/examplepercentposimg_1.png": "f99bc40457f792ce3532b4bdb0a7a0e590b204cdaf2dbe3382cbcba099b5878e", + "docs/pics/examples/exampletumorid_1.png": "b956fed52f569e1eec5e3c839fc4a35e4e6cfcd379a36411e112ffd1bc060e31", + "docs/pics/examples/exampletumorimg_5.png": "f1a62dfc9c6e8d034034f267970944f1c2d8329f21ecddaf8cd5914020af5635", + "docs/pics/examples/examplevitraid_1.png": "ef300b99a943b91dc8cd68fdc100a57277de84383343452e3d7ad831d7b27847", + "docs/pics/examples/examplevitraimg_1.png": "fa4d657a4d1aea618e3808548f24dc96aea8b439138c172d6204ac676ec036ee", + "docs/pics/examples/exampleyeastpatch_1.png": "8dc08c4a22264bf6722f5c37eb98dbbb499a86d91b41744c02bed39e47426a1c", + "docs/pics/examples/exampleyeastpatchid_1.png": "95dcf058c54526cda79c4cde415ab19cceb28d2f29d88496f1961bc209ba52fb", + "docs/pics/examples/fruitfly-coded_1.jpg": "3bae3c9479b90f9bdc0ec540326494c95fdcec8ab3fceb8ebb31a726292b44d0", + "docs/pics/examples/fruitflyimg_1.jpg": "9ea6810589dd52a0d750ed903d63959a6d47dadbdc40bfe3263c6d9a84ce6e7f", + "docs/pics/examples/humancolornucleism_2.jpg": "84482408947faae3271184ada529044177bec1b8467d0cc21d41f74a5f2cb938", + "docs/pics/examples/humannuclei_3.jpg": "f61945fc5eb0dd205ee774e5ad9430cc15314ca9012cfb24a03a303df141b8cf", + "docs/pics/examples/imaging_flow_1_1.jpg": "f96f60c7eba45b8157b418e490b3b2915bf7b753c4b2978d4ca9da822d4f347f", + "docs/pics/examples/imaging_flow_2_1.jpg": "4ef0492e8025e407785be0b1dcda4a9775d4f5ce2780912cb3e0626b598d69bc", + "docs/pics/examples/specklesab_1.gif": "8e6b1ac574fb9dc72a15a0cf632cc4716da53be012bcd579db7222402304afb2", + "docs/pics/examples/specklescd_1.gif": "b4490d876040251af57d02354581717a141fee94c24af9c847b2c9a6b2857a58", + "docs/pics/examples/tissueneighbors_1.jpg": "570bac16fc77693064333653cba2b64be2b1b1b17c11d5fbf71841ec7b0e9a5f", + "docs/pics/examples/trackingexampleid_1.gif": "d872ef1c1ec95f9e2a5795b8bc5c93610a69f362c6a1baf346eaf60ae8af427e", + "docs/pics/examples/trackingexampleimg_1.gif": "a0f02997b15d6cdaedcb741099a476a0ddfc658bee444662fb597b864897cfc9", + "docs/pics/examples/worms_untangle_1.gif": "f91acb85e01aac27262cacd62a38af25a574a739e0e6fea95c6fb2807493ad5d", + "docs/pics/examples/worms_untangle_output_1.gif": "979d10e6c0a997d41208019309fe6bd196ad5ec2a0238974e171d456e2f2519e", + "docs/pics/examples/wound-analysis_1.gif": "ffbcd5576fb23d0ada2be70c4d4d02b271b5f8004da852795bc8b12a184053e5", + "docs/pics/examples/wound-img_1.gif": "d0cc8cd65635ac0067b813b11e36d8fdc5e318a7b306f1467b5eb3b3b234ea3a", + "docs/pics/examples/yeastcrop_1.jpg": "6a91a65eb83590d21c4146f2061755571b641876b821d98e5ddeaad0cc6e3fbc", + "docs/pics/examples/yeastcropid_1.jpg": "cc41c154778d4b995d9682dd6cc07a2af3ab9edfb83bddd480b9a0d1b11257c8", + "docs/pics/fiji_logo.png": "93161bc041d929a74427abd2554ef208439733d260d7de8ee0f13bb9ef34ea90", + "docs/pics/images": "d18be4b579b6407304c1ad6ff4011eb3cada6429a9693fc45f35cb7c6442e206", + "docs/pics/napari_logo-300x300-1.png": "c9b448a1c2d580be6f9f08b6d29743b518d263a928af2eb138f010e14c341758", + "docs/pics/omero.svg": "313efbf2fbe9418a9a6e6afcbc21d428889191f14338c19be5a3a83343984722", + "docs/pics/omero_logo.png": "edd5100aab0e5ab431cc2966fdd83b83649347becd8dc5a5f605577cfc8edb51", + "docs/plans/active_non_tui_cleanup_batch_20260518.md": "7805a34ad2b532c46d9d44de85475f88e69c7be0e44796e6eaf07fe1c30d78e9", + "docs/plans/active_pyqt_residual_decomposition_20260518.md": "bc78043fbc7f840e205c478cc61ad6d08c9e2c56269aac61a1571900269b9b5b", + "docs/plans/advisor_guided_adjacent_refactor_overview_20260516.md": "5b61888a20977b9cfb0040217c178f6a3bc12c53bfd225acb3f7aaea40bed202", + "docs/plans/advisor_known_noise.md": "9bc656d255c8a5b25c35d1726fa342a42701ddf515c4844ad5b3b75842e33f81", + "docs/plans/advisor_noise_calibration_20260518.md": "2773cbd2a4f6a3437f05683872032bc6f9997ac0010bc4d300d6763bf4658ad7", + "docs/plans/architectural_debt_refactor_sequence_20260515.md": "679c5935fb1d61ee1f57a40ec3198e8b905bfb52eefd5e4e52b77fc94996ba6b", + "docs/plans/artifact_output_lineage_contract_plan_20260701.md": "9e294b7d9f6bec60fc4cc962fee54ad2a731b801f09ff627b7aad00eed58c438", + "docs/plans/artifact_plan_write_authority_20260929.md": "90de19f98ab1ff217ea1bfb6e3b025265976fd3c96abc728e273715e45376f31", + "docs/plans/backend_dimensional_dispatch_authority_20260518.md": "64799c79e7e814ad4a3617484609e01fca40f7bf55d5bda85b40836ba1e44b28", + "docs/plans/backend_parameter_request_records_refactor_20260518.md": "596bf765654ddbf559692af39b37d56632a2869280158fa82c6322bf57d3e1db", + "docs/plans/benchmark_api_style_refactor_20260630.md": "59c2526819f6e2b738e7f4bf38509d961f59e3373af4987dff23e7e1752524f3", + "docs/plans/benchmark_mcp_control_surface_20260916.md": "5c3db4271016820c296b24fb3600134518fc0d53a7a7117bc121f3f41711910a", + "docs/plans/benchmark_pipeline_wrapper_20260922.md": "e225c040d2f38a23500134e85d8ae25cb5b1cdb4509642fc3d6be79ab6747c8e", + "docs/plans/benchmark_remaining_cleanup_plan_20260701.md": "9e977aa61f6b4b26a0d8613a3f9a305101ac7cd4b86c94cbba0b869c0b24ab3b", + "docs/plans/bioformats_microscope_handler_20260518.md": "a3b08257f7454cf8f69d0ce2d7088d8862670219ef00900e465422fe0d1cdb81", + "docs/plans/callable_reference_lineage_checkpoint_20260930.md": "f389ccc2ab89cc2fa6f4448e2d49134f6f8044e8d2554fbfc43ebf3cc73a930d", + "docs/plans/callable_request_binding_refactor_20260518.md": "fc231fe45dbf3a88b535bf2a2bc046c17e81107274d18fd0ada8147aa51f1aad", + "docs/plans/cellprofiler_analyst_export_integration_20260611.md": "42930bd400a152c133cfaeea56889d0982b24c3ad66895809746489abc805c95", + "docs/plans/cellprofiler_artifact_contract_ssot_refactor_20260520.md": "9f9a90415b24db0c528a8c151d619fba811908329ed8d115614cbf6011abf5fd", + "docs/plans/cellprofiler_backend_authority_cleanup_20260518.md": "8cdfd1484bbac85b0dd2b17c1c2dcbc0083614da6bdbffdc5946c598384e57f1", + "docs/plans/cellprofiler_binding_context_cleanup_20260518.md": "3a2e1ca4a345b3698922b249d6c14d48169d65a6f6238cf569999316ec9b88bb", + "docs/plans/cellprofiler_bioformats_pyqt_binding_ui_20260526.md": "224d270d817e5227ed2fffdd66a3b8de9a3a75e993d6c26d4ce2119545f74562", + "docs/plans/cellprofiler_bioformats_workspace_binding_20260525.md": "eca55b2e5a9e9edef0dcf1c474e6fed897a96d6f4534e56be78cd0e4e56cd279", + "docs/plans/cellprofiler_boilerplate_elimination.md": "4da1e4ad9e55525396da40666394c6639fb19dd453104c9710bd74c3fd40bcf1", + "docs/plans/cellprofiler_community_cppipe_corpus_acquisition_20260719.md": "f621b0d814495019a69a9689162e000ebc9547ec383ae2c75b0bea3a804f4bd5", + "docs/plans/cellprofiler_cross_domain_skeleton_refactor_20260517.md": "9462ad752dd01d024e8952ea5c00d5899e100dbec83d3ff24efa6b690dc35f10", + "docs/plans/cellprofiler_declaration_extraction_shadow_plan_20260626.md": "573e9ed4b0172c5a0e1231a75caa1f888be1c47c23adcfa6cda9d20330c8d431", + "docs/plans/cellprofiler_function_name_variant_registry_20260517.md": "22de8cfa95af85b1e00372aa6c63b0dcd252be43d0041f88dde690dcbc050720", + "docs/plans/cellprofiler_generator_declaration_query_refactor_20260624.md": "0f8f16ca0462e17eec61f279c77acec253d0f6512913a5ee9d9c31e2013be19b", + "docs/plans/cellprofiler_gui_source_bindings.md": "4344005ccb49b1ae4747f4bdda21f0b71f9d159177fc51cf9e615dedfdf04877", + "docs/plans/cellprofiler_like_debug_mode.md": "e2fd45ac437e12a17e7fccf3801b892e70ae5bbf5cf56164d2272da9091e745f", + "docs/plans/cellprofiler_matcher_fanout_refactor_20260517.md": "2939883224d93393888263933aead5c7089afe49c40aa6bf986f7dac337f589a", + "docs/plans/cellprofiler_measurement_semantic_authority_audit_20260703.md": "bd95bb188e1b3de1d2a8685e3364391ed403840d72718dbad8fcc9fbd51f3f40", + "docs/plans/cellprofiler_naming_opt_in_plan_20260718.md": "11408284e3420164cf57c3247491f582126a0b8f9907525dc87c5433039a2fc7", + "docs/plans/cellprofiler_production_boilerplate_inventory_20260626.md": "396f56767f28c6fe8bd855a9f5263aeb40fe417efcccec65ffbb5890e62cc5b1", + "docs/plans/cellprofiler_runtime_boundary_decomposition_20260516.md": "b2081a153297a0cebdf811a2e1e1d952748cb2dbfc7f56a0f6aa3eca4ea67e64", + "docs/plans/cellprofiler_runtime_declaration_semantics_inventory_20260626.md": "eceee6733b34e5f545a060fa2354f91411231085a27f5794c80d6945eb68e1fd", + "docs/plans/cellprofiler_runtime_deep_refactor_remaining_findings.md": "867a10d0a1d082ca242fcee4251534f7eddf1118a93f419a7de3199edeb51d89", + "docs/plans/cellprofiler_runtime_semantic_authority_refactor_20260524.md": "3fe4d9edeb3ed482c28d7cc1b9daddcffb18fd76421df585423d0b3959e96a43", + "docs/plans/cellprofiler_save_images_signature_family_20260517.md": "ffd278253618cb90c07f20bec1b4dceaa373e900787e9e08d4b1c38a41b16074", + "docs/plans/cellprofiler_source_schema_plate_ingestion_20260519.md": "9cbc39f5c812127618001a23cf3b924b5caf26e7e43c2d8f5a81f9b6de9c3995", + "docs/plans/cellprofiler_symbol_table_usage_inventory_20260626.md": "3a688684579dcc4632ec9f3ed086ecfbb33afd5e6dc9ede7b0a0e8f5d93dfd36", + "docs/plans/centrosome_watershed_checkpoint_20260929.md": "6bb66cf354f40efe20233bc32d8e1f2202a96686c5cc70855e1179f209367230", + "docs/plans/centrosome_watershed_issue_20260929.md": "f69a09a8e9e01090a494548d4bc820b2b65ea0c3db8b0e0b9a9b54b7c15d10bc", + "docs/plans/code_mode_paths_and_artifact_tab_plan_20260719.md": "b8058701f1fd320a1f1738e3faef7a282fc009eeac32373edfddd963670ee7db", + "docs/plans/cp_runtime_compatibility_deletion_parity_20260517.md": "032f97808965ba8817cab7872075b7bac969f609121cc9be93c9760e8ecd8561", + "docs/plans/custom_registration_205_coordination.md": "f67fa02b59d93862f056f8d800d67deaff2e724d24ef30bcd35b37e2620fc625", + "docs/plans/custom_registration_admission_20260929.md": "edd93cc6cc106dd8ab80d77203da6a44f199379e0a4f1031c7a7ddd9c3b9fc94", + "docs/plans/custom_registration_source_live_handoff_20260929.md": "25536201923c419eec855d43502afc612d1792821486ec25f84578d9c984ea02", + "docs/plans/custom_source_inspection_20260928.md": "d063f34b60bce65b8743f2305e33ea5557541c1cf6c1d6902290fb5e7c1529aa", + "docs/plans/czi_source_bindings_and_zarr_audit_20260719.md": "ee6ba0ff204669e8b61d6390f138aa158921ad984dfa26e162d607624da42ac7", + "docs/plans/debug_gui_e2e_workflow_tests_20260517.md": "90a915d522a969347805863e8fbde36b07b620f78844c15aaaef8a0f08ec984a", + "docs/plans/debug_mode_gui_worker_integration.md": "ccd33254aa20b86c84fd2e6c62bdfe7f9f0c33df5e876788dc53c2139945b9f7", + "docs/plans/debug_source_binding_followthrough_20260516.md": "4aa3643f98d098e1dbeaa1e2cb7e61d47014586858b63f6e40c41b5956168e02", + "docs/plans/debugger_ide_workbench_plan_20260701.md": "e2a7e49e436eb0020644cede49b6749bd378b49a73ab9d01ae9561b1f066d018", + "docs/plans/debugger_runtime_projection_api_plan_20260701.md": "20a077c49a216acd6a1f9b4e7a828b52c792e0a71eccced0ee3157b98e66ec8b", + "docs/plans/debugger_session_ux_dynamic_menu_plan_20260630.md": "60454e46123644675b03aa4fd901d0736f2d2c141eea6bf434e6b44626342400", + "docs/plans/debugger_session_ux_rework_plan_20260630.md": "13ff039204842e719b1f657525447bf8b75936f10b6215d4dc681c07a0240f1d", + "docs/plans/documentation_overhaul_disposition_20260717.md": "aba9ed6f1de8f6eb2209b790934d89601d9b78c8fefc4407b3637133862727c2", + "docs/plans/domain_extraction/.gitignore": "6a37da5ac2a5ffa7214f044ac64ea315fe20d6f71d6522e6603b85c2e2502479", + "docs/plans/domain_extraction/README.md": "aa551eefa64b0f97497ed3dede5779c2248a4449a86dab6b4292debc51b4cab5", + "docs/plans/domain_extraction/component_inventory.md": "af5e7892f94f8268846d15d51cb2ca55aa7e585c99203d13bce51a33beb46dca", + "docs/plans/domain_extraction/nra_leverage.md": "a870181033f891811bfe6a0a0cc94f1d9f4dd610cf77da4f3b73fc82dfe46b4f", + "docs/plans/domain_extraction/nra_runtime_artifact_location_extraction.json": "b32fb0a3158203d308e8ba348da0336a27e598368d530b20e953620bce35ecc3", + "docs/plans/domain_extraction/nra_scan_evidence.md": "b56382df5dc70b9b9cdfc5411c6ab221adb7be84439b72d4d444880c1666e930", + "docs/plans/domain_extraction/nra_widget_action_models_extraction.json": "c623d4723ed5802dcb9d6019514eb0debe55f905547ed79af319fb86a49f5479", + "docs/plans/domain_extraction/ui_streaming_inventory.md": "555a201eaa98e0a4f38dcc14fb50c7bf10cba8c760fb79ecd7ee6238812484e8", + "docs/plans/dual_editor_config_pyqtreactive_refactor_20260526.md": "9b61fcf197624646942112315a96dc255b5267d73a70ede2dc480c9aa70b4b7f", + "docs/plans/filter_objects_special_input_contract_fix_20260719.md": "bb1a03c1219acad7a91f6393e64337e0858afb2a60af03c2057a7bde6bb55bb9", + "docs/plans/full_repo_advisor_campaign_index_20260518.md": "82d0a3cc9550fb9480787567c173af12bbaeb6b321297f143d08f210a3979eec", + "docs/plans/full_repo_advisor_triage_policy_20260518.md": "94c673d41bd02ce9a2c4dc832613d6297eb2951834983c5a052f3603df1f846c", + "docs/plans/full_repo_orchestration_hubs_refactor_20260518.md": "dc87376e84e29373bc3a1b7c968aff25a968967736d677885f17a49a68fe1bb6", + "docs/plans/full_repo_refactor_master_tracker_20260518.md": "72e914c46ebd77bbb995a1d73e4adf9f07b8343d0a2d877eec17c83204915130", + "docs/plans/global_ui_zmq_config_tabs_plan_20260719.md": "2a48fb164a156c66d710471e5d5118a5a37cc522afd42322ba05fe350d2b355e", + "docs/plans/gui_widget_decomposition_20260517.md": "d15e61f6676c39fe014d69cc0e3e8ce77b414145b716126bee9ae483ce38f6f4", + "docs/plans/h002_query_preparation_diagnosis_20260929.md": "0e0c39ab8878884e79434385b9412c2763677af3b5ee6a2d7c49c569360484b5", + "docs/plans/imagej_cache_progress_checkpoint_20260929.md": "fbf82d8a777df09a203c52d72c8a9a679129f15117a839554bad022fedb30759", + "docs/plans/input_preparation_checkpoint_20260929.md": "794058fade714177303e17ed6d212fc414828242439a036b08719bcc9bb171ff", + "docs/plans/issue133_204_measurement_contract_checkpoint_20260929.md": "ac069c0663b747253e12068bd0dc127795c062a3444c12b7ed26c920a8279906", + "docs/plans/issue135_viewer_bind_implementation_20260929.md": "468632176c9f43b08f43121528e61523344c3008ee6bcdd219ee42b93df004be", + "docs/plans/issue135_viewer_bind_receipt_20260929.md": "74b765fc676593c6dd02bd7372288fcf69a7a8030c439cec81d2d33f60a319fe", + "docs/plans/issue204_catalog_diagnosis_20260929.md": "9ae3d78efef24187aded07c791645decb3a233bcb52dad45dfa13715432fc961", + "docs/plans/issue265_main_flow_lineage_checkpoint_20260930.md": "da15d4acc12eb24bf203dc8968a205368a530bbee37d116a20e92fbb66e36a0c", + "docs/plans/live_measurement_table_20260526.md": "f0e7f0ba02e9bf192398c78608353a19ebfa202c524b16e792b7cad31f8b4327", + "docs/plans/mcp_agent_experience_implementation_dry_run_20260629.md": "8d81b2f6dce348fe5ca7d8838b94c6267ea8d0c6dc1fd0cdc8976b3d49223fef", + "docs/plans/mcp_agent_experience_plan_set_20260629.md": "650ce7677ce387671030e4ac72f143b099002845aa2a2b0a3ce20ab8a9c7c229", + "docs/plans/mcp_agent_service_projection_refactor_20260629.md": "7bfbb90903134b9f3c8adcba00643c4e860f31d3d5e5abdbfc1e177a12d72c37", + "docs/plans/mcp_artifact_source_coverage_plan_20260629.md": "3c4ab4cea73f5b6fafb5a75c5bf49ecdc4bb42a42347fbc074824671deb2ce56", + "docs/plans/mcp_beginner_folder_workflow_plan_20260629.md": "5cd0b68b7e094e5917480e3e7810551343a8306180873fd841a162f8e1c35467", + "docs/plans/mcp_capability_tool_binding_refactor_20260629.md": "37a3827e82b2149079abc10e729af1e9739783d596dd0a4b6c463ddda4339f0c", + "docs/plans/mcp_cellprofiler_example_search_plan_20260629.md": "3137ca8e180dd76c28c9402b179c2112b5e5608a6789b6da272dcb38d9190e55", + "docs/plans/mcp_dev_client_ergonomics_plan_20260629.md": "d5ef694a0f501f9bfcbbe9c8dc138ccd3d021601f83d515d15e8d517e59c86c5", + "docs/plans/mcp_dev_client_schema_derivation_refactor_20260629.md": "2a75d87484062c3644dc13f79155675c44d70fa0e208ead68f576f4a2cb55ed4", + "docs/plans/mcp_frontdoor_core_model_plan_20260629.md": "7f78dab8a80e8230ceb242e9ef16ca585beab40fda156ee03a06fcbafd92ae65", + "docs/plans/mcp_function_contract_axis_semantics_plan_20260629.md": "70a31662a563ca740d4ee3402e4aa3e1f3a54e30d99ba02862ae7265a7af1146", + "docs/plans/mcp_operating_guidance_validation_20260909.md": "a8bf78e21856e70b073d5daee9f055fc4172d9590c9e72c5f1da3f833820d7fd", + "docs/plans/mcp_plate_viewer_projection_refactor_20260629.md": "836dd519a297647e61ae2bb2b28cb838b2a5cf8a7597575507841cbd97009505", + "docs/plans/mcp_public_python_api_projection_plan_20260629.md": "5093fb681089ed77fcd663ea8f11ade514eee8edbdd71c90d3ea89c52254e1c3", + "docs/plans/mcp_release_readiness_20260720.md": "de2de90864f6f035e2eba0fe244589036d62f5b08756c2d0b3458d78670d8946", + "docs/plans/mcp_runtime_sidecar_projection_plan_20260629.md": "dbe33e8074a2dbf561641e213bd19d85b51713260f34b8966af107a036d9fe3d", + "docs/plans/mcp_semantic_authority_audit_20260629.md": "44e738f678eaf5df0281ac0ccab9cfe75ce96c2a0675c309a1abff125e1792a2", + "docs/plans/mcp_source_binding_authoring_guidance_plan_20260629.md": "af3949bbb11f875ae1f38d5b3862b1490bfb120920c1fe7ef6d53cbbc8267207", + "docs/plans/mcp_source_model_projection_plan_20260629.md": "75a79d80d3dd07e69f2f7c87feb5840b83890c97bffe5e57aface4c64edd1f9c", + "docs/plans/mcp_thesis_demo_checklist_20260708.md": "83e04389297a19811fe32076b4a5a7855514ce1ca6d30bf6b3073f73794840d4", + "docs/plans/mcp_tool_surface_grouping_plan_20260629.md": "a78341dfd806d0e40e236101ff4f0ac5cae6011da9c7d7223bf9346c821f42ee", + "docs/plans/mcp_ui_bridge_authority_refactor_20260629.md": "a02618a14dacf625b0cd18364f7b9aaaca9ad5d338799635eb62bf3d836ff1da", + "docs/plans/mcp_ui_current_handoff.md": "a978e3b15c65524d410fec205ee77865a5b908e1e415886ad9d54484770e7af6", + "docs/plans/mcp_vfs_storage_model_plan_20260629.md": "674c1a152303b03b329fd6eb34cd0181561dcbdc6773beb494e264d28b75060e", + "docs/plans/measure_object_intensity_selection_retention_fix_20260719.md": "191cb59fe3bae3508c35a13617bcc6b13c956acb8014cdba89a897df702c5331", + "docs/plans/measurement_group_scope_triage_20260929.md": "21aa1fdfebb2a31c6a16e747078e7f91800b159b11c08660d699bbd5defe7844", + "docs/plans/napari_layer_identity_display_modes_20260611.md": "7e17f5f8667e755d3cd6e2d916912462611b0b13856db7e4431525feb9f55d0b", + "docs/plans/napari_streaming_handler_axis_refactor_20260518.md": "112bd4016dd2f3546b04e15050c0251443e6c47a8672697a1109b9d32fa17325", + "docs/plans/napari_viewer_navigation_authority_20260916.dsl": "ed96623c30a18ae856ad87f580c3313d4e2313a99e3480788bc20cfee757f1d0", + "docs/plans/napari_viewer_server_refactor_20260916.md": "8f0bc4b1c656e2928976d58164c7b5064cc44c4ae40a67f56fcca73072cf249d", + "docs/plans/objectstate_granular_table_semantics_plan.md": "7676177f6bf9021257f4491c4688ec04ee81c73505b1501e438dd60ab6c44b70", + "docs/plans/official30_current_failure_classification_20260719.md": "69b89994e93ae9a52c23938a2fe88cb79cb9ee77e8e53d5fd78c79d3c388b1b2", + "docs/plans/open_issue_surface_index_20260929.md": "bc9d24a5a0a693de9fb8dcc37c4b5e6363539648c9f88c02f65963e8a90e40ba", + "docs/plans/openhcs_mcp_agent_knowledge_base_20260625.md": "7c4b546fafe4d88137e7ace235f925fa8a1083e055ca6544092d8ffd08b7c801", + "docs/plans/openhcs_mcp_api_exposition_investigation_20260616.md": "7c32408f0543eca22cb8e9cfb9f84f5e532b2125fb8d9822f91f990548f24195", + "docs/plans/openhcs_mcp_implementation_blueprint_20260616.md": "4f4ec9c530bbd6c66ad748e6f08aa1d073931b3748fd29e1623a2b377020976a", + "docs/plans/openhcs_mcp_server_plan_20260616.md": "e7ca8942b97b51875811f968dbd138ecb6dbb7db1a3edd9a57b54762068360fc", + "docs/plans/openhcs_mcp_ui_capability_registry_20260617.md": "e02198a285ccd1fbe821f9260559b4773ef4558617b84b5e242df2f2309a3833", + "docs/plans/openhcs_mcp_ui_code_document_bridge_20260617.md": "f7f7341ab0fefe779d8aeb48d76a8c8b5c2d8c3ee30bd99d145621a6e5f2b748", + "docs/plans/orchestrator_stage_split_continuation_20260518.md": "24950935d165662b5f3863f616813baa332f6a2e81b319d406325bbc8d10dc15", + "docs/plans/plan_completion_audit_20260517.md": "127ae7d3c5921ee2780ab166bd198f3f52d98db89bbe27a5db18b4c304712c91", + "docs/plans/preset_pipeline_spec_authority_refactor_20260518.md": "62b31ad5e6db3facdc44d336fdb1c90e2e3b6f0334f5c2096d50bbb58b18c187", + "docs/plans/primary_segmentation_diagnostics_checkpoint_20260929.md": "58442a82f78fde89b99852907b15e7e9aed06d7f5ae68c98db951bae0ba4a081", + "docs/plans/public_api_export_surface_authority_20260518.md": "68e12db08586fcf726440d7267507ad394a5acb7ad824f92b95690d92d59a41d", + "docs/plans/public_internal_boundary_audit_20260518.md": "34bdcb19fa61862402d046025100d92177261d72feadcf21752c4f4aad030f47", + "docs/plans/pyqt_gui_decomposition_refactor_20260518.md": "2fe29afc25593ed89a853d1557d3729485a5e8c5b8039331493617a404c99bbc", + "docs/plans/registry_key_declaration_refactor_20260516.md": "20e190b8ac7e061e0913a6c5825c854efc8629d0c1170b406a853d10241bc72f", + "docs/plans/remaining_advisor_campaign_master_20260518.md": "ee376bcd84ba7f4c4ec4f00622055ea4fe618cd14cc90816a795cd5fcafb1ab8", + "docs/plans/remaining_backend_dispatch_projection_20260518.md": "ede587cfa9c49526c9f74d968b0d9edca134e660fca2bfc8b191a378571e2a41", + "docs/plans/remaining_cellprofiler_backend_authority_20260518.md": "15f3c4bec040b0ae889098b87f4267964a391b444ae81df1f8b316a8d2545fb8", + "docs/plans/remaining_cleanup_noise_calibration_20260518.md": "577e168c3b10950249c41f5710a50df13c8eaf049bf3b9454d280b8b817ff8d7", + "docs/plans/remaining_debt_master_tracker_20260518.md": "36a5efb6e96eda88fe0c398521f76e7c46ef343f4c92d37d1a424c7e095e9325", + "docs/plans/remaining_format_microscope_authority_20260518.md": "b85f883264786654d8b5ebf882f405f739861d0c20985afc65c827425b91d72d", + "docs/plans/remaining_gui_runtime_authority_20260518.md": "3f6b59ad560ac8b676d5b68cb108d4e843b1299c10c7884a5920b2bd01daf1de", + "docs/plans/remaining_public_registry_export_authority_20260518.md": "772fcc91c7ea5278c8ba3108871ca9ccf753538dab8cfafd36cd3db971b087d3", + "docs/plans/robust_streaming_refactor_20260618.md": "f6147f482fbab33e075491bc43d587f3eb350409e3cdca2c24748d0b2373d6a1", + "docs/plans/runtime_artifact_query_axis_refactor_20260518.md": "417d5c35f5e47d84680ca1a6d8ea39432c9dc1b367fd418a28454c0ed90a2c96", + "docs/plans/runtime_artifact_semantics_consolidation_20260516.md": "dffb731602ff85346f447e01d9b86141f3b949d6f5cce3d92447c421aef8308b", + "docs/plans/runtime_plumbing_frontier_20261003.rst": "be801060337c637750cdd766711bee630113c122e5dd6b0898715b810bbd57ef", + "docs/plans/runtime_viewer_protocol_cleanup_20260518.md": "49ad080fc66cbd99fcfc48c9ad7afcbf78ce377cfa6e90a8d7057d24b0ebb70e", + "docs/plans/source_assignment_model_cleanup_20260517.md": "b23a6c908aa890f1f94d6e6c709b6c9438ff615524a0b41dba978175c8fbe02d", + "docs/plans/source_binding_activation_boundary_plan_20260701.md": "b5aa96c12fefc9b281477571615c97cccd719ad849c2ac7ae059b5930e358951", + "docs/plans/source_binding_context_architecture_refactor_20260526.md": "d857b966896db1acbd446ec282abdc9609c69d0ec23787201ef6b3b5b94175f5", + "docs/plans/source_bindings_microscope_handler_20260626.md": "3e920e59a734ad107109da34d2313d43895234a9e27d829a3e9b01dbe435056e", + "docs/plans/source_projection_cross_system_remediation_20260527.md": "e5c69706c0cd76b44d72aa6b6e6fcb9b7f169836e7c9e1a3ae4e4ed03436f9bf", + "docs/plans/source_projection_overhaul_20260527.md": "b4176123d61a1016d419c8e497c4e273a0b502bf6c7ad122af5f34547f89e625", + "docs/plans/source_projection_overhaul_code_audit_20260527.md": "4c640f24c7d86862241fa8a0cd0df18face920c553a0f1a289395ec551acad12", + "docs/plans/typed-3d-point-result-20260929.md": "f5b4de3553d12544dce4b9ab6bb066177a29b7763ab22da12dc06f9166feff51", + "docs/plans/validation_registry_family_refactor_20260518.md": "966703a39700dc4e6b4da8234b633793c3e2117cd3172d8a511fa55f63cf9df2", + "docs/plans/yeast_colonies_source_context_identity_fix_20260719.md": "48fb2ea4b19842901a02e5d68945766ea1e79fec70449b964c4bd3cdcda9c5fc", + "docs/plans/zmq_server_decomposition_20260517.md": "96522ecb9779962661e458795496be97ba76e0af1262a9a379f9cd4b0d95a150", + "docs/refactor/00-RULES.md": "8da82e8348d9f3620558fe75f249380f965cd0367fb99d83c6f440c0b21f5281", + "docs/refactor/01-INDEX.md": "8427c3c0313e1f312950582bb3b0f30090c69bbf560b117aa0f978ac602b4162", + "docs/refactor/344-engineering-acceptance.rst": "0df290b3cb92862b5aba67a27f45558d0d6f10cd53504f9fffab6f6378a9c296", + "docs/refactor/344-installed-native-acceptance.rst": "f798862aa6f3bcef1540c027dc289752b143ddbc8a6e4dc3b0fdf6e9027a5916", + "docs/refactor/345-installed-generated-input-acceptance.rst": "25bfa4666621477855b0cf2bd2c314f3664e81c03e9bf9c3ad82c2425600b143", + "docs/refactor/Canonical-dev-client-decoder-400.rst": "1b6af739b82a65a5e661c75809801e9d2874b8c45700fb47c637939325c1c520", + "docs/refactor/Classification-authoring-runtime-380-381.rst": "5e5c53ebacb1bcd09465f2ac991e0c4c6f6e0af52a550e9b6d4be57fc0ede3da", + "docs/refactor/Classification-measurement-subject-370.rst": "3aba78be9e0c5662c76522c0730d347320a5f41c9960cdb020c24ac5c281149d", + "docs/refactor/Classification-two-vector-authoring-386.rst": "37a7497a8c9b429402fc5bca2964bbd5c042e3307ccc0f392a6ab743e75872fd", + "docs/refactor/L0-experimental-cli-owner.rst": "77adf11dc6623116d38fc01ffe03a01c70fa4336db9dc4b7b4eb7d951dbe58da", + "docs/refactor/L0-pattern-resolver.rst": "c066c5bb9d618a3c591c04d26bb328c656d9e5cd7ca0b719dc8d7dc64e91c223", + "docs/refactor/MCP-authoring-roundtrip-360.rst": "b4fae4f0d03c567d138bd2154ff923bb175e1569ae3621de2953bdc508c159f5", + "docs/refactor/MCP-compile-selector-authoring-360.rst": "e3255ebfb9064713543652192359744a0ed120ce22b14c67a2ef6c3c2126078f", + "docs/refactor/OWNER-OVERRIDES.md": "c7fc3abfa8a3b2912bd443fce0fe110c717791bb5a6793d0c265f01cce1e9d7c", + "docs/refactor/R0-checkpoint-20260930.md": "239d5cdda154b8ad4a7a0a9df7ddfb8f6600c00d39f10031de1fcbc7e72f43c9", + "docs/refactor/R0-empty-report-scope.rst": "439664a890993ad20b28296379f3c119923eeef3e85b3e16c5fd88f5733ac950", + "docs/refactor/R0-stop-the-inflow.md": "a9c94236a30eae4345a4b9004d1e76650c80f94338ab40b0743f0a6a393868ea", + "docs/refactor/R1-two-revision-staging-357.rst": "47171aa139010e2acca35fdd1a28bffb36ec9b3e6281ca46f12a73d78e9c66b3", + "docs/refactor/README.md": "bae436fbae8d4dcc715619e64cfabfd1e6b110678ad58e56f46deea760be3642", + "docs/refactor/S1-config-schema-typed-renderer.rst": "3e664969d49ffb5d523766605a2f1a9d44f10294a32a47add37eedea43e14a78", + "docs/refactor/S1-generated-input-contracts-339.rst": "ae0d6f1342703c9e6d85dfcb21e5831f003c82a8f39536de18e9f0be6d41392d", + "docs/refactor/S1-imagexpress-installed-338.rst": "794ac11af297de5779499b564441eefb91c9d6509c7b2c01eff29b03abecb32e", + "docs/refactor/S1-imagexpress-readonly-inventory-337.rst": "6c2d35d2606df56d7bcf7f65abd45ca580fa0033173d7735c3c9281dd4d37cb7", + "docs/refactor/S1-main-f24-integration-20261001.rst": "42b79a6bafc48f14c6531d2f4627ac283ea5cae7f76716b6aa280b422e1212de", + "docs/refactor/S1-mcp-result-owners.rst": "1073fffb2c583041d6a6eccec670c2264f42987b16413cc90955157480949398", + "docs/refactor/S1-paired-raw-bootstrap-350.rst": "53be6f31b40675028e84551dd857e7aa7e99a5f825a1b2e1ff0babdf7c14d26e", + "docs/refactor/S1-paired-raw-installed-350.rst": "84247349d17dca8f0d372a671232cb11600adfefd7173b6785333ecce42d12e8", + "docs/refactor/S1-ui-action-result-installed-20261001.rst": "2f0dd3e03b897362dabe7f29d8eb816d965c029786aec2a3f0f214d8bf59611b", + "docs/refactor/S1-ui-action-result-ownership.rst": "0ff2b822cd55203b4014e98cb2881cf5105af5cf326717d3038a5969304b2901", + "docs/refactor/centrosome_watershed_integration_20260930.rst": "939b4d1602a6b34ec0e6ab4577c21dcf543bea6876e97d58a4419103c57bc5a4", + "docs/refactor/cleanup-20260929/H002-FRESH-C03-ENDPOINT-RCA-20261002.rst": "4b5d24a5ed8e683b53e2ad2be1b0bc6b9b6a4d3b85de15f25d792471b220f5da", + "docs/refactor/cp-aligned-full-stack-342.rst": "b7493d7e12dafec853e02498f6f65979859c5c041010716bd4cf9f4669318fd4", + "docs/refactor/evidence/cache-reuse-tests-resources.txt": "d89afd03540a141b2dd564a33f6e072d0a50cf7a96fdc88991d872a1533a4877", + "docs/refactor/evidence/cache-reuse-tests.txt": "332d84f320f1f6679439758dd688d7cbdd6381020f5aa7d0780866ad7fdf639a", + "docs/refactor/evidence/config-resource-supervisor-failure.rst": "464f1068973ddc2b4334cddd0d1473a62f29b6f17065dcaa47e368bf3c7887cf", + "docs/refactor/evidence/final-cli-tests.txt": "7703858045bbf0160002fe19390dff1039cb4e896f75de769946be28eec22029", + "docs/refactor/evidence/focused-tests-resources.txt": "ece6e843c060b3107a5d3581157e4e446354cbb608cbbde72afd165d5cd2c16c", + "docs/refactor/evidence/focused-tests.txt": "709d352ce4928ef077981e76e64e7dc9ff4082172c9d4abeccff544a29622ee2", + "docs/refactor/evidence/r1-cache-reuse-checkpoint-resources.txt": "e71dec459a0cec70de0a013733603a0d282495bc1e05b6d961deaf37b2c1d37d", + "docs/refactor/evidence/r1-cache-reuse-checkpoint-time.txt": "dd199b725c8b7f12ac03334ea8e4629aa0ae1dbb4f2f5882d052cfae47b921ea", + "docs/refactor/evidence/r1-cache-reuse-checkpoint.json": "5df03cd49cc726caf0d342dd90bb3c84e7d5695b2551434f4749ef9043ffdead", + "docs/refactor/evidence/r1-corrected-checkpoint-resources.txt": "7484374e6bf73cc0f3ea3867ef2c76bf3c1bb6edbeff211c125c6094d312a5b5", + "docs/refactor/evidence/r1-corrected-checkpoint.json": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "docs/refactor/evidence/r1-real-checkpoint-resources.txt": "880240afd2232ddcc116e8fed028675eb204a69a675f9eb853c6aa9e8ee8e09c", + "docs/refactor/evidence/r1-real-checkpoint.json": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "docs/refactor/evidence/ratchet-benchmark-resources.txt": "4cf9458b9fa70c7d9b72edc2a3dba0d1e71d3c227a8d2cf62be101d1a0808d62", + "docs/refactor/evidence/ratchet-benchmark.json.gz": "33ee215eb47196f1d361da45c1f0ef78418627789a7e29a0681b9611eaf3b415", + "docs/refactor/evidence/ratchet-openhcs-resources.txt": "ba215567deb02b310bcf440509df5545856bc458f62aba5c718ad1fee48f4c0b", + "docs/refactor/evidence/ratchet-openhcs.json.gz": "9fc448a6c5e7388a29b93019417a333bc68b3024a658bcd5f63f0f8e84908a67", + "docs/refactor/evidence/ratchet-reuse-scripts-resources.txt": "fa9e82cf96707d434abdf0f374a93a0745bca28021828ca7e586a03bdd6933ae", + "docs/refactor/evidence/ratchet-reuse-scripts.json.gz": "085041a4b6f41509c297e1d2d1ac4ec9f756ba69f2b7ee14db19f9c39285077f", + "docs/refactor/evidence/ratchet-scripts-resources.txt": "55187d48a956949c8df1c6d358896b61e0b6bf35f6a16bbfda303b4ab19e1a71", + "docs/refactor/evidence/ratchet-scripts.json.gz": "b54471e11432954d42e72b2adfb4f2870091dbeb88553eb36a6f040217d9847d", + "docs/refactor/examples/344-aligned-rescale-engineering.py": "323ab83b8462a9a8f4e8cc873e446eab1de15a114a3f3a07740ea8b682b7666f", + "docs/refactor/receipts/S1-config-schema-typed-renderer-20261001.tar.gz": "f6641d99fb42983763c3bfec0044ec4711e4787223361ddae0b3ce32ab097e68", + "docs/refactor/receipts/S1-generated-input-contracts-339-20261001.tar.gz": "50e19c75e01d8456bdcbb35f910c76254b7a00906d18084f79298dacf6078fb0", + "docs/refactor/receipts/S1-imagexpress-installed-338-20261001.tar.gz": "2afac8c852d194ec1f89e55dd7d63a06bee21efd76fcdcb34c33e5e7a0a2f21b", + "docs/refactor/receipts/S1-imagexpress-readonly-inventory-337-20261001.tar.gz": "bd635440e79af99d1a0b9f285668699ed47ce678bf8c82c0c630e054c524bc64", + "docs/refactor/receipts/S1-main-f24-integration-20261001.tar.gz": "bca2ab8be8aa58ebdfc3502a4115ff12228aa048cd215d2fe0942f7cf062fea7", + "docs/refactor/receipts/S1-pipeline-20261001.tar.gz": "9a4fdd76ba5113c20dc75b34f92c84d3fe86abe3a76acae522daab47d3fa30ed", + "docs/refactor/receipts/canonical-dev-client-decoder-400-source-20261001.tar.gz": "06d9c73a82ae7e1c58eb1213da3398a3e1a349ec939bc69bf1e1992145f4c351", + "docs/refactor/receipts/classification-370-cooperative-hook-review-20261001.tar.gz": "7c26a2c40eefcc8fd71c833cbdf016df0e20f909ab271e29587f914cb5f3ab72", + "docs/refactor/receipts/classification-authoring-runtime-380-381-source-20261001.tar.gz": "4f27a84e986968d421ba4b3011e833f8e78d3ffb1ed61142d89d883a8634589f", + "docs/refactor/receipts/classification-measurement-subject-370-source-20261001.tar.gz": "95aaa8e25c2f35cee817a3391f194733c39d66a74903e19c3f4440304c297c01", + "docs/refactor/receipts/classification-two-vector-authoring-386-source-20261001.tar.gz": "470e435d270548a0cf65ac574bfed5778798af91410484ed78906ee80f87ca96", + "docs/refactor/receipts/classification-two-vector-mode-ownership-386-source-20261001.tar.gz": "2106b6113282fa8652ab1a0c6b3a7aad4ffebf27c5e9798552838adcbe50a682", + "docs/refactor/receipts/classification-two-vector-public-driver-388-preparation-20261001.tar.gz": "3ff0f287ce9fc2022f8556590aa42cb519ac201b8337e5792bebfd3d84f9a22c", + "docs/refactor/receipts/cp-aligned-full-stack-342-20261001.tar.gz": "8bf492e2b9efb309ca071f7b034b8780ff96d5f441acf0da9a12fe55d35150ed", + "docs/refactor/receipts/cp-aligned-full-stack-342-a14-supplement.tar.gz": "34f21c4c7c3ce912d020bacb91c303b00dc9ee55629e96aa7d092c123e1714d7", + "docs/refactor/receipts/cp-aligned-full-stack-342-acquisition.tar.gz": "da7bc60170e6383b9cc2e5dde090294fc4f7dccf9ef9be757c2839c41b2f6852", + "docs/refactor/receipts/cp-stack-installed-native-344-20261001.tar.gz": "d6d53528b2a8508f4004a6d1d6b7adfc411a7dea9a1ed1844bb19736731faad5", + "docs/refactor/receipts/generated-inputs-installed-345-20261001.tar.gz": "e509a5518d575a4fe58dad635c55cceb3a926f78814b834eb1d1b2b233280dd5", + "docs/refactor/receipts/mcp-authoring-360-source-20261001.tar.gz": "366b053daf6483c44adc3d4d4a69f661fcb8fbbdbbee474b6ab698979e9eb91a", + "docs/refactor/receipts/mcp-compile-selectors-360-source-20261001.tar.gz": "dcbc336f43a851ea88743b0d58fbc28f9592be2cf724366c3af778963cd8e888", + "docs/refactor/receipts/paired-main-flow-388-closure-20261001.tar.gz": "f20d2d7798198ace853e8e77333549b29a5d69d4e35d9823659d792b390fe310", + "docs/refactor/receipts/paired-raw-350-census.txt": "1a72c6dd5cbe0baf897bab12966f008611bb82065a64bd4ee890c8e0372be308", + "docs/refactor/receipts/paired-raw-350-collection-failure.txt": "5f6ff263edfc1d56ddd97d0cd14e3adfa5aa0d367bfdf63f3d495f01dfc92acb", + "docs/refactor/receipts/paired-raw-350-full-context-r1-20261001.tar.gz": "6d596f8abef2e4d9eb0c6b72ed1915c1f77186503e722b56387141545147d384", + "docs/refactor/receipts/paired-raw-350-initial-global-audit.txt": "4a83dee9b19d8aedb6e4bafadde38288f1029ca78ae1dda7eed3dc5697aac967", + "docs/refactor/receipts/paired-raw-350-installed-20261001.tar.gz": "d9dc3cf6172a0acca50bbf15886f0582ffd34536fca2af9daab99d9f0f0c225c", + "docs/refactor/receipts/paired-raw-350-original-defects.txt": "b6368e5c9367a216d3f173a9f68b6e69c3529d1ea58c546431d1d8313d1df045", + "docs/refactor/receipts/paired-raw-350-overlay-incomplete.txt": "97f5613f88daacf944f75aabec61b66004a8ac31fe2bff226fe9a2be0574392a", + "docs/refactor/receipts/paired-raw-350-owner-review-fixture-failure.txt": "ce0f5371142d9750633d8065d2fabe7d8707959a18d382188c25f8f36e47d504", + "docs/refactor/receipts/paired-raw-350-owner-review-freeze.txt": "d9bc4d82187956dda1d828a019808e30ac74ee88f0f46205a6f0ea11f78bae0a", + "docs/refactor/receipts/paired-raw-350-owner-review-local-guards.txt": "1116c64232866231a978418a19b09bb778b1bbfdc84deac96e80dbb3408e010c", + "docs/refactor/receipts/paired-raw-350-owner-review-projection.txt": "cc623dc80b72a868b5de960a60ad4ab1d64204a79f0f1eed4dbb042be069512b", + "docs/refactor/receipts/paired-raw-350-owner-review-source.txt": "a5ef378a65443f9db401e22389e2c366ee7d52fd47df54b3e02fddf183c2a319", + "docs/refactor/receipts/paired-raw-350-owner-review-viewer.txt": "c1d277659910c61cac056f695e23fd5b2ba7bec7848c0937fb5c762d014777e7", + "docs/refactor/receipts/paired-raw-350-polymorphism-review.rst": "c80c952a89f2137ea3b82ddabf3923b230ac40263aff8a54f178ab735fe87e9c", + "docs/refactor/receipts/paired-raw-350-r0-unavailable.txt": "ff164f8da87f5fa14b24bc2e2a835b40458a460453d4ba20fa47233cd2227238", + "docs/refactor/receipts/paired-raw-350-r1-incomplete.txt": "f9d688e0c3ce656bf7b759b9b1f850a03f12136d78751a2774ec465869127aca", + "docs/refactor/receipts/paired-raw-350-source-bindings.txt": "ca18cffe286f54ca9f0f33715d1dc9faafc2a1e8345f2e67c58dae129826151c", + "docs/refactor/receipts/paired-raw-350-source-projection.txt": "9476267486a93ee37ab243a1610477dd70c926d5ba6fc0ba81f7f92eb11df0d0", + "docs/refactor/receipts/paired-raw-350-validation-environment.txt": "52323864bff1c2527a9151ebad6f681d19a8cc2918a790f958dae1a3cb3615bb", + "docs/refactor/receipts/paired-raw-350-viewer-projection.txt": "bf8894234b9431616297e73803005e880aea705de2a8c2168f832d013adef153", + "docs/refactor/receipts/public-enum-388-source-20261001.tar.gz": "d0e8e9015987245eb0988926735e257576d55f06f981db3872043b884b2a689e", + "docs/refactor/receipts/r1-357-parent-acceptance-20261001.tar.gz": "575e1fd30ffef1770cdc07b022a31fc6af66b51bcaa9086fe858da59c9b65b31", + "docs/refactor/receipts/r1-357-pinned-r0-20261001.tar.gz": "03c14fb7b8d1c37cf85b88913e09b6d8abd8f6eaaa6312027d33c7e813d947fc", + "docs/refactor/receipts/r1-357-source-20261001.tar.gz": "d41a137fb14f83f471f01a00cb94636520b8f61df70e74e56ca356d648155b2e", + "docs/refactor/receipts/ui-action-result-348-installed-20261001.tar.gz": "f438599170dd46755328a8967bffcca6d3589cd98e744c687811222e9ff55c90", + "docs/refactor/receipts/ui-action-result-348-poll-source-20261001.tar.gz": "ad1bc20fccf597bba6715d2727e7ae5f75e008122f2281afd95353c0217b8c6b", + "docs/refactor/receipts/ui-action-result-348-source-20261001.tar.gz": "bb23a531dd376857f3816c2757f4bbca3194686210221727a35079bac670ff92", + "docs/refactor/render-complete-snapshot-363.rst": "96e1923aaec6aa5dabee74e874d06253eb0f6d2c1a77fa2ed5ef406f23d12081", + "docs/source/_ext/openhcs_config_reference.py": "a45ccbd142745b0e636ea815134d6154c669f84b06a2d087fd76f993ee06aca3", + "docs/source/_ext/openhcs_gallery.py": "825d0e01d331069b93c00469fff157ec44c2febc346b6978b2e556159be7a8e1", + "docs/source/api/index.rst": "68401481faeeb4745afa97a50248df8533cfdb6ca0aa09a94557bae2cc4ef57c", + "docs/source/appendices/glossary.rst": "0d2911da50a8546e893ffbf0091415ae995700bef3f271170cd8eafb7abffb85", + "docs/source/appendices/index.rst": "2da5b38e721f42b2a0f689a47d7919770ba4cbecb2b89840ed46cccb1e75ab90", + "docs/source/appendices/research_impact.rst": "177e054c72fcf87d97b79d4055a33d4125522b1fcbaeaf14e2876808511429c7", + "docs/source/architecture/abstract_manager_widget.rst": "3c51ca0ae7b7ce37ca6bae986330b7b1285b4e3f176d3f61b9ad92ee70e2d9e8", + "docs/source/architecture/abstract_table_browser.rst": "f628d49ec314b8893df96ba0737d52ade78cc63ed997a5563129cc60765284e7", + "docs/source/architecture/abstraction_lattices.rst": "aab6e8a69b89dc4f866cd83a639a4120a9ebbdfa366bd228fe8cda4202ea51b7", + "docs/source/architecture/analysis_consolidation_system.rst": "3f3fa8f3f0ee441d4c2aa42bd79c2da3dc04635f4958b64ce49bcbfc25a618db", + "docs/source/architecture/artifact_contract_system.rst": "eb9380b348a7fccfb4385956bc1997b4dc4bf23aaf1f45abe2fd2fd8c1342d83", + "docs/source/architecture/batch_workflow_service.rst": "5b35c78968bea4782ff67974f90428b37d7f00d79ef00509155f2b6d79917798", + "docs/source/architecture/cellprofiler_interop.rst": "ca51e69ffeb5e594de80ab27c0c71bcb1d78a76fd059ab093afbab9b7a9b0040", + "docs/source/architecture/code_ui_interconversion.rst": "e92fe7d1580b8cbc121dbf13023c64faf85150de3e4bca9a13f99513ef6e2cea", + "docs/source/architecture/compilation_service.rst": "38551baf73cc9010f0dff7af6440a72645ef28d85ae014989831c502cdae2e80", + "docs/source/architecture/compilation_system_detailed.rst": "f6b652564854eafd1e1d6670a2f6290c799d5aea05ddafff3fe6c8e7587af7bb", + "docs/source/architecture/component_configuration_framework.rst": "1334fb249990281c1c8e23c74b3cb2da71aaa1fed73d58ea79b0177dc5e7bfa4", + "docs/source/architecture/component_processor_metaprogramming.rst": "82f7f46e2303d1a78f0cb9d2d2286e230447f8f32daefcbbbc5021b9e9e86561", + "docs/source/architecture/component_system_integration.rst": "5c4c60b9b9a02dfb0dd0f56ecc2bf4528e877314f14f76d0591ec112958e84c8", + "docs/source/architecture/component_validation_system.rst": "c9d15356dc02939a216b2bf7bad60b43b2a244ce30d1eda4e61556c26b80ee02", + "docs/source/architecture/concurrency_model.rst": "db244918e5b7ef7ba93ae17d1855954c630e577b56813f8ded29dafd4e7889bc", + "docs/source/architecture/configuration_framework.rst": "76255a82c6f9d5c2032ba2c31a5c5c8a08442ba463f62f65995b54e26ea74363", + "docs/source/architecture/context_system.rst": "dd532f01302a0aedf0ed9e42a478808557aa7a9b16554e274117e142e3fb57e8", + "docs/source/architecture/cross_window_update_optimization.rst": "c51c33c8b45c265d62061e5b45ae00eb84d250bd050640a623f99911257a323d", + "docs/source/architecture/custom_function_registration_system.rst": "41758478c7609c3506a501d30ee41de17df3f36036587c5b622e3a2fb38d93b9", + "docs/source/architecture/declarative_window_system.rst": "480757c11ae720000f495ecfd073bc695a59f3ccf60a1c4069e0990a28828f96", + "docs/source/architecture/dict_pattern_case_study.rst": "7b7f94d81bd19966988b9971ffbc260f8cbfa35cb6351d11689f660c3b48d3bb", + "docs/source/architecture/dynamic_dataclass_factory.rst": "cf60e0c66a1f0d8a4cc586619de26fcb176b38543c4f807d12f8da7a5053dc26", + "docs/source/architecture/experimental_analysis_system.rst": "9b143aff0b24ea2c3d9dbebe9f4789f764c3e81d1258747ee2fe33eed8e4e943", + "docs/source/architecture/external_foundations.rst": "2c95e2864cf17a5a6195c2fc3c498ac99eb4cf450a841f7a8d2417a94d53f1cb", + "docs/source/architecture/external_integrations_overview.rst": "cd950b20705e1661d76f3c2ba9a66e68a37a88020228083327b77a9e40b688f1", + "docs/source/architecture/field_change_dispatcher.rst": "0f8b6bbb4cae3582c7320d97cdd2834c8d0d03a71d09087a5191dd6c7ba0f76b", + "docs/source/architecture/fiji_streaming_system.rst": "62e265e06150472a480de0827b6c87acca44485731fd1460fcc3a3dd63c44d20", + "docs/source/architecture/flash_animation_system.rst": "64572091cf8f5ab81e3486dfd96254a8e21678ec535d442225013ac122ad1569", + "docs/source/architecture/function_pattern_system.rst": "51d989837a8c3db1e4278c72a9193051d6664719fbe65dd6e8c64c4f107df643", + "docs/source/architecture/function_reference_pattern.rst": "8fc9e57d90916b1b3296caefef94f86ba521aefb6daca2d979ab146b3636129d", + "docs/source/architecture/function_registry_system.rst": "d7b3867ded4005b0c37875b70b972f89e76baf04397ec54554679532cbc0ddf6", + "docs/source/architecture/gpu_resource_management.rst": "998a32e50c5ed6c59b44f3f7b0fc686b19fe619c63b459e0be7b273c555bc637", + "docs/source/architecture/gui_performance_patterns.rst": "11a327ebe1b1a01dd66e41b995fa5ba4af84f903b711d57e4fda58efe727ad9b", + "docs/source/architecture/index.rst": "f4f48fea35485a48919dd048ed8e053b71c8c2c25b6fa8e0f6a60db1b7f3e24d", + "docs/source/architecture/list_item_preview_system.rst": "59ec9b7019bcc8baae04e3cded674beb98cc808c040fd82f47852b253467d5ec", + "docs/source/architecture/mcp_distribution.rst": "11d1d54561689e08a1b6c8ceaedfa1f4de7fd82dfa25e20e1b2ae813528ee0ce", + "docs/source/architecture/measurement_equivalence_system.rst": "f071f7effe7d94ef86cfcfaa24f479249fce185cba690c670ff247597aafa948", + "docs/source/architecture/memory_type_system.rst": "1d921c1987ff8020a9b246e554bf23c89a40026b232aea78e2222253b1aeeefa", + "docs/source/architecture/microscope_handler_integration.rst": "7568ee810dd5ff9f13643ee2434a239e6e79d42d0e3dd57d2690b406d41b254f", + "docs/source/architecture/multiprocessing_coordination_system.rst": "816ae37a0aa964c708bf3b7855583b1e0ce706571a231df60caf1405eecee5ee", + "docs/source/architecture/napari_integration_architecture.rst": "4bbde493663df970ff002dece5777f6150773f266b3e4ecdb5fa1ab00185607d", + "docs/source/architecture/napari_streaming_system.rst": "0627ddfe19a65702fecbf151a17924f1f44bf9ba91886651b5c516384232197d", + "docs/source/architecture/nominal_ownership.rst": "ab2d1a92cb778414fb8624533ef6de74cf92e70ba87c92a495bbe8ae70faf7be", + "docs/source/architecture/omero_backend_system.rst": "c62d31950f0d453ce5d6741f628f37ed758dbae64e45398ca470526f623c6e46", + "docs/source/architecture/orchestrator_cleanup_guarantees.rst": "1fabf4a602e5856b9ac754b390478f7c2859a6bc6e088e7c55a4e5901ccb54a7", + "docs/source/architecture/orchestrator_configuration_management.rst": "1cf4d86ac3a102396af9ad8515466dc6cb55dd81c7c53190503210349b7f6b5b", + "docs/source/architecture/parameter_form_lifecycle.rst": "dd8768b55e6bbacfe6553a718a590d7c05c1fb1a802d3d6cd276c1526036a2f5", + "docs/source/architecture/parameter_form_service_architecture.rst": "ef32a0a776d3062db07cfc53763f43e98b2e860f902a1ae5a962adc01244a52c", + "docs/source/architecture/parametric_widget_creation.rst": "b99e226bc6d9ba392e003c594e4bce691188b93e0b0d34bd41d5a77c5deb4b22", + "docs/source/architecture/parser_metaprogramming_system.rst": "2e5fb290d9e25310c13084ee05ddf0e8c19cee7af435e6d3a16b5c7ca60550c9", + "docs/source/architecture/pattern_detection_system.rst": "2a59466462d8dd7f7439f2d628369bbfab955fc6cc92f82bba81bb896cc2f06d", + "docs/source/architecture/pattern_grouping_and_special_outputs.rst": "42dcebbbb001891b323a38fb786c50ad1def8d13f52ec0b77750fd8b846ab293", + "docs/source/architecture/pipeline_compilation_system.rst": "a8b349cd9e24e5ca683416666c05ce71e9c74fbdddd4f216f20c1b0837ac36a6", + "docs/source/architecture/plate_manager_services.rst": "06abc350333f3c0d51f3418db49de6c51d7eb3f5d27d523e1a113a5b2b2b943d", + "docs/source/architecture/plugin_registry_advanced.rst": "74b621c6e6e85dc47aefc7b971cfdfbd0dbc39eba911f147184dd778c93afda3", + "docs/source/architecture/plugin_registry_system.rst": "1b851cbbceb6ec89d7acb28574bc8d858903b8040f42f65760b96c0b7d3250e4", + "docs/source/architecture/processing_semantics.rst": "9d1a0203f2a4236ea102cacd8e48735a24d93d55d3d5c807ed69a2f30d451278", + "docs/source/architecture/progress_runtime_projection_system.rst": "6cd52fe8420d85bf7429ab128e3ba3a92a42bd158cc35767b9c1673ef51a6441", + "docs/source/architecture/quick_start.rst": "4dcc151050f02da75a6819db0be0053a1dcbefcf236b34998536ad9fbece915d", + "docs/source/architecture/roi_system.rst": "15c214ff7075f5e87c56620dcf2730405fdd6845c83d6c379409caca919dff61", + "docs/source/architecture/runtime_value_system.rst": "130ae142dd1d147f24317d06c900d6701482fd492d580ee819082c8a756f7703", + "docs/source/architecture/scope_visual_feedback_system.rst": "58c708f70f3e1dad618b797f3dfe6e50f69dfbcd089f6acaa7d2f3ab2d845cb9", + "docs/source/architecture/scope_window_factory_system.rst": "02e8e78701a4cd7b149cead277ffc3d52305ed153980307d4818f0949c43fc61", + "docs/source/architecture/serialization_boundaries.rst": "28083415985860019b3ee15d0c69e9c07c903584a3890cd53b7a1676a6718c74", + "docs/source/architecture/service-layer-architecture.rst": "8a2362db7c8da206ea396293f9a90bdbf728740e39cd616d5902910ce296a2b5", + "docs/source/architecture/service_registry_integration.rst": "db7c022ce0ebef1a40baed2b68e3d9e8301811d4947cef3eab6b2dcf7d3d48e6", + "docs/source/architecture/source_model.rst": "ba603eb36228385593c44bec049517320dfd29013ffb49cbdce81bc675ec11e0", + "docs/source/architecture/special_io_system.rst": "e61acca7a6cb7e2c7cf41948d399b850695c9c245c46d0d5c85d1d5b0d0bfaf1", + "docs/source/architecture/step-editor-generalization.rst": "d5e23b3c0ec74c1a0e31b253493e3c777f72f817c87f42bd72818ba0d2f0ecc6", + "docs/source/architecture/storage_and_memory_system.rst": "37be786863142d1762d151ed6a528941c79a47270ba577e93cef27cf0c474428", + "docs/source/architecture/streaming_boundary_and_wrappers.rst": "b4943eeac6a59501b60ebfa16b0b093bc415b5bf8e4124409371b0d3641cbbfd", + "docs/source/architecture/system_integration.rst": "f0756f6700cd6d8e3ed2ceeb6e5d4210ad24ead7561fcc3271fa3c9684d5b664", + "docs/source/architecture/system_overview.rst": "87f6eec65645b2fd4a01de961126c765230ae0eb3480a1e3b4a7b3fb3618b28f", + "docs/source/architecture/tui_system.rst": "da94ee23afc2e0870cfe590a263afd3855d3460a3d34b9579615a3c79246a881", + "docs/source/architecture/ui_services_architecture.rst": "f398733f3501923efcbd7e41fc8da2681c9ffe1e78c2d1997fc65df5e731de3a", + "docs/source/architecture/widget_protocol_system.rst": "930b2684c302fc8a1c60f8c11845ec10168ee29a074d6c04e78ea5ccd68706ec", + "docs/source/architecture/zmq_execution_service_extracted.rst": "dcaf4e63eb604312edadcb3442479ac6e369842231bb5caeb3e5404ecf693155", + "docs/source/architecture/zmq_server_browser_system.rst": "2ceb609f8b720a6862577d5fb1e18658d1040bc7e7507c03642a3b8471a9fa6e", + "docs/source/concepts/building_intuition.rst": "f4eb04f78f1e52241fa1b7f79b39609142f106832a0bf38b953a170592cf8f78", + "docs/source/concepts/component_system_concepts.rst": "e3816c371c55821e36e5012787776092beee9650ea638f0006673808e3e81539", + "docs/source/concepts/core_model.rst": "abf8f245abf9ca3526a63840d54dbc5f9677604422f027c0d838ff2256800830", + "docs/source/concepts/data_dimensions.rst": "09ab2832cfb480cc532867a118850e12242f2fc9951df718f95317bd746ee564", + "docs/source/concepts/domain_fundamentals.rst": "9f4a6f7aa270b5814397a0e8227f44064cc6f5e4cf2219609da187ae7557341e", + "docs/source/concepts/dtype_config_system.rst": "65715bc974d343ac1f219a175a95491a6120667fa46e32a5ea96f6f3e14628e2", + "docs/source/concepts/function_library.rst": "5f283828046aaa3e7c9e57ff0ef34cd77561031f110619b0d41aa9696047a769", + "docs/source/concepts/function_patterns.rst": "2e60a937bc001bdcc961f378c5fb29d1829580351cbf87db4eec9eda02d77411", + "docs/source/concepts/index.rst": "dd54fe9788c8e958696998abe42c55a6b49a31eb7d07211e14de761dfd8c62c2", + "docs/source/concepts/module_structure.rst": "6537ec140390a40a617770c4c4c7691a568133bf9d72b0be41ee3acc7e61c404", + "docs/source/concepts/pipelines_and_steps.rst": "ae82cf8baa401e60948956fd26590ae6d32c0a08db147df5c7a70ab09398eed6", + "docs/source/concepts/storage_system.rst": "6457c0c132bd45f195274e51b255f45713b5efda9ee7ada352de556ac4aa1dde", + "docs/source/conf.py": "37c6c130d75662601d1e0fece5e0f206b7bf366ae04b596ae39aa9f691ad1c94", + "docs/source/development/agent_workflow_validation.rst": "70fc4c26f67598a993788db2f246520836b9fc255fdd4cbb8d7d3999667a3158", + "docs/source/development/ast_refactoring_workflow.rst": "79299114e82cb13d549d24dce90abc381cd249e2c2d4e10c116d2086b73e4686", + "docs/source/development/basicpy_readiness.md": "7ae8ee1dd13f7161a2280d3632802b66c496eee37cbfb6909d6b83d3c22b503c", + "docs/source/development/callable_artifact_authoring.rst": "204bc980bf4eb9b8d388e17ea9510c15baea62dbd42cbce19b71aa59119a0b53", + "docs/source/development/cellprofiler_module_authoring.rst": "21c9257bb7f2f637f3ae4546614a5aa2dc166f770da6144d9292baa4806f5bae", + "docs/source/development/compiler_extension.rst": "4bb46f10ebfdab22de8f3f5b833e7f74b78d4d846ee50c671296e4aad33953ff", + "docs/source/development/documentation_authoring.rst": "26fd82613f2b305850a37ae899dfda4fa1eab2186fcc46cdba639f43b82e2175", + "docs/source/development/extension_workflows.rst": "02e13f4611803605d430da15b4a1fa875a16a708b4b167fa0efc19f03df672eb", + "docs/source/development/git_worktree_testing.rst": "5a2487f59d2befe94a05219b3a382f2c60e2c773054cb90f8ac2bfdde0ab9e63", + "docs/source/development/index.rst": "fd425732b5660df4f8f110174f6e773f070160f73828bb84f7dd641965184fdd", + "docs/source/development/mcp_development.rst": "829194108df1e11d2b8aaae97da3a0ce7cfd6c5a2842e58ef3ab3ad305a88de7", + "docs/source/development/mcp_knowledge_base.rst": "5c80d2a633a6c49ce2301a02d47ca10051ff5289c60e16b1e0e01ce12e7a84fa", + "docs/source/development/mcp_knowledge_base_manifest.json": "1544dfcbba774a3f6192d74975e46d406096609527bc86d0d44dfa153e7ebcdd", + "docs/source/development/mcp_release.rst": "df77e028492fbfa6856545a9eb138959a3ad7535de2878ede2583fa2b39f5ab1", + "docs/source/development/media_gallery_capture.rst": "d6a113e61be593483c1fde8ebf6c7ebc51b67a1ae05ab86e811605793a9db5e7", + "docs/source/development/omero_testing.rst": "2316f94fc1745319e710ea52f48276eac864c7c8070bbb7e0c2f67affabbe595", + "docs/source/development/parameter_analysis_audit.rst": "4e5c25f0c8a05c81d0dbf73f1a2b69e791a3da4c5261865a2fd08de47d49c2c1", + "docs/source/development/pipeline_debugging_guide.rst": "884c4ea25aa9bb02b90962a9500ab981848b143867197068cf585f1abdd5d45d", + "docs/source/development/pyclesperanto_simple_implementation.rst": "5cecda2276f76c744ad6a215f8ecd58c82c2ce6ce4859d6d5990f39ba67ffc3c", + "docs/source/development/recipe_knowledge/official30_examplehuman_nuclei.md": "f6420893f94f1c4ef060f629227f9696d000b46c1ee97f61163a21c2e3185a12", + "docs/source/development/repository_setup.rst": "745235abe25f39764df948357b010f3e973815bd10cf89bbc91ce7d6e7b5070d", + "docs/source/development/respecting_codebase_architecture.rst": "2f9665a3a063e62b401f3d44c24f596baf16d3496d0953efc269e563bda9d48b", + "docs/source/development/runtime_system_assembly_rules.rst": "a2275aad184f08bd2966a26e44463909c92c5151b79936f1ce76f83766fc7164", + "docs/source/development/runtime_value_extension.rst": "eb0f4bdd7eac7fff1ded970acbe29db94f76e06d1bb61d8e25d564f43ea9e249", + "docs/source/development/source_binding_extension.rst": "a1b709b00dca507f7183aa847d208192d6f79f2eba8cf5f452cec37b9510a63d", + "docs/source/examples/experimental_config_example.xlsx": "3333fc392d79c86f80d1f973fa31463b97ad5a1649f1255af2ad460c0e0a4bcb", + "docs/source/getting_started/getting_started.rst": "151b7d49018e4ef31c21bd96729c1ccd3af746a82a5d239bd27bf163474c067c", + "docs/source/guide_for_biologists/basic_interface.rst": "2478a75261b5e9ab12e01f897561485fb004da645f15bf030870fda628162b41", + "docs/source/guide_for_biologists/configuration_reference.rst": "ba3c4ef923c4f643c06accc8641c462b2f63a8e46694ef984dde96ef49a0d9c7", + "docs/source/guide_for_biologists/domain_expert_onboarding.rst": "99af4a6bf51653532d929d7008e20f515bf4ff1303a1eccfec7e7dd79be5ca4d", + "docs/source/guide_for_biologists/glossary.rst": "0f36a6d90e71d79e1b33eafa06f2ef4b0d8193850e6e72a2260fae11360f47e4", + "docs/source/guide_for_biologists/image_sources.rst": "8a4a45c89a263d1da25150009a8fd55af1edeae621b3028878c9f138e40375ed", + "docs/source/guide_for_biologists/index.rst": "36a25412fe848d198b00170d96fe7c4ea1ae8e0ac128354bbc2a94b869bfd4ab", + "docs/source/guide_for_biologists/installation_and_setup.rst": "12516b7e28b02f223b15977b23d94b2396af6a32cb47fc9bc84e0396505aaedb", + "docs/source/guide_for_biologists/intro.rst": "40fac7417c9d8920edb90f3b3dbae55b8e05f333ea58271ef2cce2514717fde4", + "docs/source/guide_for_biologists/intro_stitching.rst": "fde83b50eb661c94d16d4d8986b86f565f6db1bdf7f57668293af7b1612e29d2", + "docs/source/guide_for_biologists/troubleshooting_FAQ.rst": "0c2fb0dbb43ec18efb8d4aeff2a728a35630516a6e506de677de5d070089f428", + "docs/source/guides/complete_examples.rst": "ad07bdc258a85e22c4b67c3d4694c84550dc8e0c1e136998d174e558bc9682d7", + "docs/source/guides/example_corpus_map.rst": "2266d39c952e8d64b2296c524412028606276f9f0ee5883d2e0fa660649373c1", + "docs/source/guides/fiji_viewer_management.rst": "43f0e1f0effa40bf1a1efe18ac3efe8da0a184ad746da086b19f756f06178943", + "docs/source/guides/index.rst": "e4a5b15f4fb1f335477b33bfcb8e6d559b568b4264f68e3dcabf568e89fc2c58", + "docs/source/guides/memory_type_integration.rst": "7890c784379b0de4ecc3871068f1acd13de522077d16a7589436ab94a8019adb", + "docs/source/guides/omero_integration.rst": "b900bea91d2eaecb90d593786b213830c6fd689b6cbc09c25f9b0c7ab2a6183e", + "docs/source/guides/pipeline_compilation_workflow.rst": "051f6d70831eb9939375063651d825604695186de950275b512f5bd8c64a6293", + "docs/source/guides/testing_guide.rst": "82d14d7b3163bd6c7313922d7509cd80325e1021d83e80e50aa8775ac902e59b", + "docs/source/guides/viewer_management.rst": "cf6b38318a5464e6b0d30394f7a5c3bed5b3ca8af1a336486a9d4962bc104996", + "docs/source/index.rst": "708577608c7afb29ce9e3d579bcce3c099348cdad8cb05af63656db66b1c8c72", + "docs/source/installation.rst": "05f5519d47c42adea4d67ef1e70d0eebab7819f0ea4fe66b293a5c78eb34f9ca", + "docs/source/reference/configuration.rst": "526b0bd4f86e96cb888a54f9231abe57d9d6eea06e551e925d9ef6eea18bc678", + "docs/source/reference/dimensionality_and_measurements.rst": "5e76aafecd9d58677c7f0578b9bc461594ef8347cdcfb3a25ec1f563ae0ec808", + "docs/source/reference/experimental_config_syntax.rst": "730244294388f85e5e49fc46d9850cf8f38912fb4039ac45ab89da88d7825051", + "docs/source/reference/index.rst": "23988fccd4033006377ea231c93bafcb2455a6257ac242054123577c927c0e3e", + "docs/source/reference/ui_windows.rst": "f6b96b5e67dba0977de53dd8d76292e1a3577cc43fdddf4959f211f605004c11", + "docs/source/user_guide/analysis_consolidation.rst": "ddb564adfdece91f7ea7223bbd8407b78492175b83fa500c2e4449ae82cfc50a", + "docs/source/user_guide/code_ui_editing.rst": "7617d0594ee253c7a19ad940eee142a931b094171b76428ac83bb74861ad2c17", + "docs/source/user_guide/cpu_only_mode.rst": "f85c3dfdde3dee4e3a2e16ddafd2995d8a18d422477c6800caaa4f94ed5dcfff", + "docs/source/user_guide/custom_function_management.rst": "a543658a18b61b83e0e516d59f13e543090213d904b7503c89ef479eb9a55f87", + "docs/source/user_guide/custom_functions.rst": "6dec340f9cb1eb45e984513bb0ad3cdae229c546fc846df128564a0df0f61398", + "docs/source/user_guide/dtype_conversion.rst": "c95144eee07fe4134768fd2e839cb25dfaa9d61e9794332c8481ccfc39a95d4c", + "docs/source/user_guide/experimental_layouts.rst": "836f6b525b52a32672e7ea18c21e37f037c27ff102a83a762433caa4e04fb31a", + "docs/source/user_guide/index.rst": "27f8a295cf35fafbac99ee4915c653ea5ae63cdfd544b5ebd321564ce61dde36", + "docs/source/user_guide/log_viewer.rst": "fc2c9f40387305d8b2eed47c066d578b99a930a682e7c585319615d539bdfd32", + "docs/source/user_guide/mcp_clients.rst": "f47e4de0a9e8653155f47898627953ad7635c2b72e05805483a72bb6c85f0e26", + "docs/source/user_guide/real_time_visualization.rst": "c269dd225183533dbd30237661eb2f8926c9f42b46082023e457a6c9232f7e3d", + "examples/benchmark_example.py": "bc7ddb98189af17eb0502520ecc259283aab4ae400eccb0b26fcbd31fecdeea6", + "hooks/post-checkout": "f656fcc566be174503d4c3f4700b88f2e1ee71d29f777961b8d3159a3afe89e9", + "hooks/post-merge": "e6bb2b8650ee36f37ec42067ee6cb4a7b5bb3da1c66923a97ea38ca46a88b5e9", + "hooks/report-submodule-status": "776761b57f14e0bdfff14a3b52b9b22bb3440f8c686b171c69a7a40d9b2d90d6", + "napari448-existing-controls.log": "e0fca33c2a8fe4fe33b015c1ed05c7d7aa1ce89b4ef874327b8845842f625f3e", + "napari448-final-controls.log": "ab173c6a68a51f10fbb775cd7f01db8e72e680718f362592c1f4f7d38e8e85e9", + "napari448-first-controls.log": "8fc24071aab28e3b327d28c2217c49dae5901e7f8bd61957c78913722509efe6", + "napari448-original-r0.log": "e44326ccf2424a46ebf8af51dc423dc661ec3f705a5e75c8bb1cf6450d9ebe05", + "napari448-original-red.log": "4f81dad7b8a553755b1223c815dbfd076675ea50763260c16b51ad1e791a531f", + "napari448-source-red.log": "e3506f3dc69016cc0a5bf3a6dda5ded0ac4c957451f82d27889497f067f39744", + "openhcs/__init__.py": "32253e0592653cf555e52f4d4151bbe1002337ad3c459755cbfccb4a91a077b1", + "openhcs/_source_dependencies.py": "63c6f60298b8155b1abc0880cac09ac4d9676461f5198d3f15db676140ec7653", + "openhcs/agent/__init__.py": "3b5283f8384967a128270d2758883a38a7721c5c6a179c8277681c102b74f172", + "openhcs/agent/authoring_contexts.py": "98a2f00ecde98b57e6196aa461573ba84d758e2dddbc7f5944544b7c77018f03", + "openhcs/agent/blind_recipe_audit.py": "131d709b16597dd7978d2d0c98394e1e2531840af7f25063ade2b3575d60fa64", + "openhcs/agent/capabilities.py": "c72eccdfb1a2837dc8227563f1f9f34f62106f8af4533e9e481fc7121dcbd3d8", + "openhcs/agent/dto/__init__.py": "bcaae6fa3303f83e4f9f54ef33a2db2ae609a03a19a440295e0e196178c36bf7", + "openhcs/agent/dto/architecture.py": "19e8596c4bb88a107ad7cecb0fb62aab07738c991937f2ed4d4ca0a26d2d03d2", + "openhcs/agent/dto/authoring.py": "a07683356289012a2ec713fe1b7d21023e4c36880e931c756c7de2d39461afc1", + "openhcs/agent/dto/common.py": "f1f46fdb98446eb5f76e35d299fa5aa44e69617d25dc9eef97c8245b234ae9ed", + "openhcs/agent/dto/config.py": "cee5d664f80346f847186d56cd4efff64b2d6dc72fa7721d2bab02b17a82c9b8", + "openhcs/agent/dto/execution.py": "a1a325d771df05a9cd9c7b91478b1ccb6f9ae745e7aca97366521a6686290748", + "openhcs/agent/dto/execution_connection.py": "1316cd285936def8576f02d14923352e0b96554e7b9675bb1d91c6245c446785", + "openhcs/agent/dto/functions.py": "1a9d2cf4e068c2dc1ed42cafeaa2e7a4a38ef0741895daeeb7333074b5afc801", + "openhcs/agent/dto/knowledge.py": "4826294b7b50f4f1f842ff53cd0c9d9908a6a0620279a49fff2d67c88629bdd2", + "openhcs/agent/dto/mcp.py": "7a6ad911c61b62f339f2615990c3eeea1589ef6efea8941d7d76b745d3406264", + "openhcs/agent/dto/pipeline.py": "ab1f0d5bf70d9d8959ef1e1ead8c2ca1c23f1e6f6557ffcd55ee79ccba4a943b", + "openhcs/agent/dto/plate.py": "1e09656c15ed131bf0185260b8e754e93f57d95331ff51f3064ea37d3f984e4c", + "openhcs/agent/dto/ui_bridge.py": "585b1701b01b92bfce91437a60881afa769e36de9126807c3f4fcaf0ffc006fb", + "openhcs/agent/dto/viewer.py": "c4b976bfd14f83d6d14080e54671c26c507efe636a9d3a68cf660e439a171223", + "openhcs/agent/exceptions.py": "50f91dbd2b6f5ddf029f2d8ddab6b166c85e1226d03cb47efc06944ca94dad92", + "openhcs/agent/image_analysis_qa.py": "3e3b968eaf9b56a0de7177a8557aec80aa95c85dffebdd2a584e9a89f6ce960e", + "openhcs/agent/knowledge_manifest.py": "2689e052a31a075475e48bcc288a5ce6ec43417a607486f67b892f794f61ff44", + "openhcs/agent/knowledge_manifest_schema.py": "0699cb21975a3d783b47e23c0f41399e9b7ad91d58499e0207442d9010f8fea2", + "openhcs/agent/path_policy.py": "73fb2d2f729487a926861bd3919ae26c92da79229db52d9c61e94ca88c7f392d", + "openhcs/agent/resources/__init__.py": "33d4a88d70f2ea443bceaabd3d6ce18bf7617670c2eac4161c9d86ab2e345c17", + "openhcs/agent/runtime_platform.py": "1429f747a77282e9221af6b1ddfbc68d96579558f51bc78abd74607d02d6d79f", + "openhcs/agent/services/__init__.py": "b4c841ea9a2b3c7b14672083ceb2b4569575b3cef1ef9accc05b020118530345", + "openhcs/agent/services/architecture_projection_service.py": "b209098d78c5520fd34d744d17b10c1ef0375a9bc2655449f6f3f9908decce0e", + "openhcs/agent/services/config_reference_service.py": "18b740aad750284c0aeaa4c51e81a31828c8e51e609f4997b6a1a50cabfb4e32", + "openhcs/agent/services/config_service.py": "c4dc3860e1720fe6b383aec8b0ddcb498eb219da74da6ccd6709547205193f22", + "openhcs/agent/services/endpoint_function_catalog_service.py": "91aa713fe1a09b2def1b2748483338a942a0a5ac9c92c45c4d48c2c91c4f4cc0", + "openhcs/agent/services/execution_session_service.py": "40b51d5ce4db6d1e9733d410b78c770963a6f71be922f1ef05484126a9deef52", + "openhcs/agent/services/function_catalog_service.py": "14d16fa47c863f4932fad548e21759ee2499f60ad1153a80af14d22df97aa5b2", + "openhcs/agent/services/knowledge_base_service.py": "c5023d3646fffed2c7b046f068ecba0f2f7375b83b72fddbbadb8842d74c8db3", + "openhcs/agent/services/llm_context_service.py": "6cac4630478f2addcd7e054821eddd7a3243ab156e96f898eb28d2d3873ef72f", + "openhcs/agent/services/object_state_field_help_service.py": "d1f130b63e7b36aaee23b6333048de777b21fb40b7ecd85dcc7687c79dc74ce6", + "openhcs/agent/services/object_state_field_projection.py": "86d56cca7e6ee3ed4d67ca6dfd42145bda09f15114eda4769773803efbce929d", + "openhcs/agent/services/pipeline_authoring_service.py": "c966a51926e980194abc255733fdd7ed44e8de63b29520ca0dac95d72e4a317b", + "openhcs/agent/services/plate_inspection_service.py": "a488e591594195e54153cc80c9901cbe256d5a444232900dbc631de9d05fe2fd", + "openhcs/agent/services/plate_streaming_service.py": "bdb2b1cbaae388783e0c3b4a6e0c9ec143df97504a2570d459878e7788bba93f", + "openhcs/agent/services/runtime_server_service.py": "71f58c8aa17a277c707074a254edeecda61533dc43a8cbb2c89f2a48b4229ce3", + "openhcs/agent/services/selected_plate_service.py": "d4e75a6607fda7004c5363d674315996f2ee8bd4f477c9926a7c029b1be73f97", + "openhcs/agent/services/synthetic_plate_service.py": "2860714a9f1f2e66df403bc72331195d912cb180600889a1b4f4207642cc776b", + "openhcs/agent/services/ui_bridge_service.py": "37015a88828820e7ae98bf804f42c37f1fa26009b1ad28b3c537f65a656e943a", + "openhcs/agent/services/ui_bridge_transport.py": "32e759baf65b34989c99223d8fa9e64800ab566cd05160df2bc125b04dea5953", + "openhcs/agent/services/viewer_endpoint_discovery.py": "27bc74d2964961e80bac607eef383c9aa7353b9d65ad6b4dc3e7b7f76742bed8", + "openhcs/agent/services/viewer_window_service.py": "4748db99a13af087329cf1b140b5ec57727c5e8f60ecd3b4d8520f7af248c020", + "openhcs/agent/skill_bundle.py": "34e99772b73d32642cb5df713156b9d2f4ab49a937034018e9077127124b1547", + "openhcs/agent/skill_sync.py": "e3366bb7fc8c300ab12a8fcfb86b412da45e578fb3c2317394e1efe16dfa18f5", + "openhcs/agent/ui_bridge_actions.py": "28c494cc9335ac84482787f1949d9937fd818864407d56ca23ba585a3b0156a1", + "openhcs/agent/ui_bridge_environment.py": "30f459623ed48c6d653725080453630ef9813f875dd8e63d171147f5e73caacf", + "openhcs/agent/ui_bridge_identities.py": "5913d14b99340174af87b2a7cafa3cdd080a331553c0cf59392ade7d253f1234", + "openhcs/build_utils/__init__.py": "2b28663db4d6ec1638afb92e71a907c275e0c62ba2b1ea69382c51a6bf8f7f9d", + "openhcs/build_utils/zeroc_ice_installer.py": "6922f41c8fc3037b987a2a04ce43d51cd16779a130a039a488f1c5fa9e0849e2", + "openhcs/cli.py": "59025ea51309dbae40416d75fc203287657a109379f65072d77cff41c712e11e", + "openhcs/components/__init__.py": "5d3e9eae8bd7ff36ac4d7d619e1869925fc3e038f69dbf5735587ba0038e8e65", + "openhcs/components/framework.py": "5dc5ff81c9c3fa268e0a7699ae62a99e887e6f86c448ab6d1fc92255c5a9c718", + "openhcs/constants/__init__.py": "7f9ce299ea67a0772b5973e522c3c620505212fd56480bce4fa2fe742f57f236", + "openhcs/constants/constants.py": "4cd6c29abd8b8e3ecc396b424d2b8b61e819a215fc1d770d3f6852b260a0691d", + "openhcs/constants/input_source.py": "fccc1854051fbe47371df2cfb737080bdca14d932825ef7ddc9a0e1ed1cf0dc9", + "openhcs/core/__init__.py": "faa96a3ec6c9a966571aea7696cb4ab0fd81eca3c58f542bc9366a6fdbe3582c", + "openhcs/core/_tabular_native.cpp": "15acc82b8ab64268bd1ea4f83fa7a68f527bf317f002e9f39e15be321e03b28e", + "openhcs/core/alias_property.py": "b7716605fe9b78cd2dfc76627655a70eacdb31f7ef83cb0e09b74f2572bbbe71", + "openhcs/core/aligned_image_payload.py": "3e93fd6cf96da0b6b125055722866c5b6f11ffaf30de464d7fa7d409d8e1886a", + "openhcs/core/artifact_inspection.py": "f0033d6edc3691fb4c7c2d641b6dbdb552e0fc75ee109c6145095a96011b8916", + "openhcs/core/artifact_key_selection.py": "9d4f607483b4b444c0600026a79a2f821eec4ee679a5f662ad059121f05bb35e", + "openhcs/core/artifacts.py": "f67e1a2fe8b794bc37bd17424235d7f1af402bdc1120bb0dc50cc5a963c508af", + "openhcs/core/autoregister_preparation.py": "49762654d81fbe90073930950cb27c94b3552f529e34c5e0afa860ab0d27bdf0", + "openhcs/core/axis_filter.py": "c375f5ccfc5669aadbac035590aca578f9c209a4276d9c6752b96a7461a4214f", + "openhcs/core/callable_contract.py": "a15708ec2bec159cd4c4be2b93d213e8fb9a812e1d6c467dd67e7248898cd6fd", + "openhcs/core/compiled_execution.py": "4d5bc05df24a0874adb974f73eda79a3848d0fc573ed126f3693648bdf2f35df", + "openhcs/core/compiled_step_plan.py": "982ab3b9b79cb78c2aebc905c5acf682d9243abc602ea5ecbeabbb4da993cf04", + "openhcs/core/component_group_scope.py": "d5f1138d21187e7d84213780e3b5961f2daafad4c107f1468b1dce53811743b5", + "openhcs/core/component_set.py": "b3f7dbee802766aab3002256db044025abca8c4ce60be4648f7c1de7b13b41cc", + "openhcs/core/components/__init__.py": "58f0646aeed090cac412be6e4f5357776f7af1a5c5b8989afd09ffb27fdfa2fa", + "openhcs/core/components/component_values.py": "3813d6f7d6d66fc21ea881e3dfcb72f54bd1bfe36212895f00be8f09e59d49dd", + "openhcs/core/components/multiprocessing.py": "d30c624db431f25287d6f5036861139fb8d1ce66838eb9f08d725ae318796719", + "openhcs/core/components/parser_metaprogramming.py": "e916e680993175a78a4db2f9f97976918ddf4bb13617d5b5e2916f912a9939fd", + "openhcs/core/components/validation.py": "21ff922447de83ed3f62e54c25df7966419b4895be23a8a3c5d42a3981df554c", + "openhcs/core/config.py": "47d54358521bd7c7abfea1b3bd2cb4b8c24fd9a85826498437a6970122d59d74", + "openhcs/core/config_cache.py": "51db7f1d5010c0b633a0997b4a587cb2a9a042e0a1b1f291f1c6c8d0d2d90632", + "openhcs/core/config_document.py": "37f66fd971806e059da6194c003e20a7ad08f69066eec4af7ebb11b4b70a08db", + "openhcs/core/context/__init__.py": "309422241c5d942952aeec92d513d9e0e517f91b937fd8aa3eece5bf0a686815", + "openhcs/core/context/processing_context.py": "fa587f726d3baac27f17b064db7230d58162408e4230d426f08acc91568b1c75", + "openhcs/core/debug.py": "072259811bc50bfe71cce9a7cac3e9867bfd853bd34ab5a74b01d9fe14b68f36", + "openhcs/core/debug_session_projection.py": "0a630eb03a83d741a4ed55838adb8ee65dd4fde9be07878aeba5e7a8c86c7378", + "openhcs/core/debug_view_models.py": "1cae1ff9267cd8cbe6324cb25ccd3b50cd0237eafa781e3acffe77e668385df8", + "openhcs/core/debug_views.py": "095ef349e413148e4e49ed221a4822592f66292578d9eaadf76595771dc41996", + "openhcs/core/equivalence/__init__.py": "24516170f37a443d24843686c6d52bf27d6ce73a39f483337144a3bb12740091", + "openhcs/core/equivalence/arrays.py": "6074f4b5183a45b7295f42c6de878affbac49b284ea694aa2533547b8ec3fbfa", + "openhcs/core/equivalence/cells.py": "70785cb4ce8a75f695d97f6c685a1816f715a1b3225382d1f8b4a9a9c707c5be", + "openhcs/core/equivalence/comparison.py": "f8832104aad56c64ad1017c2c0d5ef6a3609384648c5017a394f9900f9ebaf59", + "openhcs/core/equivalence/images.py": "8fbe0bc6620beafee276c887195dab91bdf6f102b246ff63e0ab8f330707b2ad", + "openhcs/core/equivalence/keys.py": "d2b2c2702dd5ad75c7df32905e7abc3ee4bb453c427503387fd210c2631dab5e", + "openhcs/core/equivalence/measurement_facts.py": "ef1fd1f95469d55e670e6b4a874b239fa448fa0c927992723be8c8487aeb9e3c", + "openhcs/core/equivalence/measurement_features.py": "b0e1c3d3073d0af931f7acdede06102b7e493bf1e6825ee9cff41924dd59b021", + "openhcs/core/equivalence/measurement_requirements.py": "c8ae5999e47d7fdc81de1bcf92ab614e517580260188b9e6817f8eeb27d2a8bc", + "openhcs/core/equivalence/measurement_rows.py": "de8b80dc6e73a79e714137f6697ef5e0252f99f9209aac908492386a0f8b4ea7", + "openhcs/core/equivalence/object_label_measurements.py": "b4bfcc56e98dd3558d458894a78037778192aaa9752855dff6e20053eccdc053", + "openhcs/core/equivalence/outputs.py": "be3ffd9364208900a88ea1defdf750de3bd79abdea6bb843900510ed5618176e", + "openhcs/core/equivalence/policy.py": "aaae502d1bb818ba38761374f02d8d4b5cf78a798bb2965f157216eba72cef79", + "openhcs/core/equivalence/relationships.py": "b29521d60b5870f989919a8bc36adc626795264b68851836b90b768588d0468d", + "openhcs/core/equivalence/report.py": "c23d65e113f792e40855dd0843ba0cf16b62cab096fcff96e1a4168a65f187de", + "openhcs/core/equivalence/tables.py": "da6000a86fce224c967195431c86d9d256f3dc216d29a4d889e2e708c9fa60a0", + "openhcs/core/execution_state.py": "4ed84065adb562e6a43230a211cc23f021f4498af546e5eac33d4e7b98082029", + "openhcs/core/execution_visualizer.py": "88d4332fbf10d63107d56675753b16a7afab49d0f16571949ae0bf2eda2be23c", + "openhcs/core/framework_device_resolver.py": "be96430174bc576c0b8083879c579ef3fb9b83c4690234751459c2d7bc5556a8", + "openhcs/core/function_contract_metadata.py": "e3313cc1667719a948913cf819d028f37868b081b69a052795417f4484f872b8", + "openhcs/core/function_patterns.py": "ac438272b3b0b08fed59e1105f7b16d30db27888aab19d8e7baf8b4ac3075e78", + "openhcs/core/function_reference.py": "546a8eb0e315cfe9ff264a4c3f446e05b6c2935f2c6ac1810c196843baf73b3a", + "openhcs/core/function_step_document.py": "9991a14903ca5160e96bc268728943fd524c55896ed2e26825a3639240e0ec2f", + "openhcs/core/function_step_transport.py": "b4c89539ace519926d178eb85a7909b4a7894203bf340213279e4f2a11e1152f", + "openhcs/core/image_file_serialization.py": "85f20b9939ea01b772fd95fd24b4cd82f02d3439f7257b1860f20275a86464cd", + "openhcs/core/image_payload_execution_mode.py": "8fef20a3abe5e2812d5f0b6906ac758ef1307504af282f4ddba54737ef6a6229", + "openhcs/core/image_shapes.py": "42b27a64f00186f337077debe1e9f78c2093380bff878311bc3ec36e55d3cdda", + "openhcs/core/input_workspace.py": "657ecec0cd1bc822ae0f9595616d857e893a5e35757e872ed83356d1e195d5b4", + "openhcs/core/invocation_artifacts.py": "b90ab5bcebbdda757be3c60defaa0b17b738dab1d0f3b691790cef3a50591abb", + "openhcs/core/lazy_gpu_imports.py": "6881f21a00325c457e46b55865e610c8402777a22fa73540a3437cb82136a6db", + "openhcs/core/log_utils.py": "51be2e790b1abd551f9adaef2dab7bec31c34a7bdf78cf9977948eb525783ff8", + "openhcs/core/measurement_feature_queries.py": "e9e2188a7edfc9fe25b08b0ae2cc576d4960169beb0fa053d59d6544cb0f8240", + "openhcs/core/measurement_image_alignment.py": "c97a6e7c8c16cf0b399aa9bcb202c0302dc7f8c726faa0c4323432ca6b188cc9", + "openhcs/core/measurement_lookup_dialect.py": "0c9765ff7f211fe3e766f6c70f22e1e3f3b6a6a6403c3c0685a44ba40d177b46", + "openhcs/core/measurement_row_materialization.py": "2e92a6043937e1eec272b8de1edf5a2d3bd349e3298e432c69a679ee066b8189", + "openhcs/core/memory/__init__.py": "9d5223bda2ace85b6dfb91b0d510fafcbfe6fc19c99f6e2f9c4325dc0d295f47", + "openhcs/core/memory/decorators.py": "d9f9cf0be7593dc0ebfc57f258aebed75bcf9cfa508ed1e942137366d819766a", + "openhcs/core/metadata_cache.py": "e193f1c2737b14c7853bf0d436f0323f1f774d1f941d716dad177ae02dbe9037", + "openhcs/core/native_threading.py": "7f940589bf2ccec524b17081cdc39706b482377d87c114484d731bb59a1bf4aa", + "openhcs/core/orchestrator/__init__.py": "f274cb783cb5b8ce29d7353a80ef7b0ff25577e73b0ff2e328701499e0cd1f9f", + "openhcs/core/orchestrator/analysis_consolidation.py": "3ef9c20c347c1ac724353c84b24f9d7e13513aba7b9c5c01e194171b7e72bb52", + "openhcs/core/orchestrator/cancellation.py": "f825b602fb020a23d602e1f93ed9cecea881eca16e07882fd7a91ab519445244", + "openhcs/core/orchestrator/compiled_plate_execution.py": "ccf1670c632f6bd188881850b09625f1d745b31eade22e7713a13bfedeba44f1", + "openhcs/core/orchestrator/execution_result.py": "21a22fa7aac7f81522d1a391d7faa6f07271c294dc754aa177fe6493c394dc42", + "openhcs/core/orchestrator/orchestrator.py": "ae64fbf28c96f7672fd389b4647b96b11b0b90caccd28e542984692e82e2baf9", + "openhcs/core/orchestrator/worker_execution.py": "2bf7e0b7a1f7df0682939cfdfffdda53825b55bc34bd65269f8c74baeea8b23d", + "openhcs/core/orchestrator/worker_lanes.py": "2e2a17b52e06f692c924554c387ed7aed9d25b3174c60e69d28bed6906f430b0", + "openhcs/core/orchestrator/worker_profiling.py": "c2e51f5a68288bf75c937039d748baebb4402b8bfb5a5f1e2428d7d50a16b7b5", + "openhcs/core/path_cache.py": "59be89822548c957c7b9a22c6232586336e89a847be8ba13adcaa743c1a4b59b", + "openhcs/core/path_pattern_matching.py": "4d3db470c9e55fba2ddc834c145622f3dc88c9f31031c6de8e465563bd7ff8ae", + "openhcs/core/pipeline/__init__.py": "7610df23184cef8c93dcae8f1a2a23cf6370ce00530f78852725052ac478eff9", + "openhcs/core/pipeline/artifact_planning.py": "64dfd5d3e43c2a9b0603b0b19b3aecdcd7f615f8ac6f990ff121dcf7cd2a1b62", + "openhcs/core/pipeline/compilation_session.py": "3c840a89baccc8665d415175458280fd904c1a837b7fd879e303d7dbca129a1c", + "openhcs/core/pipeline/compiler.py": "f9c368f046fd338ae51ee46251ecc24f4b246b00971db423a1f14fb4d8ca4a10", + "openhcs/core/pipeline/framework_device_assignment.py": "f77f3fb2d30c4af8cab6210dbe294f9fb5e246dad0a6bbf88bcaf1e2b58dead7", + "openhcs/core/pipeline/funcstep_contract_validator.py": "73cb5250a5855625903f8b7deada667b83fba594d66de0ed4a68ec76afa3db19", + "openhcs/core/pipeline/function_contracts.py": "5c002873e7626af0376429819a66d2f170b9d8a220ec5c95f085218b668ec997", + "openhcs/core/pipeline/materialization_flag_planner.py": "a492738dd5ee9525b9a9873a2c08f6fe6a103015973a95005a4fb71a5f88ee17", + "openhcs/core/pipeline/path_planner.py": "6a73e774ee8032adea60adf83e74f61cd00380d14eeb113e6bd38e3ca765b316", + "openhcs/core/pipeline/step_attribute_stripper.py": "1aff2f8e6ea87b326c36ef094339e76e4efc784a02ac24f504c712104599a6fe", + "openhcs/core/pipeline/step_snapshot.py": "d81a70b84d5c68fb738603653516d20da882fb51d74d41a6dca238823e3279cb", + "openhcs/core/pipeline_document.py": "0849800b3e6474b8176fe35bc4aaebc63cfa85bd77251000c50da5f4c928b1b2", + "openhcs/core/pipeline_document_fields.py": "81178547c9d6d9a4e78bcb9e5764382ef9961afb09392c1464aa24a9dcd20d14", + "openhcs/core/plate_file_inventory.py": "1402b4f61f756375394409825d9705e996d516f62722998f119a2363f4c8a2c6", + "openhcs/core/plate_image_inventory.py": "49f02284102099abc34649b44934236b80f58148193b32db481b2b7183b9c972", + "openhcs/core/process_local_cache.py": "dcf37435ca47f4ea656e6148ba47713358943a089a927fe1949b6e667c948bea", + "openhcs/core/processing_preparation.py": "6271b4e32e6850ebe2bbafacefae191f64d9be07c34e06599bfb8486995b21e8", + "openhcs/core/progress/__init__.py": "705c78e3d79c50e25e7975f78a17ce663f063cd5752fae8c19b917ab8e9ad31c", + "openhcs/core/progress/debug_projection.py": "8a24e00b68351a1d77898f057ad0790c043e1eeaf12f21b593ab6996b9996902", + "openhcs/core/progress/emitters.py": "632edadc1093d9842d672a91b1e032f560815176ed576221668f91454190aaf8", + "openhcs/core/progress/exceptions.py": "1749f46adf2640e13e8805e32a375415105a4264db16df1e08c68c8a9da83516", + "openhcs/core/progress/live_measurements.py": "59d59c085c61eaf003ae0990d0e6d9cb06e2a37693b4edfb826fcacf0babe485", + "openhcs/core/progress/projection.py": "f07ac1c460d0761c2b70c834aae57f77994ac43bb3ef5cd688e4196ccc5d9fce", + "openhcs/core/progress/registry.py": "1925a97b9cdd6a4928c16e03bad5650b2d233426ec07a913a843c7eb543db709", + "openhcs/core/progress/runtime_artifacts.py": "452f0cd11f29ce5479086bbf81f9eea35b7454ffdcbc231a30d64f3f3ee61468", + "openhcs/core/progress/runtime_tree.py": "ec56720fea831cd358db8bbf54fac1868e9153dd56c332435633a7e2f96b96c9", + "openhcs/core/progress/types.py": "88755f8f54afdfe57288c8fd3924b8f1565cc973d80ee5568730832e1decdfaa", + "openhcs/core/projected_image_output.py": "fb14bc276442873f1a9644a566a8a9423b82438e442cb985b58fce98dbc36fde", + "openhcs/core/public_api.py": "4b98d01316612ccba6735b8edd6130d3da01d6244af51548c5e5e0945dddf54a", + "openhcs/core/python_source_literal.py": "82fea2f1fde28ca9227298c41f8bb10220d07d77ace1613445b5a8708e5476e4", + "openhcs/core/registry_strategies.py": "a41186665abbd308f224d0bb41d6618cd9d89b08acaf54ec2819f2024a6e0804", + "openhcs/core/roi_point_metadata.py": "cbb84911bdf766bda2dc1de1d8fea1c3b328d521c19e0f9a8bd3d2d006b4ad01", + "openhcs/core/roi_source_metadata.py": "aebf51a085874aaf125ad337b268a520382cd21569a278815bf3732d26f83781", + "openhcs/core/runtime_adapters.py": "6e468623ac43282989f0bfcfa31e3391ad677786cea075d6899d32b37583e260", + "openhcs/core/runtime_array_values.py": "52f1fdf015b263affa73ece768e765328a08db81a8d48e2c033c56849d31ad07", + "openhcs/core/runtime_artifact_queries.py": "a7b8e28660b483a457f0dc6e39095f560738aff92533056507e7bff30508424d", + "openhcs/core/runtime_artifact_values.py": "83c8de2f8456ca944306d1019723fb9e90a6add55596cac273415701a6d2af3f", + "openhcs/core/runtime_batch_contracts.py": "146bf643d5232d61a085ef049af1ef982d574d1ad7ea28684c1b955d61e08966", + "openhcs/core/runtime_equivalence.py": "e62f640076e8703195fb4fa5b4196e0958f12e4086515168b7aa3a888c3a42da", + "openhcs/core/runtime_execution_validation.py": "3a485cd9ab033f0cadd718779dc0b7a2efc6749d6bf5e24afaacecc84f4bff95", + "openhcs/core/runtime_exports.py": "568671497b2aeace1593e5908f376df0680992e341292f5bf14fae0cdb957808", + "openhcs/core/runtime_identifier.py": "e558552d0a788707f6e53b7a16fed7028696cde0e61b3fd340a1b4064aceb8cb", + "openhcs/core/runtime_image_loading.py": "4c6e09f3268380ab078a73b8d4c8db4e53583aef9f047c8786edfe1fd3f105d0", + "openhcs/core/runtime_image_values.py": "13f6df2684aceb604e6475850b6dc7dd6455cd36a5e700c55bf25d4b0a271829", + "openhcs/core/runtime_measurements.py": "7e8735b96a0020fd3971282d0e9ffeca76bcb6e8803e8eac8945af2a7dfc746f", + "openhcs/core/runtime_object_label_aggregation.py": "adec2b552206f022d16848803dc6a8fd07a4b3c639ddca37255bfdb30fcb25dd", + "openhcs/core/runtime_object_label_building.py": "8f77125b439d643c6253323486d7499733cecff87456d3dddb135e89cb592d7b", + "openhcs/core/runtime_object_label_domains.py": "32824678741c1caf3fbabe621619d6d8d463936a28bc2d689916ed69fa7582ed", + "openhcs/core/runtime_object_labels.py": "3a40ced06c46ec4b1dcae778b3e4a21e3606ec0c404b9ca541ef0e31538f3a02", + "openhcs/core/runtime_output_matching.py": "61ec75c36fd1d2ae588667327df2a1ccff020e35104bc583941d85b499e6ad11", + "openhcs/core/runtime_pattern_cache.py": "4ee015704ec0fa7414b352b5e7081c9a20a56162a09b4d14d19b80ee84d3305f", + "openhcs/core/runtime_plane_projection.py": "52008bcb5f919d4d69cb7c8add19918e62ba676490d56c830c73f30e0170fb05", + "openhcs/core/runtime_profile.py": "0f5dbca26d999898005bbff8d36d72004c637d1c7b20c3573be41e718df4a3c7", + "openhcs/core/runtime_relationships.py": "98e7834b43157d73450871b9a338405a027d21909179259c3377874011ea4e7f", + "openhcs/core/runtime_slice_alignment.py": "15b38191a3099846eadd63d7aeb1a322ed8f323840a82b1882bf3abc1e8daf4d", + "openhcs/core/runtime_slice_projection.py": "cd6c7553efba0838205f153e96b15570c285894e304827d8c3f3d0c38de664bd", + "openhcs/core/runtime_source_binding_cache.py": "a96a806e61afb93fe7515176c07b7d36f5df0792354a9d6379f701cff23351db", + "openhcs/core/runtime_sparse_labels.py": "aa5128f7f743a1a99011b1418aed5d6cfd3df3a82509e04d3cc03f936105096c", + "openhcs/core/runtime_spatial_graph.py": "b37a3c8d0acf530322e85a58f10607d962623044ca31eaceea090ca1a66b5e29", + "openhcs/core/runtime_spatial_grid.py": "0d7b0e785a417332bb3269bf339adf38b0328d6bc61d4c44cbb25a9ab8e031be", + "openhcs/core/runtime_stack_cache.py": "5bcfd8292feabca891c5a3997f75907f82ee51d151aa0df0825f0c5a8b460662", + "openhcs/core/runtime_stores.py": "f7bf2a9562065ceb1d68e10438724096c1da880c6f4ee8ed2c800f876587ef5d", + "openhcs/core/runtime_tabular_values.py": "51f0d628c7cc7fe73de77573395fc23aa3cf37b2f5f0b441cfa77f39a816398e", + "openhcs/core/selection.py": "da2f314e1c642cab0e1bbd75d058f8697c9c3e86d516e15c27689469668aef78", + "openhcs/core/source_binding_context.py": "ec0e396e5d8bbb702c8afbf470498252f2388959a081a870a3f4bd1dc620bca8", + "openhcs/core/source_binding_selection.py": "798a63c98fa702236860501d53f011377e8c6ed16f2b6cc4929f7217c67da06a", + "openhcs/core/source_binding_workspace.py": "4d994eaeec43135c203dce923453e75945b17bef345833c8d3b799a82a10d986", + "openhcs/core/source_bindings.py": "ef0b856fe1532d96afb155784f9b8d189878022a886ce655f93a0ba53e2b4416", + "openhcs/core/source_bindings_view.py": "0be0d846901920153f5675019817e96d1fe7faf93bf7852adab75eb45da80472", + "openhcs/core/source_image_provenance.py": "6e9ee8f83c001fc6322925b58974f9baeb931be1ad7b4af9118fc0537c108ea0", + "openhcs/core/source_image_semantics.py": "c0488e4c7b36d10df2abe730e9002e7ce82f0eb9473148d3d56f64ca9ec09075", + "openhcs/core/source_load_plan.py": "68d26b074c2b790b820d1f723d4c1ada1ada06a6c600a2084bebaa30a0602ce1", + "openhcs/core/source_matching.py": "128e3b8be9ef3c24ebe357a385a1ca6685cacdc83545bd2dd362849c1dcf95b3", + "openhcs/core/source_metadata.py": "1e50821c5f9d1e5e4bf5873d6bb37882762af1f51c3b4b1a7a26dc7485eb2a60", + "openhcs/core/source_path_identity.py": "191955b43bc53ad4b67100e0f99359bdbbc219e911881288a3470fd700d4e45a", + "openhcs/core/source_plane_alignment.py": "a93d4ec052503e2444d2cf199aa3e5b7920ce3ae233182dfa1d67e8ea1fe4c98", + "openhcs/core/source_projection.py": "e19eddf96d814919f025ff93ada0258765ed382ec0cad2a50d45f8dcf8de2a84", + "openhcs/core/source_spatial_domain.py": "c4a24735f8ff64ca47e0fdc648b40285aefa588a48d99e72cf461526d61d2c2e", + "openhcs/core/source_tile_geometry.py": "d4d4ba711f381c3344365dccba2d729472809087605021783d371f9a21d4dafa", + "openhcs/core/source_workspace_projection.py": "1e54affed309fbc0d150bee4a3deedbc127aedf39a83f0c8a0eaf719257922be", + "openhcs/core/step_dependencies.py": "2be2f66456617fc0a1c9c86e1133d57d792dcd4988adf62960f5fb499d9cfb97", + "openhcs/core/steps/__init__.py": "fa096bcf410393eaedbf5f173ab36e5997338d652242302f17952b5da483ae8b", + "openhcs/core/steps/abstract.py": "fd95ca754bb8abcb9e40c57d228edf2bdfb5dc496cd70428509139261acd4540", + "openhcs/core/steps/function_artifact_materialization.py": "954aef8ac570544ad9654d0d27b2d65ce77f8973d4ab63d6eb96971a1c2849c0", + "openhcs/core/steps/function_execution.py": "2b0c959058812c1ba79bba0aafc41417697894fcea56e5469b1e088745975160", + "openhcs/core/steps/function_io.py": "94b789a8e0581fcfaa6862a0a7cc32c54d6696bc3792606bb457b7ef16b28b8b", + "openhcs/core/steps/function_output_identity.py": "c3c1915ef996e0dd0eb8d8d43135bdc43c461da0839bc778195e8b0ff7565d3e", + "openhcs/core/steps/function_output_manifest.py": "bebb7f1355f63c05d8a4e765919ef7184e0e0377f88e244c7d31d561804cd0e9", + "openhcs/core/steps/function_outputs.py": "d68819e0acc3af1e62bfbbe83b2dc39443207fd5de0c5a0e8134a3fe4faa83ac", + "openhcs/core/steps/function_runtime.py": "933b9102350f72a4d12842d9a2482c6f296f85ca27579443d5552d3245afc344", + "openhcs/core/steps/function_step.py": "8e1243404095691030e9ebb95bb5611679f96c66c6b6819ae71e4b41249e74bc", + "openhcs/core/steps/stream_component_semantics.py": "d146326d65514e377bbc13c0ae584ff11075c1cb54c7a3b62ebec6be40b1a05f", + "openhcs/core/streaming_config_declarations.py": "96ad52912439f8dd6b395b73b041313243f86f98f60eff8a2c3d43f97c7be340", + "openhcs/core/streaming_config_factory.py": "5a3b890539e368f9f5865064dd2ed13e98ef7a8624bcec824053d5b5b714ff24", + "openhcs/core/synthetic_plate_generation.py": "cba8fee5b39a9237883032e2a87facbbde943149f1108062056c78ce058c8378", + "openhcs/core/utils.py": "a3f2f2109a1ae97231d3cc5d00bb1eecf38746190dde0a68c4aa426216cbd59a", + "openhcs/core/variable_component_stack_requirement.py": "0f34c6ecb44cae29266e6c572b80c5393ef588a144e215fa3fe135ea29f737a2", + "openhcs/core/vfs_protocol.py": "b05f57cdc1bae7f6e3cf7b0ecd9ee1a409b865ef864fe067d822e7dc1df06a69", + "openhcs/core/viewer_streaming_service.py": "6bec8b5cb7052d118d7d44fd308721bc055dd48c998565728bf09ad0c1fb556a", + "openhcs/core/virtual_workspace_metadata.py": "25987be96327dadefa1ecb63a93b2678a0957ffdf42383822d02ef666282b24a", + "openhcs/core/worker_start_policy.py": "9493c8729ddfc7457a3a58efb605457d5aa4a54cd42c3f4ef1bd9f28593d9475", + "openhcs/core/xdg_paths.py": "d015a57a46b0052a717d03febe00065041200c7adf079639aa1ac887c24cf45c", + "openhcs/demo/__init__.py": "524c01ac2742f060b054cca816e01b0e4068fd21e5e22a5edd20dda137bd33d1", + "openhcs/demo/basic_pipeline.py": "cf31cabf828a26b7b9448d1479b262beadb3c5273b054f57e953b1663c15897b", + "openhcs/demo/synthetic_data.py": "5ff0a12afcb4e52c2cd27e5a458d33af50d393464bcb5be7e7ecd956a9b58fe5", + "openhcs/demo/synthetic_plate_pipeline.py": "3f8916ff71938ac5b53c7f55329b037653519e9592cf564257f78dde158fbd11", + "openhcs/desktop_deployment.py": "4b8c03a5396050e403e312e4ba65ffdb8047a291255e974860e4720dcdea18f2", + "openhcs/desktop_deployment_cli.py": "02135761e1c5ddda4e0239f5d29f70232010a571c987a98aa95d0bf8286bc2f0", + "openhcs/desktop_installation.py": "244ade0711356f68df9f376e2517dfe1c9fb77cc960c517f7c4e8ee418b5711f", + "openhcs/formats/experimental_analysis.py": "a7067f2e1e0c016ecaed1d0bdc5ac2b5725213a53fb83b730e798fdd664ede9a", + "openhcs/formats/experimental_layout_rows.py": "69e1428d0aefe683f1ee1cf741f83915fbaa1318d9d3f19b8bf8120583d883d2", + "openhcs/formats/experimental_result_formats.py": "ce46b895e4caafec7d026112cacfdfc496de0cb4cd5978cceb94db02b98fe288", + "openhcs/formats/pattern/__init__.py": "eecd8dabd6c2dcc5d239ad51ddc7ffe37468ca8522daef4c40581f3082c752e4", + "openhcs/formats/pattern/pattern_discovery.py": "6cb2b2fc76aed22f816ffa5811a9f85c6c3a468f645d099793cde39ef3865e4b", + "openhcs/gui_startup.py": "c996f065ad2fc3569300df50b16da6c55ac6edbc50ee7ee4c01dfb75add5d77a", + "openhcs/interop/__init__.py": "72f152d416e5f0eb718840196cf58de2dcd42c9fb2088dcd09773d8390209414", + "openhcs/interop/cellprofiler/__init__.py": "af5268368a9f2ad084ed6ac979d02cddac68133a8999e62312d6f6240883e9fe", + "openhcs/interop/cellprofiler/analyst_export.py": "2c479a6da19278ae8686f1af0548bd1b800bf38112bdafad75a8e7b6233508ad", + "openhcs/interop/cellprofiler/cellprofiler_literals.py": "13481e1bf0bb20bfc075f69309cf8ee68093acacb301a88b548a8f63fea7a8c8", + "openhcs/interop/cellprofiler/compile_time_contracts.py": "901e7eb9a9f15766934ec7c596dd77d6e484773754c75690594b9abfc6d9a07b", + "openhcs/interop/cellprofiler/database_column_dialect.py": "777d3e6b1f381abfceee90a0124d289536447e06a65bc4df01851963bba67a9b", + "openhcs/interop/cellprofiler/image_normalization.py": "a840f237daecc63d9e45e65166fd5d0a91117a4e417134e5b1fd6971026a0e6e", + "openhcs/interop/cellprofiler/image_set_numbering.py": "5399510f6a3d14a29eb164f9267f13a4b58f81336bfd8c9968abe8191d403f8b", + "openhcs/interop/cellprofiler/measurement_dialect.py": "230a4b3cf11b0912404e2131f39ccabb580695a8a2143a1a61b439b286f1efc6", + "openhcs/interop/cellprofiler/measurement_lookup.py": "b2aa6fda40a250b08fa185db589c4bd4aa26ab5c6e2712540b632aa7deec0670", + "openhcs/interop/cellprofiler/measurement_scope.py": "ebdfff6cd5abdbfb8d63c50de5e120c5019c6a9ad8062e4badf1a4963fce6ade", + "openhcs/interop/cellprofiler/measurement_semantic_profiles.py": "7801213da58054a9036261f0d46b7365951b94a40d658e16ba41b6ff365358ad", + "openhcs/interop/cellprofiler/module_artifact_contracts.py": "bf2e8f6c66a181218c8fd9afa9132922e86295f65d6102d976e1b36e41e17ee5", + "openhcs/interop/cellprofiler/module_artifact_declarations.py": "c459a1e65a671e0b15e83edba097484d018c4e9236c0e4999565221072096b31", + "openhcs/interop/cellprofiler/module_callable_abi.py": "a8f8e932547bad8e4e729e3dd17a21e79ce1d1d274e04dc1afa0f30ad4178b7c", + "openhcs/interop/cellprofiler/module_declarations.py": "cf3a4609667545ef4a2488ec3c8059181e8c845c4a59fe3034b30ef9ea5fa8a5", + "openhcs/interop/cellprofiler/module_measurement_features.py": "71ea356075abf8bffb6bb6fd6765bdb12abd0f9c0856f76b5d6002a99c7693f8", + "openhcs/interop/cellprofiler/module_settings.py": "5fffa65f175d451149d85e21e9026e36d202e5e6693389af8c4ed336ebb64e4d", + "openhcs/interop/cellprofiler/module_structuring_element_settings.py": "b69e87503275df0f3d9efb70bd8557b665b09c96b83cca48b59e3091cb58fc21", + "openhcs/interop/cellprofiler/parser.py": "419aeddc3f14afc3836632de692a24c319c311e8f01b3795108411c91dc8f7f5", + "openhcs/interop/cellprofiler/pipeline_import.py": "b547c838b2c86fe7cc5fe12d6cbf6a6715e83055c2d8c1a109ac22566d22c51e", + "openhcs/interop/cellprofiler/plate_workspace.py": "d5ff490b1d75cc35b46d11c5723befba81836f442bdc2bbdf83e2fed06e54a94", + "openhcs/interop/cellprofiler/runtime/__init__.py": "9a300dffea57601e9fa252bdc78133166d005ba2b3a161d0b8e48946ab4d66df", + "openhcs/interop/cellprofiler/runtime/adapter.py": "864b31164784b522eb8f6d3a96f790ad89b68fdfd96f2d455751b62b3d178a71", + "openhcs/interop/cellprofiler/runtime/artifact_binding.py": "85736bb5a41300a80e0fddf31bd18774751ffb7d8732b4ff0d2a087fd6a09e59", + "openhcs/interop/cellprofiler/runtime/function_contract_execution.py": "6f0f837ca59465c20f417d43223f42dae89bdbe4e3342762c0dee71cfad865a7", + "openhcs/interop/cellprofiler/runtime/invocation.py": "73a2dcab287a921ea178f56c568abf07e6a9c931e3b7e1b7badde61151a0cda6", + "openhcs/interop/cellprofiler/runtime/main_flow.py": "80d9e06ec771df610c39f3e067705558f3e2e310d81cbbadb04a2159bcd1a76b", + "openhcs/interop/cellprofiler/runtime/measurement_execution_support.py": "4ab892633e9e9884f1ff9a5ce187af5f413d051fb992e712e4731514fffc7f28", + "openhcs/interop/cellprofiler/runtime/measurement_recording.py": "4f7e3ba835a75129a6e06b7d31ea7a5eb8744e2e1ca91d354f8504bc7e5c2b44", + "openhcs/interop/cellprofiler/runtime/measurement_rows.py": "4a7fa98ab0c7fdeff4c9b9d1339efe611a87274e28219451278c4296651e0160", + "openhcs/interop/cellprofiler/runtime/measurement_source_names.py": "08a66b4565d862265cbe0a4e569561c3c645635a3581103b319ce98a9f037219", + "openhcs/interop/cellprofiler/runtime/module_execution.py": "dec3c1afe05fc06ac63db6cd0fe128ac0ba4466165147e29f3b317af21284d26", + "openhcs/interop/cellprofiler/runtime/object_input_policies.py": "bb9eafd98a41436d50a587d2720407ff0cf130219cf1516f5009a5174236f30c", + "openhcs/interop/cellprofiler/runtime/object_label_measurements.py": "f20ebdaeacab0d75f417f1c052b8487dc52238e0de0773508c49d702b2ea8bd8", + "openhcs/interop/cellprofiler/runtime/object_measurement_execution.py": "a8628103964e9d94032c55b077bb971f3c953ba0e9342101b3c99c05815dc041", + "openhcs/interop/cellprofiler/runtime/object_measurement_row_completion.py": "af7ab246a92aff7512f9580ca7a30dad0ee58954dda576fac2fc3ccaa3bcab7d", + "openhcs/interop/cellprofiler/runtime/object_measurement_row_policies.py": "9f5b3bd6867f0cb44d9cc82df49354cdf9a863a22b7c6f91ae90dfe70d1f83a3", + "openhcs/interop/cellprofiler/runtime/object_measurement_tables.py": "a652af9f4ef679936696ee4064005202456651c0c68d4052104b05ddd5e40b58", + "openhcs/interop/cellprofiler/runtime/object_measurement_vectors.py": "55249bc7b43baff02bdd461787099d4a81eb7664c86a65a5b83c9abad97a372d", + "openhcs/interop/cellprofiler/runtime/output_contexts.py": "b5eb84a533cf816bd4c33498cf8ae9ded49f18d9a55f5f55b37c00b6ddbfcd61", + "openhcs/interop/cellprofiler/runtime/output_record_request.py": "89d0230deaf8b832c723cf44bf006cd36c863dddc401c382375015807400cd4b", + "openhcs/interop/cellprofiler/runtime/output_recording.py": "14e10a666ca99cacfb438c83b60703cd672aeeeb05d816062641ee17b7eefd56", + "openhcs/interop/cellprofiler/runtime/primary_image_input_policies.py": "0c7ee04723c4c57aaa1012385c05b1298db65e695d044f7620cd10477f348bcf", + "openhcs/interop/cellprofiler/runtime/profile_fields.py": "f7631160e7d2e6ea1a8427850ddfd8cd1f5b9dcf7396fd7c33a284e42e4d8bb8", + "openhcs/interop/cellprofiler/runtime/relationship_measurement_rows.py": "1c1e4c1a56a59d38ea54dac11a41976eb7031d1c814eef62013c3e5c90415343", + "openhcs/interop/cellprofiler/runtime/runtime_profile.py": "61a9f7b76f782cdba16e639c63380567bae09cd8598bd2ac4bccf5a4844c46fa", + "openhcs/interop/cellprofiler/setting_names.py": "fcbf8f7be0e03ae0751ae38132353c69429cdf04ffd56363f9e43be002b4414f", + "openhcs/interop/cellprofiler/settings_binder.py": "45406353419db00cdae6a10c000365e6edd16ef9e2b50844b4b022064f0c7910", + "openhcs/interop/cellprofiler/source_metadata.py": "ed84f0c70e46c1188ba609d0f8db07a18b6fa566611ead4bb706897542eff2e5", + "openhcs/interop/cellprofiler/workspace_export.py": "462419949f95f28574270d4ea5ffc7aa709f4c9ad8a03c5f2a406b3fd5d4f423", + "openhcs/interop/cellprofiler/worm_measurements.py": "c2d26f72f324b59425f48dab4b61431274c3c0634eb043f1759475a95a2cf144", + "openhcs/interop/cellprofiler_setting_normalization.py": "d422a6238cc40037a9f470b0455653cebc275bbaaebdb57f5aa5f259bfd1b753", + "openhcs/introspection/__init__.py": "cab4d93163c461120ecb99fbd6f5f0875f7a71b09f54a18f3db898fa56dcf6e7", + "openhcs/mcp/__init__.py": "490d5b7112a852639d475acb1d152c5364af161fd33e81e8d05c7e0e4eaa2a96", + "openhcs/mcp/__main__.py": "100cc2ba913c45566cbad083a288e1a12c2023604e1e7438aebde011e15ab283", + "openhcs/mcp/bootstrap.py": "0e8e94cf54a32a1772e52a77d1c89a9ee02c6d2a58eb04f6e18fad2ce79d0394", + "openhcs/mcp/client_registration.py": "2b42825cd36b65c90851c64f11473048fc27c0cd8ef2c0f3259b6380f92560f4", + "openhcs/mcp/context.py": "c2e9b389f5efe75c08fb73ebcd9d898e39db2b0fd1a71784b6e76f66bc4dec57", + "openhcs/mcp/control_timeout.py": "da74437a45c2c7bd3e249b202717c7adfd178433f667d60327fa513db233d0ee", + "openhcs/mcp/dev_client.py": "c4eb92e6b8bb64664269b8c35716adce222ff96712f9d0d9b2da211384435d6a", + "openhcs/mcp/dev_client_commanding.py": "7d7b8f3c18043fdf193937766056c98e89e096238f864cf50d3831854aa0f797", + "openhcs/mcp/dev_client_commands/__init__.py": "ca5ad2d6c1424b8707625674dcb845d8e08720bab007ac23ee9dad5432333a36", + "openhcs/mcp/dev_client_commands/knowledge_pipeline.py": "fcba2bfc67470f22f944a9642dda627ac17b2a9280209500559a5b8414b5e97c", + "openhcs/mcp/dev_client_commands/plate.py": "556622ecd366de7d2573f6a6390051deb3d01935514cc0f9210a7990e682abd7", + "openhcs/mcp/dev_client_commands/ui.py": "f0c849f26a4fc2f986c64c54f7dcfdf22ca5bc14295013edd794689b8c0105d6", + "openhcs/mcp/dev_client_commands/viewer.py": "9d854b05a76d67dc2ba4c4f0873a77b95630269f60d4564f4630ab1e4e3e572e", + "openhcs/mcp/dev_client_core.py": "2aaaffec501e901352192ed35016caa6ce00122b429e41b3f7c8683eb3fd1aed", + "openhcs/mcp/dev_client_renderers/__init__.py": "dff9c54099e9717efe6938f76d3b68d249c830213df114983db0d70e6cf7e8fe", + "openhcs/mcp/dev_client_renderers/config.py": "2477d75b0f8e73aa68c977cc712c53cda0dd48a584a0f47fda793ccb8ad74d5d", + "openhcs/mcp/dev_client_renderers/knowledge.py": "0df19b17d96048a8f6372193db6ede89a444383eec30a4c128f3ba79c96521a2", + "openhcs/mcp/dev_client_renderers/object_state.py": "62e6fc0bf3a9975eccebf36d4a9f18e52c9a1f19c947bfbab7540aa90590c355", + "openhcs/mcp/dev_client_renderers/pipeline.py": "58e4a2a3a4776a738791b70ac23eadc01032abfadb66353c2a2c2d1660ae6a4c", + "openhcs/mcp/dev_client_renderers/plate.py": "a17489fcfbb484c4fffcbc242d8fbf4942d7f2af148cce3af4ec5ee9803b48f5", + "openhcs/mcp/dev_client_renderers/ui_bridge.py": "a8a2b13aed35b6fb54f93ed42993fad6376176949f0a68c6df3e662aadd5a59c", + "openhcs/mcp/dev_client_renderers/viewer.py": "4d01b0968197ee693030df8dd79d3dd179594fce9c5bf2bd3cc0e76d26011841", + "openhcs/mcp/dev_client_rendering.py": "71928056a3cd0fbcc8705bbba53fc388ccad18beed7276d05bc2a74fa5b139bd", + "openhcs/mcp/execution.py": "7debd0155d1b822bc9220a5f0b4d6976f57f6142db65b749d4b6b9752452e067", + "openhcs/mcp/http.py": "56430a939d65607847486d417c693c91523bf5571cb969389c37ff917f4793c4", + "openhcs/mcp/http_auth.py": "1c2801842259c91a764940264e07ad5a05cc6492e42f04a841b0ceeb026d73e2", + "openhcs/mcp/installed_demo.py": "4ea6c0730db6d3938eea1cac558dd46ea5c956ef151a5d68b8ea956e8bf57655", + "openhcs/mcp/lifecycle.py": "671859916b91d5e2f4ce9c5cd329c6d088800d3b8282e0d9505c49c183c7c68b", + "openhcs/mcp/memory_diagnostic.py": "df7adf5331f3020cec231fe4156196c9088c43078586c3018ea189946e0a4985", + "openhcs/mcp/memory_diagnostic_launch.py": "4bf3153320cc0f6026e0f130084ff41d117daa7ed157908edda4c4eb95223c92", + "openhcs/mcp/server.py": "9755e43d18ea8913dd13e078623728967c5e8ce7eed27e8e2a29e50470e41c1a", + "openhcs/mcp/socket.py": "9ec9850cba0868ba6e2d88ac44031babd10d58603c440967396b8c859cfc904e", + "openhcs/mcp/stdio.py": "ec2935e01e9dc7c0ea56781c174f490528002bd736ac19062bd3a848f7397e7e", + "openhcs/microscopes/__init__.py": "8c02c109013bd33aa2841548a7a5ee1d241e11dc5e80a3afd83a87697f3f5f40", + "openhcs/microscopes/bioformats.py": "50df7ed37d246d424216acba266ce6a7be54a8242b3663e41597b0f414408597", + "openhcs/microscopes/bioformats_adapter.py": "6f50083a1261811fe6ed22287acff99ff01cc304f02640a43178e5e67bd0725c", + "openhcs/microscopes/bioformats_well_key.py": "2ebb92085fb6f1dee7b49e4d76dd6e9ea7f69553f7cc82746110829afbf93a17", + "openhcs/microscopes/exceptions.py": "5bfd6936b250e410c48999b13bd79d9213348bb76fd2e6df50171e0ebef7e763", + "openhcs/microscopes/handler_registry_service.py": "b90a881907e2b38fb15ff0b6220c2075107ab56bbea50aa4976709d27d01e3d4", + "openhcs/microscopes/imagexpress.py": "52cd1d7f8b585b602544e8dd2a278564ca5413cf15cde7dc5548ae133ffdd684", + "openhcs/microscopes/imagexpress_source_metadata.py": "6b621641236786eea64254c5ad71256e10d40820d79e894f8366fc4f8bf67cc9", + "openhcs/microscopes/microscope_base.py": "e9542540fffdf8944c584a351a63d6992381cd74ce231aa9b18617d474a4e8e8", + "openhcs/microscopes/microscope_interfaces.py": "cd5c39679bd41ce7805d2cf0cc66a98e7ef441e9026ec3168e625406b51f4223", + "openhcs/microscopes/omero.py": "341733257cfb4c3bbe35dbc311c60e1666d357af6c6b7dc416399f77a92c570e", + "openhcs/microscopes/openhcs.py": "0ee1d23324f21963f913d14136da2563f6c899a1a70b88785520a6c1be27dd69", + "openhcs/microscopes/opera_phenix.py": "ab2c732c47c4adada329b7d1e667a89ee1df667aab7e7182b9d582d67290ae6c", + "openhcs/microscopes/opera_phenix_xml_parser.py": "b80af276e29671a29fd4c710a12cd2727046a7164e4f1a1d22094d14d734f74b", + "openhcs/microscopes/source_bindings_handler.py": "efee19453c9f125e00c230170c0ccedf8e10afac8b9858430b2023a833f4eb54", + "openhcs/microscopes/source_schema.py": "7bb5a27358d82f770b62ba1a6ea443adfad5c6fd8289b6bc2426a2ccfab42fd2", + "openhcs/microscopes/tiff_metadata_mixin.py": "fc58d675d554db1b1ffe5653918c374eda6263e56fdaefb733f6ab7ebced7cd5", + "openhcs/napari.yaml": "a42e9d6288fd43d0855154e415eee0b998d120934754759c480e529c30bacafe", + "openhcs/napari_roi_manager/__init__.py": "640090b6aabd56c4dba6355eb3e90bfe1effab6c6804a57cd0c49e047b751e02", + "openhcs/napari_roi_manager/_dataclasses.py": "e46de1e7b34e98a0a3d39ed9f765a86d24872328e6ff155fec3b773e7938a566", + "openhcs/napari_roi_manager/ij/__init__.py": "3e0c566b0f58a7c153dbadc8d1d6baa9cf264d28f24f7573c8fbb4fb7925085a", + "openhcs/napari_roi_manager/ij/_convert.py": "95fad112389585ca91f808144249f1d1676ed3fb9b2e93a2114cec9ae00c132d", + "openhcs/napari_roi_manager/widgets/__init__.py": "c552d2cdb9cc9ef9469ec03771340a4b210f999b83182e08b3414e12f0284541", + "openhcs/napari_roi_manager/widgets/_dialogs.py": "dbd8208a9f09221d5585755795bc4c7e135d5566910cd2c80b8b7cb4aff3be01", + "openhcs/napari_roi_manager/widgets/_roi_manager.py": "9ae7518576902c4a6d0384e387a08ae73528b39cf3878039c1fc70cfaf6c8a19", + "openhcs/omero/README.md": "8d684ecc2990c93f3cf46d45ed7b15d68b478c3f0bdbbb6e11a67f49dd8e4ac8", + "openhcs/omero/docker-compose.yml": "42be16bd692852af73c92ab7680b8762cc0142c2b0b12bdaceb88317e6b1bedf", + "openhcs/omero/plugin/INSTALL.md": "36f346679688b62369b6246cf07409be659b1a037f6f2ba4a7e8dd84e88b4642", + "openhcs/omero/plugin/MANIFEST.in": "d1c67f527cc46fb6e3119a09130cc8be5d160d7c6314abeae5b1a1e35d5d30f6", + "openhcs/omero/plugin/README.md": "5257f97a9b39ade8c43e5824019924243785d971398f6fa98894e18cd129146b", + "openhcs/omero/plugin/omero_openhcs/__init__.py": "6bcaf437e9d2d1696972aa9266bdd1f05641bd1ad6e3608a60ef713a8a5ebf8b", + "openhcs/omero/plugin/omero_openhcs/apps.py": "405391087c0014ebac3ae8a1a8fb060bd90284f1e07988ca8b2d1aac18a8c31e", + "openhcs/omero/plugin/omero_openhcs/templates/omero_openhcs/panel.html": "2dbcd164bc4998c75f2bea9d31f392db5171a532cbc0b3d4870e7a9b4e3798e2", + "openhcs/omero/plugin/omero_openhcs/templates/omero_openhcs/webclient_plugins/right_plugin.js.html": "05bd03fea74edecd983d8221d7eae4ce44c0c3297d0bf21cec880c478d9c2ef1", + "openhcs/omero/plugin/omero_openhcs/urls.py": "062d2ea3134fa95d21b4945c03682c7fc26858e395422d038d0f5eb2fcb6cf06", + "openhcs/omero/plugin/omero_openhcs/views.py": "4ebf08b49719597c8991a0da558581c369da5ca8a6f885d25690407cb1e16b1d", + "openhcs/omero/plugin/setup.py": "4dacc99c1e497842d2b64199323d0a15e58abed4f05926c60026fdb1f323c2cd", + "openhcs/processing/__init__.py": "2e37d68e60060a6dc41b81b65e00e6ed8a64e39dfc6d623972bb30e41fc750b5", + "openhcs/processing/backends/__init__.py": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "openhcs/processing/backends/analysis/README_SIMPLE_IMPLEMENTATION.md": "d1e68464555e50d34bc185125cb4990ac98db1fa0698c98db1f5d451f171607c", + "openhcs/processing/backends/analysis/__init__.py": "2c2a87306df07f20daad2c37c81318b0e80fab9d3b25538ed722f54338a783bd", + "openhcs/processing/backends/analysis/cache_utils.py": "03eafe55a0739d73a6b08193e3ce17623ac6db4bcc980acc2eff636c604e717f", + "openhcs/processing/backends/analysis/cell_counting_common.py": "f76fdbc47aa26e37ebb5dcfc5c46b6a6ed71ee2c11ca0ac69bac802578a51563", + "openhcs/processing/backends/analysis/cell_counting_cpu.py": "f8ae9aec542ffe2587b70e2c2eae4d29c9589e7354c8b0296d11e2588257876a", + "openhcs/processing/backends/analysis/cell_counting_cupy.py": "884d218dfa6da9735afaf65258dca1dac015e59463d293bebd78f814f3a6ca32", + "openhcs/processing/backends/analysis/cell_counting_pyclesperanto.py": "8c4df721f6957ac724514874af63ec040837612e8cb98111234031fdf35d2054", + "openhcs/processing/backends/analysis/cell_counting_pyclesperanto_simple.py": "5262773b82d1c1cbd676c2277b3ac3784b06dc8f07818f5efa8e97c843a630ec", + "openhcs/processing/backends/analysis/config.xlsx": "3333fc392d79c86f80d1f973fa31463b97ad5a1649f1255af2ad460c0e0a4bcb", + "openhcs/processing/backends/analysis/consolidate_analysis_results.py": "aad546fdc069653d809f9ef6867ad2ae351276ea9ab66b377af7cd6ab2428d60", + "openhcs/processing/backends/analysis/consolidate_special_outputs.py": "30e2582946e21915ca318c5aafca8cb8cede94459d038ad6943c5142663bfff8", + "openhcs/processing/backends/analysis/count_cells_simple.py": "cf45eaaa771e40da20a549fd7b21e831a0ae03968623ee22d67c718a3edb5d19", + "openhcs/processing/backends/analysis/dxf_mask_pipeline.py": "80e5c5afc54ac1ea44b6002fa5401515cc907ebcc1de16525047026e5087d67f", + "openhcs/processing/backends/analysis/focus_analyzer.py": "a9570564cd65d7709c3cbffc05ccca5286284df267c25afa0e472f9064e1df9e", + "openhcs/processing/backends/analysis/hmm_axon.py": "be57f02e249bae5058d02caeea185b92721408a0c05689c7832e8f30ef71ec07", + "openhcs/processing/backends/analysis/hmm_axon_torbi.py": "72e2ffd288168a609ffb2d34943b421b51d6d0562ccafa52af350ff0f1eac92b", + "openhcs/processing/backends/analysis/metaxpress_utils.py": "8795c96a7d4355d363a742b495e770ce4805ec4f69fe0d131664fca4fea398e6", + "openhcs/processing/backends/analysis/multi_template_matching.py": "9f6ab5b607434b62012a37f0953e0b4f68e89699935c7792143fdaac8b535aee", + "openhcs/processing/backends/analysis/neurite_outgrowth.py": "af0ea1116315755200b5d70a9317efd1f52f80c87c702ce11a3a504bf95b3b16", + "openhcs/processing/backends/analysis/region_properties.py": "412b1801fdcb840c4dcf920437acffacad15475a0f470d943e1e0ceb712b8628", + "openhcs/processing/backends/analysis/self_supervised_segmentation_3d.py": "df7e482a4dd46923bae0a14e964451431b10419278d720ae3c12389d1f0fe19b", + "openhcs/processing/backends/analysis/skan_axon_analysis.py": "fffa146fe169de6eaf15c29dd34851bad66f4248a0231aee32df93587c93fabb", + "openhcs/processing/backends/analysis/skeletonize_and_save.py": "e3e6fb3d411e405670de1f10dedcc32ceea78031a62d25cd7eea7c2079d2ceec", + "openhcs/processing/backends/analysis/straighten_object_3d.py": "3f465c19f6b955be1a6d5b4489a5e9d3fe6bc74b3761fdd3447e08c3100b27eb", + "openhcs/processing/backends/analysis/test_simple_implementation.py": "7a629a493280086c72693a34a41136e88a75ba6d4f717e0497e8c732d683384f", + "openhcs/processing/backends/assemblers/__init__.py": "f76d402c04f7d29cfff92547798f16365b7742d3d2b7207a4fcbb6f361da6dbf", + "openhcs/processing/backends/assemblers/assemble_stack_cpu.py": "512503d7f505c6e71faed0c94105a5d68c9830b5c33d0c303a6d7ae5e346e943", + "openhcs/processing/backends/assemblers/assemble_stack_cupy.py": "79e19458c2fc53fe39d7ad4f25d099b0eb8fcbe4176f986e8930a43179f87c60", + "openhcs/processing/backends/assemblers/blending.py": "98edc98db410e54275f6b3f23fb5f7187ed11f7635faf85af7b37a4b34aa5d77", + "openhcs/processing/backends/assemblers/self_supervised_stitcher.py": "a84d4eebc9984fe18219fa6eaad77be2ae66ce48b96442f66512738ac2d5ff67", + "openhcs/processing/backends/cellprofiler/__init__.py": "9b53f15d83fb8ec5a855943931550cfbb4b726063ab630bd5ff2cd0354d52f5a", + "openhcs/processing/backends/cellprofiler/_backend.py": "7c22c49b8690d527d68098931124c18b54e514c0d0b9cf9ce1618eea737b8e5b", + "openhcs/processing/backends/cellprofiler/_granularity_native.cpp": "c59e6700c1e377bac257e51b82d4ef514be2e8f848bdb6ade486556be1877bd9", + "openhcs/processing/backends/cellprofiler/_preparation.py": "06fd520d5e443c7dc217f7a16736af8d39c86eb69d22f1e0f98fbef81701dcdf", + "openhcs/processing/backends/cellprofiler/alignment.py": "65c72c3d69c9516623ebf856ab45cbae8ecba866e7aa6c2bcde951c6ece436e6", + "openhcs/processing/backends/cellprofiler/alignment_mutual_information_offset.py": "0fda181e2f5b885d5eee1b55e85e55a89978c712d4bfbe2fa369c31c65d316e8", + "openhcs/processing/backends/cellprofiler/area_occupied.py": "45baaf2854980dfd769a3f8a581f6fb7bb75d0baea69236c5dac04da75745250", + "openhcs/processing/backends/cellprofiler/calculate_statistics.py": "f08ad4069f88245206e9f3876e6f45e76cb6f83c9f59fba9ee79a6d452d6dd56", + "openhcs/processing/backends/cellprofiler/classification.py": "eaad500708ccfb49403139f547d507d91381097221b0a726017e844bc414c4a9", + "openhcs/processing/backends/cellprofiler/colocalization.py": "1a076dd40300e0e388b91f9c6dd4e6bfa23b764d4a507f97ac6a2d358729bdb5", + "openhcs/processing/backends/cellprofiler/colocalization_costes.py": "b085396b457621289bb5f1a5c6045b585b45378c2a724e57b4d9fd731085c2df", + "openhcs/processing/backends/cellprofiler/colocalization_costes_prefix.py": "50630037c121b95252598cac69440f576c72c11bb5c4583855a250255874b292", + "openhcs/processing/backends/cellprofiler/colocalization_costes_prefix_numba_pearson.py": "146d77121d04b23d9804bfc22e08613fd182e1fcb128f5b62bbe87f762acf165", + "openhcs/processing/backends/cellprofiler/color.py": "3abea177ce41b4bd14d2857dc5db2c203ddbf61a711dcfaec90d8f8e47aeee7a", + "openhcs/processing/backends/cellprofiler/contracts.json": "5343868a944862d8a256a3b49b519fdb985277597bceb6dc415fa863f84e4e6d", + "openhcs/processing/backends/cellprofiler/crop.py": "2dab13183d9e5387add3ac3df38fd741da08b6301501fde53f65ed4123e6638a", + "openhcs/processing/backends/cellprofiler/display_modules.py": "52d77ece478befef1d50118be55eec5094a64ac6c90f10ba290eebdf9f3fd29a", + "openhcs/processing/backends/cellprofiler/distance_propagation_numba.py": "12c4109952634010f8d2e1b8554cba21bb28cd0b6f925c70861081fcdd961be3", + "openhcs/processing/backends/cellprofiler/edge.py": "6521c2e854cc64b28cf6a373c023c01e3dab30e762487d870617669d01734427", + "openhcs/processing/backends/cellprofiler/edit_objects.py": "516b87af88e9d4f5c6179f3b247a867045bf73c67c032c77757d18ace4e03a5a", + "openhcs/processing/backends/cellprofiler/enum_attributes.py": "4645c76a44157ff8871498a6a473c0c77a218cf3c7bc46d5ba79d6e77a8790ba", + "openhcs/processing/backends/cellprofiler/export_to_database.py": "a191f39bca826a03e7736215a4e4e1c46b3233753729fb78804e0d6b0d43d7b2", + "openhcs/processing/backends/cellprofiler/feature_enhancement.py": "03cf091a3fe0c8b14ca343f2f40d5918519259a116d5a8a48dc34f8132c9dc4b", + "openhcs/processing/backends/cellprofiler/flagging.py": "e0c79fff08a470075b114c63e40d04aa45c280d8d839aae4f61308c96c06b470", + "openhcs/processing/backends/cellprofiler/gaussian_filter.py": "097845b6e35b53448f79559f19678d439167950f63922836351b347ba2cc27ae", + "openhcs/processing/backends/cellprofiler/granularity.py": "979e653134edd62a8c4bc898ea13071f5331ab90f99618c0616dddc4e0533ad0", + "openhcs/processing/backends/cellprofiler/grid.py": "df4aff3d02f6aa501d3a4bb452e0020cbbd6827f3321fe552d1161b0ac257f02", + "openhcs/processing/backends/cellprofiler/illumination.py": "10795d6c1ac0fd89da1f88433b44409d0298c35a94165f0f0653454a5c0eb3c7", + "openhcs/processing/backends/cellprofiler/image_geometry.py": "be8b4aa4700b17f3c987b2c88501589048cde0a62278a69e4b11d9111d736c77", + "openhcs/processing/backends/cellprofiler/image_math.py": "2c48fb8dd6f9ad3c9021e9d65029b698cf59d52f1a4c348e98697250fba6b9cd", + "openhcs/processing/backends/cellprofiler/image_quality.py": "46e5301b3edda6857d4b3d187b4aa7ee9ee65a6cb06561d5b616686d1af6b77e", + "openhcs/processing/backends/cellprofiler/imagej_macro.py": "36664137c90f896d0ff093e0ebcb7bdb4717a724ef2a899f3629b28e52c4922a", + "openhcs/processing/backends/cellprofiler/infrastructure.py": "83ba9be2d39e9917fbb4ce31ac010d17a3f16716ce22c52f3f41e46d5844b5a0", + "openhcs/processing/backends/cellprofiler/intensity.py": "ae55d0e21f3600d3f53e1dda76b70604d98997d5846f0b68d9fb67887321d55b", + "openhcs/processing/backends/cellprofiler/intensity_distribution.py": "e5a400a1cd6915701e72ec824fe6f2c16e14a266b0f184eb3ae1b6f154048a53", + "openhcs/processing/backends/cellprofiler/intensity_object_quantiles_numba.py": "f2cabccad8ee096b2b07f0a453ad558ad4d87eff42ae94c9f50b6386f1334ac9", + "openhcs/processing/backends/cellprofiler/label_geometry.py": "2a738fe2767b124a7c97d4d3a1d6f2968d94e398856248f21e6cf750835cf176", + "openhcs/processing/backends/cellprofiler/manual_objects.py": "0d3d38a91c83dce72aeda280cedf4cd90d3c4dcc2eef28595b9e4669eeb0ad47", + "openhcs/processing/backends/cellprofiler/maxima.py": "11c56a566305e73366b96c388a8c355cc47ace829b9f1be90f1e73507401011f", + "openhcs/processing/backends/cellprofiler/measurement_math.py": "2d7be24588e37955c687b54c1e62ed7294e78e02165ca636223666b9a4fd5472", + "openhcs/processing/backends/cellprofiler/medial_axis.py": "ec23045ed572a5a235043b7bbbaae40945f1e57894340cb41c946b2b92adcfd9", + "openhcs/processing/backends/cellprofiler/median_filter.py": "6f5957de0ecf774bbcf96611995c17c780fe3fd96685a01459f6da0052f56618", + "openhcs/processing/backends/cellprofiler/morphology.py": "bf52da0b5e8db68f613deb0bd5c61f76e1c49d6e6b8a8b1117c9dc6e4b0ea647", + "openhcs/processing/backends/cellprofiler/neighbors.py": "2768ecaf850ad3045b8aa3d6a9e0fd5550434407a732b066dd68cd5c19a6794f", + "openhcs/processing/backends/cellprofiler/object_filtering.py": "d80fab4019854fe51c9360c04f0256f42ff26486ce00814bbf7cd267a00aebd6", + "openhcs/processing/backends/cellprofiler/object_images.py": "fff4141d1abaa00dee3f9e2a8ea2139c84505c5b179be91a881c0cfd24f41e77", + "openhcs/processing/backends/cellprofiler/object_overlap.py": "86a30689e69dc7c10b3052e4f87235e3c878325b46f1a74e53651c881dedef4b", + "openhcs/processing/backends/cellprofiler/outlines.py": "fbd5eb9ca3134d052b0db8c8c198b9c58070b040f9b848c8b49b6a7f0a83be70", + "openhcs/processing/backends/cellprofiler/perf_fixtures.py": "917d4de46d31316837188549a200d8de9c8bbb937707f83cbb372740c2b48475", + "openhcs/processing/backends/cellprofiler/primary_object_diagnostics.py": "dda963200f7dde9f016ee0e45630259388718633e5d94142fdac5a41f18db867", + "openhcs/processing/backends/cellprofiler/primary_objects.py": "36d7a49313ae32e6382483ba58dd0fdcdc23fcf412d2059932bfd3c1487f07b8", + "openhcs/processing/backends/cellprofiler/projection.py": "c64d0e6f7895eac43454048310ac5fc11c1f1b06a5dd2d954e0c4e1b962e7090", + "openhcs/processing/backends/cellprofiler/relationships.py": "e79ebc1108b35c91727529763fbd4ca37643c915449e08eb161cefee58da9713", + "openhcs/processing/backends/cellprofiler/save_images.py": "43564b98f64151803363d78f131fbc93ab0cc43f2be1a0e9fe9e3c34f3470188", + "openhcs/processing/backends/cellprofiler/secondary.py": "0ed391a057e2a9c1b943938de3920119844b7f86fd6932e54227473a2d6329ad", + "openhcs/processing/backends/cellprofiler/shape.py": "1ecd193454bf62479cd13fee34245b85bf83e846f5a39f8057037ade10065896", + "openhcs/processing/backends/cellprofiler/skeleton.py": "fdf222ed7bcdde7e81d683ccd61c78f8591b11f625c6d285012e20c322e440ed", + "openhcs/processing/backends/cellprofiler/smoothing.py": "adf70be2e2564564b65c4fce855cd9d6e702486f419cca6870b2d3d45daaa23b", + "openhcs/processing/backends/cellprofiler/spreadsheet_export.py": "a641fd459a776b046a74ea01d5052a78a0556c4626995f40c492b7e6fe31e76c", + "openhcs/processing/backends/cellprofiler/structuring_elements.py": "9e2c173ecacccf14d282bb05e7fb2849d2172223245cc83a31dc3b4ba60fb749", + "openhcs/processing/backends/cellprofiler/texture.py": "fc55e27a4f5ac3e041c9a6ec4607b4c9a989b2cca9ac6714d618d89102ffcdc4", + "openhcs/processing/backends/cellprofiler/thresholding.py": "4ed28e53ec30580fb9558b94ad4c50eb17a59b61a4a516cf992b388fadf887d0", + "openhcs/processing/backends/cellprofiler/thresholding_threshold_numba_diagnostics.py": "06b9018424f4245c6f97c43d39481b0ac267d3bb6359417834cd30ba7b27a037", + "openhcs/processing/backends/cellprofiler/thresholding_threshold_numba_diagnostics_quantized.py": "6650d7e0f35538e8a03827443b377dff7b5debc941baaaec5228c3a81671ab42", + "openhcs/processing/backends/cellprofiler/thresholding_threshold_numba_otsu.py": "2e86d897437cdc22279da20ab442299eed93c3144bb2434efd188a396c837870", + "openhcs/processing/backends/cellprofiler/thresholding_threshold_numba_otsu_histogram_multiotsu.py": "b9362aa20a6ff625d9391df7ba7f267f67dbe8d8e07968df0668119f297a0f57", + "openhcs/processing/backends/cellprofiler/thresholding_threshold_numba_otsu_weighted.py": "a4a78bc16feffaa8a8dc80cb6d62f0a6aa93fffcd1be33c0fc058f47f9ffb346", + "openhcs/processing/backends/cellprofiler/tracking.py": "893f4b423272dcd95b28954bd4c84cf31021457ed2b3466a67ddf017ca3e55c3", + "openhcs/processing/backends/cellprofiler/watershed.py": "e6fa20e6964b8774d1667dde66e6c334adc0dda3ff855bb2fa601357a3af18a5", + "openhcs/processing/backends/cellprofiler/worm_geometry.py": "7ee66581908dd9a90f482e1612fe276af85632ff0175f76c9934a41fa300bad7", + "openhcs/processing/backends/cellprofiler/worms.py": "9a03e5d77e5b1113a72d5301cf07b17886287dbcbb1ac768b252142000ff8eec", + "openhcs/processing/backends/cellprofiler/zernike.py": "90582e0df2ddfb28cc2e3f0f2a2c22f8a3e113d2eb5e0924f906ab9d3665ddd5", + "openhcs/processing/backends/enhance/__init__.py": "7406b3f590f73fdb2876f41886644f8aa6bdf77d54de4aa44b4c3f0179c29da0", + "openhcs/processing/backends/enhance/basic_processor_cupy.py": "5fa49e3e7194f537400c1b217f480124778bccfd86bb68d4a0d385f956870f78", + "openhcs/processing/backends/enhance/basic_processor_jax.py": "510c08d8c8a8cb66623136d6415ebaa001898c9f735f6d0e5e05a399250ff84e", + "openhcs/processing/backends/enhance/basic_processor_numpy.py": "c238e0a82d8ac50f48cf23b9d8ffca6acda6145f567a3185ccb276850cd7c5ba", + "openhcs/processing/backends/enhance/cupy_clahe.py": "abd37da64bf7762f1295ec9f51ce5d2e3d4bb20a436eefd5abc2994f5f49e118", + "openhcs/processing/backends/enhance/deconvolution.py": "84744a86b0caf41e07a7cf85887dadcd2569b300eebe8d516a9f19a591b0667f", + "openhcs/processing/backends/enhance/dl_edof_unsupervised.py": "0cb85a866495ac53405046f4ad9ae0adf9494fa444ce96a038dca039792cc0de", + "openhcs/processing/backends/enhance/flatfield.py": "991122d1ae9c3367e3f96a92959e96d3a0a46dafde68978e2405321e1b3a1910", + "openhcs/processing/backends/enhance/focus_torch.py": "a8d95fb34f0a5672e594cfd025176e6e64e4805a98ae90446ded67c8cf2ba096", + "openhcs/processing/backends/enhance/jax_nlm_processor.py": "88f81ae0848d13416ce00eb1957d21d81b9d1cf9b0daf37869bf3ff6deeb64b6", + "openhcs/processing/backends/enhance/n2v2_processor_torch.py": "64ff854e1fe29c6b11a74862c25f249b029e39b1a30c63f31ce1b016f9ccd4f9", + "openhcs/processing/backends/enhance/self_supervised_2d_deconvolution.py": "ad6d8bc92d520913c4c97424fdc0bea7f177537ba52fd85a111c3ff6189b6cb8", + "openhcs/processing/backends/enhance/self_supervised_3d_deconvolution.py": "134980f379ae418af782aa35f6bda3fa258a52335927e1ef6f06178793a3d2f7", + "openhcs/processing/backends/enhance/torch_nlm_processor.py": "3f3f1871d7918b4b905fdc673d2221a80cca47adee3b0c41de3fc71f75381825", + "openhcs/processing/backends/experimental_analysis/__init__.py": "dffe49d8706b4171e6d137855aee097a04b6088da466061667b9f057a1d596af", + "openhcs/processing/backends/experimental_analysis/unified_analysis_engine.py": "316be268b761bfa74ed1df137f2ba014a1f1c273aa9abd598a8a2e10388afb79", + "openhcs/processing/backends/lib_registry/cupy_registry.py": "5c87a6556d6765a18340c1c07501883211af51e4c0a9ee672b48dc51ea7d26ff", + "openhcs/processing/backends/lib_registry/openhcs_registry.py": "7f3cc2715398b1a899925023b0dffe43a44b8244454245b94a608ff872fbd395", + "openhcs/processing/backends/lib_registry/pyclesperanto_registry.py": "9049165370dbea917e643395c516e08e94509777d9bf8edf9476cf1e75bccc77", + "openhcs/processing/backends/lib_registry/registry_service.py": "d9e6e30b627fae7b01e08872c58ad26ee060919b6b6bd9fd9174223f2ec5a95b", + "openhcs/processing/backends/lib_registry/scikit_image_registry.py": "21b71abb7ff213806c9a591f210e7c0f512432d321e53fe23511f73f02e896b9", + "openhcs/processing/backends/lib_registry/unified_registry.py": "e00fd8d09e6bc384264c19777fcf466c8e742a810b09d04b667ae524e45ee07a", + "openhcs/processing/backends/numpy_runtime.py": "abcd1ee27980bb7912326d97e0f4b2c4b32c15e4a04b1bd3bf2c95d2514fc8dc", + "openhcs/processing/backends/pos_gen/__init__.py": "83acad53ff4981191b792e0a5585131214d3ba88f03c9a88f2f381421e8455cd", + "openhcs/processing/backends/pos_gen/acquisition_positions.py": "c24035d60d123dacaa1a092db6fab359d1ee41397cb5e8256172a03d88825f35", + "openhcs/processing/backends/pos_gen/ashlar_config.py": "ec595c8681c1d9fdb1facde8e2b462ef1dc7bc8053d19be6f8f48b950037751d", + "openhcs/processing/backends/pos_gen/ashlar_main_cpu.py": "2b45b34f378be4b9a35cc4f2b20972467acb334d26acb20aafb16ac113378860", + "openhcs/processing/backends/pos_gen/ashlar_main_gpu.py": "ee135e3819544fff966f0a82b4722c9289e6665c77f6347690d7be86ed69cc1e", + "openhcs/processing/backends/pos_gen/mist/__init__.py": "a752dce85aa3a2bc49b7dda9c39660ef26848f22750d39eec68da2f4b106f558", + "openhcs/processing/backends/pos_gen/mist/analysis_notes.md": "cb4a6107383366016004c5e28300751efcd0167fbab91235fa5a15da2434e8c0", + "openhcs/processing/backends/pos_gen/mist/boruvka_mst.py": "97f19fcb0b98111011a057635250a4e21719462f8e9dc96857713d31aaa33cfb", + "openhcs/processing/backends/pos_gen/mist/gpu_kernels.py": "9f0a6a4f8371ce77b9bc69d0978684d28c57846b6440659ea44bbfd7ab1ab935", + "openhcs/processing/backends/pos_gen/mist/mist-algorithm-documentation.pdf": "addc2feb0c3a31a270029bc67a5f293a3e798af4e0629bbbd67784bd49754dea", + "openhcs/processing/backends/pos_gen/mist/mist_main.py": "ff886bfe2215b9a90d17da5c8517ff630099caf119d9b15c40e709529fa209a0", + "openhcs/processing/backends/pos_gen/mist/phase_correlation.py": "d7f709686739b344e9dd2b44c29c2a33cd8d2bdd5084d7e46656a67b8a8ce68b", + "openhcs/processing/backends/pos_gen/mist/position_reconstruction.py": "34c4f7d3367d655557ed7cb72acbbadbbe7cf71a36375f0b464da95a8c340498", + "openhcs/processing/backends/pos_gen/mist/quality_metrics.py": "7a738d883a4a97e2b2081fad191bd129d227e161ccc93a8306b1fbb906baea22", + "openhcs/processing/backends/pos_gen/mist_processor_cupy.py": "c0297a6b3d6297f3d995a75915b5d2bc0b3efdbfcff2535912b5f030def35f8e", + "openhcs/processing/backends/pos_gen/tile_position_artifacts.py": "58c43c34a10f5ddb573247dce518b9ab98a588a923d2dbc3f4834f73c3c334e8", + "openhcs/processing/backends/processors/__init__.py": "6aa79aa19e049fe5e801ca49577a6e7ed4e6c3c4f7560c8cab16b06918fc16b2", + "openhcs/processing/backends/processors/cupy_processor.py": "088c4527053db8d5137491d8274d2f75eddfa5d6d3475e5703e17b552ccb61e1", + "openhcs/processing/backends/processors/jax_processor.py": "8f34a67eeb2a74c12d8a1e0062c0a60ca73dc627e388782121d99f0cc9f63a28", + "openhcs/processing/backends/processors/method_axes.py": "21e4e9b0a1fb376c95b91c01bd5b893a4d958b656a5629f7788ea0b80360c8f4", + "openhcs/processing/backends/processors/numpy_processor.py": "7152a478305b90c7910b7068ff476cc8d1d0063c7be658f4c5e5e2a0650b99a0", + "openhcs/processing/backends/processors/percentile_utils.py": "6f36816eb0988183efbda89921a91d424451a9d7cf1a1adbb128cb7ce0d31edb", + "openhcs/processing/backends/processors/pyclesperanto_processor.py": "0504e1434a489005a1e0c3d7edeeb306acf1b4f9aba463e45ab0c817970a9652", + "openhcs/processing/backends/processors/tensorflow_processor.py": "3ecc376b692c94435f470dbd6a3066fade6e424f28dd620441e1c3921f8c33b0", + "openhcs/processing/backends/processors/torch_processor.py": "4323080152c5810139e09140477ab24de0b6f11f8205f20d2ae13e11d0767e77", + "openhcs/processing/custom_functions/__init__.py": "375b0fb11eabec55990307fc0b6d082e1611fc2a1ab9bed625d9835bb358ad37", + "openhcs/processing/custom_functions/events.py": "ed62fe7aef90d56ef4c3421d61257a272e9a54f7ec2005cc865ff0e605d8ea35", + "openhcs/processing/custom_functions/manager.py": "f2271f396ab4018ad24e4e8844665bd181bc8baef383499b7be60e9644b3c1db", + "openhcs/processing/custom_functions/runtime_registry.py": "3b1eeb412dbc5a61b1322d709680534e399bccbd697f133415308dde38200827", + "openhcs/processing/custom_functions/signals.py": "027020e7c3f3bdad0cec85b941afe86c471c11a4e6c2fe377e7a625b53ce0325", + "openhcs/processing/custom_functions/source_namespace.py": "c921b5dfb8fe1a59eb4dea180d45be64e962d5b49f58f97236e053da93d63e40", + "openhcs/processing/custom_functions/templates.py": "e114252a60ce5e57111519b98b44954f08733216335fab64413b643f5af61e50", + "openhcs/processing/custom_functions/validation.py": "245ed87136deb3176438f6c4d8d3b401c528bcbf8363a817c50c70c6867b794e", + "openhcs/processing/func_registry.py": "caed456f292f237e6c573fe4985b268261d4873469a0bb14f7e57be71b1fd7ac", + "openhcs/processing/materialization/__init__.py": "132f2a10933d539caf4489c4391fc9e305aea4b0ffa540699bc9dd5f71905b18", + "openhcs/processing/materialization/constants.py": "73a3382baf3ba00ef3dbcf0c02835e95121c76ac90944cf81a70cf324e7df4a8", + "openhcs/processing/materialization/core.py": "38bee7aa75ff14455c4de5e39dca0acfa36e438cb5fd8908980c6e5b0007e3a4", + "openhcs/processing/materialization/options.py": "d3936d9e52c4a1b9352e30ac6256a413d82c4cd807a9b65819e59c66579eba97", + "openhcs/processing/materialization/path_scopes.py": "2392cbe874f19d057855a29a70f0e039f224d53cc32d0cada5795e118b70133e", + "openhcs/processing/materialization/persistence.py": "c88ee00d398270f1d97aaedb3b1ae9d44c2e6171c9ef3549b2cc810c6701828e", + "openhcs/processing/materialization/presets.py": "6f1dc92a9a0a6e3693f96609044fa40234bf3a65059ac6ef308cf0cab2f31e05", + "openhcs/processing/presets/INDEX.md": "81fe902f5f0e0b02d02cc155cbd0c8302e76bd8462e17d6de5caf82b5ab1764b", + "openhcs/processing/presets/QUICK_REFERENCE.md": "251d1f4e5b890f102ab1f5def0c4bf40e1489bf347cb79584c0eca0bfb8293c5", + "openhcs/processing/presets/README.md": "1e829b16ac2883121b8d0d29c9352b1afd421284e7598ca58fb700ccdb5b2e8d", + "openhcs/processing/presets/__init__.py": "6fd2c1076699f05ff50423f75405f71a9295b14b87a061ee0d6261fd2f8849bc", + "openhcs/processing/presets/demo_contribution.py": "df4a0b83856e9b80a328f38a8677534d3874c7a6cda74c34c15d76939c35a5d4", + "openhcs/processing/presets/mfd_specs.py": "6f1808c7cecab58e3317bfa020db7d087947ed2e3b3d11838f11d47924e3a18c", + "openhcs/processing/presets/pipelines/10x_mfd_crop_analyze.py": "f375b0741b13bfc493092448f43756e80792057bfb94dafd457bf83544e8ab9d", + "openhcs/processing/presets/pipelines/10x_mfd_crop_analyze_dapi-fitc-cy5.py": "0893579f9e923dc3eb21af7df763bd458f5c3ee1cb97db44b04e20ca8b77a23c", + "openhcs/processing/presets/pipelines/10x_mfd_stitch_ashlar_cpu.py": "381536d37d958fb287dc0a8f3dd4e2ac6e6d25e789b6241d896c888b7646de0d", + "openhcs/processing/presets/pipelines/10x_mfd_stitch_gpu.py": "a9c5332782ab15e418d1a3deda2d177656041a623818bd56a6c8348c940a50f4", + "openhcs/processing/presets/pipelines/cy5_axon_cell_body_crop_analysis.py": "bdeef81988aaed1087a87e3b7ae6929efa8be608006fca9203a86e8b3fd0da62", + "openhcs/processing/presets/pipelines/cy5_ctb_cell_count.py": "e5d8f5c0f46622f195feb47d4a1350c43426dbee146c399b93632d009b6671d6", + "openhcs/processing/presets/pipelines/czi_brain_axon_cellbody.py": "7ed08d237d265ca438bf1966e2a649ce4868d6f0aa62a35a0c387d0dd77e174f", + "openhcs/processing/presets/pipelines/imx_96_well_neurite_outgrowth_pipeline_cpu.py": "5c1cf5b8343756dae9dad98fb257a9249cd8cc6a4a5de69a8d7c38d52ca628f2", + "openhcs/processing/presets/pipelines/imx_96_well_neurite_outgrowth_pipeline_gpu.py": "c4817f7cf785de126777124dcfe6b75415b664cc731f876b8a2107319ba6a3e7", + "openhcs/processing/presets/pipelines/loose_operaphenix_neurite_outgrowth.py": "46fb1ff75071febae663e5efa088590f579ccdd60e81a997476c32b07ddea491", + "openhcs/processing/presets/pipelines/loose_operaphenix_neurite_outgrowth_metaxpress.py": "8f18c81255963d346d5be6806eda63c39f1a0cbbdb97e0707484315b6ee6c19a", + "openhcs/processing/presets/pipelines/neuroncyto_ii_crossover_neurite_outgrowth.py": "ece248b4282000b14069ae510e3ccf0d8c18b04d194afb81f4537b31f3b8671f", + "openhcs/processing/presets/pipelines/test.py": "48cce18a46a115b8ef9038bfeb3e8d65ea50fb539193cc8d9a477eca0c0fc1fd", + "openhcs/pyqt_gui/__init__.py": "1c5e1b42fc81c7459bb0002499a3d0e69c7fd4d68dee94bbc352e918aa743c4a", + "openhcs/pyqt_gui/__main__.py": "aafd7f6f59b68998cbb3be82048b8746f9a55622b9b92b5f08eb035c75e8bd0d", + "openhcs/pyqt_gui/app.py": "3257f148dfddac3f1ff7409881db8100273adac5c3564b9cd3745e8f75313086", + "openhcs/pyqt_gui/branding.py": "acbd5b5fd51b0aeeead42f5086666661b353c78872095ee78e9f692452182e82", + "openhcs/pyqt_gui/config.py": "a3eece2e5b2926fc6fc0edd197221da451ab7c0d0d6f4ffc8bb2ff865ad74bd4", + "openhcs/pyqt_gui/dialogs/custom_function_manager_dialog.py": "fc67b871212b512661ebbf82641814194472bdc94febc67af85aa3f1e7eb2b4e", + "openhcs/pyqt_gui/dialogs/function_selector_dialog.py": "e11c456a5322038408edd42df7d2674cda42e1992f7f6c6e567e7923bf3526c3", + "openhcs/pyqt_gui/dialogs/metadata_viewer_dialog.py": "b9598d07f87a6a162897cddf6c9c026e073dc472cbb9bba2eae6f42299428770", + "openhcs/pyqt_gui/launch.py": "1bdada29836a4da092136673d8076a108190f239762a3cfec19384850f58326a", + "openhcs/pyqt_gui/main.py": "a156a006a3c40f43107b9b1f6ecb97ed150879eb88ded8ef89e25214309ae33a", + "openhcs/pyqt_gui/services/__init__.py": "98dd5e13cc9f391ac6888af66f633a93af8817e9d1a57f29758d61e2df738321", + "openhcs/pyqt_gui/services/code_document_normalization.py": "3dda5c20ba555311ac936b3be1ef021a90effe95491803de10d43f96cbb9e05f", + "openhcs/pyqt_gui/services/config_window_code_document.py": "bdbf33b254451945239bd1c1f256dedbed40d2a8c0e598c4426bcba939ab5d2d", + "openhcs/pyqt_gui/services/desktop_restart.py": "24ca24c3afc0038a268280a73ae7615045eb107ee541a59c13fcdf07005603a2", + "openhcs/pyqt_gui/services/desktop_restart_worker.py": "52d8a8f223d49dbd9ef42951fe507dfbea803b33ff4dfce3e198f4b1a9fe4ed7", + "openhcs/pyqt_gui/services/desktop_update.py": "42910339c4f11afe8d413dfbe024910a20ac6b275c563d1c6947452f8baec908", + "openhcs/pyqt_gui/services/desktop_update_worker.py": "c8d157ea5d61b5cb52f09f3835b430fe1ef3358822b988451e4ee2a6832729bf", + "openhcs/pyqt_gui/services/embedded_code_documents.py": "a1170ab0ccf1225c8781cd8a718c6109e96075a074745ed67d77d5db42615d49", + "openhcs/pyqt_gui/services/function_step_code_document.py": "3cb9ee3bf0411b0cd9ddf254934dd402de12abb630bc764cd724a9492f912814", + "openhcs/pyqt_gui/services/history_migration.py": "407452243851ccbd87f13554fb32c374a8abd84a0b9394235a4fe93b1a4223a7", + "openhcs/pyqt_gui/services/logging_config.py": "9cf2640115ea4add483834b345cadf9260ff4e9604357acdb977636b7fb8a667", + "openhcs/pyqt_gui/services/main_window_workflows.py": "74d3e4fa2dda4a8223c8d878aad55de9df78a71ff359c0b83346be991d14a594", + "openhcs/pyqt_gui/services/pipeline_object_state_binding.py": "afb190ac9ac38e800c8fe04497bbb78a2f1b6381a21eb14890e3061c61ae5b0b", + "openhcs/pyqt_gui/services/plate_manager_batch_workflow.py": "974aebec62dc0f283f92d8153594771d1206f02decdfd8ea5faae0a563676699", + "openhcs/pyqt_gui/services/plate_manager_root_state.py": "ed0e336a21c69e4845f3215685b593047b3892183945a0cfe237e9c8c365f247", + "openhcs/pyqt_gui/services/plate_manager_row.py": "a4f915edab3b46656526ad102242c50456916f85065f71669ce0c0601e77e708", + "openhcs/pyqt_gui/services/plate_manager_state_projection.py": "814651335543000eb3855975a80af311b6290926da233d02ab5e95ba0daa82ba", + "openhcs/pyqt_gui/services/reactor_providers.py": "e937c310e6dd7f06bf8e2cfc227ce442310f6d249fc0abed4ed0e588b4121fe8", + "openhcs/pyqt_gui/services/service_adapter.py": "518ba7df2494b97c6f79ececb58df24114083d95f250309a5096c5b2443a90e1", + "openhcs/pyqt_gui/services/step_scope_identity.py": "cbccdf20ee85e94c2aeef5d7cf925a645eb7f18c22bb78cbd29323b7ffb76758", + "openhcs/pyqt_gui/services/time_travel_navigation.py": "01edfbf4cc1ea78cef771968f1cf0db4e4e4631c47cb750d6d233cdf24bfa9f2", + "openhcs/pyqt_gui/services/ui_agent_bridge.py": "fa8e7a1cd300c6e5cfde5ac155d3e0fd0a7324632fb150280f57c828d8dfcecd", + "openhcs/pyqt_gui/services/ui_bridge_composition.py": "e7ff43261aba18f4b6f7444716e8614483b5c1286160029247484f42d58168e6", + "openhcs/pyqt_gui/services/ui_bridge_contracts.py": "7b61d3b568f28e01d9faef4f6314f56b2539f52748d057c4cfea3b309bae47ca", + "openhcs/pyqt_gui/services/ui_bridge_live_overview.py": "68302204a1e7d9d0cba2da20f50d98036d76daa5efcebb3652268a33ff1ad77d", + "openhcs/pyqt_gui/services/ui_bridge_object_state.py": "747c284c37e420092ffda9abbb8275fc7deaaff7f3676cecf3f5b1f324314744", + "openhcs/pyqt_gui/services/ui_bridge_object_state_scope_policy.py": "95d38c3dbdceaa1495da564ea8bbc2546cc5173680ee43d634b610bc75273b82", + "openhcs/pyqt_gui/services/ui_bridge_pipeline_editor.py": "49349b907d4fdd3ace506168b2d3fa7291365ce8cc4a0c0fb3c319d406ee296d", + "openhcs/pyqt_gui/services/ui_bridge_plate_manager.py": "66c4a64fece372937926ae4de3ffdafdb26491e801478127b163c1cdfe3037ae", + "openhcs/pyqt_gui/services/ui_bridge_registry.py": "f03f6f335aa56aab921c4471f4bfbcf1d39a4be8a11cd0a0b7f6bc425b05e26c", + "openhcs/pyqt_gui/services/ui_bridge_server.py": "667f582fffd4b489ca7274f8962a759c3d33d24b10ec8e6d1aa5cac01233b067", + "openhcs/pyqt_gui/services/ui_bridge_windows.py": "edb992542dffb88a9cc4557dbd6647b76d5b68584dca27a4c9486ef545be74e9", + "openhcs/pyqt_gui/services/ui_window_ids.py": "f58482c524d445b8297eb3116ec79ccb22403eb2f12dd9ecf6e27e154aa028c1", + "openhcs/pyqt_gui/services/window_config.py": "781408d9ce8e5cc9115ee7db0512d1d16a63dc6a6426707e5b9bdd0f78ec67c0", + "openhcs/pyqt_gui/services/window_handlers.py": "5080a81191a8cfc78ce826db7e241bd41d3df7607deea32128a06449e04a3aa8", + "openhcs/pyqt_gui/services/zmq_version_restart.py": "86d4bf273c75417b2c4f5c38ac83a66e3487bb233be88a608140a0bdb4c8bc67", + "openhcs/pyqt_gui/ui_tab_identities.py": "598bcfb97bdf4ed9671b84e390796e55b97836167c1d2c4ae9baf2df89f9ef71", + "openhcs/pyqt_gui/utils/__init__.py": "956321d3d5c6f226d69d17c8bd6943583fa2c230abf879e511bacd4816d92c1d", + "openhcs/pyqt_gui/widgets/__init__.py": "9b3362d93a343d8a9b9ed7d3562fb95ec63c58d15b7a7e049e131498e06d5a6f", + "openhcs/pyqt_gui/widgets/artifact_plan_view.py": "caa8d75207367831e61ce7586d1dd28eef7e8293a7d4cd7f4653e8ddeacf38f5", + "openhcs/pyqt_gui/widgets/config_preview_formatters.py": "67b2cbdddc7ef1bced7d549ce5bd29ed7222cacb4d8ea255476ec36f5cab6f62", + "openhcs/pyqt_gui/widgets/debug_toolbar.py": "307f1b5a3113c6286214037db9adc49abadbc0a10ea04d19096a7cc40a0a6d5f", + "openhcs/pyqt_gui/widgets/image_browser.py": "72610ea41c3573c46f83303543691df8fc8c3babc091032bb439c257b818575c", + "openhcs/pyqt_gui/widgets/pipeline_editor.py": "450d311712d032d2f811de28a618915bc7e8a7f389e9f23c8a12c3e6ae273206", + "openhcs/pyqt_gui/widgets/plate_manager.py": "6fa48fbcd58e47eb7132d5d9f86a002c6a0e8ff077d9654a03a791629e135378", + "openhcs/pyqt_gui/widgets/shared/__init__.py": "9089c6a140baa85367034e5bd639cb64978fc789bc196b819295a45e98151b04", + "openhcs/pyqt_gui/widgets/shared/openhcs_manager_mixins.py": "f380546421b12f0e5676faf7cf5df59d611dca0452890455c9e7d09eb41496f6", + "openhcs/pyqt_gui/widgets/shared/plate_view_widget.py": "d539dfd21a69c0e88d7194bd07fbdc8057aff65706188d7c8b9b681a1211bf01", + "openhcs/pyqt_gui/widgets/shared/server_browser/__init__.py": "287a6c5e32877fc13ffea0693f694eb0eaee3ea926fe2a27b58dc4169455b24a", + "openhcs/pyqt_gui/widgets/shared/server_browser/live_tree_sync.py": "81dbee5b7f471082bcfeb8ca63e753840ad7134aff7101fd14f1c821a121d1bb", + "openhcs/pyqt_gui/widgets/shared/server_browser/presentation_models.py": "1c41613a3760262a4f68c7f1301b40f24e6bbf9bcafeda44e9ac8c62f90da36c", + "openhcs/pyqt_gui/widgets/shared/server_browser/progress_projection.py": "4b9c4bd90fbe6ed4ac55f5fe1d23cf2a46c3a7f743218d685df8ac2f27be7998", + "openhcs/pyqt_gui/widgets/shared/server_browser/progress_tree_builder.py": "cc826076895f5cb5a7828d7c502ff7591d37124871c60b266ae4bcfa80dcc75e", + "openhcs/pyqt_gui/widgets/shared/services/batch_context.py": "7a502eed9e118e3c662c319c1cd2b740dde960e5427347579b0510e5a1e1e376", + "openhcs/pyqt_gui/widgets/shared/services/batch_workflow_components.py": "abd29f375ab12fdc56058f1c860ce26c3b52129658795eb1c95581d83b7480bd", + "openhcs/pyqt_gui/widgets/shared/services/compile_batch_workflow_service.py": "1b37faeb90ecbc326727bef124ba990a067dffca430955772f8f002356a451f8", + "openhcs/pyqt_gui/widgets/shared/services/compile_workflow_service.py": "e69e5b22686627f2f7794578f4439b15c4fde2b1ad1bb1af7896000603cb320a", + "openhcs/pyqt_gui/widgets/shared/services/debug_progress_service.py": "4eb10d47c39e9e5eab7557e6b6ab348a01c62c481aa5afeb3f63dafb1f044eb7", + "openhcs/pyqt_gui/widgets/shared/services/debug_session_projection.py": "09172141287fea46f3ebe560aa0fa3294a96cd3f265f2be65375b3a0a60f54c3", + "openhcs/pyqt_gui/widgets/shared/services/debug_workflow_service.py": "a92c75f6ab96bdc02a70dbd5908f2d02c03fb4de9b238a75d58c97f656b75701", + "openhcs/pyqt_gui/widgets/shared/services/execution_control_service.py": "a125648738de09f589cd87227c85c6b2dc724fa8afbd91226a664be224772b41", + "openhcs/pyqt_gui/widgets/shared/services/execution_server_status_presenter.py": "b3dccb164b0f64ae12003698770e114d96cedbe9863e36f16bb85c0d170a8223", + "openhcs/pyqt_gui/widgets/shared/services/execution_state.py": "73d03020ffe437d8aaf4b6a5eeb4f43b91130de747d411648cb7fa2e4df27777", + "openhcs/pyqt_gui/widgets/shared/services/execution_submission_service.py": "4a17389c654a72c8b3bee6b7740efda60183542dce492914c858faa8c45f1408", + "openhcs/pyqt_gui/widgets/shared/services/gui_event_bus_broadcast.py": "8ce46c251767078b9d7ee3ea306c2e48ede47e7fdc2a2cba331a7c73e69b2fb5", + "openhcs/pyqt_gui/widgets/shared/services/live_measurement_progress_service.py": "4cdda4296d2e9dba732dc0cfea1e26ab8f09cf70a4c275dd4bc52abf73697d4c", + "openhcs/pyqt_gui/widgets/shared/services/pipeline_debug_actions.py": "8b7d9fed1c024702ac2805c0196ec8ffa2efa92557cdec20cb8323fec584f76b", + "openhcs/pyqt_gui/widgets/shared/services/pipeline_editor_workflows.py": "6664126ac4daa851cacff5014e04a0c09a56bbc76f62661ff97d713e9137d447", + "openhcs/pyqt_gui/widgets/shared/services/plate_config_resolver.py": "b42ef47454d20dcbea8bf27af8021df09812449298133462c79e9160b5962436", + "openhcs/pyqt_gui/widgets/shared/services/plate_manager_workflows.py": "d640bf4b2710009dc3f92719ec97cf09fe3d93ed7eb1bebaf2b02913e8fafbfd", + "openhcs/pyqt_gui/widgets/shared/services/plate_pipeline_request_builder.py": "a650f77d70deeba55362b5b5f9a18de6026fe298f913aedc70a748d962d30295", + "openhcs/pyqt_gui/widgets/shared/services/plate_status_presenter.py": "316a89398b2ff343a0e8ca9ed35183ef0f6da2ac452eff7205399fb283fa934a", + "openhcs/pyqt_gui/widgets/shared/services/progress_batch_reset.py": "8a41bae88512e2f4c9e2d5f4c4aca3f0fb9c7697900b1dcc32f08286b7f926f6", + "openhcs/pyqt_gui/widgets/shared/services/progress_workflow_service.py": "2ac31ade9c7242af5033babd251fff1bba9306842ab93ea40ada2ebbdadb76bd", + "openhcs/pyqt_gui/widgets/shared/services/qt_widget_edit_commit.py": "67989b647ec56867db5c4ccb47614ed0ba78d9a7ee00fd02f0c84617578c73a3", + "openhcs/pyqt_gui/widgets/shared/services/runtime_artifact_progress_service.py": "9ea326fb30ab59494098a157e8b8b2e964e0a5b492ade84af9b82353566566bc", + "openhcs/pyqt_gui/widgets/shared/services/widget_action_dispatch.py": "f4a202513fc3b1de7684fb6a6d4857bdc0c1f8fa68b5f9309b304f139b063155", + "openhcs/pyqt_gui/widgets/shared/services/zmq_client_service.py": "f56f23fc860dab554584cb5fe44d29332c73f7bb7a650c167702efeb986363b7", + "openhcs/pyqt_gui/widgets/shared/time_travel_widget.py": "36bf88227b3e90247b1d0573a209f20b69179f22b51b1d713867108e84c05c9f", + "openhcs/pyqt_gui/widgets/shared/zmq_server_manager.py": "2d2d0ae9e70e04bc7b6c738877bc52e8a37e79a80a849e87388aea400b42977a", + "openhcs/pyqt_gui/widgets/source_bindings_editor.py": "acbfb6b1e86f2ec05535a5d242ce59e48c165c9d946a292c0d6cfa3754ea61e1", + "openhcs/pyqt_gui/widgets/step_parameter_editor.py": "54e0bdd74f8d781c2bfdf1da559bbe7e2393f19ef74826d374a2281e8eb96258", + "openhcs/pyqt_gui/windows/__init__.py": "2793a830be87aee341883bd160124e82b118c991c3ed9294a5e226b2dc46de85", + "openhcs/pyqt_gui/windows/about_window.py": "6d71ed291e59d8fe877189c1b1aef9f8370ae890201ab2fa395bcc4b6a69cc05", + "openhcs/pyqt_gui/windows/config_edit_session.py": "b6d7173ed6c9073bc19cb2d1f535e2b71efe0ec269d3e2bcb088ea500f564ca5", + "openhcs/pyqt_gui/windows/config_window.py": "5775ffba24066c78253af26eac51815deacbfc3922fb6d96fd3f8ea0c0ac5dab", + "openhcs/pyqt_gui/windows/debug_inspector_window.py": "6a5f62db6f9b55ef9e53c5b7b089f96fdad4eafe44e0e234ceca8489361bf9c3", + "openhcs/pyqt_gui/windows/dual_editor_session.py": "bcc8062cda516f9dfd72ecccb4f86c88b2ac3fbcbb0eabbd4880522dc316a036", + "openhcs/pyqt_gui/windows/dual_editor_tab_builder.py": "08fe16896ca3cbb7f4ba64e0115422c7c26fb538014e029d2f4a9f0dc907e1aa", + "openhcs/pyqt_gui/windows/dual_editor_window.py": "7aeb2af64e824d5c9749ee7ec35b18a2db303717ae15c37099e54ce0bdde17c5", + "openhcs/pyqt_gui/windows/help_window.py": "437c849f68698cd78d56e1fd11077d7740eb22233e7d2314cf0220f5618ec4f1", + "openhcs/pyqt_gui/windows/live_measurements_window.py": "0bdea475a11f6b062b9775b05253d21e8a2700099d84f5041f44c8e09891ea52", + "openhcs/pyqt_gui/windows/managed_windows.py": "b1913deeea6b83ec96fa350bebf8038425dd36f6d025a107adda7a33bce51e8b", + "openhcs/pyqt_gui/windows/plate_viewer_window.py": "1960f0c9871eeecded94dfcfea683a49fdc35e1ab55e5f5320a137f2d11d9572", + "openhcs/pyqt_gui/windows/snapshot_browser_window.py": "c8ca14502ddd01bae5c50b7eb06dadc9d52b5f769872e7c45a037db501b69efd", + "openhcs/pyqt_gui/windows/synthetic_plate_generator_window.py": "66ddba05885106acab8f5f9219665e8b611f50984b09c6a4e3408ab783685a9c", + "openhcs/resources/__init__.py": "3dd04dff758036aef8f6fe23354e84ab695039989b2ad26baf2b4f1c0c4ca028", + "openhcs/resources/assets/README.md": "27e72cb5601e7bedd4b795034058a024994364509d21c88f7362a1ec2da68323", + "openhcs/resources/assets/openhcs-favicon.svg": "43ba8aeb9771a218e64acb48769e30ac830430b80d41df444af558eea12cfa21", + "openhcs/resources/assets/openhcs-icon-square.png": "77e70b972785ddae7b7a3a33a7b341ebb105179c5b1df76179edc795398cc68a", + "openhcs/resources/assets/openhcs-icon-square.svg": "267c0d248f6eccbe670d1e0e830016f32ad0785246d90e0c0115afb59d2f9a79", + "openhcs/resources/assets/openhcs-lockup-horizontal.svg": "38b5d3dedc5d142fc162609b9066d5278402e51f57fd2e173e0d3a3281fc56dc", + "openhcs/resources/assets/openhcs-lockup-stacked.svg": "2168570ea95cd1db0223c7cdab6a77295e91eb4c79e8e7d1cd1dd034f1b947f4", + "openhcs/resources/assets/openhcs-logo-source.svg": "a2fcc12bcd5d42e93101e09b360535b853a7166a805ec6d3c5d2347bc1acad19", + "openhcs/resources/assets/openhcs-mark-mono.svg": "c9ab9202d1066d187231692074d5f957432925f12d84a322a5fd2c3d965a5ae0", + "openhcs/resources/assets/openhcs-mark.svg": "8b3357a275ea3233d3ce895a3f24c10144fd4f8e060e71d11d96880373eabd9d", + "openhcs/resources/assets/openhcs.icns": "cfa1313f13b552397aa1c815cd9ec56a3dd366074ce82d063b8a52cd9332f8a6", + "openhcs/resources/assets/openhcs.ico": "e5990ec4267ecd51a234886de80e034932a0d945004887bab0fcc6e248332c17", + "openhcs/resources/brand.py": "e810da87bf0cb643d5466b4858a5c006362fa7935b721bdbcdd5b3ba883c6793", + "openhcs/resources/windows/OpenHCSLauncher.cs": "0b581cdde998fa6f5b46eb85d89a11aa5688914ca8901c1b5e35b15d38870922", + "openhcs/resources/windows/__init__.py": "c7d7dff08a4bcb1e6d669829e5e8bae89b0d966c2cdc933fefb95488b4f3dc7c", + "openhcs/runtime/environment_provenance.py": "2bd3abb4682e94de6437b0f81cf2b3e6b7e4481cafc1d85f088b57e996b37148", + "openhcs/runtime/fiji_macro_runtime.py": "661d759c788112abd323fd0d49d51b6d22a4a0bee288957b075d7f820ad47677", + "openhcs/runtime/fiji_stream_visualizer.py": "62000e9d4116204777dc78573e5ee6d2f0739ea5a658cde30df4e4e3d6a0efc4", + "openhcs/runtime/fiji_viewer_server.py": "56d9e929384a28dd7f5546d0533616fd533c2e8b533743891bb3f675726080b1", + "openhcs/runtime/function_catalog_preparation.py": "88ffcb443ea905cc0249d4c6d4c7e7989489b11b6c5b82b7f1c31ad677dfdae9", + "openhcs/runtime/import_authority.py": "a613ea9dfedd63c9384a04d24b658849bd53294c1b066f855a7333ed9040f6f6", + "openhcs/runtime/napari_orthogonal_widget.py": "c4f0d6d40d12e2244b3f0b0039f12518897dababb7e403746fdb6652e6848635", + "openhcs/runtime/napari_stream_visualizer.py": "f41f1c52e4a99cf357ddc9ad2047ebb9292e8a6978a8db9c8edd7d8c8a5d3f51", + "openhcs/runtime/napari_streaming_handlers.py": "d123be39bfc67a03ae3206bd2afc410c8f11f42a5af1cc61d158a1a598c1edb1", + "openhcs/runtime/napari_swc_reader.py": "cb13aa5a12b1713914c308bb1457917d98f97c0a083a77d516a39dc9889225d6", + "openhcs/runtime/napari_viewer_server.py": "9a38e6e384d3b1924515c617b97644dceb6ac60a010144225bc6a6072674b9f7", + "openhcs/runtime/omero_instance_manager.py": "af94c8b851bd4e1456e5c85b16f83ac8008944b05a71ed9d9db5574416704933", + "openhcs/runtime/viewer_component_system.py": "20640d435d5af97c27def5c2b23f48239418fed229ffbaa094fbf68fec7764a4", + "openhcs/runtime/viewer_controls.py": "4e3a50ad5f83c51716b0bbcff966de628648ee8aa77106b76dc8812e38b2d0b3", + "openhcs/runtime/viewer_measurements.py": "36cb96e1b90f0a1203de0670cb853edee59c4feca5fb6e21c6a62c117dd5023d", + "openhcs/runtime/viewer_protocol.py": "896a630bbbb67c152dd533d57280df098f261b714ea65ee1979f91c813e2ffe9", + "openhcs/runtime/viewer_snapshot.py": "34a71a06066ccad0a9dee5be6696a106447f207b9b1aea320be545622249ca24", + "openhcs/runtime/zmq_application.py": "078dc20848ff11777edefe355b90aa403594f18b6c1abbacaaa7af5773f3577c", + "openhcs/runtime/zmq_compilation.py": "ad958c78ec9a26b778ad231501751f38ec6dd3d0c73ca2c1082207031fc6781f", + "openhcs/runtime/zmq_config.py": "eff825c945cf52f658438f4328997eb3cf5bef970df63a7ff13b198bdd144e7a", + "openhcs/runtime/zmq_control.py": "5c18a1f146076c15d50e5d16446d81871e4fc0661facf894ab6e2049bcfec05a", + "openhcs/runtime/zmq_execution_client.py": "8f98169438b1f9d65dd2a4f1517b5901e5d2ffd944fce4b0779a8f9b17d01db3", + "openhcs/runtime/zmq_execution_observation.py": "01e3a316291a951bc7f9c389cb0ab659b2dfb8085b0bb554be9b46a03e7a603c", + "openhcs/runtime/zmq_execution_server.py": "738450e5af28b1b54595d21685d43e3961273c0d3284ec8dae7161a558353f5d", + "openhcs/runtime/zmq_execution_server_launcher.py": "09c3dd01d5bfd0fe2d08d6d8ffe0c9c806a817940d2df2d176ae5108cf0a1708", + "openhcs/runtime/zmq_execution_signature.py": "41dcdb0a40446947a42a87482f27da051408015eec972e11e4d3329464db11ac", + "openhcs/runtime/zmq_orchestrator_environment.py": "7498c32e2657fdc9d411be2f2791708950c5d764c88de0762fc72b424b20f1a9", + "openhcs/runtime/zmq_progress.py": "354f437fc08bde998b12126b36f92aef793e4a5fd2a0027582b7886130cbaee2", + "openhcs/runtime/zmq_server_hooks.py": "b10f2a9a409ea1b97ad04ab4fa1fcea2fb41b9fbeaa43f7f31f0371be6ef484c", + "openhcs/runtime/zmq_worker_execution.py": "000a65d1619b75d802b3b38001e3e3787524e3859c9430b5d74813f197b29dcb", + "openhcs/serialization/__init__.py": "fb53e3cf59733013cdfde9eeab44c0c95f5d5177ba08b6e198ff0e2a9b4187e6", + "openhcs/serialization/json.py": "5145f2956c583ccce35dd24cc1d87c4d5bf959c96daae1ba0cdbdb903d69bfaa", + "openhcs/serialization/pycodify_formatters.py": "b86d8a18b32084839667ebeb1322fd2e86ccb5fd8bea0365f01c03bc56042e9f", + "openhcs/serialization/source_path_factoring.py": "d8dd55b7d45882e067f9fddbe87327000a7e226dbc531a90fc543f201ea665ab", + "openhcs/ui/shared/__init__.py": "923d954c0cdd5d417114a10688a5afcdb81a3b8d11dfa7c52fdd06e9a9f94122", + "openhcs/ui/shared/code_editor_form_updater.py": "bdef35064a2a003873f0358fe336e77cf1089357d6335233e84b13c89f2740cc", + "openhcs/ui/shared/pattern_data_manager.py": "862ade241cbeb33b0496a486f3d55dca6db508bac71abba4ecf866323a25ca3e", + "openhcs/ui/shared/pattern_file_service.py": "1e8addb223c8cbfa817b3c13564fe2332e58c147caecc6218c63c48b98c3edb7", + "openhcs/ui/shared/plate_manager_code_document.py": "e742b4e6227318a59678e33746b94f74d9224a4071756eb2fd03033e8cff932b", + "openhcs/ui/shared/plate_scope_identity.py": "e6d8e83375d4e493c59ea5af5adb585ecee42763a7799773bca195e9c22de1f1", + "openhcs/utils/environment.py": "8c8c021cbb3ee213656da1ad30ad9237ecb235e60524eff73e64c30f7c56cf3f", + "openhcs/utils/import_utils.py": "39ba3547cff334da81551fb75b85fd2691db4e729b510b701ac89b63cf791a14", + "openhcs/utils/performance_monitor.py": "4235d3e703caaf41a03e92335103d10b6e7b001eea90fafbbca96c00a8ff62c3", + "openhcs/utils/pipeline_migration.py": "2bbabcc0a50bd54c8b28b7038a418435c532e58d0432c3b2b17d7fd6a2b78ead", + "openhcs/utils/recache_function_registry.py": "70bc5ff8b87ca22c90ebc0a048cfd5f3d960b6b47bb2ada34297c036c024737d", + "openhcs/validation/__init__.py": "35a49caca4df9fa87335ca25ed5d551840152fe11f1e0cbe619d70a7f5d891ca", + "openhcs/validation/ast_validator.py": "07a305357e4d902366e97f756456ef76438ac2b91580631609c2ba848420e0bb", + "openhcs/validation/validate.py": "50e0503909e06fb85c6e91df98240b6f2a5181c7bc16f35899706c5120ec73bf", + "packaging/codex/openhcs/.codex-plugin/plugin.json": "94633e826327cfc68d88caff5eccc9bb200ed92e0f72be764032097f4a785089", + "packaging/codex/openhcs/.mcp.json": "83d842ea5fc48ba191ba19914771b10c6685d9cb8748611358a15a92cd152123", + "packaging/codex/openhcs/skills/use-openhcs/SKILL.md": "d8e48c44735e9aae2c7f3e2b96e3981cad91c994a48d2ccf7c402ad139234d0e", + "packaging/codex/openhcs/skills/use-openhcs/agents/openai.yaml": "ea7c4babb6d65844d476f49f8492fe40ede6d7348c9c7943e131ce7855ffc83d", + "packaging/codex/openhcs/skills/use-openhcs/references/analysis-learning.md": "98aaed3480e8115184e7bcac319ff1210d30f9f27f7ee5250e9e7b44da20e25b", + "packaging/codex/openhcs/skills/use-openhcs/references/analysis-strategy.md": "d64aad6085612acc254e3db992ef4219f6540691918334751524aa2039e6b3c0", + "packaging/codex/openhcs/skills/use-openhcs/references/biological-image-analysis-evidence.md": "3d64f5fabc070accd9a8f08e079171de30d00e00ed1b7536f4cf40e8684ed361", + "packaging/codex/openhcs/skills/use-openhcs/references/blind-recipe-promotion.md": "c8a2fc19c859817582608d17f30c179faf486e3ee94af2e1c70a6bf43ebc5710", + "packaging/codex/openhcs/skills/use-openhcs/references/custom-function-authoring.md": "32d7f2de2192a6439a518828d0d6b2c7080383574deda24a78522b9a5dce50e5", + "packaging/codex/openhcs/skills/use-openhcs/references/image-interpretation.md": "1b47bb7f7e6ad9bdf198f0fa5c7c6247352ae271665e4a18446422dd83f4e5d5", + "packaging/codex/openhcs/skills/use-openhcs/references/image-preprocessing.md": "a18933442e627df611ddfb362a13e605389430c1dce0a7745aee94b6efb3f148", + "packaging/codex/openhcs/skills/use-openhcs/references/installation.md": "f9051b9c5f427650994d2a2b4fa9599d91a168ba2a68c4d5f044a07043a15cf7", + "packaging/codex/openhcs/skills/use-openhcs/references/measurement-interpretation.md": "fb25ac9aa3b777f775daa76242546406642d2a75b587ae8adb81ca74f18cfdd0", + "packaging/codex/openhcs/skills/use-openhcs/references/segmentation-diagnostics.md": "beda4b305d81dfe8d9b892b3d721fd07f7b355ff4fa9132a1c05ba680f2f8e85", + "packaging/codex/openhcs/skills/use-openhcs/references/viewer-qa.md": "c5d20201582abb10610adf128518d193a8508409ae9775e5b11622718788c47c", + "packaging/hosted-mcp/Dockerfile": "0a70807959c0cd7f125ed64bfe062761bdc05b5fa943e4aaba4321cc1fa46af8", + "packaging/hosted-mcp/README.md": "fe111c091c78b901c9e3265796aac728ff06857c363224935ae1b7ac24667707", + "packaging/installers/README.md": "447bdf95ab4b7a7ee256919a25f6f8c42a1ed718e5386110dfcb18dbb6a342ac", + "packaging/installers/macos/OpenHCSInstaller.swift": "b79ae9d0be4932cd47ed8b139ba2fb2c6205d685775469bc525422bc6245850c", + "packaging/installers/macos/README.md": "f7e7a45c5e0b2a80cd2360f9dced3717738c103181c148330437f951de54be3a", + "packaging/installers/macos/Sign-Installer.sh": "7aeb2c8bd4163adf529d5990ce10578de00e47325858ccc468fc736e47c56891", + "packaging/installers/macos/build-dmg.sh": "81bb96b0adfb8025520cb36c332723794b2ce526033962e84dca18aa09ead70c", + "packaging/installers/macos/build-installer.sh": "2495c0126f7f2990ab88b53e4a04c01d19b678f24ce3cacbf696c0e4b27e01db", + "packaging/installers/macos/dmg-lifecycle.sh": "b6041ab0e2cfd4bb5fd10b8b130f0f64f4307c9e9caecfe2dae011f9d2199b4b", + "packaging/installers/macos/install-openhcs.sh": "3a0f43c57c14bd7f8ceeb38464e174728f903e0ec524d980dbdb2b5f9bc11544", + "packaging/installers/windows/Build-InstallerLauncher.ps1": "19b9d35464041652a69f2ebc2e870a4eca73f241fb12491b9617c7aff930e9e9", + "packaging/installers/windows/Install-OpenHCS.cmd": "00d1d89b03b5ec4df910ec9b8bd2f2f3be1fa5381f4aff42ddbbf09b44899bfb", + "packaging/installers/windows/Install-OpenHCS.ps1": "ca368082f3d7a414f9e9136b2c5ae459bfb2a9a2a37c096d06eeb93d5c403358", + "packaging/installers/windows/InstallerLauncher.cs": "6567afedc37b0957a8c3e3ed39c8ed30a2d7afe2c7df998dbef2ca71b3ff7ab3", + "packaging/installers/windows/InstallerLauncher.csproj": "3aa0f7afef4bd515c2d3ad45c0bd5b9fd58a7e17594acd89f8173621f4d3439a", + "packaging/installers/windows/Sign-Installer.ps1": "127d46834f7377b16869df0641146c75780787199b49f6a255daa521c52ff097", + "packaging/mcpb/openhcs/.mcpbignore": "c30274f74a4e9f2c17b3866f9054f4136f132c9fc4eef9ffb1ea37b0e212910f", + "packaging/mcpb/openhcs/manifest.json": "a51d5f32ad56d1ea22cf338b5b4eed1e343b7bbb62d9a1acde0ce5cf1fab20e5", + "packaging/mcpb/openhcs/pyproject.toml": "56b8807a2c2c86c2ba59eebcd49ca1f3a9f8c75499d63115b2534a4acbc2f53f", + "packaging/mcpb/openhcs/server/main.py": "a66f6c4826ac16c5b6fa12f08a7f2f9f5cf345b2191a4b44302dbd9bf773cf8d", + "paper/.gitignore": "f3dc4ffa6afb0ce8b250b3bc012ffc8121349d2d5d413a514254ecb1feeec6fb", + "paper/01_abstract.md": "9b2dd7b6ac22a3c6816b5346caf32d8690aafaf9a349bf8f1e2bb9153aebe585", + "paper/02_introduction.md": "6471f9cd05a4ab12fef57035835fc2afcaca53034d524d477c9646ef9792f5dc", + "paper/06_figures.md": "d6fa40fdc527d7b72ddad3a53e91af58b4ab9f57d525bb30685867c326b48293", + "paper/README.md": "1ba1541e0f91a28f70f405246c2e2bd072a524e00f199a358fd148f872337d59", + "paper/benchmarking_plan.md": "560037370a27b5eec4447b51e955d7e244f06db886cfd8de749b5ec1a336ed1b", + "paper/build_docx_from_markdown.py": "72ffa96c57f4dc8ad7ccb78402c33f87b1f376c67e79936be2a21be4cc3939d6", + "paper/build_paper.py": "2c77ee7106a3bf2bf48d6172fdf9ed3fa6605f0122a95f243dbd4707ab3284bc", + "paper/figures/build_slas_agent.py": "a8315f9f435a36c30115a5655f040180e95db2022466ec3731dcd0e0c62040e9", + "paper/figures/build_slas_benchmark.py": "d757d90ea049afff2d4ae6cd0b8264146413e2c4181fca3c1ee4b62e96d62398", + "paper/figures/build_slas_cellprofiler.py": "fa19dc2a6f5e9b4992fd58686b6af2192e134e8137498cc1458f2e23eab2b147", + "paper/figures/build_slas_compiler.py": "4695327ac930ad313f2af161dc46c5e192d6bce3008a201b78fe5a1fdececb29", + "paper/figures/build_slas_complex_workflows.py": "98d7ea099c4165e2872a23ffbd6541254b5a5494e4589dfe24841060149345c4", + "paper/figures/build_slas_extension.py": "33f547f5efb4f44a1d86ddbc76a19507c67d5943810ce3b51910b47ebecf2cab", + "paper/figures/build_slas_independent_validation.py": "71bdde1fd0ff4a11c58770d6d5660ce5347c58eab4b87a62712323cf79327cc8", + "paper/figures/build_slas_outputs.py": "1d2ad59edd73b24d41589abe3cfa3617cc113d87a3114670c976a3e7e27e3c36", + "paper/figures/build_slas_processes.py": "8681d66290044aee52191d517d287cc0510d96e94d580f69651bc14ae36bd8f1", + "paper/figures/build_slas_runtime.py": "be189377a89a1bab69fb428e1151cf0a89d9dccde1df896011049419baae7aa3", + "paper/figures/build_slas_visual_story.py": "9fcb6c749a4b55d8c89c8c5841a004a6a85a96541b01d55a7214ec52314b0c10", + "paper/figures/capture_slas_authoring.py": "a5c0b917e5c807ac0a5a41c33e33b7cc4e3e28ace44cf827bdbbcac66647fbfb", + "paper/figures/diagrams/README.md": "983a70bc3d77a6ad17510addceec23af5fde61f5ffc218aa559c3492ca6ddada", + "paper/figures/diagrams/fig01_field_integration_gap.dot": "ddfe67b88a21a2ff82f4d2b2a80617572881345c2ad1872299c6117c6c897bde", + "paper/figures/diagrams/fig02_compiler_runtime_architecture.dot": "8ba8156db605154181244338ef51a37a5d52d72fc09519a0a8d54eb64aa5e600", + "paper/figures/diagrams/fig03_cellprofiler_import_path.dot": "b93bb6b83ee271078e99eb9740461b34617fdfd4e491e515050f87d1c1cabd0d", + "paper/figures/diagrams/fig04_benchmark_validation_structure.dot": "4006ef6be7bd1be50cc26cf55d1140a3a1e5ae2b625d97da1748f160738b6ba8", + "paper/figures/diagrams/fig05_throughput_amortization.dot": "859ccffd882420502b4f400e360fb1c259accd3e95eff269e442af81a829339d", + "paper/figures/diagrams/fig06_backend_extensibility.dot": "18ba5c5e0642b771d377b1e1b83f84836a990127bd5ad33930f235d4fd1ced46", + "paper/figures/diagrams/fig07_typed_state_bidirectional_editing.dot": "949fa632cc592408abcbffe5eda2e6731253cc740d8210833131b065f96480ea", + "paper/figures/diagrams/fig08_benchmark_corpus_categories.dot": "a61c3d1b5a24ddc1aa0a5e2df2b4b8e113f2433d29e4684e9cf111e1f0b018a9", + "paper/figures/render_diagrams.py": "0e4a33f7f7ba28aca81bdb207c6777f3755d25b79be2a4257216558db2632187", + "paper/figures/rendered/fig01_field_integration_gap.png": "c7afb89193de18b8995de4beece888b3d5431c143fbbe1180bb4fe9da8f2a787", + "paper/figures/rendered/fig01_field_integration_gap.svg": "45abc09cd26032d3bcdee0c03e6875600f7bb8de59d7824dec2bc830cebf1bc7", + "paper/figures/rendered/fig02_compiler_runtime_architecture.png": "bcc4bec4b9a3bcbb34549e31d700b5179f7f2a660eb68641df5904172cec6048", + "paper/figures/rendered/fig02_compiler_runtime_architecture.svg": "519144ad09a0377af5489edcc25d36575e347afed9bedd4ad16936c58727a038", + "paper/figures/rendered/fig03_cellprofiler_import_path.png": "06584f01b2d10ddf219fdb12482a6cc902eb0deb5e1eda93c1862b0ebae50daf", + "paper/figures/rendered/fig03_cellprofiler_import_path.svg": "36026b58ccdb666d73487235eb0f59cc04767bf3284d2d947073958d7d9b963e", + "paper/figures/rendered/fig04_benchmark_validation_structure.png": "aeb74b4622d3f9ac8643e663c32dabe66c781543fa12c072ab7b852e97ac50b9", + "paper/figures/rendered/fig04_benchmark_validation_structure.svg": "eb88c65f4878197a4267f72285a25d0627e63e103bb64c470d7983061b851463", + "paper/figures/rendered/fig05_throughput_amortization.png": "0786ad9ca71980619419f068481bec5f9a0201eaab2b1a11de3aac9fcb7508d0", + "paper/figures/rendered/fig05_throughput_amortization.svg": "6df0457705d6490e54cf2672424f8e86a921e5a2ca56940b5e56d466445c86ec", + "paper/figures/rendered/fig06_backend_extensibility.png": "a1d07d9a2c073e8220ad33c1681c74037e31f05fbe1276a5f8f5c9798bcf5a56", + "paper/figures/rendered/fig06_backend_extensibility.svg": "6b8bae2c53d29366905eb8f5df2f4935b8a7304d0aed447512a15d4e6e4b85e9", + "paper/figures/rendered/fig07_typed_state_bidirectional_editing.png": "469f34e83fb1e80f069c9e6ab882dc1fb1b7957cdf399a89765bacd60c04182d", + "paper/figures/rendered/fig07_typed_state_bidirectional_editing.svg": "6f1222cbb149c1e283b4805fb7431ac32d14f4c056bb78c5b1bd09badcb25205", + "paper/figures/rendered/fig08_benchmark_corpus_categories.png": "b935e03bc3742242c81ab848f50d5ab13271ff3711c11cd7b8867c3e6c819ae2", + "paper/figures/rendered/fig08_benchmark_corpus_categories.svg": "cbbdac66caff4f1435a795bc73460aaae4ac0144e1e2d076f9302e654135e5b4", + "paper/figures/rendered/figure_contact_sheet.png": "b1955c7032f4ff118eaf28088109e1fd58490e53ca468f8f915cef196c9c2537", + "paper/figures/slas/authoring_actions_provenance.json": "c136049995c71be1a421811e594976aef958ab27d606d63175e207fc14f5cdd4", + "paper/figures/slas/authoring_apply_provenance.json": "bec9e9b1e45b9881c436ff3e676e22ad4b1e9e0a8463965d39caafb526dbe235", + "paper/figures/slas/authoring_code_capture.png": "b328e409ac28e08f7b25bb166b72cc36551d9f8eadac0f8ce8a72fd91896e303", + "paper/figures/slas/authoring_code_capture_provenance.json": "1f1e06a61ace7c219fbb9cd0de78603ce6b801b7c9da88416165b7c8ae74a9f4", + "paper/figures/slas/authoring_code_tree_provenance.json": "b7527fb63456c8722f35c0e421f21d2051d8d2b1ea7a8e223ab1581a664435d9", + "paper/figures/slas/authoring_code_verified_capture.png": "9154c252f4d2a86be1ceac9a235d4a5c97e1cdca9516f6ad4699250105f1715b", + "paper/figures/slas/authoring_code_verified_capture_provenance.json": "8bcc0683f03695fb1c70aef2cf883ad3503778a4e9721fb77e883d824191910f", + "paper/figures/slas/authoring_code_windows_provenance.json": "09d85f79ad5c300d432ccc7ee67707c10d3a9a888eac2f203ed26ecc16368c5f", + "paper/figures/slas/authoring_documents_provenance.json": "0e5214541766eed0063b2f77ede57fe83f1d023432ecf344f674592ae957fae7", + "paper/figures/slas/authoring_function_capture.png": "f4add68e31c21a657dcaed9984864f0a52c32bb60a7f6ac5f2ec7f803b23232f", + "paper/figures/slas/authoring_function_capture_provenance.json": "bc706b05e5ab98191ce66f74ea35a15bd6c3fae04c4a15ae4fd3f140c27c2d2a", + "paper/figures/slas/authoring_function_code_provenance.json": "30e51a9a541b4392ee6c160be8fdb6c7663d9f17a774533408911da148d0a2da", + "paper/figures/slas/authoring_function_final_capture.png": "d92ef7cf0b63a9c5027d374198ae66c7bd6bfc218e14e07cbca9e903584d94fe", + "paper/figures/slas/authoring_function_final_capture_provenance.json": "b133b9f714d214d8b256e5582e1bc3a39e268528f93c442e16a0910fa0af1cce", + "paper/figures/slas/authoring_function_fixed_capture.png": "442498fbaf70731422b19c5730c70027376d0477ec8e3ef3013e82e70357aceb", + "paper/figures/slas/authoring_function_fixed_capture_provenance.json": "c654002212695dffed84840df586ab2936a6c17574f6f81c3c67c9ef6796bce3", + "paper/figures/slas/authoring_function_tree_provenance.json": "4261a813b5c6641e579bea7c849a48c1c53e58d95be5db3dfdba3ec00165fd49", + "paper/figures/slas/authoring_function_verified_capture.png": "56c35478fb88c3cd9f0aa4db82f247a287742ba3947db28da18fd7b7bed09d29", + "paper/figures/slas/authoring_function_verified_capture_provenance.json": "b55929ab5aff4c2bff4cd3ab12fe5781930163d8d49d5c477bdff5c1b3740dda", + "paper/figures/slas/authoring_load_provenance.json": "2204a3cf5da20af6b4e3bcaaf8fd507566ed935bb9c3ea9d0b49f1af034253a1", + "paper/figures/slas/authoring_main_capture.png": "fef97515c69409193cf040a680222ad3e2e77c6c3f20f286488c654d4037576e", + "paper/figures/slas/authoring_main_capture_provenance.json": "9412cb35168ed9f896f4d80f8873d623dc10cb5816043ec2d5339d16d1f446d2", + "paper/figures/slas/authoring_main_verified_capture.png": "ed486ff1f356b11db0632530b85cb535e37f688be6617d21b1d41c8ee506f0d7", + "paper/figures/slas/authoring_main_verified_capture_provenance.json": "d33fd64340a7afe504cf33f6b6f33d65c1c561288876842900acce59721e4be5", + "paper/figures/slas/authoring_navigate_provenance.json": "8fc8d1e335b78a18332e206e6050fea17b01b00d87603db3a415962c2ba182f8", + "paper/figures/slas/authoring_parameter_tree_provenance.json": "aad84bb13d31197f66089d09c4c1c04c65cc27f27830a1cd3735541d6b5fab03", + "paper/figures/slas/authoring_plate_tree_provenance.json": "5162833b085384969836a3563c01648bb9398420891415ce416581b20e804b0d", + "paper/figures/slas/authoring_restart_provenance.json": "57cd6a5dd019e80968677225532f6d6010bddcb53ba2c6622efa002cda747d66", + "paper/figures/slas/authoring_roundtrip_apply_provenance.json": "d126fd8d041a1684cf673a14e4c26169c181b447a2ec794fc25d21ddc1e2646c", + "paper/figures/slas/authoring_step_capture.png": "137d21b4f8f977c6df5dc4c9ed51276641e04daa4e5f2479f735ccffe59af100", + "paper/figures/slas/authoring_step_capture_provenance.json": "23ad7329323f1b38a74f14813edc1c85c5a8b718fdf095de13f50788fc825855", + "paper/figures/slas/authoring_step_code_provenance.json": "6bec34e1a6c0980b507fcebe39d09891087be14d04a8ec5e393b49be71015dca", + "paper/figures/slas/authoring_step_fixed_code_provenance.json": "4c7b5cf5a3893340df99b820bfa306c4c4b0969a650c50df8a1d8ce98037d80a", + "paper/figures/slas/authoring_step_tree_provenance.json": "e6de617835acc87626d53913dd095fddebd51b548d03a5a0cd4701c91b2b4b5d", + "paper/figures/slas/authoring_verified_roundtrip_provenance.json": "d5d98d6cb2946ef0b11a4547aa560591174b7c036e99dc2a8af315b4c857ccdb", + "paper/figures/slas/authoring_windows_provenance.json": "920e97e5446bec05ff432c6d47be5c9f1b2f29645d38c14eb706738b5d934ba5", + "paper/figures/slas/cellprofiler_translation.pdf": "5ec3ec0ca3ce0268230a948ddabfc09ebff1c3db8edf93dfc79239afda231880", + "paper/figures/slas/cellprofiler_translation.png": "4a6c8f6708314c273c364abb3bb8ab2b9ee47ef0a403784116fe869018ba1fc7", + "paper/figures/slas/cellprofiler_translation.svg": "f2c4adfe20ca0b86066200735d41bf9f9c446ebe0daf7b31aa6fe34ad8f0bad4", + "paper/figures/slas/cellprofiler_translation_imported.py": "d933bad3b789811fb44669c4fe92ecb3d02b568acf09084de681232629520742", + "paper/figures/slas/cellprofiler_translation_provenance.json": "529cc0d3426fe452695f143ea34ae5a3b3700dcd979d029cea71de80266678b5", + "paper/figures/slas/compiler_preparation.pdf": "5a7d879d13b51a883d14afde5d15b68dfeefe71ec20ee6c118d3618717edbe66", + "paper/figures/slas/compiler_preparation.png": "41d45bf597ceb66aae1e83b990889976b99b651871ceba9e8aca47b3ce52ed60", + "paper/figures/slas/compiler_preparation.svg": "fdfbe4ff5bf332d02256d1560a09db6a86edad8e14a222a0d25589f38cb5d3da", + "paper/figures/slas/compiler_preparation_provenance.json": "48050c49dbe30c0368e68b01a117ffd6046e72c30e477db03a6ce78c21e15171", + "paper/figures/slas/custom_extension_accept_custom_function.json": "83f1edffcf055087ec218b77ac21ba78abab4f139fa7d00519f6b34b9661488c", + "paper/figures/slas/custom_extension_code_before.json": "0643feb3e37b62fab001619e68e6ff993a35cc9919e96aabe696c4ad47d99f0f", + "paper/figures/slas/custom_extension_custom_parameters_tree.json": "62fb8fd96c919eb39f9e6228a7f98ae1399b5560d4acc606234761aa1fa6ac58", + "paper/figures/slas/custom_extension_description.json": "3018643ea5e4252226b2f4464f01008aeea4082f5535f7399ab2f5777af1ea19", + "paper/figures/slas/custom_extension_evidence.json": "dd72dc4c73be803c61ac6864ec611b6f2a37048caa2a0b8f7176a67a345d98bc", + "paper/figures/slas/custom_extension_function_tab.json": "79f8cf6e4b259f4c1c9acd62c90513e8f3ac7ce27b21660aa082bbfd075d59d7", + "paper/figures/slas/custom_extension_load_authoring.json": "178c109f95ed1793d616c319cceecca7d353a5292223e8e0d501cd14fc24ceec", + "paper/figures/slas/custom_extension_open_step.json": "064653c4ea78c7785571298189ffc5077c3f0f019f92dc313f81d29a432e86b7", + "paper/figures/slas/custom_extension_open_visible_selector.json": "9eea9c75c7e7a2c94a0bd233895deb294ed007cb54ab97d3250284ab5156268c", + "paper/figures/slas/custom_extension_parameters_capture.png": "49f3b150a1956bf9415b6e416a2c7c6303ad1c5fb5c0d02ce1e8335bf30e3c69", + "paper/figures/slas/custom_extension_parameters_capture_provenance.json": "3822cb974f240ecaa79bb8f3eef6a1fe5ea5f0bc6e04266c40fd328ba2e4faab", + "paper/figures/slas/custom_extension_registration.json": "2f2b17d6b5e1977d3c524432042abc4a4d3b78272ac731b7497c992245027c4d", + "paper/figures/slas/custom_extension_registration_after_second_gui_start.json": "37b6075b59ca68aa4389d0895badf4ee1a7b0c6bb5d589e51c82a2bc2c846575", + "paper/figures/slas/custom_extension_select_custom_function.json": "4322e69a31ef4fe8e65aac888d923b51b7f1681beab550e7b3e0b9607b1fa8c2", + "paper/figures/slas/custom_extension_selector_capture.png": "0fe9e88471c8adc2b46c4fc8b63a94d73b7aba1c5b164dad005560dc70602508", + "paper/figures/slas/custom_extension_selector_capture_provenance.json": "aba7a762135290dcfee47181e58ca0407f5b4317156b131692a3ae6c98f0f8fe", + "paper/figures/slas/custom_extension_selector_selected_tree.json": "ab7c7a4f0011aa3130952fb69e6ddc645d229e397d54e4434b1d2b7158e823c8", + "paper/figures/slas/custom_extension_selector_tree.json": "343b902367816f317c52daeb282d0dd9ee6a68be8050bda73f7f96b1ff9aec8b", + "paper/figures/slas/custom_extension_selector_windows_ready.json": "35b8fdd981c12c1f9c33bd7a852d2ac78343425e16f11de5bef4786cb84f75ac", + "paper/figures/slas/custom_extension_step_tree_ready.json": "c3bfd51f801438f4856705d4f577f0ed8cef08f48db7ec62138c4c860270e765", + "paper/figures/slas/custom_function_extension.pdf": "bb8bd6b74da445f800c5db5aea02998a0837a039fde7e5c9c9f1fd90221c5724", + "paper/figures/slas/custom_function_extension.png": "af4a67e06a817bad63ffaa96c0d2d7b7a779e7f85e8d183d0d4b398f872d0dcf", + "paper/figures/slas/custom_function_extension.svg": "8fcfaed367242196f4d6e9d8e459e89568dac1b1e6f70374787860e69a6feb9c", + "paper/figures/slas/custom_function_extension_provenance.json": "ee0e49848a9a0f8017d5c1942bce5d5e870eee090f1936ec4cb6f2f0eb57043e", + "paper/figures/slas/custom_signal_example.py": "b84af3e2857cd16636d631244aaf82d34d3bd5ebd658bd92a56281b2b3aef26c", + "paper/figures/slas/editable_analyses.pdf": "9d608160597f4d152f0e25f09f3ef5de4658acbe8ca5fc618671f2503125dba8", + "paper/figures/slas/editable_analyses.png": "c1c34e6426e2b6a22309bfaecea3c7e1fac200d4046e524afc123a880451ff50", + "paper/figures/slas/editable_analyses.svg": "f72fdd9893b4392d72bf7f6ad1874c17655be179cb160f80682553ed49eaa623", + "paper/figures/slas/editable_analyses_provenance.json": "2067df1235ef4aa28b4f598514124b681ddedc1bea6d3dd8cac8433577e51fba", + "paper/figures/slas/figure2_benchmarks.pdf": "03994926673d192af152fe7a96a3d82867dad156ae783a34d1812dcdc139583b", + "paper/figures/slas/figure2_benchmarks.png": "d5da70a00a7296b6a8e45ccba2e6ae29ef6518b217662f60532afb7603a8bc6c", + "paper/figures/slas/figure2_benchmarks.svg": "fe35c22c5dc3b333460b3935de9dc484b090108d47b678910578a47adf658caa", + "paper/figures/slas/figure2_benchmarks_by_workflow.pdf": "d62cc3e89e211fa7036abc44f830ddb7f379179e6dd8781010165b332b28429f", + "paper/figures/slas/figure2_benchmarks_by_workflow.png": "7c71733553595475dc5fce6b5c403884cd668ba56626d8fbde10a448acb5e3f4", + "paper/figures/slas/figure2_benchmarks_by_workflow.svg": "68ed9679a4aed56aff84420d814771f215b34d71518d5b8f9d004d1ef211ad46", + "paper/figures/slas/figure2_historical_timings.pdf": "98613c052ff5f1056f2bdfc8482e521c0f92d9988817ce5b048d3d17a599f7ce", + "paper/figures/slas/figure2_historical_timings.png": "009b64a2b8ef8fe3d6f7ce432677ae000a133fa1448c8ac803df13632f699461", + "paper/figures/slas/figure2_historical_timings.svg": "a36a96494cdd04a1718499223d5d59e1b0f03cf75e3ffdc8fba05d685f2b3574", + "paper/figures/slas/figure2_memory_observations.csv": "ef6ce818e23cc2ad2e4c2eba7d3bafa5b4bf930fbe71083c3914c1a7360c15fc", + "paper/figures/slas/figure2_provenance.json": "9ef2bf66e0b2b1ff3fe2e42aba610db0cf58a454d784b1edfe98ad8a67b1b8ee", + "paper/figures/slas/figure2_scaling_comparisons.csv": "206afb149cce99499cf36218677e74b4c4af87f14a27dc00288d09f2d85a39b2", + "paper/figures/slas/figure2_throughput_observations.csv": "f62b96ffd6e6cc285f3a32ac88389147c6691d83c7748040e1fa0735e11635ad", + "paper/figures/slas/figure2_timed_workflows.csv": "0a52b40f17043db5a89d7897806984706227795ee2303cf7837fd05c595b369f", + "paper/figures/slas/figure3_agent_workflow.pdf": "9e2329abde34358b0d057024da006e8fabd6c08f43f9e69caec87528f1748e6c", + "paper/figures/slas/figure3_agent_workflow.png": "9cacbe660cf64649c7596802d852c383f54766442003f5d1b3837dea314ebd42", + "paper/figures/slas/figure3_agent_workflow.svg": "8b933465f4d8a59fc8a5e9957dfc48906fe262c86346b23bf7cf8606599c0a51", + "paper/figures/slas/figure3_original_video_frame.png": "c52154ecfa8f7ab3c9f8e24416b735e7ca79adf57111ba2b4dc3c824903d13c1", + "paper/figures/slas/figure3_provenance.json": "f5e1d19e29ccdc07792ecadf3ca97942b52e2c2fda6f5314f6b76018bdac3076", + "paper/figures/slas/independent_agent_validation.pdf": "914ab42f9a780cff2d87d2e493bbac97f8fbfaa588dd6a10037f70db64127485", + "paper/figures/slas/independent_agent_validation.png": "6fb1bc3fb340713f2ed1247feb13e4d72908c4fe5003e2f91620cf160d2d6a39", + "paper/figures/slas/independent_agent_validation.svg": "90833b662fba60dafa1ab23a000672c7b6544abf08232c3b4dd40edb29660e33", + "paper/figures/slas/independent_agent_validation_plot_data.csv": "55e2e72e7904ca90b3aeb10cb07d28228425e8e2f13c81ab64ca6e8fdcbd7afb", + "paper/figures/slas/independent_agent_validation_provenance.json": "8a26e3dcd1589dc31b6f4713148acf4cdcc7f9ee8a041a728d1dc5bfb7757bc8", + "paper/figures/slas/inspectable_results.pdf": "975a61350adcf3d955f102901387410527bda1347116a4d6a6f26c729540b388", + "paper/figures/slas/inspectable_results.png": "ab61922ed8d66b9910d99476cf4fc8a81c70f5867deac360e1bd5ecda164ae70", + "paper/figures/slas/inspectable_results.svg": "4f218f6e2a9d9b136edc5aaa4635e69f5e68aaf4bd8d9aab54585a273a0037b6", + "paper/figures/slas/inspectable_results_provenance.json": "d03ebc3b932a3abd6144e435713b6c920c5b06446418bfc275f1fe763e5474c7", + "paper/figures/slas/outputs_and_inspection.pdf": "7719892e326b26f7038add624e0a765b366601c95821e776d98ab13be43af11a", + "paper/figures/slas/outputs_and_inspection.png": "5ecaf0ec334deb9fe6dd7b16c8c1c4b909f63620e9af81d6600bfb14f19bb129", + "paper/figures/slas/outputs_and_inspection.svg": "ce2da452dbf173c6573659557604a5c156201f8443d6b6ef39a8cd97bc656303", + "paper/figures/slas/outputs_and_inspection_provenance.json": "7e7183522611698c8b4307cd703e9e858df3891d7f92a77b40fadba1016cedd3", + "paper/figures/slas/process_architecture.pdf": "1bb8bb2e4021b3f35c70022ec77c47f2cd08fad0bd584ba44e7904d4d5adb2df", + "paper/figures/slas/process_architecture.png": "f6856a5f564d188c14b66769de880facdd9458eb1642fa119c50e11c9b735d59", + "paper/figures/slas/process_architecture.svg": "766d21f793797cdd98fc1d2ddc5fbb9585ea8cc216883c3bd5bb66dfd56c896a", + "paper/figures/slas/process_architecture_provenance.json": "112ff107feac0e86bcc53ff9183c3458b65b51d2dba10af3ac4f1f81a7ca7d96", + "paper/figures/slas/runtime_composition.pdf": "3eea90ecb49518a12c302f62d63985f8ecccbcba537288b5bd50d2d9a29856b9", + "paper/figures/slas/runtime_composition.png": "11abab9bbcccbf1cf83a7ec699815367c95f2c1ed39ec9b75411e43c2f6bbdc7", + "paper/figures/slas/runtime_composition.svg": "5356f083ddf6c9f15f57a326c862b7fe83762fb4041a702a0312b1048efc856e", + "paper/figures/slas/runtime_composition_provenance.json": "c414adb479e97dffea0cab6bb146ba16671fd385223969c3bbb9a5c6326f4b34", + "paper/figures/slas/shared_workflow.pdf": "4fe5268709e401d923777b38aaf9320f4a490dd5b58cfe45e13fa62f507e9545", + "paper/figures/slas/shared_workflow.png": "70d1df787f4f72f7911c94216ee244eb0cb239928f97b20a8eed084ffe393aa1", + "paper/figures/slas/shared_workflow.svg": "9ac75f5bb84a4b5d25588869e1d272407bb73dd28bbf744171d6cc5b1da38985", + "paper/figures/slas/shared_workflow_provenance.json": "893e1422a5c2f26f2c06b22c32a90fe9ad07551a2e4f5153cc99bb5e516c4cd0", + "paper/figures/slas/visual_story_contact.png": "d4cbbe7660ca177ebeb0d289d673f8ba586f1c0c7068a342f15b9a00b239e57f", + "paper/figures/validate_slas_roundtrip.py": "8fe1222d3432e322cf82f720531c617af945a8518d282ff3414f84ccf38b291b", + "paper/manuscript.md": "e5dda54ba0852287f90be15e0da551150fe832e2da01924c1bfa549b15db65bc", + "paper/nature_methods_comparable_papers.md": "901fe5f5d9254325d8abf304495c348533ca96fe9735d31035261c6f9ba0a22b", + "paper/nature_methods_comparator_action_plan.md": "2eedf0a4a5ab209c8adf0ce3b2535ec0e629d85551eb1237ba7a0f5beb1b5696", + "paper/notes.md": "9edef4a2c29c99220e312260a03303d592be17e9de90a80a1d5aa5659b9f7ec3", + "paper/openhcs_comparator_review_20260518.md": "883389712919cf9c00bc7bd8bf74b726300aefb577f522f83864761fcda602e1", + "paper/openhcs_nature_methods_draft.md": "d727b152d3f5d4cbf0c332a33d898ae463aa5259de37d65373132a282779227c", + "paper/openhcs_nature_methods_restructure_plan.md": "7c3a29e96a2c2abafcd251f0c16f27d6aaf1e2d7222c230cdc9346b2ba593e87", + "paper/openhcs_nature_methods_review_comment_audit.md": "2d945d088f2d81e5b5f75eb2f86bdcd1d08f5aa21991cc2eda1ed4313bc7556c", + "paper/openhcs_references.json": "36bf89f27da638ee8ce45aa3db131084c82ad94ffa02c716eaacc77e13ad77c9", + "paper/plans/RESEARCH_SUMMARY.md": "b830a44aea336f118ed282e50a528b6830da8c76a2251d804c7b8008933648bf", + "paper/plans/agenticj_accuracy_harness_gaps_20260929.md": "824979a1f7ab665ddad2e8ba6e393c9fd1bb7b0cacf4a07a29d5461ecab0869c", + "paper/plans/annotated_agent_dataset_candidates_20260915.md": "da62d41bec616793fd466f54fb68fdda7310a5af9dc7f572d91ca497c6d5c98d", + "paper/plans/compile_observation_identity_20260929.md": "d9e11331a8fbaffec98e995ee9624e7ccf1a875ae4e487d64dd4f908c8894b15", + "paper/plans/haase_notebook_translation_candidates_20260915.md": "92a25496bf1e834a10b01ce2fd1550313b827ad547b11f30b49ab493bb2bfc3f", + "paper/plans/haase_validation_corpus_20260917.md": "f03483aa4c7d8eea7240da378f1843e0353dcfceb7f711bdf7be18696b8ca39a", + "paper/plans/materialized_result_followup_20260929.md": "bc655742577585b819b03d587decfcce5bbc3be104627221d3868dd41e9edf2d", + "paper/plans/materialized_result_reopening_20260929.md": "893a3cb337684118e7b8bc4dbdf3af7d050586930ba0e2b7694e98161e00812d", + "paper/plans/materialized_result_route_fix_20260929.md": "d3dc77ee8014e1cf43c52e8bdc40cf9a0a40705741ee9d292ee1b0ff2d13795c", + "paper/plans/mcp_post_dispatch_failure_20260929.md": "291b3e7974071f5a7b3fe80ccf1ae9d4efca14ec75578b932e69cd06acacbc65", + "paper/plans/neuroncyto_fourth_replay_receipt_review_20260915.md": "6dabde2b3340b5e92614ffedfcb50a2d208d0ae9d08020c03b2216b4051b59c9", + "paper/plans/orthogonal_class_census_20260929.json": "69593f1f4729c27cb3b7b3fa9396fd0e06e358649023f309218ce8aaaf7c7142", + "paper/plans/orthogonal_integration_20260929.md": "f117d11a112008ae7c250521ff21bee8a55c6eefa30d3c109b503722d2d203c2", + "paper/plans/orthogonal_viewer_fix_20260929.md": "47a4b1fd2598bcbbdae3b47b940d02735840eb39ffd4656e11938f4b9975cbc5", + "paper/plans/orthogonal_viewer_receipt_20260929.md": "1d3acf9bd7cd51d2253b13af17a8bb5af164f3b8a77794eef19f72733b404b07", + "paper/plans/own_data_neurite_comparator_20260915.md": "cdf99d9b5f4dc6b30db2fc2a5d4c4219ebe58fa690146779370b4fb1c75e5f93", + "paper/plans/own_neurite_blind_analysis_20260915.md": "5db9003ac081f66ecc721fd953a9359033c5f53a75cf0a29071208f9a68d4f0a", + "paper/plans/plan_01_benchmark_infrastructure.md": "b6eda04aebbf0d7ba851839fed89248333b7eb6c50f167993d6f705f244de15e", + "paper/plans/plan_02_ADDENDUM_real_dataset_specs.md": "2db25d7562b4230a39e2a3d3eb179ec8d5ff2fe4175dc1e25ae11e9da65fc0bc", + "paper/plans/plan_02_dataset_acquisition.md": "915611204fb8b30fe4170a94e04483d82261f23741fc953f59564d27eb448e9d", + "paper/plans/plan_03_ADDENDUM_real_pipelines.md": "33cd218eae443f1bcd7a76d76035baead377c93aeb3ce29bf7dde48b27115d96", + "paper/plans/plan_03_tool_adapters.md": "d40c9869171bdccbf83217b950529e5cb3a1f54bec3cd4e46a0bd71856f295c2", + "paper/plans/plan_04_ADDENDUM_correctness_metrics.md": "3912f5a2039706fb3d04fe14b97baaf06c8d55d1bb41daf913d3ee2f5daec4b5", + "paper/plans/plan_04_metric_collectors.md": "d63d3f79ca6209713430bf37be98ce954c422731184731c944342e8ceb277b8b", + "paper/plans/plan_05_pipeline_equivalence.md": "49fd80c60a653d805ed5a309525da40ada2968283a93903d2e0346bba41f56da", + "paper/plans/slas_accessibility_edit_plan_20260914.md": "12f4437e55a151a465053a541f0f3d4e57ccbdec4a53347dd69132009275b751", + "paper/plans/slas_accessibility_focused_20260914.md": "a63b5dceecaf658d07d011c2ea33f5cda7374a03f4f11cafc8c5c8b75b5124bf", + "paper/plans/slas_accessibility_implementation_20260914.md": "317d777bb0718a120a7bae124d6e9270016742399484845c07e5f8c7ba45a402", + "paper/plans/slas_ci_evidence_revision_20260914.md": "f198d6f5e65a1aa1c2698508dfcf928aa368b3216e960b74610112e0a3932d6b", + "paper/plans/slas_confirmed_revision_20260914.md": "645a9bdef0e8fcaf85b54c03645ad4503a892912ec6e74bf70918fdac8bb76fb", + "paper/plans/slas_framing_discussion_20260914.md": "46864ce089a90803c8f9567b2c31471417079a2b014cbd52e52b4cc46d59e486", + "paper/plans/slas_panel_pending_corrections_20260914.md": "d061fb1df24151c4210142f114bfdb605d0041dc3266b6646b735e672a75cc0b", + "paper/plans/slas_revision_20260915.md": "f84ab1bcf4e00f7ce86f8f20163ec9592939607ae0fb56a654b7c4a10abe02b6", + "paper/plans/slas_supported_refinements_20260914.md": "f319a1d62d7d113417cc2b1f6874e0edc953c1ccad60f94776a44a92fbff7e0b", + "paper/prelim_data/figures/benchmark_figure_index.md": "4b475ba84572b6440d0b7472d2ba706f19530e02eb02cbac83002b8101a96033", + "paper/prelim_data/figures/cppipe_accuracy.png": "1008e8f706af822849df6b18759b58f9f1797ffc83f3b4f2ce0d66692e7c6e7d", + "paper/prelim_data/figures/cppipe_accuracy.svg": "97d4e2e170e6359f4b9abf5196041b3e37e7684e916e62b18d95f63ee901308a", + "paper/prelim_data/figures/cppipe_accuracy_zoom.png": "df8028826eecea55015a5ed4bfe6619838b32e4fc252cd0c8eed63bbade82aba", + "paper/prelim_data/figures/cppipe_accuracy_zoom.svg": "5161bc138e52a44abf09288bf7fc45cd3e55d58a798a33f06895fbf2f348332a", + "paper/prelim_data/figures/cppipe_assay_category_accuracy_fraction.png": "df910d8c6a9409337d49115eb73949d9e87832e05c4881a34c9917608d4333c1", + "paper/prelim_data/figures/cppipe_assay_category_accuracy_fraction.svg": "8b254816896bdd80b8fba428308b97ec9062dabf0b85e5dd2e842dda374aa03b", + "paper/prelim_data/figures/cppipe_assay_category_raw_seconds.png": "c907cc49a0f13604611860bdde6ea4344d8b3edf0825909c7fe523e30d6202f7", + "paper/prelim_data/figures/cppipe_assay_category_raw_seconds.svg": "cddd69b96160dfe7f0c0123a775817b10aa08974544bc57194912227a013e895", + "paper/prelim_data/figures/cppipe_assay_category_raw_seconds_log.png": "b8eee5c705253c4843a41194631cb4c76e0a10d30137e469cf577f7d75fec2e2", + "paper/prelim_data/figures/cppipe_assay_category_raw_seconds_log.svg": "7461fb2989c374ca22f4f23833940dc39df095ec7ee8db7c1ffb08aeeb95e36f", + "paper/prelim_data/figures/cppipe_assay_category_speedup.png": "f2ae613488733e692abcd8d8afa91f53a6738751ca92dde827426b6ad16613ca", + "paper/prelim_data/figures/cppipe_assay_category_speedup.svg": "3c5060fc3fb70cd8c76fc469fabf37a44e958b59b7d82ef521da4ad83d01dcc4", + "paper/prelim_data/figures/cppipe_assay_category_speedup_log.png": "1061d707363989fdc3e6e53fdc6c3aa8376f6119075f0e8537f65cea85c382a8", + "paper/prelim_data/figures/cppipe_assay_category_speedup_log.svg": "fcea644ad1422e78c29e4d6b286e0ad6b50913b1d34eb94c93f43be2a9083bf9", + "paper/prelim_data/figures/cppipe_average_speedup_points.csv": "22a059250707ce6ac06f5f14b45ea3ea0ff6b776fde1dd3d556cbf9ec7a889a2", + "paper/prelim_data/figures/cppipe_average_speedup_points.png": "fc9b392490ba4ce4944d5cf3d16bc3f4d1b717319c54b9d76a6b491c18d5533d", + "paper/prelim_data/figures/cppipe_average_speedup_points.svg": "1ffb2e4acc7dfc6efa5220bef963e6bbf326604964ddef15c0474571eea574cf", + "paper/prelim_data/figures/cppipe_comparison_category_metrics_long.csv": "29b2ac7be05fd378060f53a8962958ba962a11c4664eb8db672b550f6b1e14c2", + "paper/prelim_data/figures/cppipe_comparison_metrics_long.csv": "e0a492741908b7c0158bdb78682195acfa0e3f3373f81225eb912cd79e8daf29", + "paper/prelim_data/figures/cppipe_module_category_accuracy_fraction.png": "1978d937686b99fb55defd68cddedbea60a19aa924008a18fe147e1ca133e7fd", + "paper/prelim_data/figures/cppipe_module_category_accuracy_fraction.svg": "334a540ff753f6a3bb9d157dc91a872a17470322b8fa05af9b46c2010617e918", + "paper/prelim_data/figures/cppipe_module_category_raw_seconds.png": "f794f0d4ce25622262f178e8bfa620ab7b68725f830befa9c6f09f4b5d7aa5ee", + "paper/prelim_data/figures/cppipe_module_category_raw_seconds.svg": "f52456a5170ecd27d496b743e41418cfe0b15f078b889341c49d003c49107b4d", + "paper/prelim_data/figures/cppipe_module_category_raw_seconds_log.png": "d316fef1253f60b38ca8094c1e0c17c41750fd15a02ca95ad4fa12220a835d53", + "paper/prelim_data/figures/cppipe_module_category_raw_seconds_log.svg": "89095fb32e06325341e22a4404e953ef2587d5cdfa44adb81d55f4ffb2e6406a", + "paper/prelim_data/figures/cppipe_module_category_speedup.png": "a95ac2eb07e92146d8ce6c07b1b1408f63b320edb9f17ca9c27649d5d6c3fd3f", + "paper/prelim_data/figures/cppipe_module_category_speedup.svg": "0f347603f99a99d785497fd8ffc7383d81b0cd1bdb132414d7eff7e9e73f254e", + "paper/prelim_data/figures/cppipe_module_category_speedup_log.png": "fb5048b751421e98b66a38313866ab82b02818836c2e07ed9af50c39d90a74db", + "paper/prelim_data/figures/cppipe_module_category_speedup_log.svg": "1e851d4560742196aa570243d511049609425776f051c6702b3986b44106391a", + "paper/prelim_data/figures/cppipe_raw_seconds.png": "d38728e59d78f8dfdf95b53906b4cf5b8a559f7e6dceedf90a0ed27c2ea3315c", + "paper/prelim_data/figures/cppipe_raw_seconds.svg": "eb8928b34a7f02b0ff8df8b4804d02b09630c6f285c4d294bf4cd6496d931fa1", + "paper/prelim_data/figures/cppipe_raw_seconds_log.png": "4838927a2e2640d57790e8f1e1edf61f66afb6c57f4ea32d38b5a58564288e17", + "paper/prelim_data/figures/cppipe_raw_seconds_log.svg": "b3b5adc903f401e71cee4b4c9bb2dbca304162fea6412afae1bf12df801b2e32", + "paper/prelim_data/figures/cppipe_speedup.png": "69d2f05b9fe4a6538279c81c9cf6ec09e818c66ab3a003af8d0d1eb819404047", + "paper/prelim_data/figures/cppipe_speedup.svg": "865f98de6757a9d7720f80442d9fecc41da95f5104963585618ecfa6830c1ed1", + "paper/prelim_data/figures/cppipe_speedup_log.png": "48049bde3e41d503781746d8a8415c5dde9e7c1de318e4e1f6f27c5d1357a665", + "paper/prelim_data/figures/cppipe_speedup_log.svg": "ea8bd32a0bd95e7d961410526a7af5ae824e2672e1167aee00b16d5b5d33ba1d", + "paper/requirements-build.txt": "748eca6db1077e668b08accdbbbf3363dc8cd88dda17f078f5591f44ae39fafe", + "paper/review/openhcs_nature_methods_draft_review[99].docx": "482df0d16f5d22e304bf2997df35d3c537499626e47910accfc49c8edb6a2ba3", + "paper/review/openhcs_nature_methods_draft_review_plan.md": "f7d7439228f6d5161dcf31e9aac07b8a6273c90754946920425551f3183f1289", + "paper/styles/README.md": "31cd332517ab7eace982d4b4624b29962c4ea2b6e62b453fb3e33af350814d5c", + "paper/styles/elsevier-vancouver.csl": "005e915c3419eb2b84eb2b31bffc411dce3890619d3fd0ed23ac6591ac128335", + "paper/supplementary/README.md": "dd19206225db44f0cc08fb79e9ae6af400c67c26e5412742a629f5ee5ae62198", + "paper/supplementary/ci_official30_085/README.md": "0b7760be9da48d8a0e20d14e5f473f2131f47e64cf967d013ebca2fd2d2c01c3", + "paper/supplementary/ci_official30_085/observations.csv": "e87aee1631bef063b468a029d2e9c68247c0d9d90c45c8eba78fc462b615d240", + "paper/supplementary/ci_official30_085/observations.jsonl": "e5355d8625d8f774fd3aeb9eede30f807e721c0fbf668ed4ec9a3e8847783f23", + "paper/supplementary/ci_official30_085/phase_timing.csv": "bc52819438a4b1e1f1da9e9ba07a6110e337e91b8f755c2b1c5238ad8ae54c0d", + "paper/supplementary/ci_official30_085/reference_inventory.csv": "d17005e72abf61e803d0045e10448d743fd33c16e7dd1dfba7d0524326a2c68f", + "paper/supplementary/ci_official30_085/suite_metadata.json": "f20a6d2d1803e925b0e58c6221edea5d08d772160b94c1095b459f3b322a62c4", + "paper/supplementary/ci_official30_085/summary.csv": "e0e075d9b29d6b794d5dee0a2f3cf038cfe1edc8832b9188455974e8aede36a8", + "paper/supplementary/complex_cellprofiler_workflows.md": "46f46953e5e388c214b6ff07103ae0582b29af998c4c905879506113299d4ff7", + "paper/supplementary/complex_cellprofiler_workflows_provenance.json": "bb4c146789243e6c7b1184f8f52491d26bad1e670716a441b7fdf159a05491e0", + "paper/supplementary/cp_tutorial_3d_monolayer_imported.py": "d46576dc06db0ccb38756566ee81435a5dd7a7d4685037931e0ea7f8f121d3c9", + "paper/supplementary/cp_tutorial_advanced_segmentation_final_imported.py": "96a537904072149b9663ba084ab60e174b7faebbbcf8066881fa51507e0ed6a7", + "paper/supplementary/current_neurite_replay_summary.md": "47f0adfc7a54ded668eb7a3778c900466b9aec6bc0dcc07d2bbfb8067a79c68f", + "paper/supplementary/independent_agent_validation.md": "65e792fef033e0c2df1bf11f499938851038a6595b881db5040fdadfaa15ffd8", + "paper/supplementary/independent_validation/README.md": "92ee6f0a9cb858f996fa0a31b8d409b5adb4d56d43e84b4128876dd03f2f0113", + "paper/supplementary/independent_validation/bbbc007_frozen_pipeline.py": "ba22537ac2e79dc16031fe04e16ecbd1fbc84dded8c640ea7b993d5fee88a548", + "paper/supplementary/independent_validation/bbbc013_frozen_pipeline.py": "e42c6313d70ff88e0361d6795eed543a0b531b9cddb2cea36b1fba01dfe3546f", + "paper/supplementary/independent_validation/bbbc039_frozen_pipeline.py": "d30df7601c50210ea3cc16c78ee6fd1224f05e07191cb7983bf1699b821c8b88", + "paper/supplementary/neuroncyto_reference_audit.md": "0bdab3199554e593934226427ad1e3c2b0894bf5613f905572703eb8316added", + "paper/test_build_paper.py": "54aa3e622c6cd76f685ba43b005b198cab7c716f66b08864efcd1c51bdfdb07c", + "paper/test_build_regressions.py": "90ae3a7cd8ce506a1c51436772428f28e409b5fee18ec1b4d677c217945f9583", + "plans/cellprofiler_integration/architecture_design.md": "b222e7e299693a6630e7452bbc9116e46c6dbfbd673e06d3975582db65a3f6cc", + "plans/cellprofiler_integration/canonical_architecture_plan_20260504.md": "648d1622e343df7d7e76a3911731a17b86a479d83d182b1b4383a243028ee28d", + "plans/cellprofiler_openhcs_architecture_mapping.md": "010735146adf8b0e8c20cbd25a010e1dcbb5f1cdcc08fe98476dee26481d3879", + "plans/cellprofiler_refactor_plan.md": "7dd4731598cc7cca3c8166f92fa4c299ac84d4e2ee197464c9d8f5da739018e2", + "plans/context_stack/plan_02_simplify_stack_builder_call.md": "e3d3475ceac776eec930c1365799760d9f493a074e7084afa62a81e0d0982da3", + "plans/context_stack/plan_03_resolved_value_cache.md": "f4c797354a8be3ed5dd049da3dfad38d351d3294bcfd0d6bfe8758f43d26035f", + "plans/context_stack/plan_04_object_state_migration.md": "c73959ccf300ad7424b7b2b1dd71e7ed11842fb77e36e73cb5967669be810584", + "plans/context_stack/plan_04_virtual_form_state.md": "2c49b9d7ab026bbbd1d9a1c8d854ca07bd776dcfdc089372e0fab6b9f91914fb", + "plans/core_surface_reduction_plan_20260509.md": "733a1b2b24c87ea96114d157f8d62dbd7e4ec804346f5b6ca8ed5786588b78b3", + "plans/dynamic_function_registration/plan_01_dynamic_function_registration.md": "5994b1a11986ca5ae14e08fce44bd5ee9c0197f6bd5fe76cfd31b86ddbe1d9a6", + "plans/experimental_layout/plan_01_ui_widget.md": "078e0f86d84847f22731c0058f1def7dbb21d6fe2beddfa45e1cb74681a1c59f", + "plans/llm_pipeline_generation/plan_01_llm_pipeline_generator.md": "9fa8c79d4106793976278ac0257015674d5d273c5d23c817ae130738b69a3115", + "plans/object_state_extraction/plan_01_object_state.md": "efe079f196b1fb635765785bdd9198d84e40185fab3bb81f30204b9bcd3724e8", + "plans/object_state_extraction/plan_02_pfm_integration.md": "8346c24655d2a42a435a20cadb3a7e1d0ed608ae1e514ec0df28186549216c67", + "plans/pyqt_ui_extraction/plan_00_master.md": "00b7d0a5fab21082031d9157273c42ac908ee28bd9566e70fc56c671b3618f2c", + "plans/pyqt_ui_extraction/plan_01_package_scaffold.md": "620de59c2ccaf8e74d753d9e890e0b5f6065d6ef51400251ccddbbba131e8a87", + "plans/pyqt_ui_extraction/plan_02_core_utilities.md": "db94715b36944fc5d2176e59dad53008473b5903c524694f7ac29b1d75a8eb0a", + "plans/pyqt_ui_extraction/plan_03_theming_layer.md": "2519038df00630ad14e1ca30a27d1eccdef7dd41cc798cab06c421f71bfe5763", + "plans/pyqt_ui_extraction/plan_04_widget_protocols.md": "7f06fa79260774a884fb7c93f01ef3a403b8d3d9ee9c0ed6992064145e5360e8", + "plans/pyqt_ui_extraction/plan_05_extended_widgets.md": "5554b16676af640c5a4369d72ac48516acc9d7c39024c57cdf2203065c7718f3", + "plans/pyqt_ui_extraction/plan_06_animation_system.md": "022ec73d55e09ab4c66502de993c5b75aa782fba13785e366ac282c420d50278", + "plans/pyqt_ui_extraction/plan_07_service_layer.md": "f0f69c384e9e03e58c68858e0f29e7066baf409e9d74bd6907e4c0e5500b9cb5", + "plans/pyqt_ui_extraction/plan_08_widget_factory.md": "09c4e5d02c24980ff11f0a26d3a2c366f337ae53ef152e94eff021172098c30c", + "plans/pyqt_ui_extraction/plan_09_form_init_service.md": "13fca4def902719544ec8d25792b42793628df47de566c4e4e53113e783df762", + "plans/pyqt_ui_extraction/plan_10_parameter_form_manager.md": "a381115d462f9319cb0120cd0fdfc8e8e1376546369cdbdb8f46dfbdbf0486ed", + "plans/pyqt_ui_extraction/plan_11_import_updates.md": "359d71bcb96766154fec81184887db0e35ddb9e4df032e0640da499048429cff", + "plans/pyqt_ui_extraction/plan_12_test_migration.md": "01e6f20e33e6df139a48f2df4bb79a913baec3a83d490b899ba0747a8f0cc29d", + "plans/pyqt_ui_extraction/plan_13_documentation.md": "4dc181ca1487ebe353b577cae6ebc6a6c45bcbdaa758559494176ef86642d4fe", + "plans/root-artifact-cleanup.md": "68ba0de9c8eec375680855d574e2d248e41bbb0deec98c9fa9c246dec365bf57", + "plans/rot_elimination/plan_01_python_source_serializer.md": "5a5f7aead46dfc22aa2833d487d1ad486c6ca4302d4603dd4588e71f7fd45971", + "plans/runtime_externalization_plan.md": "86e610a423d2e1cb45135da721c25f6464314619eeccf8b7311f6db53cfb2293", + "plans/runtime_value_artifact_upgrade_plan.md": "d08dc2994c09828a866fb44300bfa775a8a278cf402b44e3d458555718ca556b", + "plans/time_travel/plan_06_collection_aware_object_state_UPDATED.md": "0e154feaea979d1985ffbd12ecae52a5a17fccf318d75c3e3c556c55bf5aa73b", + "plans/ui-anti-ducktyping/README.md": "1ddf19862b436ac940ed26abee2c3718e0a887cb0d1e0a27b8b85bf33b8c5a24", + "plans/ui-anti-ducktyping/plan_01_context_tree.md": "5dab2381970bacab92681b91c3a923b9058935fa208185bb2d46f05e52c5f76a", + "plans/ui-anti-ducktyping/plan_01_widget_protocol_system.md": "8fa7d7b3b8af0fc2532775f0ba46de10225a05f547f896bdb8d323c653a70af3", + "plans/ui-anti-ducktyping/plan_02_widget_adapter_pattern.md": "b510b795b7b6271f1dbb981584e8abd63bada0b95a7dfa31a44710499d9972b6", + "plans/ui-anti-ducktyping/plan_03_parameter_form_simplification.md": "e3728492d401e101ee2b41e76144f3c567265c83d9bd60474f9f1c62deb9dda8", + "plans/ui-anti-ducktyping/plan_04_signal_connection_registry.md": "c52af0884034967e87b7a94c541204daeeaa0bf91a40b81ccdf8a40060bc5347", + "plans/ui-anti-ducktyping/plan_05_migration_and_validation.md": "1d84c074ea16f26abd773209e7df4e98530db831ac67c7c5502e6806c2415300", + "plans/ui-anti-ducktyping/plan_06_metaprogramming_simplification.md": "283efb6636557fbd6a7ea4a1cad63bd293e731a949ea50d41221902c92f8e758", + "plans/ui-anti-ducktyping/plan_07_aggressive_abstraction.md": "766d6ce514430d33d4a4d1c03345c61aadd48068d4dd444d0017c59756289d82", + "plans/ui_refactor/plan_14_eliminate_values_dict.md": "7d958d7f31574d6bc98e691c9a8595592b28dedbdf4ec2f6ae2578a03433436c", + "plans/zeroc_ice_dependency_solution.md": "a683c7bb286b4ff3cca361d97ec067067e9f043e0a2e8ffb958ffab20f6863a3", + "pyproject.toml": "1d263e3866d804a642dec3a37f8d5cd9d7df95aa40c807c59624e7906d648aec", + "pytest.ini": "abeb2cd3d7ac83c5988690e5b991c0989a96b46d62febc48ba1c7664b6a30d1f", + "requirements-omero.txt": "c631b5954fe3ec51c5167d622cf1d73359dde5432635261a70ee7b73ebf51b2e", + "scripts/annotate_wells_with_experimental_metadata.py": "706221e6b1dd2b082998016845371be39f94f3ad549cac105003f09bdbda805c", + "scripts/benchmark_cellprofiler_fixtures.py": "0eb75c5235552b1f337e879eb712012b948beb41273a29466159743655be876d", + "scripts/benchmark_cellprofiler_vs_openhcs.py": "d5607333b0595b7553358db021d584af5bacbffde8c95ced5c7fdea160a06902", + "scripts/benchmark_cppipe_throughput_scaling.py": "19a231a3fa56b35954dec184cdfbb1aebcbe4cca6467270a89e4e9517b303c43", + "scripts/benchmark_cppipe_well_throughput.py": "73b30d0cc3174c04cb479a72e97fe094ef6ad9fcfdacbfb37e43fbaad53560c3", + "scripts/blind_analysis/README.rst": "66b66d96462ba385195f1e30e4d86a6cb40cf82bd70f0bf64759279514570362", + "scripts/blind_analysis/VALIDATION.rst": "a40422130589d795422db01567f90f65e5ac0a9c889a2a0dad3d9caad20d7188", + "scripts/blind_analysis/mcp-client.sh": "0b9276c85f682a944f0babb1491d63e20db0080783e5d90a091c00350e614896", + "scripts/blind_analysis/recorded-mcp.sh": "fab193c24c35ae7b80fbc4b2da9e1af4c83087f79fff7a527ebb5c854f19829c", + "scripts/build_hosted_mcp_plugin.py": "b787dfeccdf08639b3773702742e5325224653c939d25fbf33173b2f27ea497a", + "scripts/build_mcp_knowledge_assets.py": "efae1cd52a411ec009580c058583e97f10ea16bb995c72cc2231b240bc01cef8", + "scripts/build_website.py": "9850f134c1e2bc600c5b291b77d4c77d82a5ec9ba80eebfade7dd28739ea557d", + "scripts/capture_media_gallery.py": "987f0b3fe09f98fff0e7878053867311af7b65004569bd75937f627464e063b6", + "scripts/check_basicpy_source.py": "80bb44b887d4ec7cd7bc44a87163cfbd8eb3be6a783bd6ff8a2f6782b6fbc53a", + "scripts/check_mcp_memory_ownership.py": "2f177b54aada3199271c20960e749c5ad492b5a8f277faa7494d5be4bb411813", + "scripts/check_refactor_r1.py": "4c6282f52e7c188068dd9d63399f5522a7cb1d222504d0c0b809be10303884ca", + "scripts/codemods/audit_cellprofiler_declaration_semantics.py": "e5bf793ab628fd87c065c5776770c7c69448d38c6970b25f932679ec11190efa", + "scripts/codemods/collapse_cellprofiler_artifact_binding_declarations.py": "9f4db8c587411877491a81d0de546ab663dc788fae7a231fa16fbefc30cf042f", + "scripts/codemods/collapse_cellprofiler_artifact_output_hooks.py": "735d9a0c6124fdf5d1eac5f952464233ff8f2cf40b2dcb9e669b6b62469b7b17", + "scripts/codemods/delete_cellprofiler_special_input_policy.py": "bfeb1bcfe6d935d041c02351226afd5976f827ded8d9d20f676a1ab52e806cf5", + "scripts/codemods/delete_cellprofiler_typed_artifact_mixins.py": "b2e536e0f1d7dafe807f5da2415883b3da2b0f4702ce9905049a2584c932687d", + "scripts/codemods/migrate_runtime_input_binding_request_api.py": "454911bfe649a7f55591d9566bfbd566924bc77a623973dbc15b080895debd23", + "scripts/codemods/order_cellprofiler_setting_binding_declarations.py": "de5ce8f9f6648ac3bee14c2236de7d2b0a8a9b40b8f3af9f57a1abb621d64ab6", + "scripts/codemods/replace_cellprofiler_typed_binding_queries.py": "ea634ee559c7335c1077855c5043749b39630a2e2e5a48dbf08896c490a98457", + "scripts/codemods/split_cellprofiler_module_measurement_owner.py": "a23e15ceff1aaaf206732b3175ede59f137ea3fbaeeb80a51e27e7ecf67d3c44", + "scripts/codemods/split_cellprofiler_module_settings_owner.py": "7baef4284974307c68c9c17d7ddb56861fce4e125c30ce0814a1db1a4e5af9da", + "scripts/dev_install.py": "b243e436e5c04e2a7cb9a8b57b6a0965e558f7996a5ab949c2a95002adb38933", + "scripts/figures/README.md": "b0221de1a158b6f0e12f158d70dbdaf826f9261f211c2a5afbd71876bd88dfa6", + "scripts/figures/generate_automated_plots.py": "7c5ac5c64321dd347676cbd8e99345ec029d89ea5e7bb2113df6a795392dc957", + "scripts/figures/generate_figure_mosaics.py": "bdd4e5ff927e6b5ee3ee336e355d063f1d72272f3e8369c93fa82af9bfea9c5b", + "scripts/figures/generate_figure_overlays.py": "1856cea883da92c703e69c0b77910758592d4a6b837ee7900f13daff79a6abfb", + "scripts/figures/generate_xy_xz_composite_figures.py": "fc4fff47c547d77e258d9d8c798e4a75a0ce073a70edb0d594e217e8aba9713e", + "scripts/figures/orthogonal_projections/__init__.py": "ed8c1a3121eb209d918153f1fb900f42729751462371f6748a13a2b231b0b6a8", + "scripts/figures/orthogonal_projections/cli.py": "89cf3f6631890c0c2ec957daee7eecf90b7a5640c74fc98a77c231b4678e9826", + "scripts/figures/orthogonal_projections/composer.py": "4b29181a662a73b7320aa72f68f7dba645957f4580e8a65d63a61f1e9b557557", + "scripts/figures/orthogonal_projections/constants.py": "870673e584e224f9a492f84c7aec044c71660cb9c722bade114dc734f8958c9e", + "scripts/figures/orthogonal_projections/discovery.py": "139e6fb2e67809e23da465c5a1944c156a04313d2c6b28b03bd8c641871cde6b", + "scripts/figures/orthogonal_projections/exporters/__init__.py": "f20b766823fd30f6baef38ad85337133f63cca31ce8d85807093c429f9f0b3d1", + "scripts/figures/orthogonal_projections/exporters/assets.py": "ba3f324bddb11ab1019f77db5bd2c081d79c4b734aca0b95b3e57eedbbf2c679", + "scripts/figures/orthogonal_projections/exporters/grouping.py": "bb75b6465d2ce31c60ddfa1010fb697f0e573ef2a09492838e938a4be1223e4c", + "scripts/figures/orthogonal_projections/exporters/layouts.py": "10ba73763e907ae51e5682af0845d842a4bea2fbde3336e7e5860da01f1e95e3", + "scripts/figures/orthogonal_projections/exporters/models.py": "45e84afe264aa1a2a7b7f14b496acb27913c6ff02c257870426777b9a468c457", + "scripts/figures/orthogonal_projections/exporters/powerpoint.py": "c05492f0edb9dc4a45993dbdae0f1cc62d03712dd282b9d09a7625b712df08d4", + "scripts/figures/orthogonal_projections/exporters/service.py": "834a0210eaa25cdc024dffe2b1edd6471856658484489b226299e0f9fdab529f", + "scripts/figures/orthogonal_projections/io_handler.py": "2ba9f49646db722f90334f902acebc9e57045f1c81d91d915224ac4abb52ac24", + "scripts/figures/orthogonal_projections/labeling.py": "1ffabf6335e016c0b73443b9aba8c03f2eb387f330bf6e30815317bd93365d3a", + "scripts/figures/orthogonal_projections/mosaic.py": "58e8a503f752dea44dc831718fcb013d368aa4f6b4d524815bd8ec2ee49e1d69", + "scripts/gallery_catalog.py": "7f493e0cb305e14ee4cf7d3e995c67df4200186a8a8aaf00eef0bc66d8d191cf", + "scripts/generate_brand_svgs.py": "2abd365f16d6ecfd316be22f6356d869e8e5d2c876a6a0c48181ea3c0daf813c", + "scripts/generate_cppipe_benchmark_figures.py": "5320cf489061bb49c69ab8863a3c6ae6775f53cef11edd02c989dfc7f17171e1", + "scripts/generate_cppipe_throughput_figures.py": "ef0256842ae53232b8720ff853f04f5e731243d343979691cc2d15cff0ffae1f", + "scripts/generate_lab_meeting_deck.py": "be7922c3cadb469af36287000bd6ac16e9d32c51d13b790e20423380f32c7ce9", + "scripts/generate_lab_meeting_summary_figures.py": "38e41a6782da4453c0d7976175c461c6d2c2bfe1d472cd9572dc7bbc116f5c78", + "scripts/install_ci_candidate.py": "840eb6612bb41758979104cab1165d7e2297011fc6ea907ab67b9112f349b3e2", + "scripts/install_omero_deps.py": "17d63a27e1635ad624b80fd0eb0668f8b8f01f62e30a303439353634e030b4db", + "scripts/macos_installer_window_probe.swift": "45b22046ae4c98d697beba8a6f91fa3d72c07083b91dcec83a48811795b0552d", + "scripts/master_multi_plate_demo.py": "8115619d0dd4aeedfd8a4ce1b20f7eabd8f3c1f0ffbdc20e4dc8b2231cff7ffb", + "scripts/mcp_assay_showcase.py": "b33e3bcf0babebf7715bee8f4162d65d1d48128f061b8e8ae45344a6adfdd046", + "scripts/mcp_thesis_demo_live.py": "5d26a8b3624775ca9b030b504b7bbc257f57bbf4209c4bdf5604f5cb6d27dd86", + "scripts/mcp_thesis_demo_smoke.sh": "874189a27f7191abcc48ff9d85391d59cc1497465db4721f532d2c07e189ed42", + "scripts/migrate_cellprofiler_function_facade.py": "09acc565958e4ce7bf847a3ff59868f5917ed6430bd72c796865674a1be40cbc", + "scripts/napari-plugins/README.md": "485dba3b9eafef0f216eccf0360237a37d580d0b7a100abf224a90a9660fa123", + "scripts/napari-plugins/napari_orthogonal_projections.py": "f6850edf7f42ccf8f78661594f5063054d088c6ca74d153f6ad51c9647fafcda", + "scripts/prepare_cellprofiler_benchmark_datasets.py": "5721186ca36ba9708acfdaca1f393d4826cf08c044f1fbcc93b8b8a159c010f7", + "scripts/prepare_cellprofiler_reference_exports.py": "05f7d8d3fcac499a546a3fbebf33eb9c006e6df7cd1c47affb5a9175755df43d", + "scripts/prototype_flatten_object_state.py": "b3019b9a4ab451dc10b989c9261755ace355ce0520932ffa8a5146f3d471491f", + "scripts/prototype_unified_none_forcing.py": "ae3a8dc80e258fb6b766595645b34ff0e7e81455b5b7fdbdf1e9be59a8f9d3a0", + "scripts/release.py": "c7afa31cd6f52472fcbca3a6e5df14027fd563c1169c1a414c99eff6989c413e", + "scripts/release_readiness.py": "f8822ade20e4cf9186414986bf2aea4679a5598442d46057267b2ce5dbc0a003", + "scripts/render_brand_assets.sh": "4fb956d28632b8cd8e311c6e93436213dec8c211e9074af8de8c93ed15153bc6", + "scripts/render_installer_contract.py": "da9045e1012cb0a71ff0dfd2f2d9fc98232299f5a2f019b01228e344cf3b40e7", + "scripts/requirements-quality.txt": "a57bf3bf9f3096d3b25a577c5387102e8304e6d8b3a3786c8c31bbafc8a50f80", + "scripts/requirements.txt": "1a1afae3ba770a4954322bfb18c1fe30f23c0b1390fb5664a0da6af6ad5437b3", + "scripts/resolve_successful_job_baseline.py": "8e6541a53dac376176a63cb449e9166a425ef673954847cccd96fe444d60ad48", + "scripts/run_cellprofiler_cppipe_parity.py": "78bb8501f88da47ea6b97ac40a9d138c4d01669505c10e4dff0d2bb010263359", + "scripts/run_experimental_analysis.py": "72b6c60c3e3f5710f4d6b074febb6eb8e68ea96b54b7065b80a3abc0310e145c", + "scripts/run_installed_tests.py": "db4991c7d7fa1cb1cfd5918525f26b42fce799fb4f8136d8740001a9e8ac6b56", + "scripts/smoke_installed_desktop.py": "f745f93e245e5c1b02f6e0ece3a4a0e2af086c3cab77282e3907a167f5ce0a7f", + "scripts/smoke_installed_gui.py": "a5af7f55eab45b7ab43772a9ff8cf758e4940c5ad58857611cc51dbe6004bd89", + "scripts/smoke_installed_mcp.py": "45ce72798566b618b853157d78f2020a874628e4b13d81ea919923152ffcc7bf", + "scripts/smoke_macos_installer_window.sh": "8111d1a357b745cba9620656f46ed1e9ace52c65c028e9bf1f46277dff7aca06", + "scripts/smoke_native_granularity_wheel.py": "4cb4b3738814f6ae3598373dc5689cd589aaab011f2cf356731922d81a79115c", + "scripts/smoke_owner_quickstarts.py": "0d64707dd6c075490c26721acac53ab1f68a1c6b38a38eec6be296f7a19d0c1d", + "scripts/smoke_staged_desktop_update.py": "7e20230236066f43712b0fa6bc0b13ea365ec02cb114ec0066371765ae1bb93c", + "scripts/smoke_windows_installer_window.ps1": "c41cb5b5b0ae935e43843fe0589911b41dbbf616917866bf6d02b98d14dd8044", + "scripts/stage_ci_candidate_version.py": "ce62346132229c5422d33f366763020a83fa73f786c7f61227e936dc5121e241", + "scripts/summarize_cellprofiler_reference_export_run.py": "f9aaf0c05adf30320cad72d5f80b84aafa23060b770407886165246796ff06d5", + "scripts/summarize_cellprofiler_reference_inventory.py": "4927e95b695dec31a56b093c1dfbfd12c0e2bfee001a2cdcd74923f8ba5c27ec", + "scripts/summarize_runtime_image_pair_receipts.py": "72007ab392a14fe56aca18791c7095b7b627dd0e353c0a1b73cd463c8f4ae816", + "scripts/summarize_runtime_profile.py": "e8852140959bec6e40f6f6de73152f477f9da4c8700e8e027a14d42161de1ddf", + "scripts/sync_mcp_release_metadata.py": "95b40ffc3c85af929a553f6fbc9fbaded3c7a40ba7a92bf4eeb6227747a784ca", + "scripts/test_real_global_config.py": "62f78776fb85c8671f20d82144e61063eac288923cda3c99f1884972b2e86bd7", + "scripts/validate_docs.py": "ea0870b2a4e5dcd0cee0a7dcfb00a1e7720a40c1e7413e02eb6a8e49c7423f21", + "scripts/validate_local_release_floors.py": "790b521d6fc60dc38fb4a55ba2d56f4d91a1eb956f28477e98b29313194cf6e6", + "scripts/validate_wheel_deployment.py": "8ae2c9b6c121579816fa5065f7e6ee6ce8f6ee4d3e014ce911c97dc60802d6b7", + "scripts/verify_existing_imagej_bundle.py": "2cee4885f1e1b89a7cd246833121db536dc90b9f6e9f985b85b3244a54b564d3", + "scripts/verify_release_ready.py": "d72e3290f473530fe041a9188190072fd222db991e5860c83b49b4d5aa107f18", + "scripts/wait_for_pypi_release.py": "c98c9fca6b0fa852a245eca10cbe25456c4a05fa150ac97f6c83db99428efd1c", + "scripts/website_gallery_projection.py": "22aba173524c50d274096b32af72803fe4db962f0aef829df9de21ca77020523", + "server.json": "0bbc28e4a91727a98381449c47c4669d9cbacc14bdd5bad60359b2c72da820a8", + "setup.cfg": "110938fc5f369a34d462477768998e4fafd9d2f9749e2897db19620522fe598d", + "setup.py": "4f72c93d6d64a44f02f92857fda73fd69bd8b8f7e88168b53190675ad79030c8", + "tests/FIJI_RUNTIME.md": "b51b5fb4b61b5aacf0be6f350c359e0e0e28dadd6f96e96c3010fd7ba3f7e37c", + "tests/README.md": "febeca8e52955771f8d59f29bfa70d2808344e10e5a86c0fd472d4ec18bd8bf4", + "tests/__init__.py": "735469839c842bc3fbc34792d87ecca2fb190d80bc5c631024bb850a07b5bbfb", + "tests/conftest.py": "8ddda85bd90bfed62fe62c166e5c1262752bcca5bc401617105a8f64d66e7be5", + "tests/core/test_alias_property.py": "06d3041f154e341ee17278687c92b55a1e6232f2e32851901b58917203f946b7", + "tests/diagnostics/basicpy_observation_fixture.py": "298cf2b0b86238846c78505bff1fcf49ba17cff6016fb1370352973bfc32ed51", + "tests/diagnostics/centrosome_watershed_reference_worker.py": "5667a88fccf127c2bcbf86a28ad210b1aae1abf3e06873daf312414587035382", + "tests/diagnostics/check_centrosome_watershed_reference.py": "16290b9f7f19f9795237bc8388b4d178b4f177bb3bb7f31554d60a5fb9787889", + "tests/diagnostics/check_custom_registration_live.py": "5f4db820e51c1181ba3dab1ab1d90e252303574b4d08fe8821c0dfddc86d802c", + "tests/diagnostics/check_owned_bootstrap_live.py": "8ce6a29bfc4c4279589eb5dbb3a160320c7524e491eef086442d162c575c7b09", + "tests/diagnostics/check_viewer_feature_measurement_live.py": "cc61ff29a05f34b827a05e261d312a0aa472bf90cc5d0570758641896e25adc4", + "tests/diagnostics/create_basicpy_acquisition_fixture.py": "0a773fd1d2efa85ecfc118b2f13b22186dc06f27f172bbe4d49f8e7c292ed5ff", + "tests/diagnostics/create_native_point_viewer_fixture.py": "de84bb0dc53908f32223af8750b08eb5f8af75dce2a516e9310ad688167736ba", + "tests/diagnostics/create_native_result_reopening_fixture.py": "8740d81e333548914677bbcff4fa36d4ac683fe0fd9bb441b1524ddae29e535b", + "tests/diagnostics/create_orthogonal_viewer_fixture.py": "b0a6fd3e3264586e360a54a76e7970203813df0f15dd738ff3068627bd02f1f1", + "tests/diagnostics/orthogonal_plate.HTD": "86d74d0e7397fac55569be4e9c6b5cb93d38d4510796a2da31985662a165e05d", + "tests/diagnostics/owned_bootstrap_readback.py": "c8aff5143d454b47fbbeea369fc11247b0dfaeb2b3d2702d51e2493e4d491e7e", + "tests/diagnostics/registration_probe_source.py": "64a0fc300bfe17377ffc608ccb8d714aac811abccbb5cea0b0a032a77c854f72", + "tests/diagnostics/volume_projection_fixture.py": "9b737ed5e669c6f2325cc4af9ebb0b39b706121478149315474f464e3cc30c80", + "tests/fixtures/cellprofiler/quality_control_4281.workspace": "884f14365ed643c8dd7050ea77e416a1b7253dca83ff11d2e01aa54a8e07bdc6", + "tests/fixtures/mcp/start-owned-runtime-400.json": "fe227ffa97abd12b8e394ca78b955616ab4ce522f771940f64d320bdeb33a6a6", + "tests/installer/pytest.ini": "65e6856a27f9ade23ef94ce5d5dc606836a6b4b61e57dfc9a090731497724996", + "tests/installer/test_installed_desktop_smoke.py": "5ac9f0e9e0e2ffe73cbab940da132421424e355c354b7476a8a072090c5aad00", + "tests/installer/test_installer_branding.py": "1045498970a1100616ff58797014f6fdfe2b1d6f1cb687acc9834f9d4c53a7d1", + "tests/installer/test_installer_contract.py": "42a551a7806f043ff464402795bc493ab8ee750fa0dcfa1e1822a4b6e79f8405", + "tests/installer/test_installer_signing.py": "c8f9d050c4337086766e732bb09b39bd7cd61d0f7c4866fd66f6417d99c4ca1b", + "tests/installer/test_macos_dmg_lifecycle.py": "a2cadffe4ad4353b4d2ed4664443f6714f91d6929491eae116dc9d8a91f89862", + "tests/installer/test_macos_simple_installer.py": "49c2948a689e7b406bdc0bde312fe8b2ad1043c6d3dea626f531ef72c62d6e55", + "tests/installer/test_windows_simple_installer.py": "64ac03a0674411b569fafb9ece01c73583b5c4e9184acad054015d23d4f60d6e", + "tests/integration/conftest.py": "7397b869b835c535a8707412e35f5f4b25aa92eec5b7fe9300c83dbce0bc987a", + "tests/integration/helpers/__init__.py": "69254da608bfc5c0139c0728b753886c029f4779b856578c24cfbd24a973537b", + "tests/integration/helpers/fixture_utils.py": "7c75a2f98a2a90c226d0cccdae3ce9090b8d5d45cec7656c6924214ab9222846", + "tests/integration/helpers/omero_utils.py": "0a719c540deaf090118add947bdfa8a0229089220376ecbd33373d1fcf51a407", + "tests/integration/test_agent_observation_export_zmq.py": "ee572c151db282100ebca51d185981eba522599d28571804e70f37727dde2201", + "tests/integration/test_basicpy_real_fit.py": "d6f309675773109723db671a1b16815d34931fa946d9afd9ff4421b4e07ffea2", + "tests/integration/test_benchmark_openhcs_adapter_cppipe.py": "d12bfa29a780635d7154eaa8ee02fce440be067586a58994694fe5268ecca084", + "tests/integration/test_bioformats_handler_runtime.py": "66b9c96eae751e345a82e41f5117129b3120ecac8e55ecc55f7d55b73675d7ef", + "tests/integration/test_bioformats_imagexpress_synthetic.py": "017784acafcd049360c478c3d83feef9965e06329003a9b72a9a6096bf71ae00", + "tests/integration/test_cellprofiler_generated_pipeline.py": "c9e479a9af3ad76f42f8079603b4fa31effa7597b2a7d11787272dd2b093dbf4", + "tests/integration/test_cellprofiler_official30_zmq.py": "bc5abfddf076996a050b0601c46b6039d2d812ab57cd303847b241e477176f53", + "tests/integration/test_chained_callable_lineage_journey.py": "542450fce7a490777def1fe15b095a3e63907ca0d3e124c0ff41e8ca552ee3b7", + "tests/integration/test_compiled_artifact_inspection.py": "cdf70c26c44db7716ed36bc7533de8a5dd38372ccd255b07657523639afa3fbe", + "tests/integration/test_count_cells_simple_dual_channel.py": "96aad0a93805768a9841398e6b477b6aadee7e012c1eae91897273aa57608bac", + "tests/integration/test_custom_source_transport.py": "4b6b4352765d572fb6e6462c44300f09f020e1e729519b836f2d9c3e45b2ca51", + "tests/integration/test_generated_source_relative_paths.py": "d5a5cb107ee77df903b280aa65d0954a52846fc2b073a5bd5426607ffc2f9c6e", + "tests/integration/test_main.py": "1a0c532eb631a89b77f92a4202a7b55033c86d7b872350156f106d276b061e9f", + "tests/integration/test_measurement_declaration_journey.py": "3c7cb5086c8f2e99f96a9269aafa6a9d89ccd88f6699c04e3d5ffa0d00859b9a", + "tests/integration/test_neurite_outgrowth_synthetic_plate.py": "5b220237ea1f257d7572bc0c5ce10c069c9e669c02a9d1d70148a09af63e6ca7", + "tests/integration/test_primary_segmentation_diagnostics_journey.py": "f2600725abf522cbc0e3610fa388edfee1acf61fd3fa121aa478a756befa1e6e", + "tests/integration/test_source_store_code_mode_zmq.py": "3a20bd47775026f08a1e34a0c15894fc541b61edf622decaf2ffeb5357c927f3", + "tests/ome_zarr_fixture.py": "38e65ae14e1a34e3bb89d7435d25ac2b3adb216b03d963caceed257db2b15cef", + "tests/pyqt_gui/conftest.py": "a1cbff794d8624ffd1d895103e70fa924456932472617e8b04a33f8a41823477", + "tests/pyqt_gui/integration/test_end_to_end_workflow_foundation.py": "db2f98b9878565df935d78aaf8d8365cdac5fe160cd4fcee2d6246502dc355d5", + "tests/pyqt_gui/test_code_document_normalization.py": "56efa136f5eedd8a7a724a3d8749d9191f99a25ea206164688f54ceb8c28a974", + "tests/pyqt_gui/test_dual_editor_window_context.py": "4f046917d1b88b4f9d685a3a0405eaa46cc31e907a1007c02521522044bc2854", + "tests/pyqt_gui/test_image_browser_filter_controller.py": "f0e6e297212fc72fa0a855d0244167e781441a20fd612f49740a96d0d60be1be", + "tests/pyqt_gui/test_managed_windows.py": "a20d63527e04b9563c50e4f099c6f4152ebf8967d4ae39d8142c997d4281c09f", + "tests/pytest_integration_options.py": "de36c47ead5e45020fcd6f72cf5ab20bab195962d22ab8e69cde604a324a9ab8", + "tests/runtime_diagnostics/PRIMARY-DIAGNOSTICS-20260930.rst": "772648162e7dec847680cbcd3b148f4345d2139035a2941d5f989da2bc9eef8c", + "tests/runtime_diagnostics/execution-progress-pin-20260929.md": "84e3c34b0c47ebb4b908ba17b931eddfcda038dff122501567c870bb50d9d6d6", + "tests/runtime_diagnostics/orthogonal_class_census.py": "1faa1bbf3b4c7e87cf7269c6e83f7103aa0dbb5a6ef2d06f0bd188df1ae229fa", + "tests/runtime_diagnostics/orthogonal_native_7d.py": "9fb7b5dd670fde2081c68f49b295b749fac53011a0280054e9dc63c8d373d66d", + "tests/runtime_diagnostics/registration_attach_only_20260929/pytest.txt": "fb26f89183ebadd3f83d6e84166cfb856ec0e132330328b2fe432a10fb430f73", + "tests/runtime_diagnostics/registration_attach_only_20260929/resources.txt": "644c97eb4efa42fa736a0c89ae794016f7e0bf76ef72e54fde03234b86bf47a5", + "tests/runtime_diagnostics/registration_live_20260929_attempt01/foreign-control.jsonl": "1abca4e9708c84c8b24e629d0ce8902731c50e202068c1cd502cfdb388e5ca9b", + "tests/runtime_diagnostics/registration_live_20260929_attempt01/foreign-stderr.txt": "8dd2e1b622f473b28981f812a8651b95c6a1bbc311e2388ab7fccf24a229c9fa", + "tests/runtime_diagnostics/registration_live_20260929_attempt01/journey.json": "e41190a4f016d7b64c5618da96c770c7308ed1c8610c36f107b543aad831438f", + "tests/runtime_diagnostics/registration_live_20260929_attempt01/mcp-stderr.txt": "fc2c6374f06e95a9eb5610df83cb510fd1eb89c0e174891b897355ace2f3bb5c", + "tests/runtime_diagnostics/registration_live_20260929_attempt01/owned-control.jsonl": "fe9d5adb994a86327f6e8ca9e8181038dbe04a25b78af3370ebbbc998dad7a9a", + "tests/runtime_diagnostics/registration_live_20260929_attempt01/owned-stderr.txt": "7eb6dab22289d0a5c5d130980368e672864e9e1e01fc2f94e7f6799b6b4d33ca", + "tests/runtime_diagnostics/registration_live_20260929_attempt01/unsupported-control.jsonl": "b539167f827b6829fd8d0a568089e89e7ece840e146037d773e8d6c356a2c5ad", + "tests/runtime_diagnostics/registration_live_20260929_attempt01/unsupported-stderr.txt": "14c2415d6552eda8c37656c0c526b94fa1296f9463bb82de5837b3804821c75d", + "tests/runtime_diagnostics/registration_live_20260929_attempt02/foreign-control.jsonl": "baa83d55f2841e39e194eb65f78989efe51446a973bceeec96b5051942877564", + "tests/runtime_diagnostics/registration_live_20260929_attempt02/foreign-stderr.txt": "e3a8458c276dc39eb4bce5ffa1488bc2a718799e5eab47b144d0e4b82a0b8abb", + "tests/runtime_diagnostics/registration_live_20260929_attempt02/journey.json": "cf56db02a6eaac884e8e1d6e28b870b7b6bdb11a5b994e41c1295106a1c5f72e", + "tests/runtime_diagnostics/registration_live_20260929_attempt02/mcp-stderr.txt": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "tests/runtime_diagnostics/registration_live_20260929_attempt02/owned-control.jsonl": "e8786c2b9e4310d7345412eca3a9883ad5e67c3d8d5af9b83c67f48f07f6a48e", + "tests/runtime_diagnostics/registration_live_20260929_attempt02/owned-stderr.txt": "bd147f73c5fce24293ed7b2875b989930b7003bf10d4399f064a0008b452b81d", + "tests/runtime_diagnostics/registration_live_20260929_attempt02/unsupported-control.jsonl": "a8352cb01963547616e4b1bb3501ef4db46e8cdd6eadd176af8a18440ed57076", + "tests/runtime_diagnostics/registration_live_20260929_attempt02/unsupported-stderr.txt": "0d2c51051fd9a90cd98d682d6a6358f6a0dd485f3b4b4857b74ea257b271e868", + "tests/runtime_diagnostics/registration_main240_build_20260929/build.txt": "3a29d6230f783c6fc1854fc3fcb0cdcc579c32a8cba2392c95eccb4139bf8a6e", + "tests/runtime_diagnostics/registration_main240_build_20260929/imports.txt": "f3b4d1ff39b185c6af834bec4444970531c2653b61b941dcc58750021cfd7331", + "tests/runtime_diagnostics/registration_main240_build_20260929/resources.txt": "b67acd42ff559e3316e0ceaa56b5f95ee8c7b301b7b81c322701d751ab5fd2fe", + "tests/runtime_diagnostics/registration_main240_tests_20260929/pytest.txt": "53ffa0b6948c174f3fb6d4ed3dc9ead02170b1f49e6185c420835c46ffecd1d8", + "tests/runtime_diagnostics/registration_main240_tests_20260929/resources.txt": "056b44c7059c7dd586973f0969578452370c43b95fa263d87863c6bc6a47c187", + "tests/runtime_diagnostics/registration_native_build_20260929/build.log": "7122ffcb495bb45a2b4d9063c96861c5d2683fa033bf1ae3230b52d3537dd733", + "tests/runtime_diagnostics/registration_native_build_20260929/import.json": "4e1ceab558a4b63704f41ac6375595565f4b4ba349c21eae89398521c85457c5", + "tests/runtime_diagnostics/registration_native_build_20260929/resources.txt": "02d71e1fb56faae9216d9287bd87644f1d31dcf108d508b765a4e2dc663043ba", + "tests/runtime_diagnostics/registration_onboarding_20260929/pytest.txt": "0da18c9ca7a6231353214018aa707fd81630a2b95cab45b2f0cb613d358ba929", + "tests/runtime_diagnostics/registration_onboarding_20260929/pytest_initial.txt": "37330b31b71345202a4ab78530f650f5d4a8abcd433361ddc5d3278f39fb48eb", + "tests/runtime_diagnostics/registration_onboarding_20260929/resources.txt": "c4bdad60bd8f0a99040cc3ad08455dcacb2a4cda6e544a2ea021e272fda7e13a", + "tests/runtime_diagnostics/registration_onboarding_20260929/resources_initial.txt": "b3d11bd4e27f7046304b6bcdbf48e586d1d5ac76f4313867287132f0cbb873a1", + "tests/runtime_diagnostics/registration_onboarding_20260929/scope.md": "dd23963ef6049c781aec82dc9a8907965736decced397f51e8428565661f9b6d", + "tests/runtime_diagnostics/registration_owner_five_20260929/pytest.txt": "ee91b7c460a9863f15a58aa1bfb769fb193d35d9ce105deacfdfae7c1df82a2c", + "tests/runtime_diagnostics/registration_owner_five_20260929/receipt.json": "5387b7a7ef1e3fa518ea5738f1350d54f53209cba0c1b56933b603cdf97e61a8", + "tests/runtime_diagnostics/registration_readiness_20260929/pytest.txt": "fe8eb2648bbda97f6dab3422d81e9384157eaa7a49986cc0a26b08198095180c", + "tests/runtime_diagnostics/registration_readiness_20260929/receipt.json": "085b8f0a08c05e0372279b4fa7875a7dad3f5198679e629384a2e3dff401afef", + "tests/runtime_diagnostics/registration_readiness_20260929/resources.txt": "2e7a8be119bccaa5376ad8acc2a3e984b2eb295bdbe90af70f7036570c317107", + "tests/runtime_diagnostics/viewer_admission_live_20260930/captures-reused/20260930T132110280409Z_napari_5794_OpenHCS_Napari_Visualization.png": "e6da1cc37613ef6faf4d4bd1d3c8b503e9d2f53f7fb315c661cd34b8e62735f3", + "tests/runtime_diagnostics/viewer_admission_live_20260930/captures/20260930T131815752112Z_napari_5794_OpenHCS_Napari_Visualization.png": "af10d4def4dd0e67e31ccd8425b5850a3b0df5cfd43b9c726f8b8f4621226be4", + "tests/runtime_diagnostics/viewer_admission_live_20260930/first-client-parser-failure.jsonl": "05d9234693a89b3adfef29f65fc441361a5b16544c9ab171d97e06d313be6ff6", + "tests/runtime_diagnostics/viewer_admission_live_20260930/first-client.jsonl": "106c3097326318267c5d342d46b4c231baae58ebfb15382f85f57a1617dbc308", + "tests/runtime_diagnostics/viewer_admission_live_20260930/plate/openhcs_metadata.json": "733c5b89207f89ec7e3ef514f7223bf563a4deeb28536207f2a754552b3402d5", + "tests/runtime_diagnostics/viewer_admission_live_20260930/plate/synthetic-channel1.tif": "45f5be72728c5275acd1130aeb4c6ce7a127905826cbd4407523cccc48fff760", + "tests/runtime_diagnostics/viewer_admission_live_20260930/plate/synthetic-channel2.tif": "5fcac25545d0559049895eb580dbc240b93f9c57e820556e7b52fc6b8c1d9678", + "tests/runtime_diagnostics/viewer_admission_live_20260930/second-client.jsonl": "dbb3b1fa2ccd4f34e881b29180f5a73cdbd826a6ace5e4fd394fe9c3f5647d5e", + "tests/runtime_diagnostics/viewer_feature_measurement_checkpoint_20260929.json": "490d349117273ecd6a4ce8a966c4fd94f049593216b2e188f0dd41f57c9453eb", + "tests/runtime_diagnostics/viewer_feature_measurement_foreground_20260929.png": "5b1765fb264f47e6e2b4ded5e5361ae6b97f955eb510e3b19efb785754590e98", + "tests/runtime_diagnostics/viewer_feature_measurement_gradient_20260929.png": "af10d4def4dd0e67e31ccd8425b5850a3b0df5cfd43b9c726f8b8f4621226be4", + "tests/runtime_diagnostics/viewer_feature_measurement_live_20260929.json": "a402bc72666d5a0e4fc4687af6fe16cd8fb4b1ff44601be3a039ce1f8af4d4db", + "tests/runtime_diagnostics/viewer_feature_measurement_live_accepted_20260929.json": "9f9a205e41ae50634fc8dba1df4f9dcf15a61cb7a33513b4aa2e63577fefaec1", + "tests/runtime_diagnostics/viewer_feature_measurement_live_attempt02_20260929.json": "60af74cc7241b3c5ef1414896b6092c1586b9c86fdbfadd22bab1e15c5718497", + "tests/runtime_diagnostics/viewer_feature_measurement_pr.txt": "5ae5825b94201ba468ac33fef210d56e5cb40877ea2b2994ea030a2b9fc99bdd", + "tests/runtime_diagnostics/viewer_feature_measurement_sourcechain_20260929.json": "b880f0cc03a5f229fb1794b9bd278f9c04e6eb0de8a0927aba998ed01ba2d5dc", + "tests/shell/fixtures/recorded_mcp/mcp-client.sh": "c95dd2e4aaeb39cd51631a0de50cb815c09a8abc9860ad0ec581b3b72efa195a", + "tests/shell/fixtures/recorded_mcp/resource-check.sh": "cbd34c21179a5c3b3c2c1b1271fcc99e91af3b0d1598681818f58a158abb3ada", + "tests/shell/fixtures/recorded_mcp/slot-env.sh": "feb7c1a44ccc7a8b6cfe686a501c6b5392aac31980720c981e3ef439945239aa", + "tests/shell/test_recorded_mcp_admission.sh": "d0c9e23adf58a6bce7ea511620331c4676ce9cb433ad4efc341d48a39f647d61", + "tests/source_contracts/test_opera_phenix_xml_geometry.py": "a8ae1eb57289f6ecda4cc995909b942cb6bf2185ac4739790f29384f41ce9153", + "tests/source_contracts/test_synthetic_acquisition_identity.py": "18e92f88cc71371c668b163700901591270fcba1af82d6e22eabfed650be870c", + "tests/unit/agent/test_agent_serialization.py": "876fc74f4acefbeaff4e2b6991a5bcd42cc9133518b2d26debc5bbc3b841313e", + "tests/unit/agent/test_agent_services.py": "6b723844b5df0a942238189e84cc702c2d96dc77279b2f0d1d6e96c113422e23", + "tests/unit/agent/test_agent_services_package.py": "97e743b7b987df8ef17bf1cfdb9f22980fcd3ca8e3b27f7a98ac5998883ae365", + "tests/unit/agent/test_analysis_knowledge_transfer.py": "402f80ecdec85330cf9bf61e04c8422ac36ddf63a1e141a9a5784ff750fb63c2", + "tests/unit/agent/test_architecture_projection_service.py": "e820969a0499995097af17fbde9b4694f1626c0ca6d70c75fd20bd4d2cdd7be9", + "tests/unit/agent/test_artifact_plan_renderer.py": "419ff0acf14c331f0e55dd8f1ce90c3292041bb392016f035785e511f1791b51", + "tests/unit/agent/test_authoring_roundtrip_boundaries.py": "9119d274d3ef09b739baf64b02705186488bd7d2fdbd8aa47aed3ca61f0d7f0d", + "tests/unit/agent/test_blind_recipe_audit.py": "63484e05bc715b6bb9cbc36bd31af3ceb7f44a8e76d4596121c0dfed9fe08075", + "tests/unit/agent/test_callable_artifact_reference.py": "00303992f5d5a255ccc916990f9d6916c73f1807cdb33a77ca35c3136a102ef1", + "tests/unit/agent/test_capabilities.py": "306a6c2c7998f852fceeb37439a91535b114b3e073b07b6e6781db17ed31fcc5", + "tests/unit/agent/test_cold_inspection_feedback.py": "6b83c5522a1f4ac7d8f252213612239093e061d14a4bc7c63052bf096a1a31af", + "tests/unit/agent/test_compile_inspection_declared_callable.py": "04bcc69f7a17e2d6d271205e0c4ba8675ea6be1528adf6d53a3561bf8223bb46", + "tests/unit/agent/test_compile_selector_authoring.py": "c0b7fb000bf135392c6259fe9a4bfeeaa1e9df2cf0c9b8f03908d856732647fd", + "tests/unit/agent/test_config_reference_service.py": "de18fb8ff76efe2bdbc46a3f46137622fe53fe6a2a28449d34728f4d04a32a1d", + "tests/unit/agent/test_config_schema_authority.py": "672e5090cdddc68d1c23dc51bbc38ac818fe0233e14b4d92e992a055ca1b378f", + "tests/unit/agent/test_custom_registration_admission.py": "afc4a461ffb8e0777d28bcba5c2e010b81c2d579845b67e921ba725d5088e08d", + "tests/unit/agent/test_declaration_selected_discovery.py": "bdfaed048a3ccd1074357de12340c0aae1fef32e4868c009727a17b3061f0f60", + "tests/unit/agent/test_declared_callable_documentation.py": "671bbc6945b22ff04bcb03bf3caa88610ae483e4ea46191db6ae6fd78bbb1c01", + "tests/unit/agent/test_function_declaration_help.py": "f0ddaae17386f5ae9383d5fb5f7f9b137d876556b183e4218e5e735415953ee2", + "tests/unit/agent/test_hosted_mcp_semantic_ownership.py": "22e25e43771440167a2832e0454861efaa2ee4328a6aa01aebbcdd8679e7d41f", + "tests/unit/agent/test_image_analysis_qa.py": "3b3cba98038a4d4777384fe99a044e13448fe63371fa1eefd5122e74d2b8e06b", + "tests/unit/agent/test_imagexpress_readonly_paths.py": "81cc9d13e9ac7544e1e94f81e3f6903587a9046925554e6f82ca7de4515b4a23", + "tests/unit/agent/test_knowledge_base_service.py": "b32b8039ba7a459762bc305bcdbe1928194543fb8ebb8c38755d6fd9bee1dd32", + "tests/unit/agent/test_knowledge_lazy_conversion_boundary.py": "ec9ce0dac89679eb922afbfe5ef1aa0c68f85a28d3273a90b634a7801b05f4d6", + "tests/unit/agent/test_mcp_affine_inspection_progress.py": "6577a2b4f53c4a284d3023d3943b8e13af545f06863d2503d73f8985c93685bc", + "tests/unit/agent/test_mcp_affine_real_compile.py": "2b8e7cb3ffaed41e334d6ab59cdfcbfc1772b2b141541bedb37096ce2b9bf04e", + "tests/unit/agent/test_mcp_bounded_discovery.py": "48bfc611468a677e0ee99be65e1b05b86426410040c7ee8262454b93890a818e", + "tests/unit/agent/test_mcp_capability_discovery_and_widget_projection.py": "f517cea9a75cf01b95cfd0b5e9a052713b25a35022b79d14da1d456e86fe1ae8", + "tests/unit/agent/test_mcp_cellprofiler_function_contract_friction.py": "ba55f9180fc52163e09b2a4bb631323471f872eb1ddc9fd7e6e690fd9025266f", + "tests/unit/agent/test_mcp_client_registration.py": "0763752fc04c32d2812f509845886f10c366e1a67ec290d50e433096a8d4debe", + "tests/unit/agent/test_mcp_dev_client_config_renderer.py": "c539f708c8e089951c6314cd51023ad2930ae904f300d22597fa7663463a1396", + "tests/unit/agent/test_mcp_dev_client_declared_decoder.py": "b30133ede3abaf07c09bc21ae46b2f7538e263ef70357f373ccf7d5d4954f362", + "tests/unit/agent/test_mcp_dev_client_generated_inputs.py": "2557a42ba4eca81f67fdbfa4e7faf77a5b32d81b67f6259cee64769a7a01204c", + "tests/unit/agent/test_mcp_dev_client_pipeline_results.py": "07bb376755222d31f27168caa9b80ac231a767c517410b1dbc3d89fdd043abc0", + "tests/unit/agent/test_mcp_dev_client_runtime_debug_renderer.py": "021380ec2c7f69418e17242b2d2181e3b3b35e60c06143de13fc1e67bb706ef1", + "tests/unit/agent/test_mcp_dev_client_sampling_results.py": "600f7a63af6668a64656e0a2fdf83e6546e4fdb7460f27b83e971254403b2345", + "tests/unit/agent/test_mcp_dev_client_transport_failures.py": "cd552206bdb5a4496032838eaa397ff950b20e356dbe3bbff350b50b0f200ad7", + "tests/unit/agent/test_mcp_dev_client_ui_action_results.py": "0e98ab0bab815bacec4bce1c969b201f6555a917929dedf2456b255c1d0bf6b9", + "tests/unit/agent/test_mcp_diagnostics.py": "96405a0f94af17972bfcc4b2cc958162f51307e94a18d689b42183a4fc2a160a", + "tests/unit/agent/test_mcp_distribution_runtime.py": "4f28f8eff46a5de20726bc80316f3fec276e51646b6dd133dbef7b39e6c00004", + "tests/unit/agent/test_mcp_http.py": "762e67be7516aa1cac6bc59d1b29d19d35f85b6f362d4a8d3c24a1ba2456df2c", + "tests/unit/agent/test_mcp_http_auth.py": "250326e568cfaaa0762387648e0a2f4c48f2a6ce010896deb5f1b61659a3187c", + "tests/unit/agent/test_mcp_lifecycle.py": "91eda092cec76e6bd770c4c5f8ccd6a6f9e5cae38583cc1d97bfeccf0cb6f6ed", + "tests/unit/agent/test_mcp_plate_stream_progress.py": "29633c670d918e5015abec72f58779b9059329614d7840a09f421b0cafde149c", + "tests/unit/agent/test_mcp_process_resource_lifetime.py": "641958481ff8a991d5f305a9ed0672554310af838e8b66cc4959e4c5c216b285", + "tests/unit/agent/test_mcp_progress_wire.py": "557f4ff4b04a651a197032ec8f98e4346eebcda4723903712ba491ce2674cdab", + "tests/unit/agent/test_mcp_self_description_contracts.py": "67c7f8052c852beef3dd875b5e96c0c7635e001df13a585589e320955667a59d", + "tests/unit/agent/test_mcp_server.py": "6612b576308c8dd55b907bb2993edc56fa3f403fe07c762ea06dd76ab6610d15", + "tests/unit/agent/test_mcp_skill_registration.py": "b4741c8dab110461a02d1f3c893c1f6e020f173911cc0e9ba0dc62d409c3cbff", + "tests/unit/agent/test_mcp_startup_feedback.py": "64b1f2b9c80c789f2a0c82432440e18604a739afb3f4aa3dc6eb2b5cac69f9ad", + "tests/unit/agent/test_mcp_stdio_transport.py": "ffad185f736a7ead7b6e2e7699fb3c9b034c24534af14ea8848e0eb6989cfe1a", + "tests/unit/agent/test_native_presentation_451452.py": "3bee78d7ef0ea2b23230f12b055ceda90c81c64ad87c274524ec1a7bcc0fc9f6", + "tests/unit/agent/test_native_presentation_mcp_451452.py": "16735a50874ad478816fc0c5529bfb00b53ffbb4df473054935b9c385353fde9", + "tests/unit/agent/test_neurite_body_gate_authoring.py": "cf8c8775f2dc12d82f8937268df64541ebce6209d7bcfae14d8a8dcd2db503bb", + "tests/unit/agent/test_object_state_field_help_service.py": "26df2f5ea13773abba21f267ceec43edb69a8b96295faf6b6b036a81663ff7c6", + "tests/unit/agent/test_official30_knowledge_examples.py": "6b81450dc01a33370fdad7a9a3b88bc87c3f85eddcd8b872373da9a89172c7a4", + "tests/unit/agent/test_owned_bootstrap_readback.py": "2ef367aad3af2247f30e6b705ecaa843890dc59d03e58e559db9a29e99d87072", + "tests/unit/agent/test_owned_runtime_bootstrap.py": "4cefddc71983ead4c1faa961b3e2bc66f3b087aa7498b132d3848125c649a089", + "tests/unit/agent/test_path_policy.py": "08274b555ad275e1429d0334a0a2a0432c963cd32a3be0ef1c65df87137336ab", + "tests/unit/agent/test_persisted_output_reopen.py": "9580b55180b79fe0965ac4e23b62ae78fa3fd8a64c0912bd462829c60a0a7f0e", + "tests/unit/agent/test_physical_source_inspection_445.py": "1d96e83e931e7e07f6f39a49f5610e1365fc37a935022343e1177e96cda1f103", + "tests/unit/agent/test_plate_inspection_cold_runtime.py": "da596c352d3a7b0f6e36fce04206f248c37ab84257840e7486d0782aa31a723d", + "tests/unit/agent/test_plate_inspection_service.py": "fcf6a171c5265761b3c4b3c7b730f6304e36079d0c1e689ddea8db7e2ef695f9", + "tests/unit/agent/test_plate_streaming_service.py": "299e60c33736bad13ebc68ed1b5499cee52d9e25822fe2918b5352b05a3e18f7", + "tests/unit/agent/test_progressive_authoring_context.py": "98437a0ca02cf122703e3b38083f3dc1b19e738c6970cf86aa74b93c6692b71b", + "tests/unit/agent/test_public_callable_enum_ingress.py": "8a6fdb4f75f057a6c1ebc5aa8165ebf84dac916576ed5a6a11f173cca3a220ff", + "tests/unit/agent/test_runtime_debug_inspection.py": "8cb28c6e981ba51fc4676ae318fad45952d9ebd7490ed6d50eb08cff7d6827f8", + "tests/unit/agent/test_s1_native_viewer_records.py": "57658f75ead1e01dc8c37942f34346f1337964a46feea18af58ddb3ae953cb0a", + "tests/unit/agent/test_s1_typed_viewer_rendering.py": "75541bd3c686bb9ed6d81374cf0c9dabd6135d05135c8f1be70c49b8e7d6404c", + "tests/unit/agent/test_selected_plane_reopen_stream.py": "598a4e831c596debb5713b6114cef8bb7c6ad082737491861d0ec4d4bc358649", + "tests/unit/agent/test_skill_sync.py": "0ae79c0394c339219262b8cbfeee76c6d01117d21ffbcfb4799b55175a2ba073", + "tests/unit/agent/test_synthetic_plate_mcp_progress.py": "281197bf81d03a8e477026df293dbb385342d2a68c3d10c2897f6504ea85c74d", + "tests/unit/agent/test_synthetic_plate_zero_geometry_journey.py": "d0531ec51325b8233f8a730d0a0a930c3ae50bbf3156aaa93367388647b3ac6d", + "tests/unit/agent/test_ui_bridge_service.py": "8a91032c43f2db796121a2ad5373cdef1fde04543537ed4bdf631df7285db55c", + "tests/unit/agent/test_user_facing_help_projection.py": "74b47e47ff44a251a3a8406e5250a8183567c20e327ff211dc873d99f22d930b", + "tests/unit/agent/test_viewer_render_snapshot.py": "02141305e7056279fde7c2209e089a8cd4247a4c59aacc3aa2dccf809f051686", + "tests/unit/benchmark/test_agent_validation_attempt_recorder.py": "abe151a160d2c38edb9112caaf7cb95f5e2cc98e33163b5e5239e4b4fd55f78f", + "tests/unit/benchmark/test_agent_validation_corpus.py": "766d046e237b41e02f82ad424ee6e850f9cdf7253985763ac8c11f49f9412fc0", + "tests/unit/bioformats_fixture.py": "027104f2a8f654137f2f0e599b0a0999d4a7ada8f177d1ea84e19a0c35a1c3b9", + "tests/unit/bioformats_imagexpress_fixture.py": "cbbb2857a13095516dcf19497c14f39c46a9ddd5a6ac7df9f17ef5162785a023", + "tests/unit/cellprofiler_runtime_test_support.py": "c37112c27a4f7ee2e7f853026d9e1c569709b9baae214f858076d90115cb1105", + "tests/unit/core/test_config_cache.py": "4e21db002c10fd13a12b398ebba0c291a3793dd5c17fce7babf697af2c7030f4", + "tests/unit/metadata_namespace_327_worker.py": "73c0af77ad5faf85a86935581e892874ee7c42e8db62316e010d9bc9fca2220e", + "tests/unit/progress/test_debug_projection.py": "8ea3fdacb5ba4a2c8c7a474159acabac2772bef91d4cca2c5a1fc7886d5681b4", + "tests/unit/progress/test_live_measurements.py": "9dc05a871ebb62a740b7782ef49dd88868e4b6dfa890ab2cba8f6518eded741a", + "tests/unit/progress/test_projection.py": "25a154fffdd169efe68873bf4d67430010f785d866082a29268c2e34aaa458b6", + "tests/unit/progress/test_registry_channel_keying.py": "b7e27609768bda2c31b2f63807bb30f70709c98664ae575db94afe9645e02709", + "tests/unit/pyqt_gui/test_about_window.py": "05244696d0dca4f612628a5dd3b4b511f5429d50ec438e5b9d53a1242ef52129", + "tests/unit/pyqt_gui/test_application_cleanup.py": "c5766b1f4e98db7d63da72ec88c911a67087aa0460503949bfb06f7f424d1669", + "tests/unit/pyqt_gui/test_batch_workflow_compile_engine.py": "a8295fb6edbd0229128025327c975d7c916b791f5774df4a030e6c9778f3c4cb", + "tests/unit/pyqt_gui/test_branding.py": "f4cbe865cd5a68107b0d9839233a6753039b59c3e89b83972dbc0bc719862043", + "tests/unit/pyqt_gui/test_cellprofiler_parameter_help.py": "5927653e12735107ec5b05434efae67fd149b936974c06c3ed074583f7a6d5a5", + "tests/unit/pyqt_gui/test_code_document_catalog_imports.py": "e5b8527f97c94fbec70b85d3413077c4dfaf677afa987c7a172e82af84162034", + "tests/unit/pyqt_gui/test_code_editor_form_updater.py": "9ab853be893f5d0582aef0519c33fdbb4ed5675027a514c9deb21ae02517797d", + "tests/unit/pyqt_gui/test_compile_terminal_supersession.py": "0b04d9c843ce2dcf16833b1c53b5853637802aa35c2d23de992c3db6c2bb2465", + "tests/unit/pyqt_gui/test_concurrent_initialization_progress.py": "c8e960fa83adb73f3533cf4c75acafd59249a1993f4e650733101fdd3d62ddb8", + "tests/unit/pyqt_gui/test_config_preview_formatters.py": "b77b44d666b776ab5f68030c1553c4c3f8e6fd673354f14c852b7606295694e3", + "tests/unit/pyqt_gui/test_config_window_state_resolution.py": "2b24d9e2f612c5b9510ed8d5f9729c389aaa66b9cc96c36259771e311beb5fed", + "tests/unit/pyqt_gui/test_config_window_transaction.py": "2b541713ee3cc4cdc36677bd6f86db091ffb4be038e514c04be059f3754bf1db", + "tests/unit/pyqt_gui/test_debug_inspector_window.py": "7c11dac1b9208e7d1f56494fec9b7ae61cc804e86316a78b4f52eb23d0ff218f", + "tests/unit/pyqt_gui/test_debug_toolbar.py": "e9a5dfa5fbbe7967c5339bda52814a314f14806754fb97b4d2c04f0e8394e0ce", + "tests/unit/pyqt_gui/test_desktop_history_upgrade.py": "1a6a10deb9ae83a381abf245a213a6ace8a1d8fa92e87e786132d8201cab518f", + "tests/unit/pyqt_gui/test_desktop_restart.py": "aafaa6e7a46859f50b44232e12f8db606e0dd4631340a834694a0fe795b5ff70", + "tests/unit/pyqt_gui/test_desktop_restart_worker.py": "a9fbda7d6c78972d6300cd9fe67c2e88497b6b802a9b48057568007bb33b0e1c", + "tests/unit/pyqt_gui/test_desktop_update.py": "b79c4c2620f271ad3a2c08f3002ec9d2c17c8f80b0b72b8ac6b1baba9dd8815c", + "tests/unit/pyqt_gui/test_desktop_update_worker.py": "68dfc122f38164cf6eb47e856bfe369002e03e3b0699ad6bb2fda9f45db2dc8b", + "tests/unit/pyqt_gui/test_dual_editor_window_artifact_refresh.py": "d03a84c8f8504a5cf6880662c1062209b0ed6f3f4b3d52deb4f6b2d458ede4f2", + "tests/unit/pyqt_gui/test_durable_decorated_history.py": "ce767329b25bdeb2e2f3803d5fb95588c8284aeed78bc830c81dbdaa746ecf23", + "tests/unit/pyqt_gui/test_enableable_function_ui.py": "b7bd91891fe82d62ab41a12e112d709f7559f700c8de8d7a05270bb018b22d85", + "tests/unit/pyqt_gui/test_execution_server_status_presenter.py": "1d1e9a8604c0e071a550becb874c79067e0506a52f0a09813b0a210d61cc2d26", + "tests/unit/pyqt_gui/test_execution_server_summary.py": "7e0b17ad242ab17596be60daa00c0107abe248e345a1f986a4b09dbb630b813a", + "tests/unit/pyqt_gui/test_function_catalog_form_materialization.py": "711b69ff0b145b962f0f3518c7a7a77db38d8a545bb9f5ea7d2e259d70598667", + "tests/unit/pyqt_gui/test_function_catalog_projection.py": "fbb41fb4cbffd15fed4108349e35ce2da0cba028c00e1b82ee23a173ca032d19", + "tests/unit/pyqt_gui/test_function_selector_column_presentation.py": "cf459d89d9a1b771647fd58ebe87ec0b6214de83f85b79d24f8645d1c6e05066", + "tests/unit/pyqt_gui/test_function_step_code_document.py": "50e7a17fa1912431d541066f70eaf8a1973600528b6fc0c3c77094f492a44ede", + "tests/unit/pyqt_gui/test_gui_startup_progress.py": "48dd1e33f90a33d3adee7d646a4962b0cbe2213352a69930c1882d23c8b60b94", + "tests/unit/pyqt_gui/test_image_browser_column_presentation.py": "a7b2415579145d5ca93bd410a28684f8b88bbc18a9aa3228280449447543ded3", + "tests/unit/pyqt_gui/test_image_browser_streaming_config.py": "6f030f18030e26d498cf675cc74c6998ab9352fbc19fb50c95aabbf76816516b", + "tests/unit/pyqt_gui/test_interval_snapshot_poller.py": "748d5a2c013c9e861fcd4594f51e35f1c65e0e298f5ba9a2ad933e29f2cb5183", + "tests/unit/pyqt_gui/test_live_measurements_window.py": "4769cf1397625445b4bd18dbcfca88188e3df06640804174dfb18cfa00d4f635", + "tests/unit/pyqt_gui/test_live_server_tree_sync.py": "7f4c00372640e5fbcd22d237d53403ee8ef7e0da773ac47567d481271e8438fc", + "tests/unit/pyqt_gui/test_logging_config.py": "5aa556c32bae67691f52be4ad4335054ea935ed46ba482b060da5d88c8717dec", + "tests/unit/pyqt_gui/test_main_config_propagation.py": "291dc45ca20ead6025c255a8109a2e6c55d90bd7858e80ad12484dafa733d7c5", + "tests/unit/pyqt_gui/test_main_status_bar.py": "19f38968fad8a0599b74bf773ba7bddbc64b0d1f8604d3c9a10bae6f8cc4405e", + "tests/unit/pyqt_gui/test_main_window_embedded_layout.py": "1d91b04430bdad6562db2c1e2f3b29f6b017a6e3afaccbbf18a0173bd87f0213", + "tests/unit/pyqt_gui/test_main_window_file_actions.py": "bbda2f9c205299f36497db3aba6f569ad52ae069dd8ee4ecc5328b337d5e97fa", + "tests/unit/pyqt_gui/test_main_window_time_travel_workflow.py": "a4e4ff0599e317ece876b24960e41f03d31da3cfaf4105971a6f2c8127cd3961", + "tests/unit/pyqt_gui/test_main_zmq_window_routing.py": "d02fe132fda83540f26ed4b7a79fc1409fabbf0ff6ce72f3d1ed8681258e9f6a", + "tests/unit/pyqt_gui/test_object_state_callable_preview.py": "c254e95495dc739943dbf685334ccd393003e1f83799abe1d190ac126eba889d", + "tests/unit/pyqt_gui/test_parameter_form_manager_cross_window.py": "417aab4ec3e838ee4855e27d82f909039bfa12f839870060b9fe4f0b653948bb", + "tests/unit/pyqt_gui/test_pipeline_code_document_transactionality.py": "b7e546e904b897f13d76f0c523e751afa541a75c627349ba79a3a87313d30f4c", + "tests/unit/pyqt_gui/test_pipeline_editor_auto_load.py": "3aee5e89d6206608e9867b7ebf4a25bb3296f53b16315a4e26eec83c80c73f02", + "tests/unit/pyqt_gui/test_pipeline_editor_widget.py": "8f28719c5bd9eb5681e67af8e1f7aceb445bcae97c12a13348f943c8b6bafa35", + "tests/unit/pyqt_gui/test_pipeline_history_occurrence_restart_regression.py": "fe37875c1e0d0b2b3ad62d0e98018e86718fcf6c452372719e84ccde808d1579", + "tests/unit/pyqt_gui/test_plate_compile_connection_action.py": "2ff8a68ed29ccb837e8e6efdf3cd9c03692d206034c2243e67b6de159d3da3aa", + "tests/unit/pyqt_gui/test_plate_manager_cppipe_import.py": "df080e025ddc8ba5e5d883954e6bbe3433eaf9e8ca72303871ad2346fbeca668", + "tests/unit/pyqt_gui/test_plate_manager_widget.py": "b02e81c4a73213577df02a1effbd6843935f5c92f459d6c61ab2998bd489ad58", + "tests/unit/pyqt_gui/test_plate_scope_identity.py": "7efdd8f7ed140d241c06d91283554c5a9110e9c052316f5889d8ac2eb5ced678", + "tests/unit/pyqt_gui/test_plate_status_presenter.py": "b4a23702d53c28a7cb10abe43c34e0302daf842675c258b8036d8e4ecbeffeb7", + "tests/unit/pyqt_gui/test_plate_view_grid_model.py": "80e5db964039812e11ea79aa84ea1a59bb8a0fb2cdf72700024188e6b8223b88", + "tests/unit/pyqt_gui/test_plate_viewer_lifecycle.py": "ae613b3f23faa15f37c34c27dbaf3235c8e98eda78e02d65701af2bab0199035", + "tests/unit/pyqt_gui/test_progress_topology_state.py": "f1c7499be9289b8c7591c8cf1658271bc29ccb2ed5962e3c011c6684090de6c1", + "tests/unit/pyqt_gui/test_progress_tree_aggregation.py": "618092ee65814f13d017e9eff092ff13730e7d77c13cc00ef9ee82bd71eb13dd", + "tests/unit/pyqt_gui/test_reactor_providers.py": "c685c258ba42bf606a72f256d8a529c7c6cb2a96f1fb9daef98f70100386bbde", + "tests/unit/pyqt_gui/test_server_row_presenter.py": "f7c7f172a85b08f54660a953cc02d2e88aee8b80ff1a5d0abc3e75d52d13519c", + "tests/unit/pyqt_gui/test_snapshot_browser_window.py": "568fe4836eef50a53b9c0000c18c534fda94adc1fa1137325c7951284f4460db", + "tests/unit/pyqt_gui/test_source_binding_preview_policy.py": "e0d551b50676e332190aa3ab70115d2566ae2af1ff71a6583d0717b11e9b3475", + "tests/unit/pyqt_gui/test_source_bindings_editor.py": "baa2e7b8e6a7165a8b90f6540b8867d91eb2123f3fff03cc196da28d96a9a5d3", + "tests/unit/pyqt_gui/test_source_bindings_preview_surface.py": "d424f0d9a5abf07b94cf005283bda9e4f564993eebe0768a56a3f67d5c014562", + "tests/unit/pyqt_gui/test_step_code_widget_roundtrip.py": "a67876053a5381e1767060d0ee5d54c79d96dfbaec8ef0e267bd767993a821cd", + "tests/unit/pyqt_gui/test_synthetic_plate_pipeline_document.py": "75e30548751180a92b84ad1b4346c3563798452835ac2647b60c83c1e7b21333", + "tests/unit/pyqt_gui/test_system_monitor_widget.py": "aa2a69497a4ea40c204838299f18d24af18107c2c0254923c40ca2bf5a0c0363", + "tests/unit/pyqt_gui/test_time_travel_viewport_context.py": "eade1dc2ad445b66618e248c60f44109b461c3138e665b0798754c9c957e2860", + "tests/unit/pyqt_gui/test_tree_rebuild_coordinator.py": "faabc7ba6af464c8b24324456dfa39c2df54c636bcfc92a1d5a5c8877766687c", + "tests/unit/pyqt_gui/test_tree_state_adapter.py": "8d7112aa9e633239a40521764ff81b4d877df13ba865cb93e62ad1f90801c957", + "tests/unit/pyqt_gui/test_ui_agent_bridge.py": "1feee4bb413738f04e81cbdf26244dc0d585a68921e6c7749a941a08518007a1", + "tests/unit/pyqt_gui/test_ui_bridge_lifecycle_config.py": "a2ecab6650184ea7b23f1aa65f755ef1c7fdbf82ac61c49dc48cae95b8227df3", + "tests/unit/pyqt_gui/test_ui_bridge_live_measurements.py": "2c6f3742943d968cda49e9675c0bc96370e49c80b1743a23d7d6e5f907caa7de", + "tests/unit/pyqt_gui/test_ui_bridge_live_overview.py": "4567e1a724d9ebd86e7a542f9b203210dde7a349c6e8e051152a196567465179", + "tests/unit/pyqt_gui/test_ui_bridge_object_state_mutation.py": "ae40befdb09bb6b94548425e69f4686be75151656ae38e869f2a1d1cf05961f6", + "tests/unit/pyqt_gui/test_ui_bridge_preview_disclosure.py": "d7b7b5ed3f74be5d69335743eb89e5b391eee4965ba59510dc6754ff7fc8054d", + "tests/unit/pyqt_gui/test_ui_bridge_registry_cache.py": "766a2757fc8de9a67ae30b2b9d4561257364289e2501afd190cc6d8da4639215", + "tests/unit/pyqt_gui/test_ui_config_lifecycle.py": "eeb7c260d3ae8bc7b5e6d94917b1e5e9a214feac9e2a0c2651552950cfe3a179", + "tests/unit/pyqt_gui/test_ui_config_widget_projection.py": "4cbb172354459bd021ec0dfd30c74043d83c37e58e5cda9912a04b62dfae08c3", + "tests/unit/pyqt_gui/test_ui_window_flash_snapshot.py": "8591e3e070c2b71fc0772839e0cc1058f0d17cc37e96a0fd15752dcf06312d79", + "tests/unit/pyqt_gui/test_user_facing_help_surfaces.py": "540d0fc2d796ff80543a7ef245d2450c1ecf988994e244c13472f9756cd904e1", + "tests/unit/pyqt_gui/test_widget_action_dispatch.py": "4645076d78559d61f0cf1a1072af0e656635531f4169f12a3c3a7156b35b5e6c", + "tests/unit/pyqt_gui/test_zmq_client_service.py": "4edfbf7ca673a19be34db52a15e8f3b600feb987a6d0a90e36ed767494db0b42", + "tests/unit/pyqt_gui/test_zmq_server_info.py": "72dbcaf8f28291c6807131d30f097b10d94d33a799c7b4f034f93e7efacae4cd", + "tests/unit/pyqt_gui/test_zmq_version_restart.py": "052699f393497ab23ec49d23fa136d969245c95c0973d6e9898aedfb860c43f9", + "tests/unit/test_ack_return_route_journey.py": "4785f1334577a67044db2ac900471b2d7ee7d8324fef0594105a40800037e016", + "tests/unit/test_acquisition_calibration_pipeline_journey.py": "328a63f88c843a9a6b1d2440f92dce4cf74f55c1faacc09e135f1c43c92ed0d3", + "tests/unit/test_aggregate_layer_materialization_515.py": "3050f0f395fcf58b8a4ed4b9ec25aa8bb2d6fa967c06fe8345ad0806ebd709dc", + "tests/unit/test_aligned_image_payload_device.py": "1e9794faaba06f44e826c8e7f81cff8383bdad66e9073e78fdc007bbc27bc87b", + "tests/unit/test_aligned_image_payload_registry.py": "2e361e02952c6986b3b6060cf61973d2d721bdf441cd96d002946b64b7e01275", + "tests/unit/test_aligned_main_flow_projection.py": "15b5b4535087abc5e23345ad606bd4dbb88eca686fca95399a0a2040d8099679", + "tests/unit/test_aligned_output_projection.py": "923ada2cd0dd29c0712b3c56c6ff34869992ed4549f3e05a71863322fb3c1c90", + "tests/unit/test_annotated_validation.py": "28c70bf256fa568bb3cc22cf2ff115604997d5f0e5e10a92c5879cfd7922a17a", + "tests/unit/test_artifact_declaration_step_context.py": "0252d4a928ad388b8bd319b3d95d2f94be2e4c04b01f4ddbb982a5414550c068", + "tests/unit/test_artifact_input_edge_cardinality.py": "10feb0d15272ad14de773dc2a0842ffde701932fe2c17dbc560dfaf889e19dcd", + "tests/unit/test_artifact_input_parameter_normalization.py": "9307b32bc0404e7ffad6b779cde9c1d846fd9411976833a2289b6100fd59e7b4", + "tests/unit/test_artifact_input_projection_plan.py": "a777c8fd900f15ca5f6a990fb48b36e1497a3555e5575e68487d7cb7b74cdd65", + "tests/unit/test_artifact_output_materialization_planner.py": "b36f4520f158ea915df1afc40a79d5e6da3ebabd75726f7b01082de0fb39f2da", + "tests/unit/test_artifact_output_ownership.py": "b967b89d36a5d337eeebf139acacb57aaf2ca79029572ad3a99c3a5e9963fcf6", + "tests/unit/test_artifact_plan_view.py": "f53f129d6071c3c60e12dc8a77088e948bd915640a8dc4c82e64f6ea779ed850", + "tests/unit/test_artifact_publication_journey.py": "f25dad54b6d1f43dee464a246d0d0d8e851ad39d9b8f77476a26bb5e7f408f9f", + "tests/unit/test_artifact_spec_collection.py": "892981904cff8f85ff0aa3987855ed90e8992b2aaec9e93c11fba32ac14ad263", + "tests/unit/test_ashlar_request_records.py": "28f7263f708fd490307814068a27f404315424c665bd066e862fe42717d6ccfc", + "tests/unit/test_assembly_acquisition_placement.py": "fe899cfedbabadf0820347b7bf01782c637842ae3a61aa990fd0be870d550412", + "tests/unit/test_ast_validator_registry.py": "afdc6404a95dc5f5bd97a5c6e7ab617d02614a5a001ce43512718dde4104d54a", + "tests/unit/test_automatic_image_role_materialization_435.py": "cd4bc38893dd13e3bbf22cc47d002592eff1c119141e8969619990e55503c4ec", + "tests/unit/test_basicpy_acquisition_fixture.py": "86857b170ab714f9d260d6d116456fa7f55966d44f0905c2056595a66cfbfe28", + "tests/unit/test_batch_submit_wait_engine.py": "e2fd500635f79da8ba2c039704b4094a118330b37552fc5b7ffa5352ecf9a8d8", + "tests/unit/test_benchmark_control.py": "1edf8dd50bdac5310337eb7590a4e81d688835e78101119b858c77b1fbc0f405", + "tests/unit/test_benchmark_manifests.py": "5b04a4ed0a2426f2350b133a93bf8a030017fa6bb9d299540732fcd32f82c7bd", + "tests/unit/test_benchmark_pipeline_registry.py": "00d78db059038731048e763992be8215deefe3581e096cfa204c268610f1e9b9", + "tests/unit/test_benchmark_progress.py": "d45d8d99cc679ca5e91835bbb5a1d72c3c594d1a0ebb446e50c7517d19dd4004", + "tests/unit/test_benchmark_timing.py": "2df7b3fe213504676a32406f6926907736d1d6bbe7b636ff53f12e20d40e0d9e", + "tests/unit/test_benchmark_visible_source.py": "6a2134cd19bc6be6ea2d9ed098dc17c4a1c82138a06c8189ad4de42ceea4b5ad", + "tests/unit/test_bioformats_hcs_validation.py": "1056158a2b1f7f766276fb6b90803f00b2f3e5a99be379210dba7a7b5948221b", + "tests/unit/test_bioformats_java_adapter.py": "140467f8ca95def648a88c0910692fe4dcfb4a4ac6864d147cecb57981b28c81", + "tests/unit/test_bioformats_microscope_handler.py": "d12e4e85a13bc28f7d67d1547ce8a347897c89436a129aebf45bc34cd5a193d6", + "tests/unit/test_bioformats_preparation_selection.py": "7e867853eabcd3aafa0519adbb3d34848b2ae175c1039b1bca2e3c993fff5f61", + "tests/unit/test_bioformats_saved_calibration_440.py": "6f8f3e3943b327bcefc89533681f010a2dac4688161f7eba57abf69018a8954a", + "tests/unit/test_bioformats_spw_projector.py": "05279446ee238d2114f1aca1148aee848a6b004ea9f4452a48d32b059415fe26", + "tests/unit/test_bioformats_storage_backend.py": "4756f18ddedb53127a9620c31ffd4834fff56e60938e46976c2ba8e3f46981d9", + "tests/unit/test_brand_assets.py": "ca5382cf06d51e5a4e1aa3ea836ee875d0a909f02b76eb88df8e6c5e12292140", + "tests/unit/test_build_hosted_mcp_plugin.py": "ff80eb73ac6c569be50fd6089af8835a515cedaec74d53535cc823f5e872bef7", + "tests/unit/test_build_mcp_knowledge_assets.py": "8b57a9579f943ebe1f80926a74ea0bcc235b9b197a3eeed250a95158119d0f9d", + "tests/unit/test_build_website.py": "4a8952ba0f51590ffd9f1d787986653802506807079a7e0d118343264252e0e0", + "tests/unit/test_callable_abi_preparation.py": "3725d8bace4e1742610b80943a74be21926982dd57e7bfe06e72e22a88226d79", + "tests/unit/test_callable_contract.py": "58e693b414dc83c919e77aa3429981ddd1ec2facf91433086a0789f55cdd6b8f", + "tests/unit/test_callable_request_binding.py": "a88256297b2707a1ef6de60669c20e9da33f2339fe1b696c752566d1777510da", + "tests/unit/test_canonical_custom_source_lookup.py": "8852e7ecb8e851072ecb145a09bfea5d843f735fffd0829468e98c8cfc0d8e42", + "tests/unit/test_capture_media_gallery.py": "aabfe3defb657b72150caaa3737f06352056e8580060633999a4296818ee8dcd", + "tests/unit/test_catalog_endpoint_incarnation.py": "26fb66e9bd63adba6941d75d4b1562a658bb6bdf3627bec5be4a01b3f5717107", + "tests/unit/test_cell_counting_cpu.py": "0789243bce87c24e10847c9dcccf6398e8786cea0222d5146e1777083a31812a", + "tests/unit/test_cell_counting_cpu_artifact_contract.py": "a6d5baf407224ef12567550cf527eb15aefd8593791bca870903f45fad33bfd5", + "tests/unit/test_cell_counting_cupy.py": "f63f98e676ac6412413a02cb813c79c317a33f37896a17b4177b21989084c672", + "tests/unit/test_cellprofiler_adapter.py": "2d5196ac77d12d1072d25d876088e52a1964e41faacd0b0d09e8a20986dce90a", + "tests/unit/test_cellprofiler_adaptive_guide_threshold.py": "2ce27367c9ff3a012fc998e5bfc48836a78000d4c82b0ee37feaf3aaa3da6a57", + "tests/unit/test_cellprofiler_additional_prepared_payload_abi.py": "072b6e95fed332b1526ab00f8a9ba309c30af612c0571043396ed0adaf77387f", + "tests/unit/test_cellprofiler_analyst_export.py": "d8302ebb5d59fb6fe34d45ad20d49bbb192e73124eb6fcd836818dcc6d87097e", + "tests/unit/test_cellprofiler_area_occupied.py": "df8bae02193bc9c5916c802d72f441e6516a20695a1f8a2049cbac9bddf436b9", + "tests/unit/test_cellprofiler_artifact_activation_identity.py": "b6a75f122455a505cd3dcadcc6710c7bb990a1c7fc6b3305621df0e374dc0918", + "tests/unit/test_cellprofiler_artifact_declarations.py": "71d20f26620b1ce526a20c583975abad23864200d528945346cf33140b7a5c2e", + "tests/unit/test_cellprofiler_artifact_identity_reconstruction.py": "2e9a227df16f901edf0d6f16905a18b29452f1c319271fbb0b242bb19f2c0e33", + "tests/unit/test_cellprofiler_backend_leaf_wrapper_deletion.py": "75516cd87015353da4af1c4a3108bdcebcdf5d2efa53f8f7054330f19ad87808", + "tests/unit/test_cellprofiler_backend_public_api.py": "a56a4a11424f7d0b0bbf2a2187fdf26b8de7d8622a83394c5b951cf45a2fd244", + "tests/unit/test_cellprofiler_backend_selection.py": "d1f7743717346ae426121347101f58652aa98ecdcabdfb972af24a33a83d70a8", + "tests/unit/test_cellprofiler_callable_kernel_preparation.py": "fa30685afc2170bc8718595f4cf8303f656bba585729d6f70a6e1e3af2ebb9d1", + "tests/unit/test_cellprofiler_centrosome_independence.py": "b38a72b822360a01ca3a1c55887dda93f66551e5546ac52ec02d658555e21544", + "tests/unit/test_cellprofiler_closing_hotpath.py": "938b1b26dae4359885a79ad98eccd06414d5dc6133bcae6732323bc1f36853b7", + "tests/unit/test_cellprofiler_combineobjects_source_scope.py": "3447da78c64ac01f29c680dd7095650bedf3547a0f7a2c2e23356c294b525b0f", + "tests/unit/test_cellprofiler_comparison.py": "d466dcd7c24ead62a65cce4b00476916da69920dcb9ba54000e488ee5dac94a9", + "tests/unit/test_cellprofiler_compatibility_matrix.py": "d66a7ab1c0252c169b8850aabdc55e07952607b0f46abae4d480a887be827bce", + "tests/unit/test_cellprofiler_conditional_analysis_images.py": "6a324f7e970b03f5bec32c50be882cba2b1accf9c4393582525a7b8337fd91b5", + "tests/unit/test_cellprofiler_conditional_measurement_outputs.py": "2773cdb08df663098edb3633a729325cef4bc474f6ab9e5c89537fa001eb40b9", + "tests/unit/test_cellprofiler_conditional_named_images.py": "bee6a9ed7ad9b19e29678dd3431dadf41acaad7d6f439058be4408ba62d61af8", + "tests/unit/test_cellprofiler_conditional_topology_declarations.py": "776d4e8e1fd2454e5dbb545e0427370d8861f2d068c3abcd6459924b9ecb564e", + "tests/unit/test_cellprofiler_converter_cli.py": "7060a8839bf4bb0a6389d9bef47b9745a6d9668ef9c918eb278104405c94a70b", + "tests/unit/test_cellprofiler_display_manual_projection_declarations.py": "3633ce284cf58de11eea2eab5cce7223590ae232ba7aca11a65ffe0253d511a6", + "tests/unit/test_cellprofiler_display_plate_map.py": "1d2f397c47cc4b602bb0789496be70040fa9af9509a77f0221c0ca56b18a3356", + "tests/unit/test_cellprofiler_display_runtime_payload.py": "6178b31bb6ddb8d2b29e9eb0d7090925a4333bc68a46c6d482494289200ba537", + "tests/unit/test_cellprofiler_export_equivalence.py": "b103af3823c1abd889a68cd7652c21e448c162b5237d8a44009caf3e830a9c6e", + "tests/unit/test_cellprofiler_export_measurement_outputs.py": "d887822db907057b3f24dc7462fbb746152373639b70ae0275b7ab7ac15d3cc8", + "tests/unit/test_cellprofiler_export_to_database.py": "76c656b2054eccbd2387b9256490052a407acec6e18aa9938918862386c3b365", + "tests/unit/test_cellprofiler_extended_relationship_topology.py": "bba08ad4048be87a037fc25c4bc4e9445cd5bfc3e8374c8c55519a819246ee17", + "tests/unit/test_cellprofiler_full_stack_materialization.py": "ffc6bfe96a431e72611da4e287d496065af9d4bed71984881a29c3f7a902a1ec", + "tests/unit/test_cellprofiler_function_contract_execution.py": "1fc83be8444c59c81ecd6ece3c85e917c50403aceca3d98016967f0a72b072d9", + "tests/unit/test_cellprofiler_generated_pipeline_execution.py": "82678f50e1793ee1414595afbb13eb6cb4e172e39c1a59f36e5bf5c3d6726917", + "tests/unit/test_cellprofiler_generic_special_input_binding.py": "bc6783614da9c25a732ae436d61bf9b8f421d4dcb1353e4c70e11c6656f5015e", + "tests/unit/test_cellprofiler_geometry_prepared_payload_abi.py": "44755ebf3e1b4a4768392f1a77e32f52512c5649bec627654b66e468d89cf37d", + "tests/unit/test_cellprofiler_identify_secondary_replacement_primary.py": "542a706e289becb36e9daae68250d34882a65d84edefecde1eebac2222625d13", + "tests/unit/test_cellprofiler_identify_tertiary_output_abi.py": "2b309eb89f9d092cb2ed564c706b5eefeabbcecc0c36f89029be310221500870", + "tests/unit/test_cellprofiler_image_geometry_cardinality.py": "28aca73d97996b932d4b3976276d86e4e567c2d322bb77ec019106984316381e", + "tests/unit/test_cellprofiler_image_math_hotpath.py": "1f3cd8292a2c8ecd5b162e48f7cffdbde28cd6a172af1435a32becbc461ed267", + "tests/unit/test_cellprofiler_image_math_mask_metadata.py": "8d02a637016bc55440d65cf2783673b92a99e6bc876d3a3993c71f6ec98e96b7", + "tests/unit/test_cellprofiler_image_math_operand_axes.py": "0c97d83928e5bbe5e80c1d9219886292ef4ad3858a4ceb12b98eb913c465606e", + "tests/unit/test_cellprofiler_image_math_public_roundtrip.py": "a97ef845b3c99005e43b106339967bb0dbbf21a2eb49da98b084326277174341", + "tests/unit/test_cellprofiler_image_output_metadata.py": "006cf27e04c1b0af17555adf9e8418864fc35b33dbfb478488da69908ab03bcd", + "tests/unit/test_cellprofiler_image_quality.py": "3130ade2ec2015bdc1d4e6b993c629f8a912f9c2c48ec490d173135f01aad274", + "tests/unit/test_cellprofiler_interop_import_records.py": "91dcb69e8c3fa6b00a7a897149af82077fad5d63869532b9b1333e4cd977c0f5", + "tests/unit/test_cellprofiler_interop_namespace.py": "398626ff0980e7c31644e8e59fcc80ee2cb30abc989b9caf5e320c1f864cc710", + "tests/unit/test_cellprofiler_kernel_preparation.py": "51c6b172c411c282c036d542bceea8de43ec3031e754e0c99053839ead19c609", + "tests/unit/test_cellprofiler_label_hull.py": "6cd43a6edff52996429d0b01225f8db6440768c175169e799481156b8b6146b8", + "tests/unit/test_cellprofiler_library_loading.py": "6d864e5c8b1682403f9c57a76bbe3b204c0fcfa15395c2701b01da68ac531a84", + "tests/unit/test_cellprofiler_library_untangleworms.py": "96d6dba6bf714a1a057d73f6e2f4a698b8754ad57ad767acbbe41d8db701d62b", + "tests/unit/test_cellprofiler_measure_image_skeleton_contract.py": "6b83070e8b462970ae78309b466cb132cfd5929682e6a189c3eb0be779b8711b", + "tests/unit/test_cellprofiler_measurement_math.py": "81e6dbce935f9dfcb23f3d80b1923e76babb63316ef67f8a2287b529be356fe4", + "tests/unit/test_cellprofiler_measurement_projector_cleanup.py": "efebeaf02581e8925fd10a1ede225f770da1c3bce09293c43ee57e6806f27914", + "tests/unit/test_cellprofiler_measurement_rows.py": "075840915d45f71471f10783d693144ce984778f7b9d8b5ecdf462d835fa2742", + "tests/unit/test_cellprofiler_measurement_slice_consumers.py": "6ed678176054a6bd61089506a32b81c240587a491b240d651afc03ba27653546", + "tests/unit/test_cellprofiler_measurement_source_group_scope.py": "87d2454b86ad0b3baf7b1d4a7405363420c14331dba46c544b1329dd586f3e26", + "tests/unit/test_cellprofiler_measurement_source_names.py": "63cc28a24f49c942ca29428da35563bb78feae2b8acf851009f24b085e60389d", + "tests/unit/test_cellprofiler_medial_axis_contract.py": "25f1def3ed4f47823a8c2a1735eff6c915fb602b7ba1d1cfb0345d5710527ca9", + "tests/unit/test_cellprofiler_module_execution.py": "5d28dcb3645197772b77d5f8eb4dc07e729bc4b2094322e20efd6a7468f5993f", + "tests/unit/test_cellprofiler_module_execution_semantics.py": "86cd909cb3484ab39fd9f31e0d4ed63da801bfddfc89644662ca85d42cc92662", + "tests/unit/test_cellprofiler_module_function_resolution.py": "8afc6262bb4fa22c123febba522f29c3e6330e5c5fc6c67934bf3c6f49b1bf9c", + "tests/unit/test_cellprofiler_module_measurement_features.py": "aca4b6f427006999f174396ee17a9007983979c34208e01388c514dda57c7f44", + "tests/unit/test_cellprofiler_module_measurement_scopes.py": "4e5cd550d276e8a9bf04ae370b8cca85e5762d9eaa4ed64c7e5af0744d0e969e", + "tests/unit/test_cellprofiler_module_processing_config.py": "6ae8435f4fac89f5e6963c587dec688f363ebda4d5d9e688fcb56a84664f8b75", + "tests/unit/test_cellprofiler_morphology.py": "a09f4cc9299e41952996451654eff43f33276229263bf05f338297d3e64e563c", + "tests/unit/test_cellprofiler_morphology_public_contracts.py": "d33f057a26cb49b1a792edbd2b035c529695560c2c65a1500d8bac97557aaaa9", + "tests/unit/test_cellprofiler_neighbors_contract.py": "fcdd615077a5b9696d894487ae6d51b624d208d755652da5ea2e4fc0b66c9429", + "tests/unit/test_cellprofiler_nominal_policy_migration.py": "10bd96f1da8756657b0a4b73e9a0e9b481d9b80a525b573ec9933e721d61771c", + "tests/unit/test_cellprofiler_object_intensity_hotpath.py": "125b0125452de94581c238cd4cea493277b86400dc2bf1e11f7487d16e1638e7", + "tests/unit/test_cellprofiler_object_label_payload_inputs.py": "3764b06667a642af04f5f0dc3acc317193009149b809698d1004c4b60c3048c7", + "tests/unit/test_cellprofiler_object_measurement_schema.py": "eb4330cfb3fe69c27ebab0233c64df3fc18c1015909744d604ca7ba7e2c2e7c8", + "tests/unit/test_cellprofiler_object_measurement_subject.py": "ea81a9a03ec20c6040707978b1ce6c828ea9d7b67561cf2275d57f44165722d7", + "tests/unit/test_cellprofiler_object_morphology_hotpath.py": "a62c331445e802e5820938638595472f3dc95dd0032630f9fe79a0324c17b0c9", + "tests/unit/test_cellprofiler_output_cardinality_declarations.py": "2879dd2491590334e9ba6df097dabd50da8ec89c015a5603d52398a7555765ba", + "tests/unit/test_cellprofiler_output_record_occurrence.py": "c264787de54147b0ad6f5973df7cc8dd0572ff6b9b1b3cb49117ebad0a3fd6cb", + "tests/unit/test_cellprofiler_output_recording.py": "d031716e2d20d4e97b304d6123d92a4a9c69745d85671ae94fe8f7d50d4f213e", + "tests/unit/test_cellprofiler_pipeline_import.py": "353f0c1d8e0bc3552d2e418d792794ab0631daa16c78a3e10191b1cc1ea07d73", + "tests/unit/test_cellprofiler_pixel_classification_entropy_parity.py": "e60125c783b1759a9582ea7590eb4761ac0120310fb756dcb2978a04ca3b686a", + "tests/unit/test_cellprofiler_plate_workspace.py": "97602b34774e75926857e62faa446e307cf32a0c7d2c94fab16d5cc0919eaca7", + "tests/unit/test_cellprofiler_prepared_payload_abi.py": "9c367279e90217580066e23a03e7c343e1fd5b6c2a767343b1b1814e01c97ac5", + "tests/unit/test_cellprofiler_primary_object_performance_kernels.py": "2fbe25378814bee5900595476f47ce59b29564d66b3cf24c1742452fb1a058cd", + "tests/unit/test_cellprofiler_prior_measurement_output_relations.py": "c5b21937d07d5a0906bec2cbe0f8d66b08f806656db0faabbb2fe345c663657e", + "tests/unit/test_cellprofiler_processing_backend.py": "949fe2e9c3ab8b7b416121d65cc5b70aea2c59e970aa823c7bf0c0d5e438a4b1", + "tests/unit/test_cellprofiler_producer_row_schema_authority.py": "b3cca837b73e071a4a42f2cdefe721975debd8268c5f92312e5be45af2eabd9b", + "tests/unit/test_cellprofiler_projection_backend.py": "7eae609ac6ff76229b289170b9b5c11406c0a8035555a09cde0b771b5a7b6515", + "tests/unit/test_cellprofiler_public_boundary_contracts.py": "1f025f7e8c3fb8d573408046aa65bd2cafae7615a3e782b7f9facd8720bc3283", + "tests/unit/test_cellprofiler_reference_exports.py": "8b84a9706af7c9226ef93a96a3fdb9c1c16a8f220fbee4a65a53f6e15bc7cb61", + "tests/unit/test_cellprofiler_relationship_contracts.py": "c14b67304599c32405b832c2f1e839bf1132a22b276baff23ee7720c6d117760", + "tests/unit/test_cellprofiler_relationship_hook_caller.py": "bc848110c62fe2e591e57bcedcd0c287b82dc684d1bae44b8de05d78ea368aee", + "tests/unit/test_cellprofiler_repeated_row_declarations.py": "9e9a52dcf7f8b7aefb34d92a8cf5637f3869432c19b79d5d7807979a58776c27", + "tests/unit/test_cellprofiler_rescale_intensity_hotpath.py": "80627ebb7365aae6701f7138ec8a0945930d9502c5d1a1d4bc07923e928a5721", + "tests/unit/test_cellprofiler_resize_hotpath.py": "680502c768c3628e14b4ca43909b4d5005c578079705e9aded393c389dd3fa18", + "tests/unit/test_cellprofiler_runtime_adapter.py": "e4c1e9a86e2dc93a0af24088855caf3c12566e4bd99b9852c9ad3d152e2531a0", + "tests/unit/test_cellprofiler_runtime_binding_identity.py": "ff7ce51901b7d2f6838f19c454c0ecd32c219f82fafcb56748d6a8988ec73629", + "tests/unit/test_cellprofiler_runtime_cache_authorities.py": "67c1ae2f473e272931aba7985b46de77d26ea6b057a0a3eade1ecfb9fa3766ae", + "tests/unit/test_cellprofiler_runtime_callable_introspection.py": "c4a64d9dfcee6edbd28b44582c6cd2e2b0beaab59ad7b199c42d210adb580c93", + "tests/unit/test_cellprofiler_runtime_input_binding_contract.py": "72918563a6e410956e41ba902ef2affb4fae35f60169c924dcabb233715c6e0a", + "tests/unit/test_cellprofiler_save_images.py": "3d85eadb807d48f15b7141e8e708e38ea61d7adfb4997a7519c7d6da439ae732", + "tests/unit/test_cellprofiler_save_images_hotpath.py": "bc099686d2d6cc63f8e91b474ba46881dd783b7aa773d314d0513361ee45e8a0", + "tests/unit/test_cellprofiler_secondary_distance_backend.py": "1b3fb32dbb47b6d00928926591157ee0e97d385e4928475e5ce8cd4c9192fd40", + "tests/unit/test_cellprofiler_shape_hotpath.py": "482780c189ad4e6caef0973b05337fb3b8d092781ce7a5bdbf55ca72751e15d5", + "tests/unit/test_cellprofiler_source_bindings.py": "c10ab63f23a3d3c7332d890a669322c0709ce90192ded71664b02f1a2e7bdfa9", + "tests/unit/test_cellprofiler_source_bindings_ingestion.py": "46cad46526d114aaedb6c1d587224b34cb17c1e7ca8996c09acf21e8d514eae3", + "tests/unit/test_cellprofiler_special_io_declarations.py": "7b1001a3a3c18f8cee3b287fac51c9a63c0ad1ae0d3fadfd51ba8f3ceb421437", + "tests/unit/test_cellprofiler_spreadsheet_export.py": "0bd1bdc2c27c9133ffa4d665ae31f6121f636e1aa39b3ac65a78650a9b5a034d", + "tests/unit/test_cellprofiler_static_deletion_gates.py": "83d81d01ef7d828b2d0fe971a8487aa4cd049bc69e488f8f82aca2e9f02c2cbf", + "tests/unit/test_cellprofiler_strategy_registries.py": "4c6be9a25257af4370271b242072cd2dfe645113a56778108d8fd2f3878e42b4", + "tests/unit/test_cellprofiler_threshold_hotpath.py": "bbf2ec9cce2a366bdd6edd79b4de1ce5e24c8a703ebaa91b975643f94b5d2167", + "tests/unit/test_cellprofiler_threshold_measurement_feature_parity.py": "daab012aa799fb6bf612371ed4c07b5f00c90dca1f5e7ff04d048f8da4c586d9", + "tests/unit/test_cellprofiler_thresholding.py": "3422f937c0511d957a0f2d77e7ccba3ebb194db3af513b6a8e94d3d7de479eb0", + "tests/unit/test_cellprofiler_trackobjects.py": "71cd71125c1373d04da4da046d866d212664e3abe7d8534a67aba54492abbc8c", + "tests/unit/test_cellprofiler_uncovered_image_measurement_contracts.py": "884b0012c53e179d3300d6e174a15c9d354b074f869a8ca547e87b878f956d9a", + "tests/unit/test_cellprofiler_uncovered_object_output_contracts.py": "7587de1c35b7e69ad02a2f4b9c277c4281482b7c8b0e0015fcf934740efc8a07", + "tests/unit/test_cellprofiler_watershed_3d.py": "b4ed415c2daea68654aad5f072782f9338e307d2a24597978b34dbb074abf1e3", + "tests/unit/test_cellprofiler_watershed_hotpath.py": "268dc2d112e078e4edd999a4ecba8cd53f78ffbc60051f96998ab78cb2821f72", + "tests/unit/test_cellprofiler_watershed_provider.py": "163e378d222f889921515bc1b2bc7f58e1d18fb7ca27f9ebafa784ad556ce15d", + "tests/unit/test_cellprofiler_watershed_residual_hotpath.py": "a500ffd630f911f3be5417355538a94993c7c70cf198fe942bf2cc1d2e9ee4a2", + "tests/unit/test_cellprofiler_workspace_export.py": "87cc6873e395b068e07e9ae0e44ec1d45b8ae66f4be14d8464f6bb38d84e6a13", + "tests/unit/test_cellprofiler_worm_external_resources.py": "ca093ad12535ec7208ed91f05ce4779ed7c38e5558bc2f595c5dbcc2ebe801f7", + "tests/unit/test_cellprofiler_worm_geometry.py": "8cd2adc998e3b1e07d3cb4253df8bb8031d94599514f2c7c607cc8e17261e032", + "tests/unit/test_cellprofiler_worm_measurements.py": "ae8605658f4e1efa67a8afcd4e4b7e9227f843a91169e9ffb2fce102bdb430ed", + "tests/unit/test_child_measurement_producer_selection.py": "1baececa0cf0f2420bdee854ebacc3c03d12c6dbf3eb21968c4c32bd48d3d932", + "tests/unit/test_ci_action_pinning.py": "ef540fdc31ce7415adf55e8a6dd16f6524cc44d833e0b94c4e778a7435ba2799", + "tests/unit/test_ci_package_boundaries.py": "691efcdafe66255c55ae11411b441930bdb58161cd74e2580c2531a486863303", + "tests/unit/test_classification_interop_boundaries.py": "c2e1808ad6ab4613fc11ce69d417db7efa804b4bef6d8e1b78076c2e8eb1ee89", + "tests/unit/test_classification_two_vector_authoring.py": "4072744d39aeedd7cbd8ba39b6d30ec1be0428aecd6085e55d907b827357b66c", + "tests/unit/test_cli.py": "2190d0af08ca00f3c75907464055f6a2b0279b080f78ee3494af060c64ddc08e", + "tests/unit/test_code_mode_path_artifact_architecture.py": "d5c86163a5a74d68d9796f8420ff591f4f3130884a2aa430a3a60b53517e6719", + "tests/unit/test_colocalization_metric_setting_authority.py": "9d01d0cbd08dfaef7873a72dafd2654c0ff3d72fde9e52255f2466065451367f", + "tests/unit/test_colocalization_preparation_readiness.py": "0e29f77a8be4bddb6a64cb3b1322bac65c5d975f44fa2517025042afb2904451", + "tests/unit/test_columnar_row_schema_authority.py": "195e527b6d4e80479e15a9db5da71de2eb9a08ce6690d07e5d85134c126e63b2", + "tests/unit/test_comparison_manifest_acquisition.py": "90b08e22526fba440c1108427505932f747abc5738396cb8c1729556294cbdfa", + "tests/unit/test_compilation_paths.py": "0dd385344b28b930d8937ac717af58862d8cd102d96e3a193efdf8c0c0b83e16", + "tests/unit/test_compilation_session.py": "1bf949e58b0dd65469ecc548d7defe15c3d38f76ea15c3e3bc5ee8edf4eb53b2", + "tests/unit/test_compiled_execution.py": "ce77099d48c7379a7ed46147865811308d43d6861fd81fa8325a619f9026d6b5", + "tests/unit/test_compiler_auto_materialization_policy.py": "20e99467cf383d2af65bf1608b7a25fa2ed4ccc87577cee1af15141da11437a6", + "tests/unit/test_config_document.py": "0ee12db4633ab39b34f630e4ee1b0c48b40d8226f91fddc504bb3311b9612cda", + "tests/unit/test_configuration_consumer_coverage.py": "2983d982e12bb3b6a1b20665343457eebae1b2cfe1892538f11fa6fabfb42777", + "tests/unit/test_configuration_mirror_deletion.py": "26002ddfbbf89bf06ae54888645c6b8c90154536a16e93feee1a2fae713c4963", + "tests/unit/test_core_config_annotation_validation.py": "670d1903d03cdc12baea828336da05255d5076a3c22b86752e64b3a115481da6", + "tests/unit/test_costes_parent_mean_qualification.py": "dd13c9bfd9937857c36b6b0401f892f477beb2905d59efb5d90e48fa1d1047ef", + "tests/unit/test_count_cells_simple.py": "d14b5be006f439e65e185c4166ec349cbc017cecf6645bd950357be27d118698", + "tests/unit/test_cppipe_corpus.py": "f331b8d461a705747390b280a1ea6938b18d2949ab10b87de47b2f4da19ad96d", + "tests/unit/test_cppipe_execution_validation.py": "ed60b7e839cad3e30086aaf70e7185dfeb6b1263d5ad43bc40656d30d4df3b49", + "tests/unit/test_cppipe_parser.py": "c4fc16ef34c424addfdb3bd3a8c48c48d01d4d904f0463beacb25535d6864847", + "tests/unit/test_custom_function_events.py": "8cc545a424a3c5235dbca2c123ce7f1e266aaaa007b85082154cbf06ed78122c", + "tests/unit/test_custom_function_lifecycle.py": "fa04ef4891d76e85ba5f97b301a42fbaf077d6e21a8dc9ace34651e198bfe067", + "tests/unit/test_czi_brain_axon_cellbody_demo.py": "d588efdc530936fa2e4fe2c12ef95b95561d284a0d0cfe8de71e27c86aaa338f", + "tests/unit/test_czi_zarr_source_audit.py": "e34cf1bf815561bba0a641e08b1f4cded09c5157e37230844d1e113c9c95fe83", + "tests/unit/test_dataset_acquisition_infra.py": "517e29990e5ef5c038f3933a45545ea87d7e9ba75e29aac8e3ec754f99fe2c99", + "tests/unit/test_dataset_registry.py": "229655ec34e5d9624bbf4adfbda05c414c3b5e01a3ad9c437e9ecab4b9ec3831", + "tests/unit/test_debug_runtime.py": "6435e045afe2b292ce41c11b0cc671dae47a7a4039ec8c0c70f287f406124089", + "tests/unit/test_debug_views.py": "a12558de56bb0a8a2e436fda77f2333efd5689a0422189ac8a71279d173c1d1d", + "tests/unit/test_declared_singleton_composition_437.py": "4963c5d271965647125a5818b417aa40262c1c44ddba123848326beabb787fe0", + "tests/unit/test_dense_object_coordinate_reduction.py": "68b9aed4205541b07c1a1068a395f313b1e0b6598c6a2dce785cef5131d96d1e", + "tests/unit/test_derived_role_publication_435.py": "86e34ae6ad4de6c893ff80be3b842deb908bcf1c111a462098b87e7480ec74c0", + "tests/unit/test_derived_runtime_image_provenance.py": "73d0a93c23320590674e60dfa8a7af41a815b0b806c2383c0d9f1891b59c3b18", + "tests/unit/test_desktop_deployment.py": "ff766161504893139c0639884bacfa10e018a78eecc692fad1586ffe2af4e0f1", + "tests/unit/test_documented_dimensionality.py": "83bef8dcad91750c9acda63b8a4293ccbd0904dd5aea7a32be197f9d51f300a7", + "tests/unit/test_enableable_function_identity.py": "2f7ff821ddefd79c60f9b4b8e89b287c8ebe6b773699638791137ea32fd490d2", + "tests/unit/test_exception_traceback_compatibility.py": "b6bd140d613f67dcf4379dc3937b88f4a1ca258e7c7345153595ea5b2c676467", + "tests/unit/test_execution_server_startup_preparation.py": "d9bd0a0eeccf10d4ad39be72812217d93b63cf664c1d469146773158ed5dd7b6", + "tests/unit/test_execution_status_poller.py": "02c8a35c23b37de67f4f9e03ca822ac8e640f8434db738ed80715778740bd909", + "tests/unit/test_expandorshrinkobjects.py": "ea4b28f647273e6ad03125df104ea47a2d18be3b50111105523be6ada560a1b1", + "tests/unit/test_experimental_analysis_engine.py": "d7033d84b188c4cf7f2e8068599a746375300212d418cbf9c4c7d00c70de0f87", + "tests/unit/test_experimental_layout_rows.py": "d71b1e77aeb31a543e9da104a9dca2169aa7ea6c6e690a5c19302b26e76ae31b", + "tests/unit/test_exported_relationship_equivalence.py": "4fb5fe7a95601b87249952ec6022a60b0e244acaa8497ce787e53a54d106e6fa", + "tests/unit/test_fiji_test_environment.py": "f0ab63aa567b045c655731fa5f52af76618a4c2fe2d96fe2e7de9650346833d3", + "tests/unit/test_fiji_viewer_server.py": "2457abce7f1673f1f05b1c368add152ef73c9cf09a4f86319b15fb49111b3d48", + "tests/unit/test_fiji_viewer_server_launch_config.py": "c575cf5e4bae0d05dc6edf5b158f200bb023a1a3b1eeeeff537774c6a0292fcb", + "tests/unit/test_file_materialization_options.py": "7b7834cc57289b82dc5c2ca50f3a08d1be2b85212f8078d1ce1bf6b76b83d227", + "tests/unit/test_fitted_illumination_fields.py": "b0dba0f59136c74a92634ab4fc7e5edba3c560ff0ce15f5245c020ffcef9bbdc", + "tests/unit/test_fork_compiler_preparation.py": "f6bf0953f28cde60ed4c2c7d4204537cc83ae77e118fa850c3f3bf0abce75de1", + "tests/unit/test_fragmented_roi_materialization.py": "aa9e07f03bd010f23a601dd90a511c840d339c065e7e3f4042b62192fc349bb2", + "tests/unit/test_framework_device_resolver.py": "ba1814ae8eae05a39b594a81b4fc0d1444bf1334574bf4efe49aa829eec43a65", + "tests/unit/test_framework_execution_scope.py": "888a7693d387b0d5007c9868f0b0344fa3df26620f35e40b45832f0931ef7126", + "tests/unit/test_func_registry_projection.py": "2149ff5b4d623ebd93f508fac786c4f76e763e29115b3991cdadbb2bbef6bb18", + "tests/unit/test_funcstep_contract_validator.py": "2188b297564b7c756af16f1138e304a1a7dadbdea28f7c440bc7b484d9eb615c", + "tests/unit/test_function_artifact_materialization.py": "42d4b00c0849bbf28f079a87dbf6552d03e2b1e40f247da1421a603996bbb351", + "tests/unit/test_function_artifact_materialization_io_hotpath.py": "be8b34aa6ce8abad7f3f821bba7484758696f58726960bf24131c5a7c280ad72", + "tests/unit/test_function_artifact_outputs.py": "8da643befd94c558b27b940278aa7695688158951d0bc3153522b9f6a414972b", + "tests/unit/test_function_catalog_zmq.py": "ad58725b8e9eb418ae5775a7f49263e5f0e2eb35bfebec6a71539aae0db2715c", + "tests/unit/test_function_execution_lifecycle_anchors.py": "344e44f83930a929a0b09d6aa7c999fe4ade2f534fd7f5849168275abcf4cd93", + "tests/unit/test_function_invocation_callable_resolver.py": "d3736dcb4733fdb8538d3f9e8f813bcbeb151ffb365b788f69136a4cc5fd7b24", + "tests/unit/test_function_io.py": "c54d592b3b0c8636150d4053eb11164556141036d8d0e1cd70a222b884aabe5f", + "tests/unit/test_function_output_manifest_input_scope.py": "a0c1619a2e7ff80e766d89a494a91536b7b45581e9349571eed63ca56498bf58", + "tests/unit/test_function_outputs.py": "f45696a138d9b1d055cda36468367dcd755e03b27681280f0f043662025371b5", + "tests/unit/test_function_patterns.py": "bb77ff3ca4b2aa7c05b1751757b0937c191e790dad2c046c5cd30273867449ce", + "tests/unit/test_function_reference_identity.py": "bb9e5a5002ffe01284178d5e611e2015d95625a0b226b5e51a9581f2b1acb619", + "tests/unit/test_function_runtime_source_projection.py": "1cadb8286047e8bc19e24aa19603ba3defff8584a23cdc7d43640b56815af6a2", + "tests/unit/test_function_step_document.py": "8a66f2625c781b136674ba7ac390736d4ce1685ee9b85308a49cfec862b7ac36", + "tests/unit/test_function_step_execution_plan.py": "e933c545ed00bcd5ab624f62b6b40a8d2d4e350e3ff19146eb43205a61407fbe", + "tests/unit/test_function_step_execution_scope.py": "c5cc0c80f99cc9a3fa862ec44aaaf9ffbe165f054c9334f16382a63b39d03b65", + "tests/unit/test_function_step_transport.py": "1d0675f3a860bcacebf5fd7b31dd5c85aec08941d20d99d8b4249507c0c43b1c", + "tests/unit/test_git_hook_submodule_safety.py": "5afcd24902dbe701803786736104f565934edb3093638a46a406e56b05dc09e5", + "tests/unit/test_graph_roi_source_roundtrip_134.py": "ad3040d68df96f7a742ec1ddf392c721650510c119ee13794431c1356ac59194", + "tests/unit/test_gray_to_color_binding_axis_432.py": "84fcb3c7471d69f8647ebf577c392c2f4805bb0d1f8b298516d8c69b48a6cd42", + "tests/unit/test_gray_to_color_carrier_compile_432.py": "38582266c22850e1dbfb82caa7a3f62f454570930dd7a161ebb64ecfd17d76be", + "tests/unit/test_haralick_feature_arithmetic.py": "a9b04b224d1f26ef706680d7dcb7b45c6f0836efee7ab40fe73996cc81db5aa9", + "tests/unit/test_hosted_mcp_container.py": "dc16ec4b6aa74ff085c2681638881b76003860378feb3e1ec30ff3b16f2746c2", + "tests/unit/test_image_file_serialization.py": "9d69cac82c9e3eb99860877d2f3ca1b0bebf6ad41a1a49161abbe584773bed60", + "tests/unit/test_image_output_context_projection_proof.py": "7217339612d0cc674cdc0813c6258ad75231e785e3fff7eeed3d097f62c7d76a", + "tests/unit/test_image_payload_projection_algebra.py": "8a53039ceb1248ba04243bbe08cb83f7a1458adf26c78775f7f6790bdf33b5d4", + "tests/unit/test_image_plane_contracts.py": "7d2672411cfad98a4ea21969268591dcfb6bd7211ce4cc79bf3658d13f1936e2", + "tests/unit/test_image_stream_request_projection.py": "4f3560759b74a7116b2c927116f40ad072204d5e62d69644cb400bbf9c71133c", + "tests/unit/test_implicit_main_flow_compilation.py": "3f4721cfbd9669af2d89da7dc0722bce7409132bf179f0033201000c53b1a446", + "tests/unit/test_import_utils.py": "9e0607b1d862a32df02777f1f22d4d08a227f5f3d4c76323bd02aad5ee8b6512", + "tests/unit/test_independent_validation.py": "41162b6c1a0f51f41df27263f24ad48276b9a0dfaaa0259368c80f4c919aabf7", + "tests/unit/test_install_ci_candidate.py": "43b636293b30767c8040d7da77d440090089d097df75dfcf0649efc07fcddcb0", + "tests/unit/test_installed_mcp_demo.py": "f5d4a719a7902b364b551a3cbc4a816ccd7f46e5d9abbc87bf2d53cb0180053b", + "tests/unit/test_invocation_contract_provider.py": "fd06be78e1e5fa09d238b327a70e75ff993c2a00509f57a85f21b2befb1b0c5a", + "tests/unit/test_invocation_input_source_context_identity.py": "2bd47803761442d8aa8d64bb642960c65d21f2abf06cee7e50e59f16814976f9", + "tests/unit/test_label_area_perimeter_performance.py": "c2a77cc2384434a424f9679a92737d3babeb218cd7897146604919d62f7fe6b3", + "tests/unit/test_label_geometry_ordered_selection.py": "d9a367590b99b1c0e6f48bcfaf2b34bd0c22115d20d768335a3e07e7d523b230", + "tests/unit/test_library_registry_discovery.py": "4ed60118ac0a052d043758a1338939fb9521f46046fc86d40376e91e0528be1d", + "tests/unit/test_logical_image_output_snapshot.py": "1543a3859d68d9d166389bc28977f5988f1736b199fc40cc2210e7728fd030a7", + "tests/unit/test_loose_operaphenix_neurite_example.py": "219e2159c50234902c09b9d757e71bfa89143d7b0ef057b6a3a9d4ad9b76876b", + "tests/unit/test_loose_operaphenix_neurite_metaxpress_example.py": "8d483849f35061aab8ab9b8dea7bd4a3491c0251fb380cd1a2178c2e96892fd1", + "tests/unit/test_master_multi_plate_demo.py": "92fe883ad54393a8b444855323c319e08fb40fe47c1a4183aac326132600c2c2", + "tests/unit/test_matched_cellprofiler_batch.py": "469fb3a9da16b1eb7224c63832c7d807d4ccf08776bc0ff9775dcead4625ab46", + "tests/unit/test_materialization_api.py": "3a089a428a541301054b2fb688880a1ccf2eb296f4b4297173b3124b8831b28a", + "tests/unit/test_materialization_batch_outcomes.py": "08abb20627d0d82f537a8c688984f04b8bdc4fa298a29fcf9cfc8c006f30a8ba", + "tests/unit/test_materialization_core.py": "59968a28cc5a838012c5e4ba8bc29a14d972e4a4df0b55bf4110c6938f1f46e9", + "tests/unit/test_materialization_filter_inheritance.py": "ec905e0a8c7188878236e460ea7dc1532ad249f35876e00137c9c7b39e11819f", + "tests/unit/test_materialization_flag_planner.py": "5e1c4751092670e28a352f463b44a57c7df491e515eb14a942bec62c44c35a5e", + "tests/unit/test_materialization_source_extension.py": "199b3e793f6cbafec7f4dd732d356c6705b91bed18f27761e75c6d5392efa3fc", + "tests/unit/test_mcp_assay_showcase.py": "916c0cfd9d75af93628ceb4ff948414078638b4638a51b1b4b075199224729f1", + "tests/unit/test_mcp_dev_client_persistent_session.py": "514cb76e38cd51ea542f7f9fece2758cbee319449a0bcfe832ae064c666bf3fd", + "tests/unit/test_mcp_dev_client_terminal_input.py": "2328b95d10f4e57c048694d3775e4039467307b95bddf8c70303c49f5d170321", + "tests/unit/test_mcp_memory_diagnostic.py": "b9ff12716ffb934b35885764292defb04f4bf542024020094d1308d4cb05b763", + "tests/unit/test_mcp_release_workflow.py": "7d8164b53ad4758e47a6e8dd5235dc0b74ddf1aa232cfaa60ba2589ebdd9a360", + "tests/unit/test_mcp_thesis_demo_live.py": "26cd0380fdd03668baa346931571c9db60ecda541d8ecaf464498f1bc0f87bd1", + "tests/unit/test_measuregranularity.py": "455d5bb8459b35fb4946ac48a565c5dfa04d3f336e10b0e028d4b5e4b92ab891", + "tests/unit/test_measurement_axis_field_projection_performance.py": "89d96a021014c28044579a60470b2509a2b030b643ec179dbdf39ba2bfb5c04c", + "tests/unit/test_measurement_image_alignment.py": "b62069ea0f4aaeddcafbdd28228fcd1a5d5eb776098ec00d8b81676b79bab6b0", + "tests/unit/test_measurement_image_plane_projection.py": "30d3de629d27f56d2ec116b6f497e1e9efdc21ce50bf51727e35b86b3eefb34e", + "tests/unit/test_measurement_lookup_dialect_performance.py": "f13b0d3a4b2396d8686f6a1606bdfcd8f40d5f1d9566771f33536c425e564073", + "tests/unit/test_measurement_output_coalescing.py": "7f059dfb0d4e17eb3865009ee3fdb6c98656e41d965fb0ead4baeac553c29045", + "tests/unit/test_measurement_row_materialization.py": "8d6f36c3782e274d9e3aed3048e793ceb93f751c5a491519817a6986372eb98a", + "tests/unit/test_measureobjectintensitydistribution.py": "630eb3e879bd42c5a74ee64d576107f72e40efb61f67cddbbc7fb13ce671c4d0", + "tests/unit/test_memory_type_authority.py": "0da705f6f5468c2bc8a866f44057c6790a2a679d80648f47c9b373ce1af64632", + "tests/unit/test_metadata_namespace_327.py": "9c5fd4bfaa6b9a800bf3ed584d3f07785f91fa74c918717507e822dccf033323", + "tests/unit/test_metrics_memory.py": "5351747a6cc5f50d989ebb21cb160296c0d0e1a13ecda7b2a31417b27db016f8", + "tests/unit/test_mfd_preset_specs.py": "fcdaff52ff1316d3828b93989ff8b36ca554c482387feb22382f4844833e4052", + "tests/unit/test_microscope_handler_identity.py": "6fcbd57c3171152dbcd24cedac92e9225c9ad48185624d3fe9429334925c42c5", + "tests/unit/test_microscope_virtual_workspace_metadata.py": "3cc22e3ca26666e89b23e7c3973cc089e9bbbcf5411d807a463e393c0754ddc4", + "tests/unit/test_module_only_setting_binding.py": "5b8014238580560aa6ebc844463f9fe68927605da5aef9e0eaeda9852c737dd2", + "tests/unit/test_morphology_declumping_readiness.py": "ad0d4237be7ca1a8ce4fe2ef67cc386b6b8593825e1e89aaa569a5c7dd35ba96", + "tests/unit/test_multi_template_matching.py": "9a9a5de430832ceddffd4aebe8f5c8f33ffc2c737e8cf3f80d108598ae212d15", + "tests/unit/test_named_checkpoint_publication.py": "59e0fabac3d4c2662cd4780e94296ef24cc13cd37782443573e59eb01b6db3c9", + "tests/unit/test_napari_accepted_work_settlement.py": "a6248305f0d4b27b92b3eb81fe5b75a31073695ad3b5ebfab1d76071d9d305e9", + "tests/unit/test_napari_orthogonal_navigation.py": "1a67c3c15c5084f611bc43cc3d9a2f7ff4f3b00d5daacff8e98b5db1854af5bb", + "tests/unit/test_napari_render_snapshot.py": "7774b519d2a9f849080a76f656165697b57a6336e81dbc641687781be1fd6eba", + "tests/unit/test_napari_reset_transitions.py": "197c0adedeaa25343ad617106b83efe62023eb6cdee2cc8155a7d01accdbc80f", + "tests/unit/test_napari_roi_manager_integration.py": "de66b3d7aeb49b100ac5bb3c3acdf1c14afc19fb3681071b4356f6d7949a52dc", + "tests/unit/test_napari_shared_axis_navigation.py": "3be9f70a45b757e1fa729584092f022066c3721f2cdd7abc8ea55c7c1b57f4f6", + "tests/unit/test_napari_source_channel_presentation_448.py": "237122e56b7cd84f15cc7e26e3a67e997ca61fed51249b69687828e4a6127c61", + "tests/unit/test_napari_streaming_handlers.py": "4ccfe29d525db370f0f548469b99daaa608ddc75378fa123a932cd1dc2c2fccf", + "tests/unit/test_napari_swc_reader.py": "9eab981062d90f46c1e73e044185ec85b490cd932280287ce20b6442ec742ca7", + "tests/unit/test_napari_transport_ownership.py": "2b91a92a059a1403dc289744026b7dd1434bbc59790b1760bbcbb6e6b0bf019c", + "tests/unit/test_native_batch_barrier.py": "edb81a784a017ff1d65e7fedb65c0566a06eb96609c8658a45813e613b1bd9ee", + "tests/unit/test_native_synthetic_well_scaling.py": "17143a2dd3f320afd7ba4b7412c7c27b127950bed6e82e0a3ed10ae09f374c5b", + "tests/unit/test_native_tabular.py": "921c715a97276f87190535a406c369c48185e982f3f1a1fe0a4093e999f61bbe", + "tests/unit/test_native_threading.py": "ff1320c75d0318bb729a43543fa94c1ac2f1098930c2a42bb6416729195fbce5", + "tests/unit/test_native_viewer_presentation.py": "2add4f4be6f6c4cdb76b5080de04ff162ec4fdbc93f26994d6b0117550ceb206", + "tests/unit/test_native_worker_durable_evidence.py": "972af31fba20ee9bd320a455e9fbb3dfd1c909ed60d0b1fee68ef249a2d00fd4", + "tests/unit/test_neurite_cell_body_size_control.py": "d25d635e2dd22f67ec3ed29567c96724f09ab3ad4e65ab2e31b21488470ea62d", + "tests/unit/test_neurite_outgrowth.py": "a819cb4b1923223730c68f5b31de1ee8ee6a73c156f18ae70678e0761d82929c", + "tests/unit/test_neuroncyto_ii_neurite_demo.py": "a43e6e5be06ad109d8ef782468d79574f4d790ff5458613537ca9164a5481303", + "tests/unit/test_nlm_slice_contract.py": "ff995327f4134f8a616c575b77459c973f64318e70943f91e9259ac8341329d1", + "tests/unit/test_nominal_contract_architecture.py": "b9a173abb1e1bb73c19d03591cdea79fc6516b0fe30ae8068ea9d2b544256bc7", + "tests/unit/test_nonpersistent_viewer_state_evidence.py": "89b766ab2bbd908cac5d96e86ba26b1bf347a84f3a9bdc3c5926a00ac8c2c9a2", + "tests/unit/test_numpy_runtime_capabilities.py": "fb8ef2e05187df66923a684d81880cdef64b5c57101f8baa41a523c17d2621d7", + "tests/unit/test_official30_lab_meeting_demo.py": "6c4df3cb80e7d6df66843f8de3fbc76f6d3777f03abfaf99041eec78dc3773ed", + "tests/unit/test_omero_instance_manager.py": "c9341221f85069ebb465e67f28f240191a94bc690c2999d9de36212505dccda1", + "tests/unit/test_omero_microscope_metadata.py": "066e612d760bb5d0a75c5301cc6fb2d39ef460f1d7e84c67679505bec11ec7b9", + "tests/unit/test_opaque_main_flow_image_domain.py": "f6af4b334f26d12f4494ae6165e9a10f80d1c004dbbe911092632b8f8f9e8a5d", + "tests/unit/test_openhcs_adapter.py": "6d2f195f0c8438d1587ce93c7ef5fbc9255ab4a237b21da0292a7474bfc8199e", + "tests/unit/test_openhcs_measured_run.py": "5173f0b2403437c2d7d4df91e6fd9dd5f4b940e2ed0b151364acc4d207f215a7", + "tests/unit/test_orchestrator_execution_result.py": "1e96be76377459a6d035920105bbbb01fe5a029a5094f1a38942b496ca799b2d", + "tests/unit/test_orchestrator_lane_planning.py": "09c39f5cba68f8c15a0def8a09843b62d6da5c5a01e7558a9263ce4d311c078d", + "tests/unit/test_path_planner_axis_source_scope.py": "8c1f8a0b581b18ba08c4cfc7fe3e57291f639ca91f2e344af535ca628573ec08", + "tests/unit/test_path_planner_materialization.py": "d81e3c1d88347cce21c3b5cec83a4935ef0ef0b6d0b4486d3cb0f538ebce7565", + "tests/unit/test_pattern_data_manager.py": "3a1edfbd36a9979a9a3609966e52ac39778247bb0b0ed40d1d7f347b3a9a9845", + "tests/unit/test_persisted_image_metadata.py": "625cbb171a283966ee4b2f2a3a81a4d33d8671700fbe375c9dcd0a5ac49459e3", + "tests/unit/test_persisted_roi_only_publication.py": "ad3ad726815f5c4e53ed3bbc37c4abd8da0b3a20b1af6a9647f3b9f7e96858b1", + "tests/unit/test_persisted_semantic_metadata.py": "31d8cca37ec42f3c0614c14612f351d86121decded5030d84f9401f7bd89d75c", + "tests/unit/test_pipeline_definition.py": "4e7f2bbf9731bb1d2430e175f602827d9049645e4480705c0c7667536068ae8d", + "tests/unit/test_pipeline_demo_contribution.py": "695f8455a748e771496734b96fd2637ac8822cd0a177452b9cdf63daaf4e3c50", + "tests/unit/test_pipeline_document.py": "2c18222ac2a34ee267788063a374112b17874c64693bfe542669c4183bc90c36", + "tests/unit/test_plane_nlm_declaration.py": "c39de413370bb3a8f9821475c0fcbbbafcb47e21065d31eca799c7be353d0712", + "tests/unit/test_plane_store_sources.py": "792a832934e9e25ddccf08796bab4ed5c519212f158af9915226f84cea08120c", + "tests/unit/test_plate_image_inventory.py": "d8af910e4e02afd9ebfb24a78eda8a3509c0eec7782feeee85097ecebf09c041", + "tests/unit/test_plate_manager_code_document.py": "23d08a4b3f3726a465d3f78d921788b951e98bb6611a173e709ecdc2c5254363", + "tests/unit/test_polystore_disk_symlink.py": "546520ff47ba2f1f39a66b0afc0257cbaf2053b169e76d1074327a1b114ef597", + "tests/unit/test_preparation_admission.py": "c67492edfd75d408b40661a873cc05940f9a1597c4f660abca1e4cec86f96f9f", + "tests/unit/test_prepare_blind_neurite.py": "255ba56feedf16f8805ccd3a42440f7ff60a29966fc081886c9b345ae127086d", + "tests/unit/test_primary_image_carrier_compile_gate.py": "1e3b1f10e7ed156a5d941b9f9c0e5bb9f5dfea310829d5d6795777de1546978d", + "tests/unit/test_primary_object_diagnostics.py": "44e65892369f02f34fbbe912ee5f236c5a1471c2c47de5186a7a04297e217e4a", + "tests/unit/test_process_environment.py": "eecab72b95e6595c31d58e8f970b1260b191fcd2a31e353e1e6281527570a894", + "tests/unit/test_process_local_cache.py": "29e89b99b665ba272ea93c9b5b3595b7236eba5494050dfa8f30b450b1407ff4", + "tests/unit/test_processing_preparation.py": "ccc9523dd0092483a66e8ddf2ec186cf388d1741c64fa4073a05cb3bc415c027", + "tests/unit/test_processor_method_strategies.py": "4c3172d38136b0961140501fb4c86a92b583c2c66ed35ee8ff94010381782f9a", + "tests/unit/test_produced_plate_source_selection.py": "d99d5f139cdc8fbbd16b955c83daa62734e085edfe8c25245125a14aa1eca928", + "tests/unit/test_projected_image_output.py": "e9d2cc9c869d8988050af1662d8ed78e925c0a801042676d9f2c4bec89d91da7", + "tests/unit/test_pycodify_formatters.py": "345fdfcf543593a4db7217fa7fde143da8510604e96c34e1a0e98eda3f4802da", + "tests/unit/test_registered_configuration_source_readiness.py": "e8e5da26ee482820375dbb756b6556e6d5ec9332661a4a8e29d51bc7bb1b9523", + "tests/unit/test_registered_object_output_boundaries.py": "9780538d98121512da19fa0b7ccc7cc0ed6a5e478e049c17714943fe0ef6e0b7", + "tests/unit/test_registry_owner_iteration.py": "3e042f8980dfac25cf7e27d42f386fc6a63329d4ca7f21348a21589ab56a3046", + "tests/unit/test_release_readiness.py": "72188f254407172a68d149f555b78b3eddfdb8096fb4c4337633c17090343b6a", + "tests/unit/test_repository_layout.py": "e7bcf714d4dce65a0a7abac4eb2bac507cfce79dd87986b3a04f747c43136ecf", + "tests/unit/test_rescale_stream_plane_declaration.py": "f63145674a5ca1885549a02169c3c4c32e646b477fcaeebcbbd877ca8007a967", + "tests/unit/test_returned_output_abi.py": "a0b33c98529e5705e3ac823661746e280d9cbb95c149ecae8a8faa6391232452", + "tests/unit/test_roi_source_metadata.py": "496035ab1ae0f0493c712cbf36a05584209f0680f6aa247566d0fa47190b317b", + "tests/unit/test_routed_channel_selection.py": "f0f7e10c99f6568048817f38ee10f648bb0b6fb4945cc0fae0f988761557745f", + "tests/unit/test_routed_composite_execution.py": "009568bcaec6b6dcfc4e88e1b448254fcf70f8a93aa37fc9ce56392057870fc0", + "tests/unit/test_runner_cellprofiler_compatibility.py": "ad6045ce7bcf4c89f781d8dd70d7e44310d89e51d73181256e5216f26dac2335", + "tests/unit/test_runtime_adapter_source_artifacts.py": "1f630506b2edaa13259d26d063cf08785f3ca385ea662a98212debe7df5bd2c7", + "tests/unit/test_runtime_artifact_queries.py": "91d9748d93994b9bd69f9969b3568fc0dea4aaecc3cc71c2b996963fa68bc904", + "tests/unit/test_runtime_callable_resolution.py": "8e91bdd459730fbb3a1cc4260a5c8d57f7448050ca968a44657c197ee5fbc798", + "tests/unit/test_runtime_equivalence.py": "e7120255a4374a7ef053a1a4e529d241cc74f2616878e6c691ce282acd4b67d0", + "tests/unit/test_runtime_equivalence_package.py": "b1f34bf57809c77f41c9a201646865f419d093380f52454bc9273a8f978a403f", + "tests/unit/test_runtime_execution_validation.py": "0891faa1b82a2aaf12d68f944749ef703211d41fa79d477f8db60f4ea4941014", + "tests/unit/test_runtime_exports.py": "aaee3cb126bd6451d56ed1ad1e4716bf3f7b2cff5041d8650ea41175a83b2017", + "tests/unit/test_runtime_image_singleton_alignment.py": "970ba88fa84cb86476054d0249f9fadd165147cc759365854a32d01376d5b4d9", + "tests/unit/test_runtime_import_authority.py": "506112f13a0dd8162b6e36d366df4fc92e7f604ef2fdea34de8e1790460925ed", + "tests/unit/test_runtime_object_label_domain_transitions.py": "205cd3bd4d7cc5cdd330d8bf87d896548c6f087fbd79d4413ed73dff3a221192", + "tests/unit/test_runtime_output_matching.py": "0c20cd4561371aa1cd2a4b12e8956710e7f4e39728bd6ac89140b249825fd9e9", + "tests/unit/test_runtime_profile.py": "221168e880919fd80097c30f10d1d2c88d5cac647b77378d1012efada8f811b1", + "tests/unit/test_runtime_semantics.py": "f55fba657fc709e558966241a2792971bad6b98ae48a265c4c5e0b0350beb4e6", + "tests/unit/test_runtime_slice_projection.py": "68a13ab84f9718048cc6952f48844c8cfd6fe312ba30c45c65402fe491338faa", + "tests/unit/test_runtime_source_resolution_snapshot.py": "5a0c62e3fa6c305e7a78856c94bcc9671037d68585b5f9ff17cf6a690563c396", + "tests/unit/test_runtime_sparse_labels.py": "2adb3d87b83d564bcbda3cce6814b5a09166f2e14307007503fac330ce5dbfa3", + "tests/unit/test_runtime_stack_cache_lifetime.py": "a63226b25afc995817b496c1cf153ddd324786cb5e746c49399082b0ad2f573c", + "tests/unit/test_runtime_tabular_values.py": "ac6370eebf512011d27eff03f683a816e017461b472709ce36c662ea8af24d64", + "tests/unit/test_runtime_value_store.py": "7f565fb8385ef7ebcf6d604738f29fb7dbbbc275de040c7cdb46bd1b39efc71c", + "tests/unit/test_runtime_values.py": "468d21c5d886176ea8845faa8551eb6f8a3c37da7b551b673f2cc502fd98c138", + "tests/unit/test_saved_source_label_plane_domain.py": "82223582d595a6e1892a5c02d8902addcf567ac3769304164c27770c539290e8", + "tests/unit/test_selected_plane_stream_materialization.py": "19cb73b4731974fd4bb9f6bba2d83f3424231eafe31382896ade9321793e1bfd", + "tests/unit/test_self_supervised_stitcher.py": "724b8666eee39c4a8631da09a94cdebbd0ceccdcaf49f90ffd46f21ff7cc8c40", + "tests/unit/test_settings_binder.py": "5b386f648fa38345eb9c27a930b92d387ef4ff0db660ac11265aac6c1adef2e7", + "tests/unit/test_singleton_rgb_label_invocation_477.py": "4087354457872cb05c1584cd373a25fe7207a4a77598f162b6ced4cdb8d2dff6", + "tests/unit/test_skan_axon_artifact_contract.py": "857de9a98ac74287873dfc26f18d86faa3591f6b5c07bc31801368e2d65f9e1a", + "tests/unit/test_skeletonize_and_save.py": "dd4a38dd043930dac28d6bf7f290cb3060d0b4a66a8ecf20b0a1e5a6234c37ea", + "tests/unit/test_smoke_installed_gui.py": "b8d1a1fcd67c58e2b2749262b44c1dcccbb2e72a0d299d180823e94a0285ae9d", + "tests/unit/test_smoke_installed_mcp.py": "141ba0d21d70791f824ffd1362e6ca84f47fff0d7b18dd109086d6ab5f8bdb61", + "tests/unit/test_snapshot_deadline.py": "66f461fe8457d41b6a7acef0e3c35b89f9cf73c543ac32cc3e2233ce70345ceb", + "tests/unit/test_source_binding_path_admission.py": "eb69e9097a03ecbc55c6aa329f26e83a321bbac3aabe3f60a106a5088db39d21", + "tests/unit/test_source_binding_workspace.py": "69eedf76a5c3cebaf1cb27ada78f00ba8b3f362cf1d31ee982a585841189232b", + "tests/unit/test_source_bindings.py": "fa17e812b265cb2c9ab38c67fa62c404438f704c21fab534a55de720c5363245", + "tests/unit/test_source_bindings_view.py": "ce59871d9820abaa052f25e4cdd8aef84377dc1c43079cb7c99ff35c6434cbcb", + "tests/unit/test_source_bound_matched_image_set_anchors.py": "5c5f98733afa7158c179558759c06033d01d61611dd532f7694ddeeffaa92127", + "tests/unit/test_source_image_provenance.py": "3fc711c7600f65213a4b4e150a09d4289d506dfa669ee1c38e523a3bac665835", + "tests/unit/test_source_image_provenance_hotpath.py": "3539d62d0c1c0706257f8cca8c54452f063b0e70e479976b45b6250621ae34b9", + "tests/unit/test_source_image_set_identity_policy.py": "f106cb03132c41485c6c3dc065481abe80eac0682d25fb3a27cf0e1be093832c", + "tests/unit/test_source_matching.py": "6297d7ba9bb96634122703a3d5d750428080e3894ea92901ae6b2e688fd5c78f", + "tests/unit/test_source_metadata_live_mutation.py": "83b576aa30cd8d8e1a35856c0555406f0dcdc2089f5fc8b3298dbd3cb44e81b6", + "tests/unit/test_source_metadata_owner.py": "e04a9cbaa99775908053421a90c8bbf852dfce5c589842f7583f59f956482728", + "tests/unit/test_source_path_factoring.py": "280b4aa846a37a171dda304b60aadc9f6def826c481e309ae26c613c8a63146e", + "tests/unit/test_source_projection.py": "35ccaa08f3c588a48dbae1eb3ad1bcb2d43fa7bb58592dbf9b0957a77da6cb5b", + "tests/unit/test_source_projection_family.py": "21be7517d3019906131e678a00c520336e9c0150062bc389666c10f5bb1d1f41", + "tests/unit/test_source_provenance_constructor_defaults.py": "e9a397292cebacce5554fedcef29aa5d20854cc57cfdbbe8992aafaedf04f964", + "tests/unit/test_source_spatial_domain.py": "a6f9110452f0a53509dcbc99aad3cab0aec7a1247f0aa59d5c31cf8c4f419e68", + "tests/unit/test_source_tile_geometry.py": "cfef1d2cbfeb4dcfdc57c666e96afbae5e309c4dd809c7fb82112c2c22186f8b", + "tests/unit/test_spatial_graph_materialization.py": "924dbef35f2726996877a8bcc3d58c58f5201a71efbbfb8932a77d72594968a7", + "tests/unit/test_stage_ci_candidate_version.py": "da9cd12d8abbf11497af1c74fc028a6a8f32b25c26ba5832e55266a47865b309", + "tests/unit/test_step_snapshot.py": "463a665e5d542b7a0f6d822f4923b52bc78e2009b2d47b82dbdd23bd043f298c", + "tests/unit/test_stream_component_semantics.py": "a83a41518a708065c8510c17aefb7556f00f9ede5b4d0dab8cefc131543ccd3f", + "tests/unit/test_streaming_config_factory_registry.py": "2c83cb1099389db4b522e3b1e1d994303a7b9cbd64558a75a120de5ee7e06387", + "tests/unit/test_streaming_service.py": "0bee83a7a3746b0399a1cb1b84347cf645360db2411f051f077d7e464c88d76d", + "tests/unit/test_successful_job_baseline.py": "d2e7cb0d1595ef16f8fbe5130fc352e644dddb3ec6bc4bb66b664551a62ccb82", + "tests/unit/test_sync_mcp_release_metadata.py": "a51aa3eaae100c9de985064188712057bbe8d374453028a56a3abc9049401f34", + "tests/unit/test_synthetic_generator_randomness.py": "6ab3efd7f4e083744dc4dda26d97c60b6a7649dd44432362020027b92271717f", + "tests/unit/test_synthetic_generator_zero_geometry.py": "13ef351e9dcee3e25dfb99adb4ab2b08486c2c08a7303a434fe68098fbad6ce8", + "tests/unit/test_synthetic_imagexpress_bioformats_compatibility.py": "f8db87da9fe7c38474b55092a93d751b492e7484dd6ec6750eeecc0c0b67e96b", + "tests/unit/test_template_crop_pipeline_execution.py": "00cb57094299ca9076a17fed870c48dcf783be601049d37250d216ca934eaff6", + "tests/unit/test_tiff_metadata_mixin.py": "4b51833fd7ec8b2e87b97e94c31eb3b947222e74e2296a70b0927b135587accd", + "tests/unit/test_tiff_output_compression.py": "458f576e545a033ed9e23b9dc349ab92c19d296ec7f0e269ea89884cd9b82942", + "tests/unit/test_time_travel_navigation.py": "930254f1a6e953056542b22367bb9b5631c529694709fcc001af3891d94be91d", + "tests/unit/test_ui_zmq_config_authority.py": "006275af836076d84fca50eb5e17df3f9ac000489639aa2e9ea9c3eb93847310", + "tests/unit/test_unified_registry_runtime_payloads.py": "decd7b16e59a1c960af0d16ca56da190b3e9c25d7984bda7129b6c8796eabdd9", + "tests/unit/test_validate_documentation_audit.py": "3934174a30ea2b7043e34809e2e71f967e5ef11c554f65a21f2517e9dbef8ddd", + "tests/unit/test_validate_local_release_floors.py": "36b9e9aa7a4d7ac78c7eaa229750582a025ea8bffe43e3b2ccd2350fe496c242", + "tests/unit/test_validate_wheel_deployment.py": "7909c30f7afc321f939aebf52c5c603e153cfa13e869492e0538f78dc632e145", + "tests/unit/test_viewer_channel_display_metadata.py": "0ba909bedf02b4b2ae27179554fbf0a1caa9ad9ac35bf10cb0b9f16d705f596d", + "tests/unit/test_viewer_feature_measurement.py": "21e716f82e6dd57be71049533ae317eb0b9dcef7163c3dd81cd4480383a6cc99", + "tests/unit/test_viewer_listener_binding.py": "f85871e6faae54781fdd9810ea6b999ed1117b3d465be75efd555c1b9c296470", + "tests/unit/test_viewer_protocol.py": "a6b78dc4b899c9bf4d89300a6518f6a915b27aacc78fbceac3288f9d2de187ad", + "tests/unit/test_viewer_receipt_replay.py": "be7662bfbd4901d6bea915a482f5f5739fc36a93964ebf87d8de33a983c7222c", + "tests/unit/test_viewer_result_element_selection.py": "1c8e1405691cd36926272ebf2463917c6a423c21cae6549fb3030682b1766dca", + "tests/unit/test_viewer_source_projection_chain.py": "8570aea44a3b6f1f44d10507c68813bfd6035576db3c339bb245b78c88629555", + "tests/unit/test_viewer_spatial_sample.py": "4bcda378eb41c3f9e457af1b119b4eaca2a535b4025a635f949dc761eac7a989", + "tests/unit/test_volume_projection_fixture.py": "9488604f53b8612083550cc952d26ea070a7167254ef610b78e6b23679b2bb5c", + "tests/unit/test_wait_for_pypi_release.py": "cabe65c08b8a9d1c5e081f597fe53bbec95c36650a2919a1992d62368096441b", + "tests/unit/test_well_filter_processor.py": "75456737119eaa40ec42b68e967dffd0e99bf28408a1c792963f73b6eb7979e1", + "tests/unit/test_well_throughput_scaling.py": "847021e10de08ad058fe5f9fbbdea8969f71abb87a2eb0907e468f95f7d12ee1", + "tests/unit/test_worker_profiling.py": "f8c103830086034020aabe8582229cf2606d0ccaa1cc212443aca8a4b1600809", + "tests/unit/test_worker_start_policy.py": "a3eaadb10898f10e50391310d65d338d60e9df2f4f6dec8b791be7dc75d31fc9", + "tests/unit/test_yeast_output_identity.py": "e1de14a41cff2cf49dd533da158b96cff7e0cb2139a3ea8b1f3957e6f4af1a1e", + "tests/unit/test_zarr_config_owner.py": "ae63cc5cccbd4276d59db048e3ecc2bc4143992b9f18f7082925518457bde609", + "tests/unit/test_zmq_application.py": "344cb50749a1e737c15078c467c2964a9d27bb8e85ab776588af51edfe937119", + "tests/unit/test_zmq_compilation.py": "a2582fa332c45a4cfbfa1f1b935c0cf87123aa140e5f5289173ac73e289c8b8b", + "tests/unit/test_zmq_compilation_identity.py": "4bc34f8d56e66a4289f4793a424b41297aa170b9654caca5ff5f15a6bdb88262", + "tests/unit/test_zmq_compiled_pipeline_run.py": "a63cc83cb4815b428840ba1b6a6c86ed2831c1204eee23d19726391307fb22cb", + "tests/unit/test_zmq_execution_server.py": "a2854a09486efeda402470a430d284ebb4979273a11e83a68feb5c59f0e6bb54", + "tests/unit/test_zmq_execution_server_process.py": "a3c9a5c03de440b49dd8575879d01514dffa0da32cbbfe15c0d363711685d964", + "tests/unit/test_zmq_orchestrator_environment.py": "ec2e38ee21c1e87af2212184f2275b8c895510dec2c1faf19d49c4c0b1014ce4", + "tests/unit/test_zmq_port_pair_authority.py": "a00dcd470b62193ee7811b39228a4ce3dbfd8e9ee5a198f79f386892233f7cc2", + "tests/unit/test_zmq_progress.py": "66da894d0de1f37f6aa7aa21fe671a5a2b3328685b919932c89f5696cfe05093", + "tests/unit/test_zmq_runtime_outcome_export.py": "038c77fc01fc693149fa3fda72e2caa1f98466c69caf303671474e9c49ba9a20", + "tests/unit/test_zmq_submission_timeout.py": "b525a1cc113bdae987c7f202369cc251ca1129188bd8d3adb834aec44f4c8393", + "tools/reproduce_issue257_filename_identity.py": "43143a3495fe98b43d5e212ad3d6b80a65fa1ed1b4803fab87f693c79306163a", + "validation/MCP-PTY483-SOURCE-CHECKPOINT-20261002.rst": "20b28ff7ff9698a77d71015ff31a5bb357d074583a91dbca61455efc1e27ca23", + "validation/PAIRED-MAIN-FLOW-DIAGNOSIS-20261001.rst": "bab950bfb95b95109081f970f726ba2dcfed6a7379d7279aeb0af5ad2f49331d", + "validation/PUBLIC-PAIRED-388-PREPARATION.rst": "99d458e209090a07641ec1764530f4015d7132dbe822801b56eeec1e20bae6c3", + "validation/canonical-custom-379-evidence.tar.gz": "a6c1f060383e218ece3fcb4580dd59de64967067ee38e50c0051bcde4af62b93", + "validation/canonical-custom-379-pinned-r0-final.json": "c73f7ccc65c0cefc9b109d8818f9bb9ff74df847c37fb4770e26b16e3c46cc25", + "validation/canonical-custom-379-published-head-controls.json": "a8866b87be7922211574ba94847e19e298df1675551cf9211c663cf1814436e5", + "validation/canonical-custom-379-published-head-controls.log": "a84322dc71bc1261493a30646f7164816f18d20f042a3abb01cc1a9a53db49fc", + "validation/cold436-consumer-closure.log": "c217208ef101871c457c808a5f8ae43d8377c77c91caf8d48550370a501ec60e", + "validation/cold436-inventory-closure.log": "84d079e1c458152fa8f527271626d8e0cc82fc7626f5ed567dec51c9c79c77c8", + "validation/cold436-pinned-r0-corrected.log.gz": "d5aacf02d5bce8783706e16886d21431ead961f4a889e503c8f4848e3637e6d9", + "validation/cold436-pinned-r0-first.log.gz": "71f0a2a59e2ea6cdafa73cb968dca44a9d8850362288a132585381667819f3f6", + "validation/cold436-real-compile.log": "ae288b1bfd5b07fca515c0919a2dce9fb865fa3a769dc9228b41e5a231160ed6", + "validation/cold436-source-controls-first.log": "d1e6f693bb95a5b7f0ca7bda2ae6ab44345f9d4d87a235ef1d0d3a0e9f9f1830", + "validation/cold436-source-corrections.log": "a261b9da61f7488c96d049c63106e34a66420600e9d5f69793e8da7ae93a2dc8", + "validation/declared-callable-documentation-461.rst": "bbc2db1be49d157b4d83029ecccd2306938294e8fc72148df925b1b0e6f950d8", + "validation/diagnose_public_paired_main_flow.py": "7fdf1826f5be36c427deb3d96e9168793e430489c268d6bf246e74fb1db5c528", + "validation/doc461-integrated-raw-nra.log.gz": "8e5f75d16e2ff2337e4cc6f829ab8e832945f1ca79e22bda38024bb6275501bb", + "validation/doc461-integrated-raw-nra02.log.gz": "2600bedf023afeea6a0e9b556b0c6ea329f6f7e11ce337fe6c41a6f3d08364d7", + "validation/doc461-owner-after.log.gz": "8fa3ac5073e4d1ae4e03be6c4cdb90323ec1834f9118d2fb733bb48dc4b4c01d", + "validation/doc461-owner-before.log.gz": "4e5d3f2705e541f279839d79cb8252b9157a628a471eca811526eb4640566857", + "validation/doc461-pinned-r0.log.gz": "f1027c6709765fe1a2c46384480c0dbd7550abc02176416b476c46c796e5aaf8", + "validation/doc461-pr-body.md": "ae0145baa4f4a5c5d685f97a043e4f53f90df3750593510d30edc27c1251eb4d", + "validation/doc461-source01.log.gz": "fa2a44ee97f7d3b72bd6391a3d30d334e16b7260e7b01d92bdee378a11e4c7eb", + "validation/doc461-source02.log.gz": "30d8c13921a65187d51e72b83e9eacfb9066161f6c5b830f83a55674d3dfcf72", + "validation/jvm455-lifetime-first.log": "1cb82755f77146a331ad7411ab41410c759558f7c8e38b6929a6c2f95909f37f", + "validation/jvm455-owner-before-corrected.log": "4164c5f4c38d618c82f39f4df1f2289a6e56231a142575b1aba4e379d297af5e", + "validation/jvm455-owner-closure.log": "1fa7f60934f02686679f06fb618c1ed619779786a73bd1dd1b0d2ce12a8dea7d", + "validation/jvm455-pinned-r0-first.log.gz": "0446c3f01995ffcf0f725e1098ec63c45563d0571d389ae9bac82463b8985f50", + "validation/jvm455-resident-first.log": "e8cfe1eaa765e0d04646e713620ff6a4e54eb4773a315fa56256c9abc978caca", + "validation/jvm455-sdkstdio-corrected.log": "96c9bc2080c88108eb6df456a4ab21dca408f780b6c6538a5e64021b84a50ec6", + "validation/jvm455-sdkstdio-exactsource.log": "0cacbb1f87050fa1bf5dca4ecb5b21c83dfd6f460a8bc2868c05e81ea96aca49", + "validation/jvm455-sdkstdio-first.log": "9dc393702998ffcc435b04b47ce8ba692d9d5a23ab1097053aaef72d6de3ef17", + "validation/mcp-pty-owner-after.typescript.gz": "be5cb3674d0a215c4bc63216c64a0b1b799cb8bf27237742ea280110b6183f45", + "validation/mcp-pty-owner-before.typescript.gz": "d4fbb9fd7be36cbac5275470d7b7f94ee867b1a6cf7de2db311ed660f3abb1e0", + "validation/mcp-pty-pinned-r0.typescript.gz": "a59f3202fdc0346174ad755cde5ad45b6cb6191f13dce94a97a2e58c2f39714e", + "validation/mcp-pty-source04.typescript": "7616cf69e817fb0c5a60e3ab82e1f2f72a419e7c04b34912562abb335895c960", + "validation/mcp-pty-source05.typescript": "1386fac4a4837a18fd277924783faf1d065aff1e7a4fc1eeb2cfc322979df06e", + "validation/mcp-pty-source06.typescript": "db57a67878cbc640d9326072c2ef9b3878e7ee879b7a4ced8b8fb8bbe219cc7e", + "validation/mcp-pty483-pr-body.md": "8bdd40cb25f013b1bde26e31e126651f19b93fb865bcbe4c7cf6e551912ef38e", + "validation/mcp_shell_pty_owner_trace.py": "f49d4ab35ffba77ff89d3549b0a69dac666efd9fa66f4824a3230161c0e9ecc7", + "validation/original_377_boundary_witness.py": "ec9ce0dac89679eb922afbfe5ef1aa0c68f85a28d3273a90b634a7801b05f4d6", + "validation/plane-nlm-458.rst": "e9d99efaa9d2001cee59b9b9cd880a4fc5761ae91ac4a987a7763126ff0ec712", + "validation/plane-nlm-issue-body.md": "79c90e4bef185887835bb7fdbb8e82ffc0ad2ceb0f34fc44d099c20dbc7cfdce", + "validation/plane-nlm-owner-closure.log.gz": "1bcb940506f8c3794b4ab950c63b31e439f8466ec70367b4b5888c8dafd3a756", + "validation/plane-nlm-pinned-r0.log.gz": "62efa48e5ec58f3cae262d41ae9a0d154f0020e6d3c1bce7aa73c34695102794", + "validation/plane-nlm-source01.log.gz": "033f426bad294551d1ed5753b837317e6ec0b69cd29cfc8910270fda07cf29e2", + "validation/plane-nlm-source02.log.gz": "bf4abd387a5bc8a49b15cfc49af8b51da1073f69a737176a11f35742098aa88e", + "validation/plane-nlm-source03.log.gz": "4147cb07dd252c2c19645179653d389ec3cb5ce5b07e6570c55f5b636fd17e87", + "validation/public_two_vector_388.py": "d3f259fae72ed5f70117646befa6b68a8301e168d785a7a56159cce1726bfd23", + "validation/r0-fixture-470-artifacts.sha256": "f6797163f02b3cf3ed78ab10e1e00ce0072230cbdfcfabab84e36be5d18d7681", + "validation/r0-fixture-470-evidence.tar.gz": "42be36c171b4e9059011c9da000de027d99f2f81c4084a4f90b046b7d6745ed8", + "validation/r0-invocation-ast.py": "12643290f7c763a7e65baa9317248fc6fae3e7741ea3574d96b45922146c0d43", + "validation/reproduce_saved_label_distance_419.py": "7785f09300db92851bc707b702b4ac2b9addeaf1f2aa42afc57ef9bace299d30", + "validation/run-s1-sampling-source.sh": "61ecabe4da3bbe4670e58c6eb4c2b827ccb14c8aefaf4f700604536fd051a4c9", + "validation/run-s1-sampling-tests.py": "eb25f0b32797eb6acb4a6fed5d43f234260ea3fc918959f6250df500519c730d", + "validation/run_pinned_r0_419.py": "ecb4ae52420f53ad2879a2736abd330eed3f61826cad01539cadd9c686f086c0", + "validation/s1-sampling-evidence.sha256": "28af798b073bde4b4f45d76ee83c2d8e962fcee09a0fe5cdf1a39a4ad606e16f", + "validation/s1-sampling-logs.sha256": "15ca69ebc9c58fd7a9e775b9db735fe7a1e30536fe9172def44789246a6bff83", + "validation/s1-sampling-owner-trace.py": "ddab2dd90c1d32282e15e5950006e0c1f02be209f3f412e5e6e36363aa7127b4", + "validation/s1-sampling-receipt.rst": "a758d781091eb75f15d833c86f5202457410b0fdefe2c6ae701cf8361ed14d23", + "validation/s1-sampling-source-evidence-20261002.tar.gz": "bf42c8b7a6d081e8e0d666b5f03e2ca2c9f3f61e39bcba681bcc2238c4bdc455", + "validation/stream441-first-source-controls.log": "597ea021a676c7e8186473b801c9914701a64f5e8c3dff98ee57f057e4f3b8a9", + "validation/stream441-fourth-source-controls.log": "b585023aeebab417f208abfca318f68e7bd27ecaad0487b91b0ec49b288e2abe", + "validation/stream441-newcase-controls.log": "32b9bcabe6483404085ed301e6300ca890c8aa1747c9978186d5bd44aa19f338", + "validation/stream441-original-controls.log": "876071ddcc1f933d4e27ac6d6ba3385389921abbac21d13b4c83eacc9842a4d4", + "validation/stream441-pinned-r0-corrected.log.gz": "20900bb6f32fa3a177f5b51979ba796e5bef30664686b90d6bc3b019e48cf1f9", + "validation/stream441-pinned-r0.log.gz": "c96accf6b7630769f9e1d2b4e9743b06454097dd515e0819b6d204bd9cf0bd06", + "validation/stream441-pr-body.md": "5f78b59407c62780c35a23a0824a758d4fa90d62316516e16c08df348e36f932", + "validation/stream441-qualified-source-controls.log": "0b548cef61c56f6c5a2a71093a2fec4626a8c5d9a1d019ea3df2204cf51ee50a", + "validation/stream441-second-source-controls.log": "432be31353bae57dabdede4b50c747bfcb66ce38fa88a2ac5a28390a70ccd86e", + "validation/stream441-terminal-controls.log": "513222a264ddd1a58f389dd8d095f1729c1e55c6c886cb84f459b360efcbc798", + "validation/stream441-third-source-controls.log": "aa24d25eed6859c288eadfdeeeab3b30810f08389bcbd128a41aaaffbc0fe001", + "validation/test_public_two_vector_driver_388.py": "3c7e3fc895ab1f169919a3687729d219f48811372c56d350e4655250706c9d12" + }, + "sparse": { + "missing_tracked_files": { + "docs/validation/ack_runtime_parent_20260930/checkpoint.rst": "bac28565a9c604286a1224b584376d4e8d01af6d", + "docs/validation/ack_runtime_parent_20260930/mcp-paired-session.log": "037a2249c1e3ba1b31d0190d07d47481b1d80a21", + "docs/validation/ack_runtime_parent_20260930/paired-source-final.log": "51e73ab105db89df4852a9bef46c16e0ab867f06", + "docs/validation/ack_runtime_parent_20260930/pipeline.py": "2128b9a1093b72b5af85547444d894aabadd41f6", + "docs/validation/ack_runtime_parent_20260930/pr-body.rst": "e03b5203490a348a725ee4aae05c7d1b7c3c2fc7", + "docs/validation/ack_runtime_parent_20260930/run-candidate.sh": "d17fc704fe384e33b9a9752f85dd019eb462d36c", + "docs/validation/ack_runtime_parent_20260930/run-source-check.sh": "a0eba8c3c19ba9575d5497f91d87a180ac67b5f7", + "docs/validation/ack_runtime_parent_20260930/viewer-followup.rst": "19d9c3136cc7466f77ac3aaa9bf121de52d71fc0", + "docs/validation/artifact_fed_parameter_declaration_contract_20261001/ISSUE.md": "777e290f08e6ef8062d2ee50a99bfa80fac08671", + "docs/validation/artifact_fed_parameter_declaration_contract_20261001/PR_BODY.md": "0d717c0157192b1192b228e87679ed3e82cb1c47", + "docs/validation/artifact_fed_parameter_declaration_contract_20261001/RECEIPT.rst": "8c0e139813c44d970a436dd4927d58e735a0fbfb", + "docs/validation/artifact_fed_parameter_declaration_contract_20261001/SHA256SUMS": "3e0cbf0d8d46cfe13e88e9ed628e8e61d6b1b3ee", + "docs/validation/artifact_fed_parameter_declaration_contract_20261001/contract-pattern-shard-resources.txt": "a2ccf235d6fc9db22c8823ac4ccfd7741535c4a7", + "docs/validation/artifact_fed_parameter_declaration_contract_20261001/contract-pattern-shard.xml": "4f97ce6b4117a00d1db716de0cb16b6039a2f2c5", + "docs/validation/artifact_fed_parameter_declaration_contract_20261001/normalization-green-resources.txt": "41cfbbe5893eb1ec417dc2aa627b0408b591ada4", + "docs/validation/artifact_fed_parameter_declaration_contract_20261001/normalization-green.xml": "0a41076760412395c5b8554d3af25813f73d0a0b", + "docs/validation/artifact_fed_parameter_declaration_contract_20261001/original-red-resources.txt": "d6162fdf26f32db3ddf8d29e14814d60d12ae0eb", + "docs/validation/artifact_fed_parameter_declaration_contract_20261001/original-red.xml": "134bab6b6a2f87a5e65177364208f4094c273cc2", + "docs/validation/artifact_publication_315_20260930/baseline-import.log": "e676c882e0fc232337c3687c48733dcd2636d957", + "docs/validation/artifact_publication_315_20260930/baseline-journey.log": "4eb316bb099a23eeaa6ca0c535fc73f955b6d45b", + "docs/validation/artifact_publication_315_20260930/baseline-journey02.log": "82e2c84d5282a9a6541767c0c4e7cd8cc3615259", + "docs/validation/artifact_publication_315_20260930/baseline-journey03.log": "8a66c38c8a742b036cdbc08cc542c3f3fd6ae9d3", + "docs/validation/artifact_publication_315_20260930/baseline-journey04.log": "d239d145d5b338de7015c5e623d987f6f86dc57f", + "docs/validation/artifact_publication_315_20260930/baseline-journey05.log": "608babc22f0958f50ade7ad2ea9ed9caff02bbde", + "docs/validation/artifact_publication_315_20260930/baseline-journey06.log": "255ba5fc2389ccf938d81af34f761cab8c13c11d", + "docs/validation/artifact_publication_315_20260930/candidate-journey01.log": "d19cb7cd08ccc7487f59e963a962c7a34c023fb3", + "docs/validation/artifact_publication_315_20260930/factored-journey.log": "114cee3150450fee197713ec080311151fee7cc3", + "docs/validation/artifact_publication_315_20260930/factored-journey02.log": "ea900336f138dcf067908eb1def6706ac457bcf6", + "docs/validation/artifact_publication_315_20260930/factored-owners.log": "ecd6d8a18eea06fdab7cbe7a3d627d5977691dc4", + "docs/validation/artifact_publication_315_20260930/geometry-final.log": "eab547cbd1fcf0cef149c8ef69d44cfa1027df79", + "docs/validation/artifact_publication_315_20260930/grid-triage.md": "b6b1c8639c8653dbfba3d8ed74eda6cd32209419", + "docs/validation/artifact_publication_315_20260930/merged-main-source.log": "6c5cd1f01778b8e5399394a689e76e2a1616bd20", + "docs/validation/artifact_publication_315_20260930/named-checkpoint-ownership.md": "ac44ad2931765a807c464dd460365bf50122431d", + "docs/validation/artifact_publication_315_20260930/original-native-failure-artifacts.tgz": "60e3ee7e4e19b5bc22d3802be1da44b2fa5b8f66", + "docs/validation/artifact_publication_315_20260930/original-public-journeys.log": "b7c24cf7c00d11b50fe1f4b8db452e3b1c33f354", + "docs/validation/artifact_publication_315_20260930/original-r1-bounded.log": "a4be9a30b4cc2ebfef36aba0dd9530986b15f3e3", + "docs/validation/artifact_publication_315_20260930/original-r1.log": "27821a68023dcfed8bd3adaaae012c4b1f49d331", + "docs/validation/artifact_publication_315_20260930/original-ratchet-benchmark-resources.txt": "80a404400aede62ed3c0307ed0e75483c59b6989", + "docs/validation/artifact_publication_315_20260930/original-ratchet-benchmark.json.gz": "045dd9c8185888dca3f359f9ad6682afcd591293", + "docs/validation/artifact_publication_315_20260930/original-ratchet-final-resources.txt": "b049cb85a7c83813b6ea98200373c3b113b8a216", + "docs/validation/artifact_publication_315_20260930/original-ratchet-final.json.gz": "8fed95cc2f07c7a29c66c8d316830f51972514de", + "docs/validation/artifact_publication_315_20260930/original-ratchet-initial-resources.txt": "28f6bfff14941899772983cafdf6d501561ecf2d", + "docs/validation/artifact_publication_315_20260930/original-ratchet-initial.json.gz": "43090773bba24ec63f568fc7b239d436dafedf6b", + "docs/validation/artifact_publication_315_20260930/original-ratchet-scripts-resources.txt": "c76998894c25f5617bf07c0216dac440c75ee602", + "docs/validation/artifact_publication_315_20260930/original-ratchet-scripts.json.gz": "bcd3423dba525ce4cb81aed7cec7c77f9d2794c0", + "docs/validation/artifact_publication_315_20260930/owner-controls.log": "0c2947406361a2604914d7042e1692ecdafe9bb6", + "docs/validation/artifact_publication_315_20260930/ownership.md": "df51bb557ddaacc0f418bce321b9fb0f2e53c2c7", + "docs/validation/artifact_publication_315_20260930/parent-installed-checkpoint.rst": "5c4fdcbf44cbffc36ce98cd53cb2a71565fdef9f", + "docs/validation/artifact_publication_315_20260930/pr-body.md": "8cb3b957f0bb6badbec20fc2e626b81c9b2ffb75", + "docs/validation/artifact_publication_315_20260930/publication-final.log": "cc9c058e52ca1ad4abad48a90e297d0447cdab56", + "docs/validation/artifact_publication_315_20260930/qualify-original.py": "cfd5b6f9f7d1b19da4e917c92c5bc2a5a551e1ff", + "docs/validation/artifact_publication_315_20260930/results.md": "91eed4df23880d686950ce5ca816d84e6f8db987", + "docs/validation/artifact_publication_315_20260930/run-bounded-r1.py": "06d2cf9a8b94bb8467e4004b996114ea30dcf5ba", + "docs/validation/artifact_publication_315_20260930/run-source.sh": "9a4a8ad48205e4bfe3ab184d31168360080ae696", + "docs/validation/artifact_publication_315_20260930/runtime-controls.log": "8097b63472af6dbfb532720da52f5a377ae63451", + "docs/validation/basicpy_parent_numeric_20260930/check-persisted-live.py": "0b55cc7f9ed8e91ba798cf606bd030337f9b34f2", + "docs/validation/basicpy_parent_numeric_20260930/check-publication-fixed.py": "78216d99b96dfde98380f78edf63c842ebc1320c", + "docs/validation/basicpy_parent_numeric_20260930/checkpoint.rst": "bddff5121dc4b75fce975022afa0063c49b97390", + "docs/validation/basicpy_parent_numeric_20260930/cpu-installed-checkpoint.rst": "88eb43cfb0245b7cf851f62b5f8090452ba73839", + "docs/validation/basicpy_parent_numeric_20260930/installed-failure-receipt.rst": "4738f20b6a1087eed1029ca61d5bb8e08aa9970e", + "docs/validation/basicpy_parent_numeric_20260930/main-integration-20261002.rst": "18ec59c4a1bdfb12cfa7da6f43e36d4bb727876c", + "docs/validation/basicpy_parent_numeric_20260930/owner-closure.rst": "fef702e2accf4bd432a87aa071770630dd5f625b", + "docs/validation/basicpy_parent_numeric_20260930/pipeline-live-wheel.py": "aef9eb3e421bec1128f1156e42131d92c9903201", + "docs/validation/basicpy_parent_numeric_20260930/pipeline-publication-fixed.py": "6af69ac835b67cb9d4af418e00f591fe2c9e1f23", + "docs/validation/basicpy_parent_numeric_20260930/project-dependency.rst": "716c2a8ebd5251054bd66001ccd47aef5466097b", + "docs/validation/basicpy_parent_numeric_20260930/publication-fixed-installed.rst": "13ced7e71eaaaf8b8a3041f2465fd22805c200aa", + "docs/validation/basicpy_parent_numeric_20260930/run-live-wheel.sh": "40721956416b1b92a0e560c0bbb79225caad8920", + "docs/validation/basicpy_parent_numeric_20260930/run-owner-check.sh": "0325194e30ef8bbb11cf7c21530b92eb5f3ba795", + "docs/validation/basicpy_parent_numeric_20260930/run-publication-fixed.sh": "f23e572900aff71dcc3e3916bbbeffa763d92437", + "docs/validation/basicpy_parent_numeric_20260930/run-source-check.sh": "0a39bc8977dcea1da21090d331b7319f1ec3c6f9", + "docs/validation/basicpy_parent_numeric_20260930/target-owner-installed.rst": "4212e100894f6a6d211b830171e0f58a7d54df18", + "docs/validation/bioformats-saved-calibration-440-source-20261002.tar.gz": "a66b8b734c464dfc6fe463e1176af4d12030cf80", + "docs/validation/bioformats-saved-calibration-440.rst": "378da61b8fd856673ff7231807e2e7fe10af7956", + "docs/validation/catalog-endpoint-incarnation-464-20261002/INSTALLED-ACCEPTANCE.rst": "3fb5b1dbbd5d12240ee9e5058447de16fccf6e41", + "docs/validation/catalog-endpoint-incarnation-464-20261002/RECEIPT.rst": "4bb816103a5a069e44cb4070a7e9450d81129146", + "docs/validation/catalog-endpoint-incarnation-464-20261002/owner-after.log.gz": "ec2f4c4dd7cb660a0c9f60a1a96362d330bfdd51", + "docs/validation/catalog-endpoint-incarnation-464-20261002/owner-before.log.gz": "4e3dcb76c3745992f53db342fa955cb67b4eb70b", + "docs/validation/catalog-endpoint-incarnation-464-20261002/pinned-r0-corrected.log.gz": "bf14c1f3a39217502ff625c1d4c6a125b093dbe1", + "docs/validation/catalog-endpoint-incarnation-464-20261002/pinned-r0-final.log.gz": "21148eb8b4f0b30cdd8ac765b373e4533518216e", + "docs/validation/catalog-endpoint-incarnation-464-20261002/pinned-r0-first.log.gz": "dece41177331469ff2bf5e07872fc1fb3c27e83f", + "docs/validation/catalog-endpoint-incarnation-464-20261002/raw-nra.log.gz": "81a74a9bd0db4e9ee6d6440f6a4939f5fc2e7ae1", + "docs/validation/catalog-endpoint-incarnation-464-20261002/source01.log.gz": "b7837059e7262d4449ce2175d96d324a0cb7492d", + "docs/validation/catalog-endpoint-incarnation-464-20261002/source02.log.gz": "2936f957182e42b20c5d5d59561cd7b6c7ead43e", + "docs/validation/catalog-endpoint-incarnation-464-20261002/source03.log.gz": "60db371ecbef789c9f4686a5473d7713a01269e0", + "docs/validation/catalog-endpoint-incarnation-464-20261002/source04.log.gz": "2f28606d55099a478be3bbdcd081a2d0d5b36a8b", + "docs/validation/catalog-endpoint-incarnation-464-20261002/source05.log.gz": "808c42cdb33b032ad4b05f03e24e39f5d3d6d6ed", + "docs/validation/catalog-endpoint-incarnation-464-20261002/source06.log.gz": "d4fc21ec89a1f750d622128c74cb4fe1dfcb5b11", + "docs/validation/catalog-endpoint-incarnation-464-20261002/source07.log.gz": "0b09b51aa1681d7dde961e31e491a351e9daadb0", + "docs/validation/catalog-endpoint-incarnation-464-20261002/source08.log.gz": "fb4737e951f27615ba28b9f2b69803a690c04308", + "docs/validation/channel-contrast-review-20261001.rst": "7fa19afe6e3e85c2a117cf925f03d15f898055be", + "docs/validation/check-bioformats-calibration-440.py": "06cab96f57762d547ce7fb7d17f1e3fdb9167118", + "docs/validation/check-function-help-390.py": "7dd02898c70c6890e7dd1819e7c5698fe0c6c700", + "docs/validation/child-measurement-producer-selection-492-495-20261002/QUALIFICATION.rst": "a673ca917914a2b21be50231eac2f83a0c2970f8", + "docs/validation/child-measurement-producer-selection-492-495-20261002/controls.log": "eca4f8fde67da1bbde2bb29af20b6c619584cc0c", + "docs/validation/child-measurement-producer-selection-492-495-20261002/integrated-controls.log": "ea0477ace4a0eabd39c97007a347cd94ad2a796a", + "docs/validation/child-measurement-producer-selection-492-495-20261002/original-r0.json.gz": "418e849c95202c6c4921772d23f830a020736a48", + "docs/validation/child-measurement-producer-selection-492-495-20261002/original-r1.log": "86814c023357b5d19384cda2b20a33ca2b3c5472", + "docs/validation/child-measurement-producer-selection-492-495-20261002/qualification.json": "3983c8190fa404bcafce2b98eff512dc9ebcdd8e", + "docs/validation/child-measurement-producer-selection-492-495-20261002/strict-native-receipt.json.gz": "a793649eb304fdefd2b5a12d20123d14c9cf5593", + "docs/validation/colocalization-metric-selection-487-20261002/cache-window-counterevidence.json": "97adf6ad8b6362baeecaaae047bdefacb230772d", + "docs/validation/colocalization-metric-selection-487-20261002/common-saved-output-comparison.json": "fbbde3a3c9ed00fd825f5c778e2797e5c1bd2133", + "docs/validation/colocalization-metric-selection-487-20261002/manifest.json": "739fc4d0013e33867d74640587e359c7a163e413", + "docs/validation/colocalization-metric-selection-487-20261002/qualified-controls.json": "3fa8db91b9e23538f4e8d49f8bd93f45f0d7f1d4", + "docs/validation/colocalization-metric-selection-487-20261002/strict-native-full.json.gz": "531b7e9fb574f09a03c8d3dd08e0f85607acb343", + "docs/validation/colocalization-metric-selection-487-20261002/strict-native-summary.json": "14a0803bc13236f9d99f48fe01ad60a4d1ba38d7", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/PR_BODY.md": "bb24ee18bd9f3565ef8c3a9dd1e07bd5a6bdac38", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/RECEIPT.rst": "6b3a647f6c0fb403650b3ff3ee5ed6c4b076d2b6", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/SHA256SUMS": "4ff1dcd41236bb1e9651a1518b12e732847f7a79", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/SOURCE_FREEZE.rst": "f45bb5da1b71de242f57d418dad5b0ac0524344f", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/baseline-legacy-shard-resources.txt": "85326f9e222c911abfbc72f05a14dfcf9b782af3", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/baseline-legacy-shard.xml": "b84a8826e6566e202fb9ab5e4cd9b5952f09f1a4", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/capture_r0.py": "e81ba48b6bce7b9785f03efd92a55cf9fbc76097", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/collection-boundary.xml": "0a7097ded86de783a5030f307e524697d53e2e94", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/final-planner-shard-resources.txt": "e953ac87e99d08216cf0ef8546438a094306d6e3", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/final-planner-shard.xml": "36c92ba1d946f7cf42baaf60f8e5e98d6eeabd03", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/metadata-controls-binding-assertion-failure-resources.txt": "61d1d800c44cb593ccde6c3ffe232b74a984fc3b", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/metadata-controls-binding-assertion-failure.xml": "0a9dd8897875185a47ce754916c3e7e8a91f626f", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/metadata-controls-fixture-failure-resources.txt": "0e2ea41c3d8aac49e1ca8e6d78d498aa883310c4", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/metadata-controls-fixture-failure.xml": "f5c1de56cc961ce257e2f01e363b6d66196f4bc8", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/metadata-controls-resources.txt": "21d7651a39f8668c5a2afda642ad1d6645d34e75", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/metadata-controls.xml": "3d62995beb8bdc92827323628aa1aabd51a41d65", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/mro-planner-shard-resources.txt": "cdd2fc230e138e34fc3d5ba86a5521b4d0370cf7", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/mro-planner-shard.xml": "1967e47ce45f125dbff9d3bb4c413d8ac33d5a5e", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/original-green-resources.txt": "5e22781772b9f196bd1d52ee2e5d3ebe275997c6", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/original-green.xml": "5d9460248817e732478ca39343d7b1e5282a4201", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/original-red-resources.txt": "10686d13a82bb77279562a6632687d5962fd01ad", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/original-red.xml": "58d6f60351c938d4b10fd97cdc90fdd79131d346", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/pattern-runtime-shard-resources.txt": "793c9aa1e537e825a45156e767124032964ed8f1", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/pattern-runtime-shard.xml": "d54ea2305ee8982c2ad27d8e31100a02638ebf65", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/planner-shard-resources.txt": "8aafaee09216b33410b72d97e1421a0c22f648f6", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/planner-shard.xml": "484b3c91eb1f7015154f346f1edb444f36aa6175", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/r0-first-resources.txt": "4c5fda7568bf50a58275aa791f1b95e6f9b7f8f2", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/r0-first.json": "bbfcbe75a91ba7a5051938109ce29bf054e793e9", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/r0-first.stderr.txt": "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/r0-qualified-resources.txt": "957ca3f00b5360afe8e18474ddfb0d47020ae606", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/r0-qualified.json": "b13e3362854657ede1663642198fc1f62145ab1f", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/r0-qualified.stderr.txt": "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/runtime-origin-shard-resources.txt": "9862f3ee51329b251b6c59a90e284614b2ad2d3e", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/runtime-origin-shard.xml": "cc480c8324cb43d79d5f16714be2acb2123faa97", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/source-edge-shard-resources.txt": "c0ee86360818fb0976ab80894c450a4749634bfe", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/source-edge-shard.xml": "dcebc49c29846dff11a726b5bfb3e81438e68a59", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/source_shard.py": "fc67a0334941c76a891a69354c9642d47209bed1", + "docs/validation/context_bounding_20260929.md": "d27dcd7cb8058b3657cc7f0815776ff2cf7db9ee", + "docs/validation/context_bounding_20260929/build-resources.txt": "957dfbf3c284c25497958b41ffbc36076860efe5", + "docs/validation/context_bounding_20260929/build.txt": "de2310ce4f4d34e0ac31335dc0e5bfdc7f4b8aec", + "docs/validation/context_bounding_20260929/pytest-checkpoint-resources.txt": "05fbbe923487fa57fef71ab3e95d6dcd406b92c7", + "docs/validation/context_bounding_20260929/pytest-resources.txt": "3d0e4e46451a43378c98a938e73c51e79497d218", + "docs/validation/context_bounding_20260929/pytest.txt": "3d6abc28e0b833f2b34eecadd18f0efde86805c8", + "docs/validation/context_bounding_20260929/pytest_checkpoint.txt": "060cf9be02a964bea8bea524aa54fea050d14da9", + "docs/validation/context_bounding_20260929/source-projection.json": "44649a6c37a4596b9de401bb2b635b2db7260d83", + "docs/validation/cpa-secondary-source-metadata-444.rst": "46a42dd5477bec0933b9a20e3dbf814418fa79ca", + "docs/validation/cpa-workspace-export-main-480-20261002.json": "dec7f9cf3a67d60a59bf875fdaf19584155b0a32", + "docs/validation/custom-plate-admission-500.rst": "37185a89ab3f535a1cdd4609e2145f96a22ed9f2", + "docs/validation/custom_plate_admission_journey.py": "272c1551f5d70ec3e750067d13f60690fd3d55e3", + "docs/validation/declaration-selected-376.rst": "96fa283dbe971274f5777c6e6397cffffadb28b2", + "docs/validation/diagnose-knowledge-376.py": "653aeb25fb85e002c8cefa6e7743a8c05560647b", + "docs/validation/diagnose-selected-discovery-release-022.py": "c5436844e7ec9903d388b50bf36be346e3f4001f", + "docs/validation/duplicate_editable_metadata_20260930/README.md": "e88df7224d197e1a7e70436b2868bf071b661c17", + "docs/validation/duplicate_editable_metadata_20260930/accept_shared_main.py": "94cc198e13382b85fc7115785c278737cd112795", + "docs/validation/duplicate_editable_metadata_20260930/after.json": "5bc0a8239d9118e06d7e7f5209c4ade3099c1c05", + "docs/validation/duplicate_editable_metadata_20260930/before.json": "25ef5367c5a022ea330de381ba292ae21c84a522", + "docs/validation/duplicate_editable_metadata_20260930/install-retry.log": "323fe4331d58c89c0958b63f6559c03769c80451", + "docs/validation/duplicate_editable_metadata_20260930/install.log": "13cd7a2f6597b0bb2ced9e0862befd6e8939abdf", + "docs/validation/duplicate_editable_metadata_20260930/perf-runtime-projection-shared-main-acceptance-20260930.json": "b57c9dd26d033b5c4114da3abcad113ce1e5aa93", + "docs/validation/duplicate_editable_metadata_20260930/perf-runtime-projection-shared-main-clean-acceptance-20260930.json": "36e0757f6d6adc2c25cd238b13099054db5b1b71", + "docs/validation/duplicate_editable_metadata_20260930/pip-check-after.log": "c688d8ccdad82420475fc31cb40020cccfeeef2b", + "docs/validation/duplicate_editable_metadata_20260930/pip-check-before.log": "28c49ae3de561e41fb102673ff43b61cdb8e8cbf", + "docs/validation/duplicate_editable_metadata_20260930/quarantine.json": "6135d8819601e6963df1e276b419e41bd9461dcd", + "docs/validation/duplicate_editable_metadata_20260930/uninstall.log": "65719ff57aad445cbbc51fb441227ef730189f42", + "docs/validation/faint-structure-viewer-qa-396-20261001.rst": "b0af38756a7f69b54808c0296012c2e561d2a931", + "docs/validation/faint-structure-viewer-qa-396-20261001.tar.gz": "810974b950f5ac395588435261f7b4bc0c1bba7a", + "docs/validation/function-declaration-help-390.rst": "8b77b1fdbebc87fdcabfd588cc40d5663b2e1cf1", + "docs/validation/function-help390-source-20261001.tar.gz": "6bcf831b76cb77e0ac98a56215f3cfdb3ee0cdf4", + "docs/validation/function-help391-registered-source-20261001.tar.gz": "e34ad9b8ce643b29daafd723a23db94c34ca78b6", + "docs/validation/generic_runtime_owners_323_20260930/README.md": "f20b4a29281e83affd55f4e643271b4933633408", + "docs/validation/generic_runtime_owners_323_20260930/SHA256SUMS": "1f4b7d68b451d696a6f7ee952ff38499da8df886", + "docs/validation/generic_runtime_owners_323_20260930/apply_generic_measurement_runtime_owners_20260930.py.txt": "6b8dc7400785769e787711fc49a4a6ce7d1c10cc", + "docs/validation/generic_runtime_owners_323_20260930/census_refill_preparation_20260930.py.txt": "bb17e3da346a0148bf167a55928e1382f98b0da2", + "docs/validation/generic_runtime_owners_323_20260930/check_runtime_owner_fresh_native_facts_20261001.py.txt": "a8302873ef0d4582bc2e41dd759181b06f7851cc", + "docs/validation/generic_runtime_owners_323_20260930/check_runtime_owner_fresh_native_tables_20261001.py.txt": "0d6052e84cc7d1a45393ae476eeb9b37c512885b", + "docs/validation/generic_runtime_owners_323_20260930/closure-global-gc-sitecustomize.py.txt": "57f090330a744cda34da349f04e8ccab1e602bf8", + "docs/validation/generic_runtime_owners_323_20260930/cpu-timeline-candidate-20261001.jsonl.gz": "2d027c1308c2e6cdb593f44e600e6ceb08229bf8", + "docs/validation/generic_runtime_owners_323_20260930/cpu-timeline-main-20261001.jsonl.gz": "7c4d316607269196741f9b0707fd6169e7630be3", + "docs/validation/generic_runtime_owners_323_20260930/cpu-timeline-sitecustomize.py.txt": "1ef8f01a3b330e373af24ad8fe3c74ac69dcd9f9", + "docs/validation/generic_runtime_owners_323_20260930/fresh-native-imaging-provenance.json.gz": "4f05d9295205a014117c23f23dc9a3e45e192e14", + "docs/validation/generic_runtime_owners_323_20260930/fresh-native-imaging-request_0.json.gz": "9a8fc8c8c74239a56cfc5df6fb34583b8fdf0d27", + "docs/validation/generic_runtime_owners_323_20260930/fresh-native-imaging-summary.json.gz": "4117e415eccb3bb227c61c44dab7efc84624b488", + "docs/validation/generic_runtime_owners_323_20260930/global-gc-candidate-intervals-20260930.jsonl.gz": "22db29316b83a06d6d466d1cb48cae92d9d1146c", + "docs/validation/generic_runtime_owners_323_20260930/global-gc-closure-candidate-a-20261001.jsonl.gz": "3c159ac630410f5a8125ce042e9930770d31f704", + "docs/validation/generic_runtime_owners_323_20260930/global-gc-closure-candidate-b-20261001.jsonl.gz": "d85911397036582b207ac80bffe9569619ddb179", + "docs/validation/generic_runtime_owners_323_20260930/global-gc-closure-main-a-20261001.jsonl.gz": "2ab2c982e23def246f2dd86478d2a28b8900419b", + "docs/validation/generic_runtime_owners_323_20260930/global-gc-closure-main-b-20261001.jsonl.gz": "600e0d160157315d321d2f4369940a7766ea1c49", + "docs/validation/generic_runtime_owners_323_20260930/global-gc-diagnostic-sitecustomize.py.txt": "a9a09411fdcbec627d942ef2b5afd4ffc39c7315", + "docs/validation/generic_runtime_owners_323_20260930/global-gc-main-intervals-20260930.jsonl.gz": "38e2685d67a5f943c9136c9eefdd99b67d60744b", + "docs/validation/generic_runtime_owners_323_20260930/kernel-preparation-ownership.json": "dd9d70dea20136283e59358d2fff724baba33450", + "docs/validation/generic_runtime_owners_323_20260930/make_runtime_owner_cpu_timeline_20261001.py.txt": "572cab4beab3e7bd3fd5260bd9a34ae724157312", + "docs/validation/generic_runtime_owners_323_20260930/measurement-cache-and-column-ownership.json": "345cc05b9f1f04c0e8b5342a7dd6494e44904361", + "docs/validation/generic_runtime_owners_323_20260930/move_bound_runtime_profiler_20261001.py.txt": "847be28e27fc2e4cac12bf464a44a2031e156b7c", + "docs/validation/generic_runtime_owners_323_20260930/move_generic_numba_preparation_owner_20260930.py.txt": "27d56d7639dd68537b1d5c430888343468545a06", + "docs/validation/generic_runtime_owners_323_20260930/move_numerical_cache_mechanics_20261001.py.txt": "3984204934d1c698f9277ed9a86c68759a3f12cb", + "docs/validation/generic_runtime_owners_323_20260930/nra-r1.log": "154b8ac1dbc761d7ef1865b5b70a33a7564df9b5", + "docs/validation/generic_runtime_owners_323_20260930/perf-bound-runtime-profiler-plan-20261001.log.gz": "38bae12efbe845f3adc1bb93228939627b1e2374", + "docs/validation/generic_runtime_owners_323_20260930/perf-bound-runtime-profiler-simulation-20261001.json.gz": "79682639751339da47552fd69b823b99c9f8ba58", + "docs/validation/generic_runtime_owners_323_20260930/perf-bound-runtime-profiler-transaction-20261001.diff.gz": "3c60b38126383fa93e9747f2464e51382d72fb7c", + "docs/validation/generic_runtime_owners_323_20260930/perf-columnar-export-frontier-probe-20260930.json": "2930f7d6998565dc8c7d212ee35282b2c8caa737", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-measurement-runtime-all-consumers-20260930.xml.gz": "00e53d117eb0f626c9bf647195087de85a10c8e6", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-measurement-runtime-before-census-20260930.json.gz": "d870c00d102886fba7441eff8b95d67cb47f8024", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-measurement-runtime-focused-20260930.xml.gz": "796dc98b1307483d728b80803a4602ba5d5b5136", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-measurement-runtime-nra-transaction-20260930.diff": "8ed87673dfb02c39631074c0b57c4b8cd65440f9", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-measurement-runtime-public-comparison-20260930.json": "83f45404599d541da814252787b8fb4e9f754eda", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-measurement-runtime-public-observations-20260930.json": "eb71be869df69a3f8375cfb664ba99b4fd10373e", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-measurement-runtime-refinement-20260930.diff": "a3ef7ebd30854f0d2d7cd17641bc1e12c77bffdc", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-measurement-runtime-unchanged-main-materialization-20260930.xml.gz": "9827bbcb3ad3bfc115e205b734f30049daee35b4", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-numba-owner-application-20260930.log": "e5862874dc1f96e2c6a5067a8256d1e851de9cec", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-numba-owner-application-corrected-20260930.log": "05cfe3c61f44d06e3b330a500de7b46ca62ffa93", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-numba-owner-complete-simulation-20260930.json.gz": "89479339c6a818e228be9bb2bf40e12f0dd72aa2", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-numba-owner-consumer-test-20260930.diff": "acdfe30b7d1f59bef69c0fd67199b11e5f2a550c", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-numba-owner-initial-simulation-20260930.json.gz": "8b144a6dc9102dab2bb04e88a8a52873f674c31d", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-numba-owner-transaction-20260930.diff": "2de1761043fb1ed70a306f1c6b64f9deea315821", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-runtime-cache-lifetime-refinement-20260930.diff": "0fda6cc93f6ca7c5c58f38a4ae8c956d55f65a47", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-runtime-global-gc-diagnostic-observations-20260930.json.gz": "44b43d9c0c98433eaf78f800169616ece7d38d75", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-runtime-global-gc-overlap-20260930.json.gz": "f2fcb5577c70ddc108323a532cb4d404ae6fa36c", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-runtime-imaging-confirmation-observations-20260930.json": "b342f9aaf6f2a5828fc81ebfca3779976cb7e6be", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-runtime-owners-complete-native-artifact-parity-20260930.json": "3d97717a7c9eb41cd5c41b75ea006f13fa52634e", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-runtime-owners-complete-public-comparison-20260930.json": "741a18be8a71f1741941035d99a0566cde62241c", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-runtime-owners-complete-public-observations-20260930.json": "1073c59b3e5397baafb34b7e12f0b34942116346", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-runtime-owners-paired-main-census-20260930.json.gz": "c2d2136632f787a56cdef6cf5efac6a66f528b9b", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-runtime-owners-paired-main-consumers-20260930.xml.gz": "6de5fbfeebd8086f1ad4521177dde4b5c68e7024", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-runtime-owners-paired-main-public-comparison-20260930.json": "d1ad1d42316b4a52f0938dd97c738b22f670e445", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-runtime-owners-paired-main-public-observations-20260930.json": "0302c8fa8ab7a7e1bc1554a4ec84298df0b984ab", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-runtime-owners-paired-main-r0-benchmark-20260930.json.gz": "c9cefbf2a2a31a60900bdc50c4d6081b41484ddb", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-runtime-owners-paired-main-r0-openhcs-20260930.json.gz": "15451ec237ed89a87c80051dc257bf67b243cd87", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-runtime-owners-paired-main-r0-scripts-20260930.json.gz": "97aa8635095963f67f9c602c26e77c951cf60468", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-runtime-owners-paired-main-r1-20260930.json.gz": "9f57ed8cbf970e3fe5811cbe8bacf0fb1883b7de", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-runtime-phase-diagnostic-20260930.json.gz": "191406a910209b0421d4afbfc7467ea846c4e0ae", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-runtime-phase-diagnostic-observations-20260930.json": "d58e1b9a2555ed05bfaf3f520732d7026c17262b", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-runtime-query-hash-costs-20260930.json": "d4a1f09b0fea1144d3fc3c483c78c1bfb49ba926", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-runtime-query-hash-diagnostic-20260930.json.gz": "df813b183b2f55b5c586fcfc8070c2877f7f162c", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-runtime-query-hash-diagnostic-observations-20260930.json": "83feae001a1222789525257cfddf3cd48ca92285", + "docs/validation/generic_runtime_owners_323_20260930/perf-granularity-active-spectra-production-replay-20261001.json.gz": "b6c5a425432ac65844ca41bab61ad3a3708e4211", + "docs/validation/generic_runtime_owners_323_20260930/perf-granularity-constant-spectra-production-replay-20261001.json.gz": "dc5c24c7dd020de222f58f834cb58a50760911e0", + "docs/validation/generic_runtime_owners_323_20260930/perf-granularity-maxtree-admission-20261001.json.gz": "408652714561165d92212f2493ee96083182911b", + "docs/validation/generic_runtime_owners_323_20260930/perf-materialization-ack-current-main-corrected-20260930.xml.gz": "6c1d82298046e463f3bc58a1636255aa6f933a46", + "docs/validation/generic_runtime_owners_323_20260930/perf-measurement-provenance-frontier-probe-20260930.json": "1b2c27389d65eaa9d8fb2cb3ae0009f393f30a97", + "docs/validation/generic_runtime_owners_323_20260930/perf-numerical-cache-owner-simulation-20261001.json.gz": "59e36df308d7d17fbe226785f8523237e2960ea6", + "docs/validation/generic_runtime_owners_323_20260930/perf-numerical-cache-owner-transaction-20261001.diff.gz": "dd1ad53e987e16d3ab66fc40e12a7272850ef678", + "docs/validation/generic_runtime_owners_323_20260930/perf-numerical-cache-owners-before-census-20261001.json.gz": "bfdbaeb39a55f12c5a80f29f33294c7e30337a29", + "docs/validation/generic_runtime_owners_323_20260930/perf-numerical-cache-owners-public-comparison-20261001.json.gz": "5b66a0a1aa9c4ca8cf95129b809d08caf0bdd989", + "docs/validation/generic_runtime_owners_323_20260930/perf-numerical-cache-owners-public-observations-20261001.json.gz": "e7a85a53cbb8c9fc1c81941ce04e5f2cdffddcb1", + "docs/validation/generic_runtime_owners_323_20260930/perf-runtime-owner-closure-census-20261001.json.gz": "fc9cd5f9ee8a9086aa5fcfe3579b3094134a1cdb", + "docs/validation/generic_runtime_owners_323_20260930/perf-runtime-owner-closure-combined-tests-20261001.xml.gz": "01317aff2e7af5c8a0c30d33f7e0b32e772d78aa", + "docs/validation/generic_runtime_owners_323_20260930/perf-runtime-owner-closure-decision-20261001.json.gz": "92fb90e14e36d1d418fbbce03b0b14a75fb0bab9", + "docs/validation/generic_runtime_owners_323_20260930/perf-runtime-owner-closure-global-gc-diagnostic-observations-20261001.json.gz": "bd577cf7fddc470df3e000105b166e0e202a8352", + "docs/validation/generic_runtime_owners_323_20260930/perf-runtime-owner-closure-global-gc-overlap-20261001.json.gz": "7447a61c6d7ec9a02a92cf67e235e652839388f5", + "docs/validation/generic_runtime_owners_323_20260930/perf-runtime-owner-closure-native-artifact-parity-20261001.json.gz": "64c7548874bab4eb3342c8daef1dccfaef047e1e", + "docs/validation/generic_runtime_owners_323_20260930/perf-runtime-owner-closure-r0-benchmark-20261001.json.gz": "8ebae02b05af3c499ec64d6647eeb1ab071d591f", + "docs/validation/generic_runtime_owners_323_20260930/perf-runtime-owner-closure-r0-openhcs-20261001.json.gz": "013b861ad3e798d859c602c31c48e068b2025c5e", + "docs/validation/generic_runtime_owners_323_20260930/perf-runtime-owner-closure-r0-scripts-20261001.json.gz": "dc89aa25ed4d269b3598a64738074c2ea0424d4e", + "docs/validation/generic_runtime_owners_323_20260930/perf-runtime-owner-closure-r1-20261001.json.gz": "da02b68ed8dabddf1729ae5722ce89fa0e97c76a", + "docs/validation/generic_runtime_owners_323_20260930/perf-runtime-owner-closure-step-deltas-20261001.json.gz": "715b42c8fe4dc099422b3b7aa7edd2a93da2d82a", + "docs/validation/generic_runtime_owners_323_20260930/perf-runtime-owner-cpu-timeline-attribution-20261001.json.gz": "7c96f8c9fa65e483c2f2970d4c4a9e8cec41b6f0", + "docs/validation/generic_runtime_owners_323_20260930/perf-runtime-owner-cpu-timeline-observations-20261001.json.gz": "1e1572ede43a67ac8b5c87ccd8112534a054dd80", + "docs/validation/generic_runtime_owners_323_20260930/perf-runtime-owner-fresh-native-fact-parity-20261001.log.gz": "cd56f41e0b9001c2f81713644db92efb489ddfbe", + "docs/validation/generic_runtime_owners_323_20260930/perf-runtime-owner-fresh-native-reference-result-20261001.json.gz": "593fc88fdf83fcbb8beca8ef0065be016c8b1f30", + "docs/validation/generic_runtime_owners_323_20260930/perf-runtime-owner-fresh-native-table-parity-20261001.json.gz": "45f344603d6cb2c58f9e2af2c017da39274ed9bb", + "docs/validation/generic_runtime_owners_323_20260930/perf-runtime-owner-native-binary-inventory-20261001.json.gz": "3d301c123216762399b172f3ee509a9b2d267a89", + "docs/validation/generic_runtime_owners_323_20260930/perf-runtime-owner-native-binary-sections-20261001.json.gz": "6632fec1e3c34d6eb4b68ce9cd1e0fe2a76153d9", + "docs/validation/generic_runtime_owners_323_20260930/phase-diagnostic-sitecustomize.py.txt": "ce6dd100c6874f020e9d7b90fe964569e86e109a", + "docs/validation/generic_runtime_owners_323_20260930/prototype_granularity_active_spectra_20261001.cpp.txt": "d168b48f36a1e3ede3690d037b96b9f713814af5", + "docs/validation/generic_runtime_owners_323_20260930/prototype_granularity_constant_spectra_20261001.cpp.txt": "deb475bec7ef24ceabee04b84751f0179886f263", + "docs/validation/generic_runtime_owners_323_20260930/query-hash-diagnostic-sitecustomize.py.txt": "e453975864a3379ed0d7a45266c7d4d65dcd94fe", + "docs/validation/generic_runtime_owners_323_20260930/refine_generic_measurement_runtime_owners_20260930.py.txt": "c63383ceaf9fdb3b3f9aff2707e7cf622d67b97e", + "docs/validation/generic_runtime_owners_323_20260930/replay_granularity_active_spectra_20261001.py.txt": "0ffffc3af9ff92d1bfbbe49fcbfef09fe1eb15eb", + "docs/validation/generic_runtime_owners_323_20260930/replay_granularity_constant_spectra_20261001.py.txt": "30c839bfddbf9be870ed4568fe11ba452597c3ec", + "docs/validation/generic_runtime_owners_323_20260930/run_generic_runtime_global_gc_diagnostic_20260930.py.txt": "63c0a646190561a95adf2976d921a30d4783c4af", + "docs/validation/generic_runtime_owners_323_20260930/run_generic_runtime_owners_complete_acceptance_20260930.py.txt": "5eab7c6b3e6f27bdc4b6ffa28eb090415959ee14", + "docs/validation/generic_runtime_owners_323_20260930/run_generic_runtime_owners_paired_main_acceptance_20260930.py.txt": "0721a7944a3fe8a54a878b6b30e376a9feb140b5", + "docs/validation/generic_runtime_owners_323_20260930/run_numerical_cache_owners_acceptance_20261001.py.txt": "27902922b6ab3a23451f80afbc0c4ed7b2609910", + "docs/validation/generic_runtime_owners_323_20260930/run_runtime_owner_closure_global_gc_diagnostic_20261001.py.txt": "cc1a8db221cba15d9dd44aa7d9efd859fc7831c2", + "docs/validation/generic_runtime_owners_323_20260930/run_runtime_owner_cpu_timeline_20261001.py.txt": "0f5caba061caf9e8229f32fa9f68b5ff828b52c2", + "docs/validation/generic_runtime_owners_323_20260930/run_runtime_owner_fresh_native_reference_20261001.py.txt": "91d588639fceb23608ae5bc17b3d30d5e3aa29af", + "docs/validation/generic_runtime_owners_323_20260930/separate_cache_values_from_process_lifetime_20260930.py.txt": "17dc0ff15b36d8ebdbb33ec1d02c90bd51c8a85e", + "docs/validation/generic_runtime_owners_323_20260930/test_generic_registered_numba_preparation_20260930.py.txt": "b72e564b794623233858f635f1c8d5131c629d80", + "docs/validation/generic_runtime_owners_323_20260930/test_shared_numerical_cache_owners_20261001.py.txt": "3a12375e8edd8c8d097ca360e55f48e41f856f27", + "docs/validation/generic_runtime_owners_323_20260930/validate_generic_runtime_native_artifacts_20260930.py.txt": "53bb7ab869af4f47bc8892b75499044f170e7f2a", + "docs/validation/generic_runtime_owners_323_20260930/validate_generic_runtime_owners_complete_outputs_20260930.py.txt": "2f1468dbeabe7f7a95f810bfc5f3c2ad1ad96d17", + "docs/validation/generic_runtime_owners_323_20260930/validate_generic_runtime_owners_paired_main_outputs_20260930.py.txt": "ed11df57938eb9ba40f8de453ead30a289f17a58", + "docs/validation/generic_runtime_owners_323_20260930/validate_runtime_owner_closure_native_artifacts_20261001.py.txt": "a07c4caf394e00d77bb72675ac58e62d9dbdd529", + "docs/validation/generic_runtime_owners_323_20260930/validate_runtime_owner_closure_owners_paired_main_outputs_20261001.py.txt": "da798773f1987c6f836fb73a67fbcc21119e8b99", + "docs/validation/generic_runtime_owners_323_20260930/validated-source-tree-95a98b008.txt": "ba6247b4f17ded789bda51d33daa21a575a9733e", + "docs/validation/generic_runtime_owners_323_20260930/validated-source-tree.txt": "088fbd2fa8dd6cddef358e22190946c11ecc9163", + "docs/validation/graytocolor-binding-axis-432-20261002/carrier-compile-diagnosis.log": "d900bf3ed92c8d001d23e3c282a0f78690ddf88a", + "docs/validation/graytocolor-binding-axis-432-20261002/carrier-compile-parent-admission.log": "c4d845baef34ec4eee0a144b5e04bd1e21cbe402", + "docs/validation/graytocolor-binding-axis-432-20261002/carrier-creation-receipt.rst": "f7b766779a88f3ed9ce3d20b2574908f94aeb0d2", + "docs/validation/graytocolor-binding-axis-432-20261002/carrier-repair-binding-controls.log": "0b313f79dddbdc7c5f53cdf15515532988530266", + "docs/validation/graytocolor-binding-axis-432-20261002/carrier-repair-controls.log": "b1ec3adc77269eb81d1afc8c8372188bdcd8c29f", + "docs/validation/graytocolor-binding-axis-432-20261002/carrier-repair-initial.log": "761452635faf23a6d55ac465f0c7d4aba94d6ede", + "docs/validation/graytocolor-binding-axis-432-20261002/carrier-repair-newcase.log": "08658dee1f0747f5dd13444e16efee92c80ea193", + "docs/validation/graytocolor-binding-axis-432-20261002/carrier-repair-owned-proof-controls.log": "35b16abec20c43271b34f31ad3d16541aa290f91", + "docs/validation/graytocolor-binding-axis-432-20261002/carrier-repair-owned-proof-r0.log": "244488f13af27fbac5a60207a0ff955014ed3965", + "docs/validation/graytocolor-binding-axis-432-20261002/carrier-repair-r0.log": "2809ae0efe0625172badfb0ef10e8eb10a39143a", + "docs/validation/graytocolor-binding-axis-432-20261002/carrier-repair-receipt.rst": "7221b311b3496a0805c7620df50b70ab796634b9", + "docs/validation/graytocolor-binding-axis-432-20261002/carrier-runtime-lanes.log": "d768f4328151a488d0095d62ad2c83594083e5c2", + "docs/validation/graytocolor-binding-axis-432-20261002/integer-binding-units.log": "cd005d448eba53a9176333b650bd173677429305", + "docs/validation/graytocolor-binding-axis-432-20261002/obligation-owner-controls.log": "00582b8ffc3a49b828746a6c89ce9e11cf40f25c", + "docs/validation/graytocolor-binding-axis-432-20261002/obligation-owner-final-controls.log": "0b316b37f8929b6ac5d7cfe4897b46444f886583", + "docs/validation/graytocolor-binding-axis-432-20261002/obligation-owner-r0.log": "2145b1d2ced90d2172fc6ada8270d7eb0a58ee9f", + "docs/validation/graytocolor-binding-axis-432-20261002/obligation-owner-receipt.rst": "205c0897a0b5c120f37a3921ead8130e174b7867", + "docs/validation/graytocolor-binding-axis-432-20261002/obligation-owner-scoped-anchor-controls.log": "d3fa09e91d1f8547c66b8b8f824003ae1b6a5133", + "docs/validation/graytocolor-binding-axis-432-20261002/original-assisted4-status-excerpts.log": "175ca08161c3f1d7bf01becba8e7b9bb2b3307b9", + "docs/validation/graytocolor-binding-axis-432-20261002/original-native-excerpts.log": "ca0577c346bd90ea0d38dec3e23f98896e2f78a9", + "docs/validation/graytocolor-binding-axis-432-20261002/receipt.rst": "b389c1e8ee25c3e306b147dba3bed609e29126db", + "docs/validation/graytocolor-binding-axis-432-20261002/source-diagnosis.log": "993b14cd14d00f0277e79548a9b5c968cd54af6c", + "docs/validation/graytocolor-binding-axis-432-20261002/working-pr-body.md": "9753e2e50ec9c0ca6fa609ac475e0f33768bc4f7", + "docs/validation/graytocolor-singleton-binding-432-20261002/composed-consumer-controls.log": "d3ccb44bc3ccb9f42cb742f277b6fc76324dbd0c", + "docs/validation/graytocolor-singleton-binding-432-20261002/declaration-plane-controls.log": "a433301bbf9cfb5785d383bc07d3324daeccb256", + "docs/validation/graytocolor-singleton-binding-432-20261002/existing-module-controls.log": "9667d629f817513009bd796ae467d01b7b1b3fec", + "docs/validation/graytocolor-singleton-binding-432-20261002/main-integration-controls.log": "d0297b69f77ba1aa5c4348f4250ae2a939cb941e", + "docs/validation/graytocolor-singleton-binding-432-20261002/main-integration-r0.log": "15c3286d4ac2f70b6dcf16e3c91b0eb917e494a7", + "docs/validation/graytocolor-singleton-binding-432-20261002/new-declaration-controls.log": "060f5d29d98d55c0a6d4cc9c58c06d1a90354489", + "docs/validation/graytocolor-singleton-binding-432-20261002/new-declaration-first.log": "5e643ab702c603d9983f876b4912890e0c625415", + "docs/validation/graytocolor-singleton-binding-432-20261002/original-unary-red.log": "e812aac7ed0e24d8a73f0eebaddc9ea52ed20b09", + "docs/validation/graytocolor-singleton-binding-432-20261002/pr-body.md": "91671e07993ba6832afcb1bf0ceff30426aa8b83", + "docs/validation/graytocolor-singleton-binding-432-20261002/receipt.rst": "8ba80ee347b0d249dcc9265a3ba0e5c2df5c44e7", + "docs/validation/graytocolor-singleton-binding-432-20261002/repair-receipt.rst": "623e1466840edb5c2db7c8a46e92ef430a781ea7", + "docs/validation/graytocolor-singleton-binding-432-20261002/run_original_r1.py": "aa9be27fd0e6a4a26e4602d21c4f3a615c92c5cb", + "docs/validation/graytocolor-singleton-binding-432-20261002/unary-repair-controls.log": "7e4399444af83f30fba9181ea752535ed876bb1d", + "docs/validation/graytocolor-singleton-binding-432-20261002/unary-repair-first.log": "1bade343800cc1fe87cabf07ec1cb7ce2d051112", + "docs/validation/graytocolor-singleton-binding-432-20261002/unary-repair-r0.log": "74527ac75c6684cb7e769d194b61b6d9f1ceea20", + "docs/validation/graytocolor-singleton-binding-432-20261002/unary-repair-r1-exact-context.log": "3905f55b0b48891088eae323df788626e318b157", + "docs/validation/graytocolor-singleton-binding-432-20261002/unary-repair-r1-verified.log": "42c3da804ff7f1b34fa9c05a2365d9155df23458", + "docs/validation/graytocolor-singleton-binding-432-20261002/unary-repair-r1.log": "c5d54a72c4acbf2f659a746e3068c5fc1749e3e8", + "docs/validation/gui-cold-connect-355.rst": "98f313c20f5ab9188a2323c98d0dceba3f086b2b", + "docs/validation/h001-stream-plane-427-20261002/GREEN-contract-execution.log": "ebfc2d086ec2ea2d5a2fcaca0c30da04901ce03b", + "docs/validation/h001-stream-plane-427-20261002/GREEN-declaration-controls.log": "d2f80a49e6ec8a6eb86d9b93976b3d4494a1cfac", + "docs/validation/h001-stream-plane-427-20261002/GREEN-final-stream-shard.log": "595b3c312e531b09b8da622dd21c4ae804888916", + "docs/validation/h001-stream-plane-427-20261002/GREEN-first.log": "f830c1d4c7dd05da3c208db9f64aa23c8099062f", + "docs/validation/h001-stream-plane-427-20261002/GREEN-main-flow.log": "0296896244a6781f2b4bbeafa1a8222d967d2522", + "docs/validation/h001-stream-plane-427-20261002/GREEN-newcase.log": "9ada5dbf7454d353b5954b1bb293fb177ab67cdd", + "docs/validation/h001-stream-plane-427-20261002/GREEN-strict-native-shard.log": "49ba595caf2d6d745d6c87fc761670ea5fd58b33", + "docs/validation/h001-stream-plane-427-20261002/R0.json": "65307891958cfa9481b0e46878c2b347f40635f5", + "docs/validation/h001-stream-plane-427-20261002/R0.resources.txt": "8e5a7e0ee4bafea2cabad98516779b5594b88aec", + "docs/validation/h001-stream-plane-427-20261002/RECEIPT.rst": "e7c23bb03e43e3bce335d87a67a65b61e56a9b72", + "docs/validation/h001-stream-plane-427-20261002/RED-original-producer.log": "e6c478378e9379bbcc2dbca993d583ef09366ff9", + "docs/validation/h001-stream-plane-427-20261002/RED-production-canonical-config.log": "9ff0a07a73d7da3034a69da7fcac52b69b71ed91", + "docs/validation/h001-stream-plane-427-20261002/RED-production.log": "e3499635b6e34bb4189e6af5de281bd09d3577a6", + "docs/validation/h001-stream-plane-427-20261002/RED-seeded-producer.log": "f41963696adfc11af114c316039dc76a83ef8416", + "docs/validation/h001-stream-plane-427-20261002/RED.log": "c8e645a8099eba6bf2b34c48e248aa2b00ea0e2b", + "docs/validation/input_preparation_installed_20261001.rst": "fd668663d02fcc31e76f0661ee852b581732b7c3", + "docs/validation/input_preparation_parent_20261001.rst": "7266a6373b2313f81a8d3efe25c686bd739e0a3e", + "docs/validation/input_preparation_quality_20261001.rst": "9a7577c747f7bdd337b00c51854239cbc779d913", + "docs/validation/intensity-domain-skill-20261001/VALIDATION.rst": "79179def6cc760fb8eef5ac983ea7d89233f21ed", + "docs/validation/intensity-domain-skill-20261001/quick-validate.log": "bcdbc5fc48236e8bd17b591cf96d1539ec249311", + "docs/validation/intensity-domain-skill-20261001/registry-g.log": "ff2bd9475e4f28261911659d749b3d68bfffd3f8", + "docs/validation/intensity-domain-skill-20261001/tests-a.log": "91538c9ea8f078438de3d4dded3e0c0c730a2c1a", + "docs/validation/intensity-domain-skill-20261001/tests-b.log": "6f700588a88decd597c101f43d545ddd4c05acbe", + "docs/validation/intensity-domain-skill-20261001/tests-c.log": "f75ecb8b7db4e678ef75bc2a672648947e2f190a", + "docs/validation/intensity-domain-skill-20261001/tests-d.log": "7a470d8eba4fabdd8a91f04c6549a00fdd809b7a", + "docs/validation/intensity-domain-skill-20261001/tests-f.log": "1e8e0d76c1cc064f766e28483d479e3c5fe52893", + "docs/validation/intensity-domain-skill-20261001/tests-h.log": "7f24538287d9bcb4a6f0809258e35dbc0ea3bed6", + "docs/validation/issue257-draft-pr.md": "e1686d61590ee8048e5a35bfda38345a0b42a306", + "docs/validation/issue257-implementation-20260930.md": "ad3f7b96849d003e395f94f5b491e386c2be0f6f", + "docs/validation/issue257-runtime-composition-20260930.md": "fb3cd4709dd200ea46f66af8e29dad7bf7ffcbb0", + "docs/validation/issue257-runtime-composition-receipt.json": "059ac40607469c5cd72bf08835806db3658c883e", + "docs/validation/issue257-source-investigation-20260930.md": "dce143b79b3fa34889753e10110a3209fb73b8a5", + "docs/validation/issue257-source-receipt.json": "66b2ba2fd530ed979e604cf98814b8e3503ed18d", + "docs/validation/issue257_projection_contract_20260930.rst": "ba3ef4654f918f442d890c862931c3559038247c", + "docs/validation/issue264-source-extension-20260930.md": "fb710b1c04340b2aeb74347b066e0cc2fbb1d983", + "docs/validation/issue264-source-receipt.json": "9f947c21efa5b3166be92bf9f193f21c2e2535cb", + "docs/validation/issue355-evidence/openhcs-checkpoint.log": "c026eea7f66748014c8875b63c365c18aeed4d50", + "docs/validation/issue355-evidence/openhcs-r0-checkpoint.json.gz": "073411e00640d06813a2a3e2bedc8658cb24ab53", + "docs/validation/issue355-evidence/openhcs-r0-checkpoint.resources.txt": "99f01ef1fe68eae1bae535ac25a2e9d6e864c7ce", + "docs/validation/issue363-binding-source-checks-20261001.tar.gz": "cd5ba424230a486e6cf52784fb9cd3decccd57cc", + "docs/validation/issue363-evidence/reused-extension.sha256": "cf06d3127bed5ff5e57ab607fcc29560aabd012e", + "docs/validation/issue363-source-checks-20261001.tar.gz": "6f791f1dd2816a2edd5d6d02dbd351f8508b7ae0", + "docs/validation/issue376-source-diagnosis-20261001.tar.gz": "3144f9bfe01e1e062189a22eece31a801ddcc2b0", + "docs/validation/kernel_readiness_352_20261001.rst": "70d479b0fd5df4ce8db03cde002e87398b0e3ca3", + "docs/validation/kernel_readiness_352_evidence_20261001.tgz": "3b6253415af10a0f2c4b1c51a62dfaa711c513d8", + "docs/validation/knowledge-lazy-conversion-376.rst": "90de971cf1dbc9b84563a65f52e295e99669d0db", + "docs/validation/knowledge-native-source-499-20261003.rst": "9d45a1ee8b0cff7caebb0d33a1699447e4357e45", + "docs/validation/knowledge-selected-source-tests.py": "80e42cf144ba9b2bf7d97239a77d8069ce2461ed", + "docs/validation/label_selection_346_20261001.rst": "a6a5b2136f8f2bcb17303d66578219b84d2ef999", + "docs/validation/label_selection_346_evidence_20261001.tgz": "bbc52ed08aee23142961577a49137289177ff9ca", + "docs/validation/matched_csv_356_20261001.rst": "d43796e0d10b6f1e1bd02f38ea045111d2cb1621", + "docs/validation/matched_csv_356_evidence_20261001.tgz": "8ef924ae21251564b09e6eb4e921c28025a97921", + "docs/validation/materialization_plumbing_384_20261001.rst": "8125d2b4b78be6d7340cbb73587fa322b3a40f46", + "docs/validation/mcp-affine-inspection-436-20261002/affinity-resident-controls.log": "48c8dfe4c8c07169f3b0394fadc933b3b782f43b", + "docs/validation/mcp-affine-inspection-436-20261002/bootstrap-source-admission-controls.log": "c8791381c728cf3338f9c9c4ff57e6d2fc9da68d", + "docs/validation/mcp-affine-inspection-436-20261002/generated-progress-controls.log": "d09b5decb56b37c50ef9f9cc8aad85d9438adc80", + "docs/validation/mcp-affine-inspection-436-20261002/mcp-boundary-controls.log": "78e5fca9be9a14c10624d564afbb3a0a1591ff95", + "docs/validation/mcp-affine-inspection-436-20261002/pr-body.md": "fc184908bdb460ae5326c2d9972b9d36d2601804", + "docs/validation/mcp-affine-inspection-436-20261002/probe_stdio.py": "6a613dc61b96989a7d6ffeedd902b0f484364b7a", + "docs/validation/mcp-affine-inspection-436-20261002/r0-original-pin.log.gz": "ae69d0108516a05e67a8891d058cf403869bd68e", + "docs/validation/mcp-affine-inspection-436-20261002/real-compiler-controls.log": "e3ac0c845a0a2f19c44503b2f458ba77c43099de", + "docs/validation/mcp-affine-inspection-436-20261002/real-compiler-qualified-controls.log": "7d03b121a128f59a3ab7fdd00013290526d171eb", + "docs/validation/mcp-affine-inspection-436-20261002/receipt.rst": "60a20bf0f1b1bbbb375b0b94658126438cfd6094", + "docs/validation/mcp-affine-inspection-436-20261002/resident-twelve-second-controls.log": "f110325bcbc772419b34226533e97eca348cdde6", + "docs/validation/mcp-affine-inspection-436-20261002/source_fixture.py": "9349baea4a39f24a8ebec86274d1be639dd8ffa5", + "docs/validation/mcp-affine-inspection-436-20261002/stdio-original-controls.log": "a8b232c06d384415b1a41c5aac9c05872be89a75", + "docs/validation/mcp-affine-inspection-436-20261002/stdio-twelve-second-controls.log": "be8d1aae6ca7beddbdf53e543b760f4bcdc8a27f", + "docs/validation/mcp-affine-inspection-436-20261002/stdio-whole-scope-controls.log": "0fe7b136aaeaeb203d79eb642b825dda9ef42cc6", + "docs/validation/mcp-affine-inspection-436-20261002/stdio-wire-controls.log": "6cd2c02206eba2f553d4d53beb403452a434d56c", + "docs/validation/mcp-affine-inspection-436-20261002/stdio-wire-qualified-controls.log": "d98953ed9ff290623d4a3d0cb013347d833bb6be", + "docs/validation/mcp-cold-inspection-feedback-436-20261002.rst": "bcdb35d51554cd8246194627ca86a4e04cee030b", + "docs/validation/mcp-jvm-lifetime-455-20261002.rst": "a2de5f5e5e35ca8becd5923f1e2226217a026376", + "docs/validation/mcp-jvm-lifetime-455-20261002/qualify.py": "3ca6aa903504b65f791aa8ad9d12d59b18b02d89", + "docs/validation/mcp-progress-ack-20261002.rst": "36502f5c9951b86ab4ee05b3c57bd4e6d3bcf685", + "docs/validation/mcp-roi-stream-progress-441-20261002.rst": "dafc566a99e1bd96d03b0807e534f8ac8bb4b32e", + "docs/validation/mcp-terminal-input-483/INSTALLED-ACCEPTANCE.rst": "4611caf7460256a3dfa3014c5730cb1da4fcef59", + "docs/validation/mcp_local_argument_errors_20260930.rst": "a472f70f7ce0ce3bc98d3f9ab862682339825ef4", + "docs/validation/memory-session-20260929.md": "cc804ea4bc7b010557350b47e5e1d529b1860485", + "docs/validation/memory_current_main_integration_20260930.rst": "36f91a6c989b9839439efff02a88f2a36cd494a4", + "docs/validation/metaclass-registry-022-publication-20261001/RECEIPT.rst": "c608530b3edbf9806dd3e8360e87d4d8bd731929", + "docs/validation/metaclass-registry-022-publication-20261001/SHA256SUMS": "bb77bc3ad2f9a85bc314fbde9446831fcba95e78", + "docs/validation/metaclass-registry-022-publication-20261001/artifact-verification.log": "7f9008bc07f37ba7075cc4449417b3c79670324a", + "docs/validation/metaclass-registry-022-publication-20261001/artifact-verification.resources.txt": "673479c067a09a0efb1d05608ff978c336392f68", + "docs/validation/metaclass-registry-022-publication-20261001/artifacts/metaclass_registry-0.2.2-py3-none-any.whl": "ecda496085ead4b4c0188582d42e536d9fac9ae0", + "docs/validation/metaclass-registry-022-publication-20261001/artifacts/metaclass_registry-0.2.2.tar.gz": "095e2ac191c71dcdde2c1805f1335f3ae8b9848c", + "docs/validation/metaclass-registry-022-publication-20261001/github-release.json": "6d60fa911f3a66aeeceec43c61515e82898996dc", + "docs/validation/metaclass-registry-022-publication-20261001/job-queued.json": "6aa5094ac2b96cfc2bcca31d35910463c872559c", + "docs/validation/metaclass-registry-022-publication-20261001/pending-deployments.json": "0637a088a01e8ddab3bf3fa98dbe804cbde1a0dc", + "docs/validation/metaclass-registry-022-publication-20261001/release-session.typescript": "86dcdfc564875a563715634ff7da30fafd325d86", + "docs/validation/metaclass-registry-022-publication-20261001/verify_publication.py": "156249134803eee969e40b4f7aab8853914775f0", + "docs/validation/metaclass-registry-022-publication-20261001/workflow-initial.json": "f292434cbbb523842ce073dc0472c29aa84d7e9c", + "docs/validation/metaclass-registry-022-publication-20261001/workflow-terminal.json": "d0afd19f4da005e8dba3b66439c126ffb77a9787", + "docs/validation/metaclass-registry-022-publication-20261001/workflow-watch.log": "2d4473c9b02781f20c300fbe14450296efc6a16f", + "docs/validation/metadata_namespace_327.rst": "dba3083cdf64896f54cb83de862e67e702c03e98", + "docs/validation/metadata_namespace_327_resources.rst": "7eabc5070dd6d9636dbde176a59685734d87f0df", + "docs/validation/metadata_namespace_327_source_evidence.tgz": "6c2dd1e466edab0ba8c4c08f71726c0fe6c62a2f", + "docs/validation/morphology-library-readiness-481-20261002.rst": "443efd1b3538b88b0ebb85b33d4cb9505b14251f", + "docs/validation/napari-source-channel-presentation-448-20261002/pr-body.md": "d104084329d7d572ab3eb429ff5349a0a7d35e3e", + "docs/validation/napari-source-channel-presentation-448-20261002/receipt.rst": "1443b98cc92103a8f5c419d497beaf4740b672b6", + "docs/validation/native-presentation-451452-20261002/complete-presentation-final.log": "b4294ffb8c1b8059a84a7ca5e34832dc8e1979d2", + "docs/validation/native-presentation-451452-20261002/complete-presentation-first.log": "13ec12bd66cc3f060d3fac66102772c2b6ff74bf", + "docs/validation/native-presentation-451452-20261002/complete-presentation-second.log": "d3f3814fcf525c7b59cbe8ff519fa1a137cbe2c6", + "docs/validation/native-presentation-451452-20261002/display-admission.log": "c7efc378b2f62c11d0362647f93d013d1e282a82", + "docs/validation/native-presentation-451452-20261002/final-original-r0.log": "8fbe80175c974b39d98541004507afecda45ab2e", + "docs/validation/native-presentation-451452-20261002/final-presentation-controls.log": "60f34afca89aaeea5f8269e4693820e937557e67", + "docs/validation/native-presentation-451452-20261002/mcp-controls-first.log": "d60dca98fa1c8f9d33e4ae55bcd0c688264cd2b3", + "docs/validation/native-presentation-451452-20261002/mcp-record-descent.log": "cce248ae4331931e8938bcbb5b7827829b76fb70", + "docs/validation/native-presentation-451452-20261002/merged-main-ast-closure.log": "398c2f39dff12417f1adc99bfe0463e140d2055f", + "docs/validation/native-presentation-451452-20261002/merged-main-native-controls.log": "794ae252346bd87ca00af7985a885e695c8d88af", + "docs/validation/native-presentation-451452-20261002/merged-main-original-r0.log": "e97ebc2ca9a087ce9f3315f199438f22fd9ebdd8", + "docs/validation/native-presentation-451452-20261002/merged-main-public-mcp-controls.log": "77ef1a0371ee947c085d8f223342a2424e1edc08", + "docs/validation/native-presentation-451452-20261002/native-and-consumer-controls.log": "7d3cd96dfe22e1981c94bd4dc3be441d7c84881e", + "docs/validation/native-presentation-451452-20261002/native-family-integration.log": "8e16398e47af72ade418de1a5ed0a4316affa200", + "docs/validation/native-presentation-451452-20261002/original-presentation-controls.log": "4133e2c792e90b055e4ab5001383721c94276307", + "docs/validation/native-presentation-451452-20261002/original-r0.log": "07c67e2f7ec251756c241799e37e64b365a57808", + "docs/validation/native-presentation-451452-20261002/ownership-after-linear.log": "bfd305b0fcb68189c4939c8b7c8d24b8da3f4163", + "docs/validation/native-presentation-451452-20261002/ownership-after.log": "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", + "docs/validation/native-presentation-451452-20261002/ownership-before.log": "b066ebfd6ec273ae288f40b305971780854fa425", + "docs/validation/native-presentation-451452-20261002/pr-body.md": "82af0a055c8bb88abc2b72b46441859e4d071cfc", + "docs/validation/native-presentation-451452-20261002/qualified-native-controls.log": "4f37dab52bc26f28faa09ab45d882789e4d684eb", + "docs/validation/native-presentation-451452-20261002/qualified-public-mcp-controls.log": "ed89ddddc5b870230938c75ada7e1699103598d3", + "docs/validation/native-presentation-451452-20261002/receipt.rst": "13699c70024cd6bb228ff81b8490950e5cf790ea", + "docs/validation/native-presentation-451452-20261002/root394-roster.json": "00a4f9bcc1606139e817a73a4ab269c6a94ef504", + "docs/validation/neurite-primary-diagnostic-consumption-410.rst": "50f1a71a1ff4723d2648108dcc79c5254496b1ad", + "docs/validation/neurite_body_gate_424_20261002/ARCHIVE-SHA256SUM": "123251e0380b8ec0a76911f9a8ee8df47fe1e8b1", + "docs/validation/neurite_body_gate_424_20261002/EVIDENCE.tar.gz": "ff3917c0599c6f37633d3f032fb4a891a4269490", + "docs/validation/neurite_body_gate_424_20261002/QUALIFICATION.rst": "85ce0adae2accf0ef76dda1b487c1f760c73c9eb", + "docs/validation/neurite_body_gate_424_20261002/RECEIVING.rst": "138d1a0968199bc5d2a240055501056fe320f33a", + "docs/validation/neurite_body_gate_424_20261002/SHA256SUMS": "b444c1d5cc323ca74a2d9bcf4ead1fd3ebb597ef", + "docs/validation/neurite_body_gate_424_20261002/SOURCE-SHA256SUMS": "33cffe0e8b85dd65406b280f5ae458838fd3693a", + "docs/validation/official30_qualification_330_20261001.rst": "203dc1eec4324d05990dfbd7d4b27b784200c987", + "docs/validation/official30_qualification_330_evidence_20261001.tgz": "35e7d883c07b4b2cd276a87da79c54504fa1669e", + "docs/validation/opera_producer_inventory_172.rst": "2fe74656b6005691c87ebe66937f238ff18e90a5", + "docs/validation/opera_producer_inventory_172_main8c512_evidence.tgz": "3714662c0ff6623ac90418878723397dd69bb8c7", + "docs/validation/opera_producer_inventory_172_source_evidence.tgz": "d3d8dbed17d33048b8c248f1ff16d7c54367ce08", + "docs/validation/opera_singleton_172.rst": "e2291c7b1f614f0dee2245605d5a12562cd4b87d", + "docs/validation/opera_singleton_172_source_evidence.tgz": "8508a1c9f5bd64d2d14c1aa550eee4b157ea553b", + "docs/validation/paired-feedback-integration-358.rst": "118e6a971f48597d9820ed9778d3fef7f2c8e804", + "docs/validation/paired-feedback-integration-evidence/PR358.md": "d6dc1ffdb7037b323db6709c47bb15a363e3e6f3", + "docs/validation/paired-feedback-integration-evidence/SHA256SUMS": "5c2d8540d797ed4df48df09ea92bf0d3bb558361", + "docs/validation/paired-feedback-integration-evidence/ZMQRuntime-PR13.md": "89c1836cea7304a96d574a3f539d6134f8ed8c90", + "docs/validation/paired-feedback-integration-evidence/openhcs-r0.json.gz": "5a65c3255e3757a13393bf6a0b862a2670b691f8", + "docs/validation/paired-feedback-integration-evidence/openhcs-r0.resources.txt": "50e3a38b684ec5cf468c95f79287bc5d2ecbdd66", + "docs/validation/paired-feedback-integration-evidence/openhcs-r0.stderr.txt": "f1d833af047295a8cff3918716824be9a7e40a00", + "docs/validation/paired-feedback-integration-evidence/zmqruntime-r0.json.gz": "63effef5b92ed9667ebfca816eb9f96d80de2cb9", + "docs/validation/paired-feedback-integration-evidence/zmqruntime-r0.resources.txt": "53dee34d792b997366eca667c34751aeb9042955", + "docs/validation/paired-feedback-integration-evidence/zmqruntime-r0.stderr.txt": "880125daf16a99d33387375630afce13ffb5da61", + "docs/validation/paired_field_csv_314_20260930/PREFLIGHT.json": "aff217990966162f3b5d3c4de2fdf3af63cbd43b", + "docs/validation/paired_field_csv_314_20260930/SHA256SUMS": "fb062f64846d2b00a87d715601545920617472d7", + "docs/validation/paired_field_csv_314_20260930/check-installed-parent.py": "1fcc3c9c73fc4294c744b5309eec437570cb615c", + "docs/validation/paired_field_csv_314_20260930/checkpoint.rst": "fd32fe1384d5acbd4add88469e36e31e91f95a1e", + "docs/validation/paired_field_csv_314_20260930/core-census.json.gz": "7f12ab38fdcfc4ee5e79cf377a8324c772dce884", + "docs/validation/paired_field_csv_314_20260930/core-census.log": "54631c41b6bea651c74badb8401f59de5a3b6539", + "docs/validation/paired_field_csv_314_20260930/core-overlay.json.gz": "c01643a81743a9e656b6cbc676d4ab29973b0ac7", + "docs/validation/paired_field_csv_314_20260930/core-overlay.log": "75a58cb2db6cffbc77a06f058a843303d770bac6", + "docs/validation/paired_field_csv_314_20260930/create-live-fixture.py": "bb5e57f0e4a83cb94d2b68cac3bc9883cc1a9185", + "docs/validation/paired_field_csv_314_20260930/effective-imports-error.log": "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", + "docs/validation/paired_field_csv_314_20260930/effective-imports.json": "00903619fd92078d72a4de29bd3f658c657a9028", + "docs/validation/paired_field_csv_314_20260930/final-policy-exporter.log": "3f3e8b5fa5cfe6d7e2edfeb2870d20be49032909", + "docs/validation/paired_field_csv_314_20260930/installed-parent-checkpoint.rst": "44d35e319273ca90492193daffb689d1c4596ad3", + "docs/validation/paired_field_csv_314_20260930/merged-main-census.json": "a32071ea499acd04f746817512244bf0fc717fdd", + "docs/validation/paired_field_csv_314_20260930/merged-main-census.log": "9692aea2a969ae42bc4e90f08c52d6586fa96089", + "docs/validation/paired_field_csv_314_20260930/merged-main-owner.log": "fc5fcfd85ffb0ebebabeb6913e116ba2d45310c5", + "docs/validation/paired_field_csv_314_20260930/merged-main-related.log": "a28aa8e39595aa46fac04e31b2c2e6f0951574e9", + "docs/validation/paired_field_csv_314_20260930/nra-original-resources.log": "f0cc5becc628c4fd4b30c10751eb527e3e7592e2", + "docs/validation/paired_field_csv_314_20260930/nra-original.json": "7a16c89d4e117048161cb379ddd1d294f07f0123", + "docs/validation/paired_field_csv_314_20260930/nra-owner-loop-resources.log": "6f6f8d83e84b11cf88c826f3dd0eb83cd0641cb4", + "docs/validation/paired_field_csv_314_20260930/nra-owner-loop.json": "f3042364c8ccc05dc70a5925d4a15f8b02512d60", + "docs/validation/paired_field_csv_314_20260930/owned-disposable.tar.gz": "3e6e11fdc9afa8d4d50dd7cff516ca598b8401b5", + "docs/validation/paired_field_csv_314_20260930/owner-census-delta.json": "5a074405bb39a6d5b308ad35ddf53879b82abb3e", + "docs/validation/paired_field_csv_314_20260930/owner-census-delta.log": "eaac50f19839e150c8c9053a2ca163460e220678", + "docs/validation/paired_field_csv_314_20260930/owner-fixed-corrected.log": "3a2515575f3e180191767347f13092bf85979adb", + "docs/validation/paired_field_csv_314_20260930/owner-fixed.log": "db16ae61129762a6c50d49d3fa5fe8c11086f4a9", + "docs/validation/paired_field_csv_314_20260930/ownership-receipt.rst": "23ab08d78a78ee265daf00e7950f56a602527204", + "docs/validation/paired_field_csv_314_20260930/pipeline-live-parent.py": "b30ec89770f11554167bc01677db5b3376c96121", + "docs/validation/paired_field_csv_314_20260930/policy-import.log": "90b2be632490c79a40082b6fde0f2e116dc0915a", + "docs/validation/paired_field_csv_314_20260930/pr-body.md": "4455fdfddc7a92fb55eceea7d85e856679a72eab", + "docs/validation/paired_field_csv_314_20260930/related-owner-contracts.log": "f1bdfead08c366c3c6da5f19e7b1e7420e22f50a", + "docs/validation/paired_field_csv_314_20260930/reproducer-original-built.log": "a1ca749ad0cbc1e8540f0a81502801c86bdb951b", + "docs/validation/paired_field_csv_314_20260930/reproducer-original.log": "3ae63c2bd511c69286dd71725d4aca371c202623", + "docs/validation/paired_field_csv_314_20260930/ruff-focused.log": "79cdd685850125f1097bff64d6a4e9da32cf6141", + "docs/validation/paired_field_csv_314_20260930/run-installed-parent.sh": "65efa139bdd8613cc5afdb520be836de465e12ce", + "docs/validation/paired_field_csv_314_20260930/source-anchors-base-empty-snapshot.log": "4a8efd805635061e236d81e3c764b5884895d3d8", + "docs/validation/paired_field_csv_314_20260930/source-anchors-base-verified.log": "379ff18faf5cf5cae18550e19ddf2320cf3f20e9", + "docs/validation/paired_field_csv_314_20260930/source-anchors-base.log": "42da0a71fd65ef9235a810fe96a3db01c6c8c72c", + "docs/validation/paired_field_csv_314_20260930/source-build.log": "1818a8deeae6a8d306b8166048f9f0a81beac546", + "docs/validation/persisted-result-reopen-398-source-20261001.tar.gz": "b0f33d3b7ca0608158a5756682d52d0dbb372f25", + "docs/validation/persisted-result-reopen-398.rst": "166be7e9d0f4f0cbacd2d424481b58c5d9386c0f", + "docs/validation/persisted_result_publication_134_20261002.rst": "fe8e06108650c74ccc3bd0e06fcd307881ab5942", + "docs/validation/physical-source-inspection-445-original-20261002.tar.gz": "aaa04a250a73824cd87fc5f5d1967e3a9c64c9a3", + "docs/validation/physical-source-inspection-445-source-20261002.tar.gz": "42e16130424793d78738636891550ea47e0a0385", + "docs/validation/physical-source-inspection-445.rst": "f78b558b9ba96e7edf929b79f11e8a466bc89cab", + "docs/validation/polynomial-library-readiness-473-20261002.rst": "a1b06c23d69dddd9d4522d342b5e98441b338786", + "docs/validation/preparation-admission-355.rst": "8b60f6c38207e46c9e8367c8860a98d1219301f7", + "docs/validation/preparation-admission-evidence/SHA256SUMS": "911a31629c7b34ed2861174802063538de243213", + "docs/validation/preparation-admission-evidence/after-checkpoint.log": "05dd20e693b5e70ea142ac3af17da5e0003fee43", + "docs/validation/preparation-admission-evidence/after-initial.log": "7a5874764f4b5f1e27e3269068dd6ded7c4db024", + "docs/validation/preparation-admission-evidence/after-paired-source.log": "e4d44069d8f9d557db8725849c2439acc6db3be6", + "docs/validation/preparation-admission-evidence/before-with-native-dependency.log": "1d31be76ba4ac92fda55fb77b52a5e75cb638a4c", + "docs/validation/preparation-admission-evidence/before.log": "2ce8ff176b5f84b7150c714a94ec854ce9619176", + "docs/validation/preparation-admission-evidence/manifest.json": "fad0f4df17f59340fac6342b39ff1f9d9a889098", + "docs/validation/preparation-admission-evidence/r0.json.gz": "1f2ad2510301a42496189222c9e3193aa4dd0989", + "docs/validation/preparation-admission-evidence/r0.resources.txt": "3f093ad5f5d386b2adbc8d5fcb981bdd998f5f16", + "docs/validation/preparation-admission-evidence/source-owner-profile.log": "508ba9d9d7a2ee780883e84d3bf7e9b7de52ba07", + "docs/validation/prepared_callable_signatures_419_20261002.rst": "e987dab3b91b681f9bcaefad18546202f7f5f1cb", + "docs/validation/primary_diagnostics_current_main_20260930.rst": "6a65fd57b81bdcc4ffe3877f6879e4240cfe160f", + "docs/validation/pyqt_zmq03_dependency_20260930/README.md": "2effcf4d718d46b9c97f22b514131450b8b4979b", + "docs/validation/pyqt_zmq03_dependency_20260930/normal-resolving-install.log": "6bc205ab6fca1a701c4fb986a16c0ff1c29a8be9", + "docs/validation/pyqt_zmq03_dependency_20260930/openhcs-consumers.log": "4f0395dee63513f763b6445d78fbda0dcd37ed65", + "docs/validation/pyqt_zmq03_dependency_20260930/pyqt-consumers.log": "90f8b4f16be29da332b468fee80890d99eb9975d", + "docs/validation/pyqt_zmq03_dependency_20260930/resolver-counterexample.log": "fe3b3ab9c647b8141bc6012a91df9c851c9c3470", + "docs/validation/r0-policy-fixture-invocation-20261002/CLI-SOURCE.py": "e601cb9ce16da8b4554eeaf87345094815f6e829", + "docs/validation/r0-policy-fixture-invocation-20261002/PLAN.rst": "a2ebe0339e946b1fc3aa831a5bfa6cd61f571f79", + "docs/validation/r0-policy-fixture-invocation-20261002/RECEIPT.rst": "f302bd77586f376c41f09495bb8fe0e980d7ad5f", + "docs/validation/registered-object-output-categorical03-20261003.tar.gz": "4b485a0e1f7bccccaa608b58fea9606436a67ae4", + "docs/validation/registered-object-output-categorical95-20261003.tar.gz": "563455dc8b3e66b8b2680cc83152d0b03bf5ae65", + "docs/validation/registered-object-output-installed-20261003.tar.gz": "22f2037597697d0208edb85c3989b04086b639d3", + "docs/validation/registered-object-output-public04-accepted-20261003.tar.gz": "c2d075d899036eaa6ebd576c96f188eb3442e9e5", + "docs/validation/registered-object-output-public95-20261003.tar.gz": "df9564734c84e14558ab87f99efed79e95242ead", + "docs/validation/registered-object-output-scalar04-20261003.tar.gz": "08f7147bd3115421b3e28831b7c3079cbabd6292", + "docs/validation/registered-object-output-source-20261003.tar.gz": "90abba6a5f6faa6f8418424eec0978d1b69475c7", + "docs/validation/registered-object-output-split95-20261003.tar.gz": "c99da45b3e7c8ba17f5015daea7461bf83113544", + "docs/validation/registered-object-output-working-20261003.tar.gz": "1997e9a84a3f43f323252bb4feccb9884cf7384d", + "docs/validation/registered_object_output_boundaries_20261003.rst": "836d3070db48f93600732f94581437b92ef49539", + "docs/validation/run_metadata_namespace_327.py": "c5811c1f78683a95cc6d259fedbb801cf929e8f0", + "docs/validation/runtime_bootstrap_20260930.md": "6ca6e264997e9f36e8aeb0e410cd95b1007ae52a", + "docs/validation/runtime_bootstrap_20260930/close-dependency-census.json": "cc2af236d1bab57fe723c2cf4daeae446fbfc319", + "docs/validation/runtime_bootstrap_20260930/close-dependency-census.txt": "f3641a0f906abd40c591c85a7daa1cc8027aeab5", + "docs/validation/runtime_bootstrap_20260930/close-source-census.json": "7ef88093cf291976df551e9173b2698c55697178", + "docs/validation/runtime_bootstrap_20260930/close-source-census.txt": "c4827ff49ef96da0166a922b79ea44815537cec4", + "docs/validation/runtime_bootstrap_20260930/dependency-census.json": "d5167ec1f06443e4c6478e888ddbdd68fa2c16ec", + "docs/validation/runtime_bootstrap_20260930/dependency-census.txt": "ea316a4d756eaeb11edb180f3f8635866c9432c2", + "docs/validation/runtime_bootstrap_20260930/effective-transport/checkpoint.md": "dff016387d14d72c15b7015d1e2c55262abb57bc", + "docs/validation/runtime_bootstrap_20260930/effective-transport/parent-witness.txt": "d2c25899c45e48be81858ccb0e67ec3259e193cd", + "docs/validation/runtime_bootstrap_20260930/effective-transport/source-tests-b.xml": "90b84977426308d0281a2b78377b64d7673d5249", + "docs/validation/runtime_bootstrap_20260930/effective-transport/source-tests.xml": "0d7564bcb3a2cf1397ceba15733b8530e0bd5d36", + "docs/validation/runtime_bootstrap_20260930/launch-plan-factoring/checkpoint.md": "570233428716019df69bc7214073856485539fb4", + "docs/validation/runtime_bootstrap_20260930/launch-plan-factoring/source-tests-b.xml": "3e319d1ba111689e13123a42286c3cb96969e5ec", + "docs/validation/runtime_bootstrap_20260930/launch-plan-factoring/source-tests.xml": "ef92711f5d7dbbb153ffd7f1a4c2b689b50023ca", + "docs/validation/runtime_bootstrap_20260930/native-attempt01/build.txt": "b0d6aff4fe7699a9a251162cc7ac2544e100e524", + "docs/validation/runtime_bootstrap_20260930/native-attempt01/checkpoint.md": "16cc31aaacbe213eb995eb15bd5cb58ac684f8c1", + "docs/validation/runtime_bootstrap_20260930/native-attempt01/journey.json": "cc9bf7bafbd142de0ae6cab64122f53c0bd02a7a", + "docs/validation/runtime_bootstrap_20260930/native-attempt01/mcp-stderr.txt": "ca517f933b9788cf76a8dc7c1e2a8c2b92426847", + "docs/validation/runtime_bootstrap_20260930/native-attempt01/pipeline.py": "2872892f8254ea6b88d0e1efbb7f7aecdb4c897b", + "docs/validation/runtime_bootstrap_20260930/native-attempt02/build.txt": "6b34b579eb65f33519a4ed908d064d368d9c814e", + "docs/validation/runtime_bootstrap_20260930/native-attempt02/checkpoint.md": "61518ced9e95c57d28f3d769c8557727f75d7774", + "docs/validation/runtime_bootstrap_20260930/native-attempt02/journey.json": "21668791073aac5fc4b78917e021850894d9624b", + "docs/validation/runtime_bootstrap_20260930/native-attempt02/mcp-stderr.txt": "592433884d187f541fca263a42228249f6976fbd", + "docs/validation/runtime_bootstrap_20260930/native-attempt02/owned/data/openhcs/logs/openhcs_zmq_server_port_5964_1790754393037041240.log": "97f28af65b2dc6adbbff3f06b4e6304a3312dc93", + "docs/validation/runtime_bootstrap_20260930/native-attempt02/owned/data/openhcs/logs/openhcs_zmq_server_port_5964_1790754393037041240.startup.jsonl": "367d70e1ddfc889f5f0f994f99d4d924aa0b7c55", + "docs/validation/runtime_bootstrap_20260930/native-attempt02/pipeline.py": "66468189fff3aea7e22a0347ed924c93b338f6d4", + "docs/validation/runtime_bootstrap_20260930/native-attempt03/build.txt": "c21bcf6297db51561a91ff8be44998fd544d13cb", + "docs/validation/runtime_bootstrap_20260930/native-attempt03/checkpoint.md": "c58f54d249ca8b9d5c5c9cbc0572127c3689c983", + "docs/validation/runtime_bootstrap_20260930/native-attempt03/inspect_volume_fixture_v2.py": "8d89630cc331f58a84b149bc1972f5b93fffb257", + "docs/validation/runtime_bootstrap_20260930/native-attempt03/journey.json": "b0bdf59c8196c7b5609ee47f383431b8f5205251", + "docs/validation/runtime_bootstrap_20260930/native-attempt03/mcp-stderr.txt": "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", + "docs/validation/runtime_bootstrap_20260930/native-attempt03/owned/data/openhcs/logs/openhcs_zmq_server_port_5965_1790758999303299469.log": "ee86a18d35f73c27cdfc223e527efb272502b8a1", + "docs/validation/runtime_bootstrap_20260930/native-attempt03/owned/data/openhcs/logs/openhcs_zmq_server_port_5965_1790758999303299469.startup.jsonl": "b0d01bd9b7e0b958163ff1ca96898faef47b5930", + "docs/validation/runtime_bootstrap_20260930/native-attempt03/select_volume_fixture_planes_v2.py": "9ffae9cc867d25517ec0db4190559af521067bca", + "docs/validation/runtime_bootstrap_20260930/native-owner-tests-b.xml": "373d16afb7d0c80dd072253c60c59acd20ee4499", + "docs/validation/runtime_bootstrap_20260930/native-owner-tests.xml": "d5f41caf8b56660204555746e1218f88c2545a7c", + "docs/validation/runtime_bootstrap_20260930/owner-factoring-checkpoint.md": "bd19c8cdedac8b5b58da18fa74ec1eb6d016b986", + "docs/validation/runtime_bootstrap_20260930/owner-factoring-ratchet-resources.txt": "1404eb0e547cac7f8fc12978786edfd3e764db87", + "docs/validation/runtime_bootstrap_20260930/owner-factoring-ratchet.json": "a3cfb179d7f4cf5dcf6d9cb0cf61a18fc50d0a67", + "docs/validation/runtime_bootstrap_20260930/owner-factoring-source-tests-b.xml": "65d87d542477d5e98698b03f20b4194b0cdca771", + "docs/validation/runtime_bootstrap_20260930/owner-factoring-source-tests.xml": "0334221ad8d6d7f7a3559ddc977d5295792a53fb", + "docs/validation/runtime_bootstrap_20260930/paired-native-owner-checkpoint.md": "1df91d486b53b15dd0bc70589ef953e4b7e35356", + "docs/validation/runtime_bootstrap_20260930/paired-owner-source-tests.xml": "54929c5cd715cbfdbc4b4aa00405b9a1ac77de03", + "docs/validation/runtime_bootstrap_20260930/paired-pin-handoff.md": "6ba5105c6c2db19804d729acdb746626dfe0386c", + "docs/validation/runtime_bootstrap_20260930/paired-pin-source-resources.txt": "df43decc13b9b8cba6d081b15f7f97b7cd39e913", + "docs/validation/runtime_bootstrap_20260930/paired-pin-source-tests.xml": "4303fdfbff3a99dc9ceb244dad86f850788b5af1", + "docs/validation/runtime_bootstrap_20260930/parent-main-integration/checkpoint.rst": "a47d6a1c7018214abba7ab82827d445f88c5e0a9", + "docs/validation/runtime_bootstrap_20260930/parent-main-integration/native-build.log": "330b8da57c1e9b23ba9b11a775d9372b0620a402", + "docs/validation/runtime_bootstrap_20260930/parent-main-integration/ratchet-resources.txt": "075cfdf68740c526762a59d5c084e87c9ecbcfef", + "docs/validation/runtime_bootstrap_20260930/parent-main-integration/ratchet.json": "c4ee3670c841481bb3d934d20344425783b13ed5", + "docs/validation/runtime_bootstrap_20260930/parent-main-integration/run-source-check.sh": "fe37cdd037920a6455afd274ca4e3fb4907bd64d", + "docs/validation/runtime_bootstrap_20260930/parent-main-integration/source-tests-native-built.log": "273ca3c96373fdecaecd5ed80d0b17a229880815", + "docs/validation/runtime_bootstrap_20260930/parent-main-integration/source-tests-native-built.xml": "9152ebb3b6799d5c1d5925868f7f2e01c3890cb4", + "docs/validation/runtime_bootstrap_20260930/parent-main-integration/source-tests.log": "edfbce2c41d567076236915d8fc0ea032e0c6ffd", + "docs/validation/runtime_bootstrap_20260930/parent-main-integration/source-tests.xml": "18f86e293c6ab762368b0f395b716a87abce2594", + "docs/validation/runtime_bootstrap_20260930/pr-body.md": "ccee14e64209f0d9cea2c0fff13a5d39fa2d2fda", + "docs/validation/runtime_bootstrap_20260930/pre-spawn-rollback/checkpoint.md": "321d259f44da9847f72a805085e37f8a18d9e571", + "docs/validation/runtime_bootstrap_20260930/pre-spawn-rollback/combined-resources.txt": "9ed8a88f427a2ca9f5f6ff8adf5a77808836c8a6", + "docs/validation/runtime_bootstrap_20260930/pre-spawn-rollback/combined-tests.txt": "017a6cb772097bb700fef2e198a9f6c578108f42", + "docs/validation/runtime_bootstrap_20260930/pre-spawn-rollback/dependency-initial-resources.txt": "46728632f1e8a841633fd94f7cc8c5041a0e34b4", + "docs/validation/runtime_bootstrap_20260930/pre-spawn-rollback/dependency-initial-tests.txt": "961d59015dca03b62e11d3e7bf6093cc9abd1fcf", + "docs/validation/runtime_bootstrap_20260930/pre-spawn-rollback/dependency-resources.txt": "cc0ae92337496dd2b92a59067353f0946f894ca6", + "docs/validation/runtime_bootstrap_20260930/pre-spawn-rollback/dependency-tests.txt": "e271aec270b4cef4168cf7e41bbf232b204d2807", + "docs/validation/runtime_bootstrap_20260930/pre-spawn-rollback/zmq-pr-body.md": "214b20a269b2bcfd2784e745d8937546d11809c1", + "docs/validation/runtime_bootstrap_20260930/projection-attempt03-preparation.md": "934e797f60d3d89a830fd8ab7b04f0b126dd57c8", + "docs/validation/runtime_bootstrap_20260930/r0-ratchet-before-resources.txt": "38a357812ddd7963c42e4b6b40a0249fc17893d5", + "docs/validation/runtime_bootstrap_20260930/r0-ratchet-before.json": "280dafe2978b56a7819c10a9d7af27f17288e9d7", + "docs/validation/runtime_bootstrap_20260930/reserved-child-takeover/after-resources.txt": "cf24b472a39330f25da9b8a1040d34ce1f917cc5", + "docs/validation/runtime_bootstrap_20260930/reserved-child-takeover/after-tests.log": "38c39ad4f3a2a649f2455460ffcdc38137729c29", + "docs/validation/runtime_bootstrap_20260930/reserved-child-takeover/after-tests.xml": "cf6d08018f12208f03046d4a41fe075d76d41ba2", + "docs/validation/runtime_bootstrap_20260930/reserved-child-takeover/before-corrected-resources.txt": "8c45933a072db3415df96d3cdf016a3dea2ae6ae", + "docs/validation/runtime_bootstrap_20260930/reserved-child-takeover/before-corrected-tests.log": "16925a458c5d1f09155b6af76cea189c73e74b6a", + "docs/validation/runtime_bootstrap_20260930/reserved-child-takeover/before-corrected-tests.xml": "e78c93b030fece092757149383231edb16d18288", + "docs/validation/runtime_bootstrap_20260930/reserved-child-takeover/before-resources.txt": "a9fe891dd334a9cf2ddeea7cb73eea164b547aad", + "docs/validation/runtime_bootstrap_20260930/reserved-child-takeover/before-tests.log": "dd4c18fc74a01ff023a12eedb64dd2e27019b08d", + "docs/validation/runtime_bootstrap_20260930/reserved-child-takeover/before-tests.xml": "db7f6d5c9fb6c70308fb8dab21ef6f121feb566c", + "docs/validation/runtime_bootstrap_20260930/reserved-child-takeover/checkpoint.rst": "6ce1e1f00a24997a547cd0f7aad7845df475b26e", + "docs/validation/runtime_bootstrap_20260930/reserved-child-takeover/pr256-body.md": "c773b5b3196af0207655b244db1d8ea43d5e8ec8", + "docs/validation/runtime_bootstrap_20260930/reserved-child-takeover/pr9-body.md": "4fdbff385553cb9327a41889f8df7c531a415683", + "docs/validation/runtime_bootstrap_20260930/source-census.json": "78310e9c93058de0c820b2998f7c3d3bf1328521", + "docs/validation/runtime_bootstrap_20260930/source-census.txt": "8eda50d914bb26deb3fd3919d558022009f08556", + "docs/validation/runtime_bootstrap_20260930/source-close-122-resources.txt": "f8747c95f5a762ac66b3e1ebb81c73579b022528", + "docs/validation/runtime_bootstrap_20260930/source-close-122.txt": "275d1d8ef135185545d8609d39dd20fd538a4884", + "docs/validation/runtime_bootstrap_20260930/source-close-final-resources.txt": "ce8669d67f1ab48ce3920bbdd461fe8a22e88137", + "docs/validation/runtime_bootstrap_20260930/source-close-final.txt": "38db049079337c42d13ea6a2ba5076ff7dcc32e1", + "docs/validation/runtime_bootstrap_20260930/source-close-fixtures-resources.txt": "2a2b4e7d2268af365a9ddf161a213f4b0d21a54a", + "docs/validation/runtime_bootstrap_20260930/source-close-fixtures.txt": "c9df67d54d6541585ff62b611a14f302510dfc5c", + "docs/validation/runtime_bootstrap_20260930/source-close-initial-resources.txt": "f5c8d6f803dc6a5253e33b29e1354c7a12e3832d", + "docs/validation/runtime_bootstrap_20260930/source-close-initial.txt": "2c499ebc6892c48a3f543e99a6698956e2f86114", + "docs/validation/runtime_bootstrap_20260930/source-close-resources.txt": "aa50b3ffb9046f5a60e9b1b5756b8b89188d5d00", + "docs/validation/runtime_bootstrap_20260930/source-close-wire-fixtures-resources.txt": "2c79f55227db91a82c7703b5812728fa8440af42", + "docs/validation/runtime_bootstrap_20260930/source-close-wire-fixtures.txt": "1157b4a179a25dc309ea8aa747cfa7b75b6f7a42", + "docs/validation/runtime_bootstrap_20260930/source-close.txt": "5756a47cf9bd82ca3a0baf948f22d858182750c7", + "docs/validation/runtime_bootstrap_20260930/source-hardening-resources.txt": "3c94c2faa1461bd116e019854df810402d4b3e6b", + "docs/validation/runtime_bootstrap_20260930/source-hardening.txt": "07107201b0537718e5e1083709659a9243d87a50", + "docs/validation/runtime_bootstrap_20260930/source-test-resources.txt": "c54f1ee47e2b00697eeadfecd9fadd620c3ccad4", + "docs/validation/runtime_bootstrap_20260930/source-tests.txt": "c544751de60fc35cd50f81d3f5c67f9672b91915", + "docs/validation/runtime_bootstrap_20260930/viewer-main-source-resources.txt": "2576dca7141c581017988b6e9e7d5aa09283e220", + "docs/validation/runtime_bootstrap_20260930/viewer-main-source-tests.xml": "b79b9b01cd2f8e84d52c4b2005a32f16c5ae8fab", + "docs/validation/runtime_bootstrap_20260930/viewer-merged-source-resources.txt": "b657651f041de5a4589cff4e9ba29905e070f517", + "docs/validation/runtime_bootstrap_20260930/viewer-merged-source-tests.xml": "8787d51722de8d08ab3b4b311ab94110afeefa1e", + "docs/validation/runtime_fixture_sync_387_20261001/RECEIPT.rst": "4b5fcae674b70309072814bc91573ef97e21b74b", + "docs/validation/runtime_fixture_sync_387_20261001/SHA256SUMS": "a4783d90dc58f553f5429a0126b7f21598011128", + "docs/validation/runtime_fixture_sync_387_20261001/pytest_evidence.tgz": "182b99251992b19fe5546663ddab183bcc7193ca", + "docs/validation/runtime_owner_sync_323_20261001.rst": "9179812109e25e5c122e08bf7a635db7e8e5b5b8", + "docs/validation/runtime_owner_sync_323_evidence_20261001.tgz": "65704fb6b7ddab59afbf83525898bcbf370598ff", + "docs/validation/s1_typed_viewer_20261001/NATIVE-RECORDS-ARCHIVE-MEMBERS.txt": "241a927f9dc976613330da2536c8df4944b0ecf2", + "docs/validation/s1_typed_viewer_20261001/NATIVE-RECORDS-ARCHIVE-SHA256SUM": "06992c747a033d5e3595bdaa7ae9132a5640b580", + "docs/validation/s1_typed_viewer_20261001/NATIVE-RECORDS-INCREMENT.rst": "de67df9abed42e110ffdcc80e5590244bda047e8", + "docs/validation/s1_typed_viewer_20261001/NATIVE-RECORDS-RECEIVING.rst": "b6fafe2874d43a55a8d16795765729cdcc8d22bf", + "docs/validation/s1_typed_viewer_20261001/NATIVE-RECORDS-SHA256SUMS": "d130c9872fcdda97b79a7689668259cf561d6524", + "docs/validation/s1_typed_viewer_20261001/NATIVE-RECORDS-SOURCE-SHA256SUMS": "fc4c7bd2a758700fa5ca6b68884d092f8fe45b6a", + "docs/validation/s1_typed_viewer_20261001/PUBLISHED650-QUALIFICATION.rst": "fc0cad39f8b279c441f1d5903aa63e400adf9639", + "docs/validation/s1_typed_viewer_20261001/RECEIVING.rst": "2eeb4afd4f1ad951f42a63b850c4327b89becfdc", + "docs/validation/s1_typed_viewer_20261001/STATE-INCREMENT.rst": "2592bb0983f7f29bf26c9515aa7f7723ee7afb38", + "docs/validation/s1_typed_viewer_20261001/checkpoint-framed-resources.txt": "b6d5c0cd5d9529f900d1d5c8960eb94a95b0a160", + "docs/validation/s1_typed_viewer_20261001/checkpoint-framed.log": "595135b4bc6fa4ac8ef28ea2ed9b72915ed405c8", + "docs/validation/s1_typed_viewer_20261001/checkpoint-framed.xml": "b89f86bb89b3d2a4dd0eb065a10003b16a174434", + "docs/validation/s1_typed_viewer_20261001/checkpoint-resources.txt": "8b74c1b4b38ceb069431f4d19f2c8b4b08e422e7", + "docs/validation/s1_typed_viewer_20261001/checkpoint.log": "77f19a02ed286ca18f3dcc4c91213641db4ffdb6", + "docs/validation/s1_typed_viewer_20261001/checkpoint.xml": "52f42d6489e3392e16eddc84afbd3d60ea31ed76", + "docs/validation/s1_typed_viewer_20261001/native-records-evidence.tar.gz": "9a6fd1d3a408c3efab582acab70e2dc760634054", + "docs/validation/s1_typed_viewer_20261001/original-state-omission-resources.txt": "dcf3775991d891ddfd8006902ac9776bb1a78a45", + "docs/validation/s1_typed_viewer_20261001/original-state-omission.log": "68535cdbf6ed7a176debc4603f78cc2befaaab94", + "docs/validation/s1_typed_viewer_20261001/original-state-omission.xml": "e5d6625b1ac43f147a6ae650dde362ab96c6b68a", + "docs/validation/s1_typed_viewer_20261001/post406-installed-state.json": "628ec0b4a20717cc94c9af00faef6e7e9fa9d0d1", + "docs/validation/s1_typed_viewer_20261001/published34-r0-monitor.log": "862ebc7495da714a4cddc3b20e0cb8724c2a126d", + "docs/validation/s1_typed_viewer_20261001/published34-r0-resources.txt": "e35aaf44a2d6439971ac250f2ede4649b2f326a6", + "docs/validation/s1_typed_viewer_20261001/published34-r0.json.gz": "42fa248963e850084a5e15a00954bca9e7ef1b11", + "docs/validation/s1_typed_viewer_20261001/published34-r0.stderr.txt": "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", + "docs/validation/s1_typed_viewer_20261001/published650-behavior-guard-resources.txt": "ce4dc0f401c957ab381038d2402d32a2e9a7fd90", + "docs/validation/s1_typed_viewer_20261001/published650-behavior-guard.log": "d36db5155fe2b153859255d0c6ba08d73b361575", + "docs/validation/s1_typed_viewer_20261001/published650-behavior-guard.xml": "1173ea663eedabd0a13af50f54a59620b6243e03", + "docs/validation/s1_typed_viewer_20261001/published650-r0-resources.txt": "100b169ab77c83914b616b2335bec17cda96dd8d", + "docs/validation/s1_typed_viewer_20261001/published650-r0.json.gz": "d711494afb20f3081624d06a520b4b511828d03f", + "docs/validation/s1_typed_viewer_20261001/published650-r0.stderr.txt": "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", + "docs/validation/s1_typed_viewer_20261001/reproduce_original_state_omission.py": "f6801b08faa0e20914c812766714ec7d680c6e5a", + "docs/validation/s1_typed_viewer_20261001/state-increment-corrected-resources.txt": "bf2c743000a61af3d3712d21b312022399b472d6", + "docs/validation/s1_typed_viewer_20261001/state-increment-corrected.log": "afd659f67964a7ac0e289385d687950d50d18a83", + "docs/validation/s1_typed_viewer_20261001/state-increment-corrected.xml": "1ea8d5b8d00f106b0e22ddc67aea1e023b4b47c4", + "docs/validation/s1_typed_viewer_20261001/state-increment-resources.txt": "74a66dac08309d0cea9e3a13ba676a2f3797d6af", + "docs/validation/s1_typed_viewer_20261001/state-increment.log": "2be4a6e85ccaed4e680e28418d78d79cd97cd3d5", + "docs/validation/s1_typed_viewer_20261001/state-increment.xml": "18375bae4ff5d70b76fa0ea4e0341e4eb4272268", + "docs/validation/s1_typed_viewer_20261001/state294-r0-monitor.log": "f7a60b9dc5575d66d4ded85934a7eae7bfc3285a", + "docs/validation/s1_typed_viewer_20261001/state294-r0-resources.txt": "a240c3958d4b8a51ad51ab772e27deffde951c6c", + "docs/validation/s1_typed_viewer_20261001/state294-r0.json.gz": "60994794eb31bacc43cf67977c46aceb7d561be8", + "docs/validation/s1_typed_viewer_20261001/state294-r0.stderr.txt": "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", + "docs/validation/s1_typed_viewer_20261001/state294-r1-monitor.log": "0209bee5e9027650cf1273db7cfcd0bd9d675f51", + "docs/validation/s1_typed_viewer_20261001/state294-r1-resources.txt": "ef338765487ba9891073e7729f565ae96dbe68cf", + "docs/validation/s1_typed_viewer_20261001/state294-r1.json": "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", + "docs/validation/s1_typed_viewer_20261001/state294-r1.stderr.txt": "ab09c3ebb1356cd02a2ddf9e4f13282eeac2ba68", + "docs/validation/saved-label-plane-domain-419-20261002/COMMANDS.rst": "385c712a83a98d616761dd5eda82bb2b097924b0", + "docs/validation/saved-label-plane-domain-419-20261002/RECEIPT.rst": "492dc048d7585598757432b7c6a37329e0a1c463", + "docs/validation/saved-label-plane-domain-419-20261002/adjacent-controls.log": "217db116620257f55d58b81663c88e8785c1d25b", + "docs/validation/saved-label-plane-domain-419-20261002/adjacent-controls.xml": "88de603f5597a8165d44f967a139af49893a56e9", + "docs/validation/saved-label-plane-domain-419-20261002/distance-original-red.log": "0935b82fd8a52ae802d79f7507e85f19bb395ed1", + "docs/validation/saved-label-plane-domain-419-20261002/distance-original-red.xml": "1b50b9250c47c06161de8b20dfa255c903125e97", + "docs/validation/saved-label-plane-domain-419-20261002/final-owner-controls.log": "0633afc7cd25a953f56f56666c48cb48b58abf4f", + "docs/validation/saved-label-plane-domain-419-20261002/final-owner-controls.xml": "1af5068e03712169271c089ea5a4f69391f7bbe8", + "docs/validation/saved-label-plane-domain-419-20261002/geometry-and-runtime-values.log": "85672c40e5d041b16c369b9a0f3f20adf45312a8", + "docs/validation/saved-label-plane-domain-419-20261002/geometry-and-runtime-values.xml": "b711aac3c208935e3eebd695afc6bc36f6ed012e", + "docs/validation/saved-label-plane-domain-419-20261002/geometry-focused-corrected.log": "67961abd2ebf88f506d7b181fd99b564f0764d94", + "docs/validation/saved-label-plane-domain-419-20261002/geometry-focused-corrected.xml": "cf1cbc8bf10904f6fbdb82255c9573b255c86e2c", + "docs/validation/saved-label-plane-domain-419-20261002/geometry-focused.log": "bd808eea17d64a6f1d16ed412a90845d629b04d2", + "docs/validation/saved-label-plane-domain-419-20261002/geometry-focused.xml": "2c5220952da545f94ce019b0db3cbbbc2701a1d2", + "docs/validation/saved-label-plane-domain-419-20261002/metadata-owner-proposal.patch": "a58137aae0915104f871e92176478f57eef74dac", + "docs/validation/saved-label-plane-domain-419-20261002/original-red.log": "f0a4d63621aa6fbbd7af1f2223a40c0329a56c43", + "docs/validation/saved-label-plane-domain-419-20261002/original-red.xml": "0ea07a91ad7faa482ebde026158d10888e1f6e01", + "docs/validation/saved-label-plane-domain-419-20261002/projection-owner-controls.log": "94c17b5c289c2ebbd8ed5c6a663b7952d7b78193", + "docs/validation/saved-label-plane-domain-419-20261002/projection-owner-controls.xml": "edffba841649a5809631acfcf5b10d3941f013c7", + "docs/validation/saved-label-plane-domain-419-20261002/r0-final-production.log.gz": "0afc257ae98b5428acc92f911c12350df879c1d1", + "docs/validation/saved-label-plane-domain-419-20261002/r0-original-pin.log.gz": "e02043a93a49a6de57bab11f5fd1b7f86c3bbf25", + "docs/validation/saved-label-plane-domain-419-20261002/r0-unapplied-metadata-proposal.log": "53c0e7f55a14d605b2a68715a0bfacbf4ee2f240", + "docs/validation/saved-label-plane-domain-419-20261002/r0-unapplied-proposal-commit.log.gz": "16cf8a7b0eed1052267dae69040d95e649c9fe4d", + "docs/validation/selected-discovery-evidence/INTEGRATION-R0-UNCOMPRESSED-SHA256SUMS": "60ab702ea6a83acb95041aa3e63d6f9b06ab0686", + "docs/validation/selected-discovery-evidence/INTEGRATION-SHA256SUMS": "a82e500d504cdd966ae453e82e483d2a727e1634", + "docs/validation/selected-discovery-evidence/POST409-R0-UNCOMPRESSED-SHA256SUMS": "a2982775f4cd29ae57ae917beb12cb240c4019a0", + "docs/validation/selected-discovery-evidence/POST409-SHA256SUMS": "6f7204ea783b9c90b93eb5581beb66381c355045", + "docs/validation/selected-discovery-evidence/R0-UNCOMPRESSED-SHA256SUMS": "53874490e7062142ca36733e8cd97d7a69a7d95b", + "docs/validation/selected-discovery-evidence/SHA256SUMS": "9c09ab29b9d225c295cf5b574b14ec7e726c42ba", + "docs/validation/selected-discovery-evidence/batched-source.log": "49ea4edb76ce4e644624e66a13b4b2e7e5d95bc0", + "docs/validation/selected-discovery-evidence/dependency-r0-final.json.gz": "a9ec6a671edb44f90c00b4f966e09e0d89d43887", + "docs/validation/selected-discovery-evidence/dependency-r0-final.resources.txt": "0c3c98ac15d07852f987d3542c5b74160c8cb2c4", + "docs/validation/selected-discovery-evidence/dependency-r0.json.gz": "e272aac8d8cf7a791c19a1fb238801a988a0cba5", + "docs/validation/selected-discovery-evidence/dependency-r0.resources.txt": "793a1a8fe61e2737f243b83342e81c76deb0bdad", + "docs/validation/selected-discovery-evidence/final-cold-profile.log": "8290ce24ed1121d87377ce042caf2a0ca808057a", + "docs/validation/selected-discovery-evidence/final-focused-source.log": "18a634a74c7cc7e56acbd2a3cab7f1ca1299d123", + "docs/validation/selected-discovery-evidence/final-owner-source.log": "2fc3df39ad80b65d98e701f1008262711abbab76", + "docs/validation/selected-discovery-evidence/final-source.log": "3113156d5731e814300e01facaf145da706772a0", + "docs/validation/selected-discovery-evidence/first-source.log": "0bfd077db4a182f2260a745f248bf8916e31f063", + "docs/validation/selected-discovery-evidence/integration-376-commands.sh": "c2477afd71c77dc44051383e624fdc2b46bbd26f", + "docs/validation/selected-discovery-evidence/integration-dependency-r0.json.gz": "32cc0253c8443f42edc87b6836331d345341495c", + "docs/validation/selected-discovery-evidence/integration-dependency-r0.resources.txt": "a4009ff8c023c68d1b96ad1c7db551975dd76032", + "docs/validation/selected-discovery-evidence/integration-focused-source.log": "5ac12e45098d2dc19b73590396760adc290ba26e", + "docs/validation/selected-discovery-evidence/integration-openhcs-r0.json.gz": "5a0bfb4d8da3bd5caec78476a7f2cfe7f292fdc9", + "docs/validation/selected-discovery-evidence/integration-openhcs-r0.resources.txt": "b6636aee03c140cce4b459dd40330a85eeabc02e", + "docs/validation/selected-discovery-evidence/openhcs-r0-final.json.gz": "f0c8ef0b51f6c7e515643b23e7c39029c9107746", + "docs/validation/selected-discovery-evidence/openhcs-r0-final.resources.txt": "d8426f591c0ab26776f5552c3210a50094a79ea1", + "docs/validation/selected-discovery-evidence/openhcs-r0-rejected-24a.json.gz": "1b827134822a8c7bd98b9efcc714f6c799ececd8", + "docs/validation/selected-discovery-evidence/openhcs-r0-rejected-24a.resources.txt": "954f53d05babb60b291f94aad2fb431a37a50671", + "docs/validation/selected-discovery-evidence/openhcs-r0-rejected-5fd.json.gz": "c2efbf879d45865162dc6b9251e7b95a18bb09e0", + "docs/validation/selected-discovery-evidence/openhcs-r0-rejected-5fd.resources.txt": "e0de745d2076531d1262f875a0fe1d1e27240eb0", + "docs/validation/selected-discovery-evidence/owner-source.log": "9eacde31351bcf8c9441d52cd955149e1959d79d", + "docs/validation/selected-discovery-evidence/post409-focused-source.log": "ed8f481e215212d842e746a92da29e279503fd7a", + "docs/validation/selected-discovery-evidence/post409-openhcs-r0.json.gz": "28282d69330b9a72bc433ee010907e3321df81bc", + "docs/validation/selected-discovery-evidence/post409-openhcs-r0.resources.txt": "b6b29f817cca798b6a9b9bced7a4c81f89920de2", + "docs/validation/selected-discovery-evidence/real-callable-newcase.log": "b8242db498b0e06ec73ebf6969e36604b3e6475c", + "docs/validation/selected-discovery-evidence/retrieval-admission-newcase.log": "c3fd2c3424ec4cd778dc34299ac7f849a7812d21", + "docs/validation/selected-discovery-evidence/second-source.log": "1e857c7fe4cca32c83e3c755434f4485cbcfd11b", + "docs/validation/selected-discovery-evidence/selected-cold-profile.log": "5ad0bba48e610bb72bbea10e98416e5503fb5aea", + "docs/validation/selected-discovery-evidence/source-roundtrip-newcase.log": "055310a63fdd0179556bceb57ed65c0242ccdde0", + "docs/validation/selected-discovery-release-022-SHA256SUMS": "2b860575ffbd1824e023d30f8bbd1d0331b5a19c", + "docs/validation/selected-discovery-release-022-dependency-r0.json": "f94be654ceabe530c9e9d1e9da090695b41caf0e", + "docs/validation/selected-discovery-release-022-dependency-r0.resources.txt": "0ed151cf0d9223295ad2bf784611ad0636c024e7", + "docs/validation/selected-discovery-release-022-probe.json": "3ba6ce3299d8b79bcb2286bb8cee1c0a7fa0f7b4", + "docs/validation/selected-discovery-release-022-probe.resources.txt": "6ab52a7ac2358ce27eb00b2ac5877ba1ad4231d5", + "docs/validation/selected-discovery-release-022-source.log": "a01d698b541773b2eb42900a0498c9d61e8ff70e", + "docs/validation/selected-discovery-release-022.rst": "b0dd34398fba8d42e12bd24791bf6a4a5e7bca77", + "docs/validation/selected_plane_reopen_stream_20261001/ARCHIVE_COMPARE.txt": "81fb9e59f5e1631ae07e37084d63c963ba8dfe07", + "docs/validation/selected_plane_reopen_stream_20261001/ARCHIVE_SHA256.txt": "633fea014f2c1034c83388825dff1833cf46ef45", + "docs/validation/selected_plane_reopen_stream_20261001/MANIFEST.json": "279f3d06cead04b99703bd40cff90ff947ec6dec", + "docs/validation/selected_plane_reopen_stream_20261001/RAW_EVIDENCE_SHA256.txt": "8b4193e2d0be8039f4fcfaab3c51a0c914a42418", + "docs/validation/selected_plane_reopen_stream_20261001/RECEIPT.rst": "fdbc750541d375d0a3a684760094b21615a6495e", + "docs/validation/selected_plane_reopen_stream_20261001/pr.md": "c2aea34186795575d3d8df8203374d74c628a24b", + "docs/validation/selected_plane_reopen_stream_20261001/raw-evidence.tar.gz": "45169b39d8b20c583d8596c437c410d242b19ba8", + "docs/validation/selected_plane_stream_materialization_20261001/RECEIPT.rst": "5a4f1fbc52b9ec93947210eb4c552f9bfe2ae629", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-cellprofiler-stack-resources.txt": "e1a283237d800b18ab4a1b65792e78c9d4ff278c", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-cellprofiler-stack.log": "bc31f99cc1de8fc0fa90c9a1689affe1b25e3de6", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-cellprofiler-stack.xml": "1d4c75f08c042b7bd161141d05c2a51d635b89b9", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-current-focused-resources.txt": "b84393e1e529b0e2baaa1c2a47820ce6a0d13d6f", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-current-focused.log": "16a0e9858bcec8f8f9ac792cf0179d3e2fb793bb", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-current-main-controls-resources.txt": "fff95ca06e667bdb4a2ecea8392ad698a0ab7b32", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-current-main-controls.log": "fc4c79d4c0a3563778f6dab8c2fb09fb2bf602f7", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-current-main-controls.xml": "cbd0d08ecf02731548109cd909db9c07bfe32b6f", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-function-artifacts-resources.txt": "d728112daf14889aefdd378dc574ff0b6ec41348", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-function-artifacts.log": "c214af2e137b6f7af6c3f1f498a3c72ad14eeb17", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-function-artifacts.xml": "b6693849fecb1535d219630ae1d2a0a0f9e9c205", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-image-contracts-resources.txt": "ece83ad94f83b6ee1cd5ef13ed824779bb98f3a1", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-image-contracts.log": "813e428658b44e0ba9be66aa3e267371b2b96e03", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-image-contracts.xml": "8cee761c14c1235f15a11e21c12a5bc65301491f", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-main-qualified-resources.txt": "b1669d6bd307795ba1d877d9a637a2dae578cef7", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-main-qualified.log": "723253fde729c23d90511be8897716e31480aba5", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-materialization-core-resources.txt": "351afbcfb2ab7c57d9f75b5bffdd1044a864afdc", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-materialization-core.log": "60e90ebf74deac34629a5cb54f1b6dbe12142897", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-materialization-core.xml": "c91c6cd297d75ca49cf0ecab55221f8bd45dfdd2", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-materialization-resources.txt": "606ad5e0be4db00f8d3c3575ade1ab6d364d1c61", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-materialization.log": "85d6a6f805c92fcaa6a820eee1857e95f1190aa2", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-no-autoload-resources.txt": "4ad4fd312796f97de5af39a735927d88a83b239c", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-no-autoload.log": "9cbbfefc43651f0e3c01de0e67a5fd35045332b1", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-no-autoload.xml": "f0dafe1dd6734ffda06149a6824b6400a4e6642f", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-public-path-resources.txt": "0f98d46222836207950737f5faecd30c724839aa", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-public-path.log": "1bb15d38df56971062ff523d88191959e5153320", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-public-path.xml": "43503c464a26482305a7f9d55cc0bb696067c10a", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-viewer-controls-resources.txt": "d0e09319c1de7474b7ed52287aaa6cbc5751c5f0", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-viewer-controls.log": "79bad4861e84d3c2644ff59695b302e7fe304c2e", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-viewer-controls.xml": "e157e2c382647ff2e2af15237e506b9e6d09ee52", + "docs/validation/selected_plane_stream_materialization_20261001/current-main-controls-resources.txt": "8c4cd9f78f2e45fe22adc6051aa5f50cc8b9f8dd", + "docs/validation/selected_plane_stream_materialization_20261001/current-main-controls.log": "5713da9bacb7187771e12fd4631e357e4788917c", + "docs/validation/selected_plane_stream_materialization_20261001/current-main-controls.xml": "0b5cc2c17847f6090cc8bad5160953eb9dbf998b", + "docs/validation/selected_plane_stream_materialization_20261001/expanded-first-resources.txt": "a12ff8fafb487a27bd201c9e54c1cf8dc6a69a4d", + "docs/validation/selected_plane_stream_materialization_20261001/expanded-first.log": "4d14c3465cbf02f4983037a148c16b472532a1f5", + "docs/validation/selected_plane_stream_materialization_20261001/expanded-first.xml": "3c56c3ff15c998be0486b9ce537b163fbde2b3c9", + "docs/validation/selected_plane_stream_materialization_20261001/expanded-second-resources.txt": "e4d95537a58ac5782157689574da5e108385efdf", + "docs/validation/selected_plane_stream_materialization_20261001/expanded-second.log": "4d0b14aebc15158fe4624c03aeb618c6a9056250", + "docs/validation/selected_plane_stream_materialization_20261001/expanded-second.xml": "a496e2ce72960cfc17c3be5080cb28fc8d95a206", + "docs/validation/selected_plane_stream_materialization_20261001/expanded-third-resources.txt": "7d696a1a3d92f9f6f67ce082253151a58b5d250d", + "docs/validation/selected_plane_stream_materialization_20261001/expanded-third.log": "5025a52217b6d83bb3c96d82c470a96d7384364a", + "docs/validation/selected_plane_stream_materialization_20261001/expanded-third.xml": "c50499837ae490d513f701cf24b89a838434f24c", + "docs/validation/selected_plane_stream_materialization_20261001/green-initial-resources.txt": "3f3bebeefc03122e9a8e1720fbefa2913a29460d", + "docs/validation/selected_plane_stream_materialization_20261001/green-initial.log": "a7bdbf582528708d44165952968985e51072bd7c", + "docs/validation/selected_plane_stream_materialization_20261001/green-initial.xml": "0d83b06f2b7428c6f1eabaff51b50a5aa83068b8", + "docs/validation/selected_plane_stream_materialization_20261001/image-contract-controls-resources.txt": "ba8602a882602d2910a63ef8954850e6da3e6ca6", + "docs/validation/selected_plane_stream_materialization_20261001/image-contract-controls.log": "f6fd70513601458dd44fc35e83037a4d6d8b0aa0", + "docs/validation/selected_plane_stream_materialization_20261001/image-contract-controls.xml": "19adda2ebce593d99c717ccadec12a6dfb718015", + "docs/validation/selected_plane_stream_materialization_20261001/issue.md": "47e58a72d676456c25e7594d3d0ab84140e396c6", + "docs/validation/selected_plane_stream_materialization_20261001/materialization-controls-resources.txt": "1762eb0901fac062c06b9c77db532a0a2b48b566", + "docs/validation/selected_plane_stream_materialization_20261001/materialization-controls.log": "5e2e0d089a7656ba9c710d736bbc28a591e2ae96", + "docs/validation/selected_plane_stream_materialization_20261001/materialization-controls.xml": "327a26d0604c63b1348380bd6ecedf55c6656f55", + "docs/validation/selected_plane_stream_materialization_20261001/neurite-output-base-resources.txt": "9c794e8bdff10ea4be6ff041685cced65230388f", + "docs/validation/selected_plane_stream_materialization_20261001/neurite-output-base.log": "3f08b87b14a77213b0f83c492374c468f547a857", + "docs/validation/selected_plane_stream_materialization_20261001/neurite-output-base.xml": "cf9bb97876c5af58ae1db44fe572364b18482365", + "docs/validation/selected_plane_stream_materialization_20261001/neurite-output-controls-resources.txt": "287eaff2cdcf02267c8f6b38aad70ac945e18cda", + "docs/validation/selected_plane_stream_materialization_20261001/neurite-output-controls.log": "40e4215ac49ca259704922d7629148b0487c0b2b", + "docs/validation/selected_plane_stream_materialization_20261001/neurite-output-controls.xml": "fe937f0df0b09f5153f25bbcadc417f8ad31ef25", + "docs/validation/selected_plane_stream_materialization_20261001/nra-current-full.json": "5f0d0430651f5354ee095da8d8b77bf33363335c", + "docs/validation/selected_plane_stream_materialization_20261001/nra-current-full.stderr.txt": "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", + "docs/validation/selected_plane_stream_materialization_20261001/nra-current-resources.txt": "7e9334bb4b4dab78f6995a6fb8a582e8af33122e", + "docs/validation/selected_plane_stream_materialization_20261001/pr.md": "cdcf590788576a0358f0dc4e115af6a497c861c5", + "docs/validation/selected_plane_stream_materialization_20261001/r0-current-main-resources.txt": "69f179098bcd29f89a01a6e2ad1df8890bf2462b", + "docs/validation/selected_plane_stream_materialization_20261001/r0-current-main.json": "ca71a7b7840309572d124e2a85f6b248461889d9", + "docs/validation/selected_plane_stream_materialization_20261001/r0-current-main.stderr.txt": "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", + "docs/validation/selected_plane_stream_materialization_20261001/r0-resources.txt": "b14e0cf66910c7c28150f747bea44cd90194c2d2", + "docs/validation/selected_plane_stream_materialization_20261001/r0.json": "b3b6b60a8cdcbe58c67b242129d4775cf53e3852", + "docs/validation/selected_plane_stream_materialization_20261001/r0.stderr.txt": "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", + "docs/validation/selected_plane_stream_materialization_20261001/r1-exact-policy-resources.txt": "530d313eb927836acec4ba9511fb02e02ee29581", + "docs/validation/selected_plane_stream_materialization_20261001/r1-exact-policy.json": "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", + "docs/validation/selected_plane_stream_materialization_20261001/r1-exact-policy.stderr.txt": "2c46a4c72ba4028f03a8cf53d35491a571e94209", + "docs/validation/selected_plane_stream_materialization_20261001/r1-original-resources.txt": "707ed1413918b0c5962f72a5c1b88685209226b8", + "docs/validation/selected_plane_stream_materialization_20261001/r1-original.json": "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", + "docs/validation/selected_plane_stream_materialization_20261001/r1-original.stderr.txt": "82c013ba078eb9fac0a5915cf9497f17d51f5a18", + "docs/validation/selected_plane_stream_materialization_20261001/red-resources.txt": "37ca748e13d6ebce82aa9b2147f4ac3a29fb6486", + "docs/validation/selected_plane_stream_materialization_20261001/red.log": "803c889307080e402828278706ccc4481c903530", + "docs/validation/selected_plane_stream_materialization_20261001/red.xml": "563f93f39c4019a6c42e1cb7a86ccd5b32492231", + "docs/validation/selected_plane_stream_materialization_20261001/run_bounded_source.py": "d56801d4f9d4fa8cd59a2dc712fa875cfe86d529", + "docs/validation/selected_plane_stream_materialization_20261001/run_original_projection.py": "dae94c422f00893fe30b57dbb6b6452970466669", + "docs/validation/selected_plane_stream_materialization_20261001/validation_summary.json": "b16d35df609780902f22bd44ac32ac976eb6c7d4", + "docs/validation/selected_plane_stream_materialization_20261001/viewer-rejection-controls-resources.txt": "80ce1f2419e748d6afcaba3c8c650f821866ee9e", + "docs/validation/selected_plane_stream_materialization_20261001/viewer-rejection-controls.log": "df1dffe90373aca50b736183443a930d8bf54408", + "docs/validation/selected_plane_stream_materialization_20261001/viewer-rejection-controls.xml": "400984b717426746439ab885e5df490c519837fa", + "docs/validation/shared-dense-coordinate-owner-20261002.rst": "4d865af45f6a5ed0dd88b5fea81f260805d99c70", + "docs/validation/shared-quantized-hull-geometry-20261002.rst": "2760a5d2be84cf452f2504ea107aaaf20600e827", + "docs/validation/shared_runtime_plumbing_384_20261001.rst": "ef9275122450bf2d0b27132c78db9425191487d6", + "docs/validation/singleton-label-invocation-477-20261002.rst": "faad1c18a468a3fdb7324fa6b6a0cf1194bd7efd", + "docs/validation/singleton-label-invocation-main-477-20261002.rst": "1ffb2b32130bc342aa54a5c21557e0b89227503f", + "docs/validation/skill478-distributed-threshold-diagnosis-20261002.rst": "25cfcdf6722eb8406e7d4eed1da069f37a7f3662", + "docs/validation/source-projection-family-20261002.rst": "a73cb632402afd8723e6f45076308110b3de1ce9", + "docs/validation/source_projection_fixtures_20260930.rst": "94e414fcab1e415f782b6c7213b4140b7bfe74b6", + "docs/validation/startup-feedback-358.rst": "11807fe78d670160dbcdabcf7ca36021e0969b7f", + "docs/validation/startup-feedback-evidence/SHA256SUMS": "1700bda0530d87e8e707461c09341f3b3dfb4889", + "docs/validation/startup-feedback-evidence/green-final-feedback.log": "3ac02840d869cb2aeb60cdc65cc4778d8866e2b2", + "docs/validation/startup-feedback-evidence/green-initial.log": "415fe42acdfdd3d29ee0c442e11dd46ede50b285", + "docs/validation/startup-feedback-evidence/green-lifecycle.log": "82fdda617344359633f27452ed2b01879f802180", + "docs/validation/startup-feedback-evidence/green-published.log": "d3b70233618488b24b4ae1c975771d72ed484ad3", + "docs/validation/startup-feedback-evidence/green-wrapper.log": "0bbcaa2c8afd4f7c720d2d84d3f6ca808b54cdd8", + "docs/validation/startup-feedback-evidence/lint-final.log": "6b20a95e0253c4ec65e5a5756067ef6949039a7b", + "docs/validation/startup-feedback-evidence/lint.log": "807b43a96184cc913ba8144f7711073770d6cdb7", + "docs/validation/startup-feedback-evidence/red.log": "3c81c6e1313402a667138689d66652aedcd85be3", + "docs/validation/startup-feedback-evidence/source_tests.py": "92b660c35623e543dd7dba4c4779efecca44b5cf", + "docs/validation/startup-feedback-evidence/wrapper-delivery-diagnostic.log": "4d2f32589e23d84d97184c13f97a99e1b576a730", + "docs/validation/startup-feedback-evidence/wrapper-diagnostic.log": "dfb821979227bd115c5a309efe0288fc4a718840", + "docs/validation/startup-feedback-evidence/wrapper-identity-diagnostic.log": "378693425b31b9db8e032f00596a076b1e62c04a", + "docs/validation/startup-feedback-evidence/wrapper-scope-diagnostic.log": "7a7aaccdc6ba727e365fb25d042ff87266f209c5", + "docs/validation/strict-mcp-arguments-484.rst": "c28c02b340540e4f0a89f7faa2b6be14d381a958", + "docs/validation/viewer_current_main_integration_20260930.rst": "fa17021a832cb5b06db07721299926b12ccae96e", + "docs/validation/viewer_reset_lifecycle_308_20260930/all-owner-bounded-first.log": "86ae5a05cd75fa2e0e8b7d576a5e5f770b43096a", + "docs/validation/viewer_reset_lifecycle_308_20260930/baseline-shapes-bounded.log": "0ee8745d3e43e441bdafe16feafc415ab261012f", + "docs/validation/viewer_reset_lifecycle_308_20260930/baseline-valid-transitions.log": "137c936229b9d7f307d7c69c91b964fd915721f8", + "docs/validation/viewer_reset_lifecycle_308_20260930/check-parent-artifacts.py": "6c809beddaf0009d5a4ea9fbf6f0c86251d1af1d", + "docs/validation/viewer_reset_lifecycle_308_20260930/checkpoint.rst": "c68deb8666a45ee2d4cd147e236bc01c766bac18", + "docs/validation/viewer_reset_lifecycle_308_20260930/final-matrix.log": "88500fd025fc2e3758c48bfc88aebed997bed875", + "docs/validation/viewer_reset_lifecycle_308_20260930/mcp-witness.py": "921a41830f22c9446eef4680b7e42eedf7058174", + "docs/validation/viewer_reset_lifecycle_308_20260930/native-calibration-owner.log": "69c8773bcabc2c676c24ce08c19700114c31cb22", + "docs/validation/viewer_reset_lifecycle_308_20260930/native-layer-owner-complete.log": "d422fd94462821e67c403373e93a4771ab5a752f", + "docs/validation/viewer_reset_lifecycle_308_20260930/owner-image-clear.log": "4dd89c31ffaf70a9ef1da333d35877ca6bdb1167", + "docs/validation/viewer_reset_lifecycle_308_20260930/owner-regressions-first.log": "a04db7cb1ef062ce756ac5127f772a936f3a9310", + "docs/validation/viewer_reset_lifecycle_308_20260930/parent-acceptance.rst": "8b3705122e275ae2961fea870ee4a5b466635b7b", + "docs/validation/viewer_reset_lifecycle_308_20260930/pipeline-parent.py": "e9d60f5b4ac1b0479f761a909611f4946c5a5ce0", + "docs/validation/viewer_reset_lifecycle_308_20260930/pr-body.md": "d949b9cea0c326c0273ee91d9fd87c4f84c0a6eb", + "docs/validation/viewer_reset_lifecycle_308_20260930/queue-owner-shard.log": "74d755424bb132585fde329dd4cd61f5b2534580", + "docs/validation/viewer_reset_lifecycle_308_20260930/ratchet-final-resources.txt": "122957dff43465c040348092132ab7066a3519f1", + "docs/validation/viewer_reset_lifecycle_308_20260930/ratchet-final.json.gz": "e862eb3ffb51d07df16bf52fde03bb3466c0978b", + "docs/validation/viewer_reset_lifecycle_308_20260930/ratchet-first-314-resources.txt": "44218bcc365b3229c5dabb76630b401c30c6038f", + "docs/validation/viewer_reset_lifecycle_308_20260930/ratchet-first-314.json.gz": "5e9413806ea2c20fd49511c6adcd90ce0e25f760", + "docs/validation/viewer_reset_lifecycle_308_20260930/ratchet-first-resources.txt": "f07bb39be5aaaeb0a1b2f6e6dc77f5556e859833", + "docs/validation/viewer_reset_lifecycle_308_20260930/run-parent.sh": "d84f18caf1daf3ab35ddbe1fc7885b8c979c27d0", + "docs/validation/viewer_reset_lifecycle_308_20260930/run-source-check.sh": "b40a096759c874c441a8e3c8d89d9293223814ab", + "docs/validation/viewer_reset_lifecycle_308_20260930/shapes-4097-owner.log": "fd61d005e34b312b8ca2208538f5c895f904a382", + "docs/validation/viewer_reset_lifecycle_308_20260930/shared-axis-authority.log": "ffc43913c71c8000b67dfd42db66bc6f692110fe", + "docs/validation/viewer_reset_lifecycle_308_20260930/transitions-final.log": "1f0a6a1bb1e3efad6dd622b33c1ad9b7a300e630", + "docs/validation/viewer_route_spatial_302/checkpoint.rst": "7ff23f0464c40a89dff5f5741c39eb32de684fbe", + "docs/validation/viewer_route_spatial_302/pipeline.py": "08ee53dfada49729735c5ddd9eefe43e4c173012", + "docs/validation/viewer_route_spatial_302/run-candidate.sh": "00ec1187921a7ee4f3509d41cd139e90420ed615", + "docs/validation/viewer_route_spatial_302/run-source-check.sh": "d772039d7c19c91bd8b95121eb7721a7d610fe6a", + "website/assets/agent/cold-start-workflow-events.jsonl": "18ecc15b33bfe9378fec68c1535d7e33a2ba4085", + "website/assets/agent/cold-start-workflow-final.md": "69a3f13c652d089edf20692eaff5269771766cf0", + "website/assets/agent/cold-start-workflow-pipeline.py": "e6e4b5a7e3e12e86a559404e1d6b260ce17e35e7", + "website/assets/agent/cold-start-workflow-record.json": "5477f32586f486edeae4daa113dd95dc119c3c36", + "website/assets/agent/cold-start-workflow-transcript.txt": "f7699d258d92bb96afe2476259132320f76205b8", + "website/assets/agent/cold-start-workflow-uncut.mp4": "4db8ca6871bdeaa9d5f6d5e7b092f6cc1d48b2cf", + "website/assets/agent/neurite-outgrowth-workflow-poster.webp": "fedbae315af0b56e6288341c56913b3337814655", + "website/assets/agent/neurite-outgrowth-workflow-record.json": "1b882cb6be4098f37a5ed8a930b70af52caa4c92", + "website/assets/agent/neurite-outgrowth-workflow.mp4": "3224afe95c92b9bd1617d4ca2d93148fdfc9203d", + "website/assets/agent/neurite-outgrowth-workflow.webm": "51488d55e5e6cbe67161629a26ddde1c285e9238", + "website/assets/gallery/current-neurite-result.webp": "13a8fa80486c4d7e1ce331d4c9f689ac8168ca64", + "website/assets/gallery/execution-log-viewer.webp": "6a7b24d78fa287a2155ca45527e632ba41dabcdc", + "website/assets/gallery/fiji-review.webp": "2b4394e0faae58a41560560949d121d6fd3c5c5f", + "website/assets/gallery/first-plate-workflow.webp": "d619829751cba891a74f6db950ed0cba7dcd0c6a", + "website/assets/gallery/global-configuration-editor.webp": "55780efb53dcb5d2bba6a6d8ea912ca877e56564", + "website/assets/gallery/lazy-inheritance-poster.webp": "4a4c20313a6cb3a58aa30319dbf46616314b1a64", + "website/assets/gallery/lazy-inheritance.mp4": "9d4fc4701299354d3e7dd5f5aa119be313af59e7", + "website/assets/gallery/lazy-inheritance.webm": "51a17360669fe49e321f3bb21486045ef90a03de", + "website/assets/gallery/multi-plate-overview.webp": "76578dd9936d5560eddaac7fd8bec52882bc2404", + "website/assets/gallery/napari-roi-navigation-poster.webp": "1647a27ea6383a2b862d9cb29a414ef8593951ff", + "website/assets/gallery/napari-roi-navigation.mp4": "355a78bd4c9679f931bde6ee5ac7bf293d857d9d", + "website/assets/gallery/napari-roi-navigation.webm": "71df0cb54d071b657fc0283b7db0f370f8c67947", + "website/assets/gallery/pipeline-editor.webp": "f4034aaac227ad4b0d0c3b54030ae459721a12be", + "website/assets/gallery/release-media-record.json": "d934e58f1fe06bba152669d58a1ecdb726541ec8", + "website/assets/gallery/source-capture-evidence.json": "ffd79ddf629e75e70625381ba87e52c03f25f3f6", + "website/assets/gallery/source-image-browser.webp": "685b9428b0060724e527759e517d8ae3c76228a7", + "website/assets/gallery/stitched-neurite-overview.webp": "2c00993a6c69c2a9385f928d3cb47b029a323179", + "website/assets/gallery/ui-about-openhcs.webp": "6c40ca74d3de9b7efe3b0723cd19cb3b2db5814f", + "website/assets/gallery/ui-custom-function-manager.webp": "99366549cdfbc1e38ce4c0874482d1d80d4a257f", + "website/assets/gallery/ui-function-configuration.webp": "b41a55999e69063eabcd72e675425a817c5fef0d", + "website/assets/gallery/ui-function-selector.webp": "071c5d26a49770333fa2a2d8b7915823ca779dbf", + "website/assets/gallery/ui-global-config.webp": "55780efb53dcb5d2bba6a6d8ea912ca877e56564", + "website/assets/gallery/ui-image-browser.webp": "18b414b2d33b9e292be12f7b7b13dd226d9c2cc0", + "website/assets/gallery/ui-knowledge-base.webp": "c337bf822c05e489855003c52aad4e122487e3d2", + "website/assets/gallery/ui-live-measurement-results.webp": "fa0ffb29451046b6d0664d70bdba77b5c7f68b40", + "website/assets/gallery/ui-log-viewer.webp": "4a7529d02062c1522996e318730c950042a9ac3d", + "website/assets/gallery/ui-main-window.webp": "ae12c6e2d482e6bd637cf01037a2e99ffc187667", + "website/assets/gallery/ui-pipeline-configuration.webp": "c0753903187c1f8c92d0a6744243c7cd70b18aae", + "website/assets/gallery/ui-pipeline-editor.webp": "6a6269c1d12066db84f947523f6fc8f49fdf7d0a", + "website/assets/gallery/ui-plate-manager.webp": "e30ea9c0357e8f869000754daa623290ebeb94fa", + "website/assets/gallery/ui-plate-metadata.webp": "a7f4cb7584a98fec5b6bf118e9387dada06be2a3", + "website/assets/gallery/ui-step-settings-editor.webp": "bf1b857403e02d6033476a87317a393ff20920cc", + "website/assets/gallery/ui-synthetic-plate-generator.webp": "8635f1cb8fde1c061df942c334b10e3a1d4db1dc", + "website/assets/gallery/ui-system-monitor.webp": "91e02cfc1dc54ddaa47e1320bdfa094960de6d0e", + "website/assets/gallery/ui-zmq-server-manager.webp": "dca97c12e45c093deb09ef0c789e5b2ab0e9ff86", + "website/assets/gallery/zmq-startup-compile-poster.webp": "28fefa04e06985cf838c2651d95a4e3b9dbe8069", + "website/assets/gallery/zmq-startup-compile.mp4": "fb13e04cf1d2bb1e94aec18214682d012c5ff766", + "website/assets/gallery/zmq-startup-compile.webm": "4f8b84d308885c1fecacd9ace5a9964c37a1cebb", + "website/assets/logos/README.md": "b54b2ed49cd8d548b0b49727b6c6d68687e00d03", + "website/assets/logos/bioformats.svg": "759f3df7d6f7b4147f92ce99845c1f7d713475cf", + "website/assets/logos/cellprofiler.png": "90c4212f6017c0611140b897922b450d599a5ece", + "website/assets/logos/client-claude-desktop.svg": "0f8385a6554aaa25cd24d8222ba62b4778c7e1b7", + "website/assets/logos/client-codex.svg": "7d9d324ed02e32254e021834e7991e667f5e93bf", + "website/assets/logos/client-cursor.svg": "b9672494f2d63174153dae038ca72d821027c530", + "website/assets/logos/client-gemini-cli.svg": "60197dc4adcf912128756b32ead43b8b1da61222", + "website/assets/logos/client-vscode.svg": "d7701ef43ffef61bf0f96df0a4edea54662736b2", + "website/assets/logos/client-windsurf.svg": "7670303f63da0ef1f7224367f06a04930cd9de6e", + "website/assets/logos/cupy.svg": "60ecb29e7142ef02e2b239c79413d53575a6ed6d", + "website/assets/logos/fiji.svg": "8c85740e15b13d8dbeb6e9c2b51e1f5142ad92c1", + "website/assets/logos/jax.png": "a0c7393ef78578f6b3d38cf38c219a8075555f5c", + "website/assets/logos/mcp.svg": "03d9f85d32bf343db07e6e472d555d0a7be41c58", + "website/assets/logos/napari.svg": "1bed786fc4cd7717c56f314cf536f75274b62f8a", + "website/assets/logos/platform-macos.svg": "796e5662f1445b8a195fa2bb147d004695726bdc", + "website/assets/logos/platform-windows.svg": "e635dcd64607e9324318675aeff53a62ac6cb219", + "website/assets/logos/pyclesperanto.png": "e00cd372b95cd75fb1e948060b865296440fd87a", + "website/assets/logos/python.svg": "297a11e090227bfd6bab3ff110f3ce39b400d745", + "website/assets/logos/pytorch.svg": "575f6823e4762f1c5ad7dd4ff7229a51537eb6e4", + "website/assets/logos/tensorflow.svg": "2c510f01c53758c4dbae20f6db3f88f6f565d04d", + "website/assets/logos/zarr.svg": "fb85504af0fb79424c109526b175b3d43b643a07", + "website/globals.css": "e170e436411140dab7c1ba07cf160faa4940b042", + "website/index.html": "ba570cbd696d1638be91f4b4cf54355ecbf07ea7", + "website/privacy.html": "e783fead7959d6eb4264a5ffc4cec4a075d9e08a", + "website/styles.css": "60f4d0aefd282f53cd35ecf3dcffcc32244ddf73", + "website/support.html": "40e06cffd9a8b80742c07994712e6e21c4f2a2a0", + "website/terms.html": "0ab5910bb72357832b31d70c83354c2d9e33ec2c" + }, + "declared_gitlinks": { + "external/ObjectState": "a01f8939b9bf4659502a33fe3febe62cab0f3c9a", + "external/PolyStore": "84f322e46871de5ed47e7fd20976ad03e533c1c4", + "external/arraybridge": "1e53d03d9f468322a1c085c8be29485fb139caf2", + "external/metaclass-registry": "393a7e03003cdc56df9013f932ed4f26e632d77a", + "external/pycodify": "792b03413331fdf107e369c924c257a2f0b16126", + "external/pyqt-reactive": "ad4948775ab81180a354d4b793d17ee5ddff3972", + "external/python-introspect": "ed1e4eb15f7a0bf8315274d3e47858fa2b4372bb", + "external/zmqruntime": "668edafcb5ee530a163377c936431ae1e8334e85" + }, + "sparse_patterns": [ + "/*", + "!/website/", + "!/docs/validation/" + ], + "website_receipt_sha256": "743be25a32804961ba4c05bd32b8ecc696bdcb15e52f5350678260bcf3fad36f", + "website_preserved_sha256": { + "terms.html": "e12f9e60c4906bfaa4e4769a06ef0715e26bdaf1b14684f9590c026dbe9b31df", + "support.html": "c77d2a2a5c1c1362bb27372e9773c23569bd5c9834ea0b1777d960dc4e1f0003", + "globals.css": "4b9a2d89a05db28816a3ebba363247e774a277cc414e2bd918c4a9d8984beef1", + "privacy.html": "09dbc87f2b3749be4926f97d67aa2c4264844731bf0f037cf754297891b1eaf9", + "styles.css": "2ad02ff21a51c047cea2a5c3f9b19bfaa85e4d20d70f326c339b71206310cddf", + "index.html": "311b8c9d5b232cb4eb3e27958da3d5dd6cf0c8665f3474833b9f720925af153b", + "assets/logos/README.md": "244baac8bc62478d51d3f50a750559edc1205763bcdb4c6fbab87e698a93e43b", + "assets/logos/platform-windows.svg": "e50c2a77d210b422a90dcebf271bfa7a2b6cb1a7132bb798e045100b823e123e", + "assets/logos/client-claude-desktop.svg": "aef68a9c94bdbb26485c7bcc79c94ccc9d798b4f17d20a9019d7932598dc5c1b", + "assets/logos/client-windsurf.svg": "310a4f7fd2a2591ec45cba3313790f42ad8b805896e825c8fcbfada06f8dda7a", + "assets/logos/pyclesperanto.png": "b551c7e270028cfc29b5d8997a49ed83c3c49029e41124c2e0ec339b533157db", + "assets/logos/bioformats.svg": "19cfb3bc6a2274b98c76a1e9af43f1d94c56b5b5e718e60b1fecdb633c7ddf9a", + "assets/logos/zarr.svg": "4e7d278e12a6000c54ce3dfdee274d4dd98595bdf04d2afacda17b5a6e61ecf6", + "assets/logos/client-cursor.svg": "ea5d9706762f2eae285b69c8769c376892ba1efe068febd8e2857aff0afebc85", + "assets/logos/mcp.svg": "05f47fb3ffb7323bdbf6b397330229a7f32a3a1f2d17365e748f8777a82dd6c0", + "assets/logos/cupy.svg": "30100286bd74ae633d2196b2d8ea175e405c258c22441582e6bb1715866d1756", + "assets/logos/python.svg": "adca85e1d0bd6d796abea422d983fd131a432fe78efeac58af18461eb1548780", + "assets/logos/fiji.svg": "36b2b05f93e28fb37085f8d9eedaddcdda7718732bbf3f4ed40e0bd4722babea", + "assets/logos/pytorch.svg": "fa653a3e79a36d6c8738c9c05e10c0265e466db23fc4b2d7ef65d32ff84cc839", + "assets/logos/client-codex.svg": "730508e7487d4e4ba758bfcca3e4cdfd31953938efad6d506acd502858559db3", + "assets/logos/napari.svg": "e7d39d1a8d8ddc43190e55fb09c7ac6f3653120b4afff2dd981b3a66b326623d", + "assets/logos/jax.png": "b336e0959efcf4835f1c4ab717398b4a8fe11dccac6790f2dcff504820d9b095", + "assets/logos/tensorflow.svg": "5ce40657105dc138823732bc446fe8e0bee666e9348fe80b7ef1b03bd873b953", + "assets/logos/platform-macos.svg": "aab854e12a2a1a639c90662705452313b06b22c670bcfeeb49975a065d792afc", + "assets/logos/client-vscode.svg": "e6a85a48bf3f458a3c061ec2096f6ed435a17f809a6ecfb66905cc16ec1feb37", + "assets/logos/client-gemini-cli.svg": "cc58217af6ff40b9de0d105a3820ca2cbc8b806905dbedf3baf47ff7eec91a7a", + "assets/logos/cellprofiler.png": "820dc710604ae6bfa5605b7a6c17817c484d91e289c15d5953e2769b083333b3", + "assets/agent/cold-start-workflow-final.md": "145a0231f59bc359d88e0213a401912aeaf4921ab951e11451278e9535bed503", + "assets/agent/cold-start-workflow-pipeline.py": "48b6e4f9dbc54932df8a95c405f4b832c1ca35856d866a261faf15fa3c14b754", + "assets/agent/neurite-outgrowth-workflow-poster.webp": "d523a664ecaf1f9f06d1cbe6e8717795f4a463a5c5e191ba1724a78cd88be3b5", + "assets/agent/cold-start-workflow-record.json": "52d126027b39a05cd5e799e4aca28e45fd8edd251c7b523204c33c14fa7de65c", + "assets/agent/neurite-outgrowth-workflow-record.json": "da03fea3e052de378582999b0a7b67ab2a16196f7863064189a11cf7469f4997", + "assets/agent/cold-start-workflow-transcript.txt": "0a1242f902d8fdb76137edf6f5d37055ff3f43650b3691f54fc77a8c333a9e50", + "assets/agent/cold-start-workflow-uncut.mp4": "e3c71086fbe6ea85bfb8ae557d3cc129c6a745779fcfa6c0c0b5388e13956c58", + "assets/agent/neurite-outgrowth-workflow.mp4": "b5c2a0489a3cbace65a1abc27393a57620515ceeb7a4968bcab5724473c3b8eb", + "assets/agent/cold-start-workflow-events.jsonl": "de8342948e39a9a9a8dc487770bdb32012f683799d4479a1ebeb5d125a563749", + "assets/agent/neurite-outgrowth-workflow.webm": "fbc9a4ce52cb7a02682e2b9f71a5479836e7c35e9c0d9edc8b96b402f72ec1e7", + "assets/gallery/ui-synthetic-plate-generator.webp": "e175587baba4afdd7826f168a99308b66e1287e5286e17ae72e73bd4d86675ff", + "assets/gallery/source-capture-evidence.json": "ae0d19fca99f3bb5e98eb299b044075b812cf414b4917216fa8233fa721e9c64", + "assets/gallery/fiji-review.webp": "c02d9e4e02c3e8f504fcedc4440dafa154072a876f8669c3b7d9df89254ed893", + "assets/gallery/first-plate-workflow.webp": "991de0cb39524792a41c1d47bf1078d31fcdbd5b2dc55c81daf064a74a3a18a5", + "assets/gallery/stitched-neurite-overview.webp": "537df1dbc4e2ccc3ae6b96245daf79a504dee29e8b8b498a7a2f12c2dd43bd0b", + "assets/gallery/release-media-record.json": "fffe771e55e88377e8c46a40ff81dbd3222d9cbb152842f5edce49d89ac23fa9", + "assets/gallery/ui-log-viewer.webp": "0ac13269eed5832e5aa4c9a9ca27da59044da18709da4a17f40687c14fccb9ba", + "assets/gallery/lazy-inheritance-poster.webp": "84c9dd0c6c9b3af0facc08adc4f8cebf2169c610e34271991c06a54c7291341a", + "assets/gallery/ui-plate-metadata.webp": "061b1f5d32b64427c0ef06ef1c82664eac09a7e06aac797b1a25bb73c3cd15e1", + "assets/gallery/ui-zmq-server-manager.webp": "7a8e86bd8c11c914103cd685f87177b367cf8219ef0ee2ac375a66557c97b3ff", + "assets/gallery/napari-roi-navigation.webm": "345676c83e17d7ff634daf880587b44f16c6364ae67475f06cc6a5aad2618d26", + "assets/gallery/ui-live-measurement-results.webp": "b39a8ed25ca440cfb799a73f5b54c23d030dd7eed92c57c1fa8ac0f5c8a8565b", + "assets/gallery/ui-about-openhcs.webp": "00a701ee47c35da96788e1dfcf63ef161fa8d92ead27f47b5302e8997fe251f8", + "assets/gallery/current-neurite-result.webp": "e55ea2fda442740f58e93f4773419c53026faa4b3336d0148039ea996f65f551", + "assets/gallery/lazy-inheritance.webm": "4f6184f6c5e75febdfcd7d5c36c7d9499aafbd37c17e9bd6d58b76145ca9ec82", + "assets/gallery/ui-global-config.webp": "b4b69cd74899639c12c9f7014449501f9fafe91a2cd65533631583e2f5ec48e9", + "assets/gallery/ui-function-selector.webp": "4bff760ae10c02e41e8a6f5754438021cb92ae7985ad4ec6e8183e43a92fd8bd", + "assets/gallery/execution-log-viewer.webp": "ac0476352392fe6135fca60bdcf37c40c91a21f00384eb4fef3be023b9165eab", + "assets/gallery/ui-custom-function-manager.webp": "10e8e32bdb175ea13e0861479f843ae1ef9f18746a7548ec75d2148a060ef144", + "assets/gallery/ui-system-monitor.webp": "7a910e3b565712d75050ec5846282e733ab218132d5eb09a41853f48777782f0", + "assets/gallery/zmq-startup-compile.mp4": "1aa4588d577f575beac0249423d63fdec7a81149990d1916b2866dca6d6ba28f", + "assets/gallery/napari-roi-navigation.mp4": "30b8a83a2304b4dc053563d84d5e8b2ee34192bf95a3c161320ee4fd41f05686", + "assets/gallery/lazy-inheritance.mp4": "af7d69d8a12bc80648af2236d564bda31d111c6c67d32d396112c57b964380df", + "assets/gallery/ui-knowledge-base.webp": "f436036fc3b3cf85481051ebd690a2167468d09d186efce811936949b734613c", + "assets/gallery/source-image-browser.webp": "cf9631e3b61b22772eec772c3b7acc9f761c1bc46a0528de19107a86d7419d1c", + "assets/gallery/ui-function-configuration.webp": "003c0278077d2d239b90308958715808e3a1a99d767a7abfca3dd65ba6be1bd9", + "assets/gallery/ui-image-browser.webp": "adecdbec3e1381eb71f8fff813f1a56824e4c57a9d5f2d3c6659606682d7bfd8", + "assets/gallery/ui-main-window.webp": "b42b3b03c23e6e168fdb1c515739831cd98fe022a9e41de3949e2f0cebd787bc", + "assets/gallery/napari-roi-navigation-poster.webp": "f1e9c29db9436df7e6e2dcdb134b6d8529538143f05b732cbe38ec100cd5e1b5", + "assets/gallery/ui-step-settings-editor.webp": "df440ca834f62e34f8da792320c4159424b05848bada8fc2cb8ea502da741c4a", + "assets/gallery/ui-pipeline-configuration.webp": "1ca2eb172ca33a2cd9ba3f74d1aaae81f448f5c48c85bd9b6f51642e9b1fa570", + "assets/gallery/global-configuration-editor.webp": "b4b69cd74899639c12c9f7014449501f9fafe91a2cd65533631583e2f5ec48e9", + "assets/gallery/multi-plate-overview.webp": "dbbafaea1a1eced14c1c521b0d4efb2d0306bda6c2353a9011106bebdc07b324", + "assets/gallery/zmq-startup-compile-poster.webp": "cd86354953790f70041504f2be4cf225f678294f51624ce477d9414cede7028d", + "assets/gallery/ui-pipeline-editor.webp": "88b9ecfce130e15d6bc98765414f36a02fc2a0eef37953a1cdea7bcadf07b82f", + "assets/gallery/zmq-startup-compile.webm": "64109edac63e3bb22d8a155cc757114e11af6028d7a08d33d81a61aae11f93cf", + "assets/gallery/pipeline-editor.webp": "b0931e1c10d905123a1b5ad32ed7362b07c401fefcba2211ae5688a5003e6d8e", + "assets/gallery/ui-plate-manager.webp": "09a340b7b6320827429ca67e22b35aa39ed9543424be07bae399de881201d5b3" + }, + "validation_receipt_sha256": "be95471be7db9104c4f70679043f36c0dcf3dd6afef7c375a7c42dd252ec308e", + "validation_archive_sha256": "a94d12b0267c813da88398c151c1b1f21938e5d2eeccb3324daa7b81b1fbb6e4", + "validation_preserved_sha256": { + "docs/validation/ack_runtime_parent_20260930/checkpoint.rst": "7725593e1289e6dd311a08ab5ec94376a69908fe2da73eac452ce1df6fb4e273", + "docs/validation/ack_runtime_parent_20260930/mcp-paired-session.log": "92b11754a5fa805b64726dc08c85249f26b299acb3bd075dbf22889cd241309b", + "docs/validation/ack_runtime_parent_20260930/paired-source-final.log": "6321bbee4f8d59ef3d54726c0d96052d0b13bde1bb2ee3f3acb58dd71c35e664", + "docs/validation/ack_runtime_parent_20260930/pipeline.py": "04bb12bff2d2afd64de5df0489d84360c517aadfe51a77d4bbcd1d232f4f17e1", + "docs/validation/ack_runtime_parent_20260930/pr-body.rst": "7513b54a7ca1f2c64040fb1f1e9ee934133fe3c9a45964df307f942fae21794d", + "docs/validation/ack_runtime_parent_20260930/run-candidate.sh": "12d5f43f03bbb068cf94aa10043d396dfd688fa04e432815b7e03df4f03b5003", + "docs/validation/ack_runtime_parent_20260930/run-source-check.sh": "fd035c31bd369ef3b9b9d53c4a369dc78bd8e396d16013a46d344f7bffd80324", + "docs/validation/ack_runtime_parent_20260930/viewer-followup.rst": "ca27aff503b361489148c9d4c6b508cd2af8006825abbbdafe7a01ec486ed6bd", + "docs/validation/artifact_fed_parameter_declaration_contract_20261001/ISSUE.md": "6356aae96cf5b14277df9344123561e4a82cacb07ad16f79406ad92fcbab1032", + "docs/validation/artifact_fed_parameter_declaration_contract_20261001/PR_BODY.md": "6d76ba7b7a4a05fa5b6dcdf91d11113af4a6cef5f6c9fcc34427be096bf96d4f", + "docs/validation/artifact_fed_parameter_declaration_contract_20261001/RECEIPT.rst": "58ea474286448e3033c3da2ac9add3b0c990d0ac7c5d35aac4b6dd293a09d57c", + "docs/validation/artifact_fed_parameter_declaration_contract_20261001/SHA256SUMS": "6c57519baeb5c5348e0166212ca0ced6f52b4f79b7405b161d0c41994bbdc0d7", + "docs/validation/artifact_fed_parameter_declaration_contract_20261001/contract-pattern-shard-resources.txt": "54ee7530366072ed9b4a208d4c3ec8b5ede84d80ce3f5d5ef1e02d2f12bbd26e", + "docs/validation/artifact_fed_parameter_declaration_contract_20261001/contract-pattern-shard.xml": "5072c684af61b581e7c1e3aa17da822cd18e56338f4465e356ba3f7a9a3da3ce", + "docs/validation/artifact_fed_parameter_declaration_contract_20261001/normalization-green-resources.txt": "0cb0f323a14b6651017a46817b7faf85295a590ed77f2caf536565eaeddc51bd", + "docs/validation/artifact_fed_parameter_declaration_contract_20261001/normalization-green.xml": "e4518c3a08d37eb26a01bf2a4e40edf8ceae7985dc1261b3ccc5a4892a583789", + "docs/validation/artifact_fed_parameter_declaration_contract_20261001/original-red-resources.txt": "a42ec8b7e6414d532521dead2ba7d6acbc849bbca9e53faf61cc1c0739a2ee3e", + "docs/validation/artifact_fed_parameter_declaration_contract_20261001/original-red.xml": "a181c710d8abb7a64d77e17fc060a2053b0b4dba3a2831ff4107ed4073ab2cd7", + "docs/validation/artifact_publication_315_20260930/baseline-import.log": "6e609cec624cda587fdb991174260ad1f95cbcdfb3b4dd5cdf84729c6ce6a02d", + "docs/validation/artifact_publication_315_20260930/baseline-journey.log": "b65e4cc426d5806c491f9c0eb72cd7817d248a38a99dd2cc49cb971bb0ac7277", + "docs/validation/artifact_publication_315_20260930/baseline-journey02.log": "fcfb1067f66b6a2abf560d0a180ed2ad2c0d39ff04adbde7ec5ae5992277b5d0", + "docs/validation/artifact_publication_315_20260930/baseline-journey03.log": "af8f1dab9665e91dd1885d790cef63e055895ef95afc4f4f3246838827ba31f2", + "docs/validation/artifact_publication_315_20260930/baseline-journey04.log": "6eda53043b7f4c1bb86c17d992f53283aa7411643a0bae8bcb9db430403031f6", + "docs/validation/artifact_publication_315_20260930/baseline-journey05.log": "b35b49a9c6b804077f5d7361cd2a524a588597170bf519e6f5ea6a212ad99daf", + "docs/validation/artifact_publication_315_20260930/baseline-journey06.log": "3580af3a73576001e4862d84fdb9fc2b15ab0c7e88d17f17e7ceec002a6a65ce", + "docs/validation/artifact_publication_315_20260930/candidate-journey01.log": "4e4eeb7110157fa40178637b3953d6f06c9875058a70b85cb187a7e5de6b3158", + "docs/validation/artifact_publication_315_20260930/factored-journey.log": "c914dfeebf2e14f997cb7826c61eaf29aefb99a293f657f473b41c984d60b80a", + "docs/validation/artifact_publication_315_20260930/factored-journey02.log": "f53b3271d1b98d0687be6d328d4c9466652f3af54dfe18e0b74ec88f5749233b", + "docs/validation/artifact_publication_315_20260930/factored-owners.log": "37eb73da5e30dbee9824b55e9da3a90306b8d2e1739348e3b224fc193be58369", + "docs/validation/artifact_publication_315_20260930/geometry-final.log": "c487742702d708c9cb943a35b5708e215203fd98ef7497d2f9ada4cbca9ca75c", + "docs/validation/artifact_publication_315_20260930/grid-triage.md": "32a845a94a8cdc93919a1fa64ef7f3e0674c3abda35382f22f876f13b51a903e", + "docs/validation/artifact_publication_315_20260930/merged-main-source.log": "fc4f6f8d33865b47a8aaacdcee162ef044c694c3b32abcb56661885ce06be8ec", + "docs/validation/artifact_publication_315_20260930/named-checkpoint-ownership.md": "2ec76388caf5660710272ebaaa32e6c8f645a62c1ff1e8cfa6f407e7419d3595", + "docs/validation/artifact_publication_315_20260930/original-native-failure-artifacts.tgz": "f98ba196b7be496c01604ba4a75dc66657f8664020ec0678ef2a95f287252308", + "docs/validation/artifact_publication_315_20260930/original-public-journeys.log": "3390b50f0482f49b51478d95ebc9c04c014321af3de38da2c0eccd5d928c310f", + "docs/validation/artifact_publication_315_20260930/original-r1-bounded.log": "e3b7f9f3b95320b52817126e0526adb6d2d7bdfa46c8efb2720e0ad4bba8cea9", + "docs/validation/artifact_publication_315_20260930/original-r1.log": "7437360b9deda8a490549b853a57b88b6278c171a44bd63a4ae4c43e9fe5929a", + "docs/validation/artifact_publication_315_20260930/original-ratchet-benchmark-resources.txt": "1d401d96d59f2a7e62dd58fc6482fa341f7e6e3708c6ce08c5e15802bc73f29e", + "docs/validation/artifact_publication_315_20260930/original-ratchet-benchmark.json.gz": "0527c9effac1a406c457f9069d8f70bf49dd6094ae55ef378d1ce533a05acaa8", + "docs/validation/artifact_publication_315_20260930/original-ratchet-final-resources.txt": "3c1fffae4b2d7fa23828c18e39b6a233566a8f389118bc521a3738530a029c34", + "docs/validation/artifact_publication_315_20260930/original-ratchet-final.json.gz": "d1033144eb999abd2d017a3dd9b247f685e0b488fdf7700c2184b06c4a5f06f2", + "docs/validation/artifact_publication_315_20260930/original-ratchet-initial-resources.txt": "bdb9721d92a0fac9cfa9f3c959b6e6480311cf4df4d599955b3e9ac963dad527", + "docs/validation/artifact_publication_315_20260930/original-ratchet-initial.json.gz": "85d628579b2d771875e47b4e8c235e5afd57bd15ad55c9619574681d347c70b6", + "docs/validation/artifact_publication_315_20260930/original-ratchet-scripts-resources.txt": "f8ad26db9e228856c0a5ad90e304c4e67d562e56ecfdb689e277a78946a292ed", + "docs/validation/artifact_publication_315_20260930/original-ratchet-scripts.json.gz": "f721787b37da1689019b7a6e2e24417a57ce372da245d9e8d27ed702fb66f1ba", + "docs/validation/artifact_publication_315_20260930/owner-controls.log": "6fa26fabf287c57a0fe06c5f244b46a7ea34b482bbcf3a8004b1e1aff9691e2c", + "docs/validation/artifact_publication_315_20260930/ownership.md": "d879067753f5ceb41428bf2a08f786bb5422083789c5efba376917ed0b71d4bf", + "docs/validation/artifact_publication_315_20260930/parent-installed-checkpoint.rst": "c07c834bc806b0713d07bf51545260e9420e0fee1e2db6847146ccee422b8900", + "docs/validation/artifact_publication_315_20260930/pr-body.md": "217aa78c2dcd386fb093ca5d4461c83332b26947c15a0bef92e32dd9b443c406", + "docs/validation/artifact_publication_315_20260930/publication-final.log": "adcaed1c83152ae331487094408cd8d360f5a9db9b3cb7a9a10bfe178755bc91", + "docs/validation/artifact_publication_315_20260930/qualify-original.py": "9d7280aa3bb4b2a50ea056304f8e7283d8e94ec3aaa681c2ab525b3f4f657018", + "docs/validation/artifact_publication_315_20260930/results.md": "4196b68aa14afae530ceca966a8f2c911343c86c457148f9b99efa64a2cbd58e", + "docs/validation/artifact_publication_315_20260930/run-bounded-r1.py": "fb2b897beda3dfb1da3be2fffe5b39b69187ab263c07009e4522c5765e104032", + "docs/validation/artifact_publication_315_20260930/run-source.sh": "af094220c56f56aabdb44406c3e3f003aa126d0b88f02230d7fc0c48063947f1", + "docs/validation/artifact_publication_315_20260930/runtime-controls.log": "7e234186cd5d05cbe6f881a18ced10993869144d1e177b554f8f7bd3d03ff325", + "docs/validation/basicpy_parent_numeric_20260930/check-persisted-live.py": "3038b6b84deab7def98068fc8877fcb34d48486738b3729c66c70c04c5addbdd", + "docs/validation/basicpy_parent_numeric_20260930/check-publication-fixed.py": "55fc3bafda5df955cb1026ea6f6aedc0056be4a2242b41bc7155942439a9c0b2", + "docs/validation/basicpy_parent_numeric_20260930/checkpoint.rst": "ccad590b657883de7fcb1cc5b7f5f2c6985f882d1bc57ddabd074c7f0c2a9e45", + "docs/validation/basicpy_parent_numeric_20260930/cpu-installed-checkpoint.rst": "5dd93a1cc8647c4a74c64a666b7d24381b6a3680e18af359fd3cded8a6c26221", + "docs/validation/basicpy_parent_numeric_20260930/installed-failure-receipt.rst": "e18ab6bc8c23b0b46cbfff2ed52bc25f0aa917a2e43220c9d419d3d4de03a80d", + "docs/validation/basicpy_parent_numeric_20260930/main-integration-20261002.rst": "18ceb85b7fba53b734698af0e078e515018fdcf01f89d0c9ad555ec988f974a6", + "docs/validation/basicpy_parent_numeric_20260930/owner-closure.rst": "7892cc2c64a3f8768e55935d8465c11ccd38b86fe2ae7fdd1854a5c8900c79f7", + "docs/validation/basicpy_parent_numeric_20260930/pipeline-live-wheel.py": "c78a56251fec8c8238071dc5aca9e170261ce91db6ed4f8ec380a01ff24ae086", + "docs/validation/basicpy_parent_numeric_20260930/pipeline-publication-fixed.py": "ad1a268f92f95127b6e59b46c885f17f6237774b97e2194c023627363dcc60b3", + "docs/validation/basicpy_parent_numeric_20260930/project-dependency.rst": "3aebb960c3031db6abd46819c801b6b41622993afd051852e966d4b59d10cb60", + "docs/validation/basicpy_parent_numeric_20260930/publication-fixed-installed.rst": "d11c75f1053c0238fd190fa2e71b99a73741ef16cec4727b1b54fc19e838c496", + "docs/validation/basicpy_parent_numeric_20260930/run-live-wheel.sh": "8242cd3c2e06073575ef55f799ab75c9587eb331d447a4d0dd43d851dced4dcb", + "docs/validation/basicpy_parent_numeric_20260930/run-owner-check.sh": "fa65185ca29803380e6055a0d1143a869276c45d3b64ff1f8b6d23f2c6c4aeda", + "docs/validation/basicpy_parent_numeric_20260930/run-publication-fixed.sh": "6aefef1898e5dc4914e7f7060462b9ec0323f1d7dd9ed8fdfc4c0cf7c7d9c462", + "docs/validation/basicpy_parent_numeric_20260930/run-source-check.sh": "029dcd6f1bc4f38ad3637418d939339953daebb36b5300afc627a34c64cda4bf", + "docs/validation/basicpy_parent_numeric_20260930/target-owner-installed.rst": "18df22a4c4feb0ac980c1f4f520079184642c0691aeb118d12fd58b47ad9bb76", + "docs/validation/bioformats-saved-calibration-440-source-20261002.tar.gz": "944296e6c2ebce08cda8734da534cc6bcecd5e7abd01e3aa2e9190ac1c986477", + "docs/validation/bioformats-saved-calibration-440.rst": "9abf682f48f8b209756ff02bca957005e16b9da0fe57f7ad2f5ea792072a9d1a", + "docs/validation/catalog-endpoint-incarnation-464-20261002/INSTALLED-ACCEPTANCE.rst": "a3757722f4b07352784053340d550be6d91109f43198c13edad4b4226577a44c", + "docs/validation/catalog-endpoint-incarnation-464-20261002/RECEIPT.rst": "0d75557d6928e148d36005aad2ef18402ba84f2c87dfa8ca6521d87a1a044ba8", + "docs/validation/catalog-endpoint-incarnation-464-20261002/owner-after.log.gz": "e49966caeb5b54f3a9d7ddc388bfb1deaf47d8e03b96e0f27d55929aa716d453", + "docs/validation/catalog-endpoint-incarnation-464-20261002/owner-before.log.gz": "43dd40957531c0327aa8822c729e6b9ff03c6ddee57f5d351d2fe81935e28036", + "docs/validation/catalog-endpoint-incarnation-464-20261002/pinned-r0-corrected.log.gz": "8880443eb248e7e443bbc24a2bc3227d9ffe25554f5c3cad2aa5a3d8592ee4f9", + "docs/validation/catalog-endpoint-incarnation-464-20261002/pinned-r0-final.log.gz": "621c9325eb9bcf5a9266ff6e7233e6027b71e03ec8e6d6c07c4cd24492e8a38f", + "docs/validation/catalog-endpoint-incarnation-464-20261002/pinned-r0-first.log.gz": "561a76791c429223de21d6b93cb5398db4943b54a9cc54770e1fbdc941026d46", + "docs/validation/catalog-endpoint-incarnation-464-20261002/raw-nra.log.gz": "2e6f2c9b98e9bce4a3b526041a4d6a918c88dd82c0e08f4a0995ad5c7de27f5f", + "docs/validation/catalog-endpoint-incarnation-464-20261002/source01.log.gz": "27d9d55ca17cf6fb5464cf9a3ef0365ef2257acc4d666cd7dd5a61da1c263fe9", + "docs/validation/catalog-endpoint-incarnation-464-20261002/source02.log.gz": "1612c6f3b5254c24db7c5bf5e4ff71fffdc9b7564a9c70f04cbb43e11a025d07", + "docs/validation/catalog-endpoint-incarnation-464-20261002/source03.log.gz": "14d3781b0a8cd44fb4912225ee9c6d37d3344301ad0bbcdea1dce0c06e6a8e86", + "docs/validation/catalog-endpoint-incarnation-464-20261002/source04.log.gz": "f149a9760a153740b975e7147aa9913bc5c71e6e6e0b81306e67af7f6bb12866", + "docs/validation/catalog-endpoint-incarnation-464-20261002/source05.log.gz": "ad04d3d7c8a2db779cb09c7140074ab2310c8c41b4cb2c97ab2c2e6298eeec7b", + "docs/validation/catalog-endpoint-incarnation-464-20261002/source06.log.gz": "c621ace1387a712bf307f25972b0db28203d2eb4f82ba5abf27f50315033f6e6", + "docs/validation/catalog-endpoint-incarnation-464-20261002/source07.log.gz": "71625a44885b90d5314f252dcf79838abae6164ebe964fd30bf10a1e4a686b78", + "docs/validation/catalog-endpoint-incarnation-464-20261002/source08.log.gz": "627d98bcf81b21048de5434d80238877fdb3d0c9c5baed2f72b6360854100e13", + "docs/validation/channel-contrast-review-20261001.rst": "1ea1785d8fd4a53a988d4855e72095dc23b20123e24cf476aeaf83983fc4e79a", + "docs/validation/check-bioformats-calibration-440.py": "96464e5b4e4bdbc20e029f901a64b74afa33b8279d4f228632aeb9c91b1d7d38", + "docs/validation/check-function-help-390.py": "88c0ac5465be59f4e6f3684b769ef939c7f860a6ca2eda5a2ef551b06a86e9a2", + "docs/validation/child-measurement-producer-selection-492-495-20261002/QUALIFICATION.rst": "5f369e46b1550fc10ee240d305fc44be483d8ff08b1b6ad163779f4aa448ed09", + "docs/validation/child-measurement-producer-selection-492-495-20261002/controls.log": "48038b27256912c2e3c977b9a035778c7cec1640137184c11ecfa93ea045f05b", + "docs/validation/child-measurement-producer-selection-492-495-20261002/integrated-controls.log": "25e6892c70e8be88adcc2d55ff41d8bd4208ef3c070f2170149bd219445e1a55", + "docs/validation/child-measurement-producer-selection-492-495-20261002/original-r0.json.gz": "4bf0e5414f2b3b0a60e2703713a13a4b3435180911e85a64d497cead44a3b3ea", + "docs/validation/child-measurement-producer-selection-492-495-20261002/original-r1.log": "b9f0c9e862359e5826e827359edffa4edecc345ded1840c3cfcccbe5891befe2", + "docs/validation/child-measurement-producer-selection-492-495-20261002/qualification.json": "37fd10e40b71c3f3ca2ad2d14bfe2eb552a97e23232d275b922b1f2a5c792f9f", + "docs/validation/child-measurement-producer-selection-492-495-20261002/strict-native-receipt.json.gz": "638ab12edb5efff30583aaff3b39ba32ca2b790d84345b7a055b5aa1554225ac", + "docs/validation/colocalization-metric-selection-487-20261002/cache-window-counterevidence.json": "c643418cbb9aaaf9da1de962b1f105a7f3272d6019e66dbe413c514fb185f07a", + "docs/validation/colocalization-metric-selection-487-20261002/common-saved-output-comparison.json": "6a3b2737c6adb71734af22dcefb3d41f2d6a3ec9960380c383652a748ee1a5fb", + "docs/validation/colocalization-metric-selection-487-20261002/manifest.json": "4df3d1e37b7f49efb6d4dfb65d657fad43e636c8a95fecf6abc4aee1fd8808e3", + "docs/validation/colocalization-metric-selection-487-20261002/qualified-controls.json": "96f34ac97ff5ca1bb96bbb50e83d4e9b49fe17b9c518f341728aff629d7db0ab", + "docs/validation/colocalization-metric-selection-487-20261002/strict-native-full.json.gz": "c84d03bf38a2e4b10e4a36feb1a7dd9f9933872b66473031f5083c371ce18aad", + "docs/validation/colocalization-metric-selection-487-20261002/strict-native-summary.json": "a73d12dd885125fa5837aad42e263ba5c6e48240c223a99039a9fd4373befb65", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/PR_BODY.md": "846b72ad5b71004c44dc1a239d3911eea65b898afac99209796af63c96b6fc49", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/RECEIPT.rst": "583596a6a0b36784ab93e7fff086077d95d34193e11db9fab65c329669cb18d7", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/SHA256SUMS": "e7da910e4cd04d99d66cd4af4b7cc9dc2dbc650f4a87a33b4c2fae4f37b77b87", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/SOURCE_FREEZE.rst": "800f4a1b25bda0885c966e2fb16387e2181b3ec879a9d23dbcf6a5ba9b7dddd7", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/baseline-legacy-shard-resources.txt": "e098be0e08dea8e2ae2abd8d3b44fd9706b7ffa2487eeaa9456a54fd4d279ebc", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/baseline-legacy-shard.xml": "5626928be73ca37ee3b9f968032d4669f657258034aeb0d9d9085a4cfafee14f", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/capture_r0.py": "6e4399a4368d60b39cc43ea4d82cfe072748dfded55c8611d4e4f3647699a38a", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/collection-boundary.xml": "2b0f0505cbe10a0208dfb2466ed4fbb6ff2d202421d63329fa2b13d9ba83e227", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/final-planner-shard-resources.txt": "136faf3d79164f9f6a7ce3e7554d90b94cd0d619c2cc2685ff0cf5db34775def", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/final-planner-shard.xml": "b5d0098adc4dcb011f81daa309cffb5784a1394741de205fb4aaffbeaa6838cf", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/metadata-controls-binding-assertion-failure-resources.txt": "a1bf255764570adb72c3eff6b2214b574a67579e463f0bc0374bac90baa308a0", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/metadata-controls-binding-assertion-failure.xml": "bc220f6fdb4f321fa25bb3dd33ba212169c3d49df8077627ca0a6fb7125a2874", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/metadata-controls-fixture-failure-resources.txt": "831ed27902a79af947c433225b353f1014f951a9cd4e5d3361dc42550a628d04", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/metadata-controls-fixture-failure.xml": "3b06f5e927616b0dc826f9dddfc1ec1febdd98b63760a67e076c22d6fda0375e", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/metadata-controls-resources.txt": "5985fb2a1184a670d205b882fdea9e2cd5c9be1a7180f8cdb347e1f6fddbd3f4", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/metadata-controls.xml": "ee2a825a1631139c223f458d48bfd2183911c6797d7efb471a6838bb707aa56b", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/mro-planner-shard-resources.txt": "6496eb0aae10667c590bad24a6e5335a1eb599983fd0762773765eb46223cac4", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/mro-planner-shard.xml": "17a6fa26bdf2fb3c89ebbb488b9d9763bd38ed3360121e525fdc875cde9b081f", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/original-green-resources.txt": "e601f920c1119835eb0f8a65c1c2723f4f2fe239c1f25aac314996bc194f3b9c", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/original-green.xml": "5519de5a0fe5480532de4f2fdb0f91b3c1fbed34deac8519e32c701b855070d9", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/original-red-resources.txt": "b3d61ddda15ca06ee95df4e58f6384d29d86ce64b2d4e8e612dbb7379fd65119", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/original-red.xml": "ee96bf627f8604b1e294a6edd26bfa01e28f18719d0b909ccd38a1daefd765ef", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/pattern-runtime-shard-resources.txt": "60f9f4e5cefbdf5858318fbdeabfc6c6c5ef29b1e8e0c57a9d32eb5e299a5bf6", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/pattern-runtime-shard.xml": "acea7a32f81c4b53d6fc98d5f0ff53bd5134b32313db5832734c9a189db60e2b", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/planner-shard-resources.txt": "9a5250beb89376ad414396491ec5156642b4cf376c331adce82fb8eba9731a05", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/planner-shard.xml": "aec249fa55af23238720d577ca4ebc931e577f06d956280605d2fa51bc594bf9", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/r0-first-resources.txt": "56d7c2afbcd1c452cf1908d5c1a267e45afb4ae2316895b3ea4aea2dfdcbe054", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/r0-first.json": "e6e403e314bf8084d9f82676c56cfef0c32a027226bf636aedd4290621aed68c", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/r0-first.stderr.txt": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/r0-qualified-resources.txt": "4527b1b5afaaa515d2bde22c33b26f73f8e1e23855bd1ffd2f2f8c6259a511d9", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/r0-qualified.json": "b9885273c495cdddf5c081d2f9c4fc5e45246a24ee57509bad4d805b01816ce8", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/r0-qualified.stderr.txt": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/runtime-origin-shard-resources.txt": "760c8c5b1c5cb64a755883bdbdc040611e3c24c555bfa31572b80492273e5ac8", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/runtime-origin-shard.xml": "5212829da9a87d3dfaf63558eb447e2356337584742fe4b113e2811ba59c4448", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/source-edge-shard-resources.txt": "2c705aba27e7066d0ab2d9f0d22619edc01f1cb2a145c0af124eaed74e046cfd", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/source-edge-shard.xml": "4712921e735971d3ed83ea74fb1df7e0d5006ef5859217de96d9555b3bd44ed8", + "docs/validation/compiled_metadata_artifact_binding_368_20261001/source_shard.py": "05f7dd98f7a449a0d86863b163807f700ba328cc9c5a1225c8fe66dd4a1634a2", + "docs/validation/context_bounding_20260929.md": "5b89b2440f9f97ad97e3d9871052ae47d4f15e0c6b1f6894f4bbb0244d06f7ec", + "docs/validation/context_bounding_20260929/build-resources.txt": "b9177265b12ec93e2b2fafd4d71000aef3cc6e177847848fdcd84fb00d3bdc4e", + "docs/validation/context_bounding_20260929/build.txt": "5d92d736ca4e0aa7086db67cf6a581c85645c7ad432516d791746386778a0c86", + "docs/validation/context_bounding_20260929/pytest-checkpoint-resources.txt": "e9d0ef10b1ca453d0387607b0603fc955956b7c6bb4a2403fc21e1f88499a3d7", + "docs/validation/context_bounding_20260929/pytest-resources.txt": "4730b783914d69df2df5559f4b496fd02d0463527176f228b0070626c9c084a5", + "docs/validation/context_bounding_20260929/pytest.txt": "c644c1958d436a41aed4ba1eac418d11ed66bca31d2750e8604ff066abdbe117", + "docs/validation/context_bounding_20260929/pytest_checkpoint.txt": "94a847d124339d6c353014d17e560447eacedc27bf567fe222fafe63686b3435", + "docs/validation/context_bounding_20260929/source-projection.json": "9c35601c2331c300256ccb088a10725bcf8b56e9df8503041543c840abac5e06", + "docs/validation/cpa-secondary-source-metadata-444.rst": "fbecd57b752d6ac6af5803d6a96698210de68332d6a147462ea5b96e2b83aa38", + "docs/validation/cpa-workspace-export-main-480-20261002.json": "83fc927bb066cc4d85ee1bfe5b14e8017dc8a5eea2ef09451c131ac3ab985e1b", + "docs/validation/declaration-selected-376.rst": "e71bf11e640a2bc50dafa1db22dd8375a161a7522412cd29db8c7eb7be01350a", + "docs/validation/diagnose-knowledge-376.py": "d09f0ed43b220a1452f02ecba88ebfca267247b2c607722c6b7b0b124d96ef0b", + "docs/validation/diagnose-selected-discovery-release-022.py": "f11048350dc61fe50d7638a4bbd43396a37f65b6aa4bcea64ee33803c794f3c6", + "docs/validation/duplicate_editable_metadata_20260930/README.md": "f804bd89cf3c89e67f822522150f6ff5c648e22640ed3aa02596a469174ee5fc", + "docs/validation/duplicate_editable_metadata_20260930/accept_shared_main.py": "10849580de618e902a8621012d0e68972e5a93d008d939a9452517d288b4d94a", + "docs/validation/duplicate_editable_metadata_20260930/after.json": "55b1f80727738bf8a89908f1f7401b72e7cff88681ad24d702ab9e2b6e9dce48", + "docs/validation/duplicate_editable_metadata_20260930/before.json": "7785adfbd5c4631aae1acba015805dce33b284fcbfce8972a91844eea5ffb178", + "docs/validation/duplicate_editable_metadata_20260930/install-retry.log": "971ea6d97b6c8cb9b7bf8288b866a9f872bee18082c864427baa1d413e212ad2", + "docs/validation/duplicate_editable_metadata_20260930/install.log": "1779b8c6f08324246a7082d7f09208f944518de3a40fde66d7b6c09769f0fdcb", + "docs/validation/duplicate_editable_metadata_20260930/perf-runtime-projection-shared-main-acceptance-20260930.json": "d5d7ea1e170f561833676c4d67c23cf899fa486f6920214d39601e4ba8169acf", + "docs/validation/duplicate_editable_metadata_20260930/perf-runtime-projection-shared-main-clean-acceptance-20260930.json": "0260592aae772fdb2062a7d5453479d3ee7d67499da663343bec89a99729490f", + "docs/validation/duplicate_editable_metadata_20260930/pip-check-after.log": "9261363b733079a641c2e4cc9bc46ffa1d8336945a87f807b6cf68847dbc9b09", + "docs/validation/duplicate_editable_metadata_20260930/pip-check-before.log": "375012431ab8acd88f801cac8b443b2122312f05f1fb9dd25e3a58d319338e1c", + "docs/validation/duplicate_editable_metadata_20260930/quarantine.json": "e3372214bde19c0260244bcedc7a7fd6da79d28c4bacf391c8e768fa0966917e", + "docs/validation/duplicate_editable_metadata_20260930/uninstall.log": "bada22327e0164fda3fcfb72fe3d3749db6138c153861ee8d0d88f5c123d3817", + "docs/validation/faint-structure-viewer-qa-396-20261001.rst": "69f9f7d3b0e5a92b3a83db317bf14625c333ea545e8bda3f1ef2abe35d0a4742", + "docs/validation/faint-structure-viewer-qa-396-20261001.tar.gz": "5e522283673fdb90c0ad5f6369ed9ad393aa3d24c3c049b040deb804e355e0f7", + "docs/validation/function-declaration-help-390.rst": "790f22ce0854f0eb8d54f7b41add83ff76174f3ae8d1d64f3213300f33db9ee3", + "docs/validation/function-help390-source-20261001.tar.gz": "49cf8f15566fa412242bbe0d8b3dcd22ab3d705564e36c23f6d0a21836929ee8", + "docs/validation/function-help391-registered-source-20261001.tar.gz": "f358213706b8dd8fba26939d4e699f0df1a29b50967e3f7bc51262b789e9d02b", + "docs/validation/generic_runtime_owners_323_20260930/README.md": "acaf4581c3404cabf9c7488b812a1d73139402125f5b72e96887cca66e979973", + "docs/validation/generic_runtime_owners_323_20260930/SHA256SUMS": "5d0c2150ce5990eb73a80c6fb7929731348c0e05002b7a2a863eebddf414fccf", + "docs/validation/generic_runtime_owners_323_20260930/apply_generic_measurement_runtime_owners_20260930.py.txt": "f811c8c56ad671caaa92d51be13c4fcf87eb55d098e22808f9c2dc84310cb83f", + "docs/validation/generic_runtime_owners_323_20260930/census_refill_preparation_20260930.py.txt": "94ce22b82a2ff871397f5aa827b79e7a390cb46c2a2fab08e77dbd19304fdd0c", + "docs/validation/generic_runtime_owners_323_20260930/check_runtime_owner_fresh_native_facts_20261001.py.txt": "33d08eea6b45dbbe76c7e0eb4afd9a861cbd832b98eb74688ed31df8993018b0", + "docs/validation/generic_runtime_owners_323_20260930/check_runtime_owner_fresh_native_tables_20261001.py.txt": "584a3b4d5d848aa9aa6400d552f6a7ca0a398160c17c92b29115d695306d1f6b", + "docs/validation/generic_runtime_owners_323_20260930/closure-global-gc-sitecustomize.py.txt": "69ce8bac6365fca894e58c666e72547f15e98ba3e2ebe154fdf7c7a1984765fd", + "docs/validation/generic_runtime_owners_323_20260930/cpu-timeline-candidate-20261001.jsonl.gz": "21e987a04a75fae94244197cca4cfaf87734ec208ca3b6555f848c76e21de49b", + "docs/validation/generic_runtime_owners_323_20260930/cpu-timeline-main-20261001.jsonl.gz": "794fe3c70b50df604d26ad73d646a946f07bc55daa22e28344fe5aaa19dd7aee", + "docs/validation/generic_runtime_owners_323_20260930/cpu-timeline-sitecustomize.py.txt": "5e6eade3d2c8b404ff7910001133bf5f51e0b5e2fa5c14780d888c1e812123cf", + "docs/validation/generic_runtime_owners_323_20260930/fresh-native-imaging-provenance.json.gz": "cde3ebcd93bbea6287a5e809225401118badd15c5c70cae2a31bf3aadfee2874", + "docs/validation/generic_runtime_owners_323_20260930/fresh-native-imaging-request_0.json.gz": "6007fd0a0bb5c49d6c2b9f2ed4536c0dc4a937563c4877dc1813ddf6508a583f", + "docs/validation/generic_runtime_owners_323_20260930/fresh-native-imaging-summary.json.gz": "84d17ba1384d1e908bd781b1b24d419d13949f804c41999634958cbd63b51fcc", + "docs/validation/generic_runtime_owners_323_20260930/global-gc-candidate-intervals-20260930.jsonl.gz": "a97203b9937186d312725de6314314f513e60e5ca4f5f65d5ffc481e53b8ac09", + "docs/validation/generic_runtime_owners_323_20260930/global-gc-closure-candidate-a-20261001.jsonl.gz": "b326124946b5391403c2874fc08961b91d9f07ac65a8e5252f291f6cf7d5313b", + "docs/validation/generic_runtime_owners_323_20260930/global-gc-closure-candidate-b-20261001.jsonl.gz": "868a746a14ce2ac5e076b069e7fd53400f9363ec9e301af66295718e89fb730a", + "docs/validation/generic_runtime_owners_323_20260930/global-gc-closure-main-a-20261001.jsonl.gz": "7c6808bb95297f94c40bd3ee1a074808bb7c4bc38e9e17c2c0d7f1f0a68dc35a", + "docs/validation/generic_runtime_owners_323_20260930/global-gc-closure-main-b-20261001.jsonl.gz": "03e545a5f82951362b3cab6260b86926e97454c893b8917d232f89307ab5afb9", + "docs/validation/generic_runtime_owners_323_20260930/global-gc-diagnostic-sitecustomize.py.txt": "1a6517c2b8c2cc8e9b1b1576f0f7c4811eb6b8b88fadb913d6081c50444465c0", + "docs/validation/generic_runtime_owners_323_20260930/global-gc-main-intervals-20260930.jsonl.gz": "8c31483ab3505148aad41f026180d5c79e875d84e95cd3846fb33bd3c2046fc3", + "docs/validation/generic_runtime_owners_323_20260930/kernel-preparation-ownership.json": "7b73fc33d3d06a0c967338f21fd1396ae51216703d200a39133117bae00f4956", + "docs/validation/generic_runtime_owners_323_20260930/make_runtime_owner_cpu_timeline_20261001.py.txt": "120db5ef628f5ea3fd637500535287b668f24aff61a253ed1653d09ea1970543", + "docs/validation/generic_runtime_owners_323_20260930/measurement-cache-and-column-ownership.json": "b04939348a028e78b884376d5aa0122d130cd767aff916c3dbfc4624679a3945", + "docs/validation/generic_runtime_owners_323_20260930/move_bound_runtime_profiler_20261001.py.txt": "476b7ebe896143deac434c944affaf7bb9b39ca4350fa1bbd9c392c40066041d", + "docs/validation/generic_runtime_owners_323_20260930/move_generic_numba_preparation_owner_20260930.py.txt": "fe0a2e90822203cff4a070e47ef02e5aaa4b8216f95afcd2ea3e8c7136f9d405", + "docs/validation/generic_runtime_owners_323_20260930/move_numerical_cache_mechanics_20261001.py.txt": "d527ce08164db70c6daa478cdc873a83ac4745758a80389fecde7dabb7a9aeac", + "docs/validation/generic_runtime_owners_323_20260930/nra-r1.log": "cd910aa324d5f1e8fc9573e18d76a546798b0f131e27cbb8b48cdbbbe4936fd9", + "docs/validation/generic_runtime_owners_323_20260930/perf-bound-runtime-profiler-plan-20261001.log.gz": "eabdb9b75fa08eedb84c33363b9e35b3e82a384a43b500c5f6f7bd21f967d46d", + "docs/validation/generic_runtime_owners_323_20260930/perf-bound-runtime-profiler-simulation-20261001.json.gz": "f149be2f9a012008661151a404d51d511a0d3b2400b5b7bed6a238f3e2f66592", + "docs/validation/generic_runtime_owners_323_20260930/perf-bound-runtime-profiler-transaction-20261001.diff.gz": "1e1c7c8d830f8906012e60598bc5899cfdfc4a44914fb3705f4ea7a678b36729", + "docs/validation/generic_runtime_owners_323_20260930/perf-columnar-export-frontier-probe-20260930.json": "5c54512dcdce2b8526bf2efc5ed75f2862649632475ab6ab78f6e5be266dd7ac", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-measurement-runtime-all-consumers-20260930.xml.gz": "715f4ebf7e8d88180eacce70b90153c783616dcce797d36ea2135bef1ef957ec", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-measurement-runtime-before-census-20260930.json.gz": "3999c8d302700616164169c4d2a1075263dc004984eaed8fb22ab1f465bbc9bc", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-measurement-runtime-focused-20260930.xml.gz": "24b9af7fa6d55c8799fc10828c6984f28042a04d55caad5b965c2fdffa244ec8", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-measurement-runtime-nra-transaction-20260930.diff": "351016c98e961a1908a59b828c09ee4fc293d1c126923be3e88783a148ecc694", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-measurement-runtime-public-comparison-20260930.json": "37c1f8f5044ac4865731904290a376ec4d52ac3d9d2096961fb9883bfc3336a3", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-measurement-runtime-public-observations-20260930.json": "068e41e5ef204ed81d194e4d74ab8f680788ad89985b26976e67fdedb1a58a18", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-measurement-runtime-refinement-20260930.diff": "5d6a203617b60d9f666b645db0f6418216b74c95f478babdba7a3dce941af472", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-measurement-runtime-unchanged-main-materialization-20260930.xml.gz": "aa6ca25c2c7710b28239424328cd5db70f45a352af6103024c92c7bb9fa50d2c", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-numba-owner-application-20260930.log": "608fd83495f1cab4d6de55f2b2594e35abee5767beef577944a0cb33fe3154b2", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-numba-owner-application-corrected-20260930.log": "547bd55b8968612ee9a7fa4e1fdbc2d8cd8539f8b2d624aa8cbee6548e6c008e", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-numba-owner-complete-simulation-20260930.json.gz": "07fdeb69d3f170e9fb5970f81c160081a2ed9f692e6610f6ba28797b8c74296b", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-numba-owner-consumer-test-20260930.diff": "02953b04c2af9f34709a77a5f8d31cd14fd63323cbe2d92016072db6da16e953", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-numba-owner-initial-simulation-20260930.json.gz": "0ef0e0ea3c540db1b0c2a0a67cf74aca0043fb3b05c3ba6eb3a5e02f4c17a51d", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-numba-owner-transaction-20260930.diff": "a4edd93a01f35364580d1a72f4d22d535cb032a385e0528c29d8f57ffdb736ad", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-runtime-cache-lifetime-refinement-20260930.diff": "442259f07ddef37242fd141c700f2364a7aa6e188c5ad0dc461dcb4dd4349302", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-runtime-global-gc-diagnostic-observations-20260930.json.gz": "9f4dcf635fd7f5edf1d93a0a6dd3b8dbaf2403efd2eedb7998ec7de98885d828", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-runtime-global-gc-overlap-20260930.json.gz": "fbbf338c32b3775dbc8cb664015ea98c344525d46c46f01ef831fc7b62f6d149", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-runtime-imaging-confirmation-observations-20260930.json": "231afd93b32397b47daf0a4b3d6d7400bb4dec8fc3d5262a016a75402138795a", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-runtime-owners-complete-native-artifact-parity-20260930.json": "0e4f21a37c8b9efaa88162c7f28bc18238a605123e4498bf6c1ca464d0eaf6f2", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-runtime-owners-complete-public-comparison-20260930.json": "2ecdf52add8d59670cad2fe7bae2382bb80fa4ee3c47a5f44f038e7dcab80ed3", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-runtime-owners-complete-public-observations-20260930.json": "c9ac1892db4386160f4671a3b1b32135543b0c6aa1ac24babbf837f1ceac617d", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-runtime-owners-paired-main-census-20260930.json.gz": "93efa739dfe467693536873670c2d35ceaa988962e16d21431d9948843fb63e9", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-runtime-owners-paired-main-consumers-20260930.xml.gz": "c07f660f556cfad7606c45da0bd2e202e98786e0aa03ed1a486229fd9dcb2c80", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-runtime-owners-paired-main-public-comparison-20260930.json": "b3c8851acfe6d889c3a4da647697a881384b3e8a06b14254c33862f8a8e4a245", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-runtime-owners-paired-main-public-observations-20260930.json": "8457700a5d399e9f1a44c5e8822844a5fd5260835424486cb5dc5e0ac5d0808b", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-runtime-owners-paired-main-r0-benchmark-20260930.json.gz": "3a090e8bc409e732966d5d5f4493b93cae8628c235f1a499e806f04493a89a29", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-runtime-owners-paired-main-r0-openhcs-20260930.json.gz": "39fc10c2f32b6527ddd922b2d7422144818a74dfcc21b0daf5a2727a20c8ff0f", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-runtime-owners-paired-main-r0-scripts-20260930.json.gz": "7fd1e33e5ed6bcc1ee3399f51106c6fab21fe96afb56baca13da4d24e2e39a51", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-runtime-owners-paired-main-r1-20260930.json.gz": "66f0f0b649c6664db782e050c8012ac444d5ee72c97c1222fca0ede32eae7c0e", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-runtime-phase-diagnostic-20260930.json.gz": "79b09a99387139ab0eb0252e707ddec5576fb1d370a886fb19e1b15ee95b91de", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-runtime-phase-diagnostic-observations-20260930.json": "260e95439002eaba2f1a079d517fbf7e82ff338821bdb286cb6619b5d0ba5232", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-runtime-query-hash-costs-20260930.json": "edfc603c06fc80f01c3caccb6a638ef201cef3d22bc74f0ea0fe2bb59380c0a5", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-runtime-query-hash-diagnostic-20260930.json.gz": "1c876e12ec9e6d7d4101e6b25311397bd1396ae8d5601dcffaad7f5d85bd4bf2", + "docs/validation/generic_runtime_owners_323_20260930/perf-generic-runtime-query-hash-diagnostic-observations-20260930.json": "9971939a1ba0f59790497f823c87c80b7bfc13b41089532c4be8251e5eaa8ccb", + "docs/validation/generic_runtime_owners_323_20260930/perf-granularity-active-spectra-production-replay-20261001.json.gz": "f5fb958c42347bd57cce48b10df065c60396c5b995c788d34f2d0d8a07e1d56f", + "docs/validation/generic_runtime_owners_323_20260930/perf-granularity-constant-spectra-production-replay-20261001.json.gz": "aeb705f6a2c2192e04121b0569e389266b8a5ca4a034af811ec774810072344b", + "docs/validation/generic_runtime_owners_323_20260930/perf-granularity-maxtree-admission-20261001.json.gz": "97acbd937fdeab5a484cfc0778e3edcd1efdbb8d0145457abdf3e8c690c21e6b", + "docs/validation/generic_runtime_owners_323_20260930/perf-materialization-ack-current-main-corrected-20260930.xml.gz": "b9848aa105498128470b5a39e84c874ae97a6c7a6b3416ebaa576f8777ea8066", + "docs/validation/generic_runtime_owners_323_20260930/perf-measurement-provenance-frontier-probe-20260930.json": "6c2e8b8e742394ebaabf1b5a90a77f2cc83aff8edc2ea9b7a994bda3531774e7", + "docs/validation/generic_runtime_owners_323_20260930/perf-numerical-cache-owner-simulation-20261001.json.gz": "5f61e515cf8cfdda7ec41642bdcb50df8368cf15601a9d11607f541939cb5337", + "docs/validation/generic_runtime_owners_323_20260930/perf-numerical-cache-owner-transaction-20261001.diff.gz": "b68864ab4ac025a031b2cf6cb0d578a58a0114fe9bb2812fe52acf4a07405bcd", + "docs/validation/generic_runtime_owners_323_20260930/perf-numerical-cache-owners-before-census-20261001.json.gz": "1a6f5fd354b349b3828917595bf625a1fce6cb095658a00d87aa9c48a5dbb653", + "docs/validation/generic_runtime_owners_323_20260930/perf-numerical-cache-owners-public-comparison-20261001.json.gz": "1607f17f03daeadfcb3ae7584b9644e38c03ce0d0f6f643fd61aca4b9084b494", + "docs/validation/generic_runtime_owners_323_20260930/perf-numerical-cache-owners-public-observations-20261001.json.gz": "0d647bc19eab2609c04dd9effe7a4d1546ddbc705261e679de0343c5dbb8071b", + "docs/validation/generic_runtime_owners_323_20260930/perf-runtime-owner-closure-census-20261001.json.gz": "a81c59e0a166b6cd7d5187d3cb98fe4644eecffa5bcbe6cc97c80a15b556e8ca", + "docs/validation/generic_runtime_owners_323_20260930/perf-runtime-owner-closure-combined-tests-20261001.xml.gz": "e22c93ed6b91282c3a9ad5772696a20d2cfb51600e4fabe72dcbff4e46bdbda9", + "docs/validation/generic_runtime_owners_323_20260930/perf-runtime-owner-closure-decision-20261001.json.gz": "4df0f9526b980d3fa365ba7291f97e5bab086b81165058b99eb386ca61d438d4", + "docs/validation/generic_runtime_owners_323_20260930/perf-runtime-owner-closure-global-gc-diagnostic-observations-20261001.json.gz": "2e99a110b98c5cb17953b082f6b65bf6f4c3fc628ef99dc227d8b59007b6e9c0", + "docs/validation/generic_runtime_owners_323_20260930/perf-runtime-owner-closure-global-gc-overlap-20261001.json.gz": "24d626dd75926894689beeaa850ad6c9b046f0d69a820ca0544cd7e6baa4efdf", + "docs/validation/generic_runtime_owners_323_20260930/perf-runtime-owner-closure-native-artifact-parity-20261001.json.gz": "97235d573eb8500fbb4a3df157c68ff21adf1a800433611b0443bd95afdf823c", + "docs/validation/generic_runtime_owners_323_20260930/perf-runtime-owner-closure-r0-benchmark-20261001.json.gz": "e54285d23bfbdc7274698b6291be496048c560a8bcae693d8ab5a58ea383413f", + "docs/validation/generic_runtime_owners_323_20260930/perf-runtime-owner-closure-r0-openhcs-20261001.json.gz": "9da0cdeae0c1a6ce89a028860270468ed8b8d752573bd4124e7c3e265f4bbb9c", + "docs/validation/generic_runtime_owners_323_20260930/perf-runtime-owner-closure-r0-scripts-20261001.json.gz": "6e1eed08ebae793c0dfb8c28a99fb8210d318095cb73692438f440193e0354fa", + "docs/validation/generic_runtime_owners_323_20260930/perf-runtime-owner-closure-r1-20261001.json.gz": "ef8ed3b62f225ee4049ff4bda30c91925e09e35f933f3bfb0ff71c9ce80c6a26", + "docs/validation/generic_runtime_owners_323_20260930/perf-runtime-owner-closure-step-deltas-20261001.json.gz": "c54b6f8d9846e2018417a634e879568386f07975525ed97c479b38ce6c713d63", + "docs/validation/generic_runtime_owners_323_20260930/perf-runtime-owner-cpu-timeline-attribution-20261001.json.gz": "0e10472d75e657b9bef4a2c64e16e1ed435fd8fc00e1fe205f654d47cbf5a346", + "docs/validation/generic_runtime_owners_323_20260930/perf-runtime-owner-cpu-timeline-observations-20261001.json.gz": "4d5387738be36f2a347f919eb1e0d4452d5537ee0512d9b5bfb35fc9c8f1a866", + "docs/validation/generic_runtime_owners_323_20260930/perf-runtime-owner-fresh-native-fact-parity-20261001.log.gz": "a5505b33d3b9dec8731ac0c1e54959cfcef8ed4d73f288de81c832a8659f489f", + "docs/validation/generic_runtime_owners_323_20260930/perf-runtime-owner-fresh-native-reference-result-20261001.json.gz": "d8b42efa1d665f118c963c756cb27abbc42ec74ef2b3a514b3f7ae523016b930", + "docs/validation/generic_runtime_owners_323_20260930/perf-runtime-owner-fresh-native-table-parity-20261001.json.gz": "7b0f898e093ec6fb7103adfbd053c96073d7f3678a0687e51dadb510db02ff03", + "docs/validation/generic_runtime_owners_323_20260930/perf-runtime-owner-native-binary-inventory-20261001.json.gz": "87fd01d23102c614e6f6eeec6b1d2d7142f438ce32574a05bf836fab4a941c49", + "docs/validation/generic_runtime_owners_323_20260930/perf-runtime-owner-native-binary-sections-20261001.json.gz": "d3b53cba58c6fa534103474240df70b182612a6ad37e9c4345ae1b7b4e33532b", + "docs/validation/generic_runtime_owners_323_20260930/phase-diagnostic-sitecustomize.py.txt": "ff2bc219b1a5268cf4a064f224d89cfcd3b0c69a32427bb6633d31074f4af031", + "docs/validation/generic_runtime_owners_323_20260930/prototype_granularity_active_spectra_20261001.cpp.txt": "751863adb3ebe1bad8d0226144ace36011a3cb7507c7075660918129e3d0c789", + "docs/validation/generic_runtime_owners_323_20260930/prototype_granularity_constant_spectra_20261001.cpp.txt": "e7d285828c814cd8e772473f26657472fc948e204ddcdb8410882d653844bd7e", + "docs/validation/generic_runtime_owners_323_20260930/query-hash-diagnostic-sitecustomize.py.txt": "1277d57e6e12687ee2815f27309dae9e97ee5701b44b83091211e0e904e26a3f", + "docs/validation/generic_runtime_owners_323_20260930/refine_generic_measurement_runtime_owners_20260930.py.txt": "788638395ab7ba7ff35bb3ad0ce15de745ad8bd028dabbca4c586d38a00ca17d", + "docs/validation/generic_runtime_owners_323_20260930/replay_granularity_active_spectra_20261001.py.txt": "44cc7382bbdf46221fb60b57923b520b806e09b472811d243cb3c3f31550b4c8", + "docs/validation/generic_runtime_owners_323_20260930/replay_granularity_constant_spectra_20261001.py.txt": "3904cd2d117602fd10873e84b1b44150345128f1f3298d66dc3d26f1fd03dae5", + "docs/validation/generic_runtime_owners_323_20260930/run_generic_runtime_global_gc_diagnostic_20260930.py.txt": "dc913b82d2c5ff5697cccc2f86a758b6be1c1da0b13fd58f75348d8591e6c625", + "docs/validation/generic_runtime_owners_323_20260930/run_generic_runtime_owners_complete_acceptance_20260930.py.txt": "bceb63cf7887e666b41659255b9c9a90711eabf9f19ba6be4ac4a300c7d81c6a", + "docs/validation/generic_runtime_owners_323_20260930/run_generic_runtime_owners_paired_main_acceptance_20260930.py.txt": "6d27682ebc27534b4239c6f521e57511c252cbbf33b46a6c055745e96b8223e6", + "docs/validation/generic_runtime_owners_323_20260930/run_numerical_cache_owners_acceptance_20261001.py.txt": "701ffd4cdc9411d5554679c04d1bc63d3da44ad1cedf7672491ffa5ed5ff66e9", + "docs/validation/generic_runtime_owners_323_20260930/run_runtime_owner_closure_global_gc_diagnostic_20261001.py.txt": "d5fea9a13a7274e107c152de14422e0c6bf6e5a619bf6df590ba46807161ea63", + "docs/validation/generic_runtime_owners_323_20260930/run_runtime_owner_cpu_timeline_20261001.py.txt": "bbd45e7ef81557afa8247897c116ace623ecde1bd57c3e11986dbc519a18c106", + "docs/validation/generic_runtime_owners_323_20260930/run_runtime_owner_fresh_native_reference_20261001.py.txt": "8c9a358652706862a06b023b732189d614e628546bfb084d2ca79813c6bf1bb5", + "docs/validation/generic_runtime_owners_323_20260930/separate_cache_values_from_process_lifetime_20260930.py.txt": "c75793d666d48231d018d00c5a9a5888484f19044dc0e5203aac91cf1763419e", + "docs/validation/generic_runtime_owners_323_20260930/test_generic_registered_numba_preparation_20260930.py.txt": "a1caf62cc3213dc4b22012d7bec071c96f69291da2be7e7813e8e910ba0261a7", + "docs/validation/generic_runtime_owners_323_20260930/test_shared_numerical_cache_owners_20261001.py.txt": "4499f08266a16f84cf98756eee83ad4c2b65d13ae12bd0d153c733a3051b3413", + "docs/validation/generic_runtime_owners_323_20260930/validate_generic_runtime_native_artifacts_20260930.py.txt": "cf1507206fed074104e432033711f4ea98762fc4a328bede9e031464fecc90dd", + "docs/validation/generic_runtime_owners_323_20260930/validate_generic_runtime_owners_complete_outputs_20260930.py.txt": "77b93bf1147aed2551cb0d4099194f17b1e9d9c203c2a11ea3f751d479686915", + "docs/validation/generic_runtime_owners_323_20260930/validate_generic_runtime_owners_paired_main_outputs_20260930.py.txt": "c1b5cb55dc12208ffad5c904da93235d6a33616a580c2775a37ab67c011f240c", + "docs/validation/generic_runtime_owners_323_20260930/validate_runtime_owner_closure_native_artifacts_20261001.py.txt": "945bfa8f8e001e9e5a3c6a231de82bf682eb1b7eb653186242af5932b0ddbb37", + "docs/validation/generic_runtime_owners_323_20260930/validate_runtime_owner_closure_owners_paired_main_outputs_20261001.py.txt": "c6965dbee67d9d759f20f22f6294c4d69840a164fe2853edf4603d8a21959c4c", + "docs/validation/generic_runtime_owners_323_20260930/validated-source-tree-95a98b008.txt": "1f47d79bc11ae78c7aa9d1740b94da396ab48969966b2d4cb254b8fb7b79f89b", + "docs/validation/generic_runtime_owners_323_20260930/validated-source-tree.txt": "6b28fec58c0a0dce691a0ab0bcd4e1372cb8c05ad71b3222d41158201d0ffdec", + "docs/validation/graytocolor-binding-axis-432-20261002/carrier-compile-diagnosis.log": "907dde7a9413803ddd90424b658d1b2bf706a7c80d6a452b13242b255e3f26ad", + "docs/validation/graytocolor-binding-axis-432-20261002/carrier-compile-parent-admission.log": "91f69e3172ceceb00ad5f426b13ec551988d97c2b2cb28069c062f4f67d4bed6", + "docs/validation/graytocolor-binding-axis-432-20261002/carrier-creation-receipt.rst": "69e0547d53be831e6ee84d1c32227fb3581e203c5b8ed8cc4db5c929c3ca5b4b", + "docs/validation/graytocolor-binding-axis-432-20261002/carrier-repair-binding-controls.log": "eb34d888882103ff4daa79c44226d7cf2ecc88682bf71b4c934729ac70fab417", + "docs/validation/graytocolor-binding-axis-432-20261002/carrier-repair-controls.log": "3800c60f6a49421a64d44c14a200054eb3e722926b05505aa7a09749dce128d0", + "docs/validation/graytocolor-binding-axis-432-20261002/carrier-repair-initial.log": "a39a2d6fd3db7dbc654ce4c860a48fc6d30e0a48c2ca439c4be0108c2032486b", + "docs/validation/graytocolor-binding-axis-432-20261002/carrier-repair-newcase.log": "5e54e8f5f865f27e2c0066551cbc93b5de7f45100a6af1c53d182b9c872f96ce", + "docs/validation/graytocolor-binding-axis-432-20261002/carrier-repair-owned-proof-controls.log": "a6059017178e17a14d030ea0b80204f502debb86acc7ce58f67abf0d90117cb9", + "docs/validation/graytocolor-binding-axis-432-20261002/carrier-repair-owned-proof-r0.log": "190618d057afb4b4a73d9282869db1e15e77d238816f57e226ff0b022c65b781", + "docs/validation/graytocolor-binding-axis-432-20261002/carrier-repair-r0.log": "cdef90688df3363145a613992dd5b033c7fcb61239bb1804d42b816f7e2bed0d", + "docs/validation/graytocolor-binding-axis-432-20261002/carrier-repair-receipt.rst": "b149e5985857b640d047cbcb23eec4c181b94f5e33e8d3dc2d9917d66aa10964", + "docs/validation/graytocolor-binding-axis-432-20261002/carrier-runtime-lanes.log": "08c1407f95b3ab3e3ec11226310d99b307832d28baef19248f207ecf7f2c49b9", + "docs/validation/graytocolor-binding-axis-432-20261002/integer-binding-units.log": "1af396a9bd634d34391fc57024211caf0d5371cd6c0d74657ec264fe7528397a", + "docs/validation/graytocolor-binding-axis-432-20261002/obligation-owner-controls.log": "0f7a895e1e12b95e52eed0609a06f5b0e2fef0647024d6223e249d35e059365f", + "docs/validation/graytocolor-binding-axis-432-20261002/obligation-owner-final-controls.log": "4f92bce420f26bb650ffc95c9095dc6e8365b0a3dd5d49dd085d3169c6c1ab82", + "docs/validation/graytocolor-binding-axis-432-20261002/obligation-owner-r0.log": "887015a10f6db24f7aca9f3e9cfac33957c1d6d73ef7c0567a06009998b6e68b", + "docs/validation/graytocolor-binding-axis-432-20261002/obligation-owner-receipt.rst": "f97f0e68dfa0bd9532447b20e6812442adaa40a5d8284fd503e2ba24266e6c7a", + "docs/validation/graytocolor-binding-axis-432-20261002/obligation-owner-scoped-anchor-controls.log": "92dd0c2024dbe42e72a15a237803c12d156058ce9c76e31e086180fe6c5f016f", + "docs/validation/graytocolor-binding-axis-432-20261002/original-assisted4-status-excerpts.log": "1e424bf7fcc646dfa9bb039f437925a39fc81b33f1f7fc25f8c476ba0cc63b51", + "docs/validation/graytocolor-binding-axis-432-20261002/original-native-excerpts.log": "31d2d4a60cbe700993e2a772fe4a5178769cc1043e828dd98179b9d896ce50af", + "docs/validation/graytocolor-binding-axis-432-20261002/receipt.rst": "e7d229284ca917e0173c714a5ebcaa5004d520f4e897ee7b98ee2118def868a1", + "docs/validation/graytocolor-binding-axis-432-20261002/source-diagnosis.log": "fb20f19413a334217179f30046bdd4d4607a0ab3cb0216c734eb048abbd305ca", + "docs/validation/graytocolor-binding-axis-432-20261002/working-pr-body.md": "82615acbe9d2bc156ee9b36facb4f3e6faf568fd5c960fc200d926b645de5e6c", + "docs/validation/graytocolor-singleton-binding-432-20261002/composed-consumer-controls.log": "06c57ff3a4615f5ebe330454a6e526ecab692d6b570a9c5b6e6300438d6a7d84", + "docs/validation/graytocolor-singleton-binding-432-20261002/declaration-plane-controls.log": "65a0c94fa21a3b5c79d03cce732792e517df9d42976a5b846bcfe9ee614f6dea", + "docs/validation/graytocolor-singleton-binding-432-20261002/existing-module-controls.log": "74ebbfbb24ebd5967e0f2e4724a40e0351ad9030ad6f0099570a8ed1a4779c92", + "docs/validation/graytocolor-singleton-binding-432-20261002/main-integration-controls.log": "eacb5e228d71398484c4b6b04b02a1bbefb97be0e8d05b4cc852c0fbbdc3ab45", + "docs/validation/graytocolor-singleton-binding-432-20261002/main-integration-r0.log": "61b329cd166dc48365ba4012fb01fcaf620e2a4c4f118d3af3c64efad7c75165", + "docs/validation/graytocolor-singleton-binding-432-20261002/new-declaration-controls.log": "a702d2b16cee42fec12ba5c41a51a1e06f39bac0264e18f6dfe7168e472f03cb", + "docs/validation/graytocolor-singleton-binding-432-20261002/new-declaration-first.log": "6f498cc3dacab75a1ce44f9dbc0430443c24c431ca8baf617ac5cbf07cf35ecd", + "docs/validation/graytocolor-singleton-binding-432-20261002/original-unary-red.log": "bea7fe37138af10be784f516570fc118efb1150442b2cf70304fb89161279c3e", + "docs/validation/graytocolor-singleton-binding-432-20261002/pr-body.md": "d798f67f805773c6c3a33d228a75532dc2136d73370cd275722641f16c65ad78", + "docs/validation/graytocolor-singleton-binding-432-20261002/receipt.rst": "89adec88cafb225ed65e623826c4980086a0737a6e02042fb3df476a5cad6104", + "docs/validation/graytocolor-singleton-binding-432-20261002/repair-receipt.rst": "09254298002cf762cb574889bb3e2594013ca9fdd8c82190d5319d26901b17cb", + "docs/validation/graytocolor-singleton-binding-432-20261002/run_original_r1.py": "0fd56084e477277733c4e8073d8c89614af38554f7d1efca78e9d0c8266701f6", + "docs/validation/graytocolor-singleton-binding-432-20261002/unary-repair-controls.log": "8b8cd3c734d070dd9be99b0b75c9dbf9ed1059e1a458cec2b878e5ca719103b0", + "docs/validation/graytocolor-singleton-binding-432-20261002/unary-repair-first.log": "f84c357bde143e5ba566fc58b5658e079aa55272a37691465c492f5449dabac2", + "docs/validation/graytocolor-singleton-binding-432-20261002/unary-repair-r0.log": "a361b4720b0fa860ebed4af5a5f6438aa52cbd073f4f341e5844294335aceb91", + "docs/validation/graytocolor-singleton-binding-432-20261002/unary-repair-r1-exact-context.log": "3f6b96ba2512bbef04c4042f986061d783a991daaa063d48dda71585854cdc9d", + "docs/validation/graytocolor-singleton-binding-432-20261002/unary-repair-r1-verified.log": "416882a9636e31bd0081d82200463b4d79f1f858f6c0da98dde6c6c8a8e2e2e8", + "docs/validation/graytocolor-singleton-binding-432-20261002/unary-repair-r1.log": "0f8a790b5dab177921b963cccd61fb94513d4293a2aa80b0fcb1e637478e48a9", + "docs/validation/gui-cold-connect-355.rst": "a2d76fd5e697217f5a65714ed5ffbfc28c49ac50bad219d9fac0b2bdeae6392d", + "docs/validation/h001-stream-plane-427-20261002/GREEN-contract-execution.log": "faabccf838d5dfe0d4297daef6a31b85445ed75bf8734803a29d304e1ed9770b", + "docs/validation/h001-stream-plane-427-20261002/GREEN-declaration-controls.log": "32eb22bf00e5e5c62f4844fbe2a03e09e95ca60a8aa39bb9f138465bce2c787a", + "docs/validation/h001-stream-plane-427-20261002/GREEN-final-stream-shard.log": "4e37ef683861fc79d6352fda0cb6d695223431e7e08a7723c7aa38f190d25b5b", + "docs/validation/h001-stream-plane-427-20261002/GREEN-first.log": "70c0a3186736f1dfa4e5f2cc03b4504fe525b721186d19c8ff3980422b9091e9", + "docs/validation/h001-stream-plane-427-20261002/GREEN-main-flow.log": "39824bd95cfc7f27a18fcea7c6c3aa5bb47181b7bb9fd6edf46b7b25d5cecf56", + "docs/validation/h001-stream-plane-427-20261002/GREEN-newcase.log": "a26cab67cdc63f2d91391eae580da7cf31a3c713072bf21d4ab1379a51efb035", + "docs/validation/h001-stream-plane-427-20261002/GREEN-strict-native-shard.log": "08fa2b6eada6da8d121075bdf39c24d62c74d8969f1984fc615cbff4daba6610", + "docs/validation/h001-stream-plane-427-20261002/R0.json": "e82f99e97fef098bd0f48320cd887c64f71fa9ec9d756d827d54159d967cc26b", + "docs/validation/h001-stream-plane-427-20261002/R0.resources.txt": "2a70155afa1e06aa58c5cad5ccd220bb9618055c5410cf7a3c648cda4ec79735", + "docs/validation/h001-stream-plane-427-20261002/RECEIPT.rst": "0721cfb7fcf65c1aa565964db405e0d92133455b58668d5f7d7c908a4177e1ef", + "docs/validation/h001-stream-plane-427-20261002/RED-original-producer.log": "334e41c5ca62bfaf4e0f46eeb0976d677e5ae58f4eb302a813277bddff989501", + "docs/validation/h001-stream-plane-427-20261002/RED-production-canonical-config.log": "723e5b0b8df0af583b7221268727e4bc057309a2d96a9adcb8e91145051a2e29", + "docs/validation/h001-stream-plane-427-20261002/RED-production.log": "18d1efe4be0240f2bddb33ac5a40bbcb94133f98a8d79ae99f0e4815f0fd17f8", + "docs/validation/h001-stream-plane-427-20261002/RED-seeded-producer.log": "6da534ad4b089b898fe118edfc4ee27bb10faa0b1ea51c46f20c60cafa5e7677", + "docs/validation/h001-stream-plane-427-20261002/RED.log": "85beb75abf569c53ac9fe104f99010dab29168d2bed81c78161bba917a9cbca5", + "docs/validation/input_preparation_installed_20261001.rst": "e6eaeb49b2e7f05af98874dbecce6b3b1570b01f455cfb8ee7a11aee77c95f71", + "docs/validation/input_preparation_parent_20261001.rst": "9bd95a547e786b42c7a1d882fcad2dca518f497afd51f177c8ee3cf4b18e973c", + "docs/validation/input_preparation_quality_20261001.rst": "1dee61ebca89b1685b12a14dbfcb9ff40f2b0cab03a10ba4eb80d0f60c61f48b", + "docs/validation/intensity-domain-skill-20261001/VALIDATION.rst": "2b37cffb0fcc0a9b94d218e7cc208c88b9912052ad8a536af2308525e852902b", + "docs/validation/intensity-domain-skill-20261001/quick-validate.log": "bb45634c2dfb41fe24517a54162996b5649fd30cdc5aa58a07998ad52b3123e3", + "docs/validation/intensity-domain-skill-20261001/registry-g.log": "5e4d8ad984fd69a1805d8c7f13ab53c5d3aff7defaab42732268c80e50a88c6b", + "docs/validation/intensity-domain-skill-20261001/tests-a.log": "c01f3653fd221c66d766a585b0ba4e28a2f9dc95894c72590a615bff6ae29c6b", + "docs/validation/intensity-domain-skill-20261001/tests-b.log": "66990fbc0235687378d87924c2137af18959472bedf5ce53e8d5e8961809d739", + "docs/validation/intensity-domain-skill-20261001/tests-c.log": "1ded53aee79417dda59e6368a2b0984d85ed5260c29b96266c8a39638a8cffab", + "docs/validation/intensity-domain-skill-20261001/tests-d.log": "fe079be6315a5667045d060eb24b67dee9be8ead2be574ea4a46c1ef9387fc49", + "docs/validation/intensity-domain-skill-20261001/tests-f.log": "7d86b8be4cd48a31f94cfb4e3d2402215d881524fdac29257822f98225921472", + "docs/validation/intensity-domain-skill-20261001/tests-h.log": "c89edf06d6e634894e9bf84abf3c14fe0a6d0deb8199a69faffe0d066584095e", + "docs/validation/issue257-draft-pr.md": "dd94b04ce76f03512d405ef63a8d8246c961473a99437e78d035258917b1868a", + "docs/validation/issue257-implementation-20260930.md": "e622c9a6df5af72bfe328873103551e5ad9fcf856c97cabf945707e672889140", + "docs/validation/issue257-runtime-composition-20260930.md": "075321618b6e138179737c75a71bd7cd27323c384c247fb894b622b3741ae956", + "docs/validation/issue257-runtime-composition-receipt.json": "f279afca16d3c4157582ee6d33a124789f5946046652086697890996c2267b65", + "docs/validation/issue257-source-investigation-20260930.md": "f02eb2feda1405140ed8c737dfb30f8d2a64894ae6ece785f9b318b78d4c048c", + "docs/validation/issue257-source-receipt.json": "047c384d28f4e74d69341876401175c4dc0a54f8ddc9717e1f7acb79d6585ef5", + "docs/validation/issue257_projection_contract_20260930.rst": "ed74c97c2688a362b1467aa2aae2468e1ec9fc9490fbacfa490f246315fb6341", + "docs/validation/issue264-source-extension-20260930.md": "f99a72aa06cb93c758f27dce28583d4f6d77ab0264412ab26f470873ddde27c8", + "docs/validation/issue264-source-receipt.json": "3563d2581846b0fab24d74b1a39c0fcb32957aad81d74afb50ea9719b13a0d95", + "docs/validation/issue355-evidence/openhcs-checkpoint.log": "076778a6c9e9a50306036070a6dafa29832e11b6c0d53c5960fc0e441b23b6a5", + "docs/validation/issue355-evidence/openhcs-r0-checkpoint.json.gz": "6521b6baaa80a213f7234a227b5505059f0facfdeec415883a4afb0347c38da9", + "docs/validation/issue355-evidence/openhcs-r0-checkpoint.resources.txt": "c02a718e6f40892816fa267a804b08f84a84449ed926f55ff4f4614534f359dd", + "docs/validation/issue363-binding-source-checks-20261001.tar.gz": "e20adeb454127d6f8a2bba945b44ef4a2a2a435f6fbf4e3a396c1bc511091029", + "docs/validation/issue363-evidence/reused-extension.sha256": "3ef64c7ce27b2bb31b60ac8321f9d10e68b1305bc619009dad3c4fe3d31ee1a4", + "docs/validation/issue363-source-checks-20261001.tar.gz": "9d6413d78a20c74a8129869c40a5a182d528f22f64d88c0341f59300d17292f7", + "docs/validation/issue376-source-diagnosis-20261001.tar.gz": "f9cbf53216cf89ce0ddd0c6ea80b5d44c35b4aa5d40f968c127215df37409321", + "docs/validation/kernel_readiness_352_20261001.rst": "1476315ac6f5b7e16fc19973e41b4518c976263a3d0e32c3a84a3e4e10289126", + "docs/validation/kernel_readiness_352_evidence_20261001.tgz": "186b39e0b8abdacffdd7a41c1b3e1bda1b0c37d75e98245e873113dd45b56f1b", + "docs/validation/knowledge-lazy-conversion-376.rst": "75a80e46208dedbb992fa34a0b166efbf7984b7daa108474b1f4f8a64720f024", + "docs/validation/knowledge-selected-source-tests.py": "55ef9ffd991301a415c5f025bee47de83a35a00f48d4e5a17e3f13cc266576d3", + "docs/validation/label_selection_346_20261001.rst": "053411e208ccbd4d57c55fed297bbc913d64a8047306d8810e579641be09c633", + "docs/validation/label_selection_346_evidence_20261001.tgz": "c665ef02ec2be0ccd743663e8f1c4335aff55411358671e3689fafbca427a38b", + "docs/validation/matched_csv_356_20261001.rst": "beecd672a88313ad63f45a9e8177ced650828175041c57481a3f7372c4cae7f8", + "docs/validation/matched_csv_356_evidence_20261001.tgz": "d33e0bcb4caa5d1c14f1929cb03a691e3a872f4b933d22938f9dd3329923c467", + "docs/validation/materialization_plumbing_384_20261001.rst": "3912d4a87180a81e1891591a27093120ac4a1e0128a441b7005988727d0912f6", + "docs/validation/mcp-affine-inspection-436-20261002/affinity-resident-controls.log": "d5c3af0a05e652e55a1a6fc418788fef9b47e8e58be8a8a691e4d37b386b1277", + "docs/validation/mcp-affine-inspection-436-20261002/bootstrap-source-admission-controls.log": "ba664407767d8db1503840a952e9a1039700ec8e9b7e5d29576572498ec387e0", + "docs/validation/mcp-affine-inspection-436-20261002/generated-progress-controls.log": "3eee7d7aa59e34ba1c71c4ef3534de6895be0370d8ccc58c1d871ea593bce153", + "docs/validation/mcp-affine-inspection-436-20261002/mcp-boundary-controls.log": "b8d8d1759c4401f5c4ecb73a6e3b62105c588f5cee67656be5044c83b2c56c4f", + "docs/validation/mcp-affine-inspection-436-20261002/pr-body.md": "fec6eb0d360da0b151f409833881b593435497c38f1e3c6752184b664d50132d", + "docs/validation/mcp-affine-inspection-436-20261002/probe_stdio.py": "3c34070e543db4f702670ffa04aaf503bfce33c6c7a0dab0f5c5e1a479f0b731", + "docs/validation/mcp-affine-inspection-436-20261002/r0-original-pin.log.gz": "23550ce952948178d8a4749e1208a6f960b2f422bdfb676fd86fa8b2abf3a3b0", + "docs/validation/mcp-affine-inspection-436-20261002/real-compiler-controls.log": "9a7d57cb6b836929853549b006fe9facdf534957621d46c563ff2224f083d10b", + "docs/validation/mcp-affine-inspection-436-20261002/real-compiler-qualified-controls.log": "128cf685659d2f17562b863122854e506f95d815128d8035771580d3d8adebd3", + "docs/validation/mcp-affine-inspection-436-20261002/receipt.rst": "2d51e7bc6db7cbfa5c9cd5eee196d6baad7d545dad7394c69506a2730a25d758", + "docs/validation/mcp-affine-inspection-436-20261002/resident-twelve-second-controls.log": "884d65cdf912c1f05bcfb20800be2c0da047c9bd5a9fa8de6b264c0d361c6879", + "docs/validation/mcp-affine-inspection-436-20261002/source_fixture.py": "cf2a8941c22fb66ec89e06d089b635718010134c540f9e182a7c7612042c5406", + "docs/validation/mcp-affine-inspection-436-20261002/stdio-original-controls.log": "2107c6e8ff29e0becadd40decbb1abebfdcf3cdb29af0290c4bc44529f29d870", + "docs/validation/mcp-affine-inspection-436-20261002/stdio-twelve-second-controls.log": "5941a0753f066157355dfa504c6b763c231c8c1395a68f3f06fca7d8de83d426", + "docs/validation/mcp-affine-inspection-436-20261002/stdio-whole-scope-controls.log": "ffb3455097892e18cceeefaef44563d8577f2ac85a46bc9516b4fa2352ef9120", + "docs/validation/mcp-affine-inspection-436-20261002/stdio-wire-controls.log": "80aa37d9fad7cd43435d6798b535b558d6b9fcc30b36315cad0dc172173b386d", + "docs/validation/mcp-affine-inspection-436-20261002/stdio-wire-qualified-controls.log": "341bd70033a13eaecdf050d52314a12e56ff095f9c5cec9340cef00a7a9c49c6", + "docs/validation/mcp-cold-inspection-feedback-436-20261002.rst": "60dc29871484fceb90f0a7eeb30dc9d6b458902a4c14eec6cf4385ff0469b6e9", + "docs/validation/mcp-jvm-lifetime-455-20261002.rst": "dc9ffd17d81b73bcebae79b4886cf4fc97d0077b89409454d1ad15147f12d1cc", + "docs/validation/mcp-jvm-lifetime-455-20261002/qualify.py": "f806e5105cc0ae01e4f97e3650bec5b6b530356afbe5b392c9f7a920e97bf173", + "docs/validation/mcp-progress-ack-20261002.rst": "c58f0b08879fce9de65df3f12b7926efa3bd9e5c6d364a177b306c5d0cd973fa", + "docs/validation/mcp-roi-stream-progress-441-20261002.rst": "78eb8fd599f8987b4c1522835199322fe5e2cc6578def585aec897492342f72f", + "docs/validation/mcp-terminal-input-483/INSTALLED-ACCEPTANCE.rst": "bf9d6ad92834af37c39165fac3b124b1c7222787dabf4039226574d4a742d193", + "docs/validation/mcp_local_argument_errors_20260930.rst": "ee95239a8530e56ba2d84bf83b00196b645c2860308a2d4dfdce30f301ac6d2b", + "docs/validation/memory-session-20260929.md": "1be51da878f05a777d916e56b7479be6a7fac5c39ca08ae90991352a915a9fed", + "docs/validation/memory_current_main_integration_20260930.rst": "5ba2810cc9c3df769aeeae9920c39496abf8f99495300238dbbdb93a0f1cb74b", + "docs/validation/metaclass-registry-022-publication-20261001/RECEIPT.rst": "45eee23e4ec97cefbfcad768f165e9e9ee8d58b035656e11b555db85d4eb6369", + "docs/validation/metaclass-registry-022-publication-20261001/SHA256SUMS": "2793231c3c5665011cd2ae5623d5ec54d91853fd7132b1e0f66341a3e3768572", + "docs/validation/metaclass-registry-022-publication-20261001/artifact-verification.log": "0beee0649d3efa7468b4f630d1202595d0bdc81180fe08127d492e298c00f493", + "docs/validation/metaclass-registry-022-publication-20261001/artifact-verification.resources.txt": "25afa796fc6061dd476f43347c8c74a3d8659ec1dc1f8955c5a0f08a905a0ff4", + "docs/validation/metaclass-registry-022-publication-20261001/artifacts/metaclass_registry-0.2.2-py3-none-any.whl": "67ae17c58d9d10cd58acf83ebe01833c1f1fe0a1a2e281924fab836958d592ed", + "docs/validation/metaclass-registry-022-publication-20261001/artifacts/metaclass_registry-0.2.2.tar.gz": "540bd730c52a535e08866c487b34aa4f9cb2fe857eb07a312882cdc43a15517c", + "docs/validation/metaclass-registry-022-publication-20261001/github-release.json": "742a3ac59191573de6dc09097b9d7890776e950d28542d30b75185b232c2fab3", + "docs/validation/metaclass-registry-022-publication-20261001/job-queued.json": "0422e1a4841db83e61d052634a80036358978812d6b07215d46c53553d34a177", + "docs/validation/metaclass-registry-022-publication-20261001/pending-deployments.json": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", + "docs/validation/metaclass-registry-022-publication-20261001/release-session.typescript": "0231889209c76b3d9f6fe4c11dddba6d8cc91359010d82c3d83e967d27cb94ff", + "docs/validation/metaclass-registry-022-publication-20261001/verify_publication.py": "9690d44f2e0426c5ffb19d80356df2f41c32b7ea5a24c144912e8632e5fc8960", + "docs/validation/metaclass-registry-022-publication-20261001/workflow-initial.json": "7b80e7b28ee1f3111fd7462853e15636d064a368726e6f1d3898ad465036c3f9", + "docs/validation/metaclass-registry-022-publication-20261001/workflow-terminal.json": "13a18c7696575cf7ec50bbe4c0787637880fcf76471dc46570ad983a31e7598e", + "docs/validation/metaclass-registry-022-publication-20261001/workflow-watch.log": "0470677218e39615ce28d29f1bd1747c4017ce4d6113bcf52a475a22933fd498", + "docs/validation/metadata_namespace_327.rst": "a7d4213c30ea3fd731ddc08565c9708499d7f6782ba8ee4484c688d9ddc5bb78", + "docs/validation/metadata_namespace_327_resources.rst": "aa889cde67e850e2e6452b3a2cf892338304d06df5dcdc70b42fd40afc70e3c9", + "docs/validation/metadata_namespace_327_source_evidence.tgz": "05b24739c81fbdc86c3dbc9e96f45e1764a5f50ffe946953271c386df68c8dfa", + "docs/validation/morphology-library-readiness-481-20261002.rst": "d0330dd4c8466200f166799f78fa99bb37ef4c2f0436cb1b7111a93665754a02", + "docs/validation/napari-source-channel-presentation-448-20261002/pr-body.md": "05d7d27c19836a7a57737d9ae69320cab55bebc76023244a3d1a8ef0bd35801d", + "docs/validation/napari-source-channel-presentation-448-20261002/receipt.rst": "5ab74657d73dd22abe3928c2e76de01181805b5b3710a601c9427def457c8cb0", + "docs/validation/native-presentation-451452-20261002/complete-presentation-final.log": "0e449d5f53262937e0b1b7994355c2024ca392ba343f12300f98774347613dd7", + "docs/validation/native-presentation-451452-20261002/complete-presentation-first.log": "29286b91f073c7b6fa2385bdaeb6f6e0123fb5c07a9d14a8a5421ca1e1d17a49", + "docs/validation/native-presentation-451452-20261002/complete-presentation-second.log": "5b5316ee7bf57b71e000bd83d80eefe4894e5b7731b7354fb5c35c2df12bbddb", + "docs/validation/native-presentation-451452-20261002/display-admission.log": "0071b826032c2b52f5ae8f88f6e2b18fb26887cc90d91ed3f29ab0371f90d9dd", + "docs/validation/native-presentation-451452-20261002/final-original-r0.log": "150466f41c07f5c911070b4986bf028bdde2349a226dfb6ed11ead9638d7f05d", + "docs/validation/native-presentation-451452-20261002/final-presentation-controls.log": "26626a2a8dcec2311a16b79651bd9f2164cf6778bc4c0ceb3394277db0945e9b", + "docs/validation/native-presentation-451452-20261002/mcp-controls-first.log": "6647123a7108427adfccd39227c7d8f115d5caa2036be8056556846a9f844513", + "docs/validation/native-presentation-451452-20261002/mcp-record-descent.log": "e7de3c456abd999d9bd863b42c93ca776a369a59a6a559082608cdf60284d3a3", + "docs/validation/native-presentation-451452-20261002/merged-main-ast-closure.log": "31454a498ba95dd73ad41e654e9de469cf7765ba4abde917f38e3f6ffd354804", + "docs/validation/native-presentation-451452-20261002/merged-main-native-controls.log": "b058c5cef92808c5de09573179b76d59a018e164ff9ee6473fc5d1eb095645ff", + "docs/validation/native-presentation-451452-20261002/merged-main-original-r0.log": "f9a615a12b5d67c05cd3fa47ed332436a86c9b8021e4d99d7a91cc99f5c4a3ce", + "docs/validation/native-presentation-451452-20261002/merged-main-public-mcp-controls.log": "aa0a2211d3676d0c547422e19d504611ffd647d8947db7bd23bbeb5b40d959c1", + "docs/validation/native-presentation-451452-20261002/native-and-consumer-controls.log": "f37fd50443d138e23f4c6bdf9a9df32c2add50d56d63805cb6193dc734fcb432", + "docs/validation/native-presentation-451452-20261002/native-family-integration.log": "a5fd1911a024aad9ed195e1a328252091f862c04f6fc175c1b0541ca8af03c3a", + "docs/validation/native-presentation-451452-20261002/original-presentation-controls.log": "eaf958911e7d9fa199633a2485ec2df017f416000e49372311e306f18f51832f", + "docs/validation/native-presentation-451452-20261002/original-r0.log": "003f1092ae56bae05efceff379520e6c14c0a77fc2e4dca297e38eb267630c61", + "docs/validation/native-presentation-451452-20261002/ownership-after-linear.log": "5e34488b9cba397149c88c16e05f0838baaabcfdb72ecc5842536a1cb312256d", + "docs/validation/native-presentation-451452-20261002/ownership-after.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "docs/validation/native-presentation-451452-20261002/ownership-before.log": "8a95a83d3e8d3aeef953cf69483060436246dea038fd77840d743a161e38e3fc", + "docs/validation/native-presentation-451452-20261002/pr-body.md": "48a44086b2e0e03d585412f27384f82a3bba9f096b2d5e792e83b209d505193f", + "docs/validation/native-presentation-451452-20261002/qualified-native-controls.log": "d6e2ce08fc95bc04037a5e690298dd516bcd0f092acfc68477c2482e8a9a4d10", + "docs/validation/native-presentation-451452-20261002/qualified-public-mcp-controls.log": "4b6aca8b6d87466feb33c679dd70e9dbfd616705ee9adbe2e54cc1f8a5d490b3", + "docs/validation/native-presentation-451452-20261002/receipt.rst": "00e217a1229aafb420c69553946c14c11da601c28311b733a41e3c9bdd934e81", + "docs/validation/native-presentation-451452-20261002/root394-roster.json": "28f2c08e5b9cd7f33201db1c757b187207069a993205b9cac65d8deb6961f606", + "docs/validation/neurite-primary-diagnostic-consumption-410.rst": "b62f5b9f224eecf34c856b360153a5e3d5b7650b7c0fa3be0b7b523958479b87", + "docs/validation/neurite_body_gate_424_20261002/ARCHIVE-SHA256SUM": "50a5d341534d3a7b9587b26bfa26c9d049d74c257181aeadf48c8d7e56519109", + "docs/validation/neurite_body_gate_424_20261002/EVIDENCE.tar.gz": "857540666bfdc5583f3c87ded48943e5b7c0e21b0e0247c8743f2284708c017e", + "docs/validation/neurite_body_gate_424_20261002/QUALIFICATION.rst": "e8813f23c315ca5042479bf009705226625a72c59edb75c00676ddbe6941b226", + "docs/validation/neurite_body_gate_424_20261002/RECEIVING.rst": "5c7085dff22e4174109cb9e107dc234ea8691cc0a144beef8c267ac30c709c4e", + "docs/validation/neurite_body_gate_424_20261002/SHA256SUMS": "09e4cd4781b7c923d5b1a8ef840192cb5ab9b6835b3ce6a8a11c118e52b950b2", + "docs/validation/neurite_body_gate_424_20261002/SOURCE-SHA256SUMS": "eaaa78f729074d50787d5457dc35e2e797b32445f3f10a1a49bcdc9c379ad430", + "docs/validation/official30_qualification_330_20261001.rst": "e2c8006699d88990d35d1ced74a1145121b3818d65d69abd0560ef7f77d6fa58", + "docs/validation/official30_qualification_330_evidence_20261001.tgz": "6b17e4ed0bae1cd460607f4618772868d565d9377270819f5049c2caf4c672ed", + "docs/validation/opera_producer_inventory_172.rst": "bc34a6c7cfccc142d08f8941a6b21cb86428010997a8b63874f696011593e81d", + "docs/validation/opera_producer_inventory_172_main8c512_evidence.tgz": "df6cb051e96b6eded6860958ffecda1e3debe57412342676becffb9ba0676c4a", + "docs/validation/opera_producer_inventory_172_source_evidence.tgz": "326541e4ccb3a4b5378ad4322614c4d9bdc03448bba9575535548f02285aaa7b", + "docs/validation/opera_singleton_172.rst": "8a18127a76354e80be69509c2327b2f9bd5a06707f09f785cff6c947c0563bf0", + "docs/validation/opera_singleton_172_source_evidence.tgz": "f46c0a64917011026493d4c60e9435995534fb0e9574e0e6e42aa906d21d9b9b", + "docs/validation/paired-feedback-integration-358.rst": "78d7ea521016daccf366d5509829cfac7131316d404d9f8c32b94d2e1a289ff3", + "docs/validation/paired-feedback-integration-evidence/PR358.md": "a5a235895998d822b5b8f6fe3fcf47aeaea22c7e854c73dc09f5233025263150", + "docs/validation/paired-feedback-integration-evidence/SHA256SUMS": "5318151d42951c7d1431dc8ac6fae33056ec5e4cd27c7a3ec6793eec7619ad65", + "docs/validation/paired-feedback-integration-evidence/ZMQRuntime-PR13.md": "8df33100756cfb58c864fbd5cbed8e3908ce41906c670c22e2cabc2b3d2e2a5f", + "docs/validation/paired-feedback-integration-evidence/openhcs-r0.json.gz": "0f9a85aaa787e6cd63d3cae3777dc5ad8909f337ef30eb02751ba791ab4b0e0e", + "docs/validation/paired-feedback-integration-evidence/openhcs-r0.resources.txt": "52e4c5d1f650a129a1fca859793b7e97810e1439eab32ca58013a748fdafda5f", + "docs/validation/paired-feedback-integration-evidence/openhcs-r0.stderr.txt": "7986c663bbeb1f7f77b571bafc6773e8872d8666c1d2c23bd92dd33316a7ba82", + "docs/validation/paired-feedback-integration-evidence/zmqruntime-r0.json.gz": "9a2ddba42722c036ed795cc8d143007e74784085818c8fa2b6544b4cf1dad0b1", + "docs/validation/paired-feedback-integration-evidence/zmqruntime-r0.resources.txt": "b224497a3d60be1870e4dd8f14c4ce9690f22a7ab13d999d96b40fcd4ef12a13", + "docs/validation/paired-feedback-integration-evidence/zmqruntime-r0.stderr.txt": "b9156d81a5171a0ed86908a6e9d1fca8d5da5690a831aecf279e5f3b4252dcfe", + "docs/validation/paired_field_csv_314_20260930/PREFLIGHT.json": "09bc36d6a804f1601315bb842a3c67852ed5d08f5a71b6fc632dd6f7c7a1e12a", + "docs/validation/paired_field_csv_314_20260930/SHA256SUMS": "a9ab37c955e6f625f76272f318fac729b34c83fdc6af68ef6a434259e4ffaa82", + "docs/validation/paired_field_csv_314_20260930/check-installed-parent.py": "73461928418fa47201eae32d855d563a52ee5301a800cdba6ed41e697ef33b16", + "docs/validation/paired_field_csv_314_20260930/checkpoint.rst": "e935c5bcb0d0bf4da40c68f855bff5afa484c6b1a6e667405004cb06164f0cd7", + "docs/validation/paired_field_csv_314_20260930/core-census.json.gz": "f7a149a70dd9f82cadd4b1b43eae69a6c70354d04c870113bae2e80d6c697fcf", + "docs/validation/paired_field_csv_314_20260930/core-census.log": "abcc421cce2ad109a4be6f9bae9c7ffdb80c48725a416bf4db5f60266fe610d5", + "docs/validation/paired_field_csv_314_20260930/core-overlay.json.gz": "22a3cbbcc9ac4d15b10c6ae38d5b930fb2a37a687f832f978d29543d803f399b", + "docs/validation/paired_field_csv_314_20260930/core-overlay.log": "f842402ed5c9f2eb35f5a9abeb97ee7342e1e0269233d2149aab2a3b03ba4318", + "docs/validation/paired_field_csv_314_20260930/create-live-fixture.py": "f9a61c9e5235a38f21f05ae4d43a44a543adf1208d1f3e4c412d8ef1dc6c0475", + "docs/validation/paired_field_csv_314_20260930/effective-imports-error.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "docs/validation/paired_field_csv_314_20260930/effective-imports.json": "6260f67c73bdbb1e138b9733e3100f9c3b82daa032beaa1ab821891a830b8d68", + "docs/validation/paired_field_csv_314_20260930/final-policy-exporter.log": "2d22257b758889c28b6cc68e230075176e81e94b8b3eb5b93f0c8c0458040102", + "docs/validation/paired_field_csv_314_20260930/installed-parent-checkpoint.rst": "89baf9a17dce5bf5d91e0fc48791e7936b88034e59de9c0018cda40223f8727b", + "docs/validation/paired_field_csv_314_20260930/merged-main-census.json": "4e6b60bb28c884fddf203e176e39b6b2a8fec79b0db065e2d3fb87bfc11967a1", + "docs/validation/paired_field_csv_314_20260930/merged-main-census.log": "2a91055d7bc8f70cd70a234c0970babbf0bedf15cfce723dfecd4e028032cbb1", + "docs/validation/paired_field_csv_314_20260930/merged-main-owner.log": "bf1aee9710d476cd67ea9ef6e6a03805d50498d1dd38eb1f3f7e325e7fd2a1bd", + "docs/validation/paired_field_csv_314_20260930/merged-main-related.log": "ef34e28b0d9ebe85c9a364f9d64bcba276f1af3de33c782a5452c74d8dd665b5", + "docs/validation/paired_field_csv_314_20260930/nra-original-resources.log": "45bb910ec84bccf47c98eab8d48668058c32f888bf3337eadf6af54ebdcc69a0", + "docs/validation/paired_field_csv_314_20260930/nra-original.json": "76efdf6231fe3d520aa5aad4ed07aa619696ed0a1bbc0d9ac58207439fbc1d1c", + "docs/validation/paired_field_csv_314_20260930/nra-owner-loop-resources.log": "ca8e2518377ceb506f4b6616de041d8e5901d3abb7d53499aacacd284927f365", + "docs/validation/paired_field_csv_314_20260930/nra-owner-loop.json": "85fa34cc7c9365e1433cf13cfde3728b93e826212a862d5a896ee50f24215587", + "docs/validation/paired_field_csv_314_20260930/owned-disposable.tar.gz": "0ef110ed10150278f09ba7ea4abf837c87f163d834794a99f26731ec3b722a69", + "docs/validation/paired_field_csv_314_20260930/owner-census-delta.json": "4068fc6aebd829bde62c38c9fd8a9c980ff2480214f9e68d638684cb643d6bef", + "docs/validation/paired_field_csv_314_20260930/owner-census-delta.log": "d221f41dafd990a5434428d86703b59b1eec66fb99c6537fb40a0c52bdd1205f", + "docs/validation/paired_field_csv_314_20260930/owner-fixed-corrected.log": "8db82b08046f81c30100fe92751276352cef7f8a444a3297ec4df82ce2d95c0b", + "docs/validation/paired_field_csv_314_20260930/owner-fixed.log": "d50a0ddd323c80c4992f46e4a96c8a3a1515c6787ee2ad5f86191357ac964989", + "docs/validation/paired_field_csv_314_20260930/ownership-receipt.rst": "d8375d089d79fb532af311d7d3b8f1509d0f21e9e045b7cd14a9ed04a132f509", + "docs/validation/paired_field_csv_314_20260930/pipeline-live-parent.py": "37ed9534c0acddf3e8a71c62314997435a0dc8cb19f9201528fae1b7f84e5ef1", + "docs/validation/paired_field_csv_314_20260930/policy-import.log": "17988389f4355b8274b59fd1141bb8aa5757b6cb6adf94fe879c0b199ca023e3", + "docs/validation/paired_field_csv_314_20260930/pr-body.md": "6b76484efe7c0bcd2634c61337c0f73018d457338bfb077c05c836ec42e3b481", + "docs/validation/paired_field_csv_314_20260930/related-owner-contracts.log": "77b93b241966cb96c7e7e8e2adc7964d488abbaa14fb2ab94e74ddb1598c3673", + "docs/validation/paired_field_csv_314_20260930/reproducer-original-built.log": "63ad8bd8fac5a6a06141b607f4d6bd51145a8952ee63bbc9c27c844c07724dcb", + "docs/validation/paired_field_csv_314_20260930/reproducer-original.log": "9aa5f6c2c1dd4b980dc653be3b1fca951a5eb42ba05e2a99709140190f08ffd0", + "docs/validation/paired_field_csv_314_20260930/ruff-focused.log": "93abbe5307fdb809eae183b7e3fd79f276fb1044b1e889cf95ae3ec754dd36aa", + "docs/validation/paired_field_csv_314_20260930/run-installed-parent.sh": "206e183ccb312731fea5e7c66b9a536c633c0e0de659beb49932a30bd6244752", + "docs/validation/paired_field_csv_314_20260930/source-anchors-base-empty-snapshot.log": "9a5daa28530cfdb8a9a5dfaf7e4469862c7755067be421ee89f83f824ac2a6d7", + "docs/validation/paired_field_csv_314_20260930/source-anchors-base-verified.log": "05d26c97cc6db8b17026c5dfb984b13f5cc667479d0962613118c6d764edc22e", + "docs/validation/paired_field_csv_314_20260930/source-anchors-base.log": "89e8f8287e324c431d734ec1f3dbb247459ed329ec939cc9478143cf239ccba3", + "docs/validation/paired_field_csv_314_20260930/source-build.log": "5e739265347dec1ea5319579c3575f0ca9d4ba12a0a6c4209cbab692196c0b50", + "docs/validation/persisted-result-reopen-398-source-20261001.tar.gz": "7c1f69c9663445e9a489a86d593eaefe5dc1b71bceb1cac183058b2f3d6502ee", + "docs/validation/persisted-result-reopen-398.rst": "332c29d0070e41bb404d4c0205110a7653bf090dd43fb5598442df8b97b177a3", + "docs/validation/persisted_result_publication_134_20261002.rst": "5521ecb67990b8598faa704bf76430dc23f4b460895cf23d83868c80d355b213", + "docs/validation/physical-source-inspection-445-original-20261002.tar.gz": "f60cedd5c063c2ea06c23bc3902125bf013f459648f2700acdb5dd8eefe99fba", + "docs/validation/physical-source-inspection-445-source-20261002.tar.gz": "ffc8001ed2d76c43626f6154f84b06fa54a4b1865f209ab4a00212d9b6ccef8a", + "docs/validation/physical-source-inspection-445.rst": "1a82d5fe328a0a199e097d79125eb441934059b24942c5cda1ad9f8a63d79528", + "docs/validation/polynomial-library-readiness-473-20261002.rst": "784accee8adad50b3ba146596add42406b2119275965d50f0ec9ebca54f10654", + "docs/validation/preparation-admission-355.rst": "e3ca89ba99ba4d48388b000b7cc011eaf4da630b85ea704379e1125c8bd9478a", + "docs/validation/preparation-admission-evidence/SHA256SUMS": "b5c6cb1b83ce8ff6d35ea9ce172f078f6165a4493d8e1d5dde593c66ee3c41de", + "docs/validation/preparation-admission-evidence/after-checkpoint.log": "b0447f8cfd6a8ac550889aaa4d0e0ad5651fb7067740e6003d6bdae6d1af3167", + "docs/validation/preparation-admission-evidence/after-initial.log": "05a2c1cff034b92c78db1774241e0dc96a95a0f92f8928b1e7f2426fda9b4221", + "docs/validation/preparation-admission-evidence/after-paired-source.log": "15cb97e5524a2c776df6d3dd806a1b5bbfe456635c089fc749ad219de3f533ff", + "docs/validation/preparation-admission-evidence/before-with-native-dependency.log": "98aea15afd57065d6b2a3616521c2516c71a0f3bad3560837ebdbf793b1dcf15", + "docs/validation/preparation-admission-evidence/before.log": "e04fef9816be769f039bc3403c705cd221a00d2107a784edf0cd93e5938e5ae0", + "docs/validation/preparation-admission-evidence/manifest.json": "5f5212b235270e3131957c894366d608866140e8cee15d189e85030b06d46069", + "docs/validation/preparation-admission-evidence/r0.json.gz": "d8c1f6c1fe25438b3fdde770a40702ac429594419e1782b374cabf278c088693", + "docs/validation/preparation-admission-evidence/r0.resources.txt": "85ed6d829fdf9f34f335d8809e922f755473de3fe92c77d31a17a35c423197ac", + "docs/validation/preparation-admission-evidence/source-owner-profile.log": "c5792a79cba3868812d96fa7027ca0fbfcfe50f1f99b5f3e0291a553a8516517", + "docs/validation/prepared_callable_signatures_419_20261002.rst": "bfe3cf254f75a2927919f38718927ed7956395909ad48d48e37fdcf0774642a5", + "docs/validation/primary_diagnostics_current_main_20260930.rst": "e44fb70f5df372fa11b495952942a7fac30a777c29105628079fbf17fc0962f3", + "docs/validation/pyqt_zmq03_dependency_20260930/README.md": "95b2e6dc991b8df24e621c702bb5b494d88b5777a84dbb63014f81d476593642", + "docs/validation/pyqt_zmq03_dependency_20260930/normal-resolving-install.log": "3ca114de7bc8509814f0476b3237799158e505ad56b3bde03e1d65dff9a63d0c", + "docs/validation/pyqt_zmq03_dependency_20260930/openhcs-consumers.log": "cd716188cc66c2cffbdcf62f21534893a8aa4e071587a1760dc59a31df7646cb", + "docs/validation/pyqt_zmq03_dependency_20260930/pyqt-consumers.log": "f80274d2745eee4aabafa0a8a73f8f3d8826bdf9dfa95b5a2d813fbf845eaed3", + "docs/validation/pyqt_zmq03_dependency_20260930/resolver-counterexample.log": "891c272ba48618d0d646af3aa8572fefbec747ee4efa0745bbf7effa83ab7f0e", + "docs/validation/r0-policy-fixture-invocation-20261002/CLI-SOURCE.py": "b1446d088d0da9f004c20082cfbc771c1384f28b131e69b26bf0470b63c31961", + "docs/validation/r0-policy-fixture-invocation-20261002/PLAN.rst": "51e2f94277e9b8d4af6977dd15aff57041425aed2de29132c37eb0dcb7edefc7", + "docs/validation/r0-policy-fixture-invocation-20261002/RECEIPT.rst": "2060d0d8796bc3650a2826cf93cda2b577cfed32c04c7807a57acbaa38e02bd7", + "docs/validation/run_metadata_namespace_327.py": "c2c42e718c51ac5d388b3091808fea5e8e385e5908c3b3a1c499b45e7799dcea", + "docs/validation/runtime_bootstrap_20260930.md": "37c444da091e57ea5ad47634d6f94ac8ab7518cb61b8c4053592b429f45a5d44", + "docs/validation/runtime_bootstrap_20260930/close-dependency-census.json": "5dc06e14485b6fb48713496568760398b6d41508299faf88ca369c38b524eadc", + "docs/validation/runtime_bootstrap_20260930/close-dependency-census.txt": "ba0a3ca99e97fe18b76b61294729c9aea4f6f9b48fcc360876cf5b06c18df3b9", + "docs/validation/runtime_bootstrap_20260930/close-source-census.json": "154026a741b6c991b9b0a9c2dd4c10ae59214031b725a9189406ccbdb03816ce", + "docs/validation/runtime_bootstrap_20260930/close-source-census.txt": "34d8fb01701dd0800223c5f0cba5af77aa55fc4ba7f2c0d916ca4fbba304bbd9", + "docs/validation/runtime_bootstrap_20260930/dependency-census.json": "f3b2643a5cb52a26c0e313a9dd98ea111df3d92a4b94e212d5f154ab3a1b44a8", + "docs/validation/runtime_bootstrap_20260930/dependency-census.txt": "16e8e97d1ae50311ef08160df72ab5abec680b191ba05b13ca5aa0f74bfc1e16", + "docs/validation/runtime_bootstrap_20260930/effective-transport/checkpoint.md": "7ff7d046f27b8c86829b3cca146cd147fa6cfdcc92939ce5ac3132c433388ddf", + "docs/validation/runtime_bootstrap_20260930/effective-transport/parent-witness.txt": "d6f1cca0afcfd86a7262cdf778ff55d6573e1995d968671830630f90411687d4", + "docs/validation/runtime_bootstrap_20260930/effective-transport/source-tests-b.xml": "430f95d1d0aa90a5b2295e834209cb1ec712106fb056ecae9e36952600fad763", + "docs/validation/runtime_bootstrap_20260930/effective-transport/source-tests.xml": "868283cb8f29b798d09ab87435df24237fbb05f1727a98ee01953fc49fd9c1a9", + "docs/validation/runtime_bootstrap_20260930/launch-plan-factoring/checkpoint.md": "57ce52fa593f590e5fe9b300047d2f92b2b06d09778203e797510830d8703fbe", + "docs/validation/runtime_bootstrap_20260930/launch-plan-factoring/source-tests-b.xml": "3aef4f99246993a53a89b5039005bbeb634c6de20d694376c3e164103f089efd", + "docs/validation/runtime_bootstrap_20260930/launch-plan-factoring/source-tests.xml": "6877aef3cd2456489c582f271ebd32e7ed709ebec273b1ce9a7bd13b59fce5e7", + "docs/validation/runtime_bootstrap_20260930/native-attempt01/build.txt": "52ca8c419407854176ee38bed15f7203eb3efcb31e3a5c8ef83cb150a53f7396", + "docs/validation/runtime_bootstrap_20260930/native-attempt01/checkpoint.md": "be3da4dbf04c98c71653792b3848801b85a1c210a04bcc0c64affb63ac4f4abb", + "docs/validation/runtime_bootstrap_20260930/native-attempt01/journey.json": "dd3c9669f14a11fa2662ce9af90d8329dad9fce3ba6fc8e4ed847b26342eae7a", + "docs/validation/runtime_bootstrap_20260930/native-attempt01/mcp-stderr.txt": "fc2c6374f06e95a9eb5610df83cb510fd1eb89c0e174891b897355ace2f3bb5c", + "docs/validation/runtime_bootstrap_20260930/native-attempt01/pipeline.py": "6aefbfb68105340f7eb0acce68ea712022ec3e27ba3a58ca0d940d8911a30c1d", + "docs/validation/runtime_bootstrap_20260930/native-attempt02/build.txt": "e5dfa59d188c5f682207e4bd7a6d3ef0ef24b75e8054f888e452f50e0e2609e6", + "docs/validation/runtime_bootstrap_20260930/native-attempt02/checkpoint.md": "ccb81b00a94042d9e812295d3fd4f2ddd36d1ccd9f155d21ec2ab684dd65a891", + "docs/validation/runtime_bootstrap_20260930/native-attempt02/journey.json": "2163e36b97a64be121ef167d8a58be52c992328b948dc6331c79671ed45454e7", + "docs/validation/runtime_bootstrap_20260930/native-attempt02/mcp-stderr.txt": "57ea5239335f3732bda6ba2b19002c02bfbc13f7bce1ec8f27ba1d0eb2b68261", + "docs/validation/runtime_bootstrap_20260930/native-attempt02/owned/data/openhcs/logs/openhcs_zmq_server_port_5964_1790754393037041240.log": "314bb73133434779bd2b5bc8504dd20cc8152cc8fb67109890d96a03bde4f947", + "docs/validation/runtime_bootstrap_20260930/native-attempt02/owned/data/openhcs/logs/openhcs_zmq_server_port_5964_1790754393037041240.startup.jsonl": "52e82e6734e3ce65c1720870a0b0bd33cad8fb85a8a3757d972d061f8ed4ca15", + "docs/validation/runtime_bootstrap_20260930/native-attempt02/pipeline.py": "78614093b82ba33ff428888760b1e7aa7cb1f1fc8d0ffa27b13e81c5fcd1dede", + "docs/validation/runtime_bootstrap_20260930/native-attempt03/build.txt": "185868583dd50801adb4d64ad1e26e8f5f9273f22a8ac918b2de1971be3a1698", + "docs/validation/runtime_bootstrap_20260930/native-attempt03/checkpoint.md": "141a0572959fdb2197525be2a8cf7e8a4356987277ec8da8fabaf961f28507d6", + "docs/validation/runtime_bootstrap_20260930/native-attempt03/inspect_volume_fixture_v2.py": "0e21b6aefd591e7d9842cf5695e99060fc2c85294c6619f07122884e4581d48c", + "docs/validation/runtime_bootstrap_20260930/native-attempt03/journey.json": "e4f16a83f49b00672f8125be8f5b36ad5a6b7bd2ed2c7620b1da16e13d6d8c19", + "docs/validation/runtime_bootstrap_20260930/native-attempt03/mcp-stderr.txt": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "docs/validation/runtime_bootstrap_20260930/native-attempt03/owned/data/openhcs/logs/openhcs_zmq_server_port_5965_1790758999303299469.log": "719d7554ebbc8a91fa0413062f89cbb1013540918df6d51e486073e2a687ca98", + "docs/validation/runtime_bootstrap_20260930/native-attempt03/owned/data/openhcs/logs/openhcs_zmq_server_port_5965_1790758999303299469.startup.jsonl": "eba57b5d00e0878cd0b7349a1b27b77a231be4bbb1f5022cedf3aac6f932224d", + "docs/validation/runtime_bootstrap_20260930/native-attempt03/select_volume_fixture_planes_v2.py": "cc21d744b6f454db57503928faaabce060855d06be5ad3c28271c5af53d0f587", + "docs/validation/runtime_bootstrap_20260930/native-owner-tests-b.xml": "cb918af10ab3200b83d1e931a9b3b7b2f7239b166ee5b05ab65b526595883c27", + "docs/validation/runtime_bootstrap_20260930/native-owner-tests.xml": "0184ab8acebfde82f73aa4b2ae938ad400f48959a4b1c2a2650df66ea358658d", + "docs/validation/runtime_bootstrap_20260930/owner-factoring-checkpoint.md": "6b9ba079c1cd638320f5b58ed5a116b976e98223478ed25d269dbbcf9161bc75", + "docs/validation/runtime_bootstrap_20260930/owner-factoring-ratchet-resources.txt": "31a1c5eec77e5c5f5e9d91a01150bad91162c426161fc6edef0c873fdcb2b6ef", + "docs/validation/runtime_bootstrap_20260930/owner-factoring-ratchet.json": "b5a591a281789fe0889b179acad65ab5051d80513832001e95a409e417bc66c0", + "docs/validation/runtime_bootstrap_20260930/owner-factoring-source-tests-b.xml": "50cbb06ba8ec067bee3b61bce3bfd81171729f4208e8a985484264ab85b9307b", + "docs/validation/runtime_bootstrap_20260930/owner-factoring-source-tests.xml": "ae9d53d94575d9c0f71703dd3534a5c62f4dcb7b706f8f26277e25bab5d4baad", + "docs/validation/runtime_bootstrap_20260930/paired-native-owner-checkpoint.md": "114f620851c324ccef185a7f041cd10a1827f214ccf8ff6c613f15f92fd0f7ee", + "docs/validation/runtime_bootstrap_20260930/paired-owner-source-tests.xml": "14adb1de144dcec9c3d394782dac9838b72c023eae297763c0a203b65bda70ca", + "docs/validation/runtime_bootstrap_20260930/paired-pin-handoff.md": "6510912ce02371581864fcde2db78dd74fc65be05eb5daa1154187f68920ac17", + "docs/validation/runtime_bootstrap_20260930/paired-pin-source-resources.txt": "299f8bc3dce04a53d87e16eb3f83d8f007853c5604e6153cbc7c20be0c045010", + "docs/validation/runtime_bootstrap_20260930/paired-pin-source-tests.xml": "cb46ec3208aa694fa899e6e41eeb03d79407b20380d6fa1fd74f988f5a94c589", + "docs/validation/runtime_bootstrap_20260930/parent-main-integration/checkpoint.rst": "c86f6b1d1b7d99aebe38eb693b7ce65cf01be6af0cd84a6b23c1a0147652c092", + "docs/validation/runtime_bootstrap_20260930/parent-main-integration/native-build.log": "6aa818d0832f3c541b82e3e1888549722aefc03bbd2097b223048431eb88dd42", + "docs/validation/runtime_bootstrap_20260930/parent-main-integration/ratchet-resources.txt": "96bdf1aad53c02fe8cd4bb92057bc8d68b6092d01fba861e60fe05deb3529cdd", + "docs/validation/runtime_bootstrap_20260930/parent-main-integration/ratchet.json": "692307e5b347474d02720a4c39fa99cbf53db5272b29d860d75049a100b217c7", + "docs/validation/runtime_bootstrap_20260930/parent-main-integration/run-source-check.sh": "6f1a224197241353eb44986402329e917c934f227bc045a93144781eba7bdf4f", + "docs/validation/runtime_bootstrap_20260930/parent-main-integration/source-tests-native-built.log": "e61ffc0ec57e83bffc8d73d55be5d62372d801a670b90fc16bf85b3ea175c923", + "docs/validation/runtime_bootstrap_20260930/parent-main-integration/source-tests-native-built.xml": "d45a2d2d5f66587283443eb7728bfb826c0379ee5de510c4a9a74d46df4b032d", + "docs/validation/runtime_bootstrap_20260930/parent-main-integration/source-tests.log": "ebf16a13e6b3dda87f6d5b6ae18969d55ca42bd5a4fa099325c5dedfba949d7d", + "docs/validation/runtime_bootstrap_20260930/parent-main-integration/source-tests.xml": "68b100a58a23ee6b236198aa90e7c33f68377c64d6fcbed7acd02aa796186e70", + "docs/validation/runtime_bootstrap_20260930/pr-body.md": "50e8cf8f979abece5eafad96d10c4f6fb5bc43c960ca5efff53221c324d5636c", + "docs/validation/runtime_bootstrap_20260930/pre-spawn-rollback/checkpoint.md": "624737bcaa927195479e51282e213ab36d4d4228d55e03b33adf84479edefbf5", + "docs/validation/runtime_bootstrap_20260930/pre-spawn-rollback/combined-resources.txt": "7f377facc86459f827d99b5aa4de86f46036fa6a607a0908cd50f85a7a56ef3a", + "docs/validation/runtime_bootstrap_20260930/pre-spawn-rollback/combined-tests.txt": "f075cc1d80aab228abd444761e911270d943b75f5876243f24b4370ea06bec91", + "docs/validation/runtime_bootstrap_20260930/pre-spawn-rollback/dependency-initial-resources.txt": "790b820e15f234f30f3f8fc2cb7ab0184171fbf74e1eb07e20d3bf56b256185a", + "docs/validation/runtime_bootstrap_20260930/pre-spawn-rollback/dependency-initial-tests.txt": "4c92b2d38f648ca4a2a51d87367079cc54be864b42c2eaba2d65bbd6e64963ac", + "docs/validation/runtime_bootstrap_20260930/pre-spawn-rollback/dependency-resources.txt": "d1d7ac25844cdacae8b05e9daaa40e8086cc14c42252f2b9d597eb3dcdd9e6eb", + "docs/validation/runtime_bootstrap_20260930/pre-spawn-rollback/dependency-tests.txt": "ce9378eca44c9c372d4e0a13193d90c4f04c497f702bc14919cc349bb78830ac", + "docs/validation/runtime_bootstrap_20260930/pre-spawn-rollback/zmq-pr-body.md": "d38e1685fe968d08a96b9be3e5c8656f47a8cd6a7d0653ba27855e153435f83d", + "docs/validation/runtime_bootstrap_20260930/projection-attempt03-preparation.md": "f12dac93f4174918442ef094d164caf4922f011989a63d1a48bb42834cc097ad", + "docs/validation/runtime_bootstrap_20260930/r0-ratchet-before-resources.txt": "25c56cd46cbc662501855f4689e4201721c22b50ef2f12e894ebbd6aa37a38e2", + "docs/validation/runtime_bootstrap_20260930/r0-ratchet-before.json": "b60fc0d27d705739bf7ddd5cfdcc45cd2a4f1dbbc092f00c0d609bd4e8730d98", + "docs/validation/runtime_bootstrap_20260930/reserved-child-takeover/after-resources.txt": "7175fcc16572855e2705a17fbfa60bcd5a430f8a5a96a2e05b24a43a37b5fb9b", + "docs/validation/runtime_bootstrap_20260930/reserved-child-takeover/after-tests.log": "18bb74050853e8a6738428de25d77e6812c9af6c1d506f656661aafbdf9925b5", + "docs/validation/runtime_bootstrap_20260930/reserved-child-takeover/after-tests.xml": "15eecd13b138a4dd4707ea61236a5d16578cb316d9f5fd96339458b32f2c33cb", + "docs/validation/runtime_bootstrap_20260930/reserved-child-takeover/before-corrected-resources.txt": "3f4ccca29d5d8d6f75ffb1aa39a1780224d3293ec669e38a817d39faed72b5cb", + "docs/validation/runtime_bootstrap_20260930/reserved-child-takeover/before-corrected-tests.log": "c2d423238fb96e6987461d1b9803dc4cc7fc9a2fb071b4f281dc177c154ef06e", + "docs/validation/runtime_bootstrap_20260930/reserved-child-takeover/before-corrected-tests.xml": "6c14d238f5cd486fbed5cc4a5a80f43df00666d2c95bcad805f381f9b8db89fb", + "docs/validation/runtime_bootstrap_20260930/reserved-child-takeover/before-resources.txt": "f577d6af6038b8888691831f55d5568fea81b4882b6887fb58c35d2cf03fc49e", + "docs/validation/runtime_bootstrap_20260930/reserved-child-takeover/before-tests.log": "619105d4912a46f6b684a74bd1ef9de61e8edf1fdc302f3f7ba462ab8062ef1e", + "docs/validation/runtime_bootstrap_20260930/reserved-child-takeover/before-tests.xml": "b00c6b13279c43c052154ee60fb7fd9ffcc9c6d94b9c5fc2cc9e539d6ac9c0cd", + "docs/validation/runtime_bootstrap_20260930/reserved-child-takeover/checkpoint.rst": "172530cedc3dae6bcdc1c52937108d45afba0a77b1edd41e3afb8ed0504c27e2", + "docs/validation/runtime_bootstrap_20260930/reserved-child-takeover/pr256-body.md": "109e3480dab367be700bc8111c013ef182bf33df16b7d864f9b4a031ffe362a5", + "docs/validation/runtime_bootstrap_20260930/reserved-child-takeover/pr9-body.md": "7c3c28e0e0959b32a7b277dbc47b1c294a8e5596cad252508b804fb3dfa8c1d5", + "docs/validation/runtime_bootstrap_20260930/source-census.json": "1f0468a4fe5559c1113c70202e2576034f050c4680ae76deef1cb9921be2fbc0", + "docs/validation/runtime_bootstrap_20260930/source-census.txt": "fe699d7ba5cf427a3e84b41d30783a9209cf1a2a723be44142e124ddfbf124f4", + "docs/validation/runtime_bootstrap_20260930/source-close-122-resources.txt": "4733a731eae30b391ddca5f227665e40263a16d46c41326593ab86cac3d269cd", + "docs/validation/runtime_bootstrap_20260930/source-close-122.txt": "47994d7219e7ea55288b595167756f6f63c0c74beddae4dbac6a2b102a1af386", + "docs/validation/runtime_bootstrap_20260930/source-close-final-resources.txt": "26c418ec0d2b1115327f2a8cecd0d1d674b22ed6f4c56b23add85edde59793fb", + "docs/validation/runtime_bootstrap_20260930/source-close-final.txt": "ce37a347f55eef5a0b1cf18a4b7286377fb3e1ab287ba952f944a6d834505c3a", + "docs/validation/runtime_bootstrap_20260930/source-close-fixtures-resources.txt": "be092b114b6d9cd0910544ffbbc65b3471575c778244dca136ce6ddaadc09210", + "docs/validation/runtime_bootstrap_20260930/source-close-fixtures.txt": "c2ff8cd5ec093862bfab65fe0073f9bdfb78241d941a2d2b6861ed90054e21c9", + "docs/validation/runtime_bootstrap_20260930/source-close-initial-resources.txt": "f5db7a4ae92d972ca004602325b02f206be9bc0c44ed617dc3b8a213d19b66a6", + "docs/validation/runtime_bootstrap_20260930/source-close-initial.txt": "20a9f75d4a4913e0cf0f7bf26602c5812217f84171c98d6eb5e9823f69227cac", + "docs/validation/runtime_bootstrap_20260930/source-close-resources.txt": "46cb30319bbde3d3bad80615d458fdee4a77f56831de7ace533ffde1030ebaa3", + "docs/validation/runtime_bootstrap_20260930/source-close-wire-fixtures-resources.txt": "ef4b151e50d2f24727516e2da39bf793654a5294b047675fe36e6ac39ac87e6a", + "docs/validation/runtime_bootstrap_20260930/source-close-wire-fixtures.txt": "b419217ee1cb91b44efc30ae4b2b258d4aae8f9106508593e007b507c7423d16", + "docs/validation/runtime_bootstrap_20260930/source-close.txt": "9667fecc377d148f04a685757ee1c781bddd9ab82dede509fadaa59c34218f93", + "docs/validation/runtime_bootstrap_20260930/source-hardening-resources.txt": "240bc56208ae6889d44b7e5075b0f75797304ed1ecd7651af612503fd42aa168", + "docs/validation/runtime_bootstrap_20260930/source-hardening.txt": "967e61b78ad73f5a2af58e0d8b25dd60e7dc8ffaacfecde9ba866ca4b33d858d", + "docs/validation/runtime_bootstrap_20260930/source-test-resources.txt": "d8b4fff3da0cf5b57b6704dbccd7b1c35cb991348600cdd2646b1883d7b3b620", + "docs/validation/runtime_bootstrap_20260930/source-tests.txt": "21f89c8385ea85e365ff88d5e5f313cf5a62b63679de02b2e526665ccc4462fa", + "docs/validation/runtime_bootstrap_20260930/viewer-main-source-resources.txt": "db07fba19890d876a23b740baffd9a0376cb7a650d8aff9dcc82c8cb84fc66f4", + "docs/validation/runtime_bootstrap_20260930/viewer-main-source-tests.xml": "3c010622f5100a20488313a25da972e6e5557f04f83ee9fe4cb154c39d253d9c", + "docs/validation/runtime_bootstrap_20260930/viewer-merged-source-resources.txt": "8848453ea6e2beebefacbe20ae0a48e1615f4301416e5d2664391b1eef89eb6e", + "docs/validation/runtime_bootstrap_20260930/viewer-merged-source-tests.xml": "76b246d64c73df83d36ca3aa3d05bd8f7e06e55e0aae8071726c2e65f631f156", + "docs/validation/runtime_fixture_sync_387_20261001/RECEIPT.rst": "d758b11726ec75dd63edcea635bd839b850fff1e7f443f5abe8052b2e87042c0", + "docs/validation/runtime_fixture_sync_387_20261001/SHA256SUMS": "a4ce5fa9305f612769a7b41f8370d5fc23bdc623655d2b6ec5ea341b50fa1e2c", + "docs/validation/runtime_fixture_sync_387_20261001/pytest_evidence.tgz": "17a7e26badcb55df9c225ad7b38eb45c5199c5ed53b85d3f8afe315f02f7af8c", + "docs/validation/runtime_owner_sync_323_20261001.rst": "ba42abd16e29e28996fa829e9f6923f9aaa2a21273040e499d01760e6e605616", + "docs/validation/runtime_owner_sync_323_evidence_20261001.tgz": "fcaf4f7da4cdb143f6625fea47975faa03c394786da5c3386865325c2003a30f", + "docs/validation/s1_typed_viewer_20261001/NATIVE-RECORDS-ARCHIVE-MEMBERS.txt": "ea245e97091f0757abfeb2e8d5955f01cc66dcdb9a56d0ccd2b58185fd4d4c1e", + "docs/validation/s1_typed_viewer_20261001/NATIVE-RECORDS-ARCHIVE-SHA256SUM": "cc2fb6d2f09b73b7bdcd128c9eb5a72646a6f498f42b067ac0d7b3392bf59436", + "docs/validation/s1_typed_viewer_20261001/NATIVE-RECORDS-INCREMENT.rst": "fccd8914f71fae74cec1c6d2360b6800ceaaca6a48c2a147645b5d995fc2a703", + "docs/validation/s1_typed_viewer_20261001/NATIVE-RECORDS-RECEIVING.rst": "7f722393de06400dd9e0d52cd07e23dcbd72bc5d56dc705fbab18ebe044518e7", + "docs/validation/s1_typed_viewer_20261001/NATIVE-RECORDS-SHA256SUMS": "01e336f8625b7ecedf967b065d3f551484475432318a82cca673f23888d1cabb", + "docs/validation/s1_typed_viewer_20261001/NATIVE-RECORDS-SOURCE-SHA256SUMS": "f3391e85a14633df28102c843747e1727fca4d2c3e6ec848b964f7bc4acd47f6", + "docs/validation/s1_typed_viewer_20261001/PUBLISHED650-QUALIFICATION.rst": "8f563926cdf42d2224542ed0709d573a31636aa5a0c536ef411c2e976e48eef8", + "docs/validation/s1_typed_viewer_20261001/RECEIVING.rst": "94cf5a5be5ce9ec7932b12e726c78336604545c175af66049edc5df58f42bf7e", + "docs/validation/s1_typed_viewer_20261001/STATE-INCREMENT.rst": "76df74174b5dacce4a584b7f5f844d3ab1f628cb48cfebc992e42faf2f87ba65", + "docs/validation/s1_typed_viewer_20261001/checkpoint-framed-resources.txt": "c139e3878bd18454f8221f1a5ff95b5dcb868fce23356448f2121c2cc0f1e538", + "docs/validation/s1_typed_viewer_20261001/checkpoint-framed.log": "afb38b6a1a08d99126ac6e7a9cb15189d97f8c0c28491d969a0df9e982c511c6", + "docs/validation/s1_typed_viewer_20261001/checkpoint-framed.xml": "86a4685a8bb7186f5e493b8c492b8fcb3cc01d14cec82196604325f2e3b010d8", + "docs/validation/s1_typed_viewer_20261001/checkpoint-resources.txt": "d237bf264cccfa5780605e330cb6db12ef65ed1b9eda43cd7f0a58fb6741fd87", + "docs/validation/s1_typed_viewer_20261001/checkpoint.log": "10f15e1ac5ffc96fb9291e9149f40632664cdd84c2e40601a539cfc748f64602", + "docs/validation/s1_typed_viewer_20261001/checkpoint.xml": "3c6cc3ecc626162fd3eeba3186d91e8fc5d38819ce520a4b48f16d6212b4948b", + "docs/validation/s1_typed_viewer_20261001/native-records-evidence.tar.gz": "a65b053280d100f8957d81bfe8c0db0bf64d333b5d2d25df6a2a81eacebb2c48", + "docs/validation/s1_typed_viewer_20261001/original-state-omission-resources.txt": "ad07e195688b69d151f46b18acb6fce714894bb6782adc0026dbf94317211927", + "docs/validation/s1_typed_viewer_20261001/original-state-omission.log": "5252cea3e72f2d63cb16d77b686f2999d887d32614e7e15aac93b08b38357324", + "docs/validation/s1_typed_viewer_20261001/original-state-omission.xml": "4730ec696ed68865536fc526eaea121eeebeb81a4da9c9efa3680f7ea5d2ed46", + "docs/validation/s1_typed_viewer_20261001/post406-installed-state.json": "0e3ee0c358c8d9028a6c9b0c20690adbd7d1cabd753cf1801ef2225505d3c804", + "docs/validation/s1_typed_viewer_20261001/published34-r0-monitor.log": "4eb50c1b27d739bc9b16e1a71bc9011b0050052243cb77e870acc4104e0bb1f9", + "docs/validation/s1_typed_viewer_20261001/published34-r0-resources.txt": "42c2931ba158439b1562ada139e11cc0f3081c0cb0ef12e0d15854b9a8506359", + "docs/validation/s1_typed_viewer_20261001/published34-r0.json.gz": "051e1aa392fd50f846228a4abc5bc17e702882b0eb7c23bc1b2782259a4a5fd1", + "docs/validation/s1_typed_viewer_20261001/published34-r0.stderr.txt": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "docs/validation/s1_typed_viewer_20261001/published650-behavior-guard-resources.txt": "029eff604b7e6ecc9d0d0c654208313622c0c5a944d4b3c55d4fe62f0a0c208d", + "docs/validation/s1_typed_viewer_20261001/published650-behavior-guard.log": "1a734c2bb878fcbed3260593b6550ddde3cbcd9583ca3f882714904801b42857", + "docs/validation/s1_typed_viewer_20261001/published650-behavior-guard.xml": "d2efba2fed76273726d75652c9e21f1b50493db1b10713223926bb765e50d82b", + "docs/validation/s1_typed_viewer_20261001/published650-r0-resources.txt": "371023f0109f550aa6178afadf393e345f7a335b61d91e3a0b999179265146c6", + "docs/validation/s1_typed_viewer_20261001/published650-r0.json.gz": "a23684d02d7ba5d38d7da14482ed75aee457aff63c873e0fa85a7d65af45f576", + "docs/validation/s1_typed_viewer_20261001/published650-r0.stderr.txt": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "docs/validation/s1_typed_viewer_20261001/reproduce_original_state_omission.py": "19283f743be303b4ae83332bfbff5dfa1b7235866d1b49dcb152c56057a4dc6b", + "docs/validation/s1_typed_viewer_20261001/state-increment-corrected-resources.txt": "fe16c912770db2206d6a1a8cb10a9bfa82825c5cdd7bb95a598c74989653625d", + "docs/validation/s1_typed_viewer_20261001/state-increment-corrected.log": "254d1b321849d327d913d328a9de0b7f181592c659186ad7de8fad3c197f8f01", + "docs/validation/s1_typed_viewer_20261001/state-increment-corrected.xml": "71eb7924a34c2c54ff4c18e8c8b7dfa0ac5a28b4363d8c502185a0fee4b9a402", + "docs/validation/s1_typed_viewer_20261001/state-increment-resources.txt": "2cf59a9dcc53f9ce0715d59bf21a530e5cd512f84a6120a5be2ad1d06daaa643", + "docs/validation/s1_typed_viewer_20261001/state-increment.log": "46da625e68f3ebba3dfafbd830d0e9d6d0ec975d410ee676564cbd24ab52d6ef", + "docs/validation/s1_typed_viewer_20261001/state-increment.xml": "a7d74bd83f5a3317f084114638dd94e1f638b3a7e1e7e4fa9240b95c4ae8c831", + "docs/validation/s1_typed_viewer_20261001/state294-r0-monitor.log": "ab3006a9704297f8cfc23681ad067aabd3ebb74d9f4c636b789734ccc9d3371c", + "docs/validation/s1_typed_viewer_20261001/state294-r0-resources.txt": "88da2f583390a30345c30ca2f980aa765744aeedb0439ed979e0fbcadd852e18", + "docs/validation/s1_typed_viewer_20261001/state294-r0.json.gz": "a2fed5f2c614933baf3871b024ede05a35f1e741cee9e683a89dc84a7ef0aaf1", + "docs/validation/s1_typed_viewer_20261001/state294-r0.stderr.txt": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "docs/validation/s1_typed_viewer_20261001/state294-r1-monitor.log": "016352ac2d9f99ff17391cd8152818ae24a665694924970eb7010fcc7264fe22", + "docs/validation/s1_typed_viewer_20261001/state294-r1-resources.txt": "2d7b3c3ecfb4d3c44f304a542792c88e7e1d7e5cf9b5a58ca38e05a70d8408f8", + "docs/validation/s1_typed_viewer_20261001/state294-r1.json": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "docs/validation/s1_typed_viewer_20261001/state294-r1.stderr.txt": "c73ae9d0958e21ee045b54f40aed84d1d5a168cc5db3217b03f1f7bbaaccd017", + "docs/validation/saved-label-plane-domain-419-20261002/COMMANDS.rst": "623b2e8e27b6d65ecaa0380f1b22ebed2b2a7a0e3c9884d2f990fd1830560cda", + "docs/validation/saved-label-plane-domain-419-20261002/RECEIPT.rst": "b7e2f6f5a6c81a735697625d893ee05a9e920cae8efbcb06f8a310b3c0b9d10f", + "docs/validation/saved-label-plane-domain-419-20261002/adjacent-controls.log": "2040d0529e119c1cf4bd9990dcee635363ea88e7d6fa9f310a1470fdad3e2593", + "docs/validation/saved-label-plane-domain-419-20261002/adjacent-controls.xml": "e1748f4735194d2c73c261095945a4fee33291024eec2eb1a23a8258e5c1e6d9", + "docs/validation/saved-label-plane-domain-419-20261002/distance-original-red.log": "507cff5c5a877b05998766a7230571b74b08f08f34f949ceb14e856e7eeec75e", + "docs/validation/saved-label-plane-domain-419-20261002/distance-original-red.xml": "e52f0e31f427f738e7bd0794fb6f11c6e288f097a5bc412eac79ef5d77762806", + "docs/validation/saved-label-plane-domain-419-20261002/final-owner-controls.log": "f53685e5d35f8d165bca7c5025d6529fed4e25a534941d505d29f4a352c583b8", + "docs/validation/saved-label-plane-domain-419-20261002/final-owner-controls.xml": "4c986a6c379af168ac3b2084a766f4f746d888f2e0904f8753160e8ac639ddf2", + "docs/validation/saved-label-plane-domain-419-20261002/geometry-and-runtime-values.log": "c90fc9b248c9b58d80bc925d6b423822549ff56e521bce23c9fa1a67d2468e26", + "docs/validation/saved-label-plane-domain-419-20261002/geometry-and-runtime-values.xml": "ea4f2c0f2cce1a1b0ffab25bd344964db00a97e1c59cb1b340099c767bcc4bc2", + "docs/validation/saved-label-plane-domain-419-20261002/geometry-focused-corrected.log": "537f437c7e370c4c371ada9da906e453a7df6eba507ae1015a9f3dd0e34b722f", + "docs/validation/saved-label-plane-domain-419-20261002/geometry-focused-corrected.xml": "d0fd31efd8c0ac05756911e1ce37c4c33c2742fc78717c13a390b3597161abf1", + "docs/validation/saved-label-plane-domain-419-20261002/geometry-focused.log": "0d1188e656fb161643b8a5b1e5c74c353d5a6339eabff5b73a57a4693d701692", + "docs/validation/saved-label-plane-domain-419-20261002/geometry-focused.xml": "63fb1d68390a01aa1e51cc4b56762985ac88ba5e626b741423a15e28d62713fd", + "docs/validation/saved-label-plane-domain-419-20261002/metadata-owner-proposal.patch": "70d25df2a6c124a2713bad58f26a9426301a23121b1598358886a624a54aa9fd", + "docs/validation/saved-label-plane-domain-419-20261002/original-red.log": "c91bc58472af8fba735e3c59d98fc10126078f518a0aa4c54afdb21ef3d977d3", + "docs/validation/saved-label-plane-domain-419-20261002/original-red.xml": "a19699054d7619903a6d78ff2643a6eb52a480f1ecf4b3e4e334327ad6fb5b16", + "docs/validation/saved-label-plane-domain-419-20261002/projection-owner-controls.log": "54d34e37cb02a7b303069dae6321139c1f41558d42199b2873b2f2857a558177", + "docs/validation/saved-label-plane-domain-419-20261002/projection-owner-controls.xml": "d83541cd5294bac9b6eb6582ed63d40e101e35fa10f5340a89c6a226621b8e92", + "docs/validation/saved-label-plane-domain-419-20261002/r0-final-production.log.gz": "16de3af95169cb460b3804c0739cca2be11cf5fa41da425c8ee34595b5a5f4b2", + "docs/validation/saved-label-plane-domain-419-20261002/r0-original-pin.log.gz": "fb62e3e1bd27f14962c1ff6fd80219b76fa46d439b061c45fdb2d156b37128bf", + "docs/validation/saved-label-plane-domain-419-20261002/r0-unapplied-metadata-proposal.log": "3a27d8907764a5d507e7b5224ddd1ac2531a75045467114fd41144693af3b78e", + "docs/validation/saved-label-plane-domain-419-20261002/r0-unapplied-proposal-commit.log.gz": "332e17a1bafada3a973a6c6f342582af9c37900d1b1d0f628962ed5e96f6038c", + "docs/validation/selected-discovery-evidence/INTEGRATION-R0-UNCOMPRESSED-SHA256SUMS": "be70dafdd273ed3451c0fb856232944ce1c2913d14d637477896115c6bc902d2", + "docs/validation/selected-discovery-evidence/INTEGRATION-SHA256SUMS": "e9d5f53b006829923cb21b1723c90de85a2f6831f35dfee3d866d2d134183931", + "docs/validation/selected-discovery-evidence/POST409-R0-UNCOMPRESSED-SHA256SUMS": "c4ad34524e6d3a07ae23069a6b682c1e51dba209e5eeac19eb46c84b3569c8ef", + "docs/validation/selected-discovery-evidence/POST409-SHA256SUMS": "4789abd3c950e993f62311c5e330c2538457e6f6044e25ba8b49e226a75d6de9", + "docs/validation/selected-discovery-evidence/R0-UNCOMPRESSED-SHA256SUMS": "1904920c804514e02e5724f668a9cf7a366d17802f6216fbeb3a451f2ee2f845", + "docs/validation/selected-discovery-evidence/SHA256SUMS": "d1cce732f480f0a0c560841d1886985dba17228cf9a9836461090c901827e60e", + "docs/validation/selected-discovery-evidence/batched-source.log": "dd9a7176dca9cf7fe8469b22e84bd9d18d6ea6d2403889ceb35070d964c6d722", + "docs/validation/selected-discovery-evidence/dependency-r0-final.json.gz": "2e78680aee46bd87337a6daba134a4581eba3faecaa98098b6db2cc0c4667fd5", + "docs/validation/selected-discovery-evidence/dependency-r0-final.resources.txt": "6f7f002a149e1e1f6ef637ab5a21fe91ca916be6460a9ea09ac587ae994c230c", + "docs/validation/selected-discovery-evidence/dependency-r0.json.gz": "6384b08273e512617fe093e93afc3075b763018bde0c4763b15910d96eed549f", + "docs/validation/selected-discovery-evidence/dependency-r0.resources.txt": "8807884bd3219966fbf8298f44fc8016e61fb75b4863202a93dea4bf1c456000", + "docs/validation/selected-discovery-evidence/final-cold-profile.log": "966738b1d54475af8832eba82a43e5af5a469cfc012f0c51473defd0d4d70357", + "docs/validation/selected-discovery-evidence/final-focused-source.log": "47d54fd3d8273cfd817175999c00b2371e1b78f5f76d705adbac33b620b3f63a", + "docs/validation/selected-discovery-evidence/final-owner-source.log": "add8d502e1c43e9539d3c23b867b3324c179157af274061ae33c8647ab767781", + "docs/validation/selected-discovery-evidence/final-source.log": "9e95a6eb3ee8b6dd7486cc119eec33881e67af196f027b263caf419e85f05298", + "docs/validation/selected-discovery-evidence/first-source.log": "2a9664210c327d1c1bf04666378e94a3f5d97595d833d30cbaefb26d9f208142", + "docs/validation/selected-discovery-evidence/integration-376-commands.sh": "fbece378e58ffa81e784c832eb49e12768a21c1cc4ebac11696ee413a36c417b", + "docs/validation/selected-discovery-evidence/integration-dependency-r0.json.gz": "1d2724497115a55f624731a4a1dd7fcfb9a8c597420d13a48451ed2a6ac0ebb0", + "docs/validation/selected-discovery-evidence/integration-dependency-r0.resources.txt": "e5a4e30cf0f41b3b37551048679565e31e376ea495c6efa07bd0dc284094f964", + "docs/validation/selected-discovery-evidence/integration-focused-source.log": "b56a1f6e7357a8ae759227fb2b305128207d28815c771d3e507fb8337f4efec5", + "docs/validation/selected-discovery-evidence/integration-openhcs-r0.json.gz": "a17fb99918637752df40c9c13ed3a9d0e1063066e431b3278d6996b9a3ec603c", + "docs/validation/selected-discovery-evidence/integration-openhcs-r0.resources.txt": "d5ea604cdfb2c7a7aa0146c29643680fb8aae512a8ddcd75dc956687cc7ad13a", + "docs/validation/selected-discovery-evidence/openhcs-r0-final.json.gz": "22a5b2681b463f6bd703945b215ac3771a976a73f40798fe2c1d346df35365d4", + "docs/validation/selected-discovery-evidence/openhcs-r0-final.resources.txt": "c8220ad8608fd00d673e7ee21320bff5fcdaa5c1cd4dcfd60fcf5e7256fcb6d3", + "docs/validation/selected-discovery-evidence/openhcs-r0-rejected-24a.json.gz": "e0a038a85c8ccb738b5a83ea6407f645fce077e0659504aab4207f87de6ab8b1", + "docs/validation/selected-discovery-evidence/openhcs-r0-rejected-24a.resources.txt": "7d35370cb80f8cdcd3844ce440538f7dd3f2a2e6d48c734b4df748ac0b131e54", + "docs/validation/selected-discovery-evidence/openhcs-r0-rejected-5fd.json.gz": "9d1aa365b7f5fe9f0df1fa32838cc02d5a76263e43e92a4db4d3d58100ff93a5", + "docs/validation/selected-discovery-evidence/openhcs-r0-rejected-5fd.resources.txt": "3bbf9700b0fe8e4909d85f742c03e873e1f942aca57ed817979e21a338c9eaff", + "docs/validation/selected-discovery-evidence/owner-source.log": "42e82c8604c2acbadbc9ae357b3637ef60d10cdedb158a1f662a9b067b5dddc0", + "docs/validation/selected-discovery-evidence/post409-focused-source.log": "1882553f43e2c9c9c317da989f0a01e2bc5926fdfc0cc071af21193a33b3f9f3", + "docs/validation/selected-discovery-evidence/post409-openhcs-r0.json.gz": "8b6fb413e437868d7d3a8f57d6c95357a82a24513bc08a47ef8aae14751a83aa", + "docs/validation/selected-discovery-evidence/post409-openhcs-r0.resources.txt": "26f81936ae1fa819759de8fa36ffe8b85b12171dad403e903a7a176333a7c54e", + "docs/validation/selected-discovery-evidence/real-callable-newcase.log": "a6009050c70e67e3fbbeb7ab539bb945b3ea2d90ba91b0ac2845093b5b2c5bdb", + "docs/validation/selected-discovery-evidence/retrieval-admission-newcase.log": "6be237db318800db8743aa4b1704cdbf869ae618e8d104d1f0bc8c07b786c26b", + "docs/validation/selected-discovery-evidence/second-source.log": "fc3bfdccd74bab0fa2ee1c419977b10a012b635ff23cea398436c83de0dbbee9", + "docs/validation/selected-discovery-evidence/selected-cold-profile.log": "da7dee38615dcea87f5f32107b43d508ae2c9a023d71343717e64098127ea7de", + "docs/validation/selected-discovery-evidence/source-roundtrip-newcase.log": "b6c618c01b13edeb60951cb832d16a55591117eeb64233a960dc745e0df9f504", + "docs/validation/selected-discovery-release-022-SHA256SUMS": "a63b333d22b1661f697f82ccdbd08b647caa247340aab68a0c6b4fdd3beb19c8", + "docs/validation/selected-discovery-release-022-dependency-r0.json": "29c7c539e72b2a7ea5eeecc78d0f63e47295ad7481d342f37f2ca054664304fb", + "docs/validation/selected-discovery-release-022-dependency-r0.resources.txt": "d84918440598f01fec5d398291c0a693a29877fd835181e835ca4fa23984e529", + "docs/validation/selected-discovery-release-022-probe.json": "7cd9d87e95e6fa126b026c9ebe5318cae6f0db2060fcb1f9b76946d65f65603a", + "docs/validation/selected-discovery-release-022-probe.resources.txt": "31704c7fafd27915b07fa37fe4ff2860c9fc57a84cae6d29b36b6f990de6d150", + "docs/validation/selected-discovery-release-022-source.log": "dc7ae3abeec2d9c22fc02904a2c7cae12e07ee4e82923f49507e9386ee4ebc72", + "docs/validation/selected-discovery-release-022.rst": "2721642aa582978ca9d259bf4c4ba0d8c8b97144d2cc9253b34e01888a8f60d1", + "docs/validation/selected_plane_reopen_stream_20261001/ARCHIVE_COMPARE.txt": "78f64d6b776ddef11592119e980c92143fa3ac6187ee5230927ece67c0053bd2", + "docs/validation/selected_plane_reopen_stream_20261001/ARCHIVE_SHA256.txt": "95b3799721a65e092221486597023ddaaf5ace34ece35dd34b346a4e6a2c6f86", + "docs/validation/selected_plane_reopen_stream_20261001/MANIFEST.json": "bb9e434c7624a0a33a6620ddafb1bfb98153a308fbd98ee673f92909b67613e5", + "docs/validation/selected_plane_reopen_stream_20261001/RAW_EVIDENCE_SHA256.txt": "df7689317ce04f13036b68ab9dd6109090d0c1dc1a03bc2740ee49f896bf0439", + "docs/validation/selected_plane_reopen_stream_20261001/RECEIPT.rst": "cf9f0984fe13d15b902686b271a61f2265df7d55dc4fb85c69c41e83a0f321f7", + "docs/validation/selected_plane_reopen_stream_20261001/pr.md": "c8bd030f3993bb98ea7cbcba8d03cdd022d4a843ec29a4f5327cf3e5342fea32", + "docs/validation/selected_plane_reopen_stream_20261001/raw-evidence.tar.gz": "e9a10347805e3e334c0783b742136328430389ff2abd9932ace08779bbfe28b8", + "docs/validation/selected_plane_stream_materialization_20261001/RECEIPT.rst": "cfb9e1eae7a5d3ea473258f03125a6df40eae1741f95d848b8d087458c7ff6cd", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-cellprofiler-stack-resources.txt": "2fd486f6e1036a1d74a169085092fe66da9ece0c6697722a725bb8d3f6e408a1", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-cellprofiler-stack.log": "57c950ee9f24d5e712bd6ff8bd64e7ddc06805f45545e0ff815bb510aff1a424", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-cellprofiler-stack.xml": "7c293ae9f1863dc54854dc416fd7c8a34646f4b4b850963f36d17d069f441b8f", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-current-focused-resources.txt": "245bfcc52b1060a35c8f9f1613bfa1b339aaddd399c63872a9d72a45fc0223a8", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-current-focused.log": "483ddbeac21528d0c49e8957e780f0db4f01e6112caadb2f62c2f43a5f765171", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-current-main-controls-resources.txt": "4acf2221407b8a076e06a5cd51f3d98649dc276bc0109aae62070310c60acc7d", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-current-main-controls.log": "4ecc9f939969305b5088436c1801fc2bc2f132a7230a32bd40fed3795a696df7", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-current-main-controls.xml": "05174345fa49314c637ea60efac1d0407ddd0c9baa0e9a53e13aafc07d7dac74", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-function-artifacts-resources.txt": "cf019fcdde882b2f654e4b29c15e8a74cc536746fd0ae92385a759585c09306a", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-function-artifacts.log": "53161fa4a3dcceb6845946bdb4163cf6e931385c6dcaa9f3e9fcf27af0aeecce", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-function-artifacts.xml": "cc9100925376e93d85d8a7e581d843e2e12d6efe15b9e1050759d387098939da", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-image-contracts-resources.txt": "0828ad48c2ecfb0d373f25d124a969a0577da355e85a46e576c6b16c18cee20a", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-image-contracts.log": "7d37aa4af9703aa1a86359430146dcf38d7b5a74a7f219eb1bc8d9fcd00eb0dd", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-image-contracts.xml": "1cdb2f2a3b79bd14bbefe57f94c6dc885b97cdb49f97c12e635bb8b5900b015c", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-main-qualified-resources.txt": "59d0935aaac753b956707fe42998b26d7df1c94c884b5b9634c62308d5e77fb1", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-main-qualified.log": "0036ba913e6bb8c6cccb6c99e83211f1367902f0f84f40762bfafd966cacfe42", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-materialization-core-resources.txt": "2f930896c44509a51af7f94e95d9fc7fc0a887c14591817e2ff1e75328a338ca", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-materialization-core.log": "bd05cbdcb3653fa21b5596ea24f7b260d3a8e08cff4646eec02a8699ea421bcb", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-materialization-core.xml": "4aaf829830b30fac695d4450046c8c9abcef08937df944f6b4c074eccdccfda7", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-materialization-resources.txt": "f3b98aa56c951ba800daf4ae1d749020cf1ca9e20158273aae12c1689a30f9d3", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-materialization.log": "a1ce4b0fbaed634268995c5649cc0c852ddcfb73a1aeb5bc50ebc21f0e24f2b2", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-no-autoload-resources.txt": "250e68f8d3eabf2f7668caf27f52fe7fcb65566b09eb7f66195f6607ab22eaf0", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-no-autoload.log": "120ae968c0313e0e6de36943d5fbd1b2e572d32ca8d4b93a0780d93bac796275", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-no-autoload.xml": "eb48f69dddc6cc6cfc1d91113435b9305dcaf0912702c8d9ef0d67ccddbb62cb", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-public-path-resources.txt": "eca634d87fa14d341efbd09540cc8adf386dc94697ea900c3eec78b9cc17f51b", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-public-path.log": "5411dd0da5be0bc9f8f1f6bcc4354d3432619672c8e3f1f15af532782e9dc6b2", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-public-path.xml": "040b5fbe73e4d289fb3459a8a3fc7fc86f7c03cff30bfc4713810b01e676c6d4", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-viewer-controls-resources.txt": "6fcb6bcdfd4d57b7cc015bfafbca2015563ba539416209e67f2464ae2b166445", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-viewer-controls.log": "e89efd39c9162b9e41ac96918ea1c23f0a2885026aea1eb1077ce5b2891fbbe0", + "docs/validation/selected_plane_stream_materialization_20261001/bounded-viewer-controls.xml": "ddef8fe1733027f486b299f78bd35899c1bc9b838e4c94c1219b2a3d3261b02d", + "docs/validation/selected_plane_stream_materialization_20261001/current-main-controls-resources.txt": "17d97343edfacf087b380998b354f202c35f14323e06bf811af460696fd0799c", + "docs/validation/selected_plane_stream_materialization_20261001/current-main-controls.log": "d0cc26ae4d40d6e070e56e94def8ecb144a726c6b0c076f563c92453dffaaa0e", + "docs/validation/selected_plane_stream_materialization_20261001/current-main-controls.xml": "1a1ca8a990284717bdb505b22750488f5e016c0db7cad7289419a99ddb9c80d6", + "docs/validation/selected_plane_stream_materialization_20261001/expanded-first-resources.txt": "83323e64caa0828dffdea1ce1fbe4314d1fbb09912e76293a2c7def041981493", + "docs/validation/selected_plane_stream_materialization_20261001/expanded-first.log": "bd3fde0560246e25e729e44a37d471e26800130a220ce8f46a5dd0a0e0142151", + "docs/validation/selected_plane_stream_materialization_20261001/expanded-first.xml": "a397af52eb6fffc37e8c9b69e35a4801ccf68cbbd8a352066b9a76279b00177a", + "docs/validation/selected_plane_stream_materialization_20261001/expanded-second-resources.txt": "c014b5aa67865cb831d9c13ed23010dcbd980a14b123cc87e9e6ce63f457e0ea", + "docs/validation/selected_plane_stream_materialization_20261001/expanded-second.log": "9f73cc78f977dfd3ceaf42e06cf156d32bb4069495257665edd0d597a7dd5141", + "docs/validation/selected_plane_stream_materialization_20261001/expanded-second.xml": "f58908dcb36c59d235e7f4f4d23ee046b62c69bba7e66e26ec971ffc183f28b8", + "docs/validation/selected_plane_stream_materialization_20261001/expanded-third-resources.txt": "e7415545efd460a1df144ee9163a47cf8c88efb8ee139e173990c0cba5fad3f4", + "docs/validation/selected_plane_stream_materialization_20261001/expanded-third.log": "f6979a441ce7342d673f04787891175209a9b5c4cdc29edee46cdaba57eabc95", + "docs/validation/selected_plane_stream_materialization_20261001/expanded-third.xml": "554edb8d5eb68c1e5ef7403d30f0d9339e54e7b6ec6b8fd0112cf49885fc3b9f", + "docs/validation/selected_plane_stream_materialization_20261001/green-initial-resources.txt": "d01f90da65eb6064520a111898caa5ab22d327dd2b44cc1e7caf3d8fad0b3252", + "docs/validation/selected_plane_stream_materialization_20261001/green-initial.log": "e831ffc1b00add910a59de3680f959ce40d9edb277938955dfaddd9080dd89db", + "docs/validation/selected_plane_stream_materialization_20261001/green-initial.xml": "4665aa39d7cbf3c2e6b7a1f63b729e1069f8480246bfe61ad33ebd0946842962", + "docs/validation/selected_plane_stream_materialization_20261001/image-contract-controls-resources.txt": "9f541151141bd3d845167395629fd58bcc716dc93bd9e4fd901cb3598dce971b", + "docs/validation/selected_plane_stream_materialization_20261001/image-contract-controls.log": "62d6885ca5729bdb8ae0b3e758a284583ec707f96b958d7841896e23fac16780", + "docs/validation/selected_plane_stream_materialization_20261001/image-contract-controls.xml": "4927e53aa381f5c021467ba709f7f75d7699d93e7d5984bb67869474df271171", + "docs/validation/selected_plane_stream_materialization_20261001/issue.md": "1ff26d4f02f7ece635c3679c5b7af6d3882d8f175f4a1118149b6101aaa2f9a9", + "docs/validation/selected_plane_stream_materialization_20261001/materialization-controls-resources.txt": "c989c058cf6e001f3ed615b97abdac89f178f1fe7731b0138e88c9ff5a9a940d", + "docs/validation/selected_plane_stream_materialization_20261001/materialization-controls.log": "8fd42c72e1ec52b9524d4e1a91bfbc378ebea513082e7a65e5769e5c3704a8d0", + "docs/validation/selected_plane_stream_materialization_20261001/materialization-controls.xml": "66094ff8a49d1dc0582f970560dbc4aae10a4e95dd7fc776e83919ebb1a7d503", + "docs/validation/selected_plane_stream_materialization_20261001/neurite-output-base-resources.txt": "d573eeb2fe4e6031831eeefe0046f4246483c74145f58cef69c1f53c7598d2da", + "docs/validation/selected_plane_stream_materialization_20261001/neurite-output-base.log": "d527be247c901531629833a96a5a82893ad03e686e2b98917e72f413f6fd9951", + "docs/validation/selected_plane_stream_materialization_20261001/neurite-output-base.xml": "e79c96d3a4782c8291c1c3fab32ae150f3730ac7a3529442853d8bf97717e34a", + "docs/validation/selected_plane_stream_materialization_20261001/neurite-output-controls-resources.txt": "76483b1e7e4dbccdc7401f7f6fc014f81be93ffe5bb48c7fc4a1a08029f1ac32", + "docs/validation/selected_plane_stream_materialization_20261001/neurite-output-controls.log": "0b22a8b7a65fdd23afd05010321a43b9a46316a38cf26aa83799cf2e35a0e791", + "docs/validation/selected_plane_stream_materialization_20261001/neurite-output-controls.xml": "6c15424ecab3c6af922abc720f05a8d4e9fb4c683d86e879d0e42087fc7170bc", + "docs/validation/selected_plane_stream_materialization_20261001/nra-current-full.json": "b9972265184516bb5e21f7f00618d41191553c660b558f193b77a902b55c2c4e", + "docs/validation/selected_plane_stream_materialization_20261001/nra-current-full.stderr.txt": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "docs/validation/selected_plane_stream_materialization_20261001/nra-current-resources.txt": "f28756e88ccc7d4fdf6374c4cea84a9e1dcc3a40d436378cd7a2043fbfa4f4b2", + "docs/validation/selected_plane_stream_materialization_20261001/pr.md": "c97b1560636ed18fca4d120d31fcd74a592293ffd232976a332d1b8d5725ed89", + "docs/validation/selected_plane_stream_materialization_20261001/r0-current-main-resources.txt": "c183e6b674c98de8408c822ae2bf3e04bbe44bb97cb89c2eb1f51de38f130f90", + "docs/validation/selected_plane_stream_materialization_20261001/r0-current-main.json": "6430b77980922e48afa039d43ed7df86951161ed47df85c5fddbc46e7f84106d", + "docs/validation/selected_plane_stream_materialization_20261001/r0-current-main.stderr.txt": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "docs/validation/selected_plane_stream_materialization_20261001/r0-resources.txt": "bae4f86f8ec8e541185511a82ae7a689fce1837254d241aee2d9e714385db91a", + "docs/validation/selected_plane_stream_materialization_20261001/r0.json": "081a684a115b3b32e1b42a1f67313119d1b1189657a907166ecfa4e8c982bf4f", + "docs/validation/selected_plane_stream_materialization_20261001/r0.stderr.txt": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "docs/validation/selected_plane_stream_materialization_20261001/r1-exact-policy-resources.txt": "2852c4b6a2f459c5fd0caa0cd33368e5248887c5bd1688df9eee5ed034c962ed", + "docs/validation/selected_plane_stream_materialization_20261001/r1-exact-policy.json": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "docs/validation/selected_plane_stream_materialization_20261001/r1-exact-policy.stderr.txt": "fe927dd1e1a7184b7b40aa0c2d4a396fc39caf92cce49627107fca98ad4737c1", + "docs/validation/selected_plane_stream_materialization_20261001/r1-original-resources.txt": "8282dd261e48cc8f6f8d893572f5d1eaa4f267bbe9f21164382f211ae07f5441", + "docs/validation/selected_plane_stream_materialization_20261001/r1-original.json": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "docs/validation/selected_plane_stream_materialization_20261001/r1-original.stderr.txt": "a004b8179efd0457ab5b59783330f711d65de6170d13553b5592417390c62e0d", + "docs/validation/selected_plane_stream_materialization_20261001/red-resources.txt": "b2e8463ad7747250c591bf0999a8e065c953132c44b61fb0aefeb7287cffda40", + "docs/validation/selected_plane_stream_materialization_20261001/red.log": "2f085f7c452c1af3103c0cbfb24ea5d21c2a1b8af45ddae19815236e76f34a5f", + "docs/validation/selected_plane_stream_materialization_20261001/red.xml": "764fab3bfaffb3854fae58a687a0e57a71df6b77bd7e9c33636aed3a7ec3345e", + "docs/validation/selected_plane_stream_materialization_20261001/run_bounded_source.py": "85df89047c2ce6c08c5fd9271dd99143187c8f9c69bfe249a3979625cb594204", + "docs/validation/selected_plane_stream_materialization_20261001/run_original_projection.py": "108e30547b560c85f8579f71e0ad3fa56c579f8bb89877a3c9d97ebec5934b49", + "docs/validation/selected_plane_stream_materialization_20261001/validation_summary.json": "55e0261bac8922b0ca07b21e3560ab49220011cf77813314e3ed9e49ac65f06b", + "docs/validation/selected_plane_stream_materialization_20261001/viewer-rejection-controls-resources.txt": "6e0134be1695477ae0c87d7579c5181db11c35cfef42d09edc73c022adba913c", + "docs/validation/selected_plane_stream_materialization_20261001/viewer-rejection-controls.log": "9123634ee7c6f12cc325157198f190fe103174c761594201f0ff592ce54affc5", + "docs/validation/selected_plane_stream_materialization_20261001/viewer-rejection-controls.xml": "e3c27f934172a14bed6caf2f47aa0e5d7f9f17925eac9b04602479f500afa7d1", + "docs/validation/shared-dense-coordinate-owner-20261002.rst": "76ca78efc8cf399eb57e67a47c0f5e2db0eeda7878847b8019b94b404ce70481", + "docs/validation/shared-quantized-hull-geometry-20261002.rst": "3d7ffdb5487822f018765cc31662131d32def122f2552d62fa14f15d23d3117c", + "docs/validation/shared_runtime_plumbing_384_20261001.rst": "c3a67e1dd9088cd4f52cddd8fbed167f72412b62321ce4008e88797e540ad29a", + "docs/validation/singleton-label-invocation-477-20261002.rst": "df9e2a1dc03db18a9a70ac3091a20f1e20c37ef8b07d4dfb624970981ce04425", + "docs/validation/singleton-label-invocation-main-477-20261002.rst": "df796e3e93eec384bc1708aef914295a69bf9ecb1ab46c6e7defa4e5be9b5401", + "docs/validation/skill478-distributed-threshold-diagnosis-20261002.rst": "6e55c9d155a1c244026db7831e62bce849c55b35b3e439cfdf1b7e1b9766124b", + "docs/validation/source-projection-family-20261002.rst": "5d2d8454a2cba7ad2fb4733b514a4fc6970de23e853185ddab6173f82ccc0d9d", + "docs/validation/source_projection_fixtures_20260930.rst": "2235c0638619dd25e61ae0a464b239ce326a73b3238c662fc0a83dfd986151b7", + "docs/validation/startup-feedback-358.rst": "4e7c4a5c23d615925fda680457243bec02e4c32c3242909ee116d1871729ae23", + "docs/validation/startup-feedback-evidence/SHA256SUMS": "c3ff98677ff01010ac43e2bbb294c470547d111fe3fa18077c78da4ff6bad82a", + "docs/validation/startup-feedback-evidence/green-final-feedback.log": "666edfd46df8704756cf8bba2f35aca0e1ca047811213992b8808cf0bd35d0e8", + "docs/validation/startup-feedback-evidence/green-initial.log": "285ff582ecaa4393df051d7046d72093198d06df1f759d89fb754a200c67ddea", + "docs/validation/startup-feedback-evidence/green-lifecycle.log": "5d1c5fd8b9798a219ef8ef074875fbdd893cb19c99824d854868c8ab20944da3", + "docs/validation/startup-feedback-evidence/green-published.log": "62692fd9cb6f07247ddd752dd461bd70e171e7b66ea07d3ffa234885663dcfa4", + "docs/validation/startup-feedback-evidence/green-wrapper.log": "3dcde6d6cb4b726e569267573da10136ae9dad6217042272ca1751fa466b007d", + "docs/validation/startup-feedback-evidence/lint-final.log": "d63ab3df51a50c66f9c57056288c7d4faf9dce413552d89215714320ac2b748e", + "docs/validation/startup-feedback-evidence/lint.log": "248cab557f593e8647e791bd7cd20e5f865019a3377a098f71bbd34a0d4f88d2", + "docs/validation/startup-feedback-evidence/red.log": "acee3ab05dd4773af10f01d23149a8ae084edf6b3b0f69d824da24fd2cd55f7c", + "docs/validation/startup-feedback-evidence/source_tests.py": "1325551caa7db9242fd5962a0290ff159370b67095aa31908b6a7f144937ac49", + "docs/validation/startup-feedback-evidence/wrapper-delivery-diagnostic.log": "c8666e0b95008596a4ec0fca34cc57a0c41f1affb90ac4e65ecd2861e7de144e", + "docs/validation/startup-feedback-evidence/wrapper-diagnostic.log": "810b80e0779b36bdc69f66eb32ea162161c371b34daa6893f34d52197930368c", + "docs/validation/startup-feedback-evidence/wrapper-identity-diagnostic.log": "6ae2bdbaecc6fca34e7fdd1a949c5e5368271c69de76e3e68c5227578df99272", + "docs/validation/startup-feedback-evidence/wrapper-scope-diagnostic.log": "fb70a117dbaab868c17c577ea4dc7d005729274ab0c47af731f207f4206b16cf", + "docs/validation/strict-mcp-arguments-484.rst": "8ab8b5f86d78eb057deeb41abd9acefc215618d4f6d153ef169780b0d43bba9e", + "docs/validation/viewer_current_main_integration_20260930.rst": "e38ac840a61fd88a6f726650cdca4e6c292eaab2d3ec62d53fcec1dc44f6fc89", + "docs/validation/viewer_reset_lifecycle_308_20260930/all-owner-bounded-first.log": "9916c9eebf3fea7c059532aa48bccf4858b622bb7e4e3abd0d3f68d481762b62", + "docs/validation/viewer_reset_lifecycle_308_20260930/baseline-shapes-bounded.log": "34beac2c43735d1ff1b5677765ad1ee0ebecaa6d988e1ac11bc20aae0ea909f2", + "docs/validation/viewer_reset_lifecycle_308_20260930/baseline-valid-transitions.log": "330004ca4e0e567b8e381c1776abbc342e11742b3ad8c5be084c8e4a94796732", + "docs/validation/viewer_reset_lifecycle_308_20260930/check-parent-artifacts.py": "8f556b3a73c09e4c0b08c395657f9cf82ec1f8108ad8be5d6cd2bb2e29f87a4c", + "docs/validation/viewer_reset_lifecycle_308_20260930/checkpoint.rst": "d8e96f262df080c82395dbe9bb3fdb8555330c280a004399625ffff75dd8ccf7", + "docs/validation/viewer_reset_lifecycle_308_20260930/final-matrix.log": "67fa3627739582d8e0131b80cdbc6e95e71a2973dc2ec72312f680438b064efc", + "docs/validation/viewer_reset_lifecycle_308_20260930/mcp-witness.py": "e754735e27fbe955f813a2da9ab5eb556ec3c48013aeab31b918a0b81a393f6e", + "docs/validation/viewer_reset_lifecycle_308_20260930/native-calibration-owner.log": "7c9903a4889900185e04ca6aeb5eb7da2b316dcb8f1ec857143db297566faa7a", + "docs/validation/viewer_reset_lifecycle_308_20260930/native-layer-owner-complete.log": "95d9ed4f50cd181f2e38bb887a1034e2b59e79f2656656c53829fb5121c7e2a5", + "docs/validation/viewer_reset_lifecycle_308_20260930/owner-image-clear.log": "8526bae6f0609d450c8d4fb153dc8faee16e74ce8bc525277fdf5ea46d3d6186", + "docs/validation/viewer_reset_lifecycle_308_20260930/owner-regressions-first.log": "7ce86ac591ec0f7e43c08fb45b57cd85dedfa7ad3a35077eb24194196d3d84a7", + "docs/validation/viewer_reset_lifecycle_308_20260930/parent-acceptance.rst": "d0e0241d1f4caea5d6e4ac6ee1b8a884dbb0a36f34a004fe880171afab90841e", + "docs/validation/viewer_reset_lifecycle_308_20260930/pipeline-parent.py": "d988a7b112ab8793c22cb452287d19d805f3dce8bb0c2305c9a7906c4f6d4e9e", + "docs/validation/viewer_reset_lifecycle_308_20260930/pr-body.md": "0abd364780ca4f8407464424374008cac9ceccd59b69e3876c23100cc619d898", + "docs/validation/viewer_reset_lifecycle_308_20260930/queue-owner-shard.log": "abca21fcf9ad6e1f9cefb7ef8c38a484f73375875179680771f03caaeccf89e1", + "docs/validation/viewer_reset_lifecycle_308_20260930/ratchet-final-resources.txt": "bdeab9583749d9b6d1bfce1d492fadb3a8f672a86c83730731fa1c22cadb97ad", + "docs/validation/viewer_reset_lifecycle_308_20260930/ratchet-final.json.gz": "70ee2d911ba7f205a27d83b7c7f8f294b42562e38b810af8fdd8de6524d2d6bb", + "docs/validation/viewer_reset_lifecycle_308_20260930/ratchet-first-314-resources.txt": "0ec866ba617a3c023d2ed284ead089a2818cbab29b06a3a5b9f55298118c1a65", + "docs/validation/viewer_reset_lifecycle_308_20260930/ratchet-first-314.json.gz": "344840c4424fa4780335a788858747b3a1441e599e1d1f74cc4dce2a022ff534", + "docs/validation/viewer_reset_lifecycle_308_20260930/ratchet-first-resources.txt": "dd7480f81ca757aff9fc1457129dccec6cd8bb1425a746de992e76a34ef6d27e", + "docs/validation/viewer_reset_lifecycle_308_20260930/run-parent.sh": "2d27b3cfb94d4444b91a0566840df5fda899f2a8c8d66a172134e7ada90b3fd6", + "docs/validation/viewer_reset_lifecycle_308_20260930/run-source-check.sh": "61447838b1691795ffc7e0bdb06d01b0544683267c218221ad996467f9471929", + "docs/validation/viewer_reset_lifecycle_308_20260930/shapes-4097-owner.log": "b2abbeba4cd5abc7235bc90c9216a493e9f3d5715417688036a0815313c11918", + "docs/validation/viewer_reset_lifecycle_308_20260930/shared-axis-authority.log": "e7935c30c741375b620afd614ef0078c289c24b5989ffdbe839576ba83bc0f65", + "docs/validation/viewer_reset_lifecycle_308_20260930/transitions-final.log": "c26d1c763a9a66fdcc46335ddcd37cbe103321d237c8ac5dc9196969e0f32ba9", + "docs/validation/viewer_route_spatial_302/checkpoint.rst": "a6b33c5439f99cfb65ddf08d523db9669244adc109b16f3a6de15bfd75980f71", + "docs/validation/viewer_route_spatial_302/pipeline.py": "0b7bad084f866391fa16109d63ec2c49975cccea723891a7a2c0ffe951df3ccb", + "docs/validation/viewer_route_spatial_302/run-candidate.sh": "7c0235b7b95d0047258618e111b14aa82f81e7fe7eca7538cf058e0512f965f3", + "docs/validation/viewer_route_spatial_302/run-source-check.sh": "bd8979cba48790c86a9af134694d5212367d0e77c539c27da6866c520301d033" + }, + "supplement_receipt_sha256": "4591cf83209105017f8d78bb64cc64f42d2b46d9968375186f92fa675c6b5b8a", + "supplement_preserved_sha256": { + "docs/validation/custom-plate-admission-500.rst": "efd0d37398ed532f15c1780e22d638c8527d7cf390febf8356b420ce2ee2e4f9", + "docs/validation/custom_plate_admission_journey.py": "8c16833a763456342bc521c5b12faf5904dee6c0c8265d9a99d4caf4be209eaf", + "docs/validation/knowledge-native-source-499-20261003.rst": "52a77c7752cb6b53f7a5f7974aaa2199b40e4865e6dbc4a3d3418630833fc688", + "docs/validation/registered-object-output-categorical03-20261003.tar.gz": "cfb5acbc8b132d3287a3f484586211598c6ec0004425da71b350b4e98e02c521", + "docs/validation/registered-object-output-categorical95-20261003.tar.gz": "16d29b1c1629c1908211ed1280504ae02fe0fc07b1ad63fedfd2f805bd4dc3c7", + "docs/validation/registered-object-output-installed-20261003.tar.gz": "2f76da3cf82a7f431a73bfdd73b5c13a5751677ff9c6515e61f83bf77f3c8cbc", + "docs/validation/registered-object-output-public04-accepted-20261003.tar.gz": "e9e5ef2aeddfc3c274790a8752a708b66d3d0c4d7fa400650c67b18737ebc470", + "docs/validation/registered-object-output-public95-20261003.tar.gz": "562e6d03beb246732d197fce156a2656b044f4495c997b85edff655af2b62e39", + "docs/validation/registered-object-output-scalar04-20261003.tar.gz": "0406ce8ccd7ce3ef7b72793d0aa0424c8297705c2348cd213310fc07ffd142d2", + "docs/validation/registered-object-output-source-20261003.tar.gz": "c6f6184f9ae097c05542e187398c7c0b174c108feba087463ad035cec4214f1a", + "docs/validation/registered-object-output-split95-20261003.tar.gz": "9be6f11b2dfd749aec5a2f0b71eb26515744e44d1299ccf76129236bc3a19003", + "docs/validation/registered-object-output-working-20261003.tar.gz": "c8fee189f0e2101d1bd8568094cb58fb1eaa4bc17d070fc2b027d7702e007fc4", + "docs/validation/registered_object_output_boundaries_20261003.rst": "1a6cc7d1a8490ac7845fbd0ef77687ff8fca7f2d451e3148ac30d74aa3146150" + }, + "law": "Every preserved website/validation file checked; every currently absent tracked byte equals current Git blob; exact8Gitlinks retained. Only these two explicit sparse prefixes admitted." + }, + "sparse_supplement_manifest_sha256": "4591cf83209105017f8d78bb64cc64f42d2b46d9968375186f92fa675c6b5b8a", + "canonical_head": "902913616e19f5b242928d4e835b8c2e10ab2fce", + "canonical_status": "", + "dependencies": { + "external/ObjectState": { + "head": "b0004daf3223326ac78689ba7b2e68abc18ae298", + "status": "", + "files": { + ".github/badges/coverage.svg": "5f87bee9f8cf0a67b8cf6e1eceb1fcbb1bf57b3875e1979527faedf2ab2a8c9c", + ".github/workflows/ci.yml": "f93e84bd08fd1c8254b7b863cb2d99c74db85fb8b16bb3779a698b166f660d90", + ".github/workflows/coverage-pages.yml": "222698b93c43d62c6a301b00429084c6b64efaeebad41df2c29949205992057f", + ".github/workflows/publish.yml": "9ce34113e7ff654cfa86cff6018299a98fc03a8851f0b9899777e90bb1bbee9b", + ".gitignore": "a26875c24503b80c1699f99988e68bb14ef3f4834d476864139553a63be5148a", + ".readthedocs.yaml": "2f5648c67a8a1ba1db73ec8dc32fbb82ffb83b246671e1dade4cb8836d667c6f", + "BADGE_SETUP.md": "8395730f7b6a6b5c7c97de76802bd2513bca8cc29a4dd8fcad7a91d4f713b097", + "LICENSE": "c5a804a1e4c08f5593eb8466c91dc4e871fe79319d81737a82a3cc514b7b2ff6", + "README.md": "1d72de341fc17c96587a75bba4a913787321bf598b848474a833b34a0547842e", + "docs/.gitignore": "79b7c477e1d731b44f15770e60642935f79fcaffdad433be5a9a82d6c790d66a", + "docs/Makefile": "43df277c1f15a11e9ab7180cfaa9e65e014571beec43b19530a363f4e62e0cd9", + "docs/api/modules.rst": "bb05baa62c63a0ed6a5773ca4b644de247b91486f65cb1f18e4671e3cce2a595", + "docs/architectural_fixes.rst": "278558dd9d4bd42a347147b22b5f52262192f8a72819fb4365055c2e5c5b4448", + "docs/architecture.rst": "2523332a130b3e06aae51f127543261788b208e61d0646ddbc99104d0383266e", + "docs/archive/DOCUMENTATION_AUDIT.md": "5d2ba10f1b5f3a6f20ec74ffb10ed6358866f193737a71ce4ad2af025be12807", + "docs/archive/DOCUMENTATION_FIXES.md": "89d8f817cdfc018af56c879cd5edad906acc566b2444404a94f0b00c65eb007d", + "docs/archive/README.md": "e5a389ed4d06d204e0af5dc2502f5b661804108beb404c03fb8e5f71ef0ec820", + "docs/conf.py": "bc49d86e265031fd39550513524727428685c836d73d5434a43031db4490301b", + "docs/examples/atomic_operations.rst": "c3b719cb22ddda78a4d7836d6bc7c67f56482cfeac6c8cb3823d6666f1e3b428", + "docs/examples/basic.rst": "487b5fcae6e2d98480c39bd613f17552bf95568ad1bf85168113c4872667094c", + "docs/examples/dual_axis.rst": "df17e2f8efa9bf10d1ef12cfdeb755504395b11c5fb66a22c414dd2b3a49c05b", + "docs/examples/index.rst": "0fd2ab7c22363ef78158d43928779b83b3ffbd0f4153162dd77e7e0e323993c2", + "docs/examples/ui.rst": "04d9c0c1354730c98efeb16503e3b8d31ff569ef145e09dadf60dc07991cbfe0", + "docs/index.rst": "bb8c0eb64c2e2e57ef490943cd88b0b11b1b58bd6760bcaa6b7327766a49b2bf", + "docs/make.bat": "ed525cc4d41b62b8b96d4b62a44ef32d6ccfd9324a379a82d5f71b5fb6d57776", + "docs/provenance.rst": "f7b883738fc949f3b5806dbdfe33f18715dd81dae3f98aaf68a2b8c021b5d245", + "docs/quickstart.rst": "318fd7296e2f4c3be4907cdd39b6bd89853af2132baa7df14d569b77ad7bd55f", + "docs/state_management.rst": "4a12f5b3e3438f66fa8135de44127c2feb51da0f8973cee39001af64f83a97b3", + "docs/state_propagation.rst": "b1483c16840d7ccc928b2be19e71a54166650b10d6960e173d72a116f842a203", + "docs/undo_redo.rst": "6355cd0faa82c2261645287ff1c4aebc8d85dee31c8705e698b2b0b53c0cf3ec", + "paper.bib": "4ed4113559097ad565ffa3a098e126a830e0bac39afc7798bf61d12f6b071876", + "paper.md": "f44f04ce7d7f1f037a34a89f560825a9666fb1c4c31788412cf948b2195fc41d", + "plans/n_dimensional_inheritance.md": "e55beb4463a8f0bb2d699618a44f16f3bac7f9c084154c4efc9567ebd1ac94b3", + "pyproject.toml": "a0d2835c65fe7c65b5f40259cc1d9527095bf4a3b87d1f70030df6ef9e7ef16a", + "scripts/check_ruff_added_lines.py": "e88118d5e1fb5db477cc11fb80f594ba213482614c565f3ed6383d992995c81d", + "scripts/release.py": "52abee369fee146ee8cf83fa266b5f1dbaacc31bb5934e967265054dbc7561ba", + "scripts/update_and_release.py": "80896e49f856d6a193bb1dc3917f55d2878acf1476c8240333eda16b92e8e61a", + "scripts/update_version.py": "63d0085d6b72ff34bf9560f60e4a91351ba21416b4eccd9b48aa30d40c49022a", + "scripts/verify_release_ready.py": "0a8d8fec9af999c3bba6281f2cafdadd06365090536580c8b168f43c96674dd2", + "src/objectstate/__init__.py": "6c71001be0c325b1344a9d48948c4521e1c1f7b36821741591b76271e7682b1b", + "src/objectstate/collection_containers.py": "a939d07f5dc391b7131f7ffaa7d1390b0449b4931d54ed5b09b27f59056b6e9b", + "src/objectstate/config.py": "6db2d9de077872009f1d6defe351cf8f40e441a0de31ff574b02741cefb4ffe3", + "src/objectstate/construction_binding.py": "9cfed94cad1cc9d5cf2cf0f7e8f534f72192168bdf3ba90b2b8d7ee57f4950d3", + "src/objectstate/context_manager.py": "a01b0fc8aeb9b5b0888346f725f92cbfd96b90963bf20001b5414a5e6cd6c0e6", + "src/objectstate/docs/configuration_framework.rst": "75ef312d77f009aef7b01b506797efaddb8fcd3e16b958bf7a74e752df950cb6", + "src/objectstate/docs/context_system.rst": "fe834139f63b1a92665d12ec768ec1cbf216b355dac3b12905d31081df4e0824", + "src/objectstate/docs/object_state_lifecycle.rst": "7a9b977fadc226e0fd03cc17e1ac31cb807f23b2b6794462a6f3f7ff80ceb4aa", + "src/objectstate/dual_axis_resolver.py": "17fcda62912050bdcdd018bee2fca5f81c2f4940c5f27f2af3dc19b7691e1fb0", + "src/objectstate/edit_session.py": "003cfaff8d3687b0fa115e051c3cf833a3cbb21ad9d9becfe16c59359d656350", + "src/objectstate/field_access.py": "77f483322a504c392c0ca41cc3ee6b691d258396f70f00a61b98d9f4bc03d43a", + "src/objectstate/global_config.py": "a8187811f87806579640a8147f287114e7127401530178dd37e8d28ed788a6ae", + "src/objectstate/history_migration.py": "1c24097040e38c159ea7e809424c0b94ec6c34efa3dc84cd664ff6a2f790815f", + "src/objectstate/lazy_factory.py": "f81ce722695dbab98fd02658f2015e5cb93c1e1e69547d24249e62d896e2a828", + "src/objectstate/live_context_resolver.py": "bd169b0bf2e9eea833ffc1d28331d320b2386193652fca041387f32bf018d71e", + "src/objectstate/object_state.py": "735ce860ab650b281534e24e3c7354f37b64bb06832cf186d6b1f626f87108fe", + "src/objectstate/object_state_metadata.py": "4c5be42e313e3f896f2db0d3f89ca18ca6ab09b9d25f202a7b2af9e59a099ddb", + "src/objectstate/object_state_registry.py": "f848746a9c2a023a616c37531691073ea7bdb8d28d7db3bca5c8a3dcb176107a", + "src/objectstate/parameter_owner.py": "908606ec2da70f223a93071e92ca24affb3f172f7df14c64dfd27974f1fbb85c", + "src/objectstate/parameter_structure.py": "00a786af17bca57b4e819d39c76a6523af3bec116d119c323dea8c2953c7be3d", + "src/objectstate/parametric_axes.py": "7229b518f8d5cca522a3020361c16a601f3084bc5a7d34ef92a2fef11619e40c", + "src/objectstate/placeholder.py": "4e057cf3225dd38ef3859f20fcd1305526829f044af45ef616381ea734df8f67", + "src/objectstate/reified_generics.py": "39ad0897ec45ba929c0fae694dd870df987c06e2b005644189212fe224eceab2", + "src/objectstate/snapshot_model.py": "001cc83d354b040763b2da9aad0014619fdee22c82dce622dbd93ecfba780681", + "src/objectstate/subfield_semantics.py": "97aa6a7f2659260b1950c068f47d9bccec15087ed1852f1dd182272c8230c14a", + "src/objectstate/time_travel_profile.py": "c59d157f9cb85ee29aa64e18393d916ded656c6ce88bb46782f44e2463fcd007", + "src/objectstate/token_cache.py": "d6b7cdb71c15e13e8bab7271d70e0b96ccbe0825004c45c4e88851ee30a9435f", + "src/objectstate/transaction_checkpoint.py": "4c105f2078bf23c9312c600543623b9456de256c7093b9cb8a9b753b49b67530", + "src/objectstate/ui_visibility.py": "be0c60b66b86ebb696583ee394c0d45b05fd1f68a609a6aa0c29aa1098dc2592", + "src/objectstate/value_semantics.py": "4b8a43ca8667c592b74f0d46b8507ef8331402e376230dd7cac217870b2ae2f9", + "tests/__init__.py": "b0f08e026c34c8ec04203081ad79f7ed1c6cbabd784cf475333965f279ea33ae", + "tests/conftest.py": "0fa840dddea79e86afe717e146ba7ac7ee713b032e0e398b0a9f56842e4a1d62", + "tests/test_callable_baseline.py": "638929e6eb97a7c899443272e307beaa62a12922debdc7594a45ec0a85da3356", + "tests/test_config.py": "973b08f470fd54a4990cf39c7f176b9f5d02ed0b808dbf8ea2549a01d7b30d77", + "tests/test_construction_history.py": "88469c8157d4010b9877f76b7a605bb7222222d614e1ea22aeec0c39870d1609", + "tests/test_context_manager.py": "c14d3ce760520544cda14273e76df66606e47ae1bba410f83746b216b4a6418c", + "tests/test_field_access.py": "7484f7b0127e1be332ac013ea5f5c61ec5da9f2cd7fef9fcde8a8e8ab1384c54", + "tests/test_global_config.py": "f4624ffd22cf4d09bc05466dac91cf952e81033b34ab475bd1442c3d96887833", + "tests/test_history_persistence.py": "547d0cba078c2caec8b6217ec0b8713aac1ac2efd69d143e6549b023e5cd4d33", + "tests/test_integration.py": "df082bcca0ee4d6cf1c3899b4e43ccd2a004891446a05480bcdb1930ed467c24", + "tests/test_lazy_factory.py": "b3f1602d85a6e91a1b4e42490dd5253d2d0c43371a193d7369fe149bc839ceb4", + "tests/test_object_state_metadata_contracts.py": "dbd555d1659a1dd79c8c93c1d5168282a8cfaa3d727ac3fcc4d7d913eea00360", + "tests/test_object_state_restore.py": "3319b7244e0be237896902955f4c49829d4135c057116885935791bd8f125a81", + "tests/test_object_state_update_instance.py": "aef1aa303a2f3d68246f87a1a6871c0e0e48cc12d5574ef105be5aa3202a7526", + "tests/test_parameter_owner.py": "b28675a55ab72d8fdf011f972e3c5154fc683fc8206a7195342ac0a774bd873d", + "tests/test_parametric_axes.py": "8717ac074d559294e00a3d33afaac6415230efcce78f38b1e296a11c9596dcce", + "tests/test_placeholder.py": "1b9d20e8931234622ef1cf726d12e8e4fa00328c0b1cb33e9eb0fdf7acc1631d", + "tests/test_registry_subscription.py": "c634b5e416a7ea3ca04abcab65679c2852595c595b6e23cde6424ba9f6c7643d", + "tests/test_reified_generics.py": "548f5b6a282fc26b73cb5211fc8a9eb92f37fe9cc57e37cf964597293fe98866", + "tests/test_resolved_change_notifications.py": "d0addec005be985da187845efba11e1dd03367b5b9557cc1fcc54a119e42c66a", + "tests/test_subfield_semantics.py": "f156d9666a909abe9c9dbb8f69823b1e88edab310248687c4e012ab97e5a43ec", + "tests/test_timeline_projection.py": "e02529d96f61f3601c91fe30e133c4f78107d3ea6f65c864198b232b8ad76780", + "tests/test_transaction_checkpoint.py": "531c4df6b3fa25a28ac17d020532179877d4f53d66f57af1bff7beb07d08f89d", + "tests/test_ui_visible_field_projection.py": "27e016e8755374e9322460b83344836f9a3f1f0e54e9303c4b16e6abf775baea", + "tests/test_undo_redo_cascade.py": "5f8cd61f70b966ac506b5a2ba4460d58c789137f7f992dd7537d572efdcefc67", + "tests/test_value_semantics.py": "fb98e8637abe45a981faae3b61fcc9ddc14ef84b4ef5e41b95fa5736bdae2968" + } + }, + "external/python-introspect": { + "head": "4e8f8bde5f750d9a50eacac80acd3806123addd8", + "status": "", + "files": { + ".github/actions/validate-python-release/action.yml": "ba8808c30bfcf27ce6f9dcbbf12e60af274f4d309c8e409eb3cd93ab2809436b", + ".github/actions/validate-python-release/validate_python_release.py": "96275b643ee03adac9a13996006cca803ec07a0f0d0835d47f559677af016fbc", + ".github/workflows/ci.yml": "da7bc66a370447d0dfcfb7de4ff723fe92fa3f6b34219066bee30efc3fbfb6a1", + ".github/workflows/docs.yml": "62187d940fb88d0ff42ba69a2aa1127462b04424441a039097fe956934fd0293", + ".github/workflows/publish.yml": "3dbae1430de7372a6638d339d955bc036d64b88c968edcf96ea578afbd4a4d17", + ".gitignore": "6ec409ab221e797f3f6d5e63ad9dd8e7bf09913976f8dc8a0268ecbd3cec280d", + ".readthedocs.yaml": "c95cbaad19adbbe07f93650fdb705f22629d20dfb51296e6ed566f280abfe6c6", + "CONTRIBUTING.md": "73bff7a6b106765f0e3a2f9df7e9c4da7b6243f0a9522d737ca3ed33ef16496d", + "LICENSE": "c5a804a1e4c08f5593eb8466c91dc4e871fe79319d81737a82a3cc514b7b2ff6", + "README.md": "d861f8aff2e79d9e77fbb2286b2f353c5b568412482a57efce9a488e58d543fe", + "docs/.gitignore": "7f1d49f8fa7578f81fec5b86469e14d2cf3a94ae0d85abe6bf5373f5f760ac42", + "docs/Makefile": "e33bf74d59130029ba24168a813112dd923d7044d780ce942db6645d93f56acf", + "docs/make.bat": "d2a8ebcce0de81abca77b2cbc26afd00385a61fb624e7060a8e5402fd900a72a", + "docs/source/_static/.gitkeep": "bacb37455f4ed8710d244dbe230509e632f06da53df6f849443d1deb2315dbff", + "docs/source/_templates/.gitkeep": "b32922d255e14ebf318fa34bf4519185b2498b279b89cac175150eea8e9ba76c", + "docs/source/api.rst": "f2743f0d134d71d8799d0ba4d995bf2b378234da40aa9a5008fa78b5cd2d379d", + "docs/source/conf.py": "534c6205463a628d4d7d58b2793d95cffea83a6ec2c0085b012a29b046c398ea", + "docs/source/development.rst": "fb7c848d6d5b1ee40acf259a306a20f7380c4e6928e76df938978b565e6d416c", + "docs/source/extensions.rst": "cd50883eb7b31ac94ea55b1dc662a637e844626c1950cdfc2c0cd211e776ac0a", + "docs/source/index.rst": "c98c4bcc2623fb87c1b4eb0858819ed11efa3618fd0fa56e8df3dccba415d6a9", + "pyproject.toml": "3dceb0255a1cce6f92d0ddc4d1c1e5a52de72e4594cccc73df77ce5db273e1e3", + "scripts/release.py": "f5ba54de5d2ef89ec7c75e536b63817989b8f9f17e3b2c10bb981b5bf8d08837", + "scripts/update_and_release.py": "80896e49f856d6a193bb1dc3917f55d2878acf1476c8240333eda16b92e8e61a", + "scripts/update_version.py": "303a09dca1b7374ed1d8e4aa26d76ec1bc76cfb6a769e002f2088927d51a6aa5", + "scripts/verify_release_ready.py": "5fe0aebbcaee7f60540f50221a1f8da1beab2b8b59d9b97f3b4a240f0e72211f", + "src/python_introspect/__init__.py": "5e4938dfcdfbda603c0b90a21b9a0039ebae0a59dd8e44d51059aa28c3a74e3d", + "src/python_introspect/annotation_types.py": "dc126879a8a581cea0758eec4df18ec20c47ca4b1e135abde23699b65b790bde", + "src/python_introspect/callable_declaration.py": "3bb8b474624fe6b2964a5c1f9a6aa540b2703500bfa124e7808995cb789d6951", + "src/python_introspect/dataclass_projection.py": "82662ff8eaa4f2aeb3b33edd61abae57020a7f139c3aa396d82dcecf8b801160", + "src/python_introspect/docstring_annotations.py": "444e8064f833f1a044208bd4a15107a6b688b597e36ab78ac58bee6996b51fc9", + "src/python_introspect/enableable.py": "025eadd64a6a6f1648bc36fbd1724f6049355a0b9b1a259443248d70170b8eb6", + "src/python_introspect/environment_projection.py": "8ab7d1e01e9a4529ea0b77d8996a5feeb94bdc2e905dd6263afb28d11bc217a0", + "src/python_introspect/exceptions.py": "4c6f15a3625d02b05b2fc8651e67e6b77d4bb442ad180b85121d91874ec3407b", + "src/python_introspect/runtime_parameter.py": "59fed2d467e751686447322f8e19a8633d964741d3d8afccb850dc504a619897", + "src/python_introspect/signature_analyzer.py": "51f4729861062ab857153c57d64b01bd7b0f47daa6f18fc4443819fb75c77b45", + "src/python_introspect/unified_parameter_analyzer.py": "ac52ab997fb385425920614451e8cc754fece0176bab4cc9b132eae8c930ab15", + "src/python_introspect/validation.py": "46fba78bd068c75c89b91baa4a5cbe8cdca9692e75a285eaf76311183df77b64", + "tests/__init__.py": "c752ad382f759406b1c9c102142aac7cc10a2693b8e5848536e44315567a034f", + "tests/test_annotation_types.py": "ae38706899ad48445224b09babff76496006576e7f188af7e27261521ea34ad9", + "tests/test_callable_declaration.py": "b8486dcca0c85805a14975c00987c7c3247c2c8abcb75545f89e04a01f738c32", + "tests/test_dataclass_projection.py": "6f662d0adcdd4531d8cbd94c2ffed55ced3a53c3ec481a9dce67979ac8489990", + "tests/test_environment_projection.py": "e11e2c180c605e761acd48103fcba10dada614a326e40a55d4777abcf0853899", + "tests/test_exceptions.py": "41f4a70df08a7718b4c9daa05569e6c491c0148abf975f0aeae78688e99d8744", + "tests/test_init.py": "7e9ca130ed8254034e32b991f8abd4624f6c1bfcdba65f911dc28c12d9cdfeca", + "tests/test_runtime_parameter.py": "b728307c8e6c7e50ca9b0befd1aaf154ad293eb18f332be544719d7ae9dd3fb4", + "tests/test_signature_analyzer.py": "0708fdcb848fe874eb4dae025c1c9fd9b0929e983cb1c1c53cb268af9fb37c2e", + "tests/test_unified_parameter_analyzer.py": "deb55bac9e31465c8368c6101a93e3c951e139c5c7ebd442a5f8cdc98842f1cd", + "tests/test_validate_python_release_action.py": "65fc7bdff5d20229e74e3abd44f936d7cfcaf9fa5881a7e9e65c59bbd65c6c12", + "tests/test_validation.py": "ce9cab31e33a001df0d40454a8d7de9e7a2ab6dc6296399adb234d06f7dfae39" + } + }, + "external/arraybridge": { + "head": "1e53d03d9f468322a1c085c8be29485fb139caf2", + "status": "", + "files": { + ".github/workflows/ci.yml": "cadf2e9e7f8378357df650beb1a99f89c1ce9404aa297e8d707bff14c9a3f60f", + ".github/workflows/coverage-pages.yml": "4d6692efebe182beb62829c1bf3c19616159316b649b84cf5ba3d30d2a44599f", + ".github/workflows/gpu-tests.yml": "ab3b757bfb18d9b508347ca1be122ef2b084cad5e9baec8bd99c73ca8ac44ec3", + ".github/workflows/publish.yml": "98eafc9d3a4250b4fead850300eebd9e63dda05f162614ac28fa757a7d2996ee", + ".gitignore": "a26875c24503b80c1699f99988e68bb14ef3f4834d476864139553a63be5148a", + ".readthedocs.yml": "64f56fbb4a8a1ec0124b2704c97ac75ca57dd9d75922f0f32122ccc07d9d5dec", + "ADDING_NEW_FRAMEWORKS.md": "cc7f1899e7c7fb553acfd5f86e468689c367ec9eceb334d74f5d9a9c1845fb20", + "CONTRIBUTING.md": "e8e3288c0eda555fc5a55f93c3de958951bb5d339ed1e88407be837d5b392405", + "LICENSE": "c5a804a1e4c08f5593eb8466c91dc4e871fe79319d81737a82a3cc514b7b2ff6", + "README.md": "b108a144e2f797425a1734f745a73ac7a215bd5e58b3f6fe4342b18e7d16b892", + "TESTING.md": "04f1c2f01407bf49f4144301b567e766f920bac8c90ef7361d17a94f87935292", + "docs/Makefile": "e33bf74d59130029ba24168a813112dd923d7044d780ce942db6645d93f56acf", + "docs/archive/CI_ARTIFACT_UPDATE.md": "46a466f6398118a214279f30115db82d22f20847de0afbba6947f3a807d2fe18", + "docs/archive/CI_CUPY_FIX.md": "2c271a926da57ebe1914d8784e03dde43c372ee086e69981513962dcdfc977ab", + "docs/archive/CI_IMPROVEMENTS.md": "d52addf07d29e00a4a145e164c01d2499f0d063c4d28913b361c259187e083b4", + "docs/archive/CONFLICT_VISUALIZATION.md": "5ad9a1b04c6d1271506c3a94b6e7df36c80f146014425094ae3fb9e2f649a7f8", + "docs/archive/COVERAGE_AUDIT_PLAN.md": "3c54a04e3a7f0149f7e0d2a77fe8e734fe7b85593c6542851ed1d0934aee710a", + "docs/archive/DEPLOYMENT_SUMMARY.md": "40c0e2337792f659e92d4391ff8a7bf5f5b7b8b46f3379c8792d2e9055127060", + "docs/archive/GPU_TESTING_SETUP.md": "b9b94a8b40705767eb1d192ac6c140975aa624d541352847e3cb7a4734f22efd", + "docs/archive/MERGE_CONFLICT_RESOLUTION.md": "a0a3a028c5583b8eb4903ed206946c0b712b4342371f0aa10d0b6185e2726fa2", + "docs/archive/MIGRATION_SUMMARY.md": "22c0cc300f73972dafa606cb69a28025a2e0e73490ae35eb65dd5f45deb5d998", + "docs/archive/QUICK_FIX_GUIDE.txt": "44bbfe657f6f178cc3c148ede870ae2c4b3c756179b13f532d97132c45bd5307", + "docs/archive/README.md": "a28d7cde396af47e9bd985d54e517643b6f789410aa02ddaf878bd927a9714e9", + "docs/archive/RESOLVED_FILES.md": "fb72be1ebb40adddaff772ef4b3520eb578b3d028e8fb2bd526eaefe6a897e81", + "docs/ci-cd.md": "94bedfc598f0dc498e22e1a0d46e5399ed169595eefbcb95242ffc5d00a7be77", + "docs/source/.gitignore": "0485bfc859ca5866baa1043a32e732b18fa606ced2fcf39c27a7e7d3c89695c9", + "docs/source/Makefile": "8b6587b859607f200f116e2cb043fc358e1c3a26c326b563bf348453cfc68307", + "docs/source/README.md": "537352768576aa21efe985e7abbe266e43b1000298ae74097625173fb99588df", + "docs/source/advanced_topics.rst": "97b6134aba0c54f2920cf0b2978c09c092e8baaef09a044a4bc2b54df3acb3f8", + "docs/source/api_reference.rst": "9e337146f1fd22efe834f9739eab4135775c47bb1e3b01b57b33d4f3ec055072", + "docs/source/ci-cd.rst": "22e0ad2df054c693ee570833132daeca77b14ae494b8be2ba92fa1516ca4b3c2", + "docs/source/conf.py": "1b43c862dc93e51e93c0ef0435a9e5cf208665d84a2d0ed1c2477d5e4dbf3832", + "docs/source/contributing.rst": "b71e6e678251a155fa14980d64ae833b4c028bbb089b6b4d44b7baf280909190", + "docs/source/converters.rst": "b658d186779669a7eb367dcb60963fa57995c26ae63fbaad4a5c5bb75d10eb97", + "docs/source/decorators.rst": "3ff68560435fd1e0c1d5b3ee883f06f5bb3e6bb207f96e915556a662e4e461fb", + "docs/source/examples/basic_conversion.rst": "be11e27873b61fedfe0abd5f421817f683d99b731b5dbeb913aaee60dba5f08d", + "docs/source/examples/decorators.rst": "c6b0fadc2c3d36e6dc28e7fb50024c685df4f1e9e86a18be909c9cdfb00cad9b", + "docs/source/examples/index.rst": "eabe217c250d3301bf26ba2a6ae58fa4eff82c85c12e2e60e6ced6b533e1f295", + "docs/source/examples/multi_framework.rst": "1d7534ceeb611861f69aa7a0cc55c001b5f7949298192dc5e02f68fea5838c13", + "docs/source/gpu_features.rst": "8ab989045317cf0bc7ddac077caa93d6d7fe2a6f90e205462ae09ef03e605843", + "docs/source/index.rst": "963d5249c33b2ba4103c95fbf2c65038f82a883e561e9de7db825709bde3205a", + "docs/source/installation.rst": "89b6309bde9a0b72b4e6477e2b3f99efcbdce947374149c2dec92df7d8ccc003", + "docs/source/quickstart.rst": "99324fb002fb28a08700015b54083c97a6505f733b1329504ed302b1f003ee72", + "docs/source/release_035_validation.rst": "4afb3f121805a2a63e912fd85197b76adac82d67b6d070fa73908e38c577fa3f", + "docs/source/release_036_validation.rst": "d9f902b370403e824c8047fe1cf14aae36e3ae26de50c1aebdce2783c8cc2f12", + "docs/source/stack_utils.rst": "e3203ed0755ac5714674f9db908c3fa990205e7161baacae1f97a31354fefb3e", + "docs/source/user_guide.rst": "50e205f365a839ed956103fc93f59a546dabe3a963ee5ba2285e75ded021516a", + "paper.bib": "20228af15370805fe07f53154b27a977e7bd32ae84cc44bfdf127a696c5ca0b0", + "paper.md": "363b3be3c60859de0bed4ef7db739e6e41b9345284c5c28e0b1f3b6fca61844e", + "pyproject.toml": "8134c94077df2defe89328963c250e6defaa2ec2b5f49c190f0966e9e575661b", + "scripts/release.py": "3e9caa24fbfde769e347f52542a557610bb02249b81aaa71649bd579d081b69d", + "scripts/update_version.py": "9d57e7324469c34691c4561466cc4d3330485812ce1c14418d8034d30bd85a3c", + "scripts/verify_release_ready.py": "bff7cb1232eeceb6d6d235309cfc75866c7b6c1f44c48f5b2ab65f856830477e", + "src/arraybridge/__init__.py": "ae1b2dd87b91b191356e9e1fdb74a32ba7131cda6341402c4a7aa087d47a4140", + "src/arraybridge/array_geometry.py": "0a8a5f1a61552b057997e94e4c89a19b5615875f307949e0d11784179f2a94f2", + "src/arraybridge/array_operations.py": "7e9ebda3d058702cdf8662dc5ab1faf1254649f3cbde76fa0adcbe02677fa103", + "src/arraybridge/array_payload.py": "ebf8c9c0f4726b4307ad435203d9173e54c84a92dad06ef8aea0d56a51972b1e", + "src/arraybridge/converters.py": "ab8e04e8f835ac9cfccaae964083cb3993efd65aaa7147041b81a1e03e4cd7fa", + "src/arraybridge/converters_registry.py": "fe11ed6ddfb748c20a530f1fbcdbb7b703d6258eea1f237243f361569dde4874", + "src/arraybridge/decorators.py": "28f9bf4770bd4cc1ef34045fdef28065063ede029296f62b8a6c4140cc88e40e", + "src/arraybridge/dtype_scaling.py": "283818c82bf182c4172c9265fdbb399c7e3acd08a6b00edcd4e96feb1276cf5a", + "src/arraybridge/exceptions.py": "7e17a3e594b7f50701156ccd78d623a2a78b5673a0c30a7e7162267b6e63caea", + "src/arraybridge/framework_config.py": "e724d3b7d356730cca0cc5db73e7ea09d88597ea6228eb774949f19915d70909", + "src/arraybridge/framework_ops.py": "eac427f155ed3e7e10ae0f9cc603a9a17d7c3f54b30db339e23f33873c94fff4", + "src/arraybridge/gpu_cleanup.py": "003238c0c2e4f4620ef6b27a049b8d937b9923200da00a89ed51539cb273adb1", + "src/arraybridge/oom_recovery.py": "846bceb891d54a8a6222a6e5ce830596a5bcfe82ba66500617f978a5f1258de2", + "src/arraybridge/slice_processing.py": "e51d743a61d6f0149b245c13d5445dd92cf73f5e68e4485a236567554508be44", + "src/arraybridge/stack_utils.py": "1558a43745c6f97af2c3ed69f4c6924544d97adee42dccb48131044528dcf6be", + "src/arraybridge/types.py": "7b94760d57e54008d7adf4e4d37dca45d8a2c288a9dc8400be00c26148cf7624", + "src/arraybridge/utils.py": "46a4e1b5849c3c4384406e3dd5d03c6ff56f5138e07058702db8747ab42ee4db", + "tests/__init__.py": "a0ac3b43f372047f4b85b516a7e4471f98744cf549dbeec484ff3f79728f0257", + "tests/conftest.py": "6a2fe5d7206ac30e37e544ba8ff939ffa4576565b9c7559fd660f0e41db6f24c", + "tests/test_array_geometry.py": "f9c0e91d6275a940399418f2c0de810ca90c781c30219d720b745f1cf912f0a3", + "tests/test_callable_dtype_default.py": "52f343317d9d943bee46edf4edfa7e1dfd9afbe2e2ced2a297b9bc764bacedc7", + "tests/test_converters.py": "390edeb089747cc13a6378d715996f238ece1610f7aa4e9e0c3a6c932a23e315", + "tests/test_converters_registry.py": "f40974f61e55cc49fc31dd53af0dda1790301edc2218d5ec6dbcbda0c712365f", + "tests/test_decorators.py": "0b841b293e82b59133c895159c9773552dc4e7a508bbe7976da45aaa1e5d894d", + "tests/test_dtype_scaling.py": "7fcea155eede82781b9566166079a86e403afa075c5335118696a390c1ed531b", + "tests/test_durable_decorator_context.py": "53e4bfb9a2491b6d5a238ca4aaf24ffce5853346d46621e25ebf92adfdf70642", + "tests/test_exceptions.py": "441f10606ee9175814b01a243f8aea572f341bb53517c31c2c719ce41a36fe27", + "tests/test_framework_config.py": "4995b8649bef8ff4bfbf80dae282a6c8e008b0120f1a905f118903fa903e8d8c", + "tests/test_gpu_cleanup.py": "bcd6ef4f4f56850e1f12eaf9fc237d756b06841bd1c2d093ebbd81cf1a04a0bf", + "tests/test_integration.py": "e8d87ba83d8fec2ea7a9841c413277516bf665aa08f688b29da819a4c545edd2", + "tests/test_oom_recovery.py": "773b96e901e85663b6418d4deefbb28bb3ac9298b7b20589373cfa0de05e2bf4", + "tests/test_registry_integration.py": "49b88b4f692af569b66c298c8751a78b0e28a08f4db41034ef05684352df5418", + "tests/test_slice_processing.py": "b7f996dd6c38382148c1b48c5428d33775c3067f91562f082de9ec0ce20ebb5f", + "tests/test_stack_utils.py": "918e015e452b31adef93ceb829e949043390d22888daff9a295317940a58a6bd", + "tests/test_types.py": "ae1701b48c68c6f07bb23d83800ef3ca049545c254840ad5a7863db4213b3c16", + "tests/test_utils.py": "15a78a8269843d90b8dee4a2b4a50cd1b34bc791fcd1588260d45cae3956aab5" + } + }, + "external/metaclass-registry": { + "head": "393a7e03003cdc56df9013f932ed4f26e632d77a", + "status": "", + "files": { + ".github/workflows/ci.yml": "036ed258494f0a2203882461397a9a2bc92f584b6006dbacaa77c2eb5beace14", + ".github/workflows/coverage-pages.yml": "410fdf83d1a1456975635cb4ae09dbaffaad8fb7ac8597c98268377b4c4c7896", + ".github/workflows/publish.yml": "85a7866717cc0a1285e74f751571d7d0b97b1210137b83585c66ed115619e600", + ".gitignore": "a26875c24503b80c1699f99988e68bb14ef3f4834d476864139553a63be5148a", + ".readthedocs.yml": "7506f658eb3b8583c0887684c3290910652325e33ab20f2041373feb7a89af19", + "CHANGELOG.md": "296f2431172f457518be6ad3789ed7d0ded103af4718937123269537b7e6bf6e", + "LICENSE": "c5a804a1e4c08f5593eb8466c91dc4e871fe79319d81737a82a3cc514b7b2ff6", + "PYPI_RELEASE_SETUP.md": "578df82d6426379a517799ed2edbf93298ecd6d848e39c7016514744df565de8", + "README.md": "668dab2cc7c536778ca0010f957c85335eda5eb2392e60c62d08b11d79009c37", + "RELEASE-SELECTED-DISCOVERY-022.rst": "2fa12717c0650cdccd83878b98b1f70a61ef94520acbc1cb7ca64c5e1c561c0f", + "SELECTED-DISCOVERY-376.rst": "22f0b41eeb3aa86183f8e2e83e225bff5ee8072aae39405dbcf9ff2bef07881c", + "docs/Makefile": "dd53c72b2b1c989f748fb2af7d719d8f351e757ae26bdf4f657b763bfe1ec4d9", + "docs/make.bat": "d2a8ebcce0de81abca77b2cbc26afd00385a61fb624e7060a8e5402fd900a72a", + "docs/source/api.rst": "d2da0de8358b7fb3fe1376358bffbf10cf39c827edae150a176b75a91490de52", + "docs/source/conf.py": "1d1618299bdaed818c3518ccc11dcd99f7277e4574b78a1db345c85fa2d949dd", + "docs/source/examples.rst": "fab494bfcff26a6aa84f7e3203a4f131832cf001b60fd4e87fb1ad24ce78ae24", + "docs/source/index.rst": "f707777948f83a5bba22528814b43520e61c76da730d0b93e0c5f3f91d6c5b12", + "docs/source/patterns.rst": "cb67e1b458cb073d226ad6691f4ed107bd35b887ad65452157a7283009707402", + "docs/source/quickstart.rst": "785bfb1e3d212247c730b79b756ba894af84d2dd4d61c43e9850c1ce79efdccd", + "docs/validation/issue251_cache_projection_20260930.md": "a740a1c5bd743564f795ec8563770788e78d719c221cafaf274d33987097c00c", + "paper.bib": "10b564cf153c1168960a822ff71950b16a16853f026a31838b23394d08d1cfee", + "paper.md": "d3d47e17ff3699034e9dfacaf7e0e2e18d3770c631048dc335e34e3d2fdc26d8", + "pyproject.toml": "2bc5a1a364bb17b03a4220cc3595d7889bfad3f731de3fe78453b16908b291c2", + "scripts/release.py": "19344fc3616a3d63e035df687ac82e8a040d0f7b69d5fb934c6e801df0ed5947", + "scripts/update_and_release.py": "80896e49f856d6a193bb1dc3917f55d2878acf1476c8240333eda16b92e8e61a", + "scripts/update_version.py": "f15d84a33dce9188e36576b21a84b7423dc868fc5e7eea22f8528dca83c3a5ba", + "scripts/verify_release_ready.py": "8747b1de6233c106025abe5fc4cfbbdf2a8d07641566445f4151b6fe4b5a9e1f", + "src/metaclass_registry/__init__.py": "b7dd8d905bcac00f490a6155df09e68d148f3e7b8238fed2854f2e1b5de06567", + "src/metaclass_registry/_home.py": "6949afb54c145a8c58f680c61177e3aa7a04e9b432b73552f5beb4b1b92169e0", + "src/metaclass_registry/cache.py": "10ba153bbbd6a45388a47aa28f5b18ab77847a33acec4820d31cf909153fb6ca", + "src/metaclass_registry/core.py": "5fbfc22dee24ac7cf653209ca06ca511ddedfb2c9e96a5354d7d4d4600828493", + "src/metaclass_registry/discovery.py": "757958272d11633e7b854c7104c07f0d90845c37fee6ab0c3646fb14c2ed8daa", + "src/metaclass_registry/exceptions.py": "333ae9a5b87cb884213a1833fde35c178734f5e684a8f772d6c0e35a090ec987", + "tests/__init__.py": "dadf7fb5f53a6c573dffaecd51460f3d36c83fd4ed903a5ed7ed21da331dd9d3", + "tests/conftest.py": "a2ad838c1e90bcfa8c368c51f07f473a85cbe4a95d27134349f94bfb1a57f767", + "tests/test_cache.py": "c0d077f47be112b257abf7ad574ca2bb4dbb7e7f3fbbe0c8044b69b69bf5c7cb", + "tests/test_cache_path_projection.py": "8da2d89cb7e88bc838b0a0d59068afe4a6ffc0010e3d4fcc196f55519d730ced", + "tests/test_core.py": "26446d33fcdd012ecf12ff7e541ce434aa38fb8f1fb110ee4ce4c74cbf0bea00", + "tests/test_discovery.py": "f28535b526c4fe95d68a33386c4378237468fb160867dd9b07a55de920939d3f", + "tests/test_exceptions.py": "11dd2d86ec4baea6834b28d9f368bb71c90dd91f23a12dcf795fb821a29ebf1b", + "tests/test_selected_discovery.py": "b20e2ff2441a37fc2700223e18cde1d64d1eba314521a656d8be2376da2c0b0a" + } + }, + "external/zmqruntime": { + "head": "668edafcb5ee530a163377c936431ae1e8334e85", + "status": "", + "files": { + ".github/dependabot.yml": "34790e08d06d30f3f06cf2f071fcb2430df398c0a70bf1d99b5cca71d69080ef", + ".github/workflows/publish.yml": "e0a6fd7824dfe9d4f8cd6e634038a940ee2ca78144535a64d40b44018328db81", + ".github/workflows/tests.yml": "f9b74f8a33d2b47746f7bec36c3b74979640e95d23d6da2849bc5c34a8ba7fa4", + ".gitignore": "80a49a462dd53e5509406a5305931664d212012c4cbf7ef7c48505527f632b15", + ".readthedocs.yaml": "8dd778d3217f13c531400542c6aa1e2ca620c84e68f73bbedd3bcffad5c9de33", + "ACK-ISSUE10-DRAFT-PR-BODY.md": "6e308ff02b10ab06fa58c209b1d6bfbb13191946ab1412cec644af26ed869a98", + "ACK-ISSUE10-OWNERSHIP-RECEIPT.md": "92228b50c7ca5e2d620d51f00ae46c464f3bfdc19d37cc1a7b7b8017adb74056", + "ACK-RETURN-ROUTE-DELIVERY-20260930.rst": "1a45185666390e9cf25032aa020fdeefb9988a1f7f47ff0a1fe82128e20e2454", + "LICENSE": "c5a804a1e4c08f5593eb8466c91dc4e871fe79319d81737a82a3cc514b7b2ff6", + "README.md": "c8f7fa1b36636d6cbcee78bf259f2e2511ef7e19a5533ae246548167e35b3eb9", + "docs/source/api/endpoint_handshake.rst": "b823c73359e3b140c24de84aacd567600381e726ebe3cdadfdad5bfe419a5980", + "docs/source/api/index.rst": "a598b60df852571f5d42508d9bd4025f07ca5d8cb1cf72b08910183b590c0239", + "docs/source/api/viewer_protocol.rst": "6d5d4d3abf96c69161c94a0127597b79fd0d26b6cdae5a1e0be258bbed549396", + "docs/source/architecture/execution_batch_submit_wait.rst": "f9e276c4057656c7c8ab2f594f76b04f9d25c0c1b0beeeaf103fcd86aa43e241", + "docs/source/architecture/execution_lifecycle_engine.rst": "27fb0eb4c639212c7f959ad4bdac87db55395a183163ffce713fe9c41fe58373", + "docs/source/architecture/execution_progress_stream.rst": "777bf4da41ca2c235b63a2d774a94cce1c35a03c3b87b2c501afc082a56f86ef", + "docs/source/architecture/execution_status_poller.rst": "7e801869b0b762325f889b76600eb06fa4eb04f7d80eb8acf773a9ab90308ced", + "docs/source/architecture/execution_wait_policy.rst": "acd5e60a3b294103f2b0b67e3922e49f54fd2015f360ae75ac37252ec1f1d8b3", + "docs/source/architecture/image_acknowledgment_system.rst": "650231482b3e7f38108b37a017612bc1fefa3aefddc5aaf1753d447134cc9341", + "docs/source/architecture/index.rst": "cfad2756cb9b78f0aa51b848181a113964ea547123671d4418f5fd56d2df6e37", + "docs/source/architecture/progress_registry_projection.rst": "98caca592c9f90121f14e4d71b7b756b017346303b8ca7b5d836bd3d7f19567f", + "docs/source/architecture/viewer_state_management.rst": "fe40e1beb8cfbb4c129e8a7bf5a6a2ad3d1e6e48b8df3e787a62bd2738c6c920", + "docs/source/architecture/viewer_streaming_architecture.rst": "3a380ec7fb52129c0013cfc6619c5b8686ae3531a8ab15215de04920923a6c84", + "docs/source/architecture/zmq_execution_system.rst": "6417a400634ed9aeed0f968cea0db98787304f8ab8387148ff77c8afa7f0d629", + "docs/source/conf.py": "e2224f5e41f473e9a9a213dfff036859c0834eee3561a9d6ce5415bc4487d126", + "docs/source/index.rst": "9ecc16d64526fb5799a40befecaaab6a3f55d41c5f901fed5c6a28084734a4a3", + "docs/source/installation.rst": "a3431edfd6ac76cf4b820efbdfd7a534d6e41258fb93c7d78ef290993fac8c40", + "docs/validation/issue10_ack_return_route_followup_20260930.md": "201ae9cb42395b0f774eb8bb56cf43faeb5f21106bb892320058a85874f4321b", + "docs/validation/issue10_journey.txt": "83f34a9f9ab21c1218ea1845908c1c25c705347b546958aa1ae631e1b2c9af06", + "docs/validation/issue10_journey.xml": "616ee69fb92d921ceb097bfaf7f1743b8f3385257b4afe8503e800dd43c8cf1a", + "docs/validation/issue10_native_final.txt": "db5ec543ed1f88465cd5c2d244f66d0e4199054bf65e24b5d566847f86843f24", + "docs/validation/issue10_native_final.xml": "0698e4d64d7eef028c190db21caa989bed789440b7588ce820ca804834c171aa", + "docs/validation/issue10_native_focused.txt": "44b9e7c9feb834f5c3c072184a13c8c2ff8e1cc791f42d60d74c631461212bd7", + "docs/validation/issue10_native_focused.xml": "94d03b8bfa3ee6b9ddebc9c47b6ee5ccc35871acc2a4561e16fe19d0d7593a5a", + "docs/validation/issue10_native_source.txt": "ad612a11edf1b6ebf11e0e0314cddca80566fa85d24b9fe56972ffaaf1f6775a", + "docs/validation/issue10_nra_loop_final.json": "dc6ffc60c762c658ba45db2137f1ae558ce6968cc6f628483fb259f9ff49155a", + "docs/validation/issue10_nra_loop_final.resources": "a2cf28d114cb5681018f5ff1bf36c333680479e3d80cd39a6709b1b996b82b1c", + "docs/validation/issue10_nra_source_20260930.json": "8d7f4f473df884ab1507c183e63268e552cb32f1d12096aa5bf9919c46a5dd67", + "docs/validation/issue10_nra_source_20260930.resources": "be2a0d372a464c2dc9ecf1dfb1aac25c5df38e50cfaf4d20d07a5a6c19c97478", + "docs/validation/issue10_nra_source_final.json": "775676eee04a81a811c04b2cdf74b19b36b9d9c46b813d3609c3009b63745abb", + "docs/validation/issue10_nra_source_final.resources": "5a5755bb6b22bc47592ab49d7c7a19e015510ee92294384636787d90fdd360e0", + "docs/validation/issue10_openhcs_final.txt": "e85a7720cfa71b1b84b6165d937fef22500bbe22b87db120bf76efbcda5d1a19", + "docs/validation/issue10_openhcs_final.xml": "9768040d37aed08c074e9a9b8b3da0ce3b0eef710d0ff34af5620ecd2637a4d2", + "docs/validation/issue10_openhcs_source.txt": "0671a73d20ec35588becac9898e56d16ad2ad28a76dcd94a984e336cc6adc484", + "docs/validation/issue10_openhcs_source.xml": "7da50332d3af51720f57ceae85562d750fd12e65ee62ea4196b4769cdd2433f9", + "docs/validation/issue10_polystore_final.txt": "6732430df17ceb02f4e66b69f507c7ac7cb71911a53cab4b8059175c98011d38", + "docs/validation/issue10_polystore_final.xml": "da2a6460c9a0140a6231fe786b88826cc601f674c032e570e0b5d608d38482fb", + "docs/validation/issue10_polystore_handler_import_failure.txt": "f8def2482b6d9dc99917cc00ed5c5be9b53855e0cd114821433842f229e254e3", + "docs/validation/issue10_polystore_handler_import_failure.xml": "39b00fdfbf4453c19ece38e8bc25fd270e8dd0b6d9f4c5cf5c589161e4fee6ea", + "docs/validation/issue10_ratchet_native.json": "2e8b7069023283406e8f9a66be26482eeed155c64a99dddf2d99785fcb5a1c9b", + "docs/validation/issue10_ratchet_native_final.json": "926801db2596332cc51cc84873d15bb729d57cc328a2717ae9f62013b2811352", + "docs/validation/issue10_ratchet_openhcs.json": "b4d1d3ba15de5b124227a52ccbfc9b5078ba27c7dc522fc6c2c74a32a9c72b7b", + "docs/validation/issue10_ratchet_openhcs_final.json": "4f1cfb30e7c16f4bb14758a29257164499c38f7f7264949ce4f13c0d7128e714", + "docs/validation/issue10_ratchet_polystore.json": "8cfbfc52f4caf1d169ef89007826a5f455492bd21ee795d7de5650041c71454b", + "docs/validation/issue10_ratchet_polystore_final.json": "243c642d60b78563c0d5e88f13ca3dc7dd41f82198d7105edede2977346fbf7e", + "docs/validation/issue10_startup_diagnostic.txt": "88d2d47c1b3c572a459808fd3f65f03c8d7b6e95b196fbf2ded75f8ce044564a", + "docs/validation/issue243_progress_annotation_20260929.md": "3fca37d4eed0f9e6e61013b4ead34790cf2720bbbb22ab859e26a7fc7c8ae956", + "docs/validation/issue243_progress_annotation_20260929/after-pytest.txt": "e98090351cae140807dcadf5fa326c1b48aaa9cb2e6e4e69edd31a60e476a807", + "docs/validation/issue243_progress_annotation_20260929/after-resources.txt": "26b7455faf39aff8a67d46c704f786f6cc4d5a44178add263ef9c84eda2ee95c", + "docs/validation/issue243_progress_annotation_20260929/before-bootstrap-pytest.txt": "8d68629c63d6da0ccbf8fe589eb0270d0c458ff20d3832c30ca4300f8eac010a", + "docs/validation/issue243_progress_annotation_20260929/before-bootstrap-resources.txt": "af1d21b52a918978e9674dfcfa0fc8efd0bfbcae49230a94b2a68bad846a94fd", + "docs/validation/issue243_progress_annotation_20260929/before-pytest.txt": "7307af3e5c62a2254ebf0f80e9219659834d7fb2f1d586f19f62255b0d5b5897", + "docs/validation/issue243_progress_annotation_20260929/before-resources.txt": "6db4de7dfe0424476b54d332fbdcd40f210f8f034bcaa69aa1d9c83a1504e32f", + "docs/validation/issue251_bootstrap_20260930.md": "4870894202f12f769efa03d22e4f12b9641e5ce87f2185f5a5364ef3d9825fb7", + "docs/validation/issue251_owner_factoring_20260930.md": "1aad3ec56b7994b95151c8c5ce4e284a03810b052d40edf10da0fcd6b207b2e4", + "docs/validation/issue251_owner_factoring_ratchet_20260930.json": "f3a7851e79b53234a593912e43efada908eb020fdbde6263a609e3a670a4c3d2", + "docs/validation/issue251_owner_factoring_resources_20260930.txt": "80d8faf518c6fe67c33ef071da2aaf4305d52c607d71886603efe3e1f40ee42f", + "docs/validation/issue251_pr_body_20260930.md": "fe06804512d647d7e92e66d63e55a54d1edd1a23c4ff9ea19210618b15a21913", + "docs/validation/issue251_reserved_child_ratchet_20260930.json": "2900dd5ff2046d8a6b47ab2d53c924946eb2bfaf26ff02cd8879a2be95383126", + "docs/validation/issue251_reserved_child_ratchet_resources_20260930.txt": "c6f11a8e92ed8252bca51c49813d40edc5c23c31e762d40e482ec72bb3711199", + "docs/validation/issue251_reserved_child_takeover_20260930.rst": "a55e9498a5dfb44254c3fb2d6811a882cc21ce96e72bc898b4b9cb2549cc9fc9", + "docs/validation/issue251_viewer_main_ratchet_20260930.json": "e168caa9b16ffd15a533ec5da0600d7b56b0f1117d2ae06ed5906d91343fd463", + "docs/validation/issue251_viewer_main_ratchet_resources_20260930.txt": "6d3e00434a29cedc1d4646f0d867c5d04d185f6e15a6c6f95858fb029b6341c6", + "docs/validation/issue355-evidence/dep-r0-rejected-806faa1.json.gz": "6a84c60aefc099cb05b86c29e93a4a75d201fc0759a086f23ed47707b9e26d09", + "docs/validation/issue355-evidence/dep-r0-rejected-806faa1.resources.txt": "da041805e258c61f5eedac2060ae2e6bfd234002cc78271b99846df81c4b9fd9", + "docs/validation/issue355-evidence/dep-r0.json.gz": "f8ab21863a79683d50416dc3b3e8f92fb9c979cd617e3966acae38648e1f8695", + "docs/validation/issue355-evidence/dep-r0.resources.txt": "b05ed2bcaf17ef6ca654c7b1221b8952f041806bd0b2e10ad459aa6fe92c1784", + "docs/validation/issue355-evidence/nra-local.json.gz": "8e76415ff2fedf981a01e2abeb9043b8bb231f260d38aa4ba0a896baed8a54d3", + "docs/validation/issue355-evidence/nra-local.resources.txt": "9329e39c1ff375fe1d650517412081b97a446d48b5f1e00dc63a34c7bba5d65a", + "docs/validation/issue355-evidence/startup-checkpoint.log": "836cbba2d26f0df6e09768d665e13c11f92468e5325c4f3fb36473b46e26376a", + "docs/validation/issue385-evidence/SHA256SUMS": "8a0d7b90dacce001f9704c669f1bf9d761898d8744fb612c51ce89eb429cf61a", + "docs/validation/issue385-evidence/viewer-owner-after.log": "a5c400de7c336e4c301d53e793c8de0102d9de41da5b7297c0a95f851b1bed03", + "docs/validation/issue385-evidence/viewer-owner-before.log": "fc1b12f8887694c419c99f3e97546a3ac74e26c71a3fc868125a43a557e92b3c", + "docs/validation/issue385-evidence/viewer-owner-r0.json": "f89a93d9a41729f7756749d30a9cb73f27ae013f8fcde037e5d553f87f1c7878", + "docs/validation/issue385-evidence/viewer-owner-r0.resources.txt": "1658475b38e73d7f4dac6f77d641b45b38f14fda1f41ce5b0ce1c058d8445cf8", + "docs/validation/managed-viewer-385.rst": "ff7219e3b4879fcd430d2241016293a8492aae8ea751d50ceb756e56f01bf929", + "docs/validation/openhcs355-cold-start.rst": "844f2e5836f5abe514efc69a4bdaa979def4e8fd84e7c15bbd5b02a1ee5cdfb0", + "docs/validation/startup-feedback-358.rst": "50e894dd18dae9fab0d5dc580f584d73032ec58043d92556412346eea68dd463", + "paper.bib": "cf1e534b04aaae97ffdf926efe9aeab42a6f76a7adf63553c0bf74e108837c8f", + "paper.md": "cffb0012fad980e7cd9f98bbc0275b620ee2c224ca7935c28a1f1c520ec41728", + "pyproject.toml": "e2b56ba5b94418c99ee04142edf0fc4f14a06860fbecad7170d4a2e1e4be70c5", + "scripts/release.py": "109327ab88e002e4fdd56e6a50a8b5e265fda3723fb3c0ccc184c9d1b0539c2e", + "scripts/update_and_release.py": "80896e49f856d6a193bb1dc3917f55d2878acf1476c8240333eda16b92e8e61a", + "scripts/update_version.py": "d70feec0d50ca0a8d90f0105be3e85bf5a5b17a7bb0ca63a6178ad443f0ad60f", + "scripts/verify_release_ready.py": "e8190c4f79b1fae85af698b704b339564e77090f8adb7393f0079cf0d13bd844", + "src/zmqruntime/__init__.py": "37f3e14f58b4c1dda3d338a3a2fb075aefea215f6f25b69f44091891341755ef", + "src/zmqruntime/ack_listener.py": "da5aa8d1031f971c2621071a6da40b41f10dedf12839060db13d10153050cd6d", + "src/zmqruntime/client.py": "00757150b06b837b918153bc3c2752dbdba396569ebb4f4ed91cfbff7c23d25d", + "src/zmqruntime/config.py": "79840f3b72593042b6a56e3a703a1064e0d1faa19b12446d99e0903af494f2a3", + "src/zmqruntime/execution/__init__.py": "7a5d9f1636946d5fe6edcaaef181e8740e17d5b348d81e04b50a9fccf6fb16f9", + "src/zmqruntime/execution/batch_submit_wait.py": "c447b2ccbc372a5336f5a3b32844b896dc5d58566cb3de31640ae3be2379edee", + "src/zmqruntime/execution/client.py": "1b9453ea2be540b29849770cc3fe82f284f501e0e140797dd48c2c257ec36f12", + "src/zmqruntime/execution/lifecycle.py": "23596c31b9efe812f1ab66b4300ee1bb19125ea9bef6e51ddcd8acaafb0f4ed6", + "src/zmqruntime/execution/logs.py": "c7d1816835620a2b3e82a889bd36f2a97b80aad7c0f639a07c5c9d426c6ae803", + "src/zmqruntime/execution/progress_stream.py": "13b39fb4d256cd835302f7a8949e9e56bfe0b045e3e6fc1d197ce8ae40bc57a4", + "src/zmqruntime/execution/responses.py": "43ebbaf9451c34a5cb5a0c3182cb4de4c3745e13db8482ba1373becbfc8a2a03", + "src/zmqruntime/execution/server.py": "13663ba20e62326171ff8b43c2474419940729e90b2c0b25b04ea3514734d599", + "src/zmqruntime/execution/status_poller.py": "da0c7c304f293ba4f52aaefc4c722442b5a2859d1fcb806e8fccdcd3b4f6cdc6", + "src/zmqruntime/execution/wait_policy.py": "df4a9cad5a2fb74aa0c030def22dd8cc5b851983c3f04c26d479b607075ef922", + "src/zmqruntime/messages.py": "76018d6f6ac6795e076eabed9c6dd018b8c2fb9640e194a19086a013c0a50785", + "src/zmqruntime/progress/__init__.py": "1767a50989e4a19e4ae8a7ed4343b6be564740236cdcb4c5c56c06f55c21f68f", + "src/zmqruntime/progress/projection.py": "72b22f765dabbcf6983c619c8cc5404bc52518186ef72d85a6f6c6ab65cb426f", + "src/zmqruntime/progress/registry.py": "2943b28d1490688bddcba914db5aadd445cc8e6c9b6381230f0451366e98a0c2", + "src/zmqruntime/queue_tracker.py": "b2e380a93699f85e608d1ae7c03b5a20ee62d8beb82cc93272b050084156610f", + "src/zmqruntime/runner.py": "9f52b69f23b4626adb4e1394e0ed476624d2a38d9cd2c8ec656ae3b2e6816df9", + "src/zmqruntime/server.py": "a38ed80dfbbf9d738f4a96b4bcff8ca667f592b84413c33731550513dcc924d0", + "src/zmqruntime/shutdown.py": "67acebb3ac7a2c419320005931f0de44e28f5ab0b393e6bfbdc3f11207a2073f", + "src/zmqruntime/startup.py": "55118bc44633926434f0e0b057c167dc2948f2a632a902b5bdb60dcb92731d69", + "src/zmqruntime/streaming/__init__.py": "911cac6c1f747d77ec6614dd0961ebb799a7dbaa276e4259e1f38087e3b7320b", + "src/zmqruntime/streaming/process_manager.py": "8229c322c309969f89e2a51a3e416ea38a4990248776844dbcbb01ad9796b3e5", + "src/zmqruntime/streaming/server.py": "6a587842676bdd2f414d8df8f277e5e671a5f41a7b3c4615bf1ea2a5da182834", + "src/zmqruntime/subscription.py": "4a393bd94d8a3611ff7d7423ec4ea681df6f26cf906a77e2682c76cc0e762696", + "src/zmqruntime/timeouts.py": "bb5121a03070f0f2a0ed76170a2324512ed4a4d64536ecc7ba8bdc6a4b8961d0", + "src/zmqruntime/transport.py": "e486a4215d4b977553030e897589bccac637dfe4e9abb28cfb7594139b2196c2", + "src/zmqruntime/transport_modes.py": "12bec1c13d87bb8f05b049dc76d3cb7c130414c9e69d6220228ceeb0087f9ad8", + "src/zmqruntime/viewer_protocol.py": "5580efffddca2f723b11a652f482fbdb52ebae657620fad95217b313997b9fc9", + "src/zmqruntime/viewer_state.py": "f3dd0b35c3c75b0537bf8e50624b8f05a6dce02b40f1700a878189c3312ae30c", + "tests/diagnostics/check_ack_startup_failure.py": "08856fa0ca60e4b00c8577ce2ed3bb82e6e3fbbe16eb2876b69a03500ff130ad", + "tests/test_ack_listener.py": "a616052e042aa145d4eb2b9bca6b153633662bbd54588038730c9d8a2551f70d", + "tests/test_ack_return_route.py": "4519ccf21b0d6bc892d43405541308326a8a7ae885e0ec8942e3596eda3078bd", + "tests/test_ci_action_pinning.py": "a636dd4d70e231d06c5de2802afc89c389f678b3b49a40ce8463eac5b0c308dd", + "tests/test_cold_start_activity.py": "3e0486f8e520e3bb2a767f1aca49df0ffc269476cea0f29594cb08bfce1d7896", + "tests/test_config.py": "f9a16ae6cdb8c9d1dce74bcbc95ff59e853245b219ce848d8aca9704eadfb95a", + "tests/test_endpoint_ownership.py": "e37ddca59beb48b4c1ccca4d1feee1793728cfc54b2aef69870e120bf17098e5", + "tests/test_execution.py": "7b547a68e912b012144519216097bde868264fcf7bb0ad20b3223862701bea0c", + "tests/test_execution_logs.py": "3136e1cf250b02d89e9da9be06b90ecab59fe34ba58e2e22145fd1b0e9fe2a90", + "tests/test_messages.py": "8223e0a5b461e7dd3fc6392c9f652cd74180c8de1b07fef9d28305ffffb2f229", + "tests/test_owned_close.py": "92c3149704b88278ce6771c0c2c064aa897948bd7303b8100f24f99e97d9ac84", + "tests/test_owned_startup.py": "95d3997eaf5d627925108b85dbbe5937abe448c666f26caa43f2bad5586bf73a", + "tests/test_process_manager.py": "b21d1eedd0f98549a7b0c235cca43fbea35afb3c041df81b362d94f3fde9439d", + "tests/test_progress_annotation.py": "27b237877b660d6699402a091dc7843201a4fc0636e4b9f0fc1e25d16cddee75", + "tests/test_progress_projection.py": "e56b49ba046b7c459cee386076201b252d3c831a3d8eca535eafbe0ff6affced", + "tests/test_progress_registry.py": "bf80de2da039272700a366884e5aee4163f04b156fe5c8f58912d4c7f42e7ef4", + "tests/test_progress_stream.py": "27d056146596e7e7c4f2a15ae7e7f7e01e9b4882f3258fa48108d799ecb19698", + "tests/test_queue_tracker.py": "85257cf345a02532441f188242134a2fcb4b60f793eead2f21a6bda360529a29", + "tests/test_shutdown.py": "ffff454475abed18dc520b8629a2799d5a30d0e83abd0292c5acb916d3e63eee", + "tests/test_startup.py": "3e5441886d22921b255de1016bedefd74e87e57f7099bc9e72f8021d1b115148", + "tests/test_startup_callback_scope.py": "06c9dfe205fef9aa293ca0a5dfbaaa8d5083da02fb7c2e06bc9895dea607760b", + "tests/test_timeouts.py": "8c4c95ce4c2d02d5210ef4ccfd4d0cbd1850535c8d5793b87380e995c126babb", + "tests/test_transport.py": "02072b46ca87c082eeedccb02f4b85a1ba02bc51a4c24ebfc6930a6b8b2a21f9", + "tests/test_viewer_process_ownership.py": "972bf0988a9b3b49371ddbd8e1ba7ce965c9fa9a37f04ece70fa93905335b348", + "tests/test_viewer_protocol.py": "26adaa73cc3a39cefc4c797880fb5b2e52082adaa980e479e318fa3805e7fe28", + "tests/test_viewer_state.py": "2f7b0dabbed227db35fbac5321f85b84352aafe19e33c879a50b3aee0fc9d8ea" + } + }, + "external/pycodify": { + "head": "792b03413331fdf107e369c924c257a2f0b16126", + "status": "", + "files": { + ".github/workflows/ci.yml": "7cdaf326512e98ce07256301a152b286d2c06492d67e09d6162d1312c95dd203", + ".github/workflows/publish.yml": "e10379aa724dff1ecf13fdfc3baaf32cb33de77fa65eaf8c9af270d3b3a3dbaa", + ".gitignore": "2c9d25459eec48f9d654932218ffab8dddc041060191c98a67b6505bb9726a00", + "CONTRIBUTING.md": "f39c2677fef7b7243f37e3e95e35a09e118dd1fe30b1f43a25ed119b6064ae69", + "LICENSE": "41a2e56fd632fc59805cecefbd30011033d0e67fccddc93529a1a6cff03aa766", + "README.md": "f32db990f5e1feb01ee3d7adbe1195fd691c8964acd789759901d00170cecf7e", + "docs/Makefile": "43df277c1f15a11e9ab7180cfaa9e65e014571beec43b19530a363f4e62e0cd9", + "docs/api.rst": "f613bec5644911b1d5fc6d6875ff9a843b49526a5b2e00a22488cf825a3ddd05", + "docs/architecture.rst": "dba4502673b43a48f775827acb5dc6a3c75a88836ab59ec5fc0e7dfd722d4b51", + "docs/conf.py": "3cacb5b7a11daa491ff739fe6df58494ed38e50fde40d6bd6fb7c9b4d4eb782c", + "docs/index.rst": "d20074f7b6d7d9d205d453f9fef04a3fc7120b943c6c098664dbf9376bbbdcfe", + "docs/usage.rst": "40677c937662fde6d2d96399b63c83424bc80756036df4c2f6f34c541202e606", + "paper.bib": "ba515e9b812130cc2da5991c37921c93d947808909953fc5c9949202e54507b4", + "paper.md": "c52498a740b040def7feafdd6d1aa4253515d6228e5cc534c00a417998d65f8b", + "pyproject.toml": "1103178d6661d94d66f6b43039d0961557e6d07861e46bbf8df18b6d40a0b1b5", + "scripts/release.py": "9027086b71a742b688bc984dd2fe50c4af2ad6e5ba1b1c564cf531a414e19f62", + "scripts/update_and_release.py": "80896e49f856d6a193bb1dc3917f55d2878acf1476c8240333eda16b92e8e61a", + "scripts/update_version.py": "f93a6a1ab4aa714f699edab98d36d3f610c7b7e14ff5748a22fc656214958d8d", + "scripts/verify_release_ready.py": "aa51d925335037158e0d34d0b198039e4d3833df564f01622ae4005bc7e9d6eb", + "src/pycodify/__init__.py": "b839bc2a7cd2afdfd8756eb39f22b791cb4f7aa55c0fc9c11099b7219c9c663b", + "src/pycodify/core.py": "b32dea495ceb8745c1b363f2e42aa62eaa28a1d54198a5d6541e0b1546facfb4", + "src/pycodify/formatters.py": "08791a7f3cb779889ef24a5bfb395a1acfb4d3320fe9a7cf037986c98a9d7faa", + "src/pycodify/imports.py": "17fc080d60ef168a9c211f27111b04da1d527d2bf87e2188c674e4de9d25bef1", + "tests/__init__.py": "efd53630f4e3c520366b8aa444fe2d4d33c07c7caa0e3841fbb43a04cfdd9183", + "tests/test_core.py": "164dd47f93e4adfd8fe9de851f5db74744509179f6eaaae877be879f22689e66", + "tests/test_imports.py": "ce63bc44d8fc24a02e3439a7b1504b4dacf21e64ac51f1581e60f0099664c3bb" + } + }, + "external/PolyStore": { + "head": "84f322e46871de5ed47e7fd20976ad03e533c1c4", + "status": "", + "files": { + ".github/badges/coverage.svg": "e6e5c18d37513ef246d24694ae7e124f83597ef6e839baaa8269408f6be4690d", + ".github/dependabot.yml": "34790e08d06d30f3f06cf2f071fcb2430df398c0a70bf1d99b5cca71d69080ef", + ".github/workflows/coverage-pages.yml": "1b0c45ae4b4bbd8a38c9208c9f2f6d90faee1545bcec090b776403be9c33b470", + ".github/workflows/docs.yml": "0753d09dd18b885655926115317a45200099f8b024f3df29841f92dae248a2e2", + ".github/workflows/publish.yml": "b1e8d3648e3f38a894c8fa2d98ad2842b388c5effff69a91cd53aeb9f53781ca", + ".github/workflows/smoke-test.yml": "cec1eeec266d5e67c440709ecf5efbfcd263a75046d7e4d9e99acffe057da8e7", + ".github/workflows/tests.yml": "26cf33ecbe48e0bfffdebb2834b3f31f705cbb7293a4d864cb430ffee8f75cbe", + ".gitignore": "4edac7cffd81b322d4edc9422eb4b4574b0227939c52524b95004033891828cc", + ".readthedocs.yaml": "4d16c3881aedd586c6b7f3febd7a0ccbb2df964eac8ff17748acce0745a194c2", + ".zgroup": "2383746e67b4bcc2762b3f100f06c3fa2d5f149ab5a8e5da5d33521464a01959", + "LICENSE": "63c1c82941031bd46dc233be9b616b8e9510f5e30ae57f16a581613e7378d953", + "README.md": "c054ab7a02e743a089ea7e258444ca2982318b86af27457877cc60156a3831cf", + "docs/Makefile": "eb696db665096bf6d05567dadd0213f3ac84275b621909c9014d12d01048e73c", + "docs/archive/README.md": "56692f0cd4664b0cb34e9475a30e9c73e1de1e8f88d22edb4fb00f32593e33ca", + "docs/archive/TEST_PLAN.md": "6b840ad8555985dc78b7984ae7ebd35547da6d051558a8244269551c6072a44f", + "docs/archive/TEST_PLAN_DISK_MEMORY.md": "41619cf02d71f16149b53eb5bdbe6834fb84e69e90e154ab2f070f5c2f8f9d86", + "docs/plans/native-imagej-point-roi-20260929.md": "b7943227c7be98d2315c01b898323fbafcbf392c61239df18066767045c41a68", + "docs/roi_region_properties_checkpoint_20260929.md": "6cfd26c9550315c63dd3ac41c597ad5eb3a7cb942c3c308d97d59d10a7fc8aae", + "docs/source/_static/Main_menu.png": "dbff6948a2354eac88ed6d883ae1d9b2dc2bbc39cbfa8a8a7b21ffe7ed9fba58", + "docs/source/_static/ezstitcher_logo.png": "6e2fdd7f872fa57e595ecbd296a56ea5fcb58923863df4b5852e5c72da8a9438", + "docs/source/_static/function_pattern.png": "c6ddd31d6fefa6f398a942beffb4648a30fd4d403e4000c3579cacd27d498256", + "docs/source/_static/generate_plate_tut.png": "9fe5351407df65d9a7dc66743a887a49e006cd7919c4790426529b96d6151e28", + "docs/source/_static/image_browser.png": "d19a043b5947af2af09abef3f36581ccb4e42e388cad8c24c6dde6a5fdb950ff", + "docs/source/_static/metadata_viewer.png": "abbc92272b036d406e0aed104d1eb44695af872f4e59c5cc964b64e6c5a28f5e", + "docs/source/_static/pipeline_editor.png": "b8c9a96f3613d6c46aa1d9f28446cd57d3edab2e658dc1d631d9f4208378b865", + "docs/source/_static/plate_viewer.png": "43d934e7136dce72e587639cc55336dd3f5c4f31d966d5b8e0d68e19b9fcbba7", + "docs/source/_static/step_settings.png": "9fcdb0e446de8dac2943ee29b7044d1d9def4570c2786c043de065931a767b6e", + "docs/source/api/atomic.rst": "1d82a7883d5cd5954d4ef7680a7e736419cf09dcd454575e4b3ce03a365dfea2", + "docs/source/api/backends.rst": "fe8f91f6c630146977e00d31c2f16be1225bf9fbc6563c80a6bc52a7d3ba5e8c", + "docs/source/api/exceptions.rst": "3419b20eee89d7f9fae9d9064afb4151e6b524d48da65d0f6de4a92f66aa5ebf", + "docs/source/api/filemanager.rst": "ffbd8b43975becb7c5913149c0e8cb80cc89cfc3579816b9eff7b0ee1fb2ac75", + "docs/source/api/imagej_runtime.rst": "ee69fe61cf24c7cbe24a6c0a45f5b2f30b3db685a41f6e4ca4ffb1723ba578c9", + "docs/source/api/index.rst": "23a2dfcbc3abba4e9f2fc7fc8b12ddbd1395f2873f87c651167df7185097de74", + "docs/source/api/registry.rst": "6e96ef632a2144de51adaba15ef51b32884e36ca1c09087eb8d70b59706f4a19", + "docs/source/architecture/file_format_system.rst": "97e6ff244a875b68ffd93a1429b1a66ca5754489cd2b3be67da9e1a5e4f15c1f", + "docs/source/architecture/index.rst": "b67a0219b9e16b05561233bf8a6c9d25edc62ba3f30cda7d96fbee85d2de1dab", + "docs/source/architecture/source_and_virtual_workspace.rst": "8510f954c1de38f7330b3c62448c350ad99cbeea0f0f999adcc0dc7abc65704e", + "docs/source/architecture/streaming_receiver_projection.rst": "51049541a05db0ab130468c08e0f244cf7f181c5b4cf5a17777508be797d5a42", + "docs/source/conf.py": "7cb4e271ad57f025b60ede742e8df94a095dea71e32aa44cc7527180d8d90bb7", + "docs/source/guides/custom_backends.rst": "76b85ead72c2165c0d4e578e11a83a5c65bc3d1340cf17693a0f3a7a5c3d9ee6", + "docs/source/guides/omero_backend.rst": "77a8a6b816597de6c13633f56cbc3f797976f42799b61d574bc60181b74da1d3", + "docs/source/index.rst": "e179fa4a9bfacf5bf50754cf78d66d6e4f0f60de61f84020e65f1e7ce592e55d", + "docs/source/input_preparation_installed_20261001.rst": "2c62ed3d3d85e37fcb974a38eb511cdcccfc13deabe014dea4c7da18b5a53eee", + "docs/source/input_preparation_parent_20261001.rst": "f30ffbde998099815bb1da39bab10b1c59963db43fea8777959cd9e7d4b0c903", + "docs/source/installation.rst": "f0dc946cd61f386b3fa70d767d4a8633f153726c3771a3635214a0f6e1e2413b", + "docs/source/metadata_namespace_327.rst": "73a7bc504da48035419cc09904c139ce2604c6037fbec2179a5c47687510b909", + "docs/source/quickstart.rst": "321494c996433752be2d5a2bf1659469226a69abf71dca591b871869e7d0e374", + "paper.bib": "3f6f21182c566ad8b4124604dbdd39197f92303af864e6f6e45bedb2e0ca7e32", + "paper.md": "bba1edb7a23c365db162f99944e03675246ed6e2503ffab90936a3d8f576b689", + "pyproject.toml": "2f3edfdb1307f3300e3db056b2828814017270a9726f4d7bdac52caf84f96f8c", + "scripts/release.py": "25ef09845453b73a29ea84c303c6c77a79f291775bdba97347f296f35e8629d1", + "scripts/update_and_release.py": "80896e49f856d6a193bb1dc3917f55d2878acf1476c8240333eda16b92e8e61a", + "scripts/update_version.py": "e96bc7111806eb9a15b5d739329a6a41730fc0591e31f7975e52b17e67e1f721", + "scripts/verify_release_ready.py": "1c32aae8b7ab774eb0fb99d0a51ce6535ed7ee5020f44ca9dc8f6d3ae50af01a", + "src/polystore/__init__.py": "15dabedec6acd92b025d4a0b4e8151227a4496e8b7d5e87f28713f7799919b5a", + "src/polystore/array_payload.py": "3eb7e2cf8eb01a2b10b860778a8fd4ea47e7557fdbe28ab8adf31045088e145e", + "src/polystore/async_init.py": "21d9b8af54ff21247a2012d1ae759550ae0147cf75f667a73b4a5a8387bf3d0f", + "src/polystore/atomic.py": "5e412948fb919ce27f09971b0a5fdf0851499812e4c0ae8bd456abbd8ca86b90", + "src/polystore/backend_registry.py": "a4c4e73a79aa4363301f76727b35db2c8b55fa14107f8866a05f74da4fefb64c", + "src/polystore/base.py": "ae641affae21b61448816a14bd68b7f2f3fc0082f0f1fbe51079ed32520450b1", + "src/polystore/bioformats_java.py": "e7ee3fc888dfeebaab48681ac3e9dd2463cca693a0ff0d77ce274c72135666ff", + "src/polystore/bioformats_storage.py": "0f57d0b35619de4aa6fdad0839ae689ae6f91b0be099fd0b28742c28795f3b71", + "src/polystore/config.py": "62c8acad3da3abed8ccf7f1166fc6a0d094be1c3e842cec6446780541df292a0", + "src/polystore/constants.py": "246da078ce8a3aae6d1fdb76183f1ff724795014cbca4d670b80174f74084635", + "src/polystore/disk.py": "cf089b8c79f86099a7cea1b6cbe43ceaec6331196a44b43183f2e64b7746c219", + "src/polystore/exceptions.py": "0ecbd94af34ae3d1a37eab29e1f72be3e68c651c9a93bf52264a783a8fc65ab3", + "src/polystore/fiji_stream.py": "62a60a36f1da5668f60bc0b4e3a5d0e65008042b2361787f4909224dd9b51db9", + "src/polystore/filemanager.py": "877a2b10731b12904b1784a5b42c9e39688e3a84f7f86018c1c2beee96ea2296", + "src/polystore/formats.py": "6f7ec984549b45a31bb32b9051672e996a67d9caab3ce2030ddc7d50e17101c5", + "src/polystore/imagej_distribution.py": "29000b7f7d8067a8a1667fc8ee603d3c2f33fd87f0a318a21cf0093473df49b4", + "src/polystore/imagej_runtime.py": "150983b2da981b31f974ad384a0fbadf5804003fd59c085993701b876384276a", + "src/polystore/memory.py": "4ac155187a80c5b6f096b2170d6851e6ce119938222beef4bd99440cbccba9e8", + "src/polystore/metadata_migration.py": "4390dc79fd1f9dd3fa405b961ea15573037bc19c6db2b31b36a5bcb7170abc5e", + "src/polystore/metadata_writer.py": "987b8ff3cfb45b170656fb7dd405abc68c460381871ca26cc26cf82e0550e256", + "src/polystore/napari_stream.py": "29286154e37d7eeb2c75d9a48cd12fa696f40c02d5ac35865360b7673801f905", + "src/polystore/ome_zarr_metadata.py": "ed0e4a3de24a6f590404d98578e19b76c9c76b27e854639e68b521f49924fbbc", + "src/polystore/ome_zarr_storage.py": "005bdd6ce615ff115050e1853aab0c56c50c65d1361c31ef6219bb0d6c4852b7", + "src/polystore/omero_address.py": "8a3ae8789fceba0f099eef2ee983d858a8f83b764f19b70dfa7bb88f521c0c92", + "src/polystore/omero_local.py": "35d39564317510234c2a3ded00bb06a09417d1b6f7359e52e74c7bd0a086f642", + "src/polystore/omero_tables.py": "910aaa6bea75f40bae82df71dd414415278b1387b80c7f1bc11e8d681874451e", + "src/polystore/omero_text.py": "9f51daef3d96cf191994d8a9c7aacf6182941ec2b0ada3f5ba61e0cc4f9412f1", + "src/polystore/roi.py": "fa945e9a3f7272e72f3ea022c7a3ae6cd30520ed7924591dd3f3cad5d0e71fc0", + "src/polystore/roi_converters.py": "50c37e5b070db7cbe418a5a64576595ee3ea7e5bbcf014492f38958f4debbf07", + "src/polystore/source_tile_geometry.py": "ebe4a422ec76972f2318ba56910ba6ccaef28734764ed3b88a3ba478ed69d322", + "src/polystore/streaming/__init__.py": "e401faf53b6fb9a08e237535313e5b99d7e371372875182b76132921233e5adc", + "src/polystore/streaming/_streaming_backend.py": "7caf2252677a894a5a761b60c473b92184b83745608c2b50a75eb1733c77cd10", + "src/polystore/streaming/base.py": "353e87dc92ad8df2ccec0136ab8ef7de7729896522cf8b16c0ec79bde455c176", + "src/polystore/streaming/handlers/__init__.py": "78daf5078f1914dc5b646ebeeb79bea336036b3b4737e122865be41ab7b24f03", + "src/polystore/streaming/handlers/fiji_images.py": "b5f06390cfeaef6c6a10c06bf9af00eab29f90ce1bb2899794d2e516cf86c7d0", + "src/polystore/streaming/handlers/fiji_rois.py": "5edcdc8d7344c5793730beb5a1c3baea89e96b13e62b746cc1cbeded3217a978", + "src/polystore/streaming/identity.py": "a8e48d76c8fd222515121e2ebc879dccc820e0ebf5b576a8728c1a7ffd35dc4e", + "src/polystore/streaming/receivers/__init__.py": "07953a0ecae9f307b9a2706062c1f452648267887e8cd03fbdb87d4b301b4877", + "src/polystore/streaming/receivers/batching/__init__.py": "c045df607fc2c07983659caff8b191660dd2ce5e73aeceb89894be3a457926ec", + "src/polystore/streaming/receivers/core/__init__.py": "b61b9545e4e373556d5beb87a23c3f6475238361d0c3a4cf18a3a5e197dedcba", + "src/polystore/streaming/receivers/core/batch_engine.py": "36d1dd9a7a2beae134c6f149ca148dd4cc2921fa2feb37cbcb88c42312743055", + "src/polystore/streaming/receivers/core/contracts.py": "c89f3f2dfb3e23482112a6b98ae3238d1768b2f4a01b4b2d605b698bd307f561", + "src/polystore/streaming/receivers/core/image_presentation.py": "dbe1395afce5a280fc1ab0c66bf302e324577ff15c6dff7a1698f50f1fc72a73", + "src/polystore/streaming/receivers/core/layer_inspection.py": "e93f707de62516eb0d48d9fdb0f71862b4265389723ca92a05145fce99a28802", + "src/polystore/streaming/receivers/core/metadata_presentation.py": "5e021ff3659039856616a75b4e19918b709121ee4b8902d39430f110b1adc5e8", + "src/polystore/streaming/receivers/core/viewport_presentation.py": "936378683122ad28deba10d71e9561abfc904017582e345316bf1c760d2abfb0", + "src/polystore/streaming/receivers/core/window_projection.py": "e95d0ccda66aa866082c5afc5bf9908259c727ca33270d3f069384651e1783e3", + "src/polystore/streaming/receivers/fiji/__init__.py": "a86e999752d8212da5bd58f1a3db43ffa7ce867fb0ae1209bf0f148bfe5b4b50", + "src/polystore/streaming/receivers/fiji/fiji_batch_processor.py": "165919f2e36c37e221bdd6f17e4398795745a95da9ddd7bba0ab93d74b9bdab2", + "src/polystore/streaming/receivers/napari/__init__.py": "ab4ba3ba76e98365e67abf6f9d79c0be098a774bf9e17e8523fc430a0babb9f1", + "src/polystore/streaming/receivers/napari/image_presentation.py": "1abb2d4c33ea073deb415cf9a242baa64d0b9d73985b7920d72508f2a5998a8e", + "src/polystore/streaming/receivers/napari/layer_inspection.py": "6bb86202480b5aad55d6641e3a6891105c5284d4408ff1cb7f8cbb36929888cc", + "src/polystore/streaming/receivers/napari/layer_key.py": "650eba665a58130fd67c38b164f97f677d805e07e53c2c7f367f850ae6db4a8f", + "src/polystore/streaming/receivers/napari/napari_batch_processor.py": "c3ed3f9fb4d89f82cc887a4d7775db911c96fc7e67d437cdfe8c4b78e62a16e3", + "src/polystore/streaming/receivers/napari/viewport_presentation.py": "aef743171c8d27ac56b53217f8fb2352584d447b33791ee2ce3e1c8fa4ade356", + "src/polystore/streaming/viewer_transport.py": "3b883e3078ded6b8e9c80e135a2eab876f9399fb7813dcd1dc8dd8f6049a3537", + "src/polystore/streaming_constants.py": "645e1685a1cf23167b2f6f06d4807c76444c339fbde66f3b12de9bba3adbe170", + "src/polystore/tiff_header.py": "714c6ddd0ff3928df37b2e65775c2987c285538b2375ee17254ae0dee868fa6b", + "src/polystore/utils.py": "1dd12cb9742f58f9537f1e23832f44f90deaf4332de8ae9b97e8df711ed261c7", + "src/polystore/virtual_workspace.py": "b19fcda85369d812e70bfc1a0efc5d8861ae256f69e94372e2e9515c591d36af", + "src/polystore/zarr.py": "b9404235a3a7882dd6f64383c11aeb6073c8f3a6485bec74264ad6434a1ee35f", + "src/polystore/zarr_batch.py": "8ed86ac525b561e6557a8ac55c6cb0d3954311a31d3ae65ea84af196246d60d9", + "src/polystore/zmq_config.py": "14123ce6fe232144f8a4fa086cad164a0b29e3b014ab809810d622e39ac92854", + "tests/conftest.py": "5ca35c45b774ffda10030f950823eedb160628aad1076b805e401e277bf624f4", + "tests/constraints/zarr-minimum.txt": "ddc0b24cd684f9742b7ecdbc0939072ca89dbf147c4e238268140e4678bb37a7", + "tests/imagej_cache_process_fixture.py": "bf04cebf8be7fb4e911ec1df75cc3e4fc311745981975620b2946b308c461d93", + "tests/integration/test_imagej_runtime_live.py": "bc1c16a755e12682b5b94b81b902068fb4a2e4b8f8b708a43c814b28ecd78bf5", + "tests/metadata_namespace_worker.py": "5db68ec70ba017d1b81cad2302788dbcf0a1cce5be019358259d3aec5ffd7ef5", + "tests/test_array_payload.py": "95a02bc11cd21f57da986ab8f90196a7343c70513d034e69dec581c413ee802c", + "tests/test_atomic_lock.py": "0ee06b9b53964df6777f72cd0ddf0faaa63d9316ec7edfdedb6e1b6b9087f736", + "tests/test_backend_registry.py": "6af08f5ca00bebde1a4190bb40cc2df99e025aa3465521770a22da0cc51b5651", + "tests/test_basic.py": "803e5643003e4bea60fb48c89cc84d732c77b2a3a68bccbdc6be3747ca053aba", + "tests/test_bioformats_java.py": "bf8b626c3081c26de73902822129ece5128549ec6544b604285dc9b1ac2ddc6d", + "tests/test_bioformats_storage.py": "e97642deacceab3348e8a337b34e79da235c1e1283fccfb844c0142f8632a35a", + "tests/test_ci_action_pinning.py": "a636dd4d70e231d06c5de2802afc89c389f678b3b49a40ce8463eac5b0c308dd", + "tests/test_disk_backend.py": "423c0bb9389cb002ad9c5384add0e08d4c8f99c4279ffa7d1d6621ff90ce9747", + "tests/test_disk_coverage.py": "8f0486f4611c3cfbc449e5cc7276a3cbd72f50de683eeea9647e68a0d2533be7", + "tests/test_disk_more.py": "78ae7241ab3a1d031bc733fa8f20368ba24b225c59cee47ab00624da8b77aab4", + "tests/test_filemanager_backends.py": "f506fde0e7aa4fac67c9682fcb7ed5c850c0a9ac069dd3a6ed7732427ab5578e", + "tests/test_filemanager_extended.py": "3b20c0038ba749f3d4a0f787d2e854bcd7c4e65ea58305231193f415fa734a4c", + "tests/test_imagej_cache_environment.py": "0c3b854542257e09ec13806048ceb5cc3deadf1a6302c621f44c3bc4e6efa9c7", + "tests/test_imagej_distribution.py": "fa7740c10975dfbc2e654ac3b7fc0df0f2f08599a98966e51c2c046f8cae01e1", + "tests/test_imagej_runtime.py": "fb5ce7c8d53354eb46756644d3d0c135bbeae10a21a76718f67e458f046bf480", + "tests/test_lazy_package_exports.py": "ee015f6ea3c2dc198f522db1f494f886b892540750ca3289f7f39909c4700460", + "tests/test_memory_backend.py": "ac68a5bb7c0cc4bf6a74bf3d54cc3a75df2986b9f73e92af5316d878f678b16b", + "tests/test_memory_coverage.py": "a53b737d7899f52ccc0f683ae0312094760e532be709c17a5accba7249a60542", + "tests/test_memory_type_authority.py": "ec326f948fa957b0cc8358629199442db397b9676c884d6e730e23048468cdd0", + "tests/test_metadata_namespace.py": "5a4809a6024b6eec30130ec1bc98cdd6a2541779496cea717f8a89a98d5ca5c9", + "tests/test_metadata_namespace_ownership.py": "175737cbf844b620132bea2c5e5827ffaacac01bf53de136ea49ab32727428f9", + "tests/test_napari_batch_processor.py": "a62c8875fdde0bac50059aebc3b54dfb736246ba7e1980301ebb9e7124592cd5", + "tests/test_ome_zarr_metadata.py": "7444723fcc0bd0cdedd6d8b9454be57ce929caa61698539da55c782f0b7fb420", + "tests/test_ome_zarr_storage.py": "b70a93d160cb0ba0acfd603c75ade965fbf1f95158d7c0cfc680e6a8ce907deb", + "tests/test_omero_address.py": "15d81321402ccf015f5d687b70c27e64d58ba97e719bb090efeea74821e7ffd0", + "tests/test_omero_image_plane_batch.py": "ea857b139476e127a0ee026c20cf5ca918a65f45a70bc1fe27787313d6e830b8", + "tests/test_omero_tables.py": "45ab6ce56b95dfcf9b2c202e2caa587a67bd0de4769e77b36a9986b482cea159", + "tests/test_omero_text.py": "ddf16ee95766440f5ec4d3b0e60e159cc70b2f7694e7136ec13a72c85117ca88", + "tests/test_png_format.py": "f74821025f5d962a45287d88ede69793601725f6f158fdd9cb4c9635ad75cc62", + "tests/test_receiver_layer_inspection.py": "b1b153e86978aa31e933a26072be6e3a19f4881408d10df9044fe351ad0483a8", + "tests/test_roi.py": "a2f256e55483b45ce54264fd667452230ec1024ddb7f57432711b6be7278912b", + "tests/test_roi_region_properties.py": "5cac0122893d4df5289b2e7a14018625333103a713c87ae8c559872958ba0743", + "tests/test_streaming_cleanup.py": "2edca13d06e25936076c624fd9ff6db3b66d0997c08c41082c822a0b7e8fe8ab", + "tests/test_streaming_identity.py": "7d0256ce52d48d944961bad8dff436197c45dd0ac93c8946bc354aec0cfcabff", + "tests/test_streaming_metadata.py": "806cf4998459d90ed532307a48d0f793bfada277c69537ec031fd4e28babcb46", + "tests/test_streaming_receiver_core.py": "c79e5a32d46c39b6974d421bef2ec90099e67d6b6e13d4baa5eaa48588781548", + "tests/test_streaming_return_route.py": "e20baf9443d034ab18bfff0bfc38d60af7d7245e94ec0b7e01f80d8ce403c120", + "tests/test_streaming_shared_memory.py": "c5ecc8e9c027f5fa14783ce9e568b07eb3f39ddd07254e5fa2f9847d1a86aff2", + "tests/test_viewer_transport.py": "35f4e6ffd0bb7de1c5ebb1163e2d46d71f8dcbd0544fe3494e197c2d1ac610be", + "tests/test_zarr_backend.py": "7896159c33ee7fefb89976291e35db507acf282e92361c1abd86eca54160e2fa", + "tests/test_zarr_batch.py": "883da1109c08c53ddb5e03397e5e43bdb0f878c2da8ec4c4158ee9a70520bb17", + "uv.lock": "d4465b3bae7afc90311db1265dbfcb34fbca917c5c0b99da212902ef2463bbeb" + } + }, + "external/pyqt-reactive": { + "head": "ad4948775ab81180a354d4b793d17ee5ddff3972", + "status": "", + "files": { + ".github/dependabot.yml": "34790e08d06d30f3f06cf2f071fcb2430df398c0a70bf1d99b5cca71d69080ef", + ".github/workflows/ci.yml": "043b14a1d996285f2bf77abfe65d84286e9653fb1dc238030330baa256b7818b", + ".github/workflows/publish.yml": "eacab9aaf5db73c7dc4d58de7384b12ce6532382ef51736dc3bb6dde5e8f90bc", + ".gitignore": "348e5192909c25210099ecfa274a4be8fb5faee2fe96b47595b6e4f53f3eea89", + ".readthedocs.yml": "2f5648c67a8a1ba1db73ec8dc32fbb82ffb83b246671e1dade4cb8836d667c6f", + "CHANGELOG.md": "e2e6847f7981ad1a522894ae0e15ede7eeb59b3fdbbdb7b7d3b7a5b469152d93", + "LICENSE": "c5a804a1e4c08f5593eb8466c91dc4e871fe79319d81737a82a3cc514b7b2ff6", + "README.md": "59794f0817776c30f1ba9f82919c9ce29cc96203f0e95cc0bcdaa91c55542909", + "docs/Makefile": "43df277c1f15a11e9ab7180cfaa9e65e014571beec43b19530a363f4e62e0cd9", + "docs/api/modules.rst": "617c09bc67116f3a6ad8c737392fb483471bda2a3a29102f6dddeecea541a125", + "docs/architecture/abstract_manager_widget.rst": "ec6ca7a732c048649b8627b514a653ad050685d9b776174af7303877cf756c4a", + "docs/architecture/abstract_table_browser.rst": "3c6f2e9a7b6fea5e0e2015a1051ce1decc3f339ddea21292380d02112b704363", + "docs/architecture/button_panel.rst": "454f18ec0d2dcb906caeeab5bd504838420909ccd22e9ab1b93796ec8bd68847", + "docs/architecture/cross_window_update_optimization.rst": "3e2507130ea670ed92ff1053a4f55db768e4c45055613e4046251ceb1d49a287", + "docs/architecture/enableable_form_ownership.rst": "1d61d9dd8b3ab79f1b7780b4a5380e2b876452fcf80a09a7fe8e6ff0bb6bf99a", + "docs/architecture/field_change_dispatcher.rst": "93964ac421e2ded054a4c151cc0b2383be67d7b68d4d10b871fbc1f8be4fcde2", + "docs/architecture/flash_animation_system.rst": "fc005a4748e5e312cfe61232556a38b0b2855f31d50704fbd1b6373c1178124a", + "docs/architecture/flash_callback_system.rst": "f56f1902b4747eee31c2e77848de3662580810f4295781c0e8019d373b1b5951", + "docs/architecture/form_scroll_containment.rst": "5c3b46d864459cb5900a1be2b1147d93147c8e8e781ef2b97e55d86127ef9513", + "docs/architecture/gui_performance_patterns.rst": "0ae20c36992e6f5935ce3809d6edfb81e30750791b66d7785185ada00709cc68", + "docs/architecture/index.rst": "da94b32fe4ca3245ba69b1c8c0f6551885a0bca9feb1e61af964caca5e720a94", + "docs/architecture/list_item_preview_system.rst": "d7a0fbda2c99389cfc531fde81006bbd3a32d596d395112a1e6cb97f8f2d44e4", + "docs/architecture/log_viewer_system.rst": "335f4c93673eecb1e70705cb6499cd5072a504bfb607157b0f17003f9b4df2cf", + "docs/architecture/parameter_form_lifecycle.rst": "788a41edad8bee2eaa46299f1005ada60dacbbb748ae695362163d9ae45f0c16", + "docs/architecture/parameter_form_service_architecture.rst": "307b81c106e936ba70fee6c3e1fe47f97ebb5340896ccba20a00995ec14b76c0", + "docs/architecture/parametric_widget_creation.rst": "127604ce9226b48662cf062061b2bcc20476b9331a33d5f85ec09be5ee0b4dcc", + "docs/architecture/persistent_system_monitor.rst": "4f6248e3049912c30d68cf6a57eb33ee4a8d33f36dc76aac046f490296a0f0db", + "docs/architecture/scope_visual_feedback_system.rst": "8532d95d91a514612d4e12155bc47d69a9df6f0edddc6d1548914c7465f8bd08", + "docs/architecture/scope_window_factory.rst": "7b405c0ddce22b9e1d361d090fcdefb961142d1fc8d2d942390403c8061cd230", + "docs/architecture/server_scanning_and_polling.rst": "9f8540fe237778f095f5c177abeee77734ff3e2124b2a7ccf9b520ee2459cc45", + "docs/architecture/service-layer-architecture.rst": "69e14c574ab79eb9e2c03f825cbae889893cc4d471c4a499aef0fc2e23899211", + "docs/architecture/service_registry.rst": "39c8f6361b1bcc00be0532be5faa3355b8c5e6dfb3b992f51aad3ba82ff419c1", + "docs/architecture/styling_architecture.rst": "1f7dcd355460a1634f995d7f46c13699eb9dbf41b5528535c0d5fd7673ca36bd", + "docs/architecture/system_monitor.rst": "9b099a9c66f9889ae01f7cadfdc180a48c6be1c70bbc2ba78ff1001a9f75a0e7", + "docs/architecture/system_monitor_core.rst": "2f0288776c503fed3155a206dfe068dbc40675941e97c28499119247220f08af", + "docs/architecture/tabbed_form_registry_pattern.rst": "2e3c1f428765917a7e658b0687f544025a2e46ee2622dc77611702d874593b13", + "docs/architecture/tree_aggregation_strategy.rst": "cdc28440e3560170b20c8d600da96a3a3317f1c3ce285b5a72f8922081c4ffe9", + "docs/architecture/tree_state_sync_system.rst": "1fc2178f9f7f2171cc8a05d064ca3e8a8ff89c44ca9d735c67563286dc913d85", + "docs/architecture/ui_services_architecture.rst": "afa100e0281a8b93571771d2a78c85ce1d75c654268abedd4bfaac87eba19ccc", + "docs/architecture/widget_protocol_system.rst": "9382b7b91dbcd0953bf5e68e2bd93059c84d9b2337f9c6eef1ae0893322b361b", + "docs/architecture/window_code_documents.rst": "21d801e7e83e8d084144efc4a99b23acd48832783eb147f8d88fce39619ca834", + "docs/architecture/window_snapshot.rst": "2ae5ce2a50b165e79766c91f3efb09d55d6e14365de130bf646cfe4605fa97c3", + "docs/architecture/zmq_server_browser_widget.rst": "9b133796b0588362452be0c8ddb92e03a3f1a1578a0d2b0aadd96faf617b8e3c", + "docs/archive/IMPLEMENTATION_NOTES.md": "7ff23ba35575a35cab88851ed46075d9d92c47d34e87c9b9025aecf85b3e2fed", + "docs/archive/JOSS_READINESS.md": "395798c13b9764e131eb6a62fba73bce676f274e573b941cc1c6a1b04dbcb979", + "docs/archive/README.md": "34682a4046790ad3a59f328ed23cb88af9d34f4b649656847bf9863b340f337c", + "docs/conf.py": "2ef313507c9b6ce6941cbe82863a0373b818ea5627c9dec2824953ffc7f312d3", + "docs/development/index.rst": "fc0f4501f81b733702e21d79ca31d8971e9f2605620073b933f7c8147320c5c9", + "docs/development/ui-patterns.rst": "b017023d420458c9964406bfc050242d1f16a1032243492565db35c84dd2ded0", + "docs/development/window_manager_usage.rst": "8d4fdc1d8e3f7cd3d6ac9370680cd66d935f7338b6d249cb6401057feb86279f", + "docs/examples/index.rst": "89c204c6cae088698980aaa189fc2ca0b19f5f0eddbd818f9e8b042691e70fcd", + "docs/examples/ui.rst": "b3f370b8912e8e5ceb0f5510c46a53a5fab01de513b391cd3cfda2991839bc8b", + "docs/index.rst": "f14e5ddb3320bd277ab839925e63620c1f745c2dbbc0bb17460faaffe64cc464", + "docs/make.bat": "ed525cc4d41b62b8b96d4b62a44ef32d6ccfd9324a379a82d5f71b5fb6d57776", + "docs/plans/flash_performance_optimization_plan.md": "e7cc1c22050afed51b5fa908a4099a853e4cc62b86200ad46077b8ac9037ec60", + "docs/plans/opengl_flash_acceleration.md": "28fc4cacf5c19ae7990bcc7fd81a46e8172839ba880ad8a2d85ef2ce50d8afbc", + "docs/plans/opengl_multi_window_architecture.md": "4bf4e27a62cd9d3e61a0086d8b2edb1e6595df49ae3afe0ca379e38efafd383f", + "docs/plans/parameter_form_manager_live_context.md": "dea7242aaa7c9c4e30025a457e8e77927bb1725d8c31267be0634fb24a788aee", + "docs/plans/window_manager_usage.md": "7060816a81b449c2727b1f59cecf71cc7ca5cd536a372828b178104c3e394da6", + "docs/quickstart.md": "98777cb7438f6c72d7443227e92d9b1c4a079971c7e5c6b4ac51c53baecb0b9c", + "docs/quickstart.rst": "25e9faed76719ed6d0ed95ba2416f002e3f0adcb048a605d19b57867e5ec1469", + "docs/responsive_layout_widgets.rst": "96c737263e7ba05cc2e6f21be65ead024e0d894fe6880d30c9ceea351e329156", + "docs/state_management.rst": "c23255071cf550b481797dbc91a406219be4493faca7eb835b8f02651026650d", + "docs/tear_off_tabs.rst": "fea9c04ab60cecbd54aa3b43cffda6f26d6c29ebae2ada6e3999a859bca2ecd7", + "docs/undo_redo.rst": "0cb27f79befce8a209468c86aa120a03725d1bcbabbbf055d27af06ed88169c7", + "examples/tear_off_tabs_example.py": "5506bb39a1953d99d1a8b730851a53233dddd79aa0a4cc738f422fad10b2d640", + "paper.bib": "67e65471f9b4f38e0b4beecfd7cf8b7c2ed815f7b8abfdc680e63262c3652f91", + "paper.md": "b305c23c401bc8ee17d0b1049727c867d86f2bb5c44bc51a77640e74a6c7867a", + "pyproject.toml": "524865db6ac36de136ee2b8dd43b1c741152f5ade3f8195f637650d252dbf8c2", + "requirements-ci.txt": "7da9971ef9b437b35bd507d1e35e09f37d39b4c16b4ef09c9b9a6849d20fdb04", + "scripts/benchmark_form_construction.py": "a2597d87f99c5ad0859e66732f7f7e43a73b4c896b3733d2ee9d501227d1a3d9", + "scripts/check_ruff_added_lines.py": "67ed9e6064204caca990b7b7054cd0774f59b235e241a4fa46db28ad48213ad7", + "scripts/release.py": "09fa8c9c7ccd7f4936aa9a9994b6c47726ce27e8bf99e0cfdcf06d528ff4565f", + "scripts/update_and_release.py": "80896e49f856d6a193bb1dc3917f55d2878acf1476c8240333eda16b92e8e61a", + "scripts/update_version.py": "3f771c59e65b5f6674faef3dafa3b660f152b58ea61114ae0a37f840a9ce4f04", + "scripts/verify_release_ready.py": "7a023ed885a132d922f1fd3942dd56b66158958192f7e5371e838c537cd5d2b5", + "src/pyqt_reactive/__init__.py": "5b37a90f246987a98087428e80d2cbdcc030b80ae962c347482f21719054f68c", + "src/pyqt_reactive/animation/__init__.py": "f449133ede280b74aec90dc85e88a9a2800e38c45b239996d9abde51d0cee6d9", + "src/pyqt_reactive/animation/flash_config.py": "efc6859a2de5115775a940be0a9e1c5e58f7e60e068872f11a256060b045fdeb", + "src/pyqt_reactive/animation/flash_mixin.py": "37cd2b32047109fc16255a3f9a9938ae553d5f8bd5987cd10460c7ba500f1486", + "src/pyqt_reactive/animation/flash_overlay_opengl.py": "f6bc6452e56cae0a17b07244547b55820c091fa20615de3ab1a8159f638f1dd0", + "src/pyqt_reactive/animation/flash_trace.py": "9a2e71467be16a31e0abd1f7999253e4842447289400d15cd3ff995fd06af9a2", + "src/pyqt_reactive/core/__init__.py": "c111f382ba9945651f0b385131896ea2ae522141f7a91f7a1e1eae9657e97122", + "src/pyqt_reactive/core/background_task.py": "4afa0d07107479f028557d23f15a6c5146cdb9f33e253b81f856f9092824c2aa", + "src/pyqt_reactive/core/collapsible_splitter_helper.py": "8d959845294513e0240c994c1bd9142a968f682be994cb2e3fd02f38f0403c3b", + "src/pyqt_reactive/core/debounce_timer.py": "7c3bcef0d7dc0107df0d413f6188ece136e3be5cfea473e3e74b69496af95763", + "src/pyqt_reactive/core/deferred_callback.py": "b52167f36cef2e58bc0f6b38c15359cbfb28a12a71c3a41ad9ac62155f357803", + "src/pyqt_reactive/core/future_completion.py": "7ba6ef203bae81319004ccc4ddfe4226e0230b177df663cfb0d02c364e1dbc47", + "src/pyqt_reactive/core/log_utils.py": "8a2f67434497cf5bc3d637b945b75590c735ff8d97b8db56f9efe5bbaff5e351", + "src/pyqt_reactive/core/path_cache.py": "dfca58e2ae9deb036d4df4038ce8f78125cb5ddeb2e691293bb4d54ca673b586", + "src/pyqt_reactive/core/performance_monitor.py": "09299865cc6641b87bdc195442a297d982ee3909b35c8d2063859933180834eb", + "src/pyqt_reactive/core/reorderable_list_widget.py": "edbba72d7aa88362860b923c0b86715a8349dfec9a7d85660f12cf33d859429e", + "src/pyqt_reactive/core/rich_text_appender.py": "fce3d7be3654c988f6eb16117231e6032718b28608f99f625054ff60269768a2", + "src/pyqt_reactive/core/sort_utils.py": "ac9cb5bd209113980dc398197f461e19e6c9716a5110a889bfdd7bf7b85f3d81", + "src/pyqt_reactive/dialogs/__init__.py": "a9ab9bde099f50c796b1c29c77482f0bf771591fbb12514f1b9ab8dfbddeb4a9", + "src/pyqt_reactive/dialogs/group_by_selector_dialog.py": "a62fdc652c3ff9fe2ac3babce17968a5786877e38ff5b6c2a10ea0678d5a5e67", + "src/pyqt_reactive/flash_trace.py": "c0110553bed7908221c10d1a33e48f4324366b047eda75f5b2561b68f5855063", + "src/pyqt_reactive/forms/__init__.py": "5c9c1af2df09c479f1cfc50fb8d8ee7ff3e157a940d137fcde8af95c95d035a5", + "src/pyqt_reactive/forms/form_init_service.py": "4d34e5f2494a075f9aac68e40c07b07cdb06600d832bd5b510ee971db980b82d", + "src/pyqt_reactive/forms/form_value_validation.py": "ec3ca9d48168b651e8fe0b0ef6f90a67ad32f145332a8798f1cfad220c9fd597", + "src/pyqt_reactive/forms/inline_dataclass_chrome.py": "5d224ea256bffbb840f380d9ad75f9f98b7c3ed18fc4b1ba4db0b64f92a428a1", + "src/pyqt_reactive/forms/inline_dataclass_context.py": "c0d7a19651dc1b88e66b656a031b39a7aae67ff8998e50cc4e5962a184f4fb10", + "src/pyqt_reactive/forms/layout_constants.py": "a16ebe77ea6fb5d6c26d658bc586dd8357d18506c0935cb6426edbffc906ff98", + "src/pyqt_reactive/forms/object_form_document_renderer.py": "c47fada4032d685b6bf7dca4cb9f31e4e9240a25bd1039b165b045d5de541f1f", + "src/pyqt_reactive/forms/parameter_form_base.py": "f35bb8ddd6c630a47cc5095df93ffee608a384abadf755b2f0cf12bcc963b36b", + "src/pyqt_reactive/forms/parameter_form_chrome_sync.py": "94fda5faae9adaddb1cddd1b7a4d3b0452906828d94c582f6024ff72d89ca4a3", + "src/pyqt_reactive/forms/parameter_form_constants.py": "4d4ae05b91d525108342a10e9b2da7860b5a52b0028612f244339d4531d02097", + "src/pyqt_reactive/forms/parameter_form_manager.py": "a561e62e29af42630fa107b0929fc87b74007f7ed14ec8f0a6881c49070dc923", + "src/pyqt_reactive/forms/parameter_form_service.py": "0cbf305c99b0d7ab0c558162ba5d86dcd18f81eb8cce0ad271fbd4b3d15ebc89", + "src/pyqt_reactive/forms/parameter_form_tree_index.py": "e343c3ae5535bbd46dff964844225df13fad5431253eb3924b86f8c8d1dad273", + "src/pyqt_reactive/forms/parameter_info_types.py": "3f87562ecc9b1a4cbf541767ae39d53e1b2dcfe71d9a5e211456d8b34a0a75f8", + "src/pyqt_reactive/forms/parameter_type_utils.py": "c7fd2f17faa1e3d5c3027179135de51b859d204c4a2cb9f9be10837652530ea6", + "src/pyqt_reactive/forms/parameter_value_contracts.py": "109b82548f6a90e1d495d825eeb81309b555b6122db67867a2c91769e9c54f78", + "src/pyqt_reactive/forms/ui_utils.py": "96521f8a8b75f501ca9f2dd34f377025a7d5fdf543dcbed84ec17939875ca0ec", + "src/pyqt_reactive/forms/widget_creation_config.py": "01f32efb0dadc4d5e1a42344f43e7704c47468844b4a05bd13936e490ad4489a", + "src/pyqt_reactive/forms/widget_creation_types.py": "3b1c08a0a94334facb76afa273fef0d247a93f748ce86c3fb9e7026d46ef844f", + "src/pyqt_reactive/forms/widget_dispatcher.py": "5b13da9b2aee79344c343ddcbc296f125429c9780696ed35f996e982c3ee1562", + "src/pyqt_reactive/forms/widget_operations.py": "708bf6dfd51c01154e02210abe8840eeb496af51db57cfda425d51a365080b72", + "src/pyqt_reactive/forms/widget_registry.py": "6529d5879667827e7c6adf36129e6dfdfa18239c4e0ae32bec27eab7b03805c0", + "src/pyqt_reactive/forms/widget_strategies.py": "6eb7fc78526a247dc30c39493f36f54f2df5560b0af63d743d41b30b4f469384", + "src/pyqt_reactive/pattern_metadata.py": "7c2aa3f2660d530503acdbf8e122711cf7b3d1c0aae2d05bf6addb7091dcb4cd", + "src/pyqt_reactive/process_launch.py": "c324a6bcc3b5002bd4e6e5db184d4fd80bdcdeb7b130565ca52eb37bbaa08f9e", + "src/pyqt_reactive/process_signals.py": "8b4c44c98d6979fab6935485ea94d64bdfec6eb0143855078628451d4cd9ce82", + "src/pyqt_reactive/protocols/__init__.py": "2f59ae586807161ac8d019d904646684be0cc458b8b4bcef66f3d233002fd294", + "src/pyqt_reactive/protocols/codegen_provider.py": "98c030fbf5fc99ac8ce53bba0305f0591e0b1a12403b1cd22dd282d1df476fb3", + "src/pyqt_reactive/protocols/component_selection.py": "a68d8ca325a0972c7e911901a47521e44882699729f98bd4cfde57cda7b4d1fb", + "src/pyqt_reactive/protocols/form_config.py": "cfee9524e2e2c92fe1317d9cf1229580df1e1c04a45d9583c4b97fde1b3543e2", + "src/pyqt_reactive/protocols/log_providers.py": "c6602cec12dfa687a6f4e122aa62ebaf3ba7c5472a0d63b29ab0af9db3c18514", + "src/pyqt_reactive/protocols/preview_formatter.py": "05ed6999327cf4483a79d3bb5c9d1f1bc231fd3c0a33026e1e7069f1e8d3771c", + "src/pyqt_reactive/protocols/widget_adapters.py": "1ae6653bf3f2786eafa9f53f246e145ebfa34b81c190aaa8f41c19a1463a42f9", + "src/pyqt_reactive/protocols/widget_protocols.py": "4877e65746459f556954eb718d276748fd5211d244e15f1e92973e5ee01c24d4", + "src/pyqt_reactive/qt_types.py": "cca393ec5c0126b296c2462b414a84acd0cd1b5f63de9a2b6112a0b6593921b7", + "src/pyqt_reactive/services/__init__.py": "9ec1b77dc39cd6e0f550e7ca014c4e6a560ac0fa46f071c0f6ea47a6a34fe46a", + "src/pyqt_reactive/services/async_operation_executor.py": "f1252a0244f816b2187881bee484776ebd41529d1a17df08a1360967d4e03516", + "src/pyqt_reactive/services/coalesced_scan_requests.py": "5a565f9ed1bf34871f2802400fbe6b9ae7623cc612f07dbde44df10e92a1fe21", + "src/pyqt_reactive/services/enabled_field_styling_service.py": "0a16f22878c4ff68a3a75c937ad50314a0acf14014ceb39bf2420673a75e01ac", + "src/pyqt_reactive/services/enum_dispatch_service.py": "20b18b0ef51d7ecdab99e3fe0fb20fc38c605ce80995945f5e1033e24bb4bee4", + "src/pyqt_reactive/services/executable_action.py": "6942ff087473fc102c5395184aa74c8b6c8088cab5d7fa394bc5e103e41c5d28", + "src/pyqt_reactive/services/field_change_dispatcher.py": "0875f01a405b95de12cb0d4fc396d959a758d67a39c211d3056c512352d0f1c5", + "src/pyqt_reactive/services/flag_context_manager.py": "88b634b8aa899f82f6e9525101ade1f60b0be11763a33908ed3ad23ad3bfe147", + "src/pyqt_reactive/services/function_list_editor_actions.py": "68a0fe00e8b80cf1154aaa1545725b48b0c4b75e338afbcd23fed159d6d100d3", + "src/pyqt_reactive/services/function_navigation.py": "47664c983d2f515f2c4e6599eb4fbf269817e69f9abe68101872e6380ee60e85", + "src/pyqt_reactive/services/function_pattern_code_document.py": "bdf41b58bff009890e6e696cfc4f18313adfb1669566b2566f9e7b93d863c071", + "src/pyqt_reactive/services/help_document.py": "c0c1973e7ab02823c204dfd7003ac2a2b44acea36b895a82604ac31d0f4db1e3", + "src/pyqt_reactive/services/interval_snapshot_poller.py": "77fcd5b147bcf66782ebb1bbd6a9e9a8379c9e38672a47fd14fd2432eeeed11f", + "src/pyqt_reactive/services/manager_host_abc.py": "a9a3ffc9b2cc2c5d6f6b1224c89838f74bc9d41d2c8de8da39bf33f98483b90a", + "src/pyqt_reactive/services/parameter_help_service.py": "422f9e7b818f855f28979400512b4943524606827a74bf807bbf26afef1a589c", + "src/pyqt_reactive/services/parameter_ops_service.py": "8814b7c13bc56a2b0d3f69fbfb2a6153b4e2b5d0433779db6ffce5ba480890e7", + "src/pyqt_reactive/services/parameter_service_abc.py": "c5818d83e39444cbd88e135e2a075e45872d49f8c83c34e1d0e925e7de38d387", + "src/pyqt_reactive/services/pattern_data_manager.py": "ddcf9659dab6ee50ea3c155f57e1a4111a97e414ecf74b58151da1ef6557ac88", + "src/pyqt_reactive/services/persistent_system_monitor.py": "2041aaa91a3e242bd43e1686a65feed89078d18f8f7626bff00225895bde0007", + "src/pyqt_reactive/services/process_tracker.py": "5850b647c4c75ab2ced0ea91be34fe99b8e1a004cf3b2df597caa7769f7bc899", + "src/pyqt_reactive/services/scope_color_service.py": "b9d958c27205ca5a9eed16dd729a354642d90bcca96a519b01a0b19d94b1c822", + "src/pyqt_reactive/services/scope_token_service.py": "f84056a6b542d1d9a7df9e05c2ec344d6acb7cafd3301054158e89f04eb32b17", + "src/pyqt_reactive/services/scope_window_factory.py": "fc2a4606f89c85d44014ea6d08018e34921b8898816d8a09ca8febf80a448338", + "src/pyqt_reactive/services/scope_window_navigation.py": "39a0b4e95ce6e9a478c5882b91aa0822f53b683c9f2a706727bbf88922679331", + "src/pyqt_reactive/services/search_service.py": "e72ef88fdaf28d069bc80c2eec722ffa6921015613815929006fa35fd7de2054", + "src/pyqt_reactive/services/service_registry.py": "8f6dff4b7e6db9eb4ba2a0944d18cfa3c03b75e9cef85594b1e8bc7c5bd03e58", + "src/pyqt_reactive/services/signal_service.py": "04680bff5098b52edb1f4998356c1ef9543bcca96b3f37a7377ce4f98c1f18b1", + "src/pyqt_reactive/services/system_metrics_sampler.py": "7f0df0d9425a9998e371c86f55a27cc492fb71b36817ae07b6280f73e25b8ad6", + "src/pyqt_reactive/services/system_monitor_actions.py": "2aa205be430ff8b0429279a3ec44e5bc0c672b6da7a246a208f1ed40b79aded3", + "src/pyqt_reactive/services/system_monitor_config.py": "2f7259cafbdfd28f19d2a291afcd2deeef4e5eba2402bfb0e303a0aa643d6dd4", + "src/pyqt_reactive/services/system_monitor_core.py": "1712ed50c76e6303dcb96f65c8294922084bf2e3f90d21be56b766abca0dd39a", + "src/pyqt_reactive/services/tab_identity.py": "48274c530cd0792b3565c1becf1cef9c90d2f48b4e6ccec26cdd8807c51715ff", + "src/pyqt_reactive/services/tree_item_key.py": "e908f21bd6cb1621cd8356611f1ea0d7daa59d19171b211f3c3038598b8a5aef", + "src/pyqt_reactive/services/ui_thread_dispatch.py": "f2dbe7e8ce8ac0ad3116001d7dfcd91affd516a34615775ce4ce9921480e40bd", + "src/pyqt_reactive/services/value_collection_service.py": "33562f9b50e1080a26c28467ec31c3915e5f734bdc237cc04f4802db2b36b911", + "src/pyqt_reactive/services/widget_service.py": "cf3248353552db77b3257cbcf982987848fb16acd38ab2245dbef1fcf5e25223", + "src/pyqt_reactive/services/widget_tree_projection.py": "05d5221d64e74ff14483f203975f17543381d959c30ee5918c200fa8e9957547", + "src/pyqt_reactive/services/widget_tree_projection_config.py": "8f616aa3f2fe1dc88077dc612cb7c527970363692c6a8ace15b92ba56a806e4b", + "src/pyqt_reactive/services/window_code_document.py": "d0ab321af0eb2b14fac3ab2577c8c61672763d2f13a70a7ce8a35ee7bfab7c75", + "src/pyqt_reactive/services/window_manager.py": "84fff75ff74e59c5d7080f103ad1a4baa2c884aff3d2f87891a0afdae9a5c332", + "src/pyqt_reactive/services/window_navigation.py": "88da77ed07ea220b7c6874e71029b5bd50805fb2c3048701f9df773fe17257f2", + "src/pyqt_reactive/services/window_snapshot.py": "bced886e6bbfd69ae9bdebf950027b809e72c23eddadcaa5fcf7151adf7ae913", + "src/pyqt_reactive/services/zmq_server_info.py": "1050f0e9ada59580eba9a6c4a703678f0945f4a2cd83df4fb32251733911b81d", + "src/pyqt_reactive/services/zmq_server_scan_service.py": "ae12092dfd8b85a3354b87a708f3c32aef787c2f9fd0402158d32d14b654c7de", + "src/pyqt_reactive/strategies/__init__.py": "cab5cb18e271d822d459ab8b7f6427f03b8beee5ac9a4d04b4b73a0914d58e9b", + "src/pyqt_reactive/strategies/preview_formatting.py": "dc1a423246ca40f13633847d5d3ffde3916e53c1a8a8a92de893d3e80ad8fda7", + "src/pyqt_reactive/strategies/status_presentation.py": "435d97f7804e2db714cf60b236074c7a56fc87542b05a4d7a4b1722a340a7893", + "src/pyqt_reactive/strategies/tree_aggregation.py": "d28be44b8472fe1db805fb706485f02bae2c48d99e41f422dc534d24e0084c5f", + "src/pyqt_reactive/theming/__init__.py": "ddbd3f379805b6ebc01a86c86ba41999617b46498b4436563e5a427afea3866d", + "src/pyqt_reactive/theming/accent_chrome.py": "2ae2fd7cc5b3d2b60a99942792ed0621040339e117338107be01fc6128b36f86", + "src/pyqt_reactive/theming/color_scheme.py": "45f621f809accb5dd3fab03c1bbe95b43d680e6b4e03680e89ac2fe6400f16e4", + "src/pyqt_reactive/theming/color_scheme_resolution.py": "e8c9a7501268979d0fbb3e02f693b31a63e08688e80c3cb23abe0a856e1de09a", + "src/pyqt_reactive/theming/palette_manager.py": "00b81b76f39b561eb7a4bf37d0547bf2db6e7b63efa18ef752f614005f56be90", + "src/pyqt_reactive/theming/style_generator.py": "7433368f14efdefa05a401501122704c08f061412a343531fba5400a5807c885", + "src/pyqt_reactive/theming/themed_checkbox.py": "93426143261ca2c1da431264cec69e89ee76a35d7bf85d9e54db27e72c058ea3", + "src/pyqt_reactive/utils/__init__.py": "6b8f0030c2822411d7efb3433a2276320b985423ade229529196cf09fce17e3f", + "src/pyqt_reactive/utils/log_highlight_client.py": "db8e2fd014eedc85af1d0805250931f74257a402651edad2bedbc4588cc82f7b", + "src/pyqt_reactive/utils/log_highlighter.py": "e38237cfea9d757910a7b4ac84a8095068ea1c4531abd56c6fd15228b40e4703", + "src/pyqt_reactive/utils/log_loader.py": "c0cc1d6e4c7ac286562e2659b327e738f3cbed32686c09292b0d957ea61d159d", + "src/pyqt_reactive/utils/log_streamer.py": "ac195bf633e690695d61c1ea4cdfa7bcb45a4ecc20dc237f35bc220a87a08669", + "src/pyqt_reactive/utils/preview_formatters.py": "76a383c498300e2672d6b67ee0261e0dd90df0ed2fd64fc2ffd3513cdf097838", + "src/pyqt_reactive/utils/scroll_filter.py": "cb38375d28d5613f162dc020e2b8735824ceea767ae5cc1156c7086240e22fa8", + "src/pyqt_reactive/utils/styling_utils.py": "22d32d0e01fca8f575c15fa4a61b27cde2e46616cb86088733e8d15f312a1da6", + "src/pyqt_reactive/utils/window_utils.py": "11b222927102bc79d8a5f6a957bd99ab1e828bfb6586e1d647aebd564b99afb2", + "src/pyqt_reactive/weak_events.py": "446b5ea3e71eef459a2afd3663aac3e05a4242c6e9e21379f79fadf9563202c8", + "src/pyqt_reactive/widgets/__init__.py": "ddcf678b0dc5c03aaf11d2b6f12bb9cda681a29f0105925c51a5b9263d751716", + "src/pyqt_reactive/widgets/editors/__init__.py": "6fdb2746dd03af27487402eddb929f4f10664f6dab82726dba5894dee8d33878", + "src/pyqt_reactive/widgets/editors/simple_code_editor.py": "b84df51fd669c550dddfc5484e178f585d7b05f9ca3b230cab6bb82b3d0ecdab", + "src/pyqt_reactive/widgets/enhanced_path_widget.py": "5c72029a13fcf4e58a99ece7bb898afebab083f20c104f9255082b5f9ab1b232", + "src/pyqt_reactive/widgets/function_list_editor.py": "3a71a7af92cd6a3e304a030f7e821710af53cefb63c9e13cfb331c61b86d48b6", + "src/pyqt_reactive/widgets/function_pane.py": "da0f781f1948df80c856751fb6ca5f552583395d0f5b2d2beaa83d03738e1abf", + "src/pyqt_reactive/widgets/help_document_browser.py": "f8f0bb2e454ef458f2fd4f686f4d6814a717e8f1da0de1790ddb6f8a96795e8a", + "src/pyqt_reactive/widgets/log_viewer.py": "e68eda879c7cd3f5360dd1b4a08a481298cabf85cd72bdfda66b0efbd54d56f9", + "src/pyqt_reactive/widgets/mixins/__init__.py": "fc5176659b1c3239954ce3257c3615c5053c65e233ff350c869da5a4a356bfb5", + "src/pyqt_reactive/widgets/mixins/cross_window_preview_mixin.py": "663d644a603e6da4a07eb06faabc7f4eedc21ebab392094b3c76fa2f0abb3117", + "src/pyqt_reactive/widgets/mixins/selection_preservation_mixin.py": "94fc92d98854f4bbc611fe616033abccea19ea401ef9a3e9c53ea1017a77bd66", + "src/pyqt_reactive/widgets/no_scroll_spinbox.py": "f6be116d4976fc8a83ff576ef6ca0968ed95312b6333631a7705727dede10cfd", + "src/pyqt_reactive/widgets/shared/__init__.py": "e7525c16a67b93240ce3597d7f6e6b7b9bc9841d6e4b2de1084d10fd6aea2c4b", + "src/pyqt_reactive/widgets/shared/abstract_manager_widget.py": "92f8ede35a48392112c6c1aa17e959c3cc4a9450e69f5b1447cbc4ef5498dd5b", + "src/pyqt_reactive/widgets/shared/abstract_table_browser.py": "478feab4d742bfbc4170df25e5e3cc3bc0b409506192290817a064250dd9ff5d", + "src/pyqt_reactive/widgets/shared/action_tabbed_window_body.py": "246128cd317deb382bf830a67fc6468f768e6e5547ff0e17896b16e304371ecf", + "src/pyqt_reactive/widgets/shared/base_form_dialog.py": "9b5280ef06b1c8421b3889c1c477fb311dca3b5f960e8ecfb58a0a7812551369", + "src/pyqt_reactive/widgets/shared/base_managed_window.py": "3fda59f66502bfb9ce14565981f2c8738d074c78a08ae3afc9c8e00e93c882fe", + "src/pyqt_reactive/widgets/shared/button_factory.py": "78b69f821ad925773ca049d0c08715375a4d38eaf7ed075701417401b461a14c", + "src/pyqt_reactive/widgets/shared/button_panel.py": "280f904f101681461a8899b0dae53fd10e0f616e54511ee09c3324c2a589ee43", + "src/pyqt_reactive/widgets/shared/checkbox_group_widget.py": "593b9a2c1358556c710a9a3b406042e6e335cee5c46aff1e1ef42e5025c36a4f", + "src/pyqt_reactive/widgets/shared/clickable_help_components.py": "c480f45796f893e8f9431198a0d08f5acfee3ead9034764005587aae88902e4d", + "src/pyqt_reactive/widgets/shared/column_filter_widget.py": "7e492ba7cd8d2d91ead001be293bdd69dc6a98332102e30ed45d0bb6968be589", + "src/pyqt_reactive/widgets/shared/config_hierarchy_tree.py": "150062486b1e4d092ce703cee83a3d400097267dba3670d9a0120378f0045b34", + "src/pyqt_reactive/widgets/shared/config_tree_contracts.py": "8e6b4b0047219c2f7c8297de1baf7437b2571016bd2d82e1f2e1c520bc4a35e0", + "src/pyqt_reactive/widgets/shared/detachable_action_bar.py": "37157c0874e5495d3fc5c4b33042d6334b439a57d912e354ff48529e1d434ef1", + "src/pyqt_reactive/widgets/shared/dirty_window_presenter.py": "d0171ce0b9009780675f8eee0b528fe3fd2446c8b800934a476778e1c554a147", + "src/pyqt_reactive/widgets/shared/form_window_action_header.py": "18725af2a223d9c00b2496f298f745af1628c5e0cd07a2cbfc8e39b684b574c2", + "src/pyqt_reactive/widgets/shared/function_table_browser.py": "654c394929b744ca3fd6293fadfced3966d64626ddcf5f8b74f46e63cf026340", + "src/pyqt_reactive/widgets/shared/image_table_browser.py": "3a9b45805225021f6f17174e06985ab19f75b473bc0dfbf3ecfa7a5eef9931e2", + "src/pyqt_reactive/widgets/shared/list_item_delegate.py": "282c8fe0c88f9bfd51df7eb95c739a8d428d2e9d99478796bb073c698f5907a7", + "src/pyqt_reactive/widgets/shared/list_item_text_rendering.py": "0d997923c78dcb363748daf685cdc4e93ef5d6a4ee1f7d74100cd5266cdc5220", + "src/pyqt_reactive/widgets/shared/manager_action_controller.py": "e21b2407ec7cf6f891a7a67b2ae554e741f4a16aa35537b31e3b9992c009b317", + "src/pyqt_reactive/widgets/shared/manager_item_access.py": "21540ce53728a72028cc3c9af770bbca5210aa7c18bd4f697cad42bf5d27b276", + "src/pyqt_reactive/widgets/shared/manager_item_display_builder.py": "fe1d01ba449c69a052ed796e9c6901b080e2982b565f3ae0e0d73ad6599da45f", + "src/pyqt_reactive/widgets/shared/manager_item_hooks.py": "3dbb99e17114c9869db7a4cdf9284574921f4865e10f90a2161877c41da6c222", + "src/pyqt_reactive/widgets/shared/manager_list_updater.py": "c7036bb3f37198b06f5af6859c7bc6d9a0a85520da126286d31f00750a93b50b", + "src/pyqt_reactive/widgets/shared/manager_list_visual_state.py": "ba8a54d4b682d7bf61c2286439894d59eeddebf160102f4367038866cb54a2bd", + "src/pyqt_reactive/widgets/shared/manager_preview_formatting.py": "b9057f97f87f757272027baf402740db99da945fb04a4985ad0df5dc11f29b24", + "src/pyqt_reactive/widgets/shared/manager_reorder_controller.py": "64c829c64193e6cd1b604fc883fc0406fc43ecc329adc5066fa93196e2d67551", + "src/pyqt_reactive/widgets/shared/manager_selection_controller.py": "900fcec4c0a8e61b60130cc7a53ef8b0fd157d906182f96aa85fe57e5f008d46", + "src/pyqt_reactive/widgets/shared/manager_state_binding.py": "5fc9598ab1319cdf7048ab4e20c36c9d4c64d6ba7da584efc039e6c333cf888b", + "src/pyqt_reactive/widgets/shared/manager_status_controller.py": "b0e9fb4010775206b9b66889115be864d22f8e060b67bba2ef30dc0cccca4f9c", + "src/pyqt_reactive/widgets/shared/manager_time_travel_binding.py": "4cfeeef4badfe945c8ce2bd0e86254251fe79c5884261ac2d3f07c649ac3bda3", + "src/pyqt_reactive/widgets/shared/manager_ui_scaffold.py": "1332a8b69c8c562fd49146ea29a6b9aa5e156c90db06a00731e7716788e84589", + "src/pyqt_reactive/widgets/shared/manager_workflows.py": "e9e107b3129aa6f8725031abf35b2a67a52840643efdd120554ec78b13ce157a", + "src/pyqt_reactive/widgets/shared/reflowing_vertical_scroll_area.py": "7b04fd0dcd92ae470f9b6e7005bbae811ad3a614ddac86592381d840269c4387", + "src/pyqt_reactive/widgets/shared/responsive_groupbox_title.py": "a195a584afe51c456b35038a55d63c9e8cef708ca559df01d6dbbe42e399db0e", + "src/pyqt_reactive/widgets/shared/responsive_layout_widgets.py": "4adf600b914cf0769ca37c490523f744ac5fc2e5809028a6a24f21661e05359c", + "src/pyqt_reactive/widgets/shared/scope_border_renderer.py": "ab5a8246f3ca1d88bcd935f76fe59a33782e8ccb6944aff943386fe06fc46268", + "src/pyqt_reactive/widgets/shared/scope_color_receiver.py": "8b75714a16d6cf90d0f55c68f2d7d0935b0a1fa906ac2fd659c3409406568905", + "src/pyqt_reactive/widgets/shared/scope_color_strategy.py": "aa53231e9830e66721ef637177ae1470403cc9a13519015babdec8c824febffe", + "src/pyqt_reactive/widgets/shared/scope_color_utils.py": "d4c317df9c3d1391c3f8768b2bbd041435196cc876f16be2f929b63cab8aae10", + "src/pyqt_reactive/widgets/shared/scope_style_applier.py": "cd649a820631ceb367444c38ef3722489308147f9857f451cb95d5373d9ee763", + "src/pyqt_reactive/widgets/shared/scope_visual_config.py": "51286f8aa2f960e9d656f11f5ee57a11f1b2e1213511d0d17b46ad7be3b6fa4a", + "src/pyqt_reactive/widgets/shared/scoped_border_mixin.py": "f291c293302adf863574b6abf398605687d0325a8e3af16c37abb929d3fdbfad", + "src/pyqt_reactive/widgets/shared/scoped_table_widget.py": "cf81e6a8075ae89b10a46dc0e262de9f465cf5a0cd25a915dadb91dc0601a2b0", + "src/pyqt_reactive/widgets/shared/scrollable_form_body.py": "5a00352eda3b8e7932a878c18ef6ceae68a58358d15987dd8155702b70afd20c", + "src/pyqt_reactive/widgets/shared/scrollable_form_mixin.py": "e1ac397789a515097f464fb72b9378ba91e5dfbc6bfb6c316054049004fdcffe", + "src/pyqt_reactive/widgets/shared/styled_text_layout.py": "f174770e600b58e9d7b22f10db7a7e176d11207d534d090926e747af2ab594bd", + "src/pyqt_reactive/widgets/shared/tabbed_form_widget.py": "f2914db1586963f3dfe3cda7d6940d13b2bbb7ec1ffb4bb894c3844443b34a4d", + "src/pyqt_reactive/widgets/shared/tear_off_registry.py": "b44b1d391588d2290ff50b7f73379a875698e61de926e2ea153470744399091d", + "src/pyqt_reactive/widgets/shared/tear_off_tab_widget.py": "6b4fa8aebd7134cb2451135662f6b9aaee39a688c7b3ad8b37639985c7447fc7", + "src/pyqt_reactive/widgets/shared/tree_rebuild_coordinator.py": "deb28fea82fc963f999876dc54b37a4d0e310995bb16a1892befebdef79eccaa", + "src/pyqt_reactive/widgets/shared/tree_state_adapter.py": "ba4877d3431f4cfe344c530493b934b6d181254492070ca210fdcf2ed07f148c", + "src/pyqt_reactive/widgets/shared/tree_sync_adapter.py": "0031145c849a7ab6f2c4787bdc44692306073f08b2c17bea9b12932c45698f07", + "src/pyqt_reactive/widgets/shared/zmq_server_browser_widget.py": "35a7bc0ce56461336967a2a3f2e357edf8ccae122beb179d9dd438c86df105ae", + "src/pyqt_reactive/widgets/status_bar.py": "7acd1e194c1bdb350e2dd288adc9c55a37b1027fed97df0c5fb3e77ed794c940", + "src/pyqt_reactive/widgets/status_indicator.py": "96cd497e82336c02feec80dcc7c1bbf2e81336055e5b933f1cd1b96b240a0489", + "src/pyqt_reactive/widgets/structural_table.py": "4a757027ced7529f7b4f7737c09dbfa2d9dda48e7944b5375566299d3315f346", + "src/pyqt_reactive/widgets/system_monitor.py": "620f54ccd75eeeb0e370db4d461cfbf82f3625b898d343bb7c214de1cb5d671b", + "src/pyqt_reactive/windows/help_window_manager.py": "3946e3c1f7a99e75f86c87f44fab0af1282454dbf58dc95db14bb5a3fd66c7d2", + "tests/__init__.py": "db6cc46f8f6d69c35c72a39f4829527f661a63d906f091bc59a2055943a43281", + "tests/conftest.py": "911c65976a80aa48405b9c6bf0b18830599a2185c05434cbef8b7c25c4a48b39", + "tests/test_accent_chrome.py": "0862933afb738e2866611167a7c70e412d15e72ea46f9712a0b641587cadb709", + "tests/test_action_tabbed_window_body_lazy.py": "6f5b16efb53510e2a2806e1e412d89438c5e93c4efa5a9568439daf5a9daf9e8", + "tests/test_async_operation_executor.py": "2c8bc81bff7a3fd8879b6b79d2e9e09a3a22610fbb3785d970400e5e55dcd84e", + "tests/test_ci_action_pinning.py": "a636dd4d70e231d06c5de2802afc89c389f678b3b49a40ce8463eac5b0c308dd", + "tests/test_column_presentation.py": "aa83341b1443c131637e5b2b498b42cfed858927fa3f7ded00885e03f38e688a", + "tests/test_compact_dataclass_projection.py": "b765e0b8ced31a5cc351125bb2d86670e30cc973779c0b6931c75b026a14b271", + "tests/test_core.py": "a2aba8923fd176a75a9e94f5c3b1fa0a6d18f41b28935b0c4d39ed75892894fb", + "tests/test_deferred_callback.py": "1c59913d1b19a9725e5735bbd342dc115fdf4b555863427169b5001324e83b1c", + "tests/test_enabled_field_styling_ownership.py": "d831ab09cb4a0b92c3c33e4f2c368874a454c85eb8943d9acce48388ea961263", + "tests/test_enabled_title_row_ownership.py": "01052fea145b31698ec29319f521179adda5945ce324f3d976bdcc144aeb5be4", + "tests/test_explicit_function_pattern_overrides.py": "69179658cd4d89cdbf8bd469166e819f3421c39a6fcb6a38cc330dbbcb592a33", + "tests/test_field_mutation_authorization.py": "cab1faf400063e32df23249457738b9b28f59c760a6fe4d3cfcbba34884800ea", + "tests/test_flash_destruction_lifetime.py": "a567e8b978182773d316e9a7dc7934b80da2708e2804bd8e84898b4b1a030b37", + "tests/test_flash_mask_rendering.py": "9ca43be6bb9bb98a6950e9025688a6f40d8c7d050dec2bbe5721c215c47122f8", + "tests/test_flash_playback.py": "51438a64b08edec35a54447dbf3acd8aebd77a8dde92dfd7ccb3b0755bc196df", + "tests/test_flash_scroll_ownership.py": "f660469075d1c96e1fdce5bfaa0bd94857001d5e4f9ccf33fbae9ce9b2a51072", + "tests/test_form_build_transaction.py": "fc9860a115de764ee14d2a246fb5dd2d66bb60b159ae7f5853050d1e28442417", + "tests/test_function_list_editor_tokens.py": "5eea6d8a08f76c22fe81f4fca16b80d82ab8bb2ccc6f21428e5eed739065741b", + "tests/test_function_table_browser.py": "e0289caa47564647e1952a8592cda03840a58b11fb10fac6e6db9b35b1045541", + "tests/test_future_completion.py": "fddee2a4c4b210e43c137a24f5730c0e8303a38ecc67afec7a4ac5f39d2ad860", + "tests/test_help_document.py": "b28d3fb53b253372cdd4e5fa542faf5ef9a2cd7f6b42cd3a3694c2713f4f5768", + "tests/test_help_document_browser.py": "b04eca203a5d7be904ef86f490370bb71b598bc9f9d8f2ec441d118c4baa5d71", + "tests/test_host_provider_contracts.py": "ccc75dee1cd12eb05b29debe8da84a9b98a4714607bebdb9ad75c901f24f846e", + "tests/test_inline_dataclass_chrome.py": "e2c62d17ff58ad6dbe3dc2ec7e8caa1b8eeabc7e26f92c27a1a4489d1b12656b", + "tests/test_key_sequence_adapter_lifecycle.py": "bb4e57ee2548822f06252bdf07a9e3f36d569a01165f8707c2687150e4364824", + "tests/test_list_preview_wrapping.py": "60bfe5b16e9f17f3fed1f88213e69644ad2bcf278d7012b446689fbc75e181a2", + "tests/test_log_viewer_lifecycle.py": "4c7372276b7e65d84568c01489a544df7b9b2df791910c21cba11ad9c0eaff75", + "tests/test_manager_ui_scaffold.py": "cef43797060e94628d04fc581268ccd7e054cd5810b37105a68ce7930594eba6", + "tests/test_nominal_contract_architecture.py": "d02bb10514bab0bb58ad9585f986e208d88365838597eef520186f066685867a", + "tests/test_numeric_roundtrip.py": "5975b0ce8bd7bfcf1b21bf03593b72882c775a7982d834894081b6012c556566", + "tests/test_parameter_form_service.py": "30bb0cbb28b6edd5c144a58cc30a4e9fc068f9e56ee2dbe19e39f2b64fd1982d", + "tests/test_parameter_form_tree_index.py": "d28a29f77a68a22b3fce087de2c2679f2a291dd6a66cecf94f68ac700c1fded5", + "tests/test_parameter_help_service.py": "d41f813fbefd005003ab90a6e83d44727cc1adf925a9d521095068cbed2355b7", + "tests/test_parameter_ops_placeholder_capability.py": "8e4006e9b04df1efbcc4d878a68505a7057b83d6ac44e5b47dc1ea0bdd9dad6f", + "tests/test_path_name_roles.py": "86f5ef6cc8677dd92faa349d6e3254bbf6edc734e31fe0e13f8436dc4283c34a", + "tests/test_preview_configuration_imports.py": "7bec54d0815d88d2dce8dea9b73febbf23216b32c06666eab34e1487f0ec142b", + "tests/test_preview_formatter_registry.py": "9b62974f3e42be630f8f38a72d894f937affd6a694fe6f13c71629b5a38104ee", + "tests/test_preview_formatters.py": "4d6bbd9ada4ced6e6d2b49ff8c582d30b6341dd3403ee0c458a6bc4f0e595e49", + "tests/test_process_launch.py": "49c14b825f7fd3f7f7aa51f727c436c529cd2aaabefa3bee13e9f9e9d955e489", + "tests/test_process_signals.py": "de4d134edc1345bfe53df7f8b4ed362a24e4b3da35cfbc2132c5f6fb5107a662", + "tests/test_protocols.py": "3843eb80885957639b40d1cd260c87d0680df3a63d6ac448a084a25e8a37d8f2", + "tests/test_qt_types.py": "c24a5242ec6395b8c72723540e4b100e41f573d071444fc8c27eaac9a42de911", + "tests/test_reset_flash_changes.py": "3b901bd26e339f4585d90f25a70957fd949c7bdecbcfd56c0100c5a80f006d4b", + "tests/test_scoped_border_mixin.py": "9914504211aeaeb0cc713b9aa2c11619a00bfc53dc8dc6aa68d2d5ce3bd2f538", + "tests/test_scoped_table_widget.py": "764ed376f2e284682ddab7ab8379071d19ee703c391ea8196562b8f7b5b9a2fb", + "tests/test_scroll_area_content.py": "4b6f4a9e1fed02a267e2c707b1618ad3de8c451fe5e44b2f404360ef00e579ca", + "tests/test_scrollable_form_viewport_context.py": "d445d86f08bee9cc364868d4f50c976ee95bd85827b02790353c9f7439c9df7b", + "tests/test_status_indicator.py": "78f0e0b01548b8d676fd6ed75c79a800a4399826dcc5d2770ff3bb49a7e2a85e", + "tests/test_system_metrics_sampler.py": "524776fcf817233d461402b0f096623763f119a5d690a738434b390c6c752b07", + "tests/test_system_monitor_config.py": "2cc61be642ee7c9e26dab9e13497800bd3f85490a5253e5634a1374d4fe8287a", + "tests/test_theming.py": "bb811e5a73ee0a4fb95a0b87a40165442c39e3afdfa6c6ca6c0061590dae781c", + "tests/test_tree_state_adapter.py": "76d12b348ed6a015cb9b8f5bf3173968ca1b17a219df21aae82df23b12341907", + "tests/test_ui_thread_dispatch.py": "3478d122a45ee394edd3ab191f46636c62fe3b1ba1705d80e48c9e764b31e8d7", + "tests/test_weak_events.py": "d752129493c8cdd1d427024c64c7f02cd37d0e7a87bbf854b1ed332d174fa710", + "tests/test_widget_projection_safety.py": "f124cf36ca48fe51bae0aedad633f420bccd469ea8d9059d82d64dff3ca841e2", + "tests/test_widget_tree_projection.py": "3d6a1224caf0913ca194b13f4cf791cfcb3ce9e9c38b88281fe2dac4a0e222ef", + "tests/test_widgets.py": "f24b7bace49d7c6ce5357d38e9b5362ba0625b9f4421222788ab661ae278ed19", + "tests/test_window_code_document.py": "5c546e6151f19a8bcebfaa6a332a301b888416236baa982e2ae761e015326712", + "tests/test_window_navigation.py": "0f2bd021ca9d547f2b3d64e42cba1d13d0ffb1ed52e86bd07df836d5d3f649e3", + "tests/test_window_snapshot.py": "33fa1d5509217b0c16e926f7b4643ae1d5176ac77434f16099acd9b7cd52b8e2", + "tests/test_window_snapshot_bindings.py": "44af6189d6bf64bcfcad9e87150d2f01620e2a2c0b4877d581f64428176decf9", + "tests/test_zmq_server_browser_lifecycle.py": "2789e1a2cd5c81bddd37b62eb5fbe507228972e9f0dac0d1ea06b87dda066579" + } + } + }, + "actual_import_dependencies": { + "external/ObjectState": { + "path": "/home/ts/code/projects/openhcs-shared-runtime-plumbing/external/ObjectState", + "head": "a01f8939b9bf4659502a33fe3febe62cab0f3c9a", + "status": "", + "files": { + ".github/badges/coverage.svg": "5f87bee9f8cf0a67b8cf6e1eceb1fcbb1bf57b3875e1979527faedf2ab2a8c9c", + ".github/workflows/ci.yml": "f93e84bd08fd1c8254b7b863cb2d99c74db85fb8b16bb3779a698b166f660d90", + ".github/workflows/coverage-pages.yml": "222698b93c43d62c6a301b00429084c6b64efaeebad41df2c29949205992057f", + ".github/workflows/publish.yml": "9ce34113e7ff654cfa86cff6018299a98fc03a8851f0b9899777e90bb1bbee9b", + ".gitignore": "a26875c24503b80c1699f99988e68bb14ef3f4834d476864139553a63be5148a", + ".readthedocs.yaml": "2f5648c67a8a1ba1db73ec8dc32fbb82ffb83b246671e1dade4cb8836d667c6f", + "BADGE_SETUP.md": "8395730f7b6a6b5c7c97de76802bd2513bca8cc29a4dd8fcad7a91d4f713b097", + "LICENSE": "c5a804a1e4c08f5593eb8466c91dc4e871fe79319d81737a82a3cc514b7b2ff6", + "README.md": "1d72de341fc17c96587a75bba4a913787321bf598b848474a833b34a0547842e", + "docs/.gitignore": "79b7c477e1d731b44f15770e60642935f79fcaffdad433be5a9a82d6c790d66a", + "docs/Makefile": "43df277c1f15a11e9ab7180cfaa9e65e014571beec43b19530a363f4e62e0cd9", + "docs/api/modules.rst": "bb05baa62c63a0ed6a5773ca4b644de247b91486f65cb1f18e4671e3cce2a595", + "docs/architectural_fixes.rst": "278558dd9d4bd42a347147b22b5f52262192f8a72819fb4365055c2e5c5b4448", + "docs/architecture.rst": "2523332a130b3e06aae51f127543261788b208e61d0646ddbc99104d0383266e", + "docs/archive/DOCUMENTATION_AUDIT.md": "5d2ba10f1b5f3a6f20ec74ffb10ed6358866f193737a71ce4ad2af025be12807", + "docs/archive/DOCUMENTATION_FIXES.md": "89d8f817cdfc018af56c879cd5edad906acc566b2444404a94f0b00c65eb007d", + "docs/archive/README.md": "e5a389ed4d06d204e0af5dc2502f5b661804108beb404c03fb8e5f71ef0ec820", + "docs/conf.py": "bc49d86e265031fd39550513524727428685c836d73d5434a43031db4490301b", + "docs/examples/atomic_operations.rst": "c3b719cb22ddda78a4d7836d6bc7c67f56482cfeac6c8cb3823d6666f1e3b428", + "docs/examples/basic.rst": "487b5fcae6e2d98480c39bd613f17552bf95568ad1bf85168113c4872667094c", + "docs/examples/dual_axis.rst": "df17e2f8efa9bf10d1ef12cfdeb755504395b11c5fb66a22c414dd2b3a49c05b", + "docs/examples/index.rst": "0fd2ab7c22363ef78158d43928779b83b3ffbd0f4153162dd77e7e0e323993c2", + "docs/examples/ui.rst": "04d9c0c1354730c98efeb16503e3b8d31ff569ef145e09dadf60dc07991cbfe0", + "docs/index.rst": "bb8c0eb64c2e2e57ef490943cd88b0b11b1b58bd6760bcaa6b7327766a49b2bf", + "docs/make.bat": "ed525cc4d41b62b8b96d4b62a44ef32d6ccfd9324a379a82d5f71b5fb6d57776", + "docs/provenance.rst": "f7b883738fc949f3b5806dbdfe33f18715dd81dae3f98aaf68a2b8c021b5d245", + "docs/quickstart.rst": "318fd7296e2f4c3be4907cdd39b6bd89853af2132baa7df14d569b77ad7bd55f", + "docs/state_management.rst": "4a12f5b3e3438f66fa8135de44127c2feb51da0f8973cee39001af64f83a97b3", + "docs/state_propagation.rst": "b1483c16840d7ccc928b2be19e71a54166650b10d6960e173d72a116f842a203", + "docs/undo_redo.rst": "6355cd0faa82c2261645287ff1c4aebc8d85dee31c8705e698b2b0b53c0cf3ec", + "paper.bib": "4ed4113559097ad565ffa3a098e126a830e0bac39afc7798bf61d12f6b071876", + "paper.md": "f44f04ce7d7f1f037a34a89f560825a9666fb1c4c31788412cf948b2195fc41d", + "plans/n_dimensional_inheritance.md": "e55beb4463a8f0bb2d699618a44f16f3bac7f9c084154c4efc9567ebd1ac94b3", + "pyproject.toml": "a0d2835c65fe7c65b5f40259cc1d9527095bf4a3b87d1f70030df6ef9e7ef16a", + "scripts/check_ruff_added_lines.py": "e88118d5e1fb5db477cc11fb80f594ba213482614c565f3ed6383d992995c81d", + "scripts/release.py": "52abee369fee146ee8cf83fa266b5f1dbaacc31bb5934e967265054dbc7561ba", + "scripts/update_and_release.py": "80896e49f856d6a193bb1dc3917f55d2878acf1476c8240333eda16b92e8e61a", + "scripts/update_version.py": "63d0085d6b72ff34bf9560f60e4a91351ba21416b4eccd9b48aa30d40c49022a", + "scripts/verify_release_ready.py": "0a8d8fec9af999c3bba6281f2cafdadd06365090536580c8b168f43c96674dd2", + "src/objectstate/__init__.py": "b972d45dccc8143cf141b31582fc50d2a9a1f992f3fa374dac2b07e1183ccd73", + "src/objectstate/collection_containers.py": "a939d07f5dc391b7131f7ffaa7d1390b0449b4931d54ed5b09b27f59056b6e9b", + "src/objectstate/config.py": "6db2d9de077872009f1d6defe351cf8f40e441a0de31ff574b02741cefb4ffe3", + "src/objectstate/construction_binding.py": "9cfed94cad1cc9d5cf2cf0f7e8f534f72192168bdf3ba90b2b8d7ee57f4950d3", + "src/objectstate/context_manager.py": "a01b0fc8aeb9b5b0888346f725f92cbfd96b90963bf20001b5414a5e6cd6c0e6", + "src/objectstate/docs/configuration_framework.rst": "75ef312d77f009aef7b01b506797efaddb8fcd3e16b958bf7a74e752df950cb6", + "src/objectstate/docs/context_system.rst": "fe834139f63b1a92665d12ec768ec1cbf216b355dac3b12905d31081df4e0824", + "src/objectstate/docs/object_state_lifecycle.rst": "7a9b977fadc226e0fd03cc17e1ac31cb807f23b2b6794462a6f3f7ff80ceb4aa", + "src/objectstate/dual_axis_resolver.py": "17fcda62912050bdcdd018bee2fca5f81c2f4940c5f27f2af3dc19b7691e1fb0", + "src/objectstate/edit_session.py": "003cfaff8d3687b0fa115e051c3cf833a3cbb21ad9d9becfe16c59359d656350", + "src/objectstate/field_access.py": "77f483322a504c392c0ca41cc3ee6b691d258396f70f00a61b98d9f4bc03d43a", + "src/objectstate/global_config.py": "a8187811f87806579640a8147f287114e7127401530178dd37e8d28ed788a6ae", + "src/objectstate/history_migration.py": "1c24097040e38c159ea7e809424c0b94ec6c34efa3dc84cd664ff6a2f790815f", + "src/objectstate/lazy_factory.py": "64ae924c176757863a84eef19b9df0eff516e32443c2661134ec2ae72f234bf9", + "src/objectstate/live_context_resolver.py": "bd169b0bf2e9eea833ffc1d28331d320b2386193652fca041387f32bf018d71e", + "src/objectstate/object_state.py": "735ce860ab650b281534e24e3c7354f37b64bb06832cf186d6b1f626f87108fe", + "src/objectstate/object_state_metadata.py": "4c5be42e313e3f896f2db0d3f89ca18ca6ab09b9d25f202a7b2af9e59a099ddb", + "src/objectstate/object_state_registry.py": "f848746a9c2a023a616c37531691073ea7bdb8d28d7db3bca5c8a3dcb176107a", + "src/objectstate/parameter_owner.py": "908606ec2da70f223a93071e92ca24affb3f172f7df14c64dfd27974f1fbb85c", + "src/objectstate/parameter_structure.py": "00a786af17bca57b4e819d39c76a6523af3bec116d119c323dea8c2953c7be3d", + "src/objectstate/parametric_axes.py": "7229b518f8d5cca522a3020361c16a601f3084bc5a7d34ef92a2fef11619e40c", + "src/objectstate/placeholder.py": "4e057cf3225dd38ef3859f20fcd1305526829f044af45ef616381ea734df8f67", + "src/objectstate/reified_generics.py": "39ad0897ec45ba929c0fae694dd870df987c06e2b005644189212fe224eceab2", + "src/objectstate/snapshot_model.py": "001cc83d354b040763b2da9aad0014619fdee22c82dce622dbd93ecfba780681", + "src/objectstate/subfield_semantics.py": "97aa6a7f2659260b1950c068f47d9bccec15087ed1852f1dd182272c8230c14a", + "src/objectstate/time_travel_profile.py": "c59d157f9cb85ee29aa64e18393d916ded656c6ce88bb46782f44e2463fcd007", + "src/objectstate/token_cache.py": "d6b7cdb71c15e13e8bab7271d70e0b96ccbe0825004c45c4e88851ee30a9435f", + "src/objectstate/transaction_checkpoint.py": "4c105f2078bf23c9312c600543623b9456de256c7093b9cb8a9b753b49b67530", + "src/objectstate/ui_visibility.py": "be0c60b66b86ebb696583ee394c0d45b05fd1f68a609a6aa0c29aa1098dc2592", + "src/objectstate/value_semantics.py": "4b8a43ca8667c592b74f0d46b8507ef8331402e376230dd7cac217870b2ae2f9", + "tests/__init__.py": "b0f08e026c34c8ec04203081ad79f7ed1c6cbabd784cf475333965f279ea33ae", + "tests/conftest.py": "0fa840dddea79e86afe717e146ba7ac7ee713b032e0e398b0a9f56842e4a1d62", + "tests/test_callable_baseline.py": "638929e6eb97a7c899443272e307beaa62a12922debdc7594a45ec0a85da3356", + "tests/test_config.py": "973b08f470fd54a4990cf39c7f176b9f5d02ed0b808dbf8ea2549a01d7b30d77", + "tests/test_construction_history.py": "88469c8157d4010b9877f76b7a605bb7222222d614e1ea22aeec0c39870d1609", + "tests/test_context_manager.py": "c14d3ce760520544cda14273e76df66606e47ae1bba410f83746b216b4a6418c", + "tests/test_field_access.py": "7484f7b0127e1be332ac013ea5f5c61ec5da9f2cd7fef9fcde8a8e8ab1384c54", + "tests/test_global_config.py": "f4624ffd22cf4d09bc05466dac91cf952e81033b34ab475bd1442c3d96887833", + "tests/test_history_persistence.py": "547d0cba078c2caec8b6217ec0b8713aac1ac2efd69d143e6549b023e5cd4d33", + "tests/test_integration.py": "df082bcca0ee4d6cf1c3899b4e43ccd2a004891446a05480bcdb1930ed467c24", + "tests/test_lazy_factory.py": "b3f1602d85a6e91a1b4e42490dd5253d2d0c43371a193d7369fe149bc839ceb4", + "tests/test_object_state_metadata_contracts.py": "dbd555d1659a1dd79c8c93c1d5168282a8cfaa3d727ac3fcc4d7d913eea00360", + "tests/test_object_state_restore.py": "3319b7244e0be237896902955f4c49829d4135c057116885935791bd8f125a81", + "tests/test_object_state_update_instance.py": "aef1aa303a2f3d68246f87a1a6871c0e0e48cc12d5574ef105be5aa3202a7526", + "tests/test_parameter_owner.py": "b28675a55ab72d8fdf011f972e3c5154fc683fc8206a7195342ac0a774bd873d", + "tests/test_parametric_axes.py": "8717ac074d559294e00a3d33afaac6415230efcce78f38b1e296a11c9596dcce", + "tests/test_placeholder.py": "1b9d20e8931234622ef1cf726d12e8e4fa00328c0b1cb33e9eb0fdf7acc1631d", + "tests/test_registry_subscription.py": "c634b5e416a7ea3ca04abcab65679c2852595c595b6e23cde6424ba9f6c7643d", + "tests/test_reified_generics.py": "548f5b6a282fc26b73cb5211fc8a9eb92f37fe9cc57e37cf964597293fe98866", + "tests/test_resolved_change_notifications.py": "d0addec005be985da187845efba11e1dd03367b5b9557cc1fcc54a119e42c66a", + "tests/test_subfield_semantics.py": "f156d9666a909abe9c9dbb8f69823b1e88edab310248687c4e012ab97e5a43ec", + "tests/test_timeline_projection.py": "e02529d96f61f3601c91fe30e133c4f78107d3ea6f65c864198b232b8ad76780", + "tests/test_transaction_checkpoint.py": "531c4df6b3fa25a28ac17d020532179877d4f53d66f57af1bff7beb07d08f89d", + "tests/test_ui_visible_field_projection.py": "27e016e8755374e9322460b83344836f9a3f1f0e54e9303c4b16e6abf775baea", + "tests/test_undo_redo_cascade.py": "5f8cd61f70b966ac506b5a2ba4460d58c789137f7f992dd7537d572efdcefc67", + "tests/test_value_semantics.py": "fb98e8637abe45a981faae3b61fcc9ddc14ef84b4ef5e41b95fa5736bdae2968" + } + }, + "external/python-introspect": { + "path": "/home/ts/code/projects/openhcs-shared-runtime-plumbing/external/python-introspect", + "head": "ed1e4eb15f7a0bf8315274d3e47858fa2b4372bb", + "status": "", + "files": { + ".github/actions/validate-python-release/action.yml": "ba8808c30bfcf27ce6f9dcbbf12e60af274f4d309c8e409eb3cd93ab2809436b", + ".github/actions/validate-python-release/validate_python_release.py": "96275b643ee03adac9a13996006cca803ec07a0f0d0835d47f559677af016fbc", + ".github/workflows/ci.yml": "da7bc66a370447d0dfcfb7de4ff723fe92fa3f6b34219066bee30efc3fbfb6a1", + ".github/workflows/docs.yml": "62187d940fb88d0ff42ba69a2aa1127462b04424441a039097fe956934fd0293", + ".github/workflows/publish.yml": "3dbae1430de7372a6638d339d955bc036d64b88c968edcf96ea578afbd4a4d17", + ".gitignore": "6ec409ab221e797f3f6d5e63ad9dd8e7bf09913976f8dc8a0268ecbd3cec280d", + ".readthedocs.yaml": "c95cbaad19adbbe07f93650fdb705f22629d20dfb51296e6ed566f280abfe6c6", + "CONTRIBUTING.md": "73bff7a6b106765f0e3a2f9df7e9c4da7b6243f0a9522d737ca3ed33ef16496d", + "LICENSE": "c5a804a1e4c08f5593eb8466c91dc4e871fe79319d81737a82a3cc514b7b2ff6", + "README.md": "d861f8aff2e79d9e77fbb2286b2f353c5b568412482a57efce9a488e58d543fe", + "docs/.gitignore": "7f1d49f8fa7578f81fec5b86469e14d2cf3a94ae0d85abe6bf5373f5f760ac42", + "docs/Makefile": "e33bf74d59130029ba24168a813112dd923d7044d780ce942db6645d93f56acf", + "docs/make.bat": "d2a8ebcce0de81abca77b2cbc26afd00385a61fb624e7060a8e5402fd900a72a", + "docs/source/_static/.gitkeep": "bacb37455f4ed8710d244dbe230509e632f06da53df6f849443d1deb2315dbff", + "docs/source/_templates/.gitkeep": "b32922d255e14ebf318fa34bf4519185b2498b279b89cac175150eea8e9ba76c", + "docs/source/api.rst": "f2743f0d134d71d8799d0ba4d995bf2b378234da40aa9a5008fa78b5cd2d379d", + "docs/source/conf.py": "534c6205463a628d4d7d58b2793d95cffea83a6ec2c0085b012a29b046c398ea", + "docs/source/development.rst": "fb7c848d6d5b1ee40acf259a306a20f7380c4e6928e76df938978b565e6d416c", + "docs/source/extensions.rst": "cd50883eb7b31ac94ea55b1dc662a637e844626c1950cdfc2c0cd211e776ac0a", + "docs/source/index.rst": "c98c4bcc2623fb87c1b4eb0858819ed11efa3618fd0fa56e8df3dccba415d6a9", + "pyproject.toml": "cd108ffbf3b756dbc3b8a1d85530cdeab0d2d0f452751c131db489a31178fff4", + "scripts/release.py": "f5ba54de5d2ef89ec7c75e536b63817989b8f9f17e3b2c10bb981b5bf8d08837", + "scripts/update_and_release.py": "80896e49f856d6a193bb1dc3917f55d2878acf1476c8240333eda16b92e8e61a", + "scripts/update_version.py": "303a09dca1b7374ed1d8e4aa26d76ec1bc76cfb6a769e002f2088927d51a6aa5", + "scripts/verify_release_ready.py": "5fe0aebbcaee7f60540f50221a1f8da1beab2b8b59d9b97f3b4a240f0e72211f", + "src/python_introspect/__init__.py": "d4e29e9b857a785f8ea1a1e26ea5317a39958a527df053677c335057671e4a94", + "src/python_introspect/annotation_types.py": "dc126879a8a581cea0758eec4df18ec20c47ca4b1e135abde23699b65b790bde", + "src/python_introspect/callable_declaration.py": "3bb8b474624fe6b2964a5c1f9a6aa540b2703500bfa124e7808995cb789d6951", + "src/python_introspect/dataclass_projection.py": "82662ff8eaa4f2aeb3b33edd61abae57020a7f139c3aa396d82dcecf8b801160", + "src/python_introspect/docstring_annotations.py": "444e8064f833f1a044208bd4a15107a6b688b597e36ab78ac58bee6996b51fc9", + "src/python_introspect/enableable.py": "025eadd64a6a6f1648bc36fbd1724f6049355a0b9b1a259443248d70170b8eb6", + "src/python_introspect/environment_projection.py": "8ab7d1e01e9a4529ea0b77d8996a5feeb94bdc2e905dd6263afb28d11bc217a0", + "src/python_introspect/exceptions.py": "4c6f15a3625d02b05b2fc8651e67e6b77d4bb442ad180b85121d91874ec3407b", + "src/python_introspect/runtime_parameter.py": "59fed2d467e751686447322f8e19a8633d964741d3d8afccb850dc504a619897", + "src/python_introspect/signature_analyzer.py": "7e25a0355159c86d361a21a7afc449ec758cbf8a32e9b72d06de66a924f04159", + "src/python_introspect/unified_parameter_analyzer.py": "ac52ab997fb385425920614451e8cc754fece0176bab4cc9b132eae8c930ab15", + "src/python_introspect/validation.py": "46fba78bd068c75c89b91baa4a5cbe8cdca9692e75a285eaf76311183df77b64", + "tests/__init__.py": "c752ad382f759406b1c9c102142aac7cc10a2693b8e5848536e44315567a034f", + "tests/test_annotation_types.py": "ae38706899ad48445224b09babff76496006576e7f188af7e27261521ea34ad9", + "tests/test_callable_declaration.py": "b8486dcca0c85805a14975c00987c7c3247c2c8abcb75545f89e04a01f738c32", + "tests/test_dataclass_projection.py": "6f662d0adcdd4531d8cbd94c2ffed55ced3a53c3ec481a9dce67979ac8489990", + "tests/test_dataclass_source_preparation.py": "c7438b14578eb59d53df2499c4db2004ff1b22d5e6d8350b0aa6ed2eb262a535", + "tests/test_environment_projection.py": "e11e2c180c605e761acd48103fcba10dada614a326e40a55d4777abcf0853899", + "tests/test_exceptions.py": "41f4a70df08a7718b4c9daa05569e6c491c0148abf975f0aeae78688e99d8744", + "tests/test_init.py": "7e9ca130ed8254034e32b991f8abd4624f6c1bfcdba65f911dc28c12d9cdfeca", + "tests/test_runtime_parameter.py": "b728307c8e6c7e50ca9b0befd1aaf154ad293eb18f332be544719d7ae9dd3fb4", + "tests/test_signature_analyzer.py": "0708fdcb848fe874eb4dae025c1c9fd9b0929e983cb1c1c53cb268af9fb37c2e", + "tests/test_unified_parameter_analyzer.py": "deb55bac9e31465c8368c6101a93e3c951e139c5c7ebd442a5f8cdc98842f1cd", + "tests/test_validate_python_release_action.py": "65fc7bdff5d20229e74e3abd44f936d7cfcaf9fa5881a7e9e65c59bbd65c6c12", + "tests/test_validation.py": "ce9cab31e33a001df0d40454a8d7de9e7a2ab6dc6296399adb234d06f7dfae39" + } + }, + "external/arraybridge": { + "path": "/home/ts/code/projects/openhcs-shared-runtime-plumbing/external/arraybridge", + "head": "1e53d03d9f468322a1c085c8be29485fb139caf2", + "status": "", + "files": { + ".github/workflows/ci.yml": "cadf2e9e7f8378357df650beb1a99f89c1ce9404aa297e8d707bff14c9a3f60f", + ".github/workflows/coverage-pages.yml": "4d6692efebe182beb62829c1bf3c19616159316b649b84cf5ba3d30d2a44599f", + ".github/workflows/gpu-tests.yml": "ab3b757bfb18d9b508347ca1be122ef2b084cad5e9baec8bd99c73ca8ac44ec3", + ".github/workflows/publish.yml": "98eafc9d3a4250b4fead850300eebd9e63dda05f162614ac28fa757a7d2996ee", + ".gitignore": "a26875c24503b80c1699f99988e68bb14ef3f4834d476864139553a63be5148a", + ".readthedocs.yml": "64f56fbb4a8a1ec0124b2704c97ac75ca57dd9d75922f0f32122ccc07d9d5dec", + "ADDING_NEW_FRAMEWORKS.md": "cc7f1899e7c7fb553acfd5f86e468689c367ec9eceb334d74f5d9a9c1845fb20", + "CONTRIBUTING.md": "e8e3288c0eda555fc5a55f93c3de958951bb5d339ed1e88407be837d5b392405", + "LICENSE": "c5a804a1e4c08f5593eb8466c91dc4e871fe79319d81737a82a3cc514b7b2ff6", + "README.md": "b108a144e2f797425a1734f745a73ac7a215bd5e58b3f6fe4342b18e7d16b892", + "TESTING.md": "04f1c2f01407bf49f4144301b567e766f920bac8c90ef7361d17a94f87935292", + "docs/Makefile": "e33bf74d59130029ba24168a813112dd923d7044d780ce942db6645d93f56acf", + "docs/archive/CI_ARTIFACT_UPDATE.md": "46a466f6398118a214279f30115db82d22f20847de0afbba6947f3a807d2fe18", + "docs/archive/CI_CUPY_FIX.md": "2c271a926da57ebe1914d8784e03dde43c372ee086e69981513962dcdfc977ab", + "docs/archive/CI_IMPROVEMENTS.md": "d52addf07d29e00a4a145e164c01d2499f0d063c4d28913b361c259187e083b4", + "docs/archive/CONFLICT_VISUALIZATION.md": "5ad9a1b04c6d1271506c3a94b6e7df36c80f146014425094ae3fb9e2f649a7f8", + "docs/archive/COVERAGE_AUDIT_PLAN.md": "3c54a04e3a7f0149f7e0d2a77fe8e734fe7b85593c6542851ed1d0934aee710a", + "docs/archive/DEPLOYMENT_SUMMARY.md": "40c0e2337792f659e92d4391ff8a7bf5f5b7b8b46f3379c8792d2e9055127060", + "docs/archive/GPU_TESTING_SETUP.md": "b9b94a8b40705767eb1d192ac6c140975aa624d541352847e3cb7a4734f22efd", + "docs/archive/MERGE_CONFLICT_RESOLUTION.md": "a0a3a028c5583b8eb4903ed206946c0b712b4342371f0aa10d0b6185e2726fa2", + "docs/archive/MIGRATION_SUMMARY.md": "22c0cc300f73972dafa606cb69a28025a2e0e73490ae35eb65dd5f45deb5d998", + "docs/archive/QUICK_FIX_GUIDE.txt": "44bbfe657f6f178cc3c148ede870ae2c4b3c756179b13f532d97132c45bd5307", + "docs/archive/README.md": "a28d7cde396af47e9bd985d54e517643b6f789410aa02ddaf878bd927a9714e9", + "docs/archive/RESOLVED_FILES.md": "fb72be1ebb40adddaff772ef4b3520eb578b3d028e8fb2bd526eaefe6a897e81", + "docs/ci-cd.md": "94bedfc598f0dc498e22e1a0d46e5399ed169595eefbcb95242ffc5d00a7be77", + "docs/source/.gitignore": "0485bfc859ca5866baa1043a32e732b18fa606ced2fcf39c27a7e7d3c89695c9", + "docs/source/Makefile": "8b6587b859607f200f116e2cb043fc358e1c3a26c326b563bf348453cfc68307", + "docs/source/README.md": "537352768576aa21efe985e7abbe266e43b1000298ae74097625173fb99588df", + "docs/source/advanced_topics.rst": "97b6134aba0c54f2920cf0b2978c09c092e8baaef09a044a4bc2b54df3acb3f8", + "docs/source/api_reference.rst": "9e337146f1fd22efe834f9739eab4135775c47bb1e3b01b57b33d4f3ec055072", + "docs/source/ci-cd.rst": "22e0ad2df054c693ee570833132daeca77b14ae494b8be2ba92fa1516ca4b3c2", + "docs/source/conf.py": "1b43c862dc93e51e93c0ef0435a9e5cf208665d84a2d0ed1c2477d5e4dbf3832", + "docs/source/contributing.rst": "b71e6e678251a155fa14980d64ae833b4c028bbb089b6b4d44b7baf280909190", + "docs/source/converters.rst": "b658d186779669a7eb367dcb60963fa57995c26ae63fbaad4a5c5bb75d10eb97", + "docs/source/decorators.rst": "3ff68560435fd1e0c1d5b3ee883f06f5bb3e6bb207f96e915556a662e4e461fb", + "docs/source/examples/basic_conversion.rst": "be11e27873b61fedfe0abd5f421817f683d99b731b5dbeb913aaee60dba5f08d", + "docs/source/examples/decorators.rst": "c6b0fadc2c3d36e6dc28e7fb50024c685df4f1e9e86a18be909c9cdfb00cad9b", + "docs/source/examples/index.rst": "eabe217c250d3301bf26ba2a6ae58fa4eff82c85c12e2e60e6ced6b533e1f295", + "docs/source/examples/multi_framework.rst": "1d7534ceeb611861f69aa7a0cc55c001b5f7949298192dc5e02f68fea5838c13", + "docs/source/gpu_features.rst": "8ab989045317cf0bc7ddac077caa93d6d7fe2a6f90e205462ae09ef03e605843", + "docs/source/index.rst": "963d5249c33b2ba4103c95fbf2c65038f82a883e561e9de7db825709bde3205a", + "docs/source/installation.rst": "89b6309bde9a0b72b4e6477e2b3f99efcbdce947374149c2dec92df7d8ccc003", + "docs/source/quickstart.rst": "99324fb002fb28a08700015b54083c97a6505f733b1329504ed302b1f003ee72", + "docs/source/release_035_validation.rst": "4afb3f121805a2a63e912fd85197b76adac82d67b6d070fa73908e38c577fa3f", + "docs/source/release_036_validation.rst": "d9f902b370403e824c8047fe1cf14aae36e3ae26de50c1aebdce2783c8cc2f12", + "docs/source/stack_utils.rst": "e3203ed0755ac5714674f9db908c3fa990205e7161baacae1f97a31354fefb3e", + "docs/source/user_guide.rst": "50e205f365a839ed956103fc93f59a546dabe3a963ee5ba2285e75ded021516a", + "paper.bib": "20228af15370805fe07f53154b27a977e7bd32ae84cc44bfdf127a696c5ca0b0", + "paper.md": "363b3be3c60859de0bed4ef7db739e6e41b9345284c5c28e0b1f3b6fca61844e", + "pyproject.toml": "8134c94077df2defe89328963c250e6defaa2ec2b5f49c190f0966e9e575661b", + "scripts/release.py": "3e9caa24fbfde769e347f52542a557610bb02249b81aaa71649bd579d081b69d", + "scripts/update_version.py": "9d57e7324469c34691c4561466cc4d3330485812ce1c14418d8034d30bd85a3c", + "scripts/verify_release_ready.py": "bff7cb1232eeceb6d6d235309cfc75866c7b6c1f44c48f5b2ab65f856830477e", + "src/arraybridge/__init__.py": "ae1b2dd87b91b191356e9e1fdb74a32ba7131cda6341402c4a7aa087d47a4140", + "src/arraybridge/array_geometry.py": "0a8a5f1a61552b057997e94e4c89a19b5615875f307949e0d11784179f2a94f2", + "src/arraybridge/array_operations.py": "7e9ebda3d058702cdf8662dc5ab1faf1254649f3cbde76fa0adcbe02677fa103", + "src/arraybridge/array_payload.py": "ebf8c9c0f4726b4307ad435203d9173e54c84a92dad06ef8aea0d56a51972b1e", + "src/arraybridge/converters.py": "ab8e04e8f835ac9cfccaae964083cb3993efd65aaa7147041b81a1e03e4cd7fa", + "src/arraybridge/converters_registry.py": "fe11ed6ddfb748c20a530f1fbcdbb7b703d6258eea1f237243f361569dde4874", + "src/arraybridge/decorators.py": "28f9bf4770bd4cc1ef34045fdef28065063ede029296f62b8a6c4140cc88e40e", + "src/arraybridge/dtype_scaling.py": "283818c82bf182c4172c9265fdbb399c7e3acd08a6b00edcd4e96feb1276cf5a", + "src/arraybridge/exceptions.py": "7e17a3e594b7f50701156ccd78d623a2a78b5673a0c30a7e7162267b6e63caea", + "src/arraybridge/framework_config.py": "e724d3b7d356730cca0cc5db73e7ea09d88597ea6228eb774949f19915d70909", + "src/arraybridge/framework_ops.py": "eac427f155ed3e7e10ae0f9cc603a9a17d7c3f54b30db339e23f33873c94fff4", + "src/arraybridge/gpu_cleanup.py": "003238c0c2e4f4620ef6b27a049b8d937b9923200da00a89ed51539cb273adb1", + "src/arraybridge/oom_recovery.py": "846bceb891d54a8a6222a6e5ce830596a5bcfe82ba66500617f978a5f1258de2", + "src/arraybridge/slice_processing.py": "e51d743a61d6f0149b245c13d5445dd92cf73f5e68e4485a236567554508be44", + "src/arraybridge/stack_utils.py": "1558a43745c6f97af2c3ed69f4c6924544d97adee42dccb48131044528dcf6be", + "src/arraybridge/types.py": "7b94760d57e54008d7adf4e4d37dca45d8a2c288a9dc8400be00c26148cf7624", + "src/arraybridge/utils.py": "46a4e1b5849c3c4384406e3dd5d03c6ff56f5138e07058702db8747ab42ee4db", + "tests/__init__.py": "a0ac3b43f372047f4b85b516a7e4471f98744cf549dbeec484ff3f79728f0257", + "tests/conftest.py": "6a2fe5d7206ac30e37e544ba8ff939ffa4576565b9c7559fd660f0e41db6f24c", + "tests/test_array_geometry.py": "f9c0e91d6275a940399418f2c0de810ca90c781c30219d720b745f1cf912f0a3", + "tests/test_callable_dtype_default.py": "52f343317d9d943bee46edf4edfa7e1dfd9afbe2e2ced2a297b9bc764bacedc7", + "tests/test_converters.py": "390edeb089747cc13a6378d715996f238ece1610f7aa4e9e0c3a6c932a23e315", + "tests/test_converters_registry.py": "f40974f61e55cc49fc31dd53af0dda1790301edc2218d5ec6dbcbda0c712365f", + "tests/test_decorators.py": "0b841b293e82b59133c895159c9773552dc4e7a508bbe7976da45aaa1e5d894d", + "tests/test_dtype_scaling.py": "7fcea155eede82781b9566166079a86e403afa075c5335118696a390c1ed531b", + "tests/test_durable_decorator_context.py": "53e4bfb9a2491b6d5a238ca4aaf24ffce5853346d46621e25ebf92adfdf70642", + "tests/test_exceptions.py": "441f10606ee9175814b01a243f8aea572f341bb53517c31c2c719ce41a36fe27", + "tests/test_framework_config.py": "4995b8649bef8ff4bfbf80dae282a6c8e008b0120f1a905f118903fa903e8d8c", + "tests/test_gpu_cleanup.py": "bcd6ef4f4f56850e1f12eaf9fc237d756b06841bd1c2d093ebbd81cf1a04a0bf", + "tests/test_integration.py": "e8d87ba83d8fec2ea7a9841c413277516bf665aa08f688b29da819a4c545edd2", + "tests/test_oom_recovery.py": "773b96e901e85663b6418d4deefbb28bb3ac9298b7b20589373cfa0de05e2bf4", + "tests/test_registry_integration.py": "49b88b4f692af569b66c298c8751a78b0e28a08f4db41034ef05684352df5418", + "tests/test_slice_processing.py": "b7f996dd6c38382148c1b48c5428d33775c3067f91562f082de9ec0ce20ebb5f", + "tests/test_stack_utils.py": "918e015e452b31adef93ceb829e949043390d22888daff9a295317940a58a6bd", + "tests/test_types.py": "ae1701b48c68c6f07bb23d83800ef3ca049545c254840ad5a7863db4213b3c16", + "tests/test_utils.py": "15a78a8269843d90b8dee4a2b4a50cd1b34bc791fcd1588260d45cae3956aab5" + } + }, + "external/metaclass-registry": { + "path": "/home/ts/code/projects/openhcs-shared-runtime-plumbing/external/metaclass-registry", + "head": "393a7e03003cdc56df9013f932ed4f26e632d77a", + "status": "", + "files": { + ".github/workflows/ci.yml": "036ed258494f0a2203882461397a9a2bc92f584b6006dbacaa77c2eb5beace14", + ".github/workflows/coverage-pages.yml": "410fdf83d1a1456975635cb4ae09dbaffaad8fb7ac8597c98268377b4c4c7896", + ".github/workflows/publish.yml": "85a7866717cc0a1285e74f751571d7d0b97b1210137b83585c66ed115619e600", + ".gitignore": "a26875c24503b80c1699f99988e68bb14ef3f4834d476864139553a63be5148a", + ".readthedocs.yml": "7506f658eb3b8583c0887684c3290910652325e33ab20f2041373feb7a89af19", + "CHANGELOG.md": "296f2431172f457518be6ad3789ed7d0ded103af4718937123269537b7e6bf6e", + "LICENSE": "c5a804a1e4c08f5593eb8466c91dc4e871fe79319d81737a82a3cc514b7b2ff6", + "PYPI_RELEASE_SETUP.md": "578df82d6426379a517799ed2edbf93298ecd6d848e39c7016514744df565de8", + "README.md": "668dab2cc7c536778ca0010f957c85335eda5eb2392e60c62d08b11d79009c37", + "RELEASE-SELECTED-DISCOVERY-022.rst": "2fa12717c0650cdccd83878b98b1f70a61ef94520acbc1cb7ca64c5e1c561c0f", + "SELECTED-DISCOVERY-376.rst": "22f0b41eeb3aa86183f8e2e83e225bff5ee8072aae39405dbcf9ff2bef07881c", + "docs/Makefile": "dd53c72b2b1c989f748fb2af7d719d8f351e757ae26bdf4f657b763bfe1ec4d9", + "docs/make.bat": "d2a8ebcce0de81abca77b2cbc26afd00385a61fb624e7060a8e5402fd900a72a", + "docs/source/api.rst": "d2da0de8358b7fb3fe1376358bffbf10cf39c827edae150a176b75a91490de52", + "docs/source/conf.py": "1d1618299bdaed818c3518ccc11dcd99f7277e4574b78a1db345c85fa2d949dd", + "docs/source/examples.rst": "fab494bfcff26a6aa84f7e3203a4f131832cf001b60fd4e87fb1ad24ce78ae24", + "docs/source/index.rst": "f707777948f83a5bba22528814b43520e61c76da730d0b93e0c5f3f91d6c5b12", + "docs/source/patterns.rst": "cb67e1b458cb073d226ad6691f4ed107bd35b887ad65452157a7283009707402", + "docs/source/quickstart.rst": "785bfb1e3d212247c730b79b756ba894af84d2dd4d61c43e9850c1ce79efdccd", + "docs/validation/issue251_cache_projection_20260930.md": "a740a1c5bd743564f795ec8563770788e78d719c221cafaf274d33987097c00c", + "paper.bib": "10b564cf153c1168960a822ff71950b16a16853f026a31838b23394d08d1cfee", + "paper.md": "d3d47e17ff3699034e9dfacaf7e0e2e18d3770c631048dc335e34e3d2fdc26d8", + "pyproject.toml": "2bc5a1a364bb17b03a4220cc3595d7889bfad3f731de3fe78453b16908b291c2", + "scripts/release.py": "19344fc3616a3d63e035df687ac82e8a040d0f7b69d5fb934c6e801df0ed5947", + "scripts/update_and_release.py": "80896e49f856d6a193bb1dc3917f55d2878acf1476c8240333eda16b92e8e61a", + "scripts/update_version.py": "f15d84a33dce9188e36576b21a84b7423dc868fc5e7eea22f8528dca83c3a5ba", + "scripts/verify_release_ready.py": "8747b1de6233c106025abe5fc4cfbbdf2a8d07641566445f4151b6fe4b5a9e1f", + "src/metaclass_registry/__init__.py": "b7dd8d905bcac00f490a6155df09e68d148f3e7b8238fed2854f2e1b5de06567", + "src/metaclass_registry/_home.py": "6949afb54c145a8c58f680c61177e3aa7a04e9b432b73552f5beb4b1b92169e0", + "src/metaclass_registry/cache.py": "10ba153bbbd6a45388a47aa28f5b18ab77847a33acec4820d31cf909153fb6ca", + "src/metaclass_registry/core.py": "5fbfc22dee24ac7cf653209ca06ca511ddedfb2c9e96a5354d7d4d4600828493", + "src/metaclass_registry/discovery.py": "757958272d11633e7b854c7104c07f0d90845c37fee6ab0c3646fb14c2ed8daa", + "src/metaclass_registry/exceptions.py": "333ae9a5b87cb884213a1833fde35c178734f5e684a8f772d6c0e35a090ec987", + "tests/__init__.py": "dadf7fb5f53a6c573dffaecd51460f3d36c83fd4ed903a5ed7ed21da331dd9d3", + "tests/conftest.py": "a2ad838c1e90bcfa8c368c51f07f473a85cbe4a95d27134349f94bfb1a57f767", + "tests/test_cache.py": "c0d077f47be112b257abf7ad574ca2bb4dbb7e7f3fbbe0c8044b69b69bf5c7cb", + "tests/test_cache_path_projection.py": "8da2d89cb7e88bc838b0a0d59068afe4a6ffc0010e3d4fcc196f55519d730ced", + "tests/test_core.py": "26446d33fcdd012ecf12ff7e541ce434aa38fb8f1fb110ee4ce4c74cbf0bea00", + "tests/test_discovery.py": "f28535b526c4fe95d68a33386c4378237468fb160867dd9b07a55de920939d3f", + "tests/test_exceptions.py": "11dd2d86ec4baea6834b28d9f368bb71c90dd91f23a12dcf795fb821a29ebf1b", + "tests/test_selected_discovery.py": "b20e2ff2441a37fc2700223e18cde1d64d1eba314521a656d8be2376da2c0b0a" + } + }, + "external/zmqruntime": { + "path": "/home/ts/code/projects/openhcs-shared-runtime-plumbing/external/zmqruntime", + "head": "668edafcb5ee530a163377c936431ae1e8334e85", + "status": "", + "files": { + ".github/dependabot.yml": "34790e08d06d30f3f06cf2f071fcb2430df398c0a70bf1d99b5cca71d69080ef", + ".github/workflows/publish.yml": "e0a6fd7824dfe9d4f8cd6e634038a940ee2ca78144535a64d40b44018328db81", + ".github/workflows/tests.yml": "f9b74f8a33d2b47746f7bec36c3b74979640e95d23d6da2849bc5c34a8ba7fa4", + ".gitignore": "80a49a462dd53e5509406a5305931664d212012c4cbf7ef7c48505527f632b15", + ".readthedocs.yaml": "8dd778d3217f13c531400542c6aa1e2ca620c84e68f73bbedd3bcffad5c9de33", + "ACK-ISSUE10-DRAFT-PR-BODY.md": "6e308ff02b10ab06fa58c209b1d6bfbb13191946ab1412cec644af26ed869a98", + "ACK-ISSUE10-OWNERSHIP-RECEIPT.md": "92228b50c7ca5e2d620d51f00ae46c464f3bfdc19d37cc1a7b7b8017adb74056", + "ACK-RETURN-ROUTE-DELIVERY-20260930.rst": "1a45185666390e9cf25032aa020fdeefb9988a1f7f47ff0a1fe82128e20e2454", + "LICENSE": "c5a804a1e4c08f5593eb8466c91dc4e871fe79319d81737a82a3cc514b7b2ff6", + "README.md": "c8f7fa1b36636d6cbcee78bf259f2e2511ef7e19a5533ae246548167e35b3eb9", + "docs/source/api/endpoint_handshake.rst": "b823c73359e3b140c24de84aacd567600381e726ebe3cdadfdad5bfe419a5980", + "docs/source/api/index.rst": "a598b60df852571f5d42508d9bd4025f07ca5d8cb1cf72b08910183b590c0239", + "docs/source/api/viewer_protocol.rst": "6d5d4d3abf96c69161c94a0127597b79fd0d26b6cdae5a1e0be258bbed549396", + "docs/source/architecture/execution_batch_submit_wait.rst": "f9e276c4057656c7c8ab2f594f76b04f9d25c0c1b0beeeaf103fcd86aa43e241", + "docs/source/architecture/execution_lifecycle_engine.rst": "27fb0eb4c639212c7f959ad4bdac87db55395a183163ffce713fe9c41fe58373", + "docs/source/architecture/execution_progress_stream.rst": "777bf4da41ca2c235b63a2d774a94cce1c35a03c3b87b2c501afc082a56f86ef", + "docs/source/architecture/execution_status_poller.rst": "7e801869b0b762325f889b76600eb06fa4eb04f7d80eb8acf773a9ab90308ced", + "docs/source/architecture/execution_wait_policy.rst": "acd5e60a3b294103f2b0b67e3922e49f54fd2015f360ae75ac37252ec1f1d8b3", + "docs/source/architecture/image_acknowledgment_system.rst": "650231482b3e7f38108b37a017612bc1fefa3aefddc5aaf1753d447134cc9341", + "docs/source/architecture/index.rst": "cfad2756cb9b78f0aa51b848181a113964ea547123671d4418f5fd56d2df6e37", + "docs/source/architecture/progress_registry_projection.rst": "98caca592c9f90121f14e4d71b7b756b017346303b8ca7b5d836bd3d7f19567f", + "docs/source/architecture/viewer_state_management.rst": "fe40e1beb8cfbb4c129e8a7bf5a6a2ad3d1e6e48b8df3e787a62bd2738c6c920", + "docs/source/architecture/viewer_streaming_architecture.rst": "3a380ec7fb52129c0013cfc6619c5b8686ae3531a8ab15215de04920923a6c84", + "docs/source/architecture/zmq_execution_system.rst": "6417a400634ed9aeed0f968cea0db98787304f8ab8387148ff77c8afa7f0d629", + "docs/source/conf.py": "e2224f5e41f473e9a9a213dfff036859c0834eee3561a9d6ce5415bc4487d126", + "docs/source/index.rst": "9ecc16d64526fb5799a40befecaaab6a3f55d41c5f901fed5c6a28084734a4a3", + "docs/source/installation.rst": "a3431edfd6ac76cf4b820efbdfd7a534d6e41258fb93c7d78ef290993fac8c40", + "docs/validation/issue10_ack_return_route_followup_20260930.md": "201ae9cb42395b0f774eb8bb56cf43faeb5f21106bb892320058a85874f4321b", + "docs/validation/issue10_journey.txt": "83f34a9f9ab21c1218ea1845908c1c25c705347b546958aa1ae631e1b2c9af06", + "docs/validation/issue10_journey.xml": "616ee69fb92d921ceb097bfaf7f1743b8f3385257b4afe8503e800dd43c8cf1a", + "docs/validation/issue10_native_final.txt": "db5ec543ed1f88465cd5c2d244f66d0e4199054bf65e24b5d566847f86843f24", + "docs/validation/issue10_native_final.xml": "0698e4d64d7eef028c190db21caa989bed789440b7588ce820ca804834c171aa", + "docs/validation/issue10_native_focused.txt": "44b9e7c9feb834f5c3c072184a13c8c2ff8e1cc791f42d60d74c631461212bd7", + "docs/validation/issue10_native_focused.xml": "94d03b8bfa3ee6b9ddebc9c47b6ee5ccc35871acc2a4561e16fe19d0d7593a5a", + "docs/validation/issue10_native_source.txt": "ad612a11edf1b6ebf11e0e0314cddca80566fa85d24b9fe56972ffaaf1f6775a", + "docs/validation/issue10_nra_loop_final.json": "dc6ffc60c762c658ba45db2137f1ae558ce6968cc6f628483fb259f9ff49155a", + "docs/validation/issue10_nra_loop_final.resources": "a2cf28d114cb5681018f5ff1bf36c333680479e3d80cd39a6709b1b996b82b1c", + "docs/validation/issue10_nra_source_20260930.json": "8d7f4f473df884ab1507c183e63268e552cb32f1d12096aa5bf9919c46a5dd67", + "docs/validation/issue10_nra_source_20260930.resources": "be2a0d372a464c2dc9ecf1dfb1aac25c5df38e50cfaf4d20d07a5a6c19c97478", + "docs/validation/issue10_nra_source_final.json": "775676eee04a81a811c04b2cdf74b19b36b9d9c46b813d3609c3009b63745abb", + "docs/validation/issue10_nra_source_final.resources": "5a5755bb6b22bc47592ab49d7c7a19e015510ee92294384636787d90fdd360e0", + "docs/validation/issue10_openhcs_final.txt": "e85a7720cfa71b1b84b6165d937fef22500bbe22b87db120bf76efbcda5d1a19", + "docs/validation/issue10_openhcs_final.xml": "9768040d37aed08c074e9a9b8b3da0ce3b0eef710d0ff34af5620ecd2637a4d2", + "docs/validation/issue10_openhcs_source.txt": "0671a73d20ec35588becac9898e56d16ad2ad28a76dcd94a984e336cc6adc484", + "docs/validation/issue10_openhcs_source.xml": "7da50332d3af51720f57ceae85562d750fd12e65ee62ea4196b4769cdd2433f9", + "docs/validation/issue10_polystore_final.txt": "6732430df17ceb02f4e66b69f507c7ac7cb71911a53cab4b8059175c98011d38", + "docs/validation/issue10_polystore_final.xml": "da2a6460c9a0140a6231fe786b88826cc601f674c032e570e0b5d608d38482fb", + "docs/validation/issue10_polystore_handler_import_failure.txt": "f8def2482b6d9dc99917cc00ed5c5be9b53855e0cd114821433842f229e254e3", + "docs/validation/issue10_polystore_handler_import_failure.xml": "39b00fdfbf4453c19ece38e8bc25fd270e8dd0b6d9f4c5cf5c589161e4fee6ea", + "docs/validation/issue10_ratchet_native.json": "2e8b7069023283406e8f9a66be26482eeed155c64a99dddf2d99785fcb5a1c9b", + "docs/validation/issue10_ratchet_native_final.json": "926801db2596332cc51cc84873d15bb729d57cc328a2717ae9f62013b2811352", + "docs/validation/issue10_ratchet_openhcs.json": "b4d1d3ba15de5b124227a52ccbfc9b5078ba27c7dc522fc6c2c74a32a9c72b7b", + "docs/validation/issue10_ratchet_openhcs_final.json": "4f1cfb30e7c16f4bb14758a29257164499c38f7f7264949ce4f13c0d7128e714", + "docs/validation/issue10_ratchet_polystore.json": "8cfbfc52f4caf1d169ef89007826a5f455492bd21ee795d7de5650041c71454b", + "docs/validation/issue10_ratchet_polystore_final.json": "243c642d60b78563c0d5e88f13ca3dc7dd41f82198d7105edede2977346fbf7e", + "docs/validation/issue10_startup_diagnostic.txt": "88d2d47c1b3c572a459808fd3f65f03c8d7b6e95b196fbf2ded75f8ce044564a", + "docs/validation/issue243_progress_annotation_20260929.md": "3fca37d4eed0f9e6e61013b4ead34790cf2720bbbb22ab859e26a7fc7c8ae956", + "docs/validation/issue243_progress_annotation_20260929/after-pytest.txt": "e98090351cae140807dcadf5fa326c1b48aaa9cb2e6e4e69edd31a60e476a807", + "docs/validation/issue243_progress_annotation_20260929/after-resources.txt": "26b7455faf39aff8a67d46c704f786f6cc4d5a44178add263ef9c84eda2ee95c", + "docs/validation/issue243_progress_annotation_20260929/before-bootstrap-pytest.txt": "8d68629c63d6da0ccbf8fe589eb0270d0c458ff20d3832c30ca4300f8eac010a", + "docs/validation/issue243_progress_annotation_20260929/before-bootstrap-resources.txt": "af1d21b52a918978e9674dfcfa0fc8efd0bfbcae49230a94b2a68bad846a94fd", + "docs/validation/issue243_progress_annotation_20260929/before-pytest.txt": "7307af3e5c62a2254ebf0f80e9219659834d7fb2f1d586f19f62255b0d5b5897", + "docs/validation/issue243_progress_annotation_20260929/before-resources.txt": "6db4de7dfe0424476b54d332fbdcd40f210f8f034bcaa69aa1d9c83a1504e32f", + "docs/validation/issue251_bootstrap_20260930.md": "4870894202f12f769efa03d22e4f12b9641e5ce87f2185f5a5364ef3d9825fb7", + "docs/validation/issue251_owner_factoring_20260930.md": "1aad3ec56b7994b95151c8c5ce4e284a03810b052d40edf10da0fcd6b207b2e4", + "docs/validation/issue251_owner_factoring_ratchet_20260930.json": "f3a7851e79b53234a593912e43efada908eb020fdbde6263a609e3a670a4c3d2", + "docs/validation/issue251_owner_factoring_resources_20260930.txt": "80d8faf518c6fe67c33ef071da2aaf4305d52c607d71886603efe3e1f40ee42f", + "docs/validation/issue251_pr_body_20260930.md": "fe06804512d647d7e92e66d63e55a54d1edd1a23c4ff9ea19210618b15a21913", + "docs/validation/issue251_reserved_child_ratchet_20260930.json": "2900dd5ff2046d8a6b47ab2d53c924946eb2bfaf26ff02cd8879a2be95383126", + "docs/validation/issue251_reserved_child_ratchet_resources_20260930.txt": "c6f11a8e92ed8252bca51c49813d40edc5c23c31e762d40e482ec72bb3711199", + "docs/validation/issue251_reserved_child_takeover_20260930.rst": "a55e9498a5dfb44254c3fb2d6811a882cc21ce96e72bc898b4b9cb2549cc9fc9", + "docs/validation/issue251_viewer_main_ratchet_20260930.json": "e168caa9b16ffd15a533ec5da0600d7b56b0f1117d2ae06ed5906d91343fd463", + "docs/validation/issue251_viewer_main_ratchet_resources_20260930.txt": "6d3e00434a29cedc1d4646f0d867c5d04d185f6e15a6c6f95858fb029b6341c6", + "docs/validation/issue355-evidence/dep-r0-rejected-806faa1.json.gz": "6a84c60aefc099cb05b86c29e93a4a75d201fc0759a086f23ed47707b9e26d09", + "docs/validation/issue355-evidence/dep-r0-rejected-806faa1.resources.txt": "da041805e258c61f5eedac2060ae2e6bfd234002cc78271b99846df81c4b9fd9", + "docs/validation/issue355-evidence/dep-r0.json.gz": "f8ab21863a79683d50416dc3b3e8f92fb9c979cd617e3966acae38648e1f8695", + "docs/validation/issue355-evidence/dep-r0.resources.txt": "b05ed2bcaf17ef6ca654c7b1221b8952f041806bd0b2e10ad459aa6fe92c1784", + "docs/validation/issue355-evidence/nra-local.json.gz": "8e76415ff2fedf981a01e2abeb9043b8bb231f260d38aa4ba0a896baed8a54d3", + "docs/validation/issue355-evidence/nra-local.resources.txt": "9329e39c1ff375fe1d650517412081b97a446d48b5f1e00dc63a34c7bba5d65a", + "docs/validation/issue355-evidence/startup-checkpoint.log": "836cbba2d26f0df6e09768d665e13c11f92468e5325c4f3fb36473b46e26376a", + "docs/validation/issue385-evidence/SHA256SUMS": "8a0d7b90dacce001f9704c669f1bf9d761898d8744fb612c51ce89eb429cf61a", + "docs/validation/issue385-evidence/viewer-owner-after.log": "a5c400de7c336e4c301d53e793c8de0102d9de41da5b7297c0a95f851b1bed03", + "docs/validation/issue385-evidence/viewer-owner-before.log": "fc1b12f8887694c419c99f3e97546a3ac74e26c71a3fc868125a43a557e92b3c", + "docs/validation/issue385-evidence/viewer-owner-r0.json": "f89a93d9a41729f7756749d30a9cb73f27ae013f8fcde037e5d553f87f1c7878", + "docs/validation/issue385-evidence/viewer-owner-r0.resources.txt": "1658475b38e73d7f4dac6f77d641b45b38f14fda1f41ce5b0ce1c058d8445cf8", + "docs/validation/managed-viewer-385.rst": "ff7219e3b4879fcd430d2241016293a8492aae8ea751d50ceb756e56f01bf929", + "docs/validation/openhcs355-cold-start.rst": "844f2e5836f5abe514efc69a4bdaa979def4e8fd84e7c15bbd5b02a1ee5cdfb0", + "docs/validation/startup-feedback-358.rst": "50e894dd18dae9fab0d5dc580f584d73032ec58043d92556412346eea68dd463", + "paper.bib": "cf1e534b04aaae97ffdf926efe9aeab42a6f76a7adf63553c0bf74e108837c8f", + "paper.md": "cffb0012fad980e7cd9f98bbc0275b620ee2c224ca7935c28a1f1c520ec41728", + "pyproject.toml": "e2b56ba5b94418c99ee04142edf0fc4f14a06860fbecad7170d4a2e1e4be70c5", + "scripts/release.py": "109327ab88e002e4fdd56e6a50a8b5e265fda3723fb3c0ccc184c9d1b0539c2e", + "scripts/update_and_release.py": "80896e49f856d6a193bb1dc3917f55d2878acf1476c8240333eda16b92e8e61a", + "scripts/update_version.py": "d70feec0d50ca0a8d90f0105be3e85bf5a5b17a7bb0ca63a6178ad443f0ad60f", + "scripts/verify_release_ready.py": "e8190c4f79b1fae85af698b704b339564e77090f8adb7393f0079cf0d13bd844", + "src/zmqruntime/__init__.py": "37f3e14f58b4c1dda3d338a3a2fb075aefea215f6f25b69f44091891341755ef", + "src/zmqruntime/ack_listener.py": "da5aa8d1031f971c2621071a6da40b41f10dedf12839060db13d10153050cd6d", + "src/zmqruntime/client.py": "00757150b06b837b918153bc3c2752dbdba396569ebb4f4ed91cfbff7c23d25d", + "src/zmqruntime/config.py": "79840f3b72593042b6a56e3a703a1064e0d1faa19b12446d99e0903af494f2a3", + "src/zmqruntime/execution/__init__.py": "7a5d9f1636946d5fe6edcaaef181e8740e17d5b348d81e04b50a9fccf6fb16f9", + "src/zmqruntime/execution/batch_submit_wait.py": "c447b2ccbc372a5336f5a3b32844b896dc5d58566cb3de31640ae3be2379edee", + "src/zmqruntime/execution/client.py": "1b9453ea2be540b29849770cc3fe82f284f501e0e140797dd48c2c257ec36f12", + "src/zmqruntime/execution/lifecycle.py": "23596c31b9efe812f1ab66b4300ee1bb19125ea9bef6e51ddcd8acaafb0f4ed6", + "src/zmqruntime/execution/logs.py": "c7d1816835620a2b3e82a889bd36f2a97b80aad7c0f639a07c5c9d426c6ae803", + "src/zmqruntime/execution/progress_stream.py": "13b39fb4d256cd835302f7a8949e9e56bfe0b045e3e6fc1d197ce8ae40bc57a4", + "src/zmqruntime/execution/responses.py": "43ebbaf9451c34a5cb5a0c3182cb4de4c3745e13db8482ba1373becbfc8a2a03", + "src/zmqruntime/execution/server.py": "13663ba20e62326171ff8b43c2474419940729e90b2c0b25b04ea3514734d599", + "src/zmqruntime/execution/status_poller.py": "da0c7c304f293ba4f52aaefc4c722442b5a2859d1fcb806e8fccdcd3b4f6cdc6", + "src/zmqruntime/execution/wait_policy.py": "df4a9cad5a2fb74aa0c030def22dd8cc5b851983c3f04c26d479b607075ef922", + "src/zmqruntime/messages.py": "76018d6f6ac6795e076eabed9c6dd018b8c2fb9640e194a19086a013c0a50785", + "src/zmqruntime/progress/__init__.py": "1767a50989e4a19e4ae8a7ed4343b6be564740236cdcb4c5c56c06f55c21f68f", + "src/zmqruntime/progress/projection.py": "72b22f765dabbcf6983c619c8cc5404bc52518186ef72d85a6f6c6ab65cb426f", + "src/zmqruntime/progress/registry.py": "2943b28d1490688bddcba914db5aadd445cc8e6c9b6381230f0451366e98a0c2", + "src/zmqruntime/queue_tracker.py": "b2e380a93699f85e608d1ae7c03b5a20ee62d8beb82cc93272b050084156610f", + "src/zmqruntime/runner.py": "9f52b69f23b4626adb4e1394e0ed476624d2a38d9cd2c8ec656ae3b2e6816df9", + "src/zmqruntime/server.py": "a38ed80dfbbf9d738f4a96b4bcff8ca667f592b84413c33731550513dcc924d0", + "src/zmqruntime/shutdown.py": "67acebb3ac7a2c419320005931f0de44e28f5ab0b393e6bfbdc3f11207a2073f", + "src/zmqruntime/startup.py": "55118bc44633926434f0e0b057c167dc2948f2a632a902b5bdb60dcb92731d69", + "src/zmqruntime/streaming/__init__.py": "911cac6c1f747d77ec6614dd0961ebb799a7dbaa276e4259e1f38087e3b7320b", + "src/zmqruntime/streaming/process_manager.py": "8229c322c309969f89e2a51a3e416ea38a4990248776844dbcbb01ad9796b3e5", + "src/zmqruntime/streaming/server.py": "6a587842676bdd2f414d8df8f277e5e671a5f41a7b3c4615bf1ea2a5da182834", + "src/zmqruntime/subscription.py": "4a393bd94d8a3611ff7d7423ec4ea681df6f26cf906a77e2682c76cc0e762696", + "src/zmqruntime/timeouts.py": "bb5121a03070f0f2a0ed76170a2324512ed4a4d64536ecc7ba8bdc6a4b8961d0", + "src/zmqruntime/transport.py": "e486a4215d4b977553030e897589bccac637dfe4e9abb28cfb7594139b2196c2", + "src/zmqruntime/transport_modes.py": "12bec1c13d87bb8f05b049dc76d3cb7c130414c9e69d6220228ceeb0087f9ad8", + "src/zmqruntime/viewer_protocol.py": "5580efffddca2f723b11a652f482fbdb52ebae657620fad95217b313997b9fc9", + "src/zmqruntime/viewer_state.py": "f3dd0b35c3c75b0537bf8e50624b8f05a6dce02b40f1700a878189c3312ae30c", + "tests/diagnostics/check_ack_startup_failure.py": "08856fa0ca60e4b00c8577ce2ed3bb82e6e3fbbe16eb2876b69a03500ff130ad", + "tests/test_ack_listener.py": "a616052e042aa145d4eb2b9bca6b153633662bbd54588038730c9d8a2551f70d", + "tests/test_ack_return_route.py": "4519ccf21b0d6bc892d43405541308326a8a7ae885e0ec8942e3596eda3078bd", + "tests/test_ci_action_pinning.py": "a636dd4d70e231d06c5de2802afc89c389f678b3b49a40ce8463eac5b0c308dd", + "tests/test_cold_start_activity.py": "3e0486f8e520e3bb2a767f1aca49df0ffc269476cea0f29594cb08bfce1d7896", + "tests/test_config.py": "f9a16ae6cdb8c9d1dce74bcbc95ff59e853245b219ce848d8aca9704eadfb95a", + "tests/test_endpoint_ownership.py": "e37ddca59beb48b4c1ccca4d1feee1793728cfc54b2aef69870e120bf17098e5", + "tests/test_execution.py": "7b547a68e912b012144519216097bde868264fcf7bb0ad20b3223862701bea0c", + "tests/test_execution_logs.py": "3136e1cf250b02d89e9da9be06b90ecab59fe34ba58e2e22145fd1b0e9fe2a90", + "tests/test_messages.py": "8223e0a5b461e7dd3fc6392c9f652cd74180c8de1b07fef9d28305ffffb2f229", + "tests/test_owned_close.py": "92c3149704b88278ce6771c0c2c064aa897948bd7303b8100f24f99e97d9ac84", + "tests/test_owned_startup.py": "95d3997eaf5d627925108b85dbbe5937abe448c666f26caa43f2bad5586bf73a", + "tests/test_process_manager.py": "b21d1eedd0f98549a7b0c235cca43fbea35afb3c041df81b362d94f3fde9439d", + "tests/test_progress_annotation.py": "27b237877b660d6699402a091dc7843201a4fc0636e4b9f0fc1e25d16cddee75", + "tests/test_progress_projection.py": "e56b49ba046b7c459cee386076201b252d3c831a3d8eca535eafbe0ff6affced", + "tests/test_progress_registry.py": "bf80de2da039272700a366884e5aee4163f04b156fe5c8f58912d4c7f42e7ef4", + "tests/test_progress_stream.py": "27d056146596e7e7c4f2a15ae7e7f7e01e9b4882f3258fa48108d799ecb19698", + "tests/test_queue_tracker.py": "85257cf345a02532441f188242134a2fcb4b60f793eead2f21a6bda360529a29", + "tests/test_shutdown.py": "ffff454475abed18dc520b8629a2799d5a30d0e83abd0292c5acb916d3e63eee", + "tests/test_startup.py": "3e5441886d22921b255de1016bedefd74e87e57f7099bc9e72f8021d1b115148", + "tests/test_startup_callback_scope.py": "06c9dfe205fef9aa293ca0a5dfbaaa8d5083da02fb7c2e06bc9895dea607760b", + "tests/test_timeouts.py": "8c4c95ce4c2d02d5210ef4ccfd4d0cbd1850535c8d5793b87380e995c126babb", + "tests/test_transport.py": "02072b46ca87c082eeedccb02f4b85a1ba02bc51a4c24ebfc6930a6b8b2a21f9", + "tests/test_viewer_process_ownership.py": "972bf0988a9b3b49371ddbd8e1ba7ce965c9fa9a37f04ece70fa93905335b348", + "tests/test_viewer_protocol.py": "26adaa73cc3a39cefc4c797880fb5b2e52082adaa980e479e318fa3805e7fe28", + "tests/test_viewer_state.py": "2f7b0dabbed227db35fbac5321f85b84352aafe19e33c879a50b3aee0fc9d8ea" + } + }, + "external/pycodify": { + "path": "/home/ts/code/projects/openhcs-shared-runtime-plumbing/external/pycodify", + "head": "792b03413331fdf107e369c924c257a2f0b16126", + "status": "", + "files": { + ".github/workflows/ci.yml": "7cdaf326512e98ce07256301a152b286d2c06492d67e09d6162d1312c95dd203", + ".github/workflows/publish.yml": "e10379aa724dff1ecf13fdfc3baaf32cb33de77fa65eaf8c9af270d3b3a3dbaa", + ".gitignore": "2c9d25459eec48f9d654932218ffab8dddc041060191c98a67b6505bb9726a00", + "CONTRIBUTING.md": "f39c2677fef7b7243f37e3e95e35a09e118dd1fe30b1f43a25ed119b6064ae69", + "LICENSE": "41a2e56fd632fc59805cecefbd30011033d0e67fccddc93529a1a6cff03aa766", + "README.md": "f32db990f5e1feb01ee3d7adbe1195fd691c8964acd789759901d00170cecf7e", + "docs/Makefile": "43df277c1f15a11e9ab7180cfaa9e65e014571beec43b19530a363f4e62e0cd9", + "docs/api.rst": "f613bec5644911b1d5fc6d6875ff9a843b49526a5b2e00a22488cf825a3ddd05", + "docs/architecture.rst": "dba4502673b43a48f775827acb5dc6a3c75a88836ab59ec5fc0e7dfd722d4b51", + "docs/conf.py": "3cacb5b7a11daa491ff739fe6df58494ed38e50fde40d6bd6fb7c9b4d4eb782c", + "docs/index.rst": "d20074f7b6d7d9d205d453f9fef04a3fc7120b943c6c098664dbf9376bbbdcfe", + "docs/usage.rst": "40677c937662fde6d2d96399b63c83424bc80756036df4c2f6f34c541202e606", + "paper.bib": "ba515e9b812130cc2da5991c37921c93d947808909953fc5c9949202e54507b4", + "paper.md": "c52498a740b040def7feafdd6d1aa4253515d6228e5cc534c00a417998d65f8b", + "pyproject.toml": "1103178d6661d94d66f6b43039d0961557e6d07861e46bbf8df18b6d40a0b1b5", + "scripts/release.py": "9027086b71a742b688bc984dd2fe50c4af2ad6e5ba1b1c564cf531a414e19f62", + "scripts/update_and_release.py": "80896e49f856d6a193bb1dc3917f55d2878acf1476c8240333eda16b92e8e61a", + "scripts/update_version.py": "f93a6a1ab4aa714f699edab98d36d3f610c7b7e14ff5748a22fc656214958d8d", + "scripts/verify_release_ready.py": "aa51d925335037158e0d34d0b198039e4d3833df564f01622ae4005bc7e9d6eb", + "src/pycodify/__init__.py": "b839bc2a7cd2afdfd8756eb39f22b791cb4f7aa55c0fc9c11099b7219c9c663b", + "src/pycodify/core.py": "b32dea495ceb8745c1b363f2e42aa62eaa28a1d54198a5d6541e0b1546facfb4", + "src/pycodify/formatters.py": "08791a7f3cb779889ef24a5bfb395a1acfb4d3320fe9a7cf037986c98a9d7faa", + "src/pycodify/imports.py": "17fc080d60ef168a9c211f27111b04da1d527d2bf87e2188c674e4de9d25bef1", + "tests/__init__.py": "efd53630f4e3c520366b8aa444fe2d4d33c07c7caa0e3841fbb43a04cfdd9183", + "tests/test_core.py": "164dd47f93e4adfd8fe9de851f5db74744509179f6eaaae877be879f22689e66", + "tests/test_imports.py": "ce63bc44d8fc24a02e3439a7b1504b4dacf21e64ac51f1581e60f0099664c3bb" + } + }, + "external/PolyStore": { + "path": "/home/ts/code/projects/openhcs-shared-runtime-plumbing/external/PolyStore", + "head": "84f322e46871de5ed47e7fd20976ad03e533c1c4", + "status": "", + "files": { + ".github/badges/coverage.svg": "e6e5c18d37513ef246d24694ae7e124f83597ef6e839baaa8269408f6be4690d", + ".github/dependabot.yml": "34790e08d06d30f3f06cf2f071fcb2430df398c0a70bf1d99b5cca71d69080ef", + ".github/workflows/coverage-pages.yml": "1b0c45ae4b4bbd8a38c9208c9f2f6d90faee1545bcec090b776403be9c33b470", + ".github/workflows/docs.yml": "0753d09dd18b885655926115317a45200099f8b024f3df29841f92dae248a2e2", + ".github/workflows/publish.yml": "b1e8d3648e3f38a894c8fa2d98ad2842b388c5effff69a91cd53aeb9f53781ca", + ".github/workflows/smoke-test.yml": "cec1eeec266d5e67c440709ecf5efbfcd263a75046d7e4d9e99acffe057da8e7", + ".github/workflows/tests.yml": "26cf33ecbe48e0bfffdebb2834b3f31f705cbb7293a4d864cb430ffee8f75cbe", + ".gitignore": "4edac7cffd81b322d4edc9422eb4b4574b0227939c52524b95004033891828cc", + ".readthedocs.yaml": "4d16c3881aedd586c6b7f3febd7a0ccbb2df964eac8ff17748acce0745a194c2", + ".zgroup": "2383746e67b4bcc2762b3f100f06c3fa2d5f149ab5a8e5da5d33521464a01959", + "LICENSE": "63c1c82941031bd46dc233be9b616b8e9510f5e30ae57f16a581613e7378d953", + "README.md": "c054ab7a02e743a089ea7e258444ca2982318b86af27457877cc60156a3831cf", + "docs/Makefile": "eb696db665096bf6d05567dadd0213f3ac84275b621909c9014d12d01048e73c", + "docs/archive/README.md": "56692f0cd4664b0cb34e9475a30e9c73e1de1e8f88d22edb4fb00f32593e33ca", + "docs/archive/TEST_PLAN.md": "6b840ad8555985dc78b7984ae7ebd35547da6d051558a8244269551c6072a44f", + "docs/archive/TEST_PLAN_DISK_MEMORY.md": "41619cf02d71f16149b53eb5bdbe6834fb84e69e90e154ab2f070f5c2f8f9d86", + "docs/plans/native-imagej-point-roi-20260929.md": "b7943227c7be98d2315c01b898323fbafcbf392c61239df18066767045c41a68", + "docs/roi_region_properties_checkpoint_20260929.md": "6cfd26c9550315c63dd3ac41c597ad5eb3a7cb942c3c308d97d59d10a7fc8aae", + "docs/source/_static/Main_menu.png": "dbff6948a2354eac88ed6d883ae1d9b2dc2bbc39cbfa8a8a7b21ffe7ed9fba58", + "docs/source/_static/ezstitcher_logo.png": "6e2fdd7f872fa57e595ecbd296a56ea5fcb58923863df4b5852e5c72da8a9438", + "docs/source/_static/function_pattern.png": "c6ddd31d6fefa6f398a942beffb4648a30fd4d403e4000c3579cacd27d498256", + "docs/source/_static/generate_plate_tut.png": "9fe5351407df65d9a7dc66743a887a49e006cd7919c4790426529b96d6151e28", + "docs/source/_static/image_browser.png": "d19a043b5947af2af09abef3f36581ccb4e42e388cad8c24c6dde6a5fdb950ff", + "docs/source/_static/metadata_viewer.png": "abbc92272b036d406e0aed104d1eb44695af872f4e59c5cc964b64e6c5a28f5e", + "docs/source/_static/pipeline_editor.png": "b8c9a96f3613d6c46aa1d9f28446cd57d3edab2e658dc1d631d9f4208378b865", + "docs/source/_static/plate_viewer.png": "43d934e7136dce72e587639cc55336dd3f5c4f31d966d5b8e0d68e19b9fcbba7", + "docs/source/_static/step_settings.png": "9fcdb0e446de8dac2943ee29b7044d1d9def4570c2786c043de065931a767b6e", + "docs/source/api/atomic.rst": "1d82a7883d5cd5954d4ef7680a7e736419cf09dcd454575e4b3ce03a365dfea2", + "docs/source/api/backends.rst": "fe8f91f6c630146977e00d31c2f16be1225bf9fbc6563c80a6bc52a7d3ba5e8c", + "docs/source/api/exceptions.rst": "3419b20eee89d7f9fae9d9064afb4151e6b524d48da65d0f6de4a92f66aa5ebf", + "docs/source/api/filemanager.rst": "ffbd8b43975becb7c5913149c0e8cb80cc89cfc3579816b9eff7b0ee1fb2ac75", + "docs/source/api/imagej_runtime.rst": "ee69fe61cf24c7cbe24a6c0a45f5b2f30b3db685a41f6e4ca4ffb1723ba578c9", + "docs/source/api/index.rst": "23a2dfcbc3abba4e9f2fc7fc8b12ddbd1395f2873f87c651167df7185097de74", + "docs/source/api/registry.rst": "6e96ef632a2144de51adaba15ef51b32884e36ca1c09087eb8d70b59706f4a19", + "docs/source/architecture/file_format_system.rst": "97e6ff244a875b68ffd93a1429b1a66ca5754489cd2b3be67da9e1a5e4f15c1f", + "docs/source/architecture/index.rst": "b67a0219b9e16b05561233bf8a6c9d25edc62ba3f30cda7d96fbee85d2de1dab", + "docs/source/architecture/source_and_virtual_workspace.rst": "8510f954c1de38f7330b3c62448c350ad99cbeea0f0f999adcc0dc7abc65704e", + "docs/source/architecture/streaming_receiver_projection.rst": "51049541a05db0ab130468c08e0f244cf7f181c5b4cf5a17777508be797d5a42", + "docs/source/conf.py": "7cb4e271ad57f025b60ede742e8df94a095dea71e32aa44cc7527180d8d90bb7", + "docs/source/guides/custom_backends.rst": "76b85ead72c2165c0d4e578e11a83a5c65bc3d1340cf17693a0f3a7a5c3d9ee6", + "docs/source/guides/omero_backend.rst": "77a8a6b816597de6c13633f56cbc3f797976f42799b61d574bc60181b74da1d3", + "docs/source/index.rst": "e179fa4a9bfacf5bf50754cf78d66d6e4f0f60de61f84020e65f1e7ce592e55d", + "docs/source/input_preparation_installed_20261001.rst": "2c62ed3d3d85e37fcb974a38eb511cdcccfc13deabe014dea4c7da18b5a53eee", + "docs/source/input_preparation_parent_20261001.rst": "f30ffbde998099815bb1da39bab10b1c59963db43fea8777959cd9e7d4b0c903", + "docs/source/installation.rst": "f0dc946cd61f386b3fa70d767d4a8633f153726c3771a3635214a0f6e1e2413b", + "docs/source/metadata_namespace_327.rst": "73a7bc504da48035419cc09904c139ce2604c6037fbec2179a5c47687510b909", + "docs/source/quickstart.rst": "321494c996433752be2d5a2bf1659469226a69abf71dca591b871869e7d0e374", + "paper.bib": "3f6f21182c566ad8b4124604dbdd39197f92303af864e6f6e45bedb2e0ca7e32", + "paper.md": "bba1edb7a23c365db162f99944e03675246ed6e2503ffab90936a3d8f576b689", + "pyproject.toml": "2f3edfdb1307f3300e3db056b2828814017270a9726f4d7bdac52caf84f96f8c", + "scripts/release.py": "25ef09845453b73a29ea84c303c6c77a79f291775bdba97347f296f35e8629d1", + "scripts/update_and_release.py": "80896e49f856d6a193bb1dc3917f55d2878acf1476c8240333eda16b92e8e61a", + "scripts/update_version.py": "e96bc7111806eb9a15b5d739329a6a41730fc0591e31f7975e52b17e67e1f721", + "scripts/verify_release_ready.py": "1c32aae8b7ab774eb0fb99d0a51ce6535ed7ee5020f44ca9dc8f6d3ae50af01a", + "src/polystore/__init__.py": "15dabedec6acd92b025d4a0b4e8151227a4496e8b7d5e87f28713f7799919b5a", + "src/polystore/array_payload.py": "3eb7e2cf8eb01a2b10b860778a8fd4ea47e7557fdbe28ab8adf31045088e145e", + "src/polystore/async_init.py": "21d9b8af54ff21247a2012d1ae759550ae0147cf75f667a73b4a5a8387bf3d0f", + "src/polystore/atomic.py": "5e412948fb919ce27f09971b0a5fdf0851499812e4c0ae8bd456abbd8ca86b90", + "src/polystore/backend_registry.py": "a4c4e73a79aa4363301f76727b35db2c8b55fa14107f8866a05f74da4fefb64c", + "src/polystore/base.py": "ae641affae21b61448816a14bd68b7f2f3fc0082f0f1fbe51079ed32520450b1", + "src/polystore/bioformats_java.py": "e7ee3fc888dfeebaab48681ac3e9dd2463cca693a0ff0d77ce274c72135666ff", + "src/polystore/bioformats_storage.py": "0f57d0b35619de4aa6fdad0839ae689ae6f91b0be099fd0b28742c28795f3b71", + "src/polystore/config.py": "62c8acad3da3abed8ccf7f1166fc6a0d094be1c3e842cec6446780541df292a0", + "src/polystore/constants.py": "246da078ce8a3aae6d1fdb76183f1ff724795014cbca4d670b80174f74084635", + "src/polystore/disk.py": "cf089b8c79f86099a7cea1b6cbe43ceaec6331196a44b43183f2e64b7746c219", + "src/polystore/exceptions.py": "0ecbd94af34ae3d1a37eab29e1f72be3e68c651c9a93bf52264a783a8fc65ab3", + "src/polystore/fiji_stream.py": "62a60a36f1da5668f60bc0b4e3a5d0e65008042b2361787f4909224dd9b51db9", + "src/polystore/filemanager.py": "877a2b10731b12904b1784a5b42c9e39688e3a84f7f86018c1c2beee96ea2296", + "src/polystore/formats.py": "6f7ec984549b45a31bb32b9051672e996a67d9caab3ce2030ddc7d50e17101c5", + "src/polystore/imagej_distribution.py": "29000b7f7d8067a8a1667fc8ee603d3c2f33fd87f0a318a21cf0093473df49b4", + "src/polystore/imagej_runtime.py": "150983b2da981b31f974ad384a0fbadf5804003fd59c085993701b876384276a", + "src/polystore/memory.py": "4ac155187a80c5b6f096b2170d6851e6ce119938222beef4bd99440cbccba9e8", + "src/polystore/metadata_migration.py": "4390dc79fd1f9dd3fa405b961ea15573037bc19c6db2b31b36a5bcb7170abc5e", + "src/polystore/metadata_writer.py": "987b8ff3cfb45b170656fb7dd405abc68c460381871ca26cc26cf82e0550e256", + "src/polystore/napari_stream.py": "29286154e37d7eeb2c75d9a48cd12fa696f40c02d5ac35865360b7673801f905", + "src/polystore/ome_zarr_metadata.py": "ed0e4a3de24a6f590404d98578e19b76c9c76b27e854639e68b521f49924fbbc", + "src/polystore/ome_zarr_storage.py": "005bdd6ce615ff115050e1853aab0c56c50c65d1361c31ef6219bb0d6c4852b7", + "src/polystore/omero_address.py": "8a3ae8789fceba0f099eef2ee983d858a8f83b764f19b70dfa7bb88f521c0c92", + "src/polystore/omero_local.py": "35d39564317510234c2a3ded00bb06a09417d1b6f7359e52e74c7bd0a086f642", + "src/polystore/omero_tables.py": "910aaa6bea75f40bae82df71dd414415278b1387b80c7f1bc11e8d681874451e", + "src/polystore/omero_text.py": "9f51daef3d96cf191994d8a9c7aacf6182941ec2b0ada3f5ba61e0cc4f9412f1", + "src/polystore/roi.py": "fa945e9a3f7272e72f3ea022c7a3ae6cd30520ed7924591dd3f3cad5d0e71fc0", + "src/polystore/roi_converters.py": "50c37e5b070db7cbe418a5a64576595ee3ea7e5bbcf014492f38958f4debbf07", + "src/polystore/source_tile_geometry.py": "ebe4a422ec76972f2318ba56910ba6ccaef28734764ed3b88a3ba478ed69d322", + "src/polystore/streaming/__init__.py": "e401faf53b6fb9a08e237535313e5b99d7e371372875182b76132921233e5adc", + "src/polystore/streaming/_streaming_backend.py": "7caf2252677a894a5a761b60c473b92184b83745608c2b50a75eb1733c77cd10", + "src/polystore/streaming/base.py": "353e87dc92ad8df2ccec0136ab8ef7de7729896522cf8b16c0ec79bde455c176", + "src/polystore/streaming/handlers/__init__.py": "78daf5078f1914dc5b646ebeeb79bea336036b3b4737e122865be41ab7b24f03", + "src/polystore/streaming/handlers/fiji_images.py": "b5f06390cfeaef6c6a10c06bf9af00eab29f90ce1bb2899794d2e516cf86c7d0", + "src/polystore/streaming/handlers/fiji_rois.py": "5edcdc8d7344c5793730beb5a1c3baea89e96b13e62b746cc1cbeded3217a978", + "src/polystore/streaming/identity.py": "a8e48d76c8fd222515121e2ebc879dccc820e0ebf5b576a8728c1a7ffd35dc4e", + "src/polystore/streaming/receivers/__init__.py": "07953a0ecae9f307b9a2706062c1f452648267887e8cd03fbdb87d4b301b4877", + "src/polystore/streaming/receivers/batching/__init__.py": "c045df607fc2c07983659caff8b191660dd2ce5e73aeceb89894be3a457926ec", + "src/polystore/streaming/receivers/core/__init__.py": "b61b9545e4e373556d5beb87a23c3f6475238361d0c3a4cf18a3a5e197dedcba", + "src/polystore/streaming/receivers/core/batch_engine.py": "36d1dd9a7a2beae134c6f149ca148dd4cc2921fa2feb37cbcb88c42312743055", + "src/polystore/streaming/receivers/core/contracts.py": "c89f3f2dfb3e23482112a6b98ae3238d1768b2f4a01b4b2d605b698bd307f561", + "src/polystore/streaming/receivers/core/image_presentation.py": "dbe1395afce5a280fc1ab0c66bf302e324577ff15c6dff7a1698f50f1fc72a73", + "src/polystore/streaming/receivers/core/layer_inspection.py": "e93f707de62516eb0d48d9fdb0f71862b4265389723ca92a05145fce99a28802", + "src/polystore/streaming/receivers/core/metadata_presentation.py": "5e021ff3659039856616a75b4e19918b709121ee4b8902d39430f110b1adc5e8", + "src/polystore/streaming/receivers/core/viewport_presentation.py": "936378683122ad28deba10d71e9561abfc904017582e345316bf1c760d2abfb0", + "src/polystore/streaming/receivers/core/window_projection.py": "e95d0ccda66aa866082c5afc5bf9908259c727ca33270d3f069384651e1783e3", + "src/polystore/streaming/receivers/fiji/__init__.py": "a86e999752d8212da5bd58f1a3db43ffa7ce867fb0ae1209bf0f148bfe5b4b50", + "src/polystore/streaming/receivers/fiji/fiji_batch_processor.py": "165919f2e36c37e221bdd6f17e4398795745a95da9ddd7bba0ab93d74b9bdab2", + "src/polystore/streaming/receivers/napari/__init__.py": "ab4ba3ba76e98365e67abf6f9d79c0be098a774bf9e17e8523fc430a0babb9f1", + "src/polystore/streaming/receivers/napari/image_presentation.py": "1abb2d4c33ea073deb415cf9a242baa64d0b9d73985b7920d72508f2a5998a8e", + "src/polystore/streaming/receivers/napari/layer_inspection.py": "6bb86202480b5aad55d6641e3a6891105c5284d4408ff1cb7f8cbb36929888cc", + "src/polystore/streaming/receivers/napari/layer_key.py": "650eba665a58130fd67c38b164f97f677d805e07e53c2c7f367f850ae6db4a8f", + "src/polystore/streaming/receivers/napari/napari_batch_processor.py": "c3ed3f9fb4d89f82cc887a4d7775db911c96fc7e67d437cdfe8c4b78e62a16e3", + "src/polystore/streaming/receivers/napari/viewport_presentation.py": "aef743171c8d27ac56b53217f8fb2352584d447b33791ee2ce3e1c8fa4ade356", + "src/polystore/streaming/viewer_transport.py": "3b883e3078ded6b8e9c80e135a2eab876f9399fb7813dcd1dc8dd8f6049a3537", + "src/polystore/streaming_constants.py": "645e1685a1cf23167b2f6f06d4807c76444c339fbde66f3b12de9bba3adbe170", + "src/polystore/tiff_header.py": "714c6ddd0ff3928df37b2e65775c2987c285538b2375ee17254ae0dee868fa6b", + "src/polystore/utils.py": "1dd12cb9742f58f9537f1e23832f44f90deaf4332de8ae9b97e8df711ed261c7", + "src/polystore/virtual_workspace.py": "b19fcda85369d812e70bfc1a0efc5d8861ae256f69e94372e2e9515c591d36af", + "src/polystore/zarr.py": "b9404235a3a7882dd6f64383c11aeb6073c8f3a6485bec74264ad6434a1ee35f", + "src/polystore/zarr_batch.py": "8ed86ac525b561e6557a8ac55c6cb0d3954311a31d3ae65ea84af196246d60d9", + "src/polystore/zmq_config.py": "14123ce6fe232144f8a4fa086cad164a0b29e3b014ab809810d622e39ac92854", + "tests/conftest.py": "5ca35c45b774ffda10030f950823eedb160628aad1076b805e401e277bf624f4", + "tests/constraints/zarr-minimum.txt": "ddc0b24cd684f9742b7ecdbc0939072ca89dbf147c4e238268140e4678bb37a7", + "tests/imagej_cache_process_fixture.py": "bf04cebf8be7fb4e911ec1df75cc3e4fc311745981975620b2946b308c461d93", + "tests/integration/test_imagej_runtime_live.py": "bc1c16a755e12682b5b94b81b902068fb4a2e4b8f8b708a43c814b28ecd78bf5", + "tests/metadata_namespace_worker.py": "5db68ec70ba017d1b81cad2302788dbcf0a1cce5be019358259d3aec5ffd7ef5", + "tests/test_array_payload.py": "95a02bc11cd21f57da986ab8f90196a7343c70513d034e69dec581c413ee802c", + "tests/test_atomic_lock.py": "0ee06b9b53964df6777f72cd0ddf0faaa63d9316ec7edfdedb6e1b6b9087f736", + "tests/test_backend_registry.py": "6af08f5ca00bebde1a4190bb40cc2df99e025aa3465521770a22da0cc51b5651", + "tests/test_basic.py": "803e5643003e4bea60fb48c89cc84d732c77b2a3a68bccbdc6be3747ca053aba", + "tests/test_bioformats_java.py": "bf8b626c3081c26de73902822129ece5128549ec6544b604285dc9b1ac2ddc6d", + "tests/test_bioformats_storage.py": "e97642deacceab3348e8a337b34e79da235c1e1283fccfb844c0142f8632a35a", + "tests/test_ci_action_pinning.py": "a636dd4d70e231d06c5de2802afc89c389f678b3b49a40ce8463eac5b0c308dd", + "tests/test_disk_backend.py": "423c0bb9389cb002ad9c5384add0e08d4c8f99c4279ffa7d1d6621ff90ce9747", + "tests/test_disk_coverage.py": "8f0486f4611c3cfbc449e5cc7276a3cbd72f50de683eeea9647e68a0d2533be7", + "tests/test_disk_more.py": "78ae7241ab3a1d031bc733fa8f20368ba24b225c59cee47ab00624da8b77aab4", + "tests/test_filemanager_backends.py": "f506fde0e7aa4fac67c9682fcb7ed5c850c0a9ac069dd3a6ed7732427ab5578e", + "tests/test_filemanager_extended.py": "3b20c0038ba749f3d4a0f787d2e854bcd7c4e65ea58305231193f415fa734a4c", + "tests/test_imagej_cache_environment.py": "0c3b854542257e09ec13806048ceb5cc3deadf1a6302c621f44c3bc4e6efa9c7", + "tests/test_imagej_distribution.py": "fa7740c10975dfbc2e654ac3b7fc0df0f2f08599a98966e51c2c046f8cae01e1", + "tests/test_imagej_runtime.py": "fb5ce7c8d53354eb46756644d3d0c135bbeae10a21a76718f67e458f046bf480", + "tests/test_lazy_package_exports.py": "ee015f6ea3c2dc198f522db1f494f886b892540750ca3289f7f39909c4700460", + "tests/test_memory_backend.py": "ac68a5bb7c0cc4bf6a74bf3d54cc3a75df2986b9f73e92af5316d878f678b16b", + "tests/test_memory_coverage.py": "a53b737d7899f52ccc0f683ae0312094760e532be709c17a5accba7249a60542", + "tests/test_memory_type_authority.py": "ec326f948fa957b0cc8358629199442db397b9676c884d6e730e23048468cdd0", + "tests/test_metadata_namespace.py": "5a4809a6024b6eec30130ec1bc98cdd6a2541779496cea717f8a89a98d5ca5c9", + "tests/test_metadata_namespace_ownership.py": "175737cbf844b620132bea2c5e5827ffaacac01bf53de136ea49ab32727428f9", + "tests/test_napari_batch_processor.py": "a62c8875fdde0bac50059aebc3b54dfb736246ba7e1980301ebb9e7124592cd5", + "tests/test_ome_zarr_metadata.py": "7444723fcc0bd0cdedd6d8b9454be57ce929caa61698539da55c782f0b7fb420", + "tests/test_ome_zarr_storage.py": "b70a93d160cb0ba0acfd603c75ade965fbf1f95158d7c0cfc680e6a8ce907deb", + "tests/test_omero_address.py": "15d81321402ccf015f5d687b70c27e64d58ba97e719bb090efeea74821e7ffd0", + "tests/test_omero_image_plane_batch.py": "ea857b139476e127a0ee026c20cf5ca918a65f45a70bc1fe27787313d6e830b8", + "tests/test_omero_tables.py": "45ab6ce56b95dfcf9b2c202e2caa587a67bd0de4769e77b36a9986b482cea159", + "tests/test_omero_text.py": "ddf16ee95766440f5ec4d3b0e60e159cc70b2f7694e7136ec13a72c85117ca88", + "tests/test_png_format.py": "f74821025f5d962a45287d88ede69793601725f6f158fdd9cb4c9635ad75cc62", + "tests/test_receiver_layer_inspection.py": "b1b153e86978aa31e933a26072be6e3a19f4881408d10df9044fe351ad0483a8", + "tests/test_roi.py": "a2f256e55483b45ce54264fd667452230ec1024ddb7f57432711b6be7278912b", + "tests/test_roi_region_properties.py": "5cac0122893d4df5289b2e7a14018625333103a713c87ae8c559872958ba0743", + "tests/test_streaming_cleanup.py": "2edca13d06e25936076c624fd9ff6db3b66d0997c08c41082c822a0b7e8fe8ab", + "tests/test_streaming_identity.py": "7d0256ce52d48d944961bad8dff436197c45dd0ac93c8946bc354aec0cfcabff", + "tests/test_streaming_metadata.py": "806cf4998459d90ed532307a48d0f793bfada277c69537ec031fd4e28babcb46", + "tests/test_streaming_receiver_core.py": "c79e5a32d46c39b6974d421bef2ec90099e67d6b6e13d4baa5eaa48588781548", + "tests/test_streaming_return_route.py": "e20baf9443d034ab18bfff0bfc38d60af7d7245e94ec0b7e01f80d8ce403c120", + "tests/test_streaming_shared_memory.py": "c5ecc8e9c027f5fa14783ce9e568b07eb3f39ddd07254e5fa2f9847d1a86aff2", + "tests/test_viewer_transport.py": "35f4e6ffd0bb7de1c5ebb1163e2d46d71f8dcbd0544fe3494e197c2d1ac610be", + "tests/test_zarr_backend.py": "7896159c33ee7fefb89976291e35db507acf282e92361c1abd86eca54160e2fa", + "tests/test_zarr_batch.py": "883da1109c08c53ddb5e03397e5e43bdb0f878c2da8ec4c4158ee9a70520bb17", + "uv.lock": "d4465b3bae7afc90311db1265dbfcb34fbca917c5c0b99da212902ef2463bbeb" + } + }, + "external/pyqt-reactive": { + "path": "/home/ts/code/projects/openhcs-shared-runtime-plumbing/external/pyqt-reactive", + "head": "ad4948775ab81180a354d4b793d17ee5ddff3972", + "status": "", + "files": { + ".github/dependabot.yml": "34790e08d06d30f3f06cf2f071fcb2430df398c0a70bf1d99b5cca71d69080ef", + ".github/workflows/ci.yml": "043b14a1d996285f2bf77abfe65d84286e9653fb1dc238030330baa256b7818b", + ".github/workflows/publish.yml": "eacab9aaf5db73c7dc4d58de7384b12ce6532382ef51736dc3bb6dde5e8f90bc", + ".gitignore": "348e5192909c25210099ecfa274a4be8fb5faee2fe96b47595b6e4f53f3eea89", + ".readthedocs.yml": "2f5648c67a8a1ba1db73ec8dc32fbb82ffb83b246671e1dade4cb8836d667c6f", + "CHANGELOG.md": "e2e6847f7981ad1a522894ae0e15ede7eeb59b3fdbbdb7b7d3b7a5b469152d93", + "LICENSE": "c5a804a1e4c08f5593eb8466c91dc4e871fe79319d81737a82a3cc514b7b2ff6", + "README.md": "59794f0817776c30f1ba9f82919c9ce29cc96203f0e95cc0bcdaa91c55542909", + "docs/Makefile": "43df277c1f15a11e9ab7180cfaa9e65e014571beec43b19530a363f4e62e0cd9", + "docs/api/modules.rst": "617c09bc67116f3a6ad8c737392fb483471bda2a3a29102f6dddeecea541a125", + "docs/architecture/abstract_manager_widget.rst": "ec6ca7a732c048649b8627b514a653ad050685d9b776174af7303877cf756c4a", + "docs/architecture/abstract_table_browser.rst": "3c6f2e9a7b6fea5e0e2015a1051ce1decc3f339ddea21292380d02112b704363", + "docs/architecture/button_panel.rst": "454f18ec0d2dcb906caeeab5bd504838420909ccd22e9ab1b93796ec8bd68847", + "docs/architecture/cross_window_update_optimization.rst": "3e2507130ea670ed92ff1053a4f55db768e4c45055613e4046251ceb1d49a287", + "docs/architecture/enableable_form_ownership.rst": "1d61d9dd8b3ab79f1b7780b4a5380e2b876452fcf80a09a7fe8e6ff0bb6bf99a", + "docs/architecture/field_change_dispatcher.rst": "93964ac421e2ded054a4c151cc0b2383be67d7b68d4d10b871fbc1f8be4fcde2", + "docs/architecture/flash_animation_system.rst": "fc005a4748e5e312cfe61232556a38b0b2855f31d50704fbd1b6373c1178124a", + "docs/architecture/flash_callback_system.rst": "f56f1902b4747eee31c2e77848de3662580810f4295781c0e8019d373b1b5951", + "docs/architecture/form_scroll_containment.rst": "5c3b46d864459cb5900a1be2b1147d93147c8e8e781ef2b97e55d86127ef9513", + "docs/architecture/gui_performance_patterns.rst": "0ae20c36992e6f5935ce3809d6edfb81e30750791b66d7785185ada00709cc68", + "docs/architecture/index.rst": "da94b32fe4ca3245ba69b1c8c0f6551885a0bca9feb1e61af964caca5e720a94", + "docs/architecture/list_item_preview_system.rst": "d7a0fbda2c99389cfc531fde81006bbd3a32d596d395112a1e6cb97f8f2d44e4", + "docs/architecture/log_viewer_system.rst": "335f4c93673eecb1e70705cb6499cd5072a504bfb607157b0f17003f9b4df2cf", + "docs/architecture/parameter_form_lifecycle.rst": "788a41edad8bee2eaa46299f1005ada60dacbbb748ae695362163d9ae45f0c16", + "docs/architecture/parameter_form_service_architecture.rst": "307b81c106e936ba70fee6c3e1fe47f97ebb5340896ccba20a00995ec14b76c0", + "docs/architecture/parametric_widget_creation.rst": "127604ce9226b48662cf062061b2bcc20476b9331a33d5f85ec09be5ee0b4dcc", + "docs/architecture/persistent_system_monitor.rst": "4f6248e3049912c30d68cf6a57eb33ee4a8d33f36dc76aac046f490296a0f0db", + "docs/architecture/scope_visual_feedback_system.rst": "8532d95d91a514612d4e12155bc47d69a9df6f0edddc6d1548914c7465f8bd08", + "docs/architecture/scope_window_factory.rst": "7b405c0ddce22b9e1d361d090fcdefb961142d1fc8d2d942390403c8061cd230", + "docs/architecture/server_scanning_and_polling.rst": "9f8540fe237778f095f5c177abeee77734ff3e2124b2a7ccf9b520ee2459cc45", + "docs/architecture/service-layer-architecture.rst": "69e14c574ab79eb9e2c03f825cbae889893cc4d471c4a499aef0fc2e23899211", + "docs/architecture/service_registry.rst": "39c8f6361b1bcc00be0532be5faa3355b8c5e6dfb3b992f51aad3ba82ff419c1", + "docs/architecture/styling_architecture.rst": "1f7dcd355460a1634f995d7f46c13699eb9dbf41b5528535c0d5fd7673ca36bd", + "docs/architecture/system_monitor.rst": "9b099a9c66f9889ae01f7cadfdc180a48c6be1c70bbc2ba78ff1001a9f75a0e7", + "docs/architecture/system_monitor_core.rst": "2f0288776c503fed3155a206dfe068dbc40675941e97c28499119247220f08af", + "docs/architecture/tabbed_form_registry_pattern.rst": "2e3c1f428765917a7e658b0687f544025a2e46ee2622dc77611702d874593b13", + "docs/architecture/tree_aggregation_strategy.rst": "cdc28440e3560170b20c8d600da96a3a3317f1c3ce285b5a72f8922081c4ffe9", + "docs/architecture/tree_state_sync_system.rst": "1fc2178f9f7f2171cc8a05d064ca3e8a8ff89c44ca9d735c67563286dc913d85", + "docs/architecture/ui_services_architecture.rst": "afa100e0281a8b93571771d2a78c85ce1d75c654268abedd4bfaac87eba19ccc", + "docs/architecture/widget_protocol_system.rst": "9382b7b91dbcd0953bf5e68e2bd93059c84d9b2337f9c6eef1ae0893322b361b", + "docs/architecture/window_code_documents.rst": "21d801e7e83e8d084144efc4a99b23acd48832783eb147f8d88fce39619ca834", + "docs/architecture/window_snapshot.rst": "2ae5ce2a50b165e79766c91f3efb09d55d6e14365de130bf646cfe4605fa97c3", + "docs/architecture/zmq_server_browser_widget.rst": "9b133796b0588362452be0c8ddb92e03a3f1a1578a0d2b0aadd96faf617b8e3c", + "docs/archive/IMPLEMENTATION_NOTES.md": "7ff23ba35575a35cab88851ed46075d9d92c47d34e87c9b9025aecf85b3e2fed", + "docs/archive/JOSS_READINESS.md": "395798c13b9764e131eb6a62fba73bce676f274e573b941cc1c6a1b04dbcb979", + "docs/archive/README.md": "34682a4046790ad3a59f328ed23cb88af9d34f4b649656847bf9863b340f337c", + "docs/conf.py": "2ef313507c9b6ce6941cbe82863a0373b818ea5627c9dec2824953ffc7f312d3", + "docs/development/index.rst": "fc0f4501f81b733702e21d79ca31d8971e9f2605620073b933f7c8147320c5c9", + "docs/development/ui-patterns.rst": "b017023d420458c9964406bfc050242d1f16a1032243492565db35c84dd2ded0", + "docs/development/window_manager_usage.rst": "8d4fdc1d8e3f7cd3d6ac9370680cd66d935f7338b6d249cb6401057feb86279f", + "docs/examples/index.rst": "89c204c6cae088698980aaa189fc2ca0b19f5f0eddbd818f9e8b042691e70fcd", + "docs/examples/ui.rst": "b3f370b8912e8e5ceb0f5510c46a53a5fab01de513b391cd3cfda2991839bc8b", + "docs/index.rst": "f14e5ddb3320bd277ab839925e63620c1f745c2dbbc0bb17460faaffe64cc464", + "docs/make.bat": "ed525cc4d41b62b8b96d4b62a44ef32d6ccfd9324a379a82d5f71b5fb6d57776", + "docs/plans/flash_performance_optimization_plan.md": "e7cc1c22050afed51b5fa908a4099a853e4cc62b86200ad46077b8ac9037ec60", + "docs/plans/opengl_flash_acceleration.md": "28fc4cacf5c19ae7990bcc7fd81a46e8172839ba880ad8a2d85ef2ce50d8afbc", + "docs/plans/opengl_multi_window_architecture.md": "4bf4e27a62cd9d3e61a0086d8b2edb1e6595df49ae3afe0ca379e38efafd383f", + "docs/plans/parameter_form_manager_live_context.md": "dea7242aaa7c9c4e30025a457e8e77927bb1725d8c31267be0634fb24a788aee", + "docs/plans/window_manager_usage.md": "7060816a81b449c2727b1f59cecf71cc7ca5cd536a372828b178104c3e394da6", + "docs/quickstart.md": "98777cb7438f6c72d7443227e92d9b1c4a079971c7e5c6b4ac51c53baecb0b9c", + "docs/quickstart.rst": "25e9faed76719ed6d0ed95ba2416f002e3f0adcb048a605d19b57867e5ec1469", + "docs/responsive_layout_widgets.rst": "96c737263e7ba05cc2e6f21be65ead024e0d894fe6880d30c9ceea351e329156", + "docs/state_management.rst": "c23255071cf550b481797dbc91a406219be4493faca7eb835b8f02651026650d", + "docs/tear_off_tabs.rst": "fea9c04ab60cecbd54aa3b43cffda6f26d6c29ebae2ada6e3999a859bca2ecd7", + "docs/undo_redo.rst": "0cb27f79befce8a209468c86aa120a03725d1bcbabbbf055d27af06ed88169c7", + "examples/tear_off_tabs_example.py": "5506bb39a1953d99d1a8b730851a53233dddd79aa0a4cc738f422fad10b2d640", + "paper.bib": "67e65471f9b4f38e0b4beecfd7cf8b7c2ed815f7b8abfdc680e63262c3652f91", + "paper.md": "b305c23c401bc8ee17d0b1049727c867d86f2bb5c44bc51a77640e74a6c7867a", + "pyproject.toml": "524865db6ac36de136ee2b8dd43b1c741152f5ade3f8195f637650d252dbf8c2", + "requirements-ci.txt": "7da9971ef9b437b35bd507d1e35e09f37d39b4c16b4ef09c9b9a6849d20fdb04", + "scripts/benchmark_form_construction.py": "a2597d87f99c5ad0859e66732f7f7e43a73b4c896b3733d2ee9d501227d1a3d9", + "scripts/check_ruff_added_lines.py": "67ed9e6064204caca990b7b7054cd0774f59b235e241a4fa46db28ad48213ad7", + "scripts/release.py": "09fa8c9c7ccd7f4936aa9a9994b6c47726ce27e8bf99e0cfdcf06d528ff4565f", + "scripts/update_and_release.py": "80896e49f856d6a193bb1dc3917f55d2878acf1476c8240333eda16b92e8e61a", + "scripts/update_version.py": "3f771c59e65b5f6674faef3dafa3b660f152b58ea61114ae0a37f840a9ce4f04", + "scripts/verify_release_ready.py": "7a023ed885a132d922f1fd3942dd56b66158958192f7e5371e838c537cd5d2b5", + "src/pyqt_reactive/__init__.py": "5b37a90f246987a98087428e80d2cbdcc030b80ae962c347482f21719054f68c", + "src/pyqt_reactive/animation/__init__.py": "f449133ede280b74aec90dc85e88a9a2800e38c45b239996d9abde51d0cee6d9", + "src/pyqt_reactive/animation/flash_config.py": "efc6859a2de5115775a940be0a9e1c5e58f7e60e068872f11a256060b045fdeb", + "src/pyqt_reactive/animation/flash_mixin.py": "37cd2b32047109fc16255a3f9a9938ae553d5f8bd5987cd10460c7ba500f1486", + "src/pyqt_reactive/animation/flash_overlay_opengl.py": "f6bc6452e56cae0a17b07244547b55820c091fa20615de3ab1a8159f638f1dd0", + "src/pyqt_reactive/animation/flash_trace.py": "9a2e71467be16a31e0abd1f7999253e4842447289400d15cd3ff995fd06af9a2", + "src/pyqt_reactive/core/__init__.py": "c111f382ba9945651f0b385131896ea2ae522141f7a91f7a1e1eae9657e97122", + "src/pyqt_reactive/core/background_task.py": "4afa0d07107479f028557d23f15a6c5146cdb9f33e253b81f856f9092824c2aa", + "src/pyqt_reactive/core/collapsible_splitter_helper.py": "8d959845294513e0240c994c1bd9142a968f682be994cb2e3fd02f38f0403c3b", + "src/pyqt_reactive/core/debounce_timer.py": "7c3bcef0d7dc0107df0d413f6188ece136e3be5cfea473e3e74b69496af95763", + "src/pyqt_reactive/core/deferred_callback.py": "b52167f36cef2e58bc0f6b38c15359cbfb28a12a71c3a41ad9ac62155f357803", + "src/pyqt_reactive/core/future_completion.py": "7ba6ef203bae81319004ccc4ddfe4226e0230b177df663cfb0d02c364e1dbc47", + "src/pyqt_reactive/core/log_utils.py": "8a2f67434497cf5bc3d637b945b75590c735ff8d97b8db56f9efe5bbaff5e351", + "src/pyqt_reactive/core/path_cache.py": "dfca58e2ae9deb036d4df4038ce8f78125cb5ddeb2e691293bb4d54ca673b586", + "src/pyqt_reactive/core/performance_monitor.py": "09299865cc6641b87bdc195442a297d982ee3909b35c8d2063859933180834eb", + "src/pyqt_reactive/core/reorderable_list_widget.py": "edbba72d7aa88362860b923c0b86715a8349dfec9a7d85660f12cf33d859429e", + "src/pyqt_reactive/core/rich_text_appender.py": "fce3d7be3654c988f6eb16117231e6032718b28608f99f625054ff60269768a2", + "src/pyqt_reactive/core/sort_utils.py": "ac9cb5bd209113980dc398197f461e19e6c9716a5110a889bfdd7bf7b85f3d81", + "src/pyqt_reactive/dialogs/__init__.py": "a9ab9bde099f50c796b1c29c77482f0bf771591fbb12514f1b9ab8dfbddeb4a9", + "src/pyqt_reactive/dialogs/group_by_selector_dialog.py": "a62fdc652c3ff9fe2ac3babce17968a5786877e38ff5b6c2a10ea0678d5a5e67", + "src/pyqt_reactive/flash_trace.py": "c0110553bed7908221c10d1a33e48f4324366b047eda75f5b2561b68f5855063", + "src/pyqt_reactive/forms/__init__.py": "5c9c1af2df09c479f1cfc50fb8d8ee7ff3e157a940d137fcde8af95c95d035a5", + "src/pyqt_reactive/forms/form_init_service.py": "4d34e5f2494a075f9aac68e40c07b07cdb06600d832bd5b510ee971db980b82d", + "src/pyqt_reactive/forms/form_value_validation.py": "ec3ca9d48168b651e8fe0b0ef6f90a67ad32f145332a8798f1cfad220c9fd597", + "src/pyqt_reactive/forms/inline_dataclass_chrome.py": "5d224ea256bffbb840f380d9ad75f9f98b7c3ed18fc4b1ba4db0b64f92a428a1", + "src/pyqt_reactive/forms/inline_dataclass_context.py": "c0d7a19651dc1b88e66b656a031b39a7aae67ff8998e50cc4e5962a184f4fb10", + "src/pyqt_reactive/forms/layout_constants.py": "a16ebe77ea6fb5d6c26d658bc586dd8357d18506c0935cb6426edbffc906ff98", + "src/pyqt_reactive/forms/object_form_document_renderer.py": "c47fada4032d685b6bf7dca4cb9f31e4e9240a25bd1039b165b045d5de541f1f", + "src/pyqt_reactive/forms/parameter_form_base.py": "f35bb8ddd6c630a47cc5095df93ffee608a384abadf755b2f0cf12bcc963b36b", + "src/pyqt_reactive/forms/parameter_form_chrome_sync.py": "94fda5faae9adaddb1cddd1b7a4d3b0452906828d94c582f6024ff72d89ca4a3", + "src/pyqt_reactive/forms/parameter_form_constants.py": "4d4ae05b91d525108342a10e9b2da7860b5a52b0028612f244339d4531d02097", + "src/pyqt_reactive/forms/parameter_form_manager.py": "a561e62e29af42630fa107b0929fc87b74007f7ed14ec8f0a6881c49070dc923", + "src/pyqt_reactive/forms/parameter_form_service.py": "0cbf305c99b0d7ab0c558162ba5d86dcd18f81eb8cce0ad271fbd4b3d15ebc89", + "src/pyqt_reactive/forms/parameter_form_tree_index.py": "e343c3ae5535bbd46dff964844225df13fad5431253eb3924b86f8c8d1dad273", + "src/pyqt_reactive/forms/parameter_info_types.py": "3f87562ecc9b1a4cbf541767ae39d53e1b2dcfe71d9a5e211456d8b34a0a75f8", + "src/pyqt_reactive/forms/parameter_type_utils.py": "c7fd2f17faa1e3d5c3027179135de51b859d204c4a2cb9f9be10837652530ea6", + "src/pyqt_reactive/forms/parameter_value_contracts.py": "109b82548f6a90e1d495d825eeb81309b555b6122db67867a2c91769e9c54f78", + "src/pyqt_reactive/forms/ui_utils.py": "96521f8a8b75f501ca9f2dd34f377025a7d5fdf543dcbed84ec17939875ca0ec", + "src/pyqt_reactive/forms/widget_creation_config.py": "01f32efb0dadc4d5e1a42344f43e7704c47468844b4a05bd13936e490ad4489a", + "src/pyqt_reactive/forms/widget_creation_types.py": "3b1c08a0a94334facb76afa273fef0d247a93f748ce86c3fb9e7026d46ef844f", + "src/pyqt_reactive/forms/widget_dispatcher.py": "5b13da9b2aee79344c343ddcbc296f125429c9780696ed35f996e982c3ee1562", + "src/pyqt_reactive/forms/widget_operations.py": "708bf6dfd51c01154e02210abe8840eeb496af51db57cfda425d51a365080b72", + "src/pyqt_reactive/forms/widget_registry.py": "6529d5879667827e7c6adf36129e6dfdfa18239c4e0ae32bec27eab7b03805c0", + "src/pyqt_reactive/forms/widget_strategies.py": "6eb7fc78526a247dc30c39493f36f54f2df5560b0af63d743d41b30b4f469384", + "src/pyqt_reactive/pattern_metadata.py": "7c2aa3f2660d530503acdbf8e122711cf7b3d1c0aae2d05bf6addb7091dcb4cd", + "src/pyqt_reactive/process_launch.py": "c324a6bcc3b5002bd4e6e5db184d4fd80bdcdeb7b130565ca52eb37bbaa08f9e", + "src/pyqt_reactive/process_signals.py": "8b4c44c98d6979fab6935485ea94d64bdfec6eb0143855078628451d4cd9ce82", + "src/pyqt_reactive/protocols/__init__.py": "2f59ae586807161ac8d019d904646684be0cc458b8b4bcef66f3d233002fd294", + "src/pyqt_reactive/protocols/codegen_provider.py": "98c030fbf5fc99ac8ce53bba0305f0591e0b1a12403b1cd22dd282d1df476fb3", + "src/pyqt_reactive/protocols/component_selection.py": "a68d8ca325a0972c7e911901a47521e44882699729f98bd4cfde57cda7b4d1fb", + "src/pyqt_reactive/protocols/form_config.py": "cfee9524e2e2c92fe1317d9cf1229580df1e1c04a45d9583c4b97fde1b3543e2", + "src/pyqt_reactive/protocols/log_providers.py": "c6602cec12dfa687a6f4e122aa62ebaf3ba7c5472a0d63b29ab0af9db3c18514", + "src/pyqt_reactive/protocols/preview_formatter.py": "05ed6999327cf4483a79d3bb5c9d1f1bc231fd3c0a33026e1e7069f1e8d3771c", + "src/pyqt_reactive/protocols/widget_adapters.py": "1ae6653bf3f2786eafa9f53f246e145ebfa34b81c190aaa8f41c19a1463a42f9", + "src/pyqt_reactive/protocols/widget_protocols.py": "4877e65746459f556954eb718d276748fd5211d244e15f1e92973e5ee01c24d4", + "src/pyqt_reactive/qt_types.py": "cca393ec5c0126b296c2462b414a84acd0cd1b5f63de9a2b6112a0b6593921b7", + "src/pyqt_reactive/services/__init__.py": "9ec1b77dc39cd6e0f550e7ca014c4e6a560ac0fa46f071c0f6ea47a6a34fe46a", + "src/pyqt_reactive/services/async_operation_executor.py": "f1252a0244f816b2187881bee484776ebd41529d1a17df08a1360967d4e03516", + "src/pyqt_reactive/services/coalesced_scan_requests.py": "5a565f9ed1bf34871f2802400fbe6b9ae7623cc612f07dbde44df10e92a1fe21", + "src/pyqt_reactive/services/enabled_field_styling_service.py": "0a16f22878c4ff68a3a75c937ad50314a0acf14014ceb39bf2420673a75e01ac", + "src/pyqt_reactive/services/enum_dispatch_service.py": "20b18b0ef51d7ecdab99e3fe0fb20fc38c605ce80995945f5e1033e24bb4bee4", + "src/pyqt_reactive/services/executable_action.py": "6942ff087473fc102c5395184aa74c8b6c8088cab5d7fa394bc5e103e41c5d28", + "src/pyqt_reactive/services/field_change_dispatcher.py": "0875f01a405b95de12cb0d4fc396d959a758d67a39c211d3056c512352d0f1c5", + "src/pyqt_reactive/services/flag_context_manager.py": "88b634b8aa899f82f6e9525101ade1f60b0be11763a33908ed3ad23ad3bfe147", + "src/pyqt_reactive/services/function_list_editor_actions.py": "68a0fe00e8b80cf1154aaa1545725b48b0c4b75e338afbcd23fed159d6d100d3", + "src/pyqt_reactive/services/function_navigation.py": "47664c983d2f515f2c4e6599eb4fbf269817e69f9abe68101872e6380ee60e85", + "src/pyqt_reactive/services/function_pattern_code_document.py": "bdf41b58bff009890e6e696cfc4f18313adfb1669566b2566f9e7b93d863c071", + "src/pyqt_reactive/services/help_document.py": "c0c1973e7ab02823c204dfd7003ac2a2b44acea36b895a82604ac31d0f4db1e3", + "src/pyqt_reactive/services/interval_snapshot_poller.py": "77fcd5b147bcf66782ebb1bbd6a9e9a8379c9e38672a47fd14fd2432eeeed11f", + "src/pyqt_reactive/services/manager_host_abc.py": "a9a3ffc9b2cc2c5d6f6b1224c89838f74bc9d41d2c8de8da39bf33f98483b90a", + "src/pyqt_reactive/services/parameter_help_service.py": "422f9e7b818f855f28979400512b4943524606827a74bf807bbf26afef1a589c", + "src/pyqt_reactive/services/parameter_ops_service.py": "8814b7c13bc56a2b0d3f69fbfb2a6153b4e2b5d0433779db6ffce5ba480890e7", + "src/pyqt_reactive/services/parameter_service_abc.py": "c5818d83e39444cbd88e135e2a075e45872d49f8c83c34e1d0e925e7de38d387", + "src/pyqt_reactive/services/pattern_data_manager.py": "ddcf9659dab6ee50ea3c155f57e1a4111a97e414ecf74b58151da1ef6557ac88", + "src/pyqt_reactive/services/persistent_system_monitor.py": "2041aaa91a3e242bd43e1686a65feed89078d18f8f7626bff00225895bde0007", + "src/pyqt_reactive/services/process_tracker.py": "5850b647c4c75ab2ced0ea91be34fe99b8e1a004cf3b2df597caa7769f7bc899", + "src/pyqt_reactive/services/scope_color_service.py": "b9d958c27205ca5a9eed16dd729a354642d90bcca96a519b01a0b19d94b1c822", + "src/pyqt_reactive/services/scope_token_service.py": "f84056a6b542d1d9a7df9e05c2ec344d6acb7cafd3301054158e89f04eb32b17", + "src/pyqt_reactive/services/scope_window_factory.py": "fc2a4606f89c85d44014ea6d08018e34921b8898816d8a09ca8febf80a448338", + "src/pyqt_reactive/services/scope_window_navigation.py": "39a0b4e95ce6e9a478c5882b91aa0822f53b683c9f2a706727bbf88922679331", + "src/pyqt_reactive/services/search_service.py": "e72ef88fdaf28d069bc80c2eec722ffa6921015613815929006fa35fd7de2054", + "src/pyqt_reactive/services/service_registry.py": "8f6dff4b7e6db9eb4ba2a0944d18cfa3c03b75e9cef85594b1e8bc7c5bd03e58", + "src/pyqt_reactive/services/signal_service.py": "04680bff5098b52edb1f4998356c1ef9543bcca96b3f37a7377ce4f98c1f18b1", + "src/pyqt_reactive/services/system_metrics_sampler.py": "7f0df0d9425a9998e371c86f55a27cc492fb71b36817ae07b6280f73e25b8ad6", + "src/pyqt_reactive/services/system_monitor_actions.py": "2aa205be430ff8b0429279a3ec44e5bc0c672b6da7a246a208f1ed40b79aded3", + "src/pyqt_reactive/services/system_monitor_config.py": "2f7259cafbdfd28f19d2a291afcd2deeef4e5eba2402bfb0e303a0aa643d6dd4", + "src/pyqt_reactive/services/system_monitor_core.py": "1712ed50c76e6303dcb96f65c8294922084bf2e3f90d21be56b766abca0dd39a", + "src/pyqt_reactive/services/tab_identity.py": "48274c530cd0792b3565c1becf1cef9c90d2f48b4e6ccec26cdd8807c51715ff", + "src/pyqt_reactive/services/tree_item_key.py": "e908f21bd6cb1621cd8356611f1ea0d7daa59d19171b211f3c3038598b8a5aef", + "src/pyqt_reactive/services/ui_thread_dispatch.py": "f2dbe7e8ce8ac0ad3116001d7dfcd91affd516a34615775ce4ce9921480e40bd", + "src/pyqt_reactive/services/value_collection_service.py": "33562f9b50e1080a26c28467ec31c3915e5f734bdc237cc04f4802db2b36b911", + "src/pyqt_reactive/services/widget_service.py": "cf3248353552db77b3257cbcf982987848fb16acd38ab2245dbef1fcf5e25223", + "src/pyqt_reactive/services/widget_tree_projection.py": "05d5221d64e74ff14483f203975f17543381d959c30ee5918c200fa8e9957547", + "src/pyqt_reactive/services/widget_tree_projection_config.py": "8f616aa3f2fe1dc88077dc612cb7c527970363692c6a8ace15b92ba56a806e4b", + "src/pyqt_reactive/services/window_code_document.py": "d0ab321af0eb2b14fac3ab2577c8c61672763d2f13a70a7ce8a35ee7bfab7c75", + "src/pyqt_reactive/services/window_manager.py": "84fff75ff74e59c5d7080f103ad1a4baa2c884aff3d2f87891a0afdae9a5c332", + "src/pyqt_reactive/services/window_navigation.py": "88da77ed07ea220b7c6874e71029b5bd50805fb2c3048701f9df773fe17257f2", + "src/pyqt_reactive/services/window_snapshot.py": "bced886e6bbfd69ae9bdebf950027b809e72c23eddadcaa5fcf7151adf7ae913", + "src/pyqt_reactive/services/zmq_server_info.py": "1050f0e9ada59580eba9a6c4a703678f0945f4a2cd83df4fb32251733911b81d", + "src/pyqt_reactive/services/zmq_server_scan_service.py": "ae12092dfd8b85a3354b87a708f3c32aef787c2f9fd0402158d32d14b654c7de", + "src/pyqt_reactive/strategies/__init__.py": "cab5cb18e271d822d459ab8b7f6427f03b8beee5ac9a4d04b4b73a0914d58e9b", + "src/pyqt_reactive/strategies/preview_formatting.py": "dc1a423246ca40f13633847d5d3ffde3916e53c1a8a8a92de893d3e80ad8fda7", + "src/pyqt_reactive/strategies/status_presentation.py": "435d97f7804e2db714cf60b236074c7a56fc87542b05a4d7a4b1722a340a7893", + "src/pyqt_reactive/strategies/tree_aggregation.py": "d28be44b8472fe1db805fb706485f02bae2c48d99e41f422dc534d24e0084c5f", + "src/pyqt_reactive/theming/__init__.py": "ddbd3f379805b6ebc01a86c86ba41999617b46498b4436563e5a427afea3866d", + "src/pyqt_reactive/theming/accent_chrome.py": "2ae2fd7cc5b3d2b60a99942792ed0621040339e117338107be01fc6128b36f86", + "src/pyqt_reactive/theming/color_scheme.py": "45f621f809accb5dd3fab03c1bbe95b43d680e6b4e03680e89ac2fe6400f16e4", + "src/pyqt_reactive/theming/color_scheme_resolution.py": "e8c9a7501268979d0fbb3e02f693b31a63e08688e80c3cb23abe0a856e1de09a", + "src/pyqt_reactive/theming/palette_manager.py": "00b81b76f39b561eb7a4bf37d0547bf2db6e7b63efa18ef752f614005f56be90", + "src/pyqt_reactive/theming/style_generator.py": "7433368f14efdefa05a401501122704c08f061412a343531fba5400a5807c885", + "src/pyqt_reactive/theming/themed_checkbox.py": "93426143261ca2c1da431264cec69e89ee76a35d7bf85d9e54db27e72c058ea3", + "src/pyqt_reactive/utils/__init__.py": "6b8f0030c2822411d7efb3433a2276320b985423ade229529196cf09fce17e3f", + "src/pyqt_reactive/utils/log_highlight_client.py": "db8e2fd014eedc85af1d0805250931f74257a402651edad2bedbc4588cc82f7b", + "src/pyqt_reactive/utils/log_highlighter.py": "e38237cfea9d757910a7b4ac84a8095068ea1c4531abd56c6fd15228b40e4703", + "src/pyqt_reactive/utils/log_loader.py": "c0cc1d6e4c7ac286562e2659b327e738f3cbed32686c09292b0d957ea61d159d", + "src/pyqt_reactive/utils/log_streamer.py": "ac195bf633e690695d61c1ea4cdfa7bcb45a4ecc20dc237f35bc220a87a08669", + "src/pyqt_reactive/utils/preview_formatters.py": "76a383c498300e2672d6b67ee0261e0dd90df0ed2fd64fc2ffd3513cdf097838", + "src/pyqt_reactive/utils/scroll_filter.py": "cb38375d28d5613f162dc020e2b8735824ceea767ae5cc1156c7086240e22fa8", + "src/pyqt_reactive/utils/styling_utils.py": "22d32d0e01fca8f575c15fa4a61b27cde2e46616cb86088733e8d15f312a1da6", + "src/pyqt_reactive/utils/window_utils.py": "11b222927102bc79d8a5f6a957bd99ab1e828bfb6586e1d647aebd564b99afb2", + "src/pyqt_reactive/weak_events.py": "446b5ea3e71eef459a2afd3663aac3e05a4242c6e9e21379f79fadf9563202c8", + "src/pyqt_reactive/widgets/__init__.py": "ddcf678b0dc5c03aaf11d2b6f12bb9cda681a29f0105925c51a5b9263d751716", + "src/pyqt_reactive/widgets/editors/__init__.py": "6fdb2746dd03af27487402eddb929f4f10664f6dab82726dba5894dee8d33878", + "src/pyqt_reactive/widgets/editors/simple_code_editor.py": "b84df51fd669c550dddfc5484e178f585d7b05f9ca3b230cab6bb82b3d0ecdab", + "src/pyqt_reactive/widgets/enhanced_path_widget.py": "5c72029a13fcf4e58a99ece7bb898afebab083f20c104f9255082b5f9ab1b232", + "src/pyqt_reactive/widgets/function_list_editor.py": "3a71a7af92cd6a3e304a030f7e821710af53cefb63c9e13cfb331c61b86d48b6", + "src/pyqt_reactive/widgets/function_pane.py": "da0f781f1948df80c856751fb6ca5f552583395d0f5b2d2beaa83d03738e1abf", + "src/pyqt_reactive/widgets/help_document_browser.py": "f8f0bb2e454ef458f2fd4f686f4d6814a717e8f1da0de1790ddb6f8a96795e8a", + "src/pyqt_reactive/widgets/log_viewer.py": "e68eda879c7cd3f5360dd1b4a08a481298cabf85cd72bdfda66b0efbd54d56f9", + "src/pyqt_reactive/widgets/mixins/__init__.py": "fc5176659b1c3239954ce3257c3615c5053c65e233ff350c869da5a4a356bfb5", + "src/pyqt_reactive/widgets/mixins/cross_window_preview_mixin.py": "663d644a603e6da4a07eb06faabc7f4eedc21ebab392094b3c76fa2f0abb3117", + "src/pyqt_reactive/widgets/mixins/selection_preservation_mixin.py": "94fc92d98854f4bbc611fe616033abccea19ea401ef9a3e9c53ea1017a77bd66", + "src/pyqt_reactive/widgets/no_scroll_spinbox.py": "f6be116d4976fc8a83ff576ef6ca0968ed95312b6333631a7705727dede10cfd", + "src/pyqt_reactive/widgets/shared/__init__.py": "e7525c16a67b93240ce3597d7f6e6b7b9bc9841d6e4b2de1084d10fd6aea2c4b", + "src/pyqt_reactive/widgets/shared/abstract_manager_widget.py": "92f8ede35a48392112c6c1aa17e959c3cc4a9450e69f5b1447cbc4ef5498dd5b", + "src/pyqt_reactive/widgets/shared/abstract_table_browser.py": "478feab4d742bfbc4170df25e5e3cc3bc0b409506192290817a064250dd9ff5d", + "src/pyqt_reactive/widgets/shared/action_tabbed_window_body.py": "246128cd317deb382bf830a67fc6468f768e6e5547ff0e17896b16e304371ecf", + "src/pyqt_reactive/widgets/shared/base_form_dialog.py": "9b5280ef06b1c8421b3889c1c477fb311dca3b5f960e8ecfb58a0a7812551369", + "src/pyqt_reactive/widgets/shared/base_managed_window.py": "3fda59f66502bfb9ce14565981f2c8738d074c78a08ae3afc9c8e00e93c882fe", + "src/pyqt_reactive/widgets/shared/button_factory.py": "78b69f821ad925773ca049d0c08715375a4d38eaf7ed075701417401b461a14c", + "src/pyqt_reactive/widgets/shared/button_panel.py": "280f904f101681461a8899b0dae53fd10e0f616e54511ee09c3324c2a589ee43", + "src/pyqt_reactive/widgets/shared/checkbox_group_widget.py": "593b9a2c1358556c710a9a3b406042e6e335cee5c46aff1e1ef42e5025c36a4f", + "src/pyqt_reactive/widgets/shared/clickable_help_components.py": "c480f45796f893e8f9431198a0d08f5acfee3ead9034764005587aae88902e4d", + "src/pyqt_reactive/widgets/shared/column_filter_widget.py": "7e492ba7cd8d2d91ead001be293bdd69dc6a98332102e30ed45d0bb6968be589", + "src/pyqt_reactive/widgets/shared/config_hierarchy_tree.py": "150062486b1e4d092ce703cee83a3d400097267dba3670d9a0120378f0045b34", + "src/pyqt_reactive/widgets/shared/config_tree_contracts.py": "8e6b4b0047219c2f7c8297de1baf7437b2571016bd2d82e1f2e1c520bc4a35e0", + "src/pyqt_reactive/widgets/shared/detachable_action_bar.py": "37157c0874e5495d3fc5c4b33042d6334b439a57d912e354ff48529e1d434ef1", + "src/pyqt_reactive/widgets/shared/dirty_window_presenter.py": "d0171ce0b9009780675f8eee0b528fe3fd2446c8b800934a476778e1c554a147", + "src/pyqt_reactive/widgets/shared/form_window_action_header.py": "18725af2a223d9c00b2496f298f745af1628c5e0cd07a2cbfc8e39b684b574c2", + "src/pyqt_reactive/widgets/shared/function_table_browser.py": "654c394929b744ca3fd6293fadfced3966d64626ddcf5f8b74f46e63cf026340", + "src/pyqt_reactive/widgets/shared/image_table_browser.py": "3a9b45805225021f6f17174e06985ab19f75b473bc0dfbf3ecfa7a5eef9931e2", + "src/pyqt_reactive/widgets/shared/list_item_delegate.py": "282c8fe0c88f9bfd51df7eb95c739a8d428d2e9d99478796bb073c698f5907a7", + "src/pyqt_reactive/widgets/shared/list_item_text_rendering.py": "0d997923c78dcb363748daf685cdc4e93ef5d6a4ee1f7d74100cd5266cdc5220", + "src/pyqt_reactive/widgets/shared/manager_action_controller.py": "e21b2407ec7cf6f891a7a67b2ae554e741f4a16aa35537b31e3b9992c009b317", + "src/pyqt_reactive/widgets/shared/manager_item_access.py": "21540ce53728a72028cc3c9af770bbca5210aa7c18bd4f697cad42bf5d27b276", + "src/pyqt_reactive/widgets/shared/manager_item_display_builder.py": "fe1d01ba449c69a052ed796e9c6901b080e2982b565f3ae0e0d73ad6599da45f", + "src/pyqt_reactive/widgets/shared/manager_item_hooks.py": "3dbb99e17114c9869db7a4cdf9284574921f4865e10f90a2161877c41da6c222", + "src/pyqt_reactive/widgets/shared/manager_list_updater.py": "c7036bb3f37198b06f5af6859c7bc6d9a0a85520da126286d31f00750a93b50b", + "src/pyqt_reactive/widgets/shared/manager_list_visual_state.py": "ba8a54d4b682d7bf61c2286439894d59eeddebf160102f4367038866cb54a2bd", + "src/pyqt_reactive/widgets/shared/manager_preview_formatting.py": "b9057f97f87f757272027baf402740db99da945fb04a4985ad0df5dc11f29b24", + "src/pyqt_reactive/widgets/shared/manager_reorder_controller.py": "64c829c64193e6cd1b604fc883fc0406fc43ecc329adc5066fa93196e2d67551", + "src/pyqt_reactive/widgets/shared/manager_selection_controller.py": "900fcec4c0a8e61b60130cc7a53ef8b0fd157d906182f96aa85fe57e5f008d46", + "src/pyqt_reactive/widgets/shared/manager_state_binding.py": "5fc9598ab1319cdf7048ab4e20c36c9d4c64d6ba7da584efc039e6c333cf888b", + "src/pyqt_reactive/widgets/shared/manager_status_controller.py": "b0e9fb4010775206b9b66889115be864d22f8e060b67bba2ef30dc0cccca4f9c", + "src/pyqt_reactive/widgets/shared/manager_time_travel_binding.py": "4cfeeef4badfe945c8ce2bd0e86254251fe79c5884261ac2d3f07c649ac3bda3", + "src/pyqt_reactive/widgets/shared/manager_ui_scaffold.py": "1332a8b69c8c562fd49146ea29a6b9aa5e156c90db06a00731e7716788e84589", + "src/pyqt_reactive/widgets/shared/manager_workflows.py": "e9e107b3129aa6f8725031abf35b2a67a52840643efdd120554ec78b13ce157a", + "src/pyqt_reactive/widgets/shared/reflowing_vertical_scroll_area.py": "7b04fd0dcd92ae470f9b6e7005bbae811ad3a614ddac86592381d840269c4387", + "src/pyqt_reactive/widgets/shared/responsive_groupbox_title.py": "a195a584afe51c456b35038a55d63c9e8cef708ca559df01d6dbbe42e399db0e", + "src/pyqt_reactive/widgets/shared/responsive_layout_widgets.py": "4adf600b914cf0769ca37c490523f744ac5fc2e5809028a6a24f21661e05359c", + "src/pyqt_reactive/widgets/shared/scope_border_renderer.py": "ab5a8246f3ca1d88bcd935f76fe59a33782e8ccb6944aff943386fe06fc46268", + "src/pyqt_reactive/widgets/shared/scope_color_receiver.py": "8b75714a16d6cf90d0f55c68f2d7d0935b0a1fa906ac2fd659c3409406568905", + "src/pyqt_reactive/widgets/shared/scope_color_strategy.py": "aa53231e9830e66721ef637177ae1470403cc9a13519015babdec8c824febffe", + "src/pyqt_reactive/widgets/shared/scope_color_utils.py": "d4c317df9c3d1391c3f8768b2bbd041435196cc876f16be2f929b63cab8aae10", + "src/pyqt_reactive/widgets/shared/scope_style_applier.py": "cd649a820631ceb367444c38ef3722489308147f9857f451cb95d5373d9ee763", + "src/pyqt_reactive/widgets/shared/scope_visual_config.py": "51286f8aa2f960e9d656f11f5ee57a11f1b2e1213511d0d17b46ad7be3b6fa4a", + "src/pyqt_reactive/widgets/shared/scoped_border_mixin.py": "f291c293302adf863574b6abf398605687d0325a8e3af16c37abb929d3fdbfad", + "src/pyqt_reactive/widgets/shared/scoped_table_widget.py": "cf81e6a8075ae89b10a46dc0e262de9f465cf5a0cd25a915dadb91dc0601a2b0", + "src/pyqt_reactive/widgets/shared/scrollable_form_body.py": "5a00352eda3b8e7932a878c18ef6ceae68a58358d15987dd8155702b70afd20c", + "src/pyqt_reactive/widgets/shared/scrollable_form_mixin.py": "e1ac397789a515097f464fb72b9378ba91e5dfbc6bfb6c316054049004fdcffe", + "src/pyqt_reactive/widgets/shared/styled_text_layout.py": "f174770e600b58e9d7b22f10db7a7e176d11207d534d090926e747af2ab594bd", + "src/pyqt_reactive/widgets/shared/tabbed_form_widget.py": "f2914db1586963f3dfe3cda7d6940d13b2bbb7ec1ffb4bb894c3844443b34a4d", + "src/pyqt_reactive/widgets/shared/tear_off_registry.py": "b44b1d391588d2290ff50b7f73379a875698e61de926e2ea153470744399091d", + "src/pyqt_reactive/widgets/shared/tear_off_tab_widget.py": "6b4fa8aebd7134cb2451135662f6b9aaee39a688c7b3ad8b37639985c7447fc7", + "src/pyqt_reactive/widgets/shared/tree_rebuild_coordinator.py": "deb28fea82fc963f999876dc54b37a4d0e310995bb16a1892befebdef79eccaa", + "src/pyqt_reactive/widgets/shared/tree_state_adapter.py": "ba4877d3431f4cfe344c530493b934b6d181254492070ca210fdcf2ed07f148c", + "src/pyqt_reactive/widgets/shared/tree_sync_adapter.py": "0031145c849a7ab6f2c4787bdc44692306073f08b2c17bea9b12932c45698f07", + "src/pyqt_reactive/widgets/shared/zmq_server_browser_widget.py": "35a7bc0ce56461336967a2a3f2e357edf8ccae122beb179d9dd438c86df105ae", + "src/pyqt_reactive/widgets/status_bar.py": "7acd1e194c1bdb350e2dd288adc9c55a37b1027fed97df0c5fb3e77ed794c940", + "src/pyqt_reactive/widgets/status_indicator.py": "96cd497e82336c02feec80dcc7c1bbf2e81336055e5b933f1cd1b96b240a0489", + "src/pyqt_reactive/widgets/structural_table.py": "4a757027ced7529f7b4f7737c09dbfa2d9dda48e7944b5375566299d3315f346", + "src/pyqt_reactive/widgets/system_monitor.py": "620f54ccd75eeeb0e370db4d461cfbf82f3625b898d343bb7c214de1cb5d671b", + "src/pyqt_reactive/windows/help_window_manager.py": "3946e3c1f7a99e75f86c87f44fab0af1282454dbf58dc95db14bb5a3fd66c7d2", + "tests/__init__.py": "db6cc46f8f6d69c35c72a39f4829527f661a63d906f091bc59a2055943a43281", + "tests/conftest.py": "911c65976a80aa48405b9c6bf0b18830599a2185c05434cbef8b7c25c4a48b39", + "tests/test_accent_chrome.py": "0862933afb738e2866611167a7c70e412d15e72ea46f9712a0b641587cadb709", + "tests/test_action_tabbed_window_body_lazy.py": "6f5b16efb53510e2a2806e1e412d89438c5e93c4efa5a9568439daf5a9daf9e8", + "tests/test_async_operation_executor.py": "2c8bc81bff7a3fd8879b6b79d2e9e09a3a22610fbb3785d970400e5e55dcd84e", + "tests/test_ci_action_pinning.py": "a636dd4d70e231d06c5de2802afc89c389f678b3b49a40ce8463eac5b0c308dd", + "tests/test_column_presentation.py": "aa83341b1443c131637e5b2b498b42cfed858927fa3f7ded00885e03f38e688a", + "tests/test_compact_dataclass_projection.py": "b765e0b8ced31a5cc351125bb2d86670e30cc973779c0b6931c75b026a14b271", + "tests/test_core.py": "a2aba8923fd176a75a9e94f5c3b1fa0a6d18f41b28935b0c4d39ed75892894fb", + "tests/test_deferred_callback.py": "1c59913d1b19a9725e5735bbd342dc115fdf4b555863427169b5001324e83b1c", + "tests/test_enabled_field_styling_ownership.py": "d831ab09cb4a0b92c3c33e4f2c368874a454c85eb8943d9acce48388ea961263", + "tests/test_enabled_title_row_ownership.py": "01052fea145b31698ec29319f521179adda5945ce324f3d976bdcc144aeb5be4", + "tests/test_explicit_function_pattern_overrides.py": "69179658cd4d89cdbf8bd469166e819f3421c39a6fcb6a38cc330dbbcb592a33", + "tests/test_field_mutation_authorization.py": "cab1faf400063e32df23249457738b9b28f59c760a6fe4d3cfcbba34884800ea", + "tests/test_flash_destruction_lifetime.py": "a567e8b978182773d316e9a7dc7934b80da2708e2804bd8e84898b4b1a030b37", + "tests/test_flash_mask_rendering.py": "9ca43be6bb9bb98a6950e9025688a6f40d8c7d050dec2bbe5721c215c47122f8", + "tests/test_flash_playback.py": "51438a64b08edec35a54447dbf3acd8aebd77a8dde92dfd7ccb3b0755bc196df", + "tests/test_flash_scroll_ownership.py": "f660469075d1c96e1fdce5bfaa0bd94857001d5e4f9ccf33fbae9ce9b2a51072", + "tests/test_form_build_transaction.py": "fc9860a115de764ee14d2a246fb5dd2d66bb60b159ae7f5853050d1e28442417", + "tests/test_function_list_editor_tokens.py": "5eea6d8a08f76c22fe81f4fca16b80d82ab8bb2ccc6f21428e5eed739065741b", + "tests/test_function_table_browser.py": "e0289caa47564647e1952a8592cda03840a58b11fb10fac6e6db9b35b1045541", + "tests/test_future_completion.py": "fddee2a4c4b210e43c137a24f5730c0e8303a38ecc67afec7a4ac5f39d2ad860", + "tests/test_help_document.py": "b28d3fb53b253372cdd4e5fa542faf5ef9a2cd7f6b42cd3a3694c2713f4f5768", + "tests/test_help_document_browser.py": "b04eca203a5d7be904ef86f490370bb71b598bc9f9d8f2ec441d118c4baa5d71", + "tests/test_host_provider_contracts.py": "ccc75dee1cd12eb05b29debe8da84a9b98a4714607bebdb9ad75c901f24f846e", + "tests/test_inline_dataclass_chrome.py": "e2c62d17ff58ad6dbe3dc2ec7e8caa1b8eeabc7e26f92c27a1a4489d1b12656b", + "tests/test_key_sequence_adapter_lifecycle.py": "bb4e57ee2548822f06252bdf07a9e3f36d569a01165f8707c2687150e4364824", + "tests/test_list_preview_wrapping.py": "60bfe5b16e9f17f3fed1f88213e69644ad2bcf278d7012b446689fbc75e181a2", + "tests/test_log_viewer_lifecycle.py": "4c7372276b7e65d84568c01489a544df7b9b2df791910c21cba11ad9c0eaff75", + "tests/test_manager_ui_scaffold.py": "cef43797060e94628d04fc581268ccd7e054cd5810b37105a68ce7930594eba6", + "tests/test_nominal_contract_architecture.py": "d02bb10514bab0bb58ad9585f986e208d88365838597eef520186f066685867a", + "tests/test_numeric_roundtrip.py": "5975b0ce8bd7bfcf1b21bf03593b72882c775a7982d834894081b6012c556566", + "tests/test_parameter_form_service.py": "30bb0cbb28b6edd5c144a58cc30a4e9fc068f9e56ee2dbe19e39f2b64fd1982d", + "tests/test_parameter_form_tree_index.py": "d28a29f77a68a22b3fce087de2c2679f2a291dd6a66cecf94f68ac700c1fded5", + "tests/test_parameter_help_service.py": "d41f813fbefd005003ab90a6e83d44727cc1adf925a9d521095068cbed2355b7", + "tests/test_parameter_ops_placeholder_capability.py": "8e4006e9b04df1efbcc4d878a68505a7057b83d6ac44e5b47dc1ea0bdd9dad6f", + "tests/test_path_name_roles.py": "86f5ef6cc8677dd92faa349d6e3254bbf6edc734e31fe0e13f8436dc4283c34a", + "tests/test_preview_configuration_imports.py": "7bec54d0815d88d2dce8dea9b73febbf23216b32c06666eab34e1487f0ec142b", + "tests/test_preview_formatter_registry.py": "9b62974f3e42be630f8f38a72d894f937affd6a694fe6f13c71629b5a38104ee", + "tests/test_preview_formatters.py": "4d6bbd9ada4ced6e6d2b49ff8c582d30b6341dd3403ee0c458a6bc4f0e595e49", + "tests/test_process_launch.py": "49c14b825f7fd3f7f7aa51f727c436c529cd2aaabefa3bee13e9f9e9d955e489", + "tests/test_process_signals.py": "de4d134edc1345bfe53df7f8b4ed362a24e4b3da35cfbc2132c5f6fb5107a662", + "tests/test_protocols.py": "3843eb80885957639b40d1cd260c87d0680df3a63d6ac448a084a25e8a37d8f2", + "tests/test_qt_types.py": "c24a5242ec6395b8c72723540e4b100e41f573d071444fc8c27eaac9a42de911", + "tests/test_reset_flash_changes.py": "3b901bd26e339f4585d90f25a70957fd949c7bdecbcfd56c0100c5a80f006d4b", + "tests/test_scoped_border_mixin.py": "9914504211aeaeb0cc713b9aa2c11619a00bfc53dc8dc6aa68d2d5ce3bd2f538", + "tests/test_scoped_table_widget.py": "764ed376f2e284682ddab7ab8379071d19ee703c391ea8196562b8f7b5b9a2fb", + "tests/test_scroll_area_content.py": "4b6f4a9e1fed02a267e2c707b1618ad3de8c451fe5e44b2f404360ef00e579ca", + "tests/test_scrollable_form_viewport_context.py": "d445d86f08bee9cc364868d4f50c976ee95bd85827b02790353c9f7439c9df7b", + "tests/test_status_indicator.py": "78f0e0b01548b8d676fd6ed75c79a800a4399826dcc5d2770ff3bb49a7e2a85e", + "tests/test_system_metrics_sampler.py": "524776fcf817233d461402b0f096623763f119a5d690a738434b390c6c752b07", + "tests/test_system_monitor_config.py": "2cc61be642ee7c9e26dab9e13497800bd3f85490a5253e5634a1374d4fe8287a", + "tests/test_theming.py": "bb811e5a73ee0a4fb95a0b87a40165442c39e3afdfa6c6ca6c0061590dae781c", + "tests/test_tree_state_adapter.py": "76d12b348ed6a015cb9b8f5bf3173968ca1b17a219df21aae82df23b12341907", + "tests/test_ui_thread_dispatch.py": "3478d122a45ee394edd3ab191f46636c62fe3b1ba1705d80e48c9e764b31e8d7", + "tests/test_weak_events.py": "d752129493c8cdd1d427024c64c7f02cd37d0e7a87bbf854b1ed332d174fa710", + "tests/test_widget_projection_safety.py": "f124cf36ca48fe51bae0aedad633f420bccd469ea8d9059d82d64dff3ca841e2", + "tests/test_widget_tree_projection.py": "3d6a1224caf0913ca194b13f4cf791cfcb3ce9e9c38b88281fe2dac4a0e222ef", + "tests/test_widgets.py": "f24b7bace49d7c6ce5357d38e9b5362ba0625b9f4421222788ab661ae278ed19", + "tests/test_window_code_document.py": "5c546e6151f19a8bcebfaa6a332a301b888416236baa982e2ae761e015326712", + "tests/test_window_navigation.py": "0f2bd021ca9d547f2b3d64e42cba1d13d0ffb1ed52e86bd07df836d5d3f649e3", + "tests/test_window_snapshot.py": "33fa1d5509217b0c16e926f7b4643ae1d5176ac77434f16099acd9b7cd52b8e2", + "tests/test_window_snapshot_bindings.py": "44af6189d6bf64bcfcad9e87150d2f01620e2a2c0b4877d581f64428176decf9", + "tests/test_zmq_server_browser_lifecycle.py": "2789e1a2cd5c81bddd37b62eb5fbe507228972e9f0dac0d1ea06b87dda066579" + } + } + }, + "native_binaries": { + "openhcs/core/_tabular_native.abi3.so": "2ce348417de70ad3f614962c82aed9c4977c32b3ba00e13531ecafb8c10d8d1d", + "openhcs/processing/backends/cellprofiler/_granularity_native.abi3.so": "6e45987a50d5c0588a8fb0b09a2232a63e72c122d6a3d583ae4d62830e3b4096" + }, + "inputs": { + "/home/ts/code/projects/openhcs/benchmark/reference_exports/official30_value_completion_20260914/ExampleIlluminationCorrection_Example3.cppipe": "f7ec9eb561730cc567986e6a50504a69f23a3ca78a1a033faba0b2fea52759fb", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/--W00001--P00001--Z00000--T00000--cherry.tif": "f073bc8f8102f05e6443226fcb986e7d30b3f9f0d7cd77d25c7c9f82ee11b782", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/ADSAStaphInfection2_A01_w2247376DD-6ADD-442D-AE47-F54A05F3EA94.tif": "5a57b7557bb0da5e241b48851b82d936c7ec36ede7aaca5e77b28ac4bfebf502", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O01f00d2.TIF": "5bfdeeb9e1dba87dc55220f71168af3d9949f38b8d59ba741b546619c2a49fd3", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O01f01d2.TIF": "8ddf231a2727fe0dd5e80d1747a968ecc67438def175dd2b4a5850bf54699c2b", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O01f02d2.TIF": "5a7bf7ff3504baaf787f004e2137059ea2ca890a37842df5360a035b78e49940", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O02f00d2.TIF": "57f0a36092341c746441c837c78b103f35ef6460a71df77f5ae9ee74cd82ad4e", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O02f01d2.TIF": "d1b6057f138d897c3dba0d055e90e17ca5239523264c138c863bd2c4c7e453fa", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O02f02d2.TIF": "4b52dc59a87b5230d2e8701971d42df4f0e96966cf1308ce6eb7dc4865c1000e", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O03f00d2.TIF": "269e5ab417a616ef55679321f56ee99e870abe4e519c5c24c911b39b1527e7fd", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O03f01d2.TIF": "0e95bb9ab0feeab2d0ddd1a5cc877475b5c8001ae4873b9ebcaaa44107acec78", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O03f02d2.TIF": "a26dbf0381c2404bb0f495e9c68d7f2ec8fa87eae6edc7c95b5a3fc55f548a97", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O04f00d2.TIF": "fce63090cdf6281b51ac538d7af3840610f2e275af45d1cf8dd59ee2bf92377a", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O04f01d2.TIF": "e27fd485ee359490f13efa73bf7bbaef2c6743ecadba05d21a22d156ed9b892e", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O04f02d2.TIF": "fa078664928f35b7b0d1a3a971e39602bbb775f78e9001c3dcd81e392c570bf5", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O05f00d2.TIF": "cb80c657237e9cbdadba93d49d9a10cf35531ea2cb7246c5cf15d6cac1a30651", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O05f01d2.TIF": "6035bce54914bc2ce236dd1c37eda3dbeb83752ad6c3c3746e57ae13041ec868", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O05f02d2.TIF": "64e64f69f7b6abd5ed2db49b1a39dcf4036706a2a7cebef48a5188c41e0598e2", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O06f00d2.TIF": "076a52b3f4e257d18f45b01a61bfeeac112531fb5036890f2f2aa44f2ea36fdf", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O06f01d2.TIF": "72e69058831189fb1e799209c6d8bfd90d52e2736d96352ecca08735238c8384", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O06f02d2.TIF": "3c1a864005429908707a07a7f26debc838b9fff9328953a2838b7b629b305d85", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O07f00d2.TIF": "e4adc0b64624c1e83deca015415473d77f323ed7d6cad0322472874cb32b2aa3", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O07f01d2.TIF": "ce3310b92a870a24abc9e81319d319fef07a6fe6a99961c174b1bc06bb65fc47", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O07f02d2.TIF": "0ada0de31dec60745bece90d3d67c5ca5dbec6fa0532d61afe621602eef95543", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O08f00d2.TIF": "ff903c458dbfdbdd076952533aa1f3b17aa71d6b2add8cb79ead0f1649d8a189", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O08f01d2.TIF": "0f3c0748ed5ec06b7e95e411a7744571e3338e3c37ff5b0336d707db997e939c", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O08f02d2.TIF": "97f926715e7ee9743e7b382808486de24a0b386db6712cf9b38a012fcb5b3e0e", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O09f00d2.TIF": "c60dd32928613a0905306b3697279138828520bc8a9420e662dbe79799cba89c", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O09f01d2.TIF": "917e37b5cd8d60c9e2caa89472246fb94d1c51f1a350554556cf826fae276437", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O09f02d2.TIF": "7f3750dd2ea324983f4ab1fcac59183a410267d1f8221f2963f7e74d19646450", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O10f00d2.TIF": "fe52a74223e5a46d3d2b434d7dacc3d2517f2c5b10f0d04b7a3cbf7fbf41e63d", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O10f01d2.TIF": "fc325b8baff83f25402f8398760df8b2bb72425a98077ede74fa05446b22ac88", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O10f02d2.TIF": "b763c7dc7a8a539f2eaa52517c60c39b0673b9777c4642555ba86b5774d6b0fd", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O11f00d2.TIF": "3f05a9e77c8cdf6c5c29c0af26a147bcc63c110bf0bc438abc27497501cfeda6", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O11f01d2.TIF": "93e08b9408c31d4317b2ef2db997c4df3efa7ae8d8d32738cb0ec0c5c9ec1d1f", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O11f02d2.TIF": "25304455c0b64544bcf09aed892c61cd37c3f6535db2447741f0fd0f08eea27c", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O12f00d2.TIF": "6876b895933a84d9dbe7d94e0db1862fdc19ae006a9be1ed76c0e1ec390a95cb", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O12f01d2.TIF": "b29ea362fcf4f1276977f5e09804b3caeca1dba4c030fa285ce69fb7f26b6044", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O12f02d2.TIF": "e086c6b414fa5345577bb9d1cbcfcfdbd183e639e9039b50f1c08b5b67a00f36", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O13f00d2.TIF": "cae644c4b99556fccddbab42098f8dbdaa723996c521557f3b59caf3430b82d7", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O13f01d2.TIF": "17e88f342e33075b08338cb191df919c85eb3a5fa8efb3ddfcae57e69e776be3", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O13f02d2.TIF": "7bfe66d604eeb2b996e368f6a0e36891f2dd0d6ba5d819b4104614d0f0236bd2", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O14f00d2.TIF": "844889a709a3326137fd67171c0b8ec018b397fe1ede836a9b7e5809e075f58d", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O14f01d2.TIF": "968ea530f2a3aab735664fff7936ebc4e406a7112fd84b89d92431ba27178c96", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O14f02d2.TIF": "5193e9ba72e56eaaff791c214c6be34766522b6aaab383ad8e9893f0734e60c3", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O15f00d2.TIF": "488149e5f50630d794c6c57340df6271a693035bbfa46142ed6422cbfa64c8ae", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O15f01d2.TIF": "3278f9402a8110bb622b2a8c62b8087d19b35736ae9a9e6361e675af67ecc13f", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O15f02d2.TIF": "25a40d29b3e2525c976b2288013bdbeb0a2eee7026f172d100586ff9a51ce348", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O16f00d2.TIF": "16fe3c9674e55150497d1d573ccc262419314ae62acb1c76dca858c40f72f744", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O16f01d2.TIF": "69133d455b367c919d5f2bcde19ab2c6223ef57b06d2c3011393e8c9f1a709c9", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O16f02d2.TIF": "b0c7dbcf259ad76e9fccb6de614b5d15f542317bb95c97a85233fbdcb73c9de3", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O17f00d2.TIF": "d049efceda2aea79923d4b8df69de9144ba104c77a06bf168184830cc30f30d4", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O17f01d2.TIF": "bae6ec7694d0868c74c079f5cd24534542f2835d4cab41ed8d82586c662ce4d1", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O17f02d2.TIF": "c991d219a21e6f1274b01bb6f370c4fb44addb122c1e292c36acd33def0787e2", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O18f00d2.TIF": "15e3dde0bdcd792bde186c6576cbb45d64723fb50b5d4c1841ad9ce3c6386f2d", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O18f01d2.TIF": "cf8e231352f5bf3f0b63aa29cf72a21c90a15ad2233e8495068a778399a056d0", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O18f02d2.TIF": "f67d7dba8a06dea16da609d350a98647d181c7b175109b940c74627e4c7fc4fd", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O19f00d2.TIF": "c370003080a53449570d1d780b9dd84626bd3cfa49c80c6db0ccdb493cbf6a63", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O19f01d2.TIF": "1a5b7ad836b7f86e187c52eb4e9f76dbc71b2057bf6ab8ff57d676314811a930", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O19f02d2.TIF": "e8e3774e2ad39eeb9dcfb25cb92d7c75c022f86de5fab772a36e60a332cc7cf1", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O20f00d2.TIF": "99cf977fccdc4cbdb1ba8a1a2992dbf4d1c26b7f6d822a18d0db323e842ace8e", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O20f01d2.TIF": "6847c82a3ea55a29c1f55c7dcf251e5b8a776eb207107f41aaabf3978a855c58", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O20f02d2.TIF": "2e0059c518aa222960520be791721a52837a948d22bf1d2fb4000b37ec89e881", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O21f00d2.TIF": "26fb12cc1228982b372da290eabe06897521db386800aca9518535fead716870", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O21f01d2.TIF": "1c59f7447309b5854ebcac27ed3cd10c564894b4cdb6510865b447bca4029c95", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O21f02d2.TIF": "9c0d1062e13d193427eb146b882e19c54ccbbba531df786d797e7d13756198b0", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O22f00d2.TIF": "fafa8607d475c46e645433036df14b58ec9ba7c850ef5ff37b8253bd8ea67f05", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O22f01d2.TIF": "2183fc92523891136905acf96fc2bdad04e00657f8070911ac3d4c1950ebf822", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O22f02d2.TIF": "5684cec26114f2813f41d4c42ba2bbe982b01bdcad78d80d73cfb78e07f9c790", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O23f00d2.TIF": "7e2420883575733a3d759752d633df10eec665b9d0575173063365366320a8b7", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O23f01d2.TIF": "f61467aadaf8e4c92cab7daff2cf5cf99b87783d5a055f69d9043c8b9b958554", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O23f02d2.TIF": "0a0839f901f76e8f11e85c94af74e0c8f1df302cc7fd88cd7429e83e694329c7", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O24f00d2.TIF": "ecd17866fa382365023790c9c874255e9248ce77fa374abfaec73a62f7cb898d", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O24f01d2.TIF": "8ac212dcc8b92fb953bd4a17f2b6a49a9b824c521b5c0fb62bfafacadd03a44d", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/AS_09047_050428030001_O24f02d2.TIF": "adf56aa932a3e04f19e5eb2b0f5d85a15b52fa2ffa0a0e57b6754c6de818eee0", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleWoundHealing/ExampleWoundHealing.cppipe": "61da2bdd29daec73813645fef978fe237ecd92229433106d80e9bb0c65a3c8e6", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleWoundHealing/images/DMSO_B5_t0.JPG": "1d91c175dd7aa433e63d218b9d352e6065c5c5c888631366f6d0716fa384f642", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleWoundHealing/images/DMSO_B5_t24.JPG": "fc83c19de66538f1619728cd52930b867fb87bbcfa39d85ab9838eae7e097198" + }, + "python": "edffa803e5c4d73a1357f7f2c6c9fd38e9693712b2c06793e6a5e5cc1090d03a", + "native_python": "84c920f304e265c53096e42d83309c6f7b829988f937a7dd15c24eebcb63f59a", + "native_pyvenv": "9adb0795d69940b55f3d8fb0a35b0dceff4f9388f4e0ac2c9efb11e6b4fe2cbc", + "java": "fc4c7fbe8b539370e4c435da65e562ad1c09e833a1767b0455249406ec1ca762", + "libjvm": "c8912093ef5e3f93f7409f4e9e475978ded9193ca06762d8ca6b047169af0d0a", + "pip_freeze": "abi3audit==0.0.26\nabi3info==2026.9.25\naiobotocore==3.9.1\naiohappyeyeballs==2.7.1\naiohttp==3.14.3\naioitertools==0.13.0\naiosignal==1.4.0\nannotated-doc==0.0.5\nannotated-types==0.8.0\nanyio==4.15.1\napp-model==0.5.1\narray-api-compat==1.15.0\n-e git+https://github.com/OpenHCSDev/arraybridge.git@1e53d03d9f468322a1c085c8be29485fb139caf2#egg=arraybridge&subdirectory=../../../external/arraybridge\nast_serialize==0.11.2\nasttokens==3.0.2\nattrs==26.1.0\nautopep8==2.3.2\nblack==26.5.1\nbotocore==1.43.75\nbuild==1.6.1\ncachey==0.2.1\ncattrs==26.2.1\ncentrosome==1.3.4\ncertifi==2026.7.22\ncffi==2.1.1\ncharset-normalizer==3.5.1\ncjdk==0.5.0\nclick==8.5.0\ncloudpickle==3.1.2\ncolorama==0.4.6\ncomm==0.2.3\ncontourpy==1.4.0\ncoverage==7.16.2\ncryptography==50.0.1\ncycler==0.12.1\ndask==2026.8.0\ndebugpy==1.8.22\ndefusedxml==0.7.1\nDeprecated==3.0.0\ndeprecation==2.1.0\ndill==0.4.1\ndistinctipy==1.3.4\ndocstring_parser==0.18.0\ndocutils==0.23\ndonfig==0.8.1.post1\net_xmlfile==2.0.0\nexecnet==2.1.2\nexecuting==2.2.1\nflexcache==0.3\nflexparser==0.4\nfonttools==4.66.1\nfreetype-py==2.5.1\nfrozenlist==1.8.0\nfsspec==2026.9.0\ngenbadge==1.1.3\ngoogle-crc32c==1.9.0\nGPUtil==1.4.0\nh11==0.16.0\nHeapDict==1.0.1\nhsluv==5.0.4\nhttpcore==1.0.9\nhttpx==0.28.1\nhttpx-sse==0.4.3\nidna==3.20\nimagecodecs==2026.8.16\nImageIO==2.38.0\nimglyb==2.1.0\nin-n-out==0.2.1\niniconfig==2.3.0\nipycanvas==0.14.3\nipyevents==2.0.4\nipykernel==6.31.0\nipympl==0.10.0\nipython==9.17.1\nipython_pygments_lexers==1.1.1\nipywidgets==8.1.9\njax==0.9.2\njaxlib==0.9.2\njedi==0.20.0\njgo==3.1.0\njmespath==1.1.0\njoblib==1.6.0\njpype1==1.7.1\njsonschema==4.26.0\njsonschema-specifications==2025.9.1\njupyter_client==8.10.0\njupyter_core==5.9.1\njupyterlab_widgets==3.0.17\nkaitaistruct==0.11\nkiwisolver==1.5.1\nlabeling==0.1.15\nlazy-loader==0.6\nlibrt==0.16.0\nllvmlite==0.49.0\nlocket==1.0.0\nlxml==6.1.3\nmagicgui==0.10.2\nmahotas==1.4.19\nmarkdown-it-py==4.2.0\nmatplotlib==3.11.2\nmatplotlib-inline==0.2.2\nmcp==1.30.0\nmdurl==0.1.2\n-e git+https://github.com/OpenHCSDev/metaclass-registry.git@393a7e03003cdc56df9013f932ed4f26e632d77a#egg=metaclass_registry&subdirectory=../../../external/metaclass-registry\nml_dtypes==0.6.0\nmsgspec==0.22.0\nMulti-Template-Matching==2.0.1\nmultidict==6.9.1\nmypy==2.3.1\nmypy_extensions==1.1.0\nnapari==0.9.2\nnapari-console==0.1.4\nnapari-crop==0.1.12\nnapari-plugin-engine==0.2.1\nnapari-resources==1.0.1\nnapari-svg==0.2.1\nnapari-tools-menu==0.1.20\nnapari-workflows==0.2.11\nnarwhals==2.26.0\nnest-asyncio==1.6.0\nnetworkx==3.7\nnpe2==0.9.0\nnumba==0.67.0\nnumcodecs==0.17.0\nnumpy==2.5.3\n-e git+https://github.com/OpenHCSDev/ObjectState.git@a01f8939b9bf4659502a33fe3febe62cab0f3c9a#egg=objectstate&subdirectory=../../../../../../openhcs-shared-runtime-plumbing/external/ObjectState\nome-zarr==0.19.2\nome-zarr-models==1.8.1\nopencv-python-headless==5.0.0.93\n-e git+https://github.com/OpenHCSDev/openhcs.git@902913616e19f5b242928d4e835b8c2e10ab2fce#egg=openhcs\nopenhcs-basicpy==1.3.1\nopenpyxl==3.1.5\nopt_einsum==3.4.0\npackaging==26.3\npandas==3.0.6\nparso==0.8.7\npartd==1.4.2\npathspec==1.1.1\npefile==2024.8.26\npexpect==4.9.0\npillow==11.3.0\nPint==0.26.1\npip==26.2.1\nplatformdirs==4.12.2\npluggy==1.6.0\n-e git+https://github.com/OpenHCSDev/PolyStore.git@84f322e46871de5ed47e7fd20976ad03e533c1c4#egg=polystore&subdirectory=../../../external/PolyStore\npooch==1.9.0\nportalocker==4.4.0\nprogressbar2==4.6.0\nprompt_toolkit==3.0.53\npropcache==0.5.4\npsutil==7.2.2\npsygnal==0.16.1\nptyprocess==0.7.0\npure_eval==0.2.4\npycodestyle==2.15.0\n-e git+https://github.com/OpenHCSDev/pycodify.git@792b03413331fdf107e369c924c257a2f0b16126#egg=pycodify&subdirectory=../../../external/pycodify\npyconify==0.2.1\npycparser==3.0\npydantic==2.12.5\npydantic-extra-types==2.11.1\npydantic-settings==2.15.0\npydantic-zarr==0.10.0\npydantic_core==2.41.5\npyelftools==0.33\nPygments==2.21.0\npyimagej==1.7.0\nPyJWT==2.15.1\nPyOpenGL==3.1.10\npyparsing==3.3.3\npyproject_hooks==1.3.3\n-e git+https://github.com/OpenHCSDev/PyQT-reactive.git@ad4948775ab81180a354d4b793d17ee5ddff3972#egg=pyqt_reactive&subdirectory=../../../external/pyqt-reactive\nPyQt6==6.11.0\nPyQt6-QScintilla==2.14.1\nPyQt6-Qt6==6.11.2\nPyQt6_sip==13.12.0\npyqtgraph==0.14.0\npytest==9.1.1\npytest-asyncio==1.4.0\npytest-cov==7.1.0\npytest-qt==4.5.0\npytest-xdist==3.8.0\npython-dateutil==2.9.0.post0\npython-dotenv==1.2.3\n-e git+https://github.com/OpenHCSDev/python-introspect.git@ed1e4eb15f7a0bf8315274d3e47858fa2b4372bb#egg=python_introspect&subdirectory=../../../../../../openhcs-shared-runtime-plumbing/external/python-introspect\npython-multipart==0.0.32\npython-utils==4.0.1\npytokens==0.4.1\nPyWavelets==1.10.0\nPyYAML==6.0.3\npyzmq==27.2.0\nqtconsole==5.7.2\nQtPy==2.4.3\nrangehttpserver==1.4.0\nreferencing==0.37.0\nrequests==2.34.2\nrequests-cache==1.1.1\nrich==15.0.0\nroifile==2026.9.22\nrpds-py==2026.6.3\nruff==0.16.9\ns3fs==2026.9.0\nscikit-image==0.26.0\nscikit-learn==1.9.1\nscipy==1.18.1\nscyjava==1.12.5\nsemver==3.1.0\nsetuptools==84.0.0\nshellingham==1.5.4\nsix==1.17.0\nskan==0.13.1\nsse-starlette==3.5.0\nstack-data==0.6.3\nstackview==0.19.1\nstarlette==1.7.0\nsuperqt==0.8.2\nthreadpoolctl==3.7.0\ntifffile==2026.9.20\ntomli_w==1.2.0\ntomlkit==0.15.1\ntoolz==1.1.0\ntornado==6.5.10\ntqdm==4.70.1\ntraitlets==5.16.1\ntransformnd==0.7.2\ntyper==0.27.2\ntyping-inspection==0.4.4\ntyping_extensions==4.16.0\nurl-normalize==3.0.1\nurllib3==2.8.0\nuvicorn==0.54.0\nvispy==0.17.0\nwatchdog==6.0.0\nwcag-contrast-ratio==0.9\nwcwidth==0.9.1\nwidgetsnbextension==4.0.16\nwordcloud==1.9.6\nwrapt==2.5.0\nxarray==2026.7.0\nxlsxwriter==3.2.9\nyarl==1.25.1\nzarr==3.4.0\n-e git+https://github.com/OpenHCSDev/ZMQRuntime.git@668edafcb5ee530a163377c936431ae1e8334e85#egg=zmqruntime&subdirectory=../../../external/zmqruntime\n", + "native_pip_freeze": "annotated-types==0.7.0\nboto3==1.42.97\nbotocore==1.42.97\ncellprofiler==4.2.8.1\ncellprofiler-core==4.2.8.1\ncentrosome==1.2.3\ncertifi==2026.7.22\ncharset-normalizer==3.5.1\ncontourpy==1.1.1\ncycler==0.12.1\ndeprecation==2.1.0\ndocutils==0.15.2\nfonttools==4.60.2\nfuture==1.0.0\nh5py==3.7.0\nidna==3.20\nImageIO==2.37.2\nimportlib_resources==6.5.2\ninflect==6.2.0\nJinja2==3.1.6\njmespath==1.1.0\njoblib==1.5.3\nkiwisolver==1.4.7\nmahotas==1.4.18\nMarkupSafe==3.0.3\nmatplotlib==3.7.5\nmysqlclient==1.4.6\nnetworkx==3.2.1\nnumpy==1.24.4\npackaging==26.3\npillow==11.3.0\npip==26.0.1\nprokaryote==2.4.4\npsutil==7.2.2\npydantic==2.13.5\npydantic_core==2.46.5\npyparsing==3.3.3\npython-bioformats==4.1.0\npython-dateutil==2.9.0.post0\npython-javabridge==4.0.5\nPyWavelets==1.4.1\npyzmq==22.3.0\nrequests==2.32.5\ns3transfer==0.16.1\nscikit-image==0.18.3\nscikit-learn==0.24.2\nscipy==1.9.0\nsentry-sdk==0.18.0\nsetuptools==80.9.0\nsix==1.17.0\nthreadpoolctl==3.7.0\ntifffile==2022.4.8\ntyping-inspection==0.4.2\ntyping_extensions==4.16.0\nurllib3==1.26.20\nwheel==0.48.0\nzipp==3.23.1\n", + "environment_sha256": "18b33cab3a61c45a940312d5ea4e1e108bc5d57261a520f3d9c5058db3ef3776", + "helpers": { + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-preparation-v1/controller.py": "0b7abed65ecc3d61761533a3a9a69e9b9a8acb220049175ff46fb5f004ade4aa", + "/var/tmp/run_narrow450_runtime_phase_probe_20261002.py": "93a4b1720d3d2c64e77b722c91597be42c7c9968bad42a4503e5588fc7fc4a55", + "/var/tmp/openhcs_measurement_sparse_inventory_v2_20261003.py": "481d6ee99045090e6dac0e1d287a70cd33698575e06375780c123c66ed0bf52b", + "/tmp/run_integrated_fields_ordinary_abba_20261002.py": "8a41db448a722d231777a90a3ae23091617c8ac1ba72cefcb91f72b2c7975554" + } + }, + "requests": { + "ExampleIlluminationCorrection_Example3": { + "request": { + "pipeline_path": "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleIlluminationCorrection_Example3/preparation/native_cellprofiler_headless/ExampleIlluminationCorrection_Example3.cppipe", + "input_dir": "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images", + "file_list_path": "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleIlluminationCorrection_Example3/preparation/native_cellprofiler_file_list.txt", + "output_root": "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleIlluminationCorrection_Example3/outputs", + "expected_image_sets": 1, + "repetitions": 2 + }, + "provenance": { + "native_input_domain_strategy": "selected_wells", + "native_source_workspace": "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleIlluminationCorrection_Example3/preparation/native_cellprofiler_selected_source_workspace", + "native_selected_wells": [ + "A01" + ], + "native_selected_source_file_count": 1, + "native_selected_source_mode": "file_list", + "native_selected_source_flattened": false, + "native_selected_source_staging_root": null + }, + "prepared_pipeline_sha256": "313e72af5e42332d46c8035ff72c25b6d18ac66bfe3426d72f8badfe8271d8f7", + "file_list_sha256": "84cdf25570fc4ed34acee7d8b83a456d2dbe7ba5f87d8c2d59f52501e2ff8e1b", + "expected_source_rows": [ + { + "virtual_path": "W001_s001_w1_z001_t001.tif", + "address": { + "site": "1", + "channel": "1", + "z_index": "1", + "timepoint": "1", + "well": "W001" + }, + "ref": { + "backend": "disk", + "backend_address": "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/ADSAStaphInfection2_A01_w2247376DD-6ADD-442D-AE47-F54A05F3EA94.tif", + "source_axis_indices": [] + }, + "projection_role": "primary_plane", + "source_alias": "OrigWorms", + "source_metadata": { + "OpenHCSSourceVoxelSpacingZYX": "1,1,1", + "OpenHCSSourceVoxelSpacingUnit": "relative", + "OpenHCSSourceFilterPaths": { + "0": "ADSAStaphInfection2_A01_w2247376DD-6ADD-442D-AE47-F54A05F3EA94.tif", + "1": "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images/ADSAStaphInfection2_A01_w2247376DD-6ADD-442D-AE47-F54A05F3EA94.tif" + }, + "site": "1", + "channel": "1", + "z_index": "1", + "timepoint": "1", + "well": "W001" + }, + "component_labels": { + "channel": "OrigWorms" + } + } + ], + "expected_metadata_sha256": "9488f587c8b8e1098a137a851c12a61952a1b089be6af8faf41b6d83f61f9d1b", + "plane_members": [ + "channel" + ], + "native_worker_sha256": "b377740417fbb9b78839ec792b634246a168de16736224a5991e8fb1ae70be76" + }, + "ExampleWoundHealing": { + "request": { + "pipeline_path": "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleWoundHealing/preparation/native_cellprofiler_headless/ExampleWoundHealing.cppipe", + "input_dir": "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleWoundHealing/images", + "file_list_path": "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleWoundHealing/preparation/native_cellprofiler_file_list.txt", + "output_root": "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleWoundHealing/outputs", + "expected_image_sets": 2, + "repetitions": 2 + }, + "provenance": { + "native_input_domain_strategy": "selected_wells", + "native_source_workspace": "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleWoundHealing/preparation/native_cellprofiler_selected_source_workspace", + "native_selected_wells": [ + "A01" + ], + "native_selected_source_file_count": 2, + "native_selected_source_mode": "file_list", + "native_selected_source_flattened": false, + "native_selected_source_staging_root": null + }, + "prepared_pipeline_sha256": "492f8be03ef821ebf56ac43023a956f368fd89f03abfdf9e5ed5cefe3ac790fb", + "file_list_sha256": "f00beb9e827b95803f59725b3dd7b98817fdb815bd4c0361300daeb8b5e3f2ab", + "expected_source_rows": [ + { + "virtual_path": "W001_s001_w1_z001_t001.tif", + "address": { + "site": "1", + "channel": "1", + "z_index": "1", + "timepoint": "1", + "well": "W001" + }, + "ref": { + "backend": "disk", + "backend_address": "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleWoundHealing/images/DMSO_B5_t0.JPG", + "source_axis_indices": [] + }, + "projection_role": "primary_plane", + "source_alias": "OrigColor", + "source_metadata": { + "OpenHCSSourceVoxelSpacingZYX": "1,1,1", + "OpenHCSSourceVoxelSpacingUnit": "relative", + "OpenHCSSourceFilterPaths": { + "0": "DMSO_B5_t0.JPG", + "1": "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleWoundHealing/images/DMSO_B5_t0.JPG" + }, + "site": "1", + "channel": "1", + "z_index": "1", + "timepoint": "1", + "well": "W001" + }, + "component_labels": { + "channel": "OrigColor" + } + }, + { + "virtual_path": "W001_s002_w1_z001_t001.tif", + "address": { + "site": "2", + "channel": "1", + "z_index": "1", + "timepoint": "1", + "well": "W001" + }, + "ref": { + "backend": "disk", + "backend_address": "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleWoundHealing/images/DMSO_B5_t24.JPG", + "source_axis_indices": [] + }, + "projection_role": "primary_plane", + "source_alias": "OrigColor", + "source_metadata": { + "OpenHCSSourceVoxelSpacingZYX": "1,1,1", + "OpenHCSSourceVoxelSpacingUnit": "relative", + "OpenHCSSourceFilterPaths": { + "0": "DMSO_B5_t24.JPG", + "1": "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleWoundHealing/images/DMSO_B5_t24.JPG" + }, + "site": "2", + "channel": "1", + "z_index": "1", + "timepoint": "1", + "well": "W001" + }, + "component_labels": { + "channel": "OrigColor" + } + } + ], + "expected_metadata_sha256": "205410fd1df7c623f8e1b76d1cb859da191f953ea73ee3b723dd1ef47dd103f6", + "plane_members": [ + "channel" + ], + "native_worker_sha256": "b377740417fbb9b78839ec792b634246a168de16736224a5991e8fb1ae70be76" + } + }, + "cases": [ + "ExampleIlluminationCorrection_Example3", + "ExampleWoundHealing" + ], + "repetitions": 2, + "scope": "Current-source two-case paired descriptive ordinary/native comparison. Default OUTCOMES/RSS; READY/import/JVM/load/warmup excluded through existing harness clocks. No hooks/profiles/pixel captures or manual timing." +} diff --git a/benchmark/results/perf_current_wound_illum3_paired_20261003/strict-science.json b/benchmark/results/perf_current_wound_illum3_paired_20261003/strict-science.json new file mode 100644 index 0000000000..41d4d9868c --- /dev/null +++ b/benchmark/results/perf_current_wound_illum3_paired_20261003/strict-science.json @@ -0,0 +1,491 @@ +{ + "status": "PASS", + "comparisons": [ + { + "case": "ExampleIlluminationCorrection_Example3", + "ordinary_repeat": 0, + "native_repeat": 0, + "measurement_differences": 0, + "image_differences": 0, + "declared_export_comparisons": [ + { + "artifact": { + "artifact_name": "PolynomialCorrected", + "artifact_type": "ImageArtifactType", + "semantic_kind": "numeric_image_pixels", + "output_filename": "reference_image__PolynomialCorrected.npy", + "comparison": "float pixels: atol=1e-6, rtol=1e-6, zero mismatches" + }, + "reference_path": "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleIlluminationCorrection_Example3/outputs/0/reference_image__PolynomialCorrected.npy", + "candidate_path": "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/ordinary-0/ExampleIlluminationCorrection_Example3/wells_1/workers_1/images_ExampleIlluminationCorrection_Example3_source_workspace_well_throughput/results/reference_image__PolynomialCorrected.npy", + "reference_shape": [ + 463, + 461 + ], + "candidate_shape": [ + 463, + 461 + ], + "reference_dtype": "float32", + "candidate_dtype": "float32", + "reference_pixel_digest": "b5073cb872058f88bb250be9e7a39bedd2ef281f23dfedde5262e4431f1fb0fe", + "candidate_pixel_digest": "8aedbeab3f9868c1d1d46b0aafb3b20bb9dfd107dc08bb0882577c493f08173b", + "compared_pixel_count": 213443, + "different_pixel_count": null, + "out_of_tolerance_pixel_count": 0, + "max_abs_difference": 2.9802322387695312e-08, + "equivalent": true + }, + { + "artifact": { + "artifact_name": "ConvexHullCorrWorm", + "artifact_type": "ImageArtifactType", + "semantic_kind": "numeric_image_pixels", + "output_filename": "reference_image__ConvexHullCorrWorm.npy", + "comparison": "float pixels: atol=1e-6, rtol=1e-6, zero mismatches" + }, + "reference_path": "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleIlluminationCorrection_Example3/outputs/0/reference_image__ConvexHullCorrWorm.npy", + "candidate_path": "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/ordinary-0/ExampleIlluminationCorrection_Example3/wells_1/workers_1/images_ExampleIlluminationCorrection_Example3_source_workspace_well_throughput/results/reference_image__ConvexHullCorrWorm.npy", + "reference_shape": [ + 463, + 461 + ], + "candidate_shape": [ + 463, + 461 + ], + "reference_dtype": "float32", + "candidate_dtype": "float32", + "reference_pixel_digest": "fdc35df90fb97b2d2ab79e4acdaa00aef26d4d9c76fb53d6aa53351bd598dd9b", + "candidate_pixel_digest": "fdc35df90fb97b2d2ab79e4acdaa00aef26d4d9c76fb53d6aa53351bd598dd9b", + "compared_pixel_count": 213443, + "different_pixel_count": null, + "out_of_tolerance_pixel_count": 0, + "max_abs_difference": 0.0, + "equivalent": true + } + ], + "tables": [ + 0, + 0 + ], + "physical_images": [ + 2, + 2 + ], + "logical_images": [ + 2, + 2 + ], + "known_relationship_keys": [ + 0, + 0 + ], + "ordinary_receipt_sha256": "05161397be3c2b6ab87b13e8387080ba7f4de39af35e25b9882d9ca4591e4180", + "output_hashes": { + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleIlluminationCorrection_Example3/outputs/0/reference_image__PolynomialCorrected.npy": "588e6f9951aa7ad220678dbb0053e98fb6d324e71aab089d1d828d891ee32f54", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleIlluminationCorrection_Example3/outputs/0/reference_image__ConvexHullCorrWorm.npy": "9ba1259e4ca007c06f14be6946e32a86307ec2dfb9952b73fa2cc253ccae8b3a", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/ordinary-0/ExampleIlluminationCorrection_Example3/wells_1/workers_1/images_ExampleIlluminationCorrection_Example3_source_workspace_well_throughput/results/reference_image__PolynomialCorrected.npy": "8875da63a072c8f2de4f5cb707d779a064b663bf69aa0f304fe6fa74d4ff57a6", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/ordinary-0/ExampleIlluminationCorrection_Example3/wells_1/workers_1/images_ExampleIlluminationCorrection_Example3_source_workspace_well_throughput/openhcs_metadata.json": "579ac084b396dff5f7fbaa844139bf952c2576b6cb0242772c925ba9839c2267", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/ordinary-0/ExampleIlluminationCorrection_Example3/wells_1/workers_1/images_ExampleIlluminationCorrection_Example3_source_workspace_well_throughput/results/reference_image__ConvexHullCorrWorm.npy": "9ba1259e4ca007c06f14be6946e32a86307ec2dfb9952b73fa2cc253ccae8b3a", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/ordinary-0/ExampleIlluminationCorrection_Example3/wells_1/workers_1/images_ExampleIlluminationCorrection_Example3_source_workspace_well_throughput/openhcs_metadata.json.lock": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + { + "case": "ExampleIlluminationCorrection_Example3", + "ordinary_repeat": 0, + "native_repeat": 1, + "measurement_differences": 0, + "image_differences": 0, + "declared_export_comparisons": [ + { + "artifact": { + "artifact_name": "PolynomialCorrected", + "artifact_type": "ImageArtifactType", + "semantic_kind": "numeric_image_pixels", + "output_filename": "reference_image__PolynomialCorrected.npy", + "comparison": "float pixels: atol=1e-6, rtol=1e-6, zero mismatches" + }, + "reference_path": "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleIlluminationCorrection_Example3/outputs/1/reference_image__PolynomialCorrected.npy", + "candidate_path": "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/ordinary-0/ExampleIlluminationCorrection_Example3/wells_1/workers_1/images_ExampleIlluminationCorrection_Example3_source_workspace_well_throughput/results/reference_image__PolynomialCorrected.npy", + "reference_shape": [ + 463, + 461 + ], + "candidate_shape": [ + 463, + 461 + ], + "reference_dtype": "float32", + "candidate_dtype": "float32", + "reference_pixel_digest": "b5073cb872058f88bb250be9e7a39bedd2ef281f23dfedde5262e4431f1fb0fe", + "candidate_pixel_digest": "8aedbeab3f9868c1d1d46b0aafb3b20bb9dfd107dc08bb0882577c493f08173b", + "compared_pixel_count": 213443, + "different_pixel_count": null, + "out_of_tolerance_pixel_count": 0, + "max_abs_difference": 2.9802322387695312e-08, + "equivalent": true + }, + { + "artifact": { + "artifact_name": "ConvexHullCorrWorm", + "artifact_type": "ImageArtifactType", + "semantic_kind": "numeric_image_pixels", + "output_filename": "reference_image__ConvexHullCorrWorm.npy", + "comparison": "float pixels: atol=1e-6, rtol=1e-6, zero mismatches" + }, + "reference_path": "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleIlluminationCorrection_Example3/outputs/1/reference_image__ConvexHullCorrWorm.npy", + "candidate_path": "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/ordinary-0/ExampleIlluminationCorrection_Example3/wells_1/workers_1/images_ExampleIlluminationCorrection_Example3_source_workspace_well_throughput/results/reference_image__ConvexHullCorrWorm.npy", + "reference_shape": [ + 463, + 461 + ], + "candidate_shape": [ + 463, + 461 + ], + "reference_dtype": "float32", + "candidate_dtype": "float32", + "reference_pixel_digest": "fdc35df90fb97b2d2ab79e4acdaa00aef26d4d9c76fb53d6aa53351bd598dd9b", + "candidate_pixel_digest": "fdc35df90fb97b2d2ab79e4acdaa00aef26d4d9c76fb53d6aa53351bd598dd9b", + "compared_pixel_count": 213443, + "different_pixel_count": null, + "out_of_tolerance_pixel_count": 0, + "max_abs_difference": 0.0, + "equivalent": true + } + ], + "tables": [ + 0, + 0 + ], + "physical_images": [ + 2, + 2 + ], + "logical_images": [ + 2, + 2 + ], + "known_relationship_keys": [ + 0, + 0 + ], + "ordinary_receipt_sha256": "05161397be3c2b6ab87b13e8387080ba7f4de39af35e25b9882d9ca4591e4180", + "output_hashes": { + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleIlluminationCorrection_Example3/outputs/1/reference_image__PolynomialCorrected.npy": "588e6f9951aa7ad220678dbb0053e98fb6d324e71aab089d1d828d891ee32f54", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleIlluminationCorrection_Example3/outputs/1/reference_image__ConvexHullCorrWorm.npy": "9ba1259e4ca007c06f14be6946e32a86307ec2dfb9952b73fa2cc253ccae8b3a", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/ordinary-0/ExampleIlluminationCorrection_Example3/wells_1/workers_1/images_ExampleIlluminationCorrection_Example3_source_workspace_well_throughput/results/reference_image__PolynomialCorrected.npy": "8875da63a072c8f2de4f5cb707d779a064b663bf69aa0f304fe6fa74d4ff57a6", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/ordinary-0/ExampleIlluminationCorrection_Example3/wells_1/workers_1/images_ExampleIlluminationCorrection_Example3_source_workspace_well_throughput/openhcs_metadata.json": "579ac084b396dff5f7fbaa844139bf952c2576b6cb0242772c925ba9839c2267", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/ordinary-0/ExampleIlluminationCorrection_Example3/wells_1/workers_1/images_ExampleIlluminationCorrection_Example3_source_workspace_well_throughput/results/reference_image__ConvexHullCorrWorm.npy": "9ba1259e4ca007c06f14be6946e32a86307ec2dfb9952b73fa2cc253ccae8b3a", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/ordinary-0/ExampleIlluminationCorrection_Example3/wells_1/workers_1/images_ExampleIlluminationCorrection_Example3_source_workspace_well_throughput/openhcs_metadata.json.lock": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + { + "case": "ExampleWoundHealing", + "ordinary_repeat": 0, + "native_repeat": 0, + "measurement_differences": 0, + "image_differences": 0, + "declared_export_comparisons": [], + "tables": [ + 1, + 1 + ], + "physical_images": [ + 0, + 0 + ], + "logical_images": [ + 0, + 0 + ], + "known_relationship_keys": [ + 0, + 0 + ], + "ordinary_receipt_sha256": "d5e133d0c3a20eb14fdb324cfc380019e24ad44105303e7e413bccd96113ba62", + "output_hashes": { + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleWoundHealing/outputs/0/Image.csv": "497700eb62ed0a557b902968c0fdb444eca8b748a772d8234f241c84f5dfad87", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/ordinary-0/ExampleWoundHealing/wells_1/workers_1/images_ExampleWoundHealing_source_workspace_well_throughput/openhcs_metadata.json": "71582c62cabb2571ebf8a0de57f92fdf82c55a7ece3dac0bf2a6eca95cad84c9", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/ordinary-0/ExampleWoundHealing/wells_1/workers_1/images_ExampleWoundHealing_source_workspace_well_throughput/results/Image.csv": "939735fa17a067137fea2adc77d57f9ce1df24bfcb6588c49023de1cc3998677", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/ordinary-0/ExampleWoundHealing/wells_1/workers_1/images_ExampleWoundHealing_source_workspace_well_throughput/openhcs_metadata.json.lock": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + { + "case": "ExampleWoundHealing", + "ordinary_repeat": 0, + "native_repeat": 1, + "measurement_differences": 0, + "image_differences": 0, + "declared_export_comparisons": [], + "tables": [ + 1, + 1 + ], + "physical_images": [ + 0, + 0 + ], + "logical_images": [ + 0, + 0 + ], + "known_relationship_keys": [ + 0, + 0 + ], + "ordinary_receipt_sha256": "d5e133d0c3a20eb14fdb324cfc380019e24ad44105303e7e413bccd96113ba62", + "output_hashes": { + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleWoundHealing/outputs/1/Image.csv": "373f02427a4758051bfdd80efd7071774d8adcae60a2928c6d3bb67ad993155c", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/ordinary-0/ExampleWoundHealing/wells_1/workers_1/images_ExampleWoundHealing_source_workspace_well_throughput/openhcs_metadata.json": "71582c62cabb2571ebf8a0de57f92fdf82c55a7ece3dac0bf2a6eca95cad84c9", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/ordinary-0/ExampleWoundHealing/wells_1/workers_1/images_ExampleWoundHealing_source_workspace_well_throughput/results/Image.csv": "939735fa17a067137fea2adc77d57f9ce1df24bfcb6588c49023de1cc3998677", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/ordinary-0/ExampleWoundHealing/wells_1/workers_1/images_ExampleWoundHealing_source_workspace_well_throughput/openhcs_metadata.json.lock": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + { + "case": "ExampleIlluminationCorrection_Example3", + "ordinary_repeat": 1, + "native_repeat": 0, + "measurement_differences": 0, + "image_differences": 0, + "declared_export_comparisons": [ + { + "artifact": { + "artifact_name": "PolynomialCorrected", + "artifact_type": "ImageArtifactType", + "semantic_kind": "numeric_image_pixels", + "output_filename": "reference_image__PolynomialCorrected.npy", + "comparison": "float pixels: atol=1e-6, rtol=1e-6, zero mismatches" + }, + "reference_path": "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleIlluminationCorrection_Example3/outputs/0/reference_image__PolynomialCorrected.npy", + "candidate_path": "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/ordinary-1/ExampleIlluminationCorrection_Example3/wells_1/workers_1/images_ExampleIlluminationCorrection_Example3_source_workspace_well_throughput/results/reference_image__PolynomialCorrected.npy", + "reference_shape": [ + 463, + 461 + ], + "candidate_shape": [ + 463, + 461 + ], + "reference_dtype": "float32", + "candidate_dtype": "float32", + "reference_pixel_digest": "b5073cb872058f88bb250be9e7a39bedd2ef281f23dfedde5262e4431f1fb0fe", + "candidate_pixel_digest": "8aedbeab3f9868c1d1d46b0aafb3b20bb9dfd107dc08bb0882577c493f08173b", + "compared_pixel_count": 213443, + "different_pixel_count": null, + "out_of_tolerance_pixel_count": 0, + "max_abs_difference": 2.9802322387695312e-08, + "equivalent": true + }, + { + "artifact": { + "artifact_name": "ConvexHullCorrWorm", + "artifact_type": "ImageArtifactType", + "semantic_kind": "numeric_image_pixels", + "output_filename": "reference_image__ConvexHullCorrWorm.npy", + "comparison": "float pixels: atol=1e-6, rtol=1e-6, zero mismatches" + }, + "reference_path": "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleIlluminationCorrection_Example3/outputs/0/reference_image__ConvexHullCorrWorm.npy", + "candidate_path": "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/ordinary-1/ExampleIlluminationCorrection_Example3/wells_1/workers_1/images_ExampleIlluminationCorrection_Example3_source_workspace_well_throughput/results/reference_image__ConvexHullCorrWorm.npy", + "reference_shape": [ + 463, + 461 + ], + "candidate_shape": [ + 463, + 461 + ], + "reference_dtype": "float32", + "candidate_dtype": "float32", + "reference_pixel_digest": "fdc35df90fb97b2d2ab79e4acdaa00aef26d4d9c76fb53d6aa53351bd598dd9b", + "candidate_pixel_digest": "fdc35df90fb97b2d2ab79e4acdaa00aef26d4d9c76fb53d6aa53351bd598dd9b", + "compared_pixel_count": 213443, + "different_pixel_count": null, + "out_of_tolerance_pixel_count": 0, + "max_abs_difference": 0.0, + "equivalent": true + } + ], + "tables": [ + 0, + 0 + ], + "physical_images": [ + 2, + 2 + ], + "logical_images": [ + 2, + 2 + ], + "known_relationship_keys": [ + 0, + 0 + ], + "ordinary_receipt_sha256": "c8c926e9001f1c5d01ea1d45e88603122eb9a3cb82343e848d67c7e31d7d8de8", + "output_hashes": { + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleIlluminationCorrection_Example3/outputs/0/reference_image__PolynomialCorrected.npy": "588e6f9951aa7ad220678dbb0053e98fb6d324e71aab089d1d828d891ee32f54", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleIlluminationCorrection_Example3/outputs/0/reference_image__ConvexHullCorrWorm.npy": "9ba1259e4ca007c06f14be6946e32a86307ec2dfb9952b73fa2cc253ccae8b3a", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/ordinary-1/ExampleIlluminationCorrection_Example3/wells_1/workers_1/images_ExampleIlluminationCorrection_Example3_source_workspace_well_throughput/results/reference_image__PolynomialCorrected.npy": "8875da63a072c8f2de4f5cb707d779a064b663bf69aa0f304fe6fa74d4ff57a6", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/ordinary-1/ExampleIlluminationCorrection_Example3/wells_1/workers_1/images_ExampleIlluminationCorrection_Example3_source_workspace_well_throughput/results/reference_image__ConvexHullCorrWorm.npy": "9ba1259e4ca007c06f14be6946e32a86307ec2dfb9952b73fa2cc253ccae8b3a", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/ordinary-1/ExampleIlluminationCorrection_Example3/wells_1/workers_1/images_ExampleIlluminationCorrection_Example3_source_workspace_well_throughput/openhcs_metadata.json.lock": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/ordinary-1/ExampleIlluminationCorrection_Example3/wells_1/workers_1/images_ExampleIlluminationCorrection_Example3_source_workspace_well_throughput/openhcs_metadata.json": "579ac084b396dff5f7fbaa844139bf952c2576b6cb0242772c925ba9839c2267" + } + }, + { + "case": "ExampleIlluminationCorrection_Example3", + "ordinary_repeat": 1, + "native_repeat": 1, + "measurement_differences": 0, + "image_differences": 0, + "declared_export_comparisons": [ + { + "artifact": { + "artifact_name": "PolynomialCorrected", + "artifact_type": "ImageArtifactType", + "semantic_kind": "numeric_image_pixels", + "output_filename": "reference_image__PolynomialCorrected.npy", + "comparison": "float pixels: atol=1e-6, rtol=1e-6, zero mismatches" + }, + "reference_path": "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleIlluminationCorrection_Example3/outputs/1/reference_image__PolynomialCorrected.npy", + "candidate_path": "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/ordinary-1/ExampleIlluminationCorrection_Example3/wells_1/workers_1/images_ExampleIlluminationCorrection_Example3_source_workspace_well_throughput/results/reference_image__PolynomialCorrected.npy", + "reference_shape": [ + 463, + 461 + ], + "candidate_shape": [ + 463, + 461 + ], + "reference_dtype": "float32", + "candidate_dtype": "float32", + "reference_pixel_digest": "b5073cb872058f88bb250be9e7a39bedd2ef281f23dfedde5262e4431f1fb0fe", + "candidate_pixel_digest": "8aedbeab3f9868c1d1d46b0aafb3b20bb9dfd107dc08bb0882577c493f08173b", + "compared_pixel_count": 213443, + "different_pixel_count": null, + "out_of_tolerance_pixel_count": 0, + "max_abs_difference": 2.9802322387695312e-08, + "equivalent": true + }, + { + "artifact": { + "artifact_name": "ConvexHullCorrWorm", + "artifact_type": "ImageArtifactType", + "semantic_kind": "numeric_image_pixels", + "output_filename": "reference_image__ConvexHullCorrWorm.npy", + "comparison": "float pixels: atol=1e-6, rtol=1e-6, zero mismatches" + }, + "reference_path": "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleIlluminationCorrection_Example3/outputs/1/reference_image__ConvexHullCorrWorm.npy", + "candidate_path": "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/ordinary-1/ExampleIlluminationCorrection_Example3/wells_1/workers_1/images_ExampleIlluminationCorrection_Example3_source_workspace_well_throughput/results/reference_image__ConvexHullCorrWorm.npy", + "reference_shape": [ + 463, + 461 + ], + "candidate_shape": [ + 463, + 461 + ], + "reference_dtype": "float32", + "candidate_dtype": "float32", + "reference_pixel_digest": "fdc35df90fb97b2d2ab79e4acdaa00aef26d4d9c76fb53d6aa53351bd598dd9b", + "candidate_pixel_digest": "fdc35df90fb97b2d2ab79e4acdaa00aef26d4d9c76fb53d6aa53351bd598dd9b", + "compared_pixel_count": 213443, + "different_pixel_count": null, + "out_of_tolerance_pixel_count": 0, + "max_abs_difference": 0.0, + "equivalent": true + } + ], + "tables": [ + 0, + 0 + ], + "physical_images": [ + 2, + 2 + ], + "logical_images": [ + 2, + 2 + ], + "known_relationship_keys": [ + 0, + 0 + ], + "ordinary_receipt_sha256": "c8c926e9001f1c5d01ea1d45e88603122eb9a3cb82343e848d67c7e31d7d8de8", + "output_hashes": { + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleIlluminationCorrection_Example3/outputs/1/reference_image__PolynomialCorrected.npy": "588e6f9951aa7ad220678dbb0053e98fb6d324e71aab089d1d828d891ee32f54", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleIlluminationCorrection_Example3/outputs/1/reference_image__ConvexHullCorrWorm.npy": "9ba1259e4ca007c06f14be6946e32a86307ec2dfb9952b73fa2cc253ccae8b3a", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/ordinary-1/ExampleIlluminationCorrection_Example3/wells_1/workers_1/images_ExampleIlluminationCorrection_Example3_source_workspace_well_throughput/results/reference_image__PolynomialCorrected.npy": "8875da63a072c8f2de4f5cb707d779a064b663bf69aa0f304fe6fa74d4ff57a6", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/ordinary-1/ExampleIlluminationCorrection_Example3/wells_1/workers_1/images_ExampleIlluminationCorrection_Example3_source_workspace_well_throughput/results/reference_image__ConvexHullCorrWorm.npy": "9ba1259e4ca007c06f14be6946e32a86307ec2dfb9952b73fa2cc253ccae8b3a", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/ordinary-1/ExampleIlluminationCorrection_Example3/wells_1/workers_1/images_ExampleIlluminationCorrection_Example3_source_workspace_well_throughput/openhcs_metadata.json.lock": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/ordinary-1/ExampleIlluminationCorrection_Example3/wells_1/workers_1/images_ExampleIlluminationCorrection_Example3_source_workspace_well_throughput/openhcs_metadata.json": "579ac084b396dff5f7fbaa844139bf952c2576b6cb0242772c925ba9839c2267" + } + }, + { + "case": "ExampleWoundHealing", + "ordinary_repeat": 1, + "native_repeat": 0, + "measurement_differences": 0, + "image_differences": 0, + "declared_export_comparisons": [], + "tables": [ + 1, + 1 + ], + "physical_images": [ + 0, + 0 + ], + "logical_images": [ + 0, + 0 + ], + "known_relationship_keys": [ + 0, + 0 + ], + "ordinary_receipt_sha256": "42f967eef57d1f1f85168d225258b50c79225fcafef0f1137a4f7f6de0d9e02e", + "output_hashes": { + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleWoundHealing/outputs/0/Image.csv": "497700eb62ed0a557b902968c0fdb444eca8b748a772d8234f241c84f5dfad87", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/ordinary-1/ExampleWoundHealing/wells_1/workers_1/images_ExampleWoundHealing_source_workspace_well_throughput/openhcs_metadata.json.lock": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/ordinary-1/ExampleWoundHealing/wells_1/workers_1/images_ExampleWoundHealing_source_workspace_well_throughput/openhcs_metadata.json": "71582c62cabb2571ebf8a0de57f92fdf82c55a7ece3dac0bf2a6eca95cad84c9", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/ordinary-1/ExampleWoundHealing/wells_1/workers_1/images_ExampleWoundHealing_source_workspace_well_throughput/results/Image.csv": "939735fa17a067137fea2adc77d57f9ce1df24bfcb6588c49023de1cc3998677" + } + }, + { + "case": "ExampleWoundHealing", + "ordinary_repeat": 1, + "native_repeat": 1, + "measurement_differences": 0, + "image_differences": 0, + "declared_export_comparisons": [], + "tables": [ + 1, + 1 + ], + "physical_images": [ + 0, + 0 + ], + "logical_images": [ + 0, + 0 + ], + "known_relationship_keys": [ + 0, + 0 + ], + "ordinary_receipt_sha256": "42f967eef57d1f1f85168d225258b50c79225fcafef0f1137a4f7f6de0d9e02e", + "output_hashes": { + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleWoundHealing/outputs/1/Image.csv": "373f02427a4758051bfdd80efd7071774d8adcae60a2928c6d3bb67ad993155c", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/ordinary-1/ExampleWoundHealing/wells_1/workers_1/images_ExampleWoundHealing_source_workspace_well_throughput/openhcs_metadata.json.lock": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/ordinary-1/ExampleWoundHealing/wells_1/workers_1/images_ExampleWoundHealing_source_workspace_well_throughput/openhcs_metadata.json": "71582c62cabb2571ebf8a0de57f92fdf82c55a7ece3dac0bf2a6eca95cad84c9", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/ordinary-1/ExampleWoundHealing/wells_1/workers_1/images_ExampleWoundHealing_source_workspace_well_throughput/results/Image.csv": "939735fa17a067137fea2adc77d57f9ce1df24bfcb6588c49023de1cc3998677" + } + } + ], + "source_head": "293e04acb53156a0dd9ae71476703e24f66b196c", + "scope": "Both ordinary observations versus both native measured observations; full strict files/values/correlation policy; descriptive two-repeat clocks, not full30 acceptance." +} diff --git a/benchmark/results/perf_current_wound_illum3_paired_20261003/timing_summary.json b/benchmark/results/perf_current_wound_illum3_paired_20261003/timing_summary.json new file mode 100644 index 0000000000..30e150bebd --- /dev/null +++ b/benchmark/results/perf_current_wound_illum3_paired_20261003/timing_summary.json @@ -0,0 +1,145 @@ +{ + "source_head": "293e04acb53156a0dd9ae71476703e24f66b196c", + "cases": { + "ExampleIlluminationCorrection_Example3": { + "image_sets_per_full_invocation": 1, + "raw_samples": { + "compile_seconds": [ + 0.5973958969116211, + 0.5914797782897949 + ], + "execute_seconds": [ + 0.6867749691009521, + 0.38519740104675293 + ], + "total_seconds": [ + 1.7173079238273203, + 1.3618650771677494 + ], + "native_execution_seconds": [ + 0.5970225902274251, + 0.6568904407322407 + ], + "native_invocation_seconds": [ + 0.6704615103080869, + 0.7205826421268284 + ] + }, + "means": { + "compile_seconds": 0.594437837600708, + "execute_seconds": 0.5359861850738525, + "total_seconds": 1.5395865004975349, + "native_execution_seconds": 0.6269565154798329, + "native_invocation_seconds": 0.6955220762174577 + }, + "raw_ranges": { + "compile_seconds": [ + 0.5914797782897949, + 0.5973958969116211 + ], + "execute_seconds": [ + 0.38519740104675293, + 0.6867749691009521 + ], + "total_seconds": [ + 1.3618650771677494, + 1.7173079238273203 + ], + "native_execution_seconds": [ + 0.5970225902274251, + 0.6568904407322407 + ], + "native_invocation_seconds": [ + 0.6704615103080869, + 0.7205826421268284 + ] + }, + "warmup_excluded": { + "repetition": -1, + "output_root": "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleIlluminationCorrection_Example3/outputs/-1", + "image_set_count": 1, + "invocation_seconds": 1.3667851807549596, + "pre_first_module_seconds": 0.2952348357066512, + "first_module_through_post_run_seconds": 1.0715503450483084, + "invocation_started_monotonic_seconds": 3808405.732912145, + "first_module_started_monotonic_seconds": 3808406.028146981, + "completed_monotonic_seconds": 3808407.099697326 + }, + "other_total_seconds_mean": 0.4091624778229743, + "execution_ratio_of_means": 1.1697251401982418, + "total_ratio_of_means": 0.4072239625881526, + "native_report_sha256": "437f3943b29f3335831f934c0c308f51134df18096016431276f7c976fb8b458" + }, + "ExampleWoundHealing": { + "image_sets_per_full_invocation": 2, + "raw_samples": { + "compile_seconds": [ + 0.34695887565612793, + 0.34340977668762207 + ], + "execute_seconds": [ + 2.856407642364502, + 3.1452507972717285 + ], + "total_seconds": [ + 3.471247054170817, + 3.7725879549980164 + ], + "native_execution_seconds": [ + 4.377218026202172, + 4.129441360011697 + ], + "native_invocation_seconds": [ + 4.462228200864047, + 4.201918839942664 + ] + }, + "means": { + "compile_seconds": 0.345184326171875, + "execute_seconds": 3.0008292198181152, + "total_seconds": 3.6219175045844167, + "native_execution_seconds": 4.253329693106934, + "native_invocation_seconds": 4.332073520403355 + }, + "raw_ranges": { + "compile_seconds": [ + 0.34340977668762207, + 0.34695887565612793 + ], + "execute_seconds": [ + 2.856407642364502, + 3.1452507972717285 + ], + "total_seconds": [ + 3.471247054170817, + 3.7725879549980164 + ], + "native_execution_seconds": [ + 4.129441360011697, + 4.377218026202172 + ], + "native_invocation_seconds": [ + 4.201918839942664, + 4.462228200864047 + ] + }, + "warmup_excluded": { + "repetition": -1, + "output_root": "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleWoundHealing/outputs/-1", + "image_set_count": 2, + "invocation_seconds": 5.988651636987925, + "pre_first_module_seconds": 0.3156425580382347, + "first_module_through_post_run_seconds": 5.67300907894969, + "invocation_started_monotonic_seconds": 3808416.643831191, + "first_module_started_monotonic_seconds": 3808416.959473749, + "completed_monotonic_seconds": 3808422.632482828 + }, + "other_total_seconds_mean": 0.2759039585944265, + "execution_ratio_of_means": 1.4173847898497653, + "total_ratio_of_means": 1.174330913866291, + "native_report_sha256": "907c654a35ae3f5b4ad9ea898a1d60070c75b50dd8796cb61f58b0fcf462b8ab" + } + }, + "scope": "Descriptive two observations; no causal optimization gain or full30/scaling claim. Native execution is first module through post_run/close; invocation is retained separately. JVM/import/load/warmup and OH READY/startup/shutdown excluded. OH total includes compile/client wait/default OUTCOMES/RSS completion. Wound full two-image-set native invocation is compared with full OH well, without division. Bars are means; all raw observations and ranges retained, not confidence intervals.", + "science_sha256": "3d89dc4daaceb34535ceaab05100b80b88487e32e67180a99595035b66c67236" +} From e8587bd7eb0c4aa283bc300c5eb9f2294f24890f Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 04:07:20 -0400 Subject: [PATCH 116/235] fix: validate persisted occurrence against producer filename identity Related to #435. A collapsed SITE main-flow image and its exact persisted artifact mirror share full metadata while the filename retains SITE1 and the semantic artifact address is absent. Derive the occurrence address from the existing producer authority; preserve full metadata equality, exact producer matching and standalone artifact semantics. 243 local publication/materialization/source controls pass, including real TIFF write/reopen and true-conflict controls. The original unrecorded PR494 pair remains unproven. --- openhcs/core/steps/function_outputs.py | 5 +- ...test_collapsed_persisted_occurrence_435.py | 383 ++++++++++++++++++ 2 files changed, 387 insertions(+), 1 deletion(-) create mode 100644 tests/unit/test_collapsed_persisted_occurrence_435.py diff --git a/openhcs/core/steps/function_outputs.py b/openhcs/core/steps/function_outputs.py index fcaef7bf7f..7297a44416 100644 --- a/openhcs/core/steps/function_outputs.py +++ b/openhcs/core/steps/function_outputs.py @@ -995,8 +995,11 @@ def project_runtime_artifacts( if record.owns_persisted_artifact( materialization.output_plan, destination, self.output_dir ): + # A collapsed image retains filename coordinates while + # its semantic source address can be absent. Validate + # the occurrence against its producer's filename view. if ( - projection.address != address + projection.address != record.filename_address or projection.image_metadata != metadata ): raise ValueError( diff --git a/tests/unit/test_collapsed_persisted_occurrence_435.py b/tests/unit/test_collapsed_persisted_occurrence_435.py new file mode 100644 index 0000000000..564af6fce3 --- /dev/null +++ b/tests/unit/test_collapsed_persisted_occurrence_435.py @@ -0,0 +1,383 @@ +"""Public same-path image publication controls; the original #494 pair is unknown.""" + +from dataclasses import fields, replace +from pathlib import Path +import json + +import numpy as np +import pytest +import tifffile + +from polystore.disk import DiskStorageBackend +from polystore.filemanager import FileManager +from polystore.memory import MemoryStorageBackend + +from openhcs.constants.constants import Backend, Microscope +from openhcs.core.aligned_image_payload import AlignedImageSliceContext +from openhcs.core.artifacts import ( + ArtifactOutputPlan, + ImageArtifactType, + ObjectLabelsArtifactType, +) +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 +from openhcs.core.runtime_stores import RuntimeValueStore +from openhcs.core.source_image_provenance import ( + SourceImageProvenance, + SourceImageProvenancePlanes, +) +from openhcs.core.source_metadata import SourceVoxelSpacing +from openhcs.core.source_projection import ( + OpenHCSPlaneAddress, + SourcePixelRef, + SourcePlaneProjection, + SourceProjectionSet, + SourceProjectionMetadataSerializer, +) +from openhcs.core.source_workspace_projection import VirtualWorkspaceSourceProjection +from openhcs.core.steps.function_output_identity import FunctionOutputIdentity +from openhcs.core.steps.function_outputs import ( + PrimaryImageMetadataTarget, + RuntimeArtifactMaterializationAuthority, +) +from openhcs.microscopes.source_schema import SourceSchemaFilenameParser +from openhcs.processing.materialization import ( + ImageFileOptions, + MaterializationSpec, + MaterializedFilenameIdentity, +) + +from tests.unit.test_function_outputs import ( + context_stub, + function_step_plan, + record_output_path, +) + + +def _actual_saved_occurrence(tmp_path, scenario): + filemanager = FileManager( + { + Backend.DISK.value: DiskStorageBackend(), + Backend.MEMORY.value: MemoryStorageBackend(), + } + ) + context = context_stub(filemanager, parser=SourceSchemaFilenameParser()) + context.microscope_handler.microscope_type = Microscope.OPENHCS.value + context.runtime_value_store = RuntimeValueStore() + context.metadata_cache = {} + context.tiff_config = None + plan = function_step_plan("Collapsed public mosaic") + plan.streaming_configs = {} + plan.write_backend = Backend.DISK.value + plan.output_plate_root = str(tmp_path) + plan.output_dir = tmp_path / "images" + plan.sub_dir = "images" + plan.analysis_results_dir = str(plan.output_dir) + plan.runtime_artifact_materialization = RuntimeArtifactMaterializationPlan( + persistent_enabled=True, + persistent_backend=Backend.DISK.value, + ) + context.step_plans = {plan.step_index: plan} + filename = "A01_s001_w1_z001_t001_Mosaic.tif" + output_plan = ArtifactOutputPlan( + name="Mosaic", + path="/memory/Mosaic.pkl", + artifact_type=ImageArtifactType, + producer_step_index=plan.step_index, + producer_step_scope_id=plan.step_scope_id, + producer_step_name=plan.step_name, + materialization=MaterializationSpec( + ImageFileOptions( + filename_suffix=".tif", + filename_identity=MaterializedFilenameIdentity.ARTIFACT_NAME, + relative_path_template=filename, + ) + ), + ) + plan.artifact_outputs[output_plan.ref()] = output_plan + semantic_components = { + "well": "A01", + "channel": "1", + "z_index": "1", + "timepoint": "1", + } + if scenario != "collapsed_site": + semantic_components["site"] = "1" + provenance = SourceImageProvenance( + source_component_metadata=semantic_components, + source_image_provenance_planes=( + SourceImageProvenancePlanes.from_contributor_components( + paths=("/source/site-1.tif", "/source/site-2.tif"), + component_metadata=({"site": "1"}, {"site": "2"}), + ) + if scenario == "collapsed_site" + else SourceImageProvenancePlanes() + ), + source_image_names=("Mosaic",), + ) + metadata = ImagePayloadMetadata( + source_provenance=provenance, + source_voxel_spacing=SourceVoxelSpacing((0.5, 0.5)), + ) + pixels = np.arange( + 40 if scenario == "collapsed_site" else 20, dtype=np.uint16 + ).reshape((2, 4, 5) if scenario == "collapsed_site" else (4, 5)) + payload = ImageMetadataPayload(pixels, metadata) + artifact_payload = payload + if scenario == "metadata_conflict": + artifact_payload = metadata.replace_fields( + source_image_names=("IndependentRole",), + ).payload_with(pixels) + elif scenario == "scalar_address_conflict": + artifact_payload = metadata.with_source_component_metadata( + {**semantic_components, "site": "2"}, + ).payload_with(pixels) + context.runtime_value_store.record( + RuntimeValue.normalize(output_plan, artifact_payload, axis_id="A01"), + path=output_plan.path, + backend=Backend.MEMORY.value, + ) + destination = plan.output_dir / filename + filemanager.ensure_directory(plan.output_dir, Backend.MEMORY.value) + filemanager.ensure_directory(plan.output_dir, Backend.DISK.value) + filemanager.save(payload, str(destination), Backend.MEMORY.value) + filemanager.save(pixels, str(destination), Backend.DISK.value) + record_output_path( + context, + plan, + destination, + image_metadata=metadata, + output_context=AlignedImageSliceContext.main_flow( + output_key="Mosaic", + artifact_kind=ImageArtifactType.value, + ), + identity=FunctionOutputIdentity( + component_values=semantic_components, + filename_component_values={**semantic_components, "site": "1"}, + extension=".tif", + source="original collapsed-mosaic declaration", + ), + ) + target = PrimaryImageMetadataTarget.from_plan(plan) + # This is the existing accepted main-flow publication, before a mirror is added. + original = target.produced_projection_metadata(context, plan) + assert len(original["source_projection"]) == 1 + if scenario == "collapsed_site": + row = original["source_projection"][0] + assert row["address"]["site"] == "1" + assert "site" not in row["source_metadata"] + saved = RuntimeArtifactMaterializationAuthority.materialize(context, plan) + assert len(saved) == 1 + (output,) = saved[0].outputs_for_backend(Backend.DISK.value) + assert output.path == str(destination) + persisted = tifffile.imread(destination) + np.testing.assert_array_equal(persisted, pixels) + assert persisted.dtype == pixels.dtype + return ( + context, + plan, + replace(target, artifact_materializations=saved), + output_plan, + pixels, + ) + + +def _rejected_original_pair(error): + """Read actual operands from the raising frame; do not copy the join algorithm.""" + frames = [] + traceback = error.__traceback__ + while traceback is not None: + frame = traceback.tb_frame + if ( + frame.f_code.co_name == "project_runtime_artifacts" + and frame.f_code.co_filename.endswith( + "/openhcs/core/steps/function_outputs.py" + ) + ): + frames.append(frame.f_locals.copy()) + traceback = traceback.tb_next + assert len(frames) == 1 + return frames[0] + + +@pytest.mark.parametrize( + "scenario", + ( + "scalar_address_conflict", + "metadata_conflict", + ), +) +def test_real_same_path_join_observes_original_rejected_pair(tmp_path, scenario): + context, plan, target, _output_plan, pixels = _actual_saved_occurrence( + tmp_path, scenario + ) + with pytest.raises( + ValueError, match="Conflicting metadata for persisted image occurrence" + ) as failure: + target.produced_projection_metadata(context, plan) + pair = _rejected_original_pair(failure.value) + projection = pair["projection"] + metadata = pair["metadata"] + assert pair["record"].owns_persisted_artifact( + pair["materialization"].output_plan, + pair["destination"], + target.output_dir, + ) + # Comparisons below are post-exception observations, not admission replacements. + address_equal = projection.address == pair["address"] + metadata_equal = projection.image_metadata == metadata + differing_fields = [ + member.name + for member in fields(projection.image_metadata) + if getattr(projection.image_metadata, member.name) + != getattr(metadata, member.name) + ] + witness = { + "scenario": scenario, + "exception": str(failure.value), + "manifest_address": projection.address.as_component_metadata(), + "artifact_address": ( + None if pair["address"] is None else pair["address"].as_component_metadata() + ), + "address_equal": address_equal, + "metadata_equal": metadata_equal, + "differing_fields": differing_fields, + } + (tmp_path / "actual-rejected-pair.json").write_text(json.dumps(witness, indent=2)) + if scenario == "metadata_conflict": + assert address_equal + assert not metadata_equal + assert differing_fields == ["source_provenance"] + else: + assert not address_equal + assert not metadata_equal + assert differing_fields == ["source_provenance"] + assert pair["address"].as_component_metadata()["site"] == "2" + np.testing.assert_array_equal(pair["output"].content, pixels) + + +def test_real_scalar_same_path_mirror_remains_admissible(tmp_path): + context, plan, target, _output_plan, _pixels = _actual_saved_occurrence( + tmp_path, "scalar" + ) + structured = target.produced_projection_metadata(context, plan) + assert len(structured["source_projection"]) == 1 + assert structured["source_projection"][0]["source_alias"] == "Mosaic" + + +@pytest.mark.parametrize("mismatch", ("alias", "kind", "scope", "path")) +def test_actual_producer_occurrence_never_matches_foreign_declaration( + tmp_path, mismatch +): + context, plan, target, output_plan, _pixels = _actual_saved_occurrence( + tmp_path, "scalar" + ) + (record,) = target.produced_records(context, plan) + destination = record.path_under(target.output_dir) + assert record.owns_persisted_artifact(output_plan, destination, target.output_dir) + if mismatch == "path": + destination = str(target.output_dir / "independent.tif") + else: + change = { + "alias": {"name": "IndependentRole"}, + "kind": {"artifact_type": ObjectLabelsArtifactType}, + "scope": {"producer_step_scope_id": "independent-step-scope"}, + }[mismatch] + output_plan = replace(output_plan, **change) + assert not record.owns_persisted_artifact( + output_plan, destination, target.output_dir + ) + + +def test_collapsed_same_path_mirror_retains_filename_and_semantic_views(tmp_path): + context, plan, target, _output_plan, pixels = _actual_saved_occurrence( + tmp_path, "collapsed_site" + ) + structured = target.produced_projection_metadata(context, plan) + (row,) = structured["source_projection"] + assert row["address"]["site"] == "1" + assert row["source_alias"] == "Mosaic" + assert "site" not in row["source_metadata"] + metadata = ImagePayloadMetadata.from_mapping( + row[SourceProjectionMetadataSerializer.IMAGE_METADATA_FIELD] + ) + 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 + target.write(context, produced_plan=plan) + document = json.loads((tmp_path / "openhcs_metadata.json").read_text()) + reopened = VirtualWorkspaceSourceProjection.from_openhcs_metadata( + tmp_path, document + ) + projection = reopened.source_projections_by_virtual_path[row["virtual_path"]] + assert projection.address.as_component_metadata()["site"] == "1" + assert "site" not in projection.image_metadata.source_component_metadata + assert ( + len(projection.image_metadata.source_provenance.represented_source_identities) + == 2 + ) + np.testing.assert_array_equal( + tifffile.imread(tmp_path / row["virtual_path"]), pixels + ) + + +def test_standalone_collapsed_artifact_keeps_semantic_optional_address(tmp_path): + context, plan, target, _output_plan, _pixels = _actual_saved_occurrence( + tmp_path, "collapsed_site" + ) + ((projection, _path),) = target.project_runtime_artifacts(context, plan) + assert projection.address is None + assert projection.execution_scope is not None + assert "site" not in projection.image_metadata.source_component_metadata + assert ( + len(projection.image_metadata.source_provenance.represented_source_identities) + == 2 + ) + assert SourceProjectionSet((projection,)).artifact_projections == (projection,) + + +@pytest.mark.parametrize("valid_address", (True, False)) +def test_supplied_projection_map_must_retain_the_manifest_filename_address( + tmp_path, valid_address +): + context, plan, target, output_plan, _pixels = _actual_saved_occurrence( + tmp_path, "scalar" + ) + (record,) = target.produced_records(context, plan) + destination = record.path_under(target.output_dir) + (payload,) = context.filemanager.load_batch((destination,), Backend.MEMORY.value) + metadata = target.persisted_image_metadata( + context, destination=destination, payload=payload + ) + address = record.filename_address + if not valid_address: + address = OpenHCSPlaneAddress.from_complete_source_metadata( + {**address.as_component_metadata(), "site": "2"}, + ) + projection = SourcePlaneProjection( + address=address, + ref=SourcePixelRef( + Backend.DISK.value, str(Path(destination).relative_to(tmp_path)) + ), + source_alias=record.output_context.persisted_source_alias, + source_metadata=metadata.source_component_metadata, + image_metadata=metadata, + ) + assert record.owns_persisted_artifact(output_plan, destination, target.output_dir) + supplied = {Path(destination): (record, projection)} + if valid_address: + assert ( + target.project_runtime_artifacts( + context, plan, produced_projections=supplied + ) + == () + ) + else: + with pytest.raises( + ValueError, match="Conflicting metadata for persisted image occurrence" + ): + target.project_runtime_artifacts( + context, plan, produced_projections=supplied + ) From 9eee98265c939859d49f0c60bd5b3a2168cb29fe Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 04:56:32 -0400 Subject: [PATCH 117/235] refactor: carry nominal metadata owners across runtime consumers Local performance prototype for #384 and PR #394. Preserves public mutable metadata boundaries and existing subtype hooks. Shared consumer gate: 570 passed; extended boundary gate: 700 passed with 9 identical failures reproduced on parent. Saved V10 producer and loader parity pass. Whole-pipeline payoff remains unproven; do not promote before matched measurement. --- openhcs/core/aligned_image_payload.py | 128 +- openhcs/core/artifacts.py | 40 +- openhcs/core/image_file_serialization.py | 68 +- openhcs/core/measurement_image_alignment.py | 80 +- openhcs/core/runtime_adapters.py | 46 +- openhcs/core/runtime_image_loading.py | 41 +- openhcs/core/runtime_image_values.py | 1919 ++++++++++------- openhcs/core/runtime_object_labels.py | 8 + openhcs/core/runtime_slice_projection.py | 20 +- openhcs/core/source_image_semantics.py | 31 +- openhcs/core/source_plane_alignment.py | 36 +- openhcs/core/source_workspace_projection.py | 49 +- .../function_artifact_materialization.py | 58 +- openhcs/core/steps/function_io.py | 6 +- .../core/steps/function_output_identity.py | 80 +- .../core/steps/function_output_manifest.py | 32 +- openhcs/core/steps/function_outputs.py | 32 +- openhcs/core/steps/function_runtime.py | 218 +- .../module_artifact_declarations.py | 18 +- .../cellprofiler/runtime/artifact_binding.py | 23 +- .../runtime/function_contract_execution.py | 12 +- .../cellprofiler/runtime/invocation.py | 39 +- .../interop/cellprofiler/runtime/main_flow.py | 12 +- .../runtime/measurement_recording.py | 77 +- .../cellprofiler/runtime/module_execution.py | 54 +- .../runtime/output_record_request.py | 25 +- openhcs/processing/materialization/core.py | 192 +- ...lprofiler_metadata_projection_consumers.py | 414 ++++ ...est_function_output_metadata_projection.py | 335 +++ .../test_image_metadata_owner_consumers.py | 151 ++ .../test_image_metadata_projection_owner.py | 341 +++ tests/unit/test_metadata_stage2_closure.py | 108 + 32 files changed, 3396 insertions(+), 1297 deletions(-) create mode 100644 tests/unit/test_cellprofiler_metadata_projection_consumers.py create mode 100644 tests/unit/test_function_output_metadata_projection.py create mode 100644 tests/unit/test_image_metadata_owner_consumers.py create mode 100644 tests/unit/test_image_metadata_projection_owner.py create mode 100644 tests/unit/test_metadata_stage2_closure.py diff --git a/openhcs/core/aligned_image_payload.py b/openhcs/core/aligned_image_payload.py index c939865b21..33129c48be 100644 --- a/openhcs/core/aligned_image_payload.py +++ b/openhcs/core/aligned_image_payload.py @@ -29,13 +29,14 @@ ) from openhcs.core.runtime_image_values import ( ImagePayloadMetadata, + ImageMetadataProjection, ImagePayloadSliceProjector, ImagePayloadMetadataCarrier, ImagePayloadMetadataCompositionMode, ImageMaskDomain, image_payload_data, image_payload_mask, - image_payload_metadata, + image_payload_metadata_projection, preserved_image_plane_projection, with_image_payload_data, ) @@ -84,7 +85,7 @@ def for_value( return cls( value, cls.domain_from_metadata( - image_payload_metadata(value), + image_payload_metadata_projection(value), value_name="Image payload", ), ) @@ -92,13 +93,13 @@ def for_value( @classmethod def domain_from_metadata( cls, - metadata: ImagePayloadMetadata, + metadata: ImageMetadataProjection, *, fill_value: Any = 0, source_domain: SourceSpatialDomain | None = None, value_name: str, ) -> SourceSpatialDomain: - domain = metadata.source_spatial_domain + domain = metadata.read_value("source_spatial_domain") if source_domain is not None: domain = domain.with_missing_from(source_domain) return ( @@ -109,7 +110,7 @@ def domain_from_metadata( @property def spatial_axes_yx(self) -> tuple[int, int]: - axes = image_payload_metadata(self.value).spatial_axes_yx(self.value) + axes = image_payload_metadata_projection(self.value).spatial_axes_yx(self.value) if axes is None: raise ValueError( "Source-spatial image payload metadata does not declare two " @@ -119,7 +120,9 @@ def spatial_axes_yx(self) -> tuple[int, int]: @property def spatial_shape_yx(self) -> tuple[int, int]: - shape = image_payload_metadata(self.value).spatial_shape_yx(self.value) + shape = image_payload_metadata_projection(self.value).spatial_shape_yx( + self.value + ) if shape is None: raise ValueError( "Source-spatial image payloads require at least two dimensions, " @@ -167,7 +170,7 @@ def payload_in_source_domain( payload: RuntimeArrayData, source_domain: SourceSpatialDomain, ) -> RuntimeArrayData: - metadata = image_payload_metadata(payload) + metadata = image_payload_metadata_projection(payload) source_extent = source_domain.with_origin_yx(None) source_metadata = metadata.with_materialized_source_domain(source_extent) data = cls( @@ -187,7 +190,7 @@ def payload_in_source_domain( def mask_in_source_domain( cls, payload: RuntimeArrayData, - metadata: ImagePayloadMetadata, + metadata: ImageMetadataProjection, source_domain: SourceSpatialDomain, ) -> RuntimeArrayData | None: mask = image_payload_mask(payload) @@ -215,8 +218,7 @@ def value_in_payload_domain( fill_value=self.domain.fill_value, value_name=self.domain.value_name, ) - metadata = replace( - image_payload_metadata(materialized), + metadata = image_payload_metadata_projection(materialized).derive_fields( source_spatial_domain=target_domain, physical_border_edges_yx=target_domain.physical_border_edges_for_shape( target.payload_domain.spatial_shape_yx @@ -383,8 +385,8 @@ def resolve_source_spatial_value(self, value: Any) -> Any: """Project a nominal value into the declared reference payload domain.""" if self.reference_payload is None: return value - metadata = image_payload_metadata(self.reference_payload) - source_shape = metadata.source_spatial_domain.source_shape_yx + metadata = image_payload_metadata_projection(self.reference_payload) + source_shape = metadata.read_value("source_spatial_domain").source_shape_yx if source_shape is None: return value adapter = SourceSpatialDomainAdapter.for_value( @@ -432,26 +434,36 @@ def copy_whole_image( ) -> RuntimeArrayData: """Copy pixels, mask and metadata without adding a composition axis.""" copied_data = stack_runtime_slices( - (image_payload_data(value),), memory_type, device_id, + (image_payload_data(value),), + memory_type, + device_id, )[0] mask = image_payload_mask(value) copied_mask = ( - None if mask is None + None + if mask is None else stack_runtime_slices((mask,), memory_type, device_id)[0] ) - return image_payload_metadata(value).replace_fields().payload_with( - copied_data, copied_mask, + return ( + image_payload_metadata_projection(value) + .derive_fields() + .payload_with( + copied_data, + copied_mask, + ) ) - def composition_source_metadata(self) -> tuple[ImagePayloadMetadata, ...]: + def composition_source_metadata(self) -> tuple[ImageMetadataProjection, ...]: return tuple( - self.composition_payload_metadata(image_payload_metadata(payload)) + self.composition_payload_metadata( + image_payload_metadata_projection(payload) + ) for payload in self.composition_payloads ) def composition_payload_metadata( - self, metadata: ImagePayloadMetadata - ) -> ImagePayloadMetadata: + self, metadata: ImageMetadataProjection + ) -> ImageMetadataProjection: """Preserve each input's declared provenance unless the axis owner projects it.""" return metadata @@ -475,7 +487,9 @@ def compose_unmasked( payloads, memory_type, MemoryType(memory_type).device_id_of(payloads[0]) ) - def compose_mask(self, composed: Any, metadata: ImagePayloadMetadata) -> Any | None: + def compose_mask( + self, composed: Any, metadata: ImageMetadataProjection + ) -> Any | None: return _stack_image_payload_mask( self.composition_payloads, composed, @@ -517,7 +531,7 @@ def stack_image_payloads( def stack_image_payload_context_from_metadata( image_payloads: Sequence[Any], stack: RuntimeArrayData, - metadata_by_payload: Sequence[ImagePayloadMetadata], + metadata_by_payload: Sequence[ImageMetadataProjection], *, metadata_mode: ImagePayloadMetadataCompositionMode, ) -> Any: @@ -564,7 +578,7 @@ def _stack_image_payload_mask( stacked_mask_shape ): resolved_masks = tuple( - image_payload_metadata(payload) + image_payload_metadata_projection(payload) .mask_domain(slice_domain) .broadcast_to_data(mask) for payload, slice_domain, mask in zip( @@ -583,7 +597,7 @@ def _complete_image_payload_mask( payload_data: RuntimeArrayData, mask: RuntimeArrayData | None, ) -> RuntimeArrayData: - mask_domain = image_payload_metadata(payload).mask_domain(payload_data) + mask_domain = image_payload_metadata_projection(payload).mask_domain(payload_data) if mask is not None: if not mask_domain.accepts(tuple(np.shape(mask))): raise ValueError( @@ -603,11 +617,11 @@ def unstack_image_payload_context( ) -> list[Any]: """Attach one source plane of payload context to each unstacked image slice.""" mask = image_payload_mask(payload) - metadata = image_payload_metadata(payload) + metadata = image_payload_metadata_projection(payload) if mask is None and not metadata.has_values: return list(slices) - if metadata.plane_axis is None and default_plane_axis is not None: - metadata = replace(metadata, plane_axis=default_plane_axis) + if metadata.read_value("plane_axis") is None and default_plane_axis is not None: + metadata = metadata.derive_fields(plane_axis=default_plane_axis) projector = ImagePayloadSliceProjector(mask=mask, metadata=metadata) return projector.payloads_for_slices(slices) @@ -767,7 +781,7 @@ def plane_axis(self) -> RuntimePlaneAxis | None: """Return the axis declared by the composed payload owner.""" if isinstance(self.payload, AlignedImageStack): return RuntimePlaneAxis.RUNTIME_SLICE - return image_payload_metadata(self.payload).plane_axis + return image_payload_metadata_projection(self.payload).read_value("plane_axis") def preserved_plane_projection( self, @@ -800,14 +814,14 @@ def __post_init__(self) -> None: declared_axes = tuple( ( index, - metadata.plane_axis, - metadata.source_image_names, + metadata.read_value("plane_axis"), + metadata.read_value("source_provenance").source_image_names, tuple(np.shape(image_payload_data(payload))), ) for index, (payload, metadata) in enumerate( zip(self.payloads, self.source_metadata, strict=True) ) - if metadata.plane_axis is not None + if metadata.read_value("plane_axis") is not None ) if declared_axes: raise ValueError( @@ -816,7 +830,7 @@ def __post_init__(self) -> None: ) @property - def source_metadata(self) -> tuple[ImagePayloadMetadata, ...]: + def source_metadata(self) -> tuple[ImageMetadataProjection, ...]: return self.composition_source_metadata() @property @@ -850,7 +864,7 @@ def from_payloads( def compose_mask( self, composed: Any, - metadata: ImagePayloadMetadata, + metadata: ImageMetadataProjection, ) -> Any | None: masks = self.present_masks if not masks: @@ -1117,11 +1131,11 @@ def contextualize_image_payload( if self.is_anonymous_main_flow: return payload - metadata = image_payload_metadata(payload) - return metadata.with_source_provenance( - metadata.source_provenance.with_derived_source_image_names( - (self.output_key,) - ) + metadata = image_payload_metadata_projection(payload) + return metadata.derive_fields( + source_provenance=metadata.read_value( + "source_provenance" + ).with_derived_source_image_names((self.output_key,)) ).payload_with( image_payload_data(payload), image_payload_mask(payload), @@ -1166,23 +1180,28 @@ def composition_metadata_mode(self) -> ImagePayloadMetadataCompositionMode: return ImagePayloadMetadataCompositionMode.STACK @property - def projected_output_composition_mode(self) -> ImagePayloadMetadataCompositionMode | None: + def projected_output_composition_mode( + self, + ) -> ImagePayloadMetadataCompositionMode | None: """Declare the outer runtime axis retained by projected output members.""" return self.composition_metadata_mode def plane_axis_for_output_context( - self, context: AlignedImageSliceContext, + self, + context: AlignedImageSliceContext, ) -> RuntimePlaneAxis | None: """An explicitly aligned stack declares its outer runtime-slice domain.""" return RuntimePlaneAxis.RUNTIME_SLICE def composition_payload_metadata( - self, metadata: ImagePayloadMetadata - ) -> ImagePayloadMetadata: + self, metadata: ImageMetadataProjection + ) -> ImageMetadataProjection: """Inner image bundles contribute provenance, not an outer slice axis.""" metadata = super(AlignedImageStack, self).composition_payload_metadata(metadata) - return metadata.with_source_provenance( - metadata.source_provenance.with_runtime_planes_as_contributors() + return metadata.derive_fields( + source_provenance=metadata.read_value( + "source_provenance" + ).with_runtime_planes_as_contributors() ) def __post_init__(self) -> None: @@ -1323,10 +1342,13 @@ def composition_metadata_mode(self) -> ImagePayloadMetadataCompositionMode: return ImagePayloadMetadataCompositionMode.BUNDLE @property - def projected_output_composition_mode(self) -> ImagePayloadMetadataCompositionMode | None: + def projected_output_composition_mode( + self, + ) -> ImagePayloadMetadataCompositionMode | None: """Flatten declared inner runtime planes, retaining other named image domains.""" if any( - image_payload_metadata(payload).plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + image_payload_metadata_projection(payload).read_value("plane_axis") + is RuntimePlaneAxis.RUNTIME_SLICE for payload in self.slices ): return ImagePayloadMetadataCompositionMode.STACK @@ -1335,11 +1357,15 @@ def projected_output_composition_mode(self) -> ImagePayloadMetadataCompositionMo return self.composition_metadata_mode def plane_axis_for_output_context( - self, context: AlignedImageSliceContext, + self, + context: AlignedImageSliceContext, ) -> RuntimePlaneAxis | None: """Resolve a named output's original inner domain before leaf projection.""" payloads = tuple( - payload for payload, declared_context in zip(self.slices, self.slice_contexts, strict=True) + payload + for payload, declared_context in zip( + self.slices, self.slice_contexts, strict=True + ) if declared_context == context ) if len(payloads) != 1: @@ -1347,11 +1373,11 @@ def plane_axis_for_output_context( "Named image output context requires exactly one original payload: " f"{context!r}; found {len(payloads)}." ) - return image_payload_metadata(payloads[0]).plane_axis + return image_payload_metadata_projection(payloads[0]).read_value("plane_axis") def composition_payload_metadata( - self, metadata: ImagePayloadMetadata - ) -> ImagePayloadMetadata: + self, metadata: ImageMetadataProjection + ) -> ImageMetadataProjection: """Named output surfaces remain source-binding planes, not runtime slices.""" return metadata diff --git a/openhcs/core/artifacts.py b/openhcs/core/artifacts.py index 2d11eee696..59621b2d2e 100644 --- a/openhcs/core/artifacts.py +++ b/openhcs/core/artifacts.py @@ -31,7 +31,10 @@ from openhcs.core.runtime_artifact_values import ( RuntimeValue, ) - from openhcs.core.runtime_image_values import ImagePayloadMetadata + from openhcs.core.runtime_image_values import ( + ImageMetadataProjection, + ImagePayloadMetadata, + ) from openhcs.core.runtime_measurements import ( MeasurementSubject, RuntimeMeasurementFeatureOwner, @@ -451,7 +454,7 @@ def normalize_runtime_payload( from openhcs.core.runtime_image_values import ( image_payload_data, image_payload_mask, - image_payload_metadata, + image_payload_metadata_projection, ) from openhcs.core.runtime_slice_alignment import ( RuntimeSliceAlignedValues, @@ -459,9 +462,11 @@ def normalize_runtime_payload( ) def named_payload(payload: object) -> object: - metadata = image_payload_metadata(payload) - return metadata.with_source_provenance( - metadata.source_provenance.with_derived_source_image_names((name,)) + metadata = image_payload_metadata_projection(payload) + return metadata.derive_fields( + source_provenance=metadata.read_value( + "source_provenance" + ).with_derived_source_image_names((name,)) ).payload_with( image_payload_data(payload), image_payload_mask(payload), @@ -1076,7 +1081,11 @@ def filename_identity_for_output( ) -> FunctionOutputIdentity: """Apply a declared role through the shared filename identity algorithm.""" qualifier = self.filename_qualifier(output_plan) - return identity if qualifier is None else identity.with_filename_qualifier(qualifier) + return ( + identity + if qualifier is None + else identity.with_filename_qualifier(qualifier) + ) def _coerce_artifact_plan_type( @@ -2849,17 +2858,26 @@ def materialization_payload(self, value: "RuntimeValue") -> object: or materialization_source == self.source_context_source() ): return payload - return self.materialization_metadata(value).attach_to(payload) + return self.materialization_metadata_projection(value).attach_to(payload) def materialization_metadata( self, value: "RuntimeValue", ) -> "ImagePayloadMetadata": + """Expose filename source facts as the actual mutable metadata type.""" + return self.materialization_metadata_projection(value).materialize_metadata() + + def materialization_metadata_projection( + self, + value: "RuntimeValue", + ) -> "ImageMetadataProjection": """Return payload metadata with declared materialization-source provenance.""" - from openhcs.core.runtime_image_values import image_payload_metadata + from openhcs.core.runtime_image_values import image_payload_metadata_projection - payload_metadata = image_payload_metadata(value.materialization_payload()) + payload_metadata = image_payload_metadata_projection( + value.materialization_payload() + ) materialization_source = self.materialization_source() if ( materialization_source is not None @@ -2872,8 +2890,8 @@ def materialization_metadata( f"materialization source {materialization_source!r}, but its " "runtime value carries no materialization-source metadata." ) - return payload_metadata.with_source_provenance( - source_metadata.source_provenance + return payload_metadata.derive_fields( + source_provenance=source_metadata.read_value("source_provenance") ) return payload_metadata diff --git a/openhcs/core/image_file_serialization.py b/openhcs/core/image_file_serialization.py index 0b2593fa72..7f2bb1229c 100644 --- a/openhcs/core/image_file_serialization.py +++ b/openhcs/core/image_file_serialization.py @@ -15,10 +15,11 @@ from openhcs.constants.constants import FileFormat from openhcs.core.registry_strategies import NominalTypeStrategyFamilyMixin from openhcs.core.runtime_image_values import ( + ImageMetadataProjection, ImagePayloadMetadata, image_intensity_scale_for_dtype, image_payload_data, - image_payload_metadata, + image_payload_metadata_projection, ) logger = logging.getLogger(__name__) @@ -142,44 +143,53 @@ def require_image_geometry(self) -> tuple[Any, int, int]: """Require complete format-declared dtype and physical image dimensions.""" if self.image_shape_yx is None or self.source_dtype is None: - raise ValueError("Image-file header does not declare image dtype and YX geometry.") + raise ValueError( + "Image-file header does not declare image dtype and YX geometry." + ) height, width = self.image_shape_yx return self.source_dtype, height, width def project_image_metadata( self, metadata: ImagePayloadMetadata, *, values_preserved: bool ) -> ImagePayloadMetadata: + """Expose format-projected facts as their actual mutable namespace.""" + return self.project_image_metadata_projection( + metadata, values_preserved=values_preserved + ).materialize_metadata() + + def project_image_metadata_projection( + self, metadata: ImageMetadataProjection, *, values_preserved: bool + ) -> ImageMetadataProjection: """Combine current native pixels with retained typed acquisition facts.""" if self.source_dtype is None: raise ValueError("Saved image metadata requires an actual native dtype.") native_scale_governs = self.intensity_scale is not None or not values_preserved if not values_preserved: - metadata = metadata.without_unit_interval_intensity_scale().replace_fields( - physical_border_edges_yx=None, - mask_defines_border=None, + metadata = metadata.without_unit_interval_intensity_scale().derive_fields( + physical_border_edges_yx=None, mask_defines_border=None ) - return metadata.replace_fields( + return metadata.derive_fields( source_dtype=str(self.source_dtype), - intensity_scale=( - self.intensity_scale - if native_scale_governs - else metadata.intensity_scale - ), + intensity_scale=self.intensity_scale + if native_scale_governs + else metadata.read_value("intensity_scale"), source_plane_dtypes=tuple( - str(self.source_dtype) for _ in metadata.source_plane_dtypes - ), - source_plane_intensity_scales=( - tuple( - self.intensity_scale for _ in metadata.source_plane_intensity_scales + ( + str(self.source_dtype) + for _ in metadata.read_value("source_plane_dtypes") ) - if native_scale_governs - else metadata.source_plane_intensity_scales - ), - source_channel_axis=( - metadata.source_channel_axis - if values_preserved and self.pixel_semantics.channel_axis is None - else self.pixel_semantics.channel_axis ), + source_plane_intensity_scales=tuple( + ( + self.intensity_scale + for _ in metadata.read_value("source_plane_intensity_scales") + ) + ) + if native_scale_governs + else metadata.read_value("source_plane_intensity_scales"), + source_channel_axis=metadata.read_value("source_channel_axis") + if values_preserved and self.pixel_semantics.channel_axis is None + else self.pixel_semantics.channel_axis, ) @@ -317,12 +327,18 @@ def preserves_pixel_values(self, source_dtype: Any) -> bool: return False def persisted_metadata(self, path: Path, payload: Any) -> ImagePayloadMetadata: + """Expose saved native facts as their actual mutable metadata namespace.""" + return self.persisted_metadata_projection(path, payload).materialize_metadata() + + def persisted_metadata_projection( + self, path: Path, payload: Any + ) -> ImageMetadataProjection: """Describe saved native pixels while retaining their semantic lineage.""" header = self.source_metadata(path) if header.source_dtype is None: raise ValueError(f"Cannot establish saved image metadata for {path}.") - return header.project_image_metadata( - image_payload_metadata(payload), + return header.project_image_metadata_projection( + image_payload_metadata_projection(payload), values_preserved=self.preserves_pixel_values( image_payload_data(payload).dtype ), @@ -623,7 +639,7 @@ 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 = image_payload_metadata(payload).plane_axis + plane_axis = image_payload_metadata_projection(payload).read_value("plane_axis") if plane_axis is not None: raise ValueError( "Single-image raster serialization requires a payload projected off " diff --git a/openhcs/core/measurement_image_alignment.py b/openhcs/core/measurement_image_alignment.py index f8264e9d8e..f9e4b4dcf3 100644 --- a/openhcs/core/measurement_image_alignment.py +++ b/openhcs/core/measurement_image_alignment.py @@ -12,7 +12,7 @@ from openhcs.core.runtime_array_values import RuntimeArrayData from openhcs.core.runtime_image_values import ( image_payload_data, - image_payload_metadata, + image_payload_metadata_projection, with_image_payload_data, ) from openhcs.core.runtime_object_labels import ( @@ -147,9 +147,7 @@ def image_array(self) -> np.ndarray: 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, - self.source_aliases, + self.plane_projector, RuntimePlaneAxis.SOURCE_BINDING, self.source_aliases ) if projection is None or projection.plane_index is None: return self @@ -157,16 +155,20 @@ def with_source_projected_image(self) -> "MeasurementImageLabelAlignmentRequest" if isinstance(image, AlignedImageStack): projected_image: RuntimeArrayData | AlignedImageStack = image.with_slices( tuple( - RuntimeSliceProjection.value_for_slice( - image_slice, - replace( - projection, - source_aliases=image_payload_metadata( - image_slice - ).source_image_names, - ), + ( + RuntimeSliceProjection.value_for_slice( + image_slice, + replace( + projection, + source_aliases=image_payload_metadata_projection( + image_slice + ) + .read_value("source_provenance") + .source_image_names, + ), + ) + for image_slice in image.slices ) - for image_slice in image.slices ) ) else: @@ -174,13 +176,12 @@ def with_source_projected_image(self) -> "MeasurementImageLabelAlignmentRequest" image, replace( projection, - source_aliases=image_payload_metadata(image).source_image_names, + source_aliases=image_payload_metadata_projection(image) + .read_value("source_provenance") + .source_image_names, ), ) - return replace( - self, - source=self.source.with_alignment_image(projected_image), - ) + return replace(self, source=self.source.with_alignment_image(projected_image)) def with_source_projected_labels(self) -> "MeasurementImageLabelAlignmentRequest": """Project labels only through their declared axis and runtime context.""" @@ -197,9 +198,7 @@ def with_source_projected_labels(self) -> "MeasurementImageLabelAlignmentRequest "Plane-scoped object labels require a declared local plane projection." ) runtime_projection = RuntimePlaneAxisValueProjection.from_projector( - self.plane_projector, - self.label_payload.plane_axis, - self.source_aliases, + self.plane_projector, self.label_payload.plane_axis, self.source_aliases ) if runtime_projection is None: return self @@ -211,9 +210,7 @@ def with_source_projected_labels(self) -> "MeasurementImageLabelAlignmentRequest projection = declared_projection.selected_plane(0) elif runtime_projection.axis_size != declared_projection.axis_size: raise ValueError( - "Object-label runtime plane-axis cardinality conflicts with its " - "declared local payload: " - f"{runtime_projection.axis_size} != {declared_projection.axis_size}." + f"Object-label runtime plane-axis cardinality conflicts with its declared local payload: {runtime_projection.axis_size} != {declared_projection.axis_size}." ) else: projection = declared_projection.selected_plane( @@ -226,31 +223,29 @@ def with_source_projected_labels(self) -> "MeasurementImageLabelAlignmentRequest source_payload = source_payload.with_measurement_labels(self.labels) payload_projection = RuntimeSliceProjection.value_for_slice( source_payload, - replace( - projection, - source_aliases=source_payload.source_image_names, - ), + replace(projection, source_aliases=source_payload.source_image_names), ) if not isinstance(payload_projection, ObjectLabelValue): raise TypeError( - "Object-label runtime projection must preserve ObjectLabelValue, got " - f"{type(payload_projection).__name__}." + f"Object-label runtime projection must preserve ObjectLabelValue, got {type(payload_projection).__name__}." ) if payload_projection is source_payload: return self source = self.source image = self.image - image_metadata = image_payload_metadata(image) + image_metadata = image_payload_metadata_projection(image) if ( not isinstance(image, AlignedImageStack) - and image_metadata.plane_axis is projection.axis + and image_metadata.read_value("plane_axis") is projection.axis ): source = source.with_alignment_image( RuntimeSliceProjection.value_for_slice( image, replace( projection, - source_aliases=image_metadata.source_image_names, + source_aliases=image_metadata.read_value( + "source_provenance" + ).source_image_names, ), ) ) @@ -424,7 +419,7 @@ def source_context_payload( source_payload, source_projected_labels, ) - metadata = image_payload_metadata(request.image) + metadata = image_payload_metadata_projection(request.image) payload_domain = source_payload.object_label_source_spatial_domain() source_spatial_domain = ( metadata.object_label_source_spatial_domain() @@ -723,10 +718,7 @@ def validate_aligned_stack( @staticmethod def validate_dense_pair( - image: object, - labels: object, - *, - label_payload: ObjectLabelValue | None = None, + image: object, labels: object, *, label_payload: ObjectLabelValue | None = None ) -> None: """Require exact dense shapes after nominal source-domain projection.""" image_data = image_payload_data(image) @@ -739,22 +731,14 @@ def validate_dense_pair( label_data, np.ndarray ): raise TypeError( - "Measurement image alignment requires dense image and label arrays " - "after nominal projection; got " - f"{type(image_data).__name__} and {type(label_data).__name__}." + f"Measurement image alignment requires dense image and label arrays after nominal projection; got {type(image_data).__name__} and {type(label_data).__name__}." ) if tuple(image_data.shape) != tuple(label_data.shape): label_domain = ( None if label_payload is None else label_payload.object_label_domain() ) raise ValueError( - "Measurement image alignment produced incompatible declared domains: " - f"image shape {image_data.shape!r}, label shape {label_data.shape!r}; " - f"image plane axis={image_payload_metadata(image).plane_axis!r}; " - f"label type={type(label_payload).__name__ if label_payload is not None else type(labels).__name__}; " - f"label scope={None if label_domain is None else label_domain.scope!r}; " - f"label plane axis={None if label_payload is None else label_payload.plane_axis!r}; " - f"label source aliases={None if label_payload is None else label_payload.source_aliases!r}." + f"Measurement image alignment produced incompatible declared domains: image shape {image_data.shape!r}, label shape {label_data.shape!r}; image plane axis={image_payload_metadata_projection(image).read_value('plane_axis')!r}; label type={(type(label_payload).__name__ if label_payload is not None else type(labels).__name__)}; label scope={(None if label_domain is None else label_domain.scope)!r}; label plane axis={(None if label_payload is None else label_payload.plane_axis)!r}; label source aliases={(None if label_payload is None else label_payload.source_aliases)!r}." ) diff --git a/openhcs/core/runtime_adapters.py b/openhcs/core/runtime_adapters.py index e8b2d3cf2a..12b3af5905 100644 --- a/openhcs/core/runtime_adapters.py +++ b/openhcs/core/runtime_adapters.py @@ -33,7 +33,7 @@ from openhcs.core.runtime_image_loading import ImagePayloadSourceMetadataContext from openhcs.core.runtime_image_values import ( ImagePayloadMetadataCompositionMode, - image_payload_metadata, + image_payload_metadata_projection, ) from openhcs.core.source_bindings import ( CompiledSourceBindingPlan, @@ -277,33 +277,28 @@ def runtime_artifact_input( def source_artifact_payload(self, ref: ArtifactSpecRef) -> object: """Resolve one source-bound artifact through workspace matching and VFS.""" - binding = self.source_binding_for_artifact_ref(ref) source_payload = self.source_payload source_provenance = ( None if source_payload is None - else image_payload_metadata(source_payload).source_provenance + else image_payload_metadata_projection(source_payload).read_value( + "source_provenance" + ) ) - if source_provenance is not None and not source_provenance.has_values: + if source_provenance is not None and (not source_provenance.has_values): raise ValueError( f"Source-bound artifact {ref!r} requires main-flow source provenance." ) - cache = self.context.runtime_source_workspace_projection_cache projection = VirtualWorkspaceSourceProjectionAuthority.from_context( - self.context, - cache=cache, + self.context, cache=cache ).projection_if_available() if projection is None: raise ValueError( - f"Source-bound artifact {ref!r} requires a virtual-workspace " - "source projection." + f"Source-bound artifact {ref!r} requires a virtual-workspace source projection." ) - projection = cache.filtered_by_axis( - projection, - axis_id=self.axis_scope.axis_id, - ) + projection = cache.filtered_by_axis(projection, axis_id=self.axis_scope.axis_id) source_context = ( self.context.runtime_source_binding_context_cache.source_pattern_context( parser=self.context.microscope_handler.parser, @@ -319,11 +314,13 @@ def source_artifact_payload(self, ref: ArtifactSpecRef) -> object: ) source_universe = tuple( dict.fromkeys( - source_path - for declared_binding in self.source_binding_plan.binding_declarations - for source_path in projection.files_for_projection_role( - declared_binding.projection_role, - axis_id=self.axis_scope.axis_id, + ( + source_path + for declared_binding in self.source_binding_plan.binding_declarations + for source_path in projection.files_for_projection_role( + declared_binding.projection_role, + axis_id=self.axis_scope.axis_id, + ) ) ) ) @@ -340,15 +337,12 @@ def source_artifact_payload(self, ref: ArtifactSpecRef) -> object: raise ValueError( f"Source-bound artifact {ref!r} resolved no workspace members." ) - payloads = self.context.filemanager.load_batch( - list(members), - Backend.VIRTUAL_WORKSPACE.value, + list(members), Backend.VIRTUAL_WORKSPACE.value ) if len(payloads) != len(members): raise ValueError( - f"Source-bound artifact {ref!r} loaded {len(payloads)} payloads " - f"for {len(members)} workspace members." + f"Source-bound artifact {ref!r} loaded {len(payloads)} payloads for {len(members)} workspace members." ) projected_payloads = [] for member, payload in zip(members, payloads, strict=True): @@ -356,8 +350,7 @@ def source_artifact_payload(self, ref: ArtifactSpecRef) -> object: source_projection = projection.require_source_projection_for(lookup) if not source_projection.matches_binding(binding): raise ValueError( - f"Workspace projection for {member!r} does not match compiled " - f"source artifact {ref!r}." + f"Workspace projection for {member!r} does not match compiled source artifact {ref!r}." ) projected = projection.project_payload(lookup, payload) projected_payloads.append( @@ -366,8 +359,7 @@ def source_artifact_payload(self, ref: ArtifactSpecRef) -> object: binding, ImagePayloadSourceMetadataContext( SourceImageIdentity( - member, - projection.source_metadata_for(lookup), + member, projection.source_metadata_for(lookup) ), source_projection.ref.backend, self.context.filemanager, diff --git a/openhcs/core/runtime_image_loading.py b/openhcs/core/runtime_image_loading.py index a29901c1c2..974cd38a5f 100644 --- a/openhcs/core/runtime_image_loading.py +++ b/openhcs/core/runtime_image_loading.py @@ -13,10 +13,11 @@ image_file_source_metadata, ) from openhcs.core.runtime_image_values import ( + ImageMetadataProjection, ImagePayloadMetadata, ImagePayloadMetadataCarrier, image_payload_data, - image_payload_metadata, + image_payload_metadata_projection, ) from openhcs.core.runtime_plane_projection import RuntimePlaneAxis from openhcs.core.source_image_provenance import ( @@ -57,8 +58,16 @@ def metadata( *, source_binding: NamedSourceBinding | None = None, ) -> ImagePayloadMetadata: + """Expose loaded source facts as their actual mutable metadata namespace.""" + return self.metadata_projection( + image, source_binding=source_binding + ).materialize_metadata() + + def metadata_projection( + self, image: Any, *, source_binding: NamedSourceBinding | None = None + ) -> ImageMetadataProjection: """Return source-file metadata for pixels loaded through this I/O context.""" - existing_metadata = image_payload_metadata(image) + existing_metadata = image_payload_metadata_projection(image) resolved_source_path = self.resolved_source_path() source_metadata = image_file_source_metadata(resolved_source_path) source_dtype = source_metadata.source_dtype @@ -69,7 +78,7 @@ def metadata( source_channel_axis = self.source_channel_axis(image, source_metadata) if source_binding is not None: source_channel_axis = source_binding.source_channel_axis_for_shape( - tuple(int(value) for value in np.shape(image_data)), + tuple((int(value) for value in np.shape(image_data))), observed_axis=source_channel_axis, ) source_spatial_shape_yx = ImagePayloadMetadata( @@ -95,15 +104,13 @@ def metadata( ) if source_dtype is None: array_metadata = ImagePayloadMetadata.for_array( - image_payload_data(image), - source_path=source_identity_path, + image_payload_data(image), source_path=source_identity_path ) metadata = array_metadata.replace_fields( source_component_metadata=source_component_metadata, source_image_provenance_planes=source_image_provenance_planes, source_spatial_domain=SourceSpatialDomain( - origin_yx=spatial_origin_yx, - source_shape_yx=source_spatial_shape_yx, + origin_yx=spatial_origin_yx, source_shape_yx=source_spatial_shape_yx ), source_voxel_spacing=source_voxel_spacing, source_channel_axis=source_channel_axis, @@ -117,16 +124,17 @@ def metadata( source_component_metadata=source_component_metadata, source_image_provenance_planes=source_image_provenance_planes, source_spatial_domain=SourceSpatialDomain( - origin_yx=spatial_origin_yx, - source_shape_yx=source_spatial_shape_yx, + origin_yx=spatial_origin_yx, source_shape_yx=source_spatial_shape_yx ), source_voxel_spacing=source_voxel_spacing, source_channel_axis=source_channel_axis, plane_axis=plane_axis, ) - metadata = metadata.replace_fields( - source_spatial_domain=existing_metadata.source_spatial_domain.with_native_image_context( - metadata.source_spatial_domain, + metadata = metadata.derive_fields( + source_spatial_domain=existing_metadata.read_value( + "source_spatial_domain" + ).with_native_image_context( + metadata.read_value("source_spatial_domain"), image_shape_yx=source_spatial_shape_yx, ) ) @@ -136,15 +144,14 @@ def metadata( @staticmethod def source_channel_axis( - image: Any, - source_metadata: ImageFileSourceMetadata, + image: Any, source_metadata: ImageFileSourceMetadata ) -> int | None: """Return the channel axis declared by the current pixel carrier.""" if isinstance(image, ImagePayloadMetadataCarrier): - metadata = image_payload_metadata(image) - if metadata.source_channel_axis is not None: + metadata = image_payload_metadata_projection(image) + if metadata.read_value("source_channel_axis") is not None: metadata.normalized_source_channel_axis(image) - return metadata.source_channel_axis + return metadata.read_value("source_channel_axis") return source_metadata.pixel_semantics.validated_channel_axis(image) def resolved_source_path(self) -> Path | None: diff --git a/openhcs/core/runtime_image_values.py b/openhcs/core/runtime_image_values.py index 661367cdef..97460af617 100644 --- a/openhcs/core/runtime_image_values.py +++ b/openhcs/core/runtime_image_values.py @@ -2,17 +2,19 @@ from __future__ import annotations -from abc import ABC, abstractmethod +from abc import ABC, abstractmethod, update_abstractmethods from collections.abc import ( Iterable, Sequence, ) -from dataclasses import dataclass, field, fields +from dataclasses import Field, dataclass, field, fields from enum import Enum from pathlib import Path from types import MappingProxyType from typing import Any, TypeVar +import copyreg + import numpy as np from arraybridge import ArrayGeometry from python_introspect import dataclass_from_mapping @@ -108,95 +110,378 @@ def without_source_planes(self) -> "ImageUnitIntervalIntensityMetadata": return type(self)(scale=self.scale) -@dataclass(slots=True) -class ImagePayloadMetadata( - SourceImageProvenanceFields, - SourceSpatialDomainFields, - SourceVoxelSpacingFields, -): - """Generic source-image metadata that should travel with runtime pixels.""" +class ImageMetadataProjection(ABC): + """One declared metadata namespace, with nominal source and axis projections. - intensity_scale: float | None = None - source_dtype: str | None = None - unit_interval_intensity: ImageUnitIntervalIntensityMetadata | None = None - source_plane_intensity_scales: tuple[float | None, ...] = () - source_plane_dtypes: tuple[str | None, ...] = () - physical_border_edges_yx: PhysicalBorderEdgesYX = None - mask_defines_border: bool | None = None - source_channel_axis: int | None = field( - default=None, metadata={ViewerWireField.IMAGE_METADATA: True} - ) - plane_axis: RuntimePlaneAxis | None = field( - default=None, metadata={ViewerWireField.IMAGE_METADATA: True} + Owned values are flat. Public realization transfers authority to a real mutable + dataclass instead of keeping a synchronized copy of its fields. + """ + + __slots__ = ( + "_source_metadata", + "_metadata_type", + "_captured_values", + "_realized_metadata", ) + def __init__(self, metadata: "ImageMetadataProjection") -> None: + object.__setattr__(self, "_source_metadata", metadata) + object.__setattr__(self, "_metadata_type", metadata.metadata_type) + object.__setattr__(self, "_captured_values", None) + object.__setattr__(self, "_realized_metadata", None) + + @property + def metadata(self) -> "ImageMetadataProjection": + return self._source_metadata + + @property + def metadata_type(self) -> type["ImagePayloadMetadata"]: + return self._metadata_type + + def declared_fields(self) -> tuple[Field[Any], ...]: + return fields(self.metadata_type) + + def read_field(self, member: Field[MetadataValueT]) -> MetadataValueT: + if self._realized_metadata is not None: + return getattr(self._realized_metadata, member.name) + if self._captured_values is not None: + return self._captured_values[member.name] + return self.capture().read_field(member) + + def read_value(self, name: str) -> Any: + if self._realized_metadata is not None: + return self._realized_metadata.read_value(name) + if self._captured_values is not None: + return self._captured_values[name] + return self.capture().read_value(name) + + def write_value(self, name: str, value: Any) -> None: + if self._realized_metadata is not None: + setattr(self._realized_metadata, name, value) + else: + self._captured_values[name] = value + + def field_values(self) -> dict[str, Any]: + if self._realized_metadata is not None: + return self._realized_metadata.field_values() + if self._captured_values is not None: + return dict(self._captured_values) + return self.metadata.field_values() + @classmethod - def viewer_field_names(cls) -> frozenset[str]: - """Derive the viewer projection from its actual metadata declarations.""" - return frozenset( - member.name - for member in fields(cls) - if member.metadata.get(ViewerWireField.IMAGE_METADATA, False) + def _from_captured_values( + cls, metadata_type: type["ImagePayloadMetadata"], values: dict[str, Any] + ) -> "ImageMetadataProjection": + expected = tuple(member.name for member in fields(metadata_type)) + if tuple(values) != expected: + raise ValueError( + "Captured image metadata does not match its declared schema." + ) + result = ImageMetadataProjection.__new__(ImageMetadataProjection) + object.__setattr__(result, "_source_metadata", None) + object.__setattr__(result, "_metadata_type", metadata_type) + object.__setattr__(result, "_captured_values", values) + object.__setattr__(result, "_realized_metadata", None) + return result + + @classmethod + def _from_realized_metadata(cls, metadata): + result = ImageMetadataProjection.__new__(ImageMetadataProjection) + object.__setattr__(result, "_source_metadata", None) + object.__setattr__(result, "_metadata_type", type(metadata)) + object.__setattr__(result, "_captured_values", None) + object.__setattr__(result, "_realized_metadata", metadata) + return result + + @staticmethod + def _construct_metadata(metadata_type, values): + result = metadata_type( + **{ + member.name: values[member.name] + for member in fields(metadata_type) + if member.init + } + ) + return result + + @staticmethod + def _uses_declared_constructor(metadata_type): + """Constructor overrides and dependent fields are nominal behavior.""" + return ( + metadata_type.__init__ is not ImagePayloadMetadata.__init__ + or metadata_type.__post_init__ is not ImagePayloadMetadata.__post_init__ + or metadata_type.__new__ is not object.__new__ + or metadata_type.normalize_metadata_fields + is not ImagePayloadMetadata.normalize_metadata_fields + or metadata_type.source_component_metadata + is not SourceImageProvenanceFields.source_component_metadata + or metadata_type.source_path is not SourceImageProvenanceFields.source_path + or metadata_type.normalize_source_provenance_fields + is not SourceImageProvenanceFields.normalize_source_provenance_fields + or metadata_type.normalize_source_spatial_domain_fields + is not SourceSpatialDomainFields.normalize_source_spatial_domain_fields + or metadata_type.normalize_source_voxel_spacing_fields + is not SourceVoxelSpacingFields.normalize_source_voxel_spacing_fields + or metadata_type.validate_source_channel_axis + is not ImageMetadataProjection.validate_source_channel_axis + or any(not member.init for member in fields(metadata_type)) + ) + + @classmethod + def _normalize_values(cls, metadata_type, values): + """Run the common ordered lifecycle on the flat, declared namespace.""" + owner = cls._from_captured_values(metadata_type, values) + owner.normalize_metadata_fields() + return owner._captured_values + + def normalize_metadata_fields(self) -> None: + """Normalize declared facts through their nominal lifetime hooks.""" + self.write_value( + "source_voxel_spacing", + self.read_value("source_voxel_spacing").with_missing_from( + SourceVoxelSpacing.from_source_metadata( + self.source_component_metadata_for_projection() + ) + ), + ) + self.normalize_source_provenance_fields() + self.normalize_source_spatial_domain_fields() + self.normalize_source_voxel_spacing_fields() + self.validate_source_channel_axis() + plane_axis = self.read_value("plane_axis") + if plane_axis is not None: + self.write_value("plane_axis", RuntimePlaneAxis(plane_axis)) + + def source_component_metadata_for_projection(self): + """Read the nominal scalar view, preserving a realized subtype's alias.""" + if self._realized_metadata is not None: + return self._realized_metadata.source_component_metadata_for_projection() + return self.read_value("source_provenance").source_component_metadata + + def source_path_for_projection(self): + if self._realized_metadata is not None: + return self._realized_metadata.source_path_for_projection() + return self.read_value("source_provenance").source_path + + def normalize_source_provenance_fields(self) -> None: + # Captured namespaces and mutable dataclasses both birth a new source + # identity here. An existing provenance object is publicly mutable. + provenance = self.read_value("source_provenance") + self.write_value( + "source_provenance", + SourceImageProvenance( + provenance.source_path, + provenance.source_component_metadata, + provenance.source_image_provenance_planes, + provenance.source_image_names, + ), + ) + + def normalize_source_spatial_domain_fields(self) -> None: + self.write_value( + "source_spatial_domain", + self.read_value("source_spatial_domain").normalized(), + ) + + def normalize_source_voxel_spacing_fields(self) -> None: + self.write_value( + "source_voxel_spacing", + SourceVoxelSpacing.coerce(self.read_value("source_voxel_spacing")), ) + def project_source_values(self, values: dict[str, Any]) -> dict[str, Any]: + return self._normalize_values(self.metadata_type, values) + + def project_axis_values(self, values: dict[str, Any]) -> dict[str, Any]: + return values + + def capture(self) -> "ImageMetadataProjection": + if self._captured_values is not None: + return self + if self._uses_declared_constructor(self.metadata_type): + # A genuine constructor extension keeps its original one-namespace + # public lifecycle; capture retains its facts without a live chain. + return self._from_realized_metadata(self.project()) + values = self.project_axis_values( + self.project_source_values(self.field_values()) + ) + return self._from_captured_values(self.metadata_type, values) + + def derive_fields(self, **changes: Any) -> "ImageMetadataProjection": + values = self.field_values() + for name in changes: + member = self.metadata_type.__dataclass_fields__.get(name) + if member is None or member not in self.declared_fields(): + raise TypeError(f"Undeclared image metadata field {name!r}.") + if not member.init: + raise ValueError( + f"field {name} is declared with init=False, it cannot be specified with replace()" + ) + values.update(changes) + if self._uses_declared_constructor(self.metadata_type): + return self._from_realized_metadata( + self._construct_metadata(self.metadata_type, values) + ) + return self._from_captured_values( + self.metadata_type, self._normalize_values(self.metadata_type, values) + ) + + def replace_fields(self, **changes: Any) -> "ImageMetadataProjection": + return self.derive_fields(**changes) + + def materialize_metadata(self) -> "ImagePayloadMetadata": + if self._realized_metadata is None: + if self._captured_values is None: + return self.capture().materialize_metadata() + result = self.metadata_type.__new__(self.metadata_type) + for member in self.declared_fields(): + object.__setattr__( + result, member.name, self._captured_values[member.name] + ) + object.__setattr__(self, "_realized_metadata", result) + object.__setattr__(self, "_captured_values", None) + return self._realized_metadata + + def project(self) -> "ImagePayloadMetadata": + """Explicit public projection retains the concrete nominal constructor.""" + if self._source_metadata is None: + return self._construct_metadata(self.metadata_type, self.field_values()) + return self.project_axis_metadata(self.project_source_metadata()) + + def project_source_metadata(self): + return self.metadata.materialize_metadata() + + def project_axis_metadata(self, metadata): + return metadata + + def project_source_provenance(self, metadata, provenance): + return metadata.replace_fields(source_provenance=provenance) + + def to_metadata_mapping(self) -> dict[str, Any]: + return { + member.name: to_jsonable(self.read_field(member)) + for member in self.declared_fields() + } + + def __reduce__(self): + if self._source_metadata is not None: + return (type(self), (self.metadata,)) + if self._realized_metadata is not None: + return ( + _restore_realized_image_metadata_projection, + (self._realized_metadata,), + ) + values = self.field_values() + return ( + _restore_image_metadata_projection, + (self.metadata_type, tuple(values.items())), + ) + + def without_leading_plane_axis(self, *, projection=None): + selected = ( + LeadingPlaneAxisMetadataProjection(self) + if projection is None + else projection + ) + return selected.capture() + + def for_source_plane(self, plane_index: int): + return SourcePlaneImageMetadataProjection(self, plane_index).capture() + + def with_source_context_from(self, source): + """Derive identity, axes and spatial context in one result namespace.""" + source_provenance = self.read_value("source_provenance").with_missing_from( + source.read_value("source_provenance") + ) + channel_axis = self.read_value("source_channel_axis") + if channel_axis is None: + channel_axis = source.read_value("source_channel_axis") + plane_axis = self.read_value("plane_axis") + source_axis = source.read_value("plane_axis") + if plane_axis is None: + plane_axis = source_axis + elif source_axis is not None and source_axis is not plane_axis: + raise ValueError( + "Cannot combine image metadata with conflicting plane axes: " + f"{plane_axis.value!r} != {source_axis.value!r}." + ) + spatial_domain = self.read_value("source_spatial_domain").with_missing_from( + source.read_value("source_spatial_domain") + ) + return self.replace_fields( + source_provenance=source_provenance, + source_channel_axis=channel_axis, + plane_axis=plane_axis, + source_spatial_domain=spatial_domain, + source_voxel_spacing=self.read_value( + "source_voxel_spacing" + ).with_missing_from(source.read_value("source_voxel_spacing")), + physical_border_edges_yx=( + self.read_value("physical_border_edges_yx") + if self.read_value("physical_border_edges_yx") is not None + else source.read_value("physical_border_edges_yx") + ), + mask_defines_border=( + self.read_value("mask_defines_border") + if self.read_value("mask_defines_border") is not None + else source.read_value("mask_defines_border") + ), + ) + + def payload_with(self, data, mask=None): + if mask is None: + return ( + ImageMetadataPayload.from_projection(data, self) + if self.has_values + else data + ) + return MaskedImagePayload.from_projection(data, mask, self) + + def for_source_plane(self, plane_index): + return SourcePlaneImageMetadataProjection(self, plane_index).capture() + + def for_leading_source_plane(self, plane_index): + return LeadingSourcePlaneMetadataProjection(self, plane_index).capture() + 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.read_value("plane_axis") is not None: raise ValueError("Exported source planes require scalar image metadata.") - if self.source_channel_axis is not None: + if self.read_value("source_channel_axis") is not None: raise ValueError("Exported Z planes cannot carry an undeclared color axis.") def require_source_image_pixels(self, data: Any) -> None: """Validate decoded pixels against declared XY placement and dtype.""" - self.source_spatial_domain.require_image_window(data.shape) - if self.source_dtype is not None and np.dtype(self.source_dtype) != data.dtype: + self.read_value("source_spatial_domain").require_image_window(data.shape) + if ( + self.read_value("source_dtype") is not None + and np.dtype(self.read_value("source_dtype")) != data.dtype + ): raise ValueError( "Exported image pixels conflict with their declared dtype." ) 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: + plane_count = self.read_value("source_provenance").source_plane_count + if self.read_value("plane_axis") is None or plane_count != 1: return None return RuntimePlaneAxisValueProjection.from_selected_plane( - axis=self.plane_axis, + axis=self.read_value("plane_axis"), axis_size=plane_count, plane_index=0, - source_aliases=self.source_image_names, + source_aliases=self.read_value("source_provenance").source_image_names, ) def to_viewer_image_metadata(self) -> ViewerWireMapping: return ViewerWirePayload.mapping( { - name: to_jsonable(getattr(self, name)) - for name in self.viewer_field_names() + name: to_jsonable(self.read_value(name)) + for name in self.metadata_type.viewer_field_names() }, context="viewer image metadata", ) - @classmethod - def from_viewer_image_metadata( - cls, values: Mapping[str, object] - ) -> "ImagePayloadMetadata": - """Decode only declaration-owned viewer fields, never hidden source records.""" - if not isinstance(values, Mapping): - raise TypeError("Viewer image metadata requires a mapping.") - extras = set(values) - cls.viewer_field_names() - if extras: - raise ValueError(f"Undeclared viewer metadata fields: {sorted(extras)!r}.") - return cls.from_mapping(values) - - @classmethod - def from_mapping(cls, values: Mapping[str, object]) -> "ImagePayloadMetadata": - """Restore all declared metadata fields through their canonical codecs.""" - decoded = dict(values) - if "source_provenance" in decoded: - decoded["source_provenance"] = SourceImageProvenance.from_mapping( - decoded["source_provenance"] - ) - return dataclass_from_mapping(cls, decoded) - def retained_plane_component_values( self, ) -> dict[str, tuple[SourceMetadataScalar, ...]]: @@ -206,78 +491,22 @@ def retained_plane_component_values( Only a retained plane-axis declaration makes those coordinates a pixel axis; artifact storage/grouping axes do not declare that image domain. """ - - if self.plane_axis is None: + if self.read_value("plane_axis") is None: return {} - return self.source_provenance.varying_plane_component_values( + return self.read_value("source_provenance").varying_plane_component_values( tuple(AllComponents) ) - @classmethod - def for_array( - cls, - array: Any, - *, - source_path: str | None = None, - ) -> "ImagePayloadMetadata": - """Build metadata from an image array's source dtype.""" - import numpy as np - - dtype = np.asarray(array).dtype - return cls( - intensity_scale=image_intensity_scale_for_dtype(dtype), - source_dtype=str(dtype), - source_path=source_path, - ) - - @classmethod - def for_array_payload( - cls, - array: Any, - *, - source_path: str | None = None, - ) -> "ImagePayloadMetadata": - """Build source metadata from an arraybridge-detectable payload.""" - from openhcs.core.memory import MEMORY_TYPE_NUMPY, detect_memory_type - - memory_type = detect_memory_type(array) - dtype = array.dtype - if memory_type == MEMORY_TYPE_NUMPY: - intensity_scale = image_intensity_scale_for_dtype(dtype) - else: - intensity_scale = None - return cls( - intensity_scale=intensity_scale, - source_dtype=str(dtype), - source_path=source_path, - ) - - def __post_init__(self, *source_provenance_values: object) -> None: - self.absorb_explicit_source_provenance(source_provenance_values) - self.normalize_metadata_fields() - - def normalize_metadata_fields(self) -> None: - """Normalize this metadata's typed fields in their constructor effect order.""" - self.source_voxel_spacing = self.source_voxel_spacing.with_missing_from( - SourceVoxelSpacing.from_source_metadata(self.source_component_metadata) - ) - self.normalize_source_provenance_fields() - self.normalize_source_spatial_domain_fields() - self.normalize_source_voxel_spacing_fields() - self.validate_source_channel_axis() - if self.plane_axis is not None: - self.plane_axis = RuntimePlaneAxis(self.plane_axis) - 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.read_value("plane_axis") is None: raise ValueError(message) def validate_source_channel_axis(self) -> None: """Validate the authored channel declaration before transforming axes.""" - if self.source_channel_axis is not None and ( - not isinstance(self.source_channel_axis, int) - or isinstance(self.source_channel_axis, bool) + if self.read_value("source_channel_axis") is not None and ( + not isinstance(self.read_value("source_channel_axis"), int) + or isinstance(self.read_value("source_channel_axis"), bool) ): raise TypeError( "ImagePayloadMetadata.source_channel_axis must be int or None." @@ -288,41 +517,40 @@ def has_values(self) -> bool: """Return whether this metadata carries any semantic image facts.""" return any( ( - self.intensity_scale is not None, - self.source_dtype is not None, - self.source_path is not None, - self.source_component_metadata is not None, - self.unit_interval_intensity is not None, - bool(self.source_plane_intensity_scales), - bool(self.source_plane_dtypes), - self.source_image_provenance_planes.has_values, - self.source_spatial_domain.has_values, - self.source_voxel_spacing.has_values, - self.physical_border_edges_yx is not None, - self.mask_defines_border is not None, - bool(self.source_image_names), - self.source_channel_axis is not None, - self.plane_axis is not None, + self.read_value("intensity_scale") is not None, + self.read_value("source_dtype") is not None, + self.read_value("source_provenance").source_path is not None, + self.read_value("source_provenance").source_component_metadata + is not None, + self.read_value("unit_interval_intensity") is not None, + bool(self.read_value("source_plane_intensity_scales")), + bool(self.read_value("source_plane_dtypes")), + self.read_value( + "source_provenance" + ).source_image_provenance_planes.has_values, + self.read_value("source_spatial_domain").has_values, + self.read_value("source_voxel_spacing").has_values, + self.read_value("physical_border_edges_yx") is not None, + self.read_value("mask_defines_border") is not None, + bool(self.read_value("source_provenance").source_image_names), + self.read_value("source_channel_axis") is not None, + self.read_value("plane_axis") is not None, ) ) @property def unit_interval_intensity_scale(self) -> int | None: """Return the authored scalar unit-interval quantization proof.""" - - if self.unit_interval_intensity is None: + if self.read_value("unit_interval_intensity") is None: return None - return self.unit_interval_intensity.scale + return self.read_value("unit_interval_intensity").scale @property - def source_plane_unit_interval_intensity_scales( - self, - ) -> tuple[int | None, ...]: + def source_plane_unit_interval_intensity_scales(self) -> tuple[int | None, ...]: """Return authored per-plane unit-interval quantization proofs.""" - - if self.unit_interval_intensity is None: + if self.read_value("unit_interval_intensity") is None: return () - return self.unit_interval_intensity.source_plane_scales + return self.read_value("unit_interval_intensity").source_plane_scales def attach_to(self, payload: Any) -> RuntimeArrayData: """Attach this metadata to an existing image payload.""" @@ -332,15 +560,11 @@ def attach_to(self, payload: Any) -> RuntimeArrayData: def attach_source_context_to(self, payload: Any) -> RuntimeArrayData: """Attach source context while retaining the payload's declared array axes.""" - - payload_metadata = image_payload_metadata(payload) + payload_metadata = image_payload_metadata_projection(payload) return self.replace_fields( - source_channel_axis=payload_metadata.source_channel_axis, - plane_axis=payload_metadata.plane_axis, - ).payload_with( - image_payload_data(payload), - image_payload_mask(payload), - ) + source_channel_axis=payload_metadata.read_value("source_channel_axis"), + plane_axis=payload_metadata.read_value("plane_axis"), + ).payload_with(image_payload_data(payload), image_payload_mask(payload)) def derive_payload( self, @@ -351,19 +575,19 @@ def derive_payload( ) -> RuntimeArrayData: """Project this source metadata and mask onto derived image data.""" source_shape_yx = self.spatial_shape_yx(source_payload) - output_shape_yx = image_payload_metadata(data).spatial_shape_yx(data) + output_shape_yx = image_payload_metadata_projection(data).spatial_shape_yx(data) same_spatial_domain = ( source_shape_yx is not None and output_shape_yx is not None - and source_shape_yx == output_shape_yx + and (source_shape_yx == output_shape_yx) ) - output_metadata = image_payload_metadata(data) + output_metadata = image_payload_metadata_projection(data) metadata = self._derived_metadata(data, plane_projection) output_declares_spatial_domain = ( isinstance(data, ImagePayloadMetadataCarrier) - and output_metadata.source_spatial_domain.has_values + and output_metadata.read_value("source_spatial_domain").has_values ) - if not same_spatial_domain and not output_declares_spatial_domain: + if not same_spatial_domain and (not output_declares_spatial_domain): metadata = metadata.without_spatial_domain() output_mask = image_payload_mask(data) if output_mask is None and same_spatial_domain: @@ -379,7 +603,7 @@ def _derived_metadata( data: RuntimeArrayData, plane_projection: RuntimePlaneAxisValueProjection | None, ) -> "ImagePayloadMetadata": - output_metadata = image_payload_metadata( + output_metadata = image_payload_metadata_projection( data ).with_indexed_source_plane_provenance(None) source_metadata = self.with_indexed_source_plane_provenance(None) @@ -387,18 +611,19 @@ def _derived_metadata( isinstance(data, ImagePayloadMetadataCarrier) and output_metadata.has_values ) declared_output_axis = ( - output_metadata.plane_axis + output_metadata.read_value("plane_axis") if output_metadata_is_authoritative - else source_metadata.plane_axis + else source_metadata.read_value("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 is not None + or source_metadata.read_value("plane_axis") is not None ): - declared_axis = declared_output_axis or source_metadata.plane_axis + declared_axis = declared_output_axis or source_metadata.read_value( + "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"Derived image payload carries a declared plane axis but the invocation supplied no plane projection: {declared_axis.value!r}." ) if plane_projection is not None and plane_projection.plane_index is None: for owner_name, metadata in ( @@ -406,14 +631,11 @@ def _derived_metadata( ("output", output_metadata), ): if ( - metadata.plane_axis is not None - and metadata.plane_axis is not plane_projection.axis + metadata.read_value("plane_axis") is not None + and metadata.read_value("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"Derived image {owner_name} metadata axis conflicts with the invocation projection: {metadata.read_value('plane_axis').value!r} != {plane_projection.axis.value!r}." ) source_metadata = source_metadata.with_indexed_source_plane_provenance( plane_projection.axis_size @@ -421,8 +643,7 @@ def _derived_metadata( if declared_output_axis is not None: plane_projection.validate_shape( image_payload_geometry( - data, - value_name="Derived image payload", + data, value_name="Derived image payload" ).shape, value_name="Derived image payload", ) @@ -434,25 +655,27 @@ def _derived_metadata( ("source", source_metadata), ("output", output_metadata), ): - if metadata.plane_axis is not None: + if metadata.read_value("plane_axis") is not None: raise ValueError( - f"Derived image {owner_name} metadata retains an unconsumed " - f"{metadata.plane_axis.value!r} axis after the invocation " - "selected one plane." + f"Derived image {owner_name} metadata retains an unconsumed {metadata.read_value('plane_axis').value!r} axis after the invocation selected one plane." ) source_context = source_metadata.replace_fields(plane_axis=None) if isinstance(data, ImagePayloadMetadataCarrier): source_context = source_context.replace_fields(source_channel_axis=None) metadata = output_metadata.with_source_context_from(source_context) - if source_metadata.source_provenance.has_values: + if source_metadata.read_value("source_provenance").has_values: metadata = metadata.with_source_provenance( - source_metadata.source_provenance.with_derived_source_image_names( - output_metadata.source_image_names - or source_metadata.source_image_names + source_metadata.read_value( + "source_provenance" + ).with_derived_source_image_names( + output_metadata.read_value("source_provenance").source_image_names + or source_metadata.read_value( + "source_provenance" + ).source_image_names ) ) return metadata.with_missing_intensity_from(source_metadata).replace_fields( - plane_axis=declared_output_axis, + plane_axis=declared_output_axis ) def project_channel_payload( @@ -467,16 +690,13 @@ def project_channel_payload( """Project one channel while preserving metadata and mask semantics.""" if channel_data is None: channel_data = ImageMaskDomain.channel_axis_slice( - source_data, - channel_axis=channel_axis, - channel_index=channel_index, + source_data, channel_axis=channel_axis, channel_index=channel_index ) source_channel_axis = self.normalized_source_channel_axis(source_data) projected_axis = ( channel_axis % image_payload_geometry( - source_data, - value_name="Source image payload", + source_data, value_name="Source image payload" ).ndim ) metadata = ( @@ -503,58 +723,37 @@ def has_complete_source_identity( plane_projection: RuntimePlaneAxisValueProjection | None = None, ) -> bool: """Validate source identity against declared runtime-plane semantics.""" - planes = self.source_image_provenance_planes + planes = self.read_value("source_provenance").source_image_provenance_planes if ( plane_projection is not None and plane_projection.plane_index is None - and plane_projection.axis_size > 1 - and self.plane_axis is None + and (plane_projection.axis_size > 1) + and (self.read_value("plane_axis") is None) ): return False - if plane_projection is not None and self.plane_axis is not None: - if self.plane_axis is not plane_projection.axis: + if plane_projection is not None and self.read_value("plane_axis") is not None: + if self.read_value("plane_axis") is not plane_projection.axis: raise ValueError( - "Image payload plane declaration conflicts with the execution " - f"projection: {self.plane_axis!r} != {plane_projection.axis!r}." + f"Image payload plane declaration conflicts with the execution projection: {self.read_value('plane_axis')!r} != {plane_projection.axis!r}." ) plane_projection.validate_shape( image_payload_geometry( - payload, - value_name="Source-identified image payload", + payload, value_name="Source-identified image payload" ).shape, value_name="Source-identified image payload", ) if planes.count != plane_projection.axis_size: return False if planes.has_values: - return all(plane.addressable for plane in planes.planes) - return self.source_provenance.addressable - - @classmethod - def compose( - cls, - payloads: Sequence[RuntimeArrayData], - *, - mode: "ImagePayloadMetadataCompositionMode | None" = None, - 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, - source_metadata_override=source_metadata, - metadata_type=cls, - ).compose() + return all((plane.addressable for plane in planes.planes)) + return self.read_value("source_provenance").addressable def normalized_source_channel_axis(self, data: Any) -> int | None: """Return this declared channel axis normalized for ``data``.""" - if self.source_channel_axis is None: + if self.read_value("source_channel_axis") is None: return None ndim = image_payload_geometry(data).ndim - axis = self.source_channel_axis + axis = self.read_value("source_channel_axis") normalized = axis if axis >= 0 else ndim + axis if normalized < 0 or normalized >= ndim: raise ValueError( @@ -566,22 +765,22 @@ def spatial_axes_yx(self, data: Any) -> tuple[int, int] | None: """Return Y/X axes after excluding the declared channel axis.""" ndim = image_payload_geometry(data).ndim channel_axis = self.normalized_source_channel_axis(data) - candidate_axes = tuple(axis for axis in range(ndim) if axis != channel_axis) + candidate_axes = tuple((axis for axis in range(ndim) if axis != channel_axis)) if len(candidate_axes) < 2: return None - return candidate_axes[-2], candidate_axes[-1] + return (candidate_axes[-2], candidate_axes[-1]) def is_declared_source_channel_plane(self, data: Any) -> bool: """Return whether this payload declares one channel-bearing image plane.""" if self.normalized_source_channel_axis(data) is None: return False - return self.plane_axis is None + return self.read_value("plane_axis") is None def is_declared_source_channel_stack(self, data: Any) -> bool: """Return whether this payload declares a plane stack with a channel axis.""" if self.normalized_source_channel_axis(data) is None: return False - return self.plane_axis is not None + return self.read_value("plane_axis") is not None def spatial_shape_yx(self, data: Any) -> tuple[int, int] | None: """Return Y/X shape using only declared channel-axis semantics.""" @@ -589,19 +788,19 @@ def spatial_shape_yx(self, data: Any) -> tuple[int, int] | None: if axes is None: return None shape = image_payload_geometry(data).shape - return shape[axes[0]], shape[axes[1]] + return (shape[axes[0]], shape[axes[1]]) def mask_domain(self, data: Any) -> "ImageMaskDomain": """Return the mask domain declared for this payload.""" spatial_axes_yx = ( self.spatial_axes_yx(data) - if self.source_spatial_domain.source_shape_yx is not None + if self.read_value("source_spatial_domain").source_shape_yx is not None else None ) return ImageMaskDomain( image_payload_geometry(data).shape, self.normalized_source_channel_axis(data), - self.plane_axis, + self.read_value("plane_axis"), spatial_axes_yx, ) @@ -609,44 +808,13 @@ def without_source_channel_axis(self) -> "ImagePayloadMetadata": """Return metadata after an operation collapses the source channel axis.""" return self.replace_fields(source_channel_axis=None) - def for_leading_source_plane(self, plane_index: int) -> "ImagePayloadMetadata": - """Project metadata after explicitly removing its leading plane axis.""" - return LeadingSourcePlaneMetadataProjection(self, plane_index).project() - - def without_leading_plane_axis( - self, *, projection: "ImageMetadataProjection | None" = None - ) -> "ImagePayloadMetadata": - """Remove an axis through the projection's declared result ownership.""" - self.require_leading_plane_axis( - "Image metadata has no leading plane axis to remove." - ) - source_channel_axis = self.source_channel_axis - if source_channel_axis == 0: - raise ValueError( - "Image metadata cannot declare the same leading axis as both " - "plane and channel." - ) - if source_channel_axis is not None and source_channel_axis > 0: - source_channel_axis -= 1 - if projection is None: - projection = LeadingPlaneAxisMetadataProjection(self) - projected = projection.project_source_provenance( - self, self.source_provenance.with_runtime_planes_as_contributors() - ) - projected.plane_axis = None - projected.source_channel_axis = source_channel_axis - projected.source_plane_intensity_scales = () - projected.source_plane_dtypes = () - projected.unit_interval_intensity = self.project_intensity_proof(None) - projected.normalize_metadata_fields() - return projected - def collapse_leading_plane_axis(self) -> "ImagePayloadMetadata": """Return scalar metadata after reducing every plane of the leading axis.""" - collapsed = self.without_leading_plane_axis() return collapsed.with_source_provenance( - collapsed.source_provenance.with_common_scalar_identity_from_planes() + collapsed.read_value( + "source_provenance" + ).with_common_scalar_identity_from_planes() ) @property @@ -654,11 +822,11 @@ def has_plane_specific_values(self) -> bool: """Return whether selecting a payload plane can change metadata.""" return any( ( - bool(self.source_plane_intensity_scales), - bool(self.source_plane_dtypes), + bool(self.read_value("source_plane_intensity_scales")), + bool(self.read_value("source_plane_dtypes")), bool(self.source_plane_unit_interval_intensity_scales), - self.source_provenance.source_plane_count > 0, - len(self.source_image_names) > 1, + self.read_value("source_provenance").source_plane_count > 0, + len(self.read_value("source_provenance").source_image_names) > 1, ) ) @@ -667,164 +835,156 @@ def source_image_paths(self) -> tuple[str, ...]: """Return paths from exact source identities represented by this payload.""" return tuple( dict.fromkeys( - str(identity.path) - for identity in self.source_provenance.represented_source_identities - if identity.path is not None and str(identity.path) + ( + str(identity.path) + for identity in self.read_value( + "source_provenance" + ).represented_source_identities + if identity.path is not None and str(identity.path) + ) ) ) def intensity_scale_for_source_plane(self, plane_index: int) -> float | None: """Return the best available intensity scale for one source plane.""" - plane_scale = _tuple_value(self.source_plane_intensity_scales, plane_index) - return self.intensity_scale if plane_scale is None else plane_scale + plane_scale = _tuple_value( + self.read_value("source_plane_intensity_scales"), plane_index + ) + return ( + self.read_value("intensity_scale") if plane_scale is None else plane_scale + ) def unit_interval_intensity_scale_for_source_plane( - self, - plane_index: int, + self, plane_index: int ) -> int | None: """Return the scale proving current pixels are exact integer/scale values.""" - if self.unit_interval_intensity is None: + if self.read_value("unit_interval_intensity") is None: return None - return self.unit_interval_intensity.scale_for_source_plane(plane_index) + return self.read_value("unit_interval_intensity").scale_for_source_plane( + plane_index + ) def project_intensity_proof( self, plane_index: int | None ) -> ImageUnitIntervalIntensityMetadata | None: """Select a plane's proof, or retain only the proof for the whole image.""" - if self.unit_interval_intensity is None: + if self.read_value("unit_interval_intensity") is None: return None if plane_index is None: - return self.unit_interval_intensity.without_source_planes() - return self.unit_interval_intensity.for_source_plane(plane_index) + return self.read_value("unit_interval_intensity").without_source_planes() + return self.read_value("unit_interval_intensity").for_source_plane(plane_index) @property def source_plane_metadata_count(self) -> int: """Return the cardinality represented by plane-specific metadata.""" return max( 1, - self.source_provenance.source_plane_count, - len(self.source_plane_intensity_scales), - len(self.source_plane_dtypes), + self.read_value("source_provenance").source_plane_count, + len(self.read_value("source_plane_intensity_scales")), + len(self.read_value("source_plane_dtypes")), len(self.source_plane_unit_interval_intensity_scales), ) def source_plane_metadata_records(self) -> tuple["ImagePayloadMetadata", ...]: """Return one scalar metadata record per represented source plane.""" - if ( - self.source_plane_metadata_count == 1 - and not self.source_provenance.source_plane_count + if self.source_plane_metadata_count == 1 and ( + not self.read_value("source_provenance").source_plane_count ): return (self,) return tuple( - self.for_source_plane(plane_index) - for plane_index in range(self.source_plane_metadata_count) + ( + self.for_source_plane(plane_index) + for plane_index in range(self.source_plane_metadata_count) + ) ) def common_unit_interval_intensity_scale(self) -> int | None: """Return the common unit-interval quantization proof for this payload.""" if self.source_plane_unit_interval_intensity_scales: present = tuple( - int(scale) - for plane_index in range( - len(self.source_plane_unit_interval_intensity_scales) - ) - for scale in ( - self.unit_interval_intensity_scale_for_source_plane(plane_index), + ( + int(scale) + for plane_index in range( + len(self.source_plane_unit_interval_intensity_scales) + ) + for scale in ( + self.unit_interval_intensity_scale_for_source_plane( + plane_index + ), + ) + if scale is not None ) - if scale is not None ) if not present: return None first = present[0] - if all(scale == first for scale in present): + if all((scale == first for scale in present)): return first return None return self.unit_interval_intensity_scale - def payload_with(self, data: Any, mask: Any | None = None) -> Any: - """Return image payload data carrying this metadata.""" - if mask is None and self.has_values: - return ImageMetadataPayload(data=data, metadata=self) - if mask is None: - return data - return MaskedImagePayload(data=data, mask=mask, metadata=self) - - def for_source_plane(self, plane_index: int) -> "ImagePayloadMetadata": - """Return metadata for one source plane sliced from a stacked payload.""" - return SourcePlaneImageMetadataProjection(self, plane_index).project() - - def for_source_planes( - self, - plane_indices: Sequence[int], - ) -> "ImagePayloadMetadata": + def for_source_planes(self, plane_indices: Sequence[int]) -> "ImagePayloadMetadata": """Return metadata projected to an ordered group of source planes.""" - normalized_indices = tuple(int(index) for index in plane_indices) + normalized_indices = tuple((int(index) for index in plane_indices)) if not normalized_indices: raise ValueError("Source-plane metadata projection cannot be empty.") if not self.has_plane_specific_values: return self invalid_indices = tuple( - index - for index in normalized_indices - if index < 0 or index >= self.source_plane_metadata_count + ( + index + for index in normalized_indices + if index < 0 or index >= self.source_plane_metadata_count + ) ) if invalid_indices: raise IndexError( - "Source-plane metadata projection indices must be within " - f"0..{self.source_plane_metadata_count - 1}; got " - f"{invalid_indices!r}." + f"Source-plane metadata projection indices must be within 0..{self.source_plane_metadata_count - 1}; got {invalid_indices!r}." ) if len(normalized_indices) == 1: return self.for_source_plane(normalized_indices[0]) return self.with_source_provenance( - self.source_provenance.for_source_planes(normalized_indices) + self.read_value("source_provenance").for_source_planes(normalized_indices) ).replace_fields( source_plane_intensity_scales=_tuple_values_at_indices( - self.source_plane_intensity_scales, - normalized_indices, + self.read_value("source_plane_intensity_scales"), normalized_indices ), source_plane_dtypes=_tuple_values_at_indices( - self.source_plane_dtypes, - normalized_indices, + self.read_value("source_plane_dtypes"), normalized_indices ), unit_interval_intensity=( None - if self.unit_interval_intensity is None - else self.unit_interval_intensity.for_source_planes(normalized_indices) + if self.read_value("unit_interval_intensity") is None + else self.read_value("unit_interval_intensity").for_source_planes( + normalized_indices + ) ), ) def project_declared_source_image( - self, - payload: RuntimeArrayData, - source_image_name: str, + self, payload: RuntimeArrayData, source_image_name: str ) -> RuntimeArrayData: """Project pixels and metadata to one exact declared source image.""" - - source_plane_selection = self.source_provenance.source_plane_selection( - source_image_name - ) + source_plane_selection = self.read_value( + "source_provenance" + ).source_plane_selection(source_image_name) if source_plane_selection is None: raise ValueError( - f"Image metadata does not represent declared source image " - f"{source_image_name!r}; represented names are " - f"{self.source_provenance.represented_source_image_names!r}, " - "with provenance planes " - f"{self.source_image_provenance_planes.identity!r}." + f"Image metadata does not represent declared source image {source_image_name!r}; represented names are {self.read_value('source_provenance').represented_source_image_names!r}, with provenance planes {self.read_value('source_provenance').source_image_provenance_planes.identity!r}." ) 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: + complete_source_axis = tuple( + range(self.read_value("source_provenance").source_plane_count) + ) + if self.read_value("plane_axis") is None: if source_plane_selection == complete_source_axis: return self.attach_to(payload) channel_axis = self.normalized_source_channel_axis(payload) if channel_axis is not None: if len(source_plane_selection) != 1: raise ValueError( - "Declared source-image channel projection requires exactly " - f"one channel; got source planes {source_plane_selection!r} " - f"for {source_image_name!r}." + f"Declared source-image channel projection requires exactly one channel; got source planes {source_plane_selection!r} for {source_image_name!r}." ) return self.project_channel_payload( payload, @@ -833,64 +993,59 @@ def project_declared_source_image( channel_axis=channel_axis, ) raise ValueError( - "Declared source-image payload projection requires a declared " - "plane or channel axis." + "Declared source-image payload projection requires a declared plane or channel axis." ) - if self.plane_axis not in ( + if self.read_value("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." + "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, - source_aliases=self.source_image_names, + axis=self.read_value("plane_axis"), + axis_size=self.read_value("source_provenance").source_plane_count, + source_aliases=self.read_value("source_provenance").source_image_names, ) axis_projection.validate_shape( image_payload_geometry( - payload, - value_name="Declared source-image payload", + payload, value_name="Declared source-image payload" ).shape, value_name="Declared source-image payload", ) if source_plane_selection == complete_source_axis: return self.attach_to(payload) - from openhcs.core.aligned_image_payload import stack_image_payloads from openhcs.core.runtime_slice_projection import RuntimeSliceProjection projected_planes = tuple( - RuntimeSliceProjection.value_for_slice( - payload, - axis_projection.selected_plane(plane_index), + ( + RuntimeSliceProjection.value_for_slice( + payload, axis_projection.selected_plane(plane_index) + ) + for plane_index in source_plane_selection ) - for plane_index in source_plane_selection ) if len(projected_planes) == 1: return projected_planes[0] return stack_image_payloads( projected_planes, metadata_mode=ImagePayloadMetadataCompositionMode.for_plane_axis( - self.plane_axis + self.read_value("plane_axis") ), ) def with_indexed_source_plane_provenance( - self, - expected_plane_count: int | None, + self, expected_plane_count: int | None ) -> "ImagePayloadMetadata": """Expand scalar source-plane metadata into per-plane provenance.""" source_provenance = SourcePlaneIndexedProvenanceExpansion( - self.source_provenance, + self.read_value("source_provenance"), expected_plane_count=expected_plane_count, ).expanded() - if ( - source_provenance is self.source_provenance - or source_provenance == self.source_provenance - ): + if source_provenance is self.read_value( + "source_provenance" + ) or source_provenance == self.read_value("source_provenance"): return self return self.with_source_provenance(source_provenance) @@ -910,16 +1065,13 @@ def for_grouped_source_plane_projection( if not source_plane_indices: if metadata.has_plane_specific_values: raise ValueError( - "Cannot assign one image metadata record to a runtime plane " - "that represents multiple source planes: " - f"{source_plane_indices!r}." + f"Cannot assign one image metadata record to a runtime plane that represents multiple source planes: {source_plane_indices!r}." ) return metadata return metadata.for_source_planes(source_plane_indices) def with_unit_interval_intensity_scale( - self, - scale: int | None, + self, scale: int | None ) -> "ImagePayloadMetadata": """Return metadata with the current unit-interval pixel proof updated.""" return self.replace_fields( @@ -929,205 +1081,399 @@ def with_unit_interval_intensity_scale( def without_unit_interval_intensity_scale(self) -> "ImagePayloadMetadata": """Return metadata after an arithmetic transform changed pixel values.""" return self.replace_fields( - unit_interval_intensity=ImageUnitIntervalIntensityMetadata(), + unit_interval_intensity=ImageUnitIntervalIntensityMetadata() ) def without_spatial_domain(self) -> "ImagePayloadMetadata": """Return metadata with invalidated source-spatial placement removed.""" return self.replace_fields( source_spatial_domain=SourceSpatialDomain(), - source_voxel_spacing=self.source_voxel_spacing, + source_voxel_spacing=self.read_value("source_voxel_spacing"), physical_border_edges_yx=None, mask_defines_border=None, ) def object_label_source_spatial_domain(self) -> SourceSpatialDomain: """Return this metadata's object-label source-image coordinate domain.""" - return self.source_spatial_domain.with_value_name( - OBJECT_LABEL_SOURCE_SPATIAL_VALUE_NAME, + return self.read_value("source_spatial_domain").with_value_name( + OBJECT_LABEL_SOURCE_SPATIAL_VALUE_NAME ) def with_source_spatial_context_from( + self, source: "ImagePayloadMetadata" + ) -> "ImagePayloadMetadata": + """Fill missing source-image geometry without changing provenance.""" + spatial_domain = self.read_value("source_spatial_domain").with_missing_from( + source.read_value("source_spatial_domain") + ) + return self.replace_fields( + source_spatial_domain=spatial_domain, + source_voxel_spacing=self.read_value( + "source_voxel_spacing" + ).with_missing_from(source.read_value("source_voxel_spacing")), + physical_border_edges_yx=( + self.read_value("physical_border_edges_yx") + if self.read_value("physical_border_edges_yx") is not None + else source.read_value("physical_border_edges_yx") + ), + mask_defines_border=( + self.read_value("mask_defines_border") + if self.read_value("mask_defines_border") is not None + else source.read_value("mask_defines_border") + ), + ) + + def with_missing_intensity_from( + self, source: "ImagePayloadMetadata" + ) -> "ImagePayloadMetadata": + """Fill missing pixel-type and intensity metadata from a source payload.""" + return self.replace_fields( + intensity_scale=( + self.read_value("intensity_scale") + if self.read_value("intensity_scale") is not None + else source.read_value("intensity_scale") + ), + source_dtype=( + self.read_value("source_dtype") + if self.read_value("source_dtype") is not None + else source.read_value("source_dtype") + ), + unit_interval_intensity=( + self.read_value("unit_interval_intensity") + if self.read_value("unit_interval_intensity") is not None + else source.read_value("unit_interval_intensity") + ), + source_plane_intensity_scales=self.read_value( + "source_plane_intensity_scales" + ) + or source.read_value("source_plane_intensity_scales"), + source_plane_dtypes=self.read_value("source_plane_dtypes") + or source.read_value("source_plane_dtypes"), + ) + + def with_source_provenance( + self, source_provenance: SourceImageProvenance + ) -> "ImagePayloadMetadata": + """Return metadata with source-image provenance replaced atomically.""" + return self.replace_fields(source_provenance=source_provenance) + + def with_source_component_metadata( + self, source_component_metadata: SourceComponentMetadata | None + ) -> "ImagePayloadMetadata": + """Return metadata with only scalar source component metadata changed.""" + return self.with_source_provenance( + self.read_value("source_provenance").with_source_component_metadata( + source_component_metadata + ) + ) + + def physical_border_edges_for_shape( + self, image_shape_yx: Sequence[int] + ) -> tuple[bool, bool, bool, bool]: + """Return which local image edges are true source-image edges. + + Edge tuple order is ``(top, bottom, left, right)``. Missing spatial + metadata means the current image is treated as the full physical image, + preserving historical behavior for plain arrays. + """ + if self.read_value("physical_border_edges_yx") is not None: + return tuple( + (bool(edge) for edge in self.read_value("physical_border_edges_yx")) + ) + return self.read_value("source_spatial_domain").physical_border_edges_for_shape( + image_shape_yx + ) + + def with_spatial_crop( self, - source: "ImagePayloadMetadata", + *, + input_shape_yx: Sequence[int], + output_shape_yx: Sequence[int], + offset_yx: tuple[int, int], + physical_border_edges_yx: PhysicalBorderEdgesYX = None, + ) -> "ImagePayloadMetadata": + """Return metadata for a crop of this image payload.""" + output_shape = _source_spatial_shape_pair(output_shape_yx, "output_shape_yx") + spatial_domain = self.read_value("source_spatial_domain").with_spatial_crop( + input_shape_yx=input_shape_yx, + output_shape_yx=output_shape_yx, + offset_yx=offset_yx, + ) + if physical_border_edges_yx is None: + physical_border_edges_yx = spatial_domain.physical_border_edges_for_shape( + output_shape + ) + return self.replace_fields( + source_spatial_domain=spatial_domain, + physical_border_edges_yx=tuple( + (bool(edge) for edge in physical_border_edges_yx) + ), + ) + + def with_spatial_resize( + self, output_shape_yx: Sequence[int] + ) -> "ImagePayloadMetadata": + """Return metadata in the local coordinate domain created by a resize.""" + return self.replace_fields( + source_spatial_domain=self.read_value( + "source_spatial_domain" + ).with_spatial_resize(output_shape_yx), + physical_border_edges_yx=(True, True, True, True), + mask_defines_border=None, + ) + + def with_materialized_source_domain( + self, target_domain: SourceSpatialDomain + ) -> "ImagePayloadMetadata": + """Return metadata after pixels are expanded to source-image XY.""" + return self.replace_fields( + source_spatial_domain=self.read_value( + "source_spatial_domain" + ).as_materialized_source_domain(target_domain), + physical_border_edges_yx=(True, True, True, True), + ) + + +def _restore_image_metadata_projection(metadata_type, items): + # Values retain their existing nominal source identities and stored birth caches. + # Do not reconstruct provenance via from_mapping or invoke opaque repr here. + return ImageMetadataProjection._from_captured_values(metadata_type, dict(items)) + + +def _restore_realized_image_metadata_projection(metadata): + return ImageMetadataProjection._from_realized_metadata(metadata) + + +@to_jsonable.register(ImageMetadataProjection) +def _jsonable_image_metadata_projection(value): + return value.to_metadata_mapping() + + +@dataclass(slots=True) +class ImagePayloadMetadata( + SourceImageProvenanceFields, + SourceSpatialDomainFields, + SourceVoxelSpacingFields, + ImageMetadataProjection, +): + """Generic source-image metadata that should travel with runtime pixels.""" + + intensity_scale: float | None = None + source_dtype: str | None = None + unit_interval_intensity: ImageUnitIntervalIntensityMetadata | None = None + source_plane_intensity_scales: tuple[float | None, ...] = () + source_plane_dtypes: tuple[str | None, ...] = () + physical_border_edges_yx: PhysicalBorderEdgesYX = None + mask_defines_border: bool | None = None + source_channel_axis: int | None = field( + default=None, metadata={ViewerWireField.IMAGE_METADATA: True} + ) + plane_axis: RuntimePlaneAxis | None = field( + default=None, metadata={ViewerWireField.IMAGE_METADATA: True} + ) + + @property + def metadata_type(self): + return type(self) + + def read_field(self, member): + return getattr(self, member.name) + + def read_value(self, name): + return getattr(self, name) + + def write_value(self, name, value): + setattr(self, name, value) + + def field_values(self): + return { + member.name: getattr(self, member.name) for member in fields(type(self)) + } + + def capture(self): + if self._uses_declared_constructor(type(self)): + return self._from_realized_metadata(self.replace_fields()) + return ImageMetadataProjection._from_captured_values( + type(self), self._normalize_values(type(self), self.field_values()) + ) + + def materialize_metadata(self): + return self + + def __reduce__(self): + return (copyreg.__newobj__, (type(self),), object.__getstate__(self)) + + @classmethod + def viewer_field_names(cls) -> frozenset[str]: + """Derive the viewer projection from its actual metadata declarations.""" + return frozenset( + member.name + for member in fields(cls) + if member.metadata.get(ViewerWireField.IMAGE_METADATA, False) + ) + + @classmethod + def from_viewer_image_metadata( + cls, values: Mapping[str, object] + ) -> "ImagePayloadMetadata": + """Decode only declaration-owned viewer fields, never hidden source records.""" + if not isinstance(values, Mapping): + raise TypeError("Viewer image metadata requires a mapping.") + extras = set(values) - cls.viewer_field_names() + if extras: + raise ValueError(f"Undeclared viewer metadata fields: {sorted(extras)!r}.") + return cls.from_mapping(values) + + @classmethod + def from_mapping(cls, values: Mapping[str, object]) -> "ImagePayloadMetadata": + """Restore all declared metadata fields through their canonical codecs.""" + decoded = dict(values) + if "source_provenance" in decoded: + decoded["source_provenance"] = SourceImageProvenance.from_mapping( + decoded["source_provenance"] + ) + return dataclass_from_mapping(cls, decoded) + + @classmethod + def for_array( + cls, + array: Any, + *, + source_path: str | None = None, + ) -> "ImagePayloadMetadata": + """Build metadata from an image array's source dtype.""" + import numpy as np + + dtype = np.asarray(array).dtype + return cls( + intensity_scale=image_intensity_scale_for_dtype(dtype), + source_dtype=str(dtype), + source_path=source_path, + ) + + @classmethod + def for_array_payload( + cls, + array: Any, + *, + source_path: str | None = None, + ) -> "ImagePayloadMetadata": + """Build source metadata from an arraybridge-detectable payload.""" + from openhcs.core.memory import MEMORY_TYPE_NUMPY, detect_memory_type + + memory_type = detect_memory_type(array) + dtype = array.dtype + if memory_type == MEMORY_TYPE_NUMPY: + intensity_scale = image_intensity_scale_for_dtype(dtype) + else: + intensity_scale = None + return cls( + intensity_scale=intensity_scale, + source_dtype=str(dtype), + source_path=source_path, + ) + + def __post_init__(self, *source_provenance_values: object) -> None: + self.absorb_explicit_source_provenance(source_provenance_values) + self.normalize_metadata_fields() + + def source_component_metadata_for_projection(self): + return self.source_component_metadata + + def source_path_for_projection(self): + return self.source_path + + def normalize_metadata_fields(self) -> None: + """Use the common sequence with this dataclass's actual nominal hooks.""" + ImageMetadataProjection.normalize_metadata_fields(self) + + @classmethod + def compose( + cls, + payloads: Sequence[RuntimeArrayData], + *, + mode: "ImagePayloadMetadataCompositionMode | None" = None, + source_metadata: Sequence["ImagePayloadMetadata"] | None = None, ) -> "ImagePayloadMetadata": - """Fill missing source-image geometry without changing provenance.""" - spatial_domain = self.source_spatial_domain.with_missing_from( - source.source_spatial_domain - ) - return self.replace_fields( - source_spatial_domain=spatial_domain, - source_voxel_spacing=self.source_voxel_spacing.with_missing_from( - source.source_voxel_spacing - ), - physical_border_edges_yx=( - self.physical_border_edges_yx - if self.physical_border_edges_yx is not None - else source.physical_border_edges_yx - ), - mask_defines_border=( - self.mask_defines_border - if self.mask_defines_border is not None - else source.mask_defines_border - ), + """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, + source_metadata_override=source_metadata, + metadata_type=cls, + ).compose() - def with_source_context_from( - self, - source: "ImagePayloadMetadata", + def for_leading_source_plane(self, plane_index: int) -> "ImagePayloadMetadata": + """Project metadata after explicitly removing its leading plane axis.""" + return LeadingSourcePlaneMetadataProjection(self, plane_index).project() + + def without_leading_plane_axis( + self, *, projection: "ImageMetadataProjection | None" = None ) -> "ImagePayloadMetadata": - """Fill missing source-image identity and spatial context from a source.""" - source_provenance = self.source_provenance.with_missing_from( - source.source_provenance + """Remove an axis through the projection's declared result ownership.""" + self.require_leading_plane_axis( + "Image metadata has no leading plane axis to remove." ) source_channel_axis = self.source_channel_axis - if source_channel_axis is None: - source_channel_axis = source.source_channel_axis - plane_axis = self.plane_axis - if plane_axis is None: - plane_axis = source.plane_axis - elif source.plane_axis is not None and source.plane_axis is not plane_axis: + if source_channel_axis == 0: raise ValueError( - "Cannot combine image metadata with conflicting plane axes: " - f"{plane_axis.value!r} != {source.plane_axis.value!r}." + "Image metadata cannot declare the same leading axis as both " + "plane and channel." ) - return self.with_source_spatial_context_from(source).replace_fields( - source_provenance=source_provenance, - source_channel_axis=source_channel_axis, - plane_axis=plane_axis, - ) - - def with_missing_intensity_from( - self, - source: "ImagePayloadMetadata", - ) -> "ImagePayloadMetadata": - """Fill missing pixel-type and intensity metadata from a source payload.""" - return self.replace_fields( - intensity_scale=( - self.intensity_scale - if self.intensity_scale is not None - else source.intensity_scale - ), - source_dtype=( - self.source_dtype - if self.source_dtype is not None - else source.source_dtype - ), - unit_interval_intensity=( - self.unit_interval_intensity - if self.unit_interval_intensity is not None - else source.unit_interval_intensity - ), - source_plane_intensity_scales=( - self.source_plane_intensity_scales - or source.source_plane_intensity_scales - ), - source_plane_dtypes=( - self.source_plane_dtypes or source.source_plane_dtypes - ), + if source_channel_axis is not None and source_channel_axis > 0: + source_channel_axis -= 1 + if projection is None: + projection = LeadingPlaneAxisMetadataProjection(self) + projected = projection.project_source_provenance( + self, self.source_provenance.with_runtime_planes_as_contributors() ) + projected.plane_axis = None + projected.source_channel_axis = source_channel_axis + projected.source_plane_intensity_scales = () + projected.source_plane_dtypes = () + projected.unit_interval_intensity = self.project_intensity_proof(None) + projected.normalize_metadata_fields() + return projected - def with_source_provenance( - self, - source_provenance: SourceImageProvenance, - ) -> "ImagePayloadMetadata": - """Return metadata with source-image provenance replaced atomically.""" - return self.replace_fields(source_provenance=source_provenance) + def for_source_plane(self, plane_index: int) -> "ImagePayloadMetadata": + """Return metadata for one source plane sliced from a stacked payload.""" + return SourcePlaneImageMetadataProjection(self, plane_index).project() - def with_source_component_metadata( - self, - source_component_metadata: SourceComponentMetadata | None, - ) -> "ImagePayloadMetadata": - """Return metadata with only scalar source component metadata changed.""" - return self.with_source_provenance( - self.source_provenance.with_source_component_metadata( - source_component_metadata - ) - ) - def physical_border_edges_for_shape( - self, - image_shape_yx: Sequence[int], - ) -> tuple[bool, bool, bool, bool]: - """Return which local image edges are true source-image edges. +class ImageMetadataCarrier(ABC): + """Metadata ownership, independent of whether the carrier owns image data.""" - Edge tuple order is ``(top, bottom, left, right)``. Missing spatial - metadata means the current image is treated as the full physical image, - preserving historical behavior for plain arrays. - """ - if self.physical_border_edges_yx is not None: - return tuple(bool(edge) for edge in self.physical_border_edges_yx) - return self.source_spatial_domain.physical_border_edges_for_shape( - image_shape_yx - ) + __slots__ = () - def with_spatial_crop( - self, - *, - input_shape_yx: Sequence[int], - output_shape_yx: Sequence[int], - offset_yx: tuple[int, int], - physical_border_edges_yx: PhysicalBorderEdgesYX = None, - ) -> "ImagePayloadMetadata": - """Return metadata for a crop of this image payload.""" - output_shape = _source_spatial_shape_pair(output_shape_yx, "output_shape_yx") - spatial_domain = self.source_spatial_domain.with_spatial_crop( - input_shape_yx=input_shape_yx, - output_shape_yx=output_shape_yx, - offset_yx=offset_yx, - ) - if physical_border_edges_yx is None: - physical_border_edges_yx = spatial_domain.physical_border_edges_for_shape( - output_shape - ) - return self.replace_fields( - source_spatial_domain=spatial_domain, - physical_border_edges_yx=tuple( - bool(edge) for edge in physical_border_edges_yx - ), - ) + @property + def metadata_projection(self) -> ImageMetadataProjection | None: + return self._metadata_projection - def with_spatial_resize( - self, - output_shape_yx: Sequence[int], - ) -> "ImagePayloadMetadata": - """Return metadata in the local coordinate domain created by a resize.""" + @property + def metadata(self) -> ImagePayloadMetadata | None: + """Expose the stable mutable namespace owned by this carrier.""" + owner = self.metadata_projection + return None if owner is None else owner.materialize_metadata() - return self.replace_fields( - source_spatial_domain=self.source_spatial_domain.with_spatial_resize( - output_shape_yx - ), - physical_border_edges_yx=(True, True, True, True), - mask_defines_border=None, - ) + @metadata.setter + def metadata(self, value: ImageMetadataProjection | None) -> None: + object.__setattr__(self, "_metadata_projection", value) - def with_materialized_source_domain( - self, - target_domain: SourceSpatialDomain, - ) -> "ImagePayloadMetadata": - """Return metadata after pixels are expanded to source-image XY.""" - return self.replace_fields( - source_spatial_domain=( - self.source_spatial_domain.as_materialized_source_domain(target_domain) - ), - physical_border_edges_yx=(True, True, True, True), - ) +class ImagePayloadMetadataCarrier(ImageMetadataCarrier): + """Image data and metadata remain separate nominal capabilities.""" -class ImagePayloadMetadataCarrier(ABC): - """Nominal contract for image payloads that carry runtime metadata.""" + __slots__ = () @abstractmethod def image_data(self) -> Any: """Return concrete pixels in the payload's declared image domain.""" - @property - @abstractmethod - def metadata(self) -> ImagePayloadMetadata: - """Return the image metadata attached to this payload.""" - -@dataclass(frozen=True, slots=True) +@dataclass(frozen=True) class ImageMetadataPayload(DataBackedRuntimeArrayPayload, ImagePayloadMetadataCarrier): """Image data plus metadata, without requiring a validity mask.""" @@ -1146,13 +1492,17 @@ def __post_init__(self) -> None: "ImageMetadataPayload.data requires array-like image data, " f"got {type(self.data).__name__}." ) - if not self.metadata.has_values: + if not self.metadata_projection.has_values: raise ValueError("ImageMetadataPayload.metadata cannot be empty.") def image_data(self) -> Any: """Return the concrete image pixels carried by this payload.""" return self.data + @classmethod + def from_projection(cls, data, projection): + return cls(data=data, metadata=projection) + def with_data( self, data: Any, @@ -1162,11 +1512,11 @@ def with_data( """Return the same metadata attached to replacement data.""" return type(self)( data=data, - metadata=self.metadata if metadata is None else metadata, + metadata=self.metadata_projection if metadata is None else metadata, ) -@dataclass(frozen=True, slots=True) +@dataclass(frozen=True) class MaskedImagePayload(DataBackedRuntimeArrayPayload, ImagePayloadMetadataCarrier): """Image data plus an authoritative per-pixel validity mask.""" @@ -1194,7 +1544,7 @@ def __post_init__(self) -> None: f"got {type(self.mask).__name__}." ) data_shape = data_geometry.shape - if not self.metadata.mask_domain(self.data).accepts(mask_shape): + if not self.metadata_projection.mask_domain(self.data).accepts(mask_shape): raise ValueError( "MaskedImagePayload.mask shape must match the image spatial " f"domain; got mask {mask_shape!r} for image {data_shape!r}." @@ -1204,15 +1554,35 @@ def image_data(self) -> Any: """Return the concrete image pixels carried by this payload.""" return self.data + @classmethod + def from_projection(cls, data, mask, projection): + return cls(data=data, mask=mask, metadata=projection) + def with_data(self, data: Any, mask: Any | None = None) -> "MaskedImagePayload": """Return the same semantic image mask attached to replacement data.""" return type(self)( data=data, mask=self.mask if mask is None else mask, - metadata=self.metadata, + metadata=self.metadata_projection, ) +# The dataclass declaration continues to own public reflection/JSON fields. +# Install its shared concrete descriptor after field/default generation, then +# recompute abstractness rather than leaving an inherited abstract property. +ImageMetadataPayload.metadata = ImagePayloadMetadataCarrier.metadata +MaskedImagePayload.metadata = ImagePayloadMetadataCarrier.metadata +update_abstractmethods(ImageMetadataPayload) +update_abstractmethods(MaskedImagePayload) + + +def image_payload_metadata_projection(payload: Any) -> ImageMetadataProjection: + """Read the metadata owner without forcing its public mutable namespace.""" + if isinstance(payload, ImagePayloadMetadataCarrier): + return payload.metadata_projection + return ImagePayloadMetadata() + + def image_payload_data(payload: Any) -> Any: """Return concrete image pixels from a runtime image payload.""" if isinstance(payload, ImagePayloadMetadataCarrier): @@ -1253,17 +1623,14 @@ def preserved_image_plane_projection( source_aliases: tuple[str, ...] = (), ) -> RuntimePlaneAxisValueProjection | None: """Resolve the declared leading image axis against one runtime invocation.""" - from openhcs.core.runtime_slice_projection import RuntimeSliceProjection - metadata = image_payload_metadata(payload) - if metadata.plane_axis is None: + metadata = image_payload_metadata_projection(payload) + if metadata.read_value("plane_axis") is None: return RuntimeSliceProjection.preserved_context_for_value(payload) - if metadata.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE: + if metadata.read_value("plane_axis") is RuntimePlaneAxis.RUNTIME_SLICE: invocation_projection = RuntimePlaneAxisValueProjection.from_projector( - projector, - metadata.plane_axis, - source_aliases, + projector, metadata.read_value("plane_axis"), source_aliases ) if ( invocation_projection is not None @@ -1273,27 +1640,22 @@ def preserved_image_plane_projection( payload_projection = RuntimeSliceProjection.preserved_context_for_value(payload) if payload_projection is None: raise ValueError( - "Runtime-slice image metadata requires a payload with the same " - "nominal leading axis." + "Runtime-slice image metadata requires a payload with the same nominal leading axis." ) return payload_projection - shape = image_payload_geometry( - payload, - value_name="Source-binding image payload", + payload, value_name="Source-binding image payload" ).shape if len(shape) < 3: raise ValueError( - "Source-binding image metadata requires its declared plane axis as " - f"the leading dimension, got shape {shape!r}." + f"Source-binding image metadata requires its declared plane axis as the leading dimension, got shape {shape!r}." ) axis_size = shape[0] return RuntimePlaneAxisValueProjection( axis=RuntimePlaneAxis.SOURCE_BINDING, source_aliases=source_aliases, - plane_index=metadata.source_provenance.source_alias_plane_index( - source_aliases, - axis_size, + plane_index=metadata.read_value("source_provenance").source_alias_plane_index( + source_aliases, axis_size ), axis_size=axis_size, ) @@ -1306,22 +1668,20 @@ def preserve_declared_image_payload_axis( source_payload: Any | None = None, ) -> RuntimePlaneAxisValueProjection | None: """Preserve the exact image axis declared by output or source ownership.""" - from openhcs.core.runtime_slice_projection import RuntimeSliceProjection - output_metadata = image_payload_metadata(output_payload) + output_metadata = image_payload_metadata_projection(output_payload) output_projection = RuntimeSliceProjection.preserved_context_for_value( output_payload ) - output_axis = output_metadata.plane_axis or ( + output_axis = output_metadata.read_value("plane_axis") or ( None if output_projection is None else output_projection.axis ) - - source_metadata = image_payload_metadata(source_payload) + source_metadata = image_payload_metadata_projection(source_payload) source_projection = RuntimeSliceProjection.preserved_context_for_value( source_payload ) - source_axis = source_metadata.plane_axis or ( + source_axis = source_metadata.read_value("plane_axis") or ( None if source_projection is None else source_projection.axis ) if output_axis is not None: @@ -1330,38 +1690,30 @@ def preserve_declared_image_payload_axis( owner = source_payload else: return None - return preserved_image_plane_projection( - owner, - projector, - ) + return preserved_image_plane_projection(owner, projector) def project_image_mask_to_data_domain( - mask: Any, - data: Any, - *, - metadata: ImagePayloadMetadata | None = None, + mask: Any, data: Any, *, metadata: ImagePayloadMetadata | None = None ) -> Any | None: """Validate a mask against explicit image-domain metadata.""" if mask is None: return None mask_array = np.asarray(mask, dtype=bool) mask_shape = tuple(mask_array.shape) - resolved_metadata = image_payload_metadata(data) if metadata is None else metadata + resolved_metadata = ( + image_payload_metadata_projection(data) if metadata is None else metadata + ) mask_domain = resolved_metadata.mask_domain(data) if mask_domain.accepts(mask_shape): return mask_array raise ValueError( - f"Mask shape {mask_shape!r} does not match declared image mask domain " - f"{tuple(sorted(mask_domain.valid_shapes()))!r}." + f"Mask shape {mask_shape!r} does not match declared image mask domain {tuple(sorted(mask_domain.valid_shapes()))!r}." ) def image_mask_for_data_domain( - *, - source_payload: Any, - data: Any, - explicit_mask: Any | None = None, + *, source_payload: Any, data: Any, explicit_mask: Any | None = None ) -> Any | None: """Return the effective image mask projected into a concrete data domain.""" source_mask = ( @@ -1369,11 +1721,9 @@ def image_mask_for_data_domain( if explicit_mask is None else image_payload_data(explicit_mask) ) - metadata = image_payload_metadata(source_payload) + metadata = image_payload_metadata_projection(source_payload) projected_mask = project_image_mask_to_data_domain( - source_mask, - data, - metadata=metadata, + source_mask, data, metadata=metadata ) if projected_mask is None: return None @@ -1391,88 +1741,100 @@ def with_image_payload_data( resolved_mask = project_image_mask_to_data_domain( image_payload_mask(payload) if mask is None else mask, data, - metadata=image_payload_metadata(payload) if metadata is None else metadata, + metadata=( + image_payload_metadata_projection(payload) if metadata is None else metadata + ), ) resolved_metadata = ( - image_payload_metadata(payload) if metadata is None else metadata + image_payload_metadata_projection(payload) if metadata is None else metadata ) return resolved_metadata.payload_with(data, resolved_mask) -@dataclass(frozen=True, slots=True) -class ImageMetadataProjection(ABC): - """Construct one metadata result from nominal source and axis transformations.""" - - metadata: ImagePayloadMetadata - - def project(self) -> ImagePayloadMetadata: - return self.project_axis_metadata(self.project_source_metadata()) - - @abstractmethod - def project_source_metadata(self) -> ImagePayloadMetadata: - """Select source metadata with this projection's declared ownership.""" - - def project_axis_metadata( - self, metadata: ImagePayloadMetadata - ) -> ImagePayloadMetadata: - """Retain declared axes unless a nominal axis capability transforms them.""" - return metadata - - @abstractmethod - def project_source_provenance( - self, metadata: ImagePayloadMetadata, provenance: SourceImageProvenance - ) -> ImagePayloadMetadata: - """Apply provenance using the result ownership established by source selection.""" - - -@dataclass(frozen=True, slots=True) +@dataclass(frozen=True, slots=True, init=False) class SourcePlaneImageMetadataProjection(ImageMetadataProjection): - """Select scalar source-image facts without removing an array axis.""" + """Select one scalar source without forcing a public metadata namespace.""" + metadata: ImageMetadataProjection plane_index: int - def project_source_metadata(self) -> ImagePayloadMetadata: - provenance = self.metadata.source_provenance.for_source_plane(self.plane_index) - return self.metadata.replace_fields( - intensity_scale=self.metadata.intensity_scale_for_source_plane( - self.plane_index - ), + def __init__(self, metadata, plane_index): + ImageMetadataProjection.__init__(self, metadata) + object.__setattr__(self, "metadata", metadata) + object.__setattr__(self, "plane_index", plane_index) + + def source_changes(self): + source = self.metadata + return dict( + intensity_scale=source.intensity_scale_for_source_plane(self.plane_index), source_dtype=_tuple_value( - self.metadata.source_plane_dtypes, self.plane_index + source.read_value("source_plane_dtypes"), self.plane_index ) - or self.metadata.source_dtype, - source_provenance=provenance, - unit_interval_intensity=self.metadata.project_intensity_proof( + or source.read_value("source_dtype"), + source_provenance=source.read_value("source_provenance").for_source_plane( self.plane_index ), + unit_interval_intensity=source.project_intensity_proof(self.plane_index), source_plane_intensity_scales=(), source_plane_dtypes=(), ) - def project_source_provenance( - self, metadata: ImagePayloadMetadata, provenance: SourceImageProvenance - ) -> ImagePayloadMetadata: - """Normalize the independently owned result created by source selection.""" + def project_source_values(self, values): + values.update(self.source_changes()) + return self._normalize_values(self.metadata_type, values) + + def project_source_metadata(self): + return self.metadata.materialize_metadata().replace_fields( + **self.source_changes() + ) + + def project_source_provenance(self, metadata, provenance): metadata.source_provenance = provenance metadata.normalize_metadata_fields() return metadata + def __reduce__(self): + return (type(self), (self.metadata, self.plane_index)) -class LeadingPlaneAxisMetadataProjection(ImageMetadataProjection): - """Remove a declared leading axis while preserving its source contributors.""" - def project_source_metadata(self) -> ImagePayloadMetadata: - return self.metadata +class LeadingPlaneAxisMetadataProjection(ImageMetadataProjection): + """Remove the leading axis after its source capability establishes ownership.""" + + def project_source_values(self, values): + # Axis guards and contributor effects precede source normalization for the + # standalone axis projection, unlike scalar-source selection. + return values + + def project_axis_values(self, values): + if values["plane_axis"] is None: + raise ValueError("Image metadata has no leading plane axis to remove.") + channel_axis = values["source_channel_axis"] + if channel_axis == 0: + raise ValueError( + "Image metadata cannot declare the same leading axis as both plane and channel." + ) + if channel_axis is not None and channel_axis > 0: + channel_axis -= 1 + proof = values["unit_interval_intensity"] + values["source_provenance"] = values[ + "source_provenance" + ].with_runtime_planes_as_contributors() + values = self._normalize_values(self.metadata_type, values) + values.update( + plane_axis=None, + source_channel_axis=channel_axis, + source_plane_intensity_scales=(), + source_plane_dtypes=(), + unit_interval_intensity=( + None if proof is None else proof.without_source_planes() + ), + ) + return self._normalize_values(self.metadata_type, values) - def project_axis_metadata( - self, metadata: ImagePayloadMetadata - ) -> ImagePayloadMetadata: + def project_axis_metadata(self, metadata): return metadata.without_leading_plane_axis(projection=self) - def project_source_provenance( - self, metadata: ImagePayloadMetadata, provenance: SourceImageProvenance - ) -> ImagePayloadMetadata: - """Create the standalone result after its axis guards and source derivation.""" + def project_source_provenance(self, metadata, provenance): return metadata.replace_fields(source_provenance=provenance) @@ -1480,9 +1842,15 @@ class LeadingSourcePlaneMetadataProjection( SourcePlaneImageMetadataProjection, LeadingPlaneAxisMetadataProjection, ): - """Remove the leading axis of independently owned selected-source metadata.""" + """Compose independent scalar-source and leading-axis capabilities.""" + + def project_source_values(self, values): + self.metadata.require_leading_plane_axis( + "Leading source-plane projection requires a declared plane axis." + ) + return super().project_source_values(values) - def project_source_metadata(self) -> ImagePayloadMetadata: + def project_source_metadata(self): self.metadata.require_leading_plane_axis( "Leading source-plane projection requires a declared plane axis." ) @@ -1497,37 +1865,36 @@ class ImagePayloadSliceProjector: metadata: ImagePayloadMetadata def payloads_for_slices( - self, - slices: Sequence[RuntimeArrayData], + self, slices: Sequence[RuntimeArrayData] ) -> list[RuntimeArrayData]: """Project each child once, keeping strict batch-mask cardinality.""" - if self.metadata.plane_axis is None: + if self.metadata.read_value("plane_axis") is None: if len(slices) != 1: raise ValueError( - "Image payload produced multiple slices without a declared " - "plane axis." + "Image payload produced multiple slices without a declared plane axis." ) return [self.metadata.payload_with(slices[0], self.mask)] metadata = self.metadata.with_indexed_source_plane_provenance(len(slices)) masks = self._masks_for_slices(slices) if self.mask is not None else None payloads: list[RuntimeArrayData] = [] for index, slice_data in enumerate(slices): - slice_metadata = metadata.for_leading_source_plane(index) + slice_metadata = LeadingSourcePlaneMetadataProjection( + metadata, index + ).capture() mask = None if masks is None else masks[index] - if mask is not None and not slice_metadata.mask_domain(slice_data).accepts( - tuple(np.shape(mask)) + if mask is not None and ( + not slice_metadata.mask_domain(slice_data).accepts( + tuple(np.shape(mask)) + ) ): raise ValueError( - "Image payload mask shape must match the selected slice " - f"domain; got {tuple(np.shape(mask))!r} for " - f"{tuple(np.shape(slice_data))!r}." + f"Image payload mask shape must match the selected slice domain; got {tuple(np.shape(mask))!r} for {tuple(np.shape(slice_data))!r}." ) payloads.append(slice_metadata.payload_with(slice_data, mask)) return payloads def _masks_for_slices( - self, - slices: Sequence[RuntimeArrayData], + self, slices: Sequence[RuntimeArrayData] ) -> tuple[RuntimeArrayData, ...]: """Select batch masks only after checking exact leading cardinality.""" if self.mask is None: @@ -1535,30 +1902,25 @@ def _masks_for_slices( mask_array = np.asarray(self.mask, dtype=bool) if mask_array.ndim == 0 or mask_array.shape[0] != len(slices): raise ValueError( - "Image payload mask cardinality must exactly match the declared " - f"plane axis: {mask_array.shape!r} for {len(slices)} slice(s)." + f"Image payload mask cardinality must exactly match the declared plane axis: {mask_array.shape!r} for {len(slices)} slice(s)." ) - return tuple(mask_array[index] for index in range(len(slices))) + return tuple((mask_array[index] for index in range(len(slices)))) def payload_for_slice( - self, - data_slice: RuntimeArrayData, - index: int, + self, data_slice: RuntimeArrayData, index: int ) -> RuntimeArrayData: """Project one metadata snapshot for both child pixels and mask.""" - metadata = self.metadata.for_leading_source_plane(index) + metadata = LeadingSourcePlaneMetadataProjection(self.metadata, index).capture() mask = self._mask_for_projected_slice(data_slice, index, metadata) return metadata.payload_with(data_slice, mask) def mask_for_slice( - self, - data_slice: RuntimeArrayData, - index: int, + self, data_slice: RuntimeArrayData, index: int ) -> RuntimeArrayData | None: """Project a standalone mask through the same scalar slice policy.""" if self.mask is None: return None - metadata = self.metadata.for_leading_source_plane(index) + metadata = LeadingSourcePlaneMetadataProjection(self.metadata, index).capture() return self._mask_for_projected_slice(data_slice, index, metadata) def _mask_for_projected_slice( @@ -1570,16 +1932,18 @@ def _mask_for_projected_slice( if self.mask is None: return None mask_array = np.asarray(self.mask) - if ( - self.metadata.plane_axis is RuntimePlaneAxis.SOURCE_BINDING - and slice_metadata.mask_domain(data_slice).accepts(mask_array.shape) + if self.metadata.read_value( + "plane_axis" + ) is RuntimePlaneAxis.SOURCE_BINDING and slice_metadata.mask_domain( + data_slice + ).accepts( + mask_array.shape ): candidate = mask_array else: if mask_array.ndim == 0 or plane_index >= mask_array.shape[0]: raise ValueError( - "Image payload mask does not carry the requested declared " - f"slice index {plane_index}; got shape {mask_array.shape!r}." + f"Image payload mask does not carry the requested declared slice index {plane_index}; got shape {mask_array.shape!r}." ) candidate = mask_array[plane_index] if slice_metadata.mask_domain(data_slice).accepts( @@ -1587,9 +1951,7 @@ def _mask_for_projected_slice( ): return candidate raise ValueError( - "Image payload mask cannot be projected into slice domain; " - f"got mask {mask_array.shape!r} for slice " - f"{image_payload_geometry(data_slice).shape!r}." + f"Image payload mask cannot be projected into slice domain; got mask {mask_array.shape!r} for slice {image_payload_geometry(data_slice).shape!r}." ) @@ -1601,21 +1963,20 @@ def image_payload_slice_context( plane_axis: RuntimePlaneAxis | None = None, ) -> Any: """Attach one source plane of a payload's image context to slice data.""" - metadata = image_payload_metadata(payload) + metadata = image_payload_metadata_projection(payload) 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: + plane_axis = RuntimePlaneAxis(plane_axis) + if ( + metadata.read_value("plane_axis") is not None + and metadata.read_value("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"Image slice projection axis conflicts with payload metadata: {plane_axis.value!r} != {metadata.read_value('plane_axis').value!r}." ) - if metadata.plane_axis is not plane_axis: + if metadata.read_value("plane_axis") is not plane_axis: metadata = metadata.replace_fields(plane_axis=plane_axis) return ImagePayloadSliceProjector( - mask=image_payload_mask(payload), - metadata=metadata, + mask=image_payload_mask(payload), metadata=metadata ).payload_for_slice(data, plane_index) @@ -1701,7 +2062,12 @@ def __post_init__(self) -> None: @property def source_metadata(self) -> tuple[ImagePayloadMetadata, ...]: if self.source_metadata_override is None: - return tuple(image_payload_metadata(payload) for payload in self.payloads) + return tuple( + ( + image_payload_metadata_projection(payload) + for payload in self.payloads + ) + ) return tuple(self.source_metadata_override) @staticmethod @@ -1709,55 +2075,71 @@ 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.read_value("plane_axis") is not None: return metadata - if metadata.source_provenance.source_plane_count == 1: - return metadata.for_source_plane(0) + if metadata.read_value("source_provenance").source_plane_count == 1: + return SourcePlaneImageMetadataProjection(metadata, 0).capture() return metadata def compose(self) -> ImagePayloadMetadata: metadata_by_payload = self.source_metadata - if not any(metadata.has_values for metadata in metadata_by_payload): + if not any((metadata.has_values for metadata in metadata_by_payload)): return self.metadata_type(plane_axis=self.mode.plane_axis) source_metadata_by_payload = tuple( - self.source_plane_metadata_for_payload(metadata) - for metadata in metadata_by_payload + ( + self.source_plane_metadata_for_payload(metadata) + for metadata in metadata_by_payload + ) ) source_plane_metadata_records = source_metadata_by_payload composed_source_provenance_planes = self.composed_source_provenance_planes( source_plane_metadata_records ) common_source_voxel_spacing = self.common_metadata_value( - metadata.source_voxel_spacing - for metadata in metadata_by_payload - if metadata.source_voxel_spacing.has_values + ( + metadata.read_value("source_voxel_spacing") + for metadata in metadata_by_payload + if metadata.read_value("source_voxel_spacing").has_values + ) ) if common_source_voxel_spacing is None: common_source_voxel_spacing = SourceVoxelSpacing() source_component_metadata_by_payload = tuple( ( - metadata - if metadata.source_component_metadata is not None - else source_metadata - ) - for metadata, source_metadata in zip( - metadata_by_payload, - source_metadata_by_payload, - strict=True, + ( + metadata + if metadata.read_value( + "source_provenance" + ).source_component_metadata + is not None + else source_metadata + ) + for metadata, source_metadata in zip( + metadata_by_payload, source_metadata_by_payload, strict=True + ) ) ) return self.metadata_type( source_path=self.common_metadata_value( - metadata.source_path for metadata in source_metadata_by_payload + ( + metadata.read_value("source_provenance").source_path + for metadata in source_metadata_by_payload + ) ), source_component_metadata=self.common_source_component_metadata( source_component_metadata_by_payload ), source_plane_intensity_scales=tuple( - metadata.intensity_scale for metadata in source_plane_metadata_records + ( + metadata.read_value("intensity_scale") + for metadata in source_plane_metadata_records + ) ), source_plane_dtypes=tuple( - metadata.source_dtype for metadata in source_plane_metadata_records + ( + metadata.read_value("source_dtype") + for metadata in source_plane_metadata_records + ) ), source_image_provenance_planes=composed_source_provenance_planes, unit_interval_intensity=self.composed_unit_interval_intensity( @@ -1765,43 +2147,55 @@ def compose(self) -> ImagePayloadMetadata: ), source_spatial_domain=SourceSpatialDomain( origin_yx=self.common_metadata_value( - metadata.spatial_origin_yx for metadata in metadata_by_payload + ( + metadata.read_value("source_spatial_domain").origin_yx + for metadata in metadata_by_payload + ) ), source_shape_yx=self.common_metadata_value( - metadata.source_spatial_shape_yx for metadata in metadata_by_payload + ( + metadata.read_value("source_spatial_domain").source_shape_yx + for metadata in metadata_by_payload + ) ), ), source_voxel_spacing=common_source_voxel_spacing, physical_border_edges_yx=self.common_metadata_value( - metadata.physical_border_edges_yx for metadata in metadata_by_payload + ( + metadata.read_value("physical_border_edges_yx") + for metadata in metadata_by_payload + ) ), mask_defines_border=self.common_metadata_value( - metadata.mask_defines_border for metadata in metadata_by_payload - ), - source_image_names=( - composed_source_provenance_planes.runtime_source_image_names + ( + metadata.read_value("mask_defines_border") + for metadata in metadata_by_payload + ) ), + source_image_names=composed_source_provenance_planes.runtime_source_image_names, source_channel_axis=self.composed_source_channel_axis(metadata_by_payload), plane_axis=self.mode.plane_axis, ) def composed_source_provenance_planes( - self, - metadata_records: tuple[ImagePayloadMetadata, ...], + self, metadata_records: tuple[ImagePayloadMetadata, ...] ) -> SourceImageProvenancePlanes: """Compose projectable planes without replacing compatible topology.""" - if len(metadata_records) == 1: metadata = metadata_records[0] - provenance_planes = metadata.source_image_provenance_planes + provenance_planes = metadata.read_value( + "source_provenance" + ).source_image_provenance_planes if provenance_planes.count > 1 and self.mode.preserves_plane_topology( - metadata.plane_axis + metadata.read_value("plane_axis") ): return provenance_planes return SourceImageProvenancePlanes( tuple( - self.runtime_source_provenance_plane(metadata) - for metadata in metadata_records + ( + self.runtime_source_provenance_plane(metadata) + for metadata in metadata_records + ) ) ) @@ -1810,15 +2204,19 @@ def composed_unit_interval_intensity( metadata_records: Sequence[ImagePayloadMetadata], ) -> ImageUnitIntervalIntensityMetadata | None: """Compose only unit-interval proof state authored by an input payload.""" - if not any( - metadata.unit_interval_intensity is not None - for metadata in metadata_records + ( + metadata.read_value("unit_interval_intensity") is not None + for metadata in metadata_records + ) ): return None return ImageUnitIntervalIntensityMetadata( source_plane_scales=tuple( - metadata.unit_interval_intensity_scale for metadata in metadata_records + ( + metadata.unit_interval_intensity_scale + for metadata in metadata_records + ) ) ) @@ -1827,37 +2225,40 @@ def runtime_source_provenance_plane( metadata: ImagePayloadMetadata, ) -> RuntimeSourceImageProvenancePlane: """Return one projectable plane with nested non-projectable contributors.""" - source_image_names = metadata.source_image_names - contributors = metadata.source_image_provenance_planes.as_contributors( - source_image_names - ).planes + source_image_names = metadata.read_value("source_provenance").source_image_names + contributors = ( + metadata.read_value("source_provenance") + .source_image_provenance_planes.as_contributors(source_image_names) + .planes + ) if len(source_image_names) > 1: raise ValueError( - "Composed image payload provenance permits at most one " - f"source alias per scalar plane, got {source_image_names!r}." + f"Composed image payload provenance permits at most one source alias per scalar plane, got {source_image_names!r}." ) source_image_name = source_image_names[0] if source_image_names else None return RuntimeSourceImageProvenancePlane( SourceImageIdentity( - metadata.source_path, - metadata.source_component_metadata, + metadata.read_value("source_provenance").source_path, + metadata.read_value("source_provenance").source_component_metadata, ), contributors, source_image_name, ) def common_source_component_metadata( - self, - values: Iterable[ImagePayloadMetadata], + self, values: Iterable[ImagePayloadMetadata] ) -> SourceComponentMetadata | None: """Return source metadata shared by the composed payload.""" metadata_values = tuple(values) metadata_by_plane = tuple( - SourceMetadataFields.composition_snapshot( - metadata.source_component_metadata + ( + SourceMetadataFields.composition_snapshot( + metadata.read_value("source_provenance").source_component_metadata + ) + for metadata in metadata_values + if metadata.read_value("source_provenance").source_component_metadata + is not None ) - for metadata in metadata_values - if metadata.source_component_metadata is not None ) if self.mode is ImagePayloadMetadataCompositionMode.BUNDLE: field_names = set().union( @@ -1866,20 +2267,26 @@ def common_source_component_metadata( common_metadata: dict[str, SourceMetadataValue] = {} for field_name in field_names: values_for_field = tuple( - metadata[field_name] - for metadata in metadata_by_plane - if field_name in metadata + ( + metadata[field_name] + for metadata in metadata_by_plane + if field_name in metadata + ) ) if values_for_field and all( - value == values_for_field[0] for value in values_for_field + (value == values_for_field[0] for value in values_for_field) ): common_metadata[field_name] = values_for_field[0] else: common_metadata = dict( common_source_component_metadata( tuple( - metadata.source_component_metadata - for metadata in metadata_values + ( + metadata.read_value( + "source_provenance" + ).source_component_metadata + for metadata in metadata_values + ) ) ) or {} @@ -1890,8 +2297,10 @@ def common_source_component_metadata( metadata_by_plane[0] if metadata_by_plane and all( - isinstance(metadata, OwnedSourceMetadataFields) - for metadata in metadata_by_plane + ( + isinstance(metadata, OwnedSourceMetadataFields) + for metadata in metadata_by_plane + ) ) else {} ) @@ -1907,35 +2316,33 @@ def common_metadata_value( values: Iterable[MetadataValueT | None], ) -> MetadataValueT | None: values_tuple = tuple(values) - present = tuple(value for value in values_tuple if value is not None) + present = tuple((value for value in values_tuple if value is not None)) if not present: return None first = present[0] - if all(value == first for value in present): + if all((value == first for value in present)): return first return None def composed_source_channel_axis( - self, - metadata_by_payload: tuple[ImagePayloadMetadata, ...], + self, metadata_by_payload: tuple[ImagePayloadMetadata, ...] ) -> int | None: """Return the channel axis after this composition adds a leading axis.""" normalized = tuple( - metadata.normalized_source_channel_axis(payload) - for payload, metadata in zip( - self.payloads, - metadata_by_payload, - strict=True, + ( + metadata.normalized_source_channel_axis(payload) + for payload, metadata in zip( + self.payloads, metadata_by_payload, strict=True + ) ) ) - present = tuple(value for value in normalized if value is not None) + present = tuple((value for value in normalized if value is not None)) if not present: return None first = present[0] - if any(value != first for value in present[1:]): + if any((value != first for value in present[1:])): raise ValueError( - "Cannot compose image payloads with conflicting source channel axes: " - f"{present!r}." + f"Cannot compose image payloads with conflicting source channel axes: {present!r}." ) return first + 1 @@ -1951,16 +2358,14 @@ def image_intensity_scale_for_dtype(dtype: Any) -> float | None: def image_payload_intensity_scale( - payload: Any, - *, - channel_index: int = 0, + payload: Any, *, channel_index: int = 0 ) -> float | None: """Return the best semantic intensity scale for an image payload.""" import numpy as np - metadata_scale = image_payload_metadata(payload).intensity_scale_for_source_plane( - channel_index - ) + metadata_scale = image_payload_metadata_projection( + payload + ).intensity_scale_for_source_plane(channel_index) if metadata_scale is not None and metadata_scale > 0: return float(metadata_scale) data = image_payload_data(payload) @@ -1971,10 +2376,7 @@ def image_payload_intensity_scale( def normalize_image_payload_intensity( - payload: Any, - *, - dtype: Any = None, - channel_index: int = 0, + payload: Any, *, dtype: Any = None, channel_index: int = 0 ) -> Any: """Normalize image pixels by payload metadata while preserving context.""" import numpy as np @@ -1985,14 +2387,13 @@ def normalize_image_payload_intensity( normalized = array.astype(target_dtype) elif np.issubdtype(array.dtype, np.integer): intensity_scale = image_payload_intensity_scale( - payload, - channel_index=channel_index, + payload, channel_index=channel_index ) if intensity_scale is None or intensity_scale <= 1: normalized = array.astype(target_dtype) else: normalized = array.astype(target_dtype) / float(intensity_scale) - metadata = image_payload_metadata( + metadata = image_payload_metadata_projection( payload ).with_unit_interval_intensity_scale(int(intensity_scale)) return with_image_payload_data(payload, normalized, metadata=metadata) diff --git a/openhcs/core/runtime_object_labels.py b/openhcs/core/runtime_object_labels.py index 396e677ae6..a3957fec5d 100644 --- a/openhcs/core/runtime_object_labels.py +++ b/openhcs/core/runtime_object_labels.py @@ -351,6 +351,14 @@ def unedited_labels(self) -> ObjectLabelData | None: def small_removed_labels(self) -> ObjectLabelData | None: return self.variant_data.small_removed_labels + @property + def metadata_projection(self) -> runtime_image_values.ImageMetadataProjection: + """Expose one computed metadata namespace without replaying its constructor.""" + + return runtime_image_values.ImageMetadataProjection._from_realized_metadata( + self.metadata + ) + @property def metadata(self) -> runtime_image_values.ImagePayloadMetadata: """Return image-domain metadata carried by this object-label value.""" diff --git a/openhcs/core/runtime_slice_projection.py b/openhcs/core/runtime_slice_projection.py index 283b9a9e84..28218e407a 100644 --- a/openhcs/core/runtime_slice_projection.py +++ b/openhcs/core/runtime_slice_projection.py @@ -26,12 +26,15 @@ MeasurementTableUnion, ) from openhcs.core.runtime_image_values import ( + ImageMetadataCarrier, + ImageMetadataProjection, ImageMetadataPayload, ImagePayloadMetadata, MaskedImagePayload, image_payload_data, image_payload_geometry, image_payload_metadata, + image_payload_metadata_projection, image_payload_slice_context, ) from openhcs.core.runtime_measurements import ( @@ -322,7 +325,7 @@ def validate_stack_value( @dataclass(frozen=True, slots=True) -class RuntimeProjectedPayloadItem: +class RuntimeProjectedPayloadItem(ImageMetadataCarrier): """One runtime payload projected into an execution-addressable value.""" value: RuntimeProjectionData @@ -334,12 +337,12 @@ def data(self) -> RuntimeProjectionData: return image_payload_data(self.value) @property - def metadata(self) -> ImagePayloadMetadata: - return image_payload_metadata(self.value) + def metadata_projection(self) -> ImageMetadataProjection: + return image_payload_metadata_projection(self.value) @property def source_component_metadata(self) -> SourceComponentMetadata | None: - return self.metadata.source_component_metadata + return self.metadata_projection.source_component_metadata_for_projection() def require_source_component_metadata(self) -> SourceComponentMetadata: metadata = self.source_component_metadata @@ -359,8 +362,9 @@ def validate_source_plane_component_metadata( ) -> None: """Require one source component-metadata record per projected slice.""" plane_metadata = ( - image_payload_metadata(value) + image_payload_metadata_projection(value) .with_indexed_source_plane_provenance(slice_count) + .read_value("source_provenance") .source_image_provenance_planes.component_metadata ) if not plane_metadata or any(item is None for item in plane_metadata): @@ -470,8 +474,8 @@ def slice_count_for_value( self, value: RuntimeProjectionData, ) -> int | None: - metadata = image_payload_metadata(value) - if metadata.plane_axis is not RuntimePlaneAxis.RUNTIME_SLICE: + metadata = image_payload_metadata_projection(value) + if metadata.read_value("plane_axis") is not RuntimePlaneAxis.RUNTIME_SLICE: return None data_geometry = image_payload_geometry( value, @@ -489,7 +493,7 @@ def value_for_slice( value: RuntimeProjectionData, context: RuntimePlaneAxisValueProjection, ) -> RuntimeProjectionData: - plane_axis = image_payload_metadata(value).plane_axis + plane_axis = image_payload_metadata_projection(value).read_value("plane_axis") if plane_axis is not context.axis: return value data = image_payload_data(value) diff --git a/openhcs/core/source_image_semantics.py b/openhcs/core/source_image_semantics.py index b12f30692f..639a3e9817 100644 --- a/openhcs/core/source_image_semantics.py +++ b/openhcs/core/source_image_semantics.py @@ -2,15 +2,15 @@ from __future__ import annotations -from dataclasses import replace import numpy as np from openhcs.core.runtime_image_values import ( ImageMetadataPayload, + ImageMetadataProjection, ImagePayloadMetadata, MaskedImagePayload, image_payload_data, image_payload_mask, - image_payload_metadata, + image_payload_metadata_projection, normalize_image_payload_intensity, ) from openhcs.core.runtime_image_loading import ImagePayloadSourceMetadataContext @@ -31,7 +31,7 @@ def _rgb_to_monochrome(data: np.ndarray) -> np.ndarray: def _monochrome_source_data( data: RuntimeArrayData, channel_axis: int | None, - metadata: ImagePayloadMetadata, + metadata: ImageMetadataProjection, ) -> RuntimeArrayData: if channel_axis is None: return data @@ -51,8 +51,8 @@ def _loaded_source_payload_metadata( payload: RuntimeArrayData, binding: NamedSourceBinding, source_context: ImagePayloadSourceMetadataContext | None, -) -> ImagePayloadMetadata: - existing = image_payload_metadata(payload) +) -> ImageMetadataProjection: + existing = image_payload_metadata_projection(payload) if source_context is None: metadata = ( existing @@ -60,17 +60,17 @@ def _loaded_source_payload_metadata( else ImagePayloadMetadata.for_array_payload(payload) ) else: - metadata = source_context.metadata(payload, source_binding=binding) - metadata = metadata.replace_fields( - source_provenance=metadata.source_provenance.with_source_image_names( - (binding.alias,) - ) + metadata = source_context.metadata_projection(payload, source_binding=binding) + metadata = metadata.derive_fields( + source_provenance=metadata.read_value( + "source_provenance" + ).with_source_image_names((binding.alias,)) ) source_channel_axis = binding.source_channel_axis_for_shape( np.shape(image_payload_data(payload)), - observed_axis=metadata.source_channel_axis, + observed_axis=metadata.read_value("source_channel_axis"), ) - metadata = replace(metadata, source_channel_axis=source_channel_axis) + metadata = metadata.derive_fields(source_channel_axis=source_channel_axis) metadata.normalized_source_channel_axis(payload) return metadata @@ -83,7 +83,7 @@ def apply_source_binding_payload( """Apply one resolved source binding to a loaded runtime payload.""" metadata = _loaded_source_payload_metadata(payload, binding, source_context) data = image_payload_data(payload) - source_channel_axis = metadata.source_channel_axis + source_channel_axis = metadata.read_value("source_channel_axis") data, source_channel_axis = binding.artifact_kind.normalize_source_payload( data, source_channel_axis, @@ -91,15 +91,14 @@ def apply_source_binding_payload( if binding.load_as_monochrome and source_channel_axis is not None: data = _monochrome_source_data(data, source_channel_axis, metadata) source_channel_axis = None - metadata = replace( - metadata.without_unit_interval_intensity_scale(), + metadata = metadata.without_unit_interval_intensity_scale().derive_fields( intensity_scale=ImagePayloadMetadata.for_array(data).intensity_scale, ) if binding.load_as_mask: data = np.asarray(data, dtype=bool) mask = image_payload_mask(payload) - metadata = replace(metadata, source_channel_axis=source_channel_axis) + metadata = metadata.derive_fields(source_channel_axis=source_channel_axis) if mask is not None: return MaskedImagePayload(data, mask, metadata) if metadata.has_values: diff --git a/openhcs/core/source_plane_alignment.py b/openhcs/core/source_plane_alignment.py index d3c197b581..1cfcffb437 100644 --- a/openhcs/core/source_plane_alignment.py +++ b/openhcs/core/source_plane_alignment.py @@ -4,12 +4,14 @@ from collections import OrderedDict from collections.abc import Sequence -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import Any, ClassVar +from types import MemberDescriptorType from openhcs.core.runtime_image_values import ( + ImageMetadataCarrier, ImagePayloadMetadata, - image_payload_metadata, + image_payload_metadata_projection, ) from openhcs.core.source_image_provenance import SourceImageProvenanceIdentity from openhcs.core.source_matching import ( @@ -64,11 +66,12 @@ def _store_source_payload_plane_identity_sequence( @dataclass(frozen=True, slots=True) -class SourcePayloadPlaneIdentity: +class SourcePayloadPlaneIdentity(ImageMetadataCarrier): """Source image-set identities represented by one runtime payload plane.""" - metadata: ImagePayloadMetadata + metadata: ImagePayloadMetadata = field() policy: SourceImageSetIdentityPolicy + _metadata_projection: ClassVar[MemberDescriptorType] @classmethod def from_payload( @@ -76,10 +79,18 @@ def from_payload( payload: Any, policy: SourceImageSetIdentityPolicy, ) -> "SourcePayloadPlaneIdentity": - return cls(image_payload_metadata(payload), policy) + return cls(image_payload_metadata_projection(payload), policy) def identities(self) -> frozenset[SourceImageSetIdentity]: - return self.metadata.source_provenance.image_set_identities(self.policy) + return self.metadata_projection.read_value( + "source_provenance" + ).image_set_identities(self.policy) + + +# Keep the original declared metadata slot and public dataclass schema. The same +# shared descriptor used by metadata-only manifests exposes its stable namespace. +SourcePayloadPlaneIdentity._metadata_projection = SourcePayloadPlaneIdentity.metadata +SourcePayloadPlaneIdentity.metadata = ImageMetadataCarrier.metadata @dataclass(frozen=True, slots=True) @@ -90,23 +101,24 @@ class SourcePayloadPlaneIdentitySequence: policy: SourceImageSetIdentityPolicy def identities(self) -> SourcePlaneIdentitySequence: - metadata = image_payload_metadata(self.payload) + metadata = image_payload_metadata_projection(self.payload) cache_key = SourcePayloadPlaneIdentitySequenceCacheKey( - metadata.source_provenance.equality_identity, - self.policy, + metadata.read_value("source_provenance").equality_identity, self.policy ) cached = _cached_source_payload_plane_identity_sequence(cache_key) if cached is not None: return cached return _store_source_payload_plane_identity_sequence( cache_key, - metadata.source_provenance.image_set_plane_identities(self.policy), + metadata.read_value("source_provenance").image_set_plane_identities( + self.policy + ), ) def runtime_axis_identities(self) -> SourcePlaneIdentitySequence: """Return the image-set identity axis carried by this payload.""" - metadata = image_payload_metadata(self.payload) - return metadata.source_provenance.image_set_axis(self.policy) + metadata = image_payload_metadata_projection(self.payload) + return metadata.read_value("source_provenance").image_set_axis(self.policy) @property def has_identity(self) -> bool: diff --git a/openhcs/core/source_workspace_projection.py b/openhcs/core/source_workspace_projection.py index 0b3216e025..c4c6304532 100644 --- a/openhcs/core/source_workspace_projection.py +++ b/openhcs/core/source_workspace_projection.py @@ -12,11 +12,12 @@ from openhcs.core.source_path_identity import source_path_identity, source_path_join from openhcs.constants import Backend from openhcs.core.runtime_image_values import ( + ImageMetadataProjection, ImagePayloadMetadata, ImagePayloadMetadataCompositionMode, image_payload_data, image_payload_mask, - image_payload_metadata, + image_payload_metadata_projection, ) from openhcs.core.runtime_array_values import RuntimeArrayData from openhcs.core.source_bindings import ( @@ -538,13 +539,34 @@ class VirtualWorkspaceImagePayloadProjection: persisted_metadata: ImagePayloadMetadata | None = None def metadata(self, loaded: ImagePayloadMetadata) -> ImagePayloadMetadata: + """Preserve the public mutable namespace and nominal subtype lifecycle.""" + return self._metadata_from_persisted( + self.persisted_metadata, loaded + ).materialize_metadata() + + def metadata_projection( + self, loaded: ImageMetadataProjection + ) -> ImageMetadataProjection: + """Join facts without snapshotting or normalizing merely to enter a view.""" + persisted = self.persisted_metadata + if persisted is not None: + # This existing projection state reads the same authoritative live + # dataclass. The first semantic transform creates flat owned facts. + persisted = ImageMetadataProjection._from_realized_metadata(persisted) + return self._metadata_from_persisted(persisted, loaded) + + def _metadata_from_persisted( + self, + persisted: ImageMetadataProjection | None, + loaded: ImageMetadataProjection, + ) -> ImageMetadataProjection: """Preserve semantic omissions, but not an empty header's lost identity.""" - if self.persisted_metadata is None: + if persisted is None: return loaded - metadata = self.persisted_metadata.with_source_spatial_context_from( + metadata = persisted.with_source_spatial_context_from( loaded ).with_missing_intensity_from(loaded) - provenance = metadata.source_provenance + provenance = metadata.read_value("source_provenance") if not ( provenance.source_identity.addressable or provenance.source_image_provenance_planes.has_values @@ -559,11 +581,13 @@ def apply(self, payload: RuntimeArrayData) -> RuntimeArrayData: source_metadata = SourceMetadataFields.with_fields( source_metadata, {}, without=(SOURCE_BINDING_ALIAS_METADATA_FIELD,) ) - current_metadata = image_payload_metadata(payload) - metadata = self.metadata(current_metadata) - metadata = metadata.replace_fields( - source_spatial_domain=metadata.source_spatial_domain.with_native_image_context( - current_metadata.source_spatial_domain, + current_metadata = image_payload_metadata_projection(payload) + metadata = self.metadata_projection(current_metadata) + metadata = metadata.derive_fields( + source_spatial_domain=metadata.read_value( + "source_spatial_domain" + ).with_native_image_context( + current_metadata.read_value("source_spatial_domain"), image_shape_yx=current_metadata.spatial_shape_yx( image_payload_data(payload) ), @@ -573,11 +597,12 @@ def apply(self, payload: RuntimeArrayData) -> RuntimeArrayData: metadata = metadata.with_source_component_metadata(source_metadata) if self.source_alias is not None: metadata = metadata.with_source_provenance( - metadata.source_provenance.with_source_image_names((self.source_alias,)) + metadata.read_value("source_provenance").with_source_image_names( + (self.source_alias,) + ) ) return metadata.payload_with( - image_payload_data(payload), - image_payload_mask(payload), + image_payload_data(payload), image_payload_mask(payload) ) diff --git a/openhcs/core/steps/function_artifact_materialization.py b/openhcs/core/steps/function_artifact_materialization.py index e91198c959..b3dfc131e0 100644 --- a/openhcs/core/steps/function_artifact_materialization.py +++ b/openhcs/core/steps/function_artifact_materialization.py @@ -30,9 +30,11 @@ from openhcs.core.runtime_artifact_queries import MeasurementTableUnion from openhcs.core.runtime_artifact_values import RuntimeValue from openhcs.core.runtime_image_values import ( + ImageMetadataProjection, ImagePayloadMetadata, image_payload_data, image_payload_metadata, + image_payload_metadata_projection, ) from openhcs.core.runtime_measurements import ( MeasurementTable, @@ -182,9 +184,9 @@ def records_for_output( ] = {} for record in records: payload = output_plan.materialization_payload(record.value) - metadata = image_payload_metadata(payload) + metadata = image_payload_metadata_projection(payload) address = OpenHCSPlaneAddress.from_complete_source_metadata( - metadata.source_component_metadata + metadata.source_component_metadata_for_projection() ) addressed_records.append((record, address)) if address is not None: @@ -478,8 +480,10 @@ def metadata_items( def payload_source_identity( data: MaterializationValue, ) -> SourceImageIdentity | None: - metadata = image_payload_metadata(data) - source_identity = metadata.source_provenance.scalar_source_identity + metadata = image_payload_metadata_projection(data) + source_identity = metadata.read_value( + "source_provenance" + ).scalar_source_identity if source_identity.addressable: return source_identity return None @@ -645,11 +649,13 @@ def build( if record.key.artifact_type.uses_aggregate_materialization_identity( record.value.data ): - metadata = cls.record_payload_metadata(record) + metadata = cls.record_payload_metadata_projection(record) aggregate_provenance = ( None if metadata is None - else metadata.source_provenance.with_common_scalar_identity_from_planes() + else metadata.read_value( + "source_provenance" + ).with_common_scalar_identity_from_planes() ) source_identity = ( None @@ -735,13 +741,24 @@ def missing_component_is_aggregated( def record_payload_metadata( record: StoredRuntimeValue | None, ) -> ImagePayloadMetadata | None: + metadata = ( + ArtifactStreamSourceMetadataAuthority.record_payload_metadata_projection( + record + ) + ) + return None if metadata is None else metadata.materialize_metadata() + + @staticmethod + def record_payload_metadata_projection( + record: StoredRuntimeValue | None, + ) -> ImageMetadataProjection | None: if record is None: return None if isinstance(record.value.data, SourceImageProvenanceFields): return ImagePayloadMetadata( source_provenance=record.value.data.source_provenance, ) - return image_payload_metadata(record.value.data) + return image_payload_metadata_projection(record.value.data) @classmethod def record_metadata_with_runtime_scope( @@ -754,18 +771,18 @@ def record_metadata_with_runtime_scope( scope = record.key.scope if scope.has_fixed_components: component_metadata = scope.fixed_component_metadata( - metadata.source_component_metadata + metadata.source_component_metadata_for_projection() ) elif scope.component is not None: component_metadata = with_source_component_metadata( - metadata.source_component_metadata or {}, + metadata.source_component_metadata_for_projection() or {}, scope.component, scope.require_value_text(), ) else: return metadata return metadata.with_source_provenance( - metadata.source_provenance.with_source_component_metadata( + metadata.read_value("source_provenance").with_source_component_metadata( component_metadata ), ) @@ -784,7 +801,7 @@ def record_source_descriptor( exact_fixed_scope = record.key.scope.has_fixed_components if not materialization_spec.uses_source_identity_filename(): return None - metadata = cls.record_payload_metadata(record) + metadata = cls.record_payload_metadata_projection(record) if metadata is None: return None if ( @@ -793,7 +810,7 @@ def record_source_descriptor( and output_plan.materialization_source() != output_plan.source_context_source() ): - metadata = output_plan.materialization_metadata(record.value) + metadata = output_plan.materialization_metadata_projection(record.value) metadata = cls.record_metadata_with_runtime_scope( record, metadata, @@ -822,7 +839,8 @@ def record_source_descriptor( ) if identity is not None: identity = materialization_spec.filename_identity_for_output( - identity, output_plan, + identity, + output_plan, ) try: filename = Path( @@ -857,7 +875,9 @@ def record_source_descriptor( identity, ), ) - source_path = metadata.source_provenance.scalar_source_identity.path + source_path = metadata.read_value( + "source_provenance" + ).scalar_source_identity.path if source_path is None: return None return ArtifactRecordSourceDescriptor( @@ -875,12 +895,16 @@ def record_source_identity_from_metadata( identity: FunctionOutputIdentity | None, ) -> SourceImageIdentity | None: """Return scalar source identity with parser-resolved component metadata.""" - source_identity = metadata.source_provenance.scalar_source_identity + source_identity = metadata.read_value( + "source_provenance" + ).scalar_source_identity if identity is not None: - if metadata.source_component_metadata is None: + if metadata.source_component_metadata_for_projection() is None: component_metadata = {} else: - component_metadata = dict(metadata.source_component_metadata) + component_metadata = dict( + metadata.source_component_metadata_for_projection() + ) identity_metadata = identity.filename_component_metadata() for key, value in identity_metadata.items(): component = AllComponents.from_value(str(key)) diff --git a/openhcs/core/steps/function_io.py b/openhcs/core/steps/function_io.py index d2e5e5e38b..70a3ae7ccf 100644 --- a/openhcs/core/steps/function_io.py +++ b/openhcs/core/steps/function_io.py @@ -26,7 +26,7 @@ from openhcs.core.runtime_image_values import ( image_payload_data, image_payload_mask, - image_payload_metadata, + image_payload_metadata_projection, ) from openhcs.core.source_image_provenance import SourceImageIdentity from openhcs.core.steps.function_output_identity import FunctionOutputIdentity @@ -123,13 +123,13 @@ def _preloaded_image_payload( filemanager: FileManager, ) -> RuntimeArrayData: """Preserve loader-owned metadata or derive it through the source backend.""" - metadata = image_payload_metadata(image) + metadata = image_payload_metadata_projection(image) if not metadata.has_values: metadata = ImagePayloadSourceMetadataContext( SourceImageIdentity(source_path), read_backend=read_backend, filemanager=filemanager, - ).metadata(image) + ).metadata_projection(image) return metadata.payload_with( image_payload_data(image), image_payload_mask(image), diff --git a/openhcs/core/steps/function_output_identity.py b/openhcs/core/steps/function_output_identity.py index a9775369ec..1719b53532 100644 --- a/openhcs/core/steps/function_output_identity.py +++ b/openhcs/core/steps/function_output_identity.py @@ -10,8 +10,8 @@ from openhcs.core.source_path_identity import source_path_identity from openhcs.constants.constants import AllComponents, VariableComponents from openhcs.core.runtime_image_values import ( - ImagePayloadMetadata, - image_payload_metadata, + ImageMetadataProjection, + image_payload_metadata_projection, ) from openhcs.core.runtime_array_values import RuntimeArrayData from openhcs.core.components.parser_metaprogramming import ( @@ -320,7 +320,9 @@ def _qualified_filename( if qualifier is None: return filename if not filename.endswith(extension): - raise ValueError("Constructed filename does not retain its declared extension.") + raise ValueError( + "Constructed filename does not retain its declared extension." + ) return f"{filename[:-len(extension)]}_{qualifier}{extension}" @staticmethod @@ -444,7 +446,7 @@ class FunctionOutputIdentityAuthority: @classmethod def identity(cls, request: FunctionOutputPathRequest) -> FunctionOutputIdentity: - metadata = image_payload_metadata(request.output_payload) + metadata = image_payload_metadata_projection(request.output_payload) payload_identity = cls._identity_from_metadata_with_cache( request.parser, metadata, @@ -467,7 +469,7 @@ def identity(cls, request: FunctionOutputPathRequest) -> FunctionOutputIdentity: def identity_from_metadata( cls, parser: FilenameParser, - metadata: ImagePayloadMetadata, + metadata: ImageMetadataProjection, *, fallback_identity_path: str | None = None, variable_components: Sequence[VariableComponents] = (), @@ -487,7 +489,7 @@ def identity_from_metadata( def identity_from_metadata_with_cache( cls, parser: FilenameParser, - metadata: ImagePayloadMetadata, + metadata: ImageMetadataProjection, *, fallback_identity_path: str | None = None, variable_components: Sequence[VariableComponents] = (), @@ -518,7 +520,7 @@ def _source_stack_identity_component_values( def _identity_from_metadata_with_cache( cls, parser: FilenameParser, - metadata: ImagePayloadMetadata, + metadata: ImageMetadataProjection, *, fallback_identity_path: str | None, variable_components: Sequence[VariableComponents], @@ -531,7 +533,9 @@ def _identity_from_metadata_with_cache( ) metadata_cache_key = FunctionOutputMetadataIdentityCacheKey( parser_id=id(parser), - source_provenance_identity=metadata.source_provenance.equality_identity, + source_provenance_identity=metadata.read_value( + "source_provenance" + ).equality_identity, fallback_identity_path=fallback_identity_path, identity_components=tuple(sorted(identity_component_values)), input_aligned_output=input_aligned_output, @@ -553,7 +557,7 @@ def _identity_from_metadata_with_cache( def _identity_from_metadata_uncached( cls, parser: FilenameParser, - metadata: ImagePayloadMetadata, + metadata: ImageMetadataProjection, *, fallback_identity_path: str | None, identity_component_values: frozenset[str], @@ -561,10 +565,10 @@ def _identity_from_metadata_uncached( identity_cache: FunctionOutputIdentityCache, ) -> FunctionOutputIdentity | None: """Resolve parser-backed identity without consulting the metadata cache.""" - represented_source_identities = ( - metadata.source_provenance.represented_source_identities - ) - source_plane_count = metadata.source_provenance.source_plane_count + represented_source_identities = metadata.read_value( + "source_provenance" + ).represented_source_identities + source_plane_count = metadata.read_value("source_provenance").source_plane_count if source_plane_count > 1: if identity_component_values: return cls._source_stack_identity_from_provenance( @@ -584,10 +588,12 @@ def _identity_from_metadata_uncached( ) identity = cls._identity_from_metadata( - metadata.source_component_metadata, + metadata.source_component_metadata_for_projection(), extension=FunctionOutputExtensionAuthority.from_source( - metadata.source_component_metadata, metadata.source_path, - parser=parser, identity_cache=identity_cache, + metadata.source_component_metadata_for_projection(), + metadata.source_path_for_projection(), + parser=parser, + identity_cache=identity_cache, ), source="payload component metadata", ) @@ -625,7 +631,7 @@ def _identity_from_metadata_uncached( identity, ( ( - metadata.source_path, + metadata.source_path_for_projection(), "payload source path", ), ( @@ -641,8 +647,10 @@ def _identity_from_metadata_uncached( identity = cls._identity_from_metadata( source_identity.component_metadata, extension=FunctionOutputExtensionAuthority.from_source( - source_identity.component_metadata, source_identity.path, - parser=parser, identity_cache=identity_cache, + source_identity.component_metadata, + source_identity.path, + parser=parser, + identity_cache=identity_cache, ), source="single represented payload source metadata", ) @@ -670,10 +678,10 @@ def _identity_from_metadata_uncached( identity_cache=identity_cache, ) - if metadata.source_path is not None: + if metadata.source_path_for_projection() is not None: return cls._parsed_path_identity_with_cache( parser, - metadata.source_path, + metadata.source_path_for_projection(), source="payload source path", identity_cache=identity_cache, ) @@ -683,25 +691,27 @@ def _identity_from_metadata_uncached( def filename_identity_from_metadata( cls, parser: FilenameParser, - metadata: ImagePayloadMetadata, + metadata: ImageMetadataProjection, *, fallback_identity_path: str | None = None, ) -> FunctionOutputIdentity | None: """Return the identity that should name a filename-addressed output.""" identity_cache = FunctionOutputIdentityCache() - represented_source_identities = ( - metadata.source_provenance.represented_source_identities - ) + represented_source_identities = metadata.read_value( + "source_provenance" + ).represented_source_identities represented_source_path = ( represented_source_identities[0].path if represented_source_identities else None ) identity = cls._identity_from_metadata( - metadata.source_component_metadata, + metadata.source_component_metadata_for_projection(), extension=FunctionOutputExtensionAuthority.from_source( - metadata.source_component_metadata, metadata.source_path, - parser=parser, identity_cache=identity_cache, + metadata.source_component_metadata_for_projection(), + metadata.source_path_for_projection(), + parser=parser, + identity_cache=identity_cache, ), source="payload component metadata", ) @@ -711,7 +721,7 @@ def filename_identity_from_metadata( identity, ( ( - metadata.source_path, + metadata.source_path_for_projection(), "payload source path", ), ( @@ -728,7 +738,7 @@ def filename_identity_from_metadata( for path, source in ( ( - metadata.source_path, + metadata.source_path_for_projection(), "payload source path", ), ( @@ -834,14 +844,14 @@ def _parsed_path_identity( def _source_stack_identity_from_provenance( cls, parser: FilenameParser, - metadata: ImagePayloadMetadata, + metadata: ImageMetadataProjection, identity_component_values: frozenset[str], *, fallback_identity_path: str | None, input_aligned_output: bool, identity_cache: FunctionOutputIdentityCache, ) -> FunctionOutputIdentity: - source_provenance = metadata.source_provenance + source_provenance = metadata.read_value("source_provenance") semantic_source_identities = tuple( source_provenance.for_source_plane(plane_index).scalar_source_identity for plane_index in range(source_provenance.source_plane_count) @@ -900,8 +910,10 @@ def _source_stack_source_identity( identity = cls._identity_from_metadata( source_identity.component_metadata, extension=FunctionOutputExtensionAuthority.from_source( - source_identity.component_metadata, source_identity.path, - parser=parser, identity_cache=identity_cache, + source_identity.component_metadata, + source_identity.path, + parser=parser, + identity_cache=identity_cache, ), source=f"represented source identity {identity_index} metadata", ) diff --git a/openhcs/core/steps/function_output_manifest.py b/openhcs/core/steps/function_output_manifest.py index daef36dea2..a457ac4526 100644 --- a/openhcs/core/steps/function_output_manifest.py +++ b/openhcs/core/steps/function_output_manifest.py @@ -4,6 +4,7 @@ from dataclasses import dataclass, field, replace from pathlib import Path +from types import MemberDescriptorType from typing import ClassVar, Iterator, Sequence from weakref import WeakKeyDictionary @@ -20,8 +21,10 @@ from openhcs.core.path_pattern_matching import PathPatternTemplateMatcher from openhcs.core.runtime_array_values import RuntimeArrayData from openhcs.core.runtime_image_values import ( + ImageMetadataCarrier, + ImageMetadataProjection, ImagePayloadMetadata, - image_payload_metadata, + image_payload_metadata_projection, ) from openhcs.core.runtime_plane_projection import RuntimePlaneAxis @@ -107,7 +110,7 @@ def build( @dataclass(frozen=True, slots=True) -class ProducedOutputSemantics(FunctionOutputIdentity): +class ProducedOutputSemantics(FunctionOutputIdentity, ImageMetadataCarrier): """Semantic record for one output file produced by a FunctionStep.""" producer_identity: StreamProducerIdentity @@ -115,6 +118,7 @@ class ProducedOutputSemantics(FunctionOutputIdentity): relative_output_path: str image_metadata: ImagePayloadMetadata | None = None main_flow_plane_axis: RuntimePlaneAxis | None = RuntimePlaneAxis.RUNTIME_SLICE + _metadata_projection: ClassVar[MemberDescriptorType] def passed_through(self, plan: CompiledStepPlan) -> "ProducedOutputSemantics": """Retain the image domain and physical identity under the next producer.""" @@ -122,10 +126,12 @@ def passed_through(self, plan: CompiledStepPlan) -> "ProducedOutputSemantics": self, producer_identity=FunctionStepOutputProducerIdentityAuthority.build( FunctionStepOutputProducerIdentityRequest.from_main_flow( - plan, self.output_context, + plan, + self.output_context, ) ), relative_output_path=source_path_identity(self.output_path).name, + image_metadata=self.metadata_projection, ) def contextualize_image_payload( @@ -134,9 +140,13 @@ def contextualize_image_payload( ) -> RuntimeArrayData: """Attach this exact produced-output identity to a reloaded payload.""" - metadata = self.image_metadata or image_payload_metadata(payload) + metadata = self.metadata_projection or image_payload_metadata_projection( + payload + ) return metadata.with_source_component_metadata( - self.component_metadata(metadata.source_component_metadata) + self.component_metadata( + metadata.read_value("source_provenance").source_component_metadata + ) ).attach_to(payload) def path_under(self, output_dir: str | Path) -> str: @@ -208,7 +218,7 @@ def from_output( output_path: str | Path, output_identity: FunctionOutputIdentity, output_context: AlignedImageSliceContext | None = None, - image_metadata: ImagePayloadMetadata | None = None, + image_metadata: ImageMetadataProjection | None = None, main_flow_plane_axis: RuntimePlaneAxis | None = RuntimePlaneAxis.RUNTIME_SLICE, ) -> "ProducedOutputSemantics": output_path_text = str(output_path) @@ -276,6 +286,11 @@ def from_existing_main_flow_path( ) +# Keep the declared field and its original slot; public access realizes its owner. +ProducedOutputSemantics._metadata_projection = ProducedOutputSemantics.image_metadata +ProducedOutputSemantics.image_metadata = ImageMetadataCarrier.metadata + + @dataclass(slots=True) class StepOutputManifestStore: """Execution-local main-flow output lineage for shared VFS directories.""" @@ -551,7 +566,10 @@ def _unique_output_path_records( records_by_path: dict[str, ProducedOutputSemantics] = {} for record in records: existing = records_by_path.get(record.output_path) - if existing is not None and existing.main_flow_plane_axis is not record.main_flow_plane_axis: + if ( + existing is not None + and existing.main_flow_plane_axis is not record.main_flow_plane_axis + ): raise ValueError( "One produced output path cannot declare conflicting main-flow image axes: " f"{record.output_path!r}." diff --git a/openhcs/core/steps/function_outputs.py b/openhcs/core/steps/function_outputs.py index 7297a44416..efa94ad98e 100644 --- a/openhcs/core/steps/function_outputs.py +++ b/openhcs/core/steps/function_outputs.py @@ -38,7 +38,7 @@ ImagePayloadMetadata, image_intensity_scale_for_dtype, image_payload_data, - image_payload_metadata, + image_payload_metadata_projection, ) from openhcs.core.runtime_profile import RuntimeProfileLogger from openhcs.core.runtime_slice_projection import ( @@ -843,7 +843,8 @@ def write( is_main=self.is_main, results_dir=( str(Path(self.results_dir).relative_to(self.plate_root)) - if self.results_dir is not None else None + if self.results_dir is not None + else None ), ) @@ -912,7 +913,8 @@ def produced_projection_metadata( ) ) produced_projections[Path(destination)] = ( - record, projection_paths[-1][0] + record, + projection_paths[-1][0], ) continue projection_paths.append( @@ -929,7 +931,8 @@ def produced_projection_metadata( ) ) produced_projections[Path(destination)] = ( - record, projection_paths[-1][0] + record, + projection_paths[-1][0], ) projection_paths.extend( self.runtime_artifact_projection_paths( @@ -1035,6 +1038,17 @@ def persisted_image_metadata( destination: str, payload, ) -> ImagePayloadMetadata: + return self.persisted_image_metadata_projection( + context, destination=destination, payload=payload + ).materialize_metadata() + + def persisted_image_metadata_projection( + self, + context: ProcessingContext, + *, + destination: str, + payload, + ): """Describe one saved image through its backend or physical format.""" if context.filemanager is None: @@ -1045,9 +1059,9 @@ def persisted_image_metadata( base_path=self.output_dir, ) if physical_path is not None: - return ImageFileFormat.require_path(physical_path).persisted_metadata( - Path(physical_path), payload - ) + return ImageFileFormat.require_path( + physical_path + ).persisted_metadata_projection(Path(physical_path), payload) native_dtype = context.filemanager.source_image_dtype( destination, self.backend, @@ -1056,8 +1070,8 @@ def persisted_image_metadata( return ImageFileSourceMetadata( source_dtype=native_dtype, intensity_scale=image_intensity_scale_for_dtype(native_dtype), - ).project_image_metadata( - image_payload_metadata(payload), + ).project_image_metadata_projection( + image_payload_metadata_projection(payload), values_preserved=context.filemanager.image_serialization_preserves_values( self.backend, image_payload_data(payload).dtype, diff --git a/openhcs/core/steps/function_runtime.py b/openhcs/core/steps/function_runtime.py index e0fbc70fea..1e09683d65 100644 --- a/openhcs/core/steps/function_runtime.py +++ b/openhcs/core/steps/function_runtime.py @@ -140,12 +140,14 @@ SourceProjectionRole, ) from openhcs.core.runtime_image_values import ( + ImageMetadataProjection, ImagePayloadMetadata, ImagePayloadMetadataCarrier, ImagePayloadMetadataCompositionMode, image_payload_data, image_payload_mask, image_payload_metadata, + image_payload_metadata_projection, image_payload_slice_context, preserve_declared_image_payload_axis, with_image_payload_data, @@ -469,7 +471,9 @@ def output_owns_source_context( """Return whether an image result already carries complete source identity.""" if not isinstance(output_value, AlignedImageStack) and not ( - image_payload_metadata(output_value).has_complete_source_identity( + image_payload_metadata_projection( + output_value + ).has_complete_source_identity( output_value, plane_projection, ) @@ -477,9 +481,9 @@ def output_owns_source_context( return False output_surfaces = flatten_aligned_image_payload_slices(output_value) if not all( - image_payload_metadata(output_surface).has_complete_source_identity( + image_payload_metadata_projection( output_surface - ) + ).has_complete_source_identity(output_surface) for output_surface in output_surfaces ): return False @@ -487,12 +491,12 @@ def output_owns_source_context( return True source_surfaces = flatten_aligned_image_payload_slices(source_payload) return len(output_surfaces) == len(source_surfaces) and all( - image_payload_metadata( - output_surface - ).source_provenance.represented_source_identities - == image_payload_metadata( - source_surface - ).source_provenance.represented_source_identities + image_payload_metadata_projection(output_surface) + .read_value("source_provenance") + .represented_source_identities + == image_payload_metadata_projection(source_surface) + .read_value("source_provenance") + .represented_source_identities for source_surface, output_surface in zip( source_surfaces, output_surfaces, @@ -552,27 +556,33 @@ def contextualize( and plane_projection is not None and plane_projection.plane_index is None ): - output_metadata = image_payload_metadata(output_value) + output_metadata = image_payload_metadata_projection(output_value) if ( - output_metadata.plane_axis is None - and output_metadata.source_provenance.source_plane_count + output_metadata.read_value("plane_axis") is None + and output_metadata.read_value("source_provenance").source_plane_count == plane_projection.axis_size and plane_projection.dense_shape_carries_axis( np.shape(image_payload_data(output_value)) ) ): - output_value = output_metadata.replace_fields( + output_value = output_metadata.derive_fields( plane_axis=plane_projection.axis, ).attach_to(output_value) if output_plan is not None and not output_plan.variable_components: - output_metadata = image_payload_metadata(output_value) - if output_metadata.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE: + output_metadata = image_payload_metadata_projection(output_value) + if ( + output_metadata.read_value("plane_axis") + is RuntimePlaneAxis.RUNTIME_SLICE + ): output_value = RuntimeSliceProjection.value_for_singleton_slice( output_value, source_description=f"Image output {output_plan.ref()!r}", ) - source_metadata = image_payload_metadata(source_payload) - if source_metadata.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE: + source_metadata = image_payload_metadata_projection(source_payload) + if ( + source_metadata.read_value("plane_axis") + is RuntimePlaneAxis.RUNTIME_SLICE + ): if source_ref is None: raise ValueError( f"Image output {output_plan.ref()!r} consumes a runtime " @@ -580,9 +590,9 @@ def contextualize( ) collapsed_metadata = source_metadata.collapse_leading_plane_axis() source_payload = collapsed_metadata.with_source_provenance( - collapsed_metadata.source_provenance.with_source_image_names( - (source_ref.name,) - ) + collapsed_metadata.read_value( + "source_provenance" + ).with_source_image_names((source_ref.name,)) ).attach_source_context_to(output_value) plane_projection = None source_context_strategy = ImageOutputSourceContextStrategy.for_source_payload( @@ -600,14 +610,17 @@ def contextualize( ): if isinstance(output_value, AlignedImageStack): return output_value - output_metadata = image_payload_metadata(output_value) - source_metadata = image_payload_metadata(source_payload) + output_metadata = image_payload_metadata_projection(output_value) + source_metadata = image_payload_metadata_projection(source_payload) contextualized_output = output_metadata.with_source_context_from( source_metadata ).attach_source_context_to( output_value, ) - if image_payload_metadata(contextualized_output) == output_metadata: + if ( + image_payload_metadata(contextualized_output) + == output_metadata.materialize_metadata() + ): return output_value return contextualized_output return source_context_strategy.contextualize( @@ -692,7 +705,9 @@ def contextualize( del plane_projection subject = self._declared_subject(output_plan) assert output_plan is not None - source_provenance = image_payload_metadata(source_payload).source_provenance + source_provenance = image_payload_metadata_projection( + source_payload + ).read_value("source_provenance") if isinstance(output_value, MeasurementTable): self._validate_nominal_table(output_value, output_plan, subject) contextualized_provenance = ( @@ -745,7 +760,9 @@ def contextualize( if output_plan is not None: output_value.validate_artifact_name(output_plan.name) source_provenance = output_value.contextualized_source_provenance( - image_payload_metadata(source_payload).source_provenance + image_payload_metadata_projection(source_payload).read_value( + "source_provenance" + ) ) contextualized_provenance = output_value.source_provenance.with_missing_from( source_provenance @@ -762,7 +779,7 @@ def project_declared_source_identity( ) -> RuntimePayload: """Project an image payload to one exact declared source identity.""" - metadata = image_payload_metadata(source_payload) + metadata = image_payload_metadata_projection(source_payload) return metadata.project_declared_source_image(source_payload, source_ref.name) @@ -854,7 +871,7 @@ def contextualize( ) -> RuntimePayload: if isinstance(output_value, AlignedImageStack): return output_value - return image_payload_metadata(source_payload).derive_payload( + return image_payload_metadata_projection(source_payload).derive_payload( source_payload, output_value, plane_projection=plane_projection, @@ -924,7 +941,10 @@ def requires_plane_contextualization( return ( plane_projection is not None and plane_projection.plane_index is None - and image_payload_metadata(source_payload).plane_axis is None + and image_payload_metadata_projection(source_payload).read_value( + "plane_axis" + ) + is None and np.ndim(object_label_dense_array(source_payload)) >= 3 ) @@ -939,14 +959,14 @@ def contextualize( "Object-label image output context requires ObjectLabelValue, got " f"{type(source_payload).__name__}." ) - source_metadata = image_payload_metadata(source_payload) + source_metadata = image_payload_metadata_projection(source_payload) if plane_projection is None or plane_projection.plane_index is not None: return source_metadata.derive_payload( source_payload, output_value, plane_projection=plane_projection, ) - plane_count = source_metadata.source_provenance.source_plane_count + plane_count = source_metadata.read_value("source_provenance").source_plane_count if plane_count != plane_projection.axis_size: raise ValueError( "Object-label image output source-plane provenance must match the " @@ -957,8 +977,8 @@ def contextualize( np.shape(image_payload_data(output_value)), value_name="Object-label image output payload", ) - output_metadata = image_payload_metadata(output_value) - contextualized_output = output_metadata.replace_fields( + output_metadata = image_payload_metadata_projection(output_value) + contextualized_output = output_metadata.derive_fields( plane_axis=plane_projection.axis, ).attach_to(output_value) return source_metadata.derive_payload( @@ -985,7 +1005,7 @@ def payload(self) -> RuntimePayload: "but no declared runtime plane projection." ) source_value = self.source_values.value_for_aligned_slice(0, 1) - return image_payload_metadata(source_value).derive_payload( + return image_payload_metadata_projection(source_value).derive_payload( source_value, self.output_value, ) @@ -997,7 +1017,7 @@ def payload(self) -> RuntimePayload: len(output_slices), ) contextualized_slices.append( - image_payload_metadata(source_value).derive_payload( + image_payload_metadata_projection(source_value).derive_payload( source_value, output_slice, ) @@ -1346,9 +1366,9 @@ def active_main_flow_source_binding_plan( plan = self.unscoped_main_flow_source_binding_plan represented_names = frozenset( - image_payload_metadata( - payload - ).source_provenance.represented_source_image_names + image_payload_metadata_projection(payload) + .read_value("source_provenance") + .represented_source_image_names ) if represented_names: variable_components = ComponentSet.coerce( @@ -1431,9 +1451,11 @@ def from_pattern_group( request.component_key, artifacts.outputs, ) - source_provenance = image_payload_metadata( - loaded.main_data_stack - ).source_provenance.with_common_scalar_identity_from_planes() + source_provenance = ( + image_payload_metadata_projection(loaded.main_data_stack) + .read_value("source_provenance") + .with_common_scalar_identity_from_planes() + ) common_source_metadata = source_provenance.source_component_metadata or {} variable_components = ComponentSet.coerce( request.execution_plan.variable_components or () @@ -1576,14 +1598,18 @@ class PatternGroupData: default_factory=SourceBindingRuntimeContext.empty ) - @staticmethod def validate_main_flow_cohort( producer_records: Sequence[ProducedOutputSemantics] | None, ) -> None: """One input cohort has one declared whole-image composition domain.""" - if producer_records and len({record.main_flow_plane_axis for record in producer_records}) != 1: - raise ValueError("One main-flow cohort cannot combine different declared image axes.") + if ( + producer_records + and len({record.main_flow_plane_axis for record in producer_records}) != 1 + ): + raise ValueError( + "One main-flow cohort cannot combine different declared image axes." + ) @classmethod def from_loaded_images( @@ -1601,11 +1627,15 @@ def from_loaded_images( if ( producer_records and len(producer_records) == 1 - and producer_records[0].main_flow_plane_axis is image_payload_metadata(payloads[0]).plane_axis + and producer_records[0].main_flow_plane_axis + is image_payload_metadata_projection(payloads[0]).read_value("plane_axis") ): main_data_stack = ImagePayloadStackComposition.copy_whole_image( - payloads[0], memory_type=execution_plan.input_memory_type, - device_id=execution_plan.device_id_for(execution_plan.input_memory_type), + payloads[0], + memory_type=execution_plan.input_memory_type, + device_id=execution_plan.device_id_for( + execution_plan.input_memory_type + ), ) else: metadata_mode = ImagePayloadMetadataCompositionMode.STACK @@ -1614,7 +1644,9 @@ def from_loaded_images( metadata_mode = ( ImagePayloadMetadataCompositionMode.BUNDLE if declared_axis is None - else ImagePayloadMetadataCompositionMode.for_plane_axis(declared_axis) + else ImagePayloadMetadataCompositionMode.for_plane_axis( + declared_axis + ) ) if source_projection is not None and workspace_source_lookups: metadata_mode = source_projection.payload_composition_mode( @@ -1627,11 +1659,14 @@ def from_loaded_images( execution_plan.device_id_for(execution_plan.input_memory_type), ) main_data_stack = stack_image_payload_context( - payloads, main_data_stack, metadata_mode=metadata_mode, + payloads, + main_data_stack, + metadata_mode=metadata_mode, ) elif metadata_mode is ImagePayloadMetadataCompositionMode.BUNDLE: main_data_stack = ImagePayloadBundleContext.from_payloads( - tuple(payloads), metadata_mode=metadata_mode, + tuple(payloads), + metadata_mode=metadata_mode, ).compose() return cls( matching_files=matching_files, @@ -1760,7 +1795,8 @@ def prepare_compiled_function_group(group: CompiledFunctionGroup) -> None: def prepare_compiled_context_callables( compiled_contexts: Mapping[str, ProcessingContext], - *, max_workers: int = 1, + *, + max_workers: int = 1, ) -> None: """Prepare every compiled callable visible in the compiled contexts.""" prepared_group_keys: set[tuple[str, int, str]] = set() @@ -2143,8 +2179,8 @@ def execution_group_source_payload( component = self.runtime_scope.execution_plan.execution_group_scope.component if component is None or self.group_key is None: return source_payload - metadata = image_payload_metadata(source_payload) - component_metadata = metadata.source_component_metadata or {} + metadata = image_payload_metadata_projection(source_payload) + component_metadata = metadata.source_component_metadata_for_projection() or {} component_metadata = with_source_component_metadata( component_metadata, component, @@ -2512,25 +2548,30 @@ def from_single_image( slices=(value,), slice_contexts=slice_contexts, stack_payload=ImagePayloadStackComposition.copy_whole_image( - value, memory_type=memory_type, device_id=device_id, + value, + memory_type=memory_type, + device_id=device_id, ), main_flow_source=value, ) def plane_axis_for_output_context( - self, context: AlignedImageSliceContext, + self, + context: AlignedImageSliceContext, ) -> RuntimePlaneAxis | None: """Derive each persisted member's domain from its original nominal producer.""" if isinstance(self.main_flow_source, AlignedImageStack): return self.main_flow_source.plane_axis_for_output_context(context) if isinstance(self.main_flow_source, ImagePayloadMetadataCarrier): - return image_payload_metadata(self.main_flow_source).plane_axis + return image_payload_metadata_projection(self.main_flow_source).read_value( + "plane_axis" + ) return RuntimePlaneAxis.RUNTIME_SLICE def cache_payload_for_outputs( self, payloads: Sequence[RuntimeArrayData], - metadata: Sequence[ImagePayloadMetadata], + metadata: Sequence[ImageMetadataProjection], ) -> RuntimeArrayData | None: """Keep the original whole-image domain while applying saved output context.""" if self.stack_payload is None: @@ -2538,11 +2579,17 @@ def cache_payload_for_outputs( data = image_payload_data(self.stack_payload) if ( len(payloads) == 1 - and self.plane_axis_for_output_context(self.slice_contexts[0]) is metadata[0].plane_axis + and self.plane_axis_for_output_context(self.slice_contexts[0]) + is metadata[0].read_value("plane_axis") and np.shape(data) == np.shape(image_payload_data(payloads[0])) ): - return metadata[0].replace_fields().payload_with( - data, image_payload_mask(self.stack_payload), + return ( + metadata[0] + .derive_fields() + .payload_with( + data, + image_payload_mask(self.stack_payload), + ) ) if np.shape(data)[:1] != (len(payloads),): raise ValueError( @@ -2551,10 +2598,14 @@ def cache_payload_for_outputs( f"slice count {len(payloads)}." ) return stack_image_payload_context_from_metadata( - payloads, data, metadata, + payloads, + data, + metadata, metadata_mode=( ImagePayloadMetadataCompositionMode.for_plane_axis( - image_payload_metadata(self.stack_payload).plane_axis, + image_payload_metadata_projection(self.stack_payload).read_value( + "plane_axis" + ), ) if isinstance(self.stack_payload, ImagePayloadMetadataCarrier) else ImagePayloadMetadataCompositionMode.STACK @@ -2576,7 +2627,9 @@ def from_aligned_stack( if metadata_mode is None: result = cls.from_single_image( payloads[0], - slice_contexts=tuple(context for _payload, context in projected_outputs), + slice_contexts=tuple( + context for _payload, context in projected_outputs + ), memory_type=memory_type, device_id=device_id, ) @@ -2588,15 +2641,21 @@ def from_aligned_stack( value.plane_axis_for_output_context(context) for _payload, context in projected_outputs } - if len(declared_axes) == 1 and len({tuple(np.shape(item)) for item in data}) == 1: + if ( + len(declared_axes) == 1 + and len({tuple(np.shape(item)) for item in data}) == 1 + ): if metadata_mode is ImagePayloadMetadataCompositionMode.BUNDLE: stack_payload = ImagePayloadBundleContext.from_payloads( - tuple(payloads), metadata_mode=metadata_mode, + tuple(payloads), + metadata_mode=metadata_mode, ).compose() else: stacked = stack_runtime_slices(data, memory_type, device_id) stack_payload = stack_image_payload_context( - payloads, stacked, metadata_mode=metadata_mode, + payloads, + stacked, + metadata_mode=metadata_mode, ) return cls( slices=payloads, @@ -2790,7 +2849,9 @@ def _record_main_flow_passthrough(self, matching_files: Sequence[str]) -> None: plan = self.request.execution_plan parser = self.request.context.microscope_handler.parser manifest = step_output_manifest(self.request.context) - producer_records = manifest.producer_output_records_for_paths(plan, matching_files, parser) + producer_records = manifest.producer_output_records_for_paths( + plan, matching_files, parser + ) if producer_records is None: records = tuple( ProducedOutputSemantics.from_existing_main_flow_path( @@ -2904,7 +2965,9 @@ def _load_input_stack(self) -> PatternGroupData: ).runtime_context() producer_records = ( output_manifest.producer_output_records_for_paths( - plan, matching_files, context.microscope_handler.parser, + plan, + matching_files, + context.microscope_handler.parser, ) if plan.main_input_dependency.kind is StepInputDependencyKind.STEP_OUTPUT else None @@ -3252,7 +3315,10 @@ def _validate_and_unstack( ) -> PatternGroupOutputData: if ( isinstance(processed_stack, ImagePayloadMetadataCarrier) - and processed_stack.metadata.plane_axis is None + and image_payload_metadata_projection(processed_stack).read_value( + "plane_axis" + ) + is None ): output_context = self._unwrapped_main_flow_output_context() return PatternGroupOutputData.from_single_image( @@ -3451,11 +3517,14 @@ def _save_outputs( ) ) img_slice = output_context.contextualize_image_payload(img_slice) - output_metadata = image_payload_metadata(img_slice) + output_metadata = image_payload_metadata_projection(img_slice) output_component_metadata = output_identity.component_metadata( - output_metadata.source_component_metadata, + output_metadata.source_component_metadata_for_projection(), ) - if output_metadata.source_component_metadata != output_component_metadata: + if ( + output_metadata.source_component_metadata_for_projection() + != output_component_metadata + ): output_metadata = output_metadata.with_source_component_metadata( output_component_metadata ) @@ -3466,7 +3535,9 @@ def _save_outputs( output_identity, output_context=output_context, image_metadata=output_metadata, - main_flow_plane_axis=output_data.plane_axis_for_output_context(output_context), + main_flow_plane_axis=output_data.plane_axis_for_output_context( + output_context + ), ) if output_directory_exists and context.filemanager.exists( @@ -3505,7 +3576,8 @@ def _save_outputs( Backend.MEMORY.value, ) stack_payload = output_data.cache_payload_for_outputs( - output_payloads, output_payload_metadata, + output_payloads, + output_payload_metadata, ) if stack_payload is not None: context.runtime_image_stack_cache.store( diff --git a/openhcs/interop/cellprofiler/module_artifact_declarations.py b/openhcs/interop/cellprofiler/module_artifact_declarations.py index f7534336f8..51b9ed1193 100644 --- a/openhcs/interop/cellprofiler/module_artifact_declarations.py +++ b/openhcs/interop/cellprofiler/module_artifact_declarations.py @@ -65,7 +65,7 @@ from openhcs.core.function_patterns import ( FunctionInvocationKey, ) - from openhcs.core.runtime_image_values import ImagePayloadMetadata + from openhcs.core.runtime_image_values import ImageMetadataProjection from openhcs.core.runtime_tabular_values import ColumnarRows from openhcs.core.steps.function_runtime import ( RuntimeCallableKwargs, @@ -233,19 +233,19 @@ class ObjectArtifactInputModule(CellProfilerModule): """Parent for modules that consume object-label artifacts through declared settings.""" @classmethod - def measurement_record_source_metadata( + def measurement_record_source_metadata_projection( cls, request: "CellProfilerOutputRecordRequest", rows: "ColumnarRows", - ) -> "ImagePayloadMetadata": + ) -> "ImageMetadataProjection": """Compose image-feature ownership with exact object-input provenance.""" object_inputs = ArtifactSpecCollection( request.callable_contract.artifact_inputs.specs ).of_artifact_type(ObjectLabelsArtifactType) if not object_inputs: - return super().measurement_record_source_metadata(request, rows) - return request.measurement_source_metadata(object_inputs) + return super().measurement_record_source_metadata_projection(request, rows) + return request.measurement_source_metadata_projection(object_inputs) class ObjectArtifactOutputModule( @@ -463,17 +463,17 @@ def measurement_record_object_name( return None @classmethod - def measurement_record_source_metadata( + def measurement_record_source_metadata_projection( cls, request: "CellProfilerOutputRecordRequest", rows: "ColumnarRows", - ) -> "ImagePayloadMetadata": + ) -> "ImageMetadataProjection": """Compose inherited ownership with exact object-output provenance.""" object_outputs = cls.measurement_object_output_specs_for_request(request) if not object_outputs: - return super().measurement_record_source_metadata(request, rows) - return request.measurement_source_metadata(object_outputs) + return super().measurement_record_source_metadata_projection(request, rows) + return request.measurement_source_metadata_projection(object_outputs) class LabelDimensionObjectArtifactOutputModule(ObjectArtifactOutputModule): diff --git a/openhcs/interop/cellprofiler/runtime/artifact_binding.py b/openhcs/interop/cellprofiler/runtime/artifact_binding.py index 55a279e1b8..f6ed074091 100644 --- a/openhcs/interop/cellprofiler/runtime/artifact_binding.py +++ b/openhcs/interop/cellprofiler/runtime/artifact_binding.py @@ -40,7 +40,7 @@ from openhcs.core.runtime_image_values import ( image_payload_data, image_payload_mask, - image_payload_metadata, + image_payload_metadata_projection, ) from openhcs.core.runtime_measurements import ( MeasurementTable, @@ -240,9 +240,9 @@ def raw_runtime_input_value( self, request: RuntimeArtifactInputRequest ) -> RuntimeCallableArgument: payload = request.value - metadata = image_payload_metadata(payload) + metadata = image_payload_metadata_projection(payload) metadata = metadata.with_source_provenance( - metadata.source_provenance.with_derived_source_image_names( + metadata.read_value("source_provenance").with_derived_source_image_names( (request.spec.name,) ) ) @@ -269,7 +269,9 @@ def source_image_name_from_value( value: RuntimeCallableArgument, ) -> str | None: return single_source_name( - image_payload_metadata(value).source_provenance.represented_source_image_names + image_payload_metadata_projection(value) + .read_value("source_provenance") + .represented_source_image_names ) def source_image_payload( @@ -318,12 +320,13 @@ def object_labels( value = request.value if isinstance(value, ObjectLabelSet): return value - metadata = image_payload_metadata(value) + metadata = image_payload_metadata_projection(value) return SourceImageObjectLabelBuildRequest( image=value, labels=image_payload_data(value), plane_projection=RuntimePlaneAxisValueProjection.from_source_declaration( - metadata.plane_axis, metadata.source_provenance, + metadata.read_value("plane_axis"), + metadata.read_value("source_provenance"), ), ).label_set( name=request.spec.name, @@ -589,11 +592,11 @@ def main_flow_value( f"{current_image.slice_contexts!r}." ) return payload - metadata = image_payload_metadata(current_image) + metadata = image_payload_metadata_projection(current_image) if ( len(self.primary_image_inputs) == 1 and self.primary_image_inputs[0] == spec - and metadata.source_provenance.source_plane_count == 0 + and metadata.read_value("source_provenance").source_plane_count == 0 ): return current_image return metadata.project_declared_source_image(current_image, spec.name) @@ -628,7 +631,9 @@ def stack_broadcast_source_value( f"stack-broadcast source {source_ref!r}." ) return payload - return image_payload_metadata(current_image).project_declared_source_image( + return image_payload_metadata_projection( + current_image + ).project_declared_source_image( current_image, source_ref.name, ) diff --git a/openhcs/interop/cellprofiler/runtime/function_contract_execution.py b/openhcs/interop/cellprofiler/runtime/function_contract_execution.py index 199f2dea38..f7c3589889 100644 --- a/openhcs/interop/cellprofiler/runtime/function_contract_execution.py +++ b/openhcs/interop/cellprofiler/runtime/function_contract_execution.py @@ -25,7 +25,7 @@ from openhcs.core.runtime_image_values import ( image_payload_data, image_payload_mask, - image_payload_metadata, + image_payload_metadata_projection, ) from openhcs.core.runtime_output_matching import RuntimeReturnedOutputMatcher from openhcs.core.runtime_plane_projection import ( @@ -312,7 +312,9 @@ def _execute_aligned_multi_image_stack( ) if callable_contract.processing_contract is ProcessingContract.PURE_3D: slice_plane_axes = tuple( - image_payload_metadata(slice_payload).plane_axis + image_payload_metadata_projection(slice_payload).read_value( + "plane_axis" + ) for slice_payload in image.slices ) if any( @@ -487,7 +489,9 @@ def execute_pure_2d( image, kwargs, ).call() - declared_plane_axis = image_payload_metadata(image).plane_axis + declared_plane_axis = image_payload_metadata_projection(image).read_value( + "plane_axis" + ) if declared_plane_axis is not self.plane_projection.axis: raise RuntimeSliceProjectionDeclarationError( f"{invocation_context} with ProcessingContract.PURE_2D has an image " @@ -594,7 +598,7 @@ def execute_volumetric_to_slice( ).call() result_data = image_payload_data(result_2d) result_mask = image_payload_mask(result_2d) - result_metadata = image_payload_metadata(result_2d) + result_metadata = image_payload_metadata_projection(result_2d) memory_type = detect_memory_type(result_data) stacked = stack_slices([result_data], memory_type, 0) return result_metadata.payload_with(stacked, mask=result_mask) diff --git a/openhcs/interop/cellprofiler/runtime/invocation.py b/openhcs/interop/cellprofiler/runtime/invocation.py index cfe57fa7b2..c0a1dd6ab6 100644 --- a/openhcs/interop/cellprofiler/runtime/invocation.py +++ b/openhcs/interop/cellprofiler/runtime/invocation.py @@ -24,10 +24,11 @@ RuntimeImageRequest, ) from openhcs.core.runtime_image_values import ( + ImageMetadataProjection, ImagePayloadMetadata, ImagePayloadMetadataCompositionMode, image_payload_geometry, - image_payload_metadata, + image_payload_metadata_projection, ) from openhcs.core.runtime_object_labels import ( ObjectLabelMeasurementSource, @@ -116,7 +117,9 @@ def shared_source_payload( if not sources: return None provenances = tuple( - image_payload_metadata(source.payload).source_provenance.equality_identity + image_payload_metadata_projection(source.payload) + .read_value("source_provenance") + .equality_identity for source in sources ) if CommonRuntimeValue.from_values(provenances).single is None: @@ -130,6 +133,18 @@ def composed_source_metadata( *, mode: ImagePayloadMetadataCompositionMode | None = None, ) -> ImagePayloadMetadata | None: + """Expose the actual public namespace of the ordered source composition.""" + + owner = cls.composed_source_metadata_projection(sources, mode=mode) + return None if owner is None else owner.materialize_metadata() + + @classmethod + def composed_source_metadata_projection( + cls, + sources: tuple["CellProfilerSourceIdentityMixin", ...], + *, + mode: ImagePayloadMetadataCompositionMode | None = None, + ) -> ImageMetadataProjection | None: """Return source metadata composed in runtime source order.""" if not sources: return None @@ -143,14 +158,18 @@ def composed_source_metadata( source_metadata = tuple( ImagePayloadMetadata( source_provenance=( - ImagePayloadMetadata.compose( - payload_slices_for_alignment(payload), - mode=ImagePayloadMetadataCompositionMode.BUNDLE, + ImageMetadataProjection._from_realized_metadata( + ImagePayloadMetadata.compose( + payload_slices_for_alignment(payload), + mode=ImagePayloadMetadataCompositionMode.BUNDLE, + ) ) .collapse_leading_plane_axis() - .source_provenance + .read_value("source_provenance") if isinstance(payload, AlignedImageStack) - else image_payload_metadata(payload).source_provenance + else image_payload_metadata_projection(payload).read_value( + "source_provenance" + ) ) ) for payload in source_payloads @@ -162,7 +181,7 @@ def composed_source_metadata( ) if not metadata.has_values: return None - return metadata + return ImageMetadataProjection._from_realized_metadata(metadata) @classmethod def source_metadata_composition_mode( @@ -355,7 +374,9 @@ def with_alignment_image( f"{plane_projection!r}." ) else: - image_plane_axis = image_payload_metadata(image).plane_axis + image_plane_axis = image_payload_metadata_projection(image).read_value( + "plane_axis" + ) if image_plane_axis is None: plane_projection = None elif image_plane_axis is not plane_projection.axis: diff --git a/openhcs/interop/cellprofiler/runtime/main_flow.py b/openhcs/interop/cellprofiler/runtime/main_flow.py index da54a1e105..acde5ea51a 100644 --- a/openhcs/interop/cellprofiler/runtime/main_flow.py +++ b/openhcs/interop/cellprofiler/runtime/main_flow.py @@ -4,7 +4,7 @@ from openhcs.core.runtime_image_values import ( image_payload_geometry, - image_payload_metadata, + image_payload_metadata_projection, ) from openhcs.core.runtime_plane_projection import RuntimePlaneAxisValueProjection from openhcs.core.steps.function_runtime import RuntimeCallableArgument @@ -16,7 +16,9 @@ def cellprofiler_main_flow_output( plane_projection: RuntimePlaneAxisValueProjection | None, ) -> RuntimeCallableArgument: """Preserve the declared OpenHCS plane axis on a CP output image.""" - output_axis = image_payload_metadata(output_image).plane_axis + output_axis = image_payload_metadata_projection(output_image).read_value( + "plane_axis" + ) if output_axis is None: return output_image if plane_projection is None: @@ -34,7 +36,7 @@ def cellprofiler_main_flow_output( image_payload_geometry(output_image).shape, value_name="CellProfiler main-flow output", ) - input_axis = image_payload_metadata(input_image).plane_axis + input_axis = image_payload_metadata_projection(input_image).read_value("plane_axis") if input_axis is not None: if input_axis is not plane_projection.axis: raise ValueError( @@ -46,12 +48,12 @@ def cellprofiler_main_flow_output( image_payload_geometry(input_image).shape, value_name="CellProfiler input image", ) - if image_payload_metadata(output_image).has_complete_source_identity( + if image_payload_metadata_projection(output_image).has_complete_source_identity( output_image, plane_projection, ): return output_image - return image_payload_metadata(input_image).derive_payload( + return image_payload_metadata_projection(input_image).derive_payload( input_image, output_image, plane_projection=plane_projection, diff --git a/openhcs/interop/cellprofiler/runtime/measurement_recording.py b/openhcs/interop/cellprofiler/runtime/measurement_recording.py index 63cacedd72..2094d02028 100644 --- a/openhcs/interop/cellprofiler/runtime/measurement_recording.py +++ b/openhcs/interop/cellprofiler/runtime/measurement_recording.py @@ -39,8 +39,9 @@ MeasurementSparseColumnarRows, ) from openhcs.core.runtime_image_values import ( + ImageMetadataProjection, ImagePayloadMetadata, - image_payload_metadata, + image_payload_metadata_projection, ) from openhcs.core.runtime_measurements import MeasurementTable from openhcs.core.runtime_tabular_values import ColumnarRows @@ -137,7 +138,7 @@ def build_measurement_table( rows: ColumnarRows, object_name: str | None, source_image_name: str | None, - source_metadata: ImagePayloadMetadata, + source_metadata: ImageMetadataProjection, ) -> MeasurementTable: """Build one canonical module-owned native measurement table.""" rows, source_provenance = cls.measurement_rows_and_source_provenance( @@ -263,10 +264,23 @@ def measurement_record_source_image_name( def measurement_record_source_metadata( cls, request: CellProfilerOutputRecordRequest, rows: ColumnarRows ) -> ImagePayloadMetadata: + """Expose actual metadata through the public module callback protocol.""" + + return cls.measurement_record_source_metadata_projection( + request, rows + ).materialize_metadata() + + @classmethod + def measurement_record_source_metadata_projection( + cls, request: CellProfilerOutputRecordRequest, rows: ColumnarRows + ) -> ImageMetadataProjection: del cls, rows - return request.source.composed_source_metadata( - (request.source,) - ) or image_payload_metadata(request.source.payload) + metadata = request.source.composed_source_metadata((request.source,)) + return ( + ImageMetadataProjection._from_realized_metadata(metadata) + if metadata + else image_payload_metadata_projection(request.source.payload) + ) @staticmethod def rows_only_declare_object_name(rows: ColumnarRows) -> bool: @@ -280,18 +294,20 @@ def rows_only_declare_object_name(rows: ColumnarRows) -> bool: @staticmethod def measurement_rows_and_source_provenance( rows: ColumnarRows, - source_metadata: ImagePayloadMetadata, + source_metadata: ImageMetadataProjection, ) -> tuple[ColumnarRows, SourceImageProvenance]: """Keep row slice indexes local to the table's source-plane axis.""" - source_plane_count = source_metadata.source_provenance.source_plane_count + source_plane_count = source_metadata.read_value( + "source_provenance" + ).source_plane_count if source_plane_count <= 1: - return rows, source_metadata.source_provenance + return rows, source_metadata.read_value("source_provenance") row_projection = MeasurementRowsAxisProjection.from_rows(rows) slice_indices = row_projection.present_axis_values( MeasurementRowAxisField.SLICE_INDEX.value ) if len(slice_indices) != 1: - return rows, source_metadata.source_provenance + return rows, source_metadata.read_value("source_provenance") slice_index = slice_indices[0] if slice_index >= source_plane_count: raise ValueError( @@ -305,7 +321,9 @@ def measurement_rows_and_source_provenance( ) return ( projected_rows, - source_metadata.for_source_plane(slice_index).source_provenance, + source_metadata.for_source_plane(slice_index).read_value( + "source_provenance" + ), ) @classmethod @@ -652,11 +670,11 @@ class CurrentPayloadMeasurementRecordMixin(PayloadOnlyMeasurementRecordMixin): """Uses the current runtime payload without an image-source name.""" @classmethod - def measurement_record_source_metadata( + def measurement_record_source_metadata_projection( cls, request: CellProfilerOutputRecordRequest, rows: ColumnarRows - ) -> ImagePayloadMetadata: + ) -> ImageMetadataProjection: del cls, rows - return image_payload_metadata(request.source.payload) + return image_payload_metadata_projection(request.source.payload) @classmethod def clear_source_when_rows_declare_object_name(cls) -> bool: @@ -707,12 +725,12 @@ def measurement_record_source_image_name( return None if source_spec is None else source_spec.name @classmethod - def measurement_record_source_metadata( + def measurement_record_source_metadata_projection( cls, request: CellProfilerOutputRecordRequest, rows: ColumnarRows - ) -> ImagePayloadMetadata: + ) -> ImageMetadataProjection: del rows source_spec = cls.primary_image_output_spec(request) - return image_payload_metadata( + return image_payload_metadata_projection( request.source.payload if source_spec is None else request.artifact_output_value(source_spec) @@ -732,13 +750,13 @@ class DeclaredImageOutputPayloadMeasurementRecordMixin( """Uses exact declared image-output provenance as measurement context.""" @classmethod - def measurement_record_source_metadata( + def measurement_record_source_metadata_projection( cls, request: CellProfilerOutputRecordRequest, rows: ColumnarRows - ) -> ImagePayloadMetadata: + ) -> ImageMetadataProjection: from openhcs.core.artifacts import ImageArtifactType from openhcs.core.runtime_image_values import ( ImagePayloadMetadata, - image_payload_metadata, + image_payload_metadata_projection, ) from openhcs.core.source_matching import SourceImageSetIdentityPolicy from openhcs.core.source_plane_alignment import ( @@ -791,11 +809,14 @@ def measurement_record_source_metadata( f"reference={image_output_specs[0].ref()!r}; " f"unaligned={tuple(image_output_specs[index].ref() for index in unaligned_indexes)!r}." ) - source_metadata = ImagePayloadMetadata.compose( - source_payloads, - source_metadata=tuple( - image_payload_metadata(payload) for payload in source_payloads - ), + source_metadata = ImageMetadataProjection._from_realized_metadata( + ImagePayloadMetadata.compose( + source_payloads, + source_metadata=tuple( + image_payload_metadata_projection(payload) + for payload in source_payloads + ), + ) ).collapse_leading_plane_axis() return source_metadata @@ -825,16 +846,16 @@ def measurement_record_source_names(rows: ColumnarRows) -> tuple[str, ...]: ) @classmethod - def measurement_record_source_metadata( + def measurement_record_source_metadata_projection( cls, request: CellProfilerOutputRecordRequest, rows: ColumnarRows - ) -> ImagePayloadMetadata: + ) -> ImageMetadataProjection: from openhcs.core.artifacts import ( ArtifactSpecCollection, ) source_names = cls.measurement_record_source_names(rows) if not source_names: - return super().measurement_record_source_metadata(request, rows) + return super().measurement_record_source_metadata_projection(request, rows) declared_inputs = ArtifactSpecCollection( request.callable_contract.artifact_inputs.specs @@ -849,7 +870,7 @@ def measurement_record_source_metadata( ) source_specs.append(spec) - return request.measurement_source_metadata(tuple(source_specs)) + return request.measurement_source_metadata_projection(tuple(source_specs)) @classmethod def clear_source_when_rows_declare_object_name(cls) -> bool: diff --git a/openhcs/interop/cellprofiler/runtime/module_execution.py b/openhcs/interop/cellprofiler/runtime/module_execution.py index 170b24ac37..940f545aff 100644 --- a/openhcs/interop/cellprofiler/runtime/module_execution.py +++ b/openhcs/interop/cellprofiler/runtime/module_execution.py @@ -45,7 +45,7 @@ from openhcs.core.runtime_artifact_queries import MeasurementTableUnion from openhcs.core.runtime_batch_contracts import RuntimeBatchExecutionDomain from openhcs.core.runtime_image_values import ( - image_payload_metadata, + image_payload_metadata_projection, preserved_image_plane_projection, ) from openhcs.core.runtime_measurements import MeasurementTable @@ -722,8 +722,8 @@ def _resolved_measurement_image( ImageArtifactType, ).raw_runtime_input_value(request) ) - metadata = image_payload_metadata(payload) - plane_axis = metadata.plane_axis + metadata = image_payload_metadata_projection(payload) + plane_axis = metadata.read_value("plane_axis") return CellProfilerMeasurementImage( source_image_name=spec.name, source_aliases=source_aliases, @@ -1002,7 +1002,7 @@ def _run_per_object_measurement( measurement_images, source_image_name ) combined_source_metadata = ( - CellProfilerMeasurementImage.composed_source_metadata( + CellProfilerMeasurementImage.composed_source_metadata_projection( measurement_images, mode=measurement_row_policy.source_metadata_composition_mode( measurement_images @@ -1010,7 +1010,7 @@ def _run_per_object_measurement( ) ) if combined_source_metadata is None: - combined_source_metadata = image_payload_metadata( + combined_source_metadata = image_payload_metadata_projection( CellProfilerMeasurementImage.shared_source_payload(measurement_images) ) if profile_enabled: @@ -1298,7 +1298,9 @@ def _image_request( if ( runtime_projection is not None and runtime_projection.plane_index is not None - and image_payload_metadata(current_image_payload).plane_axis + and image_payload_metadata_projection(current_image_payload).read_value( + "plane_axis" + ) is runtime_projection.axis ): current_image_payload = cast( @@ -1334,9 +1336,7 @@ def _image_request( request = input_binding.artifact_request_for_spec(spec) payload = image_strategy.runtime_input_value(request) payloads.append(payload) - source_names.append( - image_strategy.source_image_name_from_value(payload) - ) + source_names.append(image_strategy.source_image_name_from_value(payload)) parameter_image_inputs = input_binding.image_inputs broadcast_sources = tuple( sources[0] @@ -1347,24 +1347,26 @@ def _image_request( align_primary_images = len(image_inputs) > 1 and broadcast_sources == tuple( spec.ref() for spec in image_inputs ) - composition = self.callable_contract.image_payload_consumption.compose_image_payload( - f"{module_type.require_module_name()} image inputs " - f"{tuple(spec.name for spec in image_inputs)!r}", - tuple(payloads), - slice_contexts=( - tuple( - AlignedImageSliceContext.main_flow( - output_key=spec.name, - artifact_kind=spec.artifact_type.value, + composition = ( + self.callable_contract.image_payload_consumption.compose_image_payload( + f"{module_type.require_module_name()} image inputs " + f"{tuple(spec.name for spec in image_inputs)!r}", + tuple(payloads), + slice_contexts=( + tuple( + AlignedImageSliceContext.main_flow( + output_key=spec.name, + artifact_kind=spec.artifact_type.value, + ) + for spec in image_inputs ) - for spec in image_inputs - ) - if align_primary_images - else () - ), - stack_broadcast_source_indices=( - image_input_specs.stack_broadcast_source_indices() - ), + if align_primary_images + else () + ), + stack_broadcast_source_indices=( + image_input_specs.stack_broadcast_source_indices() + ), + ) ) return CellProfilerImageRequest( payload=composition.payload, diff --git a/openhcs/interop/cellprofiler/runtime/output_record_request.py b/openhcs/interop/cellprofiler/runtime/output_record_request.py index 559fda7c41..7f36004c40 100644 --- a/openhcs/interop/cellprofiler/runtime/output_record_request.py +++ b/openhcs/interop/cellprofiler/runtime/output_record_request.py @@ -22,8 +22,9 @@ ) from openhcs.core.runtime_array_values import RuntimeArrayData from openhcs.core.runtime_image_values import ( + ImageMetadataProjection, ImagePayloadMetadata, - image_payload_metadata, + image_payload_metadata_projection, ) from openhcs.core.callable_contract import CallableContract from openhcs.core.function_patterns import InvocationArtifactInputEdgePlan @@ -190,12 +191,22 @@ def measurement_source_metadata( self, specs: tuple[ArtifactSpec, ...], ) -> ImagePayloadMetadata: + """Expose the actual public metadata namespace for exact artifacts.""" + + return self.measurement_source_metadata_projection(specs).materialize_metadata() + + def measurement_source_metadata_projection( + self, + specs: tuple[ArtifactSpec, ...], + ) -> ImageMetadataProjection: """Return the contract-ordered image-set axis of exact artifacts.""" if not specs: raise ValueError("Measurement source context requires declared artifacts.") artifact_values = tuple(self.artifact_value(spec) for spec in specs) - metadata = tuple(image_payload_metadata(value) for value in artifact_values) + metadata = tuple( + image_payload_metadata_projection(value) for value in artifact_values + ) source_group_component = self.output_plan.group_component identity_policy = SourceImageSetIdentityPolicy( frozenset( @@ -271,12 +282,20 @@ def declared_source_payload(self) -> RuntimeCallableArgument: ) def materialization_source_metadata(self) -> ImagePayloadMetadata | None: + """Expose the public filename-source metadata declared by this output.""" + + owner = self.materialization_source_metadata_projection() + return None if owner is None else owner.materialize_metadata() + + def materialization_source_metadata_projection( + self, + ) -> ImageMetadataProjection | None: """Return independent filename-source metadata declared by this output.""" source_ref = self.output_plan.materialization_source() if source_ref is None or source_ref == self.output_plan.source_context_source(): return None - return image_payload_metadata( + return image_payload_metadata_projection( self.artifact_source_payload( self.adapter.request.require_artifact_input_edge(source_ref) ) diff --git a/openhcs/processing/materialization/core.py b/openhcs/processing/materialization/core.py index 7a6f9aee00..07e9ebb92f 100644 --- a/openhcs/processing/materialization/core.py +++ b/openhcs/processing/materialization/core.py @@ -42,10 +42,13 @@ ) from openhcs.core.runtime_array_values import runtime_array_operand from openhcs.core.runtime_image_values import ( + ImageMetadataCarrier, + ImageMetadataProjection, + SourcePlaneImageMetadataProjection, ImagePayloadMetadata, ImagePayloadMetadataCompositionMode, image_payload_data, - image_payload_metadata, + image_payload_metadata_projection, ) from openhcs.core.runtime_object_labels import ( ObjectLabelValue, @@ -274,7 +277,7 @@ class BackendKwargsAbsent: @dataclass(frozen=True) -class Output: +class Output(ImageMetadataCarrier): path: str content: MaterializationValue metadata: ImagePayloadMetadata | None = None @@ -296,9 +299,11 @@ def from_metadata( @property def source_identity(self) -> SourceImageIdentity | None: - if self.metadata is None: + if self.metadata_projection is None: return None - return self.metadata.source_provenance.scalar_source_identity + return self.metadata_projection.read_value( + "source_provenance" + ).scalar_source_identity @property def source_component_metadata(self) -> SourceComponentMetadata | None: @@ -330,8 +335,8 @@ def with_source_identity_fallback( ) metadata = ( fallback_metadata - if self.metadata is None - else self.metadata.with_source_context_from(fallback_metadata) + if self.metadata_projection is None + else self.metadata_projection.with_source_context_from(fallback_metadata) ) return replace( self, @@ -346,10 +351,14 @@ def with_variable_components( return replace( self, + metadata=self.metadata_projection, variable_components=ComponentSet.coerce(variable_components).as_tuple(), ) +Output.metadata = ImageMetadataCarrier.metadata + + @dataclass(frozen=True) class TextOutput(Output): """Materialization output whose canonical payload is text.""" @@ -542,12 +551,14 @@ class ROIMaterializationSourceSpatialDomain: """Resolve the source-spatial domain used by ROI extraction.""" def domain_for_item(self, item: MaterializationInputItem) -> SourceSpatialDomain: - metadata_domain = item.metadata.source_spatial_domain.with_value_name( + metadata_domain = item.metadata_projection.read_value( + "source_spatial_domain" + ).with_value_name( "ROI materialization source image", ) if metadata_domain.source_shape_yx is not None: return metadata_domain - local_shape_yx = item.metadata.spatial_shape_yx(item.data) + local_shape_yx = item.metadata_projection.spatial_shape_yx(item.data) if local_shape_yx is None: return metadata_domain return SourceSpatialDomain( @@ -595,9 +606,9 @@ def from_payloads( plane_count_source for payload in payloads for plane_count_source in ( - image_payload_metadata( - payload - ).source_provenance.plane_count_sources + image_payload_metadata_projection(payload) + .read_value("source_provenance") + .plane_count_sources ) ) ) @@ -702,7 +713,7 @@ def build_for_context( def required_source_stem( self, - metadata: ImagePayloadMetadata, + metadata: ImageMetadataProjection, filename_identity: SourceImageIdentity | None = None, ) -> str: source_stem = SourceStemResolutionPolicy.for_context( @@ -719,9 +730,9 @@ def required_source_stem( def source_replacement_suffix( self, path_name: str, - metadata: ImagePayloadMetadata, + metadata: ImageMetadataProjection, reference_source_stem: str | None, - reference_metadata: ImagePayloadMetadata | None, + reference_metadata: ImageMetadataProjection | None, ) -> str | None: for source_stem in self.source_replacement_stems( path_name, @@ -736,9 +747,9 @@ def source_replacement_suffix( def source_replacement_stems( self, path_name: str, - metadata: ImagePayloadMetadata, + metadata: ImageMetadataProjection, reference_source_stem: str | None, - reference_metadata: ImagePayloadMetadata | None, + reference_metadata: ImageMetadataProjection | None, ) -> tuple[str, ...]: del path_name, metadata, reference_metadata if reference_source_stem is None: @@ -755,7 +766,7 @@ def source_component_metadata_stem( class SourceStemResolutionPolicy( - MostDerivedContextStrategyMixin[ImagePayloadMetadata], + MostDerivedContextStrategyMixin[ImageMetadataProjection], ABC, ): """Resolve source-stem inputs through MRO precedence over payload metadata.""" @@ -764,14 +775,14 @@ class SourceStemResolutionPolicy( def source_stem( self, authority: SourceStemAuthority, - metadata: ImagePayloadMetadata, + metadata: ImageMetadataProjection, filename_identity: SourceImageIdentity | None, ) -> str | None: """Return this source-stem case's projection.""" class MissingSourceStemResolutionPolicy( - AlwaysMatchesContextMixin[ImagePayloadMetadata], + AlwaysMatchesContextMixin[ImageMetadataProjection], SourceStemResolutionPolicy, ): """No addressable source identity is present.""" @@ -781,7 +792,7 @@ class MissingSourceStemResolutionPolicy( def source_stem( self, authority: SourceStemAuthority, - metadata: ImagePayloadMetadata, + metadata: ImageMetadataProjection, filename_identity: SourceImageIdentity | None, ) -> str | None: del authority, metadata, filename_identity @@ -793,21 +804,23 @@ class ComponentMetadataSourceStemResolutionPolicy(MissingSourceStemResolutionPol strategy_key = "component_metadata" - def matches(self, context: ImagePayloadMetadata) -> bool: + def matches(self, context: ImageMetadataProjection) -> bool: return ( - context.source_provenance.scalar_source_identity.component_metadata + context.read_value( + "source_provenance" + ).scalar_source_identity.component_metadata is not None ) def source_stem( self, authority: SourceStemAuthority, - metadata: ImagePayloadMetadata, + metadata: ImageMetadataProjection, filename_identity: SourceImageIdentity | None, ) -> str: - component_metadata = ( - metadata.source_provenance.scalar_source_identity.component_metadata - ) + component_metadata = metadata.read_value( + "source_provenance" + ).scalar_source_identity.component_metadata if component_metadata is None: raise ValueError( "Component metadata source-stem policy requires scalar " @@ -824,18 +837,18 @@ class SourcePathSourceStemResolutionPolicy(ComponentMetadataSourceStemResolution strategy_key = "source_path" - def matches(self, context: ImagePayloadMetadata) -> bool: - return bool(context.source_provenance.scalar_source_identity.path) + def matches(self, context: ImageMetadataProjection) -> bool: + return bool(context.read_value("source_provenance").scalar_source_identity.path) def source_stem( self, authority: SourceStemAuthority, - metadata: ImagePayloadMetadata, + metadata: ImageMetadataProjection, filename_identity: SourceImageIdentity | None, ) -> str: del authority, filename_identity return source_path_identity( - metadata.source_provenance.scalar_source_identity.path + metadata.read_value("source_provenance").scalar_source_identity.path ).stem @@ -907,9 +920,9 @@ def path_component_prefix_stems( path_name: str, metadata: ImagePayloadMetadata, ) -> tuple[str, ...]: - component_metadata = ( - metadata.source_provenance.scalar_source_identity.component_metadata - ) + component_metadata = metadata.read_value( + "source_provenance" + ).scalar_source_identity.component_metadata if component_metadata is None: return () @@ -924,7 +937,9 @@ def path_component_prefix_stems( ) def path_parse_extensions(self, metadata: ImagePayloadMetadata) -> tuple[str, ...]: - source_identity = metadata.source_provenance.scalar_source_identity + source_identity = metadata.read_value( + "source_provenance" + ).scalar_source_identity extension = source_identity.filename_extension if extension is None: extension = source_identity.with_parsed_path_components( @@ -994,7 +1009,7 @@ def from_metadata( source_identity=( None if metadata is None - else metadata.source_provenance.scalar_source_identity + else metadata.read_value("source_provenance").scalar_source_identity ), ) @@ -1628,11 +1643,13 @@ def projected_outputs(self, outputs: Sequence[Output]) -> tuple[Output, ...]: for item in items: identity = FunctionOutputIdentityAuthority.identity_from_metadata( parser, - item.metadata, + item.metadata_projection, fallback_identity_path=output.path, ) if identity is None: - raise ValueError("Projected viewer output requires a filename identity.") + raise ValueError( + "Projected viewer output requires a filename identity." + ) identity = replace(identity, extension=output_path.suffix) filename = FunctionOutputPathAuthority.filename_for_identity( parser, @@ -1643,7 +1660,7 @@ def projected_outputs(self, outputs: Sequence[Output]) -> tuple[Output, ...]: output, path=str(output_path.parent / filename), content=item.data, - metadata=item.metadata, + metadata=item.metadata_projection, ) ) return tuple(result) @@ -1784,7 +1801,9 @@ class MaterializationContext: materialization_spec: MaterializationSpec | None = None def named_source_filename( - self, metadata: ImagePayloadMetadata, extension: str, + self, + metadata: ImagePayloadMetadata, + extension: str, ) -> str | None: """Name a retained image from the actual rendering purpose and role.""" if self.materialization_spec is None: @@ -1792,12 +1811,19 @@ def named_source_filename( qualifier = self.materialization_spec.filename_qualifier(self.output_plan) if qualifier is None: return None - parser = SourceStemAuthoritySelection.from_processing_context(self.context).required_parser() - identity = FunctionOutputIdentityAuthority.filename_identity_from_metadata(parser, metadata) + parser = SourceStemAuthoritySelection.from_processing_context( + self.context + ).required_parser() + identity = FunctionOutputIdentityAuthority.filename_identity_from_metadata( + parser, metadata + ) if identity is None: - raise ValueError("Retained image output has no addressable source filename identity.") + raise ValueError( + "Retained image output has no addressable source filename identity." + ) identity = self.materialization_spec.filename_identity_for_output( - replace(identity, extension=extension), self.output_plan, + replace(identity, extension=extension), + self.output_plan, ) return FunctionOutputPathAuthority.filename_for_identity(parser, identity) @@ -2482,8 +2508,8 @@ def _image_relative_output_path( if options.relative_path_template is None: return context.paths(options).primary_output_path(options) - metadata = image_payload_metadata(data) - source_identity = metadata.source_provenance.scalar_source_identity + metadata = image_payload_metadata_projection(data) + source_identity = metadata.read_value("source_provenance").scalar_source_identity component_metadata = source_identity.component_metadata def replace_backreference(match: re.Match[str]) -> str: @@ -2583,15 +2609,20 @@ def write_image_file( for sequence_index, item in enumerate(projected_items, start=1): if options.relative_path_template is not None: path = _image_relative_output_path( - item.value, options, context, sequence_index=sequence_index, + item.value, + options, + context, + sequence_index=sequence_index, ) elif preserves_planned_path: path = paths.primary_output_path(options) else: - filename = context.named_source_filename(item.metadata, options.primary_output_suffix) + filename = context.named_source_filename( + item.metadata_projection, options.primary_output_suffix + ) if filename is None: filename = ( - source_stem_authority.required_source_stem(item.metadata) + source_stem_authority.required_source_stem(item.metadata_projection) + options.primary_output_suffix ) path = str(paths.parent / filename) @@ -2606,7 +2637,7 @@ def write_image_file( Output.from_metadata( path=path, content=ImageFileFormat.require_path(path).prepare(item.value), - metadata=item.metadata, + metadata=item.metadata_projection, ) for path, item in outputs ] @@ -2694,7 +2725,9 @@ def items(self) -> tuple[MaterializationInputItem, ...]: @property def identities(self) -> tuple: return tuple( - item.metadata.source_provenance.scalar_source_identity.identity + item.metadata_projection.read_value( + "source_provenance" + ).scalar_source_identity.identity for item in self.items ) @@ -2715,9 +2748,9 @@ def has_single_component_address(self) -> bool: if len(self.items) != 1: return False return ( - self.items[ - 0 - ].metadata.source_provenance.scalar_source_identity.component_metadata + self.items[0] + .metadata.read_value("source_provenance") + .scalar_source_identity.component_metadata is not None ) @@ -2801,7 +2834,7 @@ def targets( ROIMaterializationTarget( archive=ROIMaterializationArchiveIdentity.from_metadata( path=request.paths.primary_output_path(request.options), - metadata=item.metadata, + metadata=item.metadata_projection, ), items=(item,), ), @@ -2825,22 +2858,22 @@ def targets( path_context = ROIMaterializationPathContext( source_stems=tuple( request.source_stem_authority.required_source_stem( - item.metadata, + item.metadata_projection, request.artifact_filename_identity, ) for item in items ), - metadata_items=tuple(item.metadata for item in items), + metadata_items=tuple(item.metadata_projection for item in items), ) return tuple( ROIMaterializationTarget( archive=ROIMaterializationArchiveIdentity.from_metadata( path=request.projected_path( - metadata=item.metadata, + metadata=item.metadata_projection, reference_source_stem=path_context.reference_source_stem, reference_metadata=path_context.reference_metadata, ), - metadata=item.metadata, + metadata=item.metadata_projection, ), items=(item,), ) @@ -2959,7 +2992,7 @@ def metadata_for_target( local_plane_count = 1 for extent in local_plane_shape: local_plane_count *= extent - source_plane_count = metadata.source_provenance.source_plane_count + source_plane_count = metadata.read_value("source_provenance").source_plane_count if source_plane_count not in (0, local_plane_count): raise ValueError( "ROI output source-plane provenance does not match its materialized " @@ -3066,9 +3099,11 @@ def _write_roi_zip( total_roi_count += len(target_rois) if target_rois: roi_paths.append(target.archive.path) - item_metadata = target.items[0].metadata + item_metadata = target.items[0].metadata_projection item_metadata = item_metadata.with_source_provenance( - item_metadata.source_provenance.with_common_scalar_identity_from_planes() + item_metadata.read_value( + "source_provenance" + ).with_common_scalar_identity_from_planes() ) item_metadata = ( ROIMaterializationPlaneMetadataAuthority.metadata_for_target( @@ -3083,9 +3118,9 @@ def _write_roi_zip( SourceImageProvenance( source_path=source_identity.path, source_component_metadata=source_identity.component_metadata, - ).with_missing_from(item_metadata.source_provenance) + ).with_missing_from(item_metadata.read_value("source_provenance")) ) - item_metadata = item_metadata.replace_fields( + item_metadata = item_metadata.derive_fields( source_spatial_domain=source_domain_authority.domain_for_target(target) ) outs.append( @@ -3423,7 +3458,7 @@ def slices( "explicit payload sequence." ) item = materialization_input.items[0] - if item.metadata.plane_axis is None: + if item.metadata_projection.read_value("plane_axis") is None: return (item.data,) data = np.asarray(item.data) if data.ndim < 3 or data.shape[0] <= 0: @@ -3638,7 +3673,7 @@ def metadata_for_item( self, item: MaterializationInputItem, ) -> ImagePayloadMetadata: - metadata = self.metadata_with_artifact_source_identity(item.metadata) + metadata = self.metadata_with_artifact_source_identity(item.metadata_projection) runtime_plane = item.runtime_plane if runtime_plane is None or not self.variable_components: return metadata @@ -3653,7 +3688,7 @@ def metadata_for_item( f"{len(runtime_plane.plane_indices)} coordinate(s)." ) - component_metadata = metadata.source_component_metadata + component_metadata = metadata.source_component_metadata_for_projection() if component_metadata is None: raise ValueError( "TIFF stack output uses variable components " @@ -3669,7 +3704,7 @@ def metadata_for_item( ) return metadata.with_source_provenance( - metadata.source_provenance.with_source_component_metadata( + metadata.read_value("source_provenance").with_source_component_metadata( projected_metadata, ) ) @@ -3685,7 +3720,9 @@ def metadata_with_artifact_source_identity( source_component_metadata=self.artifact_source_identity.component_metadata, ) return metadata.with_source_provenance( - metadata.source_provenance.with_missing_from(artifact_provenance) + metadata.read_value("source_provenance").with_missing_from( + artifact_provenance + ) ) def ordered_axes(self) -> tuple[str, ...]: @@ -3815,7 +3852,9 @@ def indexed_source_plane_axis( def item_component_metadata(self) -> tuple[SourceComponentMetadata, ...]: metadata_values = tuple( - self.effective_item_metadata(item).source_component_metadata + self.effective_item_metadata( + item + ).source_component_metadata_for_projection() for item in self.items ) if any(metadata is None for metadata in metadata_values): @@ -3829,7 +3868,7 @@ def effective_item_metadata( return RuntimePlaneStackAxisMetadataProjection( frozenset(), self.artifact_source_identity, - ).metadata_with_artifact_source_identity(item.metadata) + ).metadata_with_artifact_source_identity(item.metadata_projection) @dataclass(frozen=True, slots=True) @@ -3934,9 +3973,10 @@ def metadata_for_slices( self, request: TiffStackSliceMetadataRequest, ) -> tuple[ImagePayloadMetadata, ...]: - metadata = request.materialization_input.items[0].metadata + metadata = request.materialization_input.items[0].metadata_projection return tuple( - metadata.for_source_plane(index) for index in range(request.slice_count) + SourcePlaneImageMetadataProjection(metadata, index).capture() + for index in range(request.slice_count) ) @@ -4124,14 +4164,18 @@ def emitted_source_identities( ) ) if len(materialization_input.items) == 1: - provenance = materialization_input.items[0].metadata.source_provenance + provenance = materialization_input.items[ + 0 + ].metadata_projection.read_value("source_provenance") if provenance.source_plane_count > 1: return tuple( provenance.for_source_plane(index).scalar_source_identity for index in range(provenance.source_plane_count) ) return tuple( - item.metadata.source_provenance.scalar_source_identity + item.metadata_projection.read_value( + "source_provenance" + ).scalar_source_identity for item in materialization_input.items ) return () diff --git a/tests/unit/test_cellprofiler_metadata_projection_consumers.py b/tests/unit/test_cellprofiler_metadata_projection_consumers.py new file mode 100644 index 0000000000..e09a0e9aed --- /dev/null +++ b/tests/unit/test_cellprofiler_metadata_projection_consumers.py @@ -0,0 +1,414 @@ +"""Metadata-owner request laws, independent of public namespace allocation.""" + +from dataclasses import dataclass +from dataclasses import replace +import gc +import weakref +from types import SimpleNamespace + +import numpy as np +import pytest + +from openhcs.core.aligned_image_payload import ImagePayloadExecutionMode +from openhcs.core.artifacts import ( + ArtifactSpec, + ImageArtifactType, + MeasurementsArtifactType, + ArtifactOutputPlan, +) +from openhcs.core.callable_contract import CallableContract, CallableMetadata +from openhcs.core.runtime_image_values import ( + ImageMetadataPayload, + ImageMetadataProjection, + ImagePayloadMetadata, + image_payload_data, + image_payload_mask, + image_payload_metadata_projection, +) +from openhcs.core.source_metadata import SOURCE_VOXEL_SPACING_FIELD +from openhcs.core.runtime_object_labels import ( + ObjectLabelPayload, + ObjectLabelVariantData, +) +from openhcs.core.runtime_plane_projection import ( + RuntimePlaneAxis, + RuntimePlaneAxisValueProjection, +) +from openhcs.core.steps.function_output_identity import FunctionOutputIdentityAuthority +from openhcs.microscopes.source_schema import SourceSchemaFilenameParser +from openhcs.interop.cellprofiler.runtime.artifact_binding import ( + ImageArtifactTypeStrategy, + RuntimeArtifactInputRequest, +) +from openhcs.interop.cellprofiler.runtime.invocation import ( + CellProfilerImageRequest, + CellProfilerSourceIdentityMixin, +) +from openhcs.interop.cellprofiler.runtime.output_record_request import ( + CellProfilerOutputRecordRequest, +) +from openhcs.interop.cellprofiler.runtime.main_flow import cellprofiler_main_flow_output +from openhcs.interop.cellprofiler.module_declarations import CellProfilerModule +from openhcs.interop.cellprofiler.runtime.measurement_recording import ( + CellProfilerMeasurementTableModule, +) +from openhcs.core.measurement_row_materialization import MeasurementSparseColumnarRows + + +def _metadata(**changes): + return ImagePayloadMetadata( + source_path="/input/A01_s001_w1_z001_t001.tif", + source_component_metadata={ + "well": "A01", + "site": "1", + "channel": "1", + "z_index": "1", + "timepoint": "1", + "extension": ".tif", + }, + **changes, + ) + + +def _request(payload): + return CellProfilerImageRequest( + payload=payload, + source_image_name="DNA", + image_count=1, + execution_mode=ImagePayloadExecutionMode.NATURAL, + ) + + +def test_label_projection_preserves_computed_public_namespace_and_freshness(): + labels = ObjectLabelPayload( + variant_data=ObjectLabelVariantData(labels=np.ones((3, 4), dtype=np.int32)), + source_path="/input/labels.tif", + ) + owner = labels.metadata_projection + public = owner.materialize_metadata() + assert type(public) is ImagePayloadMetadata + assert public is owner.materialize_metadata() + assert public is not labels.metadata + public.source_path = "/input/consumer.tif" + assert labels.source_path == "/input/labels.tif" + assert owner.read_value("source_provenance").source_path == "/input/consumer.tif" + labels.source_path = "/input/new-labels.tif" + fresh = labels.metadata_projection + assert fresh.read_value("source_provenance").source_path == "/input/new-labels.tif" + assert owner.read_value("source_provenance").source_path == "/input/consumer.tif" + + +def test_label_projection_retains_no_pixels_or_label_receiver(): + pixels = np.ones((3, 4), dtype=np.int32) + labels = ObjectLabelPayload( + variant_data=ObjectLabelVariantData(labels=pixels), + source_path="/input/labels.tif", + ) + pixel_ref, label_ref = weakref.ref(pixels), weakref.ref(labels) + owner = labels.metadata_projection + del pixels, labels + gc.collect() + assert pixel_ref() is None and label_ref() is None + assert owner.read_value("source_provenance").source_path == "/input/labels.tif" + + +def test_label_projection_preserves_actual_overridden_metadata_error(): + error = ValueError("actual computed metadata error") + + class RefusingLabels(ObjectLabelPayload): + @property + def metadata(self): + raise error + + labels = RefusingLabels( + variant_data=ObjectLabelVariantData(labels=np.zeros((2, 3), dtype=np.int32)) + ) + with pytest.raises(ValueError) as caught: + image_payload_metadata_projection(labels) + assert caught.value is error + + +def test_artifact_alias_projection_preserves_pixels_mask_and_independent_metadata( + monkeypatch, +): + pixels = np.arange(12, dtype=np.float32).reshape(3, 4) + mask = np.ones_like(pixels, dtype=bool) + owner = _metadata(source_image_names=("Original",)).capture() + payload = owner.payload_with(pixels, mask) + spec = ArtifactSpec.input("DNA", ImageArtifactType) + + def no_public_namespace(self): + raise AssertionError("An internal input binding forced the public namespace.") + + monkeypatch.setattr( + ImageMetadataProjection, "materialize_metadata", no_public_namespace + ) + derived = ImageArtifactTypeStrategy().raw_runtime_input_value( + RuntimeArtifactInputRequest(spec=spec, value=payload) + ) + assert image_payload_data(derived) is pixels + assert image_payload_mask(derived) is mask + derived_owner = image_payload_metadata_projection(derived) + assert derived_owner is not owner + assert ImageArtifactTypeStrategy().source_image_name_from_value(derived) is None + assert derived_owner.read_value( + "source_provenance" + ).represented_source_image_names == ( + owner.read_value("source_provenance") + .with_derived_source_image_names(("DNA",)) + .represented_source_image_names + ) + assert owner.read_value("source_provenance").source_image_names == ("Original",) + + +def test_shared_source_payload_query_preserves_original_value_identity(monkeypatch): + owner = _metadata().capture() + first = ImageMetadataPayload.from_projection(np.zeros((3, 4)), owner) + second = ImageMetadataPayload.from_projection(np.ones((3, 4)), owner) + + def no_public_namespace(self): + raise AssertionError("A source query forced the public namespace.") + + monkeypatch.setattr( + ImageMetadataProjection, "materialize_metadata", no_public_namespace + ) + assert ( + CellProfilerSourceIdentityMixin.shared_source_payload( + (_request(first), _request(second)) + ) + is first + ) + assert CellProfilerSourceIdentityMixin.shared_source_payload(()) is None + + +def test_opaque_main_flow_returns_exact_output_without_namespace(monkeypatch): + payload = ImageMetadataPayload.from_projection( + np.ones((3, 4)), _metadata().capture() + ) + + def no_public_namespace(self): + raise AssertionError("An opaque main-flow read forced public metadata.") + + monkeypatch.setattr( + ImageMetadataProjection, "materialize_metadata", no_public_namespace + ) + assert cellprofiler_main_flow_output(payload, payload, None) is payload + + +def test_main_flow_preserves_output_axis_error_before_input_access(monkeypatch): + output = ImageMetadataPayload.from_projection( + np.ones((2, 3, 4)), + _metadata(plane_axis=RuntimePlaneAxis.RUNTIME_SLICE).capture(), + ) + projection = RuntimePlaneAxisValueProjection.preserve( + axis=RuntimePlaneAxis.SOURCE_BINDING, axis_size=2 + ) + + def no_public_namespace(self): + raise AssertionError("The malformed output forced public metadata.") + + monkeypatch.setattr( + ImageMetadataProjection, "materialize_metadata", no_public_namespace + ) + with pytest.raises(ValueError, match="output axis conflicts"): + cellprofiler_main_flow_output(object(), output, projection) + + +@dataclass +class ComponentAliasMetadata(ImagePayloadMetadata): + @property + def source_component_metadata(self): + value = super().source_component_metadata + return {**(value or {}), SOURCE_VOXEL_SPACING_FIELD: "1.0,2.5,2.5"} + + +def test_artifact_binding_keeps_real_subtype_component_alias_calibration(): + metadata = ComponentAliasMetadata(source_path="/input/calibrated.tif") + original_spacing = metadata.source_voxel_spacing + payload = metadata.capture().payload_with(np.ones((3, 4), dtype=np.float32)) + request = RuntimeArtifactInputRequest( + spec=ArtifactSpec.input("DNA", ImageArtifactType), value=payload + ) + derived = ImageArtifactTypeStrategy().raw_runtime_input_value(request) + owner = image_payload_metadata_projection(derived) + public = owner.materialize_metadata() + assert type(public) is ComponentAliasMetadata + assert public.source_component_metadata[SOURCE_VOXEL_SPACING_FIELD] == "1.0,2.5,2.5" + assert public.source_voxel_spacing == original_spacing + assert public.source_voxel_spacing.has_values + + +class DifferentComponentAliasMetadata(ImagePayloadMetadata): + @property + def source_component_metadata(self): + return {**(super().source_component_metadata or {}), "channel": "7"} + + +class DifferentPathAliasMetadata(ImagePayloadMetadata): + @property + def source_path(self): + return "/input/A01_s001_w7_z001_t001.tif" + + +def test_identity_preserves_determining_component_alias_value(): + metadata = DifferentComponentAliasMetadata( + source_path="/input/A01_s001_w1_z001_t001.tif", + source_component_metadata={ + "well": "A01", + "site": "1", + "channel": "1", + "z_index": "1", + "timepoint": "1", + }, + ) + assert metadata.source_provenance.source_component_metadata["channel"] == "1" + parser = SourceSchemaFilenameParser() + expected = FunctionOutputIdentityAuthority.identity_from_metadata(parser, metadata) + assert expected.component_values["channel"] == 7 + actual = FunctionOutputIdentityAuthority.identity_from_metadata( + parser, metadata.capture() + ) + assert actual == expected + + +def test_identity_preserves_determining_path_alias_value(): + metadata = DifferentPathAliasMetadata( + source_path="/input/A01_s001_w1_z001_t001.tif", + source_component_metadata={ + "well": "A01", + "site": "1", + "z_index": "1", + "timepoint": "1", + }, + ) + assert metadata.source_provenance.source_path == "/input/A01_s001_w1_z001_t001.tif" + parser = SourceSchemaFilenameParser() + expected = FunctionOutputIdentityAuthority.identity_from_metadata(parser, metadata) + assert expected.component_values["channel"] == 7 + actual = FunctionOutputIdentityAuthority.identity_from_metadata( + parser, metadata.capture() + ) + assert actual == expected + + +def test_public_composed_source_metadata_keeps_actual_namespace_contract(): + payload = _metadata().capture().payload_with(np.ones((3, 4), dtype=np.float32)) + source = _request(payload) + owner = CellProfilerSourceIdentityMixin.composed_source_metadata_projection( + (source,) + ) + public = CellProfilerSourceIdentityMixin.composed_source_metadata((source,)) + assert type(public) is ImagePayloadMetadata + assert public == owner.materialize_metadata() + assert CellProfilerSourceIdentityMixin.composed_source_metadata(()) is None + + +def _record_request(): + output = ArtifactSpec.output("Measurements", MeasurementsArtifactType) + payload = _metadata().capture().payload_with(np.ones((3, 4), dtype=np.float32)) + return CellProfilerOutputRecordRequest( + callable_contract=CallableContract( + func=lambda image: image, + function_name="metadata_override", + module_name=__name__, + metadata=CallableMetadata(artifact_outputs=(output,)), + ), + active_input_edges=(), + adapter=SimpleNamespace(), + spec=output, + output_plan=ArtifactOutputPlan( + name=output.name, + path="/memory/measurements.pkl", + artifact_type=output.artifact_type, + ), + output_value=MeasurementSparseColumnarRows.from_rows((), fields=()), + source=_request(payload), + call_kwargs={}, + current_image=payload, + ) + + +def test_measurement_table_preserves_external_public_override_dispatch(): + rows = MeasurementSparseColumnarRows.from_rows((), fields=()) + metadata = _metadata() + + class ExternalModule(CellProfilerModule): + @classmethod + def measurement_record_rows(cls, request): + return rows + + @classmethod + def measurement_record_object_name(cls, request, rows): + return None + + @classmethod + def measurement_record_source_image_name(cls, request, rows): + return "DNA" + + @classmethod + def measurement_record_source_metadata(cls, request, rows): + metadata.source_path = "/input/external-override.tif" + return metadata + + @classmethod + def measurement_record_source_metadata_projection(cls, request, rows): + raise AssertionError("Public module override was bypassed.") + + table = ExternalModule.measurement_table(_record_request()) + assert table.source_provenance.source_path == "/input/external-override.tif" + + +def test_measurement_table_preserves_external_public_error_identity(): + error = ValueError("external public metadata failure") + rows = MeasurementSparseColumnarRows.from_rows((), fields=()) + + class ExternalModule(CellProfilerModule): + @classmethod + def measurement_record_rows(cls, request): + return rows + + @classmethod + def measurement_record_object_name(cls, request, rows): + return None + + @classmethod + def measurement_record_source_image_name(cls, request, rows): + return "DNA" + + @classmethod + def measurement_record_source_metadata(cls, request, rows): + raise error + + with pytest.raises(ValueError) as caught: + ExternalModule.measurement_table(_record_request()) + assert caught.value is error + + +def test_measurement_source_preserves_external_public_composition_override(): + error = ValueError("external source composition error") + + class ExternalSource(CellProfilerImageRequest): + @classmethod + def composed_source_metadata(cls, sources, *, mode=None): + raise error + + @classmethod + def composed_source_metadata_projection(cls, sources, *, mode=None): + raise AssertionError("Public source composition override was bypassed.") + + request = _record_request() + request = replace( + request, + source=ExternalSource( + payload=request.current_image, + source_image_name="DNA", + image_count=1, + execution_mode=ImagePayloadExecutionMode.NATURAL, + ), + ) + with pytest.raises(ValueError) as caught: + CellProfilerMeasurementTableModule.measurement_record_source_metadata( + request, + request.output_value, + ) + assert caught.value is error diff --git a/tests/unit/test_function_output_metadata_projection.py b/tests/unit/test_function_output_metadata_projection.py new file mode 100644 index 0000000000..3d18975bf4 --- /dev/null +++ b/tests/unit/test_function_output_metadata_projection.py @@ -0,0 +1,335 @@ +"""Public identity and manifest laws for the shared metadata-only capability.""" + +from dataclasses import FrozenInstanceError, dataclass, fields, replace +import copyreg +import gc +import pickle +from pathlib import Path +from types import MappingProxyType +import weakref + +import numpy as np +import pytest + +from openhcs.constants.constants import VariableComponents +from openhcs.core.aligned_image_payload import AlignedImageSliceContext +from openhcs.core.artifacts import ImageArtifactType +from openhcs.core.runtime_image_values import ( + ImageMetadataCarrier, + ImageMetadataPayload, + ImageMetadataProjection, + ImagePayloadMetadata, + ImagePayloadMetadataCarrier, + image_payload_data, + image_payload_metadata_projection, +) +from openhcs.core.source_image_provenance import ( + SourceImageProvenance, + SourceImageProvenancePlanes, +) +from openhcs.core.steps.function_output_identity import ( + FunctionOutputIdentity, + FunctionOutputIdentityAuthority, + FunctionOutputIdentityCache, + FunctionOutputMetadataIdentityCacheKey, + FunctionOutputPathRequest, +) +from openhcs.core.steps.function_output_manifest import ProducedOutputSemantics +from openhcs.microscopes.source_schema import SourceSchemaFilenameParser +from openhcs.serialization.json import to_jsonable + +from tests.unit.test_function_outputs import function_step_plan + + +@dataclass +class CalibratedMetadata(ImagePayloadMetadata): + calibration: float = 1.25 + + def __post_init__(self, *values): + super().__post_init__(*values) + self.constructor_note = "actual subtype namespace" + + +def _metadata(metadata_type=ImagePayloadMetadata): + return metadata_type( + source_path="/input/A01_s001_w1_z001_t001.tif", + source_component_metadata={ + "well": "A01", + "site": "1", + "channel": "1", + "z_index": "1", + "timepoint": "1", + "extension": ".tif", + }, + source_dtype="float32", + ) + + +def _record(owner): + return ProducedOutputSemantics.from_output( + function_step_plan("Produce"), + "/tmp/output/A01_s001_w1_z001_t001.tif", + FunctionOutputIdentity( + component_values={ + "well": "A01", + "site": 1, + "channel": 1, + "z_index": 1, + "timepoint": 1, + }, + extension=".tif", + source="actual public source declaration", + ), + output_context=AlignedImageSliceContext.main_flow( + output_key="DNA", + artifact_kind=ImageArtifactType.value, + ), + image_metadata=owner, + ) + + +def test_identity_reads_owner_without_public_namespace_and_preserves_cache_key( + monkeypatch, +): + parser = SourceSchemaFilenameParser() + metadata = _metadata() + expected = FunctionOutputIdentityAuthority.identity_from_metadata(parser, metadata) + owner = metadata.capture() + payload = ImageMetadataPayload.from_projection( + np.zeros((3, 4), dtype=np.float32), owner + ) + cache = FunctionOutputIdentityCache() + request = FunctionOutputPathRequest( + parser, Path("/tmp/output"), payload, None, identity_cache=cache + ) + + def unexpected_public_namespace(self): + raise AssertionError("Identity query forced its public metadata namespace.") + + monkeypatch.setattr( + ImageMetadataProjection, "materialize_metadata", unexpected_public_namespace + ) + actual = FunctionOutputIdentityAuthority.identity(request) + assert actual == expected + assert FunctionOutputIdentityAuthority.identity(request) is actual + assert list(cache.metadata_identities) == [ + FunctionOutputMetadataIdentityCacheKey( + parser_id=id(parser), + source_provenance_identity=owner.read_value( + "source_provenance" + ).equality_identity, + fallback_identity_path=None, + identity_components=(), + input_aligned_output=False, + ) + ] + + +def test_collapsed_contributors_keep_distinct_filename_and_semantic_components(): + metadata = ImagePayloadMetadata( + source_provenance=SourceImageProvenance( + source_component_metadata={ + "well": "A01", + "channel": "1", + "z_index": "1", + "timepoint": "1", + "extension": ".tif", + }, + source_image_provenance_planes=SourceImageProvenancePlanes.from_contributor_components( + paths=( + "/input/A01_s001_w1_z001_t001.tif", + "/input/A01_s002_w1_z001_t001.tif", + ), + component_metadata=({"site": "1"}, {"site": "2"}), + ), + ) + ) + parser = SourceSchemaFilenameParser() + kwargs = {"variable_components": (VariableComponents.SITE,)} + public = FunctionOutputIdentityAuthority.identity_from_metadata( + parser, metadata, **kwargs + ) + projected = FunctionOutputIdentityAuthority.identity_from_metadata( + parser, metadata.capture(), **kwargs + ) + assert projected == public + assert "site" not in projected.component_values + assert projected.filename_values["site"] == 1 + + +def test_missing_extension_error_is_preserved_for_owner(): + metadata = ImagePayloadMetadata( + source_component_metadata={ + "well": "A01", + "site": "1", + "channel": "1", + "z_index": "1", + "timepoint": "1", + } + ) + payload = ImageMetadataPayload.from_projection(np.zeros((3, 4)), metadata.capture()) + request = FunctionOutputPathRequest( + SourceSchemaFilenameParser(), Path("/tmp/output"), payload, None + ) + with pytest.raises(ValueError, match="no source path or extension"): + FunctionOutputIdentityAuthority.identity(request) + + +def test_manifest_preserves_exact_owner_and_pass_through_without_namespace(monkeypatch): + owner = _metadata().capture() + payload = ImageMetadataPayload.from_projection(np.zeros((3, 4)), owner) + record = _record(owner) + assert record.metadata_projection is image_payload_metadata_projection(payload) + + def unexpected_public_namespace(self): + raise AssertionError("Manifest lifecycle forced public metadata.") + + monkeypatch.setattr( + ImageMetadataProjection, "materialize_metadata", unexpected_public_namespace + ) + passed = record.passed_through(function_step_plan("Next", pipeline_position=4)) + assert passed.metadata_projection is owner + assert passed.filename_address == record.filename_address + assert passed.component_values == record.component_values + assert ( + passed.producer_identity.step_scope_id != record.producer_identity.step_scope_id + ) + + +def test_manifest_has_metadata_capability_without_image_role_or_new_fields(): + record = _record(_metadata().capture()) + assert isinstance(record, ImageMetadataCarrier) + assert not isinstance(record, ImagePayloadMetadataCarrier) + assert not hasattr(record, "__dict__") + assert not hasattr(record, "image_data") + assert image_payload_data(record) is record + names = tuple(member.name for member in fields(record)) + assert names == ( + "component_values", + "extension", + "source", + "filename_component_values", + "filename_qualifier", + "producer_identity", + "output_path", + "relative_output_path", + "image_metadata", + "main_flow_plane_axis", + ) + wire = to_jsonable(record) + assert tuple(wire) == names + assert "_metadata_projection" not in wire + assert record.image_metadata is record.image_metadata + + +def test_image_carrier_still_requires_image_data(): + class MissingImageData(ImagePayloadMetadataCarrier): + pass + + with pytest.raises(TypeError, match="image_data"): + MissingImageData() + payload = ImageMetadataPayload.from_projection( + np.zeros((3, 4)), _metadata().capture() + ) + assert isinstance(payload, ImageMetadataCarrier) + assert isinstance(payload, ImagePayloadMetadataCarrier) + assert image_payload_data(payload) is payload.data + + +def test_public_mutation_and_subtype_share_the_same_payload_namespace(): + owner = _metadata(CalibratedMetadata).capture() + payload = ImageMetadataPayload.from_projection(np.zeros((3, 4)), owner) + record = _record(owner) + public = record.image_metadata + assert type(public) is CalibratedMetadata + assert public.constructor_note == "actual subtype namespace" + assert public is payload.metadata + public.calibration = 2.5 + public.source_path = "/input/changed.tif" + assert owner.read_value("calibration") == 2.5 + assert owner.read_value("source_provenance").source_path == "/input/changed.tif" + with pytest.raises(FrozenInstanceError): + record.image_metadata = _metadata() + + +def test_public_source_mutation_keeps_original_cache_epoch_law(): + metadata = _metadata() + owner = metadata.capture() + record = _record(owner) + parser = SourceSchemaFilenameParser() + cache = FunctionOutputIdentityCache() + first = FunctionOutputIdentityAuthority.identity_from_metadata_with_cache( + parser, owner, identity_cache=cache + ) + original_key = next(iter(cache.metadata_identities)) + record.image_metadata.source_path = "/input/changed.tif" + second = FunctionOutputIdentityAuthority.identity_from_metadata_with_cache( + parser, owner, identity_cache=cache + ) + assert second == first + assert len(cache.metadata_identities) == 2 + assert ( + original_key.source_provenance_identity + != owner.read_value("source_provenance").equality_identity + ) + + +def test_manifest_never_retains_pixels_or_payload(): + pixels = np.ones((2, 3, 4), dtype=np.float32) + owner = _metadata().capture() + payload = ImageMetadataPayload.from_projection(pixels, owner) + pixel_ref = weakref.ref(pixels) + payload_ref = weakref.ref(payload) + record = _record(image_payload_metadata_projection(payload)) + del pixels, payload + gc.collect() + assert pixel_ref() is None + assert payload_ref() is None + assert record.metadata_projection is owner + + +def test_replace_and_pickle_keep_original_public_dataclass_semantics(monkeypatch): + record = _record(_metadata(CalibratedMetadata).capture()) + public = record.image_metadata + copied = replace(record) + assert copied.image_metadata is public + # Other integrations can register a mappingproxy reducer globally. Exercise + # the standard-pickle boundary independently of test collection order. + monkeypatch.delitem(copyreg.dispatch_table, MappingProxyType, raising=False) + # Standard pickle already rejects the public metadata mappingproxy. + with pytest.raises(TypeError, match="mappingproxy"): + pickle.dumps(record) + absent = _record(None) + restored = pickle.loads(pickle.dumps(absent)) + assert restored == absent + assert restored.metadata_projection is None + assert restored.image_metadata is None + assert not hasattr(restored, "__dict__") + + +def test_contextualization_uses_record_owner_preserving_borrowed_pixels(): + owner = _metadata().capture() + pixels = np.ones((3, 4), dtype=np.float32) + record = _record(owner) + contextualized = record.contextualize_image_payload(pixels) + assert image_payload_data(contextualized) is pixels + projected = image_payload_metadata_projection(contextualized) + assert ( + projected.read_value("source_provenance").source_component_metadata["site"] + == "1" + ) + assert ( + owner.read_value("source_provenance").source_component_metadata["site"] == "1" + ) + + +def test_absent_manifest_metadata_stays_absent(): + record = _record(None) + assert record.metadata_projection is None + assert record.image_metadata is None + assert ( + record.passed_through( + function_step_plan("Next", pipeline_position=4) + ).image_metadata + is None + ) diff --git a/tests/unit/test_image_metadata_owner_consumers.py b/tests/unit/test_image_metadata_owner_consumers.py new file mode 100644 index 0000000000..35b0a35176 --- /dev/null +++ b/tests/unit/test_image_metadata_owner_consumers.py @@ -0,0 +1,151 @@ +"""Real loader/workspace/format consumers retain one metadata owner lifecycle.""" + +from dataclasses import dataclass, fields + +import numpy as np +import pytest + +from openhcs.core.artifacts import ImageArtifactType +from openhcs.core.image_file_serialization import ( + ImageFileFormat, + ImageFileSourceMetadata, +) +from openhcs.core.runtime_image_loading import ImagePayloadSourceMetadataContext +from openhcs.core.runtime_image_values import ( + ImageMetadataProjection, + ImagePayloadMetadata, + ImageUnitIntervalIntensityMetadata, + image_payload_data, + image_payload_metadata, + image_payload_metadata_projection, +) +from openhcs.core.source_image_provenance import ( + SourceImageIdentity, + SourceImageProvenance, +) +from openhcs.core.source_metadata import DurableSourceMetadata, SourceVoxelSpacing +from openhcs.core.source_spatial_domain import SourceSpatialDomain +from openhcs.core.source_workspace_projection import ( + VirtualWorkspaceImagePayloadProjection, +) +from openhcs.serialization.json import to_jsonable + + +@dataclass +class CalibratedMetadata(ImagePayloadMetadata): + calibration: float = 3.0 + + +def test_loader_owner_preserves_virtual_identity_and_public_mutable_contract(): + image = np.arange(12, dtype=np.uint16).reshape(3, 4) + context = ImagePayloadSourceMetadataContext( + SourceImageIdentity( + "/virtual/A01_s001_w1_z001_t001.tif", + DurableSourceMetadata.from_mapping({"well": "A01", "channel": "1"}), + ) + ) + owner = context.metadata_projection(image) + assert isinstance(owner, ImageMetadataProjection) + actual = owner.materialize_metadata() + assert type(actual) is ImagePayloadMetadata + assert actual.source_path == context.source_path + assert actual.source_dtype == "uint16" + assert dict(actual.source_component_metadata) == {"well": "A01", "channel": "1"} + assert actual.source_spatial_shape_yx == image.shape + assert to_jsonable(context.metadata(image)) == to_jsonable(actual) + actual.source_dtype = "float32" + assert owner.read_value("source_dtype") == "float32" + + +def test_workspace_owner_preserves_declared_omissions_alias_and_mutation(): + loaded = ImagePayloadMetadata( + source_path="/storage/raw.tif", + source_spatial_domain=SourceSpatialDomain((1, 2), (12, 14)), + source_voxel_spacing=SourceVoxelSpacing((0.5, 0.75)), + source_dtype="float32", + ) + persisted = CalibratedMetadata( + source_provenance=SourceImageProvenance( + source_component_metadata={"well": "A01"}, + source_image_names=("declared",), + ), + ) + # These are live public facts despite the provenance's stored birth identity. + persisted.source_provenance.source_image_names = ("changed-before-apply",) + projection = VirtualWorkspaceImagePayloadProjection(persisted_metadata=persisted) + owner = projection.metadata_projection(loaded) + expected = projection.metadata(loaded) + actual = owner.materialize_metadata() + assert type(actual) is CalibratedMetadata + assert actual.calibration == 3.0 + assert actual.source_path is None + assert actual.source_image_names == ("changed-before-apply",) + assert to_jsonable(actual) == to_jsonable(expected) + actual.source_image_names = ("one-output-only",) + assert persisted.source_image_names == ("changed-before-apply",) + aliased = VirtualWorkspaceImagePayloadProjection( + persisted_metadata=persisted, source_alias="DNA" + ).apply(loaded.payload_with(np.ones((3, 4), dtype=np.float32))) + assert image_payload_metadata(aliased).source_image_names == ("DNA",) + assert image_payload_data(aliased).shape == (3, 4) + + +@pytest.mark.parametrize("values_preserved", (False, True)) +def test_format_owner_retains_subtype_schema_and_native_value_facts(values_preserved): + metadata = CalibratedMetadata( + source_path="/input/image.tif", + source_dtype="float32", + intensity_scale=1.0, + unit_interval_intensity=ImageUnitIntervalIntensityMetadata(255), + physical_border_edges_yx=(True, False, False, True), + mask_defines_border=True, + ) + header = ImageFileSourceMetadata(np.dtype("uint8"), 255) + owner = header.project_image_metadata_projection( + metadata, values_preserved=values_preserved + ) + actual = owner.materialize_metadata() + public = header.project_image_metadata(metadata, values_preserved=values_preserved) + assert type(actual) is type(public) is CalibratedMetadata + assert {member.name for member in fields(actual)} == set(to_jsonable(actual)) + assert to_jsonable(actual) == to_jsonable(public) + assert actual.source_dtype == "uint8" + assert actual.intensity_scale == 255 + assert actual.source_path == metadata.source_path + if not values_preserved: + assert actual.unit_interval_intensity.scale is None + assert actual.physical_border_edges_yx is None + assert actual.mask_defines_border is None + assert metadata.source_dtype == "float32" + + +def test_actual_saved_tiff_owner_and_public_contract_share_native_facts(tmp_path): + data = np.arange(12, dtype=np.uint16).reshape(3, 4) + payload = ImagePayloadMetadata( + source_path="/source/virtual.tif", source_dtype="uint16" + ).payload_with(data) + path = tmp_path / "output.tif" + image_format = ImageFileFormat.require_path(path) + image_format.write(path, payload) + owner = image_format.persisted_metadata_projection(path, payload) + assert to_jsonable(owner.materialize_metadata()) == to_jsonable( + image_format.persisted_metadata(path, payload) + ) + np.testing.assert_array_equal(image_format.read(path), data) + + +def test_named_image_artifact_projects_owned_alias_without_changing_pixels(): + data = np.ones((3, 4), dtype=np.float32) + original = ImagePayloadMetadata( + source_path="/input/image.tif", source_image_names=("DNA",) + ).payload_with(data) + named = ImageArtifactType.normalize_runtime_payload("DerivedDNA", original) + owner = image_payload_metadata_projection(named) + assert owner.read_value("source_provenance").source_image_names == ("DerivedDNA",) + assert image_payload_data(named) is data + assert image_payload_metadata(original).source_image_names == ("DNA",) + public = image_payload_metadata(named) + assert public is image_payload_metadata(named) + public.source_path = "/changed/output.tif" + assert owner.read_value("source_provenance").source_path == "/changed/output.tif" + assert image_payload_metadata(original).source_path == "/input/image.tif" diff --git a/tests/unit/test_image_metadata_projection_owner.py b/tests/unit/test_image_metadata_projection_owner.py new file mode 100644 index 0000000000..20ec974db9 --- /dev/null +++ b/tests/unit/test_image_metadata_projection_owner.py @@ -0,0 +1,341 @@ +"""Semantic boundaries for the shared metadata projection owner.""" + +import gc +import pickle +import weakref +from dataclasses import Field, FrozenInstanceError, dataclass, field, fields + +import cloudpickle +import numpy as np +import pytest + +from openhcs.core.runtime_image_values import ( + ImageMetadataCarrier, + ImagePayloadMetadataCarrier, + ImageMetadataPayload, + ImageMetadataProjection, + ImagePayloadMetadata, + LeadingPlaneAxisMetadataProjection, + LeadingSourcePlaneMetadataProjection, + MaskedImagePayload, + SourcePlaneImageMetadataProjection, + image_payload_data, + image_payload_metadata, + image_payload_metadata_projection, + image_payload_slice_context, +) +from openhcs.core.runtime_plane_projection import RuntimePlaneAxis +from openhcs.core.runtime_slice_projection import RuntimeSliceProjection +from openhcs.core.source_image_provenance import ( + RuntimeSourceImageProvenancePlane, + SourceImageIdentity, + SourceImageProvenancePlanes, +) +from openhcs.core.source_metadata import DurableSourceMetadata +from openhcs.core.source_spatial_domain import SourceSpatialDomain +from openhcs.serialization.json import to_jsonable + + +class MutableTag(int): + def __repr__(self): + return self.tag + + def __str__(self): + return self.tag + + +SUBTYPE_EVENTS = [] + + +@dataclass +class CalibratedMetadata(ImagePayloadMetadata): + calibration: float = 2.5 + declared_layout: object = field(init=False) + + def __post_init__(self, *values): + super().__post_init__(*values) + SUBTYPE_EVENTS.append(self) + self.declared_layout = self.plane_axis + self.constructor_attribute = "preserve actual constructor namespace" + + +class ObservedMetadata(ImagePayloadMetadata): + def singleton_plane_projection(self): + self.observed = True + return super().singleton_plane_projection() + + +def stack_metadata(metadata_type=ImagePayloadMetadata): + return metadata_type( + plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + source_dtype="float32", + source_path="/input/original.tif", + source_image_names=("DNA",), + ) + + +def test_slice_owner_exposes_stable_mutable_metadata_without_changing_pixels(): + data = np.arange(24, dtype=np.float32).reshape(2, 3, 4) + original = stack_metadata().payload_with(data) + sliced = image_payload_slice_context(original, data[1], 1) + owner = image_payload_metadata_projection(sliced) + assert isinstance(owner, ImageMetadataProjection) + assert owner.read_value("plane_axis") is None + assert np.shares_memory(image_payload_data(sliced), data) + public = image_payload_metadata(sliced) + assert public is image_payload_metadata(sliced) + assert owner.materialize_metadata() is public + public.source_dtype = "uint16" + public.source_path = "/input/changed.tif" + assert owner.read_value("source_dtype") == "uint16" + assert owner.read_value("source_provenance").source_path == "/input/changed.tif" + assert image_payload_metadata(original).source_path == "/input/original.tif" + derived = owner.derive_fields(source_dtype="float64") + assert derived.read_value("source_dtype") == "float64" + assert public.source_dtype == "uint16" + + +def test_captured_siblings_keep_distinct_namespaces_and_source_epochs(): + original = stack_metadata() + first = LeadingSourcePlaneMetadataProjection(original, 0).capture() + second = LeadingSourcePlaneMetadataProjection(original, 1).capture() + original.source_path = "/input/later.tif" + assert first.read_value("source_provenance").source_path == "/input/original.tif" + one = first.materialize_metadata() + two = second.materialize_metadata() + assert one is not two + assert one.source_provenance is not two.source_provenance + one.source_path = "/input/one.tif" + assert two.source_path == "/input/original.tif" + + +@pytest.mark.parametrize("codec", (pickle, cloudpickle)) +def test_private_transport_preserves_stored_birth_after_opaque_scalar_changes(codec): + tag = MutableTag(1) + tag.tag = "before" + metadata = ImagePayloadMetadata( + source_component_metadata=DurableSourceMetadata.from_mapping({"channel": tag}) + ) + owner = SourcePlaneImageMetadataProjection(metadata, 0).capture() + birth = owner.read_value("source_provenance").equality_identity + tag.tag = "after" + restored = codec.loads(codec.dumps(owner)) + assert restored.read_value("source_provenance").equality_identity == birth + public = restored.materialize_metadata() + assert public.source_provenance.equality_identity == birth + assert str(public.source_component_metadata["channel"]) == "after" + reducer_state = owner.__reduce__()[1] + assert all(not isinstance(value, Field) for value in reducer_state) + + +@pytest.mark.parametrize("public_api", (False, True)) +def test_constructor_extensions_preserve_subclass_fields_and_one_public_lifecycle( + public_api, +): + metadata = stack_metadata(CalibratedMetadata) + before = len(SUBTYPE_EVENTS) + rule = LeadingSourcePlaneMetadataProjection(metadata, 0) + result = rule.project() if public_api else rule.capture().materialize_metadata() + assert type(result) is CalibratedMetadata + assert result.calibration == 2.5 + # This init=False fact is born in the scalar-source constructor, before the + # independent leading-axis capability clears the axis. Do not recompute it. + assert result.declared_layout is RuntimePlaneAxis.RUNTIME_SLICE + assert result.plane_axis is None + assert result.constructor_attribute == "preserve actual constructor namespace" + assert len(SUBTYPE_EVENTS) == before + 1 + assert set(to_jsonable(result)) == {member.name for member in fields(type(result))} + + +def test_public_nominal_hook_survives_capture_and_realization(): + original = stack_metadata(ObservedMetadata) + owner = SourcePlaneImageMetadataProjection(original, 0).capture() + actual = owner.materialize_metadata() + assert type(actual) is ObservedMetadata + actual.singleton_plane_projection() + assert actual.observed + + +@pytest.mark.parametrize("payload_type", (ImageMetadataPayload, MaskedImagePayload)) +def test_public_payload_schema_and_frozen_fields_remain(payload_type): + data = np.ones((3, 4), dtype=np.float32) + owner = ImagePayloadMetadata(source_dtype="float32").capture() + payload = ( + payload_type.from_projection(data, owner) + if payload_type is ImageMetadataPayload + else payload_type.from_projection(data, np.ones(data.shape, dtype=bool), owner) + ) + assert image_payload_metadata_projection(payload) is owner + assert isinstance(payload.metadata, ImagePayloadMetadata) + assert "metadata" in {member.name for member in fields(payload)} + assert not payload_type.__abstractmethods__ + with pytest.raises(FrozenInstanceError): + payload.metadata = ImagePayloadMetadata(source_dtype="uint16") + replacement = payload.with_data(data.copy()) + assert image_payload_metadata_projection(replacement) is owner + + +def test_nominal_source_and_axis_guards_preserve_first_error_order(monkeypatch): + metadata = stack_metadata() + metadata.source_spatial_domain = SourceSpatialDomain(origin_yx=(1,)) + calls = [] + + def fail_contributors(self): + calls.append(self) + raise RuntimeError("contributor first") + + monkeypatch.setattr( + type(metadata.source_provenance), + "with_runtime_planes_as_contributors", + fail_contributors, + ) + with pytest.raises(ValueError, match="spatial_origin_yx"): + LeadingSourcePlaneMetadataProjection(metadata, 0).capture() + assert calls == [] + with pytest.raises(RuntimeError, match="contributor first"): + LeadingPlaneAxisMetadataProjection(metadata).capture() + assert calls == [metadata.source_provenance] + + +def test_metadata_owner_does_not_keep_pixels_or_original_stack_alive(): + data = np.ones((2, 3, 4), dtype=np.float32) + original = stack_metadata().payload_with(data) + leaf = image_payload_slice_context(original, data[0], 0) + owner = image_payload_metadata_projection(leaf) + pixels = weakref.ref(data) + del leaf, original, data + gc.collect() + assert pixels() is None + assert owner.read_value("source_dtype") == "float32" + + +def test_runtime_slice_strategy_keeps_existing_payload_family(): + data = np.arange(24, dtype=np.float32).reshape(2, 3, 4) + payload = stack_metadata().payload_with(data) + assert RuntimeSliceProjection.slice_count_from_values((payload,)) == 2 + + +class MetadataOnlyCarrier(ImageMetadataCarrier): + __slots__ = ("_metadata_projection",) + + def __init__(self, owner): + self.metadata = owner + + +def test_metadata_capability_does_not_make_a_carrier_an_image(): + carrier = MetadataOnlyCarrier(ImagePayloadMetadata().capture()) + assert not hasattr(carrier, "__dict__") + assert isinstance(carrier, ImageMetadataCarrier) + assert not isinstance(carrier, ImagePayloadMetadataCarrier) + assert image_payload_data(carrier) is carrier + assert image_payload_metadata_projection(carrier).has_values is False + assert carrier.metadata is carrier.metadata + empty = MetadataOnlyCarrier(None) + assert empty.metadata is None + assert empty.metadata_projection is None + with pytest.raises(TypeError): + ImagePayloadMetadataCarrier() + + +@pytest.mark.parametrize( + "projection_type", + ( + SourcePlaneImageMetadataProjection, + LeadingSourcePlaneMetadataProjection, + ), +) +def test_projection_observes_public_provenance_reassignments_before_capture( + projection_type, +): + metadata = stack_metadata() + provenance = metadata.source_provenance + old_birth = provenance.equality_identity + provenance.source_image_names = ("changed-name",) + provenance.source_identity = SourceImageIdentity("/input/changed-scalar.tif") + provenance.source_image_provenance_planes = SourceImageProvenancePlanes( + ( + RuntimeSourceImageProvenancePlane( + SourceImageIdentity("/input/changed-plane.tif"), + source_image_name="changed-plane-name", + ), + ) + ) + # Mutable live fields and the stored birth are distinct existing contracts. + assert provenance.equality_identity == old_birth + expected = projection_type(metadata, 0).project() + first = projection_type(metadata, 0).capture().materialize_metadata() + second = projection_type(metadata, 0).capture().materialize_metadata() + assert ( + first.source_provenance.equality_identity + == expected.source_provenance.equality_identity + ) + assert first.source_path == "/input/changed-plane.tif" + assert first.source_image_names == expected.source_image_names + assert ( + first.source_image_provenance_planes.identity + == expected.source_image_provenance_planes.identity + ) + assert first.source_provenance is not provenance + assert first.source_provenance is not second.source_provenance + first.source_provenance.source_image_names = ("first-only",) + first.source_provenance.source_identity.path = "/input/first-only.tif" + first.source_provenance.source_image_provenance_planes = ( + SourceImageProvenancePlanes() + ) + assert second.source_path == "/input/changed-plane.tif" + assert second.source_image_names == expected.source_image_names + assert ( + second.source_image_provenance_planes.identity + == expected.source_image_provenance_planes.identity + ) + + +def test_projection_rebirth_observes_opaque_scalar_mutation_before_capture(): + tag = MutableTag(1) + tag.tag = "old-birth" + metadata = ImagePayloadMetadata( + source_component_metadata=DurableSourceMetadata.from_mapping({"channel": tag}), + ) + old_birth = metadata.source_provenance.equality_identity + tag.tag = "new-birth" + expected = SourcePlaneImageMetadataProjection(metadata, 0).project() + owner = SourcePlaneImageMetadataProjection(metadata, 0).capture() + assert metadata.source_provenance.equality_identity == old_birth + assert owner.read_value("source_provenance").equality_identity != old_birth + assert ( + owner.read_value("source_provenance").equality_identity + == expected.source_provenance.equality_identity + ) + + +class SourceNormalizerMetadata(ImagePayloadMetadata): + def normalize_source_provenance_fields(self): + super().normalize_source_provenance_fields() + self.normalizer_result = "nominal subtype hook" + + +def test_capture_preserves_nominal_source_normalizer_override(): + original = stack_metadata(SourceNormalizerMetadata) + owner = LeadingSourcePlaneMetadataProjection(original, 0).capture() + actual = owner.materialize_metadata() + assert type(actual) is SourceNormalizerMetadata + assert actual.normalizer_result == "nominal subtype hook" + + +class ComponentAliasMetadata(ImagePayloadMetadata): + @property + def source_component_metadata(self): + self.component_alias_read = True + return super().source_component_metadata + + +def test_actual_source_component_alias_hook_survives_normalization_and_capture(): + metadata = stack_metadata(ComponentAliasMetadata) + assert metadata.component_alias_read + actual = ( + LeadingSourcePlaneMetadataProjection(metadata, 0) + .capture() + .materialize_metadata() + ) + assert type(actual) is ComponentAliasMetadata + assert actual.component_alias_read diff --git a/tests/unit/test_metadata_stage2_closure.py b/tests/unit/test_metadata_stage2_closure.py new file mode 100644 index 0000000000..2588742626 --- /dev/null +++ b/tests/unit/test_metadata_stage2_closure.py @@ -0,0 +1,108 @@ +"""Public metadata record contract and its real private alignment consumers.""" + +import gc +import weakref +from dataclasses import FrozenInstanceError, dataclass, fields + +import numpy as np +import pytest + +from openhcs.core.measurement_image_alignment import ( + MeasurementImageLabelAlignmentRequest, +) +from openhcs.core.runtime_image_values import ( + ImageMetadataCarrier, + ImagePayloadMetadata, + image_payload_data, + image_payload_metadata, +) +from openhcs.core.runtime_plane_projection import RuntimePlaneAxis +from openhcs.core.source_matching import SourceImageSetIdentityPolicy +from openhcs.core.source_plane_alignment import SourcePayloadPlaneIdentity +from openhcs.core.steps.function_io import _preloaded_image_payload +from openhcs.serialization.json import to_jsonable +from tests.unit.test_measurement_image_alignment import ( + _MeasurementImageSource, + _PlaneProjector, +) + + +@dataclass +class CalibratedMetadata(ImagePayloadMetadata): + calibration: float = 4.0 + + +def test_identity_record_keeps_public_metadata_field_schema_type_and_mutation(): + original = CalibratedMetadata( + source_path="/input/A01.tif", source_component_metadata={"well": "A01"} + ) + payload = original.payload_with(np.ones((3, 4))) + record = SourcePayloadPlaneIdentity.from_payload( + payload, SourceImageSetIdentityPolicy() + ) + assert isinstance(record, ImageMetadataCarrier) + assert not hasattr(record, "__dict__") + assert {member.name for member in fields(record)} == {"metadata", "policy"} + assert fields(record)[0].type == "ImagePayloadMetadata" + assert type(record.metadata) is CalibratedMetadata + assert record.metadata is original + assert record.metadata is record.metadata + assert set(to_jsonable(record)) == {"metadata", "policy"} + initial = record.identities() + record.metadata.source_component_metadata = {"well": "A02"} + assert record.identities() != initial + assert dict(record.metadata.source_component_metadata) == {"well": "A02"} + with pytest.raises(FrozenInstanceError): + record.metadata = ImagePayloadMetadata() + + +def test_identity_record_retains_metadata_without_pixel_payload_lifetime(): + data = np.ones((3, 4)) + payload = ImagePayloadMetadata(source_path="/input/image.tif").payload_with(data) + record = SourcePayloadPlaneIdentity.from_payload( + payload, SourceImageSetIdentityPolicy() + ) + pixels = weakref.ref(data) + expected = record.identities() + del payload, data + gc.collect() + assert pixels() is None + assert record.identities() == expected + + +def test_preload_preserves_existing_metadata_owner_without_backend_reads(): + class NoReadsFileManager: + def physical_source_path(self, *args, **kwargs): + pytest.fail("Existing source facts must not trigger backend reads.") + + data = np.arange(12).reshape(3, 4) + metadata = CalibratedMetadata(source_path="/input/existing.tif") + source = metadata.payload_with(data) + result = _preloaded_image_payload( + source, + source_path="/unused.tif", + read_backend="disk", + filemanager=NoReadsFileManager(), + ) + assert image_payload_data(result) is data + assert image_payload_metadata(result) is metadata + assert type(image_payload_metadata(result)) is CalibratedMetadata + + +def test_measurement_source_projection_preserves_actual_pixels_and_alias_selection(): + data = np.arange(24, dtype=np.float32).reshape(2, 3, 4) + source = ImagePayloadMetadata( + source_path="/input/channels.tif", + source_image_names=("DNA", "RNA"), + plane_axis=RuntimePlaneAxis.SOURCE_BINDING, + ).payload_with(data) + request = MeasurementImageLabelAlignmentRequest( + source=_MeasurementImageSource(source, source_aliases=("DNA", "RNA")), + labels=np.zeros((3, 4), dtype=np.int32), + plane_projector=_PlaneProjector(source_index=1, source_count=2), + ) + projected = request.with_source_projected_image() + np.testing.assert_array_equal(image_payload_data(projected.image), data[1]) + assert np.shares_memory(image_payload_data(projected.image), data) + assert image_payload_metadata(projected.image).source_image_names == ("RNA",) + assert image_payload_metadata(source).source_image_names == ("DNA", "RNA") From 9a4967d36ae8760774c2f26edfa377f0b3846291 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 05:00:16 -0400 Subject: [PATCH 118/235] Revert "refactor: carry nominal metadata owners across runtime consumers" This reverts commit 9eee98265c939859d49f0c60bd5b3a2168cb29fe. --- openhcs/core/aligned_image_payload.py | 128 +- openhcs/core/artifacts.py | 40 +- openhcs/core/image_file_serialization.py | 68 +- openhcs/core/measurement_image_alignment.py | 80 +- openhcs/core/runtime_adapters.py | 46 +- openhcs/core/runtime_image_loading.py | 41 +- openhcs/core/runtime_image_values.py | 1931 +++++++---------- openhcs/core/runtime_object_labels.py | 8 - openhcs/core/runtime_slice_projection.py | 20 +- openhcs/core/source_image_semantics.py | 31 +- openhcs/core/source_plane_alignment.py | 36 +- openhcs/core/source_workspace_projection.py | 49 +- .../function_artifact_materialization.py | 58 +- openhcs/core/steps/function_io.py | 6 +- .../core/steps/function_output_identity.py | 80 +- .../core/steps/function_output_manifest.py | 32 +- openhcs/core/steps/function_outputs.py | 32 +- openhcs/core/steps/function_runtime.py | 218 +- .../module_artifact_declarations.py | 18 +- .../cellprofiler/runtime/artifact_binding.py | 23 +- .../runtime/function_contract_execution.py | 12 +- .../cellprofiler/runtime/invocation.py | 39 +- .../interop/cellprofiler/runtime/main_flow.py | 12 +- .../runtime/measurement_recording.py | 77 +- .../cellprofiler/runtime/module_execution.py | 54 +- .../runtime/output_record_request.py | 25 +- openhcs/processing/materialization/core.py | 192 +- ...lprofiler_metadata_projection_consumers.py | 414 ---- ...est_function_output_metadata_projection.py | 335 --- .../test_image_metadata_owner_consumers.py | 151 -- .../test_image_metadata_projection_owner.py | 341 --- tests/unit/test_metadata_stage2_closure.py | 108 - 32 files changed, 1303 insertions(+), 3402 deletions(-) delete mode 100644 tests/unit/test_cellprofiler_metadata_projection_consumers.py delete mode 100644 tests/unit/test_function_output_metadata_projection.py delete mode 100644 tests/unit/test_image_metadata_owner_consumers.py delete mode 100644 tests/unit/test_image_metadata_projection_owner.py delete mode 100644 tests/unit/test_metadata_stage2_closure.py diff --git a/openhcs/core/aligned_image_payload.py b/openhcs/core/aligned_image_payload.py index 33129c48be..c939865b21 100644 --- a/openhcs/core/aligned_image_payload.py +++ b/openhcs/core/aligned_image_payload.py @@ -29,14 +29,13 @@ ) from openhcs.core.runtime_image_values import ( ImagePayloadMetadata, - ImageMetadataProjection, ImagePayloadSliceProjector, ImagePayloadMetadataCarrier, ImagePayloadMetadataCompositionMode, ImageMaskDomain, image_payload_data, image_payload_mask, - image_payload_metadata_projection, + image_payload_metadata, preserved_image_plane_projection, with_image_payload_data, ) @@ -85,7 +84,7 @@ def for_value( return cls( value, cls.domain_from_metadata( - image_payload_metadata_projection(value), + image_payload_metadata(value), value_name="Image payload", ), ) @@ -93,13 +92,13 @@ def for_value( @classmethod def domain_from_metadata( cls, - metadata: ImageMetadataProjection, + metadata: ImagePayloadMetadata, *, fill_value: Any = 0, source_domain: SourceSpatialDomain | None = None, value_name: str, ) -> SourceSpatialDomain: - domain = metadata.read_value("source_spatial_domain") + domain = metadata.source_spatial_domain if source_domain is not None: domain = domain.with_missing_from(source_domain) return ( @@ -110,7 +109,7 @@ def domain_from_metadata( @property def spatial_axes_yx(self) -> tuple[int, int]: - axes = image_payload_metadata_projection(self.value).spatial_axes_yx(self.value) + axes = image_payload_metadata(self.value).spatial_axes_yx(self.value) if axes is None: raise ValueError( "Source-spatial image payload metadata does not declare two " @@ -120,9 +119,7 @@ def spatial_axes_yx(self) -> tuple[int, int]: @property def spatial_shape_yx(self) -> tuple[int, int]: - shape = image_payload_metadata_projection(self.value).spatial_shape_yx( - self.value - ) + shape = image_payload_metadata(self.value).spatial_shape_yx(self.value) if shape is None: raise ValueError( "Source-spatial image payloads require at least two dimensions, " @@ -170,7 +167,7 @@ def payload_in_source_domain( payload: RuntimeArrayData, source_domain: SourceSpatialDomain, ) -> RuntimeArrayData: - metadata = image_payload_metadata_projection(payload) + metadata = image_payload_metadata(payload) source_extent = source_domain.with_origin_yx(None) source_metadata = metadata.with_materialized_source_domain(source_extent) data = cls( @@ -190,7 +187,7 @@ def payload_in_source_domain( def mask_in_source_domain( cls, payload: RuntimeArrayData, - metadata: ImageMetadataProjection, + metadata: ImagePayloadMetadata, source_domain: SourceSpatialDomain, ) -> RuntimeArrayData | None: mask = image_payload_mask(payload) @@ -218,7 +215,8 @@ def value_in_payload_domain( fill_value=self.domain.fill_value, value_name=self.domain.value_name, ) - metadata = image_payload_metadata_projection(materialized).derive_fields( + metadata = replace( + image_payload_metadata(materialized), source_spatial_domain=target_domain, physical_border_edges_yx=target_domain.physical_border_edges_for_shape( target.payload_domain.spatial_shape_yx @@ -385,8 +383,8 @@ def resolve_source_spatial_value(self, value: Any) -> Any: """Project a nominal value into the declared reference payload domain.""" if self.reference_payload is None: return value - metadata = image_payload_metadata_projection(self.reference_payload) - source_shape = metadata.read_value("source_spatial_domain").source_shape_yx + metadata = image_payload_metadata(self.reference_payload) + source_shape = metadata.source_spatial_domain.source_shape_yx if source_shape is None: return value adapter = SourceSpatialDomainAdapter.for_value( @@ -434,36 +432,26 @@ def copy_whole_image( ) -> RuntimeArrayData: """Copy pixels, mask and metadata without adding a composition axis.""" copied_data = stack_runtime_slices( - (image_payload_data(value),), - memory_type, - device_id, + (image_payload_data(value),), memory_type, device_id, )[0] mask = image_payload_mask(value) copied_mask = ( - None - if mask is None + None if mask is None else stack_runtime_slices((mask,), memory_type, device_id)[0] ) - return ( - image_payload_metadata_projection(value) - .derive_fields() - .payload_with( - copied_data, - copied_mask, - ) + return image_payload_metadata(value).replace_fields().payload_with( + copied_data, copied_mask, ) - def composition_source_metadata(self) -> tuple[ImageMetadataProjection, ...]: + def composition_source_metadata(self) -> tuple[ImagePayloadMetadata, ...]: return tuple( - self.composition_payload_metadata( - image_payload_metadata_projection(payload) - ) + self.composition_payload_metadata(image_payload_metadata(payload)) for payload in self.composition_payloads ) def composition_payload_metadata( - self, metadata: ImageMetadataProjection - ) -> ImageMetadataProjection: + self, metadata: ImagePayloadMetadata + ) -> ImagePayloadMetadata: """Preserve each input's declared provenance unless the axis owner projects it.""" return metadata @@ -487,9 +475,7 @@ def compose_unmasked( payloads, memory_type, MemoryType(memory_type).device_id_of(payloads[0]) ) - def compose_mask( - self, composed: Any, metadata: ImageMetadataProjection - ) -> Any | None: + def compose_mask(self, composed: Any, metadata: ImagePayloadMetadata) -> Any | None: return _stack_image_payload_mask( self.composition_payloads, composed, @@ -531,7 +517,7 @@ def stack_image_payloads( def stack_image_payload_context_from_metadata( image_payloads: Sequence[Any], stack: RuntimeArrayData, - metadata_by_payload: Sequence[ImageMetadataProjection], + metadata_by_payload: Sequence[ImagePayloadMetadata], *, metadata_mode: ImagePayloadMetadataCompositionMode, ) -> Any: @@ -578,7 +564,7 @@ def _stack_image_payload_mask( stacked_mask_shape ): resolved_masks = tuple( - image_payload_metadata_projection(payload) + image_payload_metadata(payload) .mask_domain(slice_domain) .broadcast_to_data(mask) for payload, slice_domain, mask in zip( @@ -597,7 +583,7 @@ def _complete_image_payload_mask( payload_data: RuntimeArrayData, mask: RuntimeArrayData | None, ) -> RuntimeArrayData: - mask_domain = image_payload_metadata_projection(payload).mask_domain(payload_data) + mask_domain = image_payload_metadata(payload).mask_domain(payload_data) if mask is not None: if not mask_domain.accepts(tuple(np.shape(mask))): raise ValueError( @@ -617,11 +603,11 @@ def unstack_image_payload_context( ) -> list[Any]: """Attach one source plane of payload context to each unstacked image slice.""" mask = image_payload_mask(payload) - metadata = image_payload_metadata_projection(payload) + metadata = image_payload_metadata(payload) if mask is None and not metadata.has_values: return list(slices) - if metadata.read_value("plane_axis") is None and default_plane_axis is not None: - metadata = metadata.derive_fields(plane_axis=default_plane_axis) + if metadata.plane_axis is None and default_plane_axis is not None: + metadata = replace(metadata, plane_axis=default_plane_axis) projector = ImagePayloadSliceProjector(mask=mask, metadata=metadata) return projector.payloads_for_slices(slices) @@ -781,7 +767,7 @@ def plane_axis(self) -> RuntimePlaneAxis | None: """Return the axis declared by the composed payload owner.""" if isinstance(self.payload, AlignedImageStack): return RuntimePlaneAxis.RUNTIME_SLICE - return image_payload_metadata_projection(self.payload).read_value("plane_axis") + return image_payload_metadata(self.payload).plane_axis def preserved_plane_projection( self, @@ -814,14 +800,14 @@ def __post_init__(self) -> None: declared_axes = tuple( ( index, - metadata.read_value("plane_axis"), - metadata.read_value("source_provenance").source_image_names, + metadata.plane_axis, + metadata.source_image_names, tuple(np.shape(image_payload_data(payload))), ) for index, (payload, metadata) in enumerate( zip(self.payloads, self.source_metadata, strict=True) ) - if metadata.read_value("plane_axis") is not None + if metadata.plane_axis is not None ) if declared_axes: raise ValueError( @@ -830,7 +816,7 @@ def __post_init__(self) -> None: ) @property - def source_metadata(self) -> tuple[ImageMetadataProjection, ...]: + def source_metadata(self) -> tuple[ImagePayloadMetadata, ...]: return self.composition_source_metadata() @property @@ -864,7 +850,7 @@ def from_payloads( def compose_mask( self, composed: Any, - metadata: ImageMetadataProjection, + metadata: ImagePayloadMetadata, ) -> Any | None: masks = self.present_masks if not masks: @@ -1131,11 +1117,11 @@ def contextualize_image_payload( if self.is_anonymous_main_flow: return payload - metadata = image_payload_metadata_projection(payload) - return metadata.derive_fields( - source_provenance=metadata.read_value( - "source_provenance" - ).with_derived_source_image_names((self.output_key,)) + metadata = image_payload_metadata(payload) + return metadata.with_source_provenance( + metadata.source_provenance.with_derived_source_image_names( + (self.output_key,) + ) ).payload_with( image_payload_data(payload), image_payload_mask(payload), @@ -1180,28 +1166,23 @@ def composition_metadata_mode(self) -> ImagePayloadMetadataCompositionMode: return ImagePayloadMetadataCompositionMode.STACK @property - def projected_output_composition_mode( - self, - ) -> ImagePayloadMetadataCompositionMode | None: + def projected_output_composition_mode(self) -> ImagePayloadMetadataCompositionMode | None: """Declare the outer runtime axis retained by projected output members.""" return self.composition_metadata_mode def plane_axis_for_output_context( - self, - context: AlignedImageSliceContext, + self, context: AlignedImageSliceContext, ) -> RuntimePlaneAxis | None: """An explicitly aligned stack declares its outer runtime-slice domain.""" return RuntimePlaneAxis.RUNTIME_SLICE def composition_payload_metadata( - self, metadata: ImageMetadataProjection - ) -> ImageMetadataProjection: + self, metadata: ImagePayloadMetadata + ) -> ImagePayloadMetadata: """Inner image bundles contribute provenance, not an outer slice axis.""" metadata = super(AlignedImageStack, self).composition_payload_metadata(metadata) - return metadata.derive_fields( - source_provenance=metadata.read_value( - "source_provenance" - ).with_runtime_planes_as_contributors() + return metadata.with_source_provenance( + metadata.source_provenance.with_runtime_planes_as_contributors() ) def __post_init__(self) -> None: @@ -1342,13 +1323,10 @@ def composition_metadata_mode(self) -> ImagePayloadMetadataCompositionMode: return ImagePayloadMetadataCompositionMode.BUNDLE @property - def projected_output_composition_mode( - self, - ) -> ImagePayloadMetadataCompositionMode | None: + def projected_output_composition_mode(self) -> ImagePayloadMetadataCompositionMode | None: """Flatten declared inner runtime planes, retaining other named image domains.""" if any( - image_payload_metadata_projection(payload).read_value("plane_axis") - is RuntimePlaneAxis.RUNTIME_SLICE + image_payload_metadata(payload).plane_axis is RuntimePlaneAxis.RUNTIME_SLICE for payload in self.slices ): return ImagePayloadMetadataCompositionMode.STACK @@ -1357,15 +1335,11 @@ def projected_output_composition_mode( return self.composition_metadata_mode def plane_axis_for_output_context( - self, - context: AlignedImageSliceContext, + self, context: AlignedImageSliceContext, ) -> RuntimePlaneAxis | None: """Resolve a named output's original inner domain before leaf projection.""" payloads = tuple( - payload - for payload, declared_context in zip( - self.slices, self.slice_contexts, strict=True - ) + payload for payload, declared_context in zip(self.slices, self.slice_contexts, strict=True) if declared_context == context ) if len(payloads) != 1: @@ -1373,11 +1347,11 @@ def plane_axis_for_output_context( "Named image output context requires exactly one original payload: " f"{context!r}; found {len(payloads)}." ) - return image_payload_metadata_projection(payloads[0]).read_value("plane_axis") + return image_payload_metadata(payloads[0]).plane_axis def composition_payload_metadata( - self, metadata: ImageMetadataProjection - ) -> ImageMetadataProjection: + self, metadata: ImagePayloadMetadata + ) -> ImagePayloadMetadata: """Named output surfaces remain source-binding planes, not runtime slices.""" return metadata diff --git a/openhcs/core/artifacts.py b/openhcs/core/artifacts.py index 59621b2d2e..2d11eee696 100644 --- a/openhcs/core/artifacts.py +++ b/openhcs/core/artifacts.py @@ -31,10 +31,7 @@ from openhcs.core.runtime_artifact_values import ( RuntimeValue, ) - from openhcs.core.runtime_image_values import ( - ImageMetadataProjection, - ImagePayloadMetadata, - ) + from openhcs.core.runtime_image_values import ImagePayloadMetadata from openhcs.core.runtime_measurements import ( MeasurementSubject, RuntimeMeasurementFeatureOwner, @@ -454,7 +451,7 @@ def normalize_runtime_payload( from openhcs.core.runtime_image_values import ( image_payload_data, image_payload_mask, - image_payload_metadata_projection, + image_payload_metadata, ) from openhcs.core.runtime_slice_alignment import ( RuntimeSliceAlignedValues, @@ -462,11 +459,9 @@ def normalize_runtime_payload( ) def named_payload(payload: object) -> object: - metadata = image_payload_metadata_projection(payload) - return metadata.derive_fields( - source_provenance=metadata.read_value( - "source_provenance" - ).with_derived_source_image_names((name,)) + metadata = image_payload_metadata(payload) + return metadata.with_source_provenance( + metadata.source_provenance.with_derived_source_image_names((name,)) ).payload_with( image_payload_data(payload), image_payload_mask(payload), @@ -1081,11 +1076,7 @@ def filename_identity_for_output( ) -> FunctionOutputIdentity: """Apply a declared role through the shared filename identity algorithm.""" qualifier = self.filename_qualifier(output_plan) - return ( - identity - if qualifier is None - else identity.with_filename_qualifier(qualifier) - ) + return identity if qualifier is None else identity.with_filename_qualifier(qualifier) def _coerce_artifact_plan_type( @@ -2858,26 +2849,17 @@ def materialization_payload(self, value: "RuntimeValue") -> object: or materialization_source == self.source_context_source() ): return payload - return self.materialization_metadata_projection(value).attach_to(payload) + return self.materialization_metadata(value).attach_to(payload) def materialization_metadata( self, value: "RuntimeValue", ) -> "ImagePayloadMetadata": - """Expose filename source facts as the actual mutable metadata type.""" - return self.materialization_metadata_projection(value).materialize_metadata() - - def materialization_metadata_projection( - self, - value: "RuntimeValue", - ) -> "ImageMetadataProjection": """Return payload metadata with declared materialization-source provenance.""" - from openhcs.core.runtime_image_values import image_payload_metadata_projection + from openhcs.core.runtime_image_values import image_payload_metadata - payload_metadata = image_payload_metadata_projection( - value.materialization_payload() - ) + payload_metadata = image_payload_metadata(value.materialization_payload()) materialization_source = self.materialization_source() if ( materialization_source is not None @@ -2890,8 +2872,8 @@ def materialization_metadata_projection( f"materialization source {materialization_source!r}, but its " "runtime value carries no materialization-source metadata." ) - return payload_metadata.derive_fields( - source_provenance=source_metadata.read_value("source_provenance") + return payload_metadata.with_source_provenance( + source_metadata.source_provenance ) return payload_metadata diff --git a/openhcs/core/image_file_serialization.py b/openhcs/core/image_file_serialization.py index 7f2bb1229c..0b2593fa72 100644 --- a/openhcs/core/image_file_serialization.py +++ b/openhcs/core/image_file_serialization.py @@ -15,11 +15,10 @@ from openhcs.constants.constants import FileFormat from openhcs.core.registry_strategies import NominalTypeStrategyFamilyMixin from openhcs.core.runtime_image_values import ( - ImageMetadataProjection, ImagePayloadMetadata, image_intensity_scale_for_dtype, image_payload_data, - image_payload_metadata_projection, + image_payload_metadata, ) logger = logging.getLogger(__name__) @@ -143,53 +142,44 @@ def require_image_geometry(self) -> tuple[Any, int, int]: """Require complete format-declared dtype and physical image dimensions.""" if self.image_shape_yx is None or self.source_dtype is None: - raise ValueError( - "Image-file header does not declare image dtype and YX geometry." - ) + raise ValueError("Image-file header does not declare image dtype and YX geometry.") height, width = self.image_shape_yx return self.source_dtype, height, width def project_image_metadata( self, metadata: ImagePayloadMetadata, *, values_preserved: bool ) -> ImagePayloadMetadata: - """Expose format-projected facts as their actual mutable namespace.""" - return self.project_image_metadata_projection( - metadata, values_preserved=values_preserved - ).materialize_metadata() - - def project_image_metadata_projection( - self, metadata: ImageMetadataProjection, *, values_preserved: bool - ) -> ImageMetadataProjection: """Combine current native pixels with retained typed acquisition facts.""" if self.source_dtype is None: raise ValueError("Saved image metadata requires an actual native dtype.") native_scale_governs = self.intensity_scale is not None or not values_preserved if not values_preserved: - metadata = metadata.without_unit_interval_intensity_scale().derive_fields( - physical_border_edges_yx=None, mask_defines_border=None + metadata = metadata.without_unit_interval_intensity_scale().replace_fields( + physical_border_edges_yx=None, + mask_defines_border=None, ) - return metadata.derive_fields( + return metadata.replace_fields( source_dtype=str(self.source_dtype), - intensity_scale=self.intensity_scale - if native_scale_governs - else metadata.read_value("intensity_scale"), + intensity_scale=( + self.intensity_scale + if native_scale_governs + else metadata.intensity_scale + ), source_plane_dtypes=tuple( - ( - str(self.source_dtype) - for _ in metadata.read_value("source_plane_dtypes") - ) + str(self.source_dtype) for _ in metadata.source_plane_dtypes ), - source_plane_intensity_scales=tuple( - ( - self.intensity_scale - for _ in metadata.read_value("source_plane_intensity_scales") + source_plane_intensity_scales=( + tuple( + self.intensity_scale for _ in metadata.source_plane_intensity_scales ) - ) - if native_scale_governs - else metadata.read_value("source_plane_intensity_scales"), - source_channel_axis=metadata.read_value("source_channel_axis") - if values_preserved and self.pixel_semantics.channel_axis is None - else self.pixel_semantics.channel_axis, + if native_scale_governs + else metadata.source_plane_intensity_scales + ), + source_channel_axis=( + metadata.source_channel_axis + if values_preserved and self.pixel_semantics.channel_axis is None + else self.pixel_semantics.channel_axis + ), ) @@ -327,18 +317,12 @@ def preserves_pixel_values(self, source_dtype: Any) -> bool: return False def persisted_metadata(self, path: Path, payload: Any) -> ImagePayloadMetadata: - """Expose saved native facts as their actual mutable metadata namespace.""" - return self.persisted_metadata_projection(path, payload).materialize_metadata() - - def persisted_metadata_projection( - self, path: Path, payload: Any - ) -> ImageMetadataProjection: """Describe saved native pixels while retaining their semantic lineage.""" header = self.source_metadata(path) if header.source_dtype is None: raise ValueError(f"Cannot establish saved image metadata for {path}.") - return header.project_image_metadata_projection( - image_payload_metadata_projection(payload), + return header.project_image_metadata( + image_payload_metadata(payload), values_preserved=self.preserves_pixel_values( image_payload_data(payload).dtype ), @@ -639,7 +623,7 @@ 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 = image_payload_metadata_projection(payload).read_value("plane_axis") + plane_axis = image_payload_metadata(payload).plane_axis if plane_axis is not None: raise ValueError( "Single-image raster serialization requires a payload projected off " diff --git a/openhcs/core/measurement_image_alignment.py b/openhcs/core/measurement_image_alignment.py index f9e4b4dcf3..f8264e9d8e 100644 --- a/openhcs/core/measurement_image_alignment.py +++ b/openhcs/core/measurement_image_alignment.py @@ -12,7 +12,7 @@ from openhcs.core.runtime_array_values import RuntimeArrayData from openhcs.core.runtime_image_values import ( image_payload_data, - image_payload_metadata_projection, + image_payload_metadata, with_image_payload_data, ) from openhcs.core.runtime_object_labels import ( @@ -147,7 +147,9 @@ def image_array(self) -> np.ndarray: 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, self.source_aliases + self.plane_projector, + RuntimePlaneAxis.SOURCE_BINDING, + self.source_aliases, ) if projection is None or projection.plane_index is None: return self @@ -155,20 +157,16 @@ def with_source_projected_image(self) -> "MeasurementImageLabelAlignmentRequest" if isinstance(image, AlignedImageStack): projected_image: RuntimeArrayData | AlignedImageStack = image.with_slices( tuple( - ( - RuntimeSliceProjection.value_for_slice( - image_slice, - replace( - projection, - source_aliases=image_payload_metadata_projection( - image_slice - ) - .read_value("source_provenance") - .source_image_names, - ), - ) - for image_slice in image.slices + RuntimeSliceProjection.value_for_slice( + image_slice, + replace( + projection, + source_aliases=image_payload_metadata( + image_slice + ).source_image_names, + ), ) + for image_slice in image.slices ) ) else: @@ -176,12 +174,13 @@ def with_source_projected_image(self) -> "MeasurementImageLabelAlignmentRequest" image, replace( projection, - source_aliases=image_payload_metadata_projection(image) - .read_value("source_provenance") - .source_image_names, + source_aliases=image_payload_metadata(image).source_image_names, ), ) - return replace(self, source=self.source.with_alignment_image(projected_image)) + return replace( + self, + source=self.source.with_alignment_image(projected_image), + ) def with_source_projected_labels(self) -> "MeasurementImageLabelAlignmentRequest": """Project labels only through their declared axis and runtime context.""" @@ -198,7 +197,9 @@ def with_source_projected_labels(self) -> "MeasurementImageLabelAlignmentRequest "Plane-scoped object labels require a declared local plane projection." ) runtime_projection = RuntimePlaneAxisValueProjection.from_projector( - self.plane_projector, self.label_payload.plane_axis, self.source_aliases + self.plane_projector, + self.label_payload.plane_axis, + self.source_aliases, ) if runtime_projection is None: return self @@ -210,7 +211,9 @@ def with_source_projected_labels(self) -> "MeasurementImageLabelAlignmentRequest projection = declared_projection.selected_plane(0) elif runtime_projection.axis_size != declared_projection.axis_size: raise ValueError( - f"Object-label runtime plane-axis cardinality conflicts with its declared local payload: {runtime_projection.axis_size} != {declared_projection.axis_size}." + "Object-label runtime plane-axis cardinality conflicts with its " + "declared local payload: " + f"{runtime_projection.axis_size} != {declared_projection.axis_size}." ) else: projection = declared_projection.selected_plane( @@ -223,29 +226,31 @@ def with_source_projected_labels(self) -> "MeasurementImageLabelAlignmentRequest source_payload = source_payload.with_measurement_labels(self.labels) payload_projection = RuntimeSliceProjection.value_for_slice( source_payload, - replace(projection, source_aliases=source_payload.source_image_names), + replace( + projection, + source_aliases=source_payload.source_image_names, + ), ) if not isinstance(payload_projection, ObjectLabelValue): raise TypeError( - f"Object-label runtime projection must preserve ObjectLabelValue, got {type(payload_projection).__name__}." + "Object-label runtime projection must preserve ObjectLabelValue, got " + f"{type(payload_projection).__name__}." ) if payload_projection is source_payload: return self source = self.source image = self.image - image_metadata = image_payload_metadata_projection(image) + image_metadata = image_payload_metadata(image) if ( not isinstance(image, AlignedImageStack) - and image_metadata.read_value("plane_axis") is projection.axis + and image_metadata.plane_axis is projection.axis ): source = source.with_alignment_image( RuntimeSliceProjection.value_for_slice( image, replace( projection, - source_aliases=image_metadata.read_value( - "source_provenance" - ).source_image_names, + source_aliases=image_metadata.source_image_names, ), ) ) @@ -419,7 +424,7 @@ def source_context_payload( source_payload, source_projected_labels, ) - metadata = image_payload_metadata_projection(request.image) + metadata = image_payload_metadata(request.image) payload_domain = source_payload.object_label_source_spatial_domain() source_spatial_domain = ( metadata.object_label_source_spatial_domain() @@ -718,7 +723,10 @@ def validate_aligned_stack( @staticmethod def validate_dense_pair( - image: object, labels: object, *, label_payload: ObjectLabelValue | None = None + image: object, + labels: object, + *, + label_payload: ObjectLabelValue | None = None, ) -> None: """Require exact dense shapes after nominal source-domain projection.""" image_data = image_payload_data(image) @@ -731,14 +739,22 @@ def validate_dense_pair( label_data, np.ndarray ): raise TypeError( - f"Measurement image alignment requires dense image and label arrays after nominal projection; got {type(image_data).__name__} and {type(label_data).__name__}." + "Measurement image alignment requires dense image and label arrays " + "after nominal projection; got " + f"{type(image_data).__name__} and {type(label_data).__name__}." ) if tuple(image_data.shape) != tuple(label_data.shape): label_domain = ( None if label_payload is None else label_payload.object_label_domain() ) raise ValueError( - f"Measurement image alignment produced incompatible declared domains: image shape {image_data.shape!r}, label shape {label_data.shape!r}; image plane axis={image_payload_metadata_projection(image).read_value('plane_axis')!r}; label type={(type(label_payload).__name__ if label_payload is not None else type(labels).__name__)}; label scope={(None if label_domain is None else label_domain.scope)!r}; label plane axis={(None if label_payload is None else label_payload.plane_axis)!r}; label source aliases={(None if label_payload is None else label_payload.source_aliases)!r}." + "Measurement image alignment produced incompatible declared domains: " + f"image shape {image_data.shape!r}, label shape {label_data.shape!r}; " + f"image plane axis={image_payload_metadata(image).plane_axis!r}; " + f"label type={type(label_payload).__name__ if label_payload is not None else type(labels).__name__}; " + f"label scope={None if label_domain is None else label_domain.scope!r}; " + f"label plane axis={None if label_payload is None else label_payload.plane_axis!r}; " + f"label source aliases={None if label_payload is None else label_payload.source_aliases!r}." ) diff --git a/openhcs/core/runtime_adapters.py b/openhcs/core/runtime_adapters.py index 12b3af5905..e8b2d3cf2a 100644 --- a/openhcs/core/runtime_adapters.py +++ b/openhcs/core/runtime_adapters.py @@ -33,7 +33,7 @@ from openhcs.core.runtime_image_loading import ImagePayloadSourceMetadataContext from openhcs.core.runtime_image_values import ( ImagePayloadMetadataCompositionMode, - image_payload_metadata_projection, + image_payload_metadata, ) from openhcs.core.source_bindings import ( CompiledSourceBindingPlan, @@ -277,28 +277,33 @@ def runtime_artifact_input( def source_artifact_payload(self, ref: ArtifactSpecRef) -> object: """Resolve one source-bound artifact through workspace matching and VFS.""" + binding = self.source_binding_for_artifact_ref(ref) source_payload = self.source_payload source_provenance = ( None if source_payload is None - else image_payload_metadata_projection(source_payload).read_value( - "source_provenance" - ) + else image_payload_metadata(source_payload).source_provenance ) - if source_provenance is not None and (not source_provenance.has_values): + if source_provenance is not None and not source_provenance.has_values: raise ValueError( f"Source-bound artifact {ref!r} requires main-flow source provenance." ) + cache = self.context.runtime_source_workspace_projection_cache projection = VirtualWorkspaceSourceProjectionAuthority.from_context( - self.context, cache=cache + self.context, + cache=cache, ).projection_if_available() if projection is None: raise ValueError( - f"Source-bound artifact {ref!r} requires a virtual-workspace source projection." + f"Source-bound artifact {ref!r} requires a virtual-workspace " + "source projection." ) - projection = cache.filtered_by_axis(projection, axis_id=self.axis_scope.axis_id) + projection = cache.filtered_by_axis( + projection, + axis_id=self.axis_scope.axis_id, + ) source_context = ( self.context.runtime_source_binding_context_cache.source_pattern_context( parser=self.context.microscope_handler.parser, @@ -314,13 +319,11 @@ def source_artifact_payload(self, ref: ArtifactSpecRef) -> object: ) source_universe = tuple( dict.fromkeys( - ( - source_path - for declared_binding in self.source_binding_plan.binding_declarations - for source_path in projection.files_for_projection_role( - declared_binding.projection_role, - axis_id=self.axis_scope.axis_id, - ) + source_path + for declared_binding in self.source_binding_plan.binding_declarations + for source_path in projection.files_for_projection_role( + declared_binding.projection_role, + axis_id=self.axis_scope.axis_id, ) ) ) @@ -337,12 +340,15 @@ def source_artifact_payload(self, ref: ArtifactSpecRef) -> object: raise ValueError( f"Source-bound artifact {ref!r} resolved no workspace members." ) + payloads = self.context.filemanager.load_batch( - list(members), Backend.VIRTUAL_WORKSPACE.value + list(members), + Backend.VIRTUAL_WORKSPACE.value, ) if len(payloads) != len(members): raise ValueError( - f"Source-bound artifact {ref!r} loaded {len(payloads)} payloads for {len(members)} workspace members." + f"Source-bound artifact {ref!r} loaded {len(payloads)} payloads " + f"for {len(members)} workspace members." ) projected_payloads = [] for member, payload in zip(members, payloads, strict=True): @@ -350,7 +356,8 @@ def source_artifact_payload(self, ref: ArtifactSpecRef) -> object: source_projection = projection.require_source_projection_for(lookup) if not source_projection.matches_binding(binding): raise ValueError( - f"Workspace projection for {member!r} does not match compiled source artifact {ref!r}." + f"Workspace projection for {member!r} does not match compiled " + f"source artifact {ref!r}." ) projected = projection.project_payload(lookup, payload) projected_payloads.append( @@ -359,7 +366,8 @@ def source_artifact_payload(self, ref: ArtifactSpecRef) -> object: binding, ImagePayloadSourceMetadataContext( SourceImageIdentity( - member, projection.source_metadata_for(lookup) + member, + projection.source_metadata_for(lookup), ), source_projection.ref.backend, self.context.filemanager, diff --git a/openhcs/core/runtime_image_loading.py b/openhcs/core/runtime_image_loading.py index 974cd38a5f..a29901c1c2 100644 --- a/openhcs/core/runtime_image_loading.py +++ b/openhcs/core/runtime_image_loading.py @@ -13,11 +13,10 @@ image_file_source_metadata, ) from openhcs.core.runtime_image_values import ( - ImageMetadataProjection, ImagePayloadMetadata, ImagePayloadMetadataCarrier, image_payload_data, - image_payload_metadata_projection, + image_payload_metadata, ) from openhcs.core.runtime_plane_projection import RuntimePlaneAxis from openhcs.core.source_image_provenance import ( @@ -58,16 +57,8 @@ def metadata( *, source_binding: NamedSourceBinding | None = None, ) -> ImagePayloadMetadata: - """Expose loaded source facts as their actual mutable metadata namespace.""" - return self.metadata_projection( - image, source_binding=source_binding - ).materialize_metadata() - - def metadata_projection( - self, image: Any, *, source_binding: NamedSourceBinding | None = None - ) -> ImageMetadataProjection: """Return source-file metadata for pixels loaded through this I/O context.""" - existing_metadata = image_payload_metadata_projection(image) + existing_metadata = image_payload_metadata(image) resolved_source_path = self.resolved_source_path() source_metadata = image_file_source_metadata(resolved_source_path) source_dtype = source_metadata.source_dtype @@ -78,7 +69,7 @@ def metadata_projection( source_channel_axis = self.source_channel_axis(image, source_metadata) if source_binding is not None: source_channel_axis = source_binding.source_channel_axis_for_shape( - tuple((int(value) for value in np.shape(image_data))), + tuple(int(value) for value in np.shape(image_data)), observed_axis=source_channel_axis, ) source_spatial_shape_yx = ImagePayloadMetadata( @@ -104,13 +95,15 @@ def metadata_projection( ) if source_dtype is None: array_metadata = ImagePayloadMetadata.for_array( - image_payload_data(image), source_path=source_identity_path + image_payload_data(image), + source_path=source_identity_path, ) metadata = array_metadata.replace_fields( source_component_metadata=source_component_metadata, source_image_provenance_planes=source_image_provenance_planes, source_spatial_domain=SourceSpatialDomain( - origin_yx=spatial_origin_yx, source_shape_yx=source_spatial_shape_yx + origin_yx=spatial_origin_yx, + source_shape_yx=source_spatial_shape_yx, ), source_voxel_spacing=source_voxel_spacing, source_channel_axis=source_channel_axis, @@ -124,17 +117,16 @@ def metadata_projection( source_component_metadata=source_component_metadata, source_image_provenance_planes=source_image_provenance_planes, source_spatial_domain=SourceSpatialDomain( - origin_yx=spatial_origin_yx, source_shape_yx=source_spatial_shape_yx + origin_yx=spatial_origin_yx, + source_shape_yx=source_spatial_shape_yx, ), source_voxel_spacing=source_voxel_spacing, source_channel_axis=source_channel_axis, plane_axis=plane_axis, ) - metadata = metadata.derive_fields( - source_spatial_domain=existing_metadata.read_value( - "source_spatial_domain" - ).with_native_image_context( - metadata.read_value("source_spatial_domain"), + metadata = metadata.replace_fields( + source_spatial_domain=existing_metadata.source_spatial_domain.with_native_image_context( + metadata.source_spatial_domain, image_shape_yx=source_spatial_shape_yx, ) ) @@ -144,14 +136,15 @@ def metadata_projection( @staticmethod def source_channel_axis( - image: Any, source_metadata: ImageFileSourceMetadata + image: Any, + source_metadata: ImageFileSourceMetadata, ) -> int | None: """Return the channel axis declared by the current pixel carrier.""" if isinstance(image, ImagePayloadMetadataCarrier): - metadata = image_payload_metadata_projection(image) - if metadata.read_value("source_channel_axis") is not None: + metadata = image_payload_metadata(image) + if metadata.source_channel_axis is not None: metadata.normalized_source_channel_axis(image) - return metadata.read_value("source_channel_axis") + return metadata.source_channel_axis return source_metadata.pixel_semantics.validated_channel_axis(image) def resolved_source_path(self) -> Path | None: diff --git a/openhcs/core/runtime_image_values.py b/openhcs/core/runtime_image_values.py index 97460af617..661367cdef 100644 --- a/openhcs/core/runtime_image_values.py +++ b/openhcs/core/runtime_image_values.py @@ -2,19 +2,17 @@ from __future__ import annotations -from abc import ABC, abstractmethod, update_abstractmethods +from abc import ABC, abstractmethod from collections.abc import ( Iterable, Sequence, ) -from dataclasses import Field, dataclass, field, fields +from dataclasses import dataclass, field, fields from enum import Enum from pathlib import Path from types import MappingProxyType from typing import Any, TypeVar -import copyreg - import numpy as np from arraybridge import ArrayGeometry from python_introspect import dataclass_from_mapping @@ -110,378 +108,95 @@ def without_source_planes(self) -> "ImageUnitIntervalIntensityMetadata": return type(self)(scale=self.scale) -class ImageMetadataProjection(ABC): - """One declared metadata namespace, with nominal source and axis projections. - - Owned values are flat. Public realization transfers authority to a real mutable - dataclass instead of keeping a synchronized copy of its fields. - """ +@dataclass(slots=True) +class ImagePayloadMetadata( + SourceImageProvenanceFields, + SourceSpatialDomainFields, + SourceVoxelSpacingFields, +): + """Generic source-image metadata that should travel with runtime pixels.""" - __slots__ = ( - "_source_metadata", - "_metadata_type", - "_captured_values", - "_realized_metadata", + intensity_scale: float | None = None + source_dtype: str | None = None + unit_interval_intensity: ImageUnitIntervalIntensityMetadata | None = None + source_plane_intensity_scales: tuple[float | None, ...] = () + source_plane_dtypes: tuple[str | None, ...] = () + physical_border_edges_yx: PhysicalBorderEdgesYX = None + mask_defines_border: bool | None = None + source_channel_axis: int | None = field( + default=None, metadata={ViewerWireField.IMAGE_METADATA: True} + ) + plane_axis: RuntimePlaneAxis | None = field( + default=None, metadata={ViewerWireField.IMAGE_METADATA: True} ) - - def __init__(self, metadata: "ImageMetadataProjection") -> None: - object.__setattr__(self, "_source_metadata", metadata) - object.__setattr__(self, "_metadata_type", metadata.metadata_type) - object.__setattr__(self, "_captured_values", None) - object.__setattr__(self, "_realized_metadata", None) - - @property - def metadata(self) -> "ImageMetadataProjection": - return self._source_metadata - - @property - def metadata_type(self) -> type["ImagePayloadMetadata"]: - return self._metadata_type - - def declared_fields(self) -> tuple[Field[Any], ...]: - return fields(self.metadata_type) - - def read_field(self, member: Field[MetadataValueT]) -> MetadataValueT: - if self._realized_metadata is not None: - return getattr(self._realized_metadata, member.name) - if self._captured_values is not None: - return self._captured_values[member.name] - return self.capture().read_field(member) - - def read_value(self, name: str) -> Any: - if self._realized_metadata is not None: - return self._realized_metadata.read_value(name) - if self._captured_values is not None: - return self._captured_values[name] - return self.capture().read_value(name) - - def write_value(self, name: str, value: Any) -> None: - if self._realized_metadata is not None: - setattr(self._realized_metadata, name, value) - else: - self._captured_values[name] = value - - def field_values(self) -> dict[str, Any]: - if self._realized_metadata is not None: - return self._realized_metadata.field_values() - if self._captured_values is not None: - return dict(self._captured_values) - return self.metadata.field_values() - - @classmethod - def _from_captured_values( - cls, metadata_type: type["ImagePayloadMetadata"], values: dict[str, Any] - ) -> "ImageMetadataProjection": - expected = tuple(member.name for member in fields(metadata_type)) - if tuple(values) != expected: - raise ValueError( - "Captured image metadata does not match its declared schema." - ) - result = ImageMetadataProjection.__new__(ImageMetadataProjection) - object.__setattr__(result, "_source_metadata", None) - object.__setattr__(result, "_metadata_type", metadata_type) - object.__setattr__(result, "_captured_values", values) - object.__setattr__(result, "_realized_metadata", None) - return result - - @classmethod - def _from_realized_metadata(cls, metadata): - result = ImageMetadataProjection.__new__(ImageMetadataProjection) - object.__setattr__(result, "_source_metadata", None) - object.__setattr__(result, "_metadata_type", type(metadata)) - object.__setattr__(result, "_captured_values", None) - object.__setattr__(result, "_realized_metadata", metadata) - return result - - @staticmethod - def _construct_metadata(metadata_type, values): - result = metadata_type( - **{ - member.name: values[member.name] - for member in fields(metadata_type) - if member.init - } - ) - return result - - @staticmethod - def _uses_declared_constructor(metadata_type): - """Constructor overrides and dependent fields are nominal behavior.""" - return ( - metadata_type.__init__ is not ImagePayloadMetadata.__init__ - or metadata_type.__post_init__ is not ImagePayloadMetadata.__post_init__ - or metadata_type.__new__ is not object.__new__ - or metadata_type.normalize_metadata_fields - is not ImagePayloadMetadata.normalize_metadata_fields - or metadata_type.source_component_metadata - is not SourceImageProvenanceFields.source_component_metadata - or metadata_type.source_path is not SourceImageProvenanceFields.source_path - or metadata_type.normalize_source_provenance_fields - is not SourceImageProvenanceFields.normalize_source_provenance_fields - or metadata_type.normalize_source_spatial_domain_fields - is not SourceSpatialDomainFields.normalize_source_spatial_domain_fields - or metadata_type.normalize_source_voxel_spacing_fields - is not SourceVoxelSpacingFields.normalize_source_voxel_spacing_fields - or metadata_type.validate_source_channel_axis - is not ImageMetadataProjection.validate_source_channel_axis - or any(not member.init for member in fields(metadata_type)) - ) @classmethod - def _normalize_values(cls, metadata_type, values): - """Run the common ordered lifecycle on the flat, declared namespace.""" - owner = cls._from_captured_values(metadata_type, values) - owner.normalize_metadata_fields() - return owner._captured_values - - def normalize_metadata_fields(self) -> None: - """Normalize declared facts through their nominal lifetime hooks.""" - self.write_value( - "source_voxel_spacing", - self.read_value("source_voxel_spacing").with_missing_from( - SourceVoxelSpacing.from_source_metadata( - self.source_component_metadata_for_projection() - ) - ), - ) - self.normalize_source_provenance_fields() - self.normalize_source_spatial_domain_fields() - self.normalize_source_voxel_spacing_fields() - self.validate_source_channel_axis() - plane_axis = self.read_value("plane_axis") - if plane_axis is not None: - self.write_value("plane_axis", RuntimePlaneAxis(plane_axis)) - - def source_component_metadata_for_projection(self): - """Read the nominal scalar view, preserving a realized subtype's alias.""" - if self._realized_metadata is not None: - return self._realized_metadata.source_component_metadata_for_projection() - return self.read_value("source_provenance").source_component_metadata - - def source_path_for_projection(self): - if self._realized_metadata is not None: - return self._realized_metadata.source_path_for_projection() - return self.read_value("source_provenance").source_path - - def normalize_source_provenance_fields(self) -> None: - # Captured namespaces and mutable dataclasses both birth a new source - # identity here. An existing provenance object is publicly mutable. - provenance = self.read_value("source_provenance") - self.write_value( - "source_provenance", - SourceImageProvenance( - provenance.source_path, - provenance.source_component_metadata, - provenance.source_image_provenance_planes, - provenance.source_image_names, - ), - ) - - def normalize_source_spatial_domain_fields(self) -> None: - self.write_value( - "source_spatial_domain", - self.read_value("source_spatial_domain").normalized(), - ) - - def normalize_source_voxel_spacing_fields(self) -> None: - self.write_value( - "source_voxel_spacing", - SourceVoxelSpacing.coerce(self.read_value("source_voxel_spacing")), - ) - - def project_source_values(self, values: dict[str, Any]) -> dict[str, Any]: - return self._normalize_values(self.metadata_type, values) - - def project_axis_values(self, values: dict[str, Any]) -> dict[str, Any]: - return values - - def capture(self) -> "ImageMetadataProjection": - if self._captured_values is not None: - return self - if self._uses_declared_constructor(self.metadata_type): - # A genuine constructor extension keeps its original one-namespace - # public lifecycle; capture retains its facts without a live chain. - return self._from_realized_metadata(self.project()) - values = self.project_axis_values( - self.project_source_values(self.field_values()) - ) - return self._from_captured_values(self.metadata_type, values) - - def derive_fields(self, **changes: Any) -> "ImageMetadataProjection": - values = self.field_values() - for name in changes: - member = self.metadata_type.__dataclass_fields__.get(name) - if member is None or member not in self.declared_fields(): - raise TypeError(f"Undeclared image metadata field {name!r}.") - if not member.init: - raise ValueError( - f"field {name} is declared with init=False, it cannot be specified with replace()" - ) - values.update(changes) - if self._uses_declared_constructor(self.metadata_type): - return self._from_realized_metadata( - self._construct_metadata(self.metadata_type, values) - ) - return self._from_captured_values( - self.metadata_type, self._normalize_values(self.metadata_type, values) - ) - - def replace_fields(self, **changes: Any) -> "ImageMetadataProjection": - return self.derive_fields(**changes) - - def materialize_metadata(self) -> "ImagePayloadMetadata": - if self._realized_metadata is None: - if self._captured_values is None: - return self.capture().materialize_metadata() - result = self.metadata_type.__new__(self.metadata_type) - for member in self.declared_fields(): - object.__setattr__( - result, member.name, self._captured_values[member.name] - ) - object.__setattr__(self, "_realized_metadata", result) - object.__setattr__(self, "_captured_values", None) - return self._realized_metadata - - def project(self) -> "ImagePayloadMetadata": - """Explicit public projection retains the concrete nominal constructor.""" - if self._source_metadata is None: - return self._construct_metadata(self.metadata_type, self.field_values()) - return self.project_axis_metadata(self.project_source_metadata()) - - def project_source_metadata(self): - return self.metadata.materialize_metadata() - - def project_axis_metadata(self, metadata): - return metadata - - def project_source_provenance(self, metadata, provenance): - return metadata.replace_fields(source_provenance=provenance) - - def to_metadata_mapping(self) -> dict[str, Any]: - return { - member.name: to_jsonable(self.read_field(member)) - for member in self.declared_fields() - } - - def __reduce__(self): - if self._source_metadata is not None: - return (type(self), (self.metadata,)) - if self._realized_metadata is not None: - return ( - _restore_realized_image_metadata_projection, - (self._realized_metadata,), - ) - values = self.field_values() - return ( - _restore_image_metadata_projection, - (self.metadata_type, tuple(values.items())), - ) - - def without_leading_plane_axis(self, *, projection=None): - selected = ( - LeadingPlaneAxisMetadataProjection(self) - if projection is None - else projection - ) - return selected.capture() - - def for_source_plane(self, plane_index: int): - return SourcePlaneImageMetadataProjection(self, plane_index).capture() - - def with_source_context_from(self, source): - """Derive identity, axes and spatial context in one result namespace.""" - source_provenance = self.read_value("source_provenance").with_missing_from( - source.read_value("source_provenance") - ) - channel_axis = self.read_value("source_channel_axis") - if channel_axis is None: - channel_axis = source.read_value("source_channel_axis") - plane_axis = self.read_value("plane_axis") - source_axis = source.read_value("plane_axis") - if plane_axis is None: - plane_axis = source_axis - elif source_axis is not None and source_axis is not plane_axis: - raise ValueError( - "Cannot combine image metadata with conflicting plane axes: " - f"{plane_axis.value!r} != {source_axis.value!r}." - ) - spatial_domain = self.read_value("source_spatial_domain").with_missing_from( - source.read_value("source_spatial_domain") - ) - return self.replace_fields( - source_provenance=source_provenance, - source_channel_axis=channel_axis, - plane_axis=plane_axis, - source_spatial_domain=spatial_domain, - source_voxel_spacing=self.read_value( - "source_voxel_spacing" - ).with_missing_from(source.read_value("source_voxel_spacing")), - physical_border_edges_yx=( - self.read_value("physical_border_edges_yx") - if self.read_value("physical_border_edges_yx") is not None - else source.read_value("physical_border_edges_yx") - ), - mask_defines_border=( - self.read_value("mask_defines_border") - if self.read_value("mask_defines_border") is not None - else source.read_value("mask_defines_border") - ), + def viewer_field_names(cls) -> frozenset[str]: + """Derive the viewer projection from its actual metadata declarations.""" + return frozenset( + member.name + for member in fields(cls) + if member.metadata.get(ViewerWireField.IMAGE_METADATA, False) ) - def payload_with(self, data, mask=None): - if mask is None: - return ( - ImageMetadataPayload.from_projection(data, self) - if self.has_values - else data - ) - return MaskedImagePayload.from_projection(data, mask, self) - - def for_source_plane(self, plane_index): - return SourcePlaneImageMetadataProjection(self, plane_index).capture() - - def for_leading_source_plane(self, plane_index): - return LeadingSourcePlaneMetadataProjection(self, plane_index).capture() - def require_scalar_source_plane(self) -> None: """Require source metadata for one scalar grayscale image plane.""" - if self.read_value("plane_axis") is not None: + if self.plane_axis is not None: raise ValueError("Exported source planes require scalar image metadata.") - if self.read_value("source_channel_axis") is not None: + if self.source_channel_axis is not None: raise ValueError("Exported Z planes cannot carry an undeclared color axis.") def require_source_image_pixels(self, data: Any) -> None: """Validate decoded pixels against declared XY placement and dtype.""" - self.read_value("source_spatial_domain").require_image_window(data.shape) - if ( - self.read_value("source_dtype") is not None - and np.dtype(self.read_value("source_dtype")) != data.dtype - ): + self.source_spatial_domain.require_image_window(data.shape) + if self.source_dtype is not None and np.dtype(self.source_dtype) != data.dtype: raise ValueError( "Exported image pixels conflict with their declared dtype." ) def singleton_plane_projection(self) -> RuntimePlaneAxisValueProjection | None: """Select a declared leading axis only with one exact runtime source plane.""" - plane_count = self.read_value("source_provenance").source_plane_count - if self.read_value("plane_axis") is None or plane_count != 1: + plane_count = self.source_provenance.source_plane_count + if self.plane_axis is None or plane_count != 1: return None return RuntimePlaneAxisValueProjection.from_selected_plane( - axis=self.read_value("plane_axis"), + axis=self.plane_axis, axis_size=plane_count, plane_index=0, - source_aliases=self.read_value("source_provenance").source_image_names, + source_aliases=self.source_image_names, ) def to_viewer_image_metadata(self) -> ViewerWireMapping: return ViewerWirePayload.mapping( { - name: to_jsonable(self.read_value(name)) - for name in self.metadata_type.viewer_field_names() + name: to_jsonable(getattr(self, name)) + for name in self.viewer_field_names() }, context="viewer image metadata", ) + @classmethod + def from_viewer_image_metadata( + cls, values: Mapping[str, object] + ) -> "ImagePayloadMetadata": + """Decode only declaration-owned viewer fields, never hidden source records.""" + if not isinstance(values, Mapping): + raise TypeError("Viewer image metadata requires a mapping.") + extras = set(values) - cls.viewer_field_names() + if extras: + raise ValueError(f"Undeclared viewer metadata fields: {sorted(extras)!r}.") + return cls.from_mapping(values) + + @classmethod + def from_mapping(cls, values: Mapping[str, object]) -> "ImagePayloadMetadata": + """Restore all declared metadata fields through their canonical codecs.""" + decoded = dict(values) + if "source_provenance" in decoded: + decoded["source_provenance"] = SourceImageProvenance.from_mapping( + decoded["source_provenance"] + ) + return dataclass_from_mapping(cls, decoded) + def retained_plane_component_values( self, ) -> dict[str, tuple[SourceMetadataScalar, ...]]: @@ -491,22 +206,78 @@ def retained_plane_component_values( Only a retained plane-axis declaration makes those coordinates a pixel axis; artifact storage/grouping axes do not declare that image domain. """ - if self.read_value("plane_axis") is None: + + if self.plane_axis is None: return {} - return self.read_value("source_provenance").varying_plane_component_values( + return self.source_provenance.varying_plane_component_values( tuple(AllComponents) ) + @classmethod + def for_array( + cls, + array: Any, + *, + source_path: str | None = None, + ) -> "ImagePayloadMetadata": + """Build metadata from an image array's source dtype.""" + import numpy as np + + dtype = np.asarray(array).dtype + return cls( + intensity_scale=image_intensity_scale_for_dtype(dtype), + source_dtype=str(dtype), + source_path=source_path, + ) + + @classmethod + def for_array_payload( + cls, + array: Any, + *, + source_path: str | None = None, + ) -> "ImagePayloadMetadata": + """Build source metadata from an arraybridge-detectable payload.""" + from openhcs.core.memory import MEMORY_TYPE_NUMPY, detect_memory_type + + memory_type = detect_memory_type(array) + dtype = array.dtype + if memory_type == MEMORY_TYPE_NUMPY: + intensity_scale = image_intensity_scale_for_dtype(dtype) + else: + intensity_scale = None + return cls( + intensity_scale=intensity_scale, + source_dtype=str(dtype), + source_path=source_path, + ) + + def __post_init__(self, *source_provenance_values: object) -> None: + self.absorb_explicit_source_provenance(source_provenance_values) + self.normalize_metadata_fields() + + def normalize_metadata_fields(self) -> None: + """Normalize this metadata's typed fields in their constructor effect order.""" + self.source_voxel_spacing = self.source_voxel_spacing.with_missing_from( + SourceVoxelSpacing.from_source_metadata(self.source_component_metadata) + ) + self.normalize_source_provenance_fields() + self.normalize_source_spatial_domain_fields() + self.normalize_source_voxel_spacing_fields() + self.validate_source_channel_axis() + if self.plane_axis is not None: + self.plane_axis = RuntimePlaneAxis(self.plane_axis) + def require_leading_plane_axis(self, message: str) -> None: """Require axis presence before later ordered projection validation.""" - if self.read_value("plane_axis") is None: + if self.plane_axis is None: raise ValueError(message) def validate_source_channel_axis(self) -> None: """Validate the authored channel declaration before transforming axes.""" - if self.read_value("source_channel_axis") is not None and ( - not isinstance(self.read_value("source_channel_axis"), int) - or isinstance(self.read_value("source_channel_axis"), bool) + if self.source_channel_axis is not None and ( + not isinstance(self.source_channel_axis, int) + or isinstance(self.source_channel_axis, bool) ): raise TypeError( "ImagePayloadMetadata.source_channel_axis must be int or None." @@ -517,40 +288,41 @@ def has_values(self) -> bool: """Return whether this metadata carries any semantic image facts.""" return any( ( - self.read_value("intensity_scale") is not None, - self.read_value("source_dtype") is not None, - self.read_value("source_provenance").source_path is not None, - self.read_value("source_provenance").source_component_metadata - is not None, - self.read_value("unit_interval_intensity") is not None, - bool(self.read_value("source_plane_intensity_scales")), - bool(self.read_value("source_plane_dtypes")), - self.read_value( - "source_provenance" - ).source_image_provenance_planes.has_values, - self.read_value("source_spatial_domain").has_values, - self.read_value("source_voxel_spacing").has_values, - self.read_value("physical_border_edges_yx") is not None, - self.read_value("mask_defines_border") is not None, - bool(self.read_value("source_provenance").source_image_names), - self.read_value("source_channel_axis") is not None, - self.read_value("plane_axis") is not None, + self.intensity_scale is not None, + self.source_dtype is not None, + self.source_path is not None, + self.source_component_metadata is not None, + self.unit_interval_intensity is not None, + bool(self.source_plane_intensity_scales), + bool(self.source_plane_dtypes), + self.source_image_provenance_planes.has_values, + self.source_spatial_domain.has_values, + self.source_voxel_spacing.has_values, + self.physical_border_edges_yx is not None, + self.mask_defines_border is not None, + bool(self.source_image_names), + self.source_channel_axis is not None, + self.plane_axis is not None, ) ) @property def unit_interval_intensity_scale(self) -> int | None: """Return the authored scalar unit-interval quantization proof.""" - if self.read_value("unit_interval_intensity") is None: + + if self.unit_interval_intensity is None: return None - return self.read_value("unit_interval_intensity").scale + return self.unit_interval_intensity.scale @property - def source_plane_unit_interval_intensity_scales(self) -> tuple[int | None, ...]: + def source_plane_unit_interval_intensity_scales( + self, + ) -> tuple[int | None, ...]: """Return authored per-plane unit-interval quantization proofs.""" - if self.read_value("unit_interval_intensity") is None: + + if self.unit_interval_intensity is None: return () - return self.read_value("unit_interval_intensity").source_plane_scales + return self.unit_interval_intensity.source_plane_scales def attach_to(self, payload: Any) -> RuntimeArrayData: """Attach this metadata to an existing image payload.""" @@ -560,11 +332,15 @@ def attach_to(self, payload: Any) -> RuntimeArrayData: def attach_source_context_to(self, payload: Any) -> RuntimeArrayData: """Attach source context while retaining the payload's declared array axes.""" - payload_metadata = image_payload_metadata_projection(payload) + + payload_metadata = image_payload_metadata(payload) return self.replace_fields( - source_channel_axis=payload_metadata.read_value("source_channel_axis"), - plane_axis=payload_metadata.read_value("plane_axis"), - ).payload_with(image_payload_data(payload), image_payload_mask(payload)) + source_channel_axis=payload_metadata.source_channel_axis, + plane_axis=payload_metadata.plane_axis, + ).payload_with( + image_payload_data(payload), + image_payload_mask(payload), + ) def derive_payload( self, @@ -575,19 +351,19 @@ def derive_payload( ) -> RuntimeArrayData: """Project this source metadata and mask onto derived image data.""" source_shape_yx = self.spatial_shape_yx(source_payload) - output_shape_yx = image_payload_metadata_projection(data).spatial_shape_yx(data) + output_shape_yx = image_payload_metadata(data).spatial_shape_yx(data) same_spatial_domain = ( source_shape_yx is not None and output_shape_yx is not None - and (source_shape_yx == output_shape_yx) + and source_shape_yx == output_shape_yx ) - output_metadata = image_payload_metadata_projection(data) + output_metadata = image_payload_metadata(data) metadata = self._derived_metadata(data, plane_projection) output_declares_spatial_domain = ( isinstance(data, ImagePayloadMetadataCarrier) - and output_metadata.read_value("source_spatial_domain").has_values + and output_metadata.source_spatial_domain.has_values ) - if not same_spatial_domain and (not output_declares_spatial_domain): + if not same_spatial_domain and not output_declares_spatial_domain: metadata = metadata.without_spatial_domain() output_mask = image_payload_mask(data) if output_mask is None and same_spatial_domain: @@ -603,7 +379,7 @@ def _derived_metadata( data: RuntimeArrayData, plane_projection: RuntimePlaneAxisValueProjection | None, ) -> "ImagePayloadMetadata": - output_metadata = image_payload_metadata_projection( + output_metadata = image_payload_metadata( data ).with_indexed_source_plane_provenance(None) source_metadata = self.with_indexed_source_plane_provenance(None) @@ -611,19 +387,18 @@ def _derived_metadata( isinstance(data, ImagePayloadMetadataCarrier) and output_metadata.has_values ) declared_output_axis = ( - output_metadata.read_value("plane_axis") + output_metadata.plane_axis if output_metadata_is_authoritative - else source_metadata.read_value("plane_axis") + else source_metadata.plane_axis ) if plane_projection is None and ( - declared_output_axis is not None - or source_metadata.read_value("plane_axis") is not None + declared_output_axis is not None or source_metadata.plane_axis is not None ): - declared_axis = declared_output_axis or source_metadata.read_value( - "plane_axis" - ) + declared_axis = declared_output_axis or source_metadata.plane_axis raise ValueError( - f"Derived image payload carries a declared plane axis but the invocation supplied no plane projection: {declared_axis.value!r}." + "Derived image payload carries a declared plane axis but the " + "invocation supplied no plane projection: " + f"{declared_axis.value!r}." ) if plane_projection is not None and plane_projection.plane_index is None: for owner_name, metadata in ( @@ -631,11 +406,14 @@ def _derived_metadata( ("output", output_metadata), ): if ( - metadata.read_value("plane_axis") is not None - and metadata.read_value("plane_axis") is not plane_projection.axis + metadata.plane_axis is not None + and metadata.plane_axis is not plane_projection.axis ): raise ValueError( - f"Derived image {owner_name} metadata axis conflicts with the invocation projection: {metadata.read_value('plane_axis').value!r} != {plane_projection.axis.value!r}." + f"Derived image {owner_name} metadata axis conflicts with " + "the invocation projection: " + f"{metadata.plane_axis.value!r} != " + f"{plane_projection.axis.value!r}." ) source_metadata = source_metadata.with_indexed_source_plane_provenance( plane_projection.axis_size @@ -643,7 +421,8 @@ def _derived_metadata( if declared_output_axis is not None: plane_projection.validate_shape( image_payload_geometry( - data, value_name="Derived image payload" + data, + value_name="Derived image payload", ).shape, value_name="Derived image payload", ) @@ -655,27 +434,25 @@ def _derived_metadata( ("source", source_metadata), ("output", output_metadata), ): - if metadata.read_value("plane_axis") is not None: + if metadata.plane_axis is not None: raise ValueError( - f"Derived image {owner_name} metadata retains an unconsumed {metadata.read_value('plane_axis').value!r} axis after the invocation selected one plane." + f"Derived image {owner_name} metadata retains an unconsumed " + f"{metadata.plane_axis.value!r} axis after the invocation " + "selected one plane." ) source_context = source_metadata.replace_fields(plane_axis=None) if isinstance(data, ImagePayloadMetadataCarrier): source_context = source_context.replace_fields(source_channel_axis=None) metadata = output_metadata.with_source_context_from(source_context) - if source_metadata.read_value("source_provenance").has_values: + if source_metadata.source_provenance.has_values: metadata = metadata.with_source_provenance( - source_metadata.read_value( - "source_provenance" - ).with_derived_source_image_names( - output_metadata.read_value("source_provenance").source_image_names - or source_metadata.read_value( - "source_provenance" - ).source_image_names + source_metadata.source_provenance.with_derived_source_image_names( + output_metadata.source_image_names + or source_metadata.source_image_names ) ) return metadata.with_missing_intensity_from(source_metadata).replace_fields( - plane_axis=declared_output_axis + plane_axis=declared_output_axis, ) def project_channel_payload( @@ -690,13 +467,16 @@ def project_channel_payload( """Project one channel while preserving metadata and mask semantics.""" if channel_data is None: channel_data = ImageMaskDomain.channel_axis_slice( - source_data, channel_axis=channel_axis, channel_index=channel_index + source_data, + channel_axis=channel_axis, + channel_index=channel_index, ) source_channel_axis = self.normalized_source_channel_axis(source_data) projected_axis = ( channel_axis % image_payload_geometry( - source_data, value_name="Source image payload" + source_data, + value_name="Source image payload", ).ndim ) metadata = ( @@ -723,37 +503,58 @@ def has_complete_source_identity( plane_projection: RuntimePlaneAxisValueProjection | None = None, ) -> bool: """Validate source identity against declared runtime-plane semantics.""" - planes = self.read_value("source_provenance").source_image_provenance_planes + planes = self.source_image_provenance_planes if ( plane_projection is not None and plane_projection.plane_index is None - and (plane_projection.axis_size > 1) - and (self.read_value("plane_axis") is None) + and plane_projection.axis_size > 1 + and self.plane_axis is None ): return False - if plane_projection is not None and self.read_value("plane_axis") is not None: - if self.read_value("plane_axis") is not plane_projection.axis: + if plane_projection is not None and self.plane_axis is not None: + if self.plane_axis is not plane_projection.axis: raise ValueError( - f"Image payload plane declaration conflicts with the execution projection: {self.read_value('plane_axis')!r} != {plane_projection.axis!r}." + "Image payload plane declaration conflicts with the execution " + f"projection: {self.plane_axis!r} != {plane_projection.axis!r}." ) plane_projection.validate_shape( image_payload_geometry( - payload, value_name="Source-identified image payload" + payload, + value_name="Source-identified image payload", ).shape, value_name="Source-identified image payload", ) if planes.count != plane_projection.axis_size: return False if planes.has_values: - return all((plane.addressable for plane in planes.planes)) - return self.read_value("source_provenance").addressable + return all(plane.addressable for plane in planes.planes) + return self.source_provenance.addressable + + @classmethod + def compose( + cls, + payloads: Sequence[RuntimeArrayData], + *, + mode: "ImagePayloadMetadataCompositionMode | None" = None, + 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, + source_metadata_override=source_metadata, + metadata_type=cls, + ).compose() def normalized_source_channel_axis(self, data: Any) -> int | None: """Return this declared channel axis normalized for ``data``.""" - if self.read_value("source_channel_axis") is None: + if self.source_channel_axis is None: return None ndim = image_payload_geometry(data).ndim - axis = self.read_value("source_channel_axis") + axis = self.source_channel_axis normalized = axis if axis >= 0 else ndim + axis if normalized < 0 or normalized >= ndim: raise ValueError( @@ -765,22 +566,22 @@ def spatial_axes_yx(self, data: Any) -> tuple[int, int] | None: """Return Y/X axes after excluding the declared channel axis.""" ndim = image_payload_geometry(data).ndim channel_axis = self.normalized_source_channel_axis(data) - candidate_axes = tuple((axis for axis in range(ndim) if axis != channel_axis)) + candidate_axes = tuple(axis for axis in range(ndim) if axis != channel_axis) if len(candidate_axes) < 2: return None - return (candidate_axes[-2], candidate_axes[-1]) + return candidate_axes[-2], candidate_axes[-1] def is_declared_source_channel_plane(self, data: Any) -> bool: """Return whether this payload declares one channel-bearing image plane.""" if self.normalized_source_channel_axis(data) is None: return False - return self.read_value("plane_axis") is None + return self.plane_axis is None def is_declared_source_channel_stack(self, data: Any) -> bool: """Return whether this payload declares a plane stack with a channel axis.""" if self.normalized_source_channel_axis(data) is None: return False - return self.read_value("plane_axis") is not None + return self.plane_axis is not None def spatial_shape_yx(self, data: Any) -> tuple[int, int] | None: """Return Y/X shape using only declared channel-axis semantics.""" @@ -788,19 +589,19 @@ def spatial_shape_yx(self, data: Any) -> tuple[int, int] | None: if axes is None: return None shape = image_payload_geometry(data).shape - return (shape[axes[0]], shape[axes[1]]) + return shape[axes[0]], shape[axes[1]] def mask_domain(self, data: Any) -> "ImageMaskDomain": """Return the mask domain declared for this payload.""" spatial_axes_yx = ( self.spatial_axes_yx(data) - if self.read_value("source_spatial_domain").source_shape_yx is not None + if self.source_spatial_domain.source_shape_yx is not None else None ) return ImageMaskDomain( image_payload_geometry(data).shape, self.normalized_source_channel_axis(data), - self.read_value("plane_axis"), + self.plane_axis, spatial_axes_yx, ) @@ -808,13 +609,44 @@ def without_source_channel_axis(self) -> "ImagePayloadMetadata": """Return metadata after an operation collapses the source channel axis.""" return self.replace_fields(source_channel_axis=None) + def for_leading_source_plane(self, plane_index: int) -> "ImagePayloadMetadata": + """Project metadata after explicitly removing its leading plane axis.""" + return LeadingSourcePlaneMetadataProjection(self, plane_index).project() + + def without_leading_plane_axis( + self, *, projection: "ImageMetadataProjection | None" = None + ) -> "ImagePayloadMetadata": + """Remove an axis through the projection's declared result ownership.""" + self.require_leading_plane_axis( + "Image metadata has no leading plane axis to remove." + ) + source_channel_axis = self.source_channel_axis + if source_channel_axis == 0: + raise ValueError( + "Image metadata cannot declare the same leading axis as both " + "plane and channel." + ) + if source_channel_axis is not None and source_channel_axis > 0: + source_channel_axis -= 1 + if projection is None: + projection = LeadingPlaneAxisMetadataProjection(self) + projected = projection.project_source_provenance( + self, self.source_provenance.with_runtime_planes_as_contributors() + ) + projected.plane_axis = None + projected.source_channel_axis = source_channel_axis + projected.source_plane_intensity_scales = () + projected.source_plane_dtypes = () + projected.unit_interval_intensity = self.project_intensity_proof(None) + projected.normalize_metadata_fields() + return projected + def collapse_leading_plane_axis(self) -> "ImagePayloadMetadata": """Return scalar metadata after reducing every plane of the leading axis.""" + collapsed = self.without_leading_plane_axis() return collapsed.with_source_provenance( - collapsed.read_value( - "source_provenance" - ).with_common_scalar_identity_from_planes() + collapsed.source_provenance.with_common_scalar_identity_from_planes() ) @property @@ -822,11 +654,11 @@ def has_plane_specific_values(self) -> bool: """Return whether selecting a payload plane can change metadata.""" return any( ( - bool(self.read_value("source_plane_intensity_scales")), - bool(self.read_value("source_plane_dtypes")), + bool(self.source_plane_intensity_scales), + bool(self.source_plane_dtypes), bool(self.source_plane_unit_interval_intensity_scales), - self.read_value("source_provenance").source_plane_count > 0, - len(self.read_value("source_provenance").source_image_names) > 1, + self.source_provenance.source_plane_count > 0, + len(self.source_image_names) > 1, ) ) @@ -835,156 +667,164 @@ def source_image_paths(self) -> tuple[str, ...]: """Return paths from exact source identities represented by this payload.""" return tuple( dict.fromkeys( - ( - str(identity.path) - for identity in self.read_value( - "source_provenance" - ).represented_source_identities - if identity.path is not None and str(identity.path) - ) + str(identity.path) + for identity in self.source_provenance.represented_source_identities + if identity.path is not None and str(identity.path) ) ) def intensity_scale_for_source_plane(self, plane_index: int) -> float | None: """Return the best available intensity scale for one source plane.""" - plane_scale = _tuple_value( - self.read_value("source_plane_intensity_scales"), plane_index - ) - return ( - self.read_value("intensity_scale") if plane_scale is None else plane_scale - ) + plane_scale = _tuple_value(self.source_plane_intensity_scales, plane_index) + return self.intensity_scale if plane_scale is None else plane_scale def unit_interval_intensity_scale_for_source_plane( - self, plane_index: int + self, + plane_index: int, ) -> int | None: """Return the scale proving current pixels are exact integer/scale values.""" - if self.read_value("unit_interval_intensity") is None: + if self.unit_interval_intensity is None: return None - return self.read_value("unit_interval_intensity").scale_for_source_plane( - plane_index - ) + return self.unit_interval_intensity.scale_for_source_plane(plane_index) def project_intensity_proof( self, plane_index: int | None ) -> ImageUnitIntervalIntensityMetadata | None: """Select a plane's proof, or retain only the proof for the whole image.""" - if self.read_value("unit_interval_intensity") is None: + if self.unit_interval_intensity is None: return None if plane_index is None: - return self.read_value("unit_interval_intensity").without_source_planes() - return self.read_value("unit_interval_intensity").for_source_plane(plane_index) + return self.unit_interval_intensity.without_source_planes() + return self.unit_interval_intensity.for_source_plane(plane_index) @property def source_plane_metadata_count(self) -> int: """Return the cardinality represented by plane-specific metadata.""" return max( 1, - self.read_value("source_provenance").source_plane_count, - len(self.read_value("source_plane_intensity_scales")), - len(self.read_value("source_plane_dtypes")), + self.source_provenance.source_plane_count, + len(self.source_plane_intensity_scales), + len(self.source_plane_dtypes), len(self.source_plane_unit_interval_intensity_scales), ) def source_plane_metadata_records(self) -> tuple["ImagePayloadMetadata", ...]: """Return one scalar metadata record per represented source plane.""" - if self.source_plane_metadata_count == 1 and ( - not self.read_value("source_provenance").source_plane_count + if ( + self.source_plane_metadata_count == 1 + and not self.source_provenance.source_plane_count ): return (self,) return tuple( - ( - self.for_source_plane(plane_index) - for plane_index in range(self.source_plane_metadata_count) - ) + self.for_source_plane(plane_index) + for plane_index in range(self.source_plane_metadata_count) ) def common_unit_interval_intensity_scale(self) -> int | None: """Return the common unit-interval quantization proof for this payload.""" if self.source_plane_unit_interval_intensity_scales: present = tuple( - ( - int(scale) - for plane_index in range( - len(self.source_plane_unit_interval_intensity_scales) - ) - for scale in ( - self.unit_interval_intensity_scale_for_source_plane( - plane_index - ), - ) - if scale is not None + int(scale) + for plane_index in range( + len(self.source_plane_unit_interval_intensity_scales) + ) + for scale in ( + self.unit_interval_intensity_scale_for_source_plane(plane_index), ) + if scale is not None ) if not present: return None first = present[0] - if all((scale == first for scale in present)): + if all(scale == first for scale in present): return first return None return self.unit_interval_intensity_scale - def for_source_planes(self, plane_indices: Sequence[int]) -> "ImagePayloadMetadata": + def payload_with(self, data: Any, mask: Any | None = None) -> Any: + """Return image payload data carrying this metadata.""" + if mask is None and self.has_values: + return ImageMetadataPayload(data=data, metadata=self) + if mask is None: + return data + return MaskedImagePayload(data=data, mask=mask, metadata=self) + + def for_source_plane(self, plane_index: int) -> "ImagePayloadMetadata": + """Return metadata for one source plane sliced from a stacked payload.""" + return SourcePlaneImageMetadataProjection(self, plane_index).project() + + def for_source_planes( + self, + plane_indices: Sequence[int], + ) -> "ImagePayloadMetadata": """Return metadata projected to an ordered group of source planes.""" - normalized_indices = tuple((int(index) for index in plane_indices)) + normalized_indices = tuple(int(index) for index in plane_indices) if not normalized_indices: raise ValueError("Source-plane metadata projection cannot be empty.") if not self.has_plane_specific_values: return self invalid_indices = tuple( - ( - index - for index in normalized_indices - if index < 0 or index >= self.source_plane_metadata_count - ) + index + for index in normalized_indices + if index < 0 or index >= self.source_plane_metadata_count ) if invalid_indices: raise IndexError( - f"Source-plane metadata projection indices must be within 0..{self.source_plane_metadata_count - 1}; got {invalid_indices!r}." + "Source-plane metadata projection indices must be within " + f"0..{self.source_plane_metadata_count - 1}; got " + f"{invalid_indices!r}." ) if len(normalized_indices) == 1: return self.for_source_plane(normalized_indices[0]) return self.with_source_provenance( - self.read_value("source_provenance").for_source_planes(normalized_indices) + self.source_provenance.for_source_planes(normalized_indices) ).replace_fields( source_plane_intensity_scales=_tuple_values_at_indices( - self.read_value("source_plane_intensity_scales"), normalized_indices + self.source_plane_intensity_scales, + normalized_indices, ), source_plane_dtypes=_tuple_values_at_indices( - self.read_value("source_plane_dtypes"), normalized_indices + self.source_plane_dtypes, + normalized_indices, ), unit_interval_intensity=( None - if self.read_value("unit_interval_intensity") is None - else self.read_value("unit_interval_intensity").for_source_planes( - normalized_indices - ) + if self.unit_interval_intensity is None + else self.unit_interval_intensity.for_source_planes(normalized_indices) ), ) def project_declared_source_image( - self, payload: RuntimeArrayData, source_image_name: str + self, + payload: RuntimeArrayData, + source_image_name: str, ) -> RuntimeArrayData: """Project pixels and metadata to one exact declared source image.""" - source_plane_selection = self.read_value( - "source_provenance" - ).source_plane_selection(source_image_name) + + source_plane_selection = self.source_provenance.source_plane_selection( + source_image_name + ) if source_plane_selection is None: raise ValueError( - f"Image metadata does not represent declared source image {source_image_name!r}; represented names are {self.read_value('source_provenance').represented_source_image_names!r}, with provenance planes {self.read_value('source_provenance').source_image_provenance_planes.identity!r}." + f"Image metadata does not represent declared source image " + f"{source_image_name!r}; represented names are " + f"{self.source_provenance.represented_source_image_names!r}, " + "with provenance planes " + f"{self.source_image_provenance_planes.identity!r}." ) if not source_plane_selection: return self.attach_to(payload) - complete_source_axis = tuple( - range(self.read_value("source_provenance").source_plane_count) - ) - if self.read_value("plane_axis") is None: + complete_source_axis = tuple(range(self.source_provenance.source_plane_count)) + if self.plane_axis is None: if source_plane_selection == complete_source_axis: return self.attach_to(payload) channel_axis = self.normalized_source_channel_axis(payload) if channel_axis is not None: if len(source_plane_selection) != 1: raise ValueError( - f"Declared source-image channel projection requires exactly one channel; got source planes {source_plane_selection!r} for {source_image_name!r}." + "Declared source-image channel projection requires exactly " + f"one channel; got source planes {source_plane_selection!r} " + f"for {source_image_name!r}." ) return self.project_channel_payload( payload, @@ -993,59 +833,64 @@ def project_declared_source_image( channel_axis=channel_axis, ) raise ValueError( - "Declared source-image payload projection requires a declared plane or channel axis." + "Declared source-image payload projection requires a declared " + "plane or channel axis." ) - if self.read_value("plane_axis") not in ( + 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." + "Source-image identity projection requires a runtime-slice, " + "source-binding, or channel axis." ) axis_projection = RuntimePlaneAxisValueProjection.preserve( - axis=self.read_value("plane_axis"), - axis_size=self.read_value("source_provenance").source_plane_count, - source_aliases=self.read_value("source_provenance").source_image_names, + axis=self.plane_axis, + axis_size=self.source_provenance.source_plane_count, + source_aliases=self.source_image_names, ) axis_projection.validate_shape( image_payload_geometry( - payload, value_name="Declared source-image payload" + payload, + value_name="Declared source-image payload", ).shape, value_name="Declared source-image payload", ) if source_plane_selection == complete_source_axis: return self.attach_to(payload) + from openhcs.core.aligned_image_payload import stack_image_payloads from openhcs.core.runtime_slice_projection import RuntimeSliceProjection projected_planes = tuple( - ( - RuntimeSliceProjection.value_for_slice( - payload, axis_projection.selected_plane(plane_index) - ) - for plane_index in source_plane_selection + RuntimeSliceProjection.value_for_slice( + payload, + axis_projection.selected_plane(plane_index), ) + for plane_index in source_plane_selection ) if len(projected_planes) == 1: return projected_planes[0] return stack_image_payloads( projected_planes, metadata_mode=ImagePayloadMetadataCompositionMode.for_plane_axis( - self.read_value("plane_axis") + self.plane_axis ), ) def with_indexed_source_plane_provenance( - self, expected_plane_count: int | None + self, + expected_plane_count: int | None, ) -> "ImagePayloadMetadata": """Expand scalar source-plane metadata into per-plane provenance.""" source_provenance = SourcePlaneIndexedProvenanceExpansion( - self.read_value("source_provenance"), + self.source_provenance, expected_plane_count=expected_plane_count, ).expanded() - if source_provenance is self.read_value( - "source_provenance" - ) or source_provenance == self.read_value("source_provenance"): + if ( + source_provenance is self.source_provenance + or source_provenance == self.source_provenance + ): return self return self.with_source_provenance(source_provenance) @@ -1065,13 +910,16 @@ def for_grouped_source_plane_projection( if not source_plane_indices: if metadata.has_plane_specific_values: raise ValueError( - f"Cannot assign one image metadata record to a runtime plane that represents multiple source planes: {source_plane_indices!r}." + "Cannot assign one image metadata record to a runtime plane " + "that represents multiple source planes: " + f"{source_plane_indices!r}." ) return metadata return metadata.for_source_planes(source_plane_indices) def with_unit_interval_intensity_scale( - self, scale: int | None + self, + scale: int | None, ) -> "ImagePayloadMetadata": """Return metadata with the current unit-interval pixel proof updated.""" return self.replace_fields( @@ -1081,399 +929,205 @@ def with_unit_interval_intensity_scale( def without_unit_interval_intensity_scale(self) -> "ImagePayloadMetadata": """Return metadata after an arithmetic transform changed pixel values.""" return self.replace_fields( - unit_interval_intensity=ImageUnitIntervalIntensityMetadata() + unit_interval_intensity=ImageUnitIntervalIntensityMetadata(), ) def without_spatial_domain(self) -> "ImagePayloadMetadata": """Return metadata with invalidated source-spatial placement removed.""" return self.replace_fields( source_spatial_domain=SourceSpatialDomain(), - source_voxel_spacing=self.read_value("source_voxel_spacing"), + source_voxel_spacing=self.source_voxel_spacing, physical_border_edges_yx=None, mask_defines_border=None, ) - def object_label_source_spatial_domain(self) -> SourceSpatialDomain: - """Return this metadata's object-label source-image coordinate domain.""" - return self.read_value("source_spatial_domain").with_value_name( - OBJECT_LABEL_SOURCE_SPATIAL_VALUE_NAME - ) - - def with_source_spatial_context_from( - self, source: "ImagePayloadMetadata" - ) -> "ImagePayloadMetadata": - """Fill missing source-image geometry without changing provenance.""" - spatial_domain = self.read_value("source_spatial_domain").with_missing_from( - source.read_value("source_spatial_domain") - ) - return self.replace_fields( - source_spatial_domain=spatial_domain, - source_voxel_spacing=self.read_value( - "source_voxel_spacing" - ).with_missing_from(source.read_value("source_voxel_spacing")), - physical_border_edges_yx=( - self.read_value("physical_border_edges_yx") - if self.read_value("physical_border_edges_yx") is not None - else source.read_value("physical_border_edges_yx") - ), - mask_defines_border=( - self.read_value("mask_defines_border") - if self.read_value("mask_defines_border") is not None - else source.read_value("mask_defines_border") - ), - ) - - def with_missing_intensity_from( - self, source: "ImagePayloadMetadata" - ) -> "ImagePayloadMetadata": - """Fill missing pixel-type and intensity metadata from a source payload.""" - return self.replace_fields( - intensity_scale=( - self.read_value("intensity_scale") - if self.read_value("intensity_scale") is not None - else source.read_value("intensity_scale") - ), - source_dtype=( - self.read_value("source_dtype") - if self.read_value("source_dtype") is not None - else source.read_value("source_dtype") - ), - unit_interval_intensity=( - self.read_value("unit_interval_intensity") - if self.read_value("unit_interval_intensity") is not None - else source.read_value("unit_interval_intensity") - ), - source_plane_intensity_scales=self.read_value( - "source_plane_intensity_scales" - ) - or source.read_value("source_plane_intensity_scales"), - source_plane_dtypes=self.read_value("source_plane_dtypes") - or source.read_value("source_plane_dtypes"), - ) - - def with_source_provenance( - self, source_provenance: SourceImageProvenance - ) -> "ImagePayloadMetadata": - """Return metadata with source-image provenance replaced atomically.""" - return self.replace_fields(source_provenance=source_provenance) - - def with_source_component_metadata( - self, source_component_metadata: SourceComponentMetadata | None - ) -> "ImagePayloadMetadata": - """Return metadata with only scalar source component metadata changed.""" - return self.with_source_provenance( - self.read_value("source_provenance").with_source_component_metadata( - source_component_metadata - ) - ) - - def physical_border_edges_for_shape( - self, image_shape_yx: Sequence[int] - ) -> tuple[bool, bool, bool, bool]: - """Return which local image edges are true source-image edges. - - Edge tuple order is ``(top, bottom, left, right)``. Missing spatial - metadata means the current image is treated as the full physical image, - preserving historical behavior for plain arrays. - """ - if self.read_value("physical_border_edges_yx") is not None: - return tuple( - (bool(edge) for edge in self.read_value("physical_border_edges_yx")) - ) - return self.read_value("source_spatial_domain").physical_border_edges_for_shape( - image_shape_yx - ) - - def with_spatial_crop( - self, - *, - input_shape_yx: Sequence[int], - output_shape_yx: Sequence[int], - offset_yx: tuple[int, int], - physical_border_edges_yx: PhysicalBorderEdgesYX = None, - ) -> "ImagePayloadMetadata": - """Return metadata for a crop of this image payload.""" - output_shape = _source_spatial_shape_pair(output_shape_yx, "output_shape_yx") - spatial_domain = self.read_value("source_spatial_domain").with_spatial_crop( - input_shape_yx=input_shape_yx, - output_shape_yx=output_shape_yx, - offset_yx=offset_yx, - ) - if physical_border_edges_yx is None: - physical_border_edges_yx = spatial_domain.physical_border_edges_for_shape( - output_shape - ) - return self.replace_fields( - source_spatial_domain=spatial_domain, - physical_border_edges_yx=tuple( - (bool(edge) for edge in physical_border_edges_yx) - ), - ) - - def with_spatial_resize( - self, output_shape_yx: Sequence[int] - ) -> "ImagePayloadMetadata": - """Return metadata in the local coordinate domain created by a resize.""" - return self.replace_fields( - source_spatial_domain=self.read_value( - "source_spatial_domain" - ).with_spatial_resize(output_shape_yx), - physical_border_edges_yx=(True, True, True, True), - mask_defines_border=None, - ) - - def with_materialized_source_domain( - self, target_domain: SourceSpatialDomain - ) -> "ImagePayloadMetadata": - """Return metadata after pixels are expanded to source-image XY.""" - return self.replace_fields( - source_spatial_domain=self.read_value( - "source_spatial_domain" - ).as_materialized_source_domain(target_domain), - physical_border_edges_yx=(True, True, True, True), - ) - - -def _restore_image_metadata_projection(metadata_type, items): - # Values retain their existing nominal source identities and stored birth caches. - # Do not reconstruct provenance via from_mapping or invoke opaque repr here. - return ImageMetadataProjection._from_captured_values(metadata_type, dict(items)) - - -def _restore_realized_image_metadata_projection(metadata): - return ImageMetadataProjection._from_realized_metadata(metadata) - - -@to_jsonable.register(ImageMetadataProjection) -def _jsonable_image_metadata_projection(value): - return value.to_metadata_mapping() - - -@dataclass(slots=True) -class ImagePayloadMetadata( - SourceImageProvenanceFields, - SourceSpatialDomainFields, - SourceVoxelSpacingFields, - ImageMetadataProjection, -): - """Generic source-image metadata that should travel with runtime pixels.""" - - intensity_scale: float | None = None - source_dtype: str | None = None - unit_interval_intensity: ImageUnitIntervalIntensityMetadata | None = None - source_plane_intensity_scales: tuple[float | None, ...] = () - source_plane_dtypes: tuple[str | None, ...] = () - physical_border_edges_yx: PhysicalBorderEdgesYX = None - mask_defines_border: bool | None = None - source_channel_axis: int | None = field( - default=None, metadata={ViewerWireField.IMAGE_METADATA: True} - ) - plane_axis: RuntimePlaneAxis | None = field( - default=None, metadata={ViewerWireField.IMAGE_METADATA: True} - ) - - @property - def metadata_type(self): - return type(self) - - def read_field(self, member): - return getattr(self, member.name) - - def read_value(self, name): - return getattr(self, name) - - def write_value(self, name, value): - setattr(self, name, value) - - def field_values(self): - return { - member.name: getattr(self, member.name) for member in fields(type(self)) - } - - def capture(self): - if self._uses_declared_constructor(type(self)): - return self._from_realized_metadata(self.replace_fields()) - return ImageMetadataProjection._from_captured_values( - type(self), self._normalize_values(type(self), self.field_values()) - ) - - def materialize_metadata(self): - return self - - def __reduce__(self): - return (copyreg.__newobj__, (type(self),), object.__getstate__(self)) - - @classmethod - def viewer_field_names(cls) -> frozenset[str]: - """Derive the viewer projection from its actual metadata declarations.""" - return frozenset( - member.name - for member in fields(cls) - if member.metadata.get(ViewerWireField.IMAGE_METADATA, False) - ) - - @classmethod - def from_viewer_image_metadata( - cls, values: Mapping[str, object] - ) -> "ImagePayloadMetadata": - """Decode only declaration-owned viewer fields, never hidden source records.""" - if not isinstance(values, Mapping): - raise TypeError("Viewer image metadata requires a mapping.") - extras = set(values) - cls.viewer_field_names() - if extras: - raise ValueError(f"Undeclared viewer metadata fields: {sorted(extras)!r}.") - return cls.from_mapping(values) - - @classmethod - def from_mapping(cls, values: Mapping[str, object]) -> "ImagePayloadMetadata": - """Restore all declared metadata fields through their canonical codecs.""" - decoded = dict(values) - if "source_provenance" in decoded: - decoded["source_provenance"] = SourceImageProvenance.from_mapping( - decoded["source_provenance"] - ) - return dataclass_from_mapping(cls, decoded) - - @classmethod - def for_array( - cls, - array: Any, - *, - source_path: str | None = None, - ) -> "ImagePayloadMetadata": - """Build metadata from an image array's source dtype.""" - import numpy as np - - dtype = np.asarray(array).dtype - return cls( - intensity_scale=image_intensity_scale_for_dtype(dtype), - source_dtype=str(dtype), - source_path=source_path, - ) - - @classmethod - def for_array_payload( - cls, - array: Any, - *, - source_path: str | None = None, - ) -> "ImagePayloadMetadata": - """Build source metadata from an arraybridge-detectable payload.""" - from openhcs.core.memory import MEMORY_TYPE_NUMPY, detect_memory_type - - memory_type = detect_memory_type(array) - dtype = array.dtype - if memory_type == MEMORY_TYPE_NUMPY: - intensity_scale = image_intensity_scale_for_dtype(dtype) - else: - intensity_scale = None - return cls( - intensity_scale=intensity_scale, - source_dtype=str(dtype), - source_path=source_path, - ) - - def __post_init__(self, *source_provenance_values: object) -> None: - self.absorb_explicit_source_provenance(source_provenance_values) - self.normalize_metadata_fields() - - def source_component_metadata_for_projection(self): - return self.source_component_metadata - - def source_path_for_projection(self): - return self.source_path - - def normalize_metadata_fields(self) -> None: - """Use the common sequence with this dataclass's actual nominal hooks.""" - ImageMetadataProjection.normalize_metadata_fields(self) - - @classmethod - def compose( - cls, - payloads: Sequence[RuntimeArrayData], - *, - mode: "ImagePayloadMetadataCompositionMode | None" = None, - 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 + def object_label_source_spatial_domain(self) -> SourceSpatialDomain: + """Return this metadata's object-label source-image coordinate domain.""" + return self.source_spatial_domain.with_value_name( + OBJECT_LABEL_SOURCE_SPATIAL_VALUE_NAME, ) - return _ImagePayloadMetadataComposer( - payloads=tuple(payloads), - mode=resolved_mode, - source_metadata_override=source_metadata, - metadata_type=cls, - ).compose() - def for_leading_source_plane(self, plane_index: int) -> "ImagePayloadMetadata": - """Project metadata after explicitly removing its leading plane axis.""" - return LeadingSourcePlaneMetadataProjection(self, plane_index).project() + def with_source_spatial_context_from( + self, + source: "ImagePayloadMetadata", + ) -> "ImagePayloadMetadata": + """Fill missing source-image geometry without changing provenance.""" + spatial_domain = self.source_spatial_domain.with_missing_from( + source.source_spatial_domain + ) + return self.replace_fields( + source_spatial_domain=spatial_domain, + source_voxel_spacing=self.source_voxel_spacing.with_missing_from( + source.source_voxel_spacing + ), + physical_border_edges_yx=( + self.physical_border_edges_yx + if self.physical_border_edges_yx is not None + else source.physical_border_edges_yx + ), + mask_defines_border=( + self.mask_defines_border + if self.mask_defines_border is not None + else source.mask_defines_border + ), + ) - def without_leading_plane_axis( - self, *, projection: "ImageMetadataProjection | None" = None + def with_source_context_from( + self, + source: "ImagePayloadMetadata", ) -> "ImagePayloadMetadata": - """Remove an axis through the projection's declared result ownership.""" - self.require_leading_plane_axis( - "Image metadata has no leading plane axis to remove." + """Fill missing source-image identity and spatial context from a source.""" + source_provenance = self.source_provenance.with_missing_from( + source.source_provenance ) source_channel_axis = self.source_channel_axis - if source_channel_axis == 0: + if source_channel_axis is None: + source_channel_axis = source.source_channel_axis + plane_axis = self.plane_axis + if plane_axis is None: + plane_axis = source.plane_axis + elif source.plane_axis is not None and source.plane_axis is not plane_axis: raise ValueError( - "Image metadata cannot declare the same leading axis as both " - "plane and channel." + "Cannot combine image metadata with conflicting plane axes: " + f"{plane_axis.value!r} != {source.plane_axis.value!r}." ) - if source_channel_axis is not None and source_channel_axis > 0: - source_channel_axis -= 1 - if projection is None: - projection = LeadingPlaneAxisMetadataProjection(self) - projected = projection.project_source_provenance( - self, self.source_provenance.with_runtime_planes_as_contributors() + return self.with_source_spatial_context_from(source).replace_fields( + source_provenance=source_provenance, + source_channel_axis=source_channel_axis, + plane_axis=plane_axis, ) - projected.plane_axis = None - projected.source_channel_axis = source_channel_axis - projected.source_plane_intensity_scales = () - projected.source_plane_dtypes = () - projected.unit_interval_intensity = self.project_intensity_proof(None) - projected.normalize_metadata_fields() - return projected - def for_source_plane(self, plane_index: int) -> "ImagePayloadMetadata": - """Return metadata for one source plane sliced from a stacked payload.""" - return SourcePlaneImageMetadataProjection(self, plane_index).project() + def with_missing_intensity_from( + self, + source: "ImagePayloadMetadata", + ) -> "ImagePayloadMetadata": + """Fill missing pixel-type and intensity metadata from a source payload.""" + return self.replace_fields( + intensity_scale=( + self.intensity_scale + if self.intensity_scale is not None + else source.intensity_scale + ), + source_dtype=( + self.source_dtype + if self.source_dtype is not None + else source.source_dtype + ), + unit_interval_intensity=( + self.unit_interval_intensity + if self.unit_interval_intensity is not None + else source.unit_interval_intensity + ), + source_plane_intensity_scales=( + self.source_plane_intensity_scales + or source.source_plane_intensity_scales + ), + source_plane_dtypes=( + self.source_plane_dtypes or source.source_plane_dtypes + ), + ) + def with_source_provenance( + self, + source_provenance: SourceImageProvenance, + ) -> "ImagePayloadMetadata": + """Return metadata with source-image provenance replaced atomically.""" + return self.replace_fields(source_provenance=source_provenance) -class ImageMetadataCarrier(ABC): - """Metadata ownership, independent of whether the carrier owns image data.""" + def with_source_component_metadata( + self, + source_component_metadata: SourceComponentMetadata | None, + ) -> "ImagePayloadMetadata": + """Return metadata with only scalar source component metadata changed.""" + return self.with_source_provenance( + self.source_provenance.with_source_component_metadata( + source_component_metadata + ) + ) - __slots__ = () + def physical_border_edges_for_shape( + self, + image_shape_yx: Sequence[int], + ) -> tuple[bool, bool, bool, bool]: + """Return which local image edges are true source-image edges. - @property - def metadata_projection(self) -> ImageMetadataProjection | None: - return self._metadata_projection + Edge tuple order is ``(top, bottom, left, right)``. Missing spatial + metadata means the current image is treated as the full physical image, + preserving historical behavior for plain arrays. + """ + if self.physical_border_edges_yx is not None: + return tuple(bool(edge) for edge in self.physical_border_edges_yx) + return self.source_spatial_domain.physical_border_edges_for_shape( + image_shape_yx + ) - @property - def metadata(self) -> ImagePayloadMetadata | None: - """Expose the stable mutable namespace owned by this carrier.""" - owner = self.metadata_projection - return None if owner is None else owner.materialize_metadata() + def with_spatial_crop( + self, + *, + input_shape_yx: Sequence[int], + output_shape_yx: Sequence[int], + offset_yx: tuple[int, int], + physical_border_edges_yx: PhysicalBorderEdgesYX = None, + ) -> "ImagePayloadMetadata": + """Return metadata for a crop of this image payload.""" + output_shape = _source_spatial_shape_pair(output_shape_yx, "output_shape_yx") + spatial_domain = self.source_spatial_domain.with_spatial_crop( + input_shape_yx=input_shape_yx, + output_shape_yx=output_shape_yx, + offset_yx=offset_yx, + ) + if physical_border_edges_yx is None: + physical_border_edges_yx = spatial_domain.physical_border_edges_for_shape( + output_shape + ) + return self.replace_fields( + source_spatial_domain=spatial_domain, + physical_border_edges_yx=tuple( + bool(edge) for edge in physical_border_edges_yx + ), + ) + + def with_spatial_resize( + self, + output_shape_yx: Sequence[int], + ) -> "ImagePayloadMetadata": + """Return metadata in the local coordinate domain created by a resize.""" - @metadata.setter - def metadata(self, value: ImageMetadataProjection | None) -> None: - object.__setattr__(self, "_metadata_projection", value) + return self.replace_fields( + source_spatial_domain=self.source_spatial_domain.with_spatial_resize( + output_shape_yx + ), + physical_border_edges_yx=(True, True, True, True), + mask_defines_border=None, + ) + def with_materialized_source_domain( + self, + target_domain: SourceSpatialDomain, + ) -> "ImagePayloadMetadata": + """Return metadata after pixels are expanded to source-image XY.""" + return self.replace_fields( + source_spatial_domain=( + self.source_spatial_domain.as_materialized_source_domain(target_domain) + ), + physical_border_edges_yx=(True, True, True, True), + ) -class ImagePayloadMetadataCarrier(ImageMetadataCarrier): - """Image data and metadata remain separate nominal capabilities.""" - __slots__ = () +class ImagePayloadMetadataCarrier(ABC): + """Nominal contract for image payloads that carry runtime metadata.""" @abstractmethod def image_data(self) -> Any: """Return concrete pixels in the payload's declared image domain.""" + @property + @abstractmethod + def metadata(self) -> ImagePayloadMetadata: + """Return the image metadata attached to this payload.""" + -@dataclass(frozen=True) +@dataclass(frozen=True, slots=True) class ImageMetadataPayload(DataBackedRuntimeArrayPayload, ImagePayloadMetadataCarrier): """Image data plus metadata, without requiring a validity mask.""" @@ -1492,17 +1146,13 @@ def __post_init__(self) -> None: "ImageMetadataPayload.data requires array-like image data, " f"got {type(self.data).__name__}." ) - if not self.metadata_projection.has_values: + if not self.metadata.has_values: raise ValueError("ImageMetadataPayload.metadata cannot be empty.") def image_data(self) -> Any: """Return the concrete image pixels carried by this payload.""" return self.data - @classmethod - def from_projection(cls, data, projection): - return cls(data=data, metadata=projection) - def with_data( self, data: Any, @@ -1512,11 +1162,11 @@ def with_data( """Return the same metadata attached to replacement data.""" return type(self)( data=data, - metadata=self.metadata_projection if metadata is None else metadata, + metadata=self.metadata if metadata is None else metadata, ) -@dataclass(frozen=True) +@dataclass(frozen=True, slots=True) class MaskedImagePayload(DataBackedRuntimeArrayPayload, ImagePayloadMetadataCarrier): """Image data plus an authoritative per-pixel validity mask.""" @@ -1544,7 +1194,7 @@ def __post_init__(self) -> None: f"got {type(self.mask).__name__}." ) data_shape = data_geometry.shape - if not self.metadata_projection.mask_domain(self.data).accepts(mask_shape): + if not self.metadata.mask_domain(self.data).accepts(mask_shape): raise ValueError( "MaskedImagePayload.mask shape must match the image spatial " f"domain; got mask {mask_shape!r} for image {data_shape!r}." @@ -1554,35 +1204,15 @@ def image_data(self) -> Any: """Return the concrete image pixels carried by this payload.""" return self.data - @classmethod - def from_projection(cls, data, mask, projection): - return cls(data=data, mask=mask, metadata=projection) - def with_data(self, data: Any, mask: Any | None = None) -> "MaskedImagePayload": """Return the same semantic image mask attached to replacement data.""" return type(self)( data=data, mask=self.mask if mask is None else mask, - metadata=self.metadata_projection, + metadata=self.metadata, ) -# The dataclass declaration continues to own public reflection/JSON fields. -# Install its shared concrete descriptor after field/default generation, then -# recompute abstractness rather than leaving an inherited abstract property. -ImageMetadataPayload.metadata = ImagePayloadMetadataCarrier.metadata -MaskedImagePayload.metadata = ImagePayloadMetadataCarrier.metadata -update_abstractmethods(ImageMetadataPayload) -update_abstractmethods(MaskedImagePayload) - - -def image_payload_metadata_projection(payload: Any) -> ImageMetadataProjection: - """Read the metadata owner without forcing its public mutable namespace.""" - if isinstance(payload, ImagePayloadMetadataCarrier): - return payload.metadata_projection - return ImagePayloadMetadata() - - def image_payload_data(payload: Any) -> Any: """Return concrete image pixels from a runtime image payload.""" if isinstance(payload, ImagePayloadMetadataCarrier): @@ -1623,14 +1253,17 @@ def preserved_image_plane_projection( source_aliases: tuple[str, ...] = (), ) -> RuntimePlaneAxisValueProjection | None: """Resolve the declared leading image axis against one runtime invocation.""" + from openhcs.core.runtime_slice_projection import RuntimeSliceProjection - metadata = image_payload_metadata_projection(payload) - if metadata.read_value("plane_axis") is None: + metadata = image_payload_metadata(payload) + if metadata.plane_axis is None: return RuntimeSliceProjection.preserved_context_for_value(payload) - if metadata.read_value("plane_axis") is RuntimePlaneAxis.RUNTIME_SLICE: + if metadata.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE: invocation_projection = RuntimePlaneAxisValueProjection.from_projector( - projector, metadata.read_value("plane_axis"), source_aliases + projector, + metadata.plane_axis, + source_aliases, ) if ( invocation_projection is not None @@ -1640,22 +1273,27 @@ def preserved_image_plane_projection( payload_projection = RuntimeSliceProjection.preserved_context_for_value(payload) if payload_projection is None: raise ValueError( - "Runtime-slice image metadata requires a payload with the same nominal leading axis." + "Runtime-slice image metadata requires a payload with the same " + "nominal leading axis." ) return payload_projection + shape = image_payload_geometry( - payload, value_name="Source-binding image payload" + payload, + value_name="Source-binding image payload", ).shape if len(shape) < 3: raise ValueError( - f"Source-binding image metadata requires its declared plane axis as the leading dimension, got shape {shape!r}." + "Source-binding image metadata requires its declared plane axis as " + f"the leading dimension, got shape {shape!r}." ) axis_size = shape[0] return RuntimePlaneAxisValueProjection( axis=RuntimePlaneAxis.SOURCE_BINDING, source_aliases=source_aliases, - plane_index=metadata.read_value("source_provenance").source_alias_plane_index( - source_aliases, axis_size + plane_index=metadata.source_provenance.source_alias_plane_index( + source_aliases, + axis_size, ), axis_size=axis_size, ) @@ -1668,20 +1306,22 @@ def preserve_declared_image_payload_axis( source_payload: Any | None = None, ) -> RuntimePlaneAxisValueProjection | None: """Preserve the exact image axis declared by output or source ownership.""" + from openhcs.core.runtime_slice_projection import RuntimeSliceProjection - output_metadata = image_payload_metadata_projection(output_payload) + output_metadata = image_payload_metadata(output_payload) output_projection = RuntimeSliceProjection.preserved_context_for_value( output_payload ) - output_axis = output_metadata.read_value("plane_axis") or ( + output_axis = output_metadata.plane_axis or ( None if output_projection is None else output_projection.axis ) - source_metadata = image_payload_metadata_projection(source_payload) + + source_metadata = image_payload_metadata(source_payload) source_projection = RuntimeSliceProjection.preserved_context_for_value( source_payload ) - source_axis = source_metadata.read_value("plane_axis") or ( + source_axis = source_metadata.plane_axis or ( None if source_projection is None else source_projection.axis ) if output_axis is not None: @@ -1690,30 +1330,38 @@ def preserve_declared_image_payload_axis( owner = source_payload else: return None - return preserved_image_plane_projection(owner, projector) + return preserved_image_plane_projection( + owner, + projector, + ) def project_image_mask_to_data_domain( - mask: Any, data: Any, *, metadata: ImagePayloadMetadata | None = None + mask: Any, + data: Any, + *, + metadata: ImagePayloadMetadata | None = None, ) -> Any | None: """Validate a mask against explicit image-domain metadata.""" if mask is None: return None mask_array = np.asarray(mask, dtype=bool) mask_shape = tuple(mask_array.shape) - resolved_metadata = ( - image_payload_metadata_projection(data) if metadata is None else metadata - ) + resolved_metadata = image_payload_metadata(data) if metadata is None else metadata mask_domain = resolved_metadata.mask_domain(data) if mask_domain.accepts(mask_shape): return mask_array raise ValueError( - f"Mask shape {mask_shape!r} does not match declared image mask domain {tuple(sorted(mask_domain.valid_shapes()))!r}." + f"Mask shape {mask_shape!r} does not match declared image mask domain " + f"{tuple(sorted(mask_domain.valid_shapes()))!r}." ) def image_mask_for_data_domain( - *, source_payload: Any, data: Any, explicit_mask: Any | None = None + *, + source_payload: Any, + data: Any, + explicit_mask: Any | None = None, ) -> Any | None: """Return the effective image mask projected into a concrete data domain.""" source_mask = ( @@ -1721,9 +1369,11 @@ def image_mask_for_data_domain( if explicit_mask is None else image_payload_data(explicit_mask) ) - metadata = image_payload_metadata_projection(source_payload) + metadata = image_payload_metadata(source_payload) projected_mask = project_image_mask_to_data_domain( - source_mask, data, metadata=metadata + source_mask, + data, + metadata=metadata, ) if projected_mask is None: return None @@ -1741,100 +1391,88 @@ def with_image_payload_data( resolved_mask = project_image_mask_to_data_domain( image_payload_mask(payload) if mask is None else mask, data, - metadata=( - image_payload_metadata_projection(payload) if metadata is None else metadata - ), + metadata=image_payload_metadata(payload) if metadata is None else metadata, ) resolved_metadata = ( - image_payload_metadata_projection(payload) if metadata is None else metadata + image_payload_metadata(payload) if metadata is None else metadata ) return resolved_metadata.payload_with(data, resolved_mask) -@dataclass(frozen=True, slots=True, init=False) +@dataclass(frozen=True, slots=True) +class ImageMetadataProjection(ABC): + """Construct one metadata result from nominal source and axis transformations.""" + + metadata: ImagePayloadMetadata + + def project(self) -> ImagePayloadMetadata: + return self.project_axis_metadata(self.project_source_metadata()) + + @abstractmethod + def project_source_metadata(self) -> ImagePayloadMetadata: + """Select source metadata with this projection's declared ownership.""" + + def project_axis_metadata( + self, metadata: ImagePayloadMetadata + ) -> ImagePayloadMetadata: + """Retain declared axes unless a nominal axis capability transforms them.""" + return metadata + + @abstractmethod + def project_source_provenance( + self, metadata: ImagePayloadMetadata, provenance: SourceImageProvenance + ) -> ImagePayloadMetadata: + """Apply provenance using the result ownership established by source selection.""" + + +@dataclass(frozen=True, slots=True) class SourcePlaneImageMetadataProjection(ImageMetadataProjection): - """Select one scalar source without forcing a public metadata namespace.""" + """Select scalar source-image facts without removing an array axis.""" - metadata: ImageMetadataProjection plane_index: int - def __init__(self, metadata, plane_index): - ImageMetadataProjection.__init__(self, metadata) - object.__setattr__(self, "metadata", metadata) - object.__setattr__(self, "plane_index", plane_index) - - def source_changes(self): - source = self.metadata - return dict( - intensity_scale=source.intensity_scale_for_source_plane(self.plane_index), + def project_source_metadata(self) -> ImagePayloadMetadata: + provenance = self.metadata.source_provenance.for_source_plane(self.plane_index) + return self.metadata.replace_fields( + intensity_scale=self.metadata.intensity_scale_for_source_plane( + self.plane_index + ), source_dtype=_tuple_value( - source.read_value("source_plane_dtypes"), self.plane_index + self.metadata.source_plane_dtypes, self.plane_index ) - or source.read_value("source_dtype"), - source_provenance=source.read_value("source_provenance").for_source_plane( + or self.metadata.source_dtype, + source_provenance=provenance, + unit_interval_intensity=self.metadata.project_intensity_proof( self.plane_index ), - unit_interval_intensity=source.project_intensity_proof(self.plane_index), source_plane_intensity_scales=(), source_plane_dtypes=(), ) - def project_source_values(self, values): - values.update(self.source_changes()) - return self._normalize_values(self.metadata_type, values) - - def project_source_metadata(self): - return self.metadata.materialize_metadata().replace_fields( - **self.source_changes() - ) - - def project_source_provenance(self, metadata, provenance): + def project_source_provenance( + self, metadata: ImagePayloadMetadata, provenance: SourceImageProvenance + ) -> ImagePayloadMetadata: + """Normalize the independently owned result created by source selection.""" metadata.source_provenance = provenance metadata.normalize_metadata_fields() return metadata - def __reduce__(self): - return (type(self), (self.metadata, self.plane_index)) - class LeadingPlaneAxisMetadataProjection(ImageMetadataProjection): - """Remove the leading axis after its source capability establishes ownership.""" - - def project_source_values(self, values): - # Axis guards and contributor effects precede source normalization for the - # standalone axis projection, unlike scalar-source selection. - return values - - def project_axis_values(self, values): - if values["plane_axis"] is None: - raise ValueError("Image metadata has no leading plane axis to remove.") - channel_axis = values["source_channel_axis"] - if channel_axis == 0: - raise ValueError( - "Image metadata cannot declare the same leading axis as both plane and channel." - ) - if channel_axis is not None and channel_axis > 0: - channel_axis -= 1 - proof = values["unit_interval_intensity"] - values["source_provenance"] = values[ - "source_provenance" - ].with_runtime_planes_as_contributors() - values = self._normalize_values(self.metadata_type, values) - values.update( - plane_axis=None, - source_channel_axis=channel_axis, - source_plane_intensity_scales=(), - source_plane_dtypes=(), - unit_interval_intensity=( - None if proof is None else proof.without_source_planes() - ), - ) - return self._normalize_values(self.metadata_type, values) + """Remove a declared leading axis while preserving its source contributors.""" + + def project_source_metadata(self) -> ImagePayloadMetadata: + return self.metadata - def project_axis_metadata(self, metadata): + def project_axis_metadata( + self, metadata: ImagePayloadMetadata + ) -> ImagePayloadMetadata: return metadata.without_leading_plane_axis(projection=self) - def project_source_provenance(self, metadata, provenance): + def project_source_provenance( + self, metadata: ImagePayloadMetadata, provenance: SourceImageProvenance + ) -> ImagePayloadMetadata: + """Create the standalone result after its axis guards and source derivation.""" return metadata.replace_fields(source_provenance=provenance) @@ -1842,15 +1480,9 @@ class LeadingSourcePlaneMetadataProjection( SourcePlaneImageMetadataProjection, LeadingPlaneAxisMetadataProjection, ): - """Compose independent scalar-source and leading-axis capabilities.""" - - def project_source_values(self, values): - self.metadata.require_leading_plane_axis( - "Leading source-plane projection requires a declared plane axis." - ) - return super().project_source_values(values) + """Remove the leading axis of independently owned selected-source metadata.""" - def project_source_metadata(self): + def project_source_metadata(self) -> ImagePayloadMetadata: self.metadata.require_leading_plane_axis( "Leading source-plane projection requires a declared plane axis." ) @@ -1865,36 +1497,37 @@ class ImagePayloadSliceProjector: metadata: ImagePayloadMetadata def payloads_for_slices( - self, slices: Sequence[RuntimeArrayData] + self, + slices: Sequence[RuntimeArrayData], ) -> list[RuntimeArrayData]: """Project each child once, keeping strict batch-mask cardinality.""" - if self.metadata.read_value("plane_axis") is None: + if self.metadata.plane_axis is None: if len(slices) != 1: raise ValueError( - "Image payload produced multiple slices without a declared plane axis." + "Image payload produced multiple slices without a declared " + "plane axis." ) return [self.metadata.payload_with(slices[0], self.mask)] metadata = self.metadata.with_indexed_source_plane_provenance(len(slices)) masks = self._masks_for_slices(slices) if self.mask is not None else None payloads: list[RuntimeArrayData] = [] for index, slice_data in enumerate(slices): - slice_metadata = LeadingSourcePlaneMetadataProjection( - metadata, index - ).capture() + slice_metadata = metadata.for_leading_source_plane(index) mask = None if masks is None else masks[index] - if mask is not None and ( - not slice_metadata.mask_domain(slice_data).accepts( - tuple(np.shape(mask)) - ) + if mask is not None and not slice_metadata.mask_domain(slice_data).accepts( + tuple(np.shape(mask)) ): raise ValueError( - f"Image payload mask shape must match the selected slice domain; got {tuple(np.shape(mask))!r} for {tuple(np.shape(slice_data))!r}." + "Image payload mask shape must match the selected slice " + f"domain; got {tuple(np.shape(mask))!r} for " + f"{tuple(np.shape(slice_data))!r}." ) payloads.append(slice_metadata.payload_with(slice_data, mask)) return payloads def _masks_for_slices( - self, slices: Sequence[RuntimeArrayData] + self, + slices: Sequence[RuntimeArrayData], ) -> tuple[RuntimeArrayData, ...]: """Select batch masks only after checking exact leading cardinality.""" if self.mask is None: @@ -1902,25 +1535,30 @@ def _masks_for_slices( mask_array = np.asarray(self.mask, dtype=bool) if mask_array.ndim == 0 or mask_array.shape[0] != len(slices): raise ValueError( - f"Image payload mask cardinality must exactly match the declared plane axis: {mask_array.shape!r} for {len(slices)} slice(s)." + "Image payload mask cardinality must exactly match the declared " + f"plane axis: {mask_array.shape!r} for {len(slices)} slice(s)." ) - return tuple((mask_array[index] for index in range(len(slices)))) + return tuple(mask_array[index] for index in range(len(slices))) def payload_for_slice( - self, data_slice: RuntimeArrayData, index: int + self, + data_slice: RuntimeArrayData, + index: int, ) -> RuntimeArrayData: """Project one metadata snapshot for both child pixels and mask.""" - metadata = LeadingSourcePlaneMetadataProjection(self.metadata, index).capture() + metadata = self.metadata.for_leading_source_plane(index) mask = self._mask_for_projected_slice(data_slice, index, metadata) return metadata.payload_with(data_slice, mask) def mask_for_slice( - self, data_slice: RuntimeArrayData, index: int + self, + data_slice: RuntimeArrayData, + index: int, ) -> RuntimeArrayData | None: """Project a standalone mask through the same scalar slice policy.""" if self.mask is None: return None - metadata = LeadingSourcePlaneMetadataProjection(self.metadata, index).capture() + metadata = self.metadata.for_leading_source_plane(index) return self._mask_for_projected_slice(data_slice, index, metadata) def _mask_for_projected_slice( @@ -1932,18 +1570,16 @@ def _mask_for_projected_slice( if self.mask is None: return None mask_array = np.asarray(self.mask) - if self.metadata.read_value( - "plane_axis" - ) is RuntimePlaneAxis.SOURCE_BINDING and slice_metadata.mask_domain( - data_slice - ).accepts( - mask_array.shape + if ( + self.metadata.plane_axis is RuntimePlaneAxis.SOURCE_BINDING + and slice_metadata.mask_domain(data_slice).accepts(mask_array.shape) ): candidate = mask_array else: if mask_array.ndim == 0 or plane_index >= mask_array.shape[0]: raise ValueError( - f"Image payload mask does not carry the requested declared slice index {plane_index}; got shape {mask_array.shape!r}." + "Image payload mask does not carry the requested declared " + f"slice index {plane_index}; got shape {mask_array.shape!r}." ) candidate = mask_array[plane_index] if slice_metadata.mask_domain(data_slice).accepts( @@ -1951,7 +1587,9 @@ def _mask_for_projected_slice( ): return candidate raise ValueError( - f"Image payload mask cannot be projected into slice domain; got mask {mask_array.shape!r} for slice {image_payload_geometry(data_slice).shape!r}." + "Image payload mask cannot be projected into slice domain; " + f"got mask {mask_array.shape!r} for slice " + f"{image_payload_geometry(data_slice).shape!r}." ) @@ -1963,20 +1601,21 @@ def image_payload_slice_context( plane_axis: RuntimePlaneAxis | None = None, ) -> Any: """Attach one source plane of a payload's image context to slice data.""" - metadata = image_payload_metadata_projection(payload) + metadata = image_payload_metadata(payload) if plane_axis is not None: - plane_axis = RuntimePlaneAxis(plane_axis) - if ( - metadata.read_value("plane_axis") is not None - and metadata.read_value("plane_axis") is not plane_axis - ): + plane_axis = RuntimePlaneAxis( + plane_axis, + ) + if metadata.plane_axis is not None and metadata.plane_axis is not plane_axis: raise ValueError( - f"Image slice projection axis conflicts with payload metadata: {plane_axis.value!r} != {metadata.read_value('plane_axis').value!r}." + "Image slice projection axis conflicts with payload metadata: " + f"{plane_axis.value!r} != {metadata.plane_axis.value!r}." ) - if metadata.read_value("plane_axis") is not plane_axis: + if metadata.plane_axis is not plane_axis: metadata = metadata.replace_fields(plane_axis=plane_axis) return ImagePayloadSliceProjector( - mask=image_payload_mask(payload), metadata=metadata + mask=image_payload_mask(payload), + metadata=metadata, ).payload_for_slice(data, plane_index) @@ -2062,12 +1701,7 @@ def __post_init__(self) -> None: @property def source_metadata(self) -> tuple[ImagePayloadMetadata, ...]: if self.source_metadata_override is None: - return tuple( - ( - image_payload_metadata_projection(payload) - for payload in self.payloads - ) - ) + return tuple(image_payload_metadata(payload) for payload in self.payloads) return tuple(self.source_metadata_override) @staticmethod @@ -2075,71 +1709,55 @@ def source_plane_metadata_for_payload( metadata: ImagePayloadMetadata, ) -> ImagePayloadMetadata: """Return metadata for one payload on the newly composed leading axis.""" - if metadata.read_value("plane_axis") is not None: + if metadata.plane_axis is not None: return metadata - if metadata.read_value("source_provenance").source_plane_count == 1: - return SourcePlaneImageMetadataProjection(metadata, 0).capture() + if metadata.source_provenance.source_plane_count == 1: + return metadata.for_source_plane(0) return metadata def compose(self) -> ImagePayloadMetadata: metadata_by_payload = self.source_metadata - if not any((metadata.has_values for metadata in metadata_by_payload)): + if not any(metadata.has_values for metadata in metadata_by_payload): return self.metadata_type(plane_axis=self.mode.plane_axis) source_metadata_by_payload = tuple( - ( - self.source_plane_metadata_for_payload(metadata) - for metadata in metadata_by_payload - ) + self.source_plane_metadata_for_payload(metadata) + for metadata in metadata_by_payload ) source_plane_metadata_records = source_metadata_by_payload composed_source_provenance_planes = self.composed_source_provenance_planes( source_plane_metadata_records ) common_source_voxel_spacing = self.common_metadata_value( - ( - metadata.read_value("source_voxel_spacing") - for metadata in metadata_by_payload - if metadata.read_value("source_voxel_spacing").has_values - ) + metadata.source_voxel_spacing + for metadata in metadata_by_payload + if metadata.source_voxel_spacing.has_values ) if common_source_voxel_spacing is None: common_source_voxel_spacing = SourceVoxelSpacing() source_component_metadata_by_payload = tuple( ( - ( - metadata - if metadata.read_value( - "source_provenance" - ).source_component_metadata - is not None - else source_metadata - ) - for metadata, source_metadata in zip( - metadata_by_payload, source_metadata_by_payload, strict=True - ) + metadata + if metadata.source_component_metadata is not None + else source_metadata + ) + for metadata, source_metadata in zip( + metadata_by_payload, + source_metadata_by_payload, + strict=True, ) ) return self.metadata_type( source_path=self.common_metadata_value( - ( - metadata.read_value("source_provenance").source_path - for metadata in source_metadata_by_payload - ) + metadata.source_path for metadata in source_metadata_by_payload ), source_component_metadata=self.common_source_component_metadata( source_component_metadata_by_payload ), source_plane_intensity_scales=tuple( - ( - metadata.read_value("intensity_scale") - for metadata in source_plane_metadata_records - ) + metadata.intensity_scale for metadata in source_plane_metadata_records ), source_plane_dtypes=tuple( - ( - metadata.read_value("source_dtype") - for metadata in source_plane_metadata_records - ) + metadata.source_dtype for metadata in source_plane_metadata_records ), source_image_provenance_planes=composed_source_provenance_planes, unit_interval_intensity=self.composed_unit_interval_intensity( @@ -2147,55 +1765,43 @@ def compose(self) -> ImagePayloadMetadata: ), source_spatial_domain=SourceSpatialDomain( origin_yx=self.common_metadata_value( - ( - metadata.read_value("source_spatial_domain").origin_yx - for metadata in metadata_by_payload - ) + metadata.spatial_origin_yx for metadata in metadata_by_payload ), source_shape_yx=self.common_metadata_value( - ( - metadata.read_value("source_spatial_domain").source_shape_yx - for metadata in metadata_by_payload - ) + metadata.source_spatial_shape_yx for metadata in metadata_by_payload ), ), source_voxel_spacing=common_source_voxel_spacing, physical_border_edges_yx=self.common_metadata_value( - ( - metadata.read_value("physical_border_edges_yx") - for metadata in metadata_by_payload - ) + metadata.physical_border_edges_yx for metadata in metadata_by_payload ), mask_defines_border=self.common_metadata_value( - ( - metadata.read_value("mask_defines_border") - for metadata in metadata_by_payload - ) + metadata.mask_defines_border for metadata in metadata_by_payload + ), + source_image_names=( + composed_source_provenance_planes.runtime_source_image_names ), - source_image_names=composed_source_provenance_planes.runtime_source_image_names, source_channel_axis=self.composed_source_channel_axis(metadata_by_payload), plane_axis=self.mode.plane_axis, ) def composed_source_provenance_planes( - self, metadata_records: tuple[ImagePayloadMetadata, ...] + self, + metadata_records: tuple[ImagePayloadMetadata, ...], ) -> SourceImageProvenancePlanes: """Compose projectable planes without replacing compatible topology.""" + if len(metadata_records) == 1: metadata = metadata_records[0] - provenance_planes = metadata.read_value( - "source_provenance" - ).source_image_provenance_planes + provenance_planes = metadata.source_image_provenance_planes if provenance_planes.count > 1 and self.mode.preserves_plane_topology( - metadata.read_value("plane_axis") + metadata.plane_axis ): return provenance_planes return SourceImageProvenancePlanes( tuple( - ( - self.runtime_source_provenance_plane(metadata) - for metadata in metadata_records - ) + self.runtime_source_provenance_plane(metadata) + for metadata in metadata_records ) ) @@ -2204,19 +1810,15 @@ def composed_unit_interval_intensity( metadata_records: Sequence[ImagePayloadMetadata], ) -> ImageUnitIntervalIntensityMetadata | None: """Compose only unit-interval proof state authored by an input payload.""" + if not any( - ( - metadata.read_value("unit_interval_intensity") is not None - for metadata in metadata_records - ) + metadata.unit_interval_intensity is not None + for metadata in metadata_records ): return None return ImageUnitIntervalIntensityMetadata( source_plane_scales=tuple( - ( - metadata.unit_interval_intensity_scale - for metadata in metadata_records - ) + metadata.unit_interval_intensity_scale for metadata in metadata_records ) ) @@ -2225,40 +1827,37 @@ def runtime_source_provenance_plane( metadata: ImagePayloadMetadata, ) -> RuntimeSourceImageProvenancePlane: """Return one projectable plane with nested non-projectable contributors.""" - source_image_names = metadata.read_value("source_provenance").source_image_names - contributors = ( - metadata.read_value("source_provenance") - .source_image_provenance_planes.as_contributors(source_image_names) - .planes - ) + source_image_names = metadata.source_image_names + contributors = metadata.source_image_provenance_planes.as_contributors( + source_image_names + ).planes if len(source_image_names) > 1: raise ValueError( - f"Composed image payload provenance permits at most one source alias per scalar plane, got {source_image_names!r}." + "Composed image payload provenance permits at most one " + f"source alias per scalar plane, got {source_image_names!r}." ) source_image_name = source_image_names[0] if source_image_names else None return RuntimeSourceImageProvenancePlane( SourceImageIdentity( - metadata.read_value("source_provenance").source_path, - metadata.read_value("source_provenance").source_component_metadata, + metadata.source_path, + metadata.source_component_metadata, ), contributors, source_image_name, ) def common_source_component_metadata( - self, values: Iterable[ImagePayloadMetadata] + self, + values: Iterable[ImagePayloadMetadata], ) -> SourceComponentMetadata | None: """Return source metadata shared by the composed payload.""" metadata_values = tuple(values) metadata_by_plane = tuple( - ( - SourceMetadataFields.composition_snapshot( - metadata.read_value("source_provenance").source_component_metadata - ) - for metadata in metadata_values - if metadata.read_value("source_provenance").source_component_metadata - is not None + SourceMetadataFields.composition_snapshot( + metadata.source_component_metadata ) + for metadata in metadata_values + if metadata.source_component_metadata is not None ) if self.mode is ImagePayloadMetadataCompositionMode.BUNDLE: field_names = set().union( @@ -2267,26 +1866,20 @@ def common_source_component_metadata( common_metadata: dict[str, SourceMetadataValue] = {} for field_name in field_names: values_for_field = tuple( - ( - metadata[field_name] - for metadata in metadata_by_plane - if field_name in metadata - ) + metadata[field_name] + for metadata in metadata_by_plane + if field_name in metadata ) if values_for_field and all( - (value == values_for_field[0] for value in values_for_field) + value == values_for_field[0] for value in values_for_field ): common_metadata[field_name] = values_for_field[0] else: common_metadata = dict( common_source_component_metadata( tuple( - ( - metadata.read_value( - "source_provenance" - ).source_component_metadata - for metadata in metadata_values - ) + metadata.source_component_metadata + for metadata in metadata_values ) ) or {} @@ -2297,10 +1890,8 @@ def common_source_component_metadata( metadata_by_plane[0] if metadata_by_plane and all( - ( - isinstance(metadata, OwnedSourceMetadataFields) - for metadata in metadata_by_plane - ) + isinstance(metadata, OwnedSourceMetadataFields) + for metadata in metadata_by_plane ) else {} ) @@ -2316,33 +1907,35 @@ def common_metadata_value( values: Iterable[MetadataValueT | None], ) -> MetadataValueT | None: values_tuple = tuple(values) - present = tuple((value for value in values_tuple if value is not None)) + present = tuple(value for value in values_tuple if value is not None) if not present: return None first = present[0] - if all((value == first for value in present)): + if all(value == first for value in present): return first return None def composed_source_channel_axis( - self, metadata_by_payload: tuple[ImagePayloadMetadata, ...] + self, + metadata_by_payload: tuple[ImagePayloadMetadata, ...], ) -> int | None: """Return the channel axis after this composition adds a leading axis.""" normalized = tuple( - ( - metadata.normalized_source_channel_axis(payload) - for payload, metadata in zip( - self.payloads, metadata_by_payload, strict=True - ) + metadata.normalized_source_channel_axis(payload) + for payload, metadata in zip( + self.payloads, + metadata_by_payload, + strict=True, ) ) - present = tuple((value for value in normalized if value is not None)) + present = tuple(value for value in normalized if value is not None) if not present: return None first = present[0] - if any((value != first for value in present[1:])): + if any(value != first for value in present[1:]): raise ValueError( - f"Cannot compose image payloads with conflicting source channel axes: {present!r}." + "Cannot compose image payloads with conflicting source channel axes: " + f"{present!r}." ) return first + 1 @@ -2358,14 +1951,16 @@ def image_intensity_scale_for_dtype(dtype: Any) -> float | None: def image_payload_intensity_scale( - payload: Any, *, channel_index: int = 0 + payload: Any, + *, + channel_index: int = 0, ) -> float | None: """Return the best semantic intensity scale for an image payload.""" import numpy as np - metadata_scale = image_payload_metadata_projection( - payload - ).intensity_scale_for_source_plane(channel_index) + metadata_scale = image_payload_metadata(payload).intensity_scale_for_source_plane( + channel_index + ) if metadata_scale is not None and metadata_scale > 0: return float(metadata_scale) data = image_payload_data(payload) @@ -2376,7 +1971,10 @@ def image_payload_intensity_scale( def normalize_image_payload_intensity( - payload: Any, *, dtype: Any = None, channel_index: int = 0 + payload: Any, + *, + dtype: Any = None, + channel_index: int = 0, ) -> Any: """Normalize image pixels by payload metadata while preserving context.""" import numpy as np @@ -2387,13 +1985,14 @@ def normalize_image_payload_intensity( normalized = array.astype(target_dtype) elif np.issubdtype(array.dtype, np.integer): intensity_scale = image_payload_intensity_scale( - payload, channel_index=channel_index + payload, + channel_index=channel_index, ) if intensity_scale is None or intensity_scale <= 1: normalized = array.astype(target_dtype) else: normalized = array.astype(target_dtype) / float(intensity_scale) - metadata = image_payload_metadata_projection( + metadata = image_payload_metadata( payload ).with_unit_interval_intensity_scale(int(intensity_scale)) return with_image_payload_data(payload, normalized, metadata=metadata) diff --git a/openhcs/core/runtime_object_labels.py b/openhcs/core/runtime_object_labels.py index a3957fec5d..396e677ae6 100644 --- a/openhcs/core/runtime_object_labels.py +++ b/openhcs/core/runtime_object_labels.py @@ -351,14 +351,6 @@ def unedited_labels(self) -> ObjectLabelData | None: def small_removed_labels(self) -> ObjectLabelData | None: return self.variant_data.small_removed_labels - @property - def metadata_projection(self) -> runtime_image_values.ImageMetadataProjection: - """Expose one computed metadata namespace without replaying its constructor.""" - - return runtime_image_values.ImageMetadataProjection._from_realized_metadata( - self.metadata - ) - @property def metadata(self) -> runtime_image_values.ImagePayloadMetadata: """Return image-domain metadata carried by this object-label value.""" diff --git a/openhcs/core/runtime_slice_projection.py b/openhcs/core/runtime_slice_projection.py index 28218e407a..283b9a9e84 100644 --- a/openhcs/core/runtime_slice_projection.py +++ b/openhcs/core/runtime_slice_projection.py @@ -26,15 +26,12 @@ MeasurementTableUnion, ) from openhcs.core.runtime_image_values import ( - ImageMetadataCarrier, - ImageMetadataProjection, ImageMetadataPayload, ImagePayloadMetadata, MaskedImagePayload, image_payload_data, image_payload_geometry, image_payload_metadata, - image_payload_metadata_projection, image_payload_slice_context, ) from openhcs.core.runtime_measurements import ( @@ -325,7 +322,7 @@ def validate_stack_value( @dataclass(frozen=True, slots=True) -class RuntimeProjectedPayloadItem(ImageMetadataCarrier): +class RuntimeProjectedPayloadItem: """One runtime payload projected into an execution-addressable value.""" value: RuntimeProjectionData @@ -337,12 +334,12 @@ def data(self) -> RuntimeProjectionData: return image_payload_data(self.value) @property - def metadata_projection(self) -> ImageMetadataProjection: - return image_payload_metadata_projection(self.value) + def metadata(self) -> ImagePayloadMetadata: + return image_payload_metadata(self.value) @property def source_component_metadata(self) -> SourceComponentMetadata | None: - return self.metadata_projection.source_component_metadata_for_projection() + return self.metadata.source_component_metadata def require_source_component_metadata(self) -> SourceComponentMetadata: metadata = self.source_component_metadata @@ -362,9 +359,8 @@ def validate_source_plane_component_metadata( ) -> None: """Require one source component-metadata record per projected slice.""" plane_metadata = ( - image_payload_metadata_projection(value) + image_payload_metadata(value) .with_indexed_source_plane_provenance(slice_count) - .read_value("source_provenance") .source_image_provenance_planes.component_metadata ) if not plane_metadata or any(item is None for item in plane_metadata): @@ -474,8 +470,8 @@ def slice_count_for_value( self, value: RuntimeProjectionData, ) -> int | None: - metadata = image_payload_metadata_projection(value) - if metadata.read_value("plane_axis") is not RuntimePlaneAxis.RUNTIME_SLICE: + metadata = image_payload_metadata(value) + if metadata.plane_axis is not RuntimePlaneAxis.RUNTIME_SLICE: return None data_geometry = image_payload_geometry( value, @@ -493,7 +489,7 @@ def value_for_slice( value: RuntimeProjectionData, context: RuntimePlaneAxisValueProjection, ) -> RuntimeProjectionData: - plane_axis = image_payload_metadata_projection(value).read_value("plane_axis") + plane_axis = image_payload_metadata(value).plane_axis if plane_axis is not context.axis: return value data = image_payload_data(value) diff --git a/openhcs/core/source_image_semantics.py b/openhcs/core/source_image_semantics.py index 639a3e9817..b12f30692f 100644 --- a/openhcs/core/source_image_semantics.py +++ b/openhcs/core/source_image_semantics.py @@ -2,15 +2,15 @@ from __future__ import annotations +from dataclasses import replace import numpy as np from openhcs.core.runtime_image_values import ( ImageMetadataPayload, - ImageMetadataProjection, ImagePayloadMetadata, MaskedImagePayload, image_payload_data, image_payload_mask, - image_payload_metadata_projection, + image_payload_metadata, normalize_image_payload_intensity, ) from openhcs.core.runtime_image_loading import ImagePayloadSourceMetadataContext @@ -31,7 +31,7 @@ def _rgb_to_monochrome(data: np.ndarray) -> np.ndarray: def _monochrome_source_data( data: RuntimeArrayData, channel_axis: int | None, - metadata: ImageMetadataProjection, + metadata: ImagePayloadMetadata, ) -> RuntimeArrayData: if channel_axis is None: return data @@ -51,8 +51,8 @@ def _loaded_source_payload_metadata( payload: RuntimeArrayData, binding: NamedSourceBinding, source_context: ImagePayloadSourceMetadataContext | None, -) -> ImageMetadataProjection: - existing = image_payload_metadata_projection(payload) +) -> ImagePayloadMetadata: + existing = image_payload_metadata(payload) if source_context is None: metadata = ( existing @@ -60,17 +60,17 @@ def _loaded_source_payload_metadata( else ImagePayloadMetadata.for_array_payload(payload) ) else: - metadata = source_context.metadata_projection(payload, source_binding=binding) - metadata = metadata.derive_fields( - source_provenance=metadata.read_value( - "source_provenance" - ).with_source_image_names((binding.alias,)) + metadata = source_context.metadata(payload, source_binding=binding) + metadata = metadata.replace_fields( + source_provenance=metadata.source_provenance.with_source_image_names( + (binding.alias,) + ) ) source_channel_axis = binding.source_channel_axis_for_shape( np.shape(image_payload_data(payload)), - observed_axis=metadata.read_value("source_channel_axis"), + observed_axis=metadata.source_channel_axis, ) - metadata = metadata.derive_fields(source_channel_axis=source_channel_axis) + metadata = replace(metadata, source_channel_axis=source_channel_axis) metadata.normalized_source_channel_axis(payload) return metadata @@ -83,7 +83,7 @@ def apply_source_binding_payload( """Apply one resolved source binding to a loaded runtime payload.""" metadata = _loaded_source_payload_metadata(payload, binding, source_context) data = image_payload_data(payload) - source_channel_axis = metadata.read_value("source_channel_axis") + source_channel_axis = metadata.source_channel_axis data, source_channel_axis = binding.artifact_kind.normalize_source_payload( data, source_channel_axis, @@ -91,14 +91,15 @@ def apply_source_binding_payload( if binding.load_as_monochrome and source_channel_axis is not None: data = _monochrome_source_data(data, source_channel_axis, metadata) source_channel_axis = None - metadata = metadata.without_unit_interval_intensity_scale().derive_fields( + metadata = replace( + metadata.without_unit_interval_intensity_scale(), intensity_scale=ImagePayloadMetadata.for_array(data).intensity_scale, ) if binding.load_as_mask: data = np.asarray(data, dtype=bool) mask = image_payload_mask(payload) - metadata = metadata.derive_fields(source_channel_axis=source_channel_axis) + metadata = replace(metadata, source_channel_axis=source_channel_axis) if mask is not None: return MaskedImagePayload(data, mask, metadata) if metadata.has_values: diff --git a/openhcs/core/source_plane_alignment.py b/openhcs/core/source_plane_alignment.py index 1cfcffb437..d3c197b581 100644 --- a/openhcs/core/source_plane_alignment.py +++ b/openhcs/core/source_plane_alignment.py @@ -4,14 +4,12 @@ from collections import OrderedDict from collections.abc import Sequence -from dataclasses import dataclass, field +from dataclasses import dataclass from typing import Any, ClassVar -from types import MemberDescriptorType from openhcs.core.runtime_image_values import ( - ImageMetadataCarrier, ImagePayloadMetadata, - image_payload_metadata_projection, + image_payload_metadata, ) from openhcs.core.source_image_provenance import SourceImageProvenanceIdentity from openhcs.core.source_matching import ( @@ -66,12 +64,11 @@ def _store_source_payload_plane_identity_sequence( @dataclass(frozen=True, slots=True) -class SourcePayloadPlaneIdentity(ImageMetadataCarrier): +class SourcePayloadPlaneIdentity: """Source image-set identities represented by one runtime payload plane.""" - metadata: ImagePayloadMetadata = field() + metadata: ImagePayloadMetadata policy: SourceImageSetIdentityPolicy - _metadata_projection: ClassVar[MemberDescriptorType] @classmethod def from_payload( @@ -79,18 +76,10 @@ def from_payload( payload: Any, policy: SourceImageSetIdentityPolicy, ) -> "SourcePayloadPlaneIdentity": - return cls(image_payload_metadata_projection(payload), policy) + return cls(image_payload_metadata(payload), policy) def identities(self) -> frozenset[SourceImageSetIdentity]: - return self.metadata_projection.read_value( - "source_provenance" - ).image_set_identities(self.policy) - - -# Keep the original declared metadata slot and public dataclass schema. The same -# shared descriptor used by metadata-only manifests exposes its stable namespace. -SourcePayloadPlaneIdentity._metadata_projection = SourcePayloadPlaneIdentity.metadata -SourcePayloadPlaneIdentity.metadata = ImageMetadataCarrier.metadata + return self.metadata.source_provenance.image_set_identities(self.policy) @dataclass(frozen=True, slots=True) @@ -101,24 +90,23 @@ class SourcePayloadPlaneIdentitySequence: policy: SourceImageSetIdentityPolicy def identities(self) -> SourcePlaneIdentitySequence: - metadata = image_payload_metadata_projection(self.payload) + metadata = image_payload_metadata(self.payload) cache_key = SourcePayloadPlaneIdentitySequenceCacheKey( - metadata.read_value("source_provenance").equality_identity, self.policy + metadata.source_provenance.equality_identity, + self.policy, ) cached = _cached_source_payload_plane_identity_sequence(cache_key) if cached is not None: return cached return _store_source_payload_plane_identity_sequence( cache_key, - metadata.read_value("source_provenance").image_set_plane_identities( - self.policy - ), + metadata.source_provenance.image_set_plane_identities(self.policy), ) def runtime_axis_identities(self) -> SourcePlaneIdentitySequence: """Return the image-set identity axis carried by this payload.""" - metadata = image_payload_metadata_projection(self.payload) - return metadata.read_value("source_provenance").image_set_axis(self.policy) + metadata = image_payload_metadata(self.payload) + return metadata.source_provenance.image_set_axis(self.policy) @property def has_identity(self) -> bool: diff --git a/openhcs/core/source_workspace_projection.py b/openhcs/core/source_workspace_projection.py index c4c6304532..0b3216e025 100644 --- a/openhcs/core/source_workspace_projection.py +++ b/openhcs/core/source_workspace_projection.py @@ -12,12 +12,11 @@ from openhcs.core.source_path_identity import source_path_identity, source_path_join from openhcs.constants import Backend from openhcs.core.runtime_image_values import ( - ImageMetadataProjection, ImagePayloadMetadata, ImagePayloadMetadataCompositionMode, image_payload_data, image_payload_mask, - image_payload_metadata_projection, + image_payload_metadata, ) from openhcs.core.runtime_array_values import RuntimeArrayData from openhcs.core.source_bindings import ( @@ -539,34 +538,13 @@ class VirtualWorkspaceImagePayloadProjection: persisted_metadata: ImagePayloadMetadata | None = None def metadata(self, loaded: ImagePayloadMetadata) -> ImagePayloadMetadata: - """Preserve the public mutable namespace and nominal subtype lifecycle.""" - return self._metadata_from_persisted( - self.persisted_metadata, loaded - ).materialize_metadata() - - def metadata_projection( - self, loaded: ImageMetadataProjection - ) -> ImageMetadataProjection: - """Join facts without snapshotting or normalizing merely to enter a view.""" - persisted = self.persisted_metadata - if persisted is not None: - # This existing projection state reads the same authoritative live - # dataclass. The first semantic transform creates flat owned facts. - persisted = ImageMetadataProjection._from_realized_metadata(persisted) - return self._metadata_from_persisted(persisted, loaded) - - def _metadata_from_persisted( - self, - persisted: ImageMetadataProjection | None, - loaded: ImageMetadataProjection, - ) -> ImageMetadataProjection: """Preserve semantic omissions, but not an empty header's lost identity.""" - if persisted is None: + if self.persisted_metadata is None: return loaded - metadata = persisted.with_source_spatial_context_from( + metadata = self.persisted_metadata.with_source_spatial_context_from( loaded ).with_missing_intensity_from(loaded) - provenance = metadata.read_value("source_provenance") + provenance = metadata.source_provenance if not ( provenance.source_identity.addressable or provenance.source_image_provenance_planes.has_values @@ -581,13 +559,11 @@ def apply(self, payload: RuntimeArrayData) -> RuntimeArrayData: source_metadata = SourceMetadataFields.with_fields( source_metadata, {}, without=(SOURCE_BINDING_ALIAS_METADATA_FIELD,) ) - current_metadata = image_payload_metadata_projection(payload) - metadata = self.metadata_projection(current_metadata) - metadata = metadata.derive_fields( - source_spatial_domain=metadata.read_value( - "source_spatial_domain" - ).with_native_image_context( - current_metadata.read_value("source_spatial_domain"), + current_metadata = image_payload_metadata(payload) + metadata = self.metadata(current_metadata) + metadata = metadata.replace_fields( + source_spatial_domain=metadata.source_spatial_domain.with_native_image_context( + current_metadata.source_spatial_domain, image_shape_yx=current_metadata.spatial_shape_yx( image_payload_data(payload) ), @@ -597,12 +573,11 @@ def apply(self, payload: RuntimeArrayData) -> RuntimeArrayData: metadata = metadata.with_source_component_metadata(source_metadata) if self.source_alias is not None: metadata = metadata.with_source_provenance( - metadata.read_value("source_provenance").with_source_image_names( - (self.source_alias,) - ) + metadata.source_provenance.with_source_image_names((self.source_alias,)) ) return metadata.payload_with( - image_payload_data(payload), image_payload_mask(payload) + image_payload_data(payload), + image_payload_mask(payload), ) diff --git a/openhcs/core/steps/function_artifact_materialization.py b/openhcs/core/steps/function_artifact_materialization.py index b3dfc131e0..e91198c959 100644 --- a/openhcs/core/steps/function_artifact_materialization.py +++ b/openhcs/core/steps/function_artifact_materialization.py @@ -30,11 +30,9 @@ from openhcs.core.runtime_artifact_queries import MeasurementTableUnion from openhcs.core.runtime_artifact_values import RuntimeValue from openhcs.core.runtime_image_values import ( - ImageMetadataProjection, ImagePayloadMetadata, image_payload_data, image_payload_metadata, - image_payload_metadata_projection, ) from openhcs.core.runtime_measurements import ( MeasurementTable, @@ -184,9 +182,9 @@ def records_for_output( ] = {} for record in records: payload = output_plan.materialization_payload(record.value) - metadata = image_payload_metadata_projection(payload) + metadata = image_payload_metadata(payload) address = OpenHCSPlaneAddress.from_complete_source_metadata( - metadata.source_component_metadata_for_projection() + metadata.source_component_metadata ) addressed_records.append((record, address)) if address is not None: @@ -480,10 +478,8 @@ def metadata_items( def payload_source_identity( data: MaterializationValue, ) -> SourceImageIdentity | None: - metadata = image_payload_metadata_projection(data) - source_identity = metadata.read_value( - "source_provenance" - ).scalar_source_identity + metadata = image_payload_metadata(data) + source_identity = metadata.source_provenance.scalar_source_identity if source_identity.addressable: return source_identity return None @@ -649,13 +645,11 @@ def build( if record.key.artifact_type.uses_aggregate_materialization_identity( record.value.data ): - metadata = cls.record_payload_metadata_projection(record) + metadata = cls.record_payload_metadata(record) aggregate_provenance = ( None if metadata is None - else metadata.read_value( - "source_provenance" - ).with_common_scalar_identity_from_planes() + else metadata.source_provenance.with_common_scalar_identity_from_planes() ) source_identity = ( None @@ -741,24 +735,13 @@ def missing_component_is_aggregated( def record_payload_metadata( record: StoredRuntimeValue | None, ) -> ImagePayloadMetadata | None: - metadata = ( - ArtifactStreamSourceMetadataAuthority.record_payload_metadata_projection( - record - ) - ) - return None if metadata is None else metadata.materialize_metadata() - - @staticmethod - def record_payload_metadata_projection( - record: StoredRuntimeValue | None, - ) -> ImageMetadataProjection | None: if record is None: return None if isinstance(record.value.data, SourceImageProvenanceFields): return ImagePayloadMetadata( source_provenance=record.value.data.source_provenance, ) - return image_payload_metadata_projection(record.value.data) + return image_payload_metadata(record.value.data) @classmethod def record_metadata_with_runtime_scope( @@ -771,18 +754,18 @@ def record_metadata_with_runtime_scope( scope = record.key.scope if scope.has_fixed_components: component_metadata = scope.fixed_component_metadata( - metadata.source_component_metadata_for_projection() + metadata.source_component_metadata ) elif scope.component is not None: component_metadata = with_source_component_metadata( - metadata.source_component_metadata_for_projection() or {}, + metadata.source_component_metadata or {}, scope.component, scope.require_value_text(), ) else: return metadata return metadata.with_source_provenance( - metadata.read_value("source_provenance").with_source_component_metadata( + metadata.source_provenance.with_source_component_metadata( component_metadata ), ) @@ -801,7 +784,7 @@ def record_source_descriptor( exact_fixed_scope = record.key.scope.has_fixed_components if not materialization_spec.uses_source_identity_filename(): return None - metadata = cls.record_payload_metadata_projection(record) + metadata = cls.record_payload_metadata(record) if metadata is None: return None if ( @@ -810,7 +793,7 @@ def record_source_descriptor( and output_plan.materialization_source() != output_plan.source_context_source() ): - metadata = output_plan.materialization_metadata_projection(record.value) + metadata = output_plan.materialization_metadata(record.value) metadata = cls.record_metadata_with_runtime_scope( record, metadata, @@ -839,8 +822,7 @@ def record_source_descriptor( ) if identity is not None: identity = materialization_spec.filename_identity_for_output( - identity, - output_plan, + identity, output_plan, ) try: filename = Path( @@ -875,9 +857,7 @@ def record_source_descriptor( identity, ), ) - source_path = metadata.read_value( - "source_provenance" - ).scalar_source_identity.path + source_path = metadata.source_provenance.scalar_source_identity.path if source_path is None: return None return ArtifactRecordSourceDescriptor( @@ -895,16 +875,12 @@ def record_source_identity_from_metadata( identity: FunctionOutputIdentity | None, ) -> SourceImageIdentity | None: """Return scalar source identity with parser-resolved component metadata.""" - source_identity = metadata.read_value( - "source_provenance" - ).scalar_source_identity + source_identity = metadata.source_provenance.scalar_source_identity if identity is not None: - if metadata.source_component_metadata_for_projection() is None: + if metadata.source_component_metadata is None: component_metadata = {} else: - component_metadata = dict( - metadata.source_component_metadata_for_projection() - ) + component_metadata = dict(metadata.source_component_metadata) identity_metadata = identity.filename_component_metadata() for key, value in identity_metadata.items(): component = AllComponents.from_value(str(key)) diff --git a/openhcs/core/steps/function_io.py b/openhcs/core/steps/function_io.py index 70a3ae7ccf..d2e5e5e38b 100644 --- a/openhcs/core/steps/function_io.py +++ b/openhcs/core/steps/function_io.py @@ -26,7 +26,7 @@ from openhcs.core.runtime_image_values import ( image_payload_data, image_payload_mask, - image_payload_metadata_projection, + image_payload_metadata, ) from openhcs.core.source_image_provenance import SourceImageIdentity from openhcs.core.steps.function_output_identity import FunctionOutputIdentity @@ -123,13 +123,13 @@ def _preloaded_image_payload( filemanager: FileManager, ) -> RuntimeArrayData: """Preserve loader-owned metadata or derive it through the source backend.""" - metadata = image_payload_metadata_projection(image) + metadata = image_payload_metadata(image) if not metadata.has_values: metadata = ImagePayloadSourceMetadataContext( SourceImageIdentity(source_path), read_backend=read_backend, filemanager=filemanager, - ).metadata_projection(image) + ).metadata(image) return metadata.payload_with( image_payload_data(image), image_payload_mask(image), diff --git a/openhcs/core/steps/function_output_identity.py b/openhcs/core/steps/function_output_identity.py index 1719b53532..a9775369ec 100644 --- a/openhcs/core/steps/function_output_identity.py +++ b/openhcs/core/steps/function_output_identity.py @@ -10,8 +10,8 @@ from openhcs.core.source_path_identity import source_path_identity from openhcs.constants.constants import AllComponents, VariableComponents from openhcs.core.runtime_image_values import ( - ImageMetadataProjection, - image_payload_metadata_projection, + ImagePayloadMetadata, + image_payload_metadata, ) from openhcs.core.runtime_array_values import RuntimeArrayData from openhcs.core.components.parser_metaprogramming import ( @@ -320,9 +320,7 @@ def _qualified_filename( if qualifier is None: return filename if not filename.endswith(extension): - raise ValueError( - "Constructed filename does not retain its declared extension." - ) + raise ValueError("Constructed filename does not retain its declared extension.") return f"{filename[:-len(extension)]}_{qualifier}{extension}" @staticmethod @@ -446,7 +444,7 @@ class FunctionOutputIdentityAuthority: @classmethod def identity(cls, request: FunctionOutputPathRequest) -> FunctionOutputIdentity: - metadata = image_payload_metadata_projection(request.output_payload) + metadata = image_payload_metadata(request.output_payload) payload_identity = cls._identity_from_metadata_with_cache( request.parser, metadata, @@ -469,7 +467,7 @@ def identity(cls, request: FunctionOutputPathRequest) -> FunctionOutputIdentity: def identity_from_metadata( cls, parser: FilenameParser, - metadata: ImageMetadataProjection, + metadata: ImagePayloadMetadata, *, fallback_identity_path: str | None = None, variable_components: Sequence[VariableComponents] = (), @@ -489,7 +487,7 @@ def identity_from_metadata( def identity_from_metadata_with_cache( cls, parser: FilenameParser, - metadata: ImageMetadataProjection, + metadata: ImagePayloadMetadata, *, fallback_identity_path: str | None = None, variable_components: Sequence[VariableComponents] = (), @@ -520,7 +518,7 @@ def _source_stack_identity_component_values( def _identity_from_metadata_with_cache( cls, parser: FilenameParser, - metadata: ImageMetadataProjection, + metadata: ImagePayloadMetadata, *, fallback_identity_path: str | None, variable_components: Sequence[VariableComponents], @@ -533,9 +531,7 @@ def _identity_from_metadata_with_cache( ) metadata_cache_key = FunctionOutputMetadataIdentityCacheKey( parser_id=id(parser), - source_provenance_identity=metadata.read_value( - "source_provenance" - ).equality_identity, + source_provenance_identity=metadata.source_provenance.equality_identity, fallback_identity_path=fallback_identity_path, identity_components=tuple(sorted(identity_component_values)), input_aligned_output=input_aligned_output, @@ -557,7 +553,7 @@ def _identity_from_metadata_with_cache( def _identity_from_metadata_uncached( cls, parser: FilenameParser, - metadata: ImageMetadataProjection, + metadata: ImagePayloadMetadata, *, fallback_identity_path: str | None, identity_component_values: frozenset[str], @@ -565,10 +561,10 @@ def _identity_from_metadata_uncached( identity_cache: FunctionOutputIdentityCache, ) -> FunctionOutputIdentity | None: """Resolve parser-backed identity without consulting the metadata cache.""" - represented_source_identities = metadata.read_value( - "source_provenance" - ).represented_source_identities - source_plane_count = metadata.read_value("source_provenance").source_plane_count + represented_source_identities = ( + metadata.source_provenance.represented_source_identities + ) + source_plane_count = metadata.source_provenance.source_plane_count if source_plane_count > 1: if identity_component_values: return cls._source_stack_identity_from_provenance( @@ -588,12 +584,10 @@ def _identity_from_metadata_uncached( ) identity = cls._identity_from_metadata( - metadata.source_component_metadata_for_projection(), + metadata.source_component_metadata, extension=FunctionOutputExtensionAuthority.from_source( - metadata.source_component_metadata_for_projection(), - metadata.source_path_for_projection(), - parser=parser, - identity_cache=identity_cache, + metadata.source_component_metadata, metadata.source_path, + parser=parser, identity_cache=identity_cache, ), source="payload component metadata", ) @@ -631,7 +625,7 @@ def _identity_from_metadata_uncached( identity, ( ( - metadata.source_path_for_projection(), + metadata.source_path, "payload source path", ), ( @@ -647,10 +641,8 @@ def _identity_from_metadata_uncached( identity = cls._identity_from_metadata( source_identity.component_metadata, extension=FunctionOutputExtensionAuthority.from_source( - source_identity.component_metadata, - source_identity.path, - parser=parser, - identity_cache=identity_cache, + source_identity.component_metadata, source_identity.path, + parser=parser, identity_cache=identity_cache, ), source="single represented payload source metadata", ) @@ -678,10 +670,10 @@ def _identity_from_metadata_uncached( identity_cache=identity_cache, ) - if metadata.source_path_for_projection() is not None: + if metadata.source_path is not None: return cls._parsed_path_identity_with_cache( parser, - metadata.source_path_for_projection(), + metadata.source_path, source="payload source path", identity_cache=identity_cache, ) @@ -691,27 +683,25 @@ def _identity_from_metadata_uncached( def filename_identity_from_metadata( cls, parser: FilenameParser, - metadata: ImageMetadataProjection, + metadata: ImagePayloadMetadata, *, fallback_identity_path: str | None = None, ) -> FunctionOutputIdentity | None: """Return the identity that should name a filename-addressed output.""" identity_cache = FunctionOutputIdentityCache() - represented_source_identities = metadata.read_value( - "source_provenance" - ).represented_source_identities + represented_source_identities = ( + metadata.source_provenance.represented_source_identities + ) represented_source_path = ( represented_source_identities[0].path if represented_source_identities else None ) identity = cls._identity_from_metadata( - metadata.source_component_metadata_for_projection(), + metadata.source_component_metadata, extension=FunctionOutputExtensionAuthority.from_source( - metadata.source_component_metadata_for_projection(), - metadata.source_path_for_projection(), - parser=parser, - identity_cache=identity_cache, + metadata.source_component_metadata, metadata.source_path, + parser=parser, identity_cache=identity_cache, ), source="payload component metadata", ) @@ -721,7 +711,7 @@ def filename_identity_from_metadata( identity, ( ( - metadata.source_path_for_projection(), + metadata.source_path, "payload source path", ), ( @@ -738,7 +728,7 @@ def filename_identity_from_metadata( for path, source in ( ( - metadata.source_path_for_projection(), + metadata.source_path, "payload source path", ), ( @@ -844,14 +834,14 @@ def _parsed_path_identity( def _source_stack_identity_from_provenance( cls, parser: FilenameParser, - metadata: ImageMetadataProjection, + metadata: ImagePayloadMetadata, identity_component_values: frozenset[str], *, fallback_identity_path: str | None, input_aligned_output: bool, identity_cache: FunctionOutputIdentityCache, ) -> FunctionOutputIdentity: - source_provenance = metadata.read_value("source_provenance") + source_provenance = metadata.source_provenance semantic_source_identities = tuple( source_provenance.for_source_plane(plane_index).scalar_source_identity for plane_index in range(source_provenance.source_plane_count) @@ -910,10 +900,8 @@ def _source_stack_source_identity( identity = cls._identity_from_metadata( source_identity.component_metadata, extension=FunctionOutputExtensionAuthority.from_source( - source_identity.component_metadata, - source_identity.path, - parser=parser, - identity_cache=identity_cache, + source_identity.component_metadata, source_identity.path, + parser=parser, identity_cache=identity_cache, ), source=f"represented source identity {identity_index} metadata", ) diff --git a/openhcs/core/steps/function_output_manifest.py b/openhcs/core/steps/function_output_manifest.py index a457ac4526..daef36dea2 100644 --- a/openhcs/core/steps/function_output_manifest.py +++ b/openhcs/core/steps/function_output_manifest.py @@ -4,7 +4,6 @@ from dataclasses import dataclass, field, replace from pathlib import Path -from types import MemberDescriptorType from typing import ClassVar, Iterator, Sequence from weakref import WeakKeyDictionary @@ -21,10 +20,8 @@ from openhcs.core.path_pattern_matching import PathPatternTemplateMatcher from openhcs.core.runtime_array_values import RuntimeArrayData from openhcs.core.runtime_image_values import ( - ImageMetadataCarrier, - ImageMetadataProjection, ImagePayloadMetadata, - image_payload_metadata_projection, + image_payload_metadata, ) from openhcs.core.runtime_plane_projection import RuntimePlaneAxis @@ -110,7 +107,7 @@ def build( @dataclass(frozen=True, slots=True) -class ProducedOutputSemantics(FunctionOutputIdentity, ImageMetadataCarrier): +class ProducedOutputSemantics(FunctionOutputIdentity): """Semantic record for one output file produced by a FunctionStep.""" producer_identity: StreamProducerIdentity @@ -118,7 +115,6 @@ class ProducedOutputSemantics(FunctionOutputIdentity, ImageMetadataCarrier): relative_output_path: str image_metadata: ImagePayloadMetadata | None = None main_flow_plane_axis: RuntimePlaneAxis | None = RuntimePlaneAxis.RUNTIME_SLICE - _metadata_projection: ClassVar[MemberDescriptorType] def passed_through(self, plan: CompiledStepPlan) -> "ProducedOutputSemantics": """Retain the image domain and physical identity under the next producer.""" @@ -126,12 +122,10 @@ def passed_through(self, plan: CompiledStepPlan) -> "ProducedOutputSemantics": self, producer_identity=FunctionStepOutputProducerIdentityAuthority.build( FunctionStepOutputProducerIdentityRequest.from_main_flow( - plan, - self.output_context, + plan, self.output_context, ) ), relative_output_path=source_path_identity(self.output_path).name, - image_metadata=self.metadata_projection, ) def contextualize_image_payload( @@ -140,13 +134,9 @@ def contextualize_image_payload( ) -> RuntimeArrayData: """Attach this exact produced-output identity to a reloaded payload.""" - metadata = self.metadata_projection or image_payload_metadata_projection( - payload - ) + metadata = self.image_metadata or image_payload_metadata(payload) return metadata.with_source_component_metadata( - self.component_metadata( - metadata.read_value("source_provenance").source_component_metadata - ) + self.component_metadata(metadata.source_component_metadata) ).attach_to(payload) def path_under(self, output_dir: str | Path) -> str: @@ -218,7 +208,7 @@ def from_output( output_path: str | Path, output_identity: FunctionOutputIdentity, output_context: AlignedImageSliceContext | None = None, - image_metadata: ImageMetadataProjection | None = None, + image_metadata: ImagePayloadMetadata | None = None, main_flow_plane_axis: RuntimePlaneAxis | None = RuntimePlaneAxis.RUNTIME_SLICE, ) -> "ProducedOutputSemantics": output_path_text = str(output_path) @@ -286,11 +276,6 @@ def from_existing_main_flow_path( ) -# Keep the declared field and its original slot; public access realizes its owner. -ProducedOutputSemantics._metadata_projection = ProducedOutputSemantics.image_metadata -ProducedOutputSemantics.image_metadata = ImageMetadataCarrier.metadata - - @dataclass(slots=True) class StepOutputManifestStore: """Execution-local main-flow output lineage for shared VFS directories.""" @@ -566,10 +551,7 @@ def _unique_output_path_records( records_by_path: dict[str, ProducedOutputSemantics] = {} for record in records: existing = records_by_path.get(record.output_path) - if ( - existing is not None - and existing.main_flow_plane_axis is not record.main_flow_plane_axis - ): + if existing is not None and existing.main_flow_plane_axis is not record.main_flow_plane_axis: raise ValueError( "One produced output path cannot declare conflicting main-flow image axes: " f"{record.output_path!r}." diff --git a/openhcs/core/steps/function_outputs.py b/openhcs/core/steps/function_outputs.py index efa94ad98e..7297a44416 100644 --- a/openhcs/core/steps/function_outputs.py +++ b/openhcs/core/steps/function_outputs.py @@ -38,7 +38,7 @@ ImagePayloadMetadata, image_intensity_scale_for_dtype, image_payload_data, - image_payload_metadata_projection, + image_payload_metadata, ) from openhcs.core.runtime_profile import RuntimeProfileLogger from openhcs.core.runtime_slice_projection import ( @@ -843,8 +843,7 @@ def write( is_main=self.is_main, results_dir=( str(Path(self.results_dir).relative_to(self.plate_root)) - if self.results_dir is not None - else None + if self.results_dir is not None else None ), ) @@ -913,8 +912,7 @@ def produced_projection_metadata( ) ) produced_projections[Path(destination)] = ( - record, - projection_paths[-1][0], + record, projection_paths[-1][0] ) continue projection_paths.append( @@ -931,8 +929,7 @@ def produced_projection_metadata( ) ) produced_projections[Path(destination)] = ( - record, - projection_paths[-1][0], + record, projection_paths[-1][0] ) projection_paths.extend( self.runtime_artifact_projection_paths( @@ -1038,17 +1035,6 @@ def persisted_image_metadata( destination: str, payload, ) -> ImagePayloadMetadata: - return self.persisted_image_metadata_projection( - context, destination=destination, payload=payload - ).materialize_metadata() - - def persisted_image_metadata_projection( - self, - context: ProcessingContext, - *, - destination: str, - payload, - ): """Describe one saved image through its backend or physical format.""" if context.filemanager is None: @@ -1059,9 +1045,9 @@ def persisted_image_metadata_projection( base_path=self.output_dir, ) if physical_path is not None: - return ImageFileFormat.require_path( - physical_path - ).persisted_metadata_projection(Path(physical_path), payload) + return ImageFileFormat.require_path(physical_path).persisted_metadata( + Path(physical_path), payload + ) native_dtype = context.filemanager.source_image_dtype( destination, self.backend, @@ -1070,8 +1056,8 @@ def persisted_image_metadata_projection( return ImageFileSourceMetadata( source_dtype=native_dtype, intensity_scale=image_intensity_scale_for_dtype(native_dtype), - ).project_image_metadata_projection( - image_payload_metadata_projection(payload), + ).project_image_metadata( + image_payload_metadata(payload), values_preserved=context.filemanager.image_serialization_preserves_values( self.backend, image_payload_data(payload).dtype, diff --git a/openhcs/core/steps/function_runtime.py b/openhcs/core/steps/function_runtime.py index 1e09683d65..e0fbc70fea 100644 --- a/openhcs/core/steps/function_runtime.py +++ b/openhcs/core/steps/function_runtime.py @@ -140,14 +140,12 @@ SourceProjectionRole, ) from openhcs.core.runtime_image_values import ( - ImageMetadataProjection, ImagePayloadMetadata, ImagePayloadMetadataCarrier, ImagePayloadMetadataCompositionMode, image_payload_data, image_payload_mask, image_payload_metadata, - image_payload_metadata_projection, image_payload_slice_context, preserve_declared_image_payload_axis, with_image_payload_data, @@ -471,9 +469,7 @@ def output_owns_source_context( """Return whether an image result already carries complete source identity.""" if not isinstance(output_value, AlignedImageStack) and not ( - image_payload_metadata_projection( - output_value - ).has_complete_source_identity( + image_payload_metadata(output_value).has_complete_source_identity( output_value, plane_projection, ) @@ -481,9 +477,9 @@ def output_owns_source_context( return False output_surfaces = flatten_aligned_image_payload_slices(output_value) if not all( - image_payload_metadata_projection( + image_payload_metadata(output_surface).has_complete_source_identity( output_surface - ).has_complete_source_identity(output_surface) + ) for output_surface in output_surfaces ): return False @@ -491,12 +487,12 @@ def output_owns_source_context( return True source_surfaces = flatten_aligned_image_payload_slices(source_payload) return len(output_surfaces) == len(source_surfaces) and all( - image_payload_metadata_projection(output_surface) - .read_value("source_provenance") - .represented_source_identities - == image_payload_metadata_projection(source_surface) - .read_value("source_provenance") - .represented_source_identities + image_payload_metadata( + output_surface + ).source_provenance.represented_source_identities + == image_payload_metadata( + source_surface + ).source_provenance.represented_source_identities for source_surface, output_surface in zip( source_surfaces, output_surfaces, @@ -556,33 +552,27 @@ def contextualize( and plane_projection is not None and plane_projection.plane_index is None ): - output_metadata = image_payload_metadata_projection(output_value) + output_metadata = image_payload_metadata(output_value) if ( - output_metadata.read_value("plane_axis") is None - and output_metadata.read_value("source_provenance").source_plane_count + output_metadata.plane_axis is None + and output_metadata.source_provenance.source_plane_count == plane_projection.axis_size and plane_projection.dense_shape_carries_axis( np.shape(image_payload_data(output_value)) ) ): - output_value = output_metadata.derive_fields( + output_value = output_metadata.replace_fields( plane_axis=plane_projection.axis, ).attach_to(output_value) if output_plan is not None and not output_plan.variable_components: - output_metadata = image_payload_metadata_projection(output_value) - if ( - output_metadata.read_value("plane_axis") - is RuntimePlaneAxis.RUNTIME_SLICE - ): + output_metadata = image_payload_metadata(output_value) + if output_metadata.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE: output_value = RuntimeSliceProjection.value_for_singleton_slice( output_value, source_description=f"Image output {output_plan.ref()!r}", ) - source_metadata = image_payload_metadata_projection(source_payload) - if ( - source_metadata.read_value("plane_axis") - is RuntimePlaneAxis.RUNTIME_SLICE - ): + source_metadata = image_payload_metadata(source_payload) + if source_metadata.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE: if source_ref is None: raise ValueError( f"Image output {output_plan.ref()!r} consumes a runtime " @@ -590,9 +580,9 @@ def contextualize( ) collapsed_metadata = source_metadata.collapse_leading_plane_axis() source_payload = collapsed_metadata.with_source_provenance( - collapsed_metadata.read_value( - "source_provenance" - ).with_source_image_names((source_ref.name,)) + collapsed_metadata.source_provenance.with_source_image_names( + (source_ref.name,) + ) ).attach_source_context_to(output_value) plane_projection = None source_context_strategy = ImageOutputSourceContextStrategy.for_source_payload( @@ -610,17 +600,14 @@ def contextualize( ): if isinstance(output_value, AlignedImageStack): return output_value - output_metadata = image_payload_metadata_projection(output_value) - source_metadata = image_payload_metadata_projection(source_payload) + output_metadata = image_payload_metadata(output_value) + source_metadata = image_payload_metadata(source_payload) contextualized_output = output_metadata.with_source_context_from( source_metadata ).attach_source_context_to( output_value, ) - if ( - image_payload_metadata(contextualized_output) - == output_metadata.materialize_metadata() - ): + if image_payload_metadata(contextualized_output) == output_metadata: return output_value return contextualized_output return source_context_strategy.contextualize( @@ -705,9 +692,7 @@ def contextualize( del plane_projection subject = self._declared_subject(output_plan) assert output_plan is not None - source_provenance = image_payload_metadata_projection( - source_payload - ).read_value("source_provenance") + source_provenance = image_payload_metadata(source_payload).source_provenance if isinstance(output_value, MeasurementTable): self._validate_nominal_table(output_value, output_plan, subject) contextualized_provenance = ( @@ -760,9 +745,7 @@ def contextualize( if output_plan is not None: output_value.validate_artifact_name(output_plan.name) source_provenance = output_value.contextualized_source_provenance( - image_payload_metadata_projection(source_payload).read_value( - "source_provenance" - ) + image_payload_metadata(source_payload).source_provenance ) contextualized_provenance = output_value.source_provenance.with_missing_from( source_provenance @@ -779,7 +762,7 @@ def project_declared_source_identity( ) -> RuntimePayload: """Project an image payload to one exact declared source identity.""" - metadata = image_payload_metadata_projection(source_payload) + metadata = image_payload_metadata(source_payload) return metadata.project_declared_source_image(source_payload, source_ref.name) @@ -871,7 +854,7 @@ def contextualize( ) -> RuntimePayload: if isinstance(output_value, AlignedImageStack): return output_value - return image_payload_metadata_projection(source_payload).derive_payload( + return image_payload_metadata(source_payload).derive_payload( source_payload, output_value, plane_projection=plane_projection, @@ -941,10 +924,7 @@ def requires_plane_contextualization( return ( plane_projection is not None and plane_projection.plane_index is None - and image_payload_metadata_projection(source_payload).read_value( - "plane_axis" - ) - is None + and image_payload_metadata(source_payload).plane_axis is None and np.ndim(object_label_dense_array(source_payload)) >= 3 ) @@ -959,14 +939,14 @@ def contextualize( "Object-label image output context requires ObjectLabelValue, got " f"{type(source_payload).__name__}." ) - source_metadata = image_payload_metadata_projection(source_payload) + source_metadata = image_payload_metadata(source_payload) if plane_projection is None or plane_projection.plane_index is not None: return source_metadata.derive_payload( source_payload, output_value, plane_projection=plane_projection, ) - plane_count = source_metadata.read_value("source_provenance").source_plane_count + plane_count = source_metadata.source_provenance.source_plane_count if plane_count != plane_projection.axis_size: raise ValueError( "Object-label image output source-plane provenance must match the " @@ -977,8 +957,8 @@ def contextualize( np.shape(image_payload_data(output_value)), value_name="Object-label image output payload", ) - output_metadata = image_payload_metadata_projection(output_value) - contextualized_output = output_metadata.derive_fields( + output_metadata = image_payload_metadata(output_value) + contextualized_output = output_metadata.replace_fields( plane_axis=plane_projection.axis, ).attach_to(output_value) return source_metadata.derive_payload( @@ -1005,7 +985,7 @@ def payload(self) -> RuntimePayload: "but no declared runtime plane projection." ) source_value = self.source_values.value_for_aligned_slice(0, 1) - return image_payload_metadata_projection(source_value).derive_payload( + return image_payload_metadata(source_value).derive_payload( source_value, self.output_value, ) @@ -1017,7 +997,7 @@ def payload(self) -> RuntimePayload: len(output_slices), ) contextualized_slices.append( - image_payload_metadata_projection(source_value).derive_payload( + image_payload_metadata(source_value).derive_payload( source_value, output_slice, ) @@ -1366,9 +1346,9 @@ def active_main_flow_source_binding_plan( plan = self.unscoped_main_flow_source_binding_plan represented_names = frozenset( - image_payload_metadata_projection(payload) - .read_value("source_provenance") - .represented_source_image_names + image_payload_metadata( + payload + ).source_provenance.represented_source_image_names ) if represented_names: variable_components = ComponentSet.coerce( @@ -1451,11 +1431,9 @@ def from_pattern_group( request.component_key, artifacts.outputs, ) - source_provenance = ( - image_payload_metadata_projection(loaded.main_data_stack) - .read_value("source_provenance") - .with_common_scalar_identity_from_planes() - ) + source_provenance = image_payload_metadata( + loaded.main_data_stack + ).source_provenance.with_common_scalar_identity_from_planes() common_source_metadata = source_provenance.source_component_metadata or {} variable_components = ComponentSet.coerce( request.execution_plan.variable_components or () @@ -1598,18 +1576,14 @@ class PatternGroupData: default_factory=SourceBindingRuntimeContext.empty ) + @staticmethod def validate_main_flow_cohort( producer_records: Sequence[ProducedOutputSemantics] | None, ) -> None: """One input cohort has one declared whole-image composition domain.""" - if ( - producer_records - and len({record.main_flow_plane_axis for record in producer_records}) != 1 - ): - raise ValueError( - "One main-flow cohort cannot combine different declared image axes." - ) + if producer_records and len({record.main_flow_plane_axis for record in producer_records}) != 1: + raise ValueError("One main-flow cohort cannot combine different declared image axes.") @classmethod def from_loaded_images( @@ -1627,15 +1601,11 @@ def from_loaded_images( if ( producer_records and len(producer_records) == 1 - and producer_records[0].main_flow_plane_axis - is image_payload_metadata_projection(payloads[0]).read_value("plane_axis") + and producer_records[0].main_flow_plane_axis is image_payload_metadata(payloads[0]).plane_axis ): main_data_stack = ImagePayloadStackComposition.copy_whole_image( - payloads[0], - memory_type=execution_plan.input_memory_type, - device_id=execution_plan.device_id_for( - execution_plan.input_memory_type - ), + payloads[0], memory_type=execution_plan.input_memory_type, + device_id=execution_plan.device_id_for(execution_plan.input_memory_type), ) else: metadata_mode = ImagePayloadMetadataCompositionMode.STACK @@ -1644,9 +1614,7 @@ def from_loaded_images( metadata_mode = ( ImagePayloadMetadataCompositionMode.BUNDLE if declared_axis is None - else ImagePayloadMetadataCompositionMode.for_plane_axis( - declared_axis - ) + else ImagePayloadMetadataCompositionMode.for_plane_axis(declared_axis) ) if source_projection is not None and workspace_source_lookups: metadata_mode = source_projection.payload_composition_mode( @@ -1659,14 +1627,11 @@ def from_loaded_images( execution_plan.device_id_for(execution_plan.input_memory_type), ) main_data_stack = stack_image_payload_context( - payloads, - main_data_stack, - metadata_mode=metadata_mode, + payloads, main_data_stack, metadata_mode=metadata_mode, ) elif metadata_mode is ImagePayloadMetadataCompositionMode.BUNDLE: main_data_stack = ImagePayloadBundleContext.from_payloads( - tuple(payloads), - metadata_mode=metadata_mode, + tuple(payloads), metadata_mode=metadata_mode, ).compose() return cls( matching_files=matching_files, @@ -1795,8 +1760,7 @@ def prepare_compiled_function_group(group: CompiledFunctionGroup) -> None: def prepare_compiled_context_callables( compiled_contexts: Mapping[str, ProcessingContext], - *, - max_workers: int = 1, + *, max_workers: int = 1, ) -> None: """Prepare every compiled callable visible in the compiled contexts.""" prepared_group_keys: set[tuple[str, int, str]] = set() @@ -2179,8 +2143,8 @@ def execution_group_source_payload( component = self.runtime_scope.execution_plan.execution_group_scope.component if component is None or self.group_key is None: return source_payload - metadata = image_payload_metadata_projection(source_payload) - component_metadata = metadata.source_component_metadata_for_projection() or {} + metadata = image_payload_metadata(source_payload) + component_metadata = metadata.source_component_metadata or {} component_metadata = with_source_component_metadata( component_metadata, component, @@ -2548,30 +2512,25 @@ def from_single_image( slices=(value,), slice_contexts=slice_contexts, stack_payload=ImagePayloadStackComposition.copy_whole_image( - value, - memory_type=memory_type, - device_id=device_id, + value, memory_type=memory_type, device_id=device_id, ), main_flow_source=value, ) def plane_axis_for_output_context( - self, - context: AlignedImageSliceContext, + self, context: AlignedImageSliceContext, ) -> RuntimePlaneAxis | None: """Derive each persisted member's domain from its original nominal producer.""" if isinstance(self.main_flow_source, AlignedImageStack): return self.main_flow_source.plane_axis_for_output_context(context) if isinstance(self.main_flow_source, ImagePayloadMetadataCarrier): - return image_payload_metadata_projection(self.main_flow_source).read_value( - "plane_axis" - ) + return image_payload_metadata(self.main_flow_source).plane_axis return RuntimePlaneAxis.RUNTIME_SLICE def cache_payload_for_outputs( self, payloads: Sequence[RuntimeArrayData], - metadata: Sequence[ImageMetadataProjection], + metadata: Sequence[ImagePayloadMetadata], ) -> RuntimeArrayData | None: """Keep the original whole-image domain while applying saved output context.""" if self.stack_payload is None: @@ -2579,17 +2538,11 @@ def cache_payload_for_outputs( data = image_payload_data(self.stack_payload) if ( len(payloads) == 1 - and self.plane_axis_for_output_context(self.slice_contexts[0]) - is metadata[0].read_value("plane_axis") + and self.plane_axis_for_output_context(self.slice_contexts[0]) is metadata[0].plane_axis and np.shape(data) == np.shape(image_payload_data(payloads[0])) ): - return ( - metadata[0] - .derive_fields() - .payload_with( - data, - image_payload_mask(self.stack_payload), - ) + return metadata[0].replace_fields().payload_with( + data, image_payload_mask(self.stack_payload), ) if np.shape(data)[:1] != (len(payloads),): raise ValueError( @@ -2598,14 +2551,10 @@ def cache_payload_for_outputs( f"slice count {len(payloads)}." ) return stack_image_payload_context_from_metadata( - payloads, - data, - metadata, + payloads, data, metadata, metadata_mode=( ImagePayloadMetadataCompositionMode.for_plane_axis( - image_payload_metadata_projection(self.stack_payload).read_value( - "plane_axis" - ), + image_payload_metadata(self.stack_payload).plane_axis, ) if isinstance(self.stack_payload, ImagePayloadMetadataCarrier) else ImagePayloadMetadataCompositionMode.STACK @@ -2627,9 +2576,7 @@ def from_aligned_stack( if metadata_mode is None: result = cls.from_single_image( payloads[0], - slice_contexts=tuple( - context for _payload, context in projected_outputs - ), + slice_contexts=tuple(context for _payload, context in projected_outputs), memory_type=memory_type, device_id=device_id, ) @@ -2641,21 +2588,15 @@ def from_aligned_stack( value.plane_axis_for_output_context(context) for _payload, context in projected_outputs } - if ( - len(declared_axes) == 1 - and len({tuple(np.shape(item)) for item in data}) == 1 - ): + if len(declared_axes) == 1 and len({tuple(np.shape(item)) for item in data}) == 1: if metadata_mode is ImagePayloadMetadataCompositionMode.BUNDLE: stack_payload = ImagePayloadBundleContext.from_payloads( - tuple(payloads), - metadata_mode=metadata_mode, + tuple(payloads), metadata_mode=metadata_mode, ).compose() else: stacked = stack_runtime_slices(data, memory_type, device_id) stack_payload = stack_image_payload_context( - payloads, - stacked, - metadata_mode=metadata_mode, + payloads, stacked, metadata_mode=metadata_mode, ) return cls( slices=payloads, @@ -2849,9 +2790,7 @@ def _record_main_flow_passthrough(self, matching_files: Sequence[str]) -> None: plan = self.request.execution_plan parser = self.request.context.microscope_handler.parser manifest = step_output_manifest(self.request.context) - producer_records = manifest.producer_output_records_for_paths( - plan, matching_files, parser - ) + producer_records = manifest.producer_output_records_for_paths(plan, matching_files, parser) if producer_records is None: records = tuple( ProducedOutputSemantics.from_existing_main_flow_path( @@ -2965,9 +2904,7 @@ def _load_input_stack(self) -> PatternGroupData: ).runtime_context() producer_records = ( output_manifest.producer_output_records_for_paths( - plan, - matching_files, - context.microscope_handler.parser, + plan, matching_files, context.microscope_handler.parser, ) if plan.main_input_dependency.kind is StepInputDependencyKind.STEP_OUTPUT else None @@ -3315,10 +3252,7 @@ def _validate_and_unstack( ) -> PatternGroupOutputData: if ( isinstance(processed_stack, ImagePayloadMetadataCarrier) - and image_payload_metadata_projection(processed_stack).read_value( - "plane_axis" - ) - is None + and processed_stack.metadata.plane_axis is None ): output_context = self._unwrapped_main_flow_output_context() return PatternGroupOutputData.from_single_image( @@ -3517,14 +3451,11 @@ def _save_outputs( ) ) img_slice = output_context.contextualize_image_payload(img_slice) - output_metadata = image_payload_metadata_projection(img_slice) + output_metadata = image_payload_metadata(img_slice) output_component_metadata = output_identity.component_metadata( - output_metadata.source_component_metadata_for_projection(), + output_metadata.source_component_metadata, ) - if ( - output_metadata.source_component_metadata_for_projection() - != output_component_metadata - ): + if output_metadata.source_component_metadata != output_component_metadata: output_metadata = output_metadata.with_source_component_metadata( output_component_metadata ) @@ -3535,9 +3466,7 @@ def _save_outputs( output_identity, output_context=output_context, image_metadata=output_metadata, - main_flow_plane_axis=output_data.plane_axis_for_output_context( - output_context - ), + main_flow_plane_axis=output_data.plane_axis_for_output_context(output_context), ) if output_directory_exists and context.filemanager.exists( @@ -3576,8 +3505,7 @@ def _save_outputs( Backend.MEMORY.value, ) stack_payload = output_data.cache_payload_for_outputs( - output_payloads, - output_payload_metadata, + output_payloads, output_payload_metadata, ) if stack_payload is not None: context.runtime_image_stack_cache.store( diff --git a/openhcs/interop/cellprofiler/module_artifact_declarations.py b/openhcs/interop/cellprofiler/module_artifact_declarations.py index 51b9ed1193..f7534336f8 100644 --- a/openhcs/interop/cellprofiler/module_artifact_declarations.py +++ b/openhcs/interop/cellprofiler/module_artifact_declarations.py @@ -65,7 +65,7 @@ from openhcs.core.function_patterns import ( FunctionInvocationKey, ) - from openhcs.core.runtime_image_values import ImageMetadataProjection + from openhcs.core.runtime_image_values import ImagePayloadMetadata from openhcs.core.runtime_tabular_values import ColumnarRows from openhcs.core.steps.function_runtime import ( RuntimeCallableKwargs, @@ -233,19 +233,19 @@ class ObjectArtifactInputModule(CellProfilerModule): """Parent for modules that consume object-label artifacts through declared settings.""" @classmethod - def measurement_record_source_metadata_projection( + def measurement_record_source_metadata( cls, request: "CellProfilerOutputRecordRequest", rows: "ColumnarRows", - ) -> "ImageMetadataProjection": + ) -> "ImagePayloadMetadata": """Compose image-feature ownership with exact object-input provenance.""" object_inputs = ArtifactSpecCollection( request.callable_contract.artifact_inputs.specs ).of_artifact_type(ObjectLabelsArtifactType) if not object_inputs: - return super().measurement_record_source_metadata_projection(request, rows) - return request.measurement_source_metadata_projection(object_inputs) + return super().measurement_record_source_metadata(request, rows) + return request.measurement_source_metadata(object_inputs) class ObjectArtifactOutputModule( @@ -463,17 +463,17 @@ def measurement_record_object_name( return None @classmethod - def measurement_record_source_metadata_projection( + def measurement_record_source_metadata( cls, request: "CellProfilerOutputRecordRequest", rows: "ColumnarRows", - ) -> "ImageMetadataProjection": + ) -> "ImagePayloadMetadata": """Compose inherited ownership with exact object-output provenance.""" object_outputs = cls.measurement_object_output_specs_for_request(request) if not object_outputs: - return super().measurement_record_source_metadata_projection(request, rows) - return request.measurement_source_metadata_projection(object_outputs) + return super().measurement_record_source_metadata(request, rows) + return request.measurement_source_metadata(object_outputs) class LabelDimensionObjectArtifactOutputModule(ObjectArtifactOutputModule): diff --git a/openhcs/interop/cellprofiler/runtime/artifact_binding.py b/openhcs/interop/cellprofiler/runtime/artifact_binding.py index f6ed074091..55a279e1b8 100644 --- a/openhcs/interop/cellprofiler/runtime/artifact_binding.py +++ b/openhcs/interop/cellprofiler/runtime/artifact_binding.py @@ -40,7 +40,7 @@ from openhcs.core.runtime_image_values import ( image_payload_data, image_payload_mask, - image_payload_metadata_projection, + image_payload_metadata, ) from openhcs.core.runtime_measurements import ( MeasurementTable, @@ -240,9 +240,9 @@ def raw_runtime_input_value( self, request: RuntimeArtifactInputRequest ) -> RuntimeCallableArgument: payload = request.value - metadata = image_payload_metadata_projection(payload) + metadata = image_payload_metadata(payload) metadata = metadata.with_source_provenance( - metadata.read_value("source_provenance").with_derived_source_image_names( + metadata.source_provenance.with_derived_source_image_names( (request.spec.name,) ) ) @@ -269,9 +269,7 @@ def source_image_name_from_value( value: RuntimeCallableArgument, ) -> str | None: return single_source_name( - image_payload_metadata_projection(value) - .read_value("source_provenance") - .represented_source_image_names + image_payload_metadata(value).source_provenance.represented_source_image_names ) def source_image_payload( @@ -320,13 +318,12 @@ def object_labels( value = request.value if isinstance(value, ObjectLabelSet): return value - metadata = image_payload_metadata_projection(value) + metadata = image_payload_metadata(value) return SourceImageObjectLabelBuildRequest( image=value, labels=image_payload_data(value), plane_projection=RuntimePlaneAxisValueProjection.from_source_declaration( - metadata.read_value("plane_axis"), - metadata.read_value("source_provenance"), + metadata.plane_axis, metadata.source_provenance, ), ).label_set( name=request.spec.name, @@ -592,11 +589,11 @@ def main_flow_value( f"{current_image.slice_contexts!r}." ) return payload - metadata = image_payload_metadata_projection(current_image) + metadata = image_payload_metadata(current_image) if ( len(self.primary_image_inputs) == 1 and self.primary_image_inputs[0] == spec - and metadata.read_value("source_provenance").source_plane_count == 0 + and metadata.source_provenance.source_plane_count == 0 ): return current_image return metadata.project_declared_source_image(current_image, spec.name) @@ -631,9 +628,7 @@ def stack_broadcast_source_value( f"stack-broadcast source {source_ref!r}." ) return payload - return image_payload_metadata_projection( - current_image - ).project_declared_source_image( + return image_payload_metadata(current_image).project_declared_source_image( current_image, source_ref.name, ) diff --git a/openhcs/interop/cellprofiler/runtime/function_contract_execution.py b/openhcs/interop/cellprofiler/runtime/function_contract_execution.py index f7c3589889..199f2dea38 100644 --- a/openhcs/interop/cellprofiler/runtime/function_contract_execution.py +++ b/openhcs/interop/cellprofiler/runtime/function_contract_execution.py @@ -25,7 +25,7 @@ from openhcs.core.runtime_image_values import ( image_payload_data, image_payload_mask, - image_payload_metadata_projection, + image_payload_metadata, ) from openhcs.core.runtime_output_matching import RuntimeReturnedOutputMatcher from openhcs.core.runtime_plane_projection import ( @@ -312,9 +312,7 @@ def _execute_aligned_multi_image_stack( ) if callable_contract.processing_contract is ProcessingContract.PURE_3D: slice_plane_axes = tuple( - image_payload_metadata_projection(slice_payload).read_value( - "plane_axis" - ) + image_payload_metadata(slice_payload).plane_axis for slice_payload in image.slices ) if any( @@ -489,9 +487,7 @@ def execute_pure_2d( image, kwargs, ).call() - declared_plane_axis = image_payload_metadata_projection(image).read_value( - "plane_axis" - ) + declared_plane_axis = image_payload_metadata(image).plane_axis if declared_plane_axis is not self.plane_projection.axis: raise RuntimeSliceProjectionDeclarationError( f"{invocation_context} with ProcessingContract.PURE_2D has an image " @@ -598,7 +594,7 @@ def execute_volumetric_to_slice( ).call() result_data = image_payload_data(result_2d) result_mask = image_payload_mask(result_2d) - result_metadata = image_payload_metadata_projection(result_2d) + result_metadata = image_payload_metadata(result_2d) memory_type = detect_memory_type(result_data) stacked = stack_slices([result_data], memory_type, 0) return result_metadata.payload_with(stacked, mask=result_mask) diff --git a/openhcs/interop/cellprofiler/runtime/invocation.py b/openhcs/interop/cellprofiler/runtime/invocation.py index c0a1dd6ab6..cfe57fa7b2 100644 --- a/openhcs/interop/cellprofiler/runtime/invocation.py +++ b/openhcs/interop/cellprofiler/runtime/invocation.py @@ -24,11 +24,10 @@ RuntimeImageRequest, ) from openhcs.core.runtime_image_values import ( - ImageMetadataProjection, ImagePayloadMetadata, ImagePayloadMetadataCompositionMode, image_payload_geometry, - image_payload_metadata_projection, + image_payload_metadata, ) from openhcs.core.runtime_object_labels import ( ObjectLabelMeasurementSource, @@ -117,9 +116,7 @@ def shared_source_payload( if not sources: return None provenances = tuple( - image_payload_metadata_projection(source.payload) - .read_value("source_provenance") - .equality_identity + image_payload_metadata(source.payload).source_provenance.equality_identity for source in sources ) if CommonRuntimeValue.from_values(provenances).single is None: @@ -133,18 +130,6 @@ def composed_source_metadata( *, mode: ImagePayloadMetadataCompositionMode | None = None, ) -> ImagePayloadMetadata | None: - """Expose the actual public namespace of the ordered source composition.""" - - owner = cls.composed_source_metadata_projection(sources, mode=mode) - return None if owner is None else owner.materialize_metadata() - - @classmethod - def composed_source_metadata_projection( - cls, - sources: tuple["CellProfilerSourceIdentityMixin", ...], - *, - mode: ImagePayloadMetadataCompositionMode | None = None, - ) -> ImageMetadataProjection | None: """Return source metadata composed in runtime source order.""" if not sources: return None @@ -158,18 +143,14 @@ def composed_source_metadata_projection( source_metadata = tuple( ImagePayloadMetadata( source_provenance=( - ImageMetadataProjection._from_realized_metadata( - ImagePayloadMetadata.compose( - payload_slices_for_alignment(payload), - mode=ImagePayloadMetadataCompositionMode.BUNDLE, - ) + ImagePayloadMetadata.compose( + payload_slices_for_alignment(payload), + mode=ImagePayloadMetadataCompositionMode.BUNDLE, ) .collapse_leading_plane_axis() - .read_value("source_provenance") + .source_provenance if isinstance(payload, AlignedImageStack) - else image_payload_metadata_projection(payload).read_value( - "source_provenance" - ) + else image_payload_metadata(payload).source_provenance ) ) for payload in source_payloads @@ -181,7 +162,7 @@ def composed_source_metadata_projection( ) if not metadata.has_values: return None - return ImageMetadataProjection._from_realized_metadata(metadata) + return metadata @classmethod def source_metadata_composition_mode( @@ -374,9 +355,7 @@ def with_alignment_image( f"{plane_projection!r}." ) else: - image_plane_axis = image_payload_metadata_projection(image).read_value( - "plane_axis" - ) + image_plane_axis = image_payload_metadata(image).plane_axis if image_plane_axis is None: plane_projection = None elif image_plane_axis is not plane_projection.axis: diff --git a/openhcs/interop/cellprofiler/runtime/main_flow.py b/openhcs/interop/cellprofiler/runtime/main_flow.py index acde5ea51a..da54a1e105 100644 --- a/openhcs/interop/cellprofiler/runtime/main_flow.py +++ b/openhcs/interop/cellprofiler/runtime/main_flow.py @@ -4,7 +4,7 @@ from openhcs.core.runtime_image_values import ( image_payload_geometry, - image_payload_metadata_projection, + image_payload_metadata, ) from openhcs.core.runtime_plane_projection import RuntimePlaneAxisValueProjection from openhcs.core.steps.function_runtime import RuntimeCallableArgument @@ -16,9 +16,7 @@ def cellprofiler_main_flow_output( plane_projection: RuntimePlaneAxisValueProjection | None, ) -> RuntimeCallableArgument: """Preserve the declared OpenHCS plane axis on a CP output image.""" - output_axis = image_payload_metadata_projection(output_image).read_value( - "plane_axis" - ) + output_axis = image_payload_metadata(output_image).plane_axis if output_axis is None: return output_image if plane_projection is None: @@ -36,7 +34,7 @@ def cellprofiler_main_flow_output( image_payload_geometry(output_image).shape, value_name="CellProfiler main-flow output", ) - input_axis = image_payload_metadata_projection(input_image).read_value("plane_axis") + input_axis = image_payload_metadata(input_image).plane_axis if input_axis is not None: if input_axis is not plane_projection.axis: raise ValueError( @@ -48,12 +46,12 @@ def cellprofiler_main_flow_output( image_payload_geometry(input_image).shape, value_name="CellProfiler input image", ) - if image_payload_metadata_projection(output_image).has_complete_source_identity( + if image_payload_metadata(output_image).has_complete_source_identity( output_image, plane_projection, ): return output_image - return image_payload_metadata_projection(input_image).derive_payload( + return image_payload_metadata(input_image).derive_payload( input_image, output_image, plane_projection=plane_projection, diff --git a/openhcs/interop/cellprofiler/runtime/measurement_recording.py b/openhcs/interop/cellprofiler/runtime/measurement_recording.py index 2094d02028..63cacedd72 100644 --- a/openhcs/interop/cellprofiler/runtime/measurement_recording.py +++ b/openhcs/interop/cellprofiler/runtime/measurement_recording.py @@ -39,9 +39,8 @@ MeasurementSparseColumnarRows, ) from openhcs.core.runtime_image_values import ( - ImageMetadataProjection, ImagePayloadMetadata, - image_payload_metadata_projection, + image_payload_metadata, ) from openhcs.core.runtime_measurements import MeasurementTable from openhcs.core.runtime_tabular_values import ColumnarRows @@ -138,7 +137,7 @@ def build_measurement_table( rows: ColumnarRows, object_name: str | None, source_image_name: str | None, - source_metadata: ImageMetadataProjection, + source_metadata: ImagePayloadMetadata, ) -> MeasurementTable: """Build one canonical module-owned native measurement table.""" rows, source_provenance = cls.measurement_rows_and_source_provenance( @@ -264,23 +263,10 @@ def measurement_record_source_image_name( def measurement_record_source_metadata( cls, request: CellProfilerOutputRecordRequest, rows: ColumnarRows ) -> ImagePayloadMetadata: - """Expose actual metadata through the public module callback protocol.""" - - return cls.measurement_record_source_metadata_projection( - request, rows - ).materialize_metadata() - - @classmethod - def measurement_record_source_metadata_projection( - cls, request: CellProfilerOutputRecordRequest, rows: ColumnarRows - ) -> ImageMetadataProjection: del cls, rows - metadata = request.source.composed_source_metadata((request.source,)) - return ( - ImageMetadataProjection._from_realized_metadata(metadata) - if metadata - else image_payload_metadata_projection(request.source.payload) - ) + return request.source.composed_source_metadata( + (request.source,) + ) or image_payload_metadata(request.source.payload) @staticmethod def rows_only_declare_object_name(rows: ColumnarRows) -> bool: @@ -294,20 +280,18 @@ def rows_only_declare_object_name(rows: ColumnarRows) -> bool: @staticmethod def measurement_rows_and_source_provenance( rows: ColumnarRows, - source_metadata: ImageMetadataProjection, + source_metadata: ImagePayloadMetadata, ) -> tuple[ColumnarRows, SourceImageProvenance]: """Keep row slice indexes local to the table's source-plane axis.""" - source_plane_count = source_metadata.read_value( - "source_provenance" - ).source_plane_count + source_plane_count = source_metadata.source_provenance.source_plane_count if source_plane_count <= 1: - return rows, source_metadata.read_value("source_provenance") + return rows, source_metadata.source_provenance row_projection = MeasurementRowsAxisProjection.from_rows(rows) slice_indices = row_projection.present_axis_values( MeasurementRowAxisField.SLICE_INDEX.value ) if len(slice_indices) != 1: - return rows, source_metadata.read_value("source_provenance") + return rows, source_metadata.source_provenance slice_index = slice_indices[0] if slice_index >= source_plane_count: raise ValueError( @@ -321,9 +305,7 @@ def measurement_rows_and_source_provenance( ) return ( projected_rows, - source_metadata.for_source_plane(slice_index).read_value( - "source_provenance" - ), + source_metadata.for_source_plane(slice_index).source_provenance, ) @classmethod @@ -670,11 +652,11 @@ class CurrentPayloadMeasurementRecordMixin(PayloadOnlyMeasurementRecordMixin): """Uses the current runtime payload without an image-source name.""" @classmethod - def measurement_record_source_metadata_projection( + def measurement_record_source_metadata( cls, request: CellProfilerOutputRecordRequest, rows: ColumnarRows - ) -> ImageMetadataProjection: + ) -> ImagePayloadMetadata: del cls, rows - return image_payload_metadata_projection(request.source.payload) + return image_payload_metadata(request.source.payload) @classmethod def clear_source_when_rows_declare_object_name(cls) -> bool: @@ -725,12 +707,12 @@ def measurement_record_source_image_name( return None if source_spec is None else source_spec.name @classmethod - def measurement_record_source_metadata_projection( + def measurement_record_source_metadata( cls, request: CellProfilerOutputRecordRequest, rows: ColumnarRows - ) -> ImageMetadataProjection: + ) -> ImagePayloadMetadata: del rows source_spec = cls.primary_image_output_spec(request) - return image_payload_metadata_projection( + return image_payload_metadata( request.source.payload if source_spec is None else request.artifact_output_value(source_spec) @@ -750,13 +732,13 @@ class DeclaredImageOutputPayloadMeasurementRecordMixin( """Uses exact declared image-output provenance as measurement context.""" @classmethod - def measurement_record_source_metadata_projection( + def measurement_record_source_metadata( cls, request: CellProfilerOutputRecordRequest, rows: ColumnarRows - ) -> ImageMetadataProjection: + ) -> ImagePayloadMetadata: from openhcs.core.artifacts import ImageArtifactType from openhcs.core.runtime_image_values import ( ImagePayloadMetadata, - image_payload_metadata_projection, + image_payload_metadata, ) from openhcs.core.source_matching import SourceImageSetIdentityPolicy from openhcs.core.source_plane_alignment import ( @@ -809,14 +791,11 @@ def measurement_record_source_metadata_projection( f"reference={image_output_specs[0].ref()!r}; " f"unaligned={tuple(image_output_specs[index].ref() for index in unaligned_indexes)!r}." ) - source_metadata = ImageMetadataProjection._from_realized_metadata( - ImagePayloadMetadata.compose( - source_payloads, - source_metadata=tuple( - image_payload_metadata_projection(payload) - for payload in source_payloads - ), - ) + source_metadata = ImagePayloadMetadata.compose( + source_payloads, + source_metadata=tuple( + image_payload_metadata(payload) for payload in source_payloads + ), ).collapse_leading_plane_axis() return source_metadata @@ -846,16 +825,16 @@ def measurement_record_source_names(rows: ColumnarRows) -> tuple[str, ...]: ) @classmethod - def measurement_record_source_metadata_projection( + def measurement_record_source_metadata( cls, request: CellProfilerOutputRecordRequest, rows: ColumnarRows - ) -> ImageMetadataProjection: + ) -> ImagePayloadMetadata: from openhcs.core.artifacts import ( ArtifactSpecCollection, ) source_names = cls.measurement_record_source_names(rows) if not source_names: - return super().measurement_record_source_metadata_projection(request, rows) + return super().measurement_record_source_metadata(request, rows) declared_inputs = ArtifactSpecCollection( request.callable_contract.artifact_inputs.specs @@ -870,7 +849,7 @@ def measurement_record_source_metadata_projection( ) source_specs.append(spec) - return request.measurement_source_metadata_projection(tuple(source_specs)) + return request.measurement_source_metadata(tuple(source_specs)) @classmethod def clear_source_when_rows_declare_object_name(cls) -> bool: diff --git a/openhcs/interop/cellprofiler/runtime/module_execution.py b/openhcs/interop/cellprofiler/runtime/module_execution.py index 940f545aff..170b24ac37 100644 --- a/openhcs/interop/cellprofiler/runtime/module_execution.py +++ b/openhcs/interop/cellprofiler/runtime/module_execution.py @@ -45,7 +45,7 @@ from openhcs.core.runtime_artifact_queries import MeasurementTableUnion from openhcs.core.runtime_batch_contracts import RuntimeBatchExecutionDomain from openhcs.core.runtime_image_values import ( - image_payload_metadata_projection, + image_payload_metadata, preserved_image_plane_projection, ) from openhcs.core.runtime_measurements import MeasurementTable @@ -722,8 +722,8 @@ def _resolved_measurement_image( ImageArtifactType, ).raw_runtime_input_value(request) ) - metadata = image_payload_metadata_projection(payload) - plane_axis = metadata.read_value("plane_axis") + metadata = image_payload_metadata(payload) + plane_axis = metadata.plane_axis return CellProfilerMeasurementImage( source_image_name=spec.name, source_aliases=source_aliases, @@ -1002,7 +1002,7 @@ def _run_per_object_measurement( measurement_images, source_image_name ) combined_source_metadata = ( - CellProfilerMeasurementImage.composed_source_metadata_projection( + CellProfilerMeasurementImage.composed_source_metadata( measurement_images, mode=measurement_row_policy.source_metadata_composition_mode( measurement_images @@ -1010,7 +1010,7 @@ def _run_per_object_measurement( ) ) if combined_source_metadata is None: - combined_source_metadata = image_payload_metadata_projection( + combined_source_metadata = image_payload_metadata( CellProfilerMeasurementImage.shared_source_payload(measurement_images) ) if profile_enabled: @@ -1298,9 +1298,7 @@ def _image_request( if ( runtime_projection is not None and runtime_projection.plane_index is not None - and image_payload_metadata_projection(current_image_payload).read_value( - "plane_axis" - ) + and image_payload_metadata(current_image_payload).plane_axis is runtime_projection.axis ): current_image_payload = cast( @@ -1336,7 +1334,9 @@ def _image_request( request = input_binding.artifact_request_for_spec(spec) payload = image_strategy.runtime_input_value(request) payloads.append(payload) - source_names.append(image_strategy.source_image_name_from_value(payload)) + source_names.append( + image_strategy.source_image_name_from_value(payload) + ) parameter_image_inputs = input_binding.image_inputs broadcast_sources = tuple( sources[0] @@ -1347,26 +1347,24 @@ def _image_request( align_primary_images = len(image_inputs) > 1 and broadcast_sources == tuple( spec.ref() for spec in image_inputs ) - composition = ( - self.callable_contract.image_payload_consumption.compose_image_payload( - f"{module_type.require_module_name()} image inputs " - f"{tuple(spec.name for spec in image_inputs)!r}", - tuple(payloads), - slice_contexts=( - tuple( - AlignedImageSliceContext.main_flow( - output_key=spec.name, - artifact_kind=spec.artifact_type.value, - ) - for spec in image_inputs + composition = self.callable_contract.image_payload_consumption.compose_image_payload( + f"{module_type.require_module_name()} image inputs " + f"{tuple(spec.name for spec in image_inputs)!r}", + tuple(payloads), + slice_contexts=( + tuple( + AlignedImageSliceContext.main_flow( + output_key=spec.name, + artifact_kind=spec.artifact_type.value, ) - if align_primary_images - else () - ), - stack_broadcast_source_indices=( - image_input_specs.stack_broadcast_source_indices() - ), - ) + for spec in image_inputs + ) + if align_primary_images + else () + ), + stack_broadcast_source_indices=( + image_input_specs.stack_broadcast_source_indices() + ), ) return CellProfilerImageRequest( payload=composition.payload, diff --git a/openhcs/interop/cellprofiler/runtime/output_record_request.py b/openhcs/interop/cellprofiler/runtime/output_record_request.py index 7f36004c40..559fda7c41 100644 --- a/openhcs/interop/cellprofiler/runtime/output_record_request.py +++ b/openhcs/interop/cellprofiler/runtime/output_record_request.py @@ -22,9 +22,8 @@ ) from openhcs.core.runtime_array_values import RuntimeArrayData from openhcs.core.runtime_image_values import ( - ImageMetadataProjection, ImagePayloadMetadata, - image_payload_metadata_projection, + image_payload_metadata, ) from openhcs.core.callable_contract import CallableContract from openhcs.core.function_patterns import InvocationArtifactInputEdgePlan @@ -191,22 +190,12 @@ def measurement_source_metadata( self, specs: tuple[ArtifactSpec, ...], ) -> ImagePayloadMetadata: - """Expose the actual public metadata namespace for exact artifacts.""" - - return self.measurement_source_metadata_projection(specs).materialize_metadata() - - def measurement_source_metadata_projection( - self, - specs: tuple[ArtifactSpec, ...], - ) -> ImageMetadataProjection: """Return the contract-ordered image-set axis of exact artifacts.""" if not specs: raise ValueError("Measurement source context requires declared artifacts.") artifact_values = tuple(self.artifact_value(spec) for spec in specs) - metadata = tuple( - image_payload_metadata_projection(value) for value in artifact_values - ) + metadata = tuple(image_payload_metadata(value) for value in artifact_values) source_group_component = self.output_plan.group_component identity_policy = SourceImageSetIdentityPolicy( frozenset( @@ -282,20 +271,12 @@ def declared_source_payload(self) -> RuntimeCallableArgument: ) def materialization_source_metadata(self) -> ImagePayloadMetadata | None: - """Expose the public filename-source metadata declared by this output.""" - - owner = self.materialization_source_metadata_projection() - return None if owner is None else owner.materialize_metadata() - - def materialization_source_metadata_projection( - self, - ) -> ImageMetadataProjection | None: """Return independent filename-source metadata declared by this output.""" source_ref = self.output_plan.materialization_source() if source_ref is None or source_ref == self.output_plan.source_context_source(): return None - return image_payload_metadata_projection( + return image_payload_metadata( self.artifact_source_payload( self.adapter.request.require_artifact_input_edge(source_ref) ) diff --git a/openhcs/processing/materialization/core.py b/openhcs/processing/materialization/core.py index 07e9ebb92f..7a6f9aee00 100644 --- a/openhcs/processing/materialization/core.py +++ b/openhcs/processing/materialization/core.py @@ -42,13 +42,10 @@ ) from openhcs.core.runtime_array_values import runtime_array_operand from openhcs.core.runtime_image_values import ( - ImageMetadataCarrier, - ImageMetadataProjection, - SourcePlaneImageMetadataProjection, ImagePayloadMetadata, ImagePayloadMetadataCompositionMode, image_payload_data, - image_payload_metadata_projection, + image_payload_metadata, ) from openhcs.core.runtime_object_labels import ( ObjectLabelValue, @@ -277,7 +274,7 @@ class BackendKwargsAbsent: @dataclass(frozen=True) -class Output(ImageMetadataCarrier): +class Output: path: str content: MaterializationValue metadata: ImagePayloadMetadata | None = None @@ -299,11 +296,9 @@ def from_metadata( @property def source_identity(self) -> SourceImageIdentity | None: - if self.metadata_projection is None: + if self.metadata is None: return None - return self.metadata_projection.read_value( - "source_provenance" - ).scalar_source_identity + return self.metadata.source_provenance.scalar_source_identity @property def source_component_metadata(self) -> SourceComponentMetadata | None: @@ -335,8 +330,8 @@ def with_source_identity_fallback( ) metadata = ( fallback_metadata - if self.metadata_projection is None - else self.metadata_projection.with_source_context_from(fallback_metadata) + if self.metadata is None + else self.metadata.with_source_context_from(fallback_metadata) ) return replace( self, @@ -351,14 +346,10 @@ def with_variable_components( return replace( self, - metadata=self.metadata_projection, variable_components=ComponentSet.coerce(variable_components).as_tuple(), ) -Output.metadata = ImageMetadataCarrier.metadata - - @dataclass(frozen=True) class TextOutput(Output): """Materialization output whose canonical payload is text.""" @@ -551,14 +542,12 @@ class ROIMaterializationSourceSpatialDomain: """Resolve the source-spatial domain used by ROI extraction.""" def domain_for_item(self, item: MaterializationInputItem) -> SourceSpatialDomain: - metadata_domain = item.metadata_projection.read_value( - "source_spatial_domain" - ).with_value_name( + metadata_domain = item.metadata.source_spatial_domain.with_value_name( "ROI materialization source image", ) if metadata_domain.source_shape_yx is not None: return metadata_domain - local_shape_yx = item.metadata_projection.spatial_shape_yx(item.data) + local_shape_yx = item.metadata.spatial_shape_yx(item.data) if local_shape_yx is None: return metadata_domain return SourceSpatialDomain( @@ -606,9 +595,9 @@ def from_payloads( plane_count_source for payload in payloads for plane_count_source in ( - image_payload_metadata_projection(payload) - .read_value("source_provenance") - .plane_count_sources + image_payload_metadata( + payload + ).source_provenance.plane_count_sources ) ) ) @@ -713,7 +702,7 @@ def build_for_context( def required_source_stem( self, - metadata: ImageMetadataProjection, + metadata: ImagePayloadMetadata, filename_identity: SourceImageIdentity | None = None, ) -> str: source_stem = SourceStemResolutionPolicy.for_context( @@ -730,9 +719,9 @@ def required_source_stem( def source_replacement_suffix( self, path_name: str, - metadata: ImageMetadataProjection, + metadata: ImagePayloadMetadata, reference_source_stem: str | None, - reference_metadata: ImageMetadataProjection | None, + reference_metadata: ImagePayloadMetadata | None, ) -> str | None: for source_stem in self.source_replacement_stems( path_name, @@ -747,9 +736,9 @@ def source_replacement_suffix( def source_replacement_stems( self, path_name: str, - metadata: ImageMetadataProjection, + metadata: ImagePayloadMetadata, reference_source_stem: str | None, - reference_metadata: ImageMetadataProjection | None, + reference_metadata: ImagePayloadMetadata | None, ) -> tuple[str, ...]: del path_name, metadata, reference_metadata if reference_source_stem is None: @@ -766,7 +755,7 @@ def source_component_metadata_stem( class SourceStemResolutionPolicy( - MostDerivedContextStrategyMixin[ImageMetadataProjection], + MostDerivedContextStrategyMixin[ImagePayloadMetadata], ABC, ): """Resolve source-stem inputs through MRO precedence over payload metadata.""" @@ -775,14 +764,14 @@ class SourceStemResolutionPolicy( def source_stem( self, authority: SourceStemAuthority, - metadata: ImageMetadataProjection, + metadata: ImagePayloadMetadata, filename_identity: SourceImageIdentity | None, ) -> str | None: """Return this source-stem case's projection.""" class MissingSourceStemResolutionPolicy( - AlwaysMatchesContextMixin[ImageMetadataProjection], + AlwaysMatchesContextMixin[ImagePayloadMetadata], SourceStemResolutionPolicy, ): """No addressable source identity is present.""" @@ -792,7 +781,7 @@ class MissingSourceStemResolutionPolicy( def source_stem( self, authority: SourceStemAuthority, - metadata: ImageMetadataProjection, + metadata: ImagePayloadMetadata, filename_identity: SourceImageIdentity | None, ) -> str | None: del authority, metadata, filename_identity @@ -804,23 +793,21 @@ class ComponentMetadataSourceStemResolutionPolicy(MissingSourceStemResolutionPol strategy_key = "component_metadata" - def matches(self, context: ImageMetadataProjection) -> bool: + def matches(self, context: ImagePayloadMetadata) -> bool: return ( - context.read_value( - "source_provenance" - ).scalar_source_identity.component_metadata + context.source_provenance.scalar_source_identity.component_metadata is not None ) def source_stem( self, authority: SourceStemAuthority, - metadata: ImageMetadataProjection, + metadata: ImagePayloadMetadata, filename_identity: SourceImageIdentity | None, ) -> str: - component_metadata = metadata.read_value( - "source_provenance" - ).scalar_source_identity.component_metadata + component_metadata = ( + metadata.source_provenance.scalar_source_identity.component_metadata + ) if component_metadata is None: raise ValueError( "Component metadata source-stem policy requires scalar " @@ -837,18 +824,18 @@ class SourcePathSourceStemResolutionPolicy(ComponentMetadataSourceStemResolution strategy_key = "source_path" - def matches(self, context: ImageMetadataProjection) -> bool: - return bool(context.read_value("source_provenance").scalar_source_identity.path) + def matches(self, context: ImagePayloadMetadata) -> bool: + return bool(context.source_provenance.scalar_source_identity.path) def source_stem( self, authority: SourceStemAuthority, - metadata: ImageMetadataProjection, + metadata: ImagePayloadMetadata, filename_identity: SourceImageIdentity | None, ) -> str: del authority, filename_identity return source_path_identity( - metadata.read_value("source_provenance").scalar_source_identity.path + metadata.source_provenance.scalar_source_identity.path ).stem @@ -920,9 +907,9 @@ def path_component_prefix_stems( path_name: str, metadata: ImagePayloadMetadata, ) -> tuple[str, ...]: - component_metadata = metadata.read_value( - "source_provenance" - ).scalar_source_identity.component_metadata + component_metadata = ( + metadata.source_provenance.scalar_source_identity.component_metadata + ) if component_metadata is None: return () @@ -937,9 +924,7 @@ def path_component_prefix_stems( ) def path_parse_extensions(self, metadata: ImagePayloadMetadata) -> tuple[str, ...]: - source_identity = metadata.read_value( - "source_provenance" - ).scalar_source_identity + source_identity = metadata.source_provenance.scalar_source_identity extension = source_identity.filename_extension if extension is None: extension = source_identity.with_parsed_path_components( @@ -1009,7 +994,7 @@ def from_metadata( source_identity=( None if metadata is None - else metadata.read_value("source_provenance").scalar_source_identity + else metadata.source_provenance.scalar_source_identity ), ) @@ -1643,13 +1628,11 @@ def projected_outputs(self, outputs: Sequence[Output]) -> tuple[Output, ...]: for item in items: identity = FunctionOutputIdentityAuthority.identity_from_metadata( parser, - item.metadata_projection, + item.metadata, fallback_identity_path=output.path, ) if identity is None: - raise ValueError( - "Projected viewer output requires a filename identity." - ) + raise ValueError("Projected viewer output requires a filename identity.") identity = replace(identity, extension=output_path.suffix) filename = FunctionOutputPathAuthority.filename_for_identity( parser, @@ -1660,7 +1643,7 @@ def projected_outputs(self, outputs: Sequence[Output]) -> tuple[Output, ...]: output, path=str(output_path.parent / filename), content=item.data, - metadata=item.metadata_projection, + metadata=item.metadata, ) ) return tuple(result) @@ -1801,9 +1784,7 @@ class MaterializationContext: materialization_spec: MaterializationSpec | None = None def named_source_filename( - self, - metadata: ImagePayloadMetadata, - extension: str, + self, metadata: ImagePayloadMetadata, extension: str, ) -> str | None: """Name a retained image from the actual rendering purpose and role.""" if self.materialization_spec is None: @@ -1811,19 +1792,12 @@ def named_source_filename( qualifier = self.materialization_spec.filename_qualifier(self.output_plan) if qualifier is None: return None - parser = SourceStemAuthoritySelection.from_processing_context( - self.context - ).required_parser() - identity = FunctionOutputIdentityAuthority.filename_identity_from_metadata( - parser, metadata - ) + parser = SourceStemAuthoritySelection.from_processing_context(self.context).required_parser() + identity = FunctionOutputIdentityAuthority.filename_identity_from_metadata(parser, metadata) if identity is None: - raise ValueError( - "Retained image output has no addressable source filename identity." - ) + raise ValueError("Retained image output has no addressable source filename identity.") identity = self.materialization_spec.filename_identity_for_output( - replace(identity, extension=extension), - self.output_plan, + replace(identity, extension=extension), self.output_plan, ) return FunctionOutputPathAuthority.filename_for_identity(parser, identity) @@ -2508,8 +2482,8 @@ def _image_relative_output_path( if options.relative_path_template is None: return context.paths(options).primary_output_path(options) - metadata = image_payload_metadata_projection(data) - source_identity = metadata.read_value("source_provenance").scalar_source_identity + metadata = image_payload_metadata(data) + source_identity = metadata.source_provenance.scalar_source_identity component_metadata = source_identity.component_metadata def replace_backreference(match: re.Match[str]) -> str: @@ -2609,20 +2583,15 @@ def write_image_file( for sequence_index, item in enumerate(projected_items, start=1): if options.relative_path_template is not None: path = _image_relative_output_path( - item.value, - options, - context, - sequence_index=sequence_index, + item.value, options, context, sequence_index=sequence_index, ) elif preserves_planned_path: path = paths.primary_output_path(options) else: - filename = context.named_source_filename( - item.metadata_projection, options.primary_output_suffix - ) + filename = context.named_source_filename(item.metadata, options.primary_output_suffix) if filename is None: filename = ( - source_stem_authority.required_source_stem(item.metadata_projection) + source_stem_authority.required_source_stem(item.metadata) + options.primary_output_suffix ) path = str(paths.parent / filename) @@ -2637,7 +2606,7 @@ def write_image_file( Output.from_metadata( path=path, content=ImageFileFormat.require_path(path).prepare(item.value), - metadata=item.metadata_projection, + metadata=item.metadata, ) for path, item in outputs ] @@ -2725,9 +2694,7 @@ def items(self) -> tuple[MaterializationInputItem, ...]: @property def identities(self) -> tuple: return tuple( - item.metadata_projection.read_value( - "source_provenance" - ).scalar_source_identity.identity + item.metadata.source_provenance.scalar_source_identity.identity for item in self.items ) @@ -2748,9 +2715,9 @@ def has_single_component_address(self) -> bool: if len(self.items) != 1: return False return ( - self.items[0] - .metadata.read_value("source_provenance") - .scalar_source_identity.component_metadata + self.items[ + 0 + ].metadata.source_provenance.scalar_source_identity.component_metadata is not None ) @@ -2834,7 +2801,7 @@ def targets( ROIMaterializationTarget( archive=ROIMaterializationArchiveIdentity.from_metadata( path=request.paths.primary_output_path(request.options), - metadata=item.metadata_projection, + metadata=item.metadata, ), items=(item,), ), @@ -2858,22 +2825,22 @@ def targets( path_context = ROIMaterializationPathContext( source_stems=tuple( request.source_stem_authority.required_source_stem( - item.metadata_projection, + item.metadata, request.artifact_filename_identity, ) for item in items ), - metadata_items=tuple(item.metadata_projection for item in items), + metadata_items=tuple(item.metadata for item in items), ) return tuple( ROIMaterializationTarget( archive=ROIMaterializationArchiveIdentity.from_metadata( path=request.projected_path( - metadata=item.metadata_projection, + metadata=item.metadata, reference_source_stem=path_context.reference_source_stem, reference_metadata=path_context.reference_metadata, ), - metadata=item.metadata_projection, + metadata=item.metadata, ), items=(item,), ) @@ -2992,7 +2959,7 @@ def metadata_for_target( local_plane_count = 1 for extent in local_plane_shape: local_plane_count *= extent - source_plane_count = metadata.read_value("source_provenance").source_plane_count + source_plane_count = metadata.source_provenance.source_plane_count if source_plane_count not in (0, local_plane_count): raise ValueError( "ROI output source-plane provenance does not match its materialized " @@ -3099,11 +3066,9 @@ def _write_roi_zip( total_roi_count += len(target_rois) if target_rois: roi_paths.append(target.archive.path) - item_metadata = target.items[0].metadata_projection + item_metadata = target.items[0].metadata item_metadata = item_metadata.with_source_provenance( - item_metadata.read_value( - "source_provenance" - ).with_common_scalar_identity_from_planes() + item_metadata.source_provenance.with_common_scalar_identity_from_planes() ) item_metadata = ( ROIMaterializationPlaneMetadataAuthority.metadata_for_target( @@ -3118,9 +3083,9 @@ def _write_roi_zip( SourceImageProvenance( source_path=source_identity.path, source_component_metadata=source_identity.component_metadata, - ).with_missing_from(item_metadata.read_value("source_provenance")) + ).with_missing_from(item_metadata.source_provenance) ) - item_metadata = item_metadata.derive_fields( + item_metadata = item_metadata.replace_fields( source_spatial_domain=source_domain_authority.domain_for_target(target) ) outs.append( @@ -3458,7 +3423,7 @@ def slices( "explicit payload sequence." ) item = materialization_input.items[0] - if item.metadata_projection.read_value("plane_axis") is None: + if item.metadata.plane_axis is None: return (item.data,) data = np.asarray(item.data) if data.ndim < 3 or data.shape[0] <= 0: @@ -3673,7 +3638,7 @@ def metadata_for_item( self, item: MaterializationInputItem, ) -> ImagePayloadMetadata: - metadata = self.metadata_with_artifact_source_identity(item.metadata_projection) + metadata = self.metadata_with_artifact_source_identity(item.metadata) runtime_plane = item.runtime_plane if runtime_plane is None or not self.variable_components: return metadata @@ -3688,7 +3653,7 @@ def metadata_for_item( f"{len(runtime_plane.plane_indices)} coordinate(s)." ) - component_metadata = metadata.source_component_metadata_for_projection() + component_metadata = metadata.source_component_metadata if component_metadata is None: raise ValueError( "TIFF stack output uses variable components " @@ -3704,7 +3669,7 @@ def metadata_for_item( ) return metadata.with_source_provenance( - metadata.read_value("source_provenance").with_source_component_metadata( + metadata.source_provenance.with_source_component_metadata( projected_metadata, ) ) @@ -3720,9 +3685,7 @@ def metadata_with_artifact_source_identity( source_component_metadata=self.artifact_source_identity.component_metadata, ) return metadata.with_source_provenance( - metadata.read_value("source_provenance").with_missing_from( - artifact_provenance - ) + metadata.source_provenance.with_missing_from(artifact_provenance) ) def ordered_axes(self) -> tuple[str, ...]: @@ -3852,9 +3815,7 @@ def indexed_source_plane_axis( def item_component_metadata(self) -> tuple[SourceComponentMetadata, ...]: metadata_values = tuple( - self.effective_item_metadata( - item - ).source_component_metadata_for_projection() + self.effective_item_metadata(item).source_component_metadata for item in self.items ) if any(metadata is None for metadata in metadata_values): @@ -3868,7 +3829,7 @@ def effective_item_metadata( return RuntimePlaneStackAxisMetadataProjection( frozenset(), self.artifact_source_identity, - ).metadata_with_artifact_source_identity(item.metadata_projection) + ).metadata_with_artifact_source_identity(item.metadata) @dataclass(frozen=True, slots=True) @@ -3973,10 +3934,9 @@ def metadata_for_slices( self, request: TiffStackSliceMetadataRequest, ) -> tuple[ImagePayloadMetadata, ...]: - metadata = request.materialization_input.items[0].metadata_projection + metadata = request.materialization_input.items[0].metadata return tuple( - SourcePlaneImageMetadataProjection(metadata, index).capture() - for index in range(request.slice_count) + metadata.for_source_plane(index) for index in range(request.slice_count) ) @@ -4164,18 +4124,14 @@ def emitted_source_identities( ) ) if len(materialization_input.items) == 1: - provenance = materialization_input.items[ - 0 - ].metadata_projection.read_value("source_provenance") + provenance = materialization_input.items[0].metadata.source_provenance if provenance.source_plane_count > 1: return tuple( provenance.for_source_plane(index).scalar_source_identity for index in range(provenance.source_plane_count) ) return tuple( - item.metadata_projection.read_value( - "source_provenance" - ).scalar_source_identity + item.metadata.source_provenance.scalar_source_identity for item in materialization_input.items ) return () diff --git a/tests/unit/test_cellprofiler_metadata_projection_consumers.py b/tests/unit/test_cellprofiler_metadata_projection_consumers.py deleted file mode 100644 index e09a0e9aed..0000000000 --- a/tests/unit/test_cellprofiler_metadata_projection_consumers.py +++ /dev/null @@ -1,414 +0,0 @@ -"""Metadata-owner request laws, independent of public namespace allocation.""" - -from dataclasses import dataclass -from dataclasses import replace -import gc -import weakref -from types import SimpleNamespace - -import numpy as np -import pytest - -from openhcs.core.aligned_image_payload import ImagePayloadExecutionMode -from openhcs.core.artifacts import ( - ArtifactSpec, - ImageArtifactType, - MeasurementsArtifactType, - ArtifactOutputPlan, -) -from openhcs.core.callable_contract import CallableContract, CallableMetadata -from openhcs.core.runtime_image_values import ( - ImageMetadataPayload, - ImageMetadataProjection, - ImagePayloadMetadata, - image_payload_data, - image_payload_mask, - image_payload_metadata_projection, -) -from openhcs.core.source_metadata import SOURCE_VOXEL_SPACING_FIELD -from openhcs.core.runtime_object_labels import ( - ObjectLabelPayload, - ObjectLabelVariantData, -) -from openhcs.core.runtime_plane_projection import ( - RuntimePlaneAxis, - RuntimePlaneAxisValueProjection, -) -from openhcs.core.steps.function_output_identity import FunctionOutputIdentityAuthority -from openhcs.microscopes.source_schema import SourceSchemaFilenameParser -from openhcs.interop.cellprofiler.runtime.artifact_binding import ( - ImageArtifactTypeStrategy, - RuntimeArtifactInputRequest, -) -from openhcs.interop.cellprofiler.runtime.invocation import ( - CellProfilerImageRequest, - CellProfilerSourceIdentityMixin, -) -from openhcs.interop.cellprofiler.runtime.output_record_request import ( - CellProfilerOutputRecordRequest, -) -from openhcs.interop.cellprofiler.runtime.main_flow import cellprofiler_main_flow_output -from openhcs.interop.cellprofiler.module_declarations import CellProfilerModule -from openhcs.interop.cellprofiler.runtime.measurement_recording import ( - CellProfilerMeasurementTableModule, -) -from openhcs.core.measurement_row_materialization import MeasurementSparseColumnarRows - - -def _metadata(**changes): - return ImagePayloadMetadata( - source_path="/input/A01_s001_w1_z001_t001.tif", - source_component_metadata={ - "well": "A01", - "site": "1", - "channel": "1", - "z_index": "1", - "timepoint": "1", - "extension": ".tif", - }, - **changes, - ) - - -def _request(payload): - return CellProfilerImageRequest( - payload=payload, - source_image_name="DNA", - image_count=1, - execution_mode=ImagePayloadExecutionMode.NATURAL, - ) - - -def test_label_projection_preserves_computed_public_namespace_and_freshness(): - labels = ObjectLabelPayload( - variant_data=ObjectLabelVariantData(labels=np.ones((3, 4), dtype=np.int32)), - source_path="/input/labels.tif", - ) - owner = labels.metadata_projection - public = owner.materialize_metadata() - assert type(public) is ImagePayloadMetadata - assert public is owner.materialize_metadata() - assert public is not labels.metadata - public.source_path = "/input/consumer.tif" - assert labels.source_path == "/input/labels.tif" - assert owner.read_value("source_provenance").source_path == "/input/consumer.tif" - labels.source_path = "/input/new-labels.tif" - fresh = labels.metadata_projection - assert fresh.read_value("source_provenance").source_path == "/input/new-labels.tif" - assert owner.read_value("source_provenance").source_path == "/input/consumer.tif" - - -def test_label_projection_retains_no_pixels_or_label_receiver(): - pixels = np.ones((3, 4), dtype=np.int32) - labels = ObjectLabelPayload( - variant_data=ObjectLabelVariantData(labels=pixels), - source_path="/input/labels.tif", - ) - pixel_ref, label_ref = weakref.ref(pixels), weakref.ref(labels) - owner = labels.metadata_projection - del pixels, labels - gc.collect() - assert pixel_ref() is None and label_ref() is None - assert owner.read_value("source_provenance").source_path == "/input/labels.tif" - - -def test_label_projection_preserves_actual_overridden_metadata_error(): - error = ValueError("actual computed metadata error") - - class RefusingLabels(ObjectLabelPayload): - @property - def metadata(self): - raise error - - labels = RefusingLabels( - variant_data=ObjectLabelVariantData(labels=np.zeros((2, 3), dtype=np.int32)) - ) - with pytest.raises(ValueError) as caught: - image_payload_metadata_projection(labels) - assert caught.value is error - - -def test_artifact_alias_projection_preserves_pixels_mask_and_independent_metadata( - monkeypatch, -): - pixels = np.arange(12, dtype=np.float32).reshape(3, 4) - mask = np.ones_like(pixels, dtype=bool) - owner = _metadata(source_image_names=("Original",)).capture() - payload = owner.payload_with(pixels, mask) - spec = ArtifactSpec.input("DNA", ImageArtifactType) - - def no_public_namespace(self): - raise AssertionError("An internal input binding forced the public namespace.") - - monkeypatch.setattr( - ImageMetadataProjection, "materialize_metadata", no_public_namespace - ) - derived = ImageArtifactTypeStrategy().raw_runtime_input_value( - RuntimeArtifactInputRequest(spec=spec, value=payload) - ) - assert image_payload_data(derived) is pixels - assert image_payload_mask(derived) is mask - derived_owner = image_payload_metadata_projection(derived) - assert derived_owner is not owner - assert ImageArtifactTypeStrategy().source_image_name_from_value(derived) is None - assert derived_owner.read_value( - "source_provenance" - ).represented_source_image_names == ( - owner.read_value("source_provenance") - .with_derived_source_image_names(("DNA",)) - .represented_source_image_names - ) - assert owner.read_value("source_provenance").source_image_names == ("Original",) - - -def test_shared_source_payload_query_preserves_original_value_identity(monkeypatch): - owner = _metadata().capture() - first = ImageMetadataPayload.from_projection(np.zeros((3, 4)), owner) - second = ImageMetadataPayload.from_projection(np.ones((3, 4)), owner) - - def no_public_namespace(self): - raise AssertionError("A source query forced the public namespace.") - - monkeypatch.setattr( - ImageMetadataProjection, "materialize_metadata", no_public_namespace - ) - assert ( - CellProfilerSourceIdentityMixin.shared_source_payload( - (_request(first), _request(second)) - ) - is first - ) - assert CellProfilerSourceIdentityMixin.shared_source_payload(()) is None - - -def test_opaque_main_flow_returns_exact_output_without_namespace(monkeypatch): - payload = ImageMetadataPayload.from_projection( - np.ones((3, 4)), _metadata().capture() - ) - - def no_public_namespace(self): - raise AssertionError("An opaque main-flow read forced public metadata.") - - monkeypatch.setattr( - ImageMetadataProjection, "materialize_metadata", no_public_namespace - ) - assert cellprofiler_main_flow_output(payload, payload, None) is payload - - -def test_main_flow_preserves_output_axis_error_before_input_access(monkeypatch): - output = ImageMetadataPayload.from_projection( - np.ones((2, 3, 4)), - _metadata(plane_axis=RuntimePlaneAxis.RUNTIME_SLICE).capture(), - ) - projection = RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.SOURCE_BINDING, axis_size=2 - ) - - def no_public_namespace(self): - raise AssertionError("The malformed output forced public metadata.") - - monkeypatch.setattr( - ImageMetadataProjection, "materialize_metadata", no_public_namespace - ) - with pytest.raises(ValueError, match="output axis conflicts"): - cellprofiler_main_flow_output(object(), output, projection) - - -@dataclass -class ComponentAliasMetadata(ImagePayloadMetadata): - @property - def source_component_metadata(self): - value = super().source_component_metadata - return {**(value or {}), SOURCE_VOXEL_SPACING_FIELD: "1.0,2.5,2.5"} - - -def test_artifact_binding_keeps_real_subtype_component_alias_calibration(): - metadata = ComponentAliasMetadata(source_path="/input/calibrated.tif") - original_spacing = metadata.source_voxel_spacing - payload = metadata.capture().payload_with(np.ones((3, 4), dtype=np.float32)) - request = RuntimeArtifactInputRequest( - spec=ArtifactSpec.input("DNA", ImageArtifactType), value=payload - ) - derived = ImageArtifactTypeStrategy().raw_runtime_input_value(request) - owner = image_payload_metadata_projection(derived) - public = owner.materialize_metadata() - assert type(public) is ComponentAliasMetadata - assert public.source_component_metadata[SOURCE_VOXEL_SPACING_FIELD] == "1.0,2.5,2.5" - assert public.source_voxel_spacing == original_spacing - assert public.source_voxel_spacing.has_values - - -class DifferentComponentAliasMetadata(ImagePayloadMetadata): - @property - def source_component_metadata(self): - return {**(super().source_component_metadata or {}), "channel": "7"} - - -class DifferentPathAliasMetadata(ImagePayloadMetadata): - @property - def source_path(self): - return "/input/A01_s001_w7_z001_t001.tif" - - -def test_identity_preserves_determining_component_alias_value(): - metadata = DifferentComponentAliasMetadata( - source_path="/input/A01_s001_w1_z001_t001.tif", - source_component_metadata={ - "well": "A01", - "site": "1", - "channel": "1", - "z_index": "1", - "timepoint": "1", - }, - ) - assert metadata.source_provenance.source_component_metadata["channel"] == "1" - parser = SourceSchemaFilenameParser() - expected = FunctionOutputIdentityAuthority.identity_from_metadata(parser, metadata) - assert expected.component_values["channel"] == 7 - actual = FunctionOutputIdentityAuthority.identity_from_metadata( - parser, metadata.capture() - ) - assert actual == expected - - -def test_identity_preserves_determining_path_alias_value(): - metadata = DifferentPathAliasMetadata( - source_path="/input/A01_s001_w1_z001_t001.tif", - source_component_metadata={ - "well": "A01", - "site": "1", - "z_index": "1", - "timepoint": "1", - }, - ) - assert metadata.source_provenance.source_path == "/input/A01_s001_w1_z001_t001.tif" - parser = SourceSchemaFilenameParser() - expected = FunctionOutputIdentityAuthority.identity_from_metadata(parser, metadata) - assert expected.component_values["channel"] == 7 - actual = FunctionOutputIdentityAuthority.identity_from_metadata( - parser, metadata.capture() - ) - assert actual == expected - - -def test_public_composed_source_metadata_keeps_actual_namespace_contract(): - payload = _metadata().capture().payload_with(np.ones((3, 4), dtype=np.float32)) - source = _request(payload) - owner = CellProfilerSourceIdentityMixin.composed_source_metadata_projection( - (source,) - ) - public = CellProfilerSourceIdentityMixin.composed_source_metadata((source,)) - assert type(public) is ImagePayloadMetadata - assert public == owner.materialize_metadata() - assert CellProfilerSourceIdentityMixin.composed_source_metadata(()) is None - - -def _record_request(): - output = ArtifactSpec.output("Measurements", MeasurementsArtifactType) - payload = _metadata().capture().payload_with(np.ones((3, 4), dtype=np.float32)) - return CellProfilerOutputRecordRequest( - callable_contract=CallableContract( - func=lambda image: image, - function_name="metadata_override", - module_name=__name__, - metadata=CallableMetadata(artifact_outputs=(output,)), - ), - active_input_edges=(), - adapter=SimpleNamespace(), - spec=output, - output_plan=ArtifactOutputPlan( - name=output.name, - path="/memory/measurements.pkl", - artifact_type=output.artifact_type, - ), - output_value=MeasurementSparseColumnarRows.from_rows((), fields=()), - source=_request(payload), - call_kwargs={}, - current_image=payload, - ) - - -def test_measurement_table_preserves_external_public_override_dispatch(): - rows = MeasurementSparseColumnarRows.from_rows((), fields=()) - metadata = _metadata() - - class ExternalModule(CellProfilerModule): - @classmethod - def measurement_record_rows(cls, request): - return rows - - @classmethod - def measurement_record_object_name(cls, request, rows): - return None - - @classmethod - def measurement_record_source_image_name(cls, request, rows): - return "DNA" - - @classmethod - def measurement_record_source_metadata(cls, request, rows): - metadata.source_path = "/input/external-override.tif" - return metadata - - @classmethod - def measurement_record_source_metadata_projection(cls, request, rows): - raise AssertionError("Public module override was bypassed.") - - table = ExternalModule.measurement_table(_record_request()) - assert table.source_provenance.source_path == "/input/external-override.tif" - - -def test_measurement_table_preserves_external_public_error_identity(): - error = ValueError("external public metadata failure") - rows = MeasurementSparseColumnarRows.from_rows((), fields=()) - - class ExternalModule(CellProfilerModule): - @classmethod - def measurement_record_rows(cls, request): - return rows - - @classmethod - def measurement_record_object_name(cls, request, rows): - return None - - @classmethod - def measurement_record_source_image_name(cls, request, rows): - return "DNA" - - @classmethod - def measurement_record_source_metadata(cls, request, rows): - raise error - - with pytest.raises(ValueError) as caught: - ExternalModule.measurement_table(_record_request()) - assert caught.value is error - - -def test_measurement_source_preserves_external_public_composition_override(): - error = ValueError("external source composition error") - - class ExternalSource(CellProfilerImageRequest): - @classmethod - def composed_source_metadata(cls, sources, *, mode=None): - raise error - - @classmethod - def composed_source_metadata_projection(cls, sources, *, mode=None): - raise AssertionError("Public source composition override was bypassed.") - - request = _record_request() - request = replace( - request, - source=ExternalSource( - payload=request.current_image, - source_image_name="DNA", - image_count=1, - execution_mode=ImagePayloadExecutionMode.NATURAL, - ), - ) - with pytest.raises(ValueError) as caught: - CellProfilerMeasurementTableModule.measurement_record_source_metadata( - request, - request.output_value, - ) - assert caught.value is error diff --git a/tests/unit/test_function_output_metadata_projection.py b/tests/unit/test_function_output_metadata_projection.py deleted file mode 100644 index 3d18975bf4..0000000000 --- a/tests/unit/test_function_output_metadata_projection.py +++ /dev/null @@ -1,335 +0,0 @@ -"""Public identity and manifest laws for the shared metadata-only capability.""" - -from dataclasses import FrozenInstanceError, dataclass, fields, replace -import copyreg -import gc -import pickle -from pathlib import Path -from types import MappingProxyType -import weakref - -import numpy as np -import pytest - -from openhcs.constants.constants import VariableComponents -from openhcs.core.aligned_image_payload import AlignedImageSliceContext -from openhcs.core.artifacts import ImageArtifactType -from openhcs.core.runtime_image_values import ( - ImageMetadataCarrier, - ImageMetadataPayload, - ImageMetadataProjection, - ImagePayloadMetadata, - ImagePayloadMetadataCarrier, - image_payload_data, - image_payload_metadata_projection, -) -from openhcs.core.source_image_provenance import ( - SourceImageProvenance, - SourceImageProvenancePlanes, -) -from openhcs.core.steps.function_output_identity import ( - FunctionOutputIdentity, - FunctionOutputIdentityAuthority, - FunctionOutputIdentityCache, - FunctionOutputMetadataIdentityCacheKey, - FunctionOutputPathRequest, -) -from openhcs.core.steps.function_output_manifest import ProducedOutputSemantics -from openhcs.microscopes.source_schema import SourceSchemaFilenameParser -from openhcs.serialization.json import to_jsonable - -from tests.unit.test_function_outputs import function_step_plan - - -@dataclass -class CalibratedMetadata(ImagePayloadMetadata): - calibration: float = 1.25 - - def __post_init__(self, *values): - super().__post_init__(*values) - self.constructor_note = "actual subtype namespace" - - -def _metadata(metadata_type=ImagePayloadMetadata): - return metadata_type( - source_path="/input/A01_s001_w1_z001_t001.tif", - source_component_metadata={ - "well": "A01", - "site": "1", - "channel": "1", - "z_index": "1", - "timepoint": "1", - "extension": ".tif", - }, - source_dtype="float32", - ) - - -def _record(owner): - return ProducedOutputSemantics.from_output( - function_step_plan("Produce"), - "/tmp/output/A01_s001_w1_z001_t001.tif", - FunctionOutputIdentity( - component_values={ - "well": "A01", - "site": 1, - "channel": 1, - "z_index": 1, - "timepoint": 1, - }, - extension=".tif", - source="actual public source declaration", - ), - output_context=AlignedImageSliceContext.main_flow( - output_key="DNA", - artifact_kind=ImageArtifactType.value, - ), - image_metadata=owner, - ) - - -def test_identity_reads_owner_without_public_namespace_and_preserves_cache_key( - monkeypatch, -): - parser = SourceSchemaFilenameParser() - metadata = _metadata() - expected = FunctionOutputIdentityAuthority.identity_from_metadata(parser, metadata) - owner = metadata.capture() - payload = ImageMetadataPayload.from_projection( - np.zeros((3, 4), dtype=np.float32), owner - ) - cache = FunctionOutputIdentityCache() - request = FunctionOutputPathRequest( - parser, Path("/tmp/output"), payload, None, identity_cache=cache - ) - - def unexpected_public_namespace(self): - raise AssertionError("Identity query forced its public metadata namespace.") - - monkeypatch.setattr( - ImageMetadataProjection, "materialize_metadata", unexpected_public_namespace - ) - actual = FunctionOutputIdentityAuthority.identity(request) - assert actual == expected - assert FunctionOutputIdentityAuthority.identity(request) is actual - assert list(cache.metadata_identities) == [ - FunctionOutputMetadataIdentityCacheKey( - parser_id=id(parser), - source_provenance_identity=owner.read_value( - "source_provenance" - ).equality_identity, - fallback_identity_path=None, - identity_components=(), - input_aligned_output=False, - ) - ] - - -def test_collapsed_contributors_keep_distinct_filename_and_semantic_components(): - metadata = ImagePayloadMetadata( - source_provenance=SourceImageProvenance( - source_component_metadata={ - "well": "A01", - "channel": "1", - "z_index": "1", - "timepoint": "1", - "extension": ".tif", - }, - source_image_provenance_planes=SourceImageProvenancePlanes.from_contributor_components( - paths=( - "/input/A01_s001_w1_z001_t001.tif", - "/input/A01_s002_w1_z001_t001.tif", - ), - component_metadata=({"site": "1"}, {"site": "2"}), - ), - ) - ) - parser = SourceSchemaFilenameParser() - kwargs = {"variable_components": (VariableComponents.SITE,)} - public = FunctionOutputIdentityAuthority.identity_from_metadata( - parser, metadata, **kwargs - ) - projected = FunctionOutputIdentityAuthority.identity_from_metadata( - parser, metadata.capture(), **kwargs - ) - assert projected == public - assert "site" not in projected.component_values - assert projected.filename_values["site"] == 1 - - -def test_missing_extension_error_is_preserved_for_owner(): - metadata = ImagePayloadMetadata( - source_component_metadata={ - "well": "A01", - "site": "1", - "channel": "1", - "z_index": "1", - "timepoint": "1", - } - ) - payload = ImageMetadataPayload.from_projection(np.zeros((3, 4)), metadata.capture()) - request = FunctionOutputPathRequest( - SourceSchemaFilenameParser(), Path("/tmp/output"), payload, None - ) - with pytest.raises(ValueError, match="no source path or extension"): - FunctionOutputIdentityAuthority.identity(request) - - -def test_manifest_preserves_exact_owner_and_pass_through_without_namespace(monkeypatch): - owner = _metadata().capture() - payload = ImageMetadataPayload.from_projection(np.zeros((3, 4)), owner) - record = _record(owner) - assert record.metadata_projection is image_payload_metadata_projection(payload) - - def unexpected_public_namespace(self): - raise AssertionError("Manifest lifecycle forced public metadata.") - - monkeypatch.setattr( - ImageMetadataProjection, "materialize_metadata", unexpected_public_namespace - ) - passed = record.passed_through(function_step_plan("Next", pipeline_position=4)) - assert passed.metadata_projection is owner - assert passed.filename_address == record.filename_address - assert passed.component_values == record.component_values - assert ( - passed.producer_identity.step_scope_id != record.producer_identity.step_scope_id - ) - - -def test_manifest_has_metadata_capability_without_image_role_or_new_fields(): - record = _record(_metadata().capture()) - assert isinstance(record, ImageMetadataCarrier) - assert not isinstance(record, ImagePayloadMetadataCarrier) - assert not hasattr(record, "__dict__") - assert not hasattr(record, "image_data") - assert image_payload_data(record) is record - names = tuple(member.name for member in fields(record)) - assert names == ( - "component_values", - "extension", - "source", - "filename_component_values", - "filename_qualifier", - "producer_identity", - "output_path", - "relative_output_path", - "image_metadata", - "main_flow_plane_axis", - ) - wire = to_jsonable(record) - assert tuple(wire) == names - assert "_metadata_projection" not in wire - assert record.image_metadata is record.image_metadata - - -def test_image_carrier_still_requires_image_data(): - class MissingImageData(ImagePayloadMetadataCarrier): - pass - - with pytest.raises(TypeError, match="image_data"): - MissingImageData() - payload = ImageMetadataPayload.from_projection( - np.zeros((3, 4)), _metadata().capture() - ) - assert isinstance(payload, ImageMetadataCarrier) - assert isinstance(payload, ImagePayloadMetadataCarrier) - assert image_payload_data(payload) is payload.data - - -def test_public_mutation_and_subtype_share_the_same_payload_namespace(): - owner = _metadata(CalibratedMetadata).capture() - payload = ImageMetadataPayload.from_projection(np.zeros((3, 4)), owner) - record = _record(owner) - public = record.image_metadata - assert type(public) is CalibratedMetadata - assert public.constructor_note == "actual subtype namespace" - assert public is payload.metadata - public.calibration = 2.5 - public.source_path = "/input/changed.tif" - assert owner.read_value("calibration") == 2.5 - assert owner.read_value("source_provenance").source_path == "/input/changed.tif" - with pytest.raises(FrozenInstanceError): - record.image_metadata = _metadata() - - -def test_public_source_mutation_keeps_original_cache_epoch_law(): - metadata = _metadata() - owner = metadata.capture() - record = _record(owner) - parser = SourceSchemaFilenameParser() - cache = FunctionOutputIdentityCache() - first = FunctionOutputIdentityAuthority.identity_from_metadata_with_cache( - parser, owner, identity_cache=cache - ) - original_key = next(iter(cache.metadata_identities)) - record.image_metadata.source_path = "/input/changed.tif" - second = FunctionOutputIdentityAuthority.identity_from_metadata_with_cache( - parser, owner, identity_cache=cache - ) - assert second == first - assert len(cache.metadata_identities) == 2 - assert ( - original_key.source_provenance_identity - != owner.read_value("source_provenance").equality_identity - ) - - -def test_manifest_never_retains_pixels_or_payload(): - pixels = np.ones((2, 3, 4), dtype=np.float32) - owner = _metadata().capture() - payload = ImageMetadataPayload.from_projection(pixels, owner) - pixel_ref = weakref.ref(pixels) - payload_ref = weakref.ref(payload) - record = _record(image_payload_metadata_projection(payload)) - del pixels, payload - gc.collect() - assert pixel_ref() is None - assert payload_ref() is None - assert record.metadata_projection is owner - - -def test_replace_and_pickle_keep_original_public_dataclass_semantics(monkeypatch): - record = _record(_metadata(CalibratedMetadata).capture()) - public = record.image_metadata - copied = replace(record) - assert copied.image_metadata is public - # Other integrations can register a mappingproxy reducer globally. Exercise - # the standard-pickle boundary independently of test collection order. - monkeypatch.delitem(copyreg.dispatch_table, MappingProxyType, raising=False) - # Standard pickle already rejects the public metadata mappingproxy. - with pytest.raises(TypeError, match="mappingproxy"): - pickle.dumps(record) - absent = _record(None) - restored = pickle.loads(pickle.dumps(absent)) - assert restored == absent - assert restored.metadata_projection is None - assert restored.image_metadata is None - assert not hasattr(restored, "__dict__") - - -def test_contextualization_uses_record_owner_preserving_borrowed_pixels(): - owner = _metadata().capture() - pixels = np.ones((3, 4), dtype=np.float32) - record = _record(owner) - contextualized = record.contextualize_image_payload(pixels) - assert image_payload_data(contextualized) is pixels - projected = image_payload_metadata_projection(contextualized) - assert ( - projected.read_value("source_provenance").source_component_metadata["site"] - == "1" - ) - assert ( - owner.read_value("source_provenance").source_component_metadata["site"] == "1" - ) - - -def test_absent_manifest_metadata_stays_absent(): - record = _record(None) - assert record.metadata_projection is None - assert record.image_metadata is None - assert ( - record.passed_through( - function_step_plan("Next", pipeline_position=4) - ).image_metadata - is None - ) diff --git a/tests/unit/test_image_metadata_owner_consumers.py b/tests/unit/test_image_metadata_owner_consumers.py deleted file mode 100644 index 35b0a35176..0000000000 --- a/tests/unit/test_image_metadata_owner_consumers.py +++ /dev/null @@ -1,151 +0,0 @@ -"""Real loader/workspace/format consumers retain one metadata owner lifecycle.""" - -from dataclasses import dataclass, fields - -import numpy as np -import pytest - -from openhcs.core.artifacts import ImageArtifactType -from openhcs.core.image_file_serialization import ( - ImageFileFormat, - ImageFileSourceMetadata, -) -from openhcs.core.runtime_image_loading import ImagePayloadSourceMetadataContext -from openhcs.core.runtime_image_values import ( - ImageMetadataProjection, - ImagePayloadMetadata, - ImageUnitIntervalIntensityMetadata, - image_payload_data, - image_payload_metadata, - image_payload_metadata_projection, -) -from openhcs.core.source_image_provenance import ( - SourceImageIdentity, - SourceImageProvenance, -) -from openhcs.core.source_metadata import DurableSourceMetadata, SourceVoxelSpacing -from openhcs.core.source_spatial_domain import SourceSpatialDomain -from openhcs.core.source_workspace_projection import ( - VirtualWorkspaceImagePayloadProjection, -) -from openhcs.serialization.json import to_jsonable - - -@dataclass -class CalibratedMetadata(ImagePayloadMetadata): - calibration: float = 3.0 - - -def test_loader_owner_preserves_virtual_identity_and_public_mutable_contract(): - image = np.arange(12, dtype=np.uint16).reshape(3, 4) - context = ImagePayloadSourceMetadataContext( - SourceImageIdentity( - "/virtual/A01_s001_w1_z001_t001.tif", - DurableSourceMetadata.from_mapping({"well": "A01", "channel": "1"}), - ) - ) - owner = context.metadata_projection(image) - assert isinstance(owner, ImageMetadataProjection) - actual = owner.materialize_metadata() - assert type(actual) is ImagePayloadMetadata - assert actual.source_path == context.source_path - assert actual.source_dtype == "uint16" - assert dict(actual.source_component_metadata) == {"well": "A01", "channel": "1"} - assert actual.source_spatial_shape_yx == image.shape - assert to_jsonable(context.metadata(image)) == to_jsonable(actual) - actual.source_dtype = "float32" - assert owner.read_value("source_dtype") == "float32" - - -def test_workspace_owner_preserves_declared_omissions_alias_and_mutation(): - loaded = ImagePayloadMetadata( - source_path="/storage/raw.tif", - source_spatial_domain=SourceSpatialDomain((1, 2), (12, 14)), - source_voxel_spacing=SourceVoxelSpacing((0.5, 0.75)), - source_dtype="float32", - ) - persisted = CalibratedMetadata( - source_provenance=SourceImageProvenance( - source_component_metadata={"well": "A01"}, - source_image_names=("declared",), - ), - ) - # These are live public facts despite the provenance's stored birth identity. - persisted.source_provenance.source_image_names = ("changed-before-apply",) - projection = VirtualWorkspaceImagePayloadProjection(persisted_metadata=persisted) - owner = projection.metadata_projection(loaded) - expected = projection.metadata(loaded) - actual = owner.materialize_metadata() - assert type(actual) is CalibratedMetadata - assert actual.calibration == 3.0 - assert actual.source_path is None - assert actual.source_image_names == ("changed-before-apply",) - assert to_jsonable(actual) == to_jsonable(expected) - actual.source_image_names = ("one-output-only",) - assert persisted.source_image_names == ("changed-before-apply",) - aliased = VirtualWorkspaceImagePayloadProjection( - persisted_metadata=persisted, source_alias="DNA" - ).apply(loaded.payload_with(np.ones((3, 4), dtype=np.float32))) - assert image_payload_metadata(aliased).source_image_names == ("DNA",) - assert image_payload_data(aliased).shape == (3, 4) - - -@pytest.mark.parametrize("values_preserved", (False, True)) -def test_format_owner_retains_subtype_schema_and_native_value_facts(values_preserved): - metadata = CalibratedMetadata( - source_path="/input/image.tif", - source_dtype="float32", - intensity_scale=1.0, - unit_interval_intensity=ImageUnitIntervalIntensityMetadata(255), - physical_border_edges_yx=(True, False, False, True), - mask_defines_border=True, - ) - header = ImageFileSourceMetadata(np.dtype("uint8"), 255) - owner = header.project_image_metadata_projection( - metadata, values_preserved=values_preserved - ) - actual = owner.materialize_metadata() - public = header.project_image_metadata(metadata, values_preserved=values_preserved) - assert type(actual) is type(public) is CalibratedMetadata - assert {member.name for member in fields(actual)} == set(to_jsonable(actual)) - assert to_jsonable(actual) == to_jsonable(public) - assert actual.source_dtype == "uint8" - assert actual.intensity_scale == 255 - assert actual.source_path == metadata.source_path - if not values_preserved: - assert actual.unit_interval_intensity.scale is None - assert actual.physical_border_edges_yx is None - assert actual.mask_defines_border is None - assert metadata.source_dtype == "float32" - - -def test_actual_saved_tiff_owner_and_public_contract_share_native_facts(tmp_path): - data = np.arange(12, dtype=np.uint16).reshape(3, 4) - payload = ImagePayloadMetadata( - source_path="/source/virtual.tif", source_dtype="uint16" - ).payload_with(data) - path = tmp_path / "output.tif" - image_format = ImageFileFormat.require_path(path) - image_format.write(path, payload) - owner = image_format.persisted_metadata_projection(path, payload) - assert to_jsonable(owner.materialize_metadata()) == to_jsonable( - image_format.persisted_metadata(path, payload) - ) - np.testing.assert_array_equal(image_format.read(path), data) - - -def test_named_image_artifact_projects_owned_alias_without_changing_pixels(): - data = np.ones((3, 4), dtype=np.float32) - original = ImagePayloadMetadata( - source_path="/input/image.tif", source_image_names=("DNA",) - ).payload_with(data) - named = ImageArtifactType.normalize_runtime_payload("DerivedDNA", original) - owner = image_payload_metadata_projection(named) - assert owner.read_value("source_provenance").source_image_names == ("DerivedDNA",) - assert image_payload_data(named) is data - assert image_payload_metadata(original).source_image_names == ("DNA",) - public = image_payload_metadata(named) - assert public is image_payload_metadata(named) - public.source_path = "/changed/output.tif" - assert owner.read_value("source_provenance").source_path == "/changed/output.tif" - assert image_payload_metadata(original).source_path == "/input/image.tif" diff --git a/tests/unit/test_image_metadata_projection_owner.py b/tests/unit/test_image_metadata_projection_owner.py deleted file mode 100644 index 20ec974db9..0000000000 --- a/tests/unit/test_image_metadata_projection_owner.py +++ /dev/null @@ -1,341 +0,0 @@ -"""Semantic boundaries for the shared metadata projection owner.""" - -import gc -import pickle -import weakref -from dataclasses import Field, FrozenInstanceError, dataclass, field, fields - -import cloudpickle -import numpy as np -import pytest - -from openhcs.core.runtime_image_values import ( - ImageMetadataCarrier, - ImagePayloadMetadataCarrier, - ImageMetadataPayload, - ImageMetadataProjection, - ImagePayloadMetadata, - LeadingPlaneAxisMetadataProjection, - LeadingSourcePlaneMetadataProjection, - MaskedImagePayload, - SourcePlaneImageMetadataProjection, - image_payload_data, - image_payload_metadata, - image_payload_metadata_projection, - image_payload_slice_context, -) -from openhcs.core.runtime_plane_projection import RuntimePlaneAxis -from openhcs.core.runtime_slice_projection import RuntimeSliceProjection -from openhcs.core.source_image_provenance import ( - RuntimeSourceImageProvenancePlane, - SourceImageIdentity, - SourceImageProvenancePlanes, -) -from openhcs.core.source_metadata import DurableSourceMetadata -from openhcs.core.source_spatial_domain import SourceSpatialDomain -from openhcs.serialization.json import to_jsonable - - -class MutableTag(int): - def __repr__(self): - return self.tag - - def __str__(self): - return self.tag - - -SUBTYPE_EVENTS = [] - - -@dataclass -class CalibratedMetadata(ImagePayloadMetadata): - calibration: float = 2.5 - declared_layout: object = field(init=False) - - def __post_init__(self, *values): - super().__post_init__(*values) - SUBTYPE_EVENTS.append(self) - self.declared_layout = self.plane_axis - self.constructor_attribute = "preserve actual constructor namespace" - - -class ObservedMetadata(ImagePayloadMetadata): - def singleton_plane_projection(self): - self.observed = True - return super().singleton_plane_projection() - - -def stack_metadata(metadata_type=ImagePayloadMetadata): - return metadata_type( - plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, - source_dtype="float32", - source_path="/input/original.tif", - source_image_names=("DNA",), - ) - - -def test_slice_owner_exposes_stable_mutable_metadata_without_changing_pixels(): - data = np.arange(24, dtype=np.float32).reshape(2, 3, 4) - original = stack_metadata().payload_with(data) - sliced = image_payload_slice_context(original, data[1], 1) - owner = image_payload_metadata_projection(sliced) - assert isinstance(owner, ImageMetadataProjection) - assert owner.read_value("plane_axis") is None - assert np.shares_memory(image_payload_data(sliced), data) - public = image_payload_metadata(sliced) - assert public is image_payload_metadata(sliced) - assert owner.materialize_metadata() is public - public.source_dtype = "uint16" - public.source_path = "/input/changed.tif" - assert owner.read_value("source_dtype") == "uint16" - assert owner.read_value("source_provenance").source_path == "/input/changed.tif" - assert image_payload_metadata(original).source_path == "/input/original.tif" - derived = owner.derive_fields(source_dtype="float64") - assert derived.read_value("source_dtype") == "float64" - assert public.source_dtype == "uint16" - - -def test_captured_siblings_keep_distinct_namespaces_and_source_epochs(): - original = stack_metadata() - first = LeadingSourcePlaneMetadataProjection(original, 0).capture() - second = LeadingSourcePlaneMetadataProjection(original, 1).capture() - original.source_path = "/input/later.tif" - assert first.read_value("source_provenance").source_path == "/input/original.tif" - one = first.materialize_metadata() - two = second.materialize_metadata() - assert one is not two - assert one.source_provenance is not two.source_provenance - one.source_path = "/input/one.tif" - assert two.source_path == "/input/original.tif" - - -@pytest.mark.parametrize("codec", (pickle, cloudpickle)) -def test_private_transport_preserves_stored_birth_after_opaque_scalar_changes(codec): - tag = MutableTag(1) - tag.tag = "before" - metadata = ImagePayloadMetadata( - source_component_metadata=DurableSourceMetadata.from_mapping({"channel": tag}) - ) - owner = SourcePlaneImageMetadataProjection(metadata, 0).capture() - birth = owner.read_value("source_provenance").equality_identity - tag.tag = "after" - restored = codec.loads(codec.dumps(owner)) - assert restored.read_value("source_provenance").equality_identity == birth - public = restored.materialize_metadata() - assert public.source_provenance.equality_identity == birth - assert str(public.source_component_metadata["channel"]) == "after" - reducer_state = owner.__reduce__()[1] - assert all(not isinstance(value, Field) for value in reducer_state) - - -@pytest.mark.parametrize("public_api", (False, True)) -def test_constructor_extensions_preserve_subclass_fields_and_one_public_lifecycle( - public_api, -): - metadata = stack_metadata(CalibratedMetadata) - before = len(SUBTYPE_EVENTS) - rule = LeadingSourcePlaneMetadataProjection(metadata, 0) - result = rule.project() if public_api else rule.capture().materialize_metadata() - assert type(result) is CalibratedMetadata - assert result.calibration == 2.5 - # This init=False fact is born in the scalar-source constructor, before the - # independent leading-axis capability clears the axis. Do not recompute it. - assert result.declared_layout is RuntimePlaneAxis.RUNTIME_SLICE - assert result.plane_axis is None - assert result.constructor_attribute == "preserve actual constructor namespace" - assert len(SUBTYPE_EVENTS) == before + 1 - assert set(to_jsonable(result)) == {member.name for member in fields(type(result))} - - -def test_public_nominal_hook_survives_capture_and_realization(): - original = stack_metadata(ObservedMetadata) - owner = SourcePlaneImageMetadataProjection(original, 0).capture() - actual = owner.materialize_metadata() - assert type(actual) is ObservedMetadata - actual.singleton_plane_projection() - assert actual.observed - - -@pytest.mark.parametrize("payload_type", (ImageMetadataPayload, MaskedImagePayload)) -def test_public_payload_schema_and_frozen_fields_remain(payload_type): - data = np.ones((3, 4), dtype=np.float32) - owner = ImagePayloadMetadata(source_dtype="float32").capture() - payload = ( - payload_type.from_projection(data, owner) - if payload_type is ImageMetadataPayload - else payload_type.from_projection(data, np.ones(data.shape, dtype=bool), owner) - ) - assert image_payload_metadata_projection(payload) is owner - assert isinstance(payload.metadata, ImagePayloadMetadata) - assert "metadata" in {member.name for member in fields(payload)} - assert not payload_type.__abstractmethods__ - with pytest.raises(FrozenInstanceError): - payload.metadata = ImagePayloadMetadata(source_dtype="uint16") - replacement = payload.with_data(data.copy()) - assert image_payload_metadata_projection(replacement) is owner - - -def test_nominal_source_and_axis_guards_preserve_first_error_order(monkeypatch): - metadata = stack_metadata() - metadata.source_spatial_domain = SourceSpatialDomain(origin_yx=(1,)) - calls = [] - - def fail_contributors(self): - calls.append(self) - raise RuntimeError("contributor first") - - monkeypatch.setattr( - type(metadata.source_provenance), - "with_runtime_planes_as_contributors", - fail_contributors, - ) - with pytest.raises(ValueError, match="spatial_origin_yx"): - LeadingSourcePlaneMetadataProjection(metadata, 0).capture() - assert calls == [] - with pytest.raises(RuntimeError, match="contributor first"): - LeadingPlaneAxisMetadataProjection(metadata).capture() - assert calls == [metadata.source_provenance] - - -def test_metadata_owner_does_not_keep_pixels_or_original_stack_alive(): - data = np.ones((2, 3, 4), dtype=np.float32) - original = stack_metadata().payload_with(data) - leaf = image_payload_slice_context(original, data[0], 0) - owner = image_payload_metadata_projection(leaf) - pixels = weakref.ref(data) - del leaf, original, data - gc.collect() - assert pixels() is None - assert owner.read_value("source_dtype") == "float32" - - -def test_runtime_slice_strategy_keeps_existing_payload_family(): - data = np.arange(24, dtype=np.float32).reshape(2, 3, 4) - payload = stack_metadata().payload_with(data) - assert RuntimeSliceProjection.slice_count_from_values((payload,)) == 2 - - -class MetadataOnlyCarrier(ImageMetadataCarrier): - __slots__ = ("_metadata_projection",) - - def __init__(self, owner): - self.metadata = owner - - -def test_metadata_capability_does_not_make_a_carrier_an_image(): - carrier = MetadataOnlyCarrier(ImagePayloadMetadata().capture()) - assert not hasattr(carrier, "__dict__") - assert isinstance(carrier, ImageMetadataCarrier) - assert not isinstance(carrier, ImagePayloadMetadataCarrier) - assert image_payload_data(carrier) is carrier - assert image_payload_metadata_projection(carrier).has_values is False - assert carrier.metadata is carrier.metadata - empty = MetadataOnlyCarrier(None) - assert empty.metadata is None - assert empty.metadata_projection is None - with pytest.raises(TypeError): - ImagePayloadMetadataCarrier() - - -@pytest.mark.parametrize( - "projection_type", - ( - SourcePlaneImageMetadataProjection, - LeadingSourcePlaneMetadataProjection, - ), -) -def test_projection_observes_public_provenance_reassignments_before_capture( - projection_type, -): - metadata = stack_metadata() - provenance = metadata.source_provenance - old_birth = provenance.equality_identity - provenance.source_image_names = ("changed-name",) - provenance.source_identity = SourceImageIdentity("/input/changed-scalar.tif") - provenance.source_image_provenance_planes = SourceImageProvenancePlanes( - ( - RuntimeSourceImageProvenancePlane( - SourceImageIdentity("/input/changed-plane.tif"), - source_image_name="changed-plane-name", - ), - ) - ) - # Mutable live fields and the stored birth are distinct existing contracts. - assert provenance.equality_identity == old_birth - expected = projection_type(metadata, 0).project() - first = projection_type(metadata, 0).capture().materialize_metadata() - second = projection_type(metadata, 0).capture().materialize_metadata() - assert ( - first.source_provenance.equality_identity - == expected.source_provenance.equality_identity - ) - assert first.source_path == "/input/changed-plane.tif" - assert first.source_image_names == expected.source_image_names - assert ( - first.source_image_provenance_planes.identity - == expected.source_image_provenance_planes.identity - ) - assert first.source_provenance is not provenance - assert first.source_provenance is not second.source_provenance - first.source_provenance.source_image_names = ("first-only",) - first.source_provenance.source_identity.path = "/input/first-only.tif" - first.source_provenance.source_image_provenance_planes = ( - SourceImageProvenancePlanes() - ) - assert second.source_path == "/input/changed-plane.tif" - assert second.source_image_names == expected.source_image_names - assert ( - second.source_image_provenance_planes.identity - == expected.source_image_provenance_planes.identity - ) - - -def test_projection_rebirth_observes_opaque_scalar_mutation_before_capture(): - tag = MutableTag(1) - tag.tag = "old-birth" - metadata = ImagePayloadMetadata( - source_component_metadata=DurableSourceMetadata.from_mapping({"channel": tag}), - ) - old_birth = metadata.source_provenance.equality_identity - tag.tag = "new-birth" - expected = SourcePlaneImageMetadataProjection(metadata, 0).project() - owner = SourcePlaneImageMetadataProjection(metadata, 0).capture() - assert metadata.source_provenance.equality_identity == old_birth - assert owner.read_value("source_provenance").equality_identity != old_birth - assert ( - owner.read_value("source_provenance").equality_identity - == expected.source_provenance.equality_identity - ) - - -class SourceNormalizerMetadata(ImagePayloadMetadata): - def normalize_source_provenance_fields(self): - super().normalize_source_provenance_fields() - self.normalizer_result = "nominal subtype hook" - - -def test_capture_preserves_nominal_source_normalizer_override(): - original = stack_metadata(SourceNormalizerMetadata) - owner = LeadingSourcePlaneMetadataProjection(original, 0).capture() - actual = owner.materialize_metadata() - assert type(actual) is SourceNormalizerMetadata - assert actual.normalizer_result == "nominal subtype hook" - - -class ComponentAliasMetadata(ImagePayloadMetadata): - @property - def source_component_metadata(self): - self.component_alias_read = True - return super().source_component_metadata - - -def test_actual_source_component_alias_hook_survives_normalization_and_capture(): - metadata = stack_metadata(ComponentAliasMetadata) - assert metadata.component_alias_read - actual = ( - LeadingSourcePlaneMetadataProjection(metadata, 0) - .capture() - .materialize_metadata() - ) - assert type(actual) is ComponentAliasMetadata - assert actual.component_alias_read diff --git a/tests/unit/test_metadata_stage2_closure.py b/tests/unit/test_metadata_stage2_closure.py deleted file mode 100644 index 2588742626..0000000000 --- a/tests/unit/test_metadata_stage2_closure.py +++ /dev/null @@ -1,108 +0,0 @@ -"""Public metadata record contract and its real private alignment consumers.""" - -import gc -import weakref -from dataclasses import FrozenInstanceError, dataclass, fields - -import numpy as np -import pytest - -from openhcs.core.measurement_image_alignment import ( - MeasurementImageLabelAlignmentRequest, -) -from openhcs.core.runtime_image_values import ( - ImageMetadataCarrier, - ImagePayloadMetadata, - image_payload_data, - image_payload_metadata, -) -from openhcs.core.runtime_plane_projection import RuntimePlaneAxis -from openhcs.core.source_matching import SourceImageSetIdentityPolicy -from openhcs.core.source_plane_alignment import SourcePayloadPlaneIdentity -from openhcs.core.steps.function_io import _preloaded_image_payload -from openhcs.serialization.json import to_jsonable -from tests.unit.test_measurement_image_alignment import ( - _MeasurementImageSource, - _PlaneProjector, -) - - -@dataclass -class CalibratedMetadata(ImagePayloadMetadata): - calibration: float = 4.0 - - -def test_identity_record_keeps_public_metadata_field_schema_type_and_mutation(): - original = CalibratedMetadata( - source_path="/input/A01.tif", source_component_metadata={"well": "A01"} - ) - payload = original.payload_with(np.ones((3, 4))) - record = SourcePayloadPlaneIdentity.from_payload( - payload, SourceImageSetIdentityPolicy() - ) - assert isinstance(record, ImageMetadataCarrier) - assert not hasattr(record, "__dict__") - assert {member.name for member in fields(record)} == {"metadata", "policy"} - assert fields(record)[0].type == "ImagePayloadMetadata" - assert type(record.metadata) is CalibratedMetadata - assert record.metadata is original - assert record.metadata is record.metadata - assert set(to_jsonable(record)) == {"metadata", "policy"} - initial = record.identities() - record.metadata.source_component_metadata = {"well": "A02"} - assert record.identities() != initial - assert dict(record.metadata.source_component_metadata) == {"well": "A02"} - with pytest.raises(FrozenInstanceError): - record.metadata = ImagePayloadMetadata() - - -def test_identity_record_retains_metadata_without_pixel_payload_lifetime(): - data = np.ones((3, 4)) - payload = ImagePayloadMetadata(source_path="/input/image.tif").payload_with(data) - record = SourcePayloadPlaneIdentity.from_payload( - payload, SourceImageSetIdentityPolicy() - ) - pixels = weakref.ref(data) - expected = record.identities() - del payload, data - gc.collect() - assert pixels() is None - assert record.identities() == expected - - -def test_preload_preserves_existing_metadata_owner_without_backend_reads(): - class NoReadsFileManager: - def physical_source_path(self, *args, **kwargs): - pytest.fail("Existing source facts must not trigger backend reads.") - - data = np.arange(12).reshape(3, 4) - metadata = CalibratedMetadata(source_path="/input/existing.tif") - source = metadata.payload_with(data) - result = _preloaded_image_payload( - source, - source_path="/unused.tif", - read_backend="disk", - filemanager=NoReadsFileManager(), - ) - assert image_payload_data(result) is data - assert image_payload_metadata(result) is metadata - assert type(image_payload_metadata(result)) is CalibratedMetadata - - -def test_measurement_source_projection_preserves_actual_pixels_and_alias_selection(): - data = np.arange(24, dtype=np.float32).reshape(2, 3, 4) - source = ImagePayloadMetadata( - source_path="/input/channels.tif", - source_image_names=("DNA", "RNA"), - plane_axis=RuntimePlaneAxis.SOURCE_BINDING, - ).payload_with(data) - request = MeasurementImageLabelAlignmentRequest( - source=_MeasurementImageSource(source, source_aliases=("DNA", "RNA")), - labels=np.zeros((3, 4), dtype=np.int32), - plane_projector=_PlaneProjector(source_index=1, source_count=2), - ) - projected = request.with_source_projected_image() - np.testing.assert_array_equal(image_payload_data(projected.image), data[1]) - assert np.shares_memory(image_payload_data(projected.image), data) - assert image_payload_metadata(projected.image).source_image_names == ("RNA",) - assert image_payload_metadata(source).source_image_names == ("DNA", "RNA") From 0bdb0aebe61c9dc7435823163b4dae9cc9140271 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 05:02:06 -0400 Subject: [PATCH 119/235] perf: retain whole-driver falsification of metadata owner prototype Refs #384 and PR #394. Restore prior runtime behavior after the candidate missed its material payoff gate; full native parity passed. Keep exact source pins, scientific gates, original test failures and matched coarse clocks without promoting a speedup. --- .../metrics.csv | 3 + .../report.rst | 83 ++++++++++++ .../whole-driver-controller.py | 128 ++++++++++++++++++ .../whole-driver-site.py | 118 ++++++++++++++++ 4 files changed, 332 insertions(+) create mode 100644 benchmark/results/perf_rejected_metadata_owner_20261003/metrics.csv create mode 100644 benchmark/results/perf_rejected_metadata_owner_20261003/report.rst create mode 100644 benchmark/results/perf_rejected_metadata_owner_20261003/whole-driver-controller.py create mode 100644 benchmark/results/perf_rejected_metadata_owner_20261003/whole-driver-site.py diff --git a/benchmark/results/perf_rejected_metadata_owner_20261003/metrics.csv b/benchmark/results/perf_rejected_metadata_owner_20261003/metrics.csv new file mode 100644 index 0000000000..f9fd7ecb36 --- /dev/null +++ b/benchmark/results/perf_rejected_metadata_owner_20261003/metrics.csv @@ -0,0 +1,3 @@ +variant,source,compile_seconds,execution_seconds,total_seconds,raw_callable_seconds,outside_callable_seconds +baseline,e8587bd7eb0c4aa283bc300c5eb9f2294f24890f,1.5337145328521729,8.276516675949097,10.606972666922957,3.656715089920908,4.607541659846902 +candidate,9eee98265c939859d49f0c60bd5b3a2168cb29fe,1.2758193016052246,8.977409839630127,11.01210141601041,3.975498642306775,4.990668492857367 diff --git a/benchmark/results/perf_rejected_metadata_owner_20261003/report.rst b/benchmark/results/perf_rejected_metadata_owner_20261003/report.rst new file mode 100644 index 0000000000..d2d2b04f1d --- /dev/null +++ b/benchmark/results/perf_rejected_metadata_owner_20261003/report.rst @@ -0,0 +1,83 @@ +Rejected flat metadata owner prototype +====================================== + +The prototype did not reduce representative 3D runtime enough to justify its +additional storage and dispatch machinery. It is reverted from the final source. +Rejected source 9eee98265c939859d49f0c60bd5b3a2168cb29fe remains in Git history; +baseline e8587bd7eb0c4aa283bc300c5eb9f2294f24890f and restored production/tests +are byte-identical. This is evidence for #384 / PR #394, not a completed goal. + +One matched near-ordinary 1w_1t diagnostic pair on CPU5, 24 unchanged coarse +hooks, no cProfile/runtime profiler/captures, existing OUTCOMES and memory +observer. Server/library/kernel readiness and shutdown precede/follow pipeline +clocks. Shared dependency sources, physical inputs, native/Python binaries, +manifest/driver/site/controller and installed versions match. Submission bytes +match after the sole fresh-output-directory change. No environments or source +worktrees were duplicated. Generated outputs were 18.16 MB per variant. + + Baseline Candidate +Compilation seconds 1.533715 1.275819 +Execution seconds 8.276517 8.977410 +Total seconds 10.606973 11.012101 +Raw callable seconds 3.656715 3.975499 +Outside callable seconds 4.607542 4.990668 + +The candidate misses the required 0.7--1s execution payoff: observed execution +increased 0.700893s and outside-callable time increased 0.383127s. Finalization +exclusive time changed 0.209953 -> 0.480475s. These single observations are +falsification evidence, not statistical proof of a causal regression. The lower +compilation observation is not promoted as a gain. No full30/scaling acceptance +or new native clock is inferred. + +Full behavior controls +---------------------- + +The coherent consumer gate passed 570 tests; extended runtime boundaries passed +700 with nine failures. All nine exact original tests reproduce the same errors +on all 27 original e858 Git-blob modules with the same current dependencies; +none is waived or silently rewritten. The initial source-stem owner interface +failures were fixed before these gates. + +Both representative saved V10 edges preserve complete values, metadata, pixels, +masks, store/cache state and aliases: producer60records/123array roles and +loader104roles. A qualified offline dataclass-state decoder transfers original +stored references without constructor/capture/copy. Its instrumentation and +public realization occur outside different subclocks; edge timings are not +compared as whole-pipeline speedup evidence. + +Four complete comparisons (fresh baseline and candidate against each retained +native repetition) pass unchanged CellProfiler numerical tolerances and exact +image controls: six CSVs, 120 TIFFs (two logical volumes), schema, relationships, +full physical inventory and 180 ordered source references. Native witnesses, +inputs, source and outputs are hash-checked before and after. No scientific +output or earlier failed receipt is replaced. + +Architecture decision +--------------------- + +Flattening fields did not remove the semantic normalization lifecycle. Scalar +derivation still normalizes once and births provenance/identity plus two owner +allocations; leading-plane capture retains three normalizations and four +provenance/identity births. Public realization defers a real namespace allocation. +One source-context fusion removed a redundant normalization, but that narrow +route cannot materially close the full target gap alone. The earlier saved +normalization envelope is only 0.318s and contains mandatory work. + +Stop this representation route rather than polish namespace deferral. Next +inventory the existing authorities and production data flow for repeated graph +projection/validation/serialization and manifest/store/workspace derivation. +Fresh baseline publication 0.768611s, reconciliation 0.300908s, load 0.797152s, +save 0.656345s are investigation envelopes, not claimed removable time. Prefer +one existing nominal authority and derived views, retaining public mutable +namespace epochs, raw schema/order, alias/pixel ownership and first errors. + +Reproduction and custody +------------------------ + +Controller and site are retained byte-identically. Full local source/env freezes +remain at the absolute paths and SHA256 pins in baseline/candidate-source-pins. +Original helper scripts and native witness paths remain pinned there and in +strict-science. The saved V10 qualification preserves original graph/helper +hashes; its 1.027 GB capture is reused, not copied into this report. Exact rejected +production and tests remain reachable through the rejected Git commit. Run fresh +outputs only; finished evidence directories are immutable. diff --git a/benchmark/results/perf_rejected_metadata_owner_20261003/whole-driver-controller.py b/benchmark/results/perf_rejected_metadata_owner_20261003/whole-driver-controller.py new file mode 100644 index 0000000000..77d51c2449 --- /dev/null +++ b/benchmark/results/perf_rejected_metadata_owner_20261003/whole-driver-controller.py @@ -0,0 +1,128 @@ +"""Prepare-only near-ordinary nested boundary ledger; both profilers disabled.""" +import argparse,ast,csv,fcntl,hashlib,importlib.util,json,os,shutil,subprocess +from pathlib import Path +BASE=Path('/var/tmp/run_narrow450_runtime_phase_probe_20261002.py') +SPARSE=Path('/var/tmp/openhcs_measurement_sparse_inventory_v2_20261003.py') +SUPPLEMENT=Path('/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-preparation-v2/sparse-supplement') +ORIGINAL_SITE=Path('/var/tmp/openhcs-current-generic-job-profile-site-v1-20261002/sitecustomize.py') +SITE=Path(__file__).parent/'site' +OUTPUT=Path(__file__).parent/'output' +JSON_CAP=40*1024**2 +OUTPUT_CAP=64*1024**2 +LOG_CAP=1024**2 +HOME_RESERVE=1024**3 +MIN_FREE=512*1024**2 +INDEX='MeasurementFeatureValueIndex.from_columnar_table_by_object' + +def load_module(name,path): + spec=importlib.util.spec_from_file_location(name,path) + module=importlib.util.module_from_spec(spec);spec.loader.exec_module(module);return module + +def controller(source,revision): + m=load_module('original_phase_controller',BASE) + m.SOURCE=Path(source).resolve();m.REVISION=revision;m.SITE=SITE;m.OUTPUT=OUTPUT + m.CASES=('cp_tutorial_3d_monolayer',) + return m + +def environment(m): + env=m.ordinary_helper().environment(m.SOURCE) + for key in tuple(env): + if key.startswith('OPENHCS_DIAGNOSTIC_') or key.startswith('OPENHCS_PROFILE_FUNCTION_RUNTIME') or key=='OPENHCS_WORKER_PROFILE_DIR':del env[key] + env.update(PYTHONDONTWRITEBYTECODE='1',PYTHONPATH=str(SITE)+':'+str(m.SOURCE), + OPENHCS_DIAGNOSTIC_SOURCE=str(m.SOURCE),OPENHCS_DIAGNOSTIC_OUTPUT=str(OUTPUT/'regions'), + OPENHCS_DIAGNOSTIC_FREEZE=str(OUTPUT/'source-freeze.json'),TMPDIR=str(OUTPUT/'tmp')) + return env + +def env_hash(env): + return hashlib.sha256(json.dumps(env,sort_keys=True).encode()).hexdigest() + +def prepare_or_validate(m): + sparse=load_module('preserved_sparse_source_controller',SPARSE) + sparse.SUPPLEMENT=SUPPLEMENT + inventory=sparse.sparse_source_inventory(m.SOURCE) + freeze_path=OUTPUT/'source-freeze.json' + env=environment(m) + if freeze_path.exists(): + freeze=json.loads(freeze_path.read_text()) + assert freeze['revision']==m.REVISION and freeze['source']==str(m.SOURCE) + else: + ast.parse((SITE/'sitecustomize.py').read_text()) + freeze=m.prepare() + freeze.update(sparse_source_inventory=inventory,ledger_controller_sha256=m.digest(Path(__file__)), + original_phase_controller_sha256=m.digest(BASE),sparse_source_controller_sha256=m.digest(SPARSE), + declaring_ledger_controller_sha256=m.digest(Path('/var/tmp/run_current_3d_runtime_ledger_v1_20261003.py')), + declaring_ledger_site_sha256=m.digest(Path('/var/tmp/openhcs-current-3d-runtime-ledger-site-v1-20261003/sitecustomize.py')), + supplemental_custody_root=str(SUPPLEMENT), + actual_import_dependencies={r:{'head':m.git((m.SOURCE/r).resolve(),'rev-parse','HEAD'), + 'status':m.git((m.SOURCE/r).resolve(),'status','--porcelain'), + 'files':m.tracked_source_hashes((m.SOURCE/r).resolve())} for r in freeze['shared_dependency_sources']}, + original_profile_site_sha256=m.digest(ORIGINAL_SITE),effective_environment_sha256=env_hash(env), + profiling_environment={'worker_cprofile':'DISABLED: directory unset','runtime_profiler':'DISABLED: enable/path unset'}, + storage={'minimum_free':MIN_FREE,'available_at_prepare':shutil.disk_usage('/var/tmp').free,'retained_json_cap':JSON_CAP,'generated_output_cap':OUTPUT_CAP,'public_log_cap':LOG_CAP,'captures':False}, + scope='One source-pinned whole ordinary3D Monolayer1w1t candidate falsification with24 unchanged coarse nominal hooks: inherited23 plus actual MeasurementFeatureValueIndex index classmethod. cProfile and runtime profiler OFF, no captures/percell hooks. Existing defaultOUTCOMES/memory observers/READY clocks unchanged. Same-thread nested exclusive clocks exclude query from its real ancestors; worker independent roots are not added to parent waits. Nearordinary diagnostic only; wrapper overhead remains, no scale factors/causal performance promotion. Full saved126file (6CSV120TIFF) science/native source witness independently required.', + controls='Original callable once/result/error identity; argument mutation preserved; TLS/fork reset; exact source/descriptor/signature/declaring-code checks; both profilers disabled; no class metadata mutation. Original512MiB preflight and900s subprocess timeout retained; JSON aggregate40MiB cap; overwrite refusal; existing reviewed sparse projection admitted only after original preserved copy/current Git blob validation; exact8gitlink child context. No production omission or new source authority. Site/code for all24hooks retained byte-identical; current sparse inventory V2 checks every absent tracked byte against current Git blobs including existing supplemental custody, not old V1. Source revision must be sealed/clean before prepare; no candidate import at offline construction. All24hooks retained byte-identical; feature-query hook remains installed with zero calls admitted for a workload having no such query, while actual declared callable calls must be nonempty.') + freeze_path.write_text(json.dumps(freeze,indent=2)+'\n') + sparse.validate_sparse_source_inventory(m.SOURCE,freeze['sparse_source_inventory']) + m.validate_freeze(freeze) + assert m.digest(Path(__file__))==freeze['ledger_controller_sha256'] + assert m.digest(BASE)==freeze['original_phase_controller_sha256'] + assert m.digest(SPARSE)==freeze['sparse_source_controller_sha256'] + assert m.digest(ORIGINAL_SITE)==freeze['original_profile_site_sha256'] + assert str(SUPPLEMENT)==freeze['supplemental_custody_root'] + assert m.digest(Path('/var/tmp/run_current_3d_runtime_ledger_v1_20261003.py'))==freeze['declaring_ledger_controller_sha256'] + assert m.digest(Path('/var/tmp/openhcs-current-3d-runtime-ledger-site-v1-20261003/sitecustomize.py'))==freeze['declaring_ledger_site_sha256']==freeze['site_sha256'] + assert len(freeze['actual_import_dependencies'])==8 + for relative,expected in freeze['actual_import_dependencies'].items(): + actual=(m.SOURCE/relative).resolve() + assert not expected['status'] + assert {'head':m.git(actual,'rev-parse','HEAD'),'status':m.git(actual,'status','--porcelain'),'files':m.tracked_source_hashes(actual)}==expected + assert env_hash(env)==freeze['effective_environment_sha256'],'Effective ordinary environment changed.' + assert len(freeze['declared_hooks'])==24 + assert any(r['owner']=='MeasurementFeatureValueIndex' and r['method']=='from_columnar_table_by_object' for r in freeze['declared_hooks']) + return freeze,env + +def main(source,revision,run): + assert OUTPUT.is_relative_to(Path('/home/ts/.local/state/openhcs-maintenance/20261003')),'Owned output only' + assert shutil.disk_usage(OUTPUT.parent).free>=HOME_RESERVE,'Home reserve' + available=shutil.disk_usage('/var/tmp').free + assert available>=MIN_FREE,f'Original512MiB storage preflight refused: {available} bytes free; no pipeline started.' + m=controller(source,revision);freeze,env=prepare_or_validate(m) + (OUTPUT/'tmp').mkdir(exist_ok=True) + assert sum(p.stat().st_size for p in OUTPUT.rglob('*') if p.is_file())<=OUTPUT_CAP + if not run: + print(json.dumps({'status':'PREPARED_NOT_RUN','revision':revision,'source':str(m.SOURCE),'freeze':str(OUTPUT/'source-freeze.json'),'command':freeze['command'],'hooks':len(freeze['declared_hooks']),'profilers':'BOTH_OFF'}),flush=True);return + assert not (OUTPUT/'observations.json').exists(),'Never overwrite retained diagnostic.' + assert not list((OUTPUT/'regions').glob('*.json')),'Never rerun retained regions.' + lock=Path('/tmp/openhcs-benchmark-xdg-cache/openhcs/official30-runtime.lock') + with lock.open('a+') as lease: + fcntl.flock(lease,fcntl.LOCK_EX|fcntl.LOCK_NB);prepare_or_validate(m) + with (OUTPUT/'public.log').open('w') as stream: + result=subprocess.run(freeze['command'],cwd=OUTPUT/'tmp',env=env,stdout=stream,stderr=subprocess.STDOUT,timeout=900) + prepare_or_validate(m) + region_paths=sorted((OUTPUT/'regions').glob('*.json'));regions=[json.loads(p.read_text()) for p in region_paths] + dimensions=[json.loads(r['root_dimensions']) for r in regions] + root_sequence_complete=sum(d['kind']=='compile_only' for d in dimensions)==1 and sum(d['kind']=='execution' for d in dimensions)==1 + execution=[r for r in regions if json.loads(r['root_dimensions'])['kind']=='execution'] + rows=list(csv.DictReader((OUTPUT/'public/well_throughput.csv').open())) if (OUTPUT/'public/well_throughput.csv').exists() else [] + index_calls=sum(row['calls'] for r in execution for row in r['stats'].values() if row['owner']==INDEX) + declared_calls=sum(row['calls'] for r in execution for row in r['stats'].values() if row['owner']=='RuntimeCallableInvocation.call') + retained=sum(p.stat().st_size for p in region_paths) + output_bytes=sum(p.stat().st_size for p in OUTPUT.rglob('*') if p.is_file()) + log_bytes=(OUTPUT/'public.log').stat().st_size + disabled=not list(OUTPUT.rglob('*.prof')) and not (OUTPUT/'runtime-profile.log').exists() + admission=bool(result.returncode==0 and len(rows)==1 and rows[0]['status']=='success' and rows[0]['successful_wells']=='1' + and rows[0]['execution_route']=='ordinary-zmq-outcomes-v1' and root_sequence_complete and execution and declared_calls>0 and disabled and retained<=JSON_CAP and output_bytes<=OUTPUT_CAP and log_bytes<=LOG_CAP + and all(r['revision']==revision and not r['captures'] and not r['profiling_enabled'] and not r['runtime_profiling_enabled'] + and not r['diagnostic_errors'] and abs(sum(v['exclusive'] for v in r['stats'].values())-r['seconds'])<1e-6 for r in regions)) + evidence={'returncode':result.returncode,'diagnostic_admission':admission,'public_rows':rows,'regions':[{ 'path':str(p),'sha256':m.digest(p)} for p in region_paths], + 'root_sequence_complete':root_sequence_complete,'actual_index_calls':index_calls,'actual_declared_calls':declared_calls,'retained_json_bytes':retained,'generated_output_bytes':output_bytes,'public_log_bytes':log_bytes,'profiler_artifacts_absent':disabled,'source_env_native_input_freeze':'PASS', + 'science':'NOT_RUN: original full saved-output126file/schema/discrete/numerical/nativeinput witness separate gate. No exclusions or missing outputs waived.', + 'scope':freeze['scope']} + (OUTPUT/'observations.json').write_text(json.dumps(evidence,indent=2)+'\n');print(json.dumps(evidence),flush=True) + assert admission,'Retained original ledger admission RED.' + +if __name__=='__main__': + p=argparse.ArgumentParser();p.add_argument('--source',required=True);p.add_argument('--revision',required=True);p.add_argument('--run',action='store_true');p.add_argument('--prepare',action='store_true') + p.add_argument('--output',type=Path,default=OUTPUT);a=p.parse_args();OUTPUT=a.output.resolve() + if a.prepare or a.run:main(a.source,a.revision,a.run) + else:print(json.dumps({'status':'CODE_PREPARED_NOT_FROZEN_NOT_RUN','source':a.source,'revision':a.revision,'case':'cp_tutorial_3d_monolayer','output':str(OUTPUT),'controller':str(Path(__file__)),'instruction':'ROOT reviews then --prepare and --run same source/revision/env; no freeze created by default.'})) diff --git a/benchmark/results/perf_rejected_metadata_owner_20261003/whole-driver-site.py b/benchmark/results/perf_rejected_metadata_owner_20261003/whole-driver-site.py new file mode 100644 index 0000000000..1410ea73a8 --- /dev/null +++ b/benchmark/results/perf_rejected_metadata_owner_20261003/whole-driver-site.py @@ -0,0 +1,118 @@ +import functools, hashlib, importlib.abc, importlib.machinery, inspect, json, os, sys, threading, time +from pathlib import Path +SOURCE=Path(os.environ['OPENHCS_DIAGNOSTIC_SOURCE']).resolve() +OUT=Path(os.environ['OPENHCS_DIAGNOSTIC_OUTPUT']) +freeze=json.loads(Path(os.environ['OPENHCS_DIAGNOSTIC_FREEZE']).read_text()) +assert not os.environ.get('OPENHCS_WORKER_PROFILE_DIR'), 'Worker profiler must be disabled.' +assert os.environ.get('OPENHCS_PROFILE_FUNCTION_RUNTIME','').lower() not in {'1','true','yes'}, 'Runtime profiler must be disabled.' +assert not os.environ.get('OPENHCS_PROFILE_FUNCTION_RUNTIME_PATH'), 'No runtime profiler sink.' +active=threading.local() +installed=[] +TARGETS = {'openhcs.runtime.zmq_execution_server': {'ZMQExecutionServer': ('_execute_with_orchestrator', '_initialize_orchestrator', '_export_runtime_observation')}, 'openhcs.core.orchestrator.worker_execution': {'': ('execute_worker_lane',)}, 'openhcs.core.steps.function_runtime': {'FunctionCoreExecutor': ('execute',), 'PatternGroupRuntime': ('_load_input_stack', '_validate_and_unstack', '_save_outputs')}, 'openhcs.processing.backends.lib_registry.unified_registry': {'RuntimeCallableInvocation': ('call',)}, 'openhcs.interop.cellprofiler.runtime.module_execution': {'CellProfilerModuleExecutor': ('__call__', '_image_request')}, 'openhcs.interop.cellprofiler.runtime.function_contract_execution': {'CellProfilerFunctionContractExecutor': ('execute',)}, 'openhcs.interop.cellprofiler.runtime.output_recording': {'CellProfilerOutputRecorder': ('record_module_outputs',)}, 'openhcs.interop.cellprofiler.runtime.measurement_execution_support': {'ObjectMeasurementOutputRecorder': ('record',)}, 'openhcs.core.steps.function_outputs': {'': ('finalize_function_step_outputs',), 'OpenHCSMetadataWriter': ('finalize_completed_plate',), 'RuntimeArtifactMetadataTarget': ('reconciliation_targets',)}, 'openhcs.core.virtual_workspace_metadata': {'AtomicMetadataWriter': ('publish_source_projection_metadata',)}, 'openhcs.core.runtime_stack_cache': {'RuntimeImageStackCache': ('get', 'store')}, 'openhcs.core.aligned_image_payload': {'': ('stack_image_payload_context', 'unstack_image_payload_context')}, 'openhcs.core.orchestrator.analysis_consolidation': {'': ('consolidate_analysis_outputs',)}, 'openhcs.core.measurement_feature_queries': {'MeasurementFeatureValueIndex': ('from_columnar_table_by_object',)}} +PHASES = {'ZMQExecutionServer._execute_with_orchestrator': '_execute_with_orchestrator', 'ZMQExecutionServer._initialize_orchestrator': '_initialize_orchestrator', 'ZMQExecutionServer._export_runtime_observation': '_export_runtime_observation', 'execute_worker_lane': 'execute_worker_lane', 'FunctionCoreExecutor.execute': 'execute', 'PatternGroupRuntime._load_input_stack': '_load_input_stack', 'PatternGroupRuntime._validate_and_unstack': '_validate_and_unstack', 'PatternGroupRuntime._save_outputs': '_save_outputs', 'RuntimeCallableInvocation.call': 'declared_callable_boundary', 'CellProfilerModuleExecutor.__call__': '__call__', 'CellProfilerModuleExecutor._image_request': '_image_request', 'CellProfilerFunctionContractExecutor.execute': 'execute', 'CellProfilerOutputRecorder.record_module_outputs': 'record_module_outputs', 'ObjectMeasurementOutputRecorder.record': 'record', 'finalize_function_step_outputs': 'finalize_function_step_outputs', 'OpenHCSMetadataWriter.finalize_completed_plate': 'finalize_completed_plate', 'RuntimeArtifactMetadataTarget.reconciliation_targets': 'reconciliation_targets', 'AtomicMetadataWriter.publish_source_projection_metadata': 'publish_source_projection_metadata', 'RuntimeImageStackCache.get': 'get', 'RuntimeImageStackCache.store': 'store', 'stack_image_payload_context': 'stack_image_payload_context', 'unstack_image_payload_context': 'unstack_image_payload_context', 'consolidate_analysis_outputs': 'consolidate_analysis_outputs', 'MeasurementFeatureValueIndex.from_columnar_table_by_object': 'measurement_feature_index'} +ROOTS = {'ZMQExecutionServer._execute_with_orchestrator', 'execute_worker_lane'} +def detail(label, args, kwargs): + if label == "ZMQExecutionServer._execute_with_orchestrator": + request = kwargs.get("request_context") if "request_context" in kwargs else args[1] + selected = request.request_payload.selected_pipeline_path + return json.dumps({"kind": "compile_only" if request.compile_only else "execution", "plate": str(request.plate_id), "case": None if selected is None else Path(selected).stem, "execution_id": request.execution_id, "compile_artifact_id": request.compile_artifact_id}, sort_keys=True) + if label == "execute_worker_lane": + lane = kwargs.get("lane_context") if "lane_context" in kwargs else args[2] + return json.dumps({"kind": "worker_lane", "plate": str(lane.plate_id), "worker_slot": lane.worker_slot, "execution_id": lane.execution_id}, sort_keys=True) + if label.startswith('FunctionStepExecutor.'): + plan = args[0].plan + return f'step={plan.step_index}:{plan.step_name}' + if label.startswith('FunctionCoreExecutor.'): + return 'function=' + args[0].function_name + if label == 'CellProfilerFunctionContractExecutor.execute': + contract = kwargs.get('callable_contract') if 'callable_contract' in kwargs else args[1] + return 'function=' + contract.module_name + '.' + contract.function_name + if label == 'RuntimeCallableInvocation.call': + invocation = args[0] + func = invocation.func + return 'function=' + func.__module__ + '.' + func.__qualname__ + ';view=' + str(invocation.callable_view.value) + return '' + +def safe_detail(label,args,kwargs): + try:return detail(label,args,kwargs) + except Exception as error: + active.diagnostic_errors.append({'owner':label,'type':type(error).__name__,'message':str(error)}) + return 'DIAGNOSTIC_DETAIL_UNAVAILABLE' + +def reset_fork(): + global active + active=threading.local() +os.register_at_fork(after_in_child=reset_fork) +def instrument(original, label): + @functools.wraps(original) + def wrapped(*args, **kwargs): + root=not getattr(active, 'enabled', False) + if root and label not in ROOTS: + return original(*args, **kwargs) + if root: + active.enabled=True; active.stats={}; active.stack=[]; active.diagnostic_errors=[]; active.root_dimensions=safe_detail(label,args,kwargs) + dimensions=safe_detail(label,args,kwargs) + name=label+(' | '+dimensions if dimensions else '') + frame={'start':time.perf_counter(), 'children':0.} + active.stack.append(frame) + try: + return original(*args,**kwargs) + finally: + elapsed=time.perf_counter()-frame['start'] + assert active.stack.pop() is frame + row=active.stats.setdefault(name, {'owner':label,'dimensions':dimensions,'phase':PHASES[label],'calls':0,'inclusive':0.,'exclusive':0.}) + row['calls']+=1; row['inclusive']+=elapsed; row['exclusive']+=elapsed-frame['children'] + if active.stack: + active.stack[-1]['children']+=elapsed + if root: + active.enabled=False + sequence=getattr(active,'sequence',0)+1;active.sequence=sequence + path=OUT/f'region-{os.getpid()}-{threading.get_ident()}-{sequence:04}.json' + assert not path.exists() + value={'pid':os.getpid(),'thread':threading.get_ident(),'region':label,'root_dimensions':active.root_dimensions,'seconds':elapsed,'revision':freeze['revision'],'source':str(SOURCE),'stats':active.stats,'hooks_installed':installed,'profiling_enabled':False,'runtime_profiling_enabled':False,'diagnostic_errors':active.diagnostic_errors,'captures':False,'scope':'coarse same-run nested exclusive timing partition on the exact source revision recorded in this region. Diagnostic wrappers, no scientific or memory-observer substitutions. Not accepted performance comparison. Declared callable boundary includes filtering/decorated processing/metadata; not pure numerical kernels. Both existing profilers are disabled. These wrappers still add diagnostic overhead; no scaling to uninstrumented clocks or causal performance claims. Query boundary is nested under its actual parent and excluded from parent-exclusive elapsed.'} + assert abs(sum(r['exclusive'] for r in active.stats.values())-elapsed)<1e-6 + path.write_text(json.dumps(value,indent=2)+'\n') + return wrapped +class Loader(importlib.abc.InspectLoader): + def __init__(self, loader): self.loader=loader + def create_module(self,spec): return self.loader.create_module(spec) + def get_code(self,fullname): return self.loader.get_code(fullname) + def get_source(self,fullname): return self.loader.get_source(fullname) + def is_package(self,fullname): return self.loader.is_package(fullname) + def get_filename(self,fullname): return self.loader.get_filename(fullname) + def exec_module(self,module): + self.loader.exec_module(module) + path=Path(module.__file__).resolve();relative=str(path.relative_to(SOURCE)) + assert hashlib.sha256(path.read_bytes()).hexdigest()==freeze['files'][relative] + for owner_path,names in TARGETS[module.__name__].items(): + owner=module + for part in owner_path.split('.') if owner_path else (): owner=getattr(owner,part) + for name in names: + descriptor=inspect.getattr_static(owner,name) + declared_original=descriptor.__func__ if isinstance(descriptor,(classmethod,staticmethod)) else descriptor + original_signature=inspect.signature(declared_original) + original_code=declared_original.__code__ + source_declared=inspect.unwrap(declared_original) + assert Path(source_declared.__code__.co_filename).resolve()==path,(module.__name__,owner_path,name,source_declared.__code__.co_filename) + label=(owner_path+'.' if owner_path else '')+name + if isinstance(descriptor,classmethod): replacement=classmethod(instrument(descriptor.__func__,label));kind='classmethod' + elif isinstance(descriptor,staticmethod):replacement=staticmethod(instrument(descriptor.__func__,label));kind='staticmethod' + else: + assert callable(descriptor),(owner_path,name) + replacement=instrument(descriptor,label);kind='function' + setattr(owner,name,replacement) + assert type(inspect.getattr_static(owner,name)) is type(replacement) + wrapped_descriptor=inspect.getattr_static(owner,name) + wrapped_function=wrapped_descriptor.__func__ if isinstance(wrapped_descriptor,(classmethod,staticmethod)) else wrapped_descriptor + assert wrapped_function.__wrapped__ is declared_original + assert inspect.signature(wrapped_function)==original_signature + assert wrapped_function.__wrapped__.__code__ is original_code + installed.append({'module':module.__name__,'owner':owner_path,'method':name,'descriptor_kind':kind}) +class Finder(importlib.abc.MetaPathFinder): + def find_spec(self,fullname,path=None,target=None): + if fullname not in TARGETS:return None + spec=importlib.machinery.PathFinder.find_spec(fullname,path,target) + assert spec is not None and isinstance(spec.loader,importlib.machinery.SourceFileLoader) + spec.loader=Loader(spec.loader) + return spec +sys.meta_path.insert(0,Finder()) From 480aed49b30448de4380af5c9274e48084e67b63 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 05:03:15 -0400 Subject: [PATCH 120/235] perf: attach pinned rejection and strict native parity receipts Refs #384 and PR #394. Explicitly track reviewed JSON evidence excluded by default. Derive compact saved-witness pins instead of duplicating full inventories. No production behavior change. --- .../baseline-compile_only-region.json | 178 +++ .../baseline-execution-region.json | 714 ++++++++++++ .../baseline-observations.json | 48 + .../baseline-source-pins.json | 116 ++ .../candidate-compile_only-region.json | 178 +++ .../candidate-execution-region.json | 714 ++++++++++++ .../candidate-observations.json | 48 + .../candidate-source-pins.json | 116 ++ .../consumer-controls.json | 6 + .../extended-controls.json | 5 + .../falsification.json | 495 ++++++++ .../inventory.json | 85 ++ .../original-nine-failures.json | 113 ++ .../saved-v10-qualification.json | 277 +++++ .../science-terminal.json | 39 + .../strict-science.json | 1023 +++++++++++++++++ .../submitted-input-controls.json | 22 + 17 files changed, 4177 insertions(+) create mode 100644 benchmark/results/perf_rejected_metadata_owner_20261003/baseline-compile_only-region.json create mode 100644 benchmark/results/perf_rejected_metadata_owner_20261003/baseline-execution-region.json create mode 100644 benchmark/results/perf_rejected_metadata_owner_20261003/baseline-observations.json create mode 100644 benchmark/results/perf_rejected_metadata_owner_20261003/baseline-source-pins.json create mode 100644 benchmark/results/perf_rejected_metadata_owner_20261003/candidate-compile_only-region.json create mode 100644 benchmark/results/perf_rejected_metadata_owner_20261003/candidate-execution-region.json create mode 100644 benchmark/results/perf_rejected_metadata_owner_20261003/candidate-observations.json create mode 100644 benchmark/results/perf_rejected_metadata_owner_20261003/candidate-source-pins.json create mode 100644 benchmark/results/perf_rejected_metadata_owner_20261003/consumer-controls.json create mode 100644 benchmark/results/perf_rejected_metadata_owner_20261003/extended-controls.json create mode 100644 benchmark/results/perf_rejected_metadata_owner_20261003/falsification.json create mode 100644 benchmark/results/perf_rejected_metadata_owner_20261003/inventory.json create mode 100644 benchmark/results/perf_rejected_metadata_owner_20261003/original-nine-failures.json create mode 100644 benchmark/results/perf_rejected_metadata_owner_20261003/saved-v10-qualification.json create mode 100644 benchmark/results/perf_rejected_metadata_owner_20261003/science-terminal.json create mode 100644 benchmark/results/perf_rejected_metadata_owner_20261003/strict-science.json create mode 100644 benchmark/results/perf_rejected_metadata_owner_20261003/submitted-input-controls.json diff --git a/benchmark/results/perf_rejected_metadata_owner_20261003/baseline-compile_only-region.json b/benchmark/results/perf_rejected_metadata_owner_20261003/baseline-compile_only-region.json new file mode 100644 index 0000000000..8ff5e63f99 --- /dev/null +++ b/benchmark/results/perf_rejected_metadata_owner_20261003/baseline-compile_only-region.json @@ -0,0 +1,178 @@ +{ + "pid": 1677824, + "thread": 140643152090816, + "region": "ZMQExecutionServer._execute_with_orchestrator", + "root_dimensions": "{\"case\": \"3d_monolayer_final\", \"compile_artifact_id\": null, \"execution_id\": \"9b5738f2-c0e7-413c-9d58-7aae0b6af48d\", \"kind\": \"compile_only\", \"plate\": \"/home/ts/.cache/openhcs/benchmark_datasets/CellProfiler_tutorials/data/3d_monolayer/images\"}", + "seconds": 1.2939568259753287, + "revision": "e8587bd7eb0c4aa283bc300c5eb9f2294f24890f", + "source": "/home/ts/code/projects/openhcs-shared-runtime-plumbing", + "stats": { + "ZMQExecutionServer._initialize_orchestrator": { + "owner": "ZMQExecutionServer._initialize_orchestrator", + "dimensions": "", + "phase": "_initialize_orchestrator", + "calls": 1, + "inclusive": 0.1277877571992576, + "exclusive": 0.1277877571992576 + }, + "ZMQExecutionServer._execute_with_orchestrator | {\"case\": \"3d_monolayer_final\", \"compile_artifact_id\": null, \"execution_id\": \"9b5738f2-c0e7-413c-9d58-7aae0b6af48d\", \"kind\": \"compile_only\", \"plate\": \"/home/ts/.cache/openhcs/benchmark_datasets/CellProfiler_tutorials/data/3d_monolayer/images\"}": { + "owner": "ZMQExecutionServer._execute_with_orchestrator", + "dimensions": "{\"case\": \"3d_monolayer_final\", \"compile_artifact_id\": null, \"execution_id\": \"9b5738f2-c0e7-413c-9d58-7aae0b6af48d\", \"kind\": \"compile_only\", \"plate\": \"/home/ts/.cache/openhcs/benchmark_datasets/CellProfiler_tutorials/data/3d_monolayer/images\"}", + "phase": "_execute_with_orchestrator", + "calls": 1, + "inclusive": 1.2939568259753287, + "exclusive": 1.166169068776071 + } + }, + "hooks_installed": [ + { + "module": "openhcs.core.aligned_image_payload", + "owner": "", + "method": "stack_image_payload_context", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.aligned_image_payload", + "owner": "", + "method": "unstack_image_payload_context", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.measurement_feature_queries", + "owner": "MeasurementFeatureValueIndex", + "method": "from_columnar_table_by_object", + "descriptor_kind": "classmethod" + }, + { + "module": "openhcs.core.virtual_workspace_metadata", + "owner": "AtomicMetadataWriter", + "method": "publish_source_projection_metadata", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.runtime_stack_cache", + "owner": "RuntimeImageStackCache", + "method": "get", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.runtime_stack_cache", + "owner": "RuntimeImageStackCache", + "method": "store", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.steps.function_outputs", + "owner": "", + "method": "finalize_function_step_outputs", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.steps.function_outputs", + "owner": "OpenHCSMetadataWriter", + "method": "finalize_completed_plate", + "descriptor_kind": "classmethod" + }, + { + "module": "openhcs.core.steps.function_outputs", + "owner": "RuntimeArtifactMetadataTarget", + "method": "reconciliation_targets", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.steps.function_runtime", + "owner": "FunctionCoreExecutor", + "method": "execute", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.steps.function_runtime", + "owner": "PatternGroupRuntime", + "method": "_load_input_stack", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.steps.function_runtime", + "owner": "PatternGroupRuntime", + "method": "_validate_and_unstack", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.steps.function_runtime", + "owner": "PatternGroupRuntime", + "method": "_save_outputs", + "descriptor_kind": "function" + }, + { + "module": "openhcs.processing.backends.lib_registry.unified_registry", + "owner": "RuntimeCallableInvocation", + "method": "call", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.orchestrator.analysis_consolidation", + "owner": "", + "method": "consolidate_analysis_outputs", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.orchestrator.worker_execution", + "owner": "", + "method": "execute_worker_lane", + "descriptor_kind": "function" + }, + { + "module": "openhcs.runtime.zmq_execution_server", + "owner": "ZMQExecutionServer", + "method": "_execute_with_orchestrator", + "descriptor_kind": "function" + }, + { + "module": "openhcs.runtime.zmq_execution_server", + "owner": "ZMQExecutionServer", + "method": "_initialize_orchestrator", + "descriptor_kind": "function" + }, + { + "module": "openhcs.runtime.zmq_execution_server", + "owner": "ZMQExecutionServer", + "method": "_export_runtime_observation", + "descriptor_kind": "function" + }, + { + "module": "openhcs.interop.cellprofiler.runtime.function_contract_execution", + "owner": "CellProfilerFunctionContractExecutor", + "method": "execute", + "descriptor_kind": "function" + }, + { + "module": "openhcs.interop.cellprofiler.runtime.measurement_execution_support", + "owner": "ObjectMeasurementOutputRecorder", + "method": "record", + "descriptor_kind": "function" + }, + { + "module": "openhcs.interop.cellprofiler.runtime.output_recording", + "owner": "CellProfilerOutputRecorder", + "method": "record_module_outputs", + "descriptor_kind": "classmethod" + }, + { + "module": "openhcs.interop.cellprofiler.runtime.module_execution", + "owner": "CellProfilerModuleExecutor", + "method": "__call__", + "descriptor_kind": "function" + }, + { + "module": "openhcs.interop.cellprofiler.runtime.module_execution", + "owner": "CellProfilerModuleExecutor", + "method": "_image_request", + "descriptor_kind": "function" + } + ], + "profiling_enabled": false, + "runtime_profiling_enabled": false, + "diagnostic_errors": [], + "captures": false, + "scope": "coarse same-run nested exclusive timing partition on the exact source revision recorded in this region. Diagnostic wrappers, no scientific or memory-observer substitutions. Not accepted performance comparison. Declared callable boundary includes filtering/decorated processing/metadata; not pure numerical kernels. Both existing profilers are disabled. These wrappers still add diagnostic overhead; no scaling to uninstrumented clocks or causal performance claims. Query boundary is nested under its actual parent and excluded from parent-exclusive elapsed." +} diff --git a/benchmark/results/perf_rejected_metadata_owner_20261003/baseline-execution-region.json b/benchmark/results/perf_rejected_metadata_owner_20261003/baseline-execution-region.json new file mode 100644 index 0000000000..642dc4c637 --- /dev/null +++ b/benchmark/results/perf_rejected_metadata_owner_20261003/baseline-execution-region.json @@ -0,0 +1,714 @@ +{ + "pid": 1677824, + "thread": 140643152090816, + "region": "ZMQExecutionServer._execute_with_orchestrator", + "root_dimensions": "{\"case\": \"3d_monolayer_final\", \"compile_artifact_id\": \"9b5738f2-c0e7-413c-9d58-7aae0b6af48d\", \"execution_id\": \"c1c82c64-ace1-4d05-a31f-e33c194659b3\", \"kind\": \"execution\", \"plate\": \"/home/ts/.cache/openhcs/benchmark_datasets/CellProfiler_tutorials/data/3d_monolayer/images\"}", + "seconds": 8.26425674976781, + "revision": "e8587bd7eb0c4aa283bc300c5eb9f2294f24890f", + "source": "/home/ts/code/projects/openhcs-shared-runtime-plumbing", + "stats": { + "ZMQExecutionServer._initialize_orchestrator": { + "owner": "ZMQExecutionServer._initialize_orchestrator", + "dimensions": "", + "phase": "_initialize_orchestrator", + "calls": 1, + "inclusive": 0.08023449825122952, + "exclusive": 0.08023449825122952 + }, + "RuntimeImageStackCache.get": { + "owner": "RuntimeImageStackCache.get", + "dimensions": "", + "phase": "get", + "calls": 32, + "inclusive": 0.0003536404110491276, + "exclusive": 0.0003536404110491276 + }, + "stack_image_payload_context": { + "owner": "stack_image_payload_context", + "dimensions": "", + "phase": "stack_image_payload_context", + "calls": 28, + "inclusive": 0.04427314409986138, + "exclusive": 0.04427314409986138 + }, + "PatternGroupRuntime._load_input_stack": { + "owner": "PatternGroupRuntime._load_input_stack", + "dimensions": "", + "phase": "_load_input_stack", + "calls": 32, + "inclusive": 0.8063831464387476, + "exclusive": 0.7971521429717541 + }, + "CellProfilerModuleExecutor._image_request": { + "owner": "CellProfilerModuleExecutor._image_request", + "dimensions": "", + "phase": "_image_request", + "calls": 30, + "inclusive": 0.2785968668758869, + "exclusive": 0.2785968668758869 + }, + "RuntimeCallableInvocation.call | function=openhcs.processing.backends.cellprofiler.intensity.rescale_intensity;view=1": { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.intensity.rescale_intensity;view=1", + "phase": "declared_callable_boundary", + "calls": 2, + "inclusive": 0.013981282711029053, + "exclusive": 0.013981282711029053 + }, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.intensity.rescale_intensity": { + "owner": "CellProfilerFunctionContractExecutor.execute", + "dimensions": "function=openhcs.processing.backends.cellprofiler.intensity.rescale_intensity", + "phase": "execute", + "calls": 2, + "inclusive": 0.01470567611977458, + "exclusive": 0.0007243934087455273 + }, + "CellProfilerOutputRecorder.record_module_outputs": { + "owner": "CellProfilerOutputRecorder.record_module_outputs", + "dimensions": "", + "phase": "record_module_outputs", + "calls": 26, + "inclusive": 0.3608029861934483, + "exclusive": 0.3608029861934483 + }, + "CellProfilerModuleExecutor.__call__": { + "owner": "CellProfilerModuleExecutor.__call__", + "dimensions": "", + "phase": "__call__", + "calls": 32, + "inclusive": 4.476683502551168, + "exclusive": 0.12689677299931645 + }, + "FunctionCoreExecutor.execute | function=rescale_intensity": { + "owner": "FunctionCoreExecutor.execute", + "dimensions": "function=rescale_intensity", + "phase": "execute", + "calls": 2, + "inclusive": 0.10122189205139875, + "exclusive": 0.00037510739639401436 + }, + "PatternGroupRuntime._validate_and_unstack": { + "owner": "PatternGroupRuntime._validate_and_unstack", + "dimensions": "", + "phase": "_validate_and_unstack", + "calls": 28, + "inclusive": 0.2180978236719966, + "exclusive": 0.18270204262807965 + }, + "RuntimeImageStackCache.store": { + "owner": "RuntimeImageStackCache.store", + "dimensions": "", + "phase": "store", + "calls": 28, + "inclusive": 0.00022991374135017395, + "exclusive": 0.00022991374135017395 + }, + "PatternGroupRuntime._save_outputs": { + "owner": "PatternGroupRuntime._save_outputs", + "dimensions": "", + "phase": "_save_outputs", + "calls": 28, + "inclusive": 0.656574668828398, + "exclusive": 0.6563447550870478 + }, + "finalize_function_step_outputs": { + "owner": "finalize_function_step_outputs", + "dimensions": "", + "phase": "finalize_function_step_outputs", + "calls": 30, + "inclusive": 0.7433224944397807, + "exclusive": 0.20995302312076092 + }, + "RuntimeCallableInvocation.call | function=openhcs.processing.backends.cellprofiler.image_geometry.resize_volumetric;view=1": { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.image_geometry.resize_volumetric;view=1", + "phase": "declared_callable_boundary", + "calls": 3, + "inclusive": 0.1333684236742556, + "exclusive": 0.1333684236742556 + }, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.image_geometry.resize_volumetric": { + "owner": "CellProfilerFunctionContractExecutor.execute", + "dimensions": "function=openhcs.processing.backends.cellprofiler.image_geometry.resize_volumetric", + "phase": "execute", + "calls": 3, + "inclusive": 0.13415974332019687, + "exclusive": 0.0007913196459412575 + }, + "FunctionCoreExecutor.execute | function=resize_volumetric": { + "owner": "FunctionCoreExecutor.execute", + "dimensions": "function=resize_volumetric", + "phase": "execute", + "calls": 3, + "inclusive": 0.19819467328488827, + "exclusive": 0.0007455325685441494 + }, + "RuntimeCallableInvocation.call | function=openhcs.processing.backends.cellprofiler.median_filter.medianfilter;view=1": { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.median_filter.medianfilter;view=1", + "phase": "declared_callable_boundary", + "calls": 1, + "inclusive": 0.4704591277986765, + "exclusive": 0.4704591277986765 + }, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.median_filter.medianfilter": { + "owner": "CellProfilerFunctionContractExecutor.execute", + "dimensions": "function=openhcs.processing.backends.cellprofiler.median_filter.medianfilter", + "phase": "execute", + "calls": 1, + "inclusive": 0.4713046411052346, + "exclusive": 0.0008455133065581322 + }, + "FunctionCoreExecutor.execute | function=medianfilter": { + "owner": "FunctionCoreExecutor.execute", + "dimensions": "function=medianfilter", + "phase": "execute", + "calls": 1, + "inclusive": 0.501043249387294, + "exclusive": 0.0009696455672383308 + }, + "RuntimeCallableInvocation.call | function=openhcs.processing.backends.cellprofiler.thresholding.threshold;view=1": { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.thresholding.threshold;view=1", + "phase": "declared_callable_boundary", + "calls": 3, + "inclusive": 0.4494844200089574, + "exclusive": 0.4494844200089574 + }, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.thresholding.threshold": { + "owner": "CellProfilerFunctionContractExecutor.execute", + "dimensions": "function=openhcs.processing.backends.cellprofiler.thresholding.threshold", + "phase": "execute", + "calls": 3, + "inclusive": 0.4505709954537451, + "exclusive": 0.0010865754447877407 + }, + "FunctionCoreExecutor.execute | function=threshold": { + "owner": "FunctionCoreExecutor.execute", + "dimensions": "function=threshold", + "phase": "execute", + "calls": 3, + "inclusive": 0.5448138085193932, + "exclusive": 0.003549575340002775 + }, + "RuntimeCallableInvocation.call | function=openhcs.processing.backends.cellprofiler.morphology.remove_holes_3d;view=1": { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.morphology.remove_holes_3d;view=1", + "phase": "declared_callable_boundary", + "calls": 2, + "inclusive": 0.1073335069231689, + "exclusive": 0.1073335069231689 + }, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.morphology.remove_holes_3d": { + "owner": "CellProfilerFunctionContractExecutor.execute", + "dimensions": "function=openhcs.processing.backends.cellprofiler.morphology.remove_holes_3d", + "phase": "execute", + "calls": 2, + "inclusive": 0.10756895132362843, + "exclusive": 0.00023544440045952797 + }, + "FunctionCoreExecutor.execute | function=remove_holes_3d": { + "owner": "FunctionCoreExecutor.execute", + "dimensions": "function=remove_holes_3d", + "phase": "execute", + "calls": 2, + "inclusive": 0.11626771930605173, + "exclusive": 0.0005331146530807018 + }, + "RuntimeCallableInvocation.call | function=openhcs.processing.backends.cellprofiler.watershed.watershed_cellprofiler4;view=1": { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.watershed.watershed_cellprofiler4;view=1", + "phase": "declared_callable_boundary", + "calls": 2, + "inclusive": 0.4769804459065199, + "exclusive": 0.4769804459065199 + }, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.watershed.watershed_cellprofiler4": { + "owner": "CellProfilerFunctionContractExecutor.execute", + "dimensions": "function=openhcs.processing.backends.cellprofiler.watershed.watershed_cellprofiler4", + "phase": "execute", + "calls": 2, + "inclusive": 0.47757716896012425, + "exclusive": 0.0005967230536043644 + }, + "FunctionCoreExecutor.execute | function=watershed_cellprofiler4": { + "owner": "FunctionCoreExecutor.execute", + "dimensions": "function=watershed_cellprofiler4", + "phase": "execute", + "calls": 2, + "inclusive": 0.5291692218743265, + "exclusive": 0.003626206424087286 + }, + "RuntimeCallableInvocation.call | function=openhcs.processing.backends.cellprofiler.morphology.resize_objects_3d;view=1": { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.morphology.resize_objects_3d;view=1", + "phase": "declared_callable_boundary", + "calls": 2, + "inclusive": 0.13164904806762934, + "exclusive": 0.13164904806762934 + }, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.morphology.resize_objects_3d": { + "owner": "CellProfilerFunctionContractExecutor.execute", + "dimensions": "function=openhcs.processing.backends.cellprofiler.morphology.resize_objects_3d", + "phase": "execute", + "calls": 2, + "inclusive": 0.132229870185256, + "exclusive": 0.000580822117626667 + }, + "FunctionCoreExecutor.execute | function=resize_objects_3d": { + "owner": "FunctionCoreExecutor.execute", + "dimensions": "function=resize_objects_3d", + "phase": "execute", + "calls": 2, + "inclusive": 0.15821923222392797, + "exclusive": 0.0009479965083301067 + }, + "RuntimeCallableInvocation.call | function=openhcs.processing.backends.cellprofiler.morphology.erode_objects;view=1": { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.morphology.erode_objects;view=1", + "phase": "declared_callable_boundary", + "calls": 1, + "inclusive": 0.056926466058939695, + "exclusive": 0.056926466058939695 + }, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.morphology.erode_objects": { + "owner": "CellProfilerFunctionContractExecutor.execute", + "dimensions": "function=openhcs.processing.backends.cellprofiler.morphology.erode_objects", + "phase": "execute", + "calls": 1, + "inclusive": 0.057448259089142084, + "exclusive": 0.0005217930302023888 + }, + "FunctionCoreExecutor.execute | function=erode_objects": { + "owner": "FunctionCoreExecutor.execute", + "dimensions": "function=erode_objects", + "phase": "execute", + "calls": 1, + "inclusive": 0.06930378871038556, + "exclusive": 0.0004508676938712597 + }, + "RuntimeCallableInvocation.call | function=openhcs.processing.backends.cellprofiler.object_images.convert_objects_to_image;view=1": { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.object_images.convert_objects_to_image;view=1", + "phase": "declared_callable_boundary", + "calls": 3, + "inclusive": 0.007349567022174597, + "exclusive": 0.007349567022174597 + }, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.object_images.convert_objects_to_image": { + "owner": "CellProfilerFunctionContractExecutor.execute", + "dimensions": "function=openhcs.processing.backends.cellprofiler.object_images.convert_objects_to_image", + "phase": "execute", + "calls": 3, + "inclusive": 0.00814954238012433, + "exclusive": 0.0007999753579497337 + }, + "FunctionCoreExecutor.execute | function=convert_objects_to_image": { + "owner": "FunctionCoreExecutor.execute", + "dimensions": "function=convert_objects_to_image", + "phase": "execute", + "calls": 3, + "inclusive": 0.03970423201099038, + "exclusive": 0.0010002129711210728 + }, + "RuntimeCallableInvocation.call | function=openhcs.processing.backends.cellprofiler.image_math.image_math;view=1": { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.image_math.image_math;view=1", + "phase": "declared_callable_boundary", + "calls": 120, + "inclusive": 0.0643215891905129, + "exclusive": 0.0643215891905129 + }, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.image_math.image_math": { + "owner": "CellProfilerFunctionContractExecutor.execute", + "dimensions": "function=openhcs.processing.backends.cellprofiler.image_math.image_math", + "phase": "execute", + "calls": 2, + "inclusive": 0.09143648901954293, + "exclusive": 0.027114899829030037 + }, + "FunctionCoreExecutor.execute | function=image_math": { + "owner": "FunctionCoreExecutor.execute", + "dimensions": "function=image_math", + "phase": "execute", + "calls": 2, + "inclusive": 0.3179855141788721, + "exclusive": 0.0005593728274106979 + }, + "RuntimeCallableInvocation.call | function=openhcs.processing.backends.cellprofiler.morphology.closing;view=1": { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.morphology.closing;view=1", + "phase": "declared_callable_boundary", + "calls": 1, + "inclusive": 0.2726345621049404, + "exclusive": 0.2726345621049404 + }, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.morphology.closing": { + "owner": "CellProfilerFunctionContractExecutor.execute", + "dimensions": "function=openhcs.processing.backends.cellprofiler.morphology.closing", + "phase": "execute", + "calls": 1, + "inclusive": 0.2727289702743292, + "exclusive": 9.440816938877106e-05 + }, + "FunctionCoreExecutor.execute | function=closing": { + "owner": "FunctionCoreExecutor.execute", + "dimensions": "function=closing", + "phase": "execute", + "calls": 1, + "inclusive": 0.2932825358584523, + "exclusive": 0.00026117172092199326 + }, + "RuntimeCallableInvocation.call | function=openhcs.processing.backends.cellprofiler.image_geometry.mask_image;view=1": { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.image_geometry.mask_image;view=1", + "phase": "declared_callable_boundary", + "calls": 60, + "inclusive": 0.017262772656977177, + "exclusive": 0.017262772656977177 + }, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.image_geometry.mask_image": { + "owner": "CellProfilerFunctionContractExecutor.execute", + "dimensions": "function=openhcs.processing.backends.cellprofiler.image_geometry.mask_image", + "phase": "execute", + "calls": 1, + "inclusive": 0.0316482950001955, + "exclusive": 0.014385522343218327 + }, + "FunctionCoreExecutor.execute | function=mask_image": { + "owner": "FunctionCoreExecutor.execute", + "dimensions": "function=mask_image", + "phase": "execute", + "calls": 1, + "inclusive": 0.053500546142458916, + "exclusive": 0.00037759123370051384 + }, + "RuntimeCallableInvocation.call | function=openhcs.processing.backends.cellprofiler.morphology.erode_image;view=1": { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.morphology.erode_image;view=1", + "phase": "declared_callable_boundary", + "calls": 1, + "inclusive": 0.028324457351118326, + "exclusive": 0.028324457351118326 + }, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.morphology.erode_image": { + "owner": "CellProfilerFunctionContractExecutor.execute", + "dimensions": "function=openhcs.processing.backends.cellprofiler.morphology.erode_image", + "phase": "execute", + "calls": 1, + "inclusive": 0.028424454852938652, + "exclusive": 9.999750182032585e-05 + }, + "FunctionCoreExecutor.execute | function=erode_image": { + "owner": "FunctionCoreExecutor.execute", + "dimensions": "function=erode_image", + "phase": "execute", + "calls": 1, + "inclusive": 0.03319434495642781, + "exclusive": 0.0002447301521897316 + }, + "RuntimeCallableInvocation.call | function=openhcs.processing.backends.cellprofiler.intensity.measure_object_intensity;view=1": { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.intensity.measure_object_intensity;view=1", + "phase": "declared_callable_boundary", + "calls": 4, + "inclusive": 0.6384402797557414, + "exclusive": 0.6384402797557414 + }, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.intensity.measure_object_intensity": { + "owner": "CellProfilerFunctionContractExecutor.execute", + "dimensions": "function=openhcs.processing.backends.cellprofiler.intensity.measure_object_intensity", + "phase": "execute", + "calls": 4, + "inclusive": 0.6391628603450954, + "exclusive": 0.0007225805893540382 + }, + "ObjectMeasurementOutputRecorder.record": { + "owner": "ObjectMeasurementOutputRecorder.record", + "dimensions": "", + "phase": "record", + "calls": 6, + "inclusive": 0.0037819184362888336, + "exclusive": 0.0037819184362888336 + }, + "FunctionCoreExecutor.execute | function=measure_object_intensity": { + "owner": "FunctionCoreExecutor.execute", + "dimensions": "function=measure_object_intensity", + "phase": "execute", + "calls": 4, + "inclusive": 0.7307992740534246, + "exclusive": 0.0043295701034367085 + }, + "RuntimeCallableInvocation.call | function=openhcs.processing.backends.cellprofiler.shape.measure_object_size_shape;view=1": { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.shape.measure_object_size_shape;view=1", + "phase": "declared_callable_boundary", + "calls": 2, + "inclusive": 0.4818841591477394, + "exclusive": 0.4818841591477394 + }, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.shape.measure_object_size_shape": { + "owner": "CellProfilerFunctionContractExecutor.execute", + "dimensions": "function=openhcs.processing.backends.cellprofiler.shape.measure_object_size_shape", + "phase": "execute", + "calls": 2, + "inclusive": 0.4831168404780328, + "exclusive": 0.001232681330293417 + }, + "FunctionCoreExecutor.execute | function=measure_object_size_shape": { + "owner": "FunctionCoreExecutor.execute", + "dimensions": "function=measure_object_size_shape", + "phase": "execute", + "calls": 2, + "inclusive": 0.4943189197219908, + "exclusive": 0.00211262796074152 + }, + "RuntimeCallableInvocation.call | function=openhcs.processing.backends.cellprofiler.outlines.overlay_objects;view=1": { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.outlines.overlay_objects;view=1", + "phase": "declared_callable_boundary", + "calls": 2, + "inclusive": 0.3061672244220972, + "exclusive": 0.3061672244220972 + }, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.outlines.overlay_objects": { + "owner": "CellProfilerFunctionContractExecutor.execute", + "dimensions": "function=openhcs.processing.backends.cellprofiler.outlines.overlay_objects", + "phase": "execute", + "calls": 2, + "inclusive": 0.3063722001388669, + "exclusive": 0.00020497571676969528 + }, + "FunctionCoreExecutor.execute | function=overlay_objects": { + "owner": "FunctionCoreExecutor.execute", + "dimensions": "function=overlay_objects", + "phase": "execute", + "calls": 2, + "inclusive": 0.32670215563848615, + "exclusive": 0.010954282246530056 + }, + "RuntimeCallableInvocation.call | function=openhcs.processing.backends.cellprofiler.save_images.save_images;view=1": { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.save_images.save_images;view=1", + "phase": "declared_callable_boundary", + "calls": 2, + "inclusive": 0.0001477571204304695, + "exclusive": 0.0001477571204304695 + }, + "FunctionCoreExecutor.execute | function=save_images": { + "owner": "FunctionCoreExecutor.execute", + "dimensions": "function=save_images", + "phase": "execute", + "calls": 2, + "inclusive": 0.16580111486837268, + "exclusive": 0.1656533577479422 + }, + "AtomicMetadataWriter.publish_source_projection_metadata": { + "owner": "AtomicMetadataWriter.publish_source_projection_metadata", + "dimensions": "", + "phase": "publish_source_projection_metadata", + "calls": 5, + "inclusive": 0.7686110232025385, + "exclusive": 0.7686110232025385 + }, + "execute_worker_lane | {\"execution_id\": \"c1c82c64-ace1-4d05-a31f-e33c194659b3\", \"kind\": \"worker_lane\", \"plate\": \"/home/ts/.cache/openhcs/benchmark_datasets/CellProfiler_tutorials/data/3d_monolayer/images\", \"worker_slot\": \"worker_0\"}": { + "owner": "execute_worker_lane", + "dimensions": "{\"execution_id\": \"c1c82c64-ace1-4d05-a31f-e33c194659b3\", \"kind\": \"worker_lane\", \"plate\": \"/home/ts/.cache/openhcs/benchmark_datasets/CellProfiler_tutorials/data/3d_monolayer/images\", \"worker_slot\": \"worker_0\"}", + "phase": "execute_worker_lane", + "calls": 1, + "inclusive": 7.582170368172228, + "exclusive": 0.4842700120061636 + }, + "consolidate_analysis_outputs": { + "owner": "consolidate_analysis_outputs", + "dimensions": "", + "phase": "consolidate_analysis_outputs", + "calls": 1, + "inclusive": 1.02287158370018e-05, + "exclusive": 1.02287158370018e-05 + }, + "RuntimeArtifactMetadataTarget.reconciliation_targets": { + "owner": "RuntimeArtifactMetadataTarget.reconciliation_targets", + "dimensions": "", + "phase": "reconciliation_targets", + "calls": 1, + "inclusive": 0.3009079131297767, + "exclusive": 0.3009079131297767 + }, + "OpenHCSMetadataWriter.finalize_completed_plate": { + "owner": "OpenHCSMetadataWriter.finalize_completed_plate", + "dimensions": "", + "phase": "finalize_completed_plate", + "calls": 1, + "inclusive": 0.5221393280662596, + "exclusive": 0.00812348397448659 + }, + "ZMQExecutionServer._export_runtime_observation": { + "owner": "ZMQExecutionServer._export_runtime_observation", + "dimensions": "", + "phase": "_export_runtime_observation", + "calls": 1, + "inclusive": 0.007673155982047319, + "exclusive": 0.007673155982047319 + }, + "ZMQExecutionServer._execute_with_orchestrator | {\"case\": \"3d_monolayer_final\", \"compile_artifact_id\": \"9b5738f2-c0e7-413c-9d58-7aae0b6af48d\", \"execution_id\": \"c1c82c64-ace1-4d05-a31f-e33c194659b3\", \"kind\": \"execution\", \"plate\": \"/home/ts/.cache/openhcs/benchmark_datasets/CellProfiler_tutorials/data/3d_monolayer/images\"}": { + "owner": "ZMQExecutionServer._execute_with_orchestrator", + "dimensions": "{\"case\": \"3d_monolayer_final\", \"compile_artifact_id\": \"9b5738f2-c0e7-413c-9d58-7aae0b6af48d\", \"execution_id\": \"c1c82c64-ace1-4d05-a31f-e33c194659b3\", \"kind\": \"execution\", \"plate\": \"/home/ts/.cache/openhcs/benchmark_datasets/CellProfiler_tutorials/data/3d_monolayer/images\"}", + "phase": "_execute_with_orchestrator", + "calls": 1, + "inclusive": 8.26425674976781, + "exclusive": 0.04989554965868592 + } + }, + "hooks_installed": [ + { + "module": "openhcs.core.aligned_image_payload", + "owner": "", + "method": "stack_image_payload_context", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.aligned_image_payload", + "owner": "", + "method": "unstack_image_payload_context", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.measurement_feature_queries", + "owner": "MeasurementFeatureValueIndex", + "method": "from_columnar_table_by_object", + "descriptor_kind": "classmethod" + }, + { + "module": "openhcs.core.virtual_workspace_metadata", + "owner": "AtomicMetadataWriter", + "method": "publish_source_projection_metadata", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.runtime_stack_cache", + "owner": "RuntimeImageStackCache", + "method": "get", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.runtime_stack_cache", + "owner": "RuntimeImageStackCache", + "method": "store", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.steps.function_outputs", + "owner": "", + "method": "finalize_function_step_outputs", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.steps.function_outputs", + "owner": "OpenHCSMetadataWriter", + "method": "finalize_completed_plate", + "descriptor_kind": "classmethod" + }, + { + "module": "openhcs.core.steps.function_outputs", + "owner": "RuntimeArtifactMetadataTarget", + "method": "reconciliation_targets", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.steps.function_runtime", + "owner": "FunctionCoreExecutor", + "method": "execute", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.steps.function_runtime", + "owner": "PatternGroupRuntime", + "method": "_load_input_stack", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.steps.function_runtime", + "owner": "PatternGroupRuntime", + "method": "_validate_and_unstack", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.steps.function_runtime", + "owner": "PatternGroupRuntime", + "method": "_save_outputs", + "descriptor_kind": "function" + }, + { + "module": "openhcs.processing.backends.lib_registry.unified_registry", + "owner": "RuntimeCallableInvocation", + "method": "call", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.orchestrator.analysis_consolidation", + "owner": "", + "method": "consolidate_analysis_outputs", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.orchestrator.worker_execution", + "owner": "", + "method": "execute_worker_lane", + "descriptor_kind": "function" + }, + { + "module": "openhcs.runtime.zmq_execution_server", + "owner": "ZMQExecutionServer", + "method": "_execute_with_orchestrator", + "descriptor_kind": "function" + }, + { + "module": "openhcs.runtime.zmq_execution_server", + "owner": "ZMQExecutionServer", + "method": "_initialize_orchestrator", + "descriptor_kind": "function" + }, + { + "module": "openhcs.runtime.zmq_execution_server", + "owner": "ZMQExecutionServer", + "method": "_export_runtime_observation", + "descriptor_kind": "function" + }, + { + "module": "openhcs.interop.cellprofiler.runtime.function_contract_execution", + "owner": "CellProfilerFunctionContractExecutor", + "method": "execute", + "descriptor_kind": "function" + }, + { + "module": "openhcs.interop.cellprofiler.runtime.measurement_execution_support", + "owner": "ObjectMeasurementOutputRecorder", + "method": "record", + "descriptor_kind": "function" + }, + { + "module": "openhcs.interop.cellprofiler.runtime.output_recording", + "owner": "CellProfilerOutputRecorder", + "method": "record_module_outputs", + "descriptor_kind": "classmethod" + }, + { + "module": "openhcs.interop.cellprofiler.runtime.module_execution", + "owner": "CellProfilerModuleExecutor", + "method": "__call__", + "descriptor_kind": "function" + }, + { + "module": "openhcs.interop.cellprofiler.runtime.module_execution", + "owner": "CellProfilerModuleExecutor", + "method": "_image_request", + "descriptor_kind": "function" + } + ], + "profiling_enabled": false, + "runtime_profiling_enabled": false, + "diagnostic_errors": [], + "captures": false, + "scope": "coarse same-run nested exclusive timing partition on the exact source revision recorded in this region. Diagnostic wrappers, no scientific or memory-observer substitutions. Not accepted performance comparison. Declared callable boundary includes filtering/decorated processing/metadata; not pure numerical kernels. Both existing profilers are disabled. These wrappers still add diagnostic overhead; no scaling to uninstrumented clocks or causal performance claims. Query boundary is nested under its actual parent and excluded from parent-exclusive elapsed." +} diff --git a/benchmark/results/perf_rejected_metadata_owner_20261003/baseline-observations.json b/benchmark/results/perf_rejected_metadata_owner_20261003/baseline-observations.json new file mode 100644 index 0000000000..37736f25f1 --- /dev/null +++ b/benchmark/results/perf_rejected_metadata_owner_20261003/baseline-observations.json @@ -0,0 +1,48 @@ +{ + "returncode": 0, + "diagnostic_admission": true, + "public_rows": [ + { + "case_name": "cp_tutorial_3d_monolayer", + "mode_name": "1w_1t", + "worker_count": "1", + "well_count": "1", + "compile_seconds": "1.5337145328521729", + "prepare_seconds": "0.0", + "execute_seconds": "8.276516675949097", + "total_seconds": "10.606972666922957", + "wells_per_second": "0.12082377637272465", + "successful_wells": "1", + "native_single_sample_execution_seconds": "", + "projected_native_execution_seconds": "", + "projected_execution_speedup": "", + "peak_memory_mb": "1692.34375", + "status": "success", + "memory_limit_mb": "", + "error_message": "", + "execution_route": "ordinary-zmq-outcomes-v1", + "server_lifecycle": "reused-per-sweep", + "run_input_sha256": "7f66dd478c9a56033495f056335a0eaadb9fd5facd953b504f0e1f8f7eaa57d5" + } + ], + "regions": [ + { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/regions/region-1677824-140643152090816-0001.json", + "sha256": "d4cf4157156de4e7a41949e36ee4daed69665ea7b8c6e2ddf5b00613300b6e5a" + }, + { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/regions/region-1677824-140643152090816-0002.json", + "sha256": "eda84c30eb19709e0fc37a35550a0e5a8e27e97ddd818e9296904203cd9e9e0b" + } + ], + "root_sequence_complete": true, + "actual_index_calls": 0, + "actual_declared_calls": 211, + "retained_json_bytes": 38057, + "generated_output_bytes": 18163301, + "public_log_bytes": 538, + "profiler_artifacts_absent": true, + "source_env_native_input_freeze": "PASS", + "science": "NOT_RUN: original full saved-output126file/schema/discrete/numerical/nativeinput witness separate gate. No exclusions or missing outputs waived.", + "scope": "One source-pinned whole ordinary3D Monolayer1w1t candidate falsification with24 unchanged coarse nominal hooks: inherited23 plus actual MeasurementFeatureValueIndex index classmethod. cProfile and runtime profiler OFF, no captures/percell hooks. Existing defaultOUTCOMES/memory observers/READY clocks unchanged. Same-thread nested exclusive clocks exclude query from its real ancestors; worker independent roots are not added to parent waits. Nearordinary diagnostic only; wrapper overhead remains, no scale factors/causal performance promotion. Full saved126file (6CSV120TIFF) science/native source witness independently required." +} diff --git a/benchmark/results/perf_rejected_metadata_owner_20261003/baseline-source-pins.json b/benchmark/results/perf_rejected_metadata_owner_20261003/baseline-source-pins.json new file mode 100644 index 0000000000..a3450e2f7c --- /dev/null +++ b/benchmark/results/perf_rejected_metadata_owner_20261003/baseline-source-pins.json @@ -0,0 +1,116 @@ +{ + "revision": "e8587bd7eb0c4aa283bc300c5eb9f2294f24890f", + "source": "/home/ts/code/projects/openhcs-shared-runtime-plumbing", + "physical_inputs": { + "dataset_path": { + "path": "/home/ts/.cache/openhcs/benchmark_datasets/CellProfiler_tutorials/data/3d_monolayer/images", + "files": { + "3d_monolayer_xy1_ch0.tif": "63114f614fec95664f718a39cddac58449f0edb5cc31b57584e4ba5b1f4994c7", + "3d_monolayer_xy1_ch1.tif": "a7d9abf632850875c22613c4b740fc01153cf60e9773c16d4a670a97b46e5751", + "3d_monolayer_xy1_ch2.tif": "191754bc7f43856223180483e2737ea93bab4d0cf98b1b2a4c27425e9908147d" + } + }, + "cppipe_path": { + "path": "/home/ts/.cache/openhcs/benchmark_datasets/CellProfiler_tutorials/data/3d_monolayer/3d_monolayer_final.cppipe", + "files": { + "3d_monolayer_final.cppipe": "9fcd24a1be8372b5c2e87637a055032c3362b9ad8729e361095f93d8bbe71838" + } + } + }, + "shared_dependency_heads": "b0004daf3223326ac78689ba7b2e68abc18ae298 external/ObjectState (v1.1.8)\n 84f322e46871de5ed47e7fd20976ad03e533c1c4 external/PolyStore (v0.2.19-34-g84f322e)\n 1e53d03d9f468322a1c085c8be29485fb139caf2 external/arraybridge (v0.3.6)\n 393a7e03003cdc56df9013f932ed4f26e632d77a external/metaclass-registry (v0.2.2)\n 792b03413331fdf107e369c924c257a2f0b16126 external/pycodify (v0.1.3)\n ad4948775ab81180a354d4b793d17ee5ddff3972 external/pyqt-reactive (v0.3.24-9-gad49487)\n 4e8f8bde5f750d9a50eacac80acd3806123addd8 external/python-introspect (v0.1.14-1-g4e8f8bd)\n 668edafcb5ee530a163377c936431ae1e8334e85 external/zmqruntime (v0.2.24-40-g668edaf)", + "native_binaries": { + "openhcs/core/_tabular_native.abi3.so": { + "path": "/home/ts/code/projects/openhcs-shared-runtime-plumbing/openhcs/core/_tabular_native.abi3.so", + "sha256": "2ce348417de70ad3f614962c82aed9c4977c32b3ba00e13531ecafb8c10d8d1d" + }, + "openhcs/processing/backends/cellprofiler/_granularity_native.abi3.so": { + "path": "/home/ts/code/projects/openhcs-shared-runtime-plumbing/openhcs/processing/backends/cellprofiler/_granularity_native.abi3.so", + "sha256": "6e45987a50d5c0588a8fb0b09a2232a63e72c122d6a3d583ae4d62830e3b4096" + } + }, + "python_binary": { + "path": "/home/ts/.local/share/uv/python/cpython-3.12.14-linux-x86_64-gnu/bin/python3.12", + "sha256": "edffa803e5c4d73a1357f7f2c6c9fd38e9693712b2c06793e6a5e5cc1090d03a" + }, + "installed_versions": { + "python": "3.12.14 (main, Sep 24 2026, 17:57:43) [Clang 22.1.3 ]", + "versions": { + "openhcs": "0.8.7", + "numpy": "2.5.3", + "scipy": "1.18.1", + "arraybridge": "0.3.6", + "polystore": "0.3.0", + "python-introspect": "0.1.15", + "metaclass-registry": "0.2.2", + "zmqruntime": "0.3.0" + } + }, + "installed_freeze_sha256": "b7e5bbb9b5171476dfeb1692378419bfa157bb620028e1b91c8969eef2498b41", + "site_sha256": "391dc438b0d9d253e3467674c19d37b2670d5d7197400aa45f3c251e84f948c0", + "ordinary_helper_sha256": "8a41db448a722d231777a90a3ae23091617c8ac1ba72cefcb91f72b2c7975554", + "driver_sha256": "73b30d0cc3174c04cb479a72e97fe094ef6ad9fcfdacbfb37e43fbaad53560c3", + "manifest_sha256": "4c302e136b427cb9968eef82d7ede4a6c5b65cbcb4f8e5490fd5125c2e7ab2a8", + "ledger_controller_sha256": "c140695473eadb961b245c24344acc79778904920ba3725945ec9235354fbb9b", + "command": [ + "/usr/bin/taskset", + "-c", + "5", + "/home/ts/code/projects/openhcs/.venv/bin/python", + "-c", + "import runpy,sys;from dataclasses import replace;import openhcs.runtime.zmq_config as configuration;configuration.OPENHCS_ZMQ_CONFIG=replace(configuration.OPENHCS_ZMQ_CONFIG,client_connect_timeout_seconds=180.0);sys.argv=sys.argv[1:];runpy.run_path(sys.argv[0],run_name='__main__')", + "/home/ts/code/projects/openhcs-shared-runtime-plumbing/scripts/benchmark_cppipe_well_throughput.py", + "--manifest", + "/home/ts/code/projects/openhcs-shared-runtime-plumbing/benchmark/manifests/official30_portable_axis1.json", + "--mode", + "1w_1t", + "--output-dir", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public", + "--case", + "cp_tutorial_3d_monolayer" + ], + "scope": "One source-pinned whole ordinary3D Monolayer1w1t candidate falsification with24 unchanged coarse nominal hooks: inherited23 plus actual MeasurementFeatureValueIndex index classmethod. cProfile and runtime profiler OFF, no captures/percell hooks. Existing defaultOUTCOMES/memory observers/READY clocks unchanged. Same-thread nested exclusive clocks exclude query from its real ancestors; worker independent roots are not added to parent waits. Nearordinary diagnostic only; wrapper overhead remains, no scale factors/causal performance promotion. Full saved126file (6CSV120TIFF) science/native source witness independently required.", + "original_full_freeze_path": "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/source-freeze.json", + "original_full_freeze_sha256": "55ee41e2e0f09553de9c2be7dab5beedf0f4f2baf7245f2778c2dcedc38a092e", + "actual_import_dependency_pins": { + "external/ObjectState": { + "head": "a01f8939b9bf4659502a33fe3febe62cab0f3c9a", + "status": "", + "full_tracked_source_sha256": "511b83d27f0c0cffe973cb16e0cfce016405250185000d909e41f6343472dca1" + }, + "external/python-introspect": { + "head": "ed1e4eb15f7a0bf8315274d3e47858fa2b4372bb", + "status": "", + "full_tracked_source_sha256": "74b3a98b0ed51d505b50e828c5096aaab8d019d18b7619e7dfdcb0f923a54f7d" + }, + "external/arraybridge": { + "head": "1e53d03d9f468322a1c085c8be29485fb139caf2", + "status": "", + "full_tracked_source_sha256": "cc3300a4462a89363003190d00228618c8bc33545d445ae72359c1fb7440a931" + }, + "external/metaclass-registry": { + "head": "393a7e03003cdc56df9013f932ed4f26e632d77a", + "status": "", + "full_tracked_source_sha256": "a660ae17d4e4c03f5298ff8c2a8183635b53bf24558be6ff69b435476461e6bc" + }, + "external/zmqruntime": { + "head": "668edafcb5ee530a163377c936431ae1e8334e85", + "status": "", + "full_tracked_source_sha256": "bd020f39e177c038aa31d4579090694aede119aee668029a039dba249d311ec7" + }, + "external/pycodify": { + "head": "792b03413331fdf107e369c924c257a2f0b16126", + "status": "", + "full_tracked_source_sha256": "66f24bee56cbccc5baa5b1347a0c27bd6c9a6cc13598f679adba9668f2b54f3a" + }, + "external/PolyStore": { + "head": "84f322e46871de5ed47e7fd20976ad03e533c1c4", + "status": "", + "full_tracked_source_sha256": "47d2c21a32560e9d22581017e569846fb17f4f88a82a8ade0352eabaedea738b" + }, + "external/pyqt-reactive": { + "head": "ad4948775ab81180a354d4b793d17ee5ddff3972", + "status": "", + "full_tracked_source_sha256": "968cc99264a04019b724c11b47d53976f0f1341c50c8a86366aa7d3ef1ffe6fd" + } + } +} diff --git a/benchmark/results/perf_rejected_metadata_owner_20261003/candidate-compile_only-region.json b/benchmark/results/perf_rejected_metadata_owner_20261003/candidate-compile_only-region.json new file mode 100644 index 0000000000..aef19eceaa --- /dev/null +++ b/benchmark/results/perf_rejected_metadata_owner_20261003/candidate-compile_only-region.json @@ -0,0 +1,178 @@ +{ + "pid": 1697815, + "thread": 139997494724288, + "region": "ZMQExecutionServer._execute_with_orchestrator", + "root_dimensions": "{\"case\": \"3d_monolayer_final\", \"compile_artifact_id\": null, \"execution_id\": \"f3a82019-5257-4583-940c-4dfd40925dfb\", \"kind\": \"compile_only\", \"plate\": \"/home/ts/.cache/openhcs/benchmark_datasets/CellProfiler_tutorials/data/3d_monolayer/images\"}", + "seconds": 1.2573879919946194, + "revision": "9eee98265c939859d49f0c60bd5b3a2168cb29fe", + "source": "/home/ts/code/projects/openhcs-shared-runtime-plumbing", + "stats": { + "ZMQExecutionServer._initialize_orchestrator": { + "owner": "ZMQExecutionServer._initialize_orchestrator", + "dimensions": "", + "phase": "_initialize_orchestrator", + "calls": 1, + "inclusive": 0.12586374813690782, + "exclusive": 0.12586374813690782 + }, + "ZMQExecutionServer._execute_with_orchestrator | {\"case\": \"3d_monolayer_final\", \"compile_artifact_id\": null, \"execution_id\": \"f3a82019-5257-4583-940c-4dfd40925dfb\", \"kind\": \"compile_only\", \"plate\": \"/home/ts/.cache/openhcs/benchmark_datasets/CellProfiler_tutorials/data/3d_monolayer/images\"}": { + "owner": "ZMQExecutionServer._execute_with_orchestrator", + "dimensions": "{\"case\": \"3d_monolayer_final\", \"compile_artifact_id\": null, \"execution_id\": \"f3a82019-5257-4583-940c-4dfd40925dfb\", \"kind\": \"compile_only\", \"plate\": \"/home/ts/.cache/openhcs/benchmark_datasets/CellProfiler_tutorials/data/3d_monolayer/images\"}", + "phase": "_execute_with_orchestrator", + "calls": 1, + "inclusive": 1.2573879919946194, + "exclusive": 1.1315242438577116 + } + }, + "hooks_installed": [ + { + "module": "openhcs.core.aligned_image_payload", + "owner": "", + "method": "stack_image_payload_context", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.aligned_image_payload", + "owner": "", + "method": "unstack_image_payload_context", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.measurement_feature_queries", + "owner": "MeasurementFeatureValueIndex", + "method": "from_columnar_table_by_object", + "descriptor_kind": "classmethod" + }, + { + "module": "openhcs.core.virtual_workspace_metadata", + "owner": "AtomicMetadataWriter", + "method": "publish_source_projection_metadata", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.runtime_stack_cache", + "owner": "RuntimeImageStackCache", + "method": "get", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.runtime_stack_cache", + "owner": "RuntimeImageStackCache", + "method": "store", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.steps.function_outputs", + "owner": "", + "method": "finalize_function_step_outputs", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.steps.function_outputs", + "owner": "OpenHCSMetadataWriter", + "method": "finalize_completed_plate", + "descriptor_kind": "classmethod" + }, + { + "module": "openhcs.core.steps.function_outputs", + "owner": "RuntimeArtifactMetadataTarget", + "method": "reconciliation_targets", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.steps.function_runtime", + "owner": "FunctionCoreExecutor", + "method": "execute", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.steps.function_runtime", + "owner": "PatternGroupRuntime", + "method": "_load_input_stack", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.steps.function_runtime", + "owner": "PatternGroupRuntime", + "method": "_validate_and_unstack", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.steps.function_runtime", + "owner": "PatternGroupRuntime", + "method": "_save_outputs", + "descriptor_kind": "function" + }, + { + "module": "openhcs.processing.backends.lib_registry.unified_registry", + "owner": "RuntimeCallableInvocation", + "method": "call", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.orchestrator.analysis_consolidation", + "owner": "", + "method": "consolidate_analysis_outputs", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.orchestrator.worker_execution", + "owner": "", + "method": "execute_worker_lane", + "descriptor_kind": "function" + }, + { + "module": "openhcs.runtime.zmq_execution_server", + "owner": "ZMQExecutionServer", + "method": "_execute_with_orchestrator", + "descriptor_kind": "function" + }, + { + "module": "openhcs.runtime.zmq_execution_server", + "owner": "ZMQExecutionServer", + "method": "_initialize_orchestrator", + "descriptor_kind": "function" + }, + { + "module": "openhcs.runtime.zmq_execution_server", + "owner": "ZMQExecutionServer", + "method": "_export_runtime_observation", + "descriptor_kind": "function" + }, + { + "module": "openhcs.interop.cellprofiler.runtime.function_contract_execution", + "owner": "CellProfilerFunctionContractExecutor", + "method": "execute", + "descriptor_kind": "function" + }, + { + "module": "openhcs.interop.cellprofiler.runtime.measurement_execution_support", + "owner": "ObjectMeasurementOutputRecorder", + "method": "record", + "descriptor_kind": "function" + }, + { + "module": "openhcs.interop.cellprofiler.runtime.output_recording", + "owner": "CellProfilerOutputRecorder", + "method": "record_module_outputs", + "descriptor_kind": "classmethod" + }, + { + "module": "openhcs.interop.cellprofiler.runtime.module_execution", + "owner": "CellProfilerModuleExecutor", + "method": "__call__", + "descriptor_kind": "function" + }, + { + "module": "openhcs.interop.cellprofiler.runtime.module_execution", + "owner": "CellProfilerModuleExecutor", + "method": "_image_request", + "descriptor_kind": "function" + } + ], + "profiling_enabled": false, + "runtime_profiling_enabled": false, + "diagnostic_errors": [], + "captures": false, + "scope": "coarse same-run nested exclusive timing partition on the exact source revision recorded in this region. Diagnostic wrappers, no scientific or memory-observer substitutions. Not accepted performance comparison. Declared callable boundary includes filtering/decorated processing/metadata; not pure numerical kernels. Both existing profilers are disabled. These wrappers still add diagnostic overhead; no scaling to uninstrumented clocks or causal performance claims. Query boundary is nested under its actual parent and excluded from parent-exclusive elapsed." +} diff --git a/benchmark/results/perf_rejected_metadata_owner_20261003/candidate-execution-region.json b/benchmark/results/perf_rejected_metadata_owner_20261003/candidate-execution-region.json new file mode 100644 index 0000000000..b76eb63690 --- /dev/null +++ b/benchmark/results/perf_rejected_metadata_owner_20261003/candidate-execution-region.json @@ -0,0 +1,714 @@ +{ + "pid": 1697815, + "thread": 139997494724288, + "region": "ZMQExecutionServer._execute_with_orchestrator", + "root_dimensions": "{\"case\": \"3d_monolayer_final\", \"compile_artifact_id\": \"f3a82019-5257-4583-940c-4dfd40925dfb\", \"execution_id\": \"51ebb3de-fae0-4cd1-80bc-c03eb3ecf311\", \"kind\": \"execution\", \"plate\": \"/home/ts/.cache/openhcs/benchmark_datasets/CellProfiler_tutorials/data/3d_monolayer/images\"}", + "seconds": 8.966167135164142, + "revision": "9eee98265c939859d49f0c60bd5b3a2168cb29fe", + "source": "/home/ts/code/projects/openhcs-shared-runtime-plumbing", + "stats": { + "ZMQExecutionServer._initialize_orchestrator": { + "owner": "ZMQExecutionServer._initialize_orchestrator", + "dimensions": "", + "phase": "_initialize_orchestrator", + "calls": 1, + "inclusive": 0.08220752514898777, + "exclusive": 0.08220752514898777 + }, + "RuntimeImageStackCache.get": { + "owner": "RuntimeImageStackCache.get", + "dimensions": "", + "phase": "get", + "calls": 32, + "inclusive": 0.00034267501905560493, + "exclusive": 0.00034267501905560493 + }, + "stack_image_payload_context": { + "owner": "stack_image_payload_context", + "dimensions": "", + "phase": "stack_image_payload_context", + "calls": 28, + "inclusive": 0.04168135765939951, + "exclusive": 0.04168135765939951 + }, + "PatternGroupRuntime._load_input_stack": { + "owner": "PatternGroupRuntime._load_input_stack", + "dimensions": "", + "phase": "_load_input_stack", + "calls": 32, + "inclusive": 0.8401167313568294, + "exclusive": 0.8310769814997911 + }, + "CellProfilerModuleExecutor._image_request": { + "owner": "CellProfilerModuleExecutor._image_request", + "dimensions": "", + "phase": "_image_request", + "calls": 30, + "inclusive": 0.3633091449737549, + "exclusive": 0.3633091449737549 + }, + "RuntimeCallableInvocation.call | function=openhcs.processing.backends.cellprofiler.intensity.rescale_intensity;view=1": { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.intensity.rescale_intensity;view=1", + "phase": "declared_callable_boundary", + "calls": 2, + "inclusive": 0.029291327111423016, + "exclusive": 0.029291327111423016 + }, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.intensity.rescale_intensity": { + "owner": "CellProfilerFunctionContractExecutor.execute", + "dimensions": "function=openhcs.processing.backends.cellprofiler.intensity.rescale_intensity", + "phase": "execute", + "calls": 2, + "inclusive": 0.030176815576851368, + "exclusive": 0.0008854884654283524 + }, + "CellProfilerOutputRecorder.record_module_outputs": { + "owner": "CellProfilerOutputRecorder.record_module_outputs", + "dimensions": "", + "phase": "record_module_outputs", + "calls": 26, + "inclusive": 0.4135434296913445, + "exclusive": 0.4135434296913445 + }, + "CellProfilerModuleExecutor.__call__": { + "owner": "CellProfilerModuleExecutor.__call__", + "dimensions": "", + "phase": "__call__", + "calls": 32, + "inclusive": 5.029390189331025, + "exclusive": 0.22105292789638042 + }, + "FunctionCoreExecutor.execute | function=rescale_intensity": { + "owner": "FunctionCoreExecutor.execute", + "dimensions": "function=rescale_intensity", + "phase": "execute", + "calls": 2, + "inclusive": 0.14369932608678937, + "exclusive": 0.0003861561417579651 + }, + "PatternGroupRuntime._validate_and_unstack": { + "owner": "PatternGroupRuntime._validate_and_unstack", + "dimensions": "", + "phase": "_validate_and_unstack", + "calls": 28, + "inclusive": 0.2781928963959217, + "exclusive": 0.24520861357450485 + }, + "RuntimeImageStackCache.store": { + "owner": "RuntimeImageStackCache.store", + "dimensions": "", + "phase": "store", + "calls": 28, + "inclusive": 0.00023655686527490616, + "exclusive": 0.00023655686527490616 + }, + "PatternGroupRuntime._save_outputs": { + "owner": "PatternGroupRuntime._save_outputs", + "dimensions": "", + "phase": "_save_outputs", + "calls": 28, + "inclusive": 0.6775184879079461, + "exclusive": 0.6772819310426712 + }, + "finalize_function_step_outputs": { + "owner": "finalize_function_step_outputs", + "dimensions": "", + "phase": "finalize_function_step_outputs", + "calls": 30, + "inclusive": 0.7491098740138113, + "exclusive": 0.48047462571412325 + }, + "RuntimeCallableInvocation.call | function=openhcs.processing.backends.cellprofiler.image_geometry.resize_volumetric;view=1": { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.image_geometry.resize_volumetric;view=1", + "phase": "declared_callable_boundary", + "calls": 3, + "inclusive": 0.12918777717277408, + "exclusive": 0.12918777717277408 + }, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.image_geometry.resize_volumetric": { + "owner": "CellProfilerFunctionContractExecutor.execute", + "dimensions": "function=openhcs.processing.backends.cellprofiler.image_geometry.resize_volumetric", + "phase": "execute", + "calls": 3, + "inclusive": 0.1298334114253521, + "exclusive": 0.0006456342525780201 + }, + "FunctionCoreExecutor.execute | function=resize_volumetric": { + "owner": "FunctionCoreExecutor.execute", + "dimensions": "function=resize_volumetric", + "phase": "execute", + "calls": 3, + "inclusive": 0.19873877195641398, + "exclusive": 0.0007655806839466095 + }, + "RuntimeCallableInvocation.call | function=openhcs.processing.backends.cellprofiler.median_filter.medianfilter;view=1": { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.median_filter.medianfilter;view=1", + "phase": "declared_callable_boundary", + "calls": 1, + "inclusive": 0.5357745480723679, + "exclusive": 0.5357745480723679 + }, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.median_filter.medianfilter": { + "owner": "CellProfilerFunctionContractExecutor.execute", + "dimensions": "function=openhcs.processing.backends.cellprofiler.median_filter.medianfilter", + "phase": "execute", + "calls": 1, + "inclusive": 0.5364011479541659, + "exclusive": 0.000626599881798029 + }, + "FunctionCoreExecutor.execute | function=medianfilter": { + "owner": "FunctionCoreExecutor.execute", + "dimensions": "function=medianfilter", + "phase": "execute", + "calls": 1, + "inclusive": 0.5587063231505454, + "exclusive": 0.0010953620076179504 + }, + "RuntimeCallableInvocation.call | function=openhcs.processing.backends.cellprofiler.thresholding.threshold;view=1": { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.thresholding.threshold;view=1", + "phase": "declared_callable_boundary", + "calls": 3, + "inclusive": 0.48295382084324956, + "exclusive": 0.48295382084324956 + }, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.thresholding.threshold": { + "owner": "CellProfilerFunctionContractExecutor.execute", + "dimensions": "function=openhcs.processing.backends.cellprofiler.thresholding.threshold", + "phase": "execute", + "calls": 3, + "inclusive": 0.4840805730782449, + "exclusive": 0.0011267522349953651 + }, + "FunctionCoreExecutor.execute | function=threshold": { + "owner": "FunctionCoreExecutor.execute", + "dimensions": "function=threshold", + "phase": "execute", + "calls": 3, + "inclusive": 0.6010923348367214, + "exclusive": 0.003914329688996077 + }, + "RuntimeCallableInvocation.call | function=openhcs.processing.backends.cellprofiler.morphology.remove_holes_3d;view=1": { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.morphology.remove_holes_3d;view=1", + "phase": "declared_callable_boundary", + "calls": 2, + "inclusive": 0.10115541890263557, + "exclusive": 0.10115541890263557 + }, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.morphology.remove_holes_3d": { + "owner": "CellProfilerFunctionContractExecutor.execute", + "dimensions": "function=openhcs.processing.backends.cellprofiler.morphology.remove_holes_3d", + "phase": "execute", + "calls": 2, + "inclusive": 0.10138663509860635, + "exclusive": 0.0002312161959707737 + }, + "FunctionCoreExecutor.execute | function=remove_holes_3d": { + "owner": "FunctionCoreExecutor.execute", + "dimensions": "function=remove_holes_3d", + "phase": "execute", + "calls": 2, + "inclusive": 0.10958591010421515, + "exclusive": 0.0005047591403126717 + }, + "RuntimeCallableInvocation.call | function=openhcs.processing.backends.cellprofiler.watershed.watershed_cellprofiler4;view=1": { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.watershed.watershed_cellprofiler4;view=1", + "phase": "declared_callable_boundary", + "calls": 2, + "inclusive": 0.535675251390785, + "exclusive": 0.535675251390785 + }, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.watershed.watershed_cellprofiler4": { + "owner": "CellProfilerFunctionContractExecutor.execute", + "dimensions": "function=openhcs.processing.backends.cellprofiler.watershed.watershed_cellprofiler4", + "phase": "execute", + "calls": 2, + "inclusive": 0.5362195959314704, + "exclusive": 0.0005443445406854153 + }, + "FunctionCoreExecutor.execute | function=watershed_cellprofiler4": { + "owner": "FunctionCoreExecutor.execute", + "dimensions": "function=watershed_cellprofiler4", + "phase": "execute", + "calls": 2, + "inclusive": 0.5789050222374499, + "exclusive": 0.002408776432275772 + }, + "RuntimeCallableInvocation.call | function=openhcs.processing.backends.cellprofiler.morphology.resize_objects_3d;view=1": { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.morphology.resize_objects_3d;view=1", + "phase": "declared_callable_boundary", + "calls": 2, + "inclusive": 0.10566109092906117, + "exclusive": 0.10566109092906117 + }, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.morphology.resize_objects_3d": { + "owner": "CellProfilerFunctionContractExecutor.execute", + "dimensions": "function=openhcs.processing.backends.cellprofiler.morphology.resize_objects_3d", + "phase": "execute", + "calls": 2, + "inclusive": 0.10626277374103665, + "exclusive": 0.0006016828119754791 + }, + "FunctionCoreExecutor.execute | function=resize_objects_3d": { + "owner": "FunctionCoreExecutor.execute", + "dimensions": "function=resize_objects_3d", + "phase": "execute", + "calls": 2, + "inclusive": 0.14153605420142412, + "exclusive": 0.0009483452886343002 + }, + "RuntimeCallableInvocation.call | function=openhcs.processing.backends.cellprofiler.morphology.erode_objects;view=1": { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.morphology.erode_objects;view=1", + "phase": "declared_callable_boundary", + "calls": 1, + "inclusive": 0.0506745669990778, + "exclusive": 0.0506745669990778 + }, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.morphology.erode_objects": { + "owner": "CellProfilerFunctionContractExecutor.execute", + "dimensions": "function=openhcs.processing.backends.cellprofiler.morphology.erode_objects", + "phase": "execute", + "calls": 1, + "inclusive": 0.051196648739278316, + "exclusive": 0.0005220817402005196 + }, + "FunctionCoreExecutor.execute | function=erode_objects": { + "owner": "FunctionCoreExecutor.execute", + "dimensions": "function=erode_objects", + "phase": "execute", + "calls": 1, + "inclusive": 0.06009216699749231, + "exclusive": 0.0004721791483461857 + }, + "RuntimeCallableInvocation.call | function=openhcs.processing.backends.cellprofiler.object_images.convert_objects_to_image;view=1": { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.object_images.convert_objects_to_image;view=1", + "phase": "declared_callable_boundary", + "calls": 3, + "inclusive": 0.007585550658404827, + "exclusive": 0.007585550658404827 + }, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.object_images.convert_objects_to_image": { + "owner": "CellProfilerFunctionContractExecutor.execute", + "dimensions": "function=openhcs.processing.backends.cellprofiler.object_images.convert_objects_to_image", + "phase": "execute", + "calls": 3, + "inclusive": 0.008365179877728224, + "exclusive": 0.0007796292193233967 + }, + "FunctionCoreExecutor.execute | function=convert_objects_to_image": { + "owner": "FunctionCoreExecutor.execute", + "dimensions": "function=convert_objects_to_image", + "phase": "execute", + "calls": 3, + "inclusive": 0.04295764910057187, + "exclusive": 0.0015249010175466537 + }, + "RuntimeCallableInvocation.call | function=openhcs.processing.backends.cellprofiler.image_math.image_math;view=1": { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.image_math.image_math;view=1", + "phase": "declared_callable_boundary", + "calls": 120, + "inclusive": 0.056871647015213966, + "exclusive": 0.056871647015213966 + }, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.image_math.image_math": { + "owner": "CellProfilerFunctionContractExecutor.execute", + "dimensions": "function=openhcs.processing.backends.cellprofiler.image_math.image_math", + "phase": "execute", + "calls": 2, + "inclusive": 0.0808364930562675, + "exclusive": 0.023964846041053534 + }, + "FunctionCoreExecutor.execute | function=image_math": { + "owner": "FunctionCoreExecutor.execute", + "dimensions": "function=image_math", + "phase": "execute", + "calls": 2, + "inclusive": 0.35451433481648564, + "exclusive": 0.00045528681948781013 + }, + "RuntimeCallableInvocation.call | function=openhcs.processing.backends.cellprofiler.morphology.closing;view=1": { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.morphology.closing;view=1", + "phase": "declared_callable_boundary", + "calls": 1, + "inclusive": 0.2598985880613327, + "exclusive": 0.2598985880613327 + }, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.morphology.closing": { + "owner": "CellProfilerFunctionContractExecutor.execute", + "dimensions": "function=openhcs.processing.backends.cellprofiler.morphology.closing", + "phase": "execute", + "calls": 1, + "inclusive": 0.2600078550167382, + "exclusive": 0.00010926695540547371 + }, + "FunctionCoreExecutor.execute | function=closing": { + "owner": "FunctionCoreExecutor.execute", + "dimensions": "function=closing", + "phase": "execute", + "calls": 1, + "inclusive": 0.2873692591674626, + "exclusive": 0.0002643684856593609 + }, + "RuntimeCallableInvocation.call | function=openhcs.processing.backends.cellprofiler.image_geometry.mask_image;view=1": { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.image_geometry.mask_image;view=1", + "phase": "declared_callable_boundary", + "calls": 60, + "inclusive": 0.013158110901713371, + "exclusive": 0.013158110901713371 + }, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.image_geometry.mask_image": { + "owner": "CellProfilerFunctionContractExecutor.execute", + "dimensions": "function=openhcs.processing.backends.cellprofiler.image_geometry.mask_image", + "phase": "execute", + "calls": 1, + "inclusive": 0.03377221338450909, + "exclusive": 0.020614102482795715 + }, + "FunctionCoreExecutor.execute | function=mask_image": { + "owner": "FunctionCoreExecutor.execute", + "dimensions": "function=mask_image", + "phase": "execute", + "calls": 1, + "inclusive": 0.06323254993185401, + "exclusive": 0.00040600355714559555 + }, + "RuntimeCallableInvocation.call | function=openhcs.processing.backends.cellprofiler.morphology.erode_image;view=1": { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.morphology.erode_image;view=1", + "phase": "declared_callable_boundary", + "calls": 1, + "inclusive": 0.03734732512384653, + "exclusive": 0.03734732512384653 + }, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.morphology.erode_image": { + "owner": "CellProfilerFunctionContractExecutor.execute", + "dimensions": "function=openhcs.processing.backends.cellprofiler.morphology.erode_image", + "phase": "execute", + "calls": 1, + "inclusive": 0.037452553398907185, + "exclusive": 0.00010522827506065369 + }, + "FunctionCoreExecutor.execute | function=erode_image": { + "owner": "FunctionCoreExecutor.execute", + "dimensions": "function=erode_image", + "phase": "execute", + "calls": 1, + "inclusive": 0.04374082200229168, + "exclusive": 0.0002591479569673538 + }, + "RuntimeCallableInvocation.call | function=openhcs.processing.backends.cellprofiler.intensity.measure_object_intensity;view=1": { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.intensity.measure_object_intensity;view=1", + "phase": "declared_callable_boundary", + "calls": 4, + "inclusive": 0.8341215569525957, + "exclusive": 0.8341215569525957 + }, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.intensity.measure_object_intensity": { + "owner": "CellProfilerFunctionContractExecutor.execute", + "dimensions": "function=openhcs.processing.backends.cellprofiler.intensity.measure_object_intensity", + "phase": "execute", + "calls": 4, + "inclusive": 0.834825670812279, + "exclusive": 0.0007041138596832752 + }, + "ObjectMeasurementOutputRecorder.record": { + "owner": "ObjectMeasurementOutputRecorder.record", + "dimensions": "", + "phase": "record", + "calls": 6, + "inclusive": 0.0037249731831252575, + "exclusive": 0.0037249731831252575 + }, + "FunctionCoreExecutor.execute | function=measure_object_intensity": { + "owner": "FunctionCoreExecutor.execute", + "dimensions": "function=measure_object_intensity", + "phase": "execute", + "calls": 4, + "inclusive": 1.0454286388121545, + "exclusive": 0.004520548973232508 + }, + "RuntimeCallableInvocation.call | function=openhcs.processing.backends.cellprofiler.shape.measure_object_size_shape;view=1": { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.shape.measure_object_size_shape;view=1", + "phase": "declared_callable_boundary", + "calls": 2, + "inclusive": 0.5032332427799702, + "exclusive": 0.5032332427799702 + }, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.shape.measure_object_size_shape": { + "owner": "CellProfilerFunctionContractExecutor.execute", + "dimensions": "function=openhcs.processing.backends.cellprofiler.shape.measure_object_size_shape", + "phase": "execute", + "calls": 2, + "inclusive": 0.5039996360428631, + "exclusive": 0.0007663932628929615 + }, + "FunctionCoreExecutor.execute | function=measure_object_size_shape": { + "owner": "FunctionCoreExecutor.execute", + "dimensions": "function=measure_object_size_shape", + "phase": "execute", + "calls": 2, + "inclusive": 0.5173229603096843, + "exclusive": 0.002118128351867199 + }, + "RuntimeCallableInvocation.call | function=openhcs.processing.backends.cellprofiler.outlines.overlay_objects;view=1": { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.outlines.overlay_objects;view=1", + "phase": "declared_callable_boundary", + "calls": 2, + "inclusive": 0.2927272063679993, + "exclusive": 0.2927272063679993 + }, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.outlines.overlay_objects": { + "owner": "CellProfilerFunctionContractExecutor.execute", + "dimensions": "function=openhcs.processing.backends.cellprofiler.outlines.overlay_objects", + "phase": "execute", + "calls": 2, + "inclusive": 0.2929425104521215, + "exclusive": 0.00021530408412218094 + }, + "FunctionCoreExecutor.execute | function=overlay_objects": { + "owner": "FunctionCoreExecutor.execute", + "dimensions": "function=overlay_objects", + "phase": "execute", + "calls": 2, + "inclusive": 0.3159721801057458, + "exclusive": 0.013460240792483091 + }, + "RuntimeCallableInvocation.call | function=openhcs.processing.backends.cellprofiler.save_images.save_images;view=1": { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.save_images.save_images;view=1", + "phase": "declared_callable_boundary", + "calls": 2, + "inclusive": 0.0001816130243241787, + "exclusive": 0.0001816130243241787 + }, + "FunctionCoreExecutor.execute | function=save_images": { + "owner": "FunctionCoreExecutor.execute", + "dimensions": "function=save_images", + "phase": "execute", + "calls": 2, + "inclusive": 0.17626022081822157, + "exclusive": 0.1760786077938974 + }, + "AtomicMetadataWriter.publish_source_projection_metadata": { + "owner": "AtomicMetadataWriter.publish_source_projection_metadata", + "dimensions": "", + "phase": "publish_source_projection_metadata", + "calls": 5, + "inclusive": 0.5068132942542434, + "exclusive": 0.5068132942542434 + }, + "execute_worker_lane | {\"execution_id\": \"51ebb3de-fae0-4cd1-80bc-c03eb3ecf311\", \"kind\": \"worker_lane\", \"plate\": \"/home/ts/.cache/openhcs/benchmark_datasets/CellProfiler_tutorials/data/3d_monolayer/images\", \"worker_slot\": \"worker_0\"}": { + "owner": "execute_worker_lane", + "dimensions": "{\"execution_id\": \"51ebb3de-fae0-4cd1-80bc-c03eb3ecf311\", \"kind\": \"worker_lane\", \"plate\": \"/home/ts/.cache/openhcs/benchmark_datasets/CellProfiler_tutorials/data/3d_monolayer/images\", \"worker_slot\": \"worker_0\"}", + "phase": "execute_worker_lane", + "calls": 1, + "inclusive": 8.292205615900457, + "exclusive": 0.5081131015904248 + }, + "consolidate_analysis_outputs": { + "owner": "consolidate_analysis_outputs", + "dimensions": "", + "phase": "consolidate_analysis_outputs", + "calls": 1, + "inclusive": 1.1913012713193893e-05, + "exclusive": 1.1913012713193893e-05 + }, + "RuntimeArtifactMetadataTarget.reconciliation_targets": { + "owner": "RuntimeArtifactMetadataTarget.reconciliation_targets", + "dimensions": "", + "phase": "reconciliation_targets", + "calls": 1, + "inclusive": 0.2939795549027622, + "exclusive": 0.2939795549027622 + }, + "OpenHCSMetadataWriter.finalize_completed_plate": { + "owner": "OpenHCSMetadataWriter.finalize_completed_plate", + "dimensions": "", + "phase": "finalize_completed_plate", + "calls": 1, + "inclusive": 0.5114067010581493, + "exclusive": 0.007927724625915289 + }, + "ZMQExecutionServer._export_runtime_observation": { + "owner": "ZMQExecutionServer._export_runtime_observation", + "dimensions": "", + "phase": "_export_runtime_observation", + "calls": 1, + "inclusive": 0.010043779853731394, + "exclusive": 0.010043779853731394 + }, + "ZMQExecutionServer._execute_with_orchestrator | {\"case\": \"3d_monolayer_final\", \"compile_artifact_id\": \"f3a82019-5257-4583-940c-4dfd40925dfb\", \"execution_id\": \"51ebb3de-fae0-4cd1-80bc-c03eb3ecf311\", \"kind\": \"execution\", \"plate\": \"/home/ts/.cache/openhcs/benchmark_datasets/CellProfiler_tutorials/data/3d_monolayer/images\"}": { + "owner": "ZMQExecutionServer._execute_with_orchestrator", + "dimensions": "{\"case\": \"3d_monolayer_final\", \"compile_artifact_id\": \"f3a82019-5257-4583-940c-4dfd40925dfb\", \"execution_id\": \"51ebb3de-fae0-4cd1-80bc-c03eb3ecf311\", \"kind\": \"execution\", \"plate\": \"/home/ts/.cache/openhcs/benchmark_datasets/CellProfiler_tutorials/data/3d_monolayer/images\"}", + "phase": "_execute_with_orchestrator", + "calls": 1, + "inclusive": 8.966167135164142, + "exclusive": 0.041612975765019655 + } + }, + "hooks_installed": [ + { + "module": "openhcs.core.aligned_image_payload", + "owner": "", + "method": "stack_image_payload_context", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.aligned_image_payload", + "owner": "", + "method": "unstack_image_payload_context", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.measurement_feature_queries", + "owner": "MeasurementFeatureValueIndex", + "method": "from_columnar_table_by_object", + "descriptor_kind": "classmethod" + }, + { + "module": "openhcs.core.virtual_workspace_metadata", + "owner": "AtomicMetadataWriter", + "method": "publish_source_projection_metadata", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.runtime_stack_cache", + "owner": "RuntimeImageStackCache", + "method": "get", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.runtime_stack_cache", + "owner": "RuntimeImageStackCache", + "method": "store", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.steps.function_outputs", + "owner": "", + "method": "finalize_function_step_outputs", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.steps.function_outputs", + "owner": "OpenHCSMetadataWriter", + "method": "finalize_completed_plate", + "descriptor_kind": "classmethod" + }, + { + "module": "openhcs.core.steps.function_outputs", + "owner": "RuntimeArtifactMetadataTarget", + "method": "reconciliation_targets", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.steps.function_runtime", + "owner": "FunctionCoreExecutor", + "method": "execute", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.steps.function_runtime", + "owner": "PatternGroupRuntime", + "method": "_load_input_stack", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.steps.function_runtime", + "owner": "PatternGroupRuntime", + "method": "_validate_and_unstack", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.steps.function_runtime", + "owner": "PatternGroupRuntime", + "method": "_save_outputs", + "descriptor_kind": "function" + }, + { + "module": "openhcs.processing.backends.lib_registry.unified_registry", + "owner": "RuntimeCallableInvocation", + "method": "call", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.orchestrator.analysis_consolidation", + "owner": "", + "method": "consolidate_analysis_outputs", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.orchestrator.worker_execution", + "owner": "", + "method": "execute_worker_lane", + "descriptor_kind": "function" + }, + { + "module": "openhcs.runtime.zmq_execution_server", + "owner": "ZMQExecutionServer", + "method": "_execute_with_orchestrator", + "descriptor_kind": "function" + }, + { + "module": "openhcs.runtime.zmq_execution_server", + "owner": "ZMQExecutionServer", + "method": "_initialize_orchestrator", + "descriptor_kind": "function" + }, + { + "module": "openhcs.runtime.zmq_execution_server", + "owner": "ZMQExecutionServer", + "method": "_export_runtime_observation", + "descriptor_kind": "function" + }, + { + "module": "openhcs.interop.cellprofiler.runtime.function_contract_execution", + "owner": "CellProfilerFunctionContractExecutor", + "method": "execute", + "descriptor_kind": "function" + }, + { + "module": "openhcs.interop.cellprofiler.runtime.measurement_execution_support", + "owner": "ObjectMeasurementOutputRecorder", + "method": "record", + "descriptor_kind": "function" + }, + { + "module": "openhcs.interop.cellprofiler.runtime.output_recording", + "owner": "CellProfilerOutputRecorder", + "method": "record_module_outputs", + "descriptor_kind": "classmethod" + }, + { + "module": "openhcs.interop.cellprofiler.runtime.module_execution", + "owner": "CellProfilerModuleExecutor", + "method": "__call__", + "descriptor_kind": "function" + }, + { + "module": "openhcs.interop.cellprofiler.runtime.module_execution", + "owner": "CellProfilerModuleExecutor", + "method": "_image_request", + "descriptor_kind": "function" + } + ], + "profiling_enabled": false, + "runtime_profiling_enabled": false, + "diagnostic_errors": [], + "captures": false, + "scope": "coarse same-run nested exclusive timing partition on the exact source revision recorded in this region. Diagnostic wrappers, no scientific or memory-observer substitutions. Not accepted performance comparison. Declared callable boundary includes filtering/decorated processing/metadata; not pure numerical kernels. Both existing profilers are disabled. These wrappers still add diagnostic overhead; no scaling to uninstrumented clocks or causal performance claims. Query boundary is nested under its actual parent and excluded from parent-exclusive elapsed." +} diff --git a/benchmark/results/perf_rejected_metadata_owner_20261003/candidate-observations.json b/benchmark/results/perf_rejected_metadata_owner_20261003/candidate-observations.json new file mode 100644 index 0000000000..17f04a11e2 --- /dev/null +++ b/benchmark/results/perf_rejected_metadata_owner_20261003/candidate-observations.json @@ -0,0 +1,48 @@ +{ + "returncode": 0, + "diagnostic_admission": true, + "public_rows": [ + { + "case_name": "cp_tutorial_3d_monolayer", + "mode_name": "1w_1t", + "worker_count": "1", + "well_count": "1", + "compile_seconds": "1.2758193016052246", + "prepare_seconds": "0.0", + "execute_seconds": "8.977409839630127", + "total_seconds": "11.01210141601041", + "wells_per_second": "0.11139070376241177", + "successful_wells": "1", + "native_single_sample_execution_seconds": "", + "projected_native_execution_seconds": "", + "projected_execution_speedup": "", + "peak_memory_mb": "1663.19140625", + "status": "success", + "memory_limit_mb": "", + "error_message": "", + "execution_route": "ordinary-zmq-outcomes-v1", + "server_lifecycle": "reused-per-sweep", + "run_input_sha256": "1a217329c4ff9cc7e7328fe99edcd0bc9a6a292a9f3a116d3a23c7e05610aa36" + } + ], + "regions": [ + { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/regions/region-1697815-139997494724288-0001.json", + "sha256": "6dd3985b2d0d394e5e796d7304b03b208657e7d161484c13b3110d6451ff8bdd" + }, + { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/regions/region-1697815-139997494724288-0002.json", + "sha256": "7ec1f4e50610629ec55dc925de3b6a6b32b8b9337729b5117fa7eb57ff5f657f" + } + ], + "root_sequence_complete": true, + "actual_index_calls": 0, + "actual_declared_calls": 211, + "retained_json_bytes": 38075, + "generated_output_bytes": 18164103, + "public_log_bytes": 539, + "profiler_artifacts_absent": true, + "source_env_native_input_freeze": "PASS", + "science": "NOT_RUN: original full saved-output126file/schema/discrete/numerical/nativeinput witness separate gate. No exclusions or missing outputs waived.", + "scope": "One source-pinned whole ordinary3D Monolayer1w1t candidate falsification with24 unchanged coarse nominal hooks: inherited23 plus actual MeasurementFeatureValueIndex index classmethod. cProfile and runtime profiler OFF, no captures/percell hooks. Existing defaultOUTCOMES/memory observers/READY clocks unchanged. Same-thread nested exclusive clocks exclude query from its real ancestors; worker independent roots are not added to parent waits. Nearordinary diagnostic only; wrapper overhead remains, no scale factors/causal performance promotion. Full saved126file (6CSV120TIFF) science/native source witness independently required." +} diff --git a/benchmark/results/perf_rejected_metadata_owner_20261003/candidate-source-pins.json b/benchmark/results/perf_rejected_metadata_owner_20261003/candidate-source-pins.json new file mode 100644 index 0000000000..36ab6b78b3 --- /dev/null +++ b/benchmark/results/perf_rejected_metadata_owner_20261003/candidate-source-pins.json @@ -0,0 +1,116 @@ +{ + "revision": "9eee98265c939859d49f0c60bd5b3a2168cb29fe", + "source": "/home/ts/code/projects/openhcs-shared-runtime-plumbing", + "physical_inputs": { + "dataset_path": { + "path": "/home/ts/.cache/openhcs/benchmark_datasets/CellProfiler_tutorials/data/3d_monolayer/images", + "files": { + "3d_monolayer_xy1_ch0.tif": "63114f614fec95664f718a39cddac58449f0edb5cc31b57584e4ba5b1f4994c7", + "3d_monolayer_xy1_ch1.tif": "a7d9abf632850875c22613c4b740fc01153cf60e9773c16d4a670a97b46e5751", + "3d_monolayer_xy1_ch2.tif": "191754bc7f43856223180483e2737ea93bab4d0cf98b1b2a4c27425e9908147d" + } + }, + "cppipe_path": { + "path": "/home/ts/.cache/openhcs/benchmark_datasets/CellProfiler_tutorials/data/3d_monolayer/3d_monolayer_final.cppipe", + "files": { + "3d_monolayer_final.cppipe": "9fcd24a1be8372b5c2e87637a055032c3362b9ad8729e361095f93d8bbe71838" + } + } + }, + "shared_dependency_heads": "b0004daf3223326ac78689ba7b2e68abc18ae298 external/ObjectState (v1.1.8)\n 84f322e46871de5ed47e7fd20976ad03e533c1c4 external/PolyStore (v0.2.19-34-g84f322e)\n 1e53d03d9f468322a1c085c8be29485fb139caf2 external/arraybridge (v0.3.6)\n 393a7e03003cdc56df9013f932ed4f26e632d77a external/metaclass-registry (v0.2.2)\n 792b03413331fdf107e369c924c257a2f0b16126 external/pycodify (v0.1.3)\n ad4948775ab81180a354d4b793d17ee5ddff3972 external/pyqt-reactive (v0.3.24-9-gad49487)\n 4e8f8bde5f750d9a50eacac80acd3806123addd8 external/python-introspect (v0.1.14-1-g4e8f8bd)\n 668edafcb5ee530a163377c936431ae1e8334e85 external/zmqruntime (v0.2.24-40-g668edaf)", + "native_binaries": { + "openhcs/core/_tabular_native.abi3.so": { + "path": "/home/ts/code/projects/openhcs-shared-runtime-plumbing/openhcs/core/_tabular_native.abi3.so", + "sha256": "2ce348417de70ad3f614962c82aed9c4977c32b3ba00e13531ecafb8c10d8d1d" + }, + "openhcs/processing/backends/cellprofiler/_granularity_native.abi3.so": { + "path": "/home/ts/code/projects/openhcs-shared-runtime-plumbing/openhcs/processing/backends/cellprofiler/_granularity_native.abi3.so", + "sha256": "6e45987a50d5c0588a8fb0b09a2232a63e72c122d6a3d583ae4d62830e3b4096" + } + }, + "python_binary": { + "path": "/home/ts/.local/share/uv/python/cpython-3.12.14-linux-x86_64-gnu/bin/python3.12", + "sha256": "edffa803e5c4d73a1357f7f2c6c9fd38e9693712b2c06793e6a5e5cc1090d03a" + }, + "installed_versions": { + "python": "3.12.14 (main, Sep 24 2026, 17:57:43) [Clang 22.1.3 ]", + "versions": { + "openhcs": "0.8.7", + "numpy": "2.5.3", + "scipy": "1.18.1", + "arraybridge": "0.3.6", + "polystore": "0.3.0", + "python-introspect": "0.1.15", + "metaclass-registry": "0.2.2", + "zmqruntime": "0.3.0" + } + }, + "installed_freeze_sha256": "b7e5bbb9b5171476dfeb1692378419bfa157bb620028e1b91c8969eef2498b41", + "site_sha256": "391dc438b0d9d253e3467674c19d37b2670d5d7197400aa45f3c251e84f948c0", + "ordinary_helper_sha256": "8a41db448a722d231777a90a3ae23091617c8ac1ba72cefcb91f72b2c7975554", + "driver_sha256": "73b30d0cc3174c04cb479a72e97fe094ef6ad9fcfdacbfb37e43fbaad53560c3", + "manifest_sha256": "4c302e136b427cb9968eef82d7ede4a6c5b65cbcb4f8e5490fd5125c2e7ab2a8", + "ledger_controller_sha256": "c140695473eadb961b245c24344acc79778904920ba3725945ec9235354fbb9b", + "command": [ + "/usr/bin/taskset", + "-c", + "5", + "/home/ts/code/projects/openhcs/.venv/bin/python", + "-c", + "import runpy,sys;from dataclasses import replace;import openhcs.runtime.zmq_config as configuration;configuration.OPENHCS_ZMQ_CONFIG=replace(configuration.OPENHCS_ZMQ_CONFIG,client_connect_timeout_seconds=180.0);sys.argv=sys.argv[1:];runpy.run_path(sys.argv[0],run_name='__main__')", + "/home/ts/code/projects/openhcs-shared-runtime-plumbing/scripts/benchmark_cppipe_well_throughput.py", + "--manifest", + "/home/ts/code/projects/openhcs-shared-runtime-plumbing/benchmark/manifests/official30_portable_axis1.json", + "--mode", + "1w_1t", + "--output-dir", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public", + "--case", + "cp_tutorial_3d_monolayer" + ], + "scope": "One source-pinned whole ordinary3D Monolayer1w1t candidate falsification with24 unchanged coarse nominal hooks: inherited23 plus actual MeasurementFeatureValueIndex index classmethod. cProfile and runtime profiler OFF, no captures/percell hooks. Existing defaultOUTCOMES/memory observers/READY clocks unchanged. Same-thread nested exclusive clocks exclude query from its real ancestors; worker independent roots are not added to parent waits. Nearordinary diagnostic only; wrapper overhead remains, no scale factors/causal performance promotion. Full saved126file (6CSV120TIFF) science/native source witness independently required.", + "original_full_freeze_path": "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/source-freeze.json", + "original_full_freeze_sha256": "20c7690c2346a7e33625823ca1456b1774ec76192708a61aaf9f4b1170dc5d16", + "actual_import_dependency_pins": { + "external/ObjectState": { + "head": "a01f8939b9bf4659502a33fe3febe62cab0f3c9a", + "status": "", + "full_tracked_source_sha256": "511b83d27f0c0cffe973cb16e0cfce016405250185000d909e41f6343472dca1" + }, + "external/python-introspect": { + "head": "ed1e4eb15f7a0bf8315274d3e47858fa2b4372bb", + "status": "", + "full_tracked_source_sha256": "74b3a98b0ed51d505b50e828c5096aaab8d019d18b7619e7dfdcb0f923a54f7d" + }, + "external/arraybridge": { + "head": "1e53d03d9f468322a1c085c8be29485fb139caf2", + "status": "", + "full_tracked_source_sha256": "cc3300a4462a89363003190d00228618c8bc33545d445ae72359c1fb7440a931" + }, + "external/metaclass-registry": { + "head": "393a7e03003cdc56df9013f932ed4f26e632d77a", + "status": "", + "full_tracked_source_sha256": "a660ae17d4e4c03f5298ff8c2a8183635b53bf24558be6ff69b435476461e6bc" + }, + "external/zmqruntime": { + "head": "668edafcb5ee530a163377c936431ae1e8334e85", + "status": "", + "full_tracked_source_sha256": "bd020f39e177c038aa31d4579090694aede119aee668029a039dba249d311ec7" + }, + "external/pycodify": { + "head": "792b03413331fdf107e369c924c257a2f0b16126", + "status": "", + "full_tracked_source_sha256": "66f24bee56cbccc5baa5b1347a0c27bd6c9a6cc13598f679adba9668f2b54f3a" + }, + "external/PolyStore": { + "head": "84f322e46871de5ed47e7fd20976ad03e533c1c4", + "status": "", + "full_tracked_source_sha256": "47d2c21a32560e9d22581017e569846fb17f4f88a82a8ade0352eabaedea738b" + }, + "external/pyqt-reactive": { + "head": "ad4948775ab81180a354d4b793d17ee5ddff3972", + "status": "", + "full_tracked_source_sha256": "968cc99264a04019b724c11b47d53976f0f1341c50c8a86366aa7d3ef1ffe6fd" + } + } +} diff --git a/benchmark/results/perf_rejected_metadata_owner_20261003/consumer-controls.json b/benchmark/results/perf_rejected_metadata_owner_20261003/consumer-controls.json new file mode 100644 index 0000000000..03686eceb5 --- /dev/null +++ b/benchmark/results/perf_rejected_metadata_owner_20261003/consumer-controls.json @@ -0,0 +1,6 @@ +{ + "exit_code": 0, + "wall_seconds": 11.490860110148787, + "source_unchanged": true, + "log": "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-root-consumer-controls-v3/pytest.log" +} diff --git a/benchmark/results/perf_rejected_metadata_owner_20261003/extended-controls.json b/benchmark/results/perf_rejected_metadata_owner_20261003/extended-controls.json new file mode 100644 index 0000000000..97a1264ae2 --- /dev/null +++ b/benchmark/results/perf_rejected_metadata_owner_20261003/extended-controls.json @@ -0,0 +1,5 @@ +{ + "exit_code": 1, + "seconds": 40.09915372915566, + "source_unchanged": true +} diff --git a/benchmark/results/perf_rejected_metadata_owner_20261003/falsification.json b/benchmark/results/perf_rejected_metadata_owner_20261003/falsification.json new file mode 100644 index 0000000000..3f8d81de41 --- /dev/null +++ b/benchmark/results/perf_rejected_metadata_owner_20261003/falsification.json @@ -0,0 +1,495 @@ +{ + "decision": "REJECT_CURRENT_FLAT_NAMESPACE_ROUTE", + "scope": "One matched near-ordinary diagnostic pair; descriptive, not causal significance or full30 acceptance. No further polishing or performance promotion. Shared controls fixed; full science gate pending.", + "controls_equal": { + "physical_inputs": true, + "shared_dependency_heads": true, + "shared_dependency_sources": true, + "native_binaries": true, + "python_binary": true, + "installed_versions": true, + "installed_freeze_sha256": true, + "site_sha256": true, + "ordinary_helper_sha256": true, + "driver_sha256": true, + "manifest_sha256": true, + "actual_import_dependencies": true, + "ledger_controller_sha256": true, + "profiling_environment": true + }, + "cohorts": { + "baseline": { + "revision": "e8587bd7eb0c4aa283bc300c5eb9f2294f24890f", + "row": { + "case_name": "cp_tutorial_3d_monolayer", + "mode_name": "1w_1t", + "worker_count": "1", + "well_count": "1", + "compile_seconds": "1.5337145328521729", + "prepare_seconds": "0.0", + "execute_seconds": "8.276516675949097", + "total_seconds": "10.606972666922957", + "wells_per_second": "0.12082377637272465", + "successful_wells": "1", + "native_single_sample_execution_seconds": "", + "projected_native_execution_seconds": "", + "projected_execution_speedup": "", + "peak_memory_mb": "1692.34375", + "status": "success", + "memory_limit_mb": "", + "error_message": "", + "execution_route": "ordinary-zmq-outcomes-v1", + "server_lifecycle": "reused-per-sweep", + "run_input_sha256": "7f66dd478c9a56033495f056335a0eaadb9fd5facd953b504f0e1f8f7eaa57d5" + }, + "execution_root_seconds": 8.26425674976781, + "raw_callable_seconds": 3.656715089920908, + "outside_callable_seconds": 4.607541659846902, + "exclusive_phases": { + "ZMQExecutionServer._initialize_orchestrator": 0.08023449825122952, + "RuntimeImageStackCache.get": 0.0003536404110491276, + "stack_image_payload_context": 0.04427314409986138, + "PatternGroupRuntime._load_input_stack": 0.7971521429717541, + "CellProfilerModuleExecutor._image_request": 0.2785968668758869, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.intensity.rescale_intensity": 0.0007243934087455273, + "CellProfilerOutputRecorder.record_module_outputs": 0.3608029861934483, + "CellProfilerModuleExecutor.__call__": 0.12689677299931645, + "FunctionCoreExecutor.execute | function=rescale_intensity": 0.00037510739639401436, + "PatternGroupRuntime._validate_and_unstack": 0.18270204262807965, + "RuntimeImageStackCache.store": 0.00022991374135017395, + "PatternGroupRuntime._save_outputs": 0.6563447550870478, + "finalize_function_step_outputs": 0.20995302312076092, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.image_geometry.resize_volumetric": 0.0007913196459412575, + "FunctionCoreExecutor.execute | function=resize_volumetric": 0.0007455325685441494, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.median_filter.medianfilter": 0.0008455133065581322, + "FunctionCoreExecutor.execute | function=medianfilter": 0.0009696455672383308, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.thresholding.threshold": 0.0010865754447877407, + "FunctionCoreExecutor.execute | function=threshold": 0.003549575340002775, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.morphology.remove_holes_3d": 0.00023544440045952797, + "FunctionCoreExecutor.execute | function=remove_holes_3d": 0.0005331146530807018, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.watershed.watershed_cellprofiler4": 0.0005967230536043644, + "FunctionCoreExecutor.execute | function=watershed_cellprofiler4": 0.003626206424087286, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.morphology.resize_objects_3d": 0.000580822117626667, + "FunctionCoreExecutor.execute | function=resize_objects_3d": 0.0009479965083301067, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.morphology.erode_objects": 0.0005217930302023888, + "FunctionCoreExecutor.execute | function=erode_objects": 0.0004508676938712597, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.object_images.convert_objects_to_image": 0.0007999753579497337, + "FunctionCoreExecutor.execute | function=convert_objects_to_image": 0.0010002129711210728, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.image_math.image_math": 0.027114899829030037, + "FunctionCoreExecutor.execute | function=image_math": 0.0005593728274106979, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.morphology.closing": 9.440816938877106e-05, + "FunctionCoreExecutor.execute | function=closing": 0.00026117172092199326, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.image_geometry.mask_image": 0.014385522343218327, + "FunctionCoreExecutor.execute | function=mask_image": 0.00037759123370051384, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.morphology.erode_image": 9.999750182032585e-05, + "FunctionCoreExecutor.execute | function=erode_image": 0.0002447301521897316, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.intensity.measure_object_intensity": 0.0007225805893540382, + "ObjectMeasurementOutputRecorder.record": 0.0037819184362888336, + "FunctionCoreExecutor.execute | function=measure_object_intensity": 0.0043295701034367085, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.shape.measure_object_size_shape": 0.001232681330293417, + "FunctionCoreExecutor.execute | function=measure_object_size_shape": 0.00211262796074152, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.outlines.overlay_objects": 0.00020497571676969528, + "FunctionCoreExecutor.execute | function=overlay_objects": 0.010954282246530056, + "FunctionCoreExecutor.execute | function=save_images": 0.1656533577479422, + "AtomicMetadataWriter.publish_source_projection_metadata": 0.7686110232025385, + "consolidate_analysis_outputs": 1.02287158370018e-05, + "RuntimeArtifactMetadataTarget.reconciliation_targets": 0.3009079131297767, + "OpenHCSMetadataWriter.finalize_completed_plate": 0.00812348397448659, + "ZMQExecutionServer._export_runtime_observation": 0.007673155982047319 + }, + "source_freeze_sha256": "55ee41e2e0f09553de9c2be7dab5beedf0f4f2baf7245f2778c2dcedc38a092e" + }, + "candidate": { + "revision": "9eee98265c939859d49f0c60bd5b3a2168cb29fe", + "row": { + "case_name": "cp_tutorial_3d_monolayer", + "mode_name": "1w_1t", + "worker_count": "1", + "well_count": "1", + "compile_seconds": "1.2758193016052246", + "prepare_seconds": "0.0", + "execute_seconds": "8.977409839630127", + "total_seconds": "11.01210141601041", + "wells_per_second": "0.11139070376241177", + "successful_wells": "1", + "native_single_sample_execution_seconds": "", + "projected_native_execution_seconds": "", + "projected_execution_speedup": "", + "peak_memory_mb": "1663.19140625", + "status": "success", + "memory_limit_mb": "", + "error_message": "", + "execution_route": "ordinary-zmq-outcomes-v1", + "server_lifecycle": "reused-per-sweep", + "run_input_sha256": "1a217329c4ff9cc7e7328fe99edcd0bc9a6a292a9f3a116d3a23c7e05610aa36" + }, + "execution_root_seconds": 8.966167135164142, + "raw_callable_seconds": 3.975498642306775, + "outside_callable_seconds": 4.990668492857367, + "exclusive_phases": { + "ZMQExecutionServer._initialize_orchestrator": 0.08220752514898777, + "RuntimeImageStackCache.get": 0.00034267501905560493, + "stack_image_payload_context": 0.04168135765939951, + "PatternGroupRuntime._load_input_stack": 0.8310769814997911, + "CellProfilerModuleExecutor._image_request": 0.3633091449737549, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.intensity.rescale_intensity": 0.0008854884654283524, + "CellProfilerOutputRecorder.record_module_outputs": 0.4135434296913445, + "CellProfilerModuleExecutor.__call__": 0.22105292789638042, + "FunctionCoreExecutor.execute | function=rescale_intensity": 0.0003861561417579651, + "PatternGroupRuntime._validate_and_unstack": 0.24520861357450485, + "RuntimeImageStackCache.store": 0.00023655686527490616, + "PatternGroupRuntime._save_outputs": 0.6772819310426712, + "finalize_function_step_outputs": 0.48047462571412325, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.image_geometry.resize_volumetric": 0.0006456342525780201, + "FunctionCoreExecutor.execute | function=resize_volumetric": 0.0007655806839466095, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.median_filter.medianfilter": 0.000626599881798029, + "FunctionCoreExecutor.execute | function=medianfilter": 0.0010953620076179504, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.thresholding.threshold": 0.0011267522349953651, + "FunctionCoreExecutor.execute | function=threshold": 0.003914329688996077, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.morphology.remove_holes_3d": 0.0002312161959707737, + "FunctionCoreExecutor.execute | function=remove_holes_3d": 0.0005047591403126717, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.watershed.watershed_cellprofiler4": 0.0005443445406854153, + "FunctionCoreExecutor.execute | function=watershed_cellprofiler4": 0.002408776432275772, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.morphology.resize_objects_3d": 0.0006016828119754791, + "FunctionCoreExecutor.execute | function=resize_objects_3d": 0.0009483452886343002, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.morphology.erode_objects": 0.0005220817402005196, + "FunctionCoreExecutor.execute | function=erode_objects": 0.0004721791483461857, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.object_images.convert_objects_to_image": 0.0007796292193233967, + "FunctionCoreExecutor.execute | function=convert_objects_to_image": 0.0015249010175466537, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.image_math.image_math": 0.023964846041053534, + "FunctionCoreExecutor.execute | function=image_math": 0.00045528681948781013, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.morphology.closing": 0.00010926695540547371, + "FunctionCoreExecutor.execute | function=closing": 0.0002643684856593609, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.image_geometry.mask_image": 0.020614102482795715, + "FunctionCoreExecutor.execute | function=mask_image": 0.00040600355714559555, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.morphology.erode_image": 0.00010522827506065369, + "FunctionCoreExecutor.execute | function=erode_image": 0.0002591479569673538, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.intensity.measure_object_intensity": 0.0007041138596832752, + "ObjectMeasurementOutputRecorder.record": 0.0037249731831252575, + "FunctionCoreExecutor.execute | function=measure_object_intensity": 0.004520548973232508, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.shape.measure_object_size_shape": 0.0007663932628929615, + "FunctionCoreExecutor.execute | function=measure_object_size_shape": 0.002118128351867199, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.outlines.overlay_objects": 0.00021530408412218094, + "FunctionCoreExecutor.execute | function=overlay_objects": 0.013460240792483091, + "FunctionCoreExecutor.execute | function=save_images": 0.1760786077938974, + "AtomicMetadataWriter.publish_source_projection_metadata": 0.5068132942542434, + "consolidate_analysis_outputs": 1.1913012713193893e-05, + "RuntimeArtifactMetadataTarget.reconciliation_targets": 0.2939795549027622, + "OpenHCSMetadataWriter.finalize_completed_plate": 0.007927724625915289, + "ZMQExecutionServer._export_runtime_observation": 0.010043779853731394 + }, + "source_freeze_sha256": "20c7690c2346a7e33625823ca1456b1774ec76192708a61aaf9f4b1170dc5d16" + } + }, + "deltas": { + "execution_seconds": 0.7008931636810303, + "total_seconds": 0.4051287490874529, + "raw_callable_seconds": 0.31878355238586664, + "outside_callable_seconds": 0.3831268330104649 + }, + "exclusive_phase_changes": [ + { + "phase": "finalize_function_step_outputs", + "baseline": 0.20995302312076092, + "candidate": 0.48047462571412325, + "delta": 0.27052160259336233 + }, + { + "phase": "CellProfilerModuleExecutor.__call__", + "baseline": 0.12689677299931645, + "candidate": 0.22105292789638042, + "delta": 0.09415615489706397 + }, + { + "phase": "CellProfilerModuleExecutor._image_request", + "baseline": 0.2785968668758869, + "candidate": 0.3633091449737549, + "delta": 0.08471227809786797 + }, + { + "phase": "PatternGroupRuntime._validate_and_unstack", + "baseline": 0.18270204262807965, + "candidate": 0.24520861357450485, + "delta": 0.0625065709464252 + }, + { + "phase": "CellProfilerOutputRecorder.record_module_outputs", + "baseline": 0.3608029861934483, + "candidate": 0.4135434296913445, + "delta": 0.052740443497896194 + }, + { + "phase": "PatternGroupRuntime._load_input_stack", + "baseline": 0.7971521429717541, + "candidate": 0.8310769814997911, + "delta": 0.03392483852803707 + }, + { + "phase": "PatternGroupRuntime._save_outputs", + "baseline": 0.6563447550870478, + "candidate": 0.6772819310426712, + "delta": 0.02093717595562339 + }, + { + "phase": "FunctionCoreExecutor.execute | function=save_images", + "baseline": 0.1656533577479422, + "candidate": 0.1760786077938974, + "delta": 0.010425250045955181 + }, + { + "phase": "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.image_geometry.mask_image", + "baseline": 0.014385522343218327, + "candidate": 0.020614102482795715, + "delta": 0.006228580139577389 + }, + { + "phase": "FunctionCoreExecutor.execute | function=overlay_objects", + "baseline": 0.010954282246530056, + "candidate": 0.013460240792483091, + "delta": 0.0025059585459530354 + }, + { + "phase": "ZMQExecutionServer._export_runtime_observation", + "baseline": 0.007673155982047319, + "candidate": 0.010043779853731394, + "delta": 0.0023706238716840744 + }, + { + "phase": "ZMQExecutionServer._initialize_orchestrator", + "baseline": 0.08023449825122952, + "candidate": 0.08220752514898777, + "delta": 0.0019730268977582455 + }, + { + "phase": "FunctionCoreExecutor.execute | function=convert_objects_to_image", + "baseline": 0.0010002129711210728, + "candidate": 0.0015249010175466537, + "delta": 0.000524688046425581 + }, + { + "phase": "FunctionCoreExecutor.execute | function=threshold", + "baseline": 0.003549575340002775, + "candidate": 0.003914329688996077, + "delta": 0.0003647543489933014 + }, + { + "phase": "FunctionCoreExecutor.execute | function=measure_object_intensity", + "baseline": 0.0043295701034367085, + "candidate": 0.004520548973232508, + "delta": 0.00019097886979579926 + }, + { + "phase": "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.intensity.rescale_intensity", + "baseline": 0.0007243934087455273, + "candidate": 0.0008854884654283524, + "delta": 0.0001610950566828251 + }, + { + "phase": "FunctionCoreExecutor.execute | function=medianfilter", + "baseline": 0.0009696455672383308, + "candidate": 0.0010953620076179504, + "delta": 0.0001257164403796196 + }, + { + "phase": "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.thresholding.threshold", + "baseline": 0.0010865754447877407, + "candidate": 0.0011267522349953651, + "delta": 4.0176790207624435e-05 + }, + { + "phase": "FunctionCoreExecutor.execute | function=mask_image", + "baseline": 0.00037759123370051384, + "candidate": 0.00040600355714559555, + "delta": 2.841232344508171e-05 + }, + { + "phase": "FunctionCoreExecutor.execute | function=erode_objects", + "baseline": 0.0004508676938712597, + "candidate": 0.0004721791483461857, + "delta": 2.1311454474925995e-05 + }, + { + "phase": "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.morphology.resize_objects_3d", + "baseline": 0.000580822117626667, + "candidate": 0.0006016828119754791, + "delta": 2.0860694348812103e-05 + }, + { + "phase": "FunctionCoreExecutor.execute | function=resize_volumetric", + "baseline": 0.0007455325685441494, + "candidate": 0.0007655806839466095, + "delta": 2.0048115402460098e-05 + }, + { + "phase": "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.morphology.closing", + "baseline": 9.440816938877106e-05, + "candidate": 0.00010926695540547371, + "delta": 1.4858786016702652e-05 + }, + { + "phase": "FunctionCoreExecutor.execute | function=erode_image", + "baseline": 0.0002447301521897316, + "candidate": 0.0002591479569673538, + "delta": 1.4417804777622223e-05 + }, + { + "phase": "FunctionCoreExecutor.execute | function=rescale_intensity", + "baseline": 0.00037510739639401436, + "candidate": 0.0003861561417579651, + "delta": 1.104874536395073e-05 + }, + { + "phase": "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.outlines.overlay_objects", + "baseline": 0.00020497571676969528, + "candidate": 0.00021530408412218094, + "delta": 1.0328367352485657e-05 + }, + { + "phase": "RuntimeImageStackCache.store", + "baseline": 0.00022991374135017395, + "candidate": 0.00023655686527490616, + "delta": 6.643123924732208e-06 + }, + { + "phase": "FunctionCoreExecutor.execute | function=measure_object_size_shape", + "baseline": 0.00211262796074152, + "candidate": 0.002118128351867199, + "delta": 5.500391125679016e-06 + }, + { + "phase": "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.morphology.erode_image", + "baseline": 9.999750182032585e-05, + "candidate": 0.00010522827506065369, + "delta": 5.230773240327835e-06 + }, + { + "phase": "FunctionCoreExecutor.execute | function=closing", + "baseline": 0.00026117172092199326, + "candidate": 0.0002643684856593609, + "delta": 3.19676473736763e-06 + }, + { + "phase": "consolidate_analysis_outputs", + "baseline": 1.02287158370018e-05, + "candidate": 1.1913012713193893e-05, + "delta": 1.684296876192093e-06 + }, + { + "phase": "FunctionCoreExecutor.execute | function=resize_objects_3d", + "baseline": 0.0009479965083301067, + "candidate": 0.0009483452886343002, + "delta": 3.487803041934967e-07 + }, + { + "phase": "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.morphology.erode_objects", + "baseline": 0.0005217930302023888, + "candidate": 0.0005220817402005196, + "delta": 2.8870999813079834e-07 + }, + { + "phase": "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.morphology.remove_holes_3d", + "baseline": 0.00023544440045952797, + "candidate": 0.0002312161959707737, + "delta": -4.2282044887542725e-06 + }, + { + "phase": "RuntimeImageStackCache.get", + "baseline": 0.0003536404110491276, + "candidate": 0.00034267501905560493, + "delta": -1.0965391993522644e-05 + }, + { + "phase": "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.intensity.measure_object_intensity", + "baseline": 0.0007225805893540382, + "candidate": 0.0007041138596832752, + "delta": -1.8466729670763016e-05 + }, + { + "phase": "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.object_images.convert_objects_to_image", + "baseline": 0.0007999753579497337, + "candidate": 0.0007796292193233967, + "delta": -2.034613862633705e-05 + }, + { + "phase": "FunctionCoreExecutor.execute | function=remove_holes_3d", + "baseline": 0.0005331146530807018, + "candidate": 0.0005047591403126717, + "delta": -2.8355512768030167e-05 + }, + { + "phase": "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.watershed.watershed_cellprofiler4", + "baseline": 0.0005967230536043644, + "candidate": 0.0005443445406854153, + "delta": -5.237851291894913e-05 + }, + { + "phase": "ObjectMeasurementOutputRecorder.record", + "baseline": 0.0037819184362888336, + "candidate": 0.0037249731831252575, + "delta": -5.6945253163576126e-05 + }, + { + "phase": "FunctionCoreExecutor.execute | function=image_math", + "baseline": 0.0005593728274106979, + "candidate": 0.00045528681948781013, + "delta": -0.0001040860079228878 + }, + { + "phase": "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.image_geometry.resize_volumetric", + "baseline": 0.0007913196459412575, + "candidate": 0.0006456342525780201, + "delta": -0.00014568539336323738 + }, + { + "phase": "OpenHCSMetadataWriter.finalize_completed_plate", + "baseline": 0.00812348397448659, + "candidate": 0.007927724625915289, + "delta": -0.0001957593485713005 + }, + { + "phase": "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.median_filter.medianfilter", + "baseline": 0.0008455133065581322, + "candidate": 0.000626599881798029, + "delta": -0.00021891342476010323 + }, + { + "phase": "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.shape.measure_object_size_shape", + "baseline": 0.001232681330293417, + "candidate": 0.0007663932628929615, + "delta": -0.0004662880674004555 + }, + { + "phase": "FunctionCoreExecutor.execute | function=watershed_cellprofiler4", + "baseline": 0.003626206424087286, + "candidate": 0.002408776432275772, + "delta": -0.001217429991811514 + }, + { + "phase": "stack_image_payload_context", + "baseline": 0.04427314409986138, + "candidate": 0.04168135765939951, + "delta": -0.002591786440461874 + }, + { + "phase": "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.image_math.image_math", + "baseline": 0.027114899829030037, + "candidate": 0.023964846041053534, + "delta": -0.0031500537879765034 + }, + { + "phase": "RuntimeArtifactMetadataTarget.reconciliation_targets", + "baseline": 0.3009079131297767, + "candidate": 0.2939795549027622, + "delta": -0.006928358227014542 + }, + { + "phase": "AtomicMetadataWriter.publish_source_projection_metadata", + "baseline": 0.7686110232025385, + "candidate": 0.5068132942542434, + "delta": -0.2617977289482951 + } + ], + "science_status": "PASS_FULL_FOUR_NATIVE_COMPARISONS", + "science_sha256": "b9a35a8504dc4a2dc96d4e8e4263335be9f8c70f0b14b45e1f92aa3c8d3c6b45", + "candidate_source_reverted": true +} diff --git a/benchmark/results/perf_rejected_metadata_owner_20261003/inventory.json b/benchmark/results/perf_rejected_metadata_owner_20261003/inventory.json new file mode 100644 index 0000000000..7c0643f638 --- /dev/null +++ b/benchmark/results/perf_rejected_metadata_owner_20261003/inventory.json @@ -0,0 +1,85 @@ +{ + "files": { + "report.rst": { + "sha256": "11de21974ade24d126e635e554c9c9ad984d45aab788780b628361b0d22dafa8", + "bytes": 4714 + }, + "saved-v10-qualification.json": { + "sha256": "c9632c56a642b13a36bec564f2e128ac2a2fadf05d3a86a2e3c5b8b604a4500b", + "bytes": 16243 + }, + "metrics.csv": { + "sha256": "844b51828eafbea34153f1c4d9bcd29f752403fcd297275bc99bea296f1a360c", + "bytes": 396 + }, + "science-terminal.json": { + "sha256": "a9871f40c946d17cd2c5df275dfafe2c058e6270732e0cd51e5b2fb3cd3c5805", + "bytes": 2108 + }, + "baseline-execution-region.json": { + "sha256": "eda84c30eb19709e0fc37a35550a0e5a8e27e97ddd818e9296904203cd9e9e0b", + "bytes": 31183 + }, + "falsification.json": { + "sha256": "4a2010cca8476065b814bc7247ee80737cd29878c023930d8757033bd5f03171", + "bytes": 25432 + }, + "strict-science.json": { + "sha256": "b9a35a8504dc4a2dc96d4e8e4263335be9f8c70f0b14b45e1f92aa3c8d3c6b45", + "bytes": 268580 + }, + "baseline-compile_only-region.json": { + "sha256": "d4cf4157156de4e7a41949e36ee4daed69665ea7b8c6e2ddf5b00613300b6e5a", + "bytes": 6874 + }, + "original-nine-failures.json": { + "sha256": "06ef7de5b4dbaee2784d4233734ae90749802d38b802d66f6622eed4b3ecbbb1", + "bytes": 6093 + }, + "extended-controls.json": { + "sha256": "a7abdc5fdde8525351ab8ce0636d00963793cce71bd2f0d756deb9deb7aec945", + "bytes": 81 + }, + "consumer-controls.json": { + "sha256": "828575a11f53ac5c23865f5d47825874593c1cbd07109a7d4bf57443aa28c2fa", + "bytes": 199 + }, + "candidate-execution-region.json": { + "sha256": "7ec1f4e50610629ec55dc925de3b6a6b32b8b9337729b5117fa7eb57ff5f657f", + "bytes": 31198 + }, + "candidate-compile_only-region.json": { + "sha256": "6dd3985b2d0d394e5e796d7304b03b208657e7d161484c13b3110d6451ff8bdd", + "bytes": 6877 + }, + "baseline-source-pins.json": { + "sha256": "05f6be9206a59e4c392ef3994049287009fbcaa6937799d2fc3c53c2ba3b58be", + "bytes": 6814 + }, + "candidate-observations.json": { + "sha256": "6594071a580a83a5abd4cdb58c399899d046c84eaa52ed1c4f5dc0faab2bf88d", + "bytes": 2522 + }, + "whole-driver-controller.py": { + "sha256": "c140695473eadb961b245c24344acc79778904920ba3725945ec9235354fbb9b", + "bytes": 11489 + }, + "baseline-observations.json": { + "sha256": "a9558f52b6d74972c4244b9af73e03cfe66ecada108d1d2131db5af56c868260", + "bytes": 2518 + }, + "submitted-input-controls.json": { + "sha256": "ab65cac76f849b16a4ad67bf4e9c39818bd82b50c7a55173da42c112a50ecb33", + "bytes": 1733 + }, + "whole-driver-site.py": { + "sha256": "391dc438b0d9d253e3467674c19d37b2670d5d7197400aa45f3c251e84f948c0", + "bytes": 10930 + }, + "candidate-source-pins.json": { + "sha256": "39de2a5b69a560525666431b10b576e9de0e3c08d21ecf1f715bd22d429f541f", + "bytes": 6816 + } + }, + "scope": "Compact rejection/behavior evidence; full warm-state witness is preserved by original byte pin, no copied scientific outputs/environments" +} diff --git a/benchmark/results/perf_rejected_metadata_owner_20261003/original-nine-failures.json b/benchmark/results/perf_rejected_metadata_owner_20261003/original-nine-failures.json new file mode 100644 index 0000000000..d8d899c767 --- /dev/null +++ b/benchmark/results/perf_rejected_metadata_owner_20261003/original-nine-failures.json @@ -0,0 +1,113 @@ +{ + "conclusion": "All nine original assertion/error signatures recur against original Git blobs. No candidate-caused failure among these nine is demonstrated.", + "original_source_revision": "e8587bd7eb0c4aa283bc300c5eb9f2294f24890f", + "baseline_scope": "Original 27 changed production modules, all loaded; other source and shared dependencies/environment unchanged current installation, not reconstruction of a historical whole environment.", + "original_result": { + "failed": 9, + "passed": 0, + "seconds": 5.89, + "exit_code": 1 + }, + "candidate_result": { + "failed": 9, + "passed": 700, + "seconds": 37.58 + }, + "test_files_byte_equal_to_original_git_before_and_unchanged_after": { + "tests/unit/test_cellprofiler_runtime_adapter.py": "e4c1e9a86e2dc93a0af24088855caf3c12566e4bd99b9852c9ad3d152e2531a0", + "tests/unit/test_cellprofiler_runtime_binding_identity.py": "ff7ce51901b7d2f6838f19c454c0ecd32c219f82fafcb56748d6a8988ec73629", + "tests/unit/test_cellprofiler_module_execution.py": "5d28dcb3645197772b77d5f8eb4dc07e729bc4b2094322e20efd6a7468f5993f" + }, + "all_27_loaded_original_hashes_match": true, + "candidate_disk_modules_unchanged_since_each_overlay_import": true, + "error_lines_equal": true, + "error_line_comparison": "Exact E-line text except replacing process-specific 0x object repr addresses. Full raw logs retained.", + "error_lines": [ + "E assert [2] == [1, 2]", + "E ", + "E At index 0 diff: 2 != 1", + "E Right contains one more item: 2", + "E Use -v to get more diff", + "E assert np.float64(nan) == 30.0", + "E AssertionError: assert is ", + "E + where = object_identity()", + "E + where object_identity = >.object_identity", + "E + where > = runtime_object_measurement_row_policy()", + "E + where runtime_object_measurement_row_policy = MeasureObjectSizeShapeModule.runtime_object_measurement_row_policy", + "E + and = MeasurementObjectRowIdentity.ROW_SEQUENCE", + "E assert nan == 16.0", + "E assert [1, 2, 3, 4, 5] == [1, 2, 3]", + "E ", + "E Left contains 2 more items, first extra item: 4", + "E Use -v to get more diff", + "E AssertionError: assert CallableContr...pingproxy({})) == CallableContr...pingproxy({}))", + "E ", + "E Omitting 4 identical items, use -vv to show", + "E Differing attributes:", + "E ['metadata']", + "E ", + "E Drill down into differing attribute metadata:", + "E metadata: CallableMetadata(input_memory_type='numpy', output_memory_type='numpy', execution_memory_type='numpy', artifact_inputs=(ArtifactSpec(name='Worms', artifact_type=, parameter_name=None, materialization=None, viewer_streaming=, required=True, sidecar_role=None, relations=(), plan_type=, measuremen...", + "E ", + "E ...Full output truncated (11 lines hidden), use '-vv' to show", + "E RuntimeError: Missing dynamic grouped artifact input 'Nuclei' (object_labels) on axis 'A01' for producer component 'channel'.", + "E RuntimeError: Missing dynamic grouped artifact input 'Cells' (object_labels) on axis 'A01' for producer component 'channel'.", + "E RuntimeError: Missing dynamic grouped artifact input 'DNA' (image) on axis 'A01' for producer component 'site'." + ], + "failure_groups": [ + { + "count": 5, + "group": "Shape row-domain controls", + "failures": [ + "projected IDs [2] versus [1,2]", + "NaN versus 30", + "ROW_ORDINAL versus ROW_SEQUENCE policy", + "NaN Zernike versus 16", + "backend IDs [1,2,3,4,5] versus [1,2,3]" + ] + }, + { + "count": 1, + "group": "Crop settings contract", + "failure": "CallableContract metadata comparison differs" + }, + { + "count": 3, + "group": "Dynamic grouped artifact discovery", + "failure": "Missing Nuclei/Cells channel-grouped labels and DNA site-grouped images on A01" + } + ], + "artifacts": [ + { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-original-nine-boundary-controls-v1/selection.json", + "bytes": 2743, + "sha256": "73e2ee370c4b0c16223d7fa82777da1ddd67e1aff32c0362f0e8213a3b583fef" + }, + { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-original-nine-boundary-controls-v1/run_original.py", + "bytes": 3048, + "sha256": "53e37ff4a439bbe7e6732b5e1c45fc6ecf9207a98128337a12f40ad3c26f727c" + }, + { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-original-nine-boundary-controls-v1/imports-and-source.json", + "bytes": 15390, + "sha256": "29627098d5d59fd02c1d2a383b1cc3b03e60c08ba91b18edee441a6000b1486f" + }, + { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-original-nine-boundary-controls-v1/pytest.log", + "bytes": 31083, + "sha256": "76045c7dbfc544cff33a9ad6deacf75d8d3617605431ad65b3c069a043960aff" + }, + { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-runtime-boundary-controls-v1/pytest.log", + "bytes": 32104, + "sha256": "a9ac9e60f3f7892299f182bd839bb9ccc5d56b5d0a9d86a4c14eeed7019b37b5" + } + ], + "production_changes": false, + "new_environment_or_checkout": false, + "pipeline_or_native_run": false, + "all_handles_terminal": [ + 90981 + ] +} diff --git a/benchmark/results/perf_rejected_metadata_owner_20261003/saved-v10-qualification.json b/benchmark/results/perf_rejected_metadata_owner_20261003/saved-v10-qualification.json new file mode 100644 index 0000000000..8e75749e5c --- /dev/null +++ b/benchmark/results/perf_rejected_metadata_owner_20261003/saved-v10-qualification.json @@ -0,0 +1,277 @@ +{ + "status": "SAVED_EDGE_PARITY_PASS", + "capture_source": "4c636a281de4da7341f4f9f6bb8f718c75644a1d", + "current_candidate_head": "e8587bd7eb0c4aa283bc300c5eb9f2294f24890f", + "current_candidate_dirty_status": " M openhcs/core/aligned_image_payload.py\n M openhcs/core/artifacts.py\n M openhcs/core/image_file_serialization.py\n M openhcs/core/measurement_image_alignment.py\n M openhcs/core/runtime_adapters.py\n M openhcs/core/runtime_image_loading.py\n M openhcs/core/runtime_image_values.py\n M openhcs/core/runtime_object_labels.py\n M openhcs/core/runtime_slice_projection.py\n M openhcs/core/source_image_semantics.py\n M openhcs/core/source_plane_alignment.py\n M openhcs/core/source_workspace_projection.py\n M openhcs/core/steps/function_artifact_materialization.py\n M openhcs/core/steps/function_io.py\n M openhcs/core/steps/function_output_identity.py\n M openhcs/core/steps/function_output_manifest.py\n M openhcs/core/steps/function_outputs.py\n M openhcs/core/steps/function_runtime.py\n M openhcs/interop/cellprofiler/module_artifact_declarations.py\n M openhcs/interop/cellprofiler/runtime/artifact_binding.py\n M openhcs/interop/cellprofiler/runtime/function_contract_execution.py\n M openhcs/interop/cellprofiler/runtime/invocation.py\n M openhcs/interop/cellprofiler/runtime/main_flow.py\n M openhcs/interop/cellprofiler/runtime/measurement_recording.py\n M openhcs/interop/cellprofiler/runtime/module_execution.py\n M openhcs/interop/cellprofiler/runtime/output_record_request.py\n M openhcs/processing/materialization/core.py\n?? tests/unit/test_cellprofiler_metadata_projection_consumers.py\n?? tests/unit/test_function_output_metadata_projection.py\n?? tests/unit/test_image_metadata_owner_consumers.py\n?? tests/unit/test_image_metadata_projection_owner.py\n?? tests/unit/test_metadata_stage2_closure.py\n", + "current_candidate_source_pin_sha256": "096287ee7e010297806ffbccdf50b6a28287f5575cb59059ab079c27855c104b", + "current_candidate_source_pin_in": "/home/ts/.local/state/openhcs-maintenance/20261003/v10-current-carrier-transfer-v1/producer13-current-replay-v1.json", + "source_before_after_unchanged_both_replays": true, + "tiny_gate": { + "status": "PASS", + "scope": "Exact retained list state, original dataclass setter, stored-path order; pixels/mask/namespace/provenance/identity/birthday/cache refs borrowed unchanged; shared namespace mutation preserved; no production compatibility", + "types": [ + "ImageMetadataPayload", + "MaskedImagePayload", + "ProducedOutputSemantics" + ], + "field_order": { + "ImageMetadataPayload": [ + "data", + "metadata" + ], + "MaskedImagePayload": [ + "data", + "mask", + "metadata" + ], + "ProducedOutputSemantics": [ + "component_values", + "extension", + "source", + "filename_component_values", + "filename_qualifier", + "producer_identity", + "output_path", + "relative_output_path", + "image_metadata", + "main_flow_plane_axis" + ] + }, + "provenance_births_during_transfer": 0, + "pixel_strides": [ + 24, + -4 + ], + "mask_writeable": false, + "adapter_sha256": "3d9b67a2e59966352cf222c577437a86ea17ad5e3a5bde5de787c06474547be6" + }, + "artifacts": { + "adapter.py": { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/v10-current-carrier-transfer-v1/adapter.py", + "bytes": 2798, + "sha256": "3d9b67a2e59966352cf222c577437a86ea17ad5e3a5bde5de787c06474547be6" + }, + "qualify_tiny.py": { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/v10-current-carrier-transfer-v1/qualify_tiny.py", + "bytes": 4018, + "sha256": "9752d3033c558d39e533a3d3f1dab25b6350f65f42edefc04a6ac52a3237f316" + }, + "qualify_tiny-first-red.py": { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/v10-current-carrier-transfer-v1/qualify_tiny-first-red.py", + "bytes": 4025, + "sha256": "9d47c0f66837d3f01cdbfaef9177ad103ff45bf01cf47dbec3dfd806c3af847f" + }, + "tiny-qualification.json": { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/v10-current-carrier-transfer-v1/tiny-qualification.json", + "bytes": 1023, + "sha256": "b5ecdd2b006c641a0718e12448a8779723e21376fde2e749f538587ace6939fb" + }, + "replay.py": { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/v10-current-carrier-transfer-v1/replay.py", + "bytes": 4672, + "sha256": "d16609418f6e436c3e84ff5f8e640acc26f076fafdac216ac0c3ab5d59ab3740" + }, + "producer13-current-replay-v1.json": { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/v10-current-carrier-transfer-v1/producer13-current-replay-v1.json", + "bytes": 214799, + "sha256": "da77685ccf6dcfa984a605182243ca9e0b67bdf412e7a77e94037be5b57cc022" + }, + "producer13-current-replay-v1.log": { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/v10-current-carrier-transfer-v1/producer13-current-replay-v1.log", + "bytes": 1806, + "sha256": "e8b99fb4533034b391cfd7afa86cdeab80ef845ac0837eea87835ff6784b153a" + }, + "loader14-current-replay-v1.json": { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/v10-current-carrier-transfer-v1/loader14-current-replay-v1.json", + "bytes": 214721, + "sha256": "7651099dbe290ee91e85c9d9019a68faaa1fd28bddc6e4a81dc2878b40ab80f1" + }, + "loader14-current-replay-v1.log": { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/v10-current-carrier-transfer-v1/loader14-current-replay-v1.log", + "bytes": 1736, + "sha256": "9008c60ad1ac5ead5d5aa163a8fc8f5f9746234d0a9a9c323e74e9a64aa73e0f" + }, + "immediate_producer.py": { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/v10-current-carrier-transfer-v1/immediate_producer.py", + "bytes": 1058, + "sha256": "4a67fdba8238acf915bbbb4ca1f11b4e62811458a0dae1998c6be8077cc43bda" + }, + "producer13-full-immediate-effects-v1.json": { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/v10-current-carrier-transfer-v1/producer13-full-immediate-effects-v1.json", + "bytes": 1263, + "sha256": "5770905b2918cca5b9ab722e6dd712a469937f161ef4892e389b660d575c155b" + }, + "producer13-full-immediate-effects-v1.log": { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/v10-current-carrier-transfer-v1/producer13-full-immediate-effects-v1.log", + "bytes": 1227, + "sha256": "fbe470fe158facb1072cc385ca3f31e237854bc9744271bd88e88d6b0f36f5fa" + }, + "inspect_layout.py": { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/v10-current-carrier-transfer-v1/inspect_layout.py", + "bytes": 2129, + "sha256": "94b649935213c706fe338e493192ac8e63f908a86fdff929fb938f0c1dae66e6" + }, + "finalize.py": { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/v10-current-carrier-transfer-v1/finalize.py", + "bytes": 5170, + "sha256": "3ef583b07f244fa068decc921c88eecd8998d90289f2417eac1ce6b088092d05" + } + }, + "original_helper_hashes": { + "original_replay_v10.py": "7001af6479a05e0dd1afc52a1509a44fed45ce1d0c1151dee3b832dc7a96a74a", + "immediate_original_effects_v10.py": "66848b5c5f5dcf0ccf7552420754aaab2005dc1a2c563961c6b6bc8d49748728", + "site/openhcs_cohort_snapshot_transport_v6_20261002.py": "a8b11cf6401aa0f80617e6734ae67bbbffcecc74ad4f7a59041523e42b850763", + "site/nested_cache_retention.py": "7e6d9ca3e7dce0da18a603610576cc9eea2d8038ad936fe2599e5b87d94bf3b1", + "site/ordered_array_role_oracle.py": "cf6b799a8ade0dbf82185fdc2a80f242111b784e1e643f4e91ea3456a6c5718b", + "site/openhcs_current_cohort_capture_v10_20261003.py": "ec9af29df043c9f465ba410c7317b6668d903fca3d3308f70716a350e096c98a", + "output/source-freeze.json": "6d4f4a59e62acbebbd6e21fcff66fa5806f45d99778264e7f5399b666bb3171f", + "output/observations.json": "1559ae44e06fed622816b1518132fd738f56e0355faadf9e15cdd2159052f491" + }, + "actual_captures": [ + { + "key": "13-PatternGroupRuntime-_validate_and_unstack-before", + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/current-cohort-capture-v10/output/saved-inputs/root-813175-139641010583232-0002/cohort-13-PatternGroupRuntime-_validate_and_unstack-before.pkl", + "bytes": 272201079, + "array_count": 1184, + "sha256": "3c49c2ebc3050c4bd9e62052d7a6e9acd6c095f23849f80f86a115041c544519", + "sidecar_sha256": "3ca071d05e7516eda02f3d0cbb6f6f77d6d3defee63991af3f147247eef5bd0b", + "inventory_canonical_json_sha256": "af1e5133dd2b47972c2a624eff9e004c12cdc2b54b35ab3701ce19cada83881b", + "nested_cache_witness_canonical_json_sha256": "987bdfb47a6e95c9afedf23a716af99c9319a3990f95f4d6b5b69f94659dc9bb" + }, + { + "key": "13-save-oracle", + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/current-cohort-capture-v10/output/saved-inputs/root-813175-139641010583232-0002/cohort-13-save-oracle.pkl", + "bytes": 194819278, + "array_count": 1234, + "sha256": "c9ed3ce36c322a5d32aeb03f65b1d8d2697775e61231a6d433dae66e7d290542", + "sidecar_sha256": "dec76667df2aa673785cc176e23d309ca3fc6a552adbdd67ce957d4e0194e8c0", + "inventory_canonical_json_sha256": "2176c9eb60971f28a8f9ed04fbd4a7cb9b212468669ae798e85072b370eb2aa8", + "nested_cache_witness_canonical_json_sha256": "88629f21cd2376e902be7d548ae21f5f06c8c603b4087fb6b0b3d5b008ffc131" + }, + { + "key": "14-PatternGroupRuntime-_load_input_stack-before", + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/current-cohort-capture-v10/output/saved-inputs/root-813175-139641010583232-0002/cohort-14-PatternGroupRuntime-_load_input_stack-before.pkl", + "bytes": 280297985, + "array_count": 1244, + "sha256": "2a48c08fdf3eecfdd5af13b0052ce985a21469dbbe5e5c4f97be69682a933780", + "sidecar_sha256": "8b6f277dc8d1765e3d6b68f816a91b0b762df72f3128b3294de0f5b513455d26", + "inventory_canonical_json_sha256": "266deec29a18a1410cdc1d06f3ccf1a960e12712052d7125983a1be4a3c60dbe", + "nested_cache_witness_canonical_json_sha256": "ac01f6a886bac198005c953bc4fed12a2cd5f54892ac59ce4db98f7b1d0bc416" + }, + { + "key": "14-load-oracle", + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/current-cohort-capture-v10/output/saved-inputs/root-813175-139641010583232-0002/cohort-14-load-oracle.pkl", + "bytes": 280362714, + "array_count": 1244, + "sha256": "20c4d2721789d5c9dfdce4e6ff789cbcfb59d49e87ba53e042deb75be5c3bde7", + "sidecar_sha256": "67f9015dde0043870494880620dabbb8e5ea17feb08a1da1ad21398b032228b6", + "inventory_canonical_json_sha256": "24536b96aff10ad624c19a6048d0dfab0683a6aee23d54d91252f7866b73f124", + "nested_cache_witness_canonical_json_sha256": "bc4f64d6dd34c4730db6b8c0ab6b55da459f4259efa31161b8acf8ae8bafaefb" + } + ], + "producer": { + "kind": "producer13", + "status": "PASS", + "original_call_started": true, + "original_nested_owner_count": 1054, + "original_nonempty_views": 677, + "nested_exact_capture_refs_restored": true, + "captured_external_manifest_available": true, + "input_fixture": "/home/ts/.local/state/openhcs-maintenance/20261003/current-cohort-capture-v10/output/saved-inputs/root-813175-139641010583232-0002/cohort-13-PatternGroupRuntime-_validate_and_unstack-before.pkl", + "transport": { + "root_type": null, + "array_count": 1184, + "strict_snapshot_restore": true + }, + "original_declaring_method_counts": {}, + "records": 60, + "unstack_seconds": 0.02488934900611639, + "save_seconds": 1.2471533310599625, + "full_outputdata_records_planes_named_whole_equal": true, + "complete_array_alias_graph": true, + "array_roles": 123 + }, + "loader": { + "kind": "loader14", + "status": "PASS", + "original_call_started": true, + "original_nested_owner_count": 1115, + "original_nonempty_views": 738, + "nested_exact_capture_refs_restored": true, + "captured_external_manifest_available": true, + "input_fixture": "/home/ts/.local/state/openhcs-maintenance/20261003/current-cohort-capture-v10/output/saved-inputs/root-813175-139641010583232-0002/cohort-14-PatternGroupRuntime-_load_input_stack-before.pkl", + "transport": { + "root_type": null, + "array_count": 1244, + "strict_snapshot_restore": true + }, + "original_declaring_method_counts": {}, + "load_seconds": 1.1413843147456646, + "full_loaded_metadata_store_cache_equal": true, + "complete_array_alias_graph": true, + "array_roles": 104 + }, + "immediate_owner_effects": { + "producer13": [ + { + "method": "_save_outputs", + "actual_method_seconds": 0.12108967360109091, + "before_owners": 1054, + "before_nonempty_views": 677, + "before_view_entries": 4286, + "after_owners": 1115, + "changed_before_owner_fields": [], + "new_reachable_owner_count": 61 + } + ], + "loader14": [ + { + "method": "_load_input_stack", + "actual_method_seconds": 0.011172774713486433, + "before_owners": 1115, + "before_nonempty_views": 738, + "before_view_entries": 4408, + "after_owners": 1116, + "changed_before_owner_fields": [], + "new_reachable_owner_count": 1 + } + ] + }, + "supplemental_whole_producer_immediate": { + "receipt": "/home/ts/.local/state/openhcs-maintenance/20261003/v10-current-carrier-transfer-v1/producer13-full-immediate-effects-v1.json", + "status": "IMMEDIATE_RETAINED_OWNER_EFFECTS_PASS", + "scope": "Original immediate-effects algorithm across both unstack/save, observed before equality. No independent source-before/after hash check on this extra supplemental run; do not extend the pinned decisive gate from it." + }, + "timing_scope": { + "performance_claim": false, + "producer_unstack_outer_seconds": 0.02488934900611639, + "producer_save_actual_method_seconds": 0.12108967360109091, + "loader_actual_method_seconds": 0.011172774713486433, + "public_oracle_equality_realization_seconds": { + "producer13": 0.47686423640698195, + "loader14": 0.8740706979297101 + }, + "warning": "Not a matched original-vs-candidate timing experiment. Passive graph witness observation contaminates outer harness save/load clocks; inner method timings exclude witness but may include normal GC and cold process/global caches. Public realization/equality occurs after method timers and is recorded separately. No extrapolation to whole-driver gain or earlier edge clocks." + }, + "first_harness_failure": { + "script": "/home/ts/.local/state/openhcs-maintenance/20261003/v10-current-carrier-transfer-v1/qualify_tiny-first-red.py", + "error": "AttributeError SourceImageIdentity.source_component_metadata; corrected diagnostic member to actual component_metadata. No production failure or source change." + }, + "limits": { + "existing_snapshot_item_MiB": 384, + "existing_snapshot_total_MiB": 1024, + "new_graph_captures": 0, + "new_environments": 0, + "source_edits": 0, + "scratch_limit_MiB": 20, + "actual_artifact_bytes": 460445 + }, + "processes": "All own replay handles terminal/closed; no further source reads/imports while root baseline executes.", + "qualification_scope": "Exact independent producer13 and loader14 original saved receivers and oracles. Stdlib old dataclass list assignment only into same current nominal carrier types; original warm retained owner references and caches restored, no default fabrication, graph union, pixel/mask copies, or production compatibility layer. Full saved role values/arrays/masks/source metadata/store/cache/nonmutation/alias gates passed under current dirty candidate. Does not cover whole upstream request/record path or end-to-end performance.", + "original_full_qualification": { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/v10-current-carrier-transfer-v1/qualification.json", + "sha256": "39902e79c6197d21eb1a469c4ec35024f1081e05a443fe55c8ad4800b10dc712", + "bytes": 10930144, + "note": "Full capture inventories and nested warm-state witnesses remain immutable in original qualification/capture sidecars. This compact report derives pins rather than duplicating those rosters." + } +} diff --git a/benchmark/results/perf_rejected_metadata_owner_20261003/science-terminal.json b/benchmark/results/perf_rejected_metadata_owner_20261003/science-terminal.json new file mode 100644 index 0000000000..8261fa4fd9 --- /dev/null +++ b/benchmark/results/perf_rejected_metadata_owner_20261003/science-terminal.json @@ -0,0 +1,39 @@ +{ + "status": "STRICT_SCIENCE_PASS_PERFORMANCE_ROUTE_REJECTED", + "session": 68950, + "returncode": 0, + "source": "9eee98265c939859d49f0c60bd5b3a2168cb29fe", + "comparisons": 4, + "scope": "Fresh baseline and candidate vs both existing native repetitions, original compare function, original policies; no native rerun and no performance promotion.", + "gates": "Exact120 candidate TIFFs against2 native volume TIFFs through existing logical owners; six candidate CSV vs existing native7-table representation; strict schema/discrete/numerical/relationship correlations/full physical inventory; 180 ordered source references; all input/native/output/source hashes unchanged.", + "performance_parent_observations": { + "baseline_execute_seconds": 8.27651668, + "candidate_execute_seconds": 8.97740984, + "decision": "STOPPED by root; single source pair descriptive only" + }, + "files": [ + { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-science-v1/compare.py", + "sha256": "b28035313a58a2c90d0824a71483f575db9c28c830f9d11f2d962f4ffc978b06", + "bytes": 8186 + }, + { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-science-v1/prep.json", + "sha256": "99d0f1996e262bb15ed0a2160b29d4f4c0fff42ef1d95ac1a6395cca325a7051", + "bytes": 7092 + }, + { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-science-v1/compare.log", + "sha256": "7e25cd2d6d31fefea7e748430aef4769062f2220a201c0564c5d67dd8143d624", + "bytes": 366 + }, + { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-science-v1/strict-science.json", + "sha256": "b9a35a8504dc4a2dc96d4e8e4263335be9f8c70f0b14b45e1f92aa3c8d3c6b45", + "bytes": 268580 + } + ], + "print_notice": "Original comparator prints its original strict-science destination; write adapter actually stores ONLY this directory strict-science.json. Old native qualification file SHA checked unchanged.", + "all_handles_terminal": true, + "scratch_bytes": 284224 +} diff --git a/benchmark/results/perf_rejected_metadata_owner_20261003/strict-science.json b/benchmark/results/perf_rejected_metadata_owner_20261003/strict-science.json new file mode 100644 index 0000000000..51a8fce09d --- /dev/null +++ b/benchmark/results/perf_rejected_metadata_owner_20261003/strict-science.json @@ -0,0 +1,1023 @@ +{ + "status": "PASS", + "comparisons": [ + { + "case": "cp_tutorial_3d_monolayer", + "ordinary_repeat": 0, + "native_repeat": 0, + "measurement_differences": 0, + "image_differences": 0, + "tables": [ + 7, + 6 + ], + "physical_images": [ + 2, + 120 + ], + "logical_images": [ + 2, + 2 + ], + "known_relationship_keys": [ + 3, + 3 + ], + "ordinary_receipt_sha256": "da9430eb33c8e5724edc9c28431ff22848bef9a0eda94da59139b20a3349058e", + "output_hashes": { + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_downsizedNuclei.csv": "2c66bc7b17de52147d5a875403f9a9746019ac61371c08157979044c6ed818dd", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_Cells.csv": "cc34681db8a66c38ca77bc47db496b26703f21e4d7c7628dd74e30c33250859f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_xy1_ch2_NucleiLabels.tiff": "340bbd795541e4031749d1fff767fa4fcdcac10f5b97879056b25dac9fd7674b", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_xy1_ch0_CellsLabels.tiff": "aaed9ed23179a162ee7f5a7e8d1d88f1fa342ca55172b7fccd4cbf67f7841d81", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_erodedDownsizedNuclei.csv": "527a0cce497d2a618478be84d1a0bf174049f58aa2a666e37626a990fdca0116", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_erodedResizedNuclei.csv": "66a58f78cab0cfdbe907468e8ba383fc59fab49025b84b589855bdfa04854609", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_Nuclei.csv": "440c33edc7e8cb5611910a835454db62390301a7af617a542349f60aece57561", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_Image.csv": "1f3dec653b6909c8a81ac109ce1faf91380886ac9a18e56fafc91102d419904e", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_Experiment.csv": "c05f0a11f43a5bda943e9f739ff257216f8ab3f65d8988a0d3d0c612c1db6997", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z017_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z037_t001_NucleiLabels.tiff": "927d52441c57cb3a4014effaca3e3c23c76275fb264c26e1942a9c58b85ee6d2", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z044_t001_NucleiLabels.tiff": "a78264f2f5e245a3913a69f89105e94df30048b1aaef4eb80fc5c3235e78b196", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z052_t001_NucleiLabels.tiff": "0daf51526658e23b7c9ab8872e992ebe9322399286f95741d0ccdbd357e5d631", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z007_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z047_t001_NucleiLabels.tiff": "995d5ce77cbf61a9be8590f34987ef3f1dd698ae6ed393cac4d0960bc9e7a66a", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z019_t001_CellsLabels.tiff": "ec6ef1d319f2732c47b06f5cf02775c8cd94540f2f30a6c002f0aace1d317955", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z032_t001_CellsLabels.tiff": "8a2c5774044326ae722e41aae517266197fbebb0e4dd5eab4b22414fbfea309f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z045_t001_CellsLabels.tiff": "c678f2100c7431e5a43c391af86a79e3436d4cf13f050f98c25f62682e4632fe", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z028_t001_NucleiLabels.tiff": "1676f21bd3b7e105601c83f206890507c32ba2447988be2a75d0e8153d069b65", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z039_t001_NucleiLabels.tiff": "f93e8293a37886a36844b8748083a1f8e717efab092652755263e797f12f0364", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z055_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z047_t001_CellsLabels.tiff": "b926aa64e1fa8aa52279a69d91dc23cf14bf0ae76107f9b360c7ae77bdf53719", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z054_t001_NucleiLabels.tiff": "b841c5d901c65a6138d9ff2b0ec4aa04f00765b1dd21bcd6fc9f8dc974c6035d", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z054_t001_CellsLabels.tiff": "25ff2ec586a568e42286f73713d64aab1208c38c016099299211f04368ac49c8", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z048_t001_CellsLabels.tiff": "133a58f6fac8b07f79bfed9634867ccb5428b83e63682031a1def16c6a694568", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z019_t001_NucleiLabels.tiff": "250cb56a8054c28cea894bc3d9edaddcdce01f5151a05da78da20157c3c3be9f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z005_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z023_t001_NucleiLabels.tiff": "ecb7f78c8ca0bad2b9d0495659609b9023781b6a936d6afb0a04653eac176555", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z040_t001_CellsLabels.tiff": "1635ea20dd80ce99f0e86494666825a52a66645837d598fd3a3fa91f0bb77d53", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z046_t001_NucleiLabels.tiff": "c1250e060d32f2cfc07fc7e1594fdb1d6cbb4d0483fa94a8113efa695397963c", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z003_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z010_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z022_t001_NucleiLabels.tiff": "50aef20e28a2f9e18ca2e28eabf6d87ec2ad20c79898e56f8d63a959ba5bb507", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z010_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z026_t001_NucleiLabels.tiff": "ad22809ff3f43d15c838f697061c263070dbf1da1031da39900681cdfbf9d54c", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z041_t001_CellsLabels.tiff": "be7762756eb65f08cb337904165c6f43b5351f39405461f65ad28669abfe3899", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z027_t001_NucleiLabels.tiff": "8c8f402d3a7c65c04e7f1575dd34de0427eeeb433ff2956d20e07d06eb48fb8d", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z042_t001_NucleiLabels.tiff": "e07252ca1a55c5195c99666d8d3f0d70db636b4ef4b99e1a0049ca22b182d87a", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z029_t001_NucleiLabels.tiff": "34bce0f06f674e20667554eed0075fde8d9d33c9c0eaeffd88a1c558499cc432", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z001_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z051_t001_NucleiLabels.tiff": "f0f36ccaf744d42da036948682cfd226e6dd4a3cfb94aed38729b2d25ede5dff", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z024_t001_CellsLabels.tiff": "39899adabca74b10b9950d1f97e8b0b3bbaf136d75e7a46ef4a6e86896e3119f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z003_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z035_t001_CellsLabels.tiff": "eedeb489b46abf1eafbd9c81491cb3f85ce1cb3ce877844ad22e72547fc3c438", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z038_t001_NucleiLabels.tiff": "e2852ea83c62ab30d77681b4e39f88d3cee74ae052f1c948675ae3581c9fc28a", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z009_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z056_t001_NucleiLabels.tiff": "ca0723c8b9764d5b77034719bb75a2940be35da2e04119af4c0e6d3552c08718", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z020_t001_NucleiLabels.tiff": "b0cd031b448d8bd2075e65710bd76c064161b25242f85ee150da7cd139fcc1a8", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z039_t001_CellsLabels.tiff": "22fa79a76e64013f10373be2ca2af98a2173275c5d43c9cad33135acdc376d14", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z007_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z021_t001_NucleiLabels.tiff": "71a10b0539f3fc4cba600b275b7e948258c6a435ba33ef522faa0869ac085518", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z026_t001_CellsLabels.tiff": "5b433d931fa6637fc793b23880199c01000ef6d9e610903bb38bb8cea5574563", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z037_t001_CellsLabels.tiff": "aa1f449f1769b27cd9f1076e70f49f0242e2549f288a77984ca159cb07cf6a40", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z050_t001_NucleiLabels.tiff": "565b4f08f7503086c1f2f8e631d19063129e02a5767d2f979c0cec8fdec23601", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z032_t001_NucleiLabels.tiff": "97b09fe5444ad439c3c2007d75971dce2766d94c93268eccfee9ae49b8a04aa1", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z001_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z046_t001_CellsLabels.tiff": "dc081df050be6cd19b5d5bbfe21c96c350eecf9bc6f10f608dc361a36056632a", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z020_t001_CellsLabels.tiff": "e8105a4f04198ac75de9831247030084d6f65bf98691d7444ead7e178072fa22", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z033_t001_NucleiLabels.tiff": "512a3a0ed8025d14b0556a72ebfc13b173d14f7046817ca09876242ab63b9d6b", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z025_t001_NucleiLabels.tiff": "9cc33676a0177130609ab1331feab1722bbde8d55b2f49031730fd5a2a40029c", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z015_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z018_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z036_t001_CellsLabels.tiff": "28b68d1300cf7b08e21dbcd4858447862520dded5c27f2d84cf223513a119eed", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z008_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z030_t001_CellsLabels.tiff": "b71a2e05d270ae0cd237d6c0d97ea82a63e5ad3633723e7255c35d34e585ec80", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z031_t001_CellsLabels.tiff": "1b14aab71bf3623458ea9373852b0baf28be9727877b32f894407927ecfb6d65", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z016_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z015_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z055_t001_NucleiLabels.tiff": "44e02416cc94a70528165a4b6c42b9e19b205f9c91528076796272cb1a8504ea", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z056_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z021_t001_CellsLabels.tiff": "daa31cb1e537e5c32b1936ab7647c5690741d2bc1d0e15c60b59ba1310a8c0ce", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z022_t001_CellsLabels.tiff": "7d7ec170ed3652efa99e43d8db546c51b696821dca7727d0ff4d2f2ead7fdbaa", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z027_t001_CellsLabels.tiff": "ee9056587a19e581f5feb6078b41fa1004d923a5ddf1725aefc4b8b8c5201ad6", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z057_t001_NucleiLabels.tiff": "695517641e9fe4b8421d4a7db56b24c755b6957c98ce32bb24ad5b25f52661eb", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z057_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z041_t001_NucleiLabels.tiff": "744eab7cd170d5af8d2f72ced3893dd939c4e1b0f415afab799a37ded5d046c7", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z042_t001_CellsLabels.tiff": "88d2e9964455459499656c4f0a762cd86376c678756635aaa6b6c8d98be01852", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z013_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z008_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/openhcs_metadata.json": "b683f94e72c5c8a1f41b7680379e158dfc1caaea6659391cf24e0ee8e470e008", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z049_t001_NucleiLabels.tiff": "e92e493078672a0e0d90e31409612e215bf8486300d5c765d0e0504ff8888e7a", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z043_t001_NucleiLabels.tiff": "81a705d05b0a5bfa8783df28990c1d70634c1f4aa2bca23e02cfa8a4bc861b86", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z052_t001_CellsLabels.tiff": "7bb98f0587062e01b965e486fc687872651d7d90d271a2fdf0d35d67b464b233", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z031_t001_NucleiLabels.tiff": "247490c261fecc26c7379ab2eebaa01e4bea523218b3cc5491d2fe2b9aadfe11", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z011_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z014_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z016_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z058_t001_NucleiLabels.tiff": "a37c63c6052e40d73db918a014e6f2a18be4d0cbfbeeef65a9c26096a38a4c09", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_Image.csv": "75ac32ff5ef57bc285ec4eae1c9b501c27a35b486bf35435af5d499d856d34a4", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z036_t001_NucleiLabels.tiff": "4fa967c78709aa400246a50528a36b24ca366e825dd0e64561643369e814906b", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z060_t001_NucleiLabels.tiff": "7cc3c6c6a22f558e62194277090bdea2deb79475dd91fe7d0e49eac3c0187388", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z006_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z029_t001_CellsLabels.tiff": "b3bdde5f751b8f5d41ca8b5219a3427e450cb1b8795401d0ec638940d1ad7bd9", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z025_t001_CellsLabels.tiff": "03aabe5528198cfd6ec23a058dab540fb8be0870207c6dc94a62d9b621d1f2f9", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z004_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z060_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z013_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z014_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z024_t001_NucleiLabels.tiff": "9e96a05ed9860cac9c57c618db2f4fe5e4b769a9d235aac6fd1d628486317df0", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/openhcs_metadata.json.lock": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z005_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z044_t001_CellsLabels.tiff": "e03946d94c789ee787cc29a3c8f5e5d9c56b37a0044b297ba8f403abc98eb55e", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z002_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z034_t001_CellsLabels.tiff": "3f6c1d8cdbce20aac8f70e7315bea70b147b7b6a034432845b27eeb6cfba083c", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z038_t001_CellsLabels.tiff": "6ac8fd9c95acc1f261e96a8fb9332e5d6a403548842f9b85ecb977e6213b4c9b", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z033_t001_CellsLabels.tiff": "a320b99ee129d97b99f9fac66aabb89fc1652930b3bb8b2b3fbba7b57e994f85", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z049_t001_CellsLabels.tiff": "28d83dfbf5bbf7e4da9c3e4806353e9fd0a89680dcdab4b43ca2fd19b80f669c", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z053_t001_CellsLabels.tiff": "ff17f538baaf179752412026a250bdbee016e3101037bf4e0dbb81199fc4c8ed", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z009_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z043_t001_CellsLabels.tiff": "bd0df007524ea4e555e4d2fdbebe6a274caa03bc360a1cf90d601659155984b2", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z030_t001_NucleiLabels.tiff": "c54c8b98a5c38a47e0cf9391a1afab09b4c8c58e06294cdb650a15add4a32b58", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z002_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z023_t001_CellsLabels.tiff": "2f06989618050934c73473f9342c2fe3410fd834a0ddb976cf602fe6198db7a6", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z006_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_Nuclei.csv": "17500f5d4321bbf255df31f7a3503339029c2b077045a6c10e4d4f6f0db9ad1e", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z040_t001_NucleiLabels.tiff": "e0bd66b166e9f92120dbee183973849eda0afbbe6fed9d03fc4cfd2c56a96280", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z017_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z012_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z058_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_Cells.csv": "6f6e7431171ed5ea73a01784105147eca0e43a6e3e41f8720df93106353c9c79", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z059_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z035_t001_NucleiLabels.tiff": "13e69afc324152bb91329fa758d131d849127e8fd346078d94c6adc25582b3a6", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z045_t001_NucleiLabels.tiff": "94bd6a65795ad6395cc18a4fd63dc591354bef612ee49fffd2d9f63e4ed9db84", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z050_t001_CellsLabels.tiff": "ea7654a6fe1f92ad274a54935420923eec4bec5ad030187afc0ac7af02690af5", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z051_t001_CellsLabels.tiff": "73483bd10a0f37990084013ad054768086d6d3c929da6bc7e74f4aee37e9dbf2", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_erodedDownsizedNuclei.csv": "ee507fb9c2387f86fc768184baf2596ebe1f8e40f7fc5eb24f252345fee63ae6", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z012_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z011_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_erodedResizedNuclei.csv": "cca23f86a85bb3ee7a5c3a1d174c34b5f026ca7f399be98458e1a8c6e12d5c77", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z059_t001_NucleiLabels.tiff": "76c785cf0f481c73ad3cf34ccb576eaa19dd1bdd9c74f9eaf92adfbde5d44b97", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z028_t001_CellsLabels.tiff": "fef31040e0c54501c3bf965eb98f055cdb1d42c96f91cf1c239033d0a9a06534", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z034_t001_NucleiLabels.tiff": "08af61857cfb97fd95fcb08d1ce18139ce7087b192044c63452bee533c5b1aa4", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z053_t001_NucleiLabels.tiff": "b8613192175a4615b58aed607474657668429cb6ac635cd012569b19667f2d3d", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_downsizedNuclei.csv": "a7b998c5a524e11747fc5452fd763fc3d9abe16f46e961f62e09e9393b6a8adf", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z004_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z018_t001_NucleiLabels.tiff": "03ba7a4d6ae53e6c84aab5926ae1e2592b5f8ca24a839a256c82f08544df4270", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z048_t001_NucleiLabels.tiff": "325478230529e94ba372f38f0bf512292f131d337b6ac09ce5a91b0266724e02" + }, + "cohort": "plane-facts-whole-driver-baseline-v1" + }, + { + "case": "cp_tutorial_3d_monolayer", + "ordinary_repeat": 0, + "native_repeat": 1, + "measurement_differences": 0, + "image_differences": 0, + "tables": [ + 7, + 6 + ], + "physical_images": [ + 2, + 120 + ], + "logical_images": [ + 2, + 2 + ], + "known_relationship_keys": [ + 3, + 3 + ], + "ordinary_receipt_sha256": "da9430eb33c8e5724edc9c28431ff22848bef9a0eda94da59139b20a3349058e", + "output_hashes": { + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_downsizedNuclei.csv": "2c66bc7b17de52147d5a875403f9a9746019ac61371c08157979044c6ed818dd", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_xy1_ch0_CellsLabels.tiff": "aaed9ed23179a162ee7f5a7e8d1d88f1fa342ca55172b7fccd4cbf67f7841d81", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_Image.csv": "4cde74a38ef73b892dd9012708a3ac7d71fab206a7903f525b39e84493dcfe17", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_Cells.csv": "cc34681db8a66c38ca77bc47db496b26703f21e4d7c7628dd74e30c33250859f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_erodedResizedNuclei.csv": "66a58f78cab0cfdbe907468e8ba383fc59fab49025b84b589855bdfa04854609", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_Experiment.csv": "65f1f124e99f13ddc73b71d01732262cd353f24f0bd4697a776b274a39eaecc5", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_Nuclei.csv": "440c33edc7e8cb5611910a835454db62390301a7af617a542349f60aece57561", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_erodedDownsizedNuclei.csv": "527a0cce497d2a618478be84d1a0bf174049f58aa2a666e37626a990fdca0116", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_xy1_ch2_NucleiLabels.tiff": "340bbd795541e4031749d1fff767fa4fcdcac10f5b97879056b25dac9fd7674b", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z017_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z037_t001_NucleiLabels.tiff": "927d52441c57cb3a4014effaca3e3c23c76275fb264c26e1942a9c58b85ee6d2", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z044_t001_NucleiLabels.tiff": "a78264f2f5e245a3913a69f89105e94df30048b1aaef4eb80fc5c3235e78b196", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z052_t001_NucleiLabels.tiff": "0daf51526658e23b7c9ab8872e992ebe9322399286f95741d0ccdbd357e5d631", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z007_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z047_t001_NucleiLabels.tiff": "995d5ce77cbf61a9be8590f34987ef3f1dd698ae6ed393cac4d0960bc9e7a66a", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z019_t001_CellsLabels.tiff": "ec6ef1d319f2732c47b06f5cf02775c8cd94540f2f30a6c002f0aace1d317955", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z032_t001_CellsLabels.tiff": "8a2c5774044326ae722e41aae517266197fbebb0e4dd5eab4b22414fbfea309f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z045_t001_CellsLabels.tiff": "c678f2100c7431e5a43c391af86a79e3436d4cf13f050f98c25f62682e4632fe", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z028_t001_NucleiLabels.tiff": "1676f21bd3b7e105601c83f206890507c32ba2447988be2a75d0e8153d069b65", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z039_t001_NucleiLabels.tiff": "f93e8293a37886a36844b8748083a1f8e717efab092652755263e797f12f0364", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z055_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z047_t001_CellsLabels.tiff": "b926aa64e1fa8aa52279a69d91dc23cf14bf0ae76107f9b360c7ae77bdf53719", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z054_t001_NucleiLabels.tiff": "b841c5d901c65a6138d9ff2b0ec4aa04f00765b1dd21bcd6fc9f8dc974c6035d", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z054_t001_CellsLabels.tiff": "25ff2ec586a568e42286f73713d64aab1208c38c016099299211f04368ac49c8", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z048_t001_CellsLabels.tiff": "133a58f6fac8b07f79bfed9634867ccb5428b83e63682031a1def16c6a694568", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z019_t001_NucleiLabels.tiff": "250cb56a8054c28cea894bc3d9edaddcdce01f5151a05da78da20157c3c3be9f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z005_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z023_t001_NucleiLabels.tiff": "ecb7f78c8ca0bad2b9d0495659609b9023781b6a936d6afb0a04653eac176555", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z040_t001_CellsLabels.tiff": "1635ea20dd80ce99f0e86494666825a52a66645837d598fd3a3fa91f0bb77d53", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z046_t001_NucleiLabels.tiff": "c1250e060d32f2cfc07fc7e1594fdb1d6cbb4d0483fa94a8113efa695397963c", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z003_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z010_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z022_t001_NucleiLabels.tiff": "50aef20e28a2f9e18ca2e28eabf6d87ec2ad20c79898e56f8d63a959ba5bb507", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z010_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z026_t001_NucleiLabels.tiff": "ad22809ff3f43d15c838f697061c263070dbf1da1031da39900681cdfbf9d54c", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z041_t001_CellsLabels.tiff": "be7762756eb65f08cb337904165c6f43b5351f39405461f65ad28669abfe3899", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z027_t001_NucleiLabels.tiff": "8c8f402d3a7c65c04e7f1575dd34de0427eeeb433ff2956d20e07d06eb48fb8d", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z042_t001_NucleiLabels.tiff": "e07252ca1a55c5195c99666d8d3f0d70db636b4ef4b99e1a0049ca22b182d87a", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z029_t001_NucleiLabels.tiff": "34bce0f06f674e20667554eed0075fde8d9d33c9c0eaeffd88a1c558499cc432", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z001_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z051_t001_NucleiLabels.tiff": "f0f36ccaf744d42da036948682cfd226e6dd4a3cfb94aed38729b2d25ede5dff", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z024_t001_CellsLabels.tiff": "39899adabca74b10b9950d1f97e8b0b3bbaf136d75e7a46ef4a6e86896e3119f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z003_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z035_t001_CellsLabels.tiff": "eedeb489b46abf1eafbd9c81491cb3f85ce1cb3ce877844ad22e72547fc3c438", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z038_t001_NucleiLabels.tiff": "e2852ea83c62ab30d77681b4e39f88d3cee74ae052f1c948675ae3581c9fc28a", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z009_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z056_t001_NucleiLabels.tiff": "ca0723c8b9764d5b77034719bb75a2940be35da2e04119af4c0e6d3552c08718", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z020_t001_NucleiLabels.tiff": "b0cd031b448d8bd2075e65710bd76c064161b25242f85ee150da7cd139fcc1a8", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z039_t001_CellsLabels.tiff": "22fa79a76e64013f10373be2ca2af98a2173275c5d43c9cad33135acdc376d14", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z007_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z021_t001_NucleiLabels.tiff": "71a10b0539f3fc4cba600b275b7e948258c6a435ba33ef522faa0869ac085518", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z026_t001_CellsLabels.tiff": "5b433d931fa6637fc793b23880199c01000ef6d9e610903bb38bb8cea5574563", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z037_t001_CellsLabels.tiff": "aa1f449f1769b27cd9f1076e70f49f0242e2549f288a77984ca159cb07cf6a40", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z050_t001_NucleiLabels.tiff": "565b4f08f7503086c1f2f8e631d19063129e02a5767d2f979c0cec8fdec23601", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z032_t001_NucleiLabels.tiff": "97b09fe5444ad439c3c2007d75971dce2766d94c93268eccfee9ae49b8a04aa1", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z001_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z046_t001_CellsLabels.tiff": "dc081df050be6cd19b5d5bbfe21c96c350eecf9bc6f10f608dc361a36056632a", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z020_t001_CellsLabels.tiff": "e8105a4f04198ac75de9831247030084d6f65bf98691d7444ead7e178072fa22", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z033_t001_NucleiLabels.tiff": "512a3a0ed8025d14b0556a72ebfc13b173d14f7046817ca09876242ab63b9d6b", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z025_t001_NucleiLabels.tiff": "9cc33676a0177130609ab1331feab1722bbde8d55b2f49031730fd5a2a40029c", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z015_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z018_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z036_t001_CellsLabels.tiff": "28b68d1300cf7b08e21dbcd4858447862520dded5c27f2d84cf223513a119eed", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z008_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z030_t001_CellsLabels.tiff": "b71a2e05d270ae0cd237d6c0d97ea82a63e5ad3633723e7255c35d34e585ec80", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z031_t001_CellsLabels.tiff": "1b14aab71bf3623458ea9373852b0baf28be9727877b32f894407927ecfb6d65", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z016_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z015_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z055_t001_NucleiLabels.tiff": "44e02416cc94a70528165a4b6c42b9e19b205f9c91528076796272cb1a8504ea", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z056_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z021_t001_CellsLabels.tiff": "daa31cb1e537e5c32b1936ab7647c5690741d2bc1d0e15c60b59ba1310a8c0ce", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z022_t001_CellsLabels.tiff": "7d7ec170ed3652efa99e43d8db546c51b696821dca7727d0ff4d2f2ead7fdbaa", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z027_t001_CellsLabels.tiff": "ee9056587a19e581f5feb6078b41fa1004d923a5ddf1725aefc4b8b8c5201ad6", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z057_t001_NucleiLabels.tiff": "695517641e9fe4b8421d4a7db56b24c755b6957c98ce32bb24ad5b25f52661eb", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z057_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z041_t001_NucleiLabels.tiff": "744eab7cd170d5af8d2f72ced3893dd939c4e1b0f415afab799a37ded5d046c7", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z042_t001_CellsLabels.tiff": "88d2e9964455459499656c4f0a762cd86376c678756635aaa6b6c8d98be01852", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z013_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z008_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/openhcs_metadata.json": "b683f94e72c5c8a1f41b7680379e158dfc1caaea6659391cf24e0ee8e470e008", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z049_t001_NucleiLabels.tiff": "e92e493078672a0e0d90e31409612e215bf8486300d5c765d0e0504ff8888e7a", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z043_t001_NucleiLabels.tiff": "81a705d05b0a5bfa8783df28990c1d70634c1f4aa2bca23e02cfa8a4bc861b86", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z052_t001_CellsLabels.tiff": "7bb98f0587062e01b965e486fc687872651d7d90d271a2fdf0d35d67b464b233", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z031_t001_NucleiLabels.tiff": "247490c261fecc26c7379ab2eebaa01e4bea523218b3cc5491d2fe2b9aadfe11", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z011_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z014_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z016_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z058_t001_NucleiLabels.tiff": "a37c63c6052e40d73db918a014e6f2a18be4d0cbfbeeef65a9c26096a38a4c09", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_Image.csv": "75ac32ff5ef57bc285ec4eae1c9b501c27a35b486bf35435af5d499d856d34a4", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z036_t001_NucleiLabels.tiff": "4fa967c78709aa400246a50528a36b24ca366e825dd0e64561643369e814906b", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z060_t001_NucleiLabels.tiff": "7cc3c6c6a22f558e62194277090bdea2deb79475dd91fe7d0e49eac3c0187388", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z006_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z029_t001_CellsLabels.tiff": "b3bdde5f751b8f5d41ca8b5219a3427e450cb1b8795401d0ec638940d1ad7bd9", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z025_t001_CellsLabels.tiff": "03aabe5528198cfd6ec23a058dab540fb8be0870207c6dc94a62d9b621d1f2f9", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z004_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z060_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z013_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z014_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z024_t001_NucleiLabels.tiff": "9e96a05ed9860cac9c57c618db2f4fe5e4b769a9d235aac6fd1d628486317df0", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/openhcs_metadata.json.lock": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z005_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z044_t001_CellsLabels.tiff": "e03946d94c789ee787cc29a3c8f5e5d9c56b37a0044b297ba8f403abc98eb55e", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z002_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z034_t001_CellsLabels.tiff": "3f6c1d8cdbce20aac8f70e7315bea70b147b7b6a034432845b27eeb6cfba083c", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z038_t001_CellsLabels.tiff": "6ac8fd9c95acc1f261e96a8fb9332e5d6a403548842f9b85ecb977e6213b4c9b", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z033_t001_CellsLabels.tiff": "a320b99ee129d97b99f9fac66aabb89fc1652930b3bb8b2b3fbba7b57e994f85", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z049_t001_CellsLabels.tiff": "28d83dfbf5bbf7e4da9c3e4806353e9fd0a89680dcdab4b43ca2fd19b80f669c", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z053_t001_CellsLabels.tiff": "ff17f538baaf179752412026a250bdbee016e3101037bf4e0dbb81199fc4c8ed", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z009_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z043_t001_CellsLabels.tiff": "bd0df007524ea4e555e4d2fdbebe6a274caa03bc360a1cf90d601659155984b2", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z030_t001_NucleiLabels.tiff": "c54c8b98a5c38a47e0cf9391a1afab09b4c8c58e06294cdb650a15add4a32b58", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z002_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z023_t001_CellsLabels.tiff": "2f06989618050934c73473f9342c2fe3410fd834a0ddb976cf602fe6198db7a6", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z006_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_Nuclei.csv": "17500f5d4321bbf255df31f7a3503339029c2b077045a6c10e4d4f6f0db9ad1e", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z040_t001_NucleiLabels.tiff": "e0bd66b166e9f92120dbee183973849eda0afbbe6fed9d03fc4cfd2c56a96280", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z017_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z012_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z058_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_Cells.csv": "6f6e7431171ed5ea73a01784105147eca0e43a6e3e41f8720df93106353c9c79", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z059_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z035_t001_NucleiLabels.tiff": "13e69afc324152bb91329fa758d131d849127e8fd346078d94c6adc25582b3a6", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z045_t001_NucleiLabels.tiff": "94bd6a65795ad6395cc18a4fd63dc591354bef612ee49fffd2d9f63e4ed9db84", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z050_t001_CellsLabels.tiff": "ea7654a6fe1f92ad274a54935420923eec4bec5ad030187afc0ac7af02690af5", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z051_t001_CellsLabels.tiff": "73483bd10a0f37990084013ad054768086d6d3c929da6bc7e74f4aee37e9dbf2", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_erodedDownsizedNuclei.csv": "ee507fb9c2387f86fc768184baf2596ebe1f8e40f7fc5eb24f252345fee63ae6", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z012_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z011_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_erodedResizedNuclei.csv": "cca23f86a85bb3ee7a5c3a1d174c34b5f026ca7f399be98458e1a8c6e12d5c77", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z059_t001_NucleiLabels.tiff": "76c785cf0f481c73ad3cf34ccb576eaa19dd1bdd9c74f9eaf92adfbde5d44b97", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z028_t001_CellsLabels.tiff": "fef31040e0c54501c3bf965eb98f055cdb1d42c96f91cf1c239033d0a9a06534", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z034_t001_NucleiLabels.tiff": "08af61857cfb97fd95fcb08d1ce18139ce7087b192044c63452bee533c5b1aa4", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z053_t001_NucleiLabels.tiff": "b8613192175a4615b58aed607474657668429cb6ac635cd012569b19667f2d3d", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_downsizedNuclei.csv": "a7b998c5a524e11747fc5452fd763fc3d9abe16f46e961f62e09e9393b6a8adf", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z004_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z018_t001_NucleiLabels.tiff": "03ba7a4d6ae53e6c84aab5926ae1e2592b5f8ca24a839a256c82f08544df4270", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z048_t001_NucleiLabels.tiff": "325478230529e94ba372f38f0bf512292f131d337b6ac09ce5a91b0266724e02" + }, + "cohort": "plane-facts-whole-driver-baseline-v1" + }, + { + "case": "cp_tutorial_3d_monolayer", + "ordinary_repeat": 1, + "native_repeat": 0, + "measurement_differences": 0, + "image_differences": 0, + "tables": [ + 7, + 6 + ], + "physical_images": [ + 2, + 120 + ], + "logical_images": [ + 2, + 2 + ], + "known_relationship_keys": [ + 3, + 3 + ], + "ordinary_receipt_sha256": "49db7f2e2cfda0c0fd7dc3a4fa6a7542e70038fde080dbf73f5498f1a506ae34", + "output_hashes": { + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_downsizedNuclei.csv": "2c66bc7b17de52147d5a875403f9a9746019ac61371c08157979044c6ed818dd", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_Cells.csv": "cc34681db8a66c38ca77bc47db496b26703f21e4d7c7628dd74e30c33250859f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_xy1_ch2_NucleiLabels.tiff": "340bbd795541e4031749d1fff767fa4fcdcac10f5b97879056b25dac9fd7674b", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_xy1_ch0_CellsLabels.tiff": "aaed9ed23179a162ee7f5a7e8d1d88f1fa342ca55172b7fccd4cbf67f7841d81", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_erodedDownsizedNuclei.csv": "527a0cce497d2a618478be84d1a0bf174049f58aa2a666e37626a990fdca0116", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_erodedResizedNuclei.csv": "66a58f78cab0cfdbe907468e8ba383fc59fab49025b84b589855bdfa04854609", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_Nuclei.csv": "440c33edc7e8cb5611910a835454db62390301a7af617a542349f60aece57561", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_Image.csv": "1f3dec653b6909c8a81ac109ce1faf91380886ac9a18e56fafc91102d419904e", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_Experiment.csv": "c05f0a11f43a5bda943e9f739ff257216f8ab3f65d8988a0d3d0c612c1db6997", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z025_t001_CellsLabels.tiff": "03aabe5528198cfd6ec23a058dab540fb8be0870207c6dc94a62d9b621d1f2f9", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z039_t001_NucleiLabels.tiff": "f93e8293a37886a36844b8748083a1f8e717efab092652755263e797f12f0364", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z010_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z051_t001_NucleiLabels.tiff": "f0f36ccaf744d42da036948682cfd226e6dd4a3cfb94aed38729b2d25ede5dff", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z014_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z013_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z016_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z060_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z004_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z056_t001_NucleiLabels.tiff": "ca0723c8b9764d5b77034719bb75a2940be35da2e04119af4c0e6d3552c08718", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z045_t001_NucleiLabels.tiff": "94bd6a65795ad6395cc18a4fd63dc591354bef612ee49fffd2d9f63e4ed9db84", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z017_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z046_t001_NucleiLabels.tiff": "c1250e060d32f2cfc07fc7e1594fdb1d6cbb4d0483fa94a8113efa695397963c", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z058_t001_NucleiLabels.tiff": "a37c63c6052e40d73db918a014e6f2a18be4d0cbfbeeef65a9c26096a38a4c09", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z047_t001_NucleiLabels.tiff": "995d5ce77cbf61a9be8590f34987ef3f1dd698ae6ed393cac4d0960bc9e7a66a", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z024_t001_CellsLabels.tiff": "39899adabca74b10b9950d1f97e8b0b3bbaf136d75e7a46ef4a6e86896e3119f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_Nuclei.csv": "17500f5d4321bbf255df31f7a3503339029c2b077045a6c10e4d4f6f0db9ad1e", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z054_t001_CellsLabels.tiff": "25ff2ec586a568e42286f73713d64aab1208c38c016099299211f04368ac49c8", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z055_t001_NucleiLabels.tiff": "44e02416cc94a70528165a4b6c42b9e19b205f9c91528076796272cb1a8504ea", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z007_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z031_t001_NucleiLabels.tiff": "247490c261fecc26c7379ab2eebaa01e4bea523218b3cc5491d2fe2b9aadfe11", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z051_t001_CellsLabels.tiff": "73483bd10a0f37990084013ad054768086d6d3c929da6bc7e74f4aee37e9dbf2", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z049_t001_CellsLabels.tiff": "28d83dfbf5bbf7e4da9c3e4806353e9fd0a89680dcdab4b43ca2fd19b80f669c", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z028_t001_NucleiLabels.tiff": "1676f21bd3b7e105601c83f206890507c32ba2447988be2a75d0e8153d069b65", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z016_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z005_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z053_t001_CellsLabels.tiff": "ff17f538baaf179752412026a250bdbee016e3101037bf4e0dbb81199fc4c8ed", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z044_t001_NucleiLabels.tiff": "a78264f2f5e245a3913a69f89105e94df30048b1aaef4eb80fc5c3235e78b196", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z055_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z045_t001_CellsLabels.tiff": "c678f2100c7431e5a43c391af86a79e3436d4cf13f050f98c25f62682e4632fe", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_downsizedNuclei.csv": "a7b998c5a524e11747fc5452fd763fc3d9abe16f46e961f62e09e9393b6a8adf", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z027_t001_CellsLabels.tiff": "ee9056587a19e581f5feb6078b41fa1004d923a5ddf1725aefc4b8b8c5201ad6", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z049_t001_NucleiLabels.tiff": "e92e493078672a0e0d90e31409612e215bf8486300d5c765d0e0504ff8888e7a", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z032_t001_CellsLabels.tiff": "8a2c5774044326ae722e41aae517266197fbebb0e4dd5eab4b22414fbfea309f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z010_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z031_t001_CellsLabels.tiff": "1b14aab71bf3623458ea9373852b0baf28be9727877b32f894407927ecfb6d65", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z056_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z047_t001_CellsLabels.tiff": "b926aa64e1fa8aa52279a69d91dc23cf14bf0ae76107f9b360c7ae77bdf53719", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z041_t001_NucleiLabels.tiff": "744eab7cd170d5af8d2f72ced3893dd939c4e1b0f415afab799a37ded5d046c7", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z006_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z033_t001_CellsLabels.tiff": "a320b99ee129d97b99f9fac66aabb89fc1652930b3bb8b2b3fbba7b57e994f85", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z023_t001_NucleiLabels.tiff": "ecb7f78c8ca0bad2b9d0495659609b9023781b6a936d6afb0a04653eac176555", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z060_t001_NucleiLabels.tiff": "7cc3c6c6a22f558e62194277090bdea2deb79475dd91fe7d0e49eac3c0187388", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z003_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z038_t001_CellsLabels.tiff": "6ac8fd9c95acc1f261e96a8fb9332e5d6a403548842f9b85ecb977e6213b4c9b", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z009_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z002_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z042_t001_CellsLabels.tiff": "88d2e9964455459499656c4f0a762cd86376c678756635aaa6b6c8d98be01852", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z054_t001_NucleiLabels.tiff": "b841c5d901c65a6138d9ff2b0ec4aa04f00765b1dd21bcd6fc9f8dc974c6035d", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z018_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z024_t001_NucleiLabels.tiff": "9e96a05ed9860cac9c57c618db2f4fe5e4b769a9d235aac6fd1d628486317df0", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z052_t001_CellsLabels.tiff": "7bb98f0587062e01b965e486fc687872651d7d90d271a2fdf0d35d67b464b233", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z041_t001_CellsLabels.tiff": "be7762756eb65f08cb337904165c6f43b5351f39405461f65ad28669abfe3899", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z048_t001_CellsLabels.tiff": "133a58f6fac8b07f79bfed9634867ccb5428b83e63682031a1def16c6a694568", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z043_t001_CellsLabels.tiff": "bd0df007524ea4e555e4d2fdbebe6a274caa03bc360a1cf90d601659155984b2", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z012_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z004_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z038_t001_NucleiLabels.tiff": "e2852ea83c62ab30d77681b4e39f88d3cee74ae052f1c948675ae3581c9fc28a", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z021_t001_CellsLabels.tiff": "daa31cb1e537e5c32b1936ab7647c5690741d2bc1d0e15c60b59ba1310a8c0ce", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z019_t001_NucleiLabels.tiff": "250cb56a8054c28cea894bc3d9edaddcdce01f5151a05da78da20157c3c3be9f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z026_t001_CellsLabels.tiff": "5b433d931fa6637fc793b23880199c01000ef6d9e610903bb38bb8cea5574563", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z029_t001_CellsLabels.tiff": "b3bdde5f751b8f5d41ca8b5219a3427e450cb1b8795401d0ec638940d1ad7bd9", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z001_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z030_t001_CellsLabels.tiff": "b71a2e05d270ae0cd237d6c0d97ea82a63e5ad3633723e7255c35d34e585ec80", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/openhcs_metadata.json.lock": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z034_t001_NucleiLabels.tiff": "08af61857cfb97fd95fcb08d1ce18139ce7087b192044c63452bee533c5b1aa4", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z012_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z005_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z022_t001_NucleiLabels.tiff": "50aef20e28a2f9e18ca2e28eabf6d87ec2ad20c79898e56f8d63a959ba5bb507", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z035_t001_CellsLabels.tiff": "eedeb489b46abf1eafbd9c81491cb3f85ce1cb3ce877844ad22e72547fc3c438", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z032_t001_NucleiLabels.tiff": "97b09fe5444ad439c3c2007d75971dce2766d94c93268eccfee9ae49b8a04aa1", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z011_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z002_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z003_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z035_t001_NucleiLabels.tiff": "13e69afc324152bb91329fa758d131d849127e8fd346078d94c6adc25582b3a6", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z028_t001_CellsLabels.tiff": "fef31040e0c54501c3bf965eb98f055cdb1d42c96f91cf1c239033d0a9a06534", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z020_t001_NucleiLabels.tiff": "b0cd031b448d8bd2075e65710bd76c064161b25242f85ee150da7cd139fcc1a8", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z014_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z025_t001_NucleiLabels.tiff": "9cc33676a0177130609ab1331feab1722bbde8d55b2f49031730fd5a2a40029c", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z037_t001_CellsLabels.tiff": "aa1f449f1769b27cd9f1076e70f49f0242e2549f288a77984ca159cb07cf6a40", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z008_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z036_t001_NucleiLabels.tiff": "4fa967c78709aa400246a50528a36b24ca366e825dd0e64561643369e814906b", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z039_t001_CellsLabels.tiff": "22fa79a76e64013f10373be2ca2af98a2173275c5d43c9cad33135acdc376d14", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z020_t001_CellsLabels.tiff": "e8105a4f04198ac75de9831247030084d6f65bf98691d7444ead7e178072fa22", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z015_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z046_t001_CellsLabels.tiff": "dc081df050be6cd19b5d5bbfe21c96c350eecf9bc6f10f608dc361a36056632a", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z022_t001_CellsLabels.tiff": "7d7ec170ed3652efa99e43d8db546c51b696821dca7727d0ff4d2f2ead7fdbaa", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z026_t001_NucleiLabels.tiff": "ad22809ff3f43d15c838f697061c263070dbf1da1031da39900681cdfbf9d54c", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z001_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z036_t001_CellsLabels.tiff": "28b68d1300cf7b08e21dbcd4858447862520dded5c27f2d84cf223513a119eed", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z050_t001_NucleiLabels.tiff": "565b4f08f7503086c1f2f8e631d19063129e02a5767d2f979c0cec8fdec23601", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z034_t001_CellsLabels.tiff": "3f6c1d8cdbce20aac8f70e7315bea70b147b7b6a034432845b27eeb6cfba083c", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z037_t001_NucleiLabels.tiff": "927d52441c57cb3a4014effaca3e3c23c76275fb264c26e1942a9c58b85ee6d2", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z040_t001_NucleiLabels.tiff": "e0bd66b166e9f92120dbee183973849eda0afbbe6fed9d03fc4cfd2c56a96280", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z050_t001_CellsLabels.tiff": "ea7654a6fe1f92ad274a54935420923eec4bec5ad030187afc0ac7af02690af5", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z053_t001_NucleiLabels.tiff": "b8613192175a4615b58aed607474657668429cb6ac635cd012569b19667f2d3d", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z015_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z058_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z044_t001_CellsLabels.tiff": "e03946d94c789ee787cc29a3c8f5e5d9c56b37a0044b297ba8f403abc98eb55e", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z042_t001_NucleiLabels.tiff": "e07252ca1a55c5195c99666d8d3f0d70db636b4ef4b99e1a0049ca22b182d87a", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z030_t001_NucleiLabels.tiff": "c54c8b98a5c38a47e0cf9391a1afab09b4c8c58e06294cdb650a15add4a32b58", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z011_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z059_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_erodedResizedNuclei.csv": "cca23f86a85bb3ee7a5c3a1d174c34b5f026ca7f399be98458e1a8c6e12d5c77", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z017_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z023_t001_CellsLabels.tiff": "2f06989618050934c73473f9342c2fe3410fd834a0ddb976cf602fe6198db7a6", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z040_t001_CellsLabels.tiff": "1635ea20dd80ce99f0e86494666825a52a66645837d598fd3a3fa91f0bb77d53", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z027_t001_NucleiLabels.tiff": "8c8f402d3a7c65c04e7f1575dd34de0427eeeb433ff2956d20e07d06eb48fb8d", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z021_t001_NucleiLabels.tiff": "71a10b0539f3fc4cba600b275b7e948258c6a435ba33ef522faa0869ac085518", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z048_t001_NucleiLabels.tiff": "325478230529e94ba372f38f0bf512292f131d337b6ac09ce5a91b0266724e02", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z057_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z013_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z059_t001_NucleiLabels.tiff": "76c785cf0f481c73ad3cf34ccb576eaa19dd1bdd9c74f9eaf92adfbde5d44b97", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z018_t001_NucleiLabels.tiff": "03ba7a4d6ae53e6c84aab5926ae1e2592b5f8ca24a839a256c82f08544df4270", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z057_t001_NucleiLabels.tiff": "695517641e9fe4b8421d4a7db56b24c755b6957c98ce32bb24ad5b25f52661eb", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/openhcs_metadata.json": "9ff815827f4a4a5cec301260e570ffc7f1b56a9f6422e08309c08e3ed34e01ac", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z019_t001_CellsLabels.tiff": "ec6ef1d319f2732c47b06f5cf02775c8cd94540f2f30a6c002f0aace1d317955", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_erodedDownsizedNuclei.csv": "ee507fb9c2387f86fc768184baf2596ebe1f8e40f7fc5eb24f252345fee63ae6", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z043_t001_NucleiLabels.tiff": "81a705d05b0a5bfa8783df28990c1d70634c1f4aa2bca23e02cfa8a4bc861b86", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_Image.csv": "75ac32ff5ef57bc285ec4eae1c9b501c27a35b486bf35435af5d499d856d34a4", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z052_t001_NucleiLabels.tiff": "0daf51526658e23b7c9ab8872e992ebe9322399286f95741d0ccdbd357e5d631", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z033_t001_NucleiLabels.tiff": "512a3a0ed8025d14b0556a72ebfc13b173d14f7046817ca09876242ab63b9d6b", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z029_t001_NucleiLabels.tiff": "34bce0f06f674e20667554eed0075fde8d9d33c9c0eaeffd88a1c558499cc432", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_Cells.csv": "6f6e7431171ed5ea73a01784105147eca0e43a6e3e41f8720df93106353c9c79", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z008_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z007_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z009_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z006_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f" + }, + "cohort": "plane-facts-whole-driver-candidate-v1" + }, + { + "case": "cp_tutorial_3d_monolayer", + "ordinary_repeat": 1, + "native_repeat": 1, + "measurement_differences": 0, + "image_differences": 0, + "tables": [ + 7, + 6 + ], + "physical_images": [ + 2, + 120 + ], + "logical_images": [ + 2, + 2 + ], + "known_relationship_keys": [ + 3, + 3 + ], + "ordinary_receipt_sha256": "49db7f2e2cfda0c0fd7dc3a4fa6a7542e70038fde080dbf73f5498f1a506ae34", + "output_hashes": { + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_downsizedNuclei.csv": "2c66bc7b17de52147d5a875403f9a9746019ac61371c08157979044c6ed818dd", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_xy1_ch0_CellsLabels.tiff": "aaed9ed23179a162ee7f5a7e8d1d88f1fa342ca55172b7fccd4cbf67f7841d81", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_Image.csv": "4cde74a38ef73b892dd9012708a3ac7d71fab206a7903f525b39e84493dcfe17", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_Cells.csv": "cc34681db8a66c38ca77bc47db496b26703f21e4d7c7628dd74e30c33250859f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_erodedResizedNuclei.csv": "66a58f78cab0cfdbe907468e8ba383fc59fab49025b84b589855bdfa04854609", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_Experiment.csv": "65f1f124e99f13ddc73b71d01732262cd353f24f0bd4697a776b274a39eaecc5", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_Nuclei.csv": "440c33edc7e8cb5611910a835454db62390301a7af617a542349f60aece57561", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_erodedDownsizedNuclei.csv": "527a0cce497d2a618478be84d1a0bf174049f58aa2a666e37626a990fdca0116", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_xy1_ch2_NucleiLabels.tiff": "340bbd795541e4031749d1fff767fa4fcdcac10f5b97879056b25dac9fd7674b", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z025_t001_CellsLabels.tiff": "03aabe5528198cfd6ec23a058dab540fb8be0870207c6dc94a62d9b621d1f2f9", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z039_t001_NucleiLabels.tiff": "f93e8293a37886a36844b8748083a1f8e717efab092652755263e797f12f0364", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z010_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z051_t001_NucleiLabels.tiff": "f0f36ccaf744d42da036948682cfd226e6dd4a3cfb94aed38729b2d25ede5dff", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z014_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z013_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z016_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z060_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z004_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z056_t001_NucleiLabels.tiff": "ca0723c8b9764d5b77034719bb75a2940be35da2e04119af4c0e6d3552c08718", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z045_t001_NucleiLabels.tiff": "94bd6a65795ad6395cc18a4fd63dc591354bef612ee49fffd2d9f63e4ed9db84", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z017_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z046_t001_NucleiLabels.tiff": "c1250e060d32f2cfc07fc7e1594fdb1d6cbb4d0483fa94a8113efa695397963c", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z058_t001_NucleiLabels.tiff": "a37c63c6052e40d73db918a014e6f2a18be4d0cbfbeeef65a9c26096a38a4c09", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z047_t001_NucleiLabels.tiff": "995d5ce77cbf61a9be8590f34987ef3f1dd698ae6ed393cac4d0960bc9e7a66a", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z024_t001_CellsLabels.tiff": "39899adabca74b10b9950d1f97e8b0b3bbaf136d75e7a46ef4a6e86896e3119f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_Nuclei.csv": "17500f5d4321bbf255df31f7a3503339029c2b077045a6c10e4d4f6f0db9ad1e", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z054_t001_CellsLabels.tiff": "25ff2ec586a568e42286f73713d64aab1208c38c016099299211f04368ac49c8", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z055_t001_NucleiLabels.tiff": "44e02416cc94a70528165a4b6c42b9e19b205f9c91528076796272cb1a8504ea", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z007_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z031_t001_NucleiLabels.tiff": "247490c261fecc26c7379ab2eebaa01e4bea523218b3cc5491d2fe2b9aadfe11", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z051_t001_CellsLabels.tiff": "73483bd10a0f37990084013ad054768086d6d3c929da6bc7e74f4aee37e9dbf2", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z049_t001_CellsLabels.tiff": "28d83dfbf5bbf7e4da9c3e4806353e9fd0a89680dcdab4b43ca2fd19b80f669c", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z028_t001_NucleiLabels.tiff": "1676f21bd3b7e105601c83f206890507c32ba2447988be2a75d0e8153d069b65", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z016_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z005_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z053_t001_CellsLabels.tiff": "ff17f538baaf179752412026a250bdbee016e3101037bf4e0dbb81199fc4c8ed", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z044_t001_NucleiLabels.tiff": "a78264f2f5e245a3913a69f89105e94df30048b1aaef4eb80fc5c3235e78b196", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z055_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z045_t001_CellsLabels.tiff": "c678f2100c7431e5a43c391af86a79e3436d4cf13f050f98c25f62682e4632fe", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_downsizedNuclei.csv": "a7b998c5a524e11747fc5452fd763fc3d9abe16f46e961f62e09e9393b6a8adf", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z027_t001_CellsLabels.tiff": "ee9056587a19e581f5feb6078b41fa1004d923a5ddf1725aefc4b8b8c5201ad6", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z049_t001_NucleiLabels.tiff": "e92e493078672a0e0d90e31409612e215bf8486300d5c765d0e0504ff8888e7a", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z032_t001_CellsLabels.tiff": "8a2c5774044326ae722e41aae517266197fbebb0e4dd5eab4b22414fbfea309f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z010_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z031_t001_CellsLabels.tiff": "1b14aab71bf3623458ea9373852b0baf28be9727877b32f894407927ecfb6d65", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z056_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z047_t001_CellsLabels.tiff": "b926aa64e1fa8aa52279a69d91dc23cf14bf0ae76107f9b360c7ae77bdf53719", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z041_t001_NucleiLabels.tiff": "744eab7cd170d5af8d2f72ced3893dd939c4e1b0f415afab799a37ded5d046c7", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z006_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z033_t001_CellsLabels.tiff": "a320b99ee129d97b99f9fac66aabb89fc1652930b3bb8b2b3fbba7b57e994f85", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z023_t001_NucleiLabels.tiff": "ecb7f78c8ca0bad2b9d0495659609b9023781b6a936d6afb0a04653eac176555", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z060_t001_NucleiLabels.tiff": "7cc3c6c6a22f558e62194277090bdea2deb79475dd91fe7d0e49eac3c0187388", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z003_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z038_t001_CellsLabels.tiff": "6ac8fd9c95acc1f261e96a8fb9332e5d6a403548842f9b85ecb977e6213b4c9b", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z009_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z002_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z042_t001_CellsLabels.tiff": "88d2e9964455459499656c4f0a762cd86376c678756635aaa6b6c8d98be01852", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z054_t001_NucleiLabels.tiff": "b841c5d901c65a6138d9ff2b0ec4aa04f00765b1dd21bcd6fc9f8dc974c6035d", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z018_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z024_t001_NucleiLabels.tiff": "9e96a05ed9860cac9c57c618db2f4fe5e4b769a9d235aac6fd1d628486317df0", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z052_t001_CellsLabels.tiff": "7bb98f0587062e01b965e486fc687872651d7d90d271a2fdf0d35d67b464b233", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z041_t001_CellsLabels.tiff": "be7762756eb65f08cb337904165c6f43b5351f39405461f65ad28669abfe3899", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z048_t001_CellsLabels.tiff": "133a58f6fac8b07f79bfed9634867ccb5428b83e63682031a1def16c6a694568", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z043_t001_CellsLabels.tiff": "bd0df007524ea4e555e4d2fdbebe6a274caa03bc360a1cf90d601659155984b2", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z012_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z004_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z038_t001_NucleiLabels.tiff": "e2852ea83c62ab30d77681b4e39f88d3cee74ae052f1c948675ae3581c9fc28a", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z021_t001_CellsLabels.tiff": "daa31cb1e537e5c32b1936ab7647c5690741d2bc1d0e15c60b59ba1310a8c0ce", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z019_t001_NucleiLabels.tiff": "250cb56a8054c28cea894bc3d9edaddcdce01f5151a05da78da20157c3c3be9f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z026_t001_CellsLabels.tiff": "5b433d931fa6637fc793b23880199c01000ef6d9e610903bb38bb8cea5574563", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z029_t001_CellsLabels.tiff": "b3bdde5f751b8f5d41ca8b5219a3427e450cb1b8795401d0ec638940d1ad7bd9", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z001_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z030_t001_CellsLabels.tiff": "b71a2e05d270ae0cd237d6c0d97ea82a63e5ad3633723e7255c35d34e585ec80", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/openhcs_metadata.json.lock": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z034_t001_NucleiLabels.tiff": "08af61857cfb97fd95fcb08d1ce18139ce7087b192044c63452bee533c5b1aa4", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z012_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z005_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z022_t001_NucleiLabels.tiff": "50aef20e28a2f9e18ca2e28eabf6d87ec2ad20c79898e56f8d63a959ba5bb507", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z035_t001_CellsLabels.tiff": "eedeb489b46abf1eafbd9c81491cb3f85ce1cb3ce877844ad22e72547fc3c438", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z032_t001_NucleiLabels.tiff": "97b09fe5444ad439c3c2007d75971dce2766d94c93268eccfee9ae49b8a04aa1", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z011_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z002_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z003_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z035_t001_NucleiLabels.tiff": "13e69afc324152bb91329fa758d131d849127e8fd346078d94c6adc25582b3a6", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z028_t001_CellsLabels.tiff": "fef31040e0c54501c3bf965eb98f055cdb1d42c96f91cf1c239033d0a9a06534", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z020_t001_NucleiLabels.tiff": "b0cd031b448d8bd2075e65710bd76c064161b25242f85ee150da7cd139fcc1a8", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z014_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z025_t001_NucleiLabels.tiff": "9cc33676a0177130609ab1331feab1722bbde8d55b2f49031730fd5a2a40029c", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z037_t001_CellsLabels.tiff": "aa1f449f1769b27cd9f1076e70f49f0242e2549f288a77984ca159cb07cf6a40", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z008_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z036_t001_NucleiLabels.tiff": "4fa967c78709aa400246a50528a36b24ca366e825dd0e64561643369e814906b", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z039_t001_CellsLabels.tiff": "22fa79a76e64013f10373be2ca2af98a2173275c5d43c9cad33135acdc376d14", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z020_t001_CellsLabels.tiff": "e8105a4f04198ac75de9831247030084d6f65bf98691d7444ead7e178072fa22", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z015_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z046_t001_CellsLabels.tiff": "dc081df050be6cd19b5d5bbfe21c96c350eecf9bc6f10f608dc361a36056632a", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z022_t001_CellsLabels.tiff": "7d7ec170ed3652efa99e43d8db546c51b696821dca7727d0ff4d2f2ead7fdbaa", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z026_t001_NucleiLabels.tiff": "ad22809ff3f43d15c838f697061c263070dbf1da1031da39900681cdfbf9d54c", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z001_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z036_t001_CellsLabels.tiff": "28b68d1300cf7b08e21dbcd4858447862520dded5c27f2d84cf223513a119eed", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z050_t001_NucleiLabels.tiff": "565b4f08f7503086c1f2f8e631d19063129e02a5767d2f979c0cec8fdec23601", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z034_t001_CellsLabels.tiff": "3f6c1d8cdbce20aac8f70e7315bea70b147b7b6a034432845b27eeb6cfba083c", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z037_t001_NucleiLabels.tiff": "927d52441c57cb3a4014effaca3e3c23c76275fb264c26e1942a9c58b85ee6d2", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z040_t001_NucleiLabels.tiff": "e0bd66b166e9f92120dbee183973849eda0afbbe6fed9d03fc4cfd2c56a96280", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z050_t001_CellsLabels.tiff": "ea7654a6fe1f92ad274a54935420923eec4bec5ad030187afc0ac7af02690af5", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z053_t001_NucleiLabels.tiff": "b8613192175a4615b58aed607474657668429cb6ac635cd012569b19667f2d3d", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z015_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z058_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z044_t001_CellsLabels.tiff": "e03946d94c789ee787cc29a3c8f5e5d9c56b37a0044b297ba8f403abc98eb55e", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z042_t001_NucleiLabels.tiff": "e07252ca1a55c5195c99666d8d3f0d70db636b4ef4b99e1a0049ca22b182d87a", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z030_t001_NucleiLabels.tiff": "c54c8b98a5c38a47e0cf9391a1afab09b4c8c58e06294cdb650a15add4a32b58", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z011_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z059_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_erodedResizedNuclei.csv": "cca23f86a85bb3ee7a5c3a1d174c34b5f026ca7f399be98458e1a8c6e12d5c77", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z017_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z023_t001_CellsLabels.tiff": "2f06989618050934c73473f9342c2fe3410fd834a0ddb976cf602fe6198db7a6", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z040_t001_CellsLabels.tiff": "1635ea20dd80ce99f0e86494666825a52a66645837d598fd3a3fa91f0bb77d53", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z027_t001_NucleiLabels.tiff": "8c8f402d3a7c65c04e7f1575dd34de0427eeeb433ff2956d20e07d06eb48fb8d", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z021_t001_NucleiLabels.tiff": "71a10b0539f3fc4cba600b275b7e948258c6a435ba33ef522faa0869ac085518", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z048_t001_NucleiLabels.tiff": "325478230529e94ba372f38f0bf512292f131d337b6ac09ce5a91b0266724e02", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z057_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z013_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z059_t001_NucleiLabels.tiff": "76c785cf0f481c73ad3cf34ccb576eaa19dd1bdd9c74f9eaf92adfbde5d44b97", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z018_t001_NucleiLabels.tiff": "03ba7a4d6ae53e6c84aab5926ae1e2592b5f8ca24a839a256c82f08544df4270", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z057_t001_NucleiLabels.tiff": "695517641e9fe4b8421d4a7db56b24c755b6957c98ce32bb24ad5b25f52661eb", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/openhcs_metadata.json": "9ff815827f4a4a5cec301260e570ffc7f1b56a9f6422e08309c08e3ed34e01ac", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z019_t001_CellsLabels.tiff": "ec6ef1d319f2732c47b06f5cf02775c8cd94540f2f30a6c002f0aace1d317955", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_erodedDownsizedNuclei.csv": "ee507fb9c2387f86fc768184baf2596ebe1f8e40f7fc5eb24f252345fee63ae6", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z043_t001_NucleiLabels.tiff": "81a705d05b0a5bfa8783df28990c1d70634c1f4aa2bca23e02cfa8a4bc861b86", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_Image.csv": "75ac32ff5ef57bc285ec4eae1c9b501c27a35b486bf35435af5d499d856d34a4", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z052_t001_NucleiLabels.tiff": "0daf51526658e23b7c9ab8872e992ebe9322399286f95741d0ccdbd357e5d631", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z033_t001_NucleiLabels.tiff": "512a3a0ed8025d14b0556a72ebfc13b173d14f7046817ca09876242ab63b9d6b", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z029_t001_NucleiLabels.tiff": "34bce0f06f674e20667554eed0075fde8d9d33c9c0eaeffd88a1c558499cc432", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_Cells.csv": "6f6e7431171ed5ea73a01784105147eca0e43a6e3e41f8720df93106353c9c79", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z008_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z007_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z009_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z006_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f" + }, + "cohort": "plane-facts-whole-driver-candidate-v1" + } + ], + "source_head": "9eee98265c939859d49f0c60bd5b3a2168cb29fe", + "scope": "Fresh whole-driver baseline and candidate versus both retained qualified native repetitions; original comparator and all strict policies; no runtime/native rerun or timing promotion.", + "source_revision": "9eee98265c939859d49f0c60bd5b3a2168cb29fe", + "source_diff_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "native_witness": { + "outputs": { + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_erodedResizedNuclei.csv": "66a58f78cab0cfdbe907468e8ba383fc59fab49025b84b589855bdfa04854609", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_xy1_ch2_NucleiLabels.tiff": "340bbd795541e4031749d1fff767fa4fcdcac10f5b97879056b25dac9fd7674b", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_Experiment.csv": "c05f0a11f43a5bda943e9f739ff257216f8ab3f65d8988a0d3d0c612c1db6997", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_Cells.csv": "cc34681db8a66c38ca77bc47db496b26703f21e4d7c7628dd74e30c33250859f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_downsizedNuclei.csv": "2c66bc7b17de52147d5a875403f9a9746019ac61371c08157979044c6ed818dd", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_Nuclei.csv": "440c33edc7e8cb5611910a835454db62390301a7af617a542349f60aece57561", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_xy1_ch0_CellsLabels.tiff": "aaed9ed23179a162ee7f5a7e8d1d88f1fa342ca55172b7fccd4cbf67f7841d81", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_Image.csv": "1f3dec653b6909c8a81ac109ce1faf91380886ac9a18e56fafc91102d419904e", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_erodedDownsizedNuclei.csv": "527a0cce497d2a618478be84d1a0bf174049f58aa2a666e37626a990fdca0116", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_erodedResizedNuclei.csv": "66a58f78cab0cfdbe907468e8ba383fc59fab49025b84b589855bdfa04854609", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_xy1_ch2_NucleiLabels.tiff": "340bbd795541e4031749d1fff767fa4fcdcac10f5b97879056b25dac9fd7674b", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_Experiment.csv": "65f1f124e99f13ddc73b71d01732262cd353f24f0bd4697a776b274a39eaecc5", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_Cells.csv": "cc34681db8a66c38ca77bc47db496b26703f21e4d7c7628dd74e30c33250859f", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_downsizedNuclei.csv": "2c66bc7b17de52147d5a875403f9a9746019ac61371c08157979044c6ed818dd", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_Nuclei.csv": "440c33edc7e8cb5611910a835454db62390301a7af617a542349f60aece57561", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_xy1_ch0_CellsLabels.tiff": "aaed9ed23179a162ee7f5a7e8d1d88f1fa342ca55172b7fccd4cbf67f7841d81", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_Image.csv": "4cde74a38ef73b892dd9012708a3ac7d71fab206a7903f525b39e84493dcfe17", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_erodedDownsizedNuclei.csv": "527a0cce497d2a618478be84d1a0bf174049f58aa2a666e37626a990fdca0116" + }, + "inputs": { + "/home/ts/.cache/openhcs/benchmark_datasets/CellProfiler_tutorials/data/3d_monolayer/3d_monolayer_final.cppipe": "9fcd24a1be8372b5c2e87637a055032c3362b9ad8729e361095f93d8bbe71838", + "/home/ts/.cache/openhcs/benchmark_datasets/CellProfiler_tutorials/data/3d_monolayer/images/3d_monolayer_xy1_ch0.tif": "63114f614fec95664f718a39cddac58449f0edb5cc31b57584e4ba5b1f4994c7", + "/home/ts/.cache/openhcs/benchmark_datasets/CellProfiler_tutorials/data/3d_monolayer/images/3d_monolayer_xy1_ch1.tif": "a7d9abf632850875c22613c4b740fc01153cf60e9773c16d4a670a97b46e5751", + "/home/ts/.cache/openhcs/benchmark_datasets/CellProfiler_tutorials/data/3d_monolayer/images/3d_monolayer_xy1_ch2.tif": "191754bc7f43856223180483e2737ea93bab4d0cf98b1b2a4c27425e9908147d", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleSpeckles/ExampleSpeckles.cppipe": "6f1f0a6c2a26c99ade3a258b8dfe1cbbd741757400f632960f66bded1a7fb11c", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleSpeckles/images/1-162hrh2ax2.tif": "24cc92ed76482639161711b1ae7fc57779d7cdb723be887b4d6a887b04dda680", + "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleSpeckles/images/1-162hrhoe2.tif": "ea1874927a1448b6973d1689a694d29abaad62062fae83a8a58f8d790ad6d7c1" + }, + "receipts": [ + { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/source-freeze.json", + "sha256": "72e1b4f14c8c7bf03517c8d99329a1a49182fefa4b128d87326abd5b3ed829b8", + "bytes": 1431168 + }, + { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/strict-science.json", + "sha256": "77ab1a1e88d52502c633b646efe0d032ad87f117fc403298cb8b5742a0510190", + "bytes": 184689 + }, + { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/paired-qualification.json", + "sha256": "3db0f60d3ef04aa0ce846eaef70e746031442422cc693ba943bf78ac8db1ab32", + "bytes": 100305 + } + ], + "native_report": { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/native_report.json", + "sha256": "f1f5804eaef6af43b73f0608071ff57c62691bbd9172b33fa45ca633a4593654", + "bytes": 3230 + }, + "native_request": { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-v2/native/cp_tutorial_3d_monolayer/native_request.json", + "sha256": "dfcc3c97846c1714a81ae1bb904683e03319c37f3f996d8789575f706497c484", + "bytes": 837 + } + }, + "driver_witnesses": [ + { + "cohort": "plane-facts-whole-driver-baseline-v1", + "outputs": { + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/openhcs_metadata.json": "b683f94e72c5c8a1f41b7680379e158dfc1caaea6659391cf24e0ee8e470e008", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/openhcs_metadata.json.lock": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_erodedResizedNuclei.csv": "cca23f86a85bb3ee7a5c3a1d174c34b5f026ca7f399be98458e1a8c6e12d5c77", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_Cells.csv": "6f6e7431171ed5ea73a01784105147eca0e43a6e3e41f8720df93106353c9c79", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_downsizedNuclei.csv": "a7b998c5a524e11747fc5452fd763fc3d9abe16f46e961f62e09e9393b6a8adf", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_Nuclei.csv": "17500f5d4321bbf255df31f7a3503339029c2b077045a6c10e4d4f6f0db9ad1e", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_Image.csv": "75ac32ff5ef57bc285ec4eae1c9b501c27a35b486bf35435af5d499d856d34a4", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_erodedDownsizedNuclei.csv": "ee507fb9c2387f86fc768184baf2596ebe1f8e40f7fc5eb24f252345fee63ae6", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z052_t001_NucleiLabels.tiff": "0daf51526658e23b7c9ab8872e992ebe9322399286f95741d0ccdbd357e5d631", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z008_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z060_t001_NucleiLabels.tiff": "7cc3c6c6a22f558e62194277090bdea2deb79475dd91fe7d0e49eac3c0187388", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z015_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z057_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z021_t001_CellsLabels.tiff": "daa31cb1e537e5c32b1936ab7647c5690741d2bc1d0e15c60b59ba1310a8c0ce", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z047_t001_CellsLabels.tiff": "b926aa64e1fa8aa52279a69d91dc23cf14bf0ae76107f9b360c7ae77bdf53719", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z022_t001_NucleiLabels.tiff": "50aef20e28a2f9e18ca2e28eabf6d87ec2ad20c79898e56f8d63a959ba5bb507", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z027_t001_CellsLabels.tiff": "ee9056587a19e581f5feb6078b41fa1004d923a5ddf1725aefc4b8b8c5201ad6", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z029_t001_NucleiLabels.tiff": "34bce0f06f674e20667554eed0075fde8d9d33c9c0eaeffd88a1c558499cc432", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z007_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z018_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z047_t001_NucleiLabels.tiff": "995d5ce77cbf61a9be8590f34987ef3f1dd698ae6ed393cac4d0960bc9e7a66a", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z046_t001_CellsLabels.tiff": "dc081df050be6cd19b5d5bbfe21c96c350eecf9bc6f10f608dc361a36056632a", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z042_t001_CellsLabels.tiff": "88d2e9964455459499656c4f0a762cd86376c678756635aaa6b6c8d98be01852", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z024_t001_NucleiLabels.tiff": "9e96a05ed9860cac9c57c618db2f4fe5e4b769a9d235aac6fd1d628486317df0", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z035_t001_NucleiLabels.tiff": "13e69afc324152bb91329fa758d131d849127e8fd346078d94c6adc25582b3a6", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z055_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z050_t001_CellsLabels.tiff": "ea7654a6fe1f92ad274a54935420923eec4bec5ad030187afc0ac7af02690af5", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z014_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z031_t001_NucleiLabels.tiff": "247490c261fecc26c7379ab2eebaa01e4bea523218b3cc5491d2fe2b9aadfe11", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z020_t001_NucleiLabels.tiff": "b0cd031b448d8bd2075e65710bd76c064161b25242f85ee150da7cd139fcc1a8", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z051_t001_CellsLabels.tiff": "73483bd10a0f37990084013ad054768086d6d3c929da6bc7e74f4aee37e9dbf2", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z028_t001_CellsLabels.tiff": "fef31040e0c54501c3bf965eb98f055cdb1d42c96f91cf1c239033d0a9a06534", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z022_t001_CellsLabels.tiff": "7d7ec170ed3652efa99e43d8db546c51b696821dca7727d0ff4d2f2ead7fdbaa", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z033_t001_CellsLabels.tiff": "a320b99ee129d97b99f9fac66aabb89fc1652930b3bb8b2b3fbba7b57e994f85", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z038_t001_CellsLabels.tiff": "6ac8fd9c95acc1f261e96a8fb9332e5d6a403548842f9b85ecb977e6213b4c9b", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z051_t001_NucleiLabels.tiff": "f0f36ccaf744d42da036948682cfd226e6dd4a3cfb94aed38729b2d25ede5dff", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z023_t001_CellsLabels.tiff": "2f06989618050934c73473f9342c2fe3410fd834a0ddb976cf602fe6198db7a6", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z010_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z004_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z046_t001_NucleiLabels.tiff": "c1250e060d32f2cfc07fc7e1594fdb1d6cbb4d0483fa94a8113efa695397963c", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z003_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z026_t001_NucleiLabels.tiff": "ad22809ff3f43d15c838f697061c263070dbf1da1031da39900681cdfbf9d54c", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z054_t001_CellsLabels.tiff": "25ff2ec586a568e42286f73713d64aab1208c38c016099299211f04368ac49c8", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z039_t001_CellsLabels.tiff": "22fa79a76e64013f10373be2ca2af98a2173275c5d43c9cad33135acdc376d14", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z057_t001_NucleiLabels.tiff": "695517641e9fe4b8421d4a7db56b24c755b6957c98ce32bb24ad5b25f52661eb", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z020_t001_CellsLabels.tiff": "e8105a4f04198ac75de9831247030084d6f65bf98691d7444ead7e178072fa22", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z008_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z037_t001_NucleiLabels.tiff": "927d52441c57cb3a4014effaca3e3c23c76275fb264c26e1942a9c58b85ee6d2", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z011_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z043_t001_NucleiLabels.tiff": "81a705d05b0a5bfa8783df28990c1d70634c1f4aa2bca23e02cfa8a4bc861b86", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z015_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z041_t001_NucleiLabels.tiff": "744eab7cd170d5af8d2f72ced3893dd939c4e1b0f415afab799a37ded5d046c7", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z060_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z055_t001_NucleiLabels.tiff": "44e02416cc94a70528165a4b6c42b9e19b205f9c91528076796272cb1a8504ea", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z033_t001_NucleiLabels.tiff": "512a3a0ed8025d14b0556a72ebfc13b173d14f7046817ca09876242ab63b9d6b", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z050_t001_NucleiLabels.tiff": "565b4f08f7503086c1f2f8e631d19063129e02a5767d2f979c0cec8fdec23601", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z009_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z036_t001_CellsLabels.tiff": "28b68d1300cf7b08e21dbcd4858447862520dded5c27f2d84cf223513a119eed", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z049_t001_NucleiLabels.tiff": "e92e493078672a0e0d90e31409612e215bf8486300d5c765d0e0504ff8888e7a", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z040_t001_NucleiLabels.tiff": "e0bd66b166e9f92120dbee183973849eda0afbbe6fed9d03fc4cfd2c56a96280", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z016_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z012_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z001_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z053_t001_NucleiLabels.tiff": "b8613192175a4615b58aed607474657668429cb6ac635cd012569b19667f2d3d", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z017_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z006_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z032_t001_CellsLabels.tiff": "8a2c5774044326ae722e41aae517266197fbebb0e4dd5eab4b22414fbfea309f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z032_t001_NucleiLabels.tiff": "97b09fe5444ad439c3c2007d75971dce2766d94c93268eccfee9ae49b8a04aa1", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z058_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z048_t001_NucleiLabels.tiff": "325478230529e94ba372f38f0bf512292f131d337b6ac09ce5a91b0266724e02", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z006_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z038_t001_NucleiLabels.tiff": "e2852ea83c62ab30d77681b4e39f88d3cee74ae052f1c948675ae3581c9fc28a", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z052_t001_CellsLabels.tiff": "7bb98f0587062e01b965e486fc687872651d7d90d271a2fdf0d35d67b464b233", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z041_t001_CellsLabels.tiff": "be7762756eb65f08cb337904165c6f43b5351f39405461f65ad28669abfe3899", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z005_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z011_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z044_t001_NucleiLabels.tiff": "a78264f2f5e245a3913a69f89105e94df30048b1aaef4eb80fc5c3235e78b196", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z029_t001_CellsLabels.tiff": "b3bdde5f751b8f5d41ca8b5219a3427e450cb1b8795401d0ec638940d1ad7bd9", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z059_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z056_t001_NucleiLabels.tiff": "ca0723c8b9764d5b77034719bb75a2940be35da2e04119af4c0e6d3552c08718", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z019_t001_CellsLabels.tiff": "ec6ef1d319f2732c47b06f5cf02775c8cd94540f2f30a6c002f0aace1d317955", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z002_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z036_t001_NucleiLabels.tiff": "4fa967c78709aa400246a50528a36b24ca366e825dd0e64561643369e814906b", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z039_t001_NucleiLabels.tiff": "f93e8293a37886a36844b8748083a1f8e717efab092652755263e797f12f0364", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z044_t001_CellsLabels.tiff": "e03946d94c789ee787cc29a3c8f5e5d9c56b37a0044b297ba8f403abc98eb55e", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z025_t001_CellsLabels.tiff": "03aabe5528198cfd6ec23a058dab540fb8be0870207c6dc94a62d9b621d1f2f9", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z045_t001_NucleiLabels.tiff": "94bd6a65795ad6395cc18a4fd63dc591354bef612ee49fffd2d9f63e4ed9db84", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z026_t001_CellsLabels.tiff": "5b433d931fa6637fc793b23880199c01000ef6d9e610903bb38bb8cea5574563", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z016_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z027_t001_NucleiLabels.tiff": "8c8f402d3a7c65c04e7f1575dd34de0427eeeb433ff2956d20e07d06eb48fb8d", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z018_t001_NucleiLabels.tiff": "03ba7a4d6ae53e6c84aab5926ae1e2592b5f8ca24a839a256c82f08544df4270", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z010_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z040_t001_CellsLabels.tiff": "1635ea20dd80ce99f0e86494666825a52a66645837d598fd3a3fa91f0bb77d53", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z013_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z003_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z024_t001_CellsLabels.tiff": "39899adabca74b10b9950d1f97e8b0b3bbaf136d75e7a46ef4a6e86896e3119f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z014_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z007_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z002_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z056_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z042_t001_NucleiLabels.tiff": "e07252ca1a55c5195c99666d8d3f0d70db636b4ef4b99e1a0049ca22b182d87a", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z017_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z025_t001_NucleiLabels.tiff": "9cc33676a0177130609ab1331feab1722bbde8d55b2f49031730fd5a2a40029c", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z031_t001_CellsLabels.tiff": "1b14aab71bf3623458ea9373852b0baf28be9727877b32f894407927ecfb6d65", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z035_t001_CellsLabels.tiff": "eedeb489b46abf1eafbd9c81491cb3f85ce1cb3ce877844ad22e72547fc3c438", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z021_t001_NucleiLabels.tiff": "71a10b0539f3fc4cba600b275b7e948258c6a435ba33ef522faa0869ac085518", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z019_t001_NucleiLabels.tiff": "250cb56a8054c28cea894bc3d9edaddcdce01f5151a05da78da20157c3c3be9f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z043_t001_CellsLabels.tiff": "bd0df007524ea4e555e4d2fdbebe6a274caa03bc360a1cf90d601659155984b2", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z001_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z059_t001_NucleiLabels.tiff": "76c785cf0f481c73ad3cf34ccb576eaa19dd1bdd9c74f9eaf92adfbde5d44b97", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z048_t001_CellsLabels.tiff": "133a58f6fac8b07f79bfed9634867ccb5428b83e63682031a1def16c6a694568", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z005_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z058_t001_NucleiLabels.tiff": "a37c63c6052e40d73db918a014e6f2a18be4d0cbfbeeef65a9c26096a38a4c09", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z013_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z037_t001_CellsLabels.tiff": "aa1f449f1769b27cd9f1076e70f49f0242e2549f288a77984ca159cb07cf6a40", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z004_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z030_t001_CellsLabels.tiff": "b71a2e05d270ae0cd237d6c0d97ea82a63e5ad3633723e7255c35d34e585ec80", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z054_t001_NucleiLabels.tiff": "b841c5d901c65a6138d9ff2b0ec4aa04f00765b1dd21bcd6fc9f8dc974c6035d", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z023_t001_NucleiLabels.tiff": "ecb7f78c8ca0bad2b9d0495659609b9023781b6a936d6afb0a04653eac176555", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z053_t001_CellsLabels.tiff": "ff17f538baaf179752412026a250bdbee016e3101037bf4e0dbb81199fc4c8ed", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z034_t001_CellsLabels.tiff": "3f6c1d8cdbce20aac8f70e7315bea70b147b7b6a034432845b27eeb6cfba083c", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z045_t001_CellsLabels.tiff": "c678f2100c7431e5a43c391af86a79e3436d4cf13f050f98c25f62682e4632fe", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z034_t001_NucleiLabels.tiff": "08af61857cfb97fd95fcb08d1ce18139ce7087b192044c63452bee533c5b1aa4", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z009_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z028_t001_NucleiLabels.tiff": "1676f21bd3b7e105601c83f206890507c32ba2447988be2a75d0e8153d069b65", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z012_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z030_t001_NucleiLabels.tiff": "c54c8b98a5c38a47e0cf9391a1afab09b4c8c58e06294cdb650a15add4a32b58", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z049_t001_CellsLabels.tiff": "28d83dfbf5bbf7e4da9c3e4806353e9fd0a89680dcdab4b43ca2fd19b80f669c" + }, + "observations": { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/observations.json", + "sha256": "a9558f52b6d74972c4244b9af73e03cfe66ecada108d1d2131db5af56c868260", + "bytes": 2518 + }, + "source_freeze": { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/source-freeze.json", + "sha256": "55ee41e2e0f09553de9c2be7dab5beedf0f4f2baf7245f2778c2dcedc38a092e", + "bytes": 1161129 + }, + "measured_receipt": { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/ordinary_run_evidence/cp_tutorial_3d_monolayer-1w_1t-1791017734852071625/measured_pipeline_receipt.json", + "sha256": "da9430eb33c8e5724edc9c28431ff22848bef9a0eda94da59139b20a3349058e", + "bytes": 23182 + } + }, + { + "cohort": "plane-facts-whole-driver-candidate-v1", + "outputs": { + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/openhcs_metadata.json": "9ff815827f4a4a5cec301260e570ffc7f1b56a9f6422e08309c08e3ed34e01ac", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/openhcs_metadata.json.lock": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_erodedResizedNuclei.csv": "cca23f86a85bb3ee7a5c3a1d174c34b5f026ca7f399be98458e1a8c6e12d5c77", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_Cells.csv": "6f6e7431171ed5ea73a01784105147eca0e43a6e3e41f8720df93106353c9c79", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_downsizedNuclei.csv": "a7b998c5a524e11747fc5452fd763fc3d9abe16f46e961f62e09e9393b6a8adf", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_Nuclei.csv": "17500f5d4321bbf255df31f7a3503339029c2b077045a6c10e4d4f6f0db9ad1e", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_Image.csv": "75ac32ff5ef57bc285ec4eae1c9b501c27a35b486bf35435af5d499d856d34a4", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_erodedDownsizedNuclei.csv": "ee507fb9c2387f86fc768184baf2596ebe1f8e40f7fc5eb24f252345fee63ae6", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z052_t001_NucleiLabels.tiff": "0daf51526658e23b7c9ab8872e992ebe9322399286f95741d0ccdbd357e5d631", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z008_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z060_t001_NucleiLabels.tiff": "7cc3c6c6a22f558e62194277090bdea2deb79475dd91fe7d0e49eac3c0187388", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z015_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z057_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z021_t001_CellsLabels.tiff": "daa31cb1e537e5c32b1936ab7647c5690741d2bc1d0e15c60b59ba1310a8c0ce", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z047_t001_CellsLabels.tiff": "b926aa64e1fa8aa52279a69d91dc23cf14bf0ae76107f9b360c7ae77bdf53719", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z022_t001_NucleiLabels.tiff": "50aef20e28a2f9e18ca2e28eabf6d87ec2ad20c79898e56f8d63a959ba5bb507", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z027_t001_CellsLabels.tiff": "ee9056587a19e581f5feb6078b41fa1004d923a5ddf1725aefc4b8b8c5201ad6", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z029_t001_NucleiLabels.tiff": "34bce0f06f674e20667554eed0075fde8d9d33c9c0eaeffd88a1c558499cc432", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z007_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z018_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z047_t001_NucleiLabels.tiff": "995d5ce77cbf61a9be8590f34987ef3f1dd698ae6ed393cac4d0960bc9e7a66a", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z046_t001_CellsLabels.tiff": "dc081df050be6cd19b5d5bbfe21c96c350eecf9bc6f10f608dc361a36056632a", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z042_t001_CellsLabels.tiff": "88d2e9964455459499656c4f0a762cd86376c678756635aaa6b6c8d98be01852", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z024_t001_NucleiLabels.tiff": "9e96a05ed9860cac9c57c618db2f4fe5e4b769a9d235aac6fd1d628486317df0", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z035_t001_NucleiLabels.tiff": "13e69afc324152bb91329fa758d131d849127e8fd346078d94c6adc25582b3a6", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z055_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z050_t001_CellsLabels.tiff": "ea7654a6fe1f92ad274a54935420923eec4bec5ad030187afc0ac7af02690af5", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z014_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z031_t001_NucleiLabels.tiff": "247490c261fecc26c7379ab2eebaa01e4bea523218b3cc5491d2fe2b9aadfe11", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z020_t001_NucleiLabels.tiff": "b0cd031b448d8bd2075e65710bd76c064161b25242f85ee150da7cd139fcc1a8", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z051_t001_CellsLabels.tiff": "73483bd10a0f37990084013ad054768086d6d3c929da6bc7e74f4aee37e9dbf2", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z028_t001_CellsLabels.tiff": "fef31040e0c54501c3bf965eb98f055cdb1d42c96f91cf1c239033d0a9a06534", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z022_t001_CellsLabels.tiff": "7d7ec170ed3652efa99e43d8db546c51b696821dca7727d0ff4d2f2ead7fdbaa", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z033_t001_CellsLabels.tiff": "a320b99ee129d97b99f9fac66aabb89fc1652930b3bb8b2b3fbba7b57e994f85", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z038_t001_CellsLabels.tiff": "6ac8fd9c95acc1f261e96a8fb9332e5d6a403548842f9b85ecb977e6213b4c9b", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z051_t001_NucleiLabels.tiff": "f0f36ccaf744d42da036948682cfd226e6dd4a3cfb94aed38729b2d25ede5dff", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z023_t001_CellsLabels.tiff": "2f06989618050934c73473f9342c2fe3410fd834a0ddb976cf602fe6198db7a6", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z010_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z004_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z046_t001_NucleiLabels.tiff": "c1250e060d32f2cfc07fc7e1594fdb1d6cbb4d0483fa94a8113efa695397963c", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z003_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z026_t001_NucleiLabels.tiff": "ad22809ff3f43d15c838f697061c263070dbf1da1031da39900681cdfbf9d54c", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z054_t001_CellsLabels.tiff": "25ff2ec586a568e42286f73713d64aab1208c38c016099299211f04368ac49c8", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z039_t001_CellsLabels.tiff": "22fa79a76e64013f10373be2ca2af98a2173275c5d43c9cad33135acdc376d14", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z057_t001_NucleiLabels.tiff": "695517641e9fe4b8421d4a7db56b24c755b6957c98ce32bb24ad5b25f52661eb", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z020_t001_CellsLabels.tiff": "e8105a4f04198ac75de9831247030084d6f65bf98691d7444ead7e178072fa22", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z008_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z037_t001_NucleiLabels.tiff": "927d52441c57cb3a4014effaca3e3c23c76275fb264c26e1942a9c58b85ee6d2", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z011_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z043_t001_NucleiLabels.tiff": "81a705d05b0a5bfa8783df28990c1d70634c1f4aa2bca23e02cfa8a4bc861b86", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z015_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z041_t001_NucleiLabels.tiff": "744eab7cd170d5af8d2f72ced3893dd939c4e1b0f415afab799a37ded5d046c7", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z060_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z055_t001_NucleiLabels.tiff": "44e02416cc94a70528165a4b6c42b9e19b205f9c91528076796272cb1a8504ea", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z033_t001_NucleiLabels.tiff": "512a3a0ed8025d14b0556a72ebfc13b173d14f7046817ca09876242ab63b9d6b", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z050_t001_NucleiLabels.tiff": "565b4f08f7503086c1f2f8e631d19063129e02a5767d2f979c0cec8fdec23601", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z009_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z036_t001_CellsLabels.tiff": "28b68d1300cf7b08e21dbcd4858447862520dded5c27f2d84cf223513a119eed", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z049_t001_NucleiLabels.tiff": "e92e493078672a0e0d90e31409612e215bf8486300d5c765d0e0504ff8888e7a", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z040_t001_NucleiLabels.tiff": "e0bd66b166e9f92120dbee183973849eda0afbbe6fed9d03fc4cfd2c56a96280", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z016_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z012_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z001_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z053_t001_NucleiLabels.tiff": "b8613192175a4615b58aed607474657668429cb6ac635cd012569b19667f2d3d", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z017_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z006_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z032_t001_CellsLabels.tiff": "8a2c5774044326ae722e41aae517266197fbebb0e4dd5eab4b22414fbfea309f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z032_t001_NucleiLabels.tiff": "97b09fe5444ad439c3c2007d75971dce2766d94c93268eccfee9ae49b8a04aa1", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z058_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z048_t001_NucleiLabels.tiff": "325478230529e94ba372f38f0bf512292f131d337b6ac09ce5a91b0266724e02", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z006_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z038_t001_NucleiLabels.tiff": "e2852ea83c62ab30d77681b4e39f88d3cee74ae052f1c948675ae3581c9fc28a", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z052_t001_CellsLabels.tiff": "7bb98f0587062e01b965e486fc687872651d7d90d271a2fdf0d35d67b464b233", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z041_t001_CellsLabels.tiff": "be7762756eb65f08cb337904165c6f43b5351f39405461f65ad28669abfe3899", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z005_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z011_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z044_t001_NucleiLabels.tiff": "a78264f2f5e245a3913a69f89105e94df30048b1aaef4eb80fc5c3235e78b196", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z029_t001_CellsLabels.tiff": "b3bdde5f751b8f5d41ca8b5219a3427e450cb1b8795401d0ec638940d1ad7bd9", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z059_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z056_t001_NucleiLabels.tiff": "ca0723c8b9764d5b77034719bb75a2940be35da2e04119af4c0e6d3552c08718", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z019_t001_CellsLabels.tiff": "ec6ef1d319f2732c47b06f5cf02775c8cd94540f2f30a6c002f0aace1d317955", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z002_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z036_t001_NucleiLabels.tiff": "4fa967c78709aa400246a50528a36b24ca366e825dd0e64561643369e814906b", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z039_t001_NucleiLabels.tiff": "f93e8293a37886a36844b8748083a1f8e717efab092652755263e797f12f0364", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z044_t001_CellsLabels.tiff": "e03946d94c789ee787cc29a3c8f5e5d9c56b37a0044b297ba8f403abc98eb55e", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z025_t001_CellsLabels.tiff": "03aabe5528198cfd6ec23a058dab540fb8be0870207c6dc94a62d9b621d1f2f9", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z045_t001_NucleiLabels.tiff": "94bd6a65795ad6395cc18a4fd63dc591354bef612ee49fffd2d9f63e4ed9db84", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z026_t001_CellsLabels.tiff": "5b433d931fa6637fc793b23880199c01000ef6d9e610903bb38bb8cea5574563", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z016_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z027_t001_NucleiLabels.tiff": "8c8f402d3a7c65c04e7f1575dd34de0427eeeb433ff2956d20e07d06eb48fb8d", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z018_t001_NucleiLabels.tiff": "03ba7a4d6ae53e6c84aab5926ae1e2592b5f8ca24a839a256c82f08544df4270", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z010_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z040_t001_CellsLabels.tiff": "1635ea20dd80ce99f0e86494666825a52a66645837d598fd3a3fa91f0bb77d53", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z013_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z003_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z024_t001_CellsLabels.tiff": "39899adabca74b10b9950d1f97e8b0b3bbaf136d75e7a46ef4a6e86896e3119f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z014_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z007_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z002_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z056_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z042_t001_NucleiLabels.tiff": "e07252ca1a55c5195c99666d8d3f0d70db636b4ef4b99e1a0049ca22b182d87a", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z017_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z025_t001_NucleiLabels.tiff": "9cc33676a0177130609ab1331feab1722bbde8d55b2f49031730fd5a2a40029c", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z031_t001_CellsLabels.tiff": "1b14aab71bf3623458ea9373852b0baf28be9727877b32f894407927ecfb6d65", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z035_t001_CellsLabels.tiff": "eedeb489b46abf1eafbd9c81491cb3f85ce1cb3ce877844ad22e72547fc3c438", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z021_t001_NucleiLabels.tiff": "71a10b0539f3fc4cba600b275b7e948258c6a435ba33ef522faa0869ac085518", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z019_t001_NucleiLabels.tiff": "250cb56a8054c28cea894bc3d9edaddcdce01f5151a05da78da20157c3c3be9f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z043_t001_CellsLabels.tiff": "bd0df007524ea4e555e4d2fdbebe6a274caa03bc360a1cf90d601659155984b2", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z001_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z059_t001_NucleiLabels.tiff": "76c785cf0f481c73ad3cf34ccb576eaa19dd1bdd9c74f9eaf92adfbde5d44b97", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z048_t001_CellsLabels.tiff": "133a58f6fac8b07f79bfed9634867ccb5428b83e63682031a1def16c6a694568", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z005_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z058_t001_NucleiLabels.tiff": "a37c63c6052e40d73db918a014e6f2a18be4d0cbfbeeef65a9c26096a38a4c09", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z013_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z037_t001_CellsLabels.tiff": "aa1f449f1769b27cd9f1076e70f49f0242e2549f288a77984ca159cb07cf6a40", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z004_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z030_t001_CellsLabels.tiff": "b71a2e05d270ae0cd237d6c0d97ea82a63e5ad3633723e7255c35d34e585ec80", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z054_t001_NucleiLabels.tiff": "b841c5d901c65a6138d9ff2b0ec4aa04f00765b1dd21bcd6fc9f8dc974c6035d", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z023_t001_NucleiLabels.tiff": "ecb7f78c8ca0bad2b9d0495659609b9023781b6a936d6afb0a04653eac176555", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z053_t001_CellsLabels.tiff": "ff17f538baaf179752412026a250bdbee016e3101037bf4e0dbb81199fc4c8ed", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z034_t001_CellsLabels.tiff": "3f6c1d8cdbce20aac8f70e7315bea70b147b7b6a034432845b27eeb6cfba083c", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z045_t001_CellsLabels.tiff": "c678f2100c7431e5a43c391af86a79e3436d4cf13f050f98c25f62682e4632fe", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z034_t001_NucleiLabels.tiff": "08af61857cfb97fd95fcb08d1ce18139ce7087b192044c63452bee533c5b1aa4", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z009_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z028_t001_NucleiLabels.tiff": "1676f21bd3b7e105601c83f206890507c32ba2447988be2a75d0e8153d069b65", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z012_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z030_t001_NucleiLabels.tiff": "c54c8b98a5c38a47e0cf9391a1afab09b4c8c58e06294cdb650a15add4a32b58", + "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z049_t001_CellsLabels.tiff": "28d83dfbf5bbf7e4da9c3e4806353e9fd0a89680dcdab4b43ca2fd19b80f669c" + }, + "observations": { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/observations.json", + "sha256": "6594071a580a83a5abd4cdb58c399899d046c84eaa52ed1c4f5dc0faab2bf88d", + "bytes": 2522 + }, + "source_freeze": { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/source-freeze.json", + "sha256": "20c7690c2346a7e33625823ca1456b1774ec76192708a61aaf9f4b1170dc5d16", + "bytes": 1161766 + }, + "measured_receipt": { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/ordinary_run_evidence/cp_tutorial_3d_monolayer-1w_1t-1791017860408836180/measured_pipeline_receipt.json", + "sha256": "49db7f2e2cfda0c0fd7dc3a4fa6a7542e70038fde080dbf73f5498f1a506ae34", + "bytes": 23189 + } + } + ], + "comparator_sha256": "b772d231196eaf23d9a150ffa491f95426cb6e074470ac3febd508afd1d3ad18", + "source_tracked_hashes_sha256": "185dea17cd6233c369ce1b9145978d19e9a0394d310d38b3a360c28faa7874a1" +} diff --git a/benchmark/results/perf_rejected_metadata_owner_20261003/submitted-input-controls.json b/benchmark/results/perf_rejected_metadata_owner_20261003/submitted-input-controls.json new file mode 100644 index 0000000000..da10d0840e --- /dev/null +++ b/benchmark/results/perf_rejected_metadata_owner_20261003/submitted-input-controls.json @@ -0,0 +1,22 @@ +{ + "normalized_submissions_equal": true, + "only_pipeline_difference": "Fresh output directory baseline-v1 versus candidate-v1; original global config bytes identical.", + "files": { + "baseline-submitted_pipeline.py": { + "sha256": "722a76ce2eecbfbe409df3599d4b44841862d33dc3a24b828e8ad2d1e381f47e", + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/ordinary_run_evidence/cp_tutorial_3d_monolayer-1w_1t-1791017734852071625/submitted_pipeline.py" + }, + "candidate-submitted_pipeline.py": { + "sha256": "1db8114e4c171af265fb58b76268c8576d00a974c0eea7b17bb5ba80ca5f4ad3", + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/ordinary_run_evidence/cp_tutorial_3d_monolayer-1w_1t-1791017860408836180/submitted_pipeline.py" + }, + "baseline-submitted_global_config.py": { + "sha256": "2e2cf95d87a9465d20ecbb8c88629ce615b64de6e5843319f9b51599f343c1c3", + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/ordinary_run_evidence/cp_tutorial_3d_monolayer-1w_1t-1791017734852071625/submitted_global_config.py" + }, + "candidate-submitted_global_config.py": { + "sha256": "2e2cf95d87a9465d20ecbb8c88629ce615b64de6e5843319f9b51599f343c1c3", + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-candidate-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/ordinary_run_evidence/cp_tutorial_3d_monolayer-1w_1t-1791017860408836180/submitted_global_config.py" + } + } +} From 2643742cf92f9d9aee4574aefc781c4531ede185 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 05:51:17 -0400 Subject: [PATCH 121/235] Record current compiler and runtime owner costs for #496 --- .../report.rst | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 benchmark/results/perf_compile_runtime_ledger_20261003/report.rst diff --git a/benchmark/results/perf_compile_runtime_ledger_20261003/report.rst b/benchmark/results/perf_compile_runtime_ledger_20261003/report.rst new file mode 100644 index 0000000000..307be67079 --- /dev/null +++ b/benchmark/results/perf_compile_runtime_ledger_20261003/report.rst @@ -0,0 +1,119 @@ +Current compiler and runtime owner ledger +========================================= + +This checkpoint supports PR #394 and issue #496. It measures unchanged +production source at ``480aed49b30448de4380af5c9274e48084e67b63``; it implements +no optimization. Main #526 was subsequently merged at ``5d62bd52c`` and changes +viewer documentation, knowledge projection and its test only. All measured +production Python bytes remain unchanged. + +The original public 3D Monolayer driver runs one worker and one CPU thread, +with inline execution, default OUTCOMES completion and the normal RSS observer. +The server is reused per sweep. Server startup, mandatory library/registry/kernel +preparation and shutdown remain outside pipeline clocks. Both profilers are +disabled. This diagnostic adds 17 compiler/configuration boundaries to the +retained 24 nominal boundaries; its wrappers introduce observation overhead. +It is not a before/after ordinary performance comparison. + +The public clocks are compilation **1.495826s**, execution **8.396727s**, and +total **10.694032s**. The execution root partitions **8.383970s** into +**3.660522s** at 211 declared callable boundaries and **4.723448s** outside them +(56.34 percent). Those callable boundaries include conversion, filtering and +metadata behavior; they are not pure numerical kernel clocks. + +Selected disjoint execution-exclusive costs: + +.. list-table:: Seconds within the original execution root + :header-rows: 1 + + * - Owner + - Calls + - Seconds + * - Stack loading + - 32 + - 0.781797 + * - In-memory output saving + - 28 + - 0.667759 + * - Worker residual + - 1 + - 0.501906 + * - Metadata publication + - 5 + - 0.502201 + * - Step finalization + - 30 + - 0.462263 + * - CellProfiler output recording + - 26 + - 0.382119 + * - CellProfiler image-request construction + - 30 + - 0.315012 + * - Artifact metadata reconciliation + - 1 + - 0.295025 + * - Validation and unstacking + - 28 + - 0.196794 + +Publication and finalization attribution differs from the previous diagnostic +on the same production bytes. This does not establish a code regression or an +optimization. GC and other stalls overlap owner clocks; they cannot be added +as independent costs. In-memory saving is not final TIFF export. + +The compile-only root is **1.260937s**. Its nested, disjoint ObjectState owners +total **0.671090s**: constructor-exclusive 0.161911s, resolved snapshots +0.452570s, reconstruction 0.056494s, and saved-object forwarding 0.000115s. +There are 44 constructors, 88 snapshots, 532 recursive reconstructions and +42 saved-object reads. All ten ``get_effective_config`` calls total only +**0.195838s inclusive**, a subset of those state costs. Context creation cannot +claim the entire 0.671090s as removable work. + +The two snapshots have distinct consumers. Live resolution observes current +parameters, live ancestor objects and live global context. Saved resolution +observes detached local parameters and saved ancestors/global context. Registry +callbacks receive their completed dirty/provenance baselines. Fresh compiler +registration also avoids stripped or independently edited UI states. These +facts reject unconditional snapshot collapse or existing-state reuse. The +source audit revalidates all 731 modules in the existing NRA census, covering +5,302 original classes; lexical source evidence is not a proof of callback +purity. Execution configuration state accounts for only about 0.062s. + +An original replay of the retained 180-reference input document independently +rules out another route: 68 complete projection-cache-hit availability checks +take **0.024413s median** across nine unprofiled repetitions. Current invalid +source indices are still rejected after projection caching. A cache-before- +validation shortcut would lose that error. This is a replay ceiling with an +already-loaded document handler, not a current end-to-end saving. + +Validation and custody +---------------------- + +All 15 descriptor, argument/result/error identity, call-once, nested clock, +fork-reset and source-drift controls pass. The 41 hooks install from verified +actual declaring files, including ObjectState's dependency source. Original +source, all eight actual dependencies, native binaries, environment and physical +inputs are checked before and after the run. + +The first root launch stops before starting a driver because its full +environment fingerprint differs from the prepared agent environment only in +``CODEX_THREAD_ID``. The original matching environment runs the unchanged +controller once and validates science; no fingerprint bypass or freeze rewrite +is used. Driver session 17712 and science session 51754 terminate successfully. + +The independent original scientific comparison, exact inventory and physical +source gates pass: six CSVs, 120 TIFFs, three input volumes and 180 ordered source +references. Retained native output witnesses are verified transitively; native +CellProfiler is not rerun here and no new native timing ratio is claimed. + +``assessment.json`` joins the full execution/compile ledgers, scientific gate, +control and registration receipts to immutable original paths and hashes. +The full source freeze and recipes remain in the referenced maintenance +directory; the committed evidence totals about 164 KB before this report. +Scientific outputs, saved replay graphs and environments are not copied. + +The multi-second runtime target remains unmet. Dominant integrated runtime +changes, matched ordinary before/after gains, branch architectural acceptance, +installed consumer acceptance, full-catalog/scaling measurements and refreshed +figure packs remain outstanding. The performance goal remains active. From 4c0d62e45c8afb85e34b69d7279cd00710f519b5 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 05:51:29 -0400 Subject: [PATCH 122/235] Attach determining compiler/runtime receipts to #496 --- .../agent-environment-comparison.json | 8 + .../assessment.json | 431 +++++++ .../availability-rejection.json | 33 + .../compile_only-region.json | 416 +++++++ .../controls.json | 24 + .../execution-region.json | 864 +++++++++++++ .../observations.json | 63 + .../registration-owner-analysis.json | 233 ++++ .../science.json | 1085 +++++++++++++++++ 9 files changed, 3157 insertions(+) create mode 100644 benchmark/results/perf_compile_runtime_ledger_20261003/agent-environment-comparison.json create mode 100644 benchmark/results/perf_compile_runtime_ledger_20261003/assessment.json create mode 100644 benchmark/results/perf_compile_runtime_ledger_20261003/availability-rejection.json create mode 100644 benchmark/results/perf_compile_runtime_ledger_20261003/compile_only-region.json create mode 100644 benchmark/results/perf_compile_runtime_ledger_20261003/controls.json create mode 100644 benchmark/results/perf_compile_runtime_ledger_20261003/execution-region.json create mode 100644 benchmark/results/perf_compile_runtime_ledger_20261003/observations.json create mode 100644 benchmark/results/perf_compile_runtime_ledger_20261003/registration-owner-analysis.json create mode 100644 benchmark/results/perf_compile_runtime_ledger_20261003/science.json diff --git a/benchmark/results/perf_compile_runtime_ledger_20261003/agent-environment-comparison.json b/benchmark/results/perf_compile_runtime_ledger_20261003/agent-environment-comparison.json new file mode 100644 index 0000000000..108861c831 --- /dev/null +++ b/benchmark/results/perf_compile_runtime_ledger_20261003/agent-environment-comparison.json @@ -0,0 +1,8 @@ +{ + "current_matches_prepared": true, + "current_environment_sha256": "00b7f550f6e584e34d6070f79bdd60d37859cb58c752b866fc95ac7f5293dabd", + "prepared_environment_sha256": "00b7f550f6e584e34d6070f79bdd60d37859cb58c752b866fc95ac7f5293dabd", + "differing_key_names": [ + "CODEX_THREAD_ID" + ] +} diff --git a/benchmark/results/perf_compile_runtime_ledger_20261003/assessment.json b/benchmark/results/perf_compile_runtime_ledger_20261003/assessment.json new file mode 100644 index 0000000000..6368532f34 --- /dev/null +++ b/benchmark/results/perf_compile_runtime_ledger_20261003/assessment.json @@ -0,0 +1,431 @@ +{ + "source_revision": "480aed49b30448de4380af5c9274e48084e67b63", + "diagnostic_only": true, + "production_change": false, + "public_row": { + "case_name": "cp_tutorial_3d_monolayer", + "mode_name": "1w_1t", + "worker_count": "1", + "well_count": "1", + "compile_seconds": "1.495826244354248", + "prepare_seconds": "0.0", + "execute_seconds": "8.396726846694946", + "total_seconds": "10.694032318890095", + "wells_per_second": "0.11909402535746559", + "successful_wells": "1", + "native_single_sample_execution_seconds": "", + "projected_native_execution_seconds": "", + "projected_execution_speedup": "", + "peak_memory_mb": "1658.1875", + "status": "success", + "memory_limit_mb": "", + "error_message": "", + "execution_route": "ordinary-zmq-outcomes-v1", + "server_lifecycle": "reused-per-sweep", + "run_input_sha256": "7f66dd478c9a56033495f056335a0eaadb9fd5facd953b504f0e1f8f7eaa57d5" + }, + "execution_partition": { + "root": 8.383970148861408, + "declared_callable_exclusive": 3.6605217196047306, + "outside_declared_callable": 4.723448429256678, + "outside_fraction": 0.5633904159234333 + }, + "compilation": { + "root": 1.2609371892176569, + "state_owner_exclusive": 0.6710898061282933, + "interpretation": "Sum of nested disjoint exclusive state-owner spans. The ten effective-config reads are a .195838s inclusive subset, not an additional term or a removable .671s cost." + }, + "timing_contract": "Public original 1w_1t driver, CPU5, inline execution, reused-per-sweep server READY/library/kernel preparation before clocks; both profilers off, original OUTCOMES and RSS observer. New41 hook version with inherited24 plus17 nominal compiler/config boundaries. Diagnostic overhead remains; no ordinary gain, regression, statistical significance, or fresh native ratio inferred.", + "parity": "FULL6CSV120TIFF_AND180_PHYSICAL_SOURCE_REFS_PASS", + "native_scope": "TRANSITIVE ONLY: exact current saved science equals retained reference and B764-qualified bytes with actual same3physicalvolume inputs/180orderedrefs. Retained2native repetitions verified; native was NOT rerun for current source/environment. Original native compare API RED retained separately; no current native timing ratio or performance claim.", + "environment": { + "python": "3.12.14 (main, Sep 24 2026, 17:57:43) [Clang 22.1.3 ]", + "versions": { + "openhcs": "0.8.7", + "numpy": "2.5.3", + "scipy": "1.18.1", + "arraybridge": "0.3.6", + "polystore": "0.3.0", + "python-introspect": "0.1.15", + "metaclass-registry": "0.2.2", + "zmqruntime": "0.3.0" + } + }, + "declared_hooks": [ + { + "module": "openhcs.runtime.zmq_execution_server", + "owner": "ZMQExecutionServer", + "method": "_execute_with_orchestrator", + "line": 485, + "descriptor_kind": "function", + "source_relative": "openhcs/runtime/zmq_execution_server.py" + }, + { + "module": "openhcs.runtime.zmq_execution_server", + "owner": "ZMQExecutionServer", + "method": "_initialize_orchestrator", + "line": 592, + "descriptor_kind": "function", + "source_relative": "openhcs/runtime/zmq_execution_server.py" + }, + { + "module": "openhcs.runtime.zmq_execution_server", + "owner": "ZMQExecutionServer", + "method": "_export_runtime_observation", + "line": 741, + "descriptor_kind": "function", + "source_relative": "openhcs/runtime/zmq_execution_server.py" + }, + { + "module": "openhcs.core.orchestrator.worker_execution", + "owner": "", + "method": "execute_worker_lane", + "line": 1064, + "descriptor_kind": "function", + "source_relative": "openhcs/core/orchestrator/worker_execution.py" + }, + { + "module": "openhcs.core.steps.function_runtime", + "owner": "FunctionCoreExecutor", + "method": "execute", + "line": 2075, + "descriptor_kind": "function", + "source_relative": "openhcs/core/steps/function_runtime.py" + }, + { + "module": "openhcs.core.steps.function_runtime", + "owner": "PatternGroupRuntime", + "method": "_load_input_stack", + "line": 2821, + "descriptor_kind": "function", + "source_relative": "openhcs/core/steps/function_runtime.py" + }, + { + "module": "openhcs.core.steps.function_runtime", + "owner": "PatternGroupRuntime", + "method": "_validate_and_unstack", + "line": 3248, + "descriptor_kind": "function", + "source_relative": "openhcs/core/steps/function_runtime.py" + }, + { + "module": "openhcs.core.steps.function_runtime", + "owner": "PatternGroupRuntime", + "method": "_save_outputs", + "line": 3374, + "descriptor_kind": "function", + "source_relative": "openhcs/core/steps/function_runtime.py" + }, + { + "module": "openhcs.processing.backends.lib_registry.unified_registry", + "owner": "RuntimeCallableInvocation", + "method": "call", + "line": 264, + "descriptor_kind": "function", + "source_relative": "openhcs/processing/backends/lib_registry/unified_registry.py" + }, + { + "module": "openhcs.interop.cellprofiler.runtime.module_execution", + "owner": "CellProfilerModuleExecutor", + "method": "__call__", + "line": 251, + "descriptor_kind": "function", + "source_relative": "openhcs/interop/cellprofiler/runtime/module_execution.py" + }, + { + "module": "openhcs.interop.cellprofiler.runtime.module_execution", + "owner": "CellProfilerModuleExecutor", + "method": "_image_request", + "line": 1259, + "descriptor_kind": "function", + "source_relative": "openhcs/interop/cellprofiler/runtime/module_execution.py" + }, + { + "module": "openhcs.interop.cellprofiler.runtime.function_contract_execution", + "owner": "CellProfilerFunctionContractExecutor", + "method": "execute", + "line": 78, + "descriptor_kind": "function", + "source_relative": "openhcs/interop/cellprofiler/runtime/function_contract_execution.py" + }, + { + "module": "openhcs.interop.cellprofiler.runtime.output_recording", + "owner": "CellProfilerOutputRecorder", + "method": "record_module_outputs", + "line": 106, + "descriptor_kind": "classmethod", + "source_relative": "openhcs/interop/cellprofiler/runtime/output_recording.py" + }, + { + "module": "openhcs.interop.cellprofiler.runtime.measurement_execution_support", + "owner": "ObjectMeasurementOutputRecorder", + "method": "record", + "line": 408, + "descriptor_kind": "function", + "source_relative": "openhcs/interop/cellprofiler/runtime/measurement_execution_support.py" + }, + { + "module": "openhcs.core.steps.function_outputs", + "owner": "", + "method": "finalize_function_step_outputs", + "line": 140, + "descriptor_kind": "function", + "source_relative": "openhcs/core/steps/function_outputs.py" + }, + { + "module": "openhcs.core.steps.function_outputs", + "owner": "OpenHCSMetadataWriter", + "method": "finalize_completed_plate", + "line": 1092, + "descriptor_kind": "classmethod", + "source_relative": "openhcs/core/steps/function_outputs.py" + }, + { + "module": "openhcs.core.steps.function_outputs", + "owner": "RuntimeArtifactMetadataTarget", + "method": "reconciliation_targets", + "line": 1217, + "descriptor_kind": "function", + "source_relative": "openhcs/core/steps/function_outputs.py" + }, + { + "module": "openhcs.core.virtual_workspace_metadata", + "owner": "AtomicMetadataWriter", + "method": "publish_source_projection_metadata", + "line": 181, + "descriptor_kind": "function", + "source_relative": "openhcs/core/virtual_workspace_metadata.py" + }, + { + "module": "openhcs.core.runtime_stack_cache", + "owner": "RuntimeImageStackCache", + "method": "get", + "line": 38, + "descriptor_kind": "function", + "source_relative": "openhcs/core/runtime_stack_cache.py" + }, + { + "module": "openhcs.core.runtime_stack_cache", + "owner": "RuntimeImageStackCache", + "method": "store", + "line": 49, + "descriptor_kind": "function", + "source_relative": "openhcs/core/runtime_stack_cache.py" + }, + { + "module": "openhcs.core.aligned_image_payload", + "owner": "", + "method": "stack_image_payload_context", + "line": 400, + "descriptor_kind": "function", + "source_relative": "openhcs/core/aligned_image_payload.py" + }, + { + "module": "openhcs.core.aligned_image_payload", + "owner": "", + "method": "unstack_image_payload_context", + "line": 598, + "descriptor_kind": "function", + "source_relative": "openhcs/core/aligned_image_payload.py" + }, + { + "module": "openhcs.core.orchestrator.analysis_consolidation", + "owner": "", + "method": "consolidate_analysis_outputs", + "line": 109, + "descriptor_kind": "function", + "source_relative": "openhcs/core/orchestrator/analysis_consolidation.py" + }, + { + "module": "openhcs.core.measurement_feature_queries", + "owner": "MeasurementFeatureValueIndex", + "method": "from_columnar_table_by_object", + "line": 1327, + "descriptor_kind": "classmethod", + "source_relative": "openhcs/core/measurement_feature_queries.py" + }, + { + "module": "openhcs.core.pipeline.compiler", + "owner": "PipelineCompiler", + "method": "compile_pipelines", + "line": 1816, + "descriptor_kind": "staticmethod", + "source_relative": "openhcs/core/pipeline/compiler.py" + }, + { + "module": "openhcs.core.pipeline.compiler", + "owner": "PipelineCompiler", + "method": "_register_and_resolve_pipeline_once", + "line": 1061, + "descriptor_kind": "staticmethod", + "source_relative": "openhcs/core/pipeline/compiler.py" + }, + { + "module": "openhcs.core.pipeline.compiler", + "owner": "PipelineCompiler", + "method": "initialize_step_plans_for_context", + "line": 235, + "descriptor_kind": "staticmethod", + "source_relative": "openhcs/core/pipeline/compiler.py" + }, + { + "module": "openhcs.core.pipeline.compiler", + "owner": "PipelineCompiler", + "method": "_capture_pipeline_config", + "line": 1229, + "descriptor_kind": "staticmethod", + "source_relative": "openhcs/core/pipeline/compiler.py" + }, + { + "module": "openhcs.core.pipeline.compiler", + "owner": "PipelineCompiler", + "method": "_resolve_global_step_axis_filters", + "line": 1241, + "descriptor_kind": "staticmethod", + "source_relative": "openhcs/core/pipeline/compiler.py" + }, + { + "module": "openhcs.core.pipeline.compiler", + "owner": "PipelineCompiler", + "method": "_configure_input_conversion_if_needed", + "line": 364, + "descriptor_kind": "staticmethod", + "source_relative": "openhcs/core/pipeline/compiler.py" + }, + { + "module": "openhcs.core.pipeline.compiler", + "owner": "PipelineCompiler", + "method": "declare_zarr_stores", + "line": 590, + "descriptor_kind": "staticmethod", + "source_relative": "openhcs/core/pipeline/compiler.py" + }, + { + "module": "openhcs.core.pipeline.compiler", + "owner": "PipelineCompiler", + "method": "_finalize_compilation", + "line": 1735, + "descriptor_kind": "staticmethod", + "source_relative": "openhcs/core/pipeline/compiler.py" + }, + { + "module": "openhcs.core.pipeline.compiler", + "owner": "PipelineCompiler", + "method": "_axis_values_to_process", + "line": 1023, + "descriptor_kind": "staticmethod", + "source_relative": "openhcs/core/pipeline/compiler.py" + }, + { + "module": "openhcs.core.pipeline.compiler", + "owner": "AxisCompilationRequest", + "method": "context_for", + "line": 205, + "descriptor_kind": "function", + "source_relative": "openhcs/core/pipeline/compiler.py" + }, + { + "module": "openhcs.core.pipeline.compilation_session", + "owner": "CompilationSession", + "method": "from_context", + "line": 182, + "descriptor_kind": "classmethod", + "source_relative": "openhcs/core/pipeline/compilation_session.py" + }, + { + "module": "openhcs.core.orchestrator.orchestrator", + "owner": "PipelineOrchestrator", + "method": "create_context", + "line": 556, + "descriptor_kind": "function", + "source_relative": "openhcs/core/orchestrator/orchestrator.py" + }, + { + "module": "openhcs.core.orchestrator.orchestrator", + "owner": "PipelineOrchestrator", + "method": "get_effective_config", + "line": 1009, + "descriptor_kind": "function", + "source_relative": "openhcs/core/orchestrator/orchestrator.py" + }, + { + "module": "objectstate.object_state", + "owner": "ObjectState", + "method": "__init__", + "line": 144, + "descriptor_kind": "function", + "source_relative": "external/ObjectState/src/objectstate/object_state.py" + }, + { + "module": "objectstate.object_state", + "owner": "ObjectState", + "method": "_compute_resolved_snapshot", + "line": 2111, + "descriptor_kind": "function", + "source_relative": "external/ObjectState/src/objectstate/object_state.py" + }, + { + "module": "objectstate.object_state", + "owner": "ObjectState", + "method": "_reconstruct_from_resolved", + "line": 1245, + "descriptor_kind": "function", + "source_relative": "external/ObjectState/src/objectstate/object_state.py" + }, + { + "module": "objectstate.object_state", + "owner": "ObjectState", + "method": "to_saved_resolved_object", + "line": 470, + "descriptor_kind": "function", + "source_relative": "external/ObjectState/src/objectstate/object_state.py" + } + ], + "freeze": { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/compiler-config-coarse-ledger-v1/output/source-freeze.json", + "sha256": "0aa8d013c340ed52de584cc006becc832dfa1a9875745213d2d2e4c0904656cf", + "controller_path": "/home/ts/.local/state/openhcs-maintenance/20261003/compiler-config-coarse-ledger-v1/controller.py", + "controller_sha256": "5460564d44ffdc7e70078d63ac1250bb65a4fff2320da8dcb89b06fba999602f", + "site_path": "/home/ts/.local/state/openhcs-maintenance/20261003/compiler-config-coarse-ledger-v1/site/sitecustomize.py", + "site_sha256": "339c145b2c13548589958d12ebfe77672f87a108c82cbb44dbe1fdcc929d024b", + "installed_freeze_sha256": "b7e5bbb9b5171476dfeb1692378419bfa157bb620028e1b91c8969eef2498b41", + "full_environment_sha256": "00b7f550f6e584e34d6070f79bdd60d37859cb58c752b866fc95ac7f5293dabd" + }, + "prelaunch_refusal": "Root exec87291 terminated1 before launching a driver: full environment hash differed only in CODEX_THREAD_ID. Original matching agent environment retained the exact freeze, ran once in session17712 terminal0, and validated science in session51754 terminal0. No gate bypass/freeze rewrite.", + "completed_controls": 15, + "originals": { + "controls.json": { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/compiler-config-coarse-ledger-v1/controls.json", + "sha256": "9acdee867fb46c959a6ddbab82505cb288e1ac564d439f0e35f713d79a65b122" + }, + "science.json": { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/compiler-config-coarse-ledger-v1/science.json", + "sha256": "d19e5294a829cf98a0d0730b2729224e8a6947f258641a6f0923dfc6af732d74" + }, + "registration-owner-analysis.json": { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/compiler-config-coarse-ledger-v1/registration-owner-analysis.json", + "sha256": "45faf15f51d4a293b3e432afe86995274eee00925c159ed2db06856ea821c238" + }, + "agent-environment-comparison.json": { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/compiler-config-coarse-ledger-v1/agent-environment-comparison.json", + "sha256": "2a62e8d0f8563b8d461a8128676b933d68b89b37e0be947125acc51b7e8fcf61" + }, + "observations.json": { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/compiler-config-coarse-ledger-v1/output/observations.json", + "sha256": "07c19d9a2b6183d1cbb69691258862f3b89b65149481989557b0d1029bf728e3" + }, + "compile_only-region.json": { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/compiler-config-coarse-ledger-v1/output/regions/region-1948899-139659153200832-0001.json", + "sha256": "6b604c99652c36aba1ce3eff39b65ccd73b71d17ca827c121a98676b4608c687" + }, + "execution-region.json": { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/compiler-config-coarse-ledger-v1/output/regions/region-1948899-139659153200832-0002.json", + "sha256": "ea2b9668eb40dc72c8af0ed9a6d55250fcd46721db5c5554fd0ea3c82ff738dd" + }, + "availability-rejection.json": { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/workspace-availability-replay-v1/decision.json", + "sha256": "91d34dd84e764f3eb45ab1f15b8ddeeabbebf1f7104b3032534b1541d937e78c" + } + }, + "outstanding": "Dominant integrated runtime fix, matched ordinary before/after gain, original branch architectural gates, installed acceptance, full-catalog/scaling/native refresh and figures remain unproved. Goal active." +} diff --git a/benchmark/results/perf_compile_runtime_ledger_20261003/availability-rejection.json b/benchmark/results/perf_compile_runtime_ledger_20261003/availability-rejection.json new file mode 100644 index 0000000000..bd965f7b32 --- /dev/null +++ b/benchmark/results/perf_compile_runtime_ledger_20261003/availability-rejection.json @@ -0,0 +1,33 @@ +{ + "status": "REJECT_ROUTE_AS_DOMINANT_FIX", + "source_revision": "480aed49b30448de4380af5c9274e48084e67b63", + "source_document": "/home/ts/.local/state/openhcs-maintenance/20261003/plane-facts-whole-driver-baseline-v1/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace/openhcs_metadata.json", + "source_document_sha256": "a42ec413a0ec213d22f4ba96fac785bfbfac167dd5d2da7cbd1acc8cf9c2d862", + "source_pins": { + "/home/ts/code/projects/openhcs-shared-runtime-plumbing/openhcs/core/virtual_workspace_metadata.py": "25987be96327dadefa1ecb63a93b2678a0957ffdf42383822d02ef666282b24a", + "/home/ts/code/projects/openhcs-shared-runtime-plumbing/openhcs/core/source_workspace_projection.py": "1e54affed309fbc0d150bee4a3deedbc127aedf39a83f0c8a0eaf719257922be", + "/home/ts/code/projects/openhcs-shared-runtime-plumbing/external/PolyStore/src/polystore/virtual_workspace.py": "b19fcda85369d812e70bfc1a0efc5d8861ae256f69e94372e2e9515c591d36af" + }, + "original_projection_cache_hit_calls_per_replay": 68, + "original_unprofiled_seconds": [ + 0.024565204977989197, + 0.024529157672077417, + 0.024390146136283875, + 0.02441246807575226, + 0.024447152856737375, + 0.02439353195950389, + 0.024356392212212086, + 0.02441841922700405, + 0.024412577971816063 + ], + "median_seconds": 0.024412577971816063, + "controls": [ + { + "name": "current_invalid_ref_rejected_after_projection_cache_hit", + "passed": true, + "type": "TypeError", + "message": "SourcePixelRef.source_axis_indices must contain nonnegative integers." + } + ], + "interpretation": "This retained real source document replays all 68 availability calls observed in an older worker trace. It measures the complete original cache-hit availability operation with a trivial already-loaded document handler; it does not claim the current ordinary driver has identical call counts or equate the replay clock with an end-to-end saving. Even removing the complete replay operation cannot materially close the qualified 1.916s execution gap. Public mutable document validation is observable and cannot simply be skipped." +} diff --git a/benchmark/results/perf_compile_runtime_ledger_20261003/compile_only-region.json b/benchmark/results/perf_compile_runtime_ledger_20261003/compile_only-region.json new file mode 100644 index 0000000000..302cec62b5 --- /dev/null +++ b/benchmark/results/perf_compile_runtime_ledger_20261003/compile_only-region.json @@ -0,0 +1,416 @@ +{ + "pid": 1948899, + "thread": 139659153200832, + "region": "ZMQExecutionServer._execute_with_orchestrator", + "root_dimensions": "{\"case\": \"3d_monolayer_final\", \"compile_artifact_id\": null, \"execution_id\": \"8aba2e39-4db0-4608-bf23-08568aef528e\", \"kind\": \"compile_only\", \"plate\": \"/home/ts/.cache/openhcs/benchmark_datasets/CellProfiler_tutorials/data/3d_monolayer/images\"}", + "seconds": 1.2609371892176569, + "revision": "480aed49b30448de4380af5c9274e48084e67b63", + "source": "/home/ts/code/projects/openhcs-shared-runtime-plumbing", + "stats": { + "ObjectState._compute_resolved_snapshot": { + "owner": "ObjectState._compute_resolved_snapshot", + "dimensions": "", + "phase": "compiler_config:_compute_resolved_snapshot", + "calls": 88, + "inclusive": 0.4525698209181428, + "exclusive": 0.4525698209181428 + }, + "ObjectState.__init__": { + "owner": "ObjectState.__init__", + "dimensions": "", + "phase": "compiler_config:__init__", + "calls": 44, + "inclusive": 0.6144807459786534, + "exclusive": 0.16191092506051064 + }, + "ObjectState._reconstruct_from_resolved": { + "owner": "ObjectState._reconstruct_from_resolved", + "dimensions": "", + "phase": "compiler_config:_reconstruct_from_resolved", + "calls": 532, + "inclusive": 0.1099232928827405, + "exclusive": 0.05649412330240011 + }, + "ObjectState.to_saved_resolved_object": { + "owner": "ObjectState.to_saved_resolved_object", + "dimensions": "", + "phase": "compiler_config:to_saved_resolved_object", + "calls": 42, + "inclusive": 0.056609060149639845, + "exclusive": 0.00011493684723973274 + }, + "PipelineOrchestrator.get_effective_config": { + "owner": "PipelineOrchestrator.get_effective_config", + "dimensions": "", + "phase": "compiler_config:get_effective_config", + "calls": 10, + "inclusive": 0.19583785673603415, + "exclusive": 8.42963345348835e-05 + }, + "PipelineOrchestrator.create_context": { + "owner": "PipelineOrchestrator.create_context", + "dimensions": "", + "phase": "compiler_config:create_context", + "calls": 3, + "inclusive": 0.04134061839431524, + "exclusive": 0.0006397129036486149 + }, + "ZMQExecutionServer._initialize_orchestrator": { + "owner": "ZMQExecutionServer._initialize_orchestrator", + "dimensions": "", + "phase": "_initialize_orchestrator", + "calls": 1, + "inclusive": 0.1260731448419392, + "exclusive": 0.022322111297398806 + }, + "PipelineCompiler._axis_values_to_process": { + "owner": "PipelineCompiler._axis_values_to_process", + "dimensions": "", + "phase": "compiler_config:_axis_values_to_process", + "calls": 1, + "inclusive": 0.017245036084204912, + "exclusive": 6.654579192399979e-05 + }, + "PipelineCompiler._register_and_resolve_pipeline_once": { + "owner": "PipelineCompiler._register_and_resolve_pipeline_once", + "dimensions": "", + "phase": "compiler_config:_register_and_resolve_pipeline_once", + "calls": 1, + "inclusive": 0.47889699088409543, + "exclusive": 0.005476511083543301 + }, + "PipelineCompiler._capture_pipeline_config": { + "owner": "PipelineCompiler._capture_pipeline_config", + "dimensions": "", + "phase": "compiler_config:_capture_pipeline_config", + "calls": 1, + "inclusive": 0.0019184919074177742, + "exclusive": 2.7259811758995056e-06 + }, + "PipelineCompiler._resolve_global_step_axis_filters": { + "owner": "PipelineCompiler._resolve_global_step_axis_filters", + "dimensions": "", + "phase": "compiler_config:_resolve_global_step_axis_filters", + "calls": 1, + "inclusive": 0.015254999045282602, + "exclusive": 0.001242930069565773 + }, + "AxisCompilationRequest.context_for": { + "owner": "AxisCompilationRequest.context_for", + "dimensions": "", + "phase": "compiler_config:context_for", + "calls": 1, + "inclusive": 0.013713276013731956, + "exclusive": 0.00032007182016968727 + }, + "CompilationSession.from_context": { + "owner": "CompilationSession.from_context", + "dimensions": "", + "phase": "compiler_config:from_context", + "calls": 1, + "inclusive": 2.0298175513744354e-05, + "exclusive": 2.0298175513744354e-05 + }, + "PipelineCompiler._configure_input_conversion_if_needed": { + "owner": "PipelineCompiler._configure_input_conversion_if_needed", + "dimensions": "", + "phase": "compiler_config:_configure_input_conversion_if_needed", + "calls": 1, + "inclusive": 0.013645949773490429, + "exclusive": 5.200039595365524e-06 + }, + "PipelineCompiler.initialize_step_plans_for_context": { + "owner": "PipelineCompiler.initialize_step_plans_for_context", + "dimensions": "", + "phase": "compiler_config:initialize_step_plans_for_context", + "calls": 1, + "inclusive": 0.4779214742593467, + "exclusive": 0.46425522631034255 + }, + "PipelineCompiler.declare_zarr_stores": { + "owner": "PipelineCompiler.declare_zarr_stores", + "dimensions": "", + "phase": "compiler_config:declare_zarr_stores", + "calls": 1, + "inclusive": 0.018639610148966312, + "exclusive": 9.438721463084221e-05 + }, + "PipelineCompiler._finalize_compilation": { + "owner": "PipelineCompiler._finalize_compilation", + "dimensions": "", + "phase": "compiler_config:_finalize_compilation", + "calls": 1, + "inclusive": 0.016232328955084085, + "exclusive": 0.00027552898973226547 + }, + "PipelineCompiler.compile_pipelines": { + "owner": "PipelineCompiler.compile_pipelines", + "dimensions": "", + "phase": "compiler_config:compile_pipelines", + "calls": 1, + "inclusive": 1.094543606042862, + "exclusive": 0.05472139874473214 + }, + "ZMQExecutionServer._execute_with_orchestrator | {\"case\": \"3d_monolayer_final\", \"compile_artifact_id\": null, \"execution_id\": \"8aba2e39-4db0-4608-bf23-08568aef528e\", \"kind\": \"compile_only\", \"plate\": \"/home/ts/.cache/openhcs/benchmark_datasets/CellProfiler_tutorials/data/3d_monolayer/images\"}": { + "owner": "ZMQExecutionServer._execute_with_orchestrator", + "dimensions": "{\"case\": \"3d_monolayer_final\", \"compile_artifact_id\": null, \"execution_id\": \"8aba2e39-4db0-4608-bf23-08568aef528e\", \"kind\": \"compile_only\", \"plate\": \"/home/ts/.cache/openhcs/benchmark_datasets/CellProfiler_tutorials/data/3d_monolayer/images\"}", + "phase": "_execute_with_orchestrator", + "calls": 1, + "inclusive": 1.2609371892176569, + "exclusive": 0.0403204383328557 + } + }, + "hooks_installed": [ + { + "module": "objectstate.object_state", + "owner": "ObjectState", + "method": "__init__", + "descriptor_kind": "function" + }, + { + "module": "objectstate.object_state", + "owner": "ObjectState", + "method": "_compute_resolved_snapshot", + "descriptor_kind": "function" + }, + { + "module": "objectstate.object_state", + "owner": "ObjectState", + "method": "_reconstruct_from_resolved", + "descriptor_kind": "function" + }, + { + "module": "objectstate.object_state", + "owner": "ObjectState", + "method": "to_saved_resolved_object", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.aligned_image_payload", + "owner": "", + "method": "stack_image_payload_context", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.aligned_image_payload", + "owner": "", + "method": "unstack_image_payload_context", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.measurement_feature_queries", + "owner": "MeasurementFeatureValueIndex", + "method": "from_columnar_table_by_object", + "descriptor_kind": "classmethod" + }, + { + "module": "openhcs.core.virtual_workspace_metadata", + "owner": "AtomicMetadataWriter", + "method": "publish_source_projection_metadata", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.runtime_stack_cache", + "owner": "RuntimeImageStackCache", + "method": "get", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.runtime_stack_cache", + "owner": "RuntimeImageStackCache", + "method": "store", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.steps.function_outputs", + "owner": "", + "method": "finalize_function_step_outputs", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.steps.function_outputs", + "owner": "OpenHCSMetadataWriter", + "method": "finalize_completed_plate", + "descriptor_kind": "classmethod" + }, + { + "module": "openhcs.core.steps.function_outputs", + "owner": "RuntimeArtifactMetadataTarget", + "method": "reconciliation_targets", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.steps.function_runtime", + "owner": "FunctionCoreExecutor", + "method": "execute", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.steps.function_runtime", + "owner": "PatternGroupRuntime", + "method": "_load_input_stack", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.steps.function_runtime", + "owner": "PatternGroupRuntime", + "method": "_validate_and_unstack", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.steps.function_runtime", + "owner": "PatternGroupRuntime", + "method": "_save_outputs", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.pipeline.compilation_session", + "owner": "CompilationSession", + "method": "from_context", + "descriptor_kind": "classmethod" + }, + { + "module": "openhcs.core.pipeline.compiler", + "owner": "PipelineCompiler", + "method": "compile_pipelines", + "descriptor_kind": "staticmethod" + }, + { + "module": "openhcs.core.pipeline.compiler", + "owner": "PipelineCompiler", + "method": "_register_and_resolve_pipeline_once", + "descriptor_kind": "staticmethod" + }, + { + "module": "openhcs.core.pipeline.compiler", + "owner": "PipelineCompiler", + "method": "initialize_step_plans_for_context", + "descriptor_kind": "staticmethod" + }, + { + "module": "openhcs.core.pipeline.compiler", + "owner": "PipelineCompiler", + "method": "_capture_pipeline_config", + "descriptor_kind": "staticmethod" + }, + { + "module": "openhcs.core.pipeline.compiler", + "owner": "PipelineCompiler", + "method": "_resolve_global_step_axis_filters", + "descriptor_kind": "staticmethod" + }, + { + "module": "openhcs.core.pipeline.compiler", + "owner": "PipelineCompiler", + "method": "_configure_input_conversion_if_needed", + "descriptor_kind": "staticmethod" + }, + { + "module": "openhcs.core.pipeline.compiler", + "owner": "PipelineCompiler", + "method": "declare_zarr_stores", + "descriptor_kind": "staticmethod" + }, + { + "module": "openhcs.core.pipeline.compiler", + "owner": "PipelineCompiler", + "method": "_finalize_compilation", + "descriptor_kind": "staticmethod" + }, + { + "module": "openhcs.core.pipeline.compiler", + "owner": "PipelineCompiler", + "method": "_axis_values_to_process", + "descriptor_kind": "staticmethod" + }, + { + "module": "openhcs.core.pipeline.compiler", + "owner": "AxisCompilationRequest", + "method": "context_for", + "descriptor_kind": "function" + }, + { + "module": "openhcs.processing.backends.lib_registry.unified_registry", + "owner": "RuntimeCallableInvocation", + "method": "call", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.orchestrator.analysis_consolidation", + "owner": "", + "method": "consolidate_analysis_outputs", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.orchestrator.worker_execution", + "owner": "", + "method": "execute_worker_lane", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.orchestrator.orchestrator", + "owner": "PipelineOrchestrator", + "method": "create_context", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.orchestrator.orchestrator", + "owner": "PipelineOrchestrator", + "method": "get_effective_config", + "descriptor_kind": "function" + }, + { + "module": "openhcs.runtime.zmq_execution_server", + "owner": "ZMQExecutionServer", + "method": "_execute_with_orchestrator", + "descriptor_kind": "function" + }, + { + "module": "openhcs.runtime.zmq_execution_server", + "owner": "ZMQExecutionServer", + "method": "_initialize_orchestrator", + "descriptor_kind": "function" + }, + { + "module": "openhcs.runtime.zmq_execution_server", + "owner": "ZMQExecutionServer", + "method": "_export_runtime_observation", + "descriptor_kind": "function" + }, + { + "module": "openhcs.interop.cellprofiler.runtime.function_contract_execution", + "owner": "CellProfilerFunctionContractExecutor", + "method": "execute", + "descriptor_kind": "function" + }, + { + "module": "openhcs.interop.cellprofiler.runtime.measurement_execution_support", + "owner": "ObjectMeasurementOutputRecorder", + "method": "record", + "descriptor_kind": "function" + }, + { + "module": "openhcs.interop.cellprofiler.runtime.output_recording", + "owner": "CellProfilerOutputRecorder", + "method": "record_module_outputs", + "descriptor_kind": "classmethod" + }, + { + "module": "openhcs.interop.cellprofiler.runtime.module_execution", + "owner": "CellProfilerModuleExecutor", + "method": "__call__", + "descriptor_kind": "function" + }, + { + "module": "openhcs.interop.cellprofiler.runtime.module_execution", + "owner": "CellProfilerModuleExecutor", + "method": "_image_request", + "descriptor_kind": "function" + } + ], + "profiling_enabled": false, + "runtime_profiling_enabled": false, + "diagnostic_errors": [], + "captures": false, + "scope": "coarse same-run nested exclusive timing partition on the exact source revision recorded in this region. Diagnostic wrappers, no scientific or memory-observer substitutions. Not accepted performance comparison. Declared callable boundary includes filtering/decorated processing/metadata; not pure numerical kernels. Both existing profilers are disabled. These wrappers still add diagnostic overhead; no scaling to uninstrumented clocks or causal performance claims. Compiler/state boundaries and query boundary are nested under actual parents and excluded from parent-exclusive elapsed. This is a NEW 41-hook diagnostic version; inherited 24 boundary labels remain, site bytes are changed. ObjectState is verified against the actual source dependency path, not assumed inside the OpenHCS package." +} diff --git a/benchmark/results/perf_compile_runtime_ledger_20261003/controls.json b/benchmark/results/perf_compile_runtime_ledger_20261003/controls.json new file mode 100644 index 0000000000..2205f1cc27 --- /dev/null +++ b/benchmark/results/perf_compile_runtime_ledger_20261003/controls.json @@ -0,0 +1,24 @@ +{ + "status": "PASS", + "controls": [ + "original_called_once", + "argument_mutation_and_result_identity", + "signature_wrapped_code", + "nested_exclusive_conservation", + "error_identity_and_mutation", + "finally_restores_nested_state", + "root_namespace_no_overwrite", + "outside_root_ready_passthrough", + "fork_tls_reset", + "source_loader_real_declaring_path", + "instance_descriptor", + "classmethod_descriptor", + "staticmethod_descriptor", + "source_hash_drift_refusal", + "unchanged_storage_caps_and_41_hook_roster" + ], + "count": 15, + "site_sha256": "339c145b2c13548589958d12ebfe77672f87a108c82cbb44dbe1fdcc929d024b", + "controller_sha256": "5460564d44ffdc7e70078d63ac1250bb65a4fff2320da8dcb89b06fba999602f", + "scope": "Temporary test-owned source only, original result/error/mutation and descriptor/path/hash laws; no candidate pipeline import, compile, execution, profiler, snapshot or processing callback invocation." +} diff --git a/benchmark/results/perf_compile_runtime_ledger_20261003/execution-region.json b/benchmark/results/perf_compile_runtime_ledger_20261003/execution-region.json new file mode 100644 index 0000000000..5aa3f135dc --- /dev/null +++ b/benchmark/results/perf_compile_runtime_ledger_20261003/execution-region.json @@ -0,0 +1,864 @@ +{ + "pid": 1948899, + "thread": 139659153200832, + "region": "ZMQExecutionServer._execute_with_orchestrator", + "root_dimensions": "{\"case\": \"3d_monolayer_final\", \"compile_artifact_id\": \"8aba2e39-4db0-4608-bf23-08568aef528e\", \"execution_id\": \"37e4bb4a-7f5d-463a-a17f-ff8ff7dee0f9\", \"kind\": \"execution\", \"plate\": \"/home/ts/.cache/openhcs/benchmark_datasets/CellProfiler_tutorials/data/3d_monolayer/images\"}", + "seconds": 8.383970148861408, + "revision": "480aed49b30448de4380af5c9274e48084e67b63", + "source": "/home/ts/code/projects/openhcs-shared-runtime-plumbing", + "stats": { + "ObjectState._compute_resolved_snapshot": { + "owner": "ObjectState._compute_resolved_snapshot", + "dimensions": "", + "phase": "compiler_config:_compute_resolved_snapshot", + "calls": 8, + "inclusive": 0.04228230891749263, + "exclusive": 0.04228230891749263 + }, + "ObjectState.__init__": { + "owner": "ObjectState.__init__", + "dimensions": "", + "phase": "compiler_config:__init__", + "calls": 4, + "inclusive": 0.05480403359979391, + "exclusive": 0.012521724682301283 + }, + "ObjectState._reconstruct_from_resolved": { + "owner": "ObjectState._reconstruct_from_resolved", + "dimensions": "", + "phase": "compiler_config:_reconstruct_from_resolved", + "calls": 92, + "inclusive": 0.013641910627484322, + "exclusive": 0.007105355150997639 + }, + "ObjectState.to_saved_resolved_object": { + "owner": "ObjectState.to_saved_resolved_object", + "dimensions": "", + "phase": "compiler_config:to_saved_resolved_object", + "calls": 4, + "inclusive": 0.007119361776858568, + "exclusive": 1.4006625860929489e-05 + }, + "PipelineOrchestrator.get_effective_config": { + "owner": "PipelineOrchestrator.get_effective_config", + "dimensions": "", + "phase": "compiler_config:get_effective_config", + "calls": 4, + "inclusive": 0.06195876281708479, + "exclusive": 3.5367440432310104e-05 + }, + "PipelineOrchestrator.create_context": { + "owner": "PipelineOrchestrator.create_context", + "dimensions": "", + "phase": "compiler_config:create_context", + "calls": 1, + "inclusive": 0.014066982083022594, + "exclusive": 6.845779716968536e-05 + }, + "ZMQExecutionServer._initialize_orchestrator": { + "owner": "ZMQExecutionServer._initialize_orchestrator", + "dimensions": "", + "phase": "_initialize_orchestrator", + "calls": 1, + "inclusive": 0.08127375598996878, + "exclusive": 0.019246535375714302 + }, + "RuntimeImageStackCache.get": { + "owner": "RuntimeImageStackCache.get", + "dimensions": "", + "phase": "get", + "calls": 32, + "inclusive": 0.00039217015728354454, + "exclusive": 0.00039217015728354454 + }, + "stack_image_payload_context": { + "owner": "stack_image_payload_context", + "dimensions": "", + "phase": "stack_image_payload_context", + "calls": 28, + "inclusive": 0.04806605773046613, + "exclusive": 0.04806605773046613 + }, + "PatternGroupRuntime._load_input_stack": { + "owner": "PatternGroupRuntime._load_input_stack", + "dimensions": "", + "phase": "_load_input_stack", + "calls": 32, + "inclusive": 0.7971800449304283, + "exclusive": 0.7817966351285577 + }, + "CellProfilerModuleExecutor._image_request": { + "owner": "CellProfilerModuleExecutor._image_request", + "dimensions": "", + "phase": "_image_request", + "calls": 30, + "inclusive": 0.315012420527637, + "exclusive": 0.315012420527637 + }, + "RuntimeCallableInvocation.call | function=openhcs.processing.backends.cellprofiler.intensity.rescale_intensity;view=1": { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.intensity.rescale_intensity;view=1", + "phase": "declared_callable_boundary", + "calls": 2, + "inclusive": 0.019795356784015894, + "exclusive": 0.019795356784015894 + }, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.intensity.rescale_intensity": { + "owner": "CellProfilerFunctionContractExecutor.execute", + "dimensions": "function=openhcs.processing.backends.cellprofiler.intensity.rescale_intensity", + "phase": "execute", + "calls": 2, + "inclusive": 0.02052792627364397, + "exclusive": 0.0007325694896280766 + }, + "CellProfilerOutputRecorder.record_module_outputs": { + "owner": "CellProfilerOutputRecorder.record_module_outputs", + "dimensions": "", + "phase": "record_module_outputs", + "calls": 26, + "inclusive": 0.3821194381453097, + "exclusive": 0.3821194381453097 + }, + "CellProfilerModuleExecutor.__call__": { + "owner": "CellProfilerModuleExecutor.__call__", + "dimensions": "", + "phase": "__call__", + "calls": 32, + "inclusive": 4.5760794640518725, + "exclusive": 0.17213439382612705 + }, + "FunctionCoreExecutor.execute | function=rescale_intensity": { + "owner": "FunctionCoreExecutor.execute", + "dimensions": "function=rescale_intensity", + "phase": "execute", + "calls": 2, + "inclusive": 0.15594422025606036, + "exclusive": 0.00037235114723443985 + }, + "PatternGroupRuntime._validate_and_unstack": { + "owner": "PatternGroupRuntime._validate_and_unstack", + "dimensions": "", + "phase": "_validate_and_unstack", + "calls": 28, + "inclusive": 0.22986850701272488, + "exclusive": 0.1967936889268458 + }, + "RuntimeImageStackCache.store": { + "owner": "RuntimeImageStackCache.store", + "dimensions": "", + "phase": "store", + "calls": 28, + "inclusive": 0.00023420248180627823, + "exclusive": 0.00023420248180627823 + }, + "PatternGroupRuntime._save_outputs": { + "owner": "PatternGroupRuntime._save_outputs", + "dimensions": "", + "phase": "_save_outputs", + "calls": 28, + "inclusive": 0.6679928456433117, + "exclusive": 0.6677586431615055 + }, + "finalize_function_step_outputs": { + "owner": "finalize_function_step_outputs", + "dimensions": "", + "phase": "finalize_function_step_outputs", + "calls": 30, + "inclusive": 0.7330579869449139, + "exclusive": 0.462262537330389 + }, + "RuntimeCallableInvocation.call | function=openhcs.processing.backends.cellprofiler.image_geometry.resize_volumetric;view=1": { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.image_geometry.resize_volumetric;view=1", + "phase": "declared_callable_boundary", + "calls": 3, + "inclusive": 0.12900878163054585, + "exclusive": 0.12900878163054585 + }, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.image_geometry.resize_volumetric": { + "owner": "CellProfilerFunctionContractExecutor.execute", + "dimensions": "function=openhcs.processing.backends.cellprofiler.image_geometry.resize_volumetric", + "phase": "execute", + "calls": 3, + "inclusive": 0.12965055974200368, + "exclusive": 0.0006417781114578247 + }, + "FunctionCoreExecutor.execute | function=resize_volumetric": { + "owner": "FunctionCoreExecutor.execute", + "dimensions": "function=resize_volumetric", + "phase": "execute", + "calls": 3, + "inclusive": 0.19211693992838264, + "exclusive": 0.0007436810992658138 + }, + "RuntimeCallableInvocation.call | function=openhcs.processing.backends.cellprofiler.median_filter.medianfilter;view=1": { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.median_filter.medianfilter;view=1", + "phase": "declared_callable_boundary", + "calls": 1, + "inclusive": 0.4724126742221415, + "exclusive": 0.4724126742221415 + }, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.median_filter.medianfilter": { + "owner": "CellProfilerFunctionContractExecutor.execute", + "dimensions": "function=openhcs.processing.backends.cellprofiler.median_filter.medianfilter", + "phase": "execute", + "calls": 1, + "inclusive": 0.47297679586336017, + "exclusive": 0.0005641216412186623 + }, + "FunctionCoreExecutor.execute | function=medianfilter": { + "owner": "FunctionCoreExecutor.execute", + "dimensions": "function=medianfilter", + "phase": "execute", + "calls": 1, + "inclusive": 0.4928359529003501, + "exclusive": 0.000936092808842659 + }, + "RuntimeCallableInvocation.call | function=openhcs.processing.backends.cellprofiler.thresholding.threshold;view=1": { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.thresholding.threshold;view=1", + "phase": "declared_callable_boundary", + "calls": 3, + "inclusive": 0.4487679689191282, + "exclusive": 0.4487679689191282 + }, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.thresholding.threshold": { + "owner": "CellProfilerFunctionContractExecutor.execute", + "dimensions": "function=openhcs.processing.backends.cellprofiler.thresholding.threshold", + "phase": "execute", + "calls": 3, + "inclusive": 0.44994965521618724, + "exclusive": 0.0011816862970590591 + }, + "FunctionCoreExecutor.execute | function=threshold": { + "owner": "FunctionCoreExecutor.execute", + "dimensions": "function=threshold", + "phase": "execute", + "calls": 3, + "inclusive": 0.5471520069986582, + "exclusive": 0.0035848570987582207 + }, + "RuntimeCallableInvocation.call | function=openhcs.processing.backends.cellprofiler.morphology.remove_holes_3d;view=1": { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.morphology.remove_holes_3d;view=1", + "phase": "declared_callable_boundary", + "calls": 2, + "inclusive": 0.09644329408183694, + "exclusive": 0.09644329408183694 + }, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.morphology.remove_holes_3d": { + "owner": "CellProfilerFunctionContractExecutor.execute", + "dimensions": "function=openhcs.processing.backends.cellprofiler.morphology.remove_holes_3d", + "phase": "execute", + "calls": 2, + "inclusive": 0.09670406440272927, + "exclusive": 0.000260770320892334 + }, + "FunctionCoreExecutor.execute | function=remove_holes_3d": { + "owner": "FunctionCoreExecutor.execute", + "dimensions": "function=remove_holes_3d", + "phase": "execute", + "calls": 2, + "inclusive": 0.10615720180794597, + "exclusive": 0.0004928689450025558 + }, + "RuntimeCallableInvocation.call | function=openhcs.processing.backends.cellprofiler.watershed.watershed_cellprofiler4;view=1": { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.watershed.watershed_cellprofiler4;view=1", + "phase": "declared_callable_boundary", + "calls": 2, + "inclusive": 0.49590820586308837, + "exclusive": 0.49590820586308837 + }, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.watershed.watershed_cellprofiler4": { + "owner": "CellProfilerFunctionContractExecutor.execute", + "dimensions": "function=openhcs.processing.backends.cellprofiler.watershed.watershed_cellprofiler4", + "phase": "execute", + "calls": 2, + "inclusive": 0.49647813895717263, + "exclusive": 0.0005699330940842628 + }, + "FunctionCoreExecutor.execute | function=watershed_cellprofiler4": { + "owner": "FunctionCoreExecutor.execute", + "dimensions": "function=watershed_cellprofiler4", + "phase": "execute", + "calls": 2, + "inclusive": 0.5379135445691645, + "exclusive": 0.002366554457694292 + }, + "RuntimeCallableInvocation.call | function=openhcs.processing.backends.cellprofiler.morphology.resize_objects_3d;view=1": { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.morphology.resize_objects_3d;view=1", + "phase": "declared_callable_boundary", + "calls": 2, + "inclusive": 0.07325948867946863, + "exclusive": 0.07325948867946863 + }, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.morphology.resize_objects_3d": { + "owner": "CellProfilerFunctionContractExecutor.execute", + "dimensions": "function=openhcs.processing.backends.cellprofiler.morphology.resize_objects_3d", + "phase": "execute", + "calls": 2, + "inclusive": 0.07387770200148225, + "exclusive": 0.0006182133220136166 + }, + "FunctionCoreExecutor.execute | function=resize_objects_3d": { + "owner": "FunctionCoreExecutor.execute", + "dimensions": "function=resize_objects_3d", + "phase": "execute", + "calls": 2, + "inclusive": 0.10657328553497791, + "exclusive": 0.0009544077329337597 + }, + "RuntimeCallableInvocation.call | function=openhcs.processing.backends.cellprofiler.morphology.erode_objects;view=1": { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.morphology.erode_objects;view=1", + "phase": "declared_callable_boundary", + "calls": 1, + "inclusive": 0.04853436117991805, + "exclusive": 0.04853436117991805 + }, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.morphology.erode_objects": { + "owner": "CellProfilerFunctionContractExecutor.execute", + "dimensions": "function=openhcs.processing.backends.cellprofiler.morphology.erode_objects", + "phase": "execute", + "calls": 1, + "inclusive": 0.04904478183016181, + "exclusive": 0.0005104206502437592 + }, + "FunctionCoreExecutor.execute | function=erode_objects": { + "owner": "FunctionCoreExecutor.execute", + "dimensions": "function=erode_objects", + "phase": "execute", + "calls": 1, + "inclusive": 0.058804776053875685, + "exclusive": 0.00045938510447740555 + }, + "RuntimeCallableInvocation.call | function=openhcs.processing.backends.cellprofiler.object_images.convert_objects_to_image;view=1": { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.object_images.convert_objects_to_image;view=1", + "phase": "declared_callable_boundary", + "calls": 3, + "inclusive": 0.007222956977784634, + "exclusive": 0.007222956977784634 + }, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.object_images.convert_objects_to_image": { + "owner": "CellProfilerFunctionContractExecutor.execute", + "dimensions": "function=openhcs.processing.backends.cellprofiler.object_images.convert_objects_to_image", + "phase": "execute", + "calls": 3, + "inclusive": 0.007992795202881098, + "exclusive": 0.0007698382250964642 + }, + "FunctionCoreExecutor.execute | function=convert_objects_to_image": { + "owner": "FunctionCoreExecutor.execute", + "dimensions": "function=convert_objects_to_image", + "phase": "execute", + "calls": 3, + "inclusive": 0.05049174604937434, + "exclusive": 0.001049565151333809 + }, + "RuntimeCallableInvocation.call | function=openhcs.processing.backends.cellprofiler.image_math.image_math;view=1": { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.image_math.image_math;view=1", + "phase": "declared_callable_boundary", + "calls": 120, + "inclusive": 0.05379005940631032, + "exclusive": 0.05379005940631032 + }, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.image_math.image_math": { + "owner": "CellProfilerFunctionContractExecutor.execute", + "dimensions": "function=openhcs.processing.backends.cellprofiler.image_math.image_math", + "phase": "execute", + "calls": 2, + "inclusive": 0.07124726008623838, + "exclusive": 0.017457200679928064 + }, + "FunctionCoreExecutor.execute | function=image_math": { + "owner": "FunctionCoreExecutor.execute", + "dimensions": "function=image_math", + "phase": "execute", + "calls": 2, + "inclusive": 0.30010355403646827, + "exclusive": 0.0004685823805630207 + }, + "RuntimeCallableInvocation.call | function=openhcs.processing.backends.cellprofiler.morphology.closing;view=1": { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.morphology.closing;view=1", + "phase": "declared_callable_boundary", + "calls": 1, + "inclusive": 0.2624981193803251, + "exclusive": 0.2624981193803251 + }, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.morphology.closing": { + "owner": "CellProfilerFunctionContractExecutor.execute", + "dimensions": "function=openhcs.processing.backends.cellprofiler.morphology.closing", + "phase": "execute", + "calls": 1, + "inclusive": 0.2625978058204055, + "exclusive": 9.968644008040428e-05 + }, + "FunctionCoreExecutor.execute | function=closing": { + "owner": "FunctionCoreExecutor.execute", + "dimensions": "function=closing", + "phase": "execute", + "calls": 1, + "inclusive": 0.2876937221735716, + "exclusive": 0.00026171188801527023 + }, + "RuntimeCallableInvocation.call | function=openhcs.processing.backends.cellprofiler.image_geometry.mask_image;view=1": { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.image_geometry.mask_image;view=1", + "phase": "declared_callable_boundary", + "calls": 60, + "inclusive": 0.015584377571940422, + "exclusive": 0.015584377571940422 + }, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.image_geometry.mask_image": { + "owner": "CellProfilerFunctionContractExecutor.execute", + "dimensions": "function=openhcs.processing.backends.cellprofiler.image_geometry.mask_image", + "phase": "execute", + "calls": 1, + "inclusive": 0.03287788573652506, + "exclusive": 0.017293508164584637 + }, + "FunctionCoreExecutor.execute | function=mask_image": { + "owner": "FunctionCoreExecutor.execute", + "dimensions": "function=mask_image", + "phase": "execute", + "calls": 1, + "inclusive": 0.05533677665516734, + "exclusive": 0.0003833817318081856 + }, + "RuntimeCallableInvocation.call | function=openhcs.processing.backends.cellprofiler.morphology.erode_image;view=1": { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.morphology.erode_image;view=1", + "phase": "declared_callable_boundary", + "calls": 1, + "inclusive": 0.03985383827239275, + "exclusive": 0.03985383827239275 + }, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.morphology.erode_image": { + "owner": "CellProfilerFunctionContractExecutor.execute", + "dimensions": "function=openhcs.processing.backends.cellprofiler.morphology.erode_image", + "phase": "execute", + "calls": 1, + "inclusive": 0.03997136000543833, + "exclusive": 0.000117521733045578 + }, + "FunctionCoreExecutor.execute | function=erode_image": { + "owner": "FunctionCoreExecutor.execute", + "dimensions": "function=erode_image", + "phase": "execute", + "calls": 1, + "inclusive": 0.044346907176077366, + "exclusive": 0.00028765155002474785 + }, + "RuntimeCallableInvocation.call | function=openhcs.processing.backends.cellprofiler.intensity.measure_object_intensity;view=1": { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.intensity.measure_object_intensity;view=1", + "phase": "declared_callable_boundary", + "calls": 4, + "inclusive": 0.7575583099387586, + "exclusive": 0.7575583099387586 + }, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.intensity.measure_object_intensity": { + "owner": "CellProfilerFunctionContractExecutor.execute", + "dimensions": "function=openhcs.processing.backends.cellprofiler.intensity.measure_object_intensity", + "phase": "execute", + "calls": 4, + "inclusive": 0.7582870102487504, + "exclusive": 0.0007287003099918365 + }, + "ObjectMeasurementOutputRecorder.record": { + "owner": "ObjectMeasurementOutputRecorder.record", + "dimensions": "", + "phase": "record", + "calls": 6, + "inclusive": 0.0037890542298555374, + "exclusive": 0.0037890542298555374 + }, + "FunctionCoreExecutor.execute | function=measure_object_intensity": { + "owner": "FunctionCoreExecutor.execute", + "dimensions": "function=measure_object_intensity", + "phase": "execute", + "calls": 4, + "inclusive": 0.8959525804966688, + "exclusive": 0.003980354871600866 + }, + "RuntimeCallableInvocation.call | function=openhcs.processing.backends.cellprofiler.shape.measure_object_size_shape;view=1": { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.shape.measure_object_size_shape;view=1", + "phase": "declared_callable_boundary", + "calls": 2, + "inclusive": 0.5079499268904328, + "exclusive": 0.5079499268904328 + }, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.shape.measure_object_size_shape": { + "owner": "CellProfilerFunctionContractExecutor.execute", + "dimensions": "function=openhcs.processing.backends.cellprofiler.shape.measure_object_size_shape", + "phase": "execute", + "calls": 2, + "inclusive": 0.5088439187966287, + "exclusive": 0.000893991906195879 + }, + "FunctionCoreExecutor.execute | function=measure_object_size_shape": { + "owner": "FunctionCoreExecutor.execute", + "dimensions": "function=measure_object_size_shape", + "phase": "execute", + "calls": 2, + "inclusive": 0.5192161961458623, + "exclusive": 0.0021115639247000217 + }, + "RuntimeCallableInvocation.call | function=openhcs.processing.backends.cellprofiler.outlines.overlay_objects;view=1": { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.outlines.overlay_objects;view=1", + "phase": "declared_callable_boundary", + "calls": 2, + "inclusive": 0.2317779059521854, + "exclusive": 0.2317779059521854 + }, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.outlines.overlay_objects": { + "owner": "CellProfilerFunctionContractExecutor.execute", + "dimensions": "function=openhcs.processing.backends.cellprofiler.outlines.overlay_objects", + "phase": "execute", + "calls": 2, + "inclusive": 0.23199649713933468, + "exclusive": 0.00021859118714928627 + }, + "FunctionCoreExecutor.execute | function=overlay_objects": { + "owner": "FunctionCoreExecutor.execute", + "dimensions": "function=overlay_objects", + "phase": "execute", + "calls": 2, + "inclusive": 0.2562795919366181, + "exclusive": 0.01238652877509594 + }, + "RuntimeCallableInvocation.call | function=openhcs.processing.backends.cellprofiler.save_images.save_images;view=1": { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.save_images.save_images;view=1", + "phase": "declared_callable_boundary", + "calls": 2, + "inclusive": 0.00015609385445713997, + "exclusive": 0.00015609385445713997 + }, + "FunctionCoreExecutor.execute | function=save_images": { + "owner": "FunctionCoreExecutor.execute", + "dimensions": "function=save_images", + "phase": "execute", + "calls": 2, + "inclusive": 0.17327799275517464, + "exclusive": 0.1731218989007175 + }, + "AtomicMetadataWriter.publish_source_projection_metadata": { + "owner": "AtomicMetadataWriter.publish_source_projection_metadata", + "dimensions": "", + "phase": "publish_source_projection_metadata", + "calls": 5, + "inclusive": 0.5022010854445398, + "exclusive": 0.5022010854445398 + }, + "execute_worker_lane | {\"execution_id\": \"37e4bb4a-7f5d-463a-a17f-ff8ff7dee0f9\", \"kind\": \"worker_lane\", \"plate\": \"/home/ts/.cache/openhcs/benchmark_datasets/CellProfiler_tutorials/data/3d_monolayer/images\", \"worker_slot\": \"worker_0\"}": { + "owner": "execute_worker_lane", + "dimensions": "{\"execution_id\": \"37e4bb4a-7f5d-463a-a17f-ff8ff7dee0f9\", \"kind\": \"worker_lane\", \"plate\": \"/home/ts/.cache/openhcs/benchmark_datasets/CellProfiler_tutorials/data/3d_monolayer/images\", \"worker_slot\": \"worker_0\"}", + "phase": "execute_worker_lane", + "calls": 1, + "inclusive": 7.710202364251018, + "exclusive": 0.5019059842452407 + }, + "consolidate_analysis_outputs": { + "owner": "consolidate_analysis_outputs", + "dimensions": "", + "phase": "consolidate_analysis_outputs", + "calls": 1, + "inclusive": 1.0580290108919144e-05, + "exclusive": 1.0580290108919144e-05 + }, + "RuntimeArtifactMetadataTarget.reconciliation_targets": { + "owner": "RuntimeArtifactMetadataTarget.reconciliation_targets", + "dimensions": "", + "phase": "reconciliation_targets", + "calls": 1, + "inclusive": 0.29502451280131936, + "exclusive": 0.29502451280131936 + }, + "OpenHCSMetadataWriter.finalize_completed_plate": { + "owner": "OpenHCSMetadataWriter.finalize_completed_plate", + "dimensions": "", + "phase": "finalize_completed_plate", + "calls": 1, + "inclusive": 0.5092654940672219, + "exclusive": 0.008654322475194931 + }, + "ZMQExecutionServer._export_runtime_observation": { + "owner": "ZMQExecutionServer._export_runtime_observation", + "dimensions": "", + "phase": "_export_runtime_observation", + "calls": 1, + "inclusive": 0.012431904673576355, + "exclusive": 0.012431904673576355 + }, + "ZMQExecutionServer._execute_with_orchestrator | {\"case\": \"3d_monolayer_final\", \"compile_artifact_id\": \"8aba2e39-4db0-4608-bf23-08568aef528e\", \"execution_id\": \"37e4bb4a-7f5d-463a-a17f-ff8ff7dee0f9\", \"kind\": \"execution\", \"plate\": \"/home/ts/.cache/openhcs/benchmark_datasets/CellProfiler_tutorials/data/3d_monolayer/images\"}": { + "owner": "ZMQExecutionServer._execute_with_orchestrator", + "dimensions": "{\"case\": \"3d_monolayer_final\", \"compile_artifact_id\": \"8aba2e39-4db0-4608-bf23-08568aef528e\", \"execution_id\": \"37e4bb4a-7f5d-463a-a17f-ff8ff7dee0f9\", \"kind\": \"execution\", \"plate\": \"/home/ts/.cache/openhcs/benchmark_datasets/CellProfiler_tutorials/data/3d_monolayer/images\"}", + "phase": "_execute_with_orchestrator", + "calls": 1, + "inclusive": 8.383970148861408, + "exclusive": 0.044967072550207376 + } + }, + "hooks_installed": [ + { + "module": "objectstate.object_state", + "owner": "ObjectState", + "method": "__init__", + "descriptor_kind": "function" + }, + { + "module": "objectstate.object_state", + "owner": "ObjectState", + "method": "_compute_resolved_snapshot", + "descriptor_kind": "function" + }, + { + "module": "objectstate.object_state", + "owner": "ObjectState", + "method": "_reconstruct_from_resolved", + "descriptor_kind": "function" + }, + { + "module": "objectstate.object_state", + "owner": "ObjectState", + "method": "to_saved_resolved_object", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.aligned_image_payload", + "owner": "", + "method": "stack_image_payload_context", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.aligned_image_payload", + "owner": "", + "method": "unstack_image_payload_context", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.measurement_feature_queries", + "owner": "MeasurementFeatureValueIndex", + "method": "from_columnar_table_by_object", + "descriptor_kind": "classmethod" + }, + { + "module": "openhcs.core.virtual_workspace_metadata", + "owner": "AtomicMetadataWriter", + "method": "publish_source_projection_metadata", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.runtime_stack_cache", + "owner": "RuntimeImageStackCache", + "method": "get", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.runtime_stack_cache", + "owner": "RuntimeImageStackCache", + "method": "store", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.steps.function_outputs", + "owner": "", + "method": "finalize_function_step_outputs", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.steps.function_outputs", + "owner": "OpenHCSMetadataWriter", + "method": "finalize_completed_plate", + "descriptor_kind": "classmethod" + }, + { + "module": "openhcs.core.steps.function_outputs", + "owner": "RuntimeArtifactMetadataTarget", + "method": "reconciliation_targets", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.steps.function_runtime", + "owner": "FunctionCoreExecutor", + "method": "execute", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.steps.function_runtime", + "owner": "PatternGroupRuntime", + "method": "_load_input_stack", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.steps.function_runtime", + "owner": "PatternGroupRuntime", + "method": "_validate_and_unstack", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.steps.function_runtime", + "owner": "PatternGroupRuntime", + "method": "_save_outputs", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.pipeline.compilation_session", + "owner": "CompilationSession", + "method": "from_context", + "descriptor_kind": "classmethod" + }, + { + "module": "openhcs.core.pipeline.compiler", + "owner": "PipelineCompiler", + "method": "compile_pipelines", + "descriptor_kind": "staticmethod" + }, + { + "module": "openhcs.core.pipeline.compiler", + "owner": "PipelineCompiler", + "method": "_register_and_resolve_pipeline_once", + "descriptor_kind": "staticmethod" + }, + { + "module": "openhcs.core.pipeline.compiler", + "owner": "PipelineCompiler", + "method": "initialize_step_plans_for_context", + "descriptor_kind": "staticmethod" + }, + { + "module": "openhcs.core.pipeline.compiler", + "owner": "PipelineCompiler", + "method": "_capture_pipeline_config", + "descriptor_kind": "staticmethod" + }, + { + "module": "openhcs.core.pipeline.compiler", + "owner": "PipelineCompiler", + "method": "_resolve_global_step_axis_filters", + "descriptor_kind": "staticmethod" + }, + { + "module": "openhcs.core.pipeline.compiler", + "owner": "PipelineCompiler", + "method": "_configure_input_conversion_if_needed", + "descriptor_kind": "staticmethod" + }, + { + "module": "openhcs.core.pipeline.compiler", + "owner": "PipelineCompiler", + "method": "declare_zarr_stores", + "descriptor_kind": "staticmethod" + }, + { + "module": "openhcs.core.pipeline.compiler", + "owner": "PipelineCompiler", + "method": "_finalize_compilation", + "descriptor_kind": "staticmethod" + }, + { + "module": "openhcs.core.pipeline.compiler", + "owner": "PipelineCompiler", + "method": "_axis_values_to_process", + "descriptor_kind": "staticmethod" + }, + { + "module": "openhcs.core.pipeline.compiler", + "owner": "AxisCompilationRequest", + "method": "context_for", + "descriptor_kind": "function" + }, + { + "module": "openhcs.processing.backends.lib_registry.unified_registry", + "owner": "RuntimeCallableInvocation", + "method": "call", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.orchestrator.analysis_consolidation", + "owner": "", + "method": "consolidate_analysis_outputs", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.orchestrator.worker_execution", + "owner": "", + "method": "execute_worker_lane", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.orchestrator.orchestrator", + "owner": "PipelineOrchestrator", + "method": "create_context", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.orchestrator.orchestrator", + "owner": "PipelineOrchestrator", + "method": "get_effective_config", + "descriptor_kind": "function" + }, + { + "module": "openhcs.runtime.zmq_execution_server", + "owner": "ZMQExecutionServer", + "method": "_execute_with_orchestrator", + "descriptor_kind": "function" + }, + { + "module": "openhcs.runtime.zmq_execution_server", + "owner": "ZMQExecutionServer", + "method": "_initialize_orchestrator", + "descriptor_kind": "function" + }, + { + "module": "openhcs.runtime.zmq_execution_server", + "owner": "ZMQExecutionServer", + "method": "_export_runtime_observation", + "descriptor_kind": "function" + }, + { + "module": "openhcs.interop.cellprofiler.runtime.function_contract_execution", + "owner": "CellProfilerFunctionContractExecutor", + "method": "execute", + "descriptor_kind": "function" + }, + { + "module": "openhcs.interop.cellprofiler.runtime.measurement_execution_support", + "owner": "ObjectMeasurementOutputRecorder", + "method": "record", + "descriptor_kind": "function" + }, + { + "module": "openhcs.interop.cellprofiler.runtime.output_recording", + "owner": "CellProfilerOutputRecorder", + "method": "record_module_outputs", + "descriptor_kind": "classmethod" + }, + { + "module": "openhcs.interop.cellprofiler.runtime.module_execution", + "owner": "CellProfilerModuleExecutor", + "method": "__call__", + "descriptor_kind": "function" + }, + { + "module": "openhcs.interop.cellprofiler.runtime.module_execution", + "owner": "CellProfilerModuleExecutor", + "method": "_image_request", + "descriptor_kind": "function" + } + ], + "profiling_enabled": false, + "runtime_profiling_enabled": false, + "diagnostic_errors": [], + "captures": false, + "scope": "coarse same-run nested exclusive timing partition on the exact source revision recorded in this region. Diagnostic wrappers, no scientific or memory-observer substitutions. Not accepted performance comparison. Declared callable boundary includes filtering/decorated processing/metadata; not pure numerical kernels. Both existing profilers are disabled. These wrappers still add diagnostic overhead; no scaling to uninstrumented clocks or causal performance claims. Compiler/state boundaries and query boundary are nested under actual parents and excluded from parent-exclusive elapsed. This is a NEW 41-hook diagnostic version; inherited 24 boundary labels remain, site bytes are changed. ObjectState is verified against the actual source dependency path, not assumed inside the OpenHCS package." +} diff --git a/benchmark/results/perf_compile_runtime_ledger_20261003/observations.json b/benchmark/results/perf_compile_runtime_ledger_20261003/observations.json new file mode 100644 index 0000000000..5cb4bc8ef7 --- /dev/null +++ b/benchmark/results/perf_compile_runtime_ledger_20261003/observations.json @@ -0,0 +1,63 @@ +{ + "returncode": 0, + "diagnostic_admission": true, + "public_rows": [ + { + "case_name": "cp_tutorial_3d_monolayer", + "mode_name": "1w_1t", + "worker_count": "1", + "well_count": "1", + "compile_seconds": "1.495826244354248", + "prepare_seconds": "0.0", + "execute_seconds": "8.396726846694946", + "total_seconds": "10.694032318890095", + "wells_per_second": "0.11909402535746559", + "successful_wells": "1", + "native_single_sample_execution_seconds": "", + "projected_native_execution_seconds": "", + "projected_execution_speedup": "", + "peak_memory_mb": "1658.1875", + "status": "success", + "memory_limit_mb": "", + "error_message": "", + "execution_route": "ordinary-zmq-outcomes-v1", + "server_lifecycle": "reused-per-sweep", + "run_input_sha256": "7f66dd478c9a56033495f056335a0eaadb9fd5facd953b504f0e1f8f7eaa57d5" + } + ], + "regions": [ + { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/compiler-config-coarse-ledger-v1/output/regions/region-1948899-139659153200832-0001.json", + "sha256": "6b604c99652c36aba1ce3eff39b65ccd73b71d17ca827c121a98676b4608c687" + }, + { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/compiler-config-coarse-ledger-v1/output/regions/region-1948899-139659153200832-0002.json", + "sha256": "ea2b9668eb40dc72c8af0ed9a6d55250fcd46721db5c5554fd0ea3c82ff738dd" + } + ], + "root_sequence_complete": true, + "actual_index_calls": 0, + "actual_declared_calls": 211, + "compile_only_actual_calls": { + "PipelineCompiler.compile_pipelines": 1, + "PipelineCompiler._register_and_resolve_pipeline_once": 1, + "PipelineCompiler._capture_pipeline_config": 1, + "AxisCompilationRequest.context_for": 1, + "CompilationSession.from_context": 1, + "PipelineOrchestrator.create_context": 3, + "PipelineOrchestrator.get_effective_config": 10, + "ObjectState.__init__": 44, + "ObjectState._compute_resolved_snapshot": 88, + "ObjectState._reconstruct_from_resolved": 532, + "ObjectState.to_saved_resolved_object": 42 + }, + "all_required_compiler_boundaries_reached": true, + "all41_hooks_installed": true, + "retained_json_bytes": 51356, + "generated_output_bytes": 18189886, + "public_log_bytes": 541, + "profiler_artifacts_absent": true, + "source_env_native_input_freeze": "PASS", + "science": "NOT_RUN: original full saved-output126file/schema/discrete/numerical/nativeinput witness separate gate. No exclusions or missing outputs waived.", + "scope": "One source-pinned ordinary3D Monolayer1w1t compiler/config diagnostic with NEW41 coarse nominal hooks: inherited24 plus17 compiler/state boundaries. Site is versioned/changed, not byte-identical. cProfile and runtime profiler OFF, no captures/percell hooks. Existing defaultOUTCOMES/memory observers/READY clocks unchanged. Same-thread nested exclusive clocks exclude query from its real ancestors; worker independent roots are not added to parent waits. Nearordinary diagnostic only; wrapper overhead remains, no scale factors/causal performance promotion. Full saved126file (6CSV120TIFF) science/native source witness independently required." +} diff --git a/benchmark/results/perf_compile_runtime_ledger_20261003/registration-owner-analysis.json b/benchmark/results/perf_compile_runtime_ledger_20261003/registration-owner-analysis.json new file mode 100644 index 0000000000..1a22d65121 --- /dev/null +++ b/benchmark/results/perf_compile_runtime_ledger_20261003/registration-owner-analysis.json @@ -0,0 +1,233 @@ +{ + "status": "CURRENT_DIAGNOSTIC_SCIENCE_PASS_READ_ONLY_OWNER_MAP", + "source_revision": "480aed49b30448de4380af5c9274e48084e67b63", + "source_unchanged_against_original_NRA_census": true, + "NRA_census": { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/compiler-config-transaction-map-v1/census.json", + "sha256": "60541e7bc14f20f6e9d698ac25ea6f094b5503a0670df2f893d83bb5564d3868", + "modules": 731, + "original_classes": 5302, + "selected_families": 67, + "all_source_hashes_revalidated": 731 + }, + "science_receipt": { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/compiler-config-coarse-ledger-v1/science.json", + "sha256": "d19e5294a829cf98a0d0730b2729224e8a6947f258641a6f0923dfc6af732d74" + }, + "timing": { + "compile_only": { + "root_seconds": 1.2609371892176569, + "owners": { + "ObjectState._compute_resolved_snapshot": { + "owner": "ObjectState._compute_resolved_snapshot", + "dimensions": "", + "phase": "compiler_config:_compute_resolved_snapshot", + "calls": 88, + "inclusive": 0.4525698209181428, + "exclusive": 0.4525698209181428 + }, + "ObjectState.__init__": { + "owner": "ObjectState.__init__", + "dimensions": "", + "phase": "compiler_config:__init__", + "calls": 44, + "inclusive": 0.6144807459786534, + "exclusive": 0.16191092506051064 + }, + "ObjectState._reconstruct_from_resolved": { + "owner": "ObjectState._reconstruct_from_resolved", + "dimensions": "", + "phase": "compiler_config:_reconstruct_from_resolved", + "calls": 532, + "inclusive": 0.1099232928827405, + "exclusive": 0.05649412330240011 + }, + "ObjectState.to_saved_resolved_object": { + "owner": "ObjectState.to_saved_resolved_object", + "dimensions": "", + "phase": "compiler_config:to_saved_resolved_object", + "calls": 42, + "inclusive": 0.056609060149639845, + "exclusive": 0.00011493684723973274 + }, + "PipelineOrchestrator.get_effective_config": { + "owner": "PipelineOrchestrator.get_effective_config", + "dimensions": "", + "phase": "compiler_config:get_effective_config", + "calls": 10, + "inclusive": 0.19583785673603415, + "exclusive": 8.42963345348835e-05 + }, + "PipelineOrchestrator.create_context": { + "owner": "PipelineOrchestrator.create_context", + "dimensions": "", + "phase": "compiler_config:create_context", + "calls": 3, + "inclusive": 0.04134061839431524, + "exclusive": 0.0006397129036486149 + }, + "PipelineCompiler._axis_values_to_process": { + "owner": "PipelineCompiler._axis_values_to_process", + "dimensions": "", + "phase": "compiler_config:_axis_values_to_process", + "calls": 1, + "inclusive": 0.017245036084204912, + "exclusive": 6.654579192399979e-05 + }, + "PipelineCompiler._register_and_resolve_pipeline_once": { + "owner": "PipelineCompiler._register_and_resolve_pipeline_once", + "dimensions": "", + "phase": "compiler_config:_register_and_resolve_pipeline_once", + "calls": 1, + "inclusive": 0.47889699088409543, + "exclusive": 0.005476511083543301 + }, + "PipelineCompiler._capture_pipeline_config": { + "owner": "PipelineCompiler._capture_pipeline_config", + "dimensions": "", + "phase": "compiler_config:_capture_pipeline_config", + "calls": 1, + "inclusive": 0.0019184919074177742, + "exclusive": 2.7259811758995056e-06 + }, + "PipelineCompiler._resolve_global_step_axis_filters": { + "owner": "PipelineCompiler._resolve_global_step_axis_filters", + "dimensions": "", + "phase": "compiler_config:_resolve_global_step_axis_filters", + "calls": 1, + "inclusive": 0.015254999045282602, + "exclusive": 0.001242930069565773 + }, + "AxisCompilationRequest.context_for": { + "owner": "AxisCompilationRequest.context_for", + "dimensions": "", + "phase": "compiler_config:context_for", + "calls": 1, + "inclusive": 0.013713276013731956, + "exclusive": 0.00032007182016968727 + }, + "CompilationSession.from_context": { + "owner": "CompilationSession.from_context", + "dimensions": "", + "phase": "compiler_config:from_context", + "calls": 1, + "inclusive": 2.0298175513744354e-05, + "exclusive": 2.0298175513744354e-05 + }, + "PipelineCompiler._configure_input_conversion_if_needed": { + "owner": "PipelineCompiler._configure_input_conversion_if_needed", + "dimensions": "", + "phase": "compiler_config:_configure_input_conversion_if_needed", + "calls": 1, + "inclusive": 0.013645949773490429, + "exclusive": 5.200039595365524e-06 + }, + "PipelineCompiler.initialize_step_plans_for_context": { + "owner": "PipelineCompiler.initialize_step_plans_for_context", + "dimensions": "", + "phase": "compiler_config:initialize_step_plans_for_context", + "calls": 1, + "inclusive": 0.4779214742593467, + "exclusive": 0.46425522631034255 + }, + "PipelineCompiler.declare_zarr_stores": { + "owner": "PipelineCompiler.declare_zarr_stores", + "dimensions": "", + "phase": "compiler_config:declare_zarr_stores", + "calls": 1, + "inclusive": 0.018639610148966312, + "exclusive": 9.438721463084221e-05 + }, + "PipelineCompiler._finalize_compilation": { + "owner": "PipelineCompiler._finalize_compilation", + "dimensions": "", + "phase": "compiler_config:_finalize_compilation", + "calls": 1, + "inclusive": 0.016232328955084085, + "exclusive": 0.00027552898973226547 + }, + "PipelineCompiler.compile_pipelines": { + "owner": "PipelineCompiler.compile_pipelines", + "dimensions": "", + "phase": "compiler_config:compile_pipelines", + "calls": 1, + "inclusive": 1.094543606042862, + "exclusive": 0.05472139874473214 + } + }, + "state_disjoint_exclusive_seconds": 0.6710898061282933 + }, + "execution": { + "root_seconds": 8.383970148861408, + "owners": { + "ObjectState._compute_resolved_snapshot": { + "owner": "ObjectState._compute_resolved_snapshot", + "dimensions": "", + "phase": "compiler_config:_compute_resolved_snapshot", + "calls": 8, + "inclusive": 0.04228230891749263, + "exclusive": 0.04228230891749263 + }, + "ObjectState.__init__": { + "owner": "ObjectState.__init__", + "dimensions": "", + "phase": "compiler_config:__init__", + "calls": 4, + "inclusive": 0.05480403359979391, + "exclusive": 0.012521724682301283 + }, + "ObjectState._reconstruct_from_resolved": { + "owner": "ObjectState._reconstruct_from_resolved", + "dimensions": "", + "phase": "compiler_config:_reconstruct_from_resolved", + "calls": 92, + "inclusive": 0.013641910627484322, + "exclusive": 0.007105355150997639 + }, + "ObjectState.to_saved_resolved_object": { + "owner": "ObjectState.to_saved_resolved_object", + "dimensions": "", + "phase": "compiler_config:to_saved_resolved_object", + "calls": 4, + "inclusive": 0.007119361776858568, + "exclusive": 1.4006625860929489e-05 + }, + "PipelineOrchestrator.get_effective_config": { + "owner": "PipelineOrchestrator.get_effective_config", + "dimensions": "", + "phase": "compiler_config:get_effective_config", + "calls": 4, + "inclusive": 0.06195876281708479, + "exclusive": 3.5367440432310104e-05 + }, + "PipelineOrchestrator.create_context": { + "owner": "PipelineOrchestrator.create_context", + "dimensions": "", + "phase": "compiler_config:create_context", + "calls": 1, + "inclusive": 0.014066982083022594, + "exclusive": 6.845779716968536e-05 + } + }, + "state_disjoint_exclusive_seconds": 0.06192339537665248 + } + }, + "findings": [ + "44 constructor / 88 snapshots is exactly consistent with constructor initializing independent live and saved baselines; same count is not proof they have equivalent values/effects.", + "Constructor first computes live using current parameters, LIVE global context, and ancestor.to_object; it then detached-copies saved parameters and resolves SAVED global and saved ancestor extraction targets. Initial dirty/provenance/signature sets depend on both.", + "Compiler force_fresh registration is intentional protection from stale UI/post-compile stripped states. Every state is constructed before registry.register fires callbacks; skipping live setup changes observable state at callbacks.", + "Registry ancestral objects include current scope when old state is already registered. force_fresh constructor may therefore observe replaced old registry state before register overwrites it; not assumed empty/no-edits.", + "ObjectState.saved_object synchronizes delegate changes; ancestor live.to_object synchronizes and caches reconstruction; merged context dataclass constructors and validators are real effects. Compiler later uses to_saved_resolved_object but public registered state still has both baseline APIs.", + "Context manager extract_all_configs already owns operation cache per object ID, cleared at context changes. resolver collects layer configs once per single field, with extraction cached during unchanged stack. Do not invent an absent config extraction cache.", + "All10 effective config reads total .195838 inclusive, a subset of ObjectState .671090 disjoint owner work. Late context-only removal cannot claim whole registration lane.", + "Live-versus-saved snapshot split timing is not retained per-argument by41-hook diagnostic; half of .45257 is not an empirical removable bound. Whole snapshot .45257 is an absolute ceiling before mandatory saved resolution and live state laws.", + "Compiler-state work is a total-time opportunity only. Execution state owner work .061924 is separate and cannot bridge execution gap. No production optimization, cache, validation bypass, proxy, subtype switch or constructor mode introduced." + ], + "existing_law_controls": [ + "ObjectState/tests/test_global_config.py:test_dual_config_pattern", + "ObjectState/tests/test_construction_history.py:test_alias_graph_is_detached_and_restored_across_scopes_and_saved_live", + "ObjectState/tests/test_callable_baseline.py:test_initial_values_flatten_nested_dataclass_overrides", + "ObjectState/tests/test_lazy_factory.py:test_patch_lazy_constructors_does_not_hide_default_factory_failure" + ], + "next_admission": "Only if root selects this total-time dependency: a bounded actual registered state snapshot replay must compare both saved/live outputs, provenance/dirty states, independent mutable aliases, registration callbacks, stale same-scope state replacement, delegated object sync, exception/factory/validation ordering. Existing diagnostic does not authorize collapsing live/saved snapshots." +} diff --git a/benchmark/results/perf_compile_runtime_ledger_20261003/science.json b/benchmark/results/perf_compile_runtime_ledger_20261003/science.json new file mode 100644 index 0000000000..aa75fede4d --- /dev/null +++ b/benchmark/results/perf_compile_runtime_ledger_20261003/science.json @@ -0,0 +1,1085 @@ +{ + "status": "FULL6CSV120TIFF_AND180_PHYSICAL_SOURCE_REFS_PASS", + "validator_sha256": "efc570f4a6026079704335a8be989800427d2c7d9db3afc9379700c1121e998d", + "source_revision": "480aed49b30448de4380af5c9274e48084e67b63", + "diagnostic_gate": "PASS", + "science_does_not_waive_diagnostic_failure": true, + "original_scientific_comparison": { + "reference_root": "/var/tmp/openhcs-owned-metadata-90c30-ordinary-abba-20261002/0-main/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput", + "counts": { + "csv": 6, + "tiff": 120 + }, + "comparisons": [ + { + "name": "images/W001_s001_w0_z001_t001_CellsLabels.tiff", + "actual_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "reference_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w0_z002_t001_CellsLabels.tiff", + "actual_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "reference_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w0_z003_t001_CellsLabels.tiff", + "actual_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "reference_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w0_z004_t001_CellsLabels.tiff", + "actual_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "reference_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w0_z005_t001_CellsLabels.tiff", + "actual_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "reference_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w0_z006_t001_CellsLabels.tiff", + "actual_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "reference_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w0_z007_t001_CellsLabels.tiff", + "actual_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "reference_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w0_z008_t001_CellsLabels.tiff", + "actual_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "reference_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w0_z009_t001_CellsLabels.tiff", + "actual_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "reference_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w0_z010_t001_CellsLabels.tiff", + "actual_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "reference_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w0_z011_t001_CellsLabels.tiff", + "actual_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "reference_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w0_z012_t001_CellsLabels.tiff", + "actual_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "reference_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w0_z013_t001_CellsLabels.tiff", + "actual_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "reference_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w0_z014_t001_CellsLabels.tiff", + "actual_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "reference_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w0_z015_t001_CellsLabels.tiff", + "actual_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "reference_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w0_z016_t001_CellsLabels.tiff", + "actual_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "reference_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w0_z017_t001_CellsLabels.tiff", + "actual_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "reference_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w0_z018_t001_CellsLabels.tiff", + "actual_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "reference_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w0_z019_t001_CellsLabels.tiff", + "actual_sha256": "ec6ef1d319f2732c47b06f5cf02775c8cd94540f2f30a6c002f0aace1d317955", + "reference_sha256": "ec6ef1d319f2732c47b06f5cf02775c8cd94540f2f30a6c002f0aace1d317955", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w0_z020_t001_CellsLabels.tiff", + "actual_sha256": "e8105a4f04198ac75de9831247030084d6f65bf98691d7444ead7e178072fa22", + "reference_sha256": "e8105a4f04198ac75de9831247030084d6f65bf98691d7444ead7e178072fa22", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w0_z021_t001_CellsLabels.tiff", + "actual_sha256": "daa31cb1e537e5c32b1936ab7647c5690741d2bc1d0e15c60b59ba1310a8c0ce", + "reference_sha256": "daa31cb1e537e5c32b1936ab7647c5690741d2bc1d0e15c60b59ba1310a8c0ce", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w0_z022_t001_CellsLabels.tiff", + "actual_sha256": "7d7ec170ed3652efa99e43d8db546c51b696821dca7727d0ff4d2f2ead7fdbaa", + "reference_sha256": "7d7ec170ed3652efa99e43d8db546c51b696821dca7727d0ff4d2f2ead7fdbaa", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w0_z023_t001_CellsLabels.tiff", + "actual_sha256": "2f06989618050934c73473f9342c2fe3410fd834a0ddb976cf602fe6198db7a6", + "reference_sha256": "2f06989618050934c73473f9342c2fe3410fd834a0ddb976cf602fe6198db7a6", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w0_z024_t001_CellsLabels.tiff", + "actual_sha256": "39899adabca74b10b9950d1f97e8b0b3bbaf136d75e7a46ef4a6e86896e3119f", + "reference_sha256": "39899adabca74b10b9950d1f97e8b0b3bbaf136d75e7a46ef4a6e86896e3119f", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w0_z025_t001_CellsLabels.tiff", + "actual_sha256": "03aabe5528198cfd6ec23a058dab540fb8be0870207c6dc94a62d9b621d1f2f9", + "reference_sha256": "03aabe5528198cfd6ec23a058dab540fb8be0870207c6dc94a62d9b621d1f2f9", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w0_z026_t001_CellsLabels.tiff", + "actual_sha256": "5b433d931fa6637fc793b23880199c01000ef6d9e610903bb38bb8cea5574563", + "reference_sha256": "5b433d931fa6637fc793b23880199c01000ef6d9e610903bb38bb8cea5574563", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w0_z027_t001_CellsLabels.tiff", + "actual_sha256": "ee9056587a19e581f5feb6078b41fa1004d923a5ddf1725aefc4b8b8c5201ad6", + "reference_sha256": "ee9056587a19e581f5feb6078b41fa1004d923a5ddf1725aefc4b8b8c5201ad6", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w0_z028_t001_CellsLabels.tiff", + "actual_sha256": "fef31040e0c54501c3bf965eb98f055cdb1d42c96f91cf1c239033d0a9a06534", + "reference_sha256": "fef31040e0c54501c3bf965eb98f055cdb1d42c96f91cf1c239033d0a9a06534", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w0_z029_t001_CellsLabels.tiff", + "actual_sha256": "b3bdde5f751b8f5d41ca8b5219a3427e450cb1b8795401d0ec638940d1ad7bd9", + "reference_sha256": "b3bdde5f751b8f5d41ca8b5219a3427e450cb1b8795401d0ec638940d1ad7bd9", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w0_z030_t001_CellsLabels.tiff", + "actual_sha256": "b71a2e05d270ae0cd237d6c0d97ea82a63e5ad3633723e7255c35d34e585ec80", + "reference_sha256": "b71a2e05d270ae0cd237d6c0d97ea82a63e5ad3633723e7255c35d34e585ec80", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w0_z031_t001_CellsLabels.tiff", + "actual_sha256": "1b14aab71bf3623458ea9373852b0baf28be9727877b32f894407927ecfb6d65", + "reference_sha256": "1b14aab71bf3623458ea9373852b0baf28be9727877b32f894407927ecfb6d65", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w0_z032_t001_CellsLabels.tiff", + "actual_sha256": "8a2c5774044326ae722e41aae517266197fbebb0e4dd5eab4b22414fbfea309f", + "reference_sha256": "8a2c5774044326ae722e41aae517266197fbebb0e4dd5eab4b22414fbfea309f", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w0_z033_t001_CellsLabels.tiff", + "actual_sha256": "a320b99ee129d97b99f9fac66aabb89fc1652930b3bb8b2b3fbba7b57e994f85", + "reference_sha256": "a320b99ee129d97b99f9fac66aabb89fc1652930b3bb8b2b3fbba7b57e994f85", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w0_z034_t001_CellsLabels.tiff", + "actual_sha256": "3f6c1d8cdbce20aac8f70e7315bea70b147b7b6a034432845b27eeb6cfba083c", + "reference_sha256": "3f6c1d8cdbce20aac8f70e7315bea70b147b7b6a034432845b27eeb6cfba083c", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w0_z035_t001_CellsLabels.tiff", + "actual_sha256": "eedeb489b46abf1eafbd9c81491cb3f85ce1cb3ce877844ad22e72547fc3c438", + "reference_sha256": "eedeb489b46abf1eafbd9c81491cb3f85ce1cb3ce877844ad22e72547fc3c438", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w0_z036_t001_CellsLabels.tiff", + "actual_sha256": "28b68d1300cf7b08e21dbcd4858447862520dded5c27f2d84cf223513a119eed", + "reference_sha256": "28b68d1300cf7b08e21dbcd4858447862520dded5c27f2d84cf223513a119eed", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w0_z037_t001_CellsLabels.tiff", + "actual_sha256": "aa1f449f1769b27cd9f1076e70f49f0242e2549f288a77984ca159cb07cf6a40", + "reference_sha256": "aa1f449f1769b27cd9f1076e70f49f0242e2549f288a77984ca159cb07cf6a40", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w0_z038_t001_CellsLabels.tiff", + "actual_sha256": "6ac8fd9c95acc1f261e96a8fb9332e5d6a403548842f9b85ecb977e6213b4c9b", + "reference_sha256": "6ac8fd9c95acc1f261e96a8fb9332e5d6a403548842f9b85ecb977e6213b4c9b", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w0_z039_t001_CellsLabels.tiff", + "actual_sha256": "22fa79a76e64013f10373be2ca2af98a2173275c5d43c9cad33135acdc376d14", + "reference_sha256": "22fa79a76e64013f10373be2ca2af98a2173275c5d43c9cad33135acdc376d14", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w0_z040_t001_CellsLabels.tiff", + "actual_sha256": "1635ea20dd80ce99f0e86494666825a52a66645837d598fd3a3fa91f0bb77d53", + "reference_sha256": "1635ea20dd80ce99f0e86494666825a52a66645837d598fd3a3fa91f0bb77d53", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w0_z041_t001_CellsLabels.tiff", + "actual_sha256": "be7762756eb65f08cb337904165c6f43b5351f39405461f65ad28669abfe3899", + "reference_sha256": "be7762756eb65f08cb337904165c6f43b5351f39405461f65ad28669abfe3899", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w0_z042_t001_CellsLabels.tiff", + "actual_sha256": "88d2e9964455459499656c4f0a762cd86376c678756635aaa6b6c8d98be01852", + "reference_sha256": "88d2e9964455459499656c4f0a762cd86376c678756635aaa6b6c8d98be01852", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w0_z043_t001_CellsLabels.tiff", + "actual_sha256": "bd0df007524ea4e555e4d2fdbebe6a274caa03bc360a1cf90d601659155984b2", + "reference_sha256": "bd0df007524ea4e555e4d2fdbebe6a274caa03bc360a1cf90d601659155984b2", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w0_z044_t001_CellsLabels.tiff", + "actual_sha256": "e03946d94c789ee787cc29a3c8f5e5d9c56b37a0044b297ba8f403abc98eb55e", + "reference_sha256": "e03946d94c789ee787cc29a3c8f5e5d9c56b37a0044b297ba8f403abc98eb55e", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w0_z045_t001_CellsLabels.tiff", + "actual_sha256": "c678f2100c7431e5a43c391af86a79e3436d4cf13f050f98c25f62682e4632fe", + "reference_sha256": "c678f2100c7431e5a43c391af86a79e3436d4cf13f050f98c25f62682e4632fe", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w0_z046_t001_CellsLabels.tiff", + "actual_sha256": "dc081df050be6cd19b5d5bbfe21c96c350eecf9bc6f10f608dc361a36056632a", + "reference_sha256": "dc081df050be6cd19b5d5bbfe21c96c350eecf9bc6f10f608dc361a36056632a", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w0_z047_t001_CellsLabels.tiff", + "actual_sha256": "b926aa64e1fa8aa52279a69d91dc23cf14bf0ae76107f9b360c7ae77bdf53719", + "reference_sha256": "b926aa64e1fa8aa52279a69d91dc23cf14bf0ae76107f9b360c7ae77bdf53719", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w0_z048_t001_CellsLabels.tiff", + "actual_sha256": "133a58f6fac8b07f79bfed9634867ccb5428b83e63682031a1def16c6a694568", + "reference_sha256": "133a58f6fac8b07f79bfed9634867ccb5428b83e63682031a1def16c6a694568", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w0_z049_t001_CellsLabels.tiff", + "actual_sha256": "28d83dfbf5bbf7e4da9c3e4806353e9fd0a89680dcdab4b43ca2fd19b80f669c", + "reference_sha256": "28d83dfbf5bbf7e4da9c3e4806353e9fd0a89680dcdab4b43ca2fd19b80f669c", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w0_z050_t001_CellsLabels.tiff", + "actual_sha256": "ea7654a6fe1f92ad274a54935420923eec4bec5ad030187afc0ac7af02690af5", + "reference_sha256": "ea7654a6fe1f92ad274a54935420923eec4bec5ad030187afc0ac7af02690af5", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w0_z051_t001_CellsLabels.tiff", + "actual_sha256": "73483bd10a0f37990084013ad054768086d6d3c929da6bc7e74f4aee37e9dbf2", + "reference_sha256": "73483bd10a0f37990084013ad054768086d6d3c929da6bc7e74f4aee37e9dbf2", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w0_z052_t001_CellsLabels.tiff", + "actual_sha256": "7bb98f0587062e01b965e486fc687872651d7d90d271a2fdf0d35d67b464b233", + "reference_sha256": "7bb98f0587062e01b965e486fc687872651d7d90d271a2fdf0d35d67b464b233", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w0_z053_t001_CellsLabels.tiff", + "actual_sha256": "ff17f538baaf179752412026a250bdbee016e3101037bf4e0dbb81199fc4c8ed", + "reference_sha256": "ff17f538baaf179752412026a250bdbee016e3101037bf4e0dbb81199fc4c8ed", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w0_z054_t001_CellsLabels.tiff", + "actual_sha256": "25ff2ec586a568e42286f73713d64aab1208c38c016099299211f04368ac49c8", + "reference_sha256": "25ff2ec586a568e42286f73713d64aab1208c38c016099299211f04368ac49c8", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w0_z055_t001_CellsLabels.tiff", + "actual_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "reference_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w0_z056_t001_CellsLabels.tiff", + "actual_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "reference_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w0_z057_t001_CellsLabels.tiff", + "actual_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "reference_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w0_z058_t001_CellsLabels.tiff", + "actual_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "reference_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w0_z059_t001_CellsLabels.tiff", + "actual_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "reference_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w0_z060_t001_CellsLabels.tiff", + "actual_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "reference_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z001_t001_NucleiLabels.tiff", + "actual_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "reference_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z002_t001_NucleiLabels.tiff", + "actual_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "reference_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z003_t001_NucleiLabels.tiff", + "actual_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "reference_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z004_t001_NucleiLabels.tiff", + "actual_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "reference_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z005_t001_NucleiLabels.tiff", + "actual_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "reference_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z006_t001_NucleiLabels.tiff", + "actual_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "reference_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z007_t001_NucleiLabels.tiff", + "actual_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "reference_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z008_t001_NucleiLabels.tiff", + "actual_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "reference_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z009_t001_NucleiLabels.tiff", + "actual_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "reference_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z010_t001_NucleiLabels.tiff", + "actual_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "reference_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z011_t001_NucleiLabels.tiff", + "actual_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "reference_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z012_t001_NucleiLabels.tiff", + "actual_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "reference_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z013_t001_NucleiLabels.tiff", + "actual_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "reference_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z014_t001_NucleiLabels.tiff", + "actual_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "reference_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z015_t001_NucleiLabels.tiff", + "actual_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "reference_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z016_t001_NucleiLabels.tiff", + "actual_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "reference_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z017_t001_NucleiLabels.tiff", + "actual_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "reference_sha256": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z018_t001_NucleiLabels.tiff", + "actual_sha256": "03ba7a4d6ae53e6c84aab5926ae1e2592b5f8ca24a839a256c82f08544df4270", + "reference_sha256": "03ba7a4d6ae53e6c84aab5926ae1e2592b5f8ca24a839a256c82f08544df4270", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z019_t001_NucleiLabels.tiff", + "actual_sha256": "250cb56a8054c28cea894bc3d9edaddcdce01f5151a05da78da20157c3c3be9f", + "reference_sha256": "250cb56a8054c28cea894bc3d9edaddcdce01f5151a05da78da20157c3c3be9f", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z020_t001_NucleiLabels.tiff", + "actual_sha256": "b0cd031b448d8bd2075e65710bd76c064161b25242f85ee150da7cd139fcc1a8", + "reference_sha256": "b0cd031b448d8bd2075e65710bd76c064161b25242f85ee150da7cd139fcc1a8", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z021_t001_NucleiLabels.tiff", + "actual_sha256": "71a10b0539f3fc4cba600b275b7e948258c6a435ba33ef522faa0869ac085518", + "reference_sha256": "71a10b0539f3fc4cba600b275b7e948258c6a435ba33ef522faa0869ac085518", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z022_t001_NucleiLabels.tiff", + "actual_sha256": "50aef20e28a2f9e18ca2e28eabf6d87ec2ad20c79898e56f8d63a959ba5bb507", + "reference_sha256": "50aef20e28a2f9e18ca2e28eabf6d87ec2ad20c79898e56f8d63a959ba5bb507", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z023_t001_NucleiLabels.tiff", + "actual_sha256": "ecb7f78c8ca0bad2b9d0495659609b9023781b6a936d6afb0a04653eac176555", + "reference_sha256": "ecb7f78c8ca0bad2b9d0495659609b9023781b6a936d6afb0a04653eac176555", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z024_t001_NucleiLabels.tiff", + "actual_sha256": "9e96a05ed9860cac9c57c618db2f4fe5e4b769a9d235aac6fd1d628486317df0", + "reference_sha256": "9e96a05ed9860cac9c57c618db2f4fe5e4b769a9d235aac6fd1d628486317df0", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z025_t001_NucleiLabels.tiff", + "actual_sha256": "9cc33676a0177130609ab1331feab1722bbde8d55b2f49031730fd5a2a40029c", + "reference_sha256": "9cc33676a0177130609ab1331feab1722bbde8d55b2f49031730fd5a2a40029c", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z026_t001_NucleiLabels.tiff", + "actual_sha256": "ad22809ff3f43d15c838f697061c263070dbf1da1031da39900681cdfbf9d54c", + "reference_sha256": "ad22809ff3f43d15c838f697061c263070dbf1da1031da39900681cdfbf9d54c", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z027_t001_NucleiLabels.tiff", + "actual_sha256": "8c8f402d3a7c65c04e7f1575dd34de0427eeeb433ff2956d20e07d06eb48fb8d", + "reference_sha256": "8c8f402d3a7c65c04e7f1575dd34de0427eeeb433ff2956d20e07d06eb48fb8d", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z028_t001_NucleiLabels.tiff", + "actual_sha256": "1676f21bd3b7e105601c83f206890507c32ba2447988be2a75d0e8153d069b65", + "reference_sha256": "1676f21bd3b7e105601c83f206890507c32ba2447988be2a75d0e8153d069b65", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z029_t001_NucleiLabels.tiff", + "actual_sha256": "34bce0f06f674e20667554eed0075fde8d9d33c9c0eaeffd88a1c558499cc432", + "reference_sha256": "34bce0f06f674e20667554eed0075fde8d9d33c9c0eaeffd88a1c558499cc432", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z030_t001_NucleiLabels.tiff", + "actual_sha256": "c54c8b98a5c38a47e0cf9391a1afab09b4c8c58e06294cdb650a15add4a32b58", + "reference_sha256": "c54c8b98a5c38a47e0cf9391a1afab09b4c8c58e06294cdb650a15add4a32b58", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z031_t001_NucleiLabels.tiff", + "actual_sha256": "247490c261fecc26c7379ab2eebaa01e4bea523218b3cc5491d2fe2b9aadfe11", + "reference_sha256": "247490c261fecc26c7379ab2eebaa01e4bea523218b3cc5491d2fe2b9aadfe11", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z032_t001_NucleiLabels.tiff", + "actual_sha256": "97b09fe5444ad439c3c2007d75971dce2766d94c93268eccfee9ae49b8a04aa1", + "reference_sha256": "97b09fe5444ad439c3c2007d75971dce2766d94c93268eccfee9ae49b8a04aa1", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z033_t001_NucleiLabels.tiff", + "actual_sha256": "512a3a0ed8025d14b0556a72ebfc13b173d14f7046817ca09876242ab63b9d6b", + "reference_sha256": "512a3a0ed8025d14b0556a72ebfc13b173d14f7046817ca09876242ab63b9d6b", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z034_t001_NucleiLabels.tiff", + "actual_sha256": "08af61857cfb97fd95fcb08d1ce18139ce7087b192044c63452bee533c5b1aa4", + "reference_sha256": "08af61857cfb97fd95fcb08d1ce18139ce7087b192044c63452bee533c5b1aa4", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z035_t001_NucleiLabels.tiff", + "actual_sha256": "13e69afc324152bb91329fa758d131d849127e8fd346078d94c6adc25582b3a6", + "reference_sha256": "13e69afc324152bb91329fa758d131d849127e8fd346078d94c6adc25582b3a6", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z036_t001_NucleiLabels.tiff", + "actual_sha256": "4fa967c78709aa400246a50528a36b24ca366e825dd0e64561643369e814906b", + "reference_sha256": "4fa967c78709aa400246a50528a36b24ca366e825dd0e64561643369e814906b", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z037_t001_NucleiLabels.tiff", + "actual_sha256": "927d52441c57cb3a4014effaca3e3c23c76275fb264c26e1942a9c58b85ee6d2", + "reference_sha256": "927d52441c57cb3a4014effaca3e3c23c76275fb264c26e1942a9c58b85ee6d2", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z038_t001_NucleiLabels.tiff", + "actual_sha256": "e2852ea83c62ab30d77681b4e39f88d3cee74ae052f1c948675ae3581c9fc28a", + "reference_sha256": "e2852ea83c62ab30d77681b4e39f88d3cee74ae052f1c948675ae3581c9fc28a", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z039_t001_NucleiLabels.tiff", + "actual_sha256": "f93e8293a37886a36844b8748083a1f8e717efab092652755263e797f12f0364", + "reference_sha256": "f93e8293a37886a36844b8748083a1f8e717efab092652755263e797f12f0364", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z040_t001_NucleiLabels.tiff", + "actual_sha256": "e0bd66b166e9f92120dbee183973849eda0afbbe6fed9d03fc4cfd2c56a96280", + "reference_sha256": "e0bd66b166e9f92120dbee183973849eda0afbbe6fed9d03fc4cfd2c56a96280", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z041_t001_NucleiLabels.tiff", + "actual_sha256": "744eab7cd170d5af8d2f72ced3893dd939c4e1b0f415afab799a37ded5d046c7", + "reference_sha256": "744eab7cd170d5af8d2f72ced3893dd939c4e1b0f415afab799a37ded5d046c7", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z042_t001_NucleiLabels.tiff", + "actual_sha256": "e07252ca1a55c5195c99666d8d3f0d70db636b4ef4b99e1a0049ca22b182d87a", + "reference_sha256": "e07252ca1a55c5195c99666d8d3f0d70db636b4ef4b99e1a0049ca22b182d87a", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z043_t001_NucleiLabels.tiff", + "actual_sha256": "81a705d05b0a5bfa8783df28990c1d70634c1f4aa2bca23e02cfa8a4bc861b86", + "reference_sha256": "81a705d05b0a5bfa8783df28990c1d70634c1f4aa2bca23e02cfa8a4bc861b86", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z044_t001_NucleiLabels.tiff", + "actual_sha256": "a78264f2f5e245a3913a69f89105e94df30048b1aaef4eb80fc5c3235e78b196", + "reference_sha256": "a78264f2f5e245a3913a69f89105e94df30048b1aaef4eb80fc5c3235e78b196", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z045_t001_NucleiLabels.tiff", + "actual_sha256": "94bd6a65795ad6395cc18a4fd63dc591354bef612ee49fffd2d9f63e4ed9db84", + "reference_sha256": "94bd6a65795ad6395cc18a4fd63dc591354bef612ee49fffd2d9f63e4ed9db84", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z046_t001_NucleiLabels.tiff", + "actual_sha256": "c1250e060d32f2cfc07fc7e1594fdb1d6cbb4d0483fa94a8113efa695397963c", + "reference_sha256": "c1250e060d32f2cfc07fc7e1594fdb1d6cbb4d0483fa94a8113efa695397963c", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z047_t001_NucleiLabels.tiff", + "actual_sha256": "995d5ce77cbf61a9be8590f34987ef3f1dd698ae6ed393cac4d0960bc9e7a66a", + "reference_sha256": "995d5ce77cbf61a9be8590f34987ef3f1dd698ae6ed393cac4d0960bc9e7a66a", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z048_t001_NucleiLabels.tiff", + "actual_sha256": "325478230529e94ba372f38f0bf512292f131d337b6ac09ce5a91b0266724e02", + "reference_sha256": "325478230529e94ba372f38f0bf512292f131d337b6ac09ce5a91b0266724e02", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z049_t001_NucleiLabels.tiff", + "actual_sha256": "e92e493078672a0e0d90e31409612e215bf8486300d5c765d0e0504ff8888e7a", + "reference_sha256": "e92e493078672a0e0d90e31409612e215bf8486300d5c765d0e0504ff8888e7a", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z050_t001_NucleiLabels.tiff", + "actual_sha256": "565b4f08f7503086c1f2f8e631d19063129e02a5767d2f979c0cec8fdec23601", + "reference_sha256": "565b4f08f7503086c1f2f8e631d19063129e02a5767d2f979c0cec8fdec23601", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z051_t001_NucleiLabels.tiff", + "actual_sha256": "f0f36ccaf744d42da036948682cfd226e6dd4a3cfb94aed38729b2d25ede5dff", + "reference_sha256": "f0f36ccaf744d42da036948682cfd226e6dd4a3cfb94aed38729b2d25ede5dff", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z052_t001_NucleiLabels.tiff", + "actual_sha256": "0daf51526658e23b7c9ab8872e992ebe9322399286f95741d0ccdbd357e5d631", + "reference_sha256": "0daf51526658e23b7c9ab8872e992ebe9322399286f95741d0ccdbd357e5d631", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z053_t001_NucleiLabels.tiff", + "actual_sha256": "b8613192175a4615b58aed607474657668429cb6ac635cd012569b19667f2d3d", + "reference_sha256": "b8613192175a4615b58aed607474657668429cb6ac635cd012569b19667f2d3d", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z054_t001_NucleiLabels.tiff", + "actual_sha256": "b841c5d901c65a6138d9ff2b0ec4aa04f00765b1dd21bcd6fc9f8dc974c6035d", + "reference_sha256": "b841c5d901c65a6138d9ff2b0ec4aa04f00765b1dd21bcd6fc9f8dc974c6035d", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z055_t001_NucleiLabels.tiff", + "actual_sha256": "44e02416cc94a70528165a4b6c42b9e19b205f9c91528076796272cb1a8504ea", + "reference_sha256": "44e02416cc94a70528165a4b6c42b9e19b205f9c91528076796272cb1a8504ea", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z056_t001_NucleiLabels.tiff", + "actual_sha256": "ca0723c8b9764d5b77034719bb75a2940be35da2e04119af4c0e6d3552c08718", + "reference_sha256": "ca0723c8b9764d5b77034719bb75a2940be35da2e04119af4c0e6d3552c08718", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z057_t001_NucleiLabels.tiff", + "actual_sha256": "695517641e9fe4b8421d4a7db56b24c755b6957c98ce32bb24ad5b25f52661eb", + "reference_sha256": "695517641e9fe4b8421d4a7db56b24c755b6957c98ce32bb24ad5b25f52661eb", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z058_t001_NucleiLabels.tiff", + "actual_sha256": "a37c63c6052e40d73db918a014e6f2a18be4d0cbfbeeef65a9c26096a38a4c09", + "reference_sha256": "a37c63c6052e40d73db918a014e6f2a18be4d0cbfbeeef65a9c26096a38a4c09", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z059_t001_NucleiLabels.tiff", + "actual_sha256": "76c785cf0f481c73ad3cf34ccb576eaa19dd1bdd9c74f9eaf92adfbde5d44b97", + "reference_sha256": "76c785cf0f481c73ad3cf34ccb576eaa19dd1bdd9c74f9eaf92adfbde5d44b97", + "exact_scientific_values": true + }, + { + "name": "images/W001_s001_w2_z060_t001_NucleiLabels.tiff", + "actual_sha256": "7cc3c6c6a22f558e62194277090bdea2deb79475dd91fe7d0e49eac3c0187388", + "reference_sha256": "7cc3c6c6a22f558e62194277090bdea2deb79475dd91fe7d0e49eac3c0187388", + "exact_scientific_values": true + }, + { + "name": "results/3d_monolayer_Cells.csv", + "actual_sha256": "6f6e7431171ed5ea73a01784105147eca0e43a6e3e41f8720df93106353c9c79", + "reference_sha256": "6f6e7431171ed5ea73a01784105147eca0e43a6e3e41f8720df93106353c9c79", + "exact_scientific_values": true + }, + { + "name": "results/3d_monolayer_Image.csv", + "actual_sha256": "75ac32ff5ef57bc285ec4eae1c9b501c27a35b486bf35435af5d499d856d34a4", + "reference_sha256": "75ac32ff5ef57bc285ec4eae1c9b501c27a35b486bf35435af5d499d856d34a4", + "exact_scientific_values": true + }, + { + "name": "results/3d_monolayer_Nuclei.csv", + "actual_sha256": "17500f5d4321bbf255df31f7a3503339029c2b077045a6c10e4d4f6f0db9ad1e", + "reference_sha256": "17500f5d4321bbf255df31f7a3503339029c2b077045a6c10e4d4f6f0db9ad1e", + "exact_scientific_values": true + }, + { + "name": "results/3d_monolayer_downsizedNuclei.csv", + "actual_sha256": "a7b998c5a524e11747fc5452fd763fc3d9abe16f46e961f62e09e9393b6a8adf", + "reference_sha256": "a7b998c5a524e11747fc5452fd763fc3d9abe16f46e961f62e09e9393b6a8adf", + "exact_scientific_values": true + }, + { + "name": "results/3d_monolayer_erodedDownsizedNuclei.csv", + "actual_sha256": "ee507fb9c2387f86fc768184baf2596ebe1f8e40f7fc5eb24f252345fee63ae6", + "reference_sha256": "ee507fb9c2387f86fc768184baf2596ebe1f8e40f7fc5eb24f252345fee63ae6", + "exact_scientific_values": true + }, + { + "name": "results/3d_monolayer_erodedResizedNuclei.csv", + "actual_sha256": "cca23f86a85bb3ee7a5c3a1d174c34b5f026ca7f399be98458e1a8c6e12d5c77", + "reference_sha256": "cca23f86a85bb3ee7a5c3a1d174c34b5f026ca7f399be98458e1a8c6e12d5c77", + "exact_scientific_values": true + } + ], + "scope": "Exact saved CSV cells and TIFF dtype/shape/pixels against specified retained ordinary output. Not a current native ratio or new paired timing." + }, + "complete_inventory": { + "scientific_hashes": { + "images/W001_s001_w0_z001_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w0_z002_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w0_z003_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w0_z004_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w0_z005_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w0_z006_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w0_z007_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w0_z008_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w0_z009_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w0_z010_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w0_z011_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w0_z012_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w0_z013_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w0_z014_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w0_z015_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w0_z016_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w0_z017_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w0_z018_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w0_z019_t001_CellsLabels.tiff": "ec6ef1d319f2732c47b06f5cf02775c8cd94540f2f30a6c002f0aace1d317955", + "images/W001_s001_w0_z020_t001_CellsLabels.tiff": "e8105a4f04198ac75de9831247030084d6f65bf98691d7444ead7e178072fa22", + "images/W001_s001_w0_z021_t001_CellsLabels.tiff": "daa31cb1e537e5c32b1936ab7647c5690741d2bc1d0e15c60b59ba1310a8c0ce", + "images/W001_s001_w0_z022_t001_CellsLabels.tiff": "7d7ec170ed3652efa99e43d8db546c51b696821dca7727d0ff4d2f2ead7fdbaa", + "images/W001_s001_w0_z023_t001_CellsLabels.tiff": "2f06989618050934c73473f9342c2fe3410fd834a0ddb976cf602fe6198db7a6", + "images/W001_s001_w0_z024_t001_CellsLabels.tiff": "39899adabca74b10b9950d1f97e8b0b3bbaf136d75e7a46ef4a6e86896e3119f", + "images/W001_s001_w0_z025_t001_CellsLabels.tiff": "03aabe5528198cfd6ec23a058dab540fb8be0870207c6dc94a62d9b621d1f2f9", + "images/W001_s001_w0_z026_t001_CellsLabels.tiff": "5b433d931fa6637fc793b23880199c01000ef6d9e610903bb38bb8cea5574563", + "images/W001_s001_w0_z027_t001_CellsLabels.tiff": "ee9056587a19e581f5feb6078b41fa1004d923a5ddf1725aefc4b8b8c5201ad6", + "images/W001_s001_w0_z028_t001_CellsLabels.tiff": "fef31040e0c54501c3bf965eb98f055cdb1d42c96f91cf1c239033d0a9a06534", + "images/W001_s001_w0_z029_t001_CellsLabels.tiff": "b3bdde5f751b8f5d41ca8b5219a3427e450cb1b8795401d0ec638940d1ad7bd9", + "images/W001_s001_w0_z030_t001_CellsLabels.tiff": "b71a2e05d270ae0cd237d6c0d97ea82a63e5ad3633723e7255c35d34e585ec80", + "images/W001_s001_w0_z031_t001_CellsLabels.tiff": "1b14aab71bf3623458ea9373852b0baf28be9727877b32f894407927ecfb6d65", + "images/W001_s001_w0_z032_t001_CellsLabels.tiff": "8a2c5774044326ae722e41aae517266197fbebb0e4dd5eab4b22414fbfea309f", + "images/W001_s001_w0_z033_t001_CellsLabels.tiff": "a320b99ee129d97b99f9fac66aabb89fc1652930b3bb8b2b3fbba7b57e994f85", + "images/W001_s001_w0_z034_t001_CellsLabels.tiff": "3f6c1d8cdbce20aac8f70e7315bea70b147b7b6a034432845b27eeb6cfba083c", + "images/W001_s001_w0_z035_t001_CellsLabels.tiff": "eedeb489b46abf1eafbd9c81491cb3f85ce1cb3ce877844ad22e72547fc3c438", + "images/W001_s001_w0_z036_t001_CellsLabels.tiff": "28b68d1300cf7b08e21dbcd4858447862520dded5c27f2d84cf223513a119eed", + "images/W001_s001_w0_z037_t001_CellsLabels.tiff": "aa1f449f1769b27cd9f1076e70f49f0242e2549f288a77984ca159cb07cf6a40", + "images/W001_s001_w0_z038_t001_CellsLabels.tiff": "6ac8fd9c95acc1f261e96a8fb9332e5d6a403548842f9b85ecb977e6213b4c9b", + "images/W001_s001_w0_z039_t001_CellsLabels.tiff": "22fa79a76e64013f10373be2ca2af98a2173275c5d43c9cad33135acdc376d14", + "images/W001_s001_w0_z040_t001_CellsLabels.tiff": "1635ea20dd80ce99f0e86494666825a52a66645837d598fd3a3fa91f0bb77d53", + "images/W001_s001_w0_z041_t001_CellsLabels.tiff": "be7762756eb65f08cb337904165c6f43b5351f39405461f65ad28669abfe3899", + "images/W001_s001_w0_z042_t001_CellsLabels.tiff": "88d2e9964455459499656c4f0a762cd86376c678756635aaa6b6c8d98be01852", + "images/W001_s001_w0_z043_t001_CellsLabels.tiff": "bd0df007524ea4e555e4d2fdbebe6a274caa03bc360a1cf90d601659155984b2", + "images/W001_s001_w0_z044_t001_CellsLabels.tiff": "e03946d94c789ee787cc29a3c8f5e5d9c56b37a0044b297ba8f403abc98eb55e", + "images/W001_s001_w0_z045_t001_CellsLabels.tiff": "c678f2100c7431e5a43c391af86a79e3436d4cf13f050f98c25f62682e4632fe", + "images/W001_s001_w0_z046_t001_CellsLabels.tiff": "dc081df050be6cd19b5d5bbfe21c96c350eecf9bc6f10f608dc361a36056632a", + "images/W001_s001_w0_z047_t001_CellsLabels.tiff": "b926aa64e1fa8aa52279a69d91dc23cf14bf0ae76107f9b360c7ae77bdf53719", + "images/W001_s001_w0_z048_t001_CellsLabels.tiff": "133a58f6fac8b07f79bfed9634867ccb5428b83e63682031a1def16c6a694568", + "images/W001_s001_w0_z049_t001_CellsLabels.tiff": "28d83dfbf5bbf7e4da9c3e4806353e9fd0a89680dcdab4b43ca2fd19b80f669c", + "images/W001_s001_w0_z050_t001_CellsLabels.tiff": "ea7654a6fe1f92ad274a54935420923eec4bec5ad030187afc0ac7af02690af5", + "images/W001_s001_w0_z051_t001_CellsLabels.tiff": "73483bd10a0f37990084013ad054768086d6d3c929da6bc7e74f4aee37e9dbf2", + "images/W001_s001_w0_z052_t001_CellsLabels.tiff": "7bb98f0587062e01b965e486fc687872651d7d90d271a2fdf0d35d67b464b233", + "images/W001_s001_w0_z053_t001_CellsLabels.tiff": "ff17f538baaf179752412026a250bdbee016e3101037bf4e0dbb81199fc4c8ed", + "images/W001_s001_w0_z054_t001_CellsLabels.tiff": "25ff2ec586a568e42286f73713d64aab1208c38c016099299211f04368ac49c8", + "images/W001_s001_w0_z055_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w0_z056_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w0_z057_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w0_z058_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w0_z059_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w0_z060_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w2_z001_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w2_z002_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w2_z003_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w2_z004_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w2_z005_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w2_z006_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w2_z007_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w2_z008_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w2_z009_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w2_z010_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w2_z011_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w2_z012_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w2_z013_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w2_z014_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w2_z015_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w2_z016_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w2_z017_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w2_z018_t001_NucleiLabels.tiff": "03ba7a4d6ae53e6c84aab5926ae1e2592b5f8ca24a839a256c82f08544df4270", + "images/W001_s001_w2_z019_t001_NucleiLabels.tiff": "250cb56a8054c28cea894bc3d9edaddcdce01f5151a05da78da20157c3c3be9f", + "images/W001_s001_w2_z020_t001_NucleiLabels.tiff": "b0cd031b448d8bd2075e65710bd76c064161b25242f85ee150da7cd139fcc1a8", + "images/W001_s001_w2_z021_t001_NucleiLabels.tiff": "71a10b0539f3fc4cba600b275b7e948258c6a435ba33ef522faa0869ac085518", + "images/W001_s001_w2_z022_t001_NucleiLabels.tiff": "50aef20e28a2f9e18ca2e28eabf6d87ec2ad20c79898e56f8d63a959ba5bb507", + "images/W001_s001_w2_z023_t001_NucleiLabels.tiff": "ecb7f78c8ca0bad2b9d0495659609b9023781b6a936d6afb0a04653eac176555", + "images/W001_s001_w2_z024_t001_NucleiLabels.tiff": "9e96a05ed9860cac9c57c618db2f4fe5e4b769a9d235aac6fd1d628486317df0", + "images/W001_s001_w2_z025_t001_NucleiLabels.tiff": "9cc33676a0177130609ab1331feab1722bbde8d55b2f49031730fd5a2a40029c", + "images/W001_s001_w2_z026_t001_NucleiLabels.tiff": "ad22809ff3f43d15c838f697061c263070dbf1da1031da39900681cdfbf9d54c", + "images/W001_s001_w2_z027_t001_NucleiLabels.tiff": "8c8f402d3a7c65c04e7f1575dd34de0427eeeb433ff2956d20e07d06eb48fb8d", + "images/W001_s001_w2_z028_t001_NucleiLabels.tiff": "1676f21bd3b7e105601c83f206890507c32ba2447988be2a75d0e8153d069b65", + "images/W001_s001_w2_z029_t001_NucleiLabels.tiff": "34bce0f06f674e20667554eed0075fde8d9d33c9c0eaeffd88a1c558499cc432", + "images/W001_s001_w2_z030_t001_NucleiLabels.tiff": "c54c8b98a5c38a47e0cf9391a1afab09b4c8c58e06294cdb650a15add4a32b58", + "images/W001_s001_w2_z031_t001_NucleiLabels.tiff": "247490c261fecc26c7379ab2eebaa01e4bea523218b3cc5491d2fe2b9aadfe11", + "images/W001_s001_w2_z032_t001_NucleiLabels.tiff": "97b09fe5444ad439c3c2007d75971dce2766d94c93268eccfee9ae49b8a04aa1", + "images/W001_s001_w2_z033_t001_NucleiLabels.tiff": "512a3a0ed8025d14b0556a72ebfc13b173d14f7046817ca09876242ab63b9d6b", + "images/W001_s001_w2_z034_t001_NucleiLabels.tiff": "08af61857cfb97fd95fcb08d1ce18139ce7087b192044c63452bee533c5b1aa4", + "images/W001_s001_w2_z035_t001_NucleiLabels.tiff": "13e69afc324152bb91329fa758d131d849127e8fd346078d94c6adc25582b3a6", + "images/W001_s001_w2_z036_t001_NucleiLabels.tiff": "4fa967c78709aa400246a50528a36b24ca366e825dd0e64561643369e814906b", + "images/W001_s001_w2_z037_t001_NucleiLabels.tiff": "927d52441c57cb3a4014effaca3e3c23c76275fb264c26e1942a9c58b85ee6d2", + "images/W001_s001_w2_z038_t001_NucleiLabels.tiff": "e2852ea83c62ab30d77681b4e39f88d3cee74ae052f1c948675ae3581c9fc28a", + "images/W001_s001_w2_z039_t001_NucleiLabels.tiff": "f93e8293a37886a36844b8748083a1f8e717efab092652755263e797f12f0364", + "images/W001_s001_w2_z040_t001_NucleiLabels.tiff": "e0bd66b166e9f92120dbee183973849eda0afbbe6fed9d03fc4cfd2c56a96280", + "images/W001_s001_w2_z041_t001_NucleiLabels.tiff": "744eab7cd170d5af8d2f72ced3893dd939c4e1b0f415afab799a37ded5d046c7", + "images/W001_s001_w2_z042_t001_NucleiLabels.tiff": "e07252ca1a55c5195c99666d8d3f0d70db636b4ef4b99e1a0049ca22b182d87a", + "images/W001_s001_w2_z043_t001_NucleiLabels.tiff": "81a705d05b0a5bfa8783df28990c1d70634c1f4aa2bca23e02cfa8a4bc861b86", + "images/W001_s001_w2_z044_t001_NucleiLabels.tiff": "a78264f2f5e245a3913a69f89105e94df30048b1aaef4eb80fc5c3235e78b196", + "images/W001_s001_w2_z045_t001_NucleiLabels.tiff": "94bd6a65795ad6395cc18a4fd63dc591354bef612ee49fffd2d9f63e4ed9db84", + "images/W001_s001_w2_z046_t001_NucleiLabels.tiff": "c1250e060d32f2cfc07fc7e1594fdb1d6cbb4d0483fa94a8113efa695397963c", + "images/W001_s001_w2_z047_t001_NucleiLabels.tiff": "995d5ce77cbf61a9be8590f34987ef3f1dd698ae6ed393cac4d0960bc9e7a66a", + "images/W001_s001_w2_z048_t001_NucleiLabels.tiff": "325478230529e94ba372f38f0bf512292f131d337b6ac09ce5a91b0266724e02", + "images/W001_s001_w2_z049_t001_NucleiLabels.tiff": "e92e493078672a0e0d90e31409612e215bf8486300d5c765d0e0504ff8888e7a", + "images/W001_s001_w2_z050_t001_NucleiLabels.tiff": "565b4f08f7503086c1f2f8e631d19063129e02a5767d2f979c0cec8fdec23601", + "images/W001_s001_w2_z051_t001_NucleiLabels.tiff": "f0f36ccaf744d42da036948682cfd226e6dd4a3cfb94aed38729b2d25ede5dff", + "images/W001_s001_w2_z052_t001_NucleiLabels.tiff": "0daf51526658e23b7c9ab8872e992ebe9322399286f95741d0ccdbd357e5d631", + "images/W001_s001_w2_z053_t001_NucleiLabels.tiff": "b8613192175a4615b58aed607474657668429cb6ac635cd012569b19667f2d3d", + "images/W001_s001_w2_z054_t001_NucleiLabels.tiff": "b841c5d901c65a6138d9ff2b0ec4aa04f00765b1dd21bcd6fc9f8dc974c6035d", + "images/W001_s001_w2_z055_t001_NucleiLabels.tiff": "44e02416cc94a70528165a4b6c42b9e19b205f9c91528076796272cb1a8504ea", + "images/W001_s001_w2_z056_t001_NucleiLabels.tiff": "ca0723c8b9764d5b77034719bb75a2940be35da2e04119af4c0e6d3552c08718", + "images/W001_s001_w2_z057_t001_NucleiLabels.tiff": "695517641e9fe4b8421d4a7db56b24c755b6957c98ce32bb24ad5b25f52661eb", + "images/W001_s001_w2_z058_t001_NucleiLabels.tiff": "a37c63c6052e40d73db918a014e6f2a18be4d0cbfbeeef65a9c26096a38a4c09", + "images/W001_s001_w2_z059_t001_NucleiLabels.tiff": "76c785cf0f481c73ad3cf34ccb576eaa19dd1bdd9c74f9eaf92adfbde5d44b97", + "images/W001_s001_w2_z060_t001_NucleiLabels.tiff": "7cc3c6c6a22f558e62194277090bdea2deb79475dd91fe7d0e49eac3c0187388", + "results/3d_monolayer_Cells.csv": "6f6e7431171ed5ea73a01784105147eca0e43a6e3e41f8720df93106353c9c79", + "results/3d_monolayer_Image.csv": "75ac32ff5ef57bc285ec4eae1c9b501c27a35b486bf35435af5d499d856d34a4", + "results/3d_monolayer_Nuclei.csv": "17500f5d4321bbf255df31f7a3503339029c2b077045a6c10e4d4f6f0db9ad1e", + "results/3d_monolayer_downsizedNuclei.csv": "a7b998c5a524e11747fc5452fd763fc3d9abe16f46e961f62e09e9393b6a8adf", + "results/3d_monolayer_erodedDownsizedNuclei.csv": "ee507fb9c2387f86fc768184baf2596ebe1f8e40f7fc5eb24f252345fee63ae6", + "results/3d_monolayer_erodedResizedNuclei.csv": "cca23f86a85bb3ee7a5c3a1d174c34b5f026ca7f399be98458e1a8c6e12d5c77" + }, + "complete_inventory": { + "images/W001_s001_w0_z001_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w0_z002_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w0_z003_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w0_z004_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w0_z005_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w0_z006_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w0_z007_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w0_z008_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w0_z009_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w0_z010_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w0_z011_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w0_z012_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w0_z013_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w0_z014_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w0_z015_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w0_z016_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w0_z017_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w0_z018_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w0_z019_t001_CellsLabels.tiff": "ec6ef1d319f2732c47b06f5cf02775c8cd94540f2f30a6c002f0aace1d317955", + "images/W001_s001_w0_z020_t001_CellsLabels.tiff": "e8105a4f04198ac75de9831247030084d6f65bf98691d7444ead7e178072fa22", + "images/W001_s001_w0_z021_t001_CellsLabels.tiff": "daa31cb1e537e5c32b1936ab7647c5690741d2bc1d0e15c60b59ba1310a8c0ce", + "images/W001_s001_w0_z022_t001_CellsLabels.tiff": "7d7ec170ed3652efa99e43d8db546c51b696821dca7727d0ff4d2f2ead7fdbaa", + "images/W001_s001_w0_z023_t001_CellsLabels.tiff": "2f06989618050934c73473f9342c2fe3410fd834a0ddb976cf602fe6198db7a6", + "images/W001_s001_w0_z024_t001_CellsLabels.tiff": "39899adabca74b10b9950d1f97e8b0b3bbaf136d75e7a46ef4a6e86896e3119f", + "images/W001_s001_w0_z025_t001_CellsLabels.tiff": "03aabe5528198cfd6ec23a058dab540fb8be0870207c6dc94a62d9b621d1f2f9", + "images/W001_s001_w0_z026_t001_CellsLabels.tiff": "5b433d931fa6637fc793b23880199c01000ef6d9e610903bb38bb8cea5574563", + "images/W001_s001_w0_z027_t001_CellsLabels.tiff": "ee9056587a19e581f5feb6078b41fa1004d923a5ddf1725aefc4b8b8c5201ad6", + "images/W001_s001_w0_z028_t001_CellsLabels.tiff": "fef31040e0c54501c3bf965eb98f055cdb1d42c96f91cf1c239033d0a9a06534", + "images/W001_s001_w0_z029_t001_CellsLabels.tiff": "b3bdde5f751b8f5d41ca8b5219a3427e450cb1b8795401d0ec638940d1ad7bd9", + "images/W001_s001_w0_z030_t001_CellsLabels.tiff": "b71a2e05d270ae0cd237d6c0d97ea82a63e5ad3633723e7255c35d34e585ec80", + "images/W001_s001_w0_z031_t001_CellsLabels.tiff": "1b14aab71bf3623458ea9373852b0baf28be9727877b32f894407927ecfb6d65", + "images/W001_s001_w0_z032_t001_CellsLabels.tiff": "8a2c5774044326ae722e41aae517266197fbebb0e4dd5eab4b22414fbfea309f", + "images/W001_s001_w0_z033_t001_CellsLabels.tiff": "a320b99ee129d97b99f9fac66aabb89fc1652930b3bb8b2b3fbba7b57e994f85", + "images/W001_s001_w0_z034_t001_CellsLabels.tiff": "3f6c1d8cdbce20aac8f70e7315bea70b147b7b6a034432845b27eeb6cfba083c", + "images/W001_s001_w0_z035_t001_CellsLabels.tiff": "eedeb489b46abf1eafbd9c81491cb3f85ce1cb3ce877844ad22e72547fc3c438", + "images/W001_s001_w0_z036_t001_CellsLabels.tiff": "28b68d1300cf7b08e21dbcd4858447862520dded5c27f2d84cf223513a119eed", + "images/W001_s001_w0_z037_t001_CellsLabels.tiff": "aa1f449f1769b27cd9f1076e70f49f0242e2549f288a77984ca159cb07cf6a40", + "images/W001_s001_w0_z038_t001_CellsLabels.tiff": "6ac8fd9c95acc1f261e96a8fb9332e5d6a403548842f9b85ecb977e6213b4c9b", + "images/W001_s001_w0_z039_t001_CellsLabels.tiff": "22fa79a76e64013f10373be2ca2af98a2173275c5d43c9cad33135acdc376d14", + "images/W001_s001_w0_z040_t001_CellsLabels.tiff": "1635ea20dd80ce99f0e86494666825a52a66645837d598fd3a3fa91f0bb77d53", + "images/W001_s001_w0_z041_t001_CellsLabels.tiff": "be7762756eb65f08cb337904165c6f43b5351f39405461f65ad28669abfe3899", + "images/W001_s001_w0_z042_t001_CellsLabels.tiff": "88d2e9964455459499656c4f0a762cd86376c678756635aaa6b6c8d98be01852", + "images/W001_s001_w0_z043_t001_CellsLabels.tiff": "bd0df007524ea4e555e4d2fdbebe6a274caa03bc360a1cf90d601659155984b2", + "images/W001_s001_w0_z044_t001_CellsLabels.tiff": "e03946d94c789ee787cc29a3c8f5e5d9c56b37a0044b297ba8f403abc98eb55e", + "images/W001_s001_w0_z045_t001_CellsLabels.tiff": "c678f2100c7431e5a43c391af86a79e3436d4cf13f050f98c25f62682e4632fe", + "images/W001_s001_w0_z046_t001_CellsLabels.tiff": "dc081df050be6cd19b5d5bbfe21c96c350eecf9bc6f10f608dc361a36056632a", + "images/W001_s001_w0_z047_t001_CellsLabels.tiff": "b926aa64e1fa8aa52279a69d91dc23cf14bf0ae76107f9b360c7ae77bdf53719", + "images/W001_s001_w0_z048_t001_CellsLabels.tiff": "133a58f6fac8b07f79bfed9634867ccb5428b83e63682031a1def16c6a694568", + "images/W001_s001_w0_z049_t001_CellsLabels.tiff": "28d83dfbf5bbf7e4da9c3e4806353e9fd0a89680dcdab4b43ca2fd19b80f669c", + "images/W001_s001_w0_z050_t001_CellsLabels.tiff": "ea7654a6fe1f92ad274a54935420923eec4bec5ad030187afc0ac7af02690af5", + "images/W001_s001_w0_z051_t001_CellsLabels.tiff": "73483bd10a0f37990084013ad054768086d6d3c929da6bc7e74f4aee37e9dbf2", + "images/W001_s001_w0_z052_t001_CellsLabels.tiff": "7bb98f0587062e01b965e486fc687872651d7d90d271a2fdf0d35d67b464b233", + "images/W001_s001_w0_z053_t001_CellsLabels.tiff": "ff17f538baaf179752412026a250bdbee016e3101037bf4e0dbb81199fc4c8ed", + "images/W001_s001_w0_z054_t001_CellsLabels.tiff": "25ff2ec586a568e42286f73713d64aab1208c38c016099299211f04368ac49c8", + "images/W001_s001_w0_z055_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w0_z056_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w0_z057_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w0_z058_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w0_z059_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w0_z060_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w2_z001_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w2_z002_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w2_z003_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w2_z004_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w2_z005_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w2_z006_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w2_z007_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w2_z008_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w2_z009_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w2_z010_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w2_z011_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w2_z012_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w2_z013_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w2_z014_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w2_z015_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w2_z016_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w2_z017_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "images/W001_s001_w2_z018_t001_NucleiLabels.tiff": "03ba7a4d6ae53e6c84aab5926ae1e2592b5f8ca24a839a256c82f08544df4270", + "images/W001_s001_w2_z019_t001_NucleiLabels.tiff": "250cb56a8054c28cea894bc3d9edaddcdce01f5151a05da78da20157c3c3be9f", + "images/W001_s001_w2_z020_t001_NucleiLabels.tiff": "b0cd031b448d8bd2075e65710bd76c064161b25242f85ee150da7cd139fcc1a8", + "images/W001_s001_w2_z021_t001_NucleiLabels.tiff": "71a10b0539f3fc4cba600b275b7e948258c6a435ba33ef522faa0869ac085518", + "images/W001_s001_w2_z022_t001_NucleiLabels.tiff": "50aef20e28a2f9e18ca2e28eabf6d87ec2ad20c79898e56f8d63a959ba5bb507", + "images/W001_s001_w2_z023_t001_NucleiLabels.tiff": "ecb7f78c8ca0bad2b9d0495659609b9023781b6a936d6afb0a04653eac176555", + "images/W001_s001_w2_z024_t001_NucleiLabels.tiff": "9e96a05ed9860cac9c57c618db2f4fe5e4b769a9d235aac6fd1d628486317df0", + "images/W001_s001_w2_z025_t001_NucleiLabels.tiff": "9cc33676a0177130609ab1331feab1722bbde8d55b2f49031730fd5a2a40029c", + "images/W001_s001_w2_z026_t001_NucleiLabels.tiff": "ad22809ff3f43d15c838f697061c263070dbf1da1031da39900681cdfbf9d54c", + "images/W001_s001_w2_z027_t001_NucleiLabels.tiff": "8c8f402d3a7c65c04e7f1575dd34de0427eeeb433ff2956d20e07d06eb48fb8d", + "images/W001_s001_w2_z028_t001_NucleiLabels.tiff": "1676f21bd3b7e105601c83f206890507c32ba2447988be2a75d0e8153d069b65", + "images/W001_s001_w2_z029_t001_NucleiLabels.tiff": "34bce0f06f674e20667554eed0075fde8d9d33c9c0eaeffd88a1c558499cc432", + "images/W001_s001_w2_z030_t001_NucleiLabels.tiff": "c54c8b98a5c38a47e0cf9391a1afab09b4c8c58e06294cdb650a15add4a32b58", + "images/W001_s001_w2_z031_t001_NucleiLabels.tiff": "247490c261fecc26c7379ab2eebaa01e4bea523218b3cc5491d2fe2b9aadfe11", + "images/W001_s001_w2_z032_t001_NucleiLabels.tiff": "97b09fe5444ad439c3c2007d75971dce2766d94c93268eccfee9ae49b8a04aa1", + "images/W001_s001_w2_z033_t001_NucleiLabels.tiff": "512a3a0ed8025d14b0556a72ebfc13b173d14f7046817ca09876242ab63b9d6b", + "images/W001_s001_w2_z034_t001_NucleiLabels.tiff": "08af61857cfb97fd95fcb08d1ce18139ce7087b192044c63452bee533c5b1aa4", + "images/W001_s001_w2_z035_t001_NucleiLabels.tiff": "13e69afc324152bb91329fa758d131d849127e8fd346078d94c6adc25582b3a6", + "images/W001_s001_w2_z036_t001_NucleiLabels.tiff": "4fa967c78709aa400246a50528a36b24ca366e825dd0e64561643369e814906b", + "images/W001_s001_w2_z037_t001_NucleiLabels.tiff": "927d52441c57cb3a4014effaca3e3c23c76275fb264c26e1942a9c58b85ee6d2", + "images/W001_s001_w2_z038_t001_NucleiLabels.tiff": "e2852ea83c62ab30d77681b4e39f88d3cee74ae052f1c948675ae3581c9fc28a", + "images/W001_s001_w2_z039_t001_NucleiLabels.tiff": "f93e8293a37886a36844b8748083a1f8e717efab092652755263e797f12f0364", + "images/W001_s001_w2_z040_t001_NucleiLabels.tiff": "e0bd66b166e9f92120dbee183973849eda0afbbe6fed9d03fc4cfd2c56a96280", + "images/W001_s001_w2_z041_t001_NucleiLabels.tiff": "744eab7cd170d5af8d2f72ced3893dd939c4e1b0f415afab799a37ded5d046c7", + "images/W001_s001_w2_z042_t001_NucleiLabels.tiff": "e07252ca1a55c5195c99666d8d3f0d70db636b4ef4b99e1a0049ca22b182d87a", + "images/W001_s001_w2_z043_t001_NucleiLabels.tiff": "81a705d05b0a5bfa8783df28990c1d70634c1f4aa2bca23e02cfa8a4bc861b86", + "images/W001_s001_w2_z044_t001_NucleiLabels.tiff": "a78264f2f5e245a3913a69f89105e94df30048b1aaef4eb80fc5c3235e78b196", + "images/W001_s001_w2_z045_t001_NucleiLabels.tiff": "94bd6a65795ad6395cc18a4fd63dc591354bef612ee49fffd2d9f63e4ed9db84", + "images/W001_s001_w2_z046_t001_NucleiLabels.tiff": "c1250e060d32f2cfc07fc7e1594fdb1d6cbb4d0483fa94a8113efa695397963c", + "images/W001_s001_w2_z047_t001_NucleiLabels.tiff": "995d5ce77cbf61a9be8590f34987ef3f1dd698ae6ed393cac4d0960bc9e7a66a", + "images/W001_s001_w2_z048_t001_NucleiLabels.tiff": "325478230529e94ba372f38f0bf512292f131d337b6ac09ce5a91b0266724e02", + "images/W001_s001_w2_z049_t001_NucleiLabels.tiff": "e92e493078672a0e0d90e31409612e215bf8486300d5c765d0e0504ff8888e7a", + "images/W001_s001_w2_z050_t001_NucleiLabels.tiff": "565b4f08f7503086c1f2f8e631d19063129e02a5767d2f979c0cec8fdec23601", + "images/W001_s001_w2_z051_t001_NucleiLabels.tiff": "f0f36ccaf744d42da036948682cfd226e6dd4a3cfb94aed38729b2d25ede5dff", + "images/W001_s001_w2_z052_t001_NucleiLabels.tiff": "0daf51526658e23b7c9ab8872e992ebe9322399286f95741d0ccdbd357e5d631", + "images/W001_s001_w2_z053_t001_NucleiLabels.tiff": "b8613192175a4615b58aed607474657668429cb6ac635cd012569b19667f2d3d", + "images/W001_s001_w2_z054_t001_NucleiLabels.tiff": "b841c5d901c65a6138d9ff2b0ec4aa04f00765b1dd21bcd6fc9f8dc974c6035d", + "images/W001_s001_w2_z055_t001_NucleiLabels.tiff": "44e02416cc94a70528165a4b6c42b9e19b205f9c91528076796272cb1a8504ea", + "images/W001_s001_w2_z056_t001_NucleiLabels.tiff": "ca0723c8b9764d5b77034719bb75a2940be35da2e04119af4c0e6d3552c08718", + "images/W001_s001_w2_z057_t001_NucleiLabels.tiff": "695517641e9fe4b8421d4a7db56b24c755b6957c98ce32bb24ad5b25f52661eb", + "images/W001_s001_w2_z058_t001_NucleiLabels.tiff": "a37c63c6052e40d73db918a014e6f2a18be4d0cbfbeeef65a9c26096a38a4c09", + "images/W001_s001_w2_z059_t001_NucleiLabels.tiff": "76c785cf0f481c73ad3cf34ccb576eaa19dd1bdd9c74f9eaf92adfbde5d44b97", + "images/W001_s001_w2_z060_t001_NucleiLabels.tiff": "7cc3c6c6a22f558e62194277090bdea2deb79475dd91fe7d0e49eac3c0187388", + "openhcs_metadata.json": "d0cc10b9362796e0894c240460684bd8dbbaa423069032e8ac8efb9ffdbe2e1e", + "openhcs_metadata.json.lock": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "results/3d_monolayer_Cells.csv": "6f6e7431171ed5ea73a01784105147eca0e43a6e3e41f8720df93106353c9c79", + "results/3d_monolayer_Image.csv": "75ac32ff5ef57bc285ec4eae1c9b501c27a35b486bf35435af5d499d856d34a4", + "results/3d_monolayer_Nuclei.csv": "17500f5d4321bbf255df31f7a3503339029c2b077045a6c10e4d4f6f0db9ad1e", + "results/3d_monolayer_downsizedNuclei.csv": "a7b998c5a524e11747fc5452fd763fc3d9abe16f46e961f62e09e9393b6a8adf", + "results/3d_monolayer_erodedDownsizedNuclei.csv": "ee507fb9c2387f86fc768184baf2596ebe1f8e40f7fc5eb24f252345fee63ae6", + "results/3d_monolayer_erodedResizedNuclei.csv": "cca23f86a85bb3ee7a5c3a1d174c34b5f026ca7f399be98458e1a8c6e12d5c77" + }, + "managed_metadata": { + "openhcs_metadata.json": { + "baseline_sha256": "0af41a148e9dd6ff40febf786b819d84cb689cfeff93500f6aa7f5c8912219ec", + "actual_sha256": "d0cc10b9362796e0894c240460684bd8dbbaa423069032e8ac8efb9ffdbe2e1e", + "changed": true + }, + "openhcs_metadata.json.lock": { + "baseline_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "actual_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "changed": false + } + }, + "unknown_exclusions": [] + }, + "completion": { + "observation_export": { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/compiler-config-coarse-ledger-v1/output/public/cp_tutorial_3d_monolayer/wells_1/workers_1/ordinary_run_evidence/cp_tutorial_3d_monolayer-1w_1t-1791020555970061852/runtime_outcomes.pkl.gz", + "sha256": "51f80d2dab3f0abcdad31b7c8bb77a58a2fa4c0f0d315f68c503d7c669a6e4fa", + "bytes": 5435 + }, + "results_summary": { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/compiler-config-coarse-ledger-v1/output/public/cp_tutorial_3d_monolayer/wells_1/workers_1/ordinary_run_evidence/cp_tutorial_3d_monolayer-1w_1t-1791020555970061852/zmq_results_summary.json", + "sha256": "14228fa5cdc6f005e3605f5b5a4c9bf76271a139068139375b8da5f78713a6d6", + "bytes": 655 + } + }, + "actual_receipt": "/home/ts/.local/state/openhcs-maintenance/20261003/compiler-config-coarse-ledger-v1/output/public/cp_tutorial_3d_monolayer/wells_1/workers_1/ordinary_run_evidence/cp_tutorial_3d_monolayer-1w_1t-1791020555970061852/measured_pipeline_receipt.json", + "actual_receipt_sha256": "4a9bbba130e8175a3d3a0d27530e802eb347f0b665fef2b58a089a32fef630c0", + "source_freeze_sha256": "0aa8d013c340ed52de584cc006becc832dfa1a9875745213d2d2e4c0904656cf", + "observations_sha256": "07c19d9a2b6183d1cbb69691258862f3b89b65149481989557b0d1029bf728e3", + "controller_source_env_native_input_freeze": "Original root controller passed actual before/after gate; source/deps/native/input/helper freeze independently revalidated here.", + "original_root_effective_environment_sha256": "00b7f550f6e584e34d6070f79bdd60d37859cb58c752b866fc95ac7f5293dabd", + "validator_environment_scope": "Separate CPU3 read-only process; original controller full effective environment also revalidated from the authenticated original agent environment. Root session-specific CODEX_THREAD_ID prelaunch refusal preserved separately.", + "physical_resources": { + "/home/ts/.cache/openhcs/benchmark_datasets/CellProfiler_tutorials/data/3d_monolayer/images/3d_monolayer_xy1_ch2.tif": "191754bc7f43856223180483e2737ea93bab4d0cf98b1b2a4c27425e9908147d", + "/home/ts/.cache/openhcs/benchmark_datasets/CellProfiler_tutorials/data/3d_monolayer/images/3d_monolayer_xy1_ch1.tif": "a7d9abf632850875c22613c4b740fc01153cf60e9773c16d4a670a97b46e5751", + "/home/ts/.cache/openhcs/benchmark_datasets/CellProfiler_tutorials/data/3d_monolayer/images/3d_monolayer_xy1_ch0.tif": "63114f614fec95664f718a39cddac58449f0edb5cc31b57584e4ba5b1f4994c7" + }, + "ordered_source_ref_count": 180, + "per_volume_ordered_source_refs": 60, + "source_metadata_path": "/home/ts/.local/state/openhcs-maintenance/20261003/compiler-config-coarse-ledger-v1/output/public/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace/openhcs_metadata.json", + "source_metadata_sha256": "a42ec413a0ec213d22f4ba96fac785bfbfac167dd5d2da7cbd1acc8cf9c2d862", + "input_reference_output_bytes_unchanged_after_validation": true, + "qualified_prior_evidence": { + "/var/tmp/openhcs-current-3d-gc-ledger-v1-scientific-byte-gate-20261003.json": "d0b8d6af1b8a1777b03624cf6435d67b04746e9477a5cefbc7a962feaa2d667d", + "/var/tmp/openhcs-3d-b764-abba-science-clocks-20261002.json": "49c088e2e257f094c7e205caf9743882d41e7574a0733cd190e9edbc0a2e3f76", + "/var/tmp/openhcs-spatial-plane-sourceborn-resize-aba4-diagnostic-20261002/source-freeze.json": "c6d26196dc23e330fcd2614d68a33e35b454223d65b4e9c66a5e0ae021113ace", + "/var/tmp/run_spatial_plane_sourceborn_diagnostic_v2_20261002.py": "d11f91b71129862f623bc62dd4909643f1f589128122c08c7559cb5c12b6c5a4", + "/var/tmp/validate_current_3d_runtime_ledger_scientific_bytes_v1_20261003.py": "f86bcb1c440714e6e5b7d9c2d9a62cc813de7985fb586a59a3edba6cf9f081d0", + "/var/tmp/openhcs-native-3d-aba4-qualification-20261002/source-environment-input-witness.json": "e0fd1afeb2d4a9c73c2aa9c6f43f71055e9b93c7211a45d096789d42633d1120", + "/var/tmp/openhcs-native-3d-aba4-qualification-20261002/fresh-native-scientific-comparison-supplemental.json": "002cd9b5aa5beb22d10621da971d63ad2fa18af009d3bd93072d9a9957cab6c4" + }, + "native_scope": "TRANSITIVE ONLY: exact current saved science equals retained reference and B764-qualified bytes with actual same3physicalvolume inputs/180orderedrefs. Retained2native repetitions verified; native was NOT rerun for current source/environment. Original native compare API RED retained separately; no current native timing ratio or performance claim." +} From 0a54025793f1029a0b08f3996286d5d418f4619f Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 06:15:15 -0400 Subject: [PATCH 123/235] bench: qualify Illum3 compiler and runtime costs (#496) --- .../assessment.json | 152 ++++ .../compile_only-region.json | 430 +++++++++ .../compiler-config-class-census.json | 1 + .../compiler-config-owner-decision.json | 122 +++ .../controls.json | 37 + .../execution-region.json | 630 ++++++++++++++ .../numba-events-1.json | 1 + .../numba-events-2.json | 1 + .../numba-events-3.json | 1 + .../numba-events-4.json | 1 + .../observations.json | 93 ++ .../qualification.json | 403 +++++++++ .../reader-environment-audit.json | 35 + .../reader-first-red.json | 13 + .../render-retention-class-census.json | 671 ++++++++++++++ .../render-retention-decision.json | 72 ++ .../render-retention-replay.json | 817 ++++++++++++++++++ .../report.rst | 97 +++ .../science.json | 237 +++++ 19 files changed, 3814 insertions(+) create mode 100644 benchmark/results/perf_illum3_runtime_ledger_20261003/assessment.json create mode 100644 benchmark/results/perf_illum3_runtime_ledger_20261003/compile_only-region.json create mode 100644 benchmark/results/perf_illum3_runtime_ledger_20261003/compiler-config-class-census.json create mode 100644 benchmark/results/perf_illum3_runtime_ledger_20261003/compiler-config-owner-decision.json create mode 100644 benchmark/results/perf_illum3_runtime_ledger_20261003/controls.json create mode 100644 benchmark/results/perf_illum3_runtime_ledger_20261003/execution-region.json create mode 100644 benchmark/results/perf_illum3_runtime_ledger_20261003/numba-events-1.json create mode 100644 benchmark/results/perf_illum3_runtime_ledger_20261003/numba-events-2.json create mode 100644 benchmark/results/perf_illum3_runtime_ledger_20261003/numba-events-3.json create mode 100644 benchmark/results/perf_illum3_runtime_ledger_20261003/numba-events-4.json create mode 100644 benchmark/results/perf_illum3_runtime_ledger_20261003/observations.json create mode 100644 benchmark/results/perf_illum3_runtime_ledger_20261003/qualification.json create mode 100644 benchmark/results/perf_illum3_runtime_ledger_20261003/reader-environment-audit.json create mode 100644 benchmark/results/perf_illum3_runtime_ledger_20261003/reader-first-red.json create mode 100644 benchmark/results/perf_illum3_runtime_ledger_20261003/render-retention-class-census.json create mode 100644 benchmark/results/perf_illum3_runtime_ledger_20261003/render-retention-decision.json create mode 100644 benchmark/results/perf_illum3_runtime_ledger_20261003/render-retention-replay.json create mode 100644 benchmark/results/perf_illum3_runtime_ledger_20261003/report.rst create mode 100644 benchmark/results/perf_illum3_runtime_ledger_20261003/science.json diff --git a/benchmark/results/perf_illum3_runtime_ledger_20261003/assessment.json b/benchmark/results/perf_illum3_runtime_ledger_20261003/assessment.json new file mode 100644 index 0000000000..fbc1756fae --- /dev/null +++ b/benchmark/results/perf_illum3_runtime_ledger_20261003/assessment.json @@ -0,0 +1,152 @@ +{ + "status": "DIAGNOSTIC_AND_SCIENCE_PASS_NO_NEW_PRODUCTION_SPEEDUP", + "source_head": "4c0d62e45c8afb85e34b69d7279cd00710f519b5", + "source_freeze": { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/output/source-freeze.json", + "sha256": "86646806122c73d38de2e9d42efa0bffd471ad0a1db36d7d33ea83dfcbfeb0fc" + }, + "actual_import_dependencies": { + "external/ObjectState": { + "head": "a01f8939b9bf4659502a33fe3febe62cab0f3c9a", + "status": "" + }, + "external/python-introspect": { + "head": "ed1e4eb15f7a0bf8315274d3e47858fa2b4372bb", + "status": "" + }, + "external/arraybridge": { + "head": "1e53d03d9f468322a1c085c8be29485fb139caf2", + "status": "" + }, + "external/metaclass-registry": { + "head": "393a7e03003cdc56df9013f932ed4f26e632d77a", + "status": "" + }, + "external/zmqruntime": { + "head": "668edafcb5ee530a163377c936431ae1e8334e85", + "status": "" + }, + "external/pycodify": { + "head": "792b03413331fdf107e369c924c257a2f0b16126", + "status": "" + }, + "external/PolyStore": { + "head": "84f322e46871de5ed47e7fd20976ad03e533c1c4", + "status": "" + }, + "external/pyqt-reactive": { + "head": "ad4948775ab81180a354d4b793d17ee5ddff3972", + "status": "" + } + }, + "installed_versions": { + "python": "3.12.14 (main, Sep 24 2026, 17:57:43) [Clang 22.1.3 ]", + "versions": { + "openhcs": "0.8.7", + "numpy": "2.5.3", + "scipy": "1.18.1", + "arraybridge": "0.3.6", + "polystore": "0.3.0", + "python-introspect": "0.1.15", + "metaclass-registry": "0.2.2", + "zmqruntime": "0.3.0" + } + }, + "originals_retained_byte_identical": { + "qualification.json": { + "original_path": "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/qualification.json", + "sha256": "89f786ef58cd1c31c2418b66eb15e25a55018c53fbf06c7516a76ed76bada615", + "bytes": 17768 + }, + "observations.json": { + "original_path": "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/output/observations.json", + "sha256": "c8b1e3f0704682e2cad20b7a5e27b1651e38b677e74f31d6477eb163185106fd", + "bytes": 4831 + }, + "controls.json": { + "original_path": "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/controls.json", + "sha256": "358cd70c391cdaae89d5f2ba8514de606ccabcf43be3507dadeba7ae2341f7d1", + "bytes": 1520 + }, + "science.json": { + "original_path": "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/science-v3.json", + "sha256": "607c68f213baddf4c5dbec97b39efee87451ac16188eadf1ebdea578d5bee9e5", + "bytes": 15478 + }, + "reader-first-red.json": { + "original_path": "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/science-first-red.json", + "sha256": "de6ae869a4f1fe6eabfa4fc5b5ee87ba052462f26302d0a7a334c7b2c4276c80", + "bytes": 872 + }, + "reader-environment-audit.json": { + "original_path": "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/science-first-red-environment-audit.json", + "sha256": "4fa0497da2158f883c3af73aaea0b906a63f0d3887d7f82ee89e9c8afc03db20", + "bytes": 1285 + }, + "render-retention-decision.json": { + "original_path": "/tmp/openhcs-render-retention-4c0d62/decision-v1.json", + "sha256": "905e189541015681fb58b42da8d28453f76710fbf088156b27260de90239bf94", + "bytes": 3626 + }, + "render-retention-replay.json": { + "original_path": "/tmp/openhcs-render-retention-4c0d62/result-v3.json", + "sha256": "28c79034129b4ea13aa7236c842b7b43bb0d6a5e67d84c22c6aca720d6bbed08", + "bytes": 29211 + }, + "render-retention-class-census.json": { + "original_path": "/tmp/openhcs-render-retention-4c0d62/census.json", + "sha256": "16f23b40d587e3d9e2005fd6e66d47e4d3264ed90e655b676c425095dfbd02d5", + "bytes": 25635 + }, + "compile_only-region.json": { + "original_path": "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/output/regions/region-2078682-140361443509952-0001.json", + "sha256": "cc69523fdae5fe92c4e96ec59609a19eb889fe744316b6ed8419ed0398313ab9", + "bytes": 15563 + }, + "execution-region.json": { + "original_path": "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/output/regions/region-2078682-140361443509952-0002.json", + "sha256": "f3c9e01926b069a0103ebb9c70d638a4944c1f429b809b3a7b2e9755efbf80e5", + "bytes": 25031 + }, + "numba-events-1.json": { + "original_path": "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/output/regions/numba-events-2078555-0001.json", + "sha256": "df39cc4538c692418c9590759ee2be67c03e712d31b38e59909a2acc414299da", + "bytes": 712 + }, + "numba-events-2.json": { + "original_path": "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/output/regions/numba-events-2078682-0001.json", + "sha256": "0cc87c64a35b572441cac162e7a953685af095892641222cef4b52a71d7ace03", + "bytes": 843 + }, + "numba-events-3.json": { + "original_path": "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/output/regions/numba-events-2078682-0002.json", + "sha256": "a80829256ff2e4f13f8cb14257b45138aacefb6e2726339b64be55d585c90fde", + "bytes": 840 + }, + "numba-events-4.json": { + "original_path": "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/output/regions/numba-events-2078682-0003.json", + "sha256": "c615a8d1c29b5da41bba3237cd1ee02e6f8eba72af2f73290f11ec684571661b", + "bytes": 712 + }, + "compiler-config-owner-decision.json": { + "original_path": "/tmp/openhcs-compile-config-owner-4c0d62/decision.json", + "sha256": "f99964708cfe2efa219dc0e254d780dd156f6c29ef720a08215cc29c2470a109", + "bytes": 8494 + }, + "compiler-config-class-census.json": { + "original_path": "/tmp/openhcs-compile-config-owner-4c0d62/census.json", + "sha256": "3b89e381bf150b66800e73439e6caecbc33170455ded87d67eb0dfbfdbbec855", + "bytes": 27972 + } + }, + "execution_outside_callable_fraction": 0.7307817861504636, + "raw_callable_count": 11, + "scope": "One 41-hook ordinary-path diagnostic, READY/startup/shutdown excluded, original OUTCOMES/memory/CPU5/official lease, no cProfile or runtime profiler. Both retained native outputs compared; no fresh native timing or paired performance gain.", + "qualification_corrections": [ + "The qualification late-getter label refers to all ten effective-config reads, including pre-capture reads; .192388s is an inclusive subset of state costs, not additional time or an eligible saving.", + "Installed owned ObjectState/python-introspect editables are distinguished from separately frozen canonical dependency source copies. The original 731-module AST census is a different epoch; no current global ownership proof is inferred.", + "Framework outside-callable time is an investigation envelope, not an assertion that all work is removable.", + "Zero native compile events applies to this observation; disk-cache loading and prior timing variance remain separate questions." + ], + "ownership_scope": "Current selected13modules71originalClassDefs refreshed through existingNRA, source/countmappingonly; no callback/alias/lifecycle equivalence or eligible wall implied." +} diff --git a/benchmark/results/perf_illum3_runtime_ledger_20261003/compile_only-region.json b/benchmark/results/perf_illum3_runtime_ledger_20261003/compile_only-region.json new file mode 100644 index 0000000000..8b1a510ab7 --- /dev/null +++ b/benchmark/results/perf_illum3_runtime_ledger_20261003/compile_only-region.json @@ -0,0 +1,430 @@ +{ + "pid": 2078682, + "thread": 140361443509952, + "region": "ZMQExecutionServer._execute_with_orchestrator", + "root_dimensions": "{\"case\": \"ExampleIlluminationCorrection_Example3\", \"compile_artifact_id\": null, \"execution_id\": \"78250d97-e324-4dbd-a014-62f28444b8a3\", \"kind\": \"compile_only\", \"plate\": \"/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images\"}", + "seconds": 0.5882621230557561, + "revision": "4c0d62e45c8afb85e34b69d7279cd00710f519b5", + "source": "/home/ts/code/projects/openhcs-shared-runtime-plumbing", + "stats": { + "ObjectState._compute_resolved_snapshot": { + "owner": "ObjectState._compute_resolved_snapshot", + "dimensions": "", + "phase": "compiler_config:_compute_resolved_snapshot", + "calls": 46, + "inclusive": 0.2531436877325177, + "exclusive": 0.2531436877325177 + }, + "ObjectState.__init__": { + "owner": "ObjectState.__init__", + "dimensions": "", + "phase": "compiler_config:__init__", + "calls": 23, + "inclusive": 0.35587291372939944, + "exclusive": 0.10272922599688172 + }, + "ObjectState._reconstruct_from_resolved": { + "owner": "ObjectState._reconstruct_from_resolved", + "dimensions": "", + "phase": "compiler_config:_reconstruct_from_resolved", + "calls": 343, + "inclusive": 0.05295955156907439, + "exclusive": 0.027533238753676414 + }, + "ObjectState.to_saved_resolved_object": { + "owner": "ObjectState.to_saved_resolved_object", + "dimensions": "", + "phase": "compiler_config:to_saved_resolved_object", + "calls": 21, + "inclusive": 0.027600623667240143, + "exclusive": 6.738491356372833e-05 + }, + "PipelineOrchestrator.get_effective_config": { + "owner": "PipelineOrchestrator.get_effective_config", + "dimensions": "", + "phase": "compiler_config:get_effective_config", + "calls": 10, + "inclusive": 0.19238800555467606, + "exclusive": 8.529284968972206e-05 + }, + "PipelineOrchestrator.create_context": { + "owner": "PipelineOrchestrator.create_context", + "dimensions": "", + "phase": "compiler_config:create_context", + "calls": 3, + "inclusive": 0.04923866596072912, + "exclusive": 0.0002901456318795681 + }, + "ZMQExecutionServer._initialize_orchestrator": { + "owner": "ZMQExecutionServer._initialize_orchestrator", + "dimensions": "", + "phase": "_initialize_orchestrator", + "calls": 1, + "inclusive": 0.1088466951623559, + "exclusive": 0.003273043315857649 + }, + "PipelineCompiler._axis_values_to_process": { + "owner": "PipelineCompiler._axis_values_to_process", + "dimensions": "", + "phase": "compiler_config:_axis_values_to_process", + "calls": 1, + "inclusive": 0.014383771922439337, + "exclusive": 7.574260234832764e-05 + }, + "PipelineCompiler._register_and_resolve_pipeline_once": { + "owner": "PipelineCompiler._register_and_resolve_pipeline_once", + "dimensions": "", + "phase": "compiler_config:_register_and_resolve_pipeline_once", + "calls": 1, + "inclusive": 0.19153947802260518, + "exclusive": 0.0021261521615087986 + }, + "PipelineCompiler._capture_pipeline_config": { + "owner": "PipelineCompiler._capture_pipeline_config", + "dimensions": "", + "phase": "compiler_config:_capture_pipeline_config", + "calls": 1, + "inclusive": 0.0017600241117179394, + "exclusive": 2.52528116106987e-06 + }, + "PipelineCompiler._resolve_global_step_axis_filters": { + "owner": "PipelineCompiler._resolve_global_step_axis_filters", + "dimensions": "", + "phase": "compiler_config:_resolve_global_step_axis_filters", + "calls": 1, + "inclusive": 0.013955585192888975, + "exclusive": 0.00047726836055517197 + }, + "AxisCompilationRequest.context_for": { + "owner": "AxisCompilationRequest.context_for", + "dimensions": "", + "phase": "compiler_config:context_for", + "calls": 1, + "inclusive": 0.018960938788950443, + "exclusive": 0.00017500855028629303 + }, + "CompilationSession.from_context": { + "owner": "CompilationSession.from_context", + "dimensions": "", + "phase": "compiler_config:from_context", + "calls": 1, + "inclusive": 1.70818530023098e-05, + "exclusive": 1.70818530023098e-05 + }, + "PipelineCompiler._configure_input_conversion_if_needed": { + "owner": "PipelineCompiler._configure_input_conversion_if_needed", + "dimensions": "", + "phase": "compiler_config:_configure_input_conversion_if_needed", + "calls": 1, + "inclusive": 0.013350545894354582, + "exclusive": 4.2282044887542725e-06 + }, + "PipelineCompiler.initialize_step_plans_for_context": { + "owner": "PipelineCompiler.initialize_step_plans_for_context", + "dimensions": "", + "phase": "compiler_config:initialize_step_plans_for_context", + "calls": 1, + "inclusive": 0.14918227866292, + "exclusive": 0.1358146509155631 + }, + "PipelineCompiler.declare_zarr_stores": { + "owner": "PipelineCompiler.declare_zarr_stores", + "dimensions": "", + "phase": "compiler_config:declare_zarr_stores", + "calls": 1, + "inclusive": 0.013443371281027794, + "exclusive": 4.089716821908951e-05 + }, + "PipelineCompiler._finalize_compilation": { + "owner": "PipelineCompiler._finalize_compilation", + "dimensions": "", + "phase": "compiler_config:_finalize_compilation", + "calls": 1, + "inclusive": 0.013896864838898182, + "exclusive": 0.00011343369260430336 + }, + "PipelineCompiler.compile_pipelines": { + "owner": "PipelineCompiler.compile_pipelines", + "dimensions": "", + "phase": "compiler_config:compile_pipelines", + "calls": 1, + "inclusive": 0.437725986354053, + "exclusive": 0.02060367353260517 + }, + "ZMQExecutionServer._execute_with_orchestrator | {\"case\": \"ExampleIlluminationCorrection_Example3\", \"compile_artifact_id\": null, \"execution_id\": \"78250d97-e324-4dbd-a014-62f28444b8a3\", \"kind\": \"compile_only\", \"plate\": \"/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images\"}": { + "owner": "ZMQExecutionServer._execute_with_orchestrator", + "dimensions": "{\"case\": \"ExampleIlluminationCorrection_Example3\", \"compile_artifact_id\": null, \"execution_id\": \"78250d97-e324-4dbd-a014-62f28444b8a3\", \"kind\": \"compile_only\", \"plate\": \"/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images\"}", + "phase": "_execute_with_orchestrator", + "calls": 1, + "inclusive": 0.5882621230557561, + "exclusive": 0.04168944153934717 + } + }, + "hooks_installed": [ + { + "module": "objectstate.object_state", + "owner": "ObjectState", + "method": "__init__", + "descriptor_kind": "function" + }, + { + "module": "objectstate.object_state", + "owner": "ObjectState", + "method": "_compute_resolved_snapshot", + "descriptor_kind": "function" + }, + { + "module": "objectstate.object_state", + "owner": "ObjectState", + "method": "_reconstruct_from_resolved", + "descriptor_kind": "function" + }, + { + "module": "objectstate.object_state", + "owner": "ObjectState", + "method": "to_saved_resolved_object", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.aligned_image_payload", + "owner": "", + "method": "stack_image_payload_context", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.aligned_image_payload", + "owner": "", + "method": "unstack_image_payload_context", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.measurement_feature_queries", + "owner": "MeasurementFeatureValueIndex", + "method": "from_columnar_table_by_object", + "descriptor_kind": "classmethod" + }, + { + "module": "openhcs.core.virtual_workspace_metadata", + "owner": "AtomicMetadataWriter", + "method": "publish_source_projection_metadata", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.runtime_stack_cache", + "owner": "RuntimeImageStackCache", + "method": "get", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.runtime_stack_cache", + "owner": "RuntimeImageStackCache", + "method": "store", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.steps.function_outputs", + "owner": "", + "method": "finalize_function_step_outputs", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.steps.function_outputs", + "owner": "OpenHCSMetadataWriter", + "method": "finalize_completed_plate", + "descriptor_kind": "classmethod" + }, + { + "module": "openhcs.core.steps.function_outputs", + "owner": "RuntimeArtifactMetadataTarget", + "method": "reconciliation_targets", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.steps.function_runtime", + "owner": "FunctionCoreExecutor", + "method": "execute", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.steps.function_runtime", + "owner": "PatternGroupRuntime", + "method": "_load_input_stack", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.steps.function_runtime", + "owner": "PatternGroupRuntime", + "method": "_validate_and_unstack", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.steps.function_runtime", + "owner": "PatternGroupRuntime", + "method": "_save_outputs", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.pipeline.compilation_session", + "owner": "CompilationSession", + "method": "from_context", + "descriptor_kind": "classmethod" + }, + { + "module": "openhcs.core.pipeline.compiler", + "owner": "PipelineCompiler", + "method": "compile_pipelines", + "descriptor_kind": "staticmethod" + }, + { + "module": "openhcs.core.pipeline.compiler", + "owner": "PipelineCompiler", + "method": "_register_and_resolve_pipeline_once", + "descriptor_kind": "staticmethod" + }, + { + "module": "openhcs.core.pipeline.compiler", + "owner": "PipelineCompiler", + "method": "initialize_step_plans_for_context", + "descriptor_kind": "staticmethod" + }, + { + "module": "openhcs.core.pipeline.compiler", + "owner": "PipelineCompiler", + "method": "_capture_pipeline_config", + "descriptor_kind": "staticmethod" + }, + { + "module": "openhcs.core.pipeline.compiler", + "owner": "PipelineCompiler", + "method": "_resolve_global_step_axis_filters", + "descriptor_kind": "staticmethod" + }, + { + "module": "openhcs.core.pipeline.compiler", + "owner": "PipelineCompiler", + "method": "_configure_input_conversion_if_needed", + "descriptor_kind": "staticmethod" + }, + { + "module": "openhcs.core.pipeline.compiler", + "owner": "PipelineCompiler", + "method": "declare_zarr_stores", + "descriptor_kind": "staticmethod" + }, + { + "module": "openhcs.core.pipeline.compiler", + "owner": "PipelineCompiler", + "method": "_finalize_compilation", + "descriptor_kind": "staticmethod" + }, + { + "module": "openhcs.core.pipeline.compiler", + "owner": "PipelineCompiler", + "method": "_axis_values_to_process", + "descriptor_kind": "staticmethod" + }, + { + "module": "openhcs.core.pipeline.compiler", + "owner": "AxisCompilationRequest", + "method": "context_for", + "descriptor_kind": "function" + }, + { + "module": "openhcs.processing.backends.lib_registry.unified_registry", + "owner": "RuntimeCallableInvocation", + "method": "call", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.orchestrator.analysis_consolidation", + "owner": "", + "method": "consolidate_analysis_outputs", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.orchestrator.worker_execution", + "owner": "", + "method": "execute_worker_lane", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.orchestrator.orchestrator", + "owner": "PipelineOrchestrator", + "method": "create_context", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.orchestrator.orchestrator", + "owner": "PipelineOrchestrator", + "method": "get_effective_config", + "descriptor_kind": "function" + }, + { + "module": "openhcs.runtime.zmq_execution_server", + "owner": "ZMQExecutionServer", + "method": "_execute_with_orchestrator", + "descriptor_kind": "function" + }, + { + "module": "openhcs.runtime.zmq_execution_server", + "owner": "ZMQExecutionServer", + "method": "_initialize_orchestrator", + "descriptor_kind": "function" + }, + { + "module": "openhcs.runtime.zmq_execution_server", + "owner": "ZMQExecutionServer", + "method": "_export_runtime_observation", + "descriptor_kind": "function" + }, + { + "module": "openhcs.interop.cellprofiler.runtime.function_contract_execution", + "owner": "CellProfilerFunctionContractExecutor", + "method": "execute", + "descriptor_kind": "function" + }, + { + "module": "openhcs.interop.cellprofiler.runtime.measurement_execution_support", + "owner": "ObjectMeasurementOutputRecorder", + "method": "record", + "descriptor_kind": "function" + }, + { + "module": "openhcs.interop.cellprofiler.runtime.output_recording", + "owner": "CellProfilerOutputRecorder", + "method": "record_module_outputs", + "descriptor_kind": "classmethod" + }, + { + "module": "openhcs.interop.cellprofiler.runtime.module_execution", + "owner": "CellProfilerModuleExecutor", + "method": "__call__", + "descriptor_kind": "function" + }, + { + "module": "openhcs.interop.cellprofiler.runtime.module_execution", + "owner": "CellProfilerModuleExecutor", + "method": "_image_request", + "descriptor_kind": "function" + } + ], + "profiling_enabled": false, + "runtime_profiling_enabled": false, + "diagnostic_errors": [], + "captures": false, + "root_started_monotonic_ns": 3816282952668134, + "root_ended_monotonic_ns": 3816283540961927, + "numba_events": { + "installed": true, + "counts": {}, + "record_count": 0, + "pending_record_count": 0, + "flushed_record_count": 0, + "refused_records": 0, + "errors": [], + "error_count": 0, + "record_bytes": 0, + "cap": 262144 + }, + "scope": "coarse same-run nested exclusive timing partition on the exact source revision recorded in this region. Diagnostic wrappers, no scientific or memory-observer substitutions. Not accepted performance comparison. Declared callable boundary includes filtering/decorated processing/metadata; not pure numerical kernels. Both existing profilers are disabled. These wrappers still add diagnostic overhead; no scaling to uninstrumented clocks or causal performance claims. Compiler/state boundaries and query boundary are nested under actual parents and excluded from parent-exclusive elapsed. This is a NEW 41-hook diagnostic version; inherited 24 boundary labels remain, site bytes are changed. ObjectState is verified against the actual source dependency path, not assumed inside the OpenHCS package." +} diff --git a/benchmark/results/perf_illum3_runtime_ledger_20261003/compiler-config-class-census.json b/benchmark/results/perf_illum3_runtime_ledger_20261003/compiler-config-class-census.json new file mode 100644 index 0000000000..9e1ddb4a74 --- /dev/null +++ b/benchmark/results/perf_illum3_runtime_ledger_20261003/compiler-config-class-census.json @@ -0,0 +1 @@ +{"head":"4c0d62e45c8afb85e34b69d7279cd00710f519b5","dependency_heads":{"ObjectState":"a01f8939b9bf4659502a33fe3febe62cab0f3c9a","python-introspect":"ed1e4eb15f7a0bf8315274d3e47858fa2b4372bb"},"original_global_census":{"path":"/home/ts/.local/state/openhcs-maintenance/20261003/compiler-config-transaction-map-v1/census.json","sha256":"60541e7bc14f20f6e9d698ac25ea6f094b5503a0670df2f893d83bb5564d3868","head":"480aed49b30448de4380af5c9274e48084e67b63","modules":731,"original_classes":5302,"canonical_classes":5290,"epoch":"ORIGINAL; not a universally current inventory"},"current_selected_modules":13,"current_selected_original_classes":71,"current_selected_canonical_classes":71,"source":[{"path":"openhcs/core/pipeline/compiler.py","sha256":"f9c368f046fd338ae51ee46251ecc24f4b246b00971db423a1f14fb4d8ca4a10","original_731_module_census_sha256":"f9c368f046fd338ae51ee46251ecc24f4b246b00971db423a1f14fb4d8ca4a10"},{"path":"openhcs/core/pipeline/compilation_session.py","sha256":"3c840a89baccc8665d415175458280fd904c1a837b7fd879e303d7dbca129a1c","original_731_module_census_sha256":"3c840a89baccc8665d415175458280fd904c1a837b7fd879e303d7dbca129a1c"},{"path":"openhcs/core/orchestrator/orchestrator.py","sha256":"ae64fbf28c96f7672fd389b4647b96b11b0b90caccd28e542984692e82e2baf9","original_731_module_census_sha256":"ae64fbf28c96f7672fd389b4647b96b11b0b90caccd28e542984692e82e2baf9"},{"path":"openhcs/core/context/processing_context.py","sha256":"fa587f726d3baac27f17b064db7230d58162408e4230d426f08acc91568b1c75","original_731_module_census_sha256":"fa587f726d3baac27f17b064db7230d58162408e4230d426f08acc91568b1c75"},{"path":"openhcs/core/config.py","sha256":"47d54358521bd7c7abfea1b3bd2cb4b8c24fd9a85826498437a6970122d59d74","original_731_module_census_sha256":"47d54358521bd7c7abfea1b3bd2cb4b8c24fd9a85826498437a6970122d59d74"},{"path":"openhcs/runtime/zmq_execution_server.py","sha256":"738450e5af28b1b54595d21685d43e3961273c0d3284ec8dae7161a558353f5d","original_731_module_census_sha256":"738450e5af28b1b54595d21685d43e3961273c0d3284ec8dae7161a558353f5d"},{"path":"openhcs/runtime/zmq_execution_signature.py","sha256":"41dcdb0a40446947a42a87482f27da051408015eec972e11e4d3329464db11ac","original_731_module_census_sha256":"41dcdb0a40446947a42a87482f27da051408015eec972e11e4d3329464db11ac"},{"path":"openhcs/runtime/zmq_compilation.py","sha256":"ad958c78ec9a26b778ad231501751f38ec6dd3d0c73ca2c1082207031fc6781f","original_731_module_census_sha256":"ad958c78ec9a26b778ad231501751f38ec6dd3d0c73ca2c1082207031fc6781f"},{"path":"external/ObjectState/src/objectstate/object_state.py","sha256":"735ce860ab650b281534e24e3c7354f37b64bb06832cf186d6b1f626f87108fe","original_731_module_census_sha256":"735ce860ab650b281534e24e3c7354f37b64bb06832cf186d6b1f626f87108fe"},{"path":"external/ObjectState/src/objectstate/object_state_registry.py","sha256":"f848746a9c2a023a616c37531691073ea7bdb8d28d7db3bca5c8a3dcb176107a","original_731_module_census_sha256":"f848746a9c2a023a616c37531691073ea7bdb8d28d7db3bca5c8a3dcb176107a"},{"path":"external/ObjectState/src/objectstate/context_manager.py","sha256":"a01b0fc8aeb9b5b0888346f725f92cbfd96b90963bf20001b5414a5e6cd6c0e6","original_731_module_census_sha256":"a01b0fc8aeb9b5b0888346f725f92cbfd96b90963bf20001b5414a5e6cd6c0e6"},{"path":"external/ObjectState/src/objectstate/lazy_factory.py","sha256":"64ae924c176757863a84eef19b9df0eff516e32443c2661134ec2ae72f234bf9","original_731_module_census_sha256":"f81ce722695dbab98fd02658f2015e5cb93c1e1e69547d24249e62d896e2a828"},{"path":"external/ObjectState/src/objectstate/global_config.py","sha256":"a8187811f87806579640a8147f287114e7127401530178dd37e8d28ed788a6ae","original_731_module_census_sha256":"a8187811f87806579640a8147f287114e7127401530178dd37e8d28ed788a6ae"}],"classes":[{"file":"openhcs/core/pipeline/compiler.py","name":"AxisCompilationRequest","line":194,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.core.pipeline.compiler.AxisCompilationRequest","bases":[],"resolved_bases":[]},{"file":"openhcs/core/pipeline/compiler.py","name":"PipelineCompiler","line":223,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.core.pipeline.compiler.PipelineCompiler","bases":[],"resolved_bases":[]},{"file":"openhcs/core/pipeline/compilation_session.py","name":"CompilationPlateScope","line":34,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.core.pipeline.compilation_session.CompilationPlateScope","bases":[],"resolved_bases":[]},{"file":"openhcs/core/pipeline/compilation_session.py","name":"CompilationPathResolver","line":86,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.core.pipeline.compilation_session.CompilationPathResolver","bases":[],"resolved_bases":[]},{"file":"openhcs/core/pipeline/compilation_session.py","name":"CompilationSession","line":160,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.core.pipeline.compilation_session.CompilationSession","bases":[],"resolved_bases":[]},{"file":"openhcs/core/pipeline/compilation_session.py","name":"ResolvedPipelineDefinition","line":331,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.core.pipeline.compilation_session.ResolvedPipelineDefinition","bases":[],"resolved_bases":[]},{"file":"openhcs/core/orchestrator/orchestrator.py","name":"PipelineOrchestrator","line":79,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.core.orchestrator.orchestrator.PipelineOrchestrator","bases":[],"resolved_bases":[]},{"file":"openhcs/core/context/processing_context.py","name":"RequiredVisualizer","line":49,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.core.context.processing_context.RequiredVisualizer","bases":[],"resolved_bases":[]},{"file":"openhcs/core/context/processing_context.py","name":"ProcessingContext","line":67,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.core.context.processing_context.ProcessingContext","bases":[],"resolved_bases":[]},{"file":"openhcs/core/config.py","name":"StreamingConfigMeta","line":53,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.core.config.StreamingConfigMeta","bases":["EnableableMeta","AutoRegisterMeta"],"resolved_bases":[]},{"file":"openhcs/core/config.py","name":"MaterializationBackend","line":75,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.core.config.MaterializationBackend","bases":["Enum"],"resolved_bases":[]},{"file":"openhcs/core/config.py","name":"WellFilterMode","line":84,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.core.config.WellFilterMode","bases":["Enum"],"resolved_bases":[]},{"file":"openhcs/core/config.py","name":"NormalizationMethod","line":102,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.core.config.NormalizationMethod","bases":["Enum"],"resolved_bases":[]},{"file":"openhcs/core/config.py","name":"MultiprocessingStartMethod","line":145,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.core.config.MultiprocessingStartMethod","bases":["Enum"],"resolved_bases":[]},{"file":"openhcs/core/config.py","name":"GlobalPipelineConfig","line":156,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.core.config.GlobalPipelineConfig","bases":["AnnotatedDataclassValidationMixin"],"resolved_bases":[]},{"file":"openhcs/core/config.py","name":"NapariDimensionMode","line":240,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.core.config.NapariDimensionMode","bases":["Enum"],"resolved_bases":[]},{"file":"openhcs/core/config.py","name":"NapariVariableSizeHandling","line":247,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.core.config.NapariVariableSizeHandling","bases":["Enum"],"resolved_bases":[]},{"file":"openhcs/core/config.py","name":"NapariDisplayConfig","line":257,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.core.config.NapariDisplayConfig","bases":["ViewerDisplayConfigABC","AnnotatedDataclassValidationMixin"],"resolved_bases":[]},{"file":"openhcs/core/config.py","name":"FijiDimensionMode","line":394,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.core.config.FijiDimensionMode","bases":["Enum"],"resolved_bases":[]},{"file":"openhcs/core/config.py","name":"FijiDisplayConfig","line":414,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.core.config.FijiDisplayConfig","bases":["ViewerDisplayConfigABC","AnnotatedDataclassValidationMixin"],"resolved_bases":[]},{"file":"openhcs/core/config.py","name":"WellFilterConfig","line":552,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.core.config.WellFilterConfig","bases":["AnnotatedDataclassValidationMixin"],"resolved_bases":[]},{"file":"openhcs/core/config.py","name":"ZarrConfig","line":613,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.core.config.ZarrConfig","bases":["AnnotatedDataclassValidationMixin","_polystore_config.ZarrConfig"],"resolved_bases":[]},{"file":"openhcs/core/config.py","name":"TiffConfig","line":627,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.core.config.TiffConfig","bases":["AnnotatedDataclassValidationMixin","_polystore_config.TiffConfig"],"resolved_bases":[]},{"file":"openhcs/core/config.py","name":"VFSConfig","line":634,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.core.config.VFSConfig","bases":["AnnotatedDataclassValidationMixin"],"resolved_bases":[]},{"file":"openhcs/core/config.py","name":"DtypeConfig","line":666,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.core.config.DtypeConfig","bases":["AnnotatedDataclassValidationMixin","RuntimeSliceInvariantValue","DtypeConversionConfig"],"resolved_bases":[]},{"file":"openhcs/core/config.py","name":"ProcessingConfig","line":699,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.core.config.ProcessingConfig","bases":["AnnotatedDataclassValidationMixin"],"resolved_bases":[]},{"file":"openhcs/core/config.py","name":"SequentialProcessingConfig","line":751,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.core.config.SequentialProcessingConfig","bases":["AnnotatedDataclassValidationMixin"],"resolved_bases":[]},{"file":"openhcs/core/config.py","name":"AnalysisConsolidationConfig","line":774,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.core.config.AnalysisConsolidationConfig","bases":["AnnotatedDataclassValidationMixin","Enableable"],"resolved_bases":[]},{"file":"openhcs/core/config.py","name":"PlateMetadataConfig","line":815,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.core.config.PlateMetadataConfig","bases":["AnnotatedDataclassValidationMixin"],"resolved_bases":[]},{"file":"openhcs/core/config.py","name":"ExperimentalAnalysisConfig","line":838,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.core.config.ExperimentalAnalysisConfig","bases":["AnnotatedDataclassValidationMixin"],"resolved_bases":[]},{"file":"openhcs/core/config.py","name":"PathPlanningConfig","line":891,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.core.config.PathPlanningConfig","bases":["WellFilterConfig"],"resolved_bases":["home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.core.config.WellFilterConfig"]},{"file":"openhcs/core/config.py","name":"StepWellFilterConfig","line":931,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.core.config.StepWellFilterConfig","bases":["WellFilterConfig"],"resolved_bases":["home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.core.config.WellFilterConfig"]},{"file":"openhcs/core/config.py","name":"StepMaterializationConfig","line":942,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.core.config.StepMaterializationConfig","bases":["Enableable","StepWellFilterConfig","PathPlanningConfig"],"resolved_bases":["home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.core.config.StepWellFilterConfig","home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.core.config.PathPlanningConfig"]},{"file":"openhcs/core/config.py","name":"StreamingDefaults","line":983,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.core.config.StreamingDefaults","bases":["Enableable","StepWellFilterConfig"],"resolved_bases":["home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.core.config.StepWellFilterConfig"]},{"file":"openhcs/core/config.py","name":"StreamingConfig","line":1037,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.core.config.StreamingConfig","bases":["StreamingDefaults","ABC"],"resolved_bases":["home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.core.config.StreamingDefaults"]},{"file":"openhcs/core/config.py","name":"NapariStreamingConfig","line":1156,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.core.config.NapariStreamingConfig","bases":["StreamingConfigBehaviorMixin","StreamingConfig","NapariDisplayConfig"],"resolved_bases":["home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.core.config.StreamingConfig","home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.core.config.NapariDisplayConfig"]},{"file":"openhcs/core/config.py","name":"FijiStreamingConfig","line":1188,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.core.config.FijiStreamingConfig","bases":["StreamingConfigBehaviorMixin","StreamingConfig","FijiDisplayConfig"],"resolved_bases":["home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.core.config.StreamingConfig","home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.core.config.FijiDisplayConfig"]},{"file":"openhcs/core/config.py","name":"CompilationDebugConfig","line":1206,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.core.config.CompilationDebugConfig","bases":["AnnotatedDataclassValidationMixin","Enableable"],"resolved_bases":[]},{"file":"openhcs/runtime/zmq_execution_server.py","name":"ZMQExecutionContext","line":60,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.runtime.zmq_execution_server.ZMQExecutionContext","bases":[],"resolved_bases":[]},{"file":"openhcs/runtime/zmq_execution_server.py","name":"FunctionCatalogExecutionServer","line":126,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.runtime.zmq_execution_server.FunctionCatalogExecutionServer","bases":["ExecutionServer"],"resolved_bases":[]},{"file":"openhcs/runtime/zmq_execution_server.py","name":"ZMQExecutionServer","line":161,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.runtime.zmq_execution_server.ZMQExecutionServer","bases":["FunctionCatalogExecutionServer"],"resolved_bases":["home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.runtime.zmq_execution_server.FunctionCatalogExecutionServer"]},{"file":"openhcs/runtime/zmq_execution_signature.py","name":"ZMQAuxiliaryParamField","line":36,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.runtime.zmq_execution_signature.ZMQAuxiliaryParamField","bases":["Enum"],"resolved_bases":[]},{"file":"openhcs/runtime/zmq_execution_signature.py","name":"ZMQRuntimeObservationExportScope","line":44,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.runtime.zmq_execution_signature.ZMQRuntimeObservationExportScope","bases":["Enum"],"resolved_bases":[]},{"file":"openhcs/runtime/zmq_execution_signature.py","name":"ZMQAuxiliaryExecutionParams","line":52,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.runtime.zmq_execution_signature.ZMQAuxiliaryExecutionParams","bases":[],"resolved_bases":[]},{"file":"openhcs/runtime/zmq_execution_signature.py","name":"OpenHCSExecutionConfigBundle","line":206,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.runtime.zmq_execution_signature.OpenHCSExecutionConfigBundle","bases":[],"resolved_bases":[]},{"file":"openhcs/runtime/zmq_execution_signature.py","name":"ZMQExecutionIdentity","line":214,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.runtime.zmq_execution_signature.ZMQExecutionIdentity","bases":[],"resolved_bases":[]},{"file":"openhcs/runtime/zmq_execution_signature.py","name":"ZMQExecutionCompileControl","line":240,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.runtime.zmq_execution_signature.ZMQExecutionCompileControl","bases":[],"resolved_bases":[]},{"file":"openhcs/runtime/zmq_execution_signature.py","name":"ZMQExecutionConfigTransport","line":283,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.runtime.zmq_execution_signature.ZMQExecutionConfigTransport","bases":[],"resolved_bases":[]},{"file":"openhcs/runtime/zmq_execution_signature.py","name":"ZMQExecutionRequestPayload","line":307,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.runtime.zmq_execution_signature.ZMQExecutionRequestPayload","bases":[],"resolved_bases":[]},{"file":"openhcs/runtime/zmq_compilation.py","name":"ZMQCompilationResult","line":53,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.runtime.zmq_compilation.ZMQCompilationResult","bases":[],"resolved_bases":[]},{"file":"openhcs/runtime/zmq_compilation.py","name":"ZMQCompilationRequest","line":69,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.runtime.zmq_compilation.ZMQCompilationRequest","bases":[],"resolved_bases":[]},{"file":"openhcs/runtime/zmq_compilation.py","name":"ZMQCompileArtifactRecord","line":219,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.runtime.zmq_compilation.ZMQCompileArtifactRecord","bases":[],"resolved_bases":[]},{"file":"external/ObjectState/src/objectstate/object_state.py","name":"FieldProxy","line":48,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.external.ObjectState.src.objectstate.object_state.FieldProxy","bases":[],"resolved_bases":[]},{"file":"external/ObjectState/src/objectstate/object_state.py","name":"ObjectState","line":120,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.external.ObjectState.src.objectstate.object_state.ObjectState","bases":[],"resolved_bases":[]},{"file":"external/ObjectState/src/objectstate/object_state_registry.py","name":"ObjectStateRegistrySubscription","line":46,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.external.ObjectState.src.objectstate.object_state_registry.ObjectStateRegistrySubscription","bases":[],"resolved_bases":[]},{"file":"external/ObjectState/src/objectstate/object_state_registry.py","name":"TimeTravelTransaction","line":63,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.external.ObjectState.src.objectstate.object_state_registry.TimeTravelTransaction","bases":[],"resolved_bases":[]},{"file":"external/ObjectState/src/objectstate/object_state_registry.py","name":"TimeTravelScopeReconciliation","line":74,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.external.ObjectState.src.objectstate.object_state_registry.TimeTravelScopeReconciliation","bases":[],"resolved_bases":[]},{"file":"external/ObjectState/src/objectstate/object_state_registry.py","name":"PreparedStateRestore","line":82,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.external.ObjectState.src.objectstate.object_state_registry.PreparedStateRestore","bases":[],"resolved_bases":[]},{"file":"external/ObjectState/src/objectstate/object_state_registry.py","name":"TimeTravelScopeChange","line":94,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.external.ObjectState.src.objectstate.object_state_registry.TimeTravelScopeChange","bases":[],"resolved_bases":[]},{"file":"external/ObjectState/src/objectstate/object_state_registry.py","name":"TimeTravelStateDiff","line":109,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.external.ObjectState.src.objectstate.object_state_registry.TimeTravelStateDiff","bases":[],"resolved_bases":[]},{"file":"external/ObjectState/src/objectstate/object_state_registry.py","name":"TimeTravelChangeSet","line":133,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.external.ObjectState.src.objectstate.object_state_registry.TimeTravelChangeSet","bases":[],"resolved_bases":[]},{"file":"external/ObjectState/src/objectstate/object_state_registry.py","name":"DeferredFieldInvalidation","line":149,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.external.ObjectState.src.objectstate.object_state_registry.DeferredFieldInvalidation","bases":[],"resolved_bases":[]},{"file":"external/ObjectState/src/objectstate/object_state_registry.py","name":"ObjectStateRegistry","line":159,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.external.ObjectState.src.objectstate.object_state_registry.ObjectStateRegistry","bases":[],"resolved_bases":[]},{"file":"external/ObjectState/src/objectstate/lazy_factory.py","name":"GlobalConfigMeta","line":340,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.external.ObjectState.src.objectstate.lazy_factory.GlobalConfigMeta","bases":["type"],"resolved_bases":[]},{"file":"external/ObjectState/src/objectstate/lazy_factory.py","name":"GlobalConfigBase","line":352,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.external.ObjectState.src.objectstate.lazy_factory.GlobalConfigBase","bases":[],"resolved_bases":[]},{"file":"external/ObjectState/src/objectstate/lazy_factory.py","name":"LazyResolutionDataclass","line":368,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.external.ObjectState.src.objectstate.lazy_factory.LazyResolutionDataclass","bases":["ABC"],"resolved_bases":[]},{"file":"external/ObjectState/src/objectstate/lazy_factory.py","name":"LazyDataclass","line":372,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.external.ObjectState.src.objectstate.lazy_factory.LazyDataclass","bases":["LazyResolutionDataclass"],"resolved_bases":["home.ts.code.projects.openhcs-shared-runtime-plumbing.external.ObjectState.src.objectstate.lazy_factory.LazyResolutionDataclass"]},{"file":"external/ObjectState/src/objectstate/lazy_factory.py","name":"LazyMethodBindings","line":561,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.external.ObjectState.src.objectstate.lazy_factory.LazyMethodBindings","bases":[],"resolved_bases":[]},{"file":"external/ObjectState/src/objectstate/lazy_factory.py","name":"LazyDataclassFactory","line":795,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.external.ObjectState.src.objectstate.lazy_factory.LazyDataclassFactory","bases":[],"resolved_bases":[]},{"file":"external/ObjectState/src/objectstate/lazy_factory.py","name":"abbreviation","line":1336,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.external.ObjectState.src.objectstate.lazy_factory.abbreviation","bases":[],"resolved_bases":[]},{"file":"external/ObjectState/src/objectstate/global_config.py","name":"GlobalContextValues","line":53,"canonical":true,"symbol":"home.ts.code.projects.openhcs-shared-runtime-plumbing.external.ObjectState.src.objectstate.global_config.GlobalContextValues","bases":[],"resolved_bases":[]}],"calls":[{"file":"openhcs/core/pipeline/compiler.py","line":206,"owner":"AxisCompilationRequest.context_for","call":"self.orchestrator.create_context(axis_id)"},{"file":"openhcs/core/pipeline/compiler.py","line":373,"owner":"PipelineCompiler._configure_input_conversion_if_needed","call":"orchestrator.get_effective_config()"},{"file":"openhcs/core/pipeline/compiler.py","line":607,"owner":"PipelineCompiler.declare_zarr_stores","call":"orchestrator.get_effective_config()"},{"file":"openhcs/core/pipeline/compiler.py","line":1028,"owner":"PipelineCompiler._axis_values_to_process","call":"orchestrator.get_effective_config()"},{"file":"openhcs/core/pipeline/compiler.py","line":1197,"owner":"PipelineCompiler._replace_pipeline_with_resolved_steps","call":"step_state.to_saved_resolved_object()"},{"file":"openhcs/core/pipeline/compiler.py","line":1232,"owner":"PipelineCompiler._capture_pipeline_config","call":"pipeline_config_state.to_saved_resolved_object()"},{"file":"openhcs/core/pipeline/compiler.py","line":1246,"owner":"PipelineCompiler._resolve_global_step_axis_filters","call":"orchestrator.create_context('temp')"},{"file":"openhcs/core/pipeline/compiler.py","line":1749,"owner":"PipelineCompiler._finalize_compilation","call":"orchestrator.get_effective_config()"},{"file":"openhcs/core/pipeline/compiler.py","line":1860,"owner":"PipelineCompiler.compile_pipelines","call":"orchestrator.get_effective_config()"},{"file":"openhcs/core/pipeline/compiler.py","line":1890,"owner":"PipelineCompiler.compile_pipelines","call":"orchestrator.get_effective_config()"},{"file":"openhcs/core/pipeline/compiler.py","line":2052,"owner":"_resolve_step_axis_filters","call":"step_state.get_provenance(f'{field_name}.well_filter')"},{"file":"openhcs/core/orchestrator/orchestrator.py","line":204,"owner":"PipelineOrchestrator.__init__","call":"self.get_effective_config()"},{"file":"openhcs/core/orchestrator/orchestrator.py","line":319,"owner":"PipelineOrchestrator.initialize_microscope_handler","call":"self.get_effective_config()"},{"file":"openhcs/core/orchestrator/orchestrator.py","line":438,"owner":"PipelineOrchestrator.initialize","call":"self.get_effective_config()"},{"file":"openhcs/core/orchestrator/orchestrator.py","line":510,"owner":"PipelineOrchestrator._ensure_openhcs_metadata","call":"self.create_context(axis_id='metadata_init')"},{"file":"openhcs/core/orchestrator/orchestrator.py","line":541,"owner":"PipelineOrchestrator.get_results_path","call":"self.get_effective_config()"},{"file":"openhcs/core/orchestrator/orchestrator.py","line":569,"owner":"PipelineOrchestrator.create_context","call":"self.get_effective_config()"},{"file":"openhcs/core/orchestrator/orchestrator.py","line":961,"owner":"PipelineOrchestrator.apply_pipeline_config","call":"ObjectState(previous_config).to_saved_resolved_object()"},{"file":"openhcs/core/orchestrator/orchestrator.py","line":966,"owner":"PipelineOrchestrator.apply_pipeline_config","call":"ObjectState(pipeline_config).to_saved_resolved_object()"},{"file":"openhcs/core/orchestrator/orchestrator.py","line":1023,"owner":"PipelineOrchestrator.get_effective_config","call":"ObjectState(self.pipeline_config).to_saved_resolved_object()"}],"authority":"Existing NRA SourceModule/ModuleSyntaxIndex/build_class_family_index; no handwritten class/schema roster, no production imports or execution. Selected syntax/class binding refreshed against actual owned dependency sources; external family MRO/extension callback purity OPEN."} diff --git a/benchmark/results/perf_illum3_runtime_ledger_20261003/compiler-config-owner-decision.json b/benchmark/results/perf_illum3_runtime_ledger_20261003/compiler-config-owner-decision.json new file mode 100644 index 0000000000..ae49796286 --- /dev/null +++ b/benchmark/results/perf_illum3_runtime_ledger_20261003/compiler-config-owner-decision.json @@ -0,0 +1,122 @@ +{ + "status": "READ_ONLY; CONDITIONAL_POSTCAPTURE_ROUTE_UNQUALIFIED; NO_IMPLEMENTATION", + "source_head": "4c0d62e45c8afb85e34b69d7279cd00710f519b5", + "actual_dependency_heads": { + "ObjectState": "a01f8939b9bf4659502a33fe3febe62cab0f3c9a", + "python-introspect": "ed1e4eb15f7a0bf8315274d3e47858fa2b4372bb" + }, + "current_selected_NRA_class_census": "71 original/71 canonical classes in13 actual owned determining modules; whole731-module census remains historical. Only lazy_factory differs among selected modules from that original census. Unrefreshed external/MRO and future-head bindings OPEN.", + "diagnostic": { + "observations": "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/output/observations.json", + "sha256": "c8b1e3f0704682e2cad20b7a5e27b1651e38b677e74f31d6477eb163185106fd", + "regions": [ + { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/output/regions/region-2078682-140361443509952-0001.json", + "sha256": "cc69523fdae5fe92c4e96ec59609a19eb889fe744316b6ed8419ed0398313ab9" + }, + { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/output/regions/region-2078682-140361443509952-0002.json", + "sha256": "f3c9e01926b069a0103ebb9c70d638a4944c1f429b809b3a7b2e9755efbf80e5" + } + ], + "compile_root_seconds": 0.5882621230557561, + "ObjectState_disjoint_seconds": 0.3834735373966396, + "all10_getter_inclusive_seconds": 0.19238800555467606, + "counts": { + "PipelineCompiler.compile_pipelines": 1, + "PipelineCompiler._register_and_resolve_pipeline_once": 1, + "PipelineCompiler._capture_pipeline_config": 1, + "AxisCompilationRequest.context_for": 1, + "CompilationSession.from_context": 1, + "PipelineOrchestrator.create_context": 3, + "PipelineOrchestrator.get_effective_config": 10, + "ObjectState.__init__": 23, + "ObjectState._compute_resolved_snapshot": 46, + "ObjectState._reconstruct_from_resolved": 343, + "ObjectState.to_saved_resolved_object": 21 + }, + "execution_ObjectState_disjoint_seconds": 0.05523737985640764, + "attribution_law": "Owner aggregate only; no percall timing/source frames. Normal source path reconciles counts, not proof of individual event provenance or equal cost." + }, + "before_captured_pipeline_config": [ + { + "consumer": "orchestrator Zarr registry setup", + "source": "orchestrator.py:204" + }, + { + "consumer": "microscope initialization", + "source": "orchestrator.py:319" + }, + { + "consumer": "workspace VFS logging", + "source": "orchestrator.py:438" + }, + { + "consumer": "metadata_init context", + "source": "orchestrator.py:510 -> :569" + }, + { + "consumer": "axis selection", + "source": "compiler.py:1028" + } + ], + "after_captured_pipeline_config": [ + { + "consumer": "temp axis-filter context", + "source": "compiler.py:1246 -> orchestrator.py:569" + }, + { + "consumer": "actual axis context", + "source": "compiler.py:206 -> orchestrator.py:569" + }, + { + "consumer": "input-conversion VFS", + "source": "compiler.py:373" + }, + { + "consumer": "Zarr-declaration VFS", + "source": "compiler.py:607" + }, + { + "consumer": "final worker log after cleanup/stripping/COMPILED", + "source": "compiler.py:1749" + } + ], + "source_count_reconciliation": "Normal10-step1-axis path:13 registered compiler states(global,pipeline,delegated orchestrator,10steps)+10 ephemeral getters=23; two initial live/saved snapshots each=46. Three contexts are metadata_init/temp/axis. Arbitrary extension paths with coincident aggregate counts remain OPEN.", + "owners": [ + "ZMQExecutionContext.configs/OpenHCSExecutionConfigBundle retains decoded global_pipeline and authored lazy plate_pipeline. These are not a captured concrete merged effective config; passing either directly is wrong.", + "Server installs request global context and initializes orchestrator before registered compiler capture. Early reads can observe handler/default/validator callback epochs.", + "PipelineCompiler registers named saved hierarchy and resolves submitted steps once, retaining step ObjectStates for actual live inherited-field provenance.", + "_capture_pipeline_config produces saved concrete GlobalPipelineConfig. resolve_declared_dataclass_paths normalizes its paths; local effective_config then feeds AxisCompilationRequest.global_config and CompilationSession.global_config.", + "AxisCompilationRequest.context_for creates fresh context then overwrites captured policy/filters/analysis/plate fields. TIFF and auto-add are currently fresh. CompilationSession already owns captured VFS/runtime configuration." + ], + "strongest_existing_nominal_route": "A common PipelineOrchestrator context-construction algorithm can accept explicit existing captured compiler config while its public fresh context path stays current. Local effective_config supplies temp context before AxisCompilationRequest exists; AxisCompilationRequest/CompilationSession supply axis/VFS consumers. Finalization would need its same saved-source/late-validation law admitted separately. This is the previously conditional snapshot-owner route, not a new proved fast path/cache/wrapper.", + "counterexamples": [ + "Saved registered state has scoped ancestor sources; fresh getter creates scope-less state resolving current saved global context. Sharing authored object does not prove equal answers.", + "test_compiler_captures_scoped_worker_count changes global context after capture and proves captured worker count remains old. Fresh getter can instead read changed global values for TIFF/auto-add/VFS. Signature equality cannot prove same internal config epoch.", + "Handler get_component_keys/get_available_backends and extension planning callbacks can replace global/orchestrator config after capture. Changed source is rejected on future client submissions, but an internal callback may change effective values without changing request strings.", + "_reconstruct_from_resolved invokes real actual subtype constructors/validators every read. A later callback-only error is presently observable. Finalization cleans scopes, strips steps and sets COMPILED before its fresh getter; moving/removing it changes partial effects and first errors.", + "GlobalPipelineConfig and built-in TiffConfig are frozen; prior blanket mutable-TIFF claim is false. ProcessingContext attributes are independently replaceable. Mutable extension/container values, subtype behavior and constructor/error epochs still need controls.", + "_resolve_step_axis_filters still invokes step_state.get_provenance; live provenance is a production consumer. Unconditional live=saved collapse or deleting live snapshots remains invalid.", + "Captured config paths are normalized while fresh/early reads may preserve authored relative values. Do not silently substitute field authorities or add a synchronized field mirror." + ], + "signature_law": "Independent client compile and execution builder renders remain; mutated authored pipeline/config changes compilation_signature and reuse_artifact rejects mismatches. This protects external next-request epochs, not callback mutations inside a compile transaction.", + "maximum_payoff": { + "all10_getter_upper_envelope_seconds": 0.19238800555467606, + "eligible5_postcapture_seconds": "UNKNOWN; no average division or nested-clock sums permitted.", + "entire_family_upper_envelope_seconds": 0.3834735373966396, + "mandatory_in_entire_family": "Registered hierarchy+saved resolution+live provenance+value validation+early initialization. Not a removable cost.", + "Illum3_total_2x_gap_seconds": 1.22610824, + "all10_getter_fraction_of_total_gap": 0.15690947934146177, + "execution_gain_claim": 0, + "decision": "No current qualified route materially removes the0.383s family. Five-read capture migration cannot claim0.192s either. Do not polish: source-law/control qualification and same-phase eligible timing would be necessary before whole compiler prototype." + }, + "custody": { + "production_edits": false, + "OpenHCS_or_dependency_runtime_imports": false, + "config_replays_or_scientific_runs": false, + "new_environment_checkout_capture": false, + "original_artifacts_copied": false, + "only_NRA_static_indexes_executed": true + } +} diff --git a/benchmark/results/perf_illum3_runtime_ledger_20261003/controls.json b/benchmark/results/perf_illum3_runtime_ledger_20261003/controls.json new file mode 100644 index 0000000000..a25727674b --- /dev/null +++ b/benchmark/results/perf_illum3_runtime_ledger_20261003/controls.json @@ -0,0 +1,37 @@ +{ + "status": "PASS", + "controls": [ + "original_called_once", + "argument_mutation_and_result_identity", + "signature_wrapped_code", + "nested_exclusive_conservation", + "error_identity_and_mutation", + "finally_restores_nested_state", + "root_namespace_no_overwrite", + "outside_root_ready_passthrough", + "fork_tls_reset", + "source_loader_real_declaring_path", + "instance_descriptor", + "classmethod_descriptor", + "staticmethod_descriptor", + "source_hash_drift_refusal", + "native_listener_startup_events", + "no_signature_repr_or_str", + "native_error_and_mutation_identity", + "timed_root_start_end_classification", + "between_roots_separate_from_startup", + "bounded_refusal_preserves_counts_and_red", + "other_thread_active_root_join", + "root_end_flush_without_atexit", + "no_repeated_or_discarded_event_prefix", + "cumulative_counts_survive_flush", + "flush_namespace_and_serialized_cap", + "no_native_receiver_or_argument_retention", + "installed_event_loader_original_module_then_listener", + "unchanged_storage_caps_and_41_hook_roster" + ], + "count": 28, + "site_sha256": "1a30063decce3e67ef2e0f3c033fb4f4cb9e6acf7e4c2b43b114c433f62c1bbe", + "controller_sha256": "e00e1cb2e195842f780ed289e97700db9fd52d7046414079fb186f90a7f98df5", + "scope": "Temporary test-owned source only, original result/error/mutation and descriptor/path/hash laws; no candidate pipeline import, compile, execution, profiler, snapshot or processing callback invocation." +} diff --git a/benchmark/results/perf_illum3_runtime_ledger_20261003/execution-region.json b/benchmark/results/perf_illum3_runtime_ledger_20261003/execution-region.json new file mode 100644 index 0000000000..c8f4dcb7b3 --- /dev/null +++ b/benchmark/results/perf_illum3_runtime_ledger_20261003/execution-region.json @@ -0,0 +1,630 @@ +{ + "pid": 2078682, + "thread": 140361443509952, + "region": "ZMQExecutionServer._execute_with_orchestrator", + "root_dimensions": "{\"case\": \"ExampleIlluminationCorrection_Example3\", \"compile_artifact_id\": \"78250d97-e324-4dbd-a014-62f28444b8a3\", \"execution_id\": \"6df21ea1-70f5-4fa0-9fac-ef5e7963204d\", \"kind\": \"execution\", \"plate\": \"/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images\"}", + "seconds": 0.4150526160374284, + "revision": "4c0d62e45c8afb85e34b69d7279cd00710f519b5", + "source": "/home/ts/code/projects/openhcs-shared-runtime-plumbing", + "stats": { + "ObjectState._compute_resolved_snapshot": { + "owner": "ObjectState._compute_resolved_snapshot", + "dimensions": "", + "phase": "compiler_config:_compute_resolved_snapshot", + "calls": 8, + "inclusive": 0.038815660402178764, + "exclusive": 0.038815660402178764 + }, + "ObjectState.__init__": { + "owner": "ObjectState.__init__", + "dimensions": "", + "phase": "compiler_config:__init__", + "calls": 4, + "inclusive": 0.0482184044085443, + "exclusive": 0.009402744006365538 + }, + "ObjectState._reconstruct_from_resolved": { + "owner": "ObjectState._reconstruct_from_resolved", + "dimensions": "", + "phase": "compiler_config:_reconstruct_from_resolved", + "calls": 92, + "inclusive": 0.013413879089057446, + "exclusive": 0.0070056007243692875 + }, + "ObjectState.to_saved_resolved_object": { + "owner": "ObjectState.to_saved_resolved_object", + "dimensions": "", + "phase": "compiler_config:to_saved_resolved_object", + "calls": 4, + "inclusive": 0.00701897544786334, + "exclusive": 1.3374723494052887e-05 + }, + "PipelineOrchestrator.get_effective_config": { + "owner": "PipelineOrchestrator.get_effective_config", + "dimensions": "", + "phase": "compiler_config:get_effective_config", + "calls": 4, + "inclusive": 0.055267936550080776, + "exclusive": 3.055669367313385e-05 + }, + "PipelineOrchestrator.create_context": { + "owner": "PipelineOrchestrator.create_context", + "dimensions": "", + "phase": "compiler_config:create_context", + "calls": 1, + "inclusive": 0.01351372292265296, + "exclusive": 6.313808262348175e-05 + }, + "ZMQExecutionServer._initialize_orchestrator": { + "owner": "ZMQExecutionServer._initialize_orchestrator", + "dimensions": "", + "phase": "_initialize_orchestrator", + "calls": 1, + "inclusive": 0.0607714201323688, + "exclusive": 0.005440345499664545 + }, + "RuntimeImageStackCache.get": { + "owner": "RuntimeImageStackCache.get", + "dimensions": "", + "phase": "get", + "calls": 10, + "inclusive": 4.8863235861063004e-05, + "exclusive": 4.8863235861063004e-05 + }, + "stack_image_payload_context": { + "owner": "stack_image_payload_context", + "dimensions": "", + "phase": "stack_image_payload_context", + "calls": 10, + "inclusive": 0.0036861514672636986, + "exclusive": 0.0036861514672636986 + }, + "PatternGroupRuntime._load_input_stack": { + "owner": "PatternGroupRuntime._load_input_stack", + "dimensions": "", + "phase": "_load_input_stack", + "calls": 10, + "inclusive": 0.023979938589036465, + "exclusive": 0.021657294128090143 + }, + "CellProfilerModuleExecutor._image_request": { + "owner": "CellProfilerModuleExecutor._image_request", + "dimensions": "", + "phase": "_image_request", + "calls": 9, + "inclusive": 0.006721616256982088, + "exclusive": 0.006721616256982088 + }, + "RuntimeCallableInvocation.call | function=openhcs.processing.backends.cellprofiler.primary_objects.identify_primary_objects;view=1": { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.primary_objects.identify_primary_objects;view=1", + "phase": "declared_callable_boundary", + "calls": 1, + "inclusive": 0.02976729581132531, + "exclusive": 0.02976729581132531 + }, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.primary_objects.identify_primary_objects": { + "owner": "CellProfilerFunctionContractExecutor.execute", + "dimensions": "function=openhcs.processing.backends.cellprofiler.primary_objects.identify_primary_objects", + "phase": "execute", + "calls": 1, + "inclusive": 0.03761508176103234, + "exclusive": 0.007847785949707031 + }, + "CellProfilerOutputRecorder.record_module_outputs": { + "owner": "CellProfilerOutputRecorder.record_module_outputs", + "dimensions": "", + "phase": "record_module_outputs", + "calls": 9, + "inclusive": 0.04083343083038926, + "exclusive": 0.04083343083038926 + }, + "CellProfilerModuleExecutor.__call__": { + "owner": "CellProfilerModuleExecutor.__call__", + "dimensions": "", + "phase": "__call__", + "calls": 9, + "inclusive": 0.19400596152991056, + "exclusive": 0.01576411724090576 + }, + "FunctionCoreExecutor.execute | function=identify_primary_objects": { + "owner": "FunctionCoreExecutor.execute", + "dimensions": "function=identify_primary_objects", + "phase": "execute", + "calls": 1, + "inclusive": 0.06343830097466707, + "exclusive": 0.0014636460691690445 + }, + "PatternGroupRuntime._validate_and_unstack": { + "owner": "PatternGroupRuntime._validate_and_unstack", + "dimensions": "", + "phase": "_validate_and_unstack", + "calls": 10, + "inclusive": 0.003616310190409422, + "exclusive": 0.002203939948230982 + }, + "RuntimeImageStackCache.store": { + "owner": "RuntimeImageStackCache.store", + "dimensions": "", + "phase": "store", + "calls": 10, + "inclusive": 5.160598084330559e-05, + "exclusive": 5.160598084330559e-05 + }, + "PatternGroupRuntime._save_outputs": { + "owner": "PatternGroupRuntime._save_outputs", + "dimensions": "", + "phase": "_save_outputs", + "calls": 10, + "inclusive": 0.007888393010944128, + "exclusive": 0.007836787030100822 + }, + "finalize_function_step_outputs": { + "owner": "finalize_function_step_outputs", + "dimensions": "", + "phase": "finalize_function_step_outputs", + "calls": 10, + "inclusive": 0.033279507886618376, + "exclusive": 0.025323559064418077 + }, + "RuntimeCallableInvocation.call | function=openhcs.processing.backends.cellprofiler.morphology.expand_or_shrink_objects;view=1": { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.morphology.expand_or_shrink_objects;view=1", + "phase": "declared_callable_boundary", + "calls": 1, + "inclusive": 0.02096757711842656, + "exclusive": 0.02096757711842656 + }, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.morphology.expand_or_shrink_objects": { + "owner": "CellProfilerFunctionContractExecutor.execute", + "dimensions": "function=openhcs.processing.backends.cellprofiler.morphology.expand_or_shrink_objects", + "phase": "execute", + "calls": 1, + "inclusive": 0.022361161187291145, + "exclusive": 0.001393584068864584 + }, + "FunctionCoreExecutor.execute | function=expand_or_shrink_objects": { + "owner": "FunctionCoreExecutor.execute", + "dimensions": "function=expand_or_shrink_objects", + "phase": "execute", + "calls": 1, + "inclusive": 0.030518849845975637, + "exclusive": 0.0009220358915627003 + }, + "RuntimeCallableInvocation.call | function=openhcs.processing.backends.cellprofiler.image_math.image_math;view=1": { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.image_math.image_math;view=1", + "phase": "declared_callable_boundary", + "calls": 1, + "inclusive": 0.001754844095557928, + "exclusive": 0.001754844095557928 + }, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.image_math.image_math": { + "owner": "CellProfilerFunctionContractExecutor.execute", + "dimensions": "function=openhcs.processing.backends.cellprofiler.image_math.image_math", + "phase": "execute", + "calls": 1, + "inclusive": 0.005336186848580837, + "exclusive": 0.003581342753022909 + }, + "FunctionCoreExecutor.execute | function=image_math": { + "owner": "FunctionCoreExecutor.execute", + "dimensions": "function=image_math", + "phase": "execute", + "calls": 1, + "inclusive": 0.011326035019010305, + "exclusive": 0.0001370580866932869 + }, + "RuntimeCallableInvocation.call | function=openhcs.processing.backends.cellprofiler.image_geometry.mask_image;view=1": { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.image_geometry.mask_image;view=1", + "phase": "declared_callable_boundary", + "calls": 2, + "inclusive": 0.0009737634100019932, + "exclusive": 0.0009737634100019932 + }, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.image_geometry.mask_image": { + "owner": "CellProfilerFunctionContractExecutor.execute", + "dimensions": "function=openhcs.processing.backends.cellprofiler.image_geometry.mask_image", + "phase": "execute", + "calls": 2, + "inclusive": 0.002802106086164713, + "exclusive": 0.0018283426761627197 + }, + "FunctionCoreExecutor.execute | function=mask_image": { + "owner": "FunctionCoreExecutor.execute", + "dimensions": "function=mask_image", + "phase": "execute", + "calls": 2, + "inclusive": 0.010034974198788404, + "exclusive": 0.00037863291800022125 + }, + "RuntimeCallableInvocation.call | function=openhcs.processing.backends.cellprofiler.illumination.correct_illumination_calculate;view=1": { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.illumination.correct_illumination_calculate;view=1", + "phase": "declared_callable_boundary", + "calls": 2, + "inclusive": 0.05728212045505643, + "exclusive": 0.05728212045505643 + }, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.illumination.correct_illumination_calculate": { + "owner": "CellProfilerFunctionContractExecutor.execute", + "dimensions": "function=openhcs.processing.backends.cellprofiler.illumination.correct_illumination_calculate", + "phase": "execute", + "calls": 2, + "inclusive": 0.05979563295841217, + "exclusive": 0.0025135125033557415 + }, + "FunctionCoreExecutor.execute | function=correct_illumination_calculate": { + "owner": "FunctionCoreExecutor.execute", + "dimensions": "function=correct_illumination_calculate", + "phase": "execute", + "calls": 2, + "inclusive": 0.07384366216138005, + "exclusive": 0.002233616542071104 + }, + "RuntimeCallableInvocation.call | function=openhcs.processing.backends.cellprofiler.illumination.correct_illumination_apply;view=1": { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.illumination.correct_illumination_apply;view=1", + "phase": "declared_callable_boundary", + "calls": 2, + "inclusive": 0.0008326279930770397, + "exclusive": 0.0008326279930770397 + }, + "CellProfilerFunctionContractExecutor.execute | function=openhcs.processing.backends.cellprofiler.illumination.correct_illumination_apply": { + "owner": "CellProfilerFunctionContractExecutor.execute", + "dimensions": "function=openhcs.processing.backends.cellprofiler.illumination.correct_illumination_apply", + "phase": "execute", + "calls": 2, + "inclusive": 0.0027766283601522446, + "exclusive": 0.0019440003670752048 + }, + "FunctionCoreExecutor.execute | function=correct_illumination_apply": { + "owner": "FunctionCoreExecutor.execute", + "dimensions": "function=correct_illumination_apply", + "phase": "execute", + "calls": 2, + "inclusive": 0.012327289674431086, + "exclusive": 0.0023481608368456364 + }, + "RuntimeCallableInvocation.call | function=openhcs.processing.backends.cellprofiler.save_images.save_images;view=1": { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.save_images.save_images;view=1", + "phase": "declared_callable_boundary", + "calls": 2, + "inclusive": 0.00016149505972862244, + "exclusive": 0.00016149505972862244 + }, + "FunctionCoreExecutor.execute | function=save_images": { + "owner": "FunctionCoreExecutor.execute", + "dimensions": "function=save_images", + "phase": "execute", + "calls": 2, + "inclusive": 0.005585906561464071, + "exclusive": 0.005424411501735449 + }, + "AtomicMetadataWriter.publish_source_projection_metadata": { + "owner": "AtomicMetadataWriter.publish_source_projection_metadata", + "dimensions": "", + "phase": "publish_source_projection_metadata", + "calls": 2, + "inclusive": 0.017186708748340607, + "exclusive": 0.017186708748340607 + }, + "execute_worker_lane | {\"execution_id\": \"6df21ea1-70f5-4fa0-9fac-ef5e7963204d\", \"kind\": \"worker_lane\", \"plate\": \"/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images\", \"worker_slot\": \"worker_0\"}": { + "owner": "execute_worker_lane", + "dimensions": "{\"execution_id\": \"6df21ea1-70f5-4fa0-9fac-ef5e7963204d\", \"kind\": \"worker_lane\", \"plate\": \"/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images\", \"worker_slot\": \"worker_0\"}", + "phase": "execute_worker_lane", + "calls": 1, + "inclusive": 0.2973959231749177, + "exclusive": 0.02155675506219268 + }, + "consolidate_analysis_outputs": { + "owner": "consolidate_analysis_outputs", + "dimensions": "", + "phase": "consolidate_analysis_outputs", + "calls": 1, + "inclusive": 7.935333997011185e-06, + "exclusive": 7.935333997011185e-06 + }, + "RuntimeArtifactMetadataTarget.reconciliation_targets": { + "owner": "RuntimeArtifactMetadataTarget.reconciliation_targets", + "dimensions": "", + "phase": "reconciliation_targets", + "calls": 1, + "inclusive": 0.021559111773967743, + "exclusive": 0.021559111773967743 + }, + "OpenHCSMetadataWriter.finalize_completed_plate": { + "owner": "OpenHCSMetadataWriter.finalize_completed_plate", + "dimensions": "", + "phase": "finalize_completed_plate", + "calls": 1, + "inclusive": 0.031200694851577282, + "exclusive": 0.00041082315146923065 + }, + "ZMQExecutionServer._export_runtime_observation": { + "owner": "ZMQExecutionServer._export_runtime_observation", + "dimensions": "", + "phase": "_export_runtime_observation", + "calls": 1, + "inclusive": 0.004840843845158815, + "exclusive": 0.004840843845158815 + }, + "ZMQExecutionServer._execute_with_orchestrator | {\"case\": \"ExampleIlluminationCorrection_Example3\", \"compile_artifact_id\": \"78250d97-e324-4dbd-a014-62f28444b8a3\", \"execution_id\": \"6df21ea1-70f5-4fa0-9fac-ef5e7963204d\", \"kind\": \"execution\", \"plate\": \"/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images\"}": { + "owner": "ZMQExecutionServer._execute_with_orchestrator", + "dimensions": "{\"case\": \"ExampleIlluminationCorrection_Example3\", \"compile_artifact_id\": \"78250d97-e324-4dbd-a014-62f28444b8a3\", \"execution_id\": \"6df21ea1-70f5-4fa0-9fac-ef5e7963204d\", \"kind\": \"execution\", \"plate\": \"/home/ts/.cache/openhcs/cellprofiler_examples/ExampleIlluminationCorrection/images\"}", + "phase": "_execute_with_orchestrator", + "calls": 1, + "inclusive": 0.4150526160374284, + "exclusive": 0.02083579869940877 + } + }, + "hooks_installed": [ + { + "module": "objectstate.object_state", + "owner": "ObjectState", + "method": "__init__", + "descriptor_kind": "function" + }, + { + "module": "objectstate.object_state", + "owner": "ObjectState", + "method": "_compute_resolved_snapshot", + "descriptor_kind": "function" + }, + { + "module": "objectstate.object_state", + "owner": "ObjectState", + "method": "_reconstruct_from_resolved", + "descriptor_kind": "function" + }, + { + "module": "objectstate.object_state", + "owner": "ObjectState", + "method": "to_saved_resolved_object", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.aligned_image_payload", + "owner": "", + "method": "stack_image_payload_context", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.aligned_image_payload", + "owner": "", + "method": "unstack_image_payload_context", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.measurement_feature_queries", + "owner": "MeasurementFeatureValueIndex", + "method": "from_columnar_table_by_object", + "descriptor_kind": "classmethod" + }, + { + "module": "openhcs.core.virtual_workspace_metadata", + "owner": "AtomicMetadataWriter", + "method": "publish_source_projection_metadata", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.runtime_stack_cache", + "owner": "RuntimeImageStackCache", + "method": "get", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.runtime_stack_cache", + "owner": "RuntimeImageStackCache", + "method": "store", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.steps.function_outputs", + "owner": "", + "method": "finalize_function_step_outputs", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.steps.function_outputs", + "owner": "OpenHCSMetadataWriter", + "method": "finalize_completed_plate", + "descriptor_kind": "classmethod" + }, + { + "module": "openhcs.core.steps.function_outputs", + "owner": "RuntimeArtifactMetadataTarget", + "method": "reconciliation_targets", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.steps.function_runtime", + "owner": "FunctionCoreExecutor", + "method": "execute", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.steps.function_runtime", + "owner": "PatternGroupRuntime", + "method": "_load_input_stack", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.steps.function_runtime", + "owner": "PatternGroupRuntime", + "method": "_validate_and_unstack", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.steps.function_runtime", + "owner": "PatternGroupRuntime", + "method": "_save_outputs", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.pipeline.compilation_session", + "owner": "CompilationSession", + "method": "from_context", + "descriptor_kind": "classmethod" + }, + { + "module": "openhcs.core.pipeline.compiler", + "owner": "PipelineCompiler", + "method": "compile_pipelines", + "descriptor_kind": "staticmethod" + }, + { + "module": "openhcs.core.pipeline.compiler", + "owner": "PipelineCompiler", + "method": "_register_and_resolve_pipeline_once", + "descriptor_kind": "staticmethod" + }, + { + "module": "openhcs.core.pipeline.compiler", + "owner": "PipelineCompiler", + "method": "initialize_step_plans_for_context", + "descriptor_kind": "staticmethod" + }, + { + "module": "openhcs.core.pipeline.compiler", + "owner": "PipelineCompiler", + "method": "_capture_pipeline_config", + "descriptor_kind": "staticmethod" + }, + { + "module": "openhcs.core.pipeline.compiler", + "owner": "PipelineCompiler", + "method": "_resolve_global_step_axis_filters", + "descriptor_kind": "staticmethod" + }, + { + "module": "openhcs.core.pipeline.compiler", + "owner": "PipelineCompiler", + "method": "_configure_input_conversion_if_needed", + "descriptor_kind": "staticmethod" + }, + { + "module": "openhcs.core.pipeline.compiler", + "owner": "PipelineCompiler", + "method": "declare_zarr_stores", + "descriptor_kind": "staticmethod" + }, + { + "module": "openhcs.core.pipeline.compiler", + "owner": "PipelineCompiler", + "method": "_finalize_compilation", + "descriptor_kind": "staticmethod" + }, + { + "module": "openhcs.core.pipeline.compiler", + "owner": "PipelineCompiler", + "method": "_axis_values_to_process", + "descriptor_kind": "staticmethod" + }, + { + "module": "openhcs.core.pipeline.compiler", + "owner": "AxisCompilationRequest", + "method": "context_for", + "descriptor_kind": "function" + }, + { + "module": "openhcs.processing.backends.lib_registry.unified_registry", + "owner": "RuntimeCallableInvocation", + "method": "call", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.orchestrator.analysis_consolidation", + "owner": "", + "method": "consolidate_analysis_outputs", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.orchestrator.worker_execution", + "owner": "", + "method": "execute_worker_lane", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.orchestrator.orchestrator", + "owner": "PipelineOrchestrator", + "method": "create_context", + "descriptor_kind": "function" + }, + { + "module": "openhcs.core.orchestrator.orchestrator", + "owner": "PipelineOrchestrator", + "method": "get_effective_config", + "descriptor_kind": "function" + }, + { + "module": "openhcs.runtime.zmq_execution_server", + "owner": "ZMQExecutionServer", + "method": "_execute_with_orchestrator", + "descriptor_kind": "function" + }, + { + "module": "openhcs.runtime.zmq_execution_server", + "owner": "ZMQExecutionServer", + "method": "_initialize_orchestrator", + "descriptor_kind": "function" + }, + { + "module": "openhcs.runtime.zmq_execution_server", + "owner": "ZMQExecutionServer", + "method": "_export_runtime_observation", + "descriptor_kind": "function" + }, + { + "module": "openhcs.interop.cellprofiler.runtime.function_contract_execution", + "owner": "CellProfilerFunctionContractExecutor", + "method": "execute", + "descriptor_kind": "function" + }, + { + "module": "openhcs.interop.cellprofiler.runtime.measurement_execution_support", + "owner": "ObjectMeasurementOutputRecorder", + "method": "record", + "descriptor_kind": "function" + }, + { + "module": "openhcs.interop.cellprofiler.runtime.output_recording", + "owner": "CellProfilerOutputRecorder", + "method": "record_module_outputs", + "descriptor_kind": "classmethod" + }, + { + "module": "openhcs.interop.cellprofiler.runtime.module_execution", + "owner": "CellProfilerModuleExecutor", + "method": "__call__", + "descriptor_kind": "function" + }, + { + "module": "openhcs.interop.cellprofiler.runtime.module_execution", + "owner": "CellProfilerModuleExecutor", + "method": "_image_request", + "descriptor_kind": "function" + } + ], + "profiling_enabled": false, + "runtime_profiling_enabled": false, + "diagnostic_errors": [], + "captures": false, + "root_started_monotonic_ns": 3816283657684912, + "root_ended_monotonic_ns": 3816284072767004, + "numba_events": { + "installed": true, + "counts": {}, + "record_count": 0, + "pending_record_count": 0, + "flushed_record_count": 0, + "refused_records": 0, + "errors": [], + "error_count": 0, + "record_bytes": 0, + "cap": 262144 + }, + "scope": "coarse same-run nested exclusive timing partition on the exact source revision recorded in this region. Diagnostic wrappers, no scientific or memory-observer substitutions. Not accepted performance comparison. Declared callable boundary includes filtering/decorated processing/metadata; not pure numerical kernels. Both existing profilers are disabled. These wrappers still add diagnostic overhead; no scaling to uninstrumented clocks or causal performance claims. Compiler/state boundaries and query boundary are nested under actual parents and excluded from parent-exclusive elapsed. This is a NEW 41-hook diagnostic version; inherited 24 boundary labels remain, site bytes are changed. ObjectState is verified against the actual source dependency path, not assumed inside the OpenHCS package." +} diff --git a/benchmark/results/perf_illum3_runtime_ledger_20261003/numba-events-1.json b/benchmark/results/perf_illum3_runtime_ledger_20261003/numba-events-1.json new file mode 100644 index 0000000000..ad31fc24db --- /dev/null +++ b/benchmark/results/perf_illum3_runtime_ledger_20261003/numba-events-1.json @@ -0,0 +1 @@ +{"installed":true,"counts":{},"record_count":0,"pending_record_count":0,"flushed_record_count":0,"refused_records":0,"errors":[],"error_count":0,"record_bytes":0,"cap":262144,"pid":2078555,"records":[],"complete":true,"scope":"Passive original numba:compile START/END events; startup before first root, same-thread timed kind, other active root IDs, outside-root events separately retained. No dispatcher/data/signature references or repr/str callbacks. Compile events do not include cache loads that emit no native compile event; no policy/warmup change. Durations remain original 41-hook intervals, not listener-generated benchmark clocks.","flush_boundary":{"kind":"atexit_supplementary"},"flush_sequence":1} diff --git a/benchmark/results/perf_illum3_runtime_ledger_20261003/numba-events-2.json b/benchmark/results/perf_illum3_runtime_ledger_20261003/numba-events-2.json new file mode 100644 index 0000000000..c3ef477413 --- /dev/null +++ b/benchmark/results/perf_illum3_runtime_ledger_20261003/numba-events-2.json @@ -0,0 +1 @@ +{"installed":true,"counts":{},"record_count":0,"pending_record_count":0,"flushed_record_count":0,"refused_records":0,"errors":[],"error_count":0,"record_bytes":0,"cap":262144,"pid":2078682,"records":[],"complete":true,"scope":"Passive original numba:compile START/END events; startup before first root, same-thread timed kind, other active root IDs, outside-root events separately retained. No dispatcher/data/signature references or repr/str callbacks. Compile events do not include cache loads that emit no native compile event; no policy/warmup change. Durations remain original 41-hook intervals, not listener-generated benchmark clocks.","flush_boundary":{"kind":"compile_only","root_thread_id":140361443509952,"root_sequence":1,"root_started_monotonic_ns":3816282952668134,"root_ended_monotonic_ns":3816283540961927},"flush_sequence":1} diff --git a/benchmark/results/perf_illum3_runtime_ledger_20261003/numba-events-3.json b/benchmark/results/perf_illum3_runtime_ledger_20261003/numba-events-3.json new file mode 100644 index 0000000000..23c35fb55a --- /dev/null +++ b/benchmark/results/perf_illum3_runtime_ledger_20261003/numba-events-3.json @@ -0,0 +1 @@ +{"installed":true,"counts":{},"record_count":0,"pending_record_count":0,"flushed_record_count":0,"refused_records":0,"errors":[],"error_count":0,"record_bytes":0,"cap":262144,"pid":2078682,"records":[],"complete":true,"scope":"Passive original numba:compile START/END events; startup before first root, same-thread timed kind, other active root IDs, outside-root events separately retained. No dispatcher/data/signature references or repr/str callbacks. Compile events do not include cache loads that emit no native compile event; no policy/warmup change. Durations remain original 41-hook intervals, not listener-generated benchmark clocks.","flush_boundary":{"kind":"execution","root_thread_id":140361443509952,"root_sequence":2,"root_started_monotonic_ns":3816283657684912,"root_ended_monotonic_ns":3816284072767004},"flush_sequence":2} diff --git a/benchmark/results/perf_illum3_runtime_ledger_20261003/numba-events-4.json b/benchmark/results/perf_illum3_runtime_ledger_20261003/numba-events-4.json new file mode 100644 index 0000000000..607b666767 --- /dev/null +++ b/benchmark/results/perf_illum3_runtime_ledger_20261003/numba-events-4.json @@ -0,0 +1 @@ +{"installed":true,"counts":{},"record_count":0,"pending_record_count":0,"flushed_record_count":0,"refused_records":0,"errors":[],"error_count":0,"record_bytes":0,"cap":262144,"pid":2078682,"records":[],"complete":true,"scope":"Passive original numba:compile START/END events; startup before first root, same-thread timed kind, other active root IDs, outside-root events separately retained. No dispatcher/data/signature references or repr/str callbacks. Compile events do not include cache loads that emit no native compile event; no policy/warmup change. Durations remain original 41-hook intervals, not listener-generated benchmark clocks.","flush_boundary":{"kind":"atexit_supplementary"},"flush_sequence":3} diff --git a/benchmark/results/perf_illum3_runtime_ledger_20261003/observations.json b/benchmark/results/perf_illum3_runtime_ledger_20261003/observations.json new file mode 100644 index 0000000000..32d102b1f2 --- /dev/null +++ b/benchmark/results/perf_illum3_runtime_ledger_20261003/observations.json @@ -0,0 +1,93 @@ +{ + "returncode": 0, + "diagnostic_admission": true, + "public_rows": [ + { + "case_name": "ExampleIlluminationCorrection_Example3", + "mode_name": "1w_1t", + "worker_count": "1", + "well_count": "1", + "compile_seconds": "0.5973687171936035", + "prepare_seconds": "0.0", + "execute_seconds": "0.42289090156555176", + "total_seconds": "1.419051312841475", + "wells_per_second": "2.3646760814620915", + "successful_wells": "1", + "native_single_sample_execution_seconds": "", + "projected_native_execution_seconds": "", + "projected_execution_speedup": "", + "peak_memory_mb": "1007.28515625", + "status": "success", + "memory_limit_mb": "", + "error_message": "", + "execution_route": "ordinary-zmq-outcomes-v1", + "server_lifecycle": "reused-per-sweep", + "run_input_sha256": "53cbf79d64350e46b2692cfd3b96a8bb1be943d75193a52d3fb5a2f98cfa91df" + } + ], + "regions": [ + { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/output/regions/region-2078682-140361443509952-0001.json", + "sha256": "cc69523fdae5fe92c4e96ec59609a19eb889fe744316b6ed8419ed0398313ab9" + }, + { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/output/regions/region-2078682-140361443509952-0002.json", + "sha256": "f3c9e01926b069a0103ebb9c70d638a4944c1f429b809b3a7b2e9755efbf80e5" + } + ], + "numba_event_admission": true, + "numba_root_flushes_complete": true, + "numba_event_total_bytes": 3107, + "numba_event_aggregate_cap": 262144, + "numba_event_files": [ + { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/output/regions/numba-events-2078555-0001.json", + "sha256": "df39cc4538c692418c9590759ee2be67c03e712d31b38e59909a2acc414299da", + "counts": {}, + "complete": true + }, + { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/output/regions/numba-events-2078682-0001.json", + "sha256": "0cc87c64a35b572441cac162e7a953685af095892641222cef4b52a71d7ace03", + "counts": {}, + "complete": true + }, + { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/output/regions/numba-events-2078682-0002.json", + "sha256": "a80829256ff2e4f13f8cb14257b45138aacefb6e2726339b64be55d585c90fde", + "counts": {}, + "complete": true + }, + { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/output/regions/numba-events-2078682-0003.json", + "sha256": "c615a8d1c29b5da41bba3237cd1ee02e6f8eba72af2f73290f11ec684571661b", + "counts": {}, + "complete": true + } + ], + "root_sequence_complete": true, + "actual_index_calls": 0, + "actual_declared_calls": 11, + "compile_only_actual_calls": { + "PipelineCompiler.compile_pipelines": 1, + "PipelineCompiler._register_and_resolve_pipeline_once": 1, + "PipelineCompiler._capture_pipeline_config": 1, + "AxisCompilationRequest.context_for": 1, + "CompilationSession.from_context": 1, + "PipelineOrchestrator.create_context": 3, + "PipelineOrchestrator.get_effective_config": 10, + "ObjectState.__init__": 23, + "ObjectState._compute_resolved_snapshot": 46, + "ObjectState._reconstruct_from_resolved": 343, + "ObjectState.to_saved_resolved_object": 21 + }, + "all_required_compiler_boundaries_reached": true, + "all41_hooks_installed": true, + "retained_json_bytes": 40594, + "generated_output_bytes": 2993187, + "public_log_bytes": 438, + "profiler_artifacts_absent": true, + "source_env_native_input_freeze": "PASS", + "science": "NOT_RUN: original declared2NPY/fullphysicalinventory/numerical/nativeinput witness separate gate. No exclusions or missing outputs waived.", + "scope": "One source-pinned ordinary IlluminationCorrection Example3 1w1t compiler/runtime diagnostic with NEW41 coarse nominal hooks: inherited24 plus17 compiler/state boundaries. Site is versioned/changed, not byte-identical. cProfile and runtime profiler OFF, no captures/percell hooks. Existing defaultOUTCOMES/memory observers/READY clocks unchanged. Same-thread nested exclusive clocks exclude query from its real ancestors; worker independent roots are not added to parent waits. Nearordinary diagnostic only; wrapper overhead remains, no scale factors/causal performance promotion. Full2NPY image-only declared export science/native source witness independently required. Passive installed-native Numba compile events distinguish startup and timed roots; bounded event batches flush AFTER original root elapsed is measured, cumulative counts retained, atexit supplementary only; event cap/refusal stays diagnostic RED, no dispatcher/signature retention." +} diff --git a/benchmark/results/perf_illum3_runtime_ledger_20261003/qualification.json b/benchmark/results/perf_illum3_runtime_ledger_20261003/qualification.json new file mode 100644 index 0000000000..9f857ae0f1 --- /dev/null +++ b/benchmark/results/perf_illum3_runtime_ledger_20261003/qualification.json @@ -0,0 +1,403 @@ +{ + "status": "ONE_CURRENT_ILLUM3_DIAGNOSTIC_AND_STRICT_SCIENCE_PASS", + "source_head": "4c0d62e45c8afb85e34b69d7279cd00710f519b5", + "all_handles_terminal": true, + "pipeline_runs": 1, + "driver_session": 79456, + "science_v3_session": 72671, + "original_reader_red_session": 84897, + "public_diagnostic_seconds": { + "compile_seconds": 0.5973687171936035, + "execute_seconds": 0.42289090156555176, + "total_seconds": 1.419051312841475, + "other_seconds": 0.39879169408231974 + }, + "raw_callable_boundary_seconds": 0.11173972394317389, + "execution_outside_raw_seconds": 0.3033128920942545, + "raw_callables": [ + { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.primary_objects.identify_primary_objects;view=1", + "phase": "declared_callable_boundary", + "calls": 1, + "inclusive": 0.02976729581132531, + "exclusive": 0.02976729581132531 + }, + { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.morphology.expand_or_shrink_objects;view=1", + "phase": "declared_callable_boundary", + "calls": 1, + "inclusive": 0.02096757711842656, + "exclusive": 0.02096757711842656 + }, + { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.image_math.image_math;view=1", + "phase": "declared_callable_boundary", + "calls": 1, + "inclusive": 0.001754844095557928, + "exclusive": 0.001754844095557928 + }, + { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.image_geometry.mask_image;view=1", + "phase": "declared_callable_boundary", + "calls": 2, + "inclusive": 0.0009737634100019932, + "exclusive": 0.0009737634100019932 + }, + { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.illumination.correct_illumination_calculate;view=1", + "phase": "declared_callable_boundary", + "calls": 2, + "inclusive": 0.05728212045505643, + "exclusive": 0.05728212045505643 + }, + { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.illumination.correct_illumination_apply;view=1", + "phase": "declared_callable_boundary", + "calls": 2, + "inclusive": 0.0008326279930770397, + "exclusive": 0.0008326279930770397 + }, + { + "owner": "RuntimeCallableInvocation.call", + "dimensions": "function=openhcs.processing.backends.cellprofiler.save_images.save_images;view=1", + "phase": "declared_callable_boundary", + "calls": 2, + "inclusive": 0.00016149505972862244, + "exclusive": 0.00016149505972862244 + } + ], + "compile_and_execution_source_owner_partition": { + "compile_only": { + "seconds": 0.5882621230557561, + "sum_exclusive": 0.5882621230557561, + "ObjectState_disjoint_exclusive": 0.3834735373966396, + "owners": [ + { + "owner": "ObjectState._compute_resolved_snapshot", + "dimensions": "", + "phase": "compiler_config:_compute_resolved_snapshot", + "calls": 46, + "inclusive": 0.2531436877325177, + "exclusive": 0.2531436877325177 + }, + { + "owner": "ObjectState.__init__", + "dimensions": "", + "phase": "compiler_config:__init__", + "calls": 23, + "inclusive": 0.35587291372939944, + "exclusive": 0.10272922599688172 + }, + { + "owner": "ObjectState._reconstruct_from_resolved", + "dimensions": "", + "phase": "compiler_config:_reconstruct_from_resolved", + "calls": 343, + "inclusive": 0.05295955156907439, + "exclusive": 0.027533238753676414 + }, + { + "owner": "ObjectState.to_saved_resolved_object", + "dimensions": "", + "phase": "compiler_config:to_saved_resolved_object", + "calls": 21, + "inclusive": 0.027600623667240143, + "exclusive": 6.738491356372833e-05 + }, + { + "owner": "PipelineOrchestrator.get_effective_config", + "dimensions": "", + "phase": "compiler_config:get_effective_config", + "calls": 10, + "inclusive": 0.19238800555467606, + "exclusive": 8.529284968972206e-05 + }, + { + "owner": "PipelineOrchestrator.create_context", + "dimensions": "", + "phase": "compiler_config:create_context", + "calls": 3, + "inclusive": 0.04923866596072912, + "exclusive": 0.0002901456318795681 + }, + { + "owner": "PipelineCompiler._axis_values_to_process", + "dimensions": "", + "phase": "compiler_config:_axis_values_to_process", + "calls": 1, + "inclusive": 0.014383771922439337, + "exclusive": 7.574260234832764e-05 + }, + { + "owner": "PipelineCompiler._register_and_resolve_pipeline_once", + "dimensions": "", + "phase": "compiler_config:_register_and_resolve_pipeline_once", + "calls": 1, + "inclusive": 0.19153947802260518, + "exclusive": 0.0021261521615087986 + }, + { + "owner": "PipelineCompiler._capture_pipeline_config", + "dimensions": "", + "phase": "compiler_config:_capture_pipeline_config", + "calls": 1, + "inclusive": 0.0017600241117179394, + "exclusive": 2.52528116106987e-06 + }, + { + "owner": "PipelineCompiler._resolve_global_step_axis_filters", + "dimensions": "", + "phase": "compiler_config:_resolve_global_step_axis_filters", + "calls": 1, + "inclusive": 0.013955585192888975, + "exclusive": 0.00047726836055517197 + }, + { + "owner": "PipelineCompiler._configure_input_conversion_if_needed", + "dimensions": "", + "phase": "compiler_config:_configure_input_conversion_if_needed", + "calls": 1, + "inclusive": 0.013350545894354582, + "exclusive": 4.2282044887542725e-06 + }, + { + "owner": "PipelineCompiler.initialize_step_plans_for_context", + "dimensions": "", + "phase": "compiler_config:initialize_step_plans_for_context", + "calls": 1, + "inclusive": 0.14918227866292, + "exclusive": 0.1358146509155631 + }, + { + "owner": "PipelineCompiler.declare_zarr_stores", + "dimensions": "", + "phase": "compiler_config:declare_zarr_stores", + "calls": 1, + "inclusive": 0.013443371281027794, + "exclusive": 4.089716821908951e-05 + }, + { + "owner": "PipelineCompiler._finalize_compilation", + "dimensions": "", + "phase": "compiler_config:_finalize_compilation", + "calls": 1, + "inclusive": 0.013896864838898182, + "exclusive": 0.00011343369260430336 + }, + { + "owner": "PipelineCompiler.compile_pipelines", + "dimensions": "", + "phase": "compiler_config:compile_pipelines", + "calls": 1, + "inclusive": 0.437725986354053, + "exclusive": 0.02060367353260517 + } + ] + }, + "execution": { + "seconds": 0.4150526160374284, + "sum_exclusive": 0.4150526160374284, + "ObjectState_disjoint_exclusive": 0.05523737985640764, + "owners": [ + { + "owner": "ObjectState._compute_resolved_snapshot", + "dimensions": "", + "phase": "compiler_config:_compute_resolved_snapshot", + "calls": 8, + "inclusive": 0.038815660402178764, + "exclusive": 0.038815660402178764 + }, + { + "owner": "ObjectState.__init__", + "dimensions": "", + "phase": "compiler_config:__init__", + "calls": 4, + "inclusive": 0.0482184044085443, + "exclusive": 0.009402744006365538 + }, + { + "owner": "ObjectState._reconstruct_from_resolved", + "dimensions": "", + "phase": "compiler_config:_reconstruct_from_resolved", + "calls": 92, + "inclusive": 0.013413879089057446, + "exclusive": 0.0070056007243692875 + }, + { + "owner": "ObjectState.to_saved_resolved_object", + "dimensions": "", + "phase": "compiler_config:to_saved_resolved_object", + "calls": 4, + "inclusive": 0.00701897544786334, + "exclusive": 1.3374723494052887e-05 + }, + { + "owner": "PipelineOrchestrator.get_effective_config", + "dimensions": "", + "phase": "compiler_config:get_effective_config", + "calls": 4, + "inclusive": 0.055267936550080776, + "exclusive": 3.055669367313385e-05 + }, + { + "owner": "PipelineOrchestrator.create_context", + "dimensions": "", + "phase": "compiler_config:create_context", + "calls": 1, + "inclusive": 0.01351372292265296, + "exclusive": 6.313808262348175e-05 + } + ] + } + }, + "numba": { + "native_event_authority": { + "path": "/home/ts/code/projects/openhcs/.venv/lib/python3.12/site-packages/numba/core/event.py", + "version": "0.67.0", + "sha256": "ab878d495f8532794bfe36f39b9d6c4e256abb669c8fa7bbe2a88f06d0b4a278", + "dispatcher_path": "/home/ts/code/projects/openhcs/.venv/lib/python3.12/site-packages/numba/core/dispatcher.py", + "dispatcher_sha256": "9fd050b1095cd6a9fef90772aa065d51328583b167b52393f7be6b615b99953b" + }, + "event_admission": true, + "root_flushes_complete": true, + "aggregate_bytes": 3107, + "native_compile_start_events": 0, + "actual_cumulative_counts_by_file": [ + { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/output/regions/numba-events-2078555-0001.json", + "counts": {}, + "complete": true + }, + { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/output/regions/numba-events-2078682-0001.json", + "counts": {}, + "complete": true + }, + { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/output/regions/numba-events-2078682-0002.json", + "counts": {}, + "complete": true + }, + { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/output/regions/numba-events-2078682-0003.json", + "counts": {}, + "complete": true + } + ], + "scope": "NO original numba:compile START/END events observed in either process/startup/timed roots/outside roots in THIS run. Cache loads emit no native compile event. Does not establish prior timing-variance cause or missing polynomial warmup." + }, + "science": { + "comparisons": 2, + "image_nonvacuity": "All2authored ReferenceExportPlan artifacts compared with positive pixel counts to both retainednative reps", + "measurement_differences": 0, + "image_differences": 0, + "inventory": "ALLactualfiles accounted via unchanged nominal policy; no unknown exclusions", + "native_scope": "TRANSITIVE retainednative reps, not rerun/currentnativeclocks", + "reader_only_environment_effect_key_names": [ + "KMP_DUPLICATE_LIB_OK", + "SPARSE_AUTO_DENSIFY", + "TF_FORCE_GPU_ALLOW_GROWTH", + "XLA_PYTHON_CLIENT_PREALLOCATE" + ], + "source_env_guard": "Exact original full controller --prepare guard before/after using original raw launch env isolated from reader-only imports; no key filters/rewrite or bypass. Original failed in-reader custody invocation retained." + }, + "custody": { + "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/handoff.json": { + "sha256": "7f8f1e5d977bd4fabc5b785be7181be4bfe28b1000b8a9d00b7491673e506787", + "bytes": 5211 + }, + "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/controller.py": { + "sha256": "e00e1cb2e195842f780ed289e97700db9fd52d7046414079fb186f90a7f98df5", + "bytes": 19881 + }, + "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/controls.json": { + "sha256": "358cd70c391cdaae89d5f2ba8514de606ccabcf43be3507dadeba7ae2341f7d1", + "bytes": 1520 + }, + "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/controls-v2.log": { + "sha256": "d4c3a95ea114ad14b9f1d4759b8959f1bdf67ffb520bd391efe2758ca2d30072", + "bytes": 1390 + }, + "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/site/sitecustomize.py": { + "sha256": "1a30063decce3e67ef2e0f3c033fb4f4cb9e6acf7e4c2b43b114c433f62c1bbe", + "bytes": 14423 + }, + "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/site/openhcs_numba_compile_events_v1.py": { + "sha256": "2e6863e7d78709cabddf6b34cb5fba1a8e27daeb39f0ae3e9134c15a3c45f421", + "bytes": 4548 + }, + "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/output/source-freeze.json": { + "sha256": "86646806122c73d38de2e9d42efa0bffd471ad0a1db36d7d33ea83dfcbfeb0fc", + "bytes": 1185307 + }, + "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/output/observations.json": { + "sha256": "c8b1e3f0704682e2cad20b7a5e27b1651e38b677e74f31d6477eb163185106fd", + "bytes": 4831 + }, + "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/science_validator.py": { + "sha256": "710f87213f2ebff207aa9c0c629cdb59639479c96a32656f1f3ec4adfd57d669", + "bytes": 5552 + }, + "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/science_validator_v3.py": { + "sha256": "e71981fd54cc6a3f6e3d03c797badff250cc5c7e3fdddf1401fb3378e17b8aa4", + "bytes": 7616 + }, + "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/science-first-red.json": { + "sha256": "de6ae869a4f1fe6eabfa4fc5b5ee87ba052462f26302d0a7a334c7b2c4276c80", + "bytes": 872 + }, + "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/science-first-red-environment-audit.json": { + "sha256": "4fa0497da2158f883c3af73aaea0b906a63f0d3887d7f82ee89e9c8afc03db20", + "bytes": 1285 + }, + "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/science-v3.json": { + "sha256": "607c68f213baddf4c5dbec97b39efee87451ac16188eadf1ebdea578d5bee9e5", + "bytes": 15478 + }, + "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/science-v3-controller-checkpoint-1.log": { + "sha256": "04a54993c97b3c3f46659e451ec5e2f8cd944fae12ab9f35abf49ffef1707c00", + "bytes": 1145 + }, + "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/science-v3-controller-checkpoint-2.log": { + "sha256": "04a54993c97b3c3f46659e451ec5e2f8cd944fae12ab9f35abf49ffef1707c00", + "bytes": 1145 + }, + "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/output/regions/region-2078682-140361443509952-0001.json": { + "sha256": "cc69523fdae5fe92c4e96ec59609a19eb889fe744316b6ed8419ed0398313ab9", + "bytes": 15563 + }, + "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/output/regions/region-2078682-140361443509952-0002.json": { + "sha256": "f3c9e01926b069a0103ebb9c70d638a4944c1f429b809b3a7b2e9755efbf80e5", + "bytes": 25031 + }, + "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/output/regions/numba-events-2078555-0001.json": { + "sha256": "df39cc4538c692418c9590759ee2be67c03e712d31b38e59909a2acc414299da", + "bytes": 712 + }, + "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/output/regions/numba-events-2078682-0001.json": { + "sha256": "0cc87c64a35b572441cac162e7a953685af095892641222cef4b52a71d7ace03", + "bytes": 843 + }, + "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/output/regions/numba-events-2078682-0002.json": { + "sha256": "a80829256ff2e4f13f8cb14257b45138aacefb6e2726339b64be55d585c90fde", + "bytes": 840 + }, + "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/output/regions/numba-events-2078682-0003.json": { + "sha256": "c615a8d1c29b5da41bba3237cd1ee02e6f8eba72af2f73290f11ec684571661b", + "bytes": 712 + } + }, + "determining_limits": [ + "One instrumented diagnostic, no causal code gain/speedup ratio or replacement of qualified two-repeat means.", + "Current compile .588262 root: state owners .383474s disjoint absolute envelope, includes mandatory distinct live/saved resolution and reconstruction. Late effective context reads .192388s inclusive subset; cannot claim both.", + "Execution outside RAW .303313s has actual init/config/record/publication/finalization/worker work; source-state .055237s separate. No single small lane closes existing qualified total gap.", + "Prior Illum execution .686775/.385197 spread .301578 remains UNATTRIBUTED; zero native compile events here cannot explain that historical difference.", + "No source changes, numerical algorithm patch, environment install, fresh native run, callback/validation/cache policy change, or benchmark clock exclusions." + ] +} diff --git a/benchmark/results/perf_illum3_runtime_ledger_20261003/reader-environment-audit.json b/benchmark/results/perf_illum3_runtime_ledger_20261003/reader-environment-audit.json new file mode 100644 index 0000000000..269ccbbb0b --- /dev/null +++ b/benchmark/results/perf_illum3_runtime_ledger_20261003/reader-environment-audit.json @@ -0,0 +1,35 @@ +{ + "status": "READER_IMPORT_ENVIRONMENT_EFFECT_IDENTIFIED", + "initial_full_environment_matches_driver_freeze": true, + "changes": [ + { + "import": "benchmark.adapters.openhcs", + "changed_key_names": [ + "KMP_DUPLICATE_LIB_OK", + "SPARSE_AUTO_DENSIFY", + "TF_FORCE_GPU_ALLOW_GROWTH", + "XLA_PYTHON_CLIENT_PREALLOCATE" + ] + } + ], + "source_environment_remains_driver_authoritative": "Actual driver pre/post full environment gate PASS; scientific reader changes cannot be treated as driver changes. No raw values disclosed.", + "per_key_hashes": { + "KMP_DUPLICATE_LIB_OK": { + "before": null, + "after": "3cbc87c7681f34db4617feaa2c8801931bc5e42d8d0f560e756dd4cd92885f18" + }, + "SPARSE_AUTO_DENSIFY": { + "before": null, + "after": "6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b" + }, + "TF_FORCE_GPU_ALLOW_GROWTH": { + "before": null, + "after": "b5bea41b6c623f7c09f1bf24dcae58ebab3c0cdd90ad966bc43a45b44867e12b" + }, + "XLA_PYTHON_CLIENT_PREALLOCATE": { + "before": null, + "after": "fcbcf165908dd18a9e49f7ff27810176db8e9f63b4352213741664245224f8aa" + } + }, + "after_reader_import_environment_sha256": "4415a2780ee53da969d5dc39ef698b0e50675d444afc6629d8a68dca42b1b514" +} diff --git a/benchmark/results/perf_illum3_runtime_ledger_20261003/reader-first-red.json b/benchmark/results/perf_illum3_runtime_ledger_20261003/reader-first-red.json new file mode 100644 index 0000000000..7910967dd7 --- /dev/null +++ b/benchmark/results/perf_illum3_runtime_ledger_20261003/reader-first-red.json @@ -0,0 +1,13 @@ +{ + "status": "ORIGINAL_READER_CUSTODY_RED_RETAINED", + "process_session": 84897, + "returncode": 1, + "original_reader": "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/science_validator.py", + "original_reader_sha256": "710f87213f2ebff207aa9c0c629cdb59639479c96a32656f1f3ec4adfd57d669", + "error": "AssertionError: Effective ordinary environment changed.", + "first_failure_boundary": "Original comparator final validate after comparison assertions, before science report write; FULLSCIENCEPASS was not claimed.", + "environment_audit": "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/science-first-red-environment-audit.json", + "environment_audit_sha256": "4fa0497da2158f883c3af73aaea0b906a63f0d3887d7f82ee89e9c8afc03db20", + "driver_admission_unchanged": true, + "no_pipeline_rerun": true +} diff --git a/benchmark/results/perf_illum3_runtime_ledger_20261003/render-retention-class-census.json b/benchmark/results/perf_illum3_runtime_ledger_20261003/render-retention-class-census.json new file mode 100644 index 0000000000..0714383a44 --- /dev/null +++ b/benchmark/results/perf_illum3_runtime_ledger_20261003/render-retention-class-census.json @@ -0,0 +1,671 @@ +{ + "source_head": "4c0d62e45c8afb85e34b69d7279cd00710f519b5", + "nra_head": "0844525ecaba93e090a064a4ae4914466b2dae60", + "scope": "All original classes in11 determining benchmark/document/transport modules, joined to actual canonical NRA family; reused731module global inventory for retrieval, unresolvedexternal/MRO effects OPEN. No source edits/importingproduction/metaclass execution.", + "source_hashes": { + "openhcs/core/pipeline_document.py": "0849800b3e6474b8176fe35bc4aaebc63cfa85bd77251000c50da5f4c928b1b2", + "openhcs/core/config_document.py": "37f66fd971806e059da6194c003e20a7ad08f69066eec4af7ebb11b4b70a08db", + "openhcs/core/function_step_transport.py": "b4c89539ace519926d178eb85a7909b4a7894203bf340213279e4f2a11e1152f", + "openhcs/runtime/zmq_execution_client.py": "8f98169438b1f9d65dd2a4f1517b5901e5d2ffd944fce4b0779a8f9b17d01db3", + "openhcs/runtime/zmq_compilation.py": "ad958c78ec9a26b778ad231501751f38ec6dd3d0c73ca2c1082207031fc6781f", + "openhcs/runtime/zmq_execution_signature.py": "41dcdb0a40446947a42a87482f27da051408015eec972e11e4d3329464db11ac", + "openhcs/serialization/source_path_factoring.py": "d8dd55b7d45882e067f9fddbe87327000a7e226dbc531a90fc543f201ea665ab", + "openhcs/serialization/pycodify_formatters.py": "b86d8a18b32084839667ebeb1322fd2e86ccb5fd8bea0365f01c03bc56042e9f", + "benchmark/openhcs_measured_run.py": "1e0cccc518fb6bec9c92ea8057dd7adfc7305b2d90a6da8f152c1b97601878ab", + "benchmark/well_throughput_scaling.py": "bcb58517c9c699be96d87027725a1fe299082874002cf0dc1032c6b267283e35", + "benchmark/metrics/memory.py": "bc049ff424564b876b82d79f5a34f49a3d5dc7f374804b23d53eed1dd729e25e" + }, + "modules": 11, + "classes": [ + { + "path": "openhcs/core/pipeline_document.py", + "line": 15, + "name": "PipelineDocument", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.core.pipeline_document.PipelineDocument", + "bases": [] + }, + { + "path": "openhcs/core/pipeline_document.py", + "line": 23, + "name": "PipelineDocumentAuthority", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.core.pipeline_document.PipelineDocumentAuthority", + "bases": [] + }, + { + "path": "openhcs/core/config_document.py", + "line": 15, + "name": "ConfigDocumentField", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.core.config_document.ConfigDocumentField", + "bases": [ + "str", + "Enum" + ] + }, + { + "path": "openhcs/core/config_document.py", + "line": 21, + "name": "ConfigDocumentAuthority", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.core.config_document.ConfigDocumentAuthority", + "bases": [] + }, + { + "path": "openhcs/core/function_step_transport.py", + "line": 23, + "name": "FunctionStepTransportAuthority", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.core.function_step_transport.FunctionStepTransportAuthority", + "bases": [] + }, + { + "path": "openhcs/runtime/zmq_execution_client.py", + "line": 97, + "name": "ExecutionRuntimeLaunchPlan", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.runtime.zmq_execution_client.ExecutionRuntimeLaunchPlan", + "bases": [] + }, + { + "path": "openhcs/runtime/zmq_execution_client.py", + "line": 218, + "name": "ExecutionSubmissionPreparationTimeoutError", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.runtime.zmq_execution_client.ExecutionSubmissionPreparationTimeoutError", + "bases": [ + "TimeoutError" + ] + }, + { + "path": "openhcs/runtime/zmq_execution_client.py", + "line": 229, + "name": "OpenHCSExecutionSubmission", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.runtime.zmq_execution_client.OpenHCSExecutionSubmission", + "bases": [] + }, + { + "path": "openhcs/runtime/zmq_execution_client.py", + "line": 401, + "name": "ZMQPipelineRunPhase", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.runtime.zmq_execution_client.ZMQPipelineRunPhase", + "bases": [ + "Enum" + ] + }, + { + "path": "openhcs/runtime/zmq_execution_client.py", + "line": 411, + "name": "ZMQCompiledPipelineRun", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.runtime.zmq_execution_client.ZMQCompiledPipelineRun", + "bases": [] + }, + { + "path": "openhcs/runtime/zmq_execution_client.py", + "line": 507, + "name": "ZMQExecutionRequestBuilder", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.runtime.zmq_execution_client.ZMQExecutionRequestBuilder", + "bases": [] + }, + { + "path": "openhcs/runtime/zmq_execution_client.py", + "line": 545, + "name": "ZMQRequest", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.runtime.zmq_execution_client.ZMQRequest", + "bases": [] + }, + { + "path": "openhcs/runtime/zmq_execution_client.py", + "line": 570, + "name": "ZMQConfigParamsBoundary", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.runtime.zmq_execution_client.ZMQConfigParamsBoundary", + "bases": [] + }, + { + "path": "openhcs/runtime/zmq_execution_client.py", + "line": 600, + "name": "ZMQConfigSourceFields", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.runtime.zmq_execution_client.ZMQConfigSourceFields", + "bases": [] + }, + { + "path": "openhcs/runtime/zmq_execution_client.py", + "line": 611, + "name": "ZMQConfigProjection", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.runtime.zmq_execution_client.ZMQConfigProjection", + "bases": [] + }, + { + "path": "openhcs/runtime/zmq_execution_client.py", + "line": 657, + "name": "ZMQClientResponseView", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.runtime.zmq_execution_client.ZMQClientResponseView", + "bases": [] + }, + { + "path": "openhcs/runtime/zmq_execution_client.py", + "line": 674, + "name": "FunctionCatalogEndpointUnavailableError", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.runtime.zmq_execution_client.FunctionCatalogEndpointUnavailableError", + "bases": [ + "AgentFacingErrorMixin", + "RuntimeError" + ] + }, + { + "path": "openhcs/runtime/zmq_execution_client.py", + "line": 685, + "name": "FunctionCatalogExecutionClient", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.runtime.zmq_execution_client.FunctionCatalogExecutionClient", + "bases": [ + "ExecutionClient", + "EndpointCompatibilityClientABC" + ] + }, + { + "path": "openhcs/runtime/zmq_execution_client.py", + "line": 953, + "name": "ZMQExecutionClient", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.runtime.zmq_execution_client.ZMQExecutionClient", + "bases": [ + "FunctionCatalogExecutionClient" + ] + }, + { + "path": "openhcs/runtime/zmq_compilation.py", + "line": 53, + "name": "ZMQCompilationResult", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.runtime.zmq_compilation.ZMQCompilationResult", + "bases": [] + }, + { + "path": "openhcs/runtime/zmq_compilation.py", + "line": 69, + "name": "ZMQCompilationRequest", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.runtime.zmq_compilation.ZMQCompilationRequest", + "bases": [] + }, + { + "path": "openhcs/runtime/zmq_compilation.py", + "line": 219, + "name": "ZMQCompileArtifactRecord", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.runtime.zmq_compilation.ZMQCompileArtifactRecord", + "bases": [] + }, + { + "path": "openhcs/runtime/zmq_execution_signature.py", + "line": 36, + "name": "ZMQAuxiliaryParamField", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.runtime.zmq_execution_signature.ZMQAuxiliaryParamField", + "bases": [ + "Enum" + ] + }, + { + "path": "openhcs/runtime/zmq_execution_signature.py", + "line": 44, + "name": "ZMQRuntimeObservationExportScope", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.runtime.zmq_execution_signature.ZMQRuntimeObservationExportScope", + "bases": [ + "Enum" + ] + }, + { + "path": "openhcs/runtime/zmq_execution_signature.py", + "line": 52, + "name": "ZMQAuxiliaryExecutionParams", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.runtime.zmq_execution_signature.ZMQAuxiliaryExecutionParams", + "bases": [] + }, + { + "path": "openhcs/runtime/zmq_execution_signature.py", + "line": 206, + "name": "OpenHCSExecutionConfigBundle", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.runtime.zmq_execution_signature.OpenHCSExecutionConfigBundle", + "bases": [] + }, + { + "path": "openhcs/runtime/zmq_execution_signature.py", + "line": 214, + "name": "ZMQExecutionIdentity", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.runtime.zmq_execution_signature.ZMQExecutionIdentity", + "bases": [] + }, + { + "path": "openhcs/runtime/zmq_execution_signature.py", + "line": 240, + "name": "ZMQExecutionCompileControl", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.runtime.zmq_execution_signature.ZMQExecutionCompileControl", + "bases": [] + }, + { + "path": "openhcs/runtime/zmq_execution_signature.py", + "line": 283, + "name": "ZMQExecutionConfigTransport", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.runtime.zmq_execution_signature.ZMQExecutionConfigTransport", + "bases": [] + }, + { + "path": "openhcs/runtime/zmq_execution_signature.py", + "line": 307, + "name": "ZMQExecutionRequestPayload", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.runtime.zmq_execution_signature.ZMQExecutionRequestPayload", + "bases": [] + }, + { + "path": "openhcs/serialization/source_path_factoring.py", + "line": 23, + "name": "SourcePathOccurrenceCollector", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.serialization.source_path_factoring.SourcePathOccurrenceCollector", + "bases": [] + }, + { + "path": "openhcs/serialization/source_path_factoring.py", + "line": 33, + "name": "FactoredPathSourceLiteral", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.serialization.source_path_factoring.FactoredPathSourceLiteral", + "bases": [ + "PythonSourceLiteral" + ] + }, + { + "path": "openhcs/serialization/source_path_factoring.py", + "line": 47, + "name": "SourcePathFactoringPlan", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.serialization.source_path_factoring.SourcePathFactoringPlan", + "bases": [] + }, + { + "path": "openhcs/serialization/source_path_factoring.py", + "line": 126, + "name": "OpenHCSPythonSourceDocument", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.serialization.source_path_factoring.OpenHCSPythonSourceDocument", + "bases": [] + }, + { + "path": "openhcs/serialization/pycodify_formatters.py", + "line": 25, + "name": "NameMappingLookup", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.serialization.pycodify_formatters.NameMappingLookup", + "bases": [] + }, + { + "path": "openhcs/serialization/pycodify_formatters.py", + "line": 37, + "name": "OpenHCSCallableFormatter", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.serialization.pycodify_formatters.OpenHCSCallableFormatter", + "bases": [ + "SourceFormatter" + ] + }, + { + "path": "openhcs/serialization/pycodify_formatters.py", + "line": 54, + "name": "OpenHCSImportableTypeFormatter", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.serialization.pycodify_formatters.OpenHCSImportableTypeFormatter", + "bases": [ + "OpenHCSCallableFormatter" + ] + }, + { + "path": "openhcs/serialization/pycodify_formatters.py", + "line": 69, + "name": "PythonSourceLiteralFormatter", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.serialization.pycodify_formatters.PythonSourceLiteralFormatter", + "bases": [ + "SourceFormatter" + ] + }, + { + "path": "openhcs/serialization/pycodify_formatters.py", + "line": 84, + "name": "OpenHCSPathFormatter", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.serialization.pycodify_formatters.OpenHCSPathFormatter", + "bases": [ + "SourceFormatter" + ] + }, + { + "path": "openhcs/serialization/pycodify_formatters.py", + "line": 115, + "name": "EnumMemberFormatter", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.serialization.pycodify_formatters.EnumMemberFormatter", + "bases": [ + "SourceFormatter" + ] + }, + { + "path": "openhcs/serialization/pycodify_formatters.py", + "line": 136, + "name": "MaterializationSpecFormatter", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.serialization.pycodify_formatters.MaterializationSpecFormatter", + "bases": [ + "SourceFormatter" + ] + }, + { + "path": "openhcs/serialization/pycodify_formatters.py", + "line": 221, + "name": "FunctionPatternTupleFormatter", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.serialization.pycodify_formatters.FunctionPatternTupleFormatter", + "bases": [ + "SourceFormatter" + ] + }, + { + "path": "openhcs/serialization/pycodify_formatters.py", + "line": 273, + "name": "FunctionPatternListFormatter", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.serialization.pycodify_formatters.FunctionPatternListFormatter", + "bases": [ + "SourceFormatter" + ] + }, + { + "path": "openhcs/serialization/pycodify_formatters.py", + "line": 296, + "name": "LazyDataclassFieldEmissionState", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.serialization.pycodify_formatters.LazyDataclassFieldEmissionState", + "bases": [] + }, + { + "path": "openhcs/serialization/pycodify_formatters.py", + "line": 401, + "name": "LazyDataclassSerializedField", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.serialization.pycodify_formatters.LazyDataclassSerializedField", + "bases": [] + }, + { + "path": "openhcs/serialization/pycodify_formatters.py", + "line": 407, + "name": "LazyDataclassSerializationPlan", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.serialization.pycodify_formatters.LazyDataclassSerializationPlan", + "bases": [] + }, + { + "path": "openhcs/serialization/pycodify_formatters.py", + "line": 454, + "name": "LazyDataclassFormatEligibility", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.serialization.pycodify_formatters.LazyDataclassFormatEligibility", + "bases": [] + }, + { + "path": "openhcs/serialization/pycodify_formatters.py", + "line": 460, + "name": "FunctionStepCleanModeFieldPolicy", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.serialization.pycodify_formatters.FunctionStepCleanModeFieldPolicy", + "bases": [] + }, + { + "path": "openhcs/serialization/pycodify_formatters.py", + "line": 489, + "name": "LazyDataclassFormatter", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.serialization.pycodify_formatters.LazyDataclassFormatter", + "bases": [ + "SourceFormatter" + ] + }, + { + "path": "openhcs/serialization/pycodify_formatters.py", + "line": 517, + "name": "FunctionStepFormatter", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.openhcs.serialization.pycodify_formatters.FunctionStepFormatter", + "bases": [ + "SourceFormatter" + ] + }, + { + "path": "benchmark/openhcs_measured_run.py", + "line": 99, + "name": "_ZMQOpenHCSExecution", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.benchmark.openhcs_measured_run._ZMQOpenHCSExecution", + "bases": [] + }, + { + "path": "benchmark/openhcs_measured_run.py", + "line": 131, + "name": "_ZMQProgressTimingObserver", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.benchmark.openhcs_measured_run._ZMQProgressTimingObserver", + "bases": [] + }, + { + "path": "benchmark/well_throughput_scaling.py", + "line": 90, + "name": "WellThroughputExecutionRoute", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.benchmark.well_throughput_scaling.WellThroughputExecutionRoute", + "bases": [ + "StrEnum" + ] + }, + { + "path": "benchmark/well_throughput_scaling.py", + "line": 103, + "name": "WellThroughputServerLifecycle", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.benchmark.well_throughput_scaling.WellThroughputServerLifecycle", + "bases": [ + "StrEnum" + ] + }, + { + "path": "benchmark/well_throughput_scaling.py", + "line": 111, + "name": "WellThroughputMode", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.benchmark.well_throughput_scaling.WellThroughputMode", + "bases": [] + }, + { + "path": "benchmark/well_throughput_scaling.py", + "line": 131, + "name": "WellThroughputObservationKey", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.benchmark.well_throughput_scaling.WellThroughputObservationKey", + "bases": [] + }, + { + "path": "benchmark/well_throughput_scaling.py", + "line": 138, + "name": "WellThroughputPreset", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.benchmark.well_throughput_scaling.WellThroughputPreset", + "bases": [ + "StrEnum" + ] + }, + { + "path": "benchmark/well_throughput_scaling.py", + "line": 183, + "name": "WellThroughputModeOrder", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.benchmark.well_throughput_scaling.WellThroughputModeOrder", + "bases": [] + }, + { + "path": "benchmark/well_throughput_scaling.py", + "line": 210, + "name": "WellThroughputBenchmarkPlan", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.benchmark.well_throughput_scaling.WellThroughputBenchmarkPlan", + "bases": [] + }, + { + "path": "benchmark/well_throughput_scaling.py", + "line": 288, + "name": "WellThroughputPresentationSources", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.benchmark.well_throughput_scaling.WellThroughputPresentationSources", + "bases": [] + }, + { + "path": "benchmark/well_throughput_scaling.py", + "line": 299, + "name": "WellThroughputPresentationMode", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.benchmark.well_throughput_scaling.WellThroughputPresentationMode", + "bases": [] + }, + { + "path": "benchmark/well_throughput_scaling.py", + "line": 308, + "name": "ModuleAbstractionCoverageKind", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.benchmark.well_throughput_scaling.ModuleAbstractionCoverageKind", + "bases": [ + "StrEnum" + ] + }, + { + "path": "benchmark/well_throughput_scaling.py", + "line": 350, + "name": "ModuleAbstractionCoverageRow", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.benchmark.well_throughput_scaling.ModuleAbstractionCoverageRow", + "bases": [] + }, + { + "path": "benchmark/well_throughput_scaling.py", + "line": 365, + "name": "ModuleAbstractionCoverageTable", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.benchmark.well_throughput_scaling.ModuleAbstractionCoverageTable", + "bases": [] + }, + { + "path": "benchmark/well_throughput_scaling.py", + "line": 417, + "name": "PresentationAxisBand", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.benchmark.well_throughput_scaling.PresentationAxisBand", + "bases": [] + }, + { + "path": "benchmark/well_throughput_scaling.py", + "line": 441, + "name": "PresentationAxisBandPolicy", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.benchmark.well_throughput_scaling.PresentationAxisBandPolicy", + "bases": [] + }, + { + "path": "benchmark/well_throughput_scaling.py", + "line": 491, + "name": "WellThroughputPresentationReport", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.benchmark.well_throughput_scaling.WellThroughputPresentationReport", + "bases": [] + }, + { + "path": "benchmark/well_throughput_scaling.py", + "line": 1860, + "name": "WellsPerCoreSummaryRow", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.benchmark.well_throughput_scaling.WellsPerCoreSummaryRow", + "bases": [] + }, + { + "path": "benchmark/well_throughput_scaling.py", + "line": 1898, + "name": "NativeCellProfilerExecutionBaseline", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.benchmark.well_throughput_scaling.NativeCellProfilerExecutionBaseline", + "bases": [] + }, + { + "path": "benchmark/well_throughput_scaling.py", + "line": 1909, + "name": "WellThroughputStatus", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.benchmark.well_throughput_scaling.WellThroughputStatus", + "bases": [ + "StrEnum" + ] + }, + { + "path": "benchmark/well_throughput_scaling.py", + "line": 1918, + "name": "ChildProcessTerminator", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.benchmark.well_throughput_scaling.ChildProcessTerminator", + "bases": [] + }, + { + "path": "benchmark/well_throughput_scaling.py", + "line": 1947, + "name": "WorkerLaneEventPhase", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.benchmark.well_throughput_scaling.WorkerLaneEventPhase", + "bases": [ + "str", + "Enum" + ] + }, + { + "path": "benchmark/well_throughput_scaling.py", + "line": 2017, + "name": "WellThroughputResult", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.benchmark.well_throughput_scaling.WellThroughputResult", + "bases": [] + }, + { + "path": "benchmark/metrics/memory.py", + "line": 15, + "name": "MemoryMetric", + "canonical": true, + "symbol": "home.ts.code.projects.openhcs-shared-runtime-plumbing.benchmark.metrics.memory.MemoryMetric", + "bases": [ + "MetricCollector" + ] + } + ], + "original_class_count": 74, + "canonical_count": 74 +} diff --git a/benchmark/results/perf_illum3_runtime_ledger_20261003/render-retention-decision.json b/benchmark/results/perf_illum3_runtime_ledger_20261003/render-retention-decision.json new file mode 100644 index 0000000000..5a63b2105a --- /dev/null +++ b/benchmark/results/perf_illum3_runtime_ledger_20261003/render-retention-decision.json @@ -0,0 +1,72 @@ +{ + "status": "STOP_AS_DOMINANT_PERFORMANCE_ROUTE", + "source_head": "4c0d62e45c8afb85e34b69d7279cd00710f519b5", + "source_unchanged": true, + "actual_import_root": "/home/ts/code/projects/openhcs-shared-runtime-plumbing/openhcs/__init__.py", + "repetitions": 6, + "alternating_observation_order": true, + "mandatory_submit_builder_medians_seconds": { + "submit_compile_total": 0.04574766173027456, + "submit_execution_total": 0.04563480010256171 + }, + "optional_completion_render_medians_seconds": { + "completion_pipeline": 0.04025681456550956, + "completion_config": 0.005451454082503915, + "returned_pipeline": 0.04034627298824489 + }, + "optional_combined_removal_ceiling_seconds": { + "median_all6": 0.08608636120334268, + "minimum_all6": 0.08512948034331203, + "maximum_all6": 0.08700018096715212, + "median_steady5": 0.08606792613863945 + }, + "actual_payload_read_median_seconds": 1.0470394045114517e-06, + "current_Illum3_total_2x_gap_seconds": 1.22610824, + "local_ceiling_fraction_of_total_gap": 0.07021106162971605, + "execution_gain_claim": null, + "NRA_census": { + "modules": 11, + "original_classes": 74, + "canonical_classes": 74, + "NRA_head": "0844525ecaba93e090a064a4ae4914466b2dae60" + }, + "controls": [ + { + "name": "actual_FunctionStep_constructor_callbacks_at_both_submit_epochs", + "status": "PASS" + }, + { + "name": "original_first_constructor_error_order", + "status": "PASS" + }, + { + "name": "nested_kwargs_mutation_changes_real_compilation_signature", + "status": "PASS" + }, + { + "name": "lawful_submission_config_replacement_changes_real_compilation_signature", + "status": "PASS" + }, + { + "name": "actual_execution_payload_snapshot_survives_later_authoring_mutation", + "status": "PASS" + }, + { + "name": "reviewed_source_public_contract", + "status": "PASS" + } + ], + "original_invocation_failure": { + "path": "/tmp/openhcs-render-retention-4c0d62/failure-v1.json", + "reason": "Controller attempted direct mutation of the genuinely frozen GlobalPipelineConfig. Preserved failure; corrected control uses lawful config replacement on mutable submission and explicitly retains frozen-assignment rejection." + }, + "production_plan_if_correctness_repair_authorized": "Carry actual execution request payload from existing ZMQExecutionRequestBuilder through existing ZMQCompiledPipelineRun and use its sent pipeline/config strings for retain_measured_openhcs_completion and final return. Keep independently rebuilt compile and execution submissions and mismatch rejection. No new wrapper, mutable-object render cache, or compile-to-execution source reuse. Root owns implementation.", + "evidence_hashes": { + "replay.py": "ae8d5785adb7caa44efd753dc338ed7cee4ef2de276dc638b714ed1642eaa3f0", + "census.py": "5ebc7845157055e6859a58e4f444dd1ad7536b7b2b7e35bea2d291ccc385cc2f", + "census.json": "16f23b40d587e3d9e2005fd6e66d47e4d3264ed90e655b676c425095dfbd02d5", + "result-v3.json": "28c79034129b4ea13aa7236c842b7b43bb0d6a5e67d84c22c6aca720d6bbed08", + "failure-v1.json": "efd6c37a49510503990f6127494207141d2f26ee36246a43c535cba3569395bd" + }, + "boundaries": "Public retained-source loaders reconstruct authored typed objects outside timers; real 10-step Illum3 source/config bytes match exactly. Source-only replay; no network, pipeline/server execution, evidence IO or scientific artifact recapture. All source hashes and clean HEAD unchanged. Pure declaration preparation retains original constructor/default-factory epochs; first reflection analysis labeled separately." +} diff --git a/benchmark/results/perf_illum3_runtime_ledger_20261003/render-retention-replay.json b/benchmark/results/perf_illum3_runtime_ledger_20261003/render-retention-replay.json new file mode 100644 index 0000000000..4c862bfc36 --- /dev/null +++ b/benchmark/results/perf_illum3_runtime_ledger_20261003/render-retention-replay.json @@ -0,0 +1,817 @@ +{ + "status": "PASS", + "head": "4c0d62e45c8afb85e34b69d7279cd00710f519b5", + "source_unchanged": true, + "source_hashes": { + "openhcs/core/pipeline_document.py": "0849800b3e6474b8176fe35bc4aaebc63cfa85bd77251000c50da5f4c928b1b2", + "openhcs/core/config_document.py": "37f66fd971806e059da6194c003e20a7ad08f69066eec4af7ebb11b4b70a08db", + "openhcs/core/function_step_transport.py": "b4c89539ace519926d178eb85a7909b4a7894203bf340213279e4f2a11e1152f", + "openhcs/runtime/zmq_execution_client.py": "8f98169438b1f9d65dd2a4f1517b5901e5d2ffd944fce4b0779a8f9b17d01db3", + "openhcs/runtime/zmq_compilation.py": "ad958c78ec9a26b778ad231501751f38ec6dd3d0c73ca2c1082207031fc6781f", + "openhcs/runtime/zmq_execution_signature.py": "41dcdb0a40446947a42a87482f27da051408015eec972e11e4d3329464db11ac", + "openhcs/serialization/source_path_factoring.py": "d8dd55b7d45882e067f9fddbe87327000a7e226dbc531a90fc543f201ea665ab", + "openhcs/serialization/pycodify_formatters.py": "b86d8a18b32084839667ebeb1322fd2e86ccb5fd8bea0365f01c03bc56042e9f", + "benchmark/openhcs_measured_run.py": "1e0cccc518fb6bec9c92ea8057dd7adfc7305b2d90a6da8f152c1b97601878ab", + "benchmark/well_throughput_scaling.py": "bcb58517c9c699be96d87027725a1fe299082874002cf0dc1032c6b267283e35", + "benchmark/metrics/memory.py": "bc049ff424564b876b82d79f5a34f49a3d5dc7f374804b23d53eed1dd729e25e" + }, + "actual_import_root": "/home/ts/code/projects/openhcs-shared-runtime-plumbing/openhcs/__init__.py", + "retained_source_paths": { + "pipeline": "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/ordinary-1/ExampleIlluminationCorrection_Example3/wells_1/workers_1/ordinary_run_evidence/ExampleIlluminationCorrection_Example3-1w_1t-1791013778142438710/submitted_pipeline.py", + "config": "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/ordinary-1/ExampleIlluminationCorrection_Example3/wells_1/workers_1/ordinary_run_evidence/ExampleIlluminationCorrection_Example3-1w_1t-1791013778142438710/submitted_global_config.py" + }, + "retained_source_hashes": { + "pipeline": "c313ff0830e45c62dc23d7ce5a013c490383a007a65a633c3c5df973a5edc687", + "config": "2e2cf95d87a9465d20ecbb8c88629ce615b64de6e5843319f9b51599f343c1c3" + }, + "preparation": { + "actual_registered_declarations": 44, + "entry_point": "SignatureAnalyzer.prepare_dataclass_declaration over existing LazyDataclassFactory.registered_type_pairs", + "FunctionStep_constructors_during_pure_preparation": 0, + "law": "No default factory/formatter memo introduced. First reflection-analysis epoch remains separately labeled, not hidden in preparation." + }, + "runs": [ + { + "index": 0, + "first_reflection_analysis_epoch": true, + "alternating_observation_order": [ + "original_render", + "actual_payload_read" + ], + "completion_observations": { + "original_render": { + "seconds": 0.08703801175579429, + "hashes": [ + "c313ff0830e45c62dc23d7ce5a013c490383a007a65a633c3c5df973a5edc687", + "2e2cf95d87a9465d20ecbb8c88629ce615b64de6e5843319f9b51599f343c1c3", + "c313ff0830e45c62dc23d7ce5a013c490383a007a65a633c3c5df973a5edc687" + ] + }, + "actual_payload_read": { + "seconds": 3.3262185752391815e-06, + "hashes": [ + "c313ff0830e45c62dc23d7ce5a013c490383a007a65a633c3c5df973a5edc687", + "2e2cf95d87a9465d20ecbb8c88629ce615b64de6e5843319f9b51599f343c1c3", + "c313ff0830e45c62dc23d7ce5a013c490383a007a65a633c3c5df973a5edc687" + ] + } + }, + "submit_compile_total": 0.06521604722365737, + "submit_execution_total": 0.04581552976742387, + "original_render_events": [ + { + "epoch": "0:submit_compile", + "owner": "pipeline", + "seconds": 0.05955396592617035, + "sha256": "c313ff0830e45c62dc23d7ce5a013c490383a007a65a633c3c5df973a5edc687", + "bytes": 10647 + }, + { + "epoch": "0:submit_compile", + "owner": "config", + "seconds": 0.005617266055196524, + "sha256": "2e2cf95d87a9465d20ecbb8c88629ce615b64de6e5843319f9b51599f343c1c3", + "bytes": 407 + }, + { + "epoch": "0:submit_execution", + "owner": "pipeline", + "seconds": 0.040430141147226095, + "sha256": "c313ff0830e45c62dc23d7ce5a013c490383a007a65a633c3c5df973a5edc687", + "bytes": 10647 + }, + { + "epoch": "0:submit_execution", + "owner": "config", + "seconds": 0.005347607657313347, + "sha256": "2e2cf95d87a9465d20ecbb8c88629ce615b64de6e5843319f9b51599f343c1c3", + "bytes": 407 + }, + { + "epoch": "0:completion_pipeline", + "owner": "pipeline", + "seconds": 0.04034718498587608, + "sha256": "c313ff0830e45c62dc23d7ce5a013c490383a007a65a633c3c5df973a5edc687", + "bytes": 10647 + }, + { + "epoch": "0:completion_config", + "owner": "config", + "seconds": 0.005629708990454674, + "sha256": "2e2cf95d87a9465d20ecbb8c88629ce615b64de6e5843319f9b51599f343c1c3", + "bytes": 407 + }, + { + "epoch": "0:returned_pipeline", + "owner": "pipeline", + "seconds": 0.04102328699082136, + "sha256": "c313ff0830e45c62dc23d7ce5a013c490383a007a65a633c3c5df973a5edc687", + "bytes": 10647 + } + ], + "maximum_retention_return_removal_seconds": 0.08700018096715212, + "signature": "7b48f26c01b7c75e705c8fcaf0ccc83dabeeced6a14b0b74ef3f582be72aeabe" + }, + { + "index": 1, + "first_reflection_analysis_epoch": false, + "alternating_observation_order": [ + "actual_payload_read", + "original_render" + ], + "completion_observations": { + "actual_payload_read": { + "seconds": 1.0421499609947205e-06, + "hashes": [ + "c313ff0830e45c62dc23d7ce5a013c490383a007a65a633c3c5df973a5edc687", + "2e2cf95d87a9465d20ecbb8c88629ce615b64de6e5843319f9b51599f343c1c3", + "c313ff0830e45c62dc23d7ce5a013c490383a007a65a633c3c5df973a5edc687" + ] + }, + "original_render": { + "seconds": 0.08516591787338257, + "hashes": [ + "c313ff0830e45c62dc23d7ce5a013c490383a007a65a633c3c5df973a5edc687", + "2e2cf95d87a9465d20ecbb8c88629ce615b64de6e5843319f9b51599f343c1c3", + "c313ff0830e45c62dc23d7ce5a013c490383a007a65a633c3c5df973a5edc687" + ] + } + }, + "submit_compile_total": 0.04546358669176698, + "submit_execution_total": 0.04510248685255647, + "original_render_events": [ + { + "epoch": "1:submit_compile", + "owner": "pipeline", + "seconds": 0.04016154492273927, + "sha256": "c313ff0830e45c62dc23d7ce5a013c490383a007a65a633c3c5df973a5edc687", + "bytes": 10647 + }, + { + "epoch": "1:submit_compile", + "owner": "config", + "seconds": 0.005269180051982403, + "sha256": "2e2cf95d87a9465d20ecbb8c88629ce615b64de6e5843319f9b51599f343c1c3", + "bytes": 407 + }, + { + "epoch": "1:submit_execution", + "owner": "pipeline", + "seconds": 0.039719612803310156, + "sha256": "c313ff0830e45c62dc23d7ce5a013c490383a007a65a633c3c5df973a5edc687", + "bytes": 10647 + }, + { + "epoch": "1:submit_execution", + "owner": "config", + "seconds": 0.005344413220882416, + "sha256": "2e2cf95d87a9465d20ecbb8c88629ce615b64de6e5843319f9b51599f343c1c3", + "bytes": 407 + }, + { + "epoch": "1:completion_pipeline", + "owner": "pipeline", + "seconds": 0.03979505505412817, + "sha256": "c313ff0830e45c62dc23d7ce5a013c490383a007a65a633c3c5df973a5edc687", + "bytes": 10647 + }, + { + "epoch": "1:completion_config", + "owner": "config", + "seconds": 0.005292725283652544, + "sha256": "2e2cf95d87a9465d20ecbb8c88629ce615b64de6e5843319f9b51599f343c1c3", + "bytes": 407 + }, + { + "epoch": "1:returned_pipeline", + "owner": "pipeline", + "seconds": 0.04004170000553131, + "sha256": "c313ff0830e45c62dc23d7ce5a013c490383a007a65a633c3c5df973a5edc687", + "bytes": 10647 + } + ], + "maximum_retention_return_removal_seconds": 0.08512948034331203, + "signature": "7b48f26c01b7c75e705c8fcaf0ccc83dabeeced6a14b0b74ef3f582be72aeabe" + }, + { + "index": 2, + "first_reflection_analysis_epoch": false, + "alternating_observation_order": [ + "original_render", + "actual_payload_read" + ], + "completion_observations": { + "original_render": { + "seconds": 0.08613781817257404, + "hashes": [ + "c313ff0830e45c62dc23d7ce5a013c490383a007a65a633c3c5df973a5edc687", + "2e2cf95d87a9465d20ecbb8c88629ce615b64de6e5843319f9b51599f343c1c3", + "c313ff0830e45c62dc23d7ce5a013c490383a007a65a633c3c5df973a5edc687" + ] + }, + "actual_payload_read": { + "seconds": 1.0421499609947205e-06, + "hashes": [ + "c313ff0830e45c62dc23d7ce5a013c490383a007a65a633c3c5df973a5edc687", + "2e2cf95d87a9465d20ecbb8c88629ce615b64de6e5843319f9b51599f343c1c3", + "c313ff0830e45c62dc23d7ce5a013c490383a007a65a633c3c5df973a5edc687" + ] + } + }, + "submit_compile_total": 0.04603173676878214, + "submit_execution_total": 0.045378165785223246, + "original_render_events": [ + { + "epoch": "2:submit_compile", + "owner": "pipeline", + "seconds": 0.04065810004249215, + "sha256": "c313ff0830e45c62dc23d7ce5a013c490383a007a65a633c3c5df973a5edc687", + "bytes": 10647 + }, + { + "epoch": "2:submit_compile", + "owner": "config", + "seconds": 0.005338640883564949, + "sha256": "2e2cf95d87a9465d20ecbb8c88629ce615b64de6e5843319f9b51599f343c1c3", + "bytes": 407 + }, + { + "epoch": "2:submit_execution", + "owner": "pipeline", + "seconds": 0.040114596020430326, + "sha256": "c313ff0830e45c62dc23d7ce5a013c490383a007a65a633c3c5df973a5edc687", + "bytes": 10647 + }, + { + "epoch": "2:submit_execution", + "owner": "config", + "seconds": 0.005215710029006004, + "sha256": "2e2cf95d87a9465d20ecbb8c88629ce615b64de6e5843319f9b51599f343c1c3", + "bytes": 407 + }, + { + "epoch": "2:completion_pipeline", + "owner": "pipeline", + "seconds": 0.04045192198827863, + "sha256": "c313ff0830e45c62dc23d7ce5a013c490383a007a65a633c3c5df973a5edc687", + "bytes": 10647 + }, + { + "epoch": "2:completion_config", + "owner": "config", + "seconds": 0.005408914294093847, + "sha256": "2e2cf95d87a9465d20ecbb8c88629ce615b64de6e5843319f9b51599f343c1c3", + "bytes": 407 + }, + { + "epoch": "2:returned_pipeline", + "owner": "pipeline", + "seconds": 0.04024395998567343, + "sha256": "c313ff0830e45c62dc23d7ce5a013c490383a007a65a633c3c5df973a5edc687", + "bytes": 10647 + } + ], + "maximum_retention_return_removal_seconds": 0.0861047962680459, + "signature": "7b48f26c01b7c75e705c8fcaf0ccc83dabeeced6a14b0b74ef3f582be72aeabe" + }, + { + "index": 3, + "first_reflection_analysis_epoch": false, + "alternating_observation_order": [ + "actual_payload_read", + "original_render" + ], + "completion_observations": { + "actual_payload_read": { + "seconds": 1.051928848028183e-06, + "hashes": [ + "c313ff0830e45c62dc23d7ce5a013c490383a007a65a633c3c5df973a5edc687", + "2e2cf95d87a9465d20ecbb8c88629ce615b64de6e5843319f9b51599f343c1c3", + "c313ff0830e45c62dc23d7ce5a013c490383a007a65a633c3c5df973a5edc687" + ] + }, + "original_render": { + "seconds": 0.08610337320715189, + "hashes": [ + "c313ff0830e45c62dc23d7ce5a013c490383a007a65a633c3c5df973a5edc687", + "2e2cf95d87a9465d20ecbb8c88629ce615b64de6e5843319f9b51599f343c1c3", + "c313ff0830e45c62dc23d7ce5a013c490383a007a65a633c3c5df973a5edc687" + ] + } + }, + "submit_compile_total": 0.04541486594825983, + "submit_execution_total": 0.04553820798173547, + "original_render_events": [ + { + "epoch": "3:submit_compile", + "owner": "pipeline", + "seconds": 0.0400394550524652, + "sha256": "c313ff0830e45c62dc23d7ce5a013c490383a007a65a633c3c5df973a5edc687", + "bytes": 10647 + }, + { + "epoch": "3:submit_compile", + "owner": "config", + "seconds": 0.005345273297280073, + "sha256": "2e2cf95d87a9465d20ecbb8c88629ce615b64de6e5843319f9b51599f343c1c3", + "bytes": 407 + }, + { + "epoch": "3:submit_execution", + "owner": "pipeline", + "seconds": 0.04022560594603419, + "sha256": "c313ff0830e45c62dc23d7ce5a013c490383a007a65a633c3c5df973a5edc687", + "bytes": 10647 + }, + { + "epoch": "3:submit_execution", + "owner": "config", + "seconds": 0.005268760025501251, + "sha256": "2e2cf95d87a9465d20ecbb8c88629ce615b64de6e5843319f9b51599f343c1c3", + "bytes": 407 + }, + { + "epoch": "3:completion_pipeline", + "owner": "pipeline", + "seconds": 0.04016644414514303, + "sha256": "c313ff0830e45c62dc23d7ce5a013c490383a007a65a633c3c5df973a5edc687", + "bytes": 10647 + }, + { + "epoch": "3:completion_config", + "owner": "config", + "seconds": 0.005493993870913982, + "sha256": "2e2cf95d87a9465d20ecbb8c88629ce615b64de6e5843319f9b51599f343c1c3", + "bytes": 407 + }, + { + "epoch": "3:returned_pipeline", + "owner": "pipeline", + "seconds": 0.040407488122582436, + "sha256": "c313ff0830e45c62dc23d7ce5a013c490383a007a65a633c3c5df973a5edc687", + "bytes": 10647 + } + ], + "maximum_retention_return_removal_seconds": 0.08606792613863945, + "signature": "7b48f26c01b7c75e705c8fcaf0ccc83dabeeced6a14b0b74ef3f582be72aeabe" + }, + { + "index": 4, + "first_reflection_analysis_epoch": false, + "alternating_observation_order": [ + "original_render", + "actual_payload_read" + ], + "completion_observations": { + "original_render": { + "seconds": 0.08702337462455034, + "hashes": [ + "c313ff0830e45c62dc23d7ce5a013c490383a007a65a633c3c5df973a5edc687", + "2e2cf95d87a9465d20ecbb8c88629ce615b64de6e5843319f9b51599f343c1c3", + "c313ff0830e45c62dc23d7ce5a013c490383a007a65a633c3c5df973a5edc687" + ] + }, + "actual_payload_read": { + "seconds": 1.2721866369247437e-06, + "hashes": [ + "c313ff0830e45c62dc23d7ce5a013c490383a007a65a633c3c5df973a5edc687", + "2e2cf95d87a9465d20ecbb8c88629ce615b64de6e5843319f9b51599f343c1c3", + "c313ff0830e45c62dc23d7ce5a013c490383a007a65a633c3c5df973a5edc687" + ] + } + }, + "submit_compile_total": 0.04540669033303857, + "submit_execution_total": 0.04590281518176198, + "original_render_events": [ + { + "epoch": "4:submit_compile", + "owner": "pipeline", + "seconds": 0.040172276087105274, + "sha256": "c313ff0830e45c62dc23d7ce5a013c490383a007a65a633c3c5df973a5edc687", + "bytes": 10647 + }, + { + "epoch": "4:submit_compile", + "owner": "config", + "seconds": 0.005201412830501795, + "sha256": "2e2cf95d87a9465d20ecbb8c88629ce615b64de6e5843319f9b51599f343c1c3", + "bytes": 407 + }, + { + "epoch": "4:submit_execution", + "owner": "pipeline", + "seconds": 0.040503378957509995, + "sha256": "c313ff0830e45c62dc23d7ce5a013c490383a007a65a633c3c5df973a5edc687", + "bytes": 10647 + }, + { + "epoch": "4:submit_execution", + "owner": "config", + "seconds": 0.005346857011318207, + "sha256": "2e2cf95d87a9465d20ecbb8c88629ce615b64de6e5843319f9b51599f343c1c3", + "bytes": 407 + }, + { + "epoch": "4:completion_pipeline", + "owner": "pipeline", + "seconds": 0.04052451904863119, + "sha256": "c313ff0830e45c62dc23d7ce5a013c490383a007a65a633c3c5df973a5edc687", + "bytes": 10647 + }, + { + "epoch": "4:completion_config", + "owner": "config", + "seconds": 0.005857598967850208, + "sha256": "2e2cf95d87a9465d20ecbb8c88629ce615b64de6e5843319f9b51599f343c1c3", + "bytes": 407 + }, + { + "epoch": "4:returned_pipeline", + "owner": "pipeline", + "seconds": 0.040605089627206326, + "sha256": "c313ff0830e45c62dc23d7ce5a013c490383a007a65a633c3c5df973a5edc687", + "bytes": 10647 + } + ], + "maximum_retention_return_removal_seconds": 0.08698720764368773, + "signature": "7b48f26c01b7c75e705c8fcaf0ccc83dabeeced6a14b0b74ef3f582be72aeabe" + }, + { + "index": 5, + "first_reflection_analysis_epoch": false, + "alternating_observation_order": [ + "actual_payload_read", + "original_render" + ], + "completion_observations": { + "actual_payload_read": { + "seconds": 6.118789315223694e-07, + "hashes": [ + "c313ff0830e45c62dc23d7ce5a013c490383a007a65a633c3c5df973a5edc687", + "2e2cf95d87a9465d20ecbb8c88629ce615b64de6e5843319f9b51599f343c1c3", + "c313ff0830e45c62dc23d7ce5a013c490383a007a65a633c3c5df973a5edc687" + ] + }, + "original_render": { + "seconds": 0.08574638096615672, + "hashes": [ + "c313ff0830e45c62dc23d7ce5a013c490383a007a65a633c3c5df973a5edc687", + "2e2cf95d87a9465d20ecbb8c88629ce615b64de6e5843319f9b51599f343c1c3", + "c313ff0830e45c62dc23d7ce5a013c490383a007a65a633c3c5df973a5edc687" + ] + } + }, + "submit_compile_total": 0.0462398286908865, + "submit_execution_total": 0.04573139222338796, + "original_render_events": [ + { + "epoch": "5:submit_compile", + "owner": "pipeline", + "seconds": 0.040598487947136164, + "sha256": "c313ff0830e45c62dc23d7ce5a013c490383a007a65a633c3c5df973a5edc687", + "bytes": 10647 + }, + { + "epoch": "5:submit_compile", + "owner": "config", + "seconds": 0.0056106531992554665, + "sha256": "2e2cf95d87a9465d20ecbb8c88629ce615b64de6e5843319f9b51599f343c1c3", + "bytes": 407 + }, + { + "epoch": "5:submit_execution", + "owner": "pipeline", + "seconds": 0.04038529610261321, + "sha256": "c313ff0830e45c62dc23d7ce5a013c490383a007a65a633c3c5df973a5edc687", + "bytes": 10647 + }, + { + "epoch": "5:submit_execution", + "owner": "config", + "seconds": 0.005311900749802589, + "sha256": "2e2cf95d87a9465d20ecbb8c88629ce615b64de6e5843319f9b51599f343c1c3", + "bytes": 407 + }, + { + "epoch": "5:completion_pipeline", + "owner": "pipeline", + "seconds": 0.040041168220341206, + "sha256": "c313ff0830e45c62dc23d7ce5a013c490383a007a65a633c3c5df973a5edc687", + "bytes": 10647 + }, + { + "epoch": "5:completion_config", + "owner": "config", + "seconds": 0.0053860098123550415, + "sha256": "2e2cf95d87a9465d20ecbb8c88629ce615b64de6e5843319f9b51599f343c1c3", + "bytes": 407 + }, + { + "epoch": "5:returned_pipeline", + "owner": "pipeline", + "seconds": 0.04028505785390735, + "sha256": "c313ff0830e45c62dc23d7ce5a013c490383a007a65a633c3c5df973a5edc687", + "bytes": 10647 + } + ], + "maximum_retention_return_removal_seconds": 0.0857122358866036, + "signature": "7b48f26c01b7c75e705c8fcaf0ccc83dabeeced6a14b0b74ef3f582be72aeabe" + } + ], + "maximum_removed_summary": { + "median_all6": 0.08608636120334268, + "minimum_all6": 0.08512948034331203, + "maximum_all6": 0.08700018096715212, + "median_steady5": 0.08606792613863945 + }, + "controls": [ + { + "name": "actual_FunctionStep_constructor_callbacks_at_both_submit_epochs", + "status": "PASS", + "counts": { + "required_submit_compile": 30, + "required_submit_execution": 30 + }, + "events": [ + { + "epoch": "required_submit_compile", + "called_original": true + }, + { + "epoch": "required_submit_compile", + "called_original": true + }, + { + "epoch": "required_submit_compile", + "called_original": true + }, + { + "epoch": "required_submit_compile", + "called_original": true + }, + { + "epoch": "required_submit_compile", + "called_original": true + }, + { + "epoch": "required_submit_compile", + "called_original": true + }, + { + "epoch": "required_submit_compile", + "called_original": true + }, + { + "epoch": "required_submit_compile", + "called_original": true + }, + { + "epoch": "required_submit_compile", + "called_original": true + }, + { + "epoch": "required_submit_compile", + "called_original": true + }, + { + "epoch": "required_submit_compile", + "called_original": true + }, + { + "epoch": "required_submit_compile", + "called_original": true + }, + { + "epoch": "required_submit_compile", + "called_original": true + }, + { + "epoch": "required_submit_compile", + "called_original": true + }, + { + "epoch": "required_submit_compile", + "called_original": true + }, + { + "epoch": "required_submit_compile", + "called_original": true + }, + { + "epoch": "required_submit_compile", + "called_original": true + }, + { + "epoch": "required_submit_compile", + "called_original": true + }, + { + "epoch": "required_submit_compile", + "called_original": true + }, + { + "epoch": "required_submit_compile", + "called_original": true + }, + { + "epoch": "required_submit_compile", + "called_original": true + }, + { + "epoch": "required_submit_compile", + "called_original": true + }, + { + "epoch": "required_submit_compile", + "called_original": true + }, + { + "epoch": "required_submit_compile", + "called_original": true + }, + { + "epoch": "required_submit_compile", + "called_original": true + }, + { + "epoch": "required_submit_compile", + "called_original": true + }, + { + "epoch": "required_submit_compile", + "called_original": true + }, + { + "epoch": "required_submit_compile", + "called_original": true + }, + { + "epoch": "required_submit_compile", + "called_original": true + }, + { + "epoch": "required_submit_compile", + "called_original": true + }, + { + "epoch": "required_submit_execution", + "called_original": true + }, + { + "epoch": "required_submit_execution", + "called_original": true + }, + { + "epoch": "required_submit_execution", + "called_original": true + }, + { + "epoch": "required_submit_execution", + "called_original": true + }, + { + "epoch": "required_submit_execution", + "called_original": true + }, + { + "epoch": "required_submit_execution", + "called_original": true + }, + { + "epoch": "required_submit_execution", + "called_original": true + }, + { + "epoch": "required_submit_execution", + "called_original": true + }, + { + "epoch": "required_submit_execution", + "called_original": true + }, + { + "epoch": "required_submit_execution", + "called_original": true + }, + { + "epoch": "required_submit_execution", + "called_original": true + }, + { + "epoch": "required_submit_execution", + "called_original": true + }, + { + "epoch": "required_submit_execution", + "called_original": true + }, + { + "epoch": "required_submit_execution", + "called_original": true + }, + { + "epoch": "required_submit_execution", + "called_original": true + }, + { + "epoch": "required_submit_execution", + "called_original": true + }, + { + "epoch": "required_submit_execution", + "called_original": true + }, + { + "epoch": "required_submit_execution", + "called_original": true + }, + { + "epoch": "required_submit_execution", + "called_original": true + }, + { + "epoch": "required_submit_execution", + "called_original": true + }, + { + "epoch": "required_submit_execution", + "called_original": true + }, + { + "epoch": "required_submit_execution", + "called_original": true + }, + { + "epoch": "required_submit_execution", + "called_original": true + }, + { + "epoch": "required_submit_execution", + "called_original": true + }, + { + "epoch": "required_submit_execution", + "called_original": true + }, + { + "epoch": "required_submit_execution", + "called_original": true + }, + { + "epoch": "required_submit_execution", + "called_original": true + }, + { + "epoch": "required_submit_execution", + "called_original": true + }, + { + "epoch": "required_submit_execution", + "called_original": true + }, + { + "epoch": "required_submit_execution", + "called_original": true + } + ] + }, + { + "name": "original_first_constructor_error_order", + "status": "PASS", + "events": [ + 1, + 2, + 3 + ] + }, + { + "name": "nested_kwargs_mutation_changes_real_compilation_signature", + "status": "PASS", + "before": "7b48f26c01b7c75e705c8fcaf0ccc83dabeeced6a14b0b74ef3f582be72aeabe", + "after": "ee83d723591705cf2adf6c65e572d18d33993121e8642346f3d205d0233cf352", + "scope": "Actual request signature changes; no locally retained compiled execution artifact exists, so no fake artifact/server receiver was constructed." + }, + { + "name": "lawful_submission_config_replacement_changes_real_compilation_signature", + "status": "PASS", + "before": "7b48f26c01b7c75e705c8fcaf0ccc83dabeeced6a14b0b74ef3f582be72aeabe", + "after": "9f26d2c15fa6857faa197e5f7f2c49cc87375809f45addb44dad52d5bd71dadd" + }, + { + "name": "actual_execution_payload_snapshot_survives_later_authoring_mutation", + "status": "PASS", + "actual_sha": "c313ff0830e45c62dc23d7ce5a013c490383a007a65a633c3c5df973a5edc687" + }, + { + "name": "reviewed_source_public_contract", + "status": "PASS" + } + ], + "limits": [ + "Original API renders only, no client network/server/pipeline job or completion evidence IO was executed.", + "Each repetition executes original two submit builders plus all third/fourth pipeline and third config renders, alternating original render versus actual sent-payload read order. Payload reads are an offline observation only; no production path was implemented.", + "Timing callbacks add bounded attribution overhead and current unprofiled interpreter may differ from an ordinary reused client; savings are a local removal ceiling, not end-to-end gain.", + "No actual compiled artifact was fabricated; actual changed signature refusal must remain exercised by existing receiver tests or later authorized full transaction gate." + ], + "decision": "Local immutable source replay supplies actual removed-render ceiling only; no production caching or implementation." +} diff --git a/benchmark/results/perf_illum3_runtime_ledger_20261003/report.rst b/benchmark/results/perf_illum3_runtime_ledger_20261003/report.rst new file mode 100644 index 0000000000..40b945e71c --- /dev/null +++ b/benchmark/results/perf_illum3_runtime_ledger_20261003/report.rst @@ -0,0 +1,97 @@ +Illumination 3 compiler and runtime diagnostic +============================================= + +The weakest retained single-thread native comparison is still dominated by +framework work. On unchanged production source 4c0d62e45, one fresh ordinary-path +diagnostic records 0.422891s execution, 0.597369s compilation and 1.419051s total. +The measured execution root is 0.415053s: 11 raw callable invocations account for +0.111740s; the remaining 0.303313s is 73.08% of that root. These are diagnostic +observations, not an optimization, an ordinary paired speedup, or a replacement +for the two-repeat native comparison. The objective remains unfinished. + +The original public driver uses one well and one worker on CPU5, mandatory +library/kernel readiness before pipeline clocks, default OUTCOMES and the +original memory observer. Startup and shutdown are excluded. The official +benchmark lease, source/native/environment/input custody and bounded artifact +controls pass. Both profilers are disabled. All 41 coarse hooks preserve the +original call, result/error identity, descriptor and argument mutation; 28 +controls pass. Exclusive clocks sum exactly to each root. Original interval +JSON files are retained rather than reconstructed from percentages. + +No event-emitting Numba compilation was observed +---------------------------------------------- + +A passive listener is installed after the original installed ``numba.core.event`` +module executes, before readiness. There are zero native ``numba:compile`` START +or END events in the retained startup and measured roots. Original module and +dispatcher bytes are pinned. Unique bounded event batches flush after each +measured root stops, so forced shutdown cannot erase its evidence. All required +root flushes pass; the four event files total 3,107 bytes with no refusals/errors. + +This observation rules out event-emitting dispatcher compilation as its pipeline +cost. It does not measure cache loads or explain the older 0.686775/0.385197s +execution spread. The existing polynomial preparation already covers its +declared masked/unmasked and writable/read-only families; no missing warmup is +inferred from a truncated source excerpt or the old variance. + +The dominant compiler term is state construction and resolution +------------------------------------------------------------- + +Compilation's root is 0.588262s. Twenty-three ObjectState constructions, +46 distinct live/saved snapshots and 343 reconstruction calls consume 0.383406s +in disjoint exclusive clocks; saved-object reads add 0.000067s. Ten effective +configuration reads take 0.192388s inclusively, already inside those costs. +That is neither an additional cost nor a removable-work estimate. The reads +span before and after the compiler captures its concrete configuration. + +Execution has four state constructions and eight snapshots, totaling 0.055237s +exclusive including reconstruction and saved reads. Other measured costs include +output recording 0.040833s, finalization 0.025324s, loading 0.021657s, +reconciliation 0.021559s, worker residual 0.021557s and publication 0.017187s. +These describe separate production consumers. No entire lane is claimed removable. +The source map identifies five reads before capture and five afterward, without +per-call timing. Existing requests and contexts are the strongest candidate +owners for later reads, but callback mutation and late errors prevent blind +substitution. No saved configuration optimization is qualified yet. Live/saved +baselines cannot be collapsed unconditionally or late edits ignored. + +The installed ObjectState and python-introspect editables are the owned external +trees at a01f8939b9 and ed1e4eb15f. The separately frozen canonical dependency +copies have different revisions. Assessment pins distinguish them. The earlier +731-module AST census is a different epoch; this report does not promote it as +a current global architecture proof. The selected 13 current modules have a +refreshed 71-class NRA census, retained with the determining source map. + +Science and rejected route +-------------------------- + +Both authored float32 NPY outputs, each 463 by 461 pixels, pass the original +comparison against both retained native repetitions. Polynomial correction's +maximum difference is 2.98e-8 under the existing 1e-6 tolerances; convex-hull +correction is exact. Full physical inventory, source identities, correlations, +input/native witnesses and before/after custody pass. Native was not rerun, so +there is no fresh native timing or new paired performance ratio. + +The first science-reader invocation failed its final environment guard because +dependency imports added four reader-local environment defaults. Its failure and +per-key hash audit are preserved. The corrected reader runs the unchanged full +controller guard in fresh interpreters with the exact original launch environment +before and after comparison. No keys are filtered or rewritten; both checkpoints +pass. The scientific comparison and tolerances are unchanged. + +Separately, six actual-source render replays find only 0.086086s median removable +completion/return rendering, versus the retained Illum3 total target gap of +1.226108s. Mandatory compile/execution render epochs remain independent, and +mutation/callback/error controls pass. Reject this as the dominant performance +route. Retaining the actual executed request remains a separate provenance repair. + +Custody and reproduction +------------------------ + +``assessment.json`` pins every byte-identical compact original receipt and the +full local freeze. Frozen controller/site/reader recipes remain at their pinned +maintenance paths; the full effective environment intentionally includes the +original controller agent identity. This is not a portable turnkey replay bundle. +No environment, scientific output, saved graph or source worktree is copied into +this report. The receipts total approximately 191KB. Existing PR #394 / issues +#384 and #496 carry this investigation; no new production speedup is claimed. diff --git a/benchmark/results/perf_illum3_runtime_ledger_20261003/science.json b/benchmark/results/perf_illum3_runtime_ledger_20261003/science.json new file mode 100644 index 0000000000..36d90aeb51 --- /dev/null +++ b/benchmark/results/perf_illum3_runtime_ledger_20261003/science.json @@ -0,0 +1,237 @@ +{ + "status": "PASS", + "comparisons": [ + { + "case": "ExampleIlluminationCorrection_Example3", + "ordinary_repeat": 0, + "native_repeat": 0, + "measurement_differences": 0, + "image_differences": 0, + "declared_export_comparisons": [ + { + "artifact": { + "artifact_name": "PolynomialCorrected", + "artifact_type": "ImageArtifactType", + "semantic_kind": "numeric_image_pixels", + "output_filename": "reference_image__PolynomialCorrected.npy", + "comparison": "float pixels: atol=1e-6, rtol=1e-6, zero mismatches" + }, + "reference_path": "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleIlluminationCorrection_Example3/outputs/0/reference_image__PolynomialCorrected.npy", + "candidate_path": "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/output/public/ExampleIlluminationCorrection_Example3/wells_1/workers_1/images_ExampleIlluminationCorrection_Example3_source_workspace_well_throughput/results/reference_image__PolynomialCorrected.npy", + "reference_shape": [ + 463, + 461 + ], + "candidate_shape": [ + 463, + 461 + ], + "reference_dtype": "float32", + "candidate_dtype": "float32", + "reference_pixel_digest": "b5073cb872058f88bb250be9e7a39bedd2ef281f23dfedde5262e4431f1fb0fe", + "candidate_pixel_digest": "8aedbeab3f9868c1d1d46b0aafb3b20bb9dfd107dc08bb0882577c493f08173b", + "compared_pixel_count": 213443, + "different_pixel_count": null, + "out_of_tolerance_pixel_count": 0, + "max_abs_difference": 2.9802322387695312e-08, + "equivalent": true + }, + { + "artifact": { + "artifact_name": "ConvexHullCorrWorm", + "artifact_type": "ImageArtifactType", + "semantic_kind": "numeric_image_pixels", + "output_filename": "reference_image__ConvexHullCorrWorm.npy", + "comparison": "float pixels: atol=1e-6, rtol=1e-6, zero mismatches" + }, + "reference_path": "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleIlluminationCorrection_Example3/outputs/0/reference_image__ConvexHullCorrWorm.npy", + "candidate_path": "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/output/public/ExampleIlluminationCorrection_Example3/wells_1/workers_1/images_ExampleIlluminationCorrection_Example3_source_workspace_well_throughput/results/reference_image__ConvexHullCorrWorm.npy", + "reference_shape": [ + 463, + 461 + ], + "candidate_shape": [ + 463, + 461 + ], + "reference_dtype": "float32", + "candidate_dtype": "float32", + "reference_pixel_digest": "fdc35df90fb97b2d2ab79e4acdaa00aef26d4d9c76fb53d6aa53351bd598dd9b", + "candidate_pixel_digest": "fdc35df90fb97b2d2ab79e4acdaa00aef26d4d9c76fb53d6aa53351bd598dd9b", + "compared_pixel_count": 213443, + "different_pixel_count": null, + "out_of_tolerance_pixel_count": 0, + "max_abs_difference": 0.0, + "equivalent": true + } + ], + "tables": [ + 0, + 0 + ], + "physical_images": [ + 2, + 2 + ], + "logical_images": [ + 2, + 2 + ], + "known_relationship_keys": [ + 0, + 0 + ], + "ordinary_receipt_sha256": "8157ccac4f4940243f4ad96970eaad64e43a38c9b339887a3ef7c8aa7f6a0b44", + "output_hashes": { + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleIlluminationCorrection_Example3/outputs/0/reference_image__ConvexHullCorrWorm.npy": "9ba1259e4ca007c06f14be6946e32a86307ec2dfb9952b73fa2cc253ccae8b3a", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleIlluminationCorrection_Example3/outputs/0/reference_image__PolynomialCorrected.npy": "588e6f9951aa7ad220678dbb0053e98fb6d324e71aab089d1d828d891ee32f54", + "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/output/public/ExampleIlluminationCorrection_Example3/wells_1/workers_1/images_ExampleIlluminationCorrection_Example3_source_workspace_well_throughput/results/reference_image__PolynomialCorrected.npy": "8875da63a072c8f2de4f5cb707d779a064b663bf69aa0f304fe6fa74d4ff57a6", + "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/output/public/ExampleIlluminationCorrection_Example3/wells_1/workers_1/images_ExampleIlluminationCorrection_Example3_source_workspace_well_throughput/results/reference_image__ConvexHullCorrWorm.npy": "9ba1259e4ca007c06f14be6946e32a86307ec2dfb9952b73fa2cc253ccae8b3a", + "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/output/public/ExampleIlluminationCorrection_Example3/wells_1/workers_1/images_ExampleIlluminationCorrection_Example3_source_workspace_well_throughput/openhcs_metadata.json.lock": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/output/public/ExampleIlluminationCorrection_Example3/wells_1/workers_1/images_ExampleIlluminationCorrection_Example3_source_workspace_well_throughput/openhcs_metadata.json": "579ac084b396dff5f7fbaa844139bf952c2576b6cb0242772c925ba9839c2267" + } + }, + { + "case": "ExampleIlluminationCorrection_Example3", + "ordinary_repeat": 0, + "native_repeat": 1, + "measurement_differences": 0, + "image_differences": 0, + "declared_export_comparisons": [ + { + "artifact": { + "artifact_name": "PolynomialCorrected", + "artifact_type": "ImageArtifactType", + "semantic_kind": "numeric_image_pixels", + "output_filename": "reference_image__PolynomialCorrected.npy", + "comparison": "float pixels: atol=1e-6, rtol=1e-6, zero mismatches" + }, + "reference_path": "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleIlluminationCorrection_Example3/outputs/1/reference_image__PolynomialCorrected.npy", + "candidate_path": "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/output/public/ExampleIlluminationCorrection_Example3/wells_1/workers_1/images_ExampleIlluminationCorrection_Example3_source_workspace_well_throughput/results/reference_image__PolynomialCorrected.npy", + "reference_shape": [ + 463, + 461 + ], + "candidate_shape": [ + 463, + 461 + ], + "reference_dtype": "float32", + "candidate_dtype": "float32", + "reference_pixel_digest": "b5073cb872058f88bb250be9e7a39bedd2ef281f23dfedde5262e4431f1fb0fe", + "candidate_pixel_digest": "8aedbeab3f9868c1d1d46b0aafb3b20bb9dfd107dc08bb0882577c493f08173b", + "compared_pixel_count": 213443, + "different_pixel_count": null, + "out_of_tolerance_pixel_count": 0, + "max_abs_difference": 2.9802322387695312e-08, + "equivalent": true + }, + { + "artifact": { + "artifact_name": "ConvexHullCorrWorm", + "artifact_type": "ImageArtifactType", + "semantic_kind": "numeric_image_pixels", + "output_filename": "reference_image__ConvexHullCorrWorm.npy", + "comparison": "float pixels: atol=1e-6, rtol=1e-6, zero mismatches" + }, + "reference_path": "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleIlluminationCorrection_Example3/outputs/1/reference_image__ConvexHullCorrWorm.npy", + "candidate_path": "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/output/public/ExampleIlluminationCorrection_Example3/wells_1/workers_1/images_ExampleIlluminationCorrection_Example3_source_workspace_well_throughput/results/reference_image__ConvexHullCorrWorm.npy", + "reference_shape": [ + 463, + 461 + ], + "candidate_shape": [ + 463, + 461 + ], + "reference_dtype": "float32", + "candidate_dtype": "float32", + "reference_pixel_digest": "fdc35df90fb97b2d2ab79e4acdaa00aef26d4d9c76fb53d6aa53351bd598dd9b", + "candidate_pixel_digest": "fdc35df90fb97b2d2ab79e4acdaa00aef26d4d9c76fb53d6aa53351bd598dd9b", + "compared_pixel_count": 213443, + "different_pixel_count": null, + "out_of_tolerance_pixel_count": 0, + "max_abs_difference": 0.0, + "equivalent": true + } + ], + "tables": [ + 0, + 0 + ], + "physical_images": [ + 2, + 2 + ], + "logical_images": [ + 2, + 2 + ], + "known_relationship_keys": [ + 0, + 0 + ], + "ordinary_receipt_sha256": "8157ccac4f4940243f4ad96970eaad64e43a38c9b339887a3ef7c8aa7f6a0b44", + "output_hashes": { + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleIlluminationCorrection_Example3/outputs/1/reference_image__ConvexHullCorrWorm.npy": "9ba1259e4ca007c06f14be6946e32a86307ec2dfb9952b73fa2cc253ccae8b3a", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleIlluminationCorrection_Example3/outputs/1/reference_image__PolynomialCorrected.npy": "588e6f9951aa7ad220678dbb0053e98fb6d324e71aab089d1d828d891ee32f54", + "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/output/public/ExampleIlluminationCorrection_Example3/wells_1/workers_1/images_ExampleIlluminationCorrection_Example3_source_workspace_well_throughput/results/reference_image__PolynomialCorrected.npy": "8875da63a072c8f2de4f5cb707d779a064b663bf69aa0f304fe6fa74d4ff57a6", + "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/output/public/ExampleIlluminationCorrection_Example3/wells_1/workers_1/images_ExampleIlluminationCorrection_Example3_source_workspace_well_throughput/results/reference_image__ConvexHullCorrWorm.npy": "9ba1259e4ca007c06f14be6946e32a86307ec2dfb9952b73fa2cc253ccae8b3a", + "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/output/public/ExampleIlluminationCorrection_Example3/wells_1/workers_1/images_ExampleIlluminationCorrection_Example3_source_workspace_well_throughput/openhcs_metadata.json.lock": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/output/public/ExampleIlluminationCorrection_Example3/wells_1/workers_1/images_ExampleIlluminationCorrection_Example3_source_workspace_well_throughput/openhcs_metadata.json": "579ac084b396dff5f7fbaa844139bf952c2576b6cb0242772c925ba9839c2267" + } + } + ], + "source_head": "4c0d62e45c8afb85e34b69d7279cd00710f519b5", + "scope": "One current41-hook diagnostic versus both retained qualified native repetitions. Original image/table/correlation/inventory authorities and existing declared ReferenceExportPlan; 2NPY positive pixel nonvacuity. Native not rerun; no causal timing or current-native ratio claim. Only input receipt namespace/outer cardinality/current freeze custody adapted. Reader import environment effects retained; original full controller guard runs before/after in fresh interpreters with exact original raw launch environment. First in-reader final environment RED preserved separately; no full-driver guard bypass or environment rewrites.", + "reader_environment_effects": { + "KMP_DUPLICATE_LIB_OK": { + "before": null, + "after": "3cbc87c7681f34db4617feaa2c8801931bc5e42d8d0f560e756dd4cd92885f18" + }, + "SPARSE_AUTO_DENSIFY": { + "before": null, + "after": "6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b" + }, + "TF_FORCE_GPU_ALLOW_GROWTH": { + "before": null, + "after": "b5bea41b6c623f7c09f1bf24dcae58ebab3c0cdd90ad966bc43a45b44867e12b" + }, + "XLA_PYTHON_CLIENT_PREALLOCATE": { + "before": null, + "after": "fcbcf165908dd18a9e49f7ff27810176db8e9f63b4352213741664245224f8aa" + } + }, + "reader_isolation_checkpoints": [ + { + "index": 1, + "log": "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/science-v3-controller-checkpoint-1.log", + "sha256": "04a54993c97b3c3f46659e451ec5e2f8cd944fae12ab9f35abf49ffef1707c00", + "source_freeze_sha256": "86646806122c73d38de2e9d42efa0bffd471ad0a1db36d7d33ea83dfcbfeb0fc", + "scope": "Original unchanged full controller source/env/native/input/sparse guard in fresh interpreter using exact original raw launch environment held only in memory; no values filtered or rewritten." + }, + { + "index": 2, + "log": "/home/ts/.local/state/openhcs-maintenance/20261003/illum3-compiler-runtime-ledger-v2/science-v3-controller-checkpoint-2.log", + "sha256": "04a54993c97b3c3f46659e451ec5e2f8cd944fae12ab9f35abf49ffef1707c00", + "source_freeze_sha256": "86646806122c73d38de2e9d42efa0bffd471ad0a1db36d7d33ea83dfcbfeb0fc", + "scope": "Original unchanged full controller source/env/native/input/sparse guard in fresh interpreter using exact original raw launch environment held only in memory; no values filtered or rewritten." + } + ], + "validator_sha256": "e71981fd54cc6a3f6e3d03c797badff250cc5c7e3fdddf1401fb3378e17b8aa4", + "diagnostic_admission": true, + "native_witness": { + "files": { + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleIlluminationCorrection_Example3/outputs/0/reference_image__ConvexHullCorrWorm.npy": "9ba1259e4ca007c06f14be6946e32a86307ec2dfb9952b73fa2cc253ccae8b3a", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleIlluminationCorrection_Example3/outputs/0/reference_image__PolynomialCorrected.npy": "588e6f9951aa7ad220678dbb0053e98fb6d324e71aab089d1d828d891ee32f54", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleIlluminationCorrection_Example3/outputs/1/reference_image__ConvexHullCorrWorm.npy": "9ba1259e4ca007c06f14be6946e32a86307ec2dfb9952b73fa2cc253ccae8b3a", + "/home/ts/.local/state/openhcs-maintenance/20261003/current-wound-illum3-paired-v1/native/ExampleIlluminationCorrection_Example3/outputs/1/reference_image__PolynomialCorrected.npy": "588e6f9951aa7ad220678dbb0053e98fb6d324e71aab089d1d828d891ee32f54" + }, + "strict_science_sha256": "3d89dc4daaceb34535ceaab05100b80b88487e32e67180a99595035b66c67236", + "source_freeze_sha256": "7719484a567283a9427cd9a3742b8ca4f25b26c0678f2d8858073d5666adb284", + "native_report_sha256": "437f3943b29f3335831f934c0c308f51134df18096016431276f7c976fb8b458", + "native_request_sha256": "a2ab2cf68ad3afaf8af31652ced4c8d23ea398dc91cc915132b5562b1dbd386e" + }, + "original_comparator_sha256": "0b7abed65ecc3d61761533a3a9a69e9b9a8acb220049175ff46fb5f004ade4aa", + "diagnostic_source_freeze_sha256": "86646806122c73d38de2e9d42efa0bffd471ad0a1db36d7d33ea83dfcbfeb0fc" +} From 5af63decedc018a905c33057b47758bf84dc6520 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 06:58:38 -0400 Subject: [PATCH 124/235] docs(perf): retain transport frontier evidence and merged #515 delivery Refs #176, #384, #515. Record the rejected annotation/cache routes and the existing whole-stack transport investigation without claiming a new speedup. --- .../runtime_plumbing_frontier_20261003.rst | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/docs/plans/runtime_plumbing_frontier_20261003.rst b/docs/plans/runtime_plumbing_frontier_20261003.rst index 3b6fcd6b47..f859ea4ed1 100644 --- a/docs/plans/runtime_plumbing_frontier_20261003.rst +++ b/docs/plans/runtime_plumbing_frontier_20261003.rst @@ -476,3 +476,59 @@ qualification is in ``benchmark/results/perf_dependency_source_readiness_2026100 The unchanged original R0/R1 failures, full30/scaling work and other workloads' unresolved native image differences remain open. No whole-goal completion is claimed. + +Current delivery and dominant transport investigation +--------------------------------------------------- + +The aggregate-to-layer producer fix is merged into main as PR #523, closing +#515 through its formal closing link. The final PR head ``1419c5bd`` has the +same production source, tests and build configuration as the tested +``cab5363c`` checkpoint: 447 selected controls pass, with no deselections. +The subsequent main merge changes documentation only. A first temporary +checkout run failed collection because it lacked native extensions; the +passing run reused the existing binaries after checking both C++ sources +were byte-identical. This is local integration evidence, not installed GUI +acceptance or a performance gain. The temporary checkout has been retired. +The performance branch is synced with main ``6afc9a325`` at ``c49754e4c``; +its production and test bytes are unchanged from ``21934dad3``. + +The native-annotation experiment is stopped. Four alternating pairs of the +original five-write/publication/reconciliation sequence save 23--97ms, with +58.4ms median paired savings. All 16 controls pass, including the complete +609,001-byte ordered wire sequence, 120 produced entries and 180 source +entries. These savings do not materially close the current 1.916s 3D execution +gap. Definition-time alias binding still differs from deferred annotations; +this test-only experiment is not a universal annotation migration proof. +No production annotations, schema cache or benchmark driver were changed. +The determining decision is retained at +``~/.local/state/openhcs-maintenance/20261003/native-annotations-three-owner-replay-v1/decision.json`` +(SHA-256 ``c1159f2fda79a88e751228ad8215e1e64e79ecc5371313b79263f12b17af0396``). + +Unconditional manifest filename-cache routing is also rejected. An actual +uncached SourceSchema parser accepts ``z_index=[]`` and emits ``z[]``; the +existing explicit cache instead raises ``TypeError`` before construction. +The current filename-value mappings and parser have no persistent nominal +eligibility law. No graph replay or source patch was made. The determining +receipt is +``~/.local/state/openhcs-maintenance/20261003/manifest-filename-cache-eligibility-stop-v1.json`` +(SHA-256 ``188b02df644b1c7e6a3db0f8b222a87783e1d2b26e99137c2b4bb0285727baba``). +This does not reject a store-owned path index governed by the existing +records-revision boundary; that distinct structural route is being audited. + +The principal destination under investigation is the existing whole-stack +transport and compiled step-output dependency. Saving currently projects +per-path payloads and records, then recomposes their metadata for the stack +cache. Loading derives path, workspace and source-binding information before +checking that cache. The existing cache, manifest and compiled binding owners +must account for this handoff without another ledger. Actual VFS access, +selectors, source-identity birth epochs, independently mutable outputs and +publication remain production consumers to preserve. + +The retained coarse 3D ledger assigns about 1.646s to load, save and unstack +combined. That is an investigation ceiling including mandatory work, not +removable time. Retained profile call counts identify 10,680 normalizations +in axis-removal and source-projection operations; profiled time is not scaled +into an ordinary saving. A saved warm loader occurrence takes only 9.8ms and +cannot establish whole-pipeline payoff. The next evidence must determine +actual step-level cache hits, cohort fanout and metadata births, followed by +a consumer-complete replay and ordinary end-to-end verification. From 9d2dfca12363bab9b8b9fc6809a770ad384a76f7 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 08:15:17 -0400 Subject: [PATCH 125/235] fix(runtime): derive CP input preservation from output policy (#530) --- openhcs/core/artifact_key_selection.py | 10 +- openhcs/core/callable_contract.py | 2 +- openhcs/core/steps/function_runtime.py | 2 +- .../interop/cellprofiler/runtime/adapter.py | 10 +- tests/unit/test_artifact_output_ownership.py | 29 ++++++ tests/unit/test_debug_runtime.py | 44 +++++++++ tests/unit/test_yeast_output_identity.py | 99 +++++++++++++++++-- 7 files changed, 183 insertions(+), 13 deletions(-) diff --git a/openhcs/core/artifact_key_selection.py b/openhcs/core/artifact_key_selection.py index bf0fe59522..1d39ceec38 100644 --- a/openhcs/core/artifact_key_selection.py +++ b/openhcs/core/artifact_key_selection.py @@ -4,7 +4,7 @@ from abc import ABC, abstractmethod from collections.abc import Mapping -from typing import ClassVar, TypeVar +from typing import TYPE_CHECKING, ClassVar, TypeVar from openhcs.core.artifacts import ( ArtifactOutputPlan, @@ -17,12 +17,20 @@ ArtifactPlanT = TypeVar("ArtifactPlanT", bound=ArtifactPlan) +if TYPE_CHECKING: + from openhcs.core.callable_contract import CallableContract + class ArtifactOutputPolicy(ABC): """Declaration-owned recording and payload obligations for output artifacts.""" records_outputs: ClassVar[bool] + @classmethod + def preserves_input_main_flow(cls, contract: CallableContract) -> bool: + """Derive native return behavior from the declared output roster.""" + return bool(contract.artifact_outputs) and not contract.main_flow_outputs + @classmethod def validate_output_declarations(cls, specs: ArtifactSpecCollection) -> None: """Run common kind invariants before the ownership-specific obligation.""" diff --git a/openhcs/core/callable_contract.py b/openhcs/core/callable_contract.py index 18e75a7f7f..45a5965412 100644 --- a/openhcs/core/callable_contract.py +++ b/openhcs/core/callable_contract.py @@ -945,7 +945,7 @@ def group_scope_inputs(self) -> ArtifactSpecCollection: def preserves_input_main_flow(self) -> bool: """Return whether declared artifact outputs leave main flow unchanged.""" - return bool(self.artifact_outputs) and not self.main_flow_outputs + return self.artifact_output_policy.preserves_input_main_flow(self) @property def runtime_adapter(self) -> RuntimeAdapterSpec | None: diff --git a/openhcs/core/steps/function_runtime.py b/openhcs/core/steps/function_runtime.py index e0fbc70fea..e2e807dfc4 100644 --- a/openhcs/core/steps/function_runtime.py +++ b/openhcs/core/steps/function_runtime.py @@ -1550,7 +1550,7 @@ def execute_chain( return current_stack current_memory_type = executor.memory_types().output_type if self.compiled_group.preserves_input_main_flow() and all( - invocation.adapter_records_artifact_outputs + invocation.contract.artifact_output_policy.records_outputs for invocation in self.compiled_group.invocations ): return NoMainFlowOutput() diff --git a/openhcs/interop/cellprofiler/runtime/adapter.py b/openhcs/interop/cellprofiler/runtime/adapter.py index 5549514b43..a50c05656a 100644 --- a/openhcs/interop/cellprofiler/runtime/adapter.py +++ b/openhcs/interop/cellprofiler/runtime/adapter.py @@ -5,7 +5,7 @@ import time from collections.abc import Sequence from dataclasses import dataclass, replace -from typing import cast +from typing import TYPE_CHECKING, cast import numpy as np @@ -68,12 +68,20 @@ CellProfilerRuntimeProfileLogger, ) +if TYPE_CHECKING: + from openhcs.core.callable_contract import CallableContract + RelationshipIdVector = np.ndarray | Sequence[int] class CellProfilerRecordedArtifactOutputPolicy(AdapterRecordedArtifactOutputPolicy): """CP recorders supply per-row subjects for heterogeneous measurement tables.""" + @classmethod + def preserves_input_main_flow(cls, contract: CallableContract) -> bool: + """CP publishes the input when no canonical return output is selected.""" + return not contract.canonical_return_output_specs + @classmethod def validate_measurement_subject(cls, spec: ArtifactSpec) -> None: # Common relation invariants are checked by the artifact kind. CP's diff --git a/tests/unit/test_artifact_output_ownership.py b/tests/unit/test_artifact_output_ownership.py index 7d43b8eab8..34dd5785d4 100644 --- a/tests/unit/test_artifact_output_ownership.py +++ b/tests/unit/test_artifact_output_ownership.py @@ -43,6 +43,35 @@ def test_adapter_declaration_requires_a_concrete_output_policy(invalid_policy): ) +@pytest.mark.parametrize( + "output_policy", + [NativeReturnArtifactOutputPolicy, AdapterRecordedArtifactOutputPolicy], +) +@pytest.mark.parametrize("has_trailing_labels", [False, True]) +def test_arbitrary_return_owner_does_not_inherit_cellprofiler_passthrough( + output_policy, has_trailing_labels, +): + outputs = ( + ( + ArtifactSpec.output("Measurements", MeasurementsArtifactType), + ArtifactSpec.output("Labels", ObjectLabelsArtifactType), + ) + if has_trailing_labels + else () + ) + + @runtime_adapter( + "runtime", lambda request: object(), artifact_output_policy=output_policy + ) + @artifact_outputs(*outputs) + def replace_image(image, *, runtime): + return image + 1 + + contract = CallableContract.from_callable(replace_image) + assert not contract.canonical_return_output_specs + assert not contract.preserves_input_main_flow() + + @pytest.fixture def materialized_output_kind(monkeypatch): # Isolate the declaration, not the compiler or its registration mechanism. diff --git a/tests/unit/test_debug_runtime.py b/tests/unit/test_debug_runtime.py index 4c6ece9111..be71a846b7 100644 --- a/tests/unit/test_debug_runtime.py +++ b/tests/unit/test_debug_runtime.py @@ -79,8 +79,16 @@ ImageArtifactType, ObjectLabelsArtifactType, MeasurementsArtifactType, + NoMainFlowOutput, RelationshipsArtifactType, ) +from openhcs.core.artifact_key_selection import ( + AdapterRecordedArtifactOutputPolicy, + NativeReturnArtifactOutputPolicy, +) +from openhcs.interop.cellprofiler.runtime.adapter import ( + CellProfilerRecordedArtifactOutputPolicy, +) from openhcs.core.runtime_adapters import RuntimeAdapterSpec from openhcs.core.measurement_row_materialization import ( MeasurementSparseColumnarRows, @@ -658,6 +666,42 @@ def test_debug_execution_config_normalizes_compile_cache_payload(): ) +@pytest.mark.parametrize( + "output_policy", + [ + CellProfilerRecordedArtifactOutputPolicy, + AdapterRecordedArtifactOutputPolicy, + NativeReturnArtifactOutputPolicy, + ], +) +def test_outputless_chain_uses_declared_owner_instead_of_selected_output_count( + output_policy, +): + def publish(image, *, runtime): + return image + + invocation = DebugRuntimeFixture.compiled_invocation( + publish, + runtime_adapter=RuntimeAdapterSpec( + "runtime", + lambda request: object(), + artifact_output_policy=output_policy, + ), + ) + assert not invocation.adapter_records_artifact_outputs + image = np.arange(6, dtype=np.uint16).reshape(1, 2, 3) + result = DebugRuntimeFixture.execute_function_chain( + initial_data_stack=image, + invocations=(invocation,), + context=DebugExecutionContextStub(debug_event_sink=NoOpDebugEventSink()), + ) + if output_policy is CellProfilerRecordedArtifactOutputPolicy: + assert isinstance(result, NoMainFlowOutput) + else: + assert not isinstance(result, NoMainFlowOutput) + np.testing.assert_array_equal(image_payload_data(result), image) + + def test_execute_chain_emits_debug_invocation_events(): sink = RecordingDebugEventSink() context = DebugExecutionContextStub(debug_event_sink=sink) diff --git a/tests/unit/test_yeast_output_identity.py b/tests/unit/test_yeast_output_identity.py index a467dfae1c..fcecd52962 100644 --- a/tests/unit/test_yeast_output_identity.py +++ b/tests/unit/test_yeast_output_identity.py @@ -7,6 +7,7 @@ from unittest.mock import Mock import numpy as np +import pytest from openhcs.core.aligned_image_payload import ImageOutputBundle from openhcs.core.artifacts import ( @@ -18,6 +19,11 @@ SourceStackLineageSourceRelation, ) from openhcs.core.callable_contract import CallableContract +from openhcs.core.function_patterns import ( + CompiledFunctionGroup, + CompiledFunctionInvocation, + FunctionInvocationKey, +) from openhcs.interop.cellprofiler.runtime.adapter import CellProfilerRuntimeAdapter from openhcs.interop.cellprofiler.runtime.module_execution import ( CellProfilerModuleExecutor, @@ -95,19 +101,51 @@ def test_independent_exact_invocations_accumulate_named_image_outputs() -> None: ) -def test_true_preserves_input_adapter_contract_keeps_main_flow_unchanged() -> None: - measurements = ArtifactSpec.output("Measurements", MeasurementsArtifactType) - executor = _executor(measurements) - plan = ArtifactOutputPlan( - name=measurements.name, - path="/memory/Measurements.pkl", - artifact_type=measurements.artifact_type, +@pytest.mark.parametrize( + "output_kinds", + [ + (), + (MeasurementsArtifactType,), + (MeasurementsArtifactType, ObjectLabelsArtifactType), + ], + ids=["no-outputs", "measurements", "measurements-before-labels"], +) +def test_true_preserves_input_adapter_contract_keeps_main_flow_unchanged( + output_kinds, +) -> None: + outputs = tuple( + ArtifactSpec.output(f"Output{index}", kind) + for index, kind in enumerate(output_kinds) + ) + plans = tuple( + ArtifactOutputPlan( + name=spec.name, + path=f"/memory/{spec.name}.pkl", + artifact_type=spec.artifact_type, + ) + for spec in outputs ) + executor = _executor(*outputs) + contract = executor.callable_contract + invocation = CompiledFunctionInvocation( + key=FunctionInvocationKey.from_contract(contract, "default", 0), + contract=contract, + artifact_output_plans=plans, + ) + group = CompiledFunctionGroup("default", (invocation,)) current = np.zeros((2, 2), dtype=np.float32) - assert executor.callable_contract.preserves_input_main_flow() + assert contract.preserves_input_main_flow() + assert group.preserves_input_main_flow() + assert group.resulting_implicit_main_flow_invocation() is None + assert group.resulting_main_flow_output_plans() == () + assert group.unwrapped_main_flow_output_context( + {plan.ref(): plan for plan in plans} + ) is None result = executor._published_active_main_flow_output( - matched_outputs=((plan, measurements, object()),), + matched_outputs=tuple( + (plan, spec, object()) for plan, spec in zip(plans, outputs, strict=True) + ), declared_only_outputs={}, adapter=cast(CellProfilerRuntimeAdapter, object()), current_image=current, @@ -155,3 +193,46 @@ def test_mixed_outputs_publish_only_the_active_canonical_return_slot() -> None: assert tuple(context.output_key for context in result.slice_contexts) == ("Gray",) np.testing.assert_array_equal(result.slices[0], image_value) adapter.artifact_output_value.assert_called_once_with(image_plan) + + +@pytest.mark.parametrize("named_producer", [False, True]) +def test_preserving_cellprofiler_tail_retains_the_existing_producer(named_producer): + image, image_plan = _image_output("Original", "Gray") + producer_contract = _executor(image).callable_contract + if not named_producer: + from openhcs.core.artifact_key_selection import NativeReturnArtifactOutputPolicy + from openhcs.core.runtime_adapters import RuntimeAdapterSpec + + producer_contract = replace( + producer_contract, + metadata=replace( + producer_contract.metadata, + artifact_outputs=(), + runtime_adapter=RuntimeAdapterSpec( + "runtime", + lambda request: object(), + artifact_output_policy=NativeReturnArtifactOutputPolicy, + ), + ), + ) + producer = CompiledFunctionInvocation( + key=FunctionInvocationKey.from_contract(producer_contract, "default", 0), + contract=producer_contract, + artifact_output_plans=(image_plan,) if named_producer else (), + ) + tail_contract = _executor().callable_contract + tail = CompiledFunctionInvocation( + key=FunctionInvocationKey.from_contract(tail_contract, "default", 1), + contract=tail_contract, + ) + group = CompiledFunctionGroup("default", (producer, tail)) + + assert not group.preserves_input_main_flow() + if named_producer: + assert group.resulting_main_flow_output_plans() == (image_plan,) + assert group.resulting_implicit_main_flow_invocation() is None + context = group.unwrapped_main_flow_output_context({image_plan.ref(): image_plan}) + assert context.output_key == "Gray" + else: + assert group.resulting_main_flow_output_plans() == () + assert group.resulting_implicit_main_flow_invocation() is producer From eb2c2e6ee5418f61fbdcda12169949e6503b4d9d Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 08:24:20 -0400 Subject: [PATCH 126/235] docs(perf): retain fresh parity-qualified timings after #530 fix --- .../README.md | 10 + .../matched_runtime.png | Bin 0 -> 103224 bytes .../matched_runtime.svg | 1615 ++++++++++++++++ .../matched_speedup.png | Bin 0 -> 134455 bytes .../matched_speedup.svg | 1683 +++++++++++++++++ .../metrics.csv | 7 + .../ordinary-0.csv | 3 + .../ordinary-1.csv | 3 + 8 files changed, 3321 insertions(+) create mode 100644 benchmark/results/perf_canonical_output_policy_paired_20261003/README.md create mode 100644 benchmark/results/perf_canonical_output_policy_paired_20261003/matched_runtime.png create mode 100644 benchmark/results/perf_canonical_output_policy_paired_20261003/matched_runtime.svg create mode 100644 benchmark/results/perf_canonical_output_policy_paired_20261003/matched_speedup.png create mode 100644 benchmark/results/perf_canonical_output_policy_paired_20261003/matched_speedup.svg create mode 100644 benchmark/results/perf_canonical_output_policy_paired_20261003/metrics.csv create mode 100644 benchmark/results/perf_canonical_output_policy_paired_20261003/ordinary-0.csv create mode 100644 benchmark/results/perf_canonical_output_policy_paired_20261003/ordinary-1.csv diff --git a/benchmark/results/perf_canonical_output_policy_paired_20261003/README.md b/benchmark/results/perf_canonical_output_policy_paired_20261003/README.md new file mode 100644 index 0000000000..8af72cfe58 --- /dev/null +++ b/benchmark/results/perf_canonical_output_policy_paired_20261003/README.md @@ -0,0 +1,10 @@ +The output-policy correctness fix is merged in [PR #531](https://github.com/OpenHCSDev/openhcs/pull/531), closing #530. These fresh observations use performance source `ee8f97552b349499655bb3f961860b34dc784071`, synced with main `ba7b26b82`. Existing environments and the public ordinary/native drivers were reused; both repetitions are retained. Full strict saved-output comparisons pass for all eight ordinary/native pairings, including exact 3D images. + +| Pipeline | Compile | OH execution | OH total | Native CP invocation | Execution speedup | Total speedup | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | +| 3D Monolayer | 1.176 s | 8.994 s | 10.974 s | 14.317 s | 1.592× | 1.305× | +| Speckles | 0.498 s | 1.261 s | 2.091 s | 2.081 s | 1.650× | 0.995× | + +Means use both measured observations. Ordinary 3D execution spans 8.562–9.425 s; the earlier qualified mean was 9.127 s. These separate two-repeat observations do not establish a causal speedup. The remaining 2× execution gap is 1.835 s, so the larger generic source/context/publication work remains necessary. Seven corrected CP groups preserve their input instead of producing anonymous image replacements; this is an architectural dependency, not completion of the performance target. + +Server startup, mandatory library/kernel readiness, shutdown and native imports/JVM/loading/full warmup precede or follow pipeline clocks. Ordinary total includes compilation and default OUTCOMES/RSS completion. This is a two-case preview; full30 and scaling figures remain unfinished. Figures use the existing `benchmark.reports.cppipe_figures` owner with a 2× reference. `timing_summary.json`, raw CSVs, native reports, strict science and `qualification.json` retain determining evidence. diff --git a/benchmark/results/perf_canonical_output_policy_paired_20261003/matched_runtime.png b/benchmark/results/perf_canonical_output_policy_paired_20261003/matched_runtime.png new file mode 100644 index 0000000000000000000000000000000000000000..6f2c78a2169c6e3efece64b1e4284c74851cc058 GIT binary patch literal 103224 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%0ARJ;%@e&tMSZZnZ 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 literal 0 HcmV?d00001 diff --git a/benchmark/results/perf_canonical_output_policy_paired_20261003/matched_runtime.svg b/benchmark/results/perf_canonical_output_policy_paired_20261003/matched_runtime.svg new file mode 100644 index 0000000000..d5f4127026 --- /dev/null +++ b/benchmark/results/perf_canonical_output_policy_paired_20261003/matched_runtime.svg @@ -0,0 +1,1615 @@ + + + + + + + + 2026-10-03T08:23:14.895814 + image/svg+xml + + + Matplotlib v3.11.2, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/benchmark/results/perf_canonical_output_policy_paired_20261003/matched_speedup.png b/benchmark/results/perf_canonical_output_policy_paired_20261003/matched_speedup.png new file mode 100644 index 0000000000000000000000000000000000000000..c3a5e288984601bb87fb88f81e448405ba35f628 GIT binary patch literal 134455 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 z

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 literal 0 HcmV?d00001 diff --git a/benchmark/results/perf_canonical_output_policy_paired_20261003/matched_speedup.svg b/benchmark/results/perf_canonical_output_policy_paired_20261003/matched_speedup.svg new file mode 100644 index 0000000000..ac83fa03f4 --- /dev/null +++ b/benchmark/results/perf_canonical_output_policy_paired_20261003/matched_speedup.svg @@ -0,0 +1,1683 @@ + + + + + + + + 2026-10-03T08:23:15.216782 + image/svg+xml + + + Matplotlib v3.11.2, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/benchmark/results/perf_canonical_output_policy_paired_20261003/metrics.csv b/benchmark/results/perf_canonical_output_policy_paired_20261003/metrics.csv new file mode 100644 index 0000000000..abe4cc44c0 --- /dev/null +++ b/benchmark/results/perf_canonical_output_policy_paired_20261003/metrics.csv @@ -0,0 +1,7 @@ +pipeline_name,method,assay_category,module_category,accuracy_fraction,raw_seconds,speedup,peak_memory_mb +ExampleSpeckles,CellProfiler,,,1.0,2.0807363979984075,1.0, +ExampleSpeckles,OH execution,,,1.0,1.2607016563415527,1.6504590023594676, +ExampleSpeckles,OH total,,,1.0,2.0908219020348042,0.9951762969258254, +cp_tutorial_3d_monolayer,CellProfiler,,,1.0,14.316878200042993,1.0, +cp_tutorial_3d_monolayer,OH execution,,,1.0,8.993741154670715,1.59187127512646, +cp_tutorial_3d_monolayer,OH total,,,1.0,10.973736244486645,1.304649381129056, diff --git a/benchmark/results/perf_canonical_output_policy_paired_20261003/ordinary-0.csv b/benchmark/results/perf_canonical_output_policy_paired_20261003/ordinary-0.csv new file mode 100644 index 0000000000..fffd9799b5 --- /dev/null +++ b/benchmark/results/perf_canonical_output_policy_paired_20261003/ordinary-0.csv @@ -0,0 +1,3 @@ +case_name,mode_name,worker_count,well_count,compile_seconds,prepare_seconds,execute_seconds,total_seconds,wells_per_second,successful_wells,native_single_sample_execution_seconds,projected_native_execution_seconds,projected_execution_speedup,peak_memory_mb,status,memory_limit_mb,error_message,execution_route,server_lifecycle,run_input_sha256 +ExampleSpeckles,1w_1t,1,1,0.49239587783813477,0.0,1.2073335647583008,2.052078391890973,0.8282715143434222,1,,,,1127.01953125,success,,,ordinary-zmq-outcomes-v1,reused-per-sweep,792cfd9b4ac0e6ac65e68c15b2b94f814e085c26535b1db92b2201e640aa2b3c +cp_tutorial_3d_monolayer,1w_1t,1,1,1.1534233093261719,0.0,8.562227010726929,10.547500064130872,0.11679204472705292,1,,,,1699.5234375,success,,,ordinary-zmq-outcomes-v1,reused-per-sweep,792cfd9b4ac0e6ac65e68c15b2b94f814e085c26535b1db92b2201e640aa2b3c diff --git a/benchmark/results/perf_canonical_output_policy_paired_20261003/ordinary-1.csv b/benchmark/results/perf_canonical_output_policy_paired_20261003/ordinary-1.csv new file mode 100644 index 0000000000..36e4c442c7 --- /dev/null +++ b/benchmark/results/perf_canonical_output_policy_paired_20261003/ordinary-1.csv @@ -0,0 +1,3 @@ +case_name,mode_name,worker_count,well_count,compile_seconds,prepare_seconds,execute_seconds,total_seconds,wells_per_second,successful_wells,native_single_sample_execution_seconds,projected_native_execution_seconds,projected_execution_speedup,peak_memory_mb,status,memory_limit_mb,error_message,execution_route,server_lifecycle,run_input_sha256 +ExampleSpeckles,1w_1t,1,1,0.5033345222473145,0.0,1.3140697479248047,2.1295654121786356,0.7609946135501654,1,,,,1128.2734375,success,,,ordinary-zmq-outcomes-v1,reused-per-sweep,792cfd9b4ac0e6ac65e68c15b2b94f814e085c26535b1db92b2201e640aa2b3c +cp_tutorial_3d_monolayer,1w_1t,1,1,1.1986286640167236,0.0,9.425255298614502,11.399972424842417,0.10609792184058914,1,,,,1696.55859375,success,,,ordinary-zmq-outcomes-v1,reused-per-sweep,792cfd9b4ac0e6ac65e68c15b2b94f814e085c26535b1db92b2201e640aa2b3c From 5570b7a458ab035cd5c3a394a5178fb57ec2054d Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 08:24:30 -0400 Subject: [PATCH 127/235] docs(perf): include determining native and parity receipts for #530 --- .../native-3d.json | 61 ++ .../native-speckles.json | 61 ++ .../qualification.json | 16 + .../strict-science.json | 795 ++++++++++++++++++ .../timing_summary.json | 73 ++ 5 files changed, 1006 insertions(+) create mode 100644 benchmark/results/perf_canonical_output_policy_paired_20261003/native-3d.json create mode 100644 benchmark/results/perf_canonical_output_policy_paired_20261003/native-speckles.json create mode 100644 benchmark/results/perf_canonical_output_policy_paired_20261003/qualification.json create mode 100644 benchmark/results/perf_canonical_output_policy_paired_20261003/strict-science.json create mode 100644 benchmark/results/perf_canonical_output_policy_paired_20261003/timing_summary.json diff --git a/benchmark/results/perf_canonical_output_policy_paired_20261003/native-3d.json b/benchmark/results/perf_canonical_output_policy_paired_20261003/native-3d.json new file mode 100644 index 0000000000..93f685e587 --- /dev/null +++ b/benchmark/results/perf_canonical_output_policy_paired_20261003/native-3d.json @@ -0,0 +1,61 @@ +{ + "startup_seconds": 0.9386979886330664, + "environment": { + "python_executable": "/home/ts/code/projects/openhcs/.venv-cellprofiler39/bin/python", + "python_version": "3.9.25", + "cellprofiler_version": "4.2.8.1", + "cellprofiler_core_version": "4.2.8.1", + "numpy_version": "1.24.4", + "scipy_version": "1.9.0", + "temporary_root": "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/cp_tutorial_3d_monolayer/native_tmp" + }, + "request": { + "pipeline_path": "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/cp_tutorial_3d_monolayer/preparation/native_cellprofiler_headless/3d_monolayer_final.cppipe", + "input_dir": "/home/ts/.cache/openhcs/benchmark_datasets/CellProfiler_tutorials/data/3d_monolayer/images", + "output_root": "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/cp_tutorial_3d_monolayer/outputs", + "expected_image_sets": 1, + "repetitions": 2, + "file_list_path": "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/cp_tutorial_3d_monolayer/preparation/native_cellprofiler_file_list.txt", + "first_image_set": 1, + "last_image_set": null, + "report_path": "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/cp_tutorial_3d_monolayer/native_report.json", + "start_barrier_root": null, + "start_barrier_job_count": 1, + "start_barrier_job_index": 0 + }, + "observations": [ + { + "repetition": -1, + "output_root": "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/cp_tutorial_3d_monolayer/outputs/-1", + "image_set_count": 1, + "invocation_seconds": 17.944316200912, + "pre_first_module_seconds": 0.4406569520942867, + "first_module_through_post_run_seconds": 17.503659248817712, + "invocation_started_monotonic_seconds": 3824476.422337898, + "first_module_started_monotonic_seconds": 3824476.86299485, + "completed_monotonic_seconds": 3824494.366654099 + }, + { + "repetition": 0, + "output_root": "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/cp_tutorial_3d_monolayer/outputs/0", + "image_set_count": 1, + "invocation_seconds": 14.412360650952905, + "pre_first_module_seconds": 0.13050647964701056, + "first_module_through_post_run_seconds": 14.281854171305895, + "invocation_started_monotonic_seconds": 3824494.366666422, + "first_module_started_monotonic_seconds": 3824494.497172902, + "completed_monotonic_seconds": 3824508.779027073 + }, + { + "repetition": 1, + "output_root": "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/cp_tutorial_3d_monolayer/outputs/1", + "image_set_count": 1, + "invocation_seconds": 14.22139574913308, + "pre_first_module_seconds": 0.1260765721090138, + "first_module_through_post_run_seconds": 14.095319177024066, + "invocation_started_monotonic_seconds": 3824508.779039567, + "first_module_started_monotonic_seconds": 3824508.905116139, + "completed_monotonic_seconds": 3824523.000435316 + } + ] +} \ No newline at end of file diff --git a/benchmark/results/perf_canonical_output_policy_paired_20261003/native-speckles.json b/benchmark/results/perf_canonical_output_policy_paired_20261003/native-speckles.json new file mode 100644 index 0000000000..73edb31d11 --- /dev/null +++ b/benchmark/results/perf_canonical_output_policy_paired_20261003/native-speckles.json @@ -0,0 +1,61 @@ +{ + "startup_seconds": 0.8837718777358532, + "environment": { + "python_executable": "/home/ts/code/projects/openhcs/.venv-cellprofiler39/bin/python", + "python_version": "3.9.25", + "cellprofiler_version": "4.2.8.1", + "cellprofiler_core_version": "4.2.8.1", + "numpy_version": "1.24.4", + "scipy_version": "1.9.0", + "temporary_root": "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/ExampleSpeckles/native_tmp" + }, + "request": { + "pipeline_path": "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/ExampleSpeckles/preparation/native_cellprofiler_headless/ExampleSpeckles.cppipe", + "input_dir": "/home/ts/.cache/openhcs/cellprofiler_examples/ExampleSpeckles/images", + "output_root": "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/ExampleSpeckles/outputs", + "expected_image_sets": 1, + "repetitions": 2, + "file_list_path": "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/ExampleSpeckles/preparation/native_cellprofiler_file_list.txt", + "first_image_set": 1, + "last_image_set": null, + "report_path": "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/ExampleSpeckles/native_report.json", + "start_barrier_root": null, + "start_barrier_job_count": 1, + "start_barrier_job_index": 0 + }, + "observations": [ + { + "repetition": -1, + "output_root": "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/ExampleSpeckles/outputs/-1", + "image_set_count": 1, + "invocation_seconds": 3.3815025235526264, + "pre_first_module_seconds": 0.3202133346349001, + "first_module_through_post_run_seconds": 3.0612891889177263, + "invocation_started_monotonic_seconds": 3824531.358826258, + "first_module_started_monotonic_seconds": 3824531.679039593, + "completed_monotonic_seconds": 3824534.740328782 + }, + { + "repetition": 0, + "output_root": "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/ExampleSpeckles/outputs/0", + "image_set_count": 1, + "invocation_seconds": 2.0799103030003607, + "pre_first_module_seconds": 0.09814976202324033, + "first_module_through_post_run_seconds": 1.9817605409771204, + "invocation_started_monotonic_seconds": 3824534.740341355, + "first_module_started_monotonic_seconds": 3824534.838491117, + "completed_monotonic_seconds": 3824536.820251658 + }, + { + "repetition": 1, + "output_root": "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/ExampleSpeckles/outputs/1", + "image_set_count": 1, + "invocation_seconds": 2.0815624929964542, + "pre_first_module_seconds": 0.08911933470517397, + "first_module_through_post_run_seconds": 1.9924431582912803, + "invocation_started_monotonic_seconds": 3824536.820261787, + "first_module_started_monotonic_seconds": 3824536.909381122, + "completed_monotonic_seconds": 3824538.90182428 + } + ] +} \ No newline at end of file diff --git a/benchmark/results/perf_canonical_output_policy_paired_20261003/qualification.json b/benchmark/results/perf_canonical_output_policy_paired_20261003/qualification.json new file mode 100644 index 0000000000..6b82571e1b --- /dev/null +++ b/benchmark/results/perf_canonical_output_policy_paired_20261003/qualification.json @@ -0,0 +1,16 @@ +{ + "source_head": "ee8f97552b349499655bb3f961860b34dc784071", + "main_merge": "ba7b26b82717a7bc4de5c9701b413aa64ba13e35", + "fix_pr": "https://github.com/OpenHCSDev/openhcs/pull/531", + "closed_issue": 530, + "focused_main_tests_passed": 381, + "preexisting_shape_test_failures": 5, + "preexisting_failures_reproduced_on_unmodified_main": "6afc9a32514a204424d74527092ea318bc1cdd5e", + "freeze_path": "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/source-freeze.json", + "freeze_sha256": "1aaf1ce6405470eafe86a07db009d0477d55b8b8e2dbf0e8227b9be4e345ffba", + "nra_census_path": "/tmp/openhcs-source-cohort-5af63/policy-family-current-census-v2.json", + "nra_census_sha256": "91849adb21adacacd055a0c1a52b32c71cd58b4443916639fc38d6e6c4348f9c", + "science": "PASS: eight full ordinary/native comparisons", + "remaining_2x_execution_gap_3d_seconds": 1.835302054649219, + "performance_decision": "No causal gain admitted from two descriptive observations; joined dominant plumbing optimization remains active." +} diff --git a/benchmark/results/perf_canonical_output_policy_paired_20261003/strict-science.json b/benchmark/results/perf_canonical_output_policy_paired_20261003/strict-science.json new file mode 100644 index 0000000000..a69926264a --- /dev/null +++ b/benchmark/results/perf_canonical_output_policy_paired_20261003/strict-science.json @@ -0,0 +1,795 @@ +{ + "status": "PASS", + "comparisons": [ + { + "case": "cp_tutorial_3d_monolayer", + "ordinary_repeat": 0, + "native_repeat": 0, + "measurement_differences": 0, + "image_differences": 0, + "tables": [ + 7, + 6 + ], + "physical_images": [ + 2, + 120 + ], + "logical_images": [ + 2, + 2 + ], + "known_relationship_keys": [ + 3, + 3 + ], + "ordinary_receipt_sha256": "5f5119a8aa87e7e54f904ad9d0975810628363202e3dcbbe6f6bb138ce02fc02", + "output_hashes": { + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_erodedDownsizedNuclei.csv": "527a0cce497d2a618478be84d1a0bf174049f58aa2a666e37626a990fdca0116", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_xy1_ch2_NucleiLabels.tiff": "340bbd795541e4031749d1fff767fa4fcdcac10f5b97879056b25dac9fd7674b", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_Image.csv": "6612d34808f8d9904e801d7da5d5ef480912dc89c024548b48825c1b933f4d48", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_downsizedNuclei.csv": "2c66bc7b17de52147d5a875403f9a9746019ac61371c08157979044c6ed818dd", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_xy1_ch0_CellsLabels.tiff": "aaed9ed23179a162ee7f5a7e8d1d88f1fa342ca55172b7fccd4cbf67f7841d81", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_erodedResizedNuclei.csv": "66a58f78cab0cfdbe907468e8ba383fc59fab49025b84b589855bdfa04854609", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_Experiment.csv": "774bdd516b093073e8230071241a5e447a7e1e7ad5354f1d5c79878d07318f9a", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_Nuclei.csv": "440c33edc7e8cb5611910a835454db62390301a7af617a542349f60aece57561", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_Cells.csv": "cc34681db8a66c38ca77bc47db496b26703f21e4d7c7628dd74e30c33250859f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z041_t001_NucleiLabels.tiff": "744eab7cd170d5af8d2f72ced3893dd939c4e1b0f415afab799a37ded5d046c7", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z014_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z012_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z032_t001_CellsLabels.tiff": "8a2c5774044326ae722e41aae517266197fbebb0e4dd5eab4b22414fbfea309f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z004_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z018_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z019_t001_NucleiLabels.tiff": "250cb56a8054c28cea894bc3d9edaddcdce01f5151a05da78da20157c3c3be9f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z051_t001_CellsLabels.tiff": "73483bd10a0f37990084013ad054768086d6d3c929da6bc7e74f4aee37e9dbf2", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z052_t001_NucleiLabels.tiff": "0daf51526658e23b7c9ab8872e992ebe9322399286f95741d0ccdbd357e5d631", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_downsizedNuclei.csv": "a7b998c5a524e11747fc5452fd763fc3d9abe16f46e961f62e09e9393b6a8adf", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z043_t001_CellsLabels.tiff": "bd0df007524ea4e555e4d2fdbebe6a274caa03bc360a1cf90d601659155984b2", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z002_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_Image.csv": "75ac32ff5ef57bc285ec4eae1c9b501c27a35b486bf35435af5d499d856d34a4", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z021_t001_NucleiLabels.tiff": "71a10b0539f3fc4cba600b275b7e948258c6a435ba33ef522faa0869ac085518", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z034_t001_NucleiLabels.tiff": "08af61857cfb97fd95fcb08d1ce18139ce7087b192044c63452bee533c5b1aa4", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z019_t001_CellsLabels.tiff": "ec6ef1d319f2732c47b06f5cf02775c8cd94540f2f30a6c002f0aace1d317955", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z007_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z025_t001_NucleiLabels.tiff": "9cc33676a0177130609ab1331feab1722bbde8d55b2f49031730fd5a2a40029c", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z053_t001_CellsLabels.tiff": "ff17f538baaf179752412026a250bdbee016e3101037bf4e0dbb81199fc4c8ed", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z034_t001_CellsLabels.tiff": "3f6c1d8cdbce20aac8f70e7315bea70b147b7b6a034432845b27eeb6cfba083c", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z005_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z056_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z049_t001_CellsLabels.tiff": "28d83dfbf5bbf7e4da9c3e4806353e9fd0a89680dcdab4b43ca2fd19b80f669c", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z010_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z020_t001_NucleiLabels.tiff": "b0cd031b448d8bd2075e65710bd76c064161b25242f85ee150da7cd139fcc1a8", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z030_t001_CellsLabels.tiff": "b71a2e05d270ae0cd237d6c0d97ea82a63e5ad3633723e7255c35d34e585ec80", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z042_t001_CellsLabels.tiff": "88d2e9964455459499656c4f0a762cd86376c678756635aaa6b6c8d98be01852", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z059_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z013_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z008_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z051_t001_NucleiLabels.tiff": "f0f36ccaf744d42da036948682cfd226e6dd4a3cfb94aed38729b2d25ede5dff", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z050_t001_NucleiLabels.tiff": "565b4f08f7503086c1f2f8e631d19063129e02a5767d2f979c0cec8fdec23601", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z049_t001_NucleiLabels.tiff": "e92e493078672a0e0d90e31409612e215bf8486300d5c765d0e0504ff8888e7a", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/openhcs_metadata.json.lock": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/openhcs_metadata.json": "a0c2ccfa41ea6ec88bdc04143a144355f469b41a83caed2e71288662ff2b17ca", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z052_t001_CellsLabels.tiff": "7bb98f0587062e01b965e486fc687872651d7d90d271a2fdf0d35d67b464b233", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z035_t001_NucleiLabels.tiff": "13e69afc324152bb91329fa758d131d849127e8fd346078d94c6adc25582b3a6", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z009_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z029_t001_CellsLabels.tiff": "b3bdde5f751b8f5d41ca8b5219a3427e450cb1b8795401d0ec638940d1ad7bd9", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z056_t001_NucleiLabels.tiff": "ca0723c8b9764d5b77034719bb75a2940be35da2e04119af4c0e6d3552c08718", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z039_t001_NucleiLabels.tiff": "f93e8293a37886a36844b8748083a1f8e717efab092652755263e797f12f0364", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z013_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z047_t001_NucleiLabels.tiff": "995d5ce77cbf61a9be8590f34987ef3f1dd698ae6ed393cac4d0960bc9e7a66a", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z050_t001_CellsLabels.tiff": "ea7654a6fe1f92ad274a54935420923eec4bec5ad030187afc0ac7af02690af5", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_erodedResizedNuclei.csv": "cca23f86a85bb3ee7a5c3a1d174c34b5f026ca7f399be98458e1a8c6e12d5c77", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z029_t001_NucleiLabels.tiff": "34bce0f06f674e20667554eed0075fde8d9d33c9c0eaeffd88a1c558499cc432", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z022_t001_CellsLabels.tiff": "7d7ec170ed3652efa99e43d8db546c51b696821dca7727d0ff4d2f2ead7fdbaa", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z007_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z004_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z001_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z012_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z037_t001_CellsLabels.tiff": "aa1f449f1769b27cd9f1076e70f49f0242e2549f288a77984ca159cb07cf6a40", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z046_t001_CellsLabels.tiff": "dc081df050be6cd19b5d5bbfe21c96c350eecf9bc6f10f608dc361a36056632a", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z031_t001_CellsLabels.tiff": "1b14aab71bf3623458ea9373852b0baf28be9727877b32f894407927ecfb6d65", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z044_t001_NucleiLabels.tiff": "a78264f2f5e245a3913a69f89105e94df30048b1aaef4eb80fc5c3235e78b196", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z033_t001_NucleiLabels.tiff": "512a3a0ed8025d14b0556a72ebfc13b173d14f7046817ca09876242ab63b9d6b", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z042_t001_NucleiLabels.tiff": "e07252ca1a55c5195c99666d8d3f0d70db636b4ef4b99e1a0049ca22b182d87a", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_Nuclei.csv": "17500f5d4321bbf255df31f7a3503339029c2b077045a6c10e4d4f6f0db9ad1e", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z046_t001_NucleiLabels.tiff": "c1250e060d32f2cfc07fc7e1594fdb1d6cbb4d0483fa94a8113efa695397963c", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z032_t001_NucleiLabels.tiff": "97b09fe5444ad439c3c2007d75971dce2766d94c93268eccfee9ae49b8a04aa1", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z028_t001_CellsLabels.tiff": "fef31040e0c54501c3bf965eb98f055cdb1d42c96f91cf1c239033d0a9a06534", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z045_t001_CellsLabels.tiff": "c678f2100c7431e5a43c391af86a79e3436d4cf13f050f98c25f62682e4632fe", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z058_t001_NucleiLabels.tiff": "a37c63c6052e40d73db918a014e6f2a18be4d0cbfbeeef65a9c26096a38a4c09", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z060_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z028_t001_NucleiLabels.tiff": "1676f21bd3b7e105601c83f206890507c32ba2447988be2a75d0e8153d069b65", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z027_t001_CellsLabels.tiff": "ee9056587a19e581f5feb6078b41fa1004d923a5ddf1725aefc4b8b8c5201ad6", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z020_t001_CellsLabels.tiff": "e8105a4f04198ac75de9831247030084d6f65bf98691d7444ead7e178072fa22", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z033_t001_CellsLabels.tiff": "a320b99ee129d97b99f9fac66aabb89fc1652930b3bb8b2b3fbba7b57e994f85", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z055_t001_NucleiLabels.tiff": "44e02416cc94a70528165a4b6c42b9e19b205f9c91528076796272cb1a8504ea", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z059_t001_NucleiLabels.tiff": "76c785cf0f481c73ad3cf34ccb576eaa19dd1bdd9c74f9eaf92adfbde5d44b97", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z024_t001_NucleiLabels.tiff": "9e96a05ed9860cac9c57c618db2f4fe5e4b769a9d235aac6fd1d628486317df0", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_Cells.csv": "6f6e7431171ed5ea73a01784105147eca0e43a6e3e41f8720df93106353c9c79", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z037_t001_NucleiLabels.tiff": "927d52441c57cb3a4014effaca3e3c23c76275fb264c26e1942a9c58b85ee6d2", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z003_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z023_t001_NucleiLabels.tiff": "ecb7f78c8ca0bad2b9d0495659609b9023781b6a936d6afb0a04653eac176555", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z011_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z040_t001_CellsLabels.tiff": "1635ea20dd80ce99f0e86494666825a52a66645837d598fd3a3fa91f0bb77d53", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z017_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z035_t001_CellsLabels.tiff": "eedeb489b46abf1eafbd9c81491cb3f85ce1cb3ce877844ad22e72547fc3c438", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z027_t001_NucleiLabels.tiff": "8c8f402d3a7c65c04e7f1575dd34de0427eeeb433ff2956d20e07d06eb48fb8d", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z016_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z002_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z023_t001_CellsLabels.tiff": "2f06989618050934c73473f9342c2fe3410fd834a0ddb976cf602fe6198db7a6", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z001_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z060_t001_NucleiLabels.tiff": "7cc3c6c6a22f558e62194277090bdea2deb79475dd91fe7d0e49eac3c0187388", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z026_t001_NucleiLabels.tiff": "ad22809ff3f43d15c838f697061c263070dbf1da1031da39900681cdfbf9d54c", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z045_t001_NucleiLabels.tiff": "94bd6a65795ad6395cc18a4fd63dc591354bef612ee49fffd2d9f63e4ed9db84", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z038_t001_CellsLabels.tiff": "6ac8fd9c95acc1f261e96a8fb9332e5d6a403548842f9b85ecb977e6213b4c9b", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z055_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z025_t001_CellsLabels.tiff": "03aabe5528198cfd6ec23a058dab540fb8be0870207c6dc94a62d9b621d1f2f9", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z038_t001_NucleiLabels.tiff": "e2852ea83c62ab30d77681b4e39f88d3cee74ae052f1c948675ae3581c9fc28a", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z015_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z031_t001_NucleiLabels.tiff": "247490c261fecc26c7379ab2eebaa01e4bea523218b3cc5491d2fe2b9aadfe11", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z036_t001_CellsLabels.tiff": "28b68d1300cf7b08e21dbcd4858447862520dded5c27f2d84cf223513a119eed", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z017_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z009_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_erodedDownsizedNuclei.csv": "ee507fb9c2387f86fc768184baf2596ebe1f8e40f7fc5eb24f252345fee63ae6", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z026_t001_CellsLabels.tiff": "5b433d931fa6637fc793b23880199c01000ef6d9e610903bb38bb8cea5574563", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z048_t001_CellsLabels.tiff": "133a58f6fac8b07f79bfed9634867ccb5428b83e63682031a1def16c6a694568", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z006_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z039_t001_CellsLabels.tiff": "22fa79a76e64013f10373be2ca2af98a2173275c5d43c9cad33135acdc376d14", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z016_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z058_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z018_t001_NucleiLabels.tiff": "03ba7a4d6ae53e6c84aab5926ae1e2592b5f8ca24a839a256c82f08544df4270", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z030_t001_NucleiLabels.tiff": "c54c8b98a5c38a47e0cf9391a1afab09b4c8c58e06294cdb650a15add4a32b58", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z047_t001_CellsLabels.tiff": "b926aa64e1fa8aa52279a69d91dc23cf14bf0ae76107f9b360c7ae77bdf53719", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z054_t001_NucleiLabels.tiff": "b841c5d901c65a6138d9ff2b0ec4aa04f00765b1dd21bcd6fc9f8dc974c6035d", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z044_t001_CellsLabels.tiff": "e03946d94c789ee787cc29a3c8f5e5d9c56b37a0044b297ba8f403abc98eb55e", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z043_t001_NucleiLabels.tiff": "81a705d05b0a5bfa8783df28990c1d70634c1f4aa2bca23e02cfa8a4bc861b86", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z010_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z057_t001_NucleiLabels.tiff": "695517641e9fe4b8421d4a7db56b24c755b6957c98ce32bb24ad5b25f52661eb", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z053_t001_NucleiLabels.tiff": "b8613192175a4615b58aed607474657668429cb6ac635cd012569b19667f2d3d", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z057_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z022_t001_NucleiLabels.tiff": "50aef20e28a2f9e18ca2e28eabf6d87ec2ad20c79898e56f8d63a959ba5bb507", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z005_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z015_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z014_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z021_t001_CellsLabels.tiff": "daa31cb1e537e5c32b1936ab7647c5690741d2bc1d0e15c60b59ba1310a8c0ce", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z040_t001_NucleiLabels.tiff": "e0bd66b166e9f92120dbee183973849eda0afbbe6fed9d03fc4cfd2c56a96280", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z041_t001_CellsLabels.tiff": "be7762756eb65f08cb337904165c6f43b5351f39405461f65ad28669abfe3899", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z008_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z011_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z024_t001_CellsLabels.tiff": "39899adabca74b10b9950d1f97e8b0b3bbaf136d75e7a46ef4a6e86896e3119f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z048_t001_NucleiLabels.tiff": "325478230529e94ba372f38f0bf512292f131d337b6ac09ce5a91b0266724e02", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z036_t001_NucleiLabels.tiff": "4fa967c78709aa400246a50528a36b24ca366e825dd0e64561643369e814906b", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z003_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z054_t001_CellsLabels.tiff": "25ff2ec586a568e42286f73713d64aab1208c38c016099299211f04368ac49c8", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z006_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f" + } + }, + { + "case": "cp_tutorial_3d_monolayer", + "ordinary_repeat": 0, + "native_repeat": 1, + "measurement_differences": 0, + "image_differences": 0, + "tables": [ + 7, + 6 + ], + "physical_images": [ + 2, + 120 + ], + "logical_images": [ + 2, + 2 + ], + "known_relationship_keys": [ + 3, + 3 + ], + "ordinary_receipt_sha256": "5f5119a8aa87e7e54f904ad9d0975810628363202e3dcbbe6f6bb138ce02fc02", + "output_hashes": { + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_erodedResizedNuclei.csv": "66a58f78cab0cfdbe907468e8ba383fc59fab49025b84b589855bdfa04854609", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_xy1_ch0_CellsLabels.tiff": "aaed9ed23179a162ee7f5a7e8d1d88f1fa342ca55172b7fccd4cbf67f7841d81", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_Image.csv": "d775f1b475e2d5f725d30e736f5074301f8f17a1ee72824ad38a659f61d0a184", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_Cells.csv": "cc34681db8a66c38ca77bc47db496b26703f21e4d7c7628dd74e30c33250859f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_Experiment.csv": "f6b57eaed8c47bd7dfd14bcbaab24b1c2dbd760daaea7fbb9622ec1b1b79307d", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_Nuclei.csv": "440c33edc7e8cb5611910a835454db62390301a7af617a542349f60aece57561", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_xy1_ch2_NucleiLabels.tiff": "340bbd795541e4031749d1fff767fa4fcdcac10f5b97879056b25dac9fd7674b", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_erodedDownsizedNuclei.csv": "527a0cce497d2a618478be84d1a0bf174049f58aa2a666e37626a990fdca0116", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_downsizedNuclei.csv": "2c66bc7b17de52147d5a875403f9a9746019ac61371c08157979044c6ed818dd", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z041_t001_NucleiLabels.tiff": "744eab7cd170d5af8d2f72ced3893dd939c4e1b0f415afab799a37ded5d046c7", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z014_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z012_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z032_t001_CellsLabels.tiff": "8a2c5774044326ae722e41aae517266197fbebb0e4dd5eab4b22414fbfea309f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z004_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z018_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z019_t001_NucleiLabels.tiff": "250cb56a8054c28cea894bc3d9edaddcdce01f5151a05da78da20157c3c3be9f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z051_t001_CellsLabels.tiff": "73483bd10a0f37990084013ad054768086d6d3c929da6bc7e74f4aee37e9dbf2", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z052_t001_NucleiLabels.tiff": "0daf51526658e23b7c9ab8872e992ebe9322399286f95741d0ccdbd357e5d631", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_downsizedNuclei.csv": "a7b998c5a524e11747fc5452fd763fc3d9abe16f46e961f62e09e9393b6a8adf", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z043_t001_CellsLabels.tiff": "bd0df007524ea4e555e4d2fdbebe6a274caa03bc360a1cf90d601659155984b2", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z002_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_Image.csv": "75ac32ff5ef57bc285ec4eae1c9b501c27a35b486bf35435af5d499d856d34a4", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z021_t001_NucleiLabels.tiff": "71a10b0539f3fc4cba600b275b7e948258c6a435ba33ef522faa0869ac085518", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z034_t001_NucleiLabels.tiff": "08af61857cfb97fd95fcb08d1ce18139ce7087b192044c63452bee533c5b1aa4", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z019_t001_CellsLabels.tiff": "ec6ef1d319f2732c47b06f5cf02775c8cd94540f2f30a6c002f0aace1d317955", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z007_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z025_t001_NucleiLabels.tiff": "9cc33676a0177130609ab1331feab1722bbde8d55b2f49031730fd5a2a40029c", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z053_t001_CellsLabels.tiff": "ff17f538baaf179752412026a250bdbee016e3101037bf4e0dbb81199fc4c8ed", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z034_t001_CellsLabels.tiff": "3f6c1d8cdbce20aac8f70e7315bea70b147b7b6a034432845b27eeb6cfba083c", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z005_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z056_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z049_t001_CellsLabels.tiff": "28d83dfbf5bbf7e4da9c3e4806353e9fd0a89680dcdab4b43ca2fd19b80f669c", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z010_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z020_t001_NucleiLabels.tiff": "b0cd031b448d8bd2075e65710bd76c064161b25242f85ee150da7cd139fcc1a8", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z030_t001_CellsLabels.tiff": "b71a2e05d270ae0cd237d6c0d97ea82a63e5ad3633723e7255c35d34e585ec80", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z042_t001_CellsLabels.tiff": "88d2e9964455459499656c4f0a762cd86376c678756635aaa6b6c8d98be01852", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z059_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z013_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z008_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z051_t001_NucleiLabels.tiff": "f0f36ccaf744d42da036948682cfd226e6dd4a3cfb94aed38729b2d25ede5dff", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z050_t001_NucleiLabels.tiff": "565b4f08f7503086c1f2f8e631d19063129e02a5767d2f979c0cec8fdec23601", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z049_t001_NucleiLabels.tiff": "e92e493078672a0e0d90e31409612e215bf8486300d5c765d0e0504ff8888e7a", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/openhcs_metadata.json.lock": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/openhcs_metadata.json": "a0c2ccfa41ea6ec88bdc04143a144355f469b41a83caed2e71288662ff2b17ca", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z052_t001_CellsLabels.tiff": "7bb98f0587062e01b965e486fc687872651d7d90d271a2fdf0d35d67b464b233", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z035_t001_NucleiLabels.tiff": "13e69afc324152bb91329fa758d131d849127e8fd346078d94c6adc25582b3a6", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z009_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z029_t001_CellsLabels.tiff": "b3bdde5f751b8f5d41ca8b5219a3427e450cb1b8795401d0ec638940d1ad7bd9", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z056_t001_NucleiLabels.tiff": "ca0723c8b9764d5b77034719bb75a2940be35da2e04119af4c0e6d3552c08718", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z039_t001_NucleiLabels.tiff": "f93e8293a37886a36844b8748083a1f8e717efab092652755263e797f12f0364", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z013_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z047_t001_NucleiLabels.tiff": "995d5ce77cbf61a9be8590f34987ef3f1dd698ae6ed393cac4d0960bc9e7a66a", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z050_t001_CellsLabels.tiff": "ea7654a6fe1f92ad274a54935420923eec4bec5ad030187afc0ac7af02690af5", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_erodedResizedNuclei.csv": "cca23f86a85bb3ee7a5c3a1d174c34b5f026ca7f399be98458e1a8c6e12d5c77", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z029_t001_NucleiLabels.tiff": "34bce0f06f674e20667554eed0075fde8d9d33c9c0eaeffd88a1c558499cc432", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z022_t001_CellsLabels.tiff": "7d7ec170ed3652efa99e43d8db546c51b696821dca7727d0ff4d2f2ead7fdbaa", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z007_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z004_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z001_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z012_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z037_t001_CellsLabels.tiff": "aa1f449f1769b27cd9f1076e70f49f0242e2549f288a77984ca159cb07cf6a40", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z046_t001_CellsLabels.tiff": "dc081df050be6cd19b5d5bbfe21c96c350eecf9bc6f10f608dc361a36056632a", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z031_t001_CellsLabels.tiff": "1b14aab71bf3623458ea9373852b0baf28be9727877b32f894407927ecfb6d65", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z044_t001_NucleiLabels.tiff": "a78264f2f5e245a3913a69f89105e94df30048b1aaef4eb80fc5c3235e78b196", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z033_t001_NucleiLabels.tiff": "512a3a0ed8025d14b0556a72ebfc13b173d14f7046817ca09876242ab63b9d6b", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z042_t001_NucleiLabels.tiff": "e07252ca1a55c5195c99666d8d3f0d70db636b4ef4b99e1a0049ca22b182d87a", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_Nuclei.csv": "17500f5d4321bbf255df31f7a3503339029c2b077045a6c10e4d4f6f0db9ad1e", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z046_t001_NucleiLabels.tiff": "c1250e060d32f2cfc07fc7e1594fdb1d6cbb4d0483fa94a8113efa695397963c", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z032_t001_NucleiLabels.tiff": "97b09fe5444ad439c3c2007d75971dce2766d94c93268eccfee9ae49b8a04aa1", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z028_t001_CellsLabels.tiff": "fef31040e0c54501c3bf965eb98f055cdb1d42c96f91cf1c239033d0a9a06534", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z045_t001_CellsLabels.tiff": "c678f2100c7431e5a43c391af86a79e3436d4cf13f050f98c25f62682e4632fe", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z058_t001_NucleiLabels.tiff": "a37c63c6052e40d73db918a014e6f2a18be4d0cbfbeeef65a9c26096a38a4c09", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z060_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z028_t001_NucleiLabels.tiff": "1676f21bd3b7e105601c83f206890507c32ba2447988be2a75d0e8153d069b65", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z027_t001_CellsLabels.tiff": "ee9056587a19e581f5feb6078b41fa1004d923a5ddf1725aefc4b8b8c5201ad6", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z020_t001_CellsLabels.tiff": "e8105a4f04198ac75de9831247030084d6f65bf98691d7444ead7e178072fa22", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z033_t001_CellsLabels.tiff": "a320b99ee129d97b99f9fac66aabb89fc1652930b3bb8b2b3fbba7b57e994f85", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z055_t001_NucleiLabels.tiff": "44e02416cc94a70528165a4b6c42b9e19b205f9c91528076796272cb1a8504ea", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z059_t001_NucleiLabels.tiff": "76c785cf0f481c73ad3cf34ccb576eaa19dd1bdd9c74f9eaf92adfbde5d44b97", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z024_t001_NucleiLabels.tiff": "9e96a05ed9860cac9c57c618db2f4fe5e4b769a9d235aac6fd1d628486317df0", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_Cells.csv": "6f6e7431171ed5ea73a01784105147eca0e43a6e3e41f8720df93106353c9c79", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z037_t001_NucleiLabels.tiff": "927d52441c57cb3a4014effaca3e3c23c76275fb264c26e1942a9c58b85ee6d2", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z003_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z023_t001_NucleiLabels.tiff": "ecb7f78c8ca0bad2b9d0495659609b9023781b6a936d6afb0a04653eac176555", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z011_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z040_t001_CellsLabels.tiff": "1635ea20dd80ce99f0e86494666825a52a66645837d598fd3a3fa91f0bb77d53", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z017_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z035_t001_CellsLabels.tiff": "eedeb489b46abf1eafbd9c81491cb3f85ce1cb3ce877844ad22e72547fc3c438", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z027_t001_NucleiLabels.tiff": "8c8f402d3a7c65c04e7f1575dd34de0427eeeb433ff2956d20e07d06eb48fb8d", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z016_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z002_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z023_t001_CellsLabels.tiff": "2f06989618050934c73473f9342c2fe3410fd834a0ddb976cf602fe6198db7a6", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z001_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z060_t001_NucleiLabels.tiff": "7cc3c6c6a22f558e62194277090bdea2deb79475dd91fe7d0e49eac3c0187388", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z026_t001_NucleiLabels.tiff": "ad22809ff3f43d15c838f697061c263070dbf1da1031da39900681cdfbf9d54c", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z045_t001_NucleiLabels.tiff": "94bd6a65795ad6395cc18a4fd63dc591354bef612ee49fffd2d9f63e4ed9db84", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z038_t001_CellsLabels.tiff": "6ac8fd9c95acc1f261e96a8fb9332e5d6a403548842f9b85ecb977e6213b4c9b", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z055_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z025_t001_CellsLabels.tiff": "03aabe5528198cfd6ec23a058dab540fb8be0870207c6dc94a62d9b621d1f2f9", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z038_t001_NucleiLabels.tiff": "e2852ea83c62ab30d77681b4e39f88d3cee74ae052f1c948675ae3581c9fc28a", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z015_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z031_t001_NucleiLabels.tiff": "247490c261fecc26c7379ab2eebaa01e4bea523218b3cc5491d2fe2b9aadfe11", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z036_t001_CellsLabels.tiff": "28b68d1300cf7b08e21dbcd4858447862520dded5c27f2d84cf223513a119eed", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z017_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z009_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_erodedDownsizedNuclei.csv": "ee507fb9c2387f86fc768184baf2596ebe1f8e40f7fc5eb24f252345fee63ae6", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z026_t001_CellsLabels.tiff": "5b433d931fa6637fc793b23880199c01000ef6d9e610903bb38bb8cea5574563", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z048_t001_CellsLabels.tiff": "133a58f6fac8b07f79bfed9634867ccb5428b83e63682031a1def16c6a694568", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z006_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z039_t001_CellsLabels.tiff": "22fa79a76e64013f10373be2ca2af98a2173275c5d43c9cad33135acdc376d14", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z016_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z058_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z018_t001_NucleiLabels.tiff": "03ba7a4d6ae53e6c84aab5926ae1e2592b5f8ca24a839a256c82f08544df4270", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z030_t001_NucleiLabels.tiff": "c54c8b98a5c38a47e0cf9391a1afab09b4c8c58e06294cdb650a15add4a32b58", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z047_t001_CellsLabels.tiff": "b926aa64e1fa8aa52279a69d91dc23cf14bf0ae76107f9b360c7ae77bdf53719", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z054_t001_NucleiLabels.tiff": "b841c5d901c65a6138d9ff2b0ec4aa04f00765b1dd21bcd6fc9f8dc974c6035d", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z044_t001_CellsLabels.tiff": "e03946d94c789ee787cc29a3c8f5e5d9c56b37a0044b297ba8f403abc98eb55e", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z043_t001_NucleiLabels.tiff": "81a705d05b0a5bfa8783df28990c1d70634c1f4aa2bca23e02cfa8a4bc861b86", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z010_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z057_t001_NucleiLabels.tiff": "695517641e9fe4b8421d4a7db56b24c755b6957c98ce32bb24ad5b25f52661eb", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z053_t001_NucleiLabels.tiff": "b8613192175a4615b58aed607474657668429cb6ac635cd012569b19667f2d3d", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z057_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z022_t001_NucleiLabels.tiff": "50aef20e28a2f9e18ca2e28eabf6d87ec2ad20c79898e56f8d63a959ba5bb507", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z005_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z015_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z014_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z021_t001_CellsLabels.tiff": "daa31cb1e537e5c32b1936ab7647c5690741d2bc1d0e15c60b59ba1310a8c0ce", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z040_t001_NucleiLabels.tiff": "e0bd66b166e9f92120dbee183973849eda0afbbe6fed9d03fc4cfd2c56a96280", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z041_t001_CellsLabels.tiff": "be7762756eb65f08cb337904165c6f43b5351f39405461f65ad28669abfe3899", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z008_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z011_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z024_t001_CellsLabels.tiff": "39899adabca74b10b9950d1f97e8b0b3bbaf136d75e7a46ef4a6e86896e3119f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z048_t001_NucleiLabels.tiff": "325478230529e94ba372f38f0bf512292f131d337b6ac09ce5a91b0266724e02", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z036_t001_NucleiLabels.tiff": "4fa967c78709aa400246a50528a36b24ca366e825dd0e64561643369e814906b", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z003_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z054_t001_CellsLabels.tiff": "25ff2ec586a568e42286f73713d64aab1208c38c016099299211f04368ac49c8", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z006_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f" + } + }, + { + "case": "ExampleSpeckles", + "ordinary_repeat": 0, + "native_repeat": 0, + "measurement_differences": 0, + "image_differences": 0, + "tables": [ + 3, + 3 + ], + "physical_images": [ + 0, + 0 + ], + "logical_images": [ + 0, + 0 + ], + "known_relationship_keys": [ + 1, + 1 + ], + "ordinary_receipt_sha256": "1ef11d00b7fd5b638b104d9b8be6e03483ed1668465f817fc36905ef2f8b099d", + "output_hashes": { + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/ExampleSpeckles/outputs/0/Nuclei.csv": "15af7cac08d651c19b3e3f409b2b0d7fc4f96011c9b53394aa9a35969a8936a6", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/ExampleSpeckles/outputs/0/h2ax.csv": "0f54fc72ccdeb7e1a68bab9dc3a8d9d49788cdf81dbbc109ed119e589c09538a", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/ExampleSpeckles/outputs/0/Image.csv": "cebe6c0c9ab138ff1f65906b70a130b9e79e3b7d873cc006e88067c596b07a98", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/ExampleSpeckles/wells_1/workers_1/images_ExampleSpeckles_source_workspace_well_throughput/results/Image.csv": "85ef16f24e75f03dc7055215cf2e6cb196b1dc0a0d73713a4c5999f698d4f96e", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/ExampleSpeckles/wells_1/workers_1/images_ExampleSpeckles_source_workspace_well_throughput/results/Nuclei.csv": "e50cb9f8c23f65e9ce8db25132263cf4a4448ed65fc1088d782536abf74c5670", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/ExampleSpeckles/wells_1/workers_1/images_ExampleSpeckles_source_workspace_well_throughput/results/h2ax.csv": "eb453565f3b79ac932eeb5cde8605888b72e58c80f42edbbf0b1ba37f34fd6f8", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/ExampleSpeckles/wells_1/workers_1/images_ExampleSpeckles_source_workspace_well_throughput/openhcs_metadata.json": "71582c62cabb2571ebf8a0de57f92fdf82c55a7ece3dac0bf2a6eca95cad84c9", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/ExampleSpeckles/wells_1/workers_1/images_ExampleSpeckles_source_workspace_well_throughput/openhcs_metadata.json.lock": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + { + "case": "ExampleSpeckles", + "ordinary_repeat": 0, + "native_repeat": 1, + "measurement_differences": 0, + "image_differences": 0, + "tables": [ + 3, + 3 + ], + "physical_images": [ + 0, + 0 + ], + "logical_images": [ + 0, + 0 + ], + "known_relationship_keys": [ + 1, + 1 + ], + "ordinary_receipt_sha256": "1ef11d00b7fd5b638b104d9b8be6e03483ed1668465f817fc36905ef2f8b099d", + "output_hashes": { + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/ExampleSpeckles/outputs/1/Nuclei.csv": "15af7cac08d651c19b3e3f409b2b0d7fc4f96011c9b53394aa9a35969a8936a6", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/ExampleSpeckles/outputs/1/h2ax.csv": "0f54fc72ccdeb7e1a68bab9dc3a8d9d49788cdf81dbbc109ed119e589c09538a", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/ExampleSpeckles/outputs/1/Image.csv": "be7bbf5f19c27df330b95c8eb1784e0053a655f84d278d40a9023c1feeb1fbed", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/ExampleSpeckles/wells_1/workers_1/images_ExampleSpeckles_source_workspace_well_throughput/results/Image.csv": "85ef16f24e75f03dc7055215cf2e6cb196b1dc0a0d73713a4c5999f698d4f96e", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/ExampleSpeckles/wells_1/workers_1/images_ExampleSpeckles_source_workspace_well_throughput/results/Nuclei.csv": "e50cb9f8c23f65e9ce8db25132263cf4a4448ed65fc1088d782536abf74c5670", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/ExampleSpeckles/wells_1/workers_1/images_ExampleSpeckles_source_workspace_well_throughput/results/h2ax.csv": "eb453565f3b79ac932eeb5cde8605888b72e58c80f42edbbf0b1ba37f34fd6f8", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/ExampleSpeckles/wells_1/workers_1/images_ExampleSpeckles_source_workspace_well_throughput/openhcs_metadata.json": "71582c62cabb2571ebf8a0de57f92fdf82c55a7ece3dac0bf2a6eca95cad84c9", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-0/ExampleSpeckles/wells_1/workers_1/images_ExampleSpeckles_source_workspace_well_throughput/openhcs_metadata.json.lock": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + { + "case": "cp_tutorial_3d_monolayer", + "ordinary_repeat": 1, + "native_repeat": 0, + "measurement_differences": 0, + "image_differences": 0, + "tables": [ + 7, + 6 + ], + "physical_images": [ + 2, + 120 + ], + "logical_images": [ + 2, + 2 + ], + "known_relationship_keys": [ + 3, + 3 + ], + "ordinary_receipt_sha256": "caf9cca665b85734e682f713c153260f8c949fdabd9229f482d6b501964dbac9", + "output_hashes": { + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_erodedDownsizedNuclei.csv": "527a0cce497d2a618478be84d1a0bf174049f58aa2a666e37626a990fdca0116", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_xy1_ch2_NucleiLabels.tiff": "340bbd795541e4031749d1fff767fa4fcdcac10f5b97879056b25dac9fd7674b", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_Image.csv": "6612d34808f8d9904e801d7da5d5ef480912dc89c024548b48825c1b933f4d48", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_downsizedNuclei.csv": "2c66bc7b17de52147d5a875403f9a9746019ac61371c08157979044c6ed818dd", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_xy1_ch0_CellsLabels.tiff": "aaed9ed23179a162ee7f5a7e8d1d88f1fa342ca55172b7fccd4cbf67f7841d81", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_erodedResizedNuclei.csv": "66a58f78cab0cfdbe907468e8ba383fc59fab49025b84b589855bdfa04854609", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_Experiment.csv": "774bdd516b093073e8230071241a5e447a7e1e7ad5354f1d5c79878d07318f9a", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_Nuclei.csv": "440c33edc7e8cb5611910a835454db62390301a7af617a542349f60aece57561", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/cp_tutorial_3d_monolayer/outputs/0/3d_monolayer_Cells.csv": "cc34681db8a66c38ca77bc47db496b26703f21e4d7c7628dd74e30c33250859f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z054_t001_CellsLabels.tiff": "25ff2ec586a568e42286f73713d64aab1208c38c016099299211f04368ac49c8", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z001_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z005_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z013_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z056_t001_NucleiLabels.tiff": "ca0723c8b9764d5b77034719bb75a2940be35da2e04119af4c0e6d3552c08718", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z044_t001_CellsLabels.tiff": "e03946d94c789ee787cc29a3c8f5e5d9c56b37a0044b297ba8f403abc98eb55e", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z002_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z008_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z020_t001_CellsLabels.tiff": "e8105a4f04198ac75de9831247030084d6f65bf98691d7444ead7e178072fa22", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z029_t001_CellsLabels.tiff": "b3bdde5f751b8f5d41ca8b5219a3427e450cb1b8795401d0ec638940d1ad7bd9", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z009_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z014_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z047_t001_CellsLabels.tiff": "b926aa64e1fa8aa52279a69d91dc23cf14bf0ae76107f9b360c7ae77bdf53719", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_erodedDownsizedNuclei.csv": "ee507fb9c2387f86fc768184baf2596ebe1f8e40f7fc5eb24f252345fee63ae6", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z030_t001_CellsLabels.tiff": "b71a2e05d270ae0cd237d6c0d97ea82a63e5ad3633723e7255c35d34e585ec80", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z040_t001_CellsLabels.tiff": "1635ea20dd80ce99f0e86494666825a52a66645837d598fd3a3fa91f0bb77d53", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z032_t001_NucleiLabels.tiff": "97b09fe5444ad439c3c2007d75971dce2766d94c93268eccfee9ae49b8a04aa1", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z046_t001_NucleiLabels.tiff": "c1250e060d32f2cfc07fc7e1594fdb1d6cbb4d0483fa94a8113efa695397963c", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_erodedResizedNuclei.csv": "cca23f86a85bb3ee7a5c3a1d174c34b5f026ca7f399be98458e1a8c6e12d5c77", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z044_t001_NucleiLabels.tiff": "a78264f2f5e245a3913a69f89105e94df30048b1aaef4eb80fc5c3235e78b196", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z007_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z045_t001_NucleiLabels.tiff": "94bd6a65795ad6395cc18a4fd63dc591354bef612ee49fffd2d9f63e4ed9db84", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z052_t001_NucleiLabels.tiff": "0daf51526658e23b7c9ab8872e992ebe9322399286f95741d0ccdbd357e5d631", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z043_t001_CellsLabels.tiff": "bd0df007524ea4e555e4d2fdbebe6a274caa03bc360a1cf90d601659155984b2", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z037_t001_NucleiLabels.tiff": "927d52441c57cb3a4014effaca3e3c23c76275fb264c26e1942a9c58b85ee6d2", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z046_t001_CellsLabels.tiff": "dc081df050be6cd19b5d5bbfe21c96c350eecf9bc6f10f608dc361a36056632a", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z055_t001_NucleiLabels.tiff": "44e02416cc94a70528165a4b6c42b9e19b205f9c91528076796272cb1a8504ea", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_Nuclei.csv": "17500f5d4321bbf255df31f7a3503339029c2b077045a6c10e4d4f6f0db9ad1e", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z048_t001_CellsLabels.tiff": "133a58f6fac8b07f79bfed9634867ccb5428b83e63682031a1def16c6a694568", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z051_t001_CellsLabels.tiff": "73483bd10a0f37990084013ad054768086d6d3c929da6bc7e74f4aee37e9dbf2", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z027_t001_NucleiLabels.tiff": "8c8f402d3a7c65c04e7f1575dd34de0427eeeb433ff2956d20e07d06eb48fb8d", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z055_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z031_t001_NucleiLabels.tiff": "247490c261fecc26c7379ab2eebaa01e4bea523218b3cc5491d2fe2b9aadfe11", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z023_t001_NucleiLabels.tiff": "ecb7f78c8ca0bad2b9d0495659609b9023781b6a936d6afb0a04653eac176555", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z008_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z045_t001_CellsLabels.tiff": "c678f2100c7431e5a43c391af86a79e3436d4cf13f050f98c25f62682e4632fe", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z036_t001_CellsLabels.tiff": "28b68d1300cf7b08e21dbcd4858447862520dded5c27f2d84cf223513a119eed", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z038_t001_NucleiLabels.tiff": "e2852ea83c62ab30d77681b4e39f88d3cee74ae052f1c948675ae3581c9fc28a", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z052_t001_CellsLabels.tiff": "7bb98f0587062e01b965e486fc687872651d7d90d271a2fdf0d35d67b464b233", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z015_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z004_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z001_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z054_t001_NucleiLabels.tiff": "b841c5d901c65a6138d9ff2b0ec4aa04f00765b1dd21bcd6fc9f8dc974c6035d", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z019_t001_NucleiLabels.tiff": "250cb56a8054c28cea894bc3d9edaddcdce01f5151a05da78da20157c3c3be9f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z018_t001_NucleiLabels.tiff": "03ba7a4d6ae53e6c84aab5926ae1e2592b5f8ca24a839a256c82f08544df4270", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z058_t001_NucleiLabels.tiff": "a37c63c6052e40d73db918a014e6f2a18be4d0cbfbeeef65a9c26096a38a4c09", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z057_t001_NucleiLabels.tiff": "695517641e9fe4b8421d4a7db56b24c755b6957c98ce32bb24ad5b25f52661eb", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z032_t001_CellsLabels.tiff": "8a2c5774044326ae722e41aae517266197fbebb0e4dd5eab4b22414fbfea309f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z037_t001_CellsLabels.tiff": "aa1f449f1769b27cd9f1076e70f49f0242e2549f288a77984ca159cb07cf6a40", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z058_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z009_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z053_t001_NucleiLabels.tiff": "b8613192175a4615b58aed607474657668429cb6ac635cd012569b19667f2d3d", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z004_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z031_t001_CellsLabels.tiff": "1b14aab71bf3623458ea9373852b0baf28be9727877b32f894407927ecfb6d65", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z024_t001_CellsLabels.tiff": "39899adabca74b10b9950d1f97e8b0b3bbaf136d75e7a46ef4a6e86896e3119f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z033_t001_CellsLabels.tiff": "a320b99ee129d97b99f9fac66aabb89fc1652930b3bb8b2b3fbba7b57e994f85", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z030_t001_NucleiLabels.tiff": "c54c8b98a5c38a47e0cf9391a1afab09b4c8c58e06294cdb650a15add4a32b58", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z040_t001_NucleiLabels.tiff": "e0bd66b166e9f92120dbee183973849eda0afbbe6fed9d03fc4cfd2c56a96280", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z012_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z035_t001_NucleiLabels.tiff": "13e69afc324152bb91329fa758d131d849127e8fd346078d94c6adc25582b3a6", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z026_t001_CellsLabels.tiff": "5b433d931fa6637fc793b23880199c01000ef6d9e610903bb38bb8cea5574563", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z053_t001_CellsLabels.tiff": "ff17f538baaf179752412026a250bdbee016e3101037bf4e0dbb81199fc4c8ed", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z033_t001_NucleiLabels.tiff": "512a3a0ed8025d14b0556a72ebfc13b173d14f7046817ca09876242ab63b9d6b", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z041_t001_CellsLabels.tiff": "be7762756eb65f08cb337904165c6f43b5351f39405461f65ad28669abfe3899", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z050_t001_CellsLabels.tiff": "ea7654a6fe1f92ad274a54935420923eec4bec5ad030187afc0ac7af02690af5", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z034_t001_NucleiLabels.tiff": "08af61857cfb97fd95fcb08d1ce18139ce7087b192044c63452bee533c5b1aa4", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z026_t001_NucleiLabels.tiff": "ad22809ff3f43d15c838f697061c263070dbf1da1031da39900681cdfbf9d54c", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z016_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z019_t001_CellsLabels.tiff": "ec6ef1d319f2732c47b06f5cf02775c8cd94540f2f30a6c002f0aace1d317955", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z057_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z038_t001_CellsLabels.tiff": "6ac8fd9c95acc1f261e96a8fb9332e5d6a403548842f9b85ecb977e6213b4c9b", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z051_t001_NucleiLabels.tiff": "f0f36ccaf744d42da036948682cfd226e6dd4a3cfb94aed38729b2d25ede5dff", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z022_t001_NucleiLabels.tiff": "50aef20e28a2f9e18ca2e28eabf6d87ec2ad20c79898e56f8d63a959ba5bb507", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z005_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z021_t001_NucleiLabels.tiff": "71a10b0539f3fc4cba600b275b7e948258c6a435ba33ef522faa0869ac085518", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z042_t001_CellsLabels.tiff": "88d2e9964455459499656c4f0a762cd86376c678756635aaa6b6c8d98be01852", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/openhcs_metadata.json.lock": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z059_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z014_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_Cells.csv": "6f6e7431171ed5ea73a01784105147eca0e43a6e3e41f8720df93106353c9c79", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z048_t001_NucleiLabels.tiff": "325478230529e94ba372f38f0bf512292f131d337b6ac09ce5a91b0266724e02", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z013_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z016_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z020_t001_NucleiLabels.tiff": "b0cd031b448d8bd2075e65710bd76c064161b25242f85ee150da7cd139fcc1a8", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z043_t001_NucleiLabels.tiff": "81a705d05b0a5bfa8783df28990c1d70634c1f4aa2bca23e02cfa8a4bc861b86", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z002_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z010_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z027_t001_CellsLabels.tiff": "ee9056587a19e581f5feb6078b41fa1004d923a5ddf1725aefc4b8b8c5201ad6", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z049_t001_CellsLabels.tiff": "28d83dfbf5bbf7e4da9c3e4806353e9fd0a89680dcdab4b43ca2fd19b80f669c", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z050_t001_NucleiLabels.tiff": "565b4f08f7503086c1f2f8e631d19063129e02a5767d2f979c0cec8fdec23601", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z047_t001_NucleiLabels.tiff": "995d5ce77cbf61a9be8590f34987ef3f1dd698ae6ed393cac4d0960bc9e7a66a", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z010_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z034_t001_CellsLabels.tiff": "3f6c1d8cdbce20aac8f70e7315bea70b147b7b6a034432845b27eeb6cfba083c", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z060_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z028_t001_NucleiLabels.tiff": "1676f21bd3b7e105601c83f206890507c32ba2447988be2a75d0e8153d069b65", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z029_t001_NucleiLabels.tiff": "34bce0f06f674e20667554eed0075fde8d9d33c9c0eaeffd88a1c558499cc432", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_downsizedNuclei.csv": "a7b998c5a524e11747fc5452fd763fc3d9abe16f46e961f62e09e9393b6a8adf", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z011_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z015_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z021_t001_CellsLabels.tiff": "daa31cb1e537e5c32b1936ab7647c5690741d2bc1d0e15c60b59ba1310a8c0ce", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z060_t001_NucleiLabels.tiff": "7cc3c6c6a22f558e62194277090bdea2deb79475dd91fe7d0e49eac3c0187388", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z041_t001_NucleiLabels.tiff": "744eab7cd170d5af8d2f72ced3893dd939c4e1b0f415afab799a37ded5d046c7", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z012_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z039_t001_CellsLabels.tiff": "22fa79a76e64013f10373be2ca2af98a2173275c5d43c9cad33135acdc376d14", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z036_t001_NucleiLabels.tiff": "4fa967c78709aa400246a50528a36b24ca366e825dd0e64561643369e814906b", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z025_t001_NucleiLabels.tiff": "9cc33676a0177130609ab1331feab1722bbde8d55b2f49031730fd5a2a40029c", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/openhcs_metadata.json": "43ddc7fb22e14f8b2478e1c057a767169c6ffa396b4347fedea756a539e4500e", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z017_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z028_t001_CellsLabels.tiff": "fef31040e0c54501c3bf965eb98f055cdb1d42c96f91cf1c239033d0a9a06534", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z025_t001_CellsLabels.tiff": "03aabe5528198cfd6ec23a058dab540fb8be0870207c6dc94a62d9b621d1f2f9", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z022_t001_CellsLabels.tiff": "7d7ec170ed3652efa99e43d8db546c51b696821dca7727d0ff4d2f2ead7fdbaa", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z056_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z039_t001_NucleiLabels.tiff": "f93e8293a37886a36844b8748083a1f8e717efab092652755263e797f12f0364", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z007_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z003_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z018_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z042_t001_NucleiLabels.tiff": "e07252ca1a55c5195c99666d8d3f0d70db636b4ef4b99e1a0049ca22b182d87a", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z006_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z003_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_Image.csv": "75ac32ff5ef57bc285ec4eae1c9b501c27a35b486bf35435af5d499d856d34a4", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z011_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z006_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z024_t001_NucleiLabels.tiff": "9e96a05ed9860cac9c57c618db2f4fe5e4b769a9d235aac6fd1d628486317df0", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z023_t001_CellsLabels.tiff": "2f06989618050934c73473f9342c2fe3410fd834a0ddb976cf602fe6198db7a6", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z017_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z059_t001_NucleiLabels.tiff": "76c785cf0f481c73ad3cf34ccb576eaa19dd1bdd9c74f9eaf92adfbde5d44b97", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z049_t001_NucleiLabels.tiff": "e92e493078672a0e0d90e31409612e215bf8486300d5c765d0e0504ff8888e7a", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z035_t001_CellsLabels.tiff": "eedeb489b46abf1eafbd9c81491cb3f85ce1cb3ce877844ad22e72547fc3c438" + } + }, + { + "case": "cp_tutorial_3d_monolayer", + "ordinary_repeat": 1, + "native_repeat": 1, + "measurement_differences": 0, + "image_differences": 0, + "tables": [ + 7, + 6 + ], + "physical_images": [ + 2, + 120 + ], + "logical_images": [ + 2, + 2 + ], + "known_relationship_keys": [ + 3, + 3 + ], + "ordinary_receipt_sha256": "caf9cca665b85734e682f713c153260f8c949fdabd9229f482d6b501964dbac9", + "output_hashes": { + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_erodedResizedNuclei.csv": "66a58f78cab0cfdbe907468e8ba383fc59fab49025b84b589855bdfa04854609", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_xy1_ch0_CellsLabels.tiff": "aaed9ed23179a162ee7f5a7e8d1d88f1fa342ca55172b7fccd4cbf67f7841d81", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_Image.csv": "d775f1b475e2d5f725d30e736f5074301f8f17a1ee72824ad38a659f61d0a184", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_Cells.csv": "cc34681db8a66c38ca77bc47db496b26703f21e4d7c7628dd74e30c33250859f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_Experiment.csv": "f6b57eaed8c47bd7dfd14bcbaab24b1c2dbd760daaea7fbb9622ec1b1b79307d", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_Nuclei.csv": "440c33edc7e8cb5611910a835454db62390301a7af617a542349f60aece57561", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_xy1_ch2_NucleiLabels.tiff": "340bbd795541e4031749d1fff767fa4fcdcac10f5b97879056b25dac9fd7674b", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_erodedDownsizedNuclei.csv": "527a0cce497d2a618478be84d1a0bf174049f58aa2a666e37626a990fdca0116", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/cp_tutorial_3d_monolayer/outputs/1/3d_monolayer_downsizedNuclei.csv": "2c66bc7b17de52147d5a875403f9a9746019ac61371c08157979044c6ed818dd", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z054_t001_CellsLabels.tiff": "25ff2ec586a568e42286f73713d64aab1208c38c016099299211f04368ac49c8", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z001_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z005_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z013_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z056_t001_NucleiLabels.tiff": "ca0723c8b9764d5b77034719bb75a2940be35da2e04119af4c0e6d3552c08718", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z044_t001_CellsLabels.tiff": "e03946d94c789ee787cc29a3c8f5e5d9c56b37a0044b297ba8f403abc98eb55e", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z002_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z008_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z020_t001_CellsLabels.tiff": "e8105a4f04198ac75de9831247030084d6f65bf98691d7444ead7e178072fa22", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z029_t001_CellsLabels.tiff": "b3bdde5f751b8f5d41ca8b5219a3427e450cb1b8795401d0ec638940d1ad7bd9", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z009_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z014_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z047_t001_CellsLabels.tiff": "b926aa64e1fa8aa52279a69d91dc23cf14bf0ae76107f9b360c7ae77bdf53719", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_erodedDownsizedNuclei.csv": "ee507fb9c2387f86fc768184baf2596ebe1f8e40f7fc5eb24f252345fee63ae6", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z030_t001_CellsLabels.tiff": "b71a2e05d270ae0cd237d6c0d97ea82a63e5ad3633723e7255c35d34e585ec80", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z040_t001_CellsLabels.tiff": "1635ea20dd80ce99f0e86494666825a52a66645837d598fd3a3fa91f0bb77d53", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z032_t001_NucleiLabels.tiff": "97b09fe5444ad439c3c2007d75971dce2766d94c93268eccfee9ae49b8a04aa1", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z046_t001_NucleiLabels.tiff": "c1250e060d32f2cfc07fc7e1594fdb1d6cbb4d0483fa94a8113efa695397963c", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_erodedResizedNuclei.csv": "cca23f86a85bb3ee7a5c3a1d174c34b5f026ca7f399be98458e1a8c6e12d5c77", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z044_t001_NucleiLabels.tiff": "a78264f2f5e245a3913a69f89105e94df30048b1aaef4eb80fc5c3235e78b196", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z007_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z045_t001_NucleiLabels.tiff": "94bd6a65795ad6395cc18a4fd63dc591354bef612ee49fffd2d9f63e4ed9db84", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z052_t001_NucleiLabels.tiff": "0daf51526658e23b7c9ab8872e992ebe9322399286f95741d0ccdbd357e5d631", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z043_t001_CellsLabels.tiff": "bd0df007524ea4e555e4d2fdbebe6a274caa03bc360a1cf90d601659155984b2", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z037_t001_NucleiLabels.tiff": "927d52441c57cb3a4014effaca3e3c23c76275fb264c26e1942a9c58b85ee6d2", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z046_t001_CellsLabels.tiff": "dc081df050be6cd19b5d5bbfe21c96c350eecf9bc6f10f608dc361a36056632a", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z055_t001_NucleiLabels.tiff": "44e02416cc94a70528165a4b6c42b9e19b205f9c91528076796272cb1a8504ea", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_Nuclei.csv": "17500f5d4321bbf255df31f7a3503339029c2b077045a6c10e4d4f6f0db9ad1e", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z048_t001_CellsLabels.tiff": "133a58f6fac8b07f79bfed9634867ccb5428b83e63682031a1def16c6a694568", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z051_t001_CellsLabels.tiff": "73483bd10a0f37990084013ad054768086d6d3c929da6bc7e74f4aee37e9dbf2", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z027_t001_NucleiLabels.tiff": "8c8f402d3a7c65c04e7f1575dd34de0427eeeb433ff2956d20e07d06eb48fb8d", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z055_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z031_t001_NucleiLabels.tiff": "247490c261fecc26c7379ab2eebaa01e4bea523218b3cc5491d2fe2b9aadfe11", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z023_t001_NucleiLabels.tiff": "ecb7f78c8ca0bad2b9d0495659609b9023781b6a936d6afb0a04653eac176555", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z008_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z045_t001_CellsLabels.tiff": "c678f2100c7431e5a43c391af86a79e3436d4cf13f050f98c25f62682e4632fe", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z036_t001_CellsLabels.tiff": "28b68d1300cf7b08e21dbcd4858447862520dded5c27f2d84cf223513a119eed", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z038_t001_NucleiLabels.tiff": "e2852ea83c62ab30d77681b4e39f88d3cee74ae052f1c948675ae3581c9fc28a", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z052_t001_CellsLabels.tiff": "7bb98f0587062e01b965e486fc687872651d7d90d271a2fdf0d35d67b464b233", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z015_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z004_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z001_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z054_t001_NucleiLabels.tiff": "b841c5d901c65a6138d9ff2b0ec4aa04f00765b1dd21bcd6fc9f8dc974c6035d", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z019_t001_NucleiLabels.tiff": "250cb56a8054c28cea894bc3d9edaddcdce01f5151a05da78da20157c3c3be9f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z018_t001_NucleiLabels.tiff": "03ba7a4d6ae53e6c84aab5926ae1e2592b5f8ca24a839a256c82f08544df4270", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z058_t001_NucleiLabels.tiff": "a37c63c6052e40d73db918a014e6f2a18be4d0cbfbeeef65a9c26096a38a4c09", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z057_t001_NucleiLabels.tiff": "695517641e9fe4b8421d4a7db56b24c755b6957c98ce32bb24ad5b25f52661eb", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z032_t001_CellsLabels.tiff": "8a2c5774044326ae722e41aae517266197fbebb0e4dd5eab4b22414fbfea309f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z037_t001_CellsLabels.tiff": "aa1f449f1769b27cd9f1076e70f49f0242e2549f288a77984ca159cb07cf6a40", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z058_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z009_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z053_t001_NucleiLabels.tiff": "b8613192175a4615b58aed607474657668429cb6ac635cd012569b19667f2d3d", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z004_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z031_t001_CellsLabels.tiff": "1b14aab71bf3623458ea9373852b0baf28be9727877b32f894407927ecfb6d65", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z024_t001_CellsLabels.tiff": "39899adabca74b10b9950d1f97e8b0b3bbaf136d75e7a46ef4a6e86896e3119f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z033_t001_CellsLabels.tiff": "a320b99ee129d97b99f9fac66aabb89fc1652930b3bb8b2b3fbba7b57e994f85", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z030_t001_NucleiLabels.tiff": "c54c8b98a5c38a47e0cf9391a1afab09b4c8c58e06294cdb650a15add4a32b58", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z040_t001_NucleiLabels.tiff": "e0bd66b166e9f92120dbee183973849eda0afbbe6fed9d03fc4cfd2c56a96280", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z012_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z035_t001_NucleiLabels.tiff": "13e69afc324152bb91329fa758d131d849127e8fd346078d94c6adc25582b3a6", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z026_t001_CellsLabels.tiff": "5b433d931fa6637fc793b23880199c01000ef6d9e610903bb38bb8cea5574563", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z053_t001_CellsLabels.tiff": "ff17f538baaf179752412026a250bdbee016e3101037bf4e0dbb81199fc4c8ed", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z033_t001_NucleiLabels.tiff": "512a3a0ed8025d14b0556a72ebfc13b173d14f7046817ca09876242ab63b9d6b", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z041_t001_CellsLabels.tiff": "be7762756eb65f08cb337904165c6f43b5351f39405461f65ad28669abfe3899", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z050_t001_CellsLabels.tiff": "ea7654a6fe1f92ad274a54935420923eec4bec5ad030187afc0ac7af02690af5", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z034_t001_NucleiLabels.tiff": "08af61857cfb97fd95fcb08d1ce18139ce7087b192044c63452bee533c5b1aa4", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z026_t001_NucleiLabels.tiff": "ad22809ff3f43d15c838f697061c263070dbf1da1031da39900681cdfbf9d54c", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z016_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z019_t001_CellsLabels.tiff": "ec6ef1d319f2732c47b06f5cf02775c8cd94540f2f30a6c002f0aace1d317955", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z057_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z038_t001_CellsLabels.tiff": "6ac8fd9c95acc1f261e96a8fb9332e5d6a403548842f9b85ecb977e6213b4c9b", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z051_t001_NucleiLabels.tiff": "f0f36ccaf744d42da036948682cfd226e6dd4a3cfb94aed38729b2d25ede5dff", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z022_t001_NucleiLabels.tiff": "50aef20e28a2f9e18ca2e28eabf6d87ec2ad20c79898e56f8d63a959ba5bb507", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z005_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z021_t001_NucleiLabels.tiff": "71a10b0539f3fc4cba600b275b7e948258c6a435ba33ef522faa0869ac085518", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z042_t001_CellsLabels.tiff": "88d2e9964455459499656c4f0a762cd86376c678756635aaa6b6c8d98be01852", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/openhcs_metadata.json.lock": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z059_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z014_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_Cells.csv": "6f6e7431171ed5ea73a01784105147eca0e43a6e3e41f8720df93106353c9c79", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z048_t001_NucleiLabels.tiff": "325478230529e94ba372f38f0bf512292f131d337b6ac09ce5a91b0266724e02", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z013_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z016_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z020_t001_NucleiLabels.tiff": "b0cd031b448d8bd2075e65710bd76c064161b25242f85ee150da7cd139fcc1a8", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z043_t001_NucleiLabels.tiff": "81a705d05b0a5bfa8783df28990c1d70634c1f4aa2bca23e02cfa8a4bc861b86", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z002_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z010_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z027_t001_CellsLabels.tiff": "ee9056587a19e581f5feb6078b41fa1004d923a5ddf1725aefc4b8b8c5201ad6", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z049_t001_CellsLabels.tiff": "28d83dfbf5bbf7e4da9c3e4806353e9fd0a89680dcdab4b43ca2fd19b80f669c", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z050_t001_NucleiLabels.tiff": "565b4f08f7503086c1f2f8e631d19063129e02a5767d2f979c0cec8fdec23601", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z047_t001_NucleiLabels.tiff": "995d5ce77cbf61a9be8590f34987ef3f1dd698ae6ed393cac4d0960bc9e7a66a", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z010_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z034_t001_CellsLabels.tiff": "3f6c1d8cdbce20aac8f70e7315bea70b147b7b6a034432845b27eeb6cfba083c", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z060_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z028_t001_NucleiLabels.tiff": "1676f21bd3b7e105601c83f206890507c32ba2447988be2a75d0e8153d069b65", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z029_t001_NucleiLabels.tiff": "34bce0f06f674e20667554eed0075fde8d9d33c9c0eaeffd88a1c558499cc432", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_downsizedNuclei.csv": "a7b998c5a524e11747fc5452fd763fc3d9abe16f46e961f62e09e9393b6a8adf", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z011_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z015_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z021_t001_CellsLabels.tiff": "daa31cb1e537e5c32b1936ab7647c5690741d2bc1d0e15c60b59ba1310a8c0ce", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z060_t001_NucleiLabels.tiff": "7cc3c6c6a22f558e62194277090bdea2deb79475dd91fe7d0e49eac3c0187388", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z041_t001_NucleiLabels.tiff": "744eab7cd170d5af8d2f72ced3893dd939c4e1b0f415afab799a37ded5d046c7", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z012_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z039_t001_CellsLabels.tiff": "22fa79a76e64013f10373be2ca2af98a2173275c5d43c9cad33135acdc376d14", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z036_t001_NucleiLabels.tiff": "4fa967c78709aa400246a50528a36b24ca366e825dd0e64561643369e814906b", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z025_t001_NucleiLabels.tiff": "9cc33676a0177130609ab1331feab1722bbde8d55b2f49031730fd5a2a40029c", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/openhcs_metadata.json": "43ddc7fb22e14f8b2478e1c057a767169c6ffa396b4347fedea756a539e4500e", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z017_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z028_t001_CellsLabels.tiff": "fef31040e0c54501c3bf965eb98f055cdb1d42c96f91cf1c239033d0a9a06534", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z025_t001_CellsLabels.tiff": "03aabe5528198cfd6ec23a058dab540fb8be0870207c6dc94a62d9b621d1f2f9", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z022_t001_CellsLabels.tiff": "7d7ec170ed3652efa99e43d8db546c51b696821dca7727d0ff4d2f2ead7fdbaa", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z056_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z039_t001_NucleiLabels.tiff": "f93e8293a37886a36844b8748083a1f8e717efab092652755263e797f12f0364", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z007_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z003_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z018_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z042_t001_NucleiLabels.tiff": "e07252ca1a55c5195c99666d8d3f0d70db636b4ef4b99e1a0049ca22b182d87a", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z006_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z003_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/results/3d_monolayer_Image.csv": "75ac32ff5ef57bc285ec4eae1c9b501c27a35b486bf35435af5d499d856d34a4", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z011_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z006_t001_NucleiLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z024_t001_NucleiLabels.tiff": "9e96a05ed9860cac9c57c618db2f4fe5e4b769a9d235aac6fd1d628486317df0", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z023_t001_CellsLabels.tiff": "2f06989618050934c73473f9342c2fe3410fd834a0ddb976cf602fe6198db7a6", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z017_t001_CellsLabels.tiff": "abfe34a68285c9292a8b8ffce8d8ab1a3cf4fe8d09258071ed888461a454000f", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z059_t001_NucleiLabels.tiff": "76c785cf0f481c73ad3cf34ccb576eaa19dd1bdd9c74f9eaf92adfbde5d44b97", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w2_z049_t001_NucleiLabels.tiff": "e92e493078672a0e0d90e31409612e215bf8486300d5c765d0e0504ff8888e7a", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/cp_tutorial_3d_monolayer/wells_1/workers_1/images_3d_monolayer_final_source_workspace_well_throughput/images/W001_s001_w0_z035_t001_CellsLabels.tiff": "eedeb489b46abf1eafbd9c81491cb3f85ce1cb3ce877844ad22e72547fc3c438" + } + }, + { + "case": "ExampleSpeckles", + "ordinary_repeat": 1, + "native_repeat": 0, + "measurement_differences": 0, + "image_differences": 0, + "tables": [ + 3, + 3 + ], + "physical_images": [ + 0, + 0 + ], + "logical_images": [ + 0, + 0 + ], + "known_relationship_keys": [ + 1, + 1 + ], + "ordinary_receipt_sha256": "70cfd59c5b7e48d708823be2449bc457cfc86f9a1de56835bbc1b068e6d8c4cf", + "output_hashes": { + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/ExampleSpeckles/outputs/0/Nuclei.csv": "15af7cac08d651c19b3e3f409b2b0d7fc4f96011c9b53394aa9a35969a8936a6", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/ExampleSpeckles/outputs/0/h2ax.csv": "0f54fc72ccdeb7e1a68bab9dc3a8d9d49788cdf81dbbc109ed119e589c09538a", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/ExampleSpeckles/outputs/0/Image.csv": "cebe6c0c9ab138ff1f65906b70a130b9e79e3b7d873cc006e88067c596b07a98", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/ExampleSpeckles/wells_1/workers_1/images_ExampleSpeckles_source_workspace_well_throughput/results/Image.csv": "85ef16f24e75f03dc7055215cf2e6cb196b1dc0a0d73713a4c5999f698d4f96e", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/ExampleSpeckles/wells_1/workers_1/images_ExampleSpeckles_source_workspace_well_throughput/openhcs_metadata.json.lock": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/ExampleSpeckles/wells_1/workers_1/images_ExampleSpeckles_source_workspace_well_throughput/openhcs_metadata.json": "71582c62cabb2571ebf8a0de57f92fdf82c55a7ece3dac0bf2a6eca95cad84c9", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/ExampleSpeckles/wells_1/workers_1/images_ExampleSpeckles_source_workspace_well_throughput/results/Nuclei.csv": "e50cb9f8c23f65e9ce8db25132263cf4a4448ed65fc1088d782536abf74c5670", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/ExampleSpeckles/wells_1/workers_1/images_ExampleSpeckles_source_workspace_well_throughput/results/h2ax.csv": "eb453565f3b79ac932eeb5cde8605888b72e58c80f42edbbf0b1ba37f34fd6f8" + } + }, + { + "case": "ExampleSpeckles", + "ordinary_repeat": 1, + "native_repeat": 1, + "measurement_differences": 0, + "image_differences": 0, + "tables": [ + 3, + 3 + ], + "physical_images": [ + 0, + 0 + ], + "logical_images": [ + 0, + 0 + ], + "known_relationship_keys": [ + 1, + 1 + ], + "ordinary_receipt_sha256": "70cfd59c5b7e48d708823be2449bc457cfc86f9a1de56835bbc1b068e6d8c4cf", + "output_hashes": { + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/ExampleSpeckles/outputs/1/Nuclei.csv": "15af7cac08d651c19b3e3f409b2b0d7fc4f96011c9b53394aa9a35969a8936a6", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/ExampleSpeckles/outputs/1/h2ax.csv": "0f54fc72ccdeb7e1a68bab9dc3a8d9d49788cdf81dbbc109ed119e589c09538a", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/native/ExampleSpeckles/outputs/1/Image.csv": "be7bbf5f19c27df330b95c8eb1784e0053a655f84d278d40a9023c1feeb1fbed", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/ExampleSpeckles/wells_1/workers_1/images_ExampleSpeckles_source_workspace_well_throughput/results/Image.csv": "85ef16f24e75f03dc7055215cf2e6cb196b1dc0a0d73713a4c5999f698d4f96e", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/ExampleSpeckles/wells_1/workers_1/images_ExampleSpeckles_source_workspace_well_throughput/openhcs_metadata.json.lock": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/ExampleSpeckles/wells_1/workers_1/images_ExampleSpeckles_source_workspace_well_throughput/openhcs_metadata.json": "71582c62cabb2571ebf8a0de57f92fdf82c55a7ece3dac0bf2a6eca95cad84c9", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/ExampleSpeckles/wells_1/workers_1/images_ExampleSpeckles_source_workspace_well_throughput/results/Nuclei.csv": "e50cb9f8c23f65e9ce8db25132263cf4a4448ed65fc1088d782536abf74c5670", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/ordinary-1/ExampleSpeckles/wells_1/workers_1/images_ExampleSpeckles_source_workspace_well_throughput/results/h2ax.csv": "eb453565f3b79ac932eeb5cde8605888b72e58c80f42edbbf0b1ba37f34fd6f8" + } + } + ], + "source_head": "ee8f97552b349499655bb3f961860b34dc784071", + "scope": "Both ordinary observations versus both native measured observations; full strict files/values/correlation policy; descriptive two-repeat clocks, not full30 acceptance." +} diff --git a/benchmark/results/perf_canonical_output_policy_paired_20261003/timing_summary.json b/benchmark/results/perf_canonical_output_policy_paired_20261003/timing_summary.json new file mode 100644 index 0000000000..f303ba74dc --- /dev/null +++ b/benchmark/results/perf_canonical_output_policy_paired_20261003/timing_summary.json @@ -0,0 +1,73 @@ +{ + "source_head": "ee8f97552b349499655bb3f961860b34dc784071", + "cases": { + "ExampleSpeckles": { + "native_invocation_seconds": [ + 2.0799103030003607, + 2.0815624929964542 + ], + "native_mean_seconds": 2.0807363979984075, + "ordinary_samples": { + "compile_seconds": [ + 0.49239587783813477, + 0.5033345222473145 + ], + "execute_seconds": [ + 1.2073335647583008, + 1.3140697479248047 + ], + "total_seconds": [ + 2.052078391890973, + 2.1295654121786356 + ] + }, + "ordinary_means": { + "compile_seconds": 0.4978652000427246, + "execute_seconds": 1.2607016563415527, + "total_seconds": 2.0908219020348042 + }, + "other_total_seconds_mean": 0.3322550456505269, + "execution_speedup_ratio_of_means": 1.6504590023594676, + "total_speedup_ratio_of_means": 0.9951762969258254 + }, + "cp_tutorial_3d_monolayer": { + "native_invocation_seconds": [ + 14.412360650952905, + 14.22139574913308 + ], + "native_mean_seconds": 14.316878200042993, + "ordinary_samples": { + "compile_seconds": [ + 1.1534233093261719, + 1.1986286640167236 + ], + "execute_seconds": [ + 8.562227010726929, + 9.425255298614502 + ], + "total_seconds": [ + 10.547500064130872, + 11.399972424842417 + ] + }, + "ordinary_means": { + "compile_seconds": 1.1760259866714478, + "execute_seconds": 8.993741154670715, + "total_seconds": 10.973736244486645 + }, + "other_total_seconds_mean": 0.8039691031444818, + "execution_speedup_ratio_of_means": 1.59187127512646, + "total_speedup_ratio_of_means": 1.304649381129056 + } + }, + "scope": "Two current cases only; means of both observations, not a causal before/after comparison, confidence interval, or full30/scaling result. Server READY startup/shutdown and native imports/JVM/load/full warmup are excluded. Ordinary total includes compilation and default OUTCOMES/RSS completion.", + "science_sha256": "f63245b5fceca1e1f1a1807294b937f8fe9df483cded7491a2dc7baba3c98318", + "plot_script_sha256": "3503b70048d05ff6867397e1921265d5bd17e145956a6bc27787ed69d504b408", + "artifacts": { + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/figures-v2/metrics.csv": "8569f5075c4f5088e5d73694e0d99b2ca751d7d2d6383326eeeffa660206c2bd", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/figures-v2/matched_runtime.png": "3a9878a285b1472b9523607c2e6eaeeb9ff1c020c3a2c717aac1ca6504603c00", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/figures-v2/matched_runtime.svg": "0c4e958aa88fa545563b9ab13b78e5fcb56a29aadf079223b2b54da66b3b175a", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/figures-v2/matched_speedup.png": "3aeb724b0ef9a51c1d18135f86064edc7e76ccd79d63bcab23737c871377179d", + "/home/ts/.local/state/openhcs-maintenance/20261003/canonical-output-policy-paired-v1/figures-v2/matched_speedup.svg": "57cf838d868692a3b0c2a8bc95bb3f1e55b98ed49d41dcd7380a32f026eed467" + } +} From af175e1a9eff058f72de1189b88ec7e4f83e5766 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 09:07:37 -0400 Subject: [PATCH 128/235] fix(callables): preserve declared context injection opt-outs The existing captured runtime_context_parameter owns injection, including an explicit None. Preserve that selection through namespace and FunctionReference reconstruction, retain inference only for absent live declarations, and declare unused SaveImages context accordingly. The measurement variant retains required context independently of its shared signature target. Required consumer-closure dependency of the whole-image transport work in PR #394; no standalone performance claim. Validation: 106 related and focused tests, including reference/pickle/runtime binding and multi-axis filename controls. --- openhcs/core/callable_contract.py | 17 +- openhcs/core/pipeline/function_contracts.py | 18 ++ .../backends/cellprofiler/save_images.py | 3 + .../unit/test_runtime_context_declaration.py | 194 ++++++++++++++++++ 4 files changed, 222 insertions(+), 10 deletions(-) create mode 100644 tests/unit/test_runtime_context_declaration.py diff --git a/openhcs/core/callable_contract.py b/openhcs/core/callable_contract.py index 45a5965412..d3096e6eb8 100644 --- a/openhcs/core/callable_contract.py +++ b/openhcs/core/callable_contract.py @@ -618,7 +618,7 @@ def from_projection( FunctionContractAttribute.allowed_group_by, ), runtime_adapter=runtime_adapter, - runtime_context_parameter=_runtime_context_parameter(projection, reader, signature), + runtime_context_parameter=_runtime_context_parameter(reader, signature), execution_scope=reader.optional_execution_scope( FunctionContractAttribute.execution_scope, ), @@ -719,10 +719,9 @@ def as_namespace(self) -> dict[str, object]: ) if self.runtime_adapter is not None: namespace[FunctionContractAttribute.runtime_adapter] = self.runtime_adapter - if self.runtime_context_parameter is not None: - namespace[FunctionContractAttribute.runtime_context_parameter] = ( - self.runtime_context_parameter - ) + namespace[FunctionContractAttribute.runtime_context_parameter] = ( + self.runtime_context_parameter + ) namespace[FunctionContractAttribute.execution_scope] = self.execution_scope if self.processing_contract is not None: namespace[FunctionContractAttribute.processing_contract] = ( @@ -2049,14 +2048,12 @@ def prepare_targets(self) -> tuple[Any, ...]: def _runtime_context_parameter( - projection: CallableProjection, reader: "CallableMetadataReader", signature: inspect.Signature | None, ) -> str | None: - declared = reader.optional_string( - FunctionContractAttribute.runtime_context_parameter - ) - if declared is not None: + field_name = FunctionContractAttribute.runtime_context_parameter + declared = reader.optional_string(field_name) + if field_name in reader.namespace: return declared from openhcs.core.context.processing_context import ProcessingContext diff --git a/openhcs/core/pipeline/function_contracts.py b/openhcs/core/pipeline/function_contracts.py index e0d343fc5f..9c9a5e2cd5 100644 --- a/openhcs/core/pipeline/function_contracts.py +++ b/openhcs/core/pipeline/function_contracts.py @@ -44,6 +44,24 @@ F = TypeVar("F", bound=Callable) +def runtime_context_parameter(parameter_name: str | None) -> Callable[[F], F]: + """Declare context injection, or disable it while retaining the public ABI.""" + + if parameter_name is not None and not isinstance(parameter_name, str): + raise TypeError("runtime_context_parameter must be a parameter name or None.") + + def decorator(func: F) -> F: + if parameter_name is not None: + resolved_callable_parameter(func, parameter_name) + namespace = vars(func) + namespace.pop(FunctionContractAttribute.canonical_signature, None) + namespace.pop(FunctionContractAttribute.raw_runtime_signature, None) + namespace[FunctionContractAttribute.runtime_context_parameter] = parameter_name + return func + + return decorator + + def resolved_callable_type_hints(func: Callable) -> dict[str, Any]: """Return the callable's resolved type contract or propagate its error.""" diff --git a/openhcs/processing/backends/cellprofiler/save_images.py b/openhcs/processing/backends/cellprofiler/save_images.py index 796419fcbf..76b21c51c3 100644 --- a/openhcs/processing/backends/cellprofiler/save_images.py +++ b/openhcs/processing/backends/cellprofiler/save_images.py @@ -30,6 +30,7 @@ ) from openhcs.core.pipeline.function_contracts import ( runtime_bound_parameters, + runtime_context_parameter, special_inputs, ) from openhcs.core.runtime_array_values import RuntimeArrayData @@ -978,6 +979,7 @@ def _recorded_save_images_rows( @numpy(contract=ProcessingContract.PURE_3D) @special_inputs("image_to_save") +@runtime_context_parameter(None) def save_images( image: RuntimeArrayData, *, @@ -1033,6 +1035,7 @@ def save_images( @numpy(contract=ProcessingContract.PURE_3D) @special_inputs("image_to_save") @runtime_bound_parameters(SliceIndexRuntimeParameter) +@runtime_context_parameter("context") def save_images_with_measurements( image: RuntimeArrayData, *, diff --git a/tests/unit/test_runtime_context_declaration.py b/tests/unit/test_runtime_context_declaration.py new file mode 100644 index 0000000000..83adf94769 --- /dev/null +++ b/tests/unit/test_runtime_context_declaration.py @@ -0,0 +1,194 @@ +"""Context injection belongs to the captured callable declaration.""" + +import inspect +import pickle +from types import SimpleNamespace + +import numpy as np +import pytest +from python_introspect import parameter_exclusions + +from openhcs.core.callable_contract import ( + CallableContract, + CallableMetadata, + CallableProjection, + prepare_processing_callable, +) +from openhcs.core.compiled_step_plan import CompiledStepPlan +from openhcs.core.context.processing_context import ProcessingContext +from openhcs.core.function_contract_metadata import FunctionContractAttribute +from openhcs.core.function_patterns import compile_function_pattern +from openhcs.core.function_reference import FunctionReferenceTransportAuthority +from openhcs.core.memory import numpy +from openhcs.core.pipeline.function_contracts import runtime_context_parameter +from openhcs.core.runtime_image_values import ImagePayloadMetadata +from openhcs.core.runtime_plane_projection import RuntimePlaneProjection +from openhcs.core.source_bindings import SourceBindingRuntimeContext +from openhcs.core.steps.function_runtime import ( + ComponentArtifactPlans, + FunctionCoreExecutor, + FunctionRuntimeScope, +) +from openhcs.processing.backends.cellprofiler.save_images import ( + save_images, + save_images_with_measurements, +) +from openhcs.processing.backends.lib_registry.unified_registry import ProcessingContract + + +def _inferred_context(image, *, context=None): + return image, context + + +@runtime_context_parameter(None) +def _without_context(image, *, context=None): + return image, context + + +@runtime_context_parameter("execution_context") +def _named_context(image, *, execution_context=None): + return image, execution_context + + +def test_absent_declaration_retains_signature_inference(): + assert FunctionContractAttribute.runtime_context_parameter not in vars( + _inferred_context + ) + assert CallableContract.from_callable(_inferred_context).runtime_context_parameter == ( + "context" + ) + + +def test_none_declaration_preserves_callable_identity_and_public_signature(): + def process(image, *, context=None): + return image, context + + signature = inspect.signature(process) + assert runtime_context_parameter(None)(process) is process + assert inspect.signature(process) == signature + assert signature.parameters["context"].default is None + assert CallableContract.from_callable(process).runtime_context_parameter is None + marker = object() + assert process(marker) == (marker, None) + assert process(marker, context=marker) == (marker, marker) + + +@pytest.mark.parametrize( + ("func", "expected"), + [(_inferred_context, "context"), (_without_context, None), + (_named_context, "execution_context")], +) +def test_namespace_reconstruction_retains_captured_selection(func, expected): + metadata = CallableMetadata.from_callable(func) + namespace = metadata.as_namespace() + assert namespace[FunctionContractAttribute.runtime_context_parameter] == expected + projection = CallableProjection( + func=func, name=func.__name__, module_name=func.__module__, namespace=namespace + ) + assert CallableMetadata.from_projection(projection).runtime_context_parameter == ( + expected + ) + + +@pytest.mark.parametrize("func", [_inferred_context, _without_context, _named_context]) +def test_reference_and_pickle_reconstruction_retain_selection(func): + expected = CallableContract.from_callable(func).runtime_context_parameter + reference = FunctionReferenceTransportAuthority.function_reference(func) + restored_reference = pickle.loads(pickle.dumps(reference)) + assert restored_reference.resolve() is func + assert CallableContract.from_callable(restored_reference).runtime_context_parameter == ( + expected + ) + contract = CallableContract.from_callable(restored_reference) + assert pickle.loads(pickle.dumps(contract)).runtime_context_parameter == expected + + +def test_wrapper_preparation_does_not_resurrect_context_inference(): + @numpy(contract=ProcessingContract.PURE_2D) + @runtime_context_parameter(None) + def process(image, *, context=None): + del context + return image + + prepare_processing_callable(process) + contract = CallableContract.from_callable(process).with_prepared_signature() + assert contract.runtime_context_parameter is None + assert "context" not in contract.runtime_owned_parameter_names + assert "context" not in parameter_exclusions(process) + assert inspect.signature(contract.resolve_canonical_raw_callable()).parameters[ + "context" + ].default is None + + +@pytest.mark.parametrize( + ("func", "parameter"), + [(_inferred_context, "context"), (_without_context, None), + (_named_context, "execution_context")], +) +def test_runtime_binding_consumes_captured_selection(func, parameter): + pattern = compile_function_pattern(func, {}, {}) + context = ProcessingContext(axis_id="A01") + artifacts = ComponentArtifactPlans(inputs={}, outputs={}) + scope = FunctionRuntimeScope( + context=context, + execution_plan=CompiledStepPlan( + step_index=0, step_name="Context", step_type="FunctionStep", axis_id="A01" + ), + compiled_group=pattern.default_group, + artifacts=artifacts, + source_binding_context=SourceBindingRuntimeContext.empty(), + runtime_plane_index=0, + runtime_plane_count=1, + ) + executor = FunctionCoreExecutor( + runtime_scope=scope, + invocation=next(pattern.iter_invocations()), + artifacts=artifacts, + group_key=None, + plane_projection=RuntimePlaneProjection(), + main_data_arg=np.zeros((2, 3)), + source_memory_type="numpy", + ) + kwargs = {} + executor.bind_runtime_owned_parameters(kwargs) + assert kwargs == ({} if parameter is None else {parameter: context}) + + +def test_save_images_variants_keep_independent_injection_declarations(): + plain = CallableContract.from_callable(save_images) + measurements = CallableContract.from_callable(save_images_with_measurements) + assert plain.runtime_context_parameter is None + assert measurements.runtime_context_parameter == "context" + for func in (save_images, save_images_with_measurements): + reference = FunctionReferenceTransportAuthority.function_reference(func) + expected = CallableContract.from_callable(func).runtime_context_parameter + assert CallableContract.from_callable(reference).runtime_context_parameter == expected + + +def test_measurement_variant_keeps_real_axis_scoped_filename_context(): + context = ProcessingContext(axis_id="A01") + context.execution_runtime = SimpleNamespace(execution_axis_values=("A01", "A02")) + payload = ImagePayloadMetadata(source_path="/input/DNA.tif").payload_with( + np.ones((2, 3), dtype=np.uint16) + ) + contract = CallableContract.from_callable(save_images_with_measurements) + primary, _saved, rows = contract.resolve_canonical_raw_callable()( + payload, image_to_save=payload, saved_image_name="Saved", context=context + ) + assert primary is payload + values = {row.feature_name: row.result_value for row in rows.rows} + assert values["PathName_Saved"] == "A01" + assert values["FileName_Saved"] == "DNA.tiff" + assert values["URL_Saved"] == "file:A01/DNA.tiff" + + +def test_declaration_rejects_unknown_parameter_without_mutating_callable(): + before = dict(vars(_inferred_context)) + with pytest.raises(ValueError, match="does not declare parameter"): + runtime_context_parameter("missing")(_inferred_context) + assert vars(_inferred_context) == before + + +def test_declaration_rejects_non_string_parameter(): + with pytest.raises(TypeError, match="parameter name or None"): + runtime_context_parameter(7) From 0a80eac493e65f4a90dc3c6ed45178907eaa24ee Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 09:21:39 -0400 Subject: [PATCH 129/235] fix(callables): validate context names without resolving annotations Validate only the declared parameter membership through the signature authority. Unrelated forward-reference annotations may be defined later in a module and must not be resolved while applying the declaration. Add a real declaration-order control; all17 focused controls pass on owned and main-based sources. Refs #394. --- openhcs/core/pipeline/function_contracts.py | 10 ++++++++-- .../unit/test_runtime_context_declaration.py | 20 +++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/openhcs/core/pipeline/function_contracts.py b/openhcs/core/pipeline/function_contracts.py index 9c9a5e2cd5..c7cab4da8a 100644 --- a/openhcs/core/pipeline/function_contracts.py +++ b/openhcs/core/pipeline/function_contracts.py @@ -51,8 +51,14 @@ def runtime_context_parameter(parameter_name: str | None) -> Callable[[F], F]: raise TypeError("runtime_context_parameter must be a parameter name or None.") def decorator(func: F) -> F: - if parameter_name is not None: - resolved_callable_parameter(func, parameter_name) + if ( + parameter_name is not None + and parameter_name not in inspect.signature(func).parameters + ): + raise ValueError( + f"Callable {func.__name__!r} does not declare parameter " + f"{parameter_name!r}." + ) namespace = vars(func) namespace.pop(FunctionContractAttribute.canonical_signature, None) namespace.pop(FunctionContractAttribute.raw_runtime_signature, None) diff --git a/tests/unit/test_runtime_context_declaration.py b/tests/unit/test_runtime_context_declaration.py index 83adf94769..25da4722a8 100644 --- a/tests/unit/test_runtime_context_declaration.py +++ b/tests/unit/test_runtime_context_declaration.py @@ -182,6 +182,26 @@ def test_measurement_variant_keeps_real_axis_scoped_filename_context(): assert values["URL_Saved"] == "file:A01/DNA.tiff" +def test_named_context_declaration_does_not_resolve_unrelated_forward_references(): + namespace = {"runtime_context_parameter": runtime_context_parameter} + exec( + "@runtime_context_parameter('context')\n" + "def process(image: 'DefinedAfterDeclaration', *, context=None):\n" + " return image, context\n" + "class DefinedAfterDeclaration:\n" + " pass\n", + namespace, + ) + process = namespace["process"] + assert inspect.signature(process).parameters["image"].annotation == ( + "DefinedAfterDeclaration" + ) + assert vars(process)[FunctionContractAttribute.runtime_context_parameter] == "context" + assert CallableMetadata.from_callable(process).runtime_context_parameter == "context" + marker = namespace["DefinedAfterDeclaration"]() + assert process(marker, context=marker) == (marker, marker) + + def test_declaration_rejects_unknown_parameter_without_mutating_callable(): before = dict(vars(_inferred_context)) with pytest.raises(ValueError, match="does not declare parameter"): From f61f6c6ba0529cc0b0598ec6d17fa952846405f5 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 09:56:34 -0400 Subject: [PATCH 130/235] refactor(runtime): bind CellProfiler artifacts without a parallel request carrier --- .../cellprofiler/runtime/artifact_binding.py | 123 +++++++----------- .../cellprofiler/runtime/module_execution.py | 12 +- .../runtime/output_record_request.py | 16 +-- ...t_cellprofiler_measurement_source_names.py | 7 +- .../test_cellprofiler_module_execution.py | 59 ++++----- .../test_gray_to_color_binding_axis_432.py | 8 +- .../test_runtime_adapter_source_artifacts.py | 6 +- ...t_runtime_artifact_input_value_contract.py | 57 ++++++++ tests/unit/test_runtime_value_store.py | 2 +- .../test_saved_source_label_plane_domain.py | 6 +- 10 files changed, 155 insertions(+), 141 deletions(-) create mode 100644 tests/unit/test_runtime_artifact_input_value_contract.py diff --git a/openhcs/interop/cellprofiler/runtime/artifact_binding.py b/openhcs/interop/cellprofiler/runtime/artifact_binding.py index 55a279e1b8..2408ce01ee 100644 --- a/openhcs/interop/cellprofiler/runtime/artifact_binding.py +++ b/openhcs/interop/cellprofiler/runtime/artifact_binding.py @@ -27,7 +27,6 @@ AlignedImageStack, ImageOutputBundle, ) -from openhcs.core.callable_contract import ImagePayloadConsumption from openhcs.core.function_patterns import ( InvocationArtifactInputEdgePlan, MainFlowInputProjection, @@ -100,28 +99,6 @@ ] -@dataclass(frozen=True, slots=True, kw_only=True) -class RuntimeArtifactInputRequest: - """One resolved artifact value dispatched through its nominal type strategy.""" - - spec: ArtifactSpec - value: RuntimeCallableArgument - image_payload_consumption: ImagePayloadConsumption = ImagePayloadConsumption.NATURAL - - def __post_init__(self) -> None: - if not isinstance(self.spec, ArtifactSpec): - raise TypeError( - "RuntimeArtifactInputRequest.spec must be ArtifactSpec, got " - f"{type(self.spec).__name__}." - ) - if not isinstance(self.image_payload_consumption, ImagePayloadConsumption): - raise TypeError( - "RuntimeArtifactInputRequest.image_payload_consumption must be " - "ImagePayloadConsumption, got " - f"{type(self.image_payload_consumption).__name__}." - ) - - class RuntimeArtifactTypeStrategy( ArtifactTypeStrategyMatchMixin, MostDerivedContextStrategyMixin[type[ArtifactType]], @@ -161,24 +138,25 @@ def for_main_flow_outputs( return cls.for_artifact_type(artifact_type) def runtime_input_value( - self, request: RuntimeArtifactInputRequest + self, spec: ArtifactSpec, value: RuntimeCallableArgument ) -> RuntimeCallableArgument: """Return the runtime payload bound into absorbed function kwargs.""" - return request.value + return value def raw_runtime_input_value( - self, request: RuntimeArtifactInputRequest + self, spec: ArtifactSpec, value: RuntimeCallableArgument ) -> RuntimeCallableArgument: """Return the runtime payload before CellProfiler intensity coercion.""" - return self.runtime_input_value(request) + return self.runtime_input_value(spec, value) def source_image_name( self, - request: RuntimeArtifactInputRequest, + spec: ArtifactSpec, + value: RuntimeCallableArgument, ) -> str | None: """Return the transitive source image name for one artifact input.""" - del request + del spec, value return None def source_image_name_from_value( @@ -191,7 +169,8 @@ def source_image_name_from_value( def source_image_payload( self, - request: RuntimeArtifactInputRequest, + spec: ArtifactSpec, + value: RuntimeCallableArgument, ) -> RuntimeCallableArgument | None: """Return an image payload that carries this artifact's source paths.""" return None @@ -237,13 +216,13 @@ class ImageArtifactTypeStrategy(RuntimeArtifactTypeStrategy): artifact_type = ImageArtifactType def raw_runtime_input_value( - self, request: RuntimeArtifactInputRequest + self, spec: ArtifactSpec, value: RuntimeCallableArgument ) -> RuntimeCallableArgument: - payload = request.value + payload = value metadata = image_payload_metadata(payload) metadata = metadata.with_source_provenance( metadata.source_provenance.with_derived_source_image_names( - (request.spec.name,) + (spec.name,) ) ) return metadata.payload_with( @@ -252,17 +231,18 @@ def raw_runtime_input_value( ) def runtime_input_value( - self, request: RuntimeArtifactInputRequest + self, spec: ArtifactSpec, value: RuntimeCallableArgument ) -> RuntimeCallableArgument: return normalize_cellprofiler_image_payload( - self.raw_runtime_input_value(request) + self.raw_runtime_input_value(spec, value) ) def source_image_name( self, - request: RuntimeArtifactInputRequest, + spec: ArtifactSpec, + value: RuntimeCallableArgument, ) -> str | None: - return self.source_image_name_from_value(self.raw_runtime_input_value(request)) + return self.source_image_name_from_value(self.raw_runtime_input_value(spec, value)) def source_image_name_from_value( self, @@ -274,9 +254,10 @@ def source_image_name_from_value( def source_image_payload( self, - request: RuntimeArtifactInputRequest, + spec: ArtifactSpec, + value: RuntimeCallableArgument, ) -> RuntimeCallableArgument | None: - return self.raw_runtime_input_value(request) + return self.raw_runtime_input_value(spec, value) def published_main_flow_output( self, @@ -311,11 +292,11 @@ class ObjectLabelsArtifactTypeStrategy(RuntimeArtifactTypeStrategy): def object_labels( self, - request: RuntimeArtifactInputRequest, + spec: ArtifactSpec, + value: RuntimeCallableArgument, ) -> ObjectLabelSet: """Return the native object value carrying its source-image provenance.""" - value = request.value if isinstance(value, ObjectLabelSet): return value metadata = image_payload_metadata(value) @@ -326,28 +307,30 @@ def object_labels( metadata.plane_axis, metadata.source_provenance, ), ).label_set( - name=request.spec.name, - source_image_name=request.spec.name, + name=spec.name, + source_image_name=spec.name, ) def runtime_input_value( - self, request: RuntimeArtifactInputRequest + self, spec: ArtifactSpec, value: RuntimeCallableArgument ) -> RuntimeCallableArgument: - return self.object_labels(request) + return self.object_labels(spec, value) def raw_runtime_input_value( self, - request: RuntimeArtifactInputRequest, + spec: ArtifactSpec, + value: RuntimeCallableArgument, ) -> RuntimeCallableArgument: """Return the nominal label set in the invocation's component scope.""" - return self.object_labels(request) + return self.object_labels(spec, value) def source_image_name( self, - request: RuntimeArtifactInputRequest, + spec: ArtifactSpec, + value: RuntimeCallableArgument, ) -> str | None: - return self.source_image_name_from_value(self.object_labels(request)) + return self.source_image_name_from_value(self.object_labels(spec, value)) def source_image_name_from_value( self, @@ -357,9 +340,10 @@ def source_image_name_from_value( def source_image_payload( self, - request: RuntimeArtifactInputRequest, + spec: ArtifactSpec, + value: RuntimeCallableArgument, ) -> RuntimeCallableArgument | None: - return self.object_labels(request) + return self.object_labels(spec, value) class MeasurementsArtifactTypeStrategy(RuntimeArtifactTypeStrategy): @@ -368,24 +352,23 @@ class MeasurementsArtifactTypeStrategy(RuntimeArtifactTypeStrategy): artifact_type = MeasurementsArtifactType def runtime_input_value( - self, request: RuntimeArtifactInputRequest + self, spec: ArtifactSpec, value: RuntimeCallableArgument ) -> RuntimeCallableArgument: - value = request.value if not isinstance(value, MeasurementTable): raise TypeError( - f"Measurement artifact {request.spec.name!r} requires a " + f"Measurement artifact {spec.name!r} requires a " f"MeasurementTable, got {type(value).__name__}." ) return value.rows def source_image_name( self, - request: RuntimeArtifactInputRequest, + spec: ArtifactSpec, + value: RuntimeCallableArgument, ) -> str | None: - value = request.value if not isinstance(value, MeasurementTable): raise TypeError( - f"Measurement artifact {request.spec.name!r} requires a " + f"Measurement artifact {spec.name!r} requires a " f"MeasurementTable, got {type(value).__name__}." ) return value.source_image_name @@ -614,7 +597,7 @@ def stack_broadcast_source_value( source_edge = self.input_edge_for_spec(source_spec) return RuntimeArtifactTypeStrategy.for_artifact_type( ImageArtifactType - ).runtime_input_value(self.artifact_request(source_edge)) + ).runtime_input_value(source_edge.spec, self.artifact_value(source_edge)) current_image = self.current_image if ( @@ -633,10 +616,10 @@ def stack_broadcast_source_value( source_ref.name, ) - def artifact_request( + def artifact_value( self, edge: InvocationArtifactInputEdgePlan, - ) -> RuntimeArtifactInputRequest: + ) -> RuntimeCallableArgument: """Resolve one declaration from exactly one compiled runtime authority.""" spec = edge.spec @@ -698,25 +681,19 @@ def artifact_request( spec, projection=main_flow_projection, ) - return RuntimeArtifactInputRequest( - spec=spec, - value=value, - image_payload_consumption=( - self.adapter.request.require_callable_contract().image_payload_consumption - ), - ) + return value - def artifact_request_for_spec( + def artifact_value_for_spec( self, spec: ArtifactSpec, - ) -> RuntimeArtifactInputRequest: + ) -> RuntimeCallableArgument: """Resolve a declaration whose occurrences share one runtime authority.""" - return self.artifact_request(self.input_edge_for_spec(spec)) + return self.artifact_value(self.input_edge_for_spec(spec)) def label_payload_for(self, spec: ArtifactSpec) -> ObjectLabelValue: return ObjectLabelsArtifactTypeStrategy().object_labels( - self.artifact_request_for_spec(spec) + spec, self.artifact_value_for_spec(spec) ) def current_plane_label_payload( @@ -848,14 +825,14 @@ def runtime_value( spec = edge.spec if spec.artifact_type is ObjectLabelsArtifactType: payload = ObjectLabelsArtifactTypeStrategy().object_labels( - self.artifact_request(edge) + spec, self.artifact_value(edge) ) if parameter_name is not None: return self.project_label_argument(payload) return payload value = RuntimeArtifactTypeStrategy.for_artifact_type( spec.artifact_type - ).runtime_input_value(self.artifact_request(edge)) + ).runtime_input_value(spec, self.artifact_value(edge)) if spec.artifact_type is not ImageArtifactType: return value if not spec.stack_broadcast_sources(): diff --git a/openhcs/interop/cellprofiler/runtime/module_execution.py b/openhcs/interop/cellprofiler/runtime/module_execution.py index 170b24ac37..c59d695092 100644 --- a/openhcs/interop/cellprofiler/runtime/module_execution.py +++ b/openhcs/interop/cellprofiler/runtime/module_execution.py @@ -712,15 +712,15 @@ def _resolved_measurement_image( *, reference_domain: CellProfilerMeasurementImageDomain, ) -> CellProfilerMeasurementImage: - request = RuntimeInputBindingRequest( + value = RuntimeInputBindingRequest( adapter=adapter, kwargs={}, current_image=current_image, - ).artifact_request_for_spec(spec) + ).artifact_value_for_spec(spec) payload = normalize_cellprofiler_image_payload( RuntimeArtifactTypeStrategy.for_artifact_type( ImageArtifactType, - ).raw_runtime_input_value(request) + ).raw_runtime_input_value(spec, value) ) metadata = image_payload_metadata(payload) plane_axis = metadata.plane_axis @@ -1331,8 +1331,8 @@ def _image_request( ) input_binding = replace(input_binding, current_image=current_runtime_payload) for spec in image_inputs: - request = input_binding.artifact_request_for_spec(spec) - payload = image_strategy.runtime_input_value(request) + value = input_binding.artifact_value_for_spec(spec) + payload = image_strategy.runtime_input_value(spec, value) payloads.append(payload) source_names.append( image_strategy.source_image_name_from_value(payload) @@ -1398,7 +1398,7 @@ def _input_source_image_name( continue source_name = RuntimeArtifactTypeStrategy.for_artifact_type( spec.artifact_type - ).source_image_name(input_binding.artifact_request_for_spec(spec)) + ).source_image_name(spec, input_binding.artifact_value_for_spec(spec)) if source_name is not None: source_names.append(source_name) return single_source_name(tuple(source_names)) diff --git a/openhcs/interop/cellprofiler/runtime/output_record_request.py b/openhcs/interop/cellprofiler/runtime/output_record_request.py index 559fda7c41..afcd9e4825 100644 --- a/openhcs/interop/cellprofiler/runtime/output_record_request.py +++ b/openhcs/interop/cellprofiler/runtime/output_record_request.py @@ -32,7 +32,6 @@ CellProfilerMeasurementImage, ) from openhcs.interop.cellprofiler.runtime.artifact_binding import ( - RuntimeArtifactInputRequest, RuntimeArtifactTypeStrategy, RuntimeInputBindingRequest, ) @@ -237,16 +236,11 @@ def artifact_source_payload( payload = RuntimeArtifactTypeStrategy.for_artifact_type( spec.artifact_type ).source_image_payload( - RuntimeArtifactInputRequest( - spec=spec, - value=binding_request.runtime_value( - edge, - parameter_name=spec.parameter_name, - ), - image_payload_consumption=( - self.callable_contract.image_payload_consumption - ), - ) + spec, + binding_request.runtime_value( + edge, + parameter_name=spec.parameter_name, + ), ) if payload is None: raise TypeError( diff --git a/tests/unit/test_cellprofiler_measurement_source_names.py b/tests/unit/test_cellprofiler_measurement_source_names.py index be4807a7c5..fa1fcc1c84 100644 --- a/tests/unit/test_cellprofiler_measurement_source_names.py +++ b/tests/unit/test_cellprofiler_measurement_source_names.py @@ -63,14 +63,15 @@ def test_input_source_name_resolves_only_declared_context_carriers( requested_specs: list[ArtifactSpec] = [] class BindingRequest: - def artifact_request_for_spec(self, spec: ArtifactSpec) -> SimpleNamespace: + def artifact_value_for_spec(self, spec: ArtifactSpec) -> SimpleNamespace: requested_specs.append(spec) return SimpleNamespace(spec=spec) class ArtifactStrategy: @staticmethod - def source_image_name(request: SimpleNamespace) -> str: - return request.spec.name + def source_image_name(spec: ArtifactSpec, value: SimpleNamespace) -> str: + assert value.spec is spec + return spec.name monkeypatch.setattr( module_execution, diff --git a/tests/unit/test_cellprofiler_module_execution.py b/tests/unit/test_cellprofiler_module_execution.py index 43240f69c8..18986b57c9 100644 --- a/tests/unit/test_cellprofiler_module_execution.py +++ b/tests/unit/test_cellprofiler_module_execution.py @@ -203,7 +203,6 @@ ) from openhcs.interop.cellprofiler.runtime.artifact_binding import ( RuntimeInputBindingRequest, - RuntimeArtifactInputRequest, RuntimeArtifactTypeStrategy, ) from openhcs.interop.cellprofiler.runtime.function_contract_execution import ( @@ -1384,10 +1383,7 @@ def test_image_artifact_resolution_uses_declared_artifact_alias( payload = RuntimeArtifactTypeStrategy.for_artifact_type( ImageArtifactType ).raw_runtime_input_value( - RuntimeArtifactInputRequest( - spec=spec, - value=source, - ) + spec=spec, value=source ) metadata = image_payload_metadata(payload) @@ -1396,7 +1392,7 @@ def test_image_artifact_resolution_uses_declared_artifact_alias( assert ( RuntimeArtifactTypeStrategy.for_artifact_type( ImageArtifactType - ).source_image_name(RuntimeArtifactInputRequest(spec=spec, value=source)) + ).source_image_name(spec=spec, value=source) is None ) strategy = RuntimeArtifactTypeStrategy.for_artifact_type(ImageArtifactType) @@ -1448,11 +1444,11 @@ def test_main_flow_image_artifact_selects_declared_alias_plane() -> None: payload = RuntimeArtifactTypeStrategy.for_artifact_type( ImageArtifactType ).raw_runtime_input_value( - RuntimeInputBindingRequest( + first_spec, RuntimeInputBindingRequest( adapter=adapter, kwargs={}, current_image=current_image, - ).artifact_request_for_spec(first_spec) + ).artifact_value_for_spec(first_spec) ) np.testing.assert_array_equal(image_payload_data(payload), np.zeros((4, 5))) @@ -1488,7 +1484,7 @@ def test_single_main_flow_image_artifact_selects_declared_source_binding_plane() payload = RuntimeArtifactTypeStrategy.for_artifact_type( ImageArtifactType ).raw_runtime_input_value( - RuntimeInputBindingRequest( + spec, RuntimeInputBindingRequest( adapter=_FakeCellProfilerRuntime( {}, callable_contract=contract, @@ -1501,7 +1497,7 @@ def test_single_main_flow_image_artifact_selects_declared_source_binding_plane() ), kwargs={}, current_image=current_image, - ).artifact_request_for_spec(spec) + ).artifact_value_for_spec(spec) ) np.testing.assert_array_equal(image_payload_data(payload), np.zeros((4, 5))) @@ -1539,7 +1535,7 @@ def test_main_flow_image_artifact_projects_named_provenance_plane() -> None: payload = RuntimeArtifactTypeStrategy.for_artifact_type( ImageArtifactType ).raw_runtime_input_value( - RuntimeInputBindingRequest( + first_spec, RuntimeInputBindingRequest( adapter=_FakeCellProfilerRuntime( {}, callable_contract=contract, @@ -1552,7 +1548,7 @@ def test_main_flow_image_artifact_projects_named_provenance_plane() -> None: ), kwargs={}, current_image=current_image, - ).artifact_request_for_spec(first_spec) + ).artifact_value_for_spec(first_spec) ) np.testing.assert_array_equal(image_payload_data(payload), np.zeros((4, 5))) @@ -1584,7 +1580,7 @@ def test_main_flow_image_artifact_preserves_declared_runtime_slice_stack() -> No payload = RuntimeArtifactTypeStrategy.for_artifact_type( ImageArtifactType ).raw_runtime_input_value( - RuntimeInputBindingRequest( + first_spec, RuntimeInputBindingRequest( adapter=_FakeCellProfilerRuntime( {}, callable_contract=contract, @@ -1597,7 +1593,7 @@ def test_main_flow_image_artifact_preserves_declared_runtime_slice_stack() -> No ), kwargs={}, current_image=current_image, - ).artifact_request_for_spec(first_spec) + ).artifact_value_for_spec(first_spec) ) np.testing.assert_array_equal( @@ -1667,9 +1663,9 @@ def test_main_flow_projection_binds_singleton_broadcast_artifact_as_2d( resolve_image = strategy_type.raw_runtime_input_value resolved_specs: list[ArtifactSpec] = [] - def record_resolution(self, request: RuntimeArtifactInputRequest): - resolved_specs.append(request.spec) - return resolve_image(self, request) + def record_resolution(self, spec: ArtifactSpec, value): + resolved_specs.append(spec) + return resolve_image(self, spec, value) monkeypatch.setattr(strategy_type, "raw_runtime_input_value", record_resolution) @@ -1852,7 +1848,7 @@ def test_single_main_flow_image_preserves_runtime_slice_axis() -> None: payload = RuntimeArtifactTypeStrategy.for_artifact_type( ImageArtifactType ).raw_runtime_input_value( - RuntimeInputBindingRequest( + spec, RuntimeInputBindingRequest( adapter=_FakeCellProfilerRuntime( {}, callable_contract=contract, @@ -1865,7 +1861,7 @@ def test_single_main_flow_image_preserves_runtime_slice_axis() -> None: ), kwargs={}, current_image=current_image, - ).artifact_request_for_spec(spec) + ).artifact_value_for_spec(spec) ) assert image_payload_data(payload).shape == (1, 4, 5) @@ -1885,14 +1881,9 @@ def test_object_artifact_source_payload_uses_native_object_provenance() -> None: paths=("/input/A01_s001_w1.tif",), ), ) - request = RuntimeArtifactInputRequest( - spec=object_spec, - value=objects, - ) - payload = RuntimeArtifactTypeStrategy.for_artifact_type( ObjectLabelsArtifactType - ).source_image_payload(request) + ).source_image_payload(object_spec, objects) assert payload is objects assert image_payload_metadata(payload).source_image_provenance_planes.paths == ( @@ -12520,7 +12511,7 @@ def reject_ref_reconstruction(*_args: object, **_kwargs: object) -> None: monkeypatch.setattr( RuntimeInputBindingRequest, - "artifact_request_for_spec", + "artifact_value_for_spec", reject_ref_reconstruction, ) parent_labels = np.zeros((2, 5, 5), dtype=np.int32) @@ -16366,11 +16357,11 @@ def test_object_label_payload_preserves_source_metadata_for_measurements() -> No payload = RuntimeArtifactTypeStrategy.for_artifact_type( spec.artifact_type ).raw_runtime_input_value( - RuntimeInputBindingRequest( + spec, RuntimeInputBindingRequest( adapter=runtime, kwargs={}, current_image=image, - ).artifact_request_for_spec(spec) + ).artifact_value_for_spec(spec) ) np.testing.assert_array_equal(payload.labels, labels.labels) @@ -16424,11 +16415,11 @@ def test_runtime_object_label_payload_ignores_measurement_image_as_selector() -> payload = RuntimeArtifactTypeStrategy.for_artifact_type( spec.artifact_type ).raw_runtime_input_value( - RuntimeInputBindingRequest( + spec, RuntimeInputBindingRequest( adapter=runtime, kwargs={}, current_image=measurement_image, - ).artifact_request_for_spec(spec) + ).artifact_value_for_spec(spec) ) np.testing.assert_array_equal(payload.labels, labels.labels) @@ -16475,11 +16466,11 @@ def test_full_stack_object_measurement_resolves_complete_label_artifact() -> Non label_payload = RuntimeArtifactTypeStrategy.for_artifact_type( object_spec.artifact_type ).raw_runtime_input_value( - RuntimeInputBindingRequest( + object_spec, RuntimeInputBindingRequest( adapter=runtime, kwargs={}, current_image=image, - ).artifact_request_for_spec(object_spec) + ).artifact_value_for_spec(object_spec) ) _, executable_labels, _, _, _, _, _ = object_measurement_runtime_inputs( object_label_execution=object_label_input_execution_mode_from_callable( @@ -16555,11 +16546,11 @@ def test_object_label_payload_for_measurement_image_projects_source_spatial_crop label_payload = RuntimeArtifactTypeStrategy.for_artifact_type( spec.artifact_type ).raw_runtime_input_value( - RuntimeInputBindingRequest( + spec, RuntimeInputBindingRequest( adapter=runtime, kwargs={}, current_image=image, - ).artifact_request_for_spec(spec) + ).artifact_value_for_spec(spec) ) payload = ( CellProfilerMeasurementImage( 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 bcb3525b78..0fc44086b0 100644 --- a/tests/unit/test_gray_to_color_binding_axis_432.py +++ b/tests/unit/test_gray_to_color_binding_axis_432.py @@ -29,7 +29,7 @@ CellProfilerFunctionContractExecutor, ) from openhcs.interop.cellprofiler.runtime.module_execution import CellProfilerModuleExecutor -from openhcs.interop.cellprofiler.runtime.artifact_binding import RuntimeArtifactInputRequest, RuntimeArtifactTypeStrategy +from openhcs.interop.cellprofiler.runtime.artifact_binding import RuntimeArtifactTypeStrategy from openhcs.processing.backends.cellprofiler.color import GrayToColorModule, gray_to_color from test_cellprofiler_generic_special_input_binding import _compile_public_step from tests.unit.cellprofiler_runtime_test_support import cellprofiler_runtime_adapter_for_test @@ -192,11 +192,9 @@ def test_stack_color_output_is_not_a_second_scalar_gray_binding(): def test_integer_primary_binding_units_are_owned_before_stack_rescale_flag(): pixels = np.arange(20, dtype=np.uint16).reshape(4, 5) * 1000 source = _source_plane(pixels, "FITC") - request = RuntimeArtifactInputRequest( - spec=ArtifactSpec.input("FITC", ImageArtifactType), value=source, - ) + spec = ArtifactSpec.input("FITC", ImageArtifactType) binding_owner = RuntimeArtifactTypeStrategy.for_artifact_type(ImageArtifactType) - bound = binding_owner.runtime_input_value(request) + bound = binding_owner.runtime_input_value(spec, source) # This conversion happens in the original input owner, before GrayToColor's # rescale_intensity argument. Do not claim a raw-unit-preserving four-step fix # from a passing floating-point Stack kernel test. diff --git a/tests/unit/test_runtime_adapter_source_artifacts.py b/tests/unit/test_runtime_adapter_source_artifacts.py index dd4e4357fe..c2bea0bcff 100644 --- a/tests/unit/test_runtime_adapter_source_artifacts.py +++ b/tests/unit/test_runtime_adapter_source_artifacts.py @@ -42,7 +42,6 @@ VirtualWorkspaceSourceProjectionCache, ) from openhcs.interop.cellprofiler.runtime.artifact_binding import ( - RuntimeArtifactInputRequest, RuntimeArtifactTypeStrategy, ) from openhcs.microscopes import create_microscope_handler @@ -242,10 +241,7 @@ def request() -> RuntimeAdapterRequest: label_set = RuntimeArtifactTypeStrategy.for_artifact_type( ObjectLabelsArtifactType ).raw_runtime_input_value( - RuntimeArtifactInputRequest( - spec=labels_spec, - value=labels_payload, - ) + spec=labels_spec, value=labels_payload ) assert isinstance(label_set, ObjectLabelSet) np.testing.assert_array_equal(label_set.labels, labels[np.newaxis, ...]) diff --git a/tests/unit/test_runtime_artifact_input_value_contract.py b/tests/unit/test_runtime_artifact_input_value_contract.py new file mode 100644 index 0000000000..a99da528e9 --- /dev/null +++ b/tests/unit/test_runtime_artifact_input_value_contract.py @@ -0,0 +1,57 @@ +"""Direct artifact values retain source ownership across call mutation.""" + +import numpy as np +import pytest + +from openhcs.core.artifacts import ArtifactSpec, ImageArtifactType, MeasurementsArtifactType +from openhcs.core.runtime_image_values import ( + ImagePayloadMetadata, + image_payload_data, + image_payload_metadata, +) +from openhcs.interop.cellprofiler.runtime.artifact_binding import RuntimeArtifactTypeStrategy + + +def test_integer_call_mutation_does_not_replace_post_call_source(): + spec = ArtifactSpec.input("DNA", ImageArtifactType) + pixels = np.arange(12, dtype=np.uint8).reshape(3, 4) + source = ImagePayloadMetadata(source_image_names=("Original",)).payload_with(pixels) + strategy = RuntimeArtifactTypeStrategy.for_artifact_type(spec.artifact_type) + raw = strategy.raw_runtime_input_value(spec, source) + assert image_payload_data(raw) is pixels + assert image_payload_metadata(raw) is not image_payload_metadata(source) + bound = strategy.runtime_input_value(spec, source) + assert not np.shares_memory(image_payload_data(bound), pixels) + image_payload_data(bound)[:] = -1 + image_payload_metadata(bound).source_image_names = ("ChangedByCallable",) + + post_call = strategy.runtime_input_value(spec, source) + np.testing.assert_array_equal(image_payload_data(post_call), pixels.astype(np.float32) / 255) + assert image_payload_metadata(post_call).source_image_names == (spec.name,) + assert image_payload_metadata(source).source_image_names == ("Original",) + assert image_payload_data(source) is pixels + + +def test_raw_source_derivation_preserves_live_nested_source_mapping(): + spec = ArtifactSpec.input("DNA", ImageArtifactType) + source = ImagePayloadMetadata( + source_component_metadata={"nested": {"selected": "before"}}, + ).payload_with(np.zeros((2, 3), dtype=np.uint8)) + strategy = RuntimeArtifactTypeStrategy.for_artifact_type(spec.artifact_type) + first = strategy.raw_runtime_input_value(spec, source) + nested = image_payload_metadata(source).source_component_metadata["nested"] + assert image_payload_metadata(first) is not image_payload_metadata(source) + assert image_payload_metadata(first).source_component_metadata["nested"] is nested + image_payload_metadata(source).source_component_metadata["nested"]["selected"] = "after" + second = strategy.raw_runtime_input_value(spec, source) + assert image_payload_metadata(first).source_component_metadata["nested"]["selected"] == "after" + assert image_payload_metadata(second).source_component_metadata["nested"]["selected"] == "after" + + +def test_measurement_type_errors_remain_at_value_consumption(): + spec = ArtifactSpec.input("Intensity", MeasurementsArtifactType) + strategy = RuntimeArtifactTypeStrategy.for_artifact_type(spec.artifact_type) + with pytest.raises(TypeError, match="Measurement artifact 'Intensity' requires a MeasurementTable, got object"): + strategy.runtime_input_value(spec, object()) + with pytest.raises(TypeError, match="Measurement artifact 'Intensity' requires a MeasurementTable, got object"): + strategy.source_image_name(spec, object()) diff --git a/tests/unit/test_runtime_value_store.py b/tests/unit/test_runtime_value_store.py index 968c8e329a..d5535ffb0b 100644 --- a/tests/unit/test_runtime_value_store.py +++ b/tests/unit/test_runtime_value_store.py @@ -1598,7 +1598,7 @@ def consume_labels(image, labels): request = RuntimeInputBindingRequest( adapter=adapter, kwargs={}, current_image=np.zeros((2, 2)) ) - assert request.artifact_request(edge).value is record.value.data + assert request.artifact_value(edge) is record.value.data @pytest.mark.parametrize("component", [AllComponents.SITE, AllComponents.Z_INDEX, AllComponents.TIMEPOINT]) diff --git a/tests/unit/test_saved_source_label_plane_domain.py b/tests/unit/test_saved_source_label_plane_domain.py index a8c9f6c7ef..cda9d3e7fc 100644 --- a/tests/unit/test_saved_source_label_plane_domain.py +++ b/tests/unit/test_saved_source_label_plane_domain.py @@ -15,7 +15,7 @@ from openhcs.core.source_image_provenance import SourceImageProvenancePlanes from openhcs.core.source_metadata import SourceVoxelSpacing from openhcs.interop.cellprofiler.runtime.artifact_binding import ( - RuntimeArtifactInputRequest, RuntimeArtifactTypeStrategy, + RuntimeArtifactTypeStrategy, ) from openhcs.interop.cellprofiler.runtime.function_contract_execution import CellProfilerFunctionContractExecutor from openhcs.processing.backends.cellprofiler.shape import MeasureObjectSizeShapeModule, measure_object_size_shape @@ -37,7 +37,7 @@ def _source(labels, *, axis=RuntimePlaneAxis.RUNTIME_SLICE, plane_count=None, sp def _saved_label_input(source): spec = ArtifactSpec.input("saved_labels", ObjectLabelsArtifactType, parameter_name="labels") return RuntimeArtifactTypeStrategy.for_artifact_type(spec.artifact_type).runtime_input_value( - RuntimeArtifactInputRequest(spec=spec, value=source) + spec=spec, value=source ) @@ -78,7 +78,7 @@ def test_original_artifact_admission_and_full_stack_shape_keep_2d_planes(axis): source_bytes = labels.tobytes() spec = ArtifactSpec.input("saved_labels", ObjectLabelsArtifactType, parameter_name="labels") value = RuntimeArtifactTypeStrategy.for_artifact_type(spec.artifact_type).runtime_input_value( - RuntimeArtifactInputRequest(spec=spec, value=source) + spec=spec, value=source ) assert value.domain.scope is ObjectLabelDomainScope.PLANE assert value.plane_axis is axis From 14afeb83e9c3fc3184cddcbf93facc00dbfd35ce Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 10:03:15 -0400 Subject: [PATCH 131/235] test(runtime): migrate compiled input ownership controls to direct values --- ...nvocation_input_source_context_identity.py | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/tests/unit/test_invocation_input_source_context_identity.py b/tests/unit/test_invocation_input_source_context_identity.py index 43cea99fbd..95c31656ce 100644 --- a/tests/unit/test_invocation_input_source_context_identity.py +++ b/tests/unit/test_invocation_input_source_context_identity.py @@ -147,12 +147,12 @@ def threshold_like(image: np.ndarray) -> np.ndarray: adapter=adapter, kwargs={}, current_image=source_payload, - ).artifact_request(edge) + ).artifact_value(edge) assert edge.consumes_main_flow is True assert edge.main_flow_projection is MainFlowInputProjection.COMPLETE_PAYLOAD - assert request.value is source_payload - assert image_payload_metadata(request.value).source_image_names == ("Hoechst",) + assert request is source_payload + assert image_payload_metadata(request).source_image_names == ("Hoechst",) def test_multiple_main_flow_images_keep_declared_source_projection() -> None: @@ -232,22 +232,22 @@ def source_artifact_payload(_request, ref): ValueError, match="does not represent declared source image 'PlateTemplate'", ): - request.artifact_request( + request.artifact_value( replace( edges_by_ref[plate_template.ref()], consumes_main_flow=True, ) ) - source_request = request.artifact_request(edges_by_ref[plate_template.ref()]) - main_flow_request = request.artifact_request(edges_by_ref[combined_image.ref()]) + source_request = request.artifact_value(edges_by_ref[plate_template.ref()]) + main_flow_request = request.artifact_value(edges_by_ref[combined_image.ref()]) - np.testing.assert_array_equal(image_payload_data(source_request.value), 7.0) - np.testing.assert_array_equal(image_payload_data(main_flow_request.value), 11.0) - assert image_payload_metadata(source_request.value).source_image_names == ( + np.testing.assert_array_equal(image_payload_data(source_request), 7.0) + np.testing.assert_array_equal(image_payload_data(main_flow_request), 11.0) + assert image_payload_metadata(source_request).source_image_names == ( plate_template.name, ) - assert image_payload_metadata(main_flow_request.value).source_image_names == ( + assert image_payload_metadata(main_flow_request).source_image_names == ( combined_image.name, ) @@ -295,10 +295,10 @@ def test_storage_backed_input_keeps_exact_runtime_authority() -> None: current_image=current_payload, ) - runtime_request = request.artifact_request(edge) + runtime_request = request.artifact_value(edge) - np.testing.assert_array_equal(image_payload_data(runtime_request.value), 13.0) - assert image_payload_metadata(runtime_request.value).source_image_names == ( + np.testing.assert_array_equal(image_payload_data(runtime_request), 13.0) + assert image_payload_metadata(runtime_request).source_image_names == ( combined_image.name, ) @@ -349,9 +349,9 @@ def test_compiled_producer_edge_overrides_matching_source_binding() -> None: ).payload_with(np.full((2, 3), 23.0, dtype=np.float32), None), ) - runtime_request = request.artifact_request(edge) + runtime_request = request.artifact_value(edge) - np.testing.assert_array_equal(image_payload_data(runtime_request.value), 19.0) - assert image_payload_metadata(runtime_request.value).source_image_names == ( + np.testing.assert_array_equal(image_payload_data(runtime_request), 19.0) + assert image_payload_metadata(runtime_request).source_image_names == ( combined_image.name, ) From 5747ce73362879c79ce2489e0a83ce03432eaa3c Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 10:04:08 -0400 Subject: [PATCH 132/235] test(cellprofiler): align shape fixtures with declared label domains --- .../test_cellprofiler_module_execution.py | 45 +++++++++++-------- .../test_saved_source_label_plane_domain.py | 20 ++++++--- 2 files changed, 40 insertions(+), 25 deletions(-) diff --git a/tests/unit/test_cellprofiler_module_execution.py b/tests/unit/test_cellprofiler_module_execution.py index 18986b57c9..86be0fcf7e 100644 --- a/tests/unit/test_cellprofiler_module_execution.py +++ b/tests/unit/test_cellprofiler_module_execution.py @@ -6087,9 +6087,10 @@ def test_measure_object_size_shape_consumes_runtime_projected_label_plane() -> N calculate_zernikes=False, ) - assert [row["object_label"] for row in rows] == [1, 2] - assert rows[0]["Area"] == 4.0 - assert np.isnan(rows[1]["Area"]) + assert rows.object_row_identity is MeasurementObjectRowIdentity.LABEL_ID + assert [(row["slice_index"], row["object_label"], row["Area"]) for row in rows] == [ + (0, 2, 4.0), + ] def test_pure_2d_slice_execution_injects_slice_index_for_declared_callables() -> None: @@ -6508,7 +6509,7 @@ def test_measure_object_intensity_columnar_rows_use_declared_axis_domain() -> No assert [row["integrated_intensity"] for row in rows] == [10.0, 20.0] -def test_measure_object_size_shape_rows_project_measured_sequence_to_cp_ordinals() -> ( +def test_measure_object_size_shape_rows_project_declared_label_domain_to_cp_ordinals() -> ( None ): payload = ObjectLabelPayload( @@ -6540,10 +6541,15 @@ def test_measure_object_size_shape_rows_project_measured_sequence_to_cp_ordinals assert [row["object_label"] for row in rows] == [1, 2, 3, 4, 5] assert rows[0]["Area"] == 10.0 - assert rows[1]["Area"] == 30.0 - assert rows[2]["Area"] == 50.0 - assert np.isnan(rows[3].get("Area", np.nan)) - assert np.isnan(rows[4].get("Area", np.nan)) + assert np.isnan(rows[1]["Area"]) + assert rows[2]["Area"] == 30.0 + assert np.isnan(rows[3]["Area"]) + assert rows[4]["Area"] == 50.0 + np.testing.assert_array_equal( + [row["Center_X"] for row in rows], + [10.0, np.nan, 30.0, np.nan, 50.0], + ) + assert rows.object_row_identity is MeasurementObjectRowIdentity.ROW_ORDINAL def test_measure_object_size_shape_preserves_zero_valued_label_rows() -> None: @@ -6646,7 +6652,7 @@ def test_measure_object_size_shape_preserves_complete_dense_label_domain_rows() def test_object_measurement_modules_declare_their_cp_row_identity_policies() -> None: assert ( MeasureObjectSizeShapeModule.runtime_object_measurement_row_policy().object_identity() - is MeasurementObjectRowIdentity.ROW_SEQUENCE + is MeasurementObjectRowIdentity.ROW_ORDINAL ) assert ( MeasureObjectIntensityDistributionModule.runtime_object_measurement_row_policy().object_identity() @@ -7283,7 +7289,7 @@ def test_measure_object_size_shape_orientation_uses_positive_inertia_tie() -> No assert rows[0]["Orientation"] == 45.0 -def test_measure_object_size_shape_zernikes_use_declared_row_ordinal_domain() -> None: +def test_measure_object_size_shape_zernikes_use_declared_label_id_domain() -> None: image = np.ones((12, 12), dtype=np.float32) labels = np.zeros(image.shape, dtype=np.int32) labels[1:4, 1:4] = 1 @@ -7305,10 +7311,11 @@ def test_measure_object_size_shape_zernikes_use_declared_row_ordinal_domain() -> assert np.isfinite(rows[0]["Zernike_0_0"]) assert np.isnan(rows[1]["Zernike_0_0"]) assert np.isfinite(rows[2]["Zernike_0_0"]) - assert rows[1]["Area"] == 16.0 - assert np.isnan(rows[2]["Area"]) - assert rows[1]["MaximumRadius"] > 0.0 - assert rows[2]["MaximumRadius"] == 0.0 + np.testing.assert_array_equal([row["Area"] for row in rows], [9.0, np.nan, 16.0]) + assert rows.object_row_identity is MeasurementObjectRowIdentity.LABEL_ID + assert [row["slice_index"] for row in rows] == [0, 0, 0] + assert rows[1]["MaximumRadius"] == 0.0 + assert rows[2]["MaximumRadius"] > 0.0 assert rows[1]["MinFeretDiameter"] == 0.0 assert rows[2]["MinFeretDiameter"] > 0.0 @@ -7330,10 +7337,12 @@ def test_measure_object_size_shape_backend_emits_concrete_cp_index_domain() -> N dtype_config=DtypeConfig(), ) - assert [row["object_label"] for row in rows] == [1, 2, 3] - assert rows[0]["Area"] == 9.0 - assert rows[1]["Area"] == 16.0 - assert np.isnan(rows[2]["Area"]) + assert [row["object_label"] for row in rows] == [1, 2, 3, 4, 5] + assert rows.object_row_identity is MeasurementObjectRowIdentity.LABEL_ID + assert [row["slice_index"] for row in rows] == [0] * 5 + np.testing.assert_array_equal( + [row["Area"] for row in rows], [9.0, np.nan, 16.0, np.nan, np.nan] + ) def test_measure_object_size_shape_uses_explicit_sparse_object_domain() -> None: diff --git a/tests/unit/test_saved_source_label_plane_domain.py b/tests/unit/test_saved_source_label_plane_domain.py index cda9d3e7fc..ea6d9eedc9 100644 --- a/tests/unit/test_saved_source_label_plane_domain.py +++ b/tests/unit/test_saved_source_label_plane_domain.py @@ -9,8 +9,10 @@ from openhcs.core.artifacts import ArtifactSpec, ObjectLabelsArtifactType from openhcs.core.callable_contract import CallableContract 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 from openhcs.core.source_metadata import SourceVoxelSpacing @@ -97,13 +99,17 @@ def test_original_artifact_admission_and_full_stack_shape_keep_2d_planes(axis): execution_mode=ImagePayloadExecutionMode.FULL_STACK, ) area = MeasureObjectSizeShapeModule.MeasurementFeature.AREA.value - # The original AreaShape owner declares ROW_SEQUENCE and a dense extent - # through each plane's maximum ID, not two sparse-ID measurement rows. - # Check the entire existing vector, including every missing-value slot. - expected_area = np.full(29 + 106, np.nan) - expected_area[0] = 6.0 - expected_area[29] = 12.0 - np.testing.assert_array_equal(tuple(row[area] for row in rows), expected_area) + # Raw shape rows retain the exact authored label domain of each plane. + # Export completion separately projects that domain to CP row ordinals. + assert rows.object_row_identity is MeasurementObjectRowIdentity.LABEL_ID + assert tuple( + ( + row[MeasurementRowAxisField.SLICE_INDEX.value], + row[MeasurementRowAxisField.OBJECT_LABEL.value], + row[area], + ) + for row in rows + ) == ((0, 29, 6.0), (1, 106, 12.0)) assert "AreaShape_Volume" not in tuple(field.name for field in rows.fields) assert labels.tobytes() == source_bytes From 9a43e79f7b2b7c02430c7894ae2fa8fc16192d24 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 10:04:08 -0400 Subject: [PATCH 133/235] refactor(runtime): publish original image outputs through existing owners --- openhcs/core/aligned_image_payload.py | 84 ++- openhcs/core/steps/function_runtime.py | 536 +++++++----------- tests/unit/test_aligned_output_projection.py | 6 +- .../test_cellprofiler_module_execution.py | 22 +- tests/unit/test_function_artifact_outputs.py | 61 +- ...test_function_runtime_source_projection.py | 21 +- .../test_opaque_main_flow_image_domain.py | 80 +-- .../unit/test_output_publication_ownership.py | 157 +++++ 8 files changed, 512 insertions(+), 455 deletions(-) create mode 100644 tests/unit/test_output_publication_ownership.py diff --git a/openhcs/core/aligned_image_payload.py b/openhcs/core/aligned_image_payload.py index c939865b21..8203db5ad4 100644 --- a/openhcs/core/aligned_image_payload.py +++ b/openhcs/core/aligned_image_payload.py @@ -443,6 +443,43 @@ def copy_whole_image( copied_data, copied_mask, ) + @staticmethod + def with_saved_output_context( + stack_payload: RuntimeArrayData, + payloads: Sequence[RuntimeArrayData], + metadata: Sequence[ImagePayloadMetadata], + *, + single_output_plane_axis: RuntimePlaneAxis | None, + ) -> RuntimeArrayData: + """Attach saved member context without replacing the independent buffer.""" + data = image_payload_data(stack_payload) + if ( + len(payloads) == 1 + and single_output_plane_axis is metadata[0].plane_axis + and np.shape(data) == np.shape(image_payload_data(payloads[0])) + ): + return metadata[0].replace_fields().payload_with( + data, image_payload_mask(stack_payload), + ) + 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)}." + ) + mode = ( + ImagePayloadMetadataCompositionMode.for_plane_axis( + image_payload_metadata(stack_payload).plane_axis, + ) + if isinstance(stack_payload, ImagePayloadMetadataCarrier) + else ImagePayloadMetadataCompositionMode.STACK + ) + output_metadata = ImagePayloadMetadata.compose( + tuple(payloads), mode=mode, source_metadata=tuple(metadata), + ) + return output_metadata.payload_with( + data, _stack_image_payload_mask(tuple(payloads), data), + ) + def composition_source_metadata(self) -> tuple[ImagePayloadMetadata, ...]: return tuple( self.composition_payload_metadata(image_payload_metadata(payload)) @@ -514,23 +551,6 @@ def stack_image_payloads( return ImagePayloadStackContext(image_payloads, metadata_mode).compose() -def stack_image_payload_context_from_metadata( - image_payloads: Sequence[Any], - stack: RuntimeArrayData, - metadata_by_payload: Sequence[ImagePayloadMetadata], - *, - metadata_mode: ImagePayloadMetadataCompositionMode, -) -> Any: - """Attach composed image context using already resolved payload metadata.""" - payloads = tuple(image_payloads) - metadata = ImagePayloadMetadata.compose( - payloads, - mode=metadata_mode, - source_metadata=tuple(metadata_by_payload), - ) - return metadata.payload_with(stack, _stack_image_payload_mask(payloads, stack)) - - def _stack_image_payload_mask( image_payloads: Sequence[Any], stack: RuntimeArrayData, @@ -1230,6 +1250,36 @@ def projected_output_slices( for output_slice in payload_slices_for_alignment(payload): yield output_slice, context + def copy_projected_output_stack( + self, + projected_outputs: Sequence[tuple[Any, AlignedImageSliceContext | None]], + *, + memory_type: str, + device_id: int | None, + ) -> RuntimeArrayData | None: + """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: + return self.copy_whole_image( + payloads[0], memory_type=memory_type, device_id=device_id, + ) + data = tuple(image_payload_data(payload) for payload in payloads) + declared_axes = { + self.plane_axis_for_output_context(context) + for _payload, context in projected_outputs + } + if len(declared_axes) != 1 or len({tuple(np.shape(item)) for item in data}) != 1: + return None + if metadata_mode is ImagePayloadMetadataCompositionMode.BUNDLE: + return ImagePayloadBundleContext.from_payloads( + payloads, metadata_mode=metadata_mode, + ).compose() + stacked = stack_runtime_slices(data, memory_type, device_id) + return stack_image_payload_context( + payloads, stacked, metadata_mode=metadata_mode, + ) + def output_values_for_artifact_specs( self, canonical_specs: tuple[ArtifactSpec, ...], diff --git a/openhcs/core/steps/function_runtime.py b/openhcs/core/steps/function_runtime.py index e2e807dfc4..be69be797f 100644 --- a/openhcs/core/steps/function_runtime.py +++ b/openhcs/core/steps/function_runtime.py @@ -9,7 +9,6 @@ import logging import os import time -from collections.abc import Sequence as SequenceABC from dataclasses import dataclass, field, replace from threading import Lock from pathlib import Path @@ -18,7 +17,6 @@ Callable, ClassVar, Generic, - Iterator, Mapping, Sequence, TypeVar, @@ -78,7 +76,6 @@ ImageOutputBundle, flatten_aligned_image_payload_slices, stack_image_payload_context, - stack_image_payload_context_from_metadata, unstack_image_payload_context, ) from openhcs.core.memory import ( @@ -335,6 +332,7 @@ def contextualize( ) -> FunctionOutputContextualizedValue: """Return output with source context preserved where semantics allow it.""" + @abstractmethod def contextualize_from_projector( self, source_payload: RuntimePayload, @@ -342,45 +340,7 @@ def contextualize_from_projector( output_plan: ArtifactOutputPlan | None, plane_projector: RuntimePlaneAxisProjector | None, ) -> FunctionOutputContextualizedValue: - """Resolve plane projection only after the artifact strategy is selected.""" - - plane_projection = ( - None - if plane_projector is None - else preserve_declared_image_payload_axis( - plane_projector, - output_value, - source_payload=source_payload, - ) - ) - if ( - plane_projection is None - and output_plan is not None - and output_plan.variable_components - ): - if plane_projector is None: - if not self.output_owns_source_context( - source_payload, - output_value, - output_plan, - None, - ): - raise ValueError( - f"Artifact output {output_plan.ref()!r} preserves variable " - f"components {output_plan.variable_components!r} but the " - "runtime invocation supplies no plane projector." - ) - return output_value - plane_projection = RuntimePlaneAxisValueProjection.require_from_projector( - plane_projector, - RuntimePlaneAxis.RUNTIME_SLICE, - ) - return self.contextualize( - source_payload, - output_value, - output_plan, - plane_projection, - ) + """Contextualize output using this artifact family's plane contract.""" @staticmethod def output_owns_source_context( @@ -423,10 +383,8 @@ def contextualize( return output_value -class ProjectedFunctionOutputContextStrategy( - UnchangedFunctionOutputContextStrategy, -): - """Restore source-plane projection for contextual artifact families.""" +class ProjectedFunctionOutputContextStrategy(UnchangedFunctionOutputContextStrategy): + """Own contextual output projection while dominating the context-free fallback.""" @abstractmethod def contextualize( @@ -445,15 +403,48 @@ def contextualize_from_projector( output_plan: ArtifactOutputPlan | None, plane_projector: RuntimePlaneAxisProjector | None, ) -> FunctionOutputContextualizedValue: - return FunctionOutputContextStrategy.contextualize_from_projector( - self, + """Resolve plane projection only after the artifact strategy is selected.""" + + plane_projection = ( + None + if plane_projector is None + else preserve_declared_image_payload_axis( + plane_projector, + output_value, + source_payload=source_payload, + ) + ) + if ( + plane_projection is None + and output_plan is not None + and output_plan.variable_components + ): + if plane_projector is None: + if not self.output_owns_source_context( + source_payload, + output_value, + output_plan, + None, + ): + raise ValueError( + f"Artifact output {output_plan.ref()!r} preserves variable " + f"components {output_plan.variable_components!r} but the " + "runtime invocation supplies no plane projector." + ) + return output_value + plane_projection = RuntimePlaneAxisValueProjection.require_from_projector( + plane_projector, + RuntimePlaneAxis.RUNTIME_SLICE, + ) + return self.contextualize( source_payload, output_value, output_plan, - plane_projector, + plane_projection, ) + class ImageFunctionOutputContextStrategy(ProjectedFunctionOutputContextStrategy): """Preserve source-image metadata for image outputs derived from the main input.""" @@ -895,11 +886,67 @@ def contextualize( "Runtime-slice-aligned image output strategy requires " f"RuntimeSliceAlignedValueSet, got {type(source_values).__name__}." ) - return RuntimeSliceAlignedImageOutputContext( - source_values=source_values, - output_value=output_value, - plane_projection=plane_projection, - ).payload() + output_data = image_payload_data(output_value) + if plane_projection is None: + if source_values.slice_count != 1: + raise ValueError( + "Runtime-slice-aligned image output has multiple source values " + "but no declared runtime plane projection." + ) + source_value = source_values.value_for_aligned_slice(0, 1) + return image_payload_metadata(source_value).derive_payload( + source_value, + output_value, + ) + output_slices = self.output_slices( + output_value, output_data, source_values, plane_projection, + ) + contextualized_slices = [] + for slice_index, output_slice in enumerate(output_slices): + source_value = source_values.value_for_aligned_slice( + slice_index, + len(output_slices), + ) + contextualized_slices.append( + image_payload_metadata(source_value).derive_payload( + source_value, + output_slice, + ) + ) + return stack_image_payload_context( + tuple(contextualized_slices), + output_data, + metadata_mode=ImagePayloadMetadataCompositionMode.STACK, + ) + + @staticmethod + def output_slices( + output_value: RuntimePayload, + output_data: RuntimeArrayData, + source_values: RuntimeSliceAlignedValueSet, + plane_projection: RuntimePlaneAxisValueProjection, + ) -> tuple[RuntimeArrayData, ...]: + output_array = np.asarray(output_data) + projection = plane_projection + if projection.axis_size != source_values.slice_count: + raise ValueError( + "Runtime-slice image output projection must exactly match its " + f"aligned source count: {projection.axis_size} != " + f"{source_values.slice_count}." + ) + projection.validate_shape( + output_array.shape, + value_name="Runtime-slice-aligned image output", + ) + return tuple( + image_payload_slice_context( + output_value, + output_array[slice_index], + slice_index, + plane_axis=projection.axis, + ) + for slice_index in range(projection.axis_size) + ) class ObjectLabelImageOutputSourceContextStrategy(ImageOutputSourceContextStrategy): @@ -968,77 +1015,6 @@ def contextualize( ) -@dataclass(frozen=True, slots=True) -class RuntimeSliceAlignedImageOutputContext: - """Compose stack metadata for image output derived from aligned source values.""" - - source_values: RuntimeSliceAlignedValueSet - output_value: RuntimePayload - plane_projection: RuntimePlaneAxisValueProjection | None - - def payload(self) -> RuntimePayload: - output_data = image_payload_data(self.output_value) - if self.plane_projection is None: - if self.source_values.slice_count != 1: - raise ValueError( - "Runtime-slice-aligned image output has multiple source values " - "but no declared runtime plane projection." - ) - source_value = self.source_values.value_for_aligned_slice(0, 1) - return image_payload_metadata(source_value).derive_payload( - source_value, - self.output_value, - ) - output_slices = self.output_slices(output_data) - contextualized_slices = [] - for slice_index, output_slice in enumerate(output_slices): - source_value = self.source_values.value_for_aligned_slice( - slice_index, - len(output_slices), - ) - contextualized_slices.append( - image_payload_metadata(source_value).derive_payload( - source_value, - output_slice, - ) - ) - return stack_image_payload_context( - tuple(contextualized_slices), - output_data, - metadata_mode=ImagePayloadMetadataCompositionMode.STACK, - ) - - def output_slices( - self, - output_data: RuntimeArrayData, - ) -> tuple[RuntimeArrayData, ...]: - output_array = np.asarray(output_data) - projection = self.plane_projection - if projection is None: - raise RuntimeError( - "Runtime-slice output splitting requires a plane projection." - ) - if projection.axis_size != self.source_values.slice_count: - raise ValueError( - "Runtime-slice image output projection must exactly match its " - f"aligned source count: {projection.axis_size} != " - f"{self.source_values.slice_count}." - ) - projection.validate_shape( - output_array.shape, - value_name="Runtime-slice-aligned image output", - ) - return tuple( - image_payload_slice_context( - self.output_value, - output_array[slice_index], - slice_index, - plane_axis=projection.axis, - ) - for slice_index in range(projection.axis_size) - ) - - class ObjectLabelsFunctionOutputContextStrategy(ProjectedFunctionOutputContextStrategy): """Preserve source-image metadata for object-label outputs.""" @@ -2478,157 +2454,6 @@ def converted_payload(self) -> RuntimeArrayData: return with_image_payload_data(self.payload, converted) -@dataclass(slots=True) -class PatternGroupOutputData: - """Unstacked output slices plus declared per-slice output semantics.""" - - slices: list[RuntimeArrayData] - slice_contexts: tuple[AlignedImageSliceContext, ...] = () - stack_payload: RuntimeArrayData | None = None - main_flow_source: RuntimeArrayData | None = None - - def __post_init__(self) -> None: - if not self.slice_contexts: - self.slice_contexts = tuple( - AlignedImageSliceContext.anonymous_main_flow() for _slice in self.slices - ) - if len(self.slice_contexts) != len(self.slices): - raise ValueError( - "PatternGroupOutputData.slice_contexts must match slices; " - f"got {len(self.slice_contexts)} context(s) for {len(self.slices)} slice(s)." - ) - - @classmethod - def from_single_image( - cls, - value: RuntimeArrayData, - *, - slice_contexts: tuple[AlignedImageSliceContext, ...] = (), - memory_type: MemoryType, - device_id: int | None, - ) -> "PatternGroupOutputData": - """Copy one whole image for main-flow reuse without declaring a new axis.""" - return cls( - slices=(value,), - slice_contexts=slice_contexts, - stack_payload=ImagePayloadStackComposition.copy_whole_image( - value, memory_type=memory_type, device_id=device_id, - ), - main_flow_source=value, - ) - - def plane_axis_for_output_context( - self, context: AlignedImageSliceContext, - ) -> RuntimePlaneAxis | None: - """Derive each persisted member's domain from its original nominal producer.""" - if isinstance(self.main_flow_source, AlignedImageStack): - return self.main_flow_source.plane_axis_for_output_context(context) - if isinstance(self.main_flow_source, ImagePayloadMetadataCarrier): - return image_payload_metadata(self.main_flow_source).plane_axis - return RuntimePlaneAxis.RUNTIME_SLICE - - def cache_payload_for_outputs( - self, - payloads: Sequence[RuntimeArrayData], - metadata: Sequence[ImagePayloadMetadata], - ) -> RuntimeArrayData | None: - """Keep the original whole-image domain while applying saved output context.""" - if self.stack_payload is None: - return None - data = image_payload_data(self.stack_payload) - if ( - len(payloads) == 1 - and self.plane_axis_for_output_context(self.slice_contexts[0]) is metadata[0].plane_axis - and np.shape(data) == np.shape(image_payload_data(payloads[0])) - ): - return metadata[0].replace_fields().payload_with( - data, image_payload_mask(self.stack_payload), - ) - if np.shape(data)[:1] != (len(payloads),): - raise ValueError( - "PatternGroupOutputData.stack_payload must match its declared " - f"output slice count: stack shape {np.shape(data)!r}, " - f"slice count {len(payloads)}." - ) - return stack_image_payload_context_from_metadata( - payloads, data, metadata, - metadata_mode=( - ImagePayloadMetadataCompositionMode.for_plane_axis( - image_payload_metadata(self.stack_payload).plane_axis, - ) - if isinstance(self.stack_payload, ImagePayloadMetadataCarrier) - else ImagePayloadMetadataCompositionMode.STACK - ), - ) - - @classmethod - def from_aligned_stack( - cls, - value: AlignedImageStack, - *, - memory_type: MemoryType, - device_id: int | None, - ) -> "PatternGroupOutputData": - """Materialize correlated output slices through the alignment owner.""" - projected_outputs = tuple(value.projected_output_slices()) - payloads = [payload for payload, _context in projected_outputs] - metadata_mode = value.projected_output_composition_mode - if metadata_mode is None: - result = cls.from_single_image( - payloads[0], - slice_contexts=tuple(context for _payload, context in projected_outputs), - memory_type=memory_type, - device_id=device_id, - ) - result.main_flow_source = value - return result - data = tuple(image_payload_data(payload) for payload in payloads) - stack_payload = None - declared_axes = { - value.plane_axis_for_output_context(context) - for _payload, context in projected_outputs - } - if len(declared_axes) == 1 and len({tuple(np.shape(item)) for item in data}) == 1: - if metadata_mode is ImagePayloadMetadataCompositionMode.BUNDLE: - stack_payload = ImagePayloadBundleContext.from_payloads( - tuple(payloads), metadata_mode=metadata_mode, - ).compose() - else: - stacked = stack_runtime_slices(data, memory_type, device_id) - stack_payload = stack_image_payload_context( - payloads, stacked, metadata_mode=metadata_mode, - ) - return cls( - slices=payloads, - slice_contexts=( - tuple(context for _payload, context in projected_outputs) - if value.slice_contexts - else () - ), - stack_payload=stack_payload, - main_flow_source=value, - ) - - def __iter__(self) -> Iterator[RuntimeArrayData]: - return iter(self.slices) - - def __len__(self) -> int: - return len(self.slices) - - def __getitem__(self, index: int) -> RuntimeArrayData: - return self.slices[index] - - def __eq__(self, other: object) -> bool: - if isinstance(other, PatternGroupOutputData): - return ( - self.slices == other.slices - and self.slice_contexts == other.slice_contexts - ) - if isinstance(other, SequenceABC): - return self.slices == list(other) - return NotImplemented - - class PatternGroupRuntime: """Staged runtime for one pattern group.""" @@ -2726,37 +2551,18 @@ def run(self) -> None: plan.step_name, ) return - unstack_started_at = time.perf_counter() - output_data = self._validate_and_unstack(processed_stack, loaded) - RuntimeProfileSink.record( - "pattern_validate_unstack", - time.perf_counter() - unstack_started_at, - step=plan.step_index, - step_name=plan.step_name, - pattern=self.pattern_repr, - ) - save_started_at = time.perf_counter() - output_records = self._save_outputs(output_data, loaded.matching_files) + output_records = self._save_outputs(processed_stack, loaded.matching_files) output_paths = [record.output_path for record in output_records] - RuntimeProfileSink.record( - "pattern_save_outputs", - time.perf_counter() - save_started_at, - step=plan.step_index, - step_name=plan.step_name, - pattern=self.pattern_repr, - ) cleanup_started_at = time.perf_counter() self._cleanup_collapsed_domains( - output_data.slices, + output_records, loaded.matching_files, output_paths, ) step_output_manifest(self.request.context).record_outputs( plan, output_records, - collapsed_input_domain=( - len(output_data.slices) < len(loaded.matching_files) - ), + collapsed_input_domain=(len(output_records) < len(loaded.matching_files)), ) RuntimeProfileSink.record( "pattern_cleanup", @@ -3245,40 +3051,33 @@ def _execute_pattern( runtime_scope = FunctionRuntimeScope.from_pattern_group(request, loaded) return runtime_scope.execute_chain(loaded.main_data_stack) - def _validate_and_unstack( + def _project_output_slices( self, processed_stack: RuntimeArrayData, - loaded: PatternGroupData, - ) -> PatternGroupOutputData: + matching_files: Sequence[str], + ) -> tuple[tuple[RuntimeArrayData, AlignedImageSliceContext | None], ...]: + """Project the original output through its nominal image topology.""" if ( isinstance(processed_stack, ImagePayloadMetadataCarrier) and processed_stack.metadata.plane_axis is None ): output_context = self._unwrapped_main_flow_output_context() - return PatternGroupOutputData.from_single_image( - processed_stack, - slice_contexts=( - (output_context,) - if output_context is not None - else ( - self._producer_output_contexts(loaded.matching_files) - if len(loaded.matching_files) == 1 - else () - ) - ), - memory_type=self.request.execution_plan.output_memory_type, - device_id=self.request.execution_plan.device_id_for( - self.request.execution_plan.output_memory_type - ), + contexts = ( + (output_context,) + if output_context is not None + else ( + self._producer_output_contexts(matching_files) + if len(matching_files) == 1 + else () + ) ) - if isinstance(processed_stack, AlignedImageStack): - return PatternGroupOutputData.from_aligned_stack( - processed_stack, - memory_type=self.request.execution_plan.output_memory_type, - device_id=self.request.execution_plan.device_id_for( - self.request.execution_plan.output_memory_type - ), + context = ( + contexts[0] if contexts + else AlignedImageSliceContext.anonymous_main_flow() ) + return ((processed_stack, context),) + if isinstance(processed_stack, AlignedImageStack): + return tuple(processed_stack.projected_output_slices()) output_context = self._unwrapped_main_flow_output_context() output_projection = RuntimeSliceProjection.preserved_context_for_value( processed_stack @@ -3311,7 +3110,7 @@ def _validate_and_unstack( self.request.execution_plan.device_id_for( self.request.execution_plan.output_memory_type ), - expected_count=len(loaded.matching_files), + expected_count=len(matching_files), ) ) RuntimeProfileSink.record( @@ -3352,14 +3151,14 @@ def _validate_and_unstack( if output_context is not None else () ) - if not slice_contexts and len(output_payloads) == len(loaded.matching_files): - slice_contexts = self._producer_output_contexts(loaded.matching_files) - return PatternGroupOutputData( - slices=output_payloads, - slice_contexts=slice_contexts, - stack_payload=processed_stack, - main_flow_source=processed_stack, - ) + if not slice_contexts and len(output_payloads) == len(matching_files): + slice_contexts = self._producer_output_contexts(matching_files) + if not slice_contexts: + slice_contexts = tuple( + AlignedImageSliceContext.anonymous_main_flow() + for _payload in output_payloads + ) + return tuple(zip(output_payloads, slice_contexts, strict=True)) def _unwrapped_main_flow_output_context( self, @@ -3373,11 +3172,53 @@ def _unwrapped_main_flow_output_context( def _save_outputs( self, - output_data: PatternGroupOutputData, + processed_stack: RuntimeArrayData, matching_files: list[str], ) -> list[ProducedOutputSemantics]: context = self.request.context - output_slices = output_data.slices + unstack_started_at = time.perf_counter() + projected_outputs = self._project_output_slices(processed_stack, matching_files) + plan = self.request.execution_plan + if isinstance(processed_stack, AlignedImageStack): + stack_payload = processed_stack.copy_projected_output_stack( + projected_outputs, + memory_type=plan.output_memory_type, + device_id=plan.device_id_for(plan.output_memory_type), + ) + elif ( + isinstance(processed_stack, ImagePayloadMetadataCarrier) + and processed_stack.metadata.plane_axis is None + ): + stack_payload = ImagePayloadStackComposition.copy_whole_image( + processed_stack, + memory_type=plan.output_memory_type, + device_id=plan.device_id_for(plan.output_memory_type), + ) + else: + stack_payload = processed_stack + RuntimeProfileSink.record( + "pattern_validate_unstack", + time.perf_counter() - unstack_started_at, + step=plan.step_index, + step_name=plan.step_name, + pattern=self.pattern_repr, + ) + output_contexts = tuple( + context if context is not None else AlignedImageSliceContext.anonymous_main_flow() + for _payload, context in projected_outputs + ) + save_started_at = time.perf_counter() + + def plane_axis_for_output( + context: AlignedImageSliceContext, + ) -> RuntimePlaneAxis | None: + if isinstance(processed_stack, AlignedImageStack): + return processed_stack.plane_axis_for_output_context(context) + if isinstance(processed_stack, ImagePayloadMetadataCarrier): + return image_payload_metadata(processed_stack).plane_axis + return RuntimePlaneAxis.RUNTIME_SLICE + + output_slices = tuple(payload for payload, _context in projected_outputs) num_outputs = len(output_slices) num_inputs = len(matching_files) @@ -3432,7 +3273,7 @@ def _save_outputs( f"{i} does not carry payload component identity." ) from exc raise - output_context = output_data.slice_contexts[i] + output_context = output_contexts[i] if not output_context.is_anonymous_main_flow: output_identity = output_identity.with_filename_qualifier( output_context.output_key @@ -3466,7 +3307,7 @@ def _save_outputs( output_identity, output_context=output_context, image_metadata=output_metadata, - main_flow_plane_axis=output_data.plane_axis_for_output_context(output_context), + main_flow_plane_axis=plane_axis_for_output(output_context), ) if output_directory_exists and context.filemanager.exists( @@ -3504,10 +3345,16 @@ def _save_outputs( output_paths_batch, Backend.MEMORY.value, ) - stack_payload = output_data.cache_payload_for_outputs( - output_payloads, output_payload_metadata, - ) if stack_payload is not None: + stack_payload = ImagePayloadStackComposition.with_saved_output_context( + stack_payload, + output_payloads, + output_payload_metadata, + single_output_plane_axis=( + plane_axis_for_output(output_contexts[0]) + if len(output_payloads) == 1 else None + ), + ) context.runtime_image_stack_cache.store( tuple(output_paths_batch), memory_type=self.request.execution_plan.output_memory_type, @@ -3521,16 +3368,23 @@ def _save_outputs( paths=len(output_paths_batch), memory_type=self.request.execution_plan.output_memory_type, ) + RuntimeProfileSink.record( + "pattern_save_outputs", + time.perf_counter() - save_started_at, + step=plan.step_index, + step_name=plan.step_name, + pattern=self.pattern_repr, + ) return output_records def _cleanup_collapsed_domains( self, - output_slices: list[RuntimeArrayData], + output_records: Sequence[ProducedOutputSemantics], matching_files: list[str], output_paths: Sequence[str], ) -> None: context = self.request.context - num_outputs = len(output_slices) + num_outputs = len(output_records) num_inputs = len(matching_files) if num_outputs >= num_inputs: diff --git a/tests/unit/test_aligned_output_projection.py b/tests/unit/test_aligned_output_projection.py index 191f1c0a98..54f7462a09 100644 --- a/tests/unit/test_aligned_output_projection.py +++ b/tests/unit/test_aligned_output_projection.py @@ -51,13 +51,13 @@ def counted(payload): ), ) ) - result = runtime._validate_and_unstack(bundle, None) + result = runtime._project_output_slices(bundle, []) assert len(calls) == 2 assert calls[0] is payloads[0] and calls[1] is payloads[1] - assert result.slice_contexts == (contexts[0],) * 3 + (contexts[1],) * 2 + assert tuple(context for _payload, context in result) == (contexts[0],) * 3 + (contexts[1],) * 2 assert len(result) == 5 - for index, payload in enumerate(result.slices): + for index, payload in enumerate(payload for payload, _context in result): source = first if index < 3 else second plane = index if index < 3 else index - 3 np.testing.assert_array_equal(image_payload_data(payload), source[plane]) diff --git a/tests/unit/test_cellprofiler_module_execution.py b/tests/unit/test_cellprofiler_module_execution.py index 86be0fcf7e..0674741f40 100644 --- a/tests/unit/test_cellprofiler_module_execution.py +++ b/tests/unit/test_cellprofiler_module_execution.py @@ -9432,11 +9432,13 @@ def test_pattern_group_runtime_unstacks_aligned_image_stack_output(): ) runtime = _pattern_group_runtime_for_output_memory("numpy") - output = runtime._validate_and_unstack(aligned, loaded) - - assert output == [dna_payload, rna_payload] + output = runtime._project_output_slices(aligned, loaded.matching_files) + stack_payload = aligned.copy_projected_output_stack( + output, memory_type="numpy", device_id=None, + ) + assert [payload for payload, _context in output] == [dna_payload, rna_payload] assert image_payload_metadata( - output.stack_payload + stack_payload ).source_plane_intensity_scales == (65535, 65535) @@ -9488,7 +9490,8 @@ def test_pattern_group_runtime_does_not_invent_nested_axes_from_image_rank(): ) runtime = _pattern_group_runtime_for_output_memory("numpy") - output_slices = runtime._validate_and_unstack(aligned, loaded) + projected = runtime._project_output_slices(aligned, loaded.matching_files) + output_slices = tuple(payload for payload, _context in projected) assert len(output_slices) == 2 np.testing.assert_array_equal(image_payload_data(output_slices[0]), first_site.data) @@ -9514,10 +9517,11 @@ def test_pattern_group_runtime_leaves_variable_shape_aligned_outputs_uncached(): ) runtime = _pattern_group_runtime_for_output_memory("numpy") - output = runtime._validate_and_unstack(aligned, loaded) - - assert output == [first, second] - assert output.stack_payload is None + output = runtime._project_output_slices(aligned, loaded.matching_files) + assert [payload for payload, _context in output] == [first, second] + assert aligned.copy_projected_output_stack( + output, memory_type="numpy", device_id=None, + ) is None def test_cellprofiler_main_flow_output_preserves_input_source_planes(): diff --git a/tests/unit/test_function_artifact_outputs.py b/tests/unit/test_function_artifact_outputs.py index 7bbeb49d34..6ddc20f5ce 100644 --- a/tests/unit/test_function_artifact_outputs.py +++ b/tests/unit/test_function_artifact_outputs.py @@ -1752,23 +1752,20 @@ def rgb_output(image): component_key=None, ) - output = runtime._validate_and_unstack( - scalar_output, - PatternGroupData( - matching_files=["source-1.tif", "source-2.tif"], - main_data_stack=np.zeros((2, 2, 3), dtype=np.uint16), - ), - ) - - assert output.slices == (scalar_output,) - assert output.slice_contexts == ( + output = runtime._project_output_slices(scalar_output, ["source-1.tif", "source-2.tif"]) + assert tuple(payload for payload, _context in output) == (scalar_output,) + assert tuple(context for _payload, context in output) == ( AlignedImageSliceContext.main_flow( "RGBImage", artifact_kind=ImageArtifactType.value, ), ) - assert image_payload_data(output.stack_payload).shape == (2, 3, 3) - stack_metadata = image_payload_metadata(output.stack_payload) + from openhcs.core.aligned_image_payload import ImagePayloadStackComposition + stack_payload = ImagePayloadStackComposition.copy_whole_image( + scalar_output, memory_type=MEMORY_TYPE_NUMPY, device_id=None, + ) + assert image_payload_data(stack_payload).shape == (2, 3, 3) + stack_metadata = image_payload_metadata(stack_payload) assert stack_metadata.plane_axis is None assert stack_metadata.source_channel_axis == -1 @@ -1794,17 +1791,10 @@ def test_pattern_group_runtime_uses_declared_output_slice_cardinality(): component_key=None, ) - output = runtime._validate_and_unstack( - declared_output, - PatternGroupData( - matching_files=["source-1.tif", "source-2.tif"], - main_data_stack=np.zeros((2, 4, 5), dtype=np.float32), - ), - ) - - assert len(output.slices) == 1 - assert image_payload_data(output.slices[0]).shape == (4, 5) - assert output.stack_payload is declared_output + output = runtime._project_output_slices(declared_output, ["source-1.tif", "source-2.tif"]) + assert len(output) == 1 + assert image_payload_data(output[0][0]).shape == (4, 5) + assert image_payload_metadata(declared_output).plane_axis is RuntimePlaneAxis.RUNTIME_SLICE def test_pattern_group_runtime_projects_nominal_object_label_stack(): @@ -1841,28 +1831,21 @@ def test_pattern_group_runtime_projects_nominal_object_label_stack(): component_key=None, ) - output = runtime._validate_and_unstack( - declared_output, - PatternGroupData( - matching_files=["source-anchor.tif"], - main_data_stack=np.zeros((2, 2, 3), dtype=np.float32), - ), - ) - - assert output.stack_payload is declared_output - assert len(output.slices) == 2 - assert all(isinstance(value, ObjectLabelSet) for value in output.slices) - assert [value.name for value in output.slices] == ["SavedChildren"] * 2 - assert [value.plane_axis for value in output.slices] == [None, None] - assert [value.domain.scope for value in output.slices] == [ + projected = runtime._project_output_slices(declared_output, ["source-anchor.tif"]) + output = tuple(payload for payload, _context in projected) + 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.domain.scope for value in output] == [ ObjectLabelDomainScope.PAYLOAD, ObjectLabelDomainScope.PAYLOAD, ] - assert [value.domain.declared_object_ids for value in output.slices] == [ + assert [value.domain.declared_object_ids for value in output] == [ (1,), (2,), ] - for output_slice, expected in zip(output.slices, label_planes, strict=True): + for output_slice, expected in zip(output, label_planes, strict=True): np.testing.assert_array_equal(output_slice.labels, expected) diff --git a/tests/unit/test_function_runtime_source_projection.py b/tests/unit/test_function_runtime_source_projection.py index fc4fc52482..7548c613f0 100644 --- a/tests/unit/test_function_runtime_source_projection.py +++ b/tests/unit/test_function_runtime_source_projection.py @@ -5268,10 +5268,11 @@ def test_function_output_path_keeps_payload_split_axis_over_input_alignment( def test_save_outputs_positional_lowering_preserves_explicit_payload_identity( tmp_path: Path, ) -> None: - from openhcs.core.steps.function_runtime import ( - PatternGroupOutputData, - PatternGroupRuntime, - ) + from openhcs.core.steps.function_runtime import PatternGroupRuntime + from openhcs.core.runtime_stack_cache import RuntimeImageStackCache + from openhcs.core.compiled_step_plan import CompiledStepPlan + from openhcs.core.component_group_scope import ComponentGroupScope + from openhcs.core.function_patterns import compile_function_pattern class OutputFileManager: saved_payloads: list[object] = [] @@ -5303,13 +5304,21 @@ def save_batch( parser=SourceSchemaFilenameParser(), ), runtime_function_output_identity_cache=FunctionOutputIdentityCache(), + runtime_image_stack_cache=RuntimeImageStackCache(), ), - execution_plan=SimpleNamespace( + compiled_group=compile_function_pattern(lambda image: image, {}, {}).default_group, + component_key=None, + execution_plan=CompiledStepPlan( + step_index=0, + step_type="FunctionStep", + axis_id="A01", output_dir=tmp_path, + output_memory_type="numpy", variable_components=(VariableComponents.SITE,), step_name="ExplicitIdentity", pipeline_position=0, step_scope_id="explicit-identity", + execution_group_scope=ComponentGroupScope.ungrouped(), ), pattern_group_info="A01_s{iii}_w1_z001_t001.tif", ) @@ -5326,7 +5335,7 @@ def save_batch( ).payload_with(np.zeros((4, 5), dtype=np.float32), None) records = runtime._save_outputs( - PatternGroupOutputData(slices=[payload]), + payload, ["A01_s003_w1_z001_t001.tif"], ) diff --git a/tests/unit/test_opaque_main_flow_image_domain.py b/tests/unit/test_opaque_main_flow_image_domain.py index 6553bf96ad..acb8dd3a9f 100644 --- a/tests/unit/test_opaque_main_flow_image_domain.py +++ b/tests/unit/test_opaque_main_flow_image_domain.py @@ -20,7 +20,7 @@ from openhcs.core.runtime_slice_projection import RuntimeSliceProjection from openhcs.core.source_spatial_domain import SourceSpatialDomain from openhcs.core.steps.function_runtime import PatternGroupData, PatternGroupRuntime -from openhcs.core.aligned_image_payload import AlignedImageStack, ImageOutputBundle, AlignedImageSliceContext +from openhcs.core.aligned_image_payload import AlignedImageStack, ImageOutputBundle, AlignedImageSliceContext, ImagePayloadStackComposition from openhcs.core.context.processing_context import ProcessingContext from openhcs.core.steps.function_output_manifest import step_output_manifest, StepOutputManifestStore from openhcs.core.source_bindings import CompiledSourceBindingPlan, SourceBindingRuntimeContext @@ -75,16 +75,14 @@ def test_opaque_whole_image_is_copied_without_a_new_execution_axis(shape, channe ), ) payload = metadata.payload_with(pixels, mask) - output = _runtime()._validate_and_unstack( - payload, - PatternGroupData( - matching_files=["/input/A01_s001_w1_z001_t001.tif", "/input/A01_s001_w1_z002_t001.tif"], - main_data_stack=np.zeros((2, 4, 5), dtype=np.float32), - ), + projected = _runtime()._project_output_slices( + payload, ["/input/A01_s001_w1_z001_t001.tif", "/input/A01_s001_w1_z002_t001.tif"], + ) + assert len(projected) == 1 + assert projected[0][0] is payload + cached = ImagePayloadStackComposition.copy_whole_image( + payload, memory_type=MEMORY_TYPE_NUMPY, device_id=None, ) - assert len(output.slices) == 1 - assert output.slices[0] is payload - cached = output.stack_payload assert image_payload_data(cached).shape == shape assert image_payload_metadata(cached) == metadata np.testing.assert_array_equal(image_payload_data(cached), pixels) @@ -105,14 +103,11 @@ def test_opaque_whole_image_is_copied_without_a_new_execution_axis(shape, channe 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) - output = _runtime()._validate_and_unstack( - payload, - PatternGroupData(matching_files=["source-1.tif", "source-2.tif"], main_data_stack=pixels), - ) - assert len(output.slices) == count - assert output.stack_payload is payload - assert image_payload_data(output.stack_payload).shape == (count, 4, 5) - assert image_payload_metadata(output.stack_payload).plane_axis is RuntimePlaneAxis.RUNTIME_SLICE + projected = _runtime()._project_output_slices(payload, ["source-1.tif", "source-2.tif"]) + assert len(projected) == count + assert all(image_payload_metadata(value).plane_axis is None for value, _context in projected) + assert image_payload_data(payload).shape == (count, 4, 5) + assert image_payload_metadata(payload).plane_axis is RuntimePlaneAxis.RUNTIME_SLICE @pytest.mark.parametrize("named", [False, True]) @@ -122,16 +117,17 @@ def test_nominal_alignment_owner_preserves_single_output_topology(named, axis): 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,)) - output = _runtime()._validate_and_unstack( - value, PatternGroupData(matching_files=["source.tif"], main_data_stack=payload), + projected = _runtime()._project_output_slices(value, ["source.tif"]) + 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: 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 - assert image_payload_data(output.stack_payload).shape == expected_shape - assert image_payload_metadata(output.stack_payload).plane_axis is expected_axis + assert image_payload_data(stack_payload).shape == expected_shape + assert image_payload_metadata(stack_payload).plane_axis is expected_axis @pytest.mark.parametrize("count", [1, 3]) @@ -140,10 +136,12 @@ def test_raw_array_fallback_keeps_explicit_runtime_unstack_contract(count): runtime.request.context = ProcessingContext(axis_id="A01") runtime.request.context.microscope_handler = SimpleNamespace(parser=SourceSchemaFilenameParser()) pixels = np.arange(count * 4 * 5, dtype=np.float32).reshape(count, 4, 5) - output = runtime._validate_and_unstack( - pixels, PatternGroupData(matching_files=[f"source-{i}.tif" for i in range(count)], main_data_stack=pixels), + projected = runtime._project_output_slices(pixels, [f"source-{i}.tif" for i in range(count)]) + payloads = tuple(payload for payload, _context in projected) + cached = ImagePayloadStackComposition.with_saved_output_context( + pixels, payloads, [image_payload_metadata(payload) for payload in payloads], + single_output_plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, ) - cached = output.cache_payload_for_outputs(output.slices, [image_payload_metadata(p) for p in output.slices]) assert image_payload_data(cached).shape == (count, 4, 5) assert image_payload_metadata(cached).plane_axis is RuntimePlaneAxis.RUNTIME_SLICE @@ -200,9 +198,9 @@ def test_save_and_next_load_preserve_domain_and_independent_cache(tmp_path, monk context.microscope_handler = SimpleNamespace(parser=SourceSchemaFilenameParser()) producer.request.context = context producer.pattern_repr = "whole" - output = producer._validate_and_unstack( - ImageOutputBundle((payload,), (AlignedImageSliceContext.main_flow("WholeImage"),)) if axis is not None or raw_named else payload, - PatternGroupData(matching_files=[path, "/input/A01_s001_w1_z002_t001.tif"], main_data_stack=pixels), + output = ( + ImageOutputBundle((payload,), (AlignedImageSliceContext.main_flow("WholeImage"),)) + if axis is not None or raw_named else payload ) records = producer._save_outputs(output, [path]) manifest = step_output_manifest(context) @@ -251,10 +249,11 @@ def test_mixed_named_outputs_retain_each_original_axis_before_projection(runtime planes = ImagePayloadMetadata(plane_axis=RuntimePlaneAxis.RUNTIME_SLICE).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) - output = _runtime()._validate_and_unstack(bundle, PatternGroupData(matching_files=[], main_data_stack=planes)) - assert output.stack_payload is None - assert [output.plane_axis_for_output_context(c) for c in output.slice_contexts] == [RuntimePlaneAxis.RUNTIME_SLICE] * runtime_count + [None] - assert output.main_flow_source is bundle + 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] def test_passed_through_record_preserves_collapsed_and_filename_coordinates(tmp_path): @@ -357,11 +356,10 @@ def test_saved_mixed_named_cohort_rejects_joint_load_and_preserves_selected_doma # A prior homogeneous producer caches the same physical paths. Mixed replacement # must invalidate that cache rather than leave a false joint runtime domain. old_bundle = ImageOutputBundle((planes, declared_metadata.payload_with(opaque_pixels[None])), contexts) - old_records = producer._save_outputs(producer._validate_and_unstack(old_bundle, loaded), [source, source]) + old_records = producer._save_outputs(old_bundle, [source, source]) assert context.runtime_image_stack_cache.get(tuple(r.output_path for r in old_records), memory_type=MEMORY_TYPE_NUMPY) is not None bundle = ImageOutputBundle((planes, opaque), contexts) - output = producer._validate_and_unstack(bundle, loaded) - records = producer._save_outputs(output, [source, source]) + records = producer._save_outputs(bundle, [source, source]) 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) @@ -401,7 +399,8 @@ def test_multiple_unprojected_named_binding_domains_fail_existing_bundle_grammar payloads = tuple(ImagePayloadMetadata(plane_axis=RuntimePlaneAxis.SOURCE_BINDING).payload_with( np.ones((1, 4, 5), dtype=np.float32)) for _ in contexts) with pytest.raises(ValueError, match="plane axis to be projected"): - _runtime()._validate_and_unstack(ImageOutputBundle(payloads, contexts), PatternGroupData(matching_files=[], main_data_stack=payloads[0])) + bundle = ImageOutputBundle(payloads, contexts) + bundle.copy_projected_output_stack(tuple(bundle.projected_output_slices()), memory_type=MEMORY_TYPE_NUMPY, device_id=None) def test_opaque_named_bundle_uses_existing_same_slice_mask_composition(): @@ -413,11 +412,12 @@ def test_opaque_named_bundle_uses_existing_same_slice_mask_composition(): payloads = tuple(ImagePayloadMetadata(source_image_names=(name,)).payload_with( np.ones((4, 5), dtype=np.float32) * i, mask) for i, (name, mask) in enumerate(zip(("First", "Second"), (first_mask, second_mask), strict=True))) - output = _runtime()._validate_and_unstack(ImageOutputBundle(payloads, contexts), PatternGroupData(matching_files=[], main_data_stack=payloads[0])) + 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 image_payload_metadata(output.stack_payload).plane_axis is RuntimePlaneAxis.SOURCE_BINDING - np.testing.assert_array_equal(image_payload_data(output.stack_payload), image_payload_data(expected)) - np.testing.assert_array_equal(image_payload_mask(output.stack_payload), image_payload_mask(expected)) + assert image_payload_metadata(stack_payload).plane_axis is RuntimePlaneAxis.SOURCE_BINDING + np.testing.assert_array_equal(image_payload_data(stack_payload), image_payload_data(expected)) + np.testing.assert_array_equal(image_payload_mask(stack_payload), image_payload_mask(expected)) def test_input_bundle_composition_retains_lazy_plan_device_resolution(tmp_path, monkeypatch): diff --git a/tests/unit/test_output_publication_ownership.py b/tests/unit/test_output_publication_ownership.py new file mode 100644 index 0000000000..a8984c7f24 --- /dev/null +++ b/tests/unit/test_output_publication_ownership.py @@ -0,0 +1,157 @@ +"""Real output publication keeps buffer preparation and commit effects ordered.""" + +from dataclasses import replace +from types import SimpleNamespace + +import numpy as np +import pytest +from polystore.filemanager import FileManager +from polystore.memory import MemoryStorageBackend + +from openhcs.core.aligned_image_payload import ( + AlignedImageSliceContext, + AlignedImageStack, + ImageOutputBundle, + ImagePayloadStackComposition, +) +from openhcs.core.compiled_step_plan import CompiledStepPlan +from openhcs.core.component_group_scope import ComponentGroupScope +from openhcs.core.context.processing_context import ProcessingContext +from openhcs.core.function_patterns import compile_function_pattern +from openhcs.core.runtime_image_values import ImagePayloadMetadata, image_payload_data +from openhcs.core.steps.function_output_manifest import step_output_manifest +from openhcs.core.steps.function_runtime import PatternGroupRuntime +from openhcs.microscopes.source_schema import SourceSchemaFilenameParser + + +SOURCE = "/source/A01_s001_w1_z001_t001.tif" + + +def _identity(image): + return image + + +def _runtime(tmp_path): + filemanager = FileManager({"memory": MemoryStorageBackend()}) + context = ProcessingContext(axis_id="A01", filemanager=filemanager) + context.microscope_handler = SimpleNamespace(parser=SourceSchemaFilenameParser()) + plan = CompiledStepPlan( + step_index=0, + step_name="OutputOwnership", + step_type="FunctionStep", + step_scope_id="output-ownership", + axis_id="A01", + input_memory_type="numpy", + output_memory_type="numpy", + output_dir=tmp_path, + variable_components=(), + artifact_inputs={}, + artifact_outputs={}, + execution_group_scope=ComponentGroupScope.ungrouped(), + ) + runtime = PatternGroupRuntime(SimpleNamespace( + context=context, + execution_plan=plan, + compiled_group=compile_function_pattern(_identity, {}, {}).default_group, + component_key=None, + pattern_group_info="A01_s001_w1_z001_t001.tif", + )) + payload = ImagePayloadMetadata( + source_path=SOURCE, + source_component_metadata={ + "well": "A01", "site": "1", "channel": "1", "z_index": "1", "timepoint": "1", + }, + ).payload_with(np.ones((4, 5), dtype=np.float32)) + return runtime, payload + + +def test_buffer_preparation_failure_precedes_any_vfs_publication(tmp_path, monkeypatch): + runtime, payload = _runtime(tmp_path) + files = runtime.request.context.filemanager + events = [] + + def fail_copy(*args, **kwargs): + events.append("copy") + raise ValueError("buffer allocation failed") + + def unexpected_exists(*args, **kwargs): + pytest.fail("output path access must follow independent buffer preparation") + + monkeypatch.setattr(ImagePayloadStackComposition, "copy_whole_image", staticmethod(fail_copy)) + monkeypatch.setattr(files, "exists", unexpected_exists) + with pytest.raises(ValueError, match="buffer allocation failed"): + runtime._save_outputs(payload, [SOURCE]) + assert events == ["copy"] + assert runtime.request.context.runtime_image_stack_cache.stacks == {} + + +def test_saved_metadata_failure_keeps_actual_commit_but_no_cache_or_manifest(tmp_path, monkeypatch): + runtime, payload = _runtime(tmp_path) + files = runtime.request.context.filemanager + path = str(tmp_path / "A01_s001_w1_z001_t001.tif") + + def fail_attachment(*args, **kwargs): + assert files.load(path, "memory") is args[1][0] + assert np.shares_memory(image_payload_data(args[1][0]), payload.data) + raise ValueError("saved context failed") + + monkeypatch.setattr(ImagePayloadStackComposition, "with_saved_output_context", staticmethod(fail_attachment)) + with pytest.raises(ValueError, match="saved context failed"): + runtime._save_outputs(payload, [SOURCE]) + assert np.shares_memory(image_payload_data(files.load(path, "memory")), payload.data) + assert runtime.request.context.runtime_image_stack_cache.stacks == {} + assert step_output_manifest(runtime.request.context).produced_records_for(runtime.request.execution_plan) == () + + +def test_duplicate_output_paths_leave_existing_memory_value_and_cache_intact(tmp_path): + runtime, payload = _runtime(tmp_path) + context = runtime.request.context + path = str(tmp_path / "A01_s001_w1_z001_t001.tif") + previous = np.full((4, 5), 9, dtype=np.float32) + context.filemanager.ensure_directory(str(tmp_path), "memory") + context.filemanager.save(previous, path, "memory") + context.runtime_image_stack_cache.store((path,), memory_type="numpy", stack=previous) + output = AlignedImageStack((payload, replace(payload))) + with pytest.raises(ValueError, match="duplicate output path"): + runtime._save_outputs(output, [SOURCE, SOURCE]) + assert context.filemanager.load(path, "memory") is previous + assert context.runtime_image_stack_cache.get((path,), memory_type="numpy") is previous + + +def test_original_named_owner_is_consulted_at_saved_metadata_epoch(tmp_path): + runtime, payload = _runtime(tmp_path) + files = runtime.request.context.filemanager + path = str(tmp_path / "A01_s001_w1_z001_t001_Named.tif") + calls = [] + + class EpochSensitiveBundle(ImageOutputBundle): + def plane_axis_for_output_context(self, context): + calls.append(files.exists(path, "memory")) + if calls[-1]: + raise ValueError("post-save domain rejected") + return super().plane_axis_for_output_context(context) + + output = EpochSensitiveBundle((payload,), (AlignedImageSliceContext.main_flow("Named"),)) + with pytest.raises(ValueError, match="post-save domain rejected"): + runtime._save_outputs(output, [SOURCE]) + assert calls == [False, True] + np.testing.assert_array_equal(image_payload_data(files.load(path, "memory")), payload.data) + assert runtime.request.context.runtime_image_stack_cache.stacks == {} + + +def test_anonymous_context_is_shared_by_record_and_post_save_axis_hooks(tmp_path): + runtime, payload = _runtime(tmp_path) + calls = [] + + class AnonymousStack(AlignedImageStack): + def plane_axis_for_output_context(self, context): + calls.append(context) + return super().plane_axis_for_output_context(context) + + output = AnonymousStack((payload,)) + (record,) = runtime._save_outputs(output, [SOURCE]) + assert calls[0] is None + assert len(calls) == 3 + assert calls[1].is_anonymous_main_flow + assert calls[2] is calls[1] + assert record.output_context == calls[1] From fea586b9fd74a812e8bb0f55c27e9c5af1323589 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 10:11:48 -0400 Subject: [PATCH 134/235] refactor(publication): reconcile metadata through one document owner --- openhcs/core/source_projection.py | 14 ++++ openhcs/core/source_workspace_projection.py | 25 ++++-- openhcs/core/steps/function_outputs.py | 19 +---- openhcs/microscopes/openhcs.py | 66 +++++++++++++++- .../test_persisted_roi_only_publication.py | 76 +++++++++++++++++++ 5 files changed, 177 insertions(+), 23 deletions(-) diff --git a/openhcs/core/source_projection.py b/openhcs/core/source_projection.py index 8711e50c62..d6c270b616 100644 --- a/openhcs/core/source_projection.py +++ b/openhcs/core/source_projection.py @@ -632,6 +632,12 @@ class SourceProjection: image_metadata: ClassVar[ImagePayloadMetadata | None] = None execution_scope: ClassVar[RuntimeExecutionAxisScope | None] = None + def artifact_result_directory( + self, virtual_path: str, backend: str + ) -> Path | None: + """Return a declared artifact destination, if this role owns one.""" + return None + @property def identity_key(self) -> tuple[object, ...]: """Return the projection identity enforced within one source set.""" @@ -802,6 +808,14 @@ class SourceArtifactProjection(SourceProjection): image_metadata: ImagePayloadMetadata | None = None execution_scope: RuntimeExecutionAxisScope | None = None + def artifact_result_directory( + self, virtual_path: str, backend: str + ) -> Path | None: + """Use the persisted virtual path, not the source pixel reference address.""" + if self.ref.backend != backend: + return None + return Path(virtual_path).parent + def __post_init__(self) -> None: normalized_alias = str(self.source_alias).strip() if not normalized_alias: diff --git a/openhcs/core/source_workspace_projection.py b/openhcs/core/source_workspace_projection.py index 0b3216e025..677f214007 100644 --- a/openhcs/core/source_workspace_projection.py +++ b/openhcs/core/source_workspace_projection.py @@ -779,11 +779,9 @@ class VirtualWorkspaceSourceProjectionBuilder: def ingest_subdirectory(self, subdirectory: OpenHCSSubdirectoryPayload) -> None: workspace_mapping = VirtualWorkspaceMapping.from_subdirectory(subdirectory) self.ingest_workspace_mapping(workspace_mapping) - self.ingest_source_projections( - VirtualWorkspaceSourceProjectionEntries.from_subdirectory(subdirectory) - ) - self.ingest_source_metadata( - VirtualWorkspaceSourceMetadataEntries.from_subdirectory(subdirectory), + self.ingest_admitted_subdirectory( + subdirectory, + VirtualWorkspaceSourceProjectionEntries.from_subdirectory(subdirectory), ) def ingest_workspace_mapping( @@ -805,10 +803,27 @@ def record_workspace_source_path( self.workspace_source_refs[virtual_path] = source_ref self.workspace_source_refs[loadable_path] = source_ref + def ingest_admitted_subdirectory( + self, + subdirectory: OpenHCSSubdirectoryPayload, + source_projections: VirtualWorkspaceSourceProjectionEntries, + ) -> None: + """Ingest admitted projections, then their correlated source fields. + + Workspace mappings must already be ingested. Raw readers admit each + projection after its mapping; reconciliation admits the whole document's + projection records before any workspace fields. Both share this tail. + """ + self.ingest_source_projections(source_projections) + self.ingest_source_metadata( + VirtualWorkspaceSourceMetadataEntries.from_subdirectory(subdirectory) + ) + def ingest_source_projections( self, source_projections: VirtualWorkspaceSourceProjectionEntries, ) -> None: + """Ingest a projection-only source authority after its workspace mapping.""" for virtual_path, projection in source_projections.entries.items(): mapped_ref = self.workspace_source_refs.get(virtual_path) if mapped_ref is None: diff --git a/openhcs/core/steps/function_outputs.py b/openhcs/core/steps/function_outputs.py index 7297a44416..6f2c590ab9 100644 --- a/openhcs/core/steps/function_outputs.py +++ b/openhcs/core/steps/function_outputs.py @@ -86,8 +86,6 @@ from openhcs.core.virtual_workspace_metadata import ( METADATA_CONFIG, AtomicMetadataWriter, - OpenHCSMetadataSubdirectories, - VirtualWorkspaceSourceProjectionEntries, ) from openhcs.microscopes.microscope_interfaces import FilenameParser @@ -1220,28 +1218,15 @@ def reconciliation_targets( """Use durable typed projections, without reloading cleaned artifact values.""" from openhcs.microscopes.openhcs import OpenHCSMetadataHandler - subdirectories = OpenHCSMetadataSubdirectories.from_path( - METADATA_CONFIG.metadata_path(self.plate_root) - ) - directories = tuple( - Path(self.plate_root) / Path(path).parent - for _name, subdirectory in subdirectories.items() - for path, projection in VirtualWorkspaceSourceProjectionEntries.from_subdirectory( - subdirectory - ).entries.items() - if isinstance(projection, SourceArtifactProjection) - and projection.ref.backend == self.backend - ) - result_directories = OpenHCSMetadataHandler( + directories = OpenHCSMetadataHandler( context.filemanager - ).analysis_result_directories(self.plate_root) + ).reconciliation_directories(self.plate_root, self.backend) return tuple( self.for_directory(directory) for directory in dict.fromkeys( ( self.output_dir, *directories, - *(directory.path for directory in result_directories), ) ) ) diff --git a/openhcs/microscopes/openhcs.py b/openhcs/microscopes/openhcs.py index 08f98ca4c4..c9392894c8 100644 --- a/openhcs/microscopes/openhcs.py +++ b/openhcs/microscopes/openhcs.py @@ -29,7 +29,10 @@ SourceComponentProjectionStrategy, SourceVoxelSpacing, ) -from openhcs.core.source_workspace_projection import VirtualWorkspaceSourceProjection +from openhcs.core.source_workspace_projection import ( + VirtualWorkspaceSourceProjection, + VirtualWorkspaceSourceProjectionBuilder, +) from metaclass_registry import AutoRegisterMeta from polystore.exceptions import MetadataNotFoundError from polystore.filemanager import FileManager @@ -39,6 +42,9 @@ FIELDS, METADATA_CONFIG, MetadataWriteError, + OpenHCSMetadataSubdirectories, + VirtualWorkspaceMapping, + VirtualWorkspaceSourceProjectionEntries, component_metadata_field, get_metadata_path, ) @@ -381,7 +387,65 @@ def analysis_result_directories( plate_root, metadata_document ) ) + return self._analysis_result_directories( + plate_root, subdirectories, source_projection + ) + + def reconciliation_directories( + self, plate_path: Union[str, Path], backend: str + ) -> tuple[Path, ...]: + """Derive artifact and result destinations from one admitted document. + Projection records are admitted before workspace fields, as required by + completed-plate reconciliation. The same admitted records then populate + the result-directory source authority; they are not decoded a second time. + """ + plate_root = Path(plate_path) + metadata_path = METADATA_CONFIG.metadata_path(plate_root) + if not metadata_path.is_file(): + return tuple( + directory.path + for directory in self.analysis_result_directories(plate_root) + ) + document = OpenHCSMetadataSubdirectories.from_path(metadata_path) + admitted = tuple( + ( + subdirectory, + VirtualWorkspaceSourceProjectionEntries.from_subdirectory(subdirectory), + ) + for _name, subdirectory in document.items() + ) + directories = tuple( + plate_root / directory + for _subdirectory, entries in admitted + for path, projection in entries.entries.items() + if (directory := projection.artifact_result_directory(path, backend)) + is not None + ) + subdirectories = self._metadata_subdirectories(document.metadata, plate_root) + source_projection = None + if document.has_workspace_mapping(): + builder = VirtualWorkspaceSourceProjectionBuilder(plate_root) + for subdirectory, entries in admitted: + builder.ingest_workspace_mapping( + VirtualWorkspaceMapping.from_subdirectory(subdirectory) + ) + builder.ingest_admitted_subdirectory(subdirectory, entries) + source_projection = builder.projection() + results = self._analysis_result_directories( + plate_root, subdirectories, source_projection + ) + return tuple( + dict.fromkeys((*directories, *(directory.path for directory in results))) + ) + + def _analysis_result_directories( + self, + plate_root: Path, + subdirectories: Mapping[str, Mapping[str, Any]], + source_projection: VirtualWorkspaceSourceProjection | None, + ) -> tuple[AnalysisResultDirectory, ...]: + """Admit declared result paths against their document's source authority.""" result_directories = [] for subdirectory_name, subdirectory_data in subdirectories.items(): result_dir_name = _optional_metadata_field(subdirectory_data, "results_dir") diff --git a/tests/unit/test_persisted_roi_only_publication.py b/tests/unit/test_persisted_roi_only_publication.py index 7338a41787..216042a27d 100644 --- a/tests/unit/test_persisted_roi_only_publication.py +++ b/tests/unit/test_persisted_roi_only_publication.py @@ -244,6 +244,82 @@ def test_shared_writer_accepts_result_only_but_not_unaddressed_images( assert metadata_path.read_bytes() == before +def test_reconciliation_keeps_artifact_destination_without_results_field( + tmp_path, publication_context +): + from polystore.virtual_workspace import SourcePixelRef + + from openhcs.core.source_projection import ( + OpenHCSPlaneAddress, + SourceArtifactProjection, + SourceProjectionMetadataSerializer, + ) + from openhcs.microscopes.openhcs import OpenHCSMetadataHandler + + plate = tmp_path / "plate" + plate.mkdir() + projection = SourceArtifactProjection( + address=OpenHCSPlaneAddress.from_values( + well="A01", site="1", channel="2", z_index="1", timepoint="1" + ), + ref=SourcePixelRef(Backend.DISK.value, "physical/acquisition-source.tif"), + source_alias="Declared", + artifact_kind=MetadataArtifactType, + ) + fields = SourceProjectionMetadataSerializer( + SourceSchemaFilenameParser() + ).projection_fields(((projection, "nested/declared-result.tif"),)) + metadata_path = METADATA_CONFIG.metadata_path(plate) + metadata_path.write_text(json.dumps({"subdirectories": {"nested": fields}})) + before = metadata_path.read_bytes() + handler = OpenHCSMetadataHandler(publication_context.filemanager) + assert handler.analysis_result_directories(plate) == () + owner = RuntimeArtifactMetadataTarget.from_plan(plan_for(plate)) + assert plate / "nested" in tuple( + target.output_dir for target in owner.reconciliation_targets(publication_context) + ) + assert metadata_path.read_bytes() == before + + +@pytest.mark.parametrize("later_subdirectory", [False, True]) +def test_reconciliation_admits_all_projection_rows_before_workspace_fields( + tmp_path, publication_context, later_subdirectory +): + plate = tmp_path / "plate" + plate.mkdir() + invalid_row = { + "virtual_path": "images/unregistered.tif", + "projection_role": "unknown", + } + subdirectories = {"images": {"workspace_mapping": "invalid"}} + destination = "later" if later_subdirectory else "images" + subdirectories.setdefault(destination, {})["source_projection"] = [invalid_row] + metadata_path = METADATA_CONFIG.metadata_path(plate) + metadata_path.write_text(json.dumps({"subdirectories": subdirectories})) + before = metadata_path.read_bytes() + owner = RuntimeArtifactMetadataTarget.from_plan(plan_for(plate)) + with pytest.raises(RuntimeError, match="unknown projection_role"): + owner.reconciliation_targets(publication_context) + assert metadata_path.read_bytes() == before + + +def test_reconciliation_preserves_raw_json_error_and_missing_document_error( + tmp_path, publication_context +): + from polystore.exceptions import MetadataNotFoundError + + plate = tmp_path / "plate" + plate.mkdir() + metadata_path = METADATA_CONFIG.metadata_path(plate) + owner = RuntimeArtifactMetadataTarget.from_plan(plan_for(plate)) + with pytest.raises(MetadataNotFoundError): + owner.reconciliation_targets(publication_context) + metadata_path.write_text("{") + with pytest.raises(json.JSONDecodeError): + owner.reconciliation_targets(publication_context) + assert metadata_path.read_text() == "{" + + def test_new_result_declaration_composes_cooperative_hooks_without_consumer_edits( tmp_path, publication_context, From 042c24d7b04d397365ab63924f530b2bfde59c74 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 10:20:45 -0400 Subject: [PATCH 135/235] refactor(runtime): fold invocation scope into its executor owner --- openhcs/core/function_patterns.py | 6 +- openhcs/core/steps/function_runtime.py | 21 +-- .../test_function_core_executor_ownership.py | 150 ++++++++++++++++++ 3 files changed, 157 insertions(+), 20 deletions(-) create mode 100644 tests/unit/test_function_core_executor_ownership.py diff --git a/openhcs/core/function_patterns.py b/openhcs/core/function_patterns.py index ea9a8abe82..ab57008e0b 100644 --- a/openhcs/core/function_patterns.py +++ b/openhcs/core/function_patterns.py @@ -18,7 +18,7 @@ if TYPE_CHECKING: from openhcs.core.aligned_image_payload import AlignedImageSliceContext from openhcs.core.pipeline.compilation_session import CompilationPathResolver - from openhcs.core.steps.function_runtime import FunctionInvocationArtifactScope + from openhcs.core.steps.function_runtime import FunctionCoreExecutor from pyqt_reactive.pattern_metadata import PatternScopeToken from python_introspect import Enableable @@ -281,7 +281,7 @@ def requires_callable_binding(self) -> bool: def resolve_unstored_payload( self, - scope: "FunctionInvocationArtifactScope", + scope: "FunctionCoreExecutor", primary_source_payload: object, ) -> object: """Resolve source input through the existing exact-origin authority.""" @@ -342,7 +342,7 @@ class CompiledMetadataArtifactInputEdgePlan(InvocationArtifactInputEdgePlan): def resolve_unstored_payload( self, - scope: "FunctionInvocationArtifactScope", + scope: "FunctionCoreExecutor", primary_source_payload: object, ) -> object: return scope.invocation.artifact_parameter_value(self.spec) diff --git a/openhcs/core/steps/function_runtime.py b/openhcs/core/steps/function_runtime.py index be69be797f..74789f283a 100644 --- a/openhcs/core/steps/function_runtime.py +++ b/openhcs/core/steps/function_runtime.py @@ -1780,8 +1780,8 @@ def prepare_compiled_context_callables( @dataclass(frozen=True, slots=True) -class FunctionInvocationArtifactScope: - """Own exact invocation artifact sources and their callable argument binding.""" +class FunctionCoreExecutor: + """Execute one scoped callable invocation and route declared artifact I/O.""" runtime_scope: FunctionRuntimeScope invocation: CompiledFunctionInvocation @@ -1791,6 +1791,8 @@ class FunctionInvocationArtifactScope: ] group_key: str | None plane_projection: RuntimePlaneProjection + main_data_arg: RuntimeArrayData + source_memory_type: str @property def selected_artifact_input_edges( @@ -1967,21 +1969,6 @@ def load_artifact_input( return loaded_values -@dataclass(frozen=True, slots=True) -class FunctionCoreExecutor(FunctionInvocationArtifactScope): - """Execute one scoped callable invocation and route declared artifact I/O.""" - - runtime_scope: FunctionRuntimeScope - invocation: CompiledFunctionInvocation - artifacts: ComponentArtifactPlans[ - InvocationArtifactInputProjectionKey, - InvocationArtifactInputEdgePlan, - ] - group_key: str | None - plane_projection: RuntimePlaneProjection - main_data_arg: RuntimeArrayData - source_memory_type: str - def debug_cursor(self) -> DebugCursor: return DebugCursor.from_invocation( step_index=self.runtime_scope.execution_plan.step_index, diff --git a/tests/unit/test_function_core_executor_ownership.py b/tests/unit/test_function_core_executor_ownership.py new file mode 100644 index 0000000000..edfd6b1d89 --- /dev/null +++ b/tests/unit/test_function_core_executor_ownership.py @@ -0,0 +1,150 @@ +"""One executor owns invocation state and polymorphic artifact-edge resolution.""" + +from dataclasses import FrozenInstanceError, MISSING, fields, replace +import inspect +import pickle +from types import SimpleNamespace + +import numpy as np +import pytest + +from openhcs.core.artifacts import ArtifactSpec, ArtifactSpecCollection, MetadataArtifactType +from openhcs.core.compiled_step_plan import CompiledStepPlan +from openhcs.core.component_group_scope import ComponentGroupScope +from openhcs.core.function_patterns import ( + CompiledMetadataArtifactInputEdgePlan, + InvocationArtifactInputEdgePlan, + InvocationArtifactInputProjectionKey, + compile_function_pattern, +) +from openhcs.core.pipeline.function_contracts import artifact_inputs +from openhcs.core.runtime_plane_projection import RuntimePlaneProjection +from openhcs.core.source_bindings import CompiledSourceBindingPlan, SourceBindingRuntimeContext +from openhcs.core.steps.function_runtime import ( + ComponentArtifactPlans, + FunctionCoreExecutor, + FunctionRuntimeScope, +) + + +_FIRST = ArtifactSpec.input("First", MetadataArtifactType, parameter_name="first") +_SECOND = ArtifactSpec.input("Second", MetadataArtifactType, parameter_name="second") + + +@artifact_inputs(_FIRST, _SECOND) +def _consume_metadata(image, *, first, second): + return image + + +def _executor(): + first = {"value": 1} + second = {"value": 2} + pattern = compile_function_pattern((_consume_metadata, {"first": first, "second": second}), {}, {}) + invocation = next(pattern.iter_invocations()) + edges = tuple( + InvocationArtifactInputEdgePlan.from_source_declarations( + key=InvocationArtifactInputProjectionKey(invocation_key=invocation.key, input_index=index), + spec=spec, + main_flow_artifacts=ArtifactSpecCollection(()), + invocation_sources=ArtifactSpecCollection(()), + metadata_available=True, + ) + for index, spec in enumerate((_FIRST, _SECOND)) + ) + invocation = invocation.with_artifact_input_edges(edges) + plan = CompiledStepPlan( + step_index=0, + step_type="FunctionStep", + step_name="MetadataBinding", + step_scope_id="metadata-binding", + axis_id="A01", + input_memory_type="numpy", + output_memory_type="numpy", + execution_group_scope=ComponentGroupScope.ungrouped(), + source_binding_plan=CompiledSourceBindingPlan.empty(), + ) + artifacts = ComponentArtifactPlans(inputs={edge.key: edge for edge in edges}, outputs={}) + scope = FunctionRuntimeScope( + context=SimpleNamespace(axis_id="A01"), + execution_plan=plan, + compiled_group=replace(pattern.default_group, invocations=(invocation,)), + artifacts=ComponentArtifactPlans(inputs={}, outputs={}), + source_binding_context=SourceBindingRuntimeContext.empty(), + runtime_plane_index=0, + runtime_plane_count=1, + ) + return FunctionCoreExecutor( + scope, invocation, artifacts, None, RuntimePlaneProjection.stack(), + np.arange(6, dtype=np.float32).reshape(1, 2, 3), "numpy", + ) + + +def test_executor_retains_original_positional_constructor_and_frozen_slots(): + expected = ( + "runtime_scope", "invocation", "artifacts", "group_key", "plane_projection", + "main_data_arg", "source_memory_type", + ) + declaration = fields(FunctionCoreExecutor) + assert tuple(field.name for field in declaration) == expected + assert tuple(inspect.signature(FunctionCoreExecutor).parameters) == expected + assert all(field.init and field.compare and field.repr and not field.kw_only for field in declaration) + assert all(field.default is MISSING and field.default_factory is MISSING for field in declaration) + executor = _executor() + assert not hasattr(executor, "__dict__") + with pytest.raises(FrozenInstanceError): + executor.source_memory_type = "other" + replaced = replace(executor, group_key="changed") + assert replaced.group_key == "changed" + assert replaced.runtime_scope is executor.runtime_scope + assert replaced.artifacts is executor.artifacts + + +def test_executor_pickle_retains_real_scope_edges_and_ordered_field_state(): + executor = _executor() + original_state = executor.__getstate__() + assert len(original_state) == len(fields(FunctionCoreExecutor)) + assert original_state[0] is executor.runtime_scope + assert original_state[-1] == "numpy" + restored = pickle.loads(pickle.dumps(executor)) + assert type(restored) is FunctionCoreExecutor + assert restored.runtime_scope.execution_plan.step_scope_id == "metadata-binding" + assert tuple(edge.spec.ref() for edge in restored.selected_artifact_input_edges) == (_FIRST.ref(), _SECOND.ref()) + assert all(isinstance(edge, CompiledMetadataArtifactInputEdgePlan) for edge in restored.selected_artifact_input_edges) + assert restored.invocation.kwargs_dict == executor.invocation.kwargs_dict + np.testing.assert_array_equal(restored.main_data_arg, executor.main_data_arg) + assert not hasattr(restored, "__dict__") + + +def test_metadata_edge_dispatch_keeps_live_compiled_arguments_and_declaration_order(monkeypatch): + executor = _executor() + + def unexpected_source_resolution(*args, **kwargs): + pytest.fail("metadata edge owns its compiled argument; no image-source lookup") + + monkeypatch.setattr(FunctionCoreExecutor, "declared_source_payload", unexpected_source_resolution) + kwargs = {} + loaded = executor.load_artifact_inputs(kwargs, executor.main_data_arg) + assert tuple(kwargs) == ("first", "second") + assert tuple(loaded) == (_FIRST.ref(), _SECOND.ref()) + assert kwargs["first"] is executor.invocation.kwargs_dict["first"] + kwargs["first"]["value"] = 7 + repeated = {} + executor.load_artifact_inputs(repeated, executor.main_data_arg) + assert repeated["first"] is kwargs["first"] + assert repeated["first"]["value"] == 7 + + +def test_missing_metadata_argument_error_stays_on_selected_edge_before_later_binding(): + executor = _executor() + invocation = replace(executor.invocation, kwargs=(("second", {"value": 2}),)) + executor = replace(executor, invocation=invocation) + kwargs = {} + with pytest.raises(ValueError, match="Metadata artifact .* has no compiled parameter value"): + executor.load_artifact_inputs(kwargs, executor.main_data_arg) + assert kwargs == {} + + +def test_missing_storage_error_remains_owned_by_executor(): + executor = _executor() + with pytest.raises(ValueError, match="storage-backed edge"): + executor.load_artifact_input("first", executor.selected_artifact_input_edges[0]) From 0dd77da463b23bacd9765c3f7a19c8cac6eb06ec Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 10:36:06 -0400 Subject: [PATCH 136/235] fix(publication): separate ordinary checkpoint and named artifact identities --- openhcs/core/steps/function_runtime.py | 12 +++++- ...test_function_runtime_source_projection.py | 37 +++++++++++++++++-- 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/openhcs/core/steps/function_runtime.py b/openhcs/core/steps/function_runtime.py index 74789f283a..0a458a424a 100644 --- a/openhcs/core/steps/function_runtime.py +++ b/openhcs/core/steps/function_runtime.py @@ -3166,7 +3166,8 @@ def _save_outputs( unstack_started_at = time.perf_counter() projected_outputs = self._project_output_slices(processed_stack, matching_files) plan = self.request.execution_plan - if isinstance(processed_stack, AlignedImageStack): + explicit_output_surfaces = isinstance(processed_stack, AlignedImageStack) + if explicit_output_surfaces: stack_payload = processed_stack.copy_projected_output_stack( projected_outputs, memory_type=plan.output_memory_type, @@ -3261,7 +3262,14 @@ def plane_axis_for_output( ) from exc raise output_context = output_contexts[i] - if not output_context.is_anonymous_main_flow: + # Only explicit aligned output surfaces own filename qualifiers. + # An unwrapped canonical artifact still owns its typed source + # context, while its ordinary main-flow checkpoint keeps the + # source filename independently of named artifact materialization. + if ( + explicit_output_surfaces + and not output_context.is_anonymous_main_flow + ): output_identity = output_identity.with_filename_qualifier( output_context.output_key ) diff --git a/tests/unit/test_function_runtime_source_projection.py b/tests/unit/test_function_runtime_source_projection.py index 7548c613f0..7b75d20ffc 100644 --- a/tests/unit/test_function_runtime_source_projection.py +++ b/tests/unit/test_function_runtime_source_projection.py @@ -5265,14 +5265,17 @@ def test_function_output_path_keeps_payload_split_axis_over_input_alignment( assert identity.filename_component_values["site"] == 1 +@pytest.mark.parametrize("named_topology", ["anonymous", "unwrapped", "explicit"]) def test_save_outputs_positional_lowering_preserves_explicit_payload_identity( tmp_path: Path, + named_topology: str, ) -> None: from openhcs.core.steps.function_runtime import PatternGroupRuntime from openhcs.core.runtime_stack_cache import RuntimeImageStackCache from openhcs.core.compiled_step_plan import CompiledStepPlan from openhcs.core.component_group_scope import ComponentGroupScope from openhcs.core.function_patterns import compile_function_pattern + from openhcs.core.aligned_image_payload import AlignedImageStack class OutputFileManager: saved_payloads: list[object] = [] @@ -5295,6 +5298,17 @@ def save_batch( self.saved_payloads = payloads self.saved_paths = paths + output_plans = {} + func = lambda image: image + if named_topology != "anonymous": + spec = ArtifactSpec.output("Corrected", ImageArtifactType) + output_plan = ArtifactOutputPlan( + name=spec.name, + path=str(tmp_path / "Corrected.pkl"), + artifact_type=spec.artifact_type, + ) + output_plans[output_plan.ref()] = output_plan + func = artifact_outputs(spec)(func) filemanager = OutputFileManager() runtime = PatternGroupRuntime( SimpleNamespace( @@ -5306,7 +5320,7 @@ def save_batch( runtime_function_output_identity_cache=FunctionOutputIdentityCache(), runtime_image_stack_cache=RuntimeImageStackCache(), ), - compiled_group=compile_function_pattern(lambda image: image, {}, {}).default_group, + compiled_group=compile_function_pattern(func, {}, output_plans).default_group, component_key=None, execution_plan=CompiledStepPlan( step_index=0, @@ -5319,6 +5333,7 @@ def save_batch( pipeline_position=0, step_scope_id="explicit-identity", execution_group_scope=ComponentGroupScope.ungrouped(), + artifact_outputs=output_plans, ), pattern_group_info="A01_s{iii}_w1_z001_t001.tif", ) @@ -5334,12 +5349,22 @@ def save_batch( }, ).payload_with(np.zeros((4, 5), dtype=np.float32), None) + named_context = AlignedImageSliceContext.main_flow( + "Corrected", artifact_kind=ImageArtifactType.value, + ) + output = ( + AlignedImageStack((payload,), (named_context,)) + if named_topology == "explicit" else payload + ) records = runtime._save_outputs( - payload, + output, ["A01_s003_w1_z001_t001.tif"], ) - assert Path(filemanager.saved_paths[0]).name == "A01_s001_w1_z001_t001.tif" + expected = "A01_s001_w1_z001_t001" + if named_topology == "explicit": + expected += "_Corrected" + assert Path(filemanager.saved_paths[0]).name == expected + ".tif" assert ( image_payload_metadata(filemanager.saved_payloads[0]).source_component_metadata[ "site" @@ -5347,6 +5372,12 @@ def save_batch( == "1" ) assert records[0].component_values["site"] == 1 + if named_topology != "anonymous": + assert records[0].output_context == named_context + assert image_payload_metadata(filemanager.saved_payloads[0]).source_image_names == ( + "Corrected", + ) + assert image_payload_data(filemanager.saved_payloads[0]) is image_payload_data(payload) @pytest.fixture From f12250993d91dfa38d5de548fed0fabbaa107e61 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 10:37:32 -0400 Subject: [PATCH 137/235] docs(runtime): record owner consolidation and checkpoint qualification --- .../runtime-owner-consolidation-20261003.rst | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 docs/refactor/receipts/runtime-owner-consolidation-20261003.rst diff --git a/docs/refactor/receipts/runtime-owner-consolidation-20261003.rst b/docs/refactor/receipts/runtime-owner-consolidation-20261003.rst new file mode 100644 index 0000000000..25cc7e732f --- /dev/null +++ b/docs/refactor/receipts/runtime-owner-consolidation-20261003.rst @@ -0,0 +1,77 @@ +Runtime owner consolidation +=========================== + +These changes remove parallel runtime representations and move their behavior +onto existing authorities. They support further optimization; class deletion is +not evidence of an end-to-end speedup. + +Ownership and consumer closure +------------------------------ + +``RuntimeArtifactInputRequest`` is removed. The existing CP artifact strategy +family consumes ``ArtifactSpec`` and the resolved value directly. Input +selection, raw versus normalized values, source axes and post-call source +resolution retain their existing owners. This change is merged into main as +PR #535, closing #534. The clean main candidate ``c828acaee`` passes all 541 +module, shape, source, store and direct-value controls. Shape fixtures follow +the current declared label domain and completed CP ordinals, including missing +entries; original stale-fixture failures are retained. + +``PatternGroupOutputData`` and its validation/unstack endpoint are removed. +``PatternGroupRuntime`` publishes the original output. ``AlignedImageStack`` +owns topology; ``ImagePayloadStackComposition`` owns independent buffers and +saved context; produced records determine cleanup. The sole-consumer +``RuntimeSliceAlignedImageOutputContext`` is also removed, with its algorithm +on the existing source strategy. The source-qualified output gate passes 238 +tests, including real partial writes, masks, opaque/source-bound domains, +duplicate-path preservation and subclass effects. + +The attempted deletion of ``ProjectedFunctionOutputContextStrategy`` failed +the registered-family gate: its inheritance carries fallback precedence. +The class remains and owns the actual common algorithm. The obsolete +grandparent-forwarding implementation is removed. The selector is unchanged. +The initial failed source and control receipts are retained. + +``FunctionInvocationArtifactScope`` is folded into its only concrete consumer, +``FunctionCoreExecutor``. The existing seven field declarations and method +bodies are unchanged. Constructor order, frozen slots, pickle, metadata-edge +polymorphism, debug behavior and first errors pass 158 controls. Historical +reports naming the old scope remain as evidence of their original source. + +Publication reader consolidation uses the existing document handler and source +projection builder. Required projection-only viewer consumers remain. Its 94 +reader controls pass; four saved-operand replays remove 151ms median locally +while preserving ordered paths and five complete atomic transaction results. +This duplicate read exists in the draft branch. Main already reads once, so +no separate main performance PR is warranted for this change. + +Checkpoint ownership correction +------------------------------- + +The identical current-main checkpoint fixture passes its required 48 files +while the draft branch originally produced only 24 named files. Issue #536 +tracks this branch regression. Named artifact materialization and ordinary +checkpoint publication were assigning the same artifact-name qualifier. + +Commit ``0dd77da46`` derives topology once at the existing save boundary. +Unwrapped canonical returns use ordinary source filenames while retaining +typed producer context; explicit named aligned outputs retain qualifiers. +The materialization owner keeps its explicit artifact-name policy. No extra +copy, wrapper or per-function flag is introduced. All 199 relevant controls +pass, including unchanged 48-file inventories, metadata reopening, yeast, +labels, table passthrough and opaque domains. The original saver reproduces +the new unwrapped-path control failure. + +Evidence scope +-------------- + +The source-qualified gates above overlap and refer to separate revisions; +their counts are not a combined whole-branch pass. NRA original-class census, +family projection and method comparison support ownership inspection, not +complete runtime equivalence. Unchanged method bodies do not prove constructor +or effect behavior; those have separate executed controls. + +PR #394 remains draft. Its original whole-branch R0/R1 obligations, installed +consumer acceptance, full-catalog parity and fresh full30/scaling figures +remain open. Startup and required library/kernel preparation remain outside +pipeline clocks. The performance goal remains active. From 58c932f63adb1174b119b2105180cb0dc40f7f1f Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 10:51:13 -0400 Subject: [PATCH 138/235] refactor(cellprofiler): keep invocation state on the existing image request --- openhcs/core/runtime_adapters.py | 23 +------ .../cellprofiler/runtime/invocation.py | 11 ++-- .../cellprofiler/runtime/module_execution.py | 21 +++--- .../cellprofiler/runtime/output_recording.py | 15 +---- .../test_cellprofiler_module_execution.py | 64 ++++++++++++++++--- .../test_cellprofiler_output_recording.py | 43 ++++++------- ...test_declared_singleton_composition_437.py | 2 +- .../test_gray_to_color_binding_axis_432.py | 2 +- .../test_rescale_stream_plane_declaration.py | 2 +- ...test_singleton_rgb_label_invocation_477.py | 4 +- 10 files changed, 98 insertions(+), 89 deletions(-) diff --git a/openhcs/core/runtime_adapters.py b/openhcs/core/runtime_adapters.py index e8b2d3cf2a..26dadc2c89 100644 --- a/openhcs/core/runtime_adapters.py +++ b/openhcs/core/runtime_adapters.py @@ -5,7 +5,7 @@ import inspect from collections.abc import Callable, Mapping, MutableMapping from dataclasses import dataclass, field -from typing import TYPE_CHECKING, Any, Generic, TypeVar +from typing import TYPE_CHECKING, Any, TypeVar from python_introspect import add_parameter_exclusions @@ -67,7 +67,6 @@ _F = TypeVar("_F", bound=Callable[..., Any]) -PayloadT = TypeVar("PayloadT") @dataclass(frozen=True, slots=True, kw_only=True) @@ -79,26 +78,6 @@ class RuntimeImageExecutionContext: plane_projection: RuntimePlaneAxisValueProjection | None = None -@dataclass(frozen=True, slots=True, kw_only=True) -class RuntimeImageRequest(RuntimeImageExecutionContext, Generic[PayloadT]): - """Resolved image payload and source metadata for one runtime invocation.""" - - image_count: int - payload: PayloadT - - -@dataclass(frozen=True, slots=True, kw_only=True) -class RuntimeFunctionInvocationRequest( - RuntimeImageExecutionContext, - Generic[PayloadT], -): - """Resolved callable inputs for one runtime function invocation.""" - - image_count: int - image: PayloadT - kwargs: Mapping[str, object] - - @dataclass(frozen=True, slots=True, kw_only=True) class RuntimeAdapterRequest: """Runtime data needed to build an invocation-scoped adapter.""" diff --git a/openhcs/interop/cellprofiler/runtime/invocation.py b/openhcs/interop/cellprofiler/runtime/invocation.py index cfe57fa7b2..fff2312c33 100644 --- a/openhcs/interop/cellprofiler/runtime/invocation.py +++ b/openhcs/interop/cellprofiler/runtime/invocation.py @@ -2,7 +2,8 @@ from __future__ import annotations -from dataclasses import dataclass, replace +from collections.abc import Mapping +from dataclasses import dataclass, field, replace from typing import ClassVar import numpy as np @@ -21,7 +22,6 @@ ) from openhcs.core.runtime_adapters import ( RuntimeImageExecutionContext, - RuntimeImageRequest, ) from openhcs.core.runtime_image_values import ( ImagePayloadMetadata, @@ -177,13 +177,16 @@ def source_metadata_composition_mode( @dataclass(frozen=True, slots=True, kw_only=True) class CellProfilerImageRequest( CellProfilerSourceIdentityMixin, - RuntimeImageRequest, + RuntimeImageExecutionContext, ): - """CellProfiler image invocation payload with ordered source-plane aliases.""" + """CellProfiler image domain, callable kwargs, and ordered source aliases.""" SOURCE_ALIASES_FIELD_NAME: ClassVar[str] = "CellProfilerImageRequest.source_aliases" + image_count: int + payload: RuntimeCallableArgument source_aliases: tuple[str, ...] = () + kwargs: Mapping[str, object] = field(default_factory=dict) def __post_init__(self) -> None: self.validate_source_identity() diff --git a/openhcs/interop/cellprofiler/runtime/module_execution.py b/openhcs/interop/cellprofiler/runtime/module_execution.py index c59d695092..1d551b94e0 100644 --- a/openhcs/interop/cellprofiler/runtime/module_execution.py +++ b/openhcs/interop/cellprofiler/runtime/module_execution.py @@ -40,7 +40,6 @@ ) from openhcs.core.runtime_adapters import ( RuntimeAdapterRequest, - RuntimeFunctionInvocationRequest, ) from openhcs.core.runtime_artifact_queries import MeasurementTableUnion from openhcs.core.runtime_batch_contracts import RuntimeBatchExecutionDomain @@ -396,7 +395,7 @@ def _run_standard_image( raw_output = _CELLPROFILER_FUNCTION_CONTRACT_EXECUTOR.execute( self.callable_contract, self.raw_func, - invocation.image, + invocation.payload, invocation.kwargs, execution_mode=invocation.execution_mode, plane_projection=invocation.plane_projection, @@ -407,7 +406,7 @@ def _run_standard_image( lambda: { "module": module_name, "function": self.callable_contract.function_name, - **cellprofiler_profile_payload_fields("input", invocation.image), + **cellprofiler_profile_payload_fields("input", invocation.payload), **cellprofiler_profile_payload_fields("output", raw_output), }, ) @@ -430,7 +429,6 @@ def _run_standard_image( returned_values=returned_values, matched_outputs=matched_outputs, invocation=invocation, - image_request=image_request, current_image=image, ) CellProfilerRuntimeProfileLogger.log_module_profile( @@ -445,7 +443,7 @@ def _run_standard_image( declared_only_outputs=declared_only_outputs, adapter=adapter, current_image=image, - invocation_image=invocation.image, + invocation_image=invocation.payload, plane_projection=invocation.plane_projection, ) CellProfilerRuntimeProfileLogger.log_module_profile( @@ -1421,7 +1419,7 @@ def _invocation_request( current_image: RuntimeCallableArgument, kwargs: RuntimeCallableKwargs, module_type: type[CellProfilerModule], - ) -> "RuntimeFunctionInvocationRequest": + ) -> "CellProfilerImageRequest": profile_enabled = CellProfilerRuntimeProfileLogger.enabled() if profile_enabled: phase_started_at = time.perf_counter() @@ -1479,6 +1477,7 @@ def _invocation_request( module=module_name, ) phase_started_at = time.perf_counter() + source_aliases = image_request.source_aliases image_request = module_type.project_invocation_image_request( image_request=image_request, runtime_kwargs=runtime_kwargs, @@ -1507,13 +1506,13 @@ def _invocation_request( execution_mode, ), ) - return RuntimeFunctionInvocationRequest( - image=image_request.payload, + # Source aliases name the original input surfaces even when an object + # input supplies the callable's projected image domain. + return replace( + image_request, kwargs=invocation_kwargs, - source_image_name=image_request.source_image_name, - image_count=image_request.image_count, + source_aliases=source_aliases, execution_mode=execution_mode, - plane_projection=image_request.plane_projection, ) diff --git a/openhcs/interop/cellprofiler/runtime/output_recording.py b/openhcs/interop/cellprofiler/runtime/output_recording.py index 8e95504e27..650d7bf03f 100644 --- a/openhcs/interop/cellprofiler/runtime/output_recording.py +++ b/openhcs/interop/cellprofiler/runtime/output_recording.py @@ -5,7 +5,6 @@ import time from abc import ABC, abstractmethod from collections.abc import Mapping -from dataclasses import replace from functools import lru_cache from graphlib import TopologicalSorter from types import MappingProxyType @@ -27,7 +26,6 @@ from openhcs.core.callable_contract import CallableContract from openhcs.core.function_patterns import InvocationArtifactInputEdgePlan from openhcs.core.registry_strategies import MostDerivedContextStrategyMixin -from openhcs.core.runtime_adapters import RuntimeFunctionInvocationRequest from openhcs.core.runtime_image_values import image_payload_metadata from openhcs.core.runtime_object_labels import ( ObjectLabelValue, @@ -111,8 +109,7 @@ def record_module_outputs( adapter: CellProfilerRuntimeAdapter, returned_values: Mapping[ArtifactSpecRef, RuntimeCallableArgument], matched_outputs: tuple[RuntimeMatchedOutput, ...], - invocation: RuntimeFunctionInvocationRequest, - image_request: CellProfilerImageRequest, + invocation: CellProfilerImageRequest, current_image: RuntimeCallableArgument, ) -> Mapping[ArtifactSpecRef, RuntimeCallableArgument]: """Record one module invocation's returned artifacts.""" @@ -149,14 +146,6 @@ def record_module_outputs( ): return declared_only_outputs - output_source = replace( - image_request, - payload=invocation.image, - source_image_name=invocation.source_image_name, - image_count=invocation.image_count, - execution_mode=invocation.execution_mode, - plane_projection=invocation.plane_projection, - ) for output_plan, spec, output_value in recording_order: if profile_enabled: record_started_at = time.perf_counter() @@ -168,7 +157,7 @@ def record_module_outputs( spec=spec, output_plan=output_plan, output_value=output_value, - source=output_source, + source=invocation, call_kwargs=invocation.kwargs, current_image=current_image, declared_only_outputs=declared_only_outputs, diff --git a/tests/unit/test_cellprofiler_module_execution.py b/tests/unit/test_cellprofiler_module_execution.py index 0674741f40..40067b9881 100644 --- a/tests/unit/test_cellprofiler_module_execution.py +++ b/tests/unit/test_cellprofiler_module_execution.py @@ -83,7 +83,6 @@ runtime_bound_parameters, special_inputs, ) -from openhcs.core.runtime_adapters import RuntimeFunctionInvocationRequest from openhcs.core.runtime_artifact_queries import ( MeasurementTableAxisProjection, MeasurementTableUnion, @@ -635,14 +634,8 @@ def test_output_recording_preserves_complete_callable_return_abi() -> None: ), ), ) - image_request = CellProfilerImageRequest( + invocation = CellProfilerImageRequest( payload=stack, - source_image_name=None, - image_count=1, - plane_projection=projection, - ) - invocation = RuntimeFunctionInvocationRequest( - image=stack, kwargs={}, source_image_name=None, image_count=1, @@ -661,7 +654,6 @@ def test_output_recording_preserves_complete_callable_return_abi() -> None: ), matched_outputs=(), invocation=invocation, - image_request=image_request, current_image=stack, ) @@ -8325,7 +8317,7 @@ def test_illumination_apply_projects_broadcast_input_to_selected_primary_site( result = CellProfilerFunctionContractExecutor().execute( contract, executor.raw_func, - invocation.image, + invocation.payload, invocation.kwargs, execution_mode=invocation.execution_mode, plane_projection=invocation.plane_projection, @@ -9524,6 +9516,58 @@ def test_pattern_group_runtime_leaves_variable_shape_aligned_outputs_uncached(): ) is None +def test_invocation_request_keeps_original_aliases_when_labels_change_image_domain( + monkeypatch, +): + contract = _compiled_callable_contract(IdentifyTertiaryObjectsModule.require_callable()) + executor = _module_executor(contract) + larger = ObjectLabelSet( + name="Larger", + variant_data=ObjectLabelVariantData(labels=np.ones((2, 3), dtype=np.int32)), + ) + smaller = ObjectLabelSet( + name="Smaller", + variant_data=ObjectLabelVariantData(labels=np.ones((4, 5), dtype=np.int32)), + ) + monkeypatch.setattr( + CellProfilerModuleExecutor, + "_runtime_input_kwargs", + lambda self, *args, **kwargs: { + "secondary_labels": larger, + "primary_labels": smaller, + }, + ) + original = CellProfilerImageRequest( + payload=np.zeros((6, 7), dtype=np.float32), + source_image_name="Carrier", + source_aliases=("CarrierA", "CarrierB"), + image_count=2, + ) + marker = [] + authored_kwargs = {"secondary_labels": "authored", "marker": marker} + invocation = executor._invocation_request( + image_request=original, + adapter=_FakeCellProfilerRuntime({}, callable_contract=contract), + current_image=original.payload, + kwargs=authored_kwargs, + module_type=IdentifyTertiaryObjectsModule, + ) + + assert isinstance(invocation, CellProfilerImageRequest) + assert invocation.source_aliases is original.source_aliases + assert invocation.source_image_name is None + assert invocation.image_count == 1 + assert image_payload_data(invocation.payload).shape == (2, 3) + assert invocation.kwargs["secondary_labels"] is larger + assert invocation.kwargs["primary_labels"] is smaller + assert invocation.kwargs["marker"] is marker + assert authored_kwargs == {"secondary_labels": "authored", "marker": marker} + assert original.source_image_name == "Carrier" + assert original.image_count == 2 + assert original.kwargs == {} + assert original.payload.shape == (6, 7) + + def test_cellprofiler_main_flow_output_preserves_input_source_planes(): input_image = ImagePayloadMetadata( plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, diff --git a/tests/unit/test_cellprofiler_output_recording.py b/tests/unit/test_cellprofiler_output_recording.py index 698d2131c3..24bbc0bb5e 100644 --- a/tests/unit/test_cellprofiler_output_recording.py +++ b/tests/unit/test_cellprofiler_output_recording.py @@ -17,7 +17,6 @@ SpatialGridArtifactType, ) from openhcs.core.callable_contract import CallableContract, CallableMetadata -from openhcs.core.runtime_adapters import RuntimeFunctionInvocationRequest from openhcs.core.runtime_image_values import ( ImagePayloadMetadata, image_payload_data, @@ -93,9 +92,9 @@ def test_output_recording_uses_artifact_dependency_order() -> None: } ) image = np.zeros((2, 2), dtype=np.float32) - invocation = RuntimeFunctionInvocationRequest( + invocation = CellProfilerImageRequest( image_count=1, - image=image, + payload=image, kwargs={}, source_image_name=None, execution_mode=ImagePayloadExecutionMode.NATURAL, @@ -111,7 +110,6 @@ def test_output_recording_uses_artifact_dependency_order() -> None: (source_plan, source, "source"), ), invocation=invocation, - image_request=_image_request(image), current_image=image, ) @@ -257,10 +255,6 @@ def test_output_recording_carries_exact_invocation_plane_projection( adapter = Mock(spec=CellProfilerRuntimeAdapter) adapter.request = SimpleNamespace(artifact_outputs={output_plan.ref(): output_plan}) image = np.zeros((2, 2), dtype=np.float32) - stale_projection = RuntimePlaneAxisValueProjection.preserve( - axis=RuntimePlaneAxis.RUNTIME_SLICE, - axis_size=1, - ) invocation_projection = RuntimePlaneAxisValueProjection.preserve( axis=RuntimePlaneAxis.RUNTIME_SLICE, axis_size=2, @@ -272,30 +266,31 @@ def test_output_recording_carries_exact_invocation_plane_projection( classmethod(lambda cls, artifact_type: recorder), ) + call_kwargs = {"mutable_value": []} + invocation = CellProfilerImageRequest( + image_count=2, + payload=image, + kwargs=call_kwargs, + source_image_name=None, + source_aliases=("OriginalCarrier",), + execution_mode=ImagePayloadExecutionMode.FULL_STACK, + plane_projection=invocation_projection, + ) CellProfilerOutputRecorder.record_module_outputs( callable_contract=_contract(outputs=(output,)), active_input_edges=(), adapter=adapter, returned_values={output.ref(): "grid"}, matched_outputs=((output_plan, output, "grid"),), - invocation=RuntimeFunctionInvocationRequest( - image_count=2, - image=image, - kwargs={}, - source_image_name=None, - execution_mode=ImagePayloadExecutionMode.FULL_STACK, - plane_projection=invocation_projection, - ), - image_request=CellProfilerImageRequest( - image_count=1, - payload=ImagePayloadMetadata().payload_with(image), - source_image_name=None, - source_aliases=(), - execution_mode=ImagePayloadExecutionMode.NATURAL, - plane_projection=stale_projection, - ), + invocation=invocation, current_image=image, ) request = recorder.record.call_args.args[0] + assert request.source is invocation + assert request.call_kwargs is call_kwargs + assert request.source.payload is image + assert request.source.source_aliases == ("OriginalCarrier",) + assert request.source.image_count == 2 + assert request.source.execution_mode is ImagePayloadExecutionMode.FULL_STACK assert request.source.plane_projection is invocation_projection diff --git a/tests/unit/test_declared_singleton_composition_437.py b/tests/unit/test_declared_singleton_composition_437.py index 2256df6f97..79c882c592 100644 --- a/tests/unit/test_declared_singleton_composition_437.py +++ b/tests/unit/test_declared_singleton_composition_437.py @@ -185,7 +185,7 @@ def test_new_declaration_reuses_composition_and_executor(slice_count, aligned): kwargs={}, module_type=IndependentlyDeclaredSourceEchoModule, ) - payload = invocation.image + payload = invocation.payload execution_mode = invocation.execution_mode plane_projection = invocation.plane_projection assert execution_mode is ImagePayloadExecutionMode.ALIGNED_MULTI_IMAGE_STACK 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 0fc44086b0..a247b65a6f 100644 --- a/tests/unit/test_gray_to_color_binding_axis_432.py +++ b/tests/unit/test_gray_to_color_binding_axis_432.py @@ -98,7 +98,7 @@ def test_declared_fitc_runtime_plane_retains_values_and_physical_identity(explic kwargs={"color_scheme": GrayToColorModule.Scheme.STACK, "rescale_intensity": False}, ) result = CellProfilerFunctionContractExecutor().execute( - contract, contract.resolve_canonical_raw_callable(), execution.image, execution.kwargs, + contract, contract.resolve_canonical_raw_callable(), execution.payload, execution.kwargs, execution_mode=execution.execution_mode, plane_projection=execution.plane_projection, ) # Float32 is the original Stack runner's output contract, not unit normalization. diff --git a/tests/unit/test_rescale_stream_plane_declaration.py b/tests/unit/test_rescale_stream_plane_declaration.py index 981d2d3c79..fd5c8eb80d 100644 --- a/tests/unit/test_rescale_stream_plane_declaration.py +++ b/tests/unit/test_rescale_stream_plane_declaration.py @@ -105,7 +105,7 @@ def test_public_rescale_preserves_one_source_plane_through_stream_binding(method "source_high": 20.0, "divisor_value": 2.0}, ) result = CellProfilerFunctionContractExecutor().execute( - contract, rescale_intensity, execution.image, execution.kwargs, + contract, rescale_intensity, execution.payload, execution.kwargs, execution_mode=execution.execution_mode, plane_projection=execution.plane_projection, ) (item,) = tuple(StreamOutputBatch.project_item(RuntimeProjectionSourceIdentityRequest( diff --git a/tests/unit/test_singleton_rgb_label_invocation_477.py b/tests/unit/test_singleton_rgb_label_invocation_477.py index 9b9354338f..42b3020bb3 100644 --- a/tests/unit/test_singleton_rgb_label_invocation_477.py +++ b/tests/unit/test_singleton_rgb_label_invocation_477.py @@ -56,7 +56,7 @@ def _bind(case): def _execute(case, invocation): raw, contract, *_ = case return CellProfilerFunctionContractExecutor().execute( - contract, raw, invocation.image, invocation.kwargs, + contract, raw, invocation.payload, invocation.kwargs, execution_mode=invocation.execution_mode, plane_projection=invocation.plane_projection, ) @@ -70,7 +70,7 @@ def test_match_image_labels_consume_declared_singleton_root_for_scalar_rgb(): selected = invocation.kwargs['object_labels'][0] assert invocation.execution_mode is ImagePayloadExecutionMode.NATURAL assert invocation.plane_projection is None - assert invocation.image.metadata.source_channel_axis == 2 + assert invocation.payload.metadata.source_channel_axis == 2 assert selected.plane_axis is None assert object_label_dense_array(selected).shape == (8, 9) output = _execute(case, invocation) From 5c3e12ecdec20066a7c3cf43f2bee95bdfe45039 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 10:54:49 -0400 Subject: [PATCH 139/235] fix(cellprofiler): prepare parsed module contracts at the shared boundary Restore parsed/public contract identity in PR #394 without stripping prepared signatures. The unchanged execution and binding gate passes 641 tests. --- openhcs/interop/cellprofiler/module_artifact_contracts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openhcs/interop/cellprofiler/module_artifact_contracts.py b/openhcs/interop/cellprofiler/module_artifact_contracts.py index 6b7e254050..276c2c7b12 100644 --- a/openhcs/interop/cellprofiler/module_artifact_contracts.py +++ b/openhcs/interop/cellprofiler/module_artifact_contracts.py @@ -1192,7 +1192,7 @@ def callable_contract( group_key=invocation_key.group_key, ) func = cls.require_callable(invocation_key.function_name) - callable_contract = CallableContract.from_callable(func) + callable_contract = CallableContract.from_prepared_callable(func) inputs = cls.artifact_contract_inputs( module, invocation_key=invocation_key, From c59eaa55fe3d3e49f3ecb727a14a91e56e2957f6 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 11:02:28 -0400 Subject: [PATCH 140/235] test(runtime): qualify consolidated invocation owners against current declarations --- .../runtime-owner-consolidation-20261003.rst | 24 +++++++++ .../test_cellprofiler_module_execution.py | 52 ------------------- ...st_cellprofiler_special_io_declarations.py | 44 ++++++++++++++-- 3 files changed, 63 insertions(+), 57 deletions(-) diff --git a/docs/refactor/receipts/runtime-owner-consolidation-20261003.rst b/docs/refactor/receipts/runtime-owner-consolidation-20261003.rst index 25cc7e732f..01398b8f1a 100644 --- a/docs/refactor/receipts/runtime-owner-consolidation-20261003.rst +++ b/docs/refactor/receipts/runtime-owner-consolidation-20261003.rst @@ -38,6 +38,30 @@ bodies are unchanged. Constructor order, frozen slots, pickle, metadata-edge polymorphism, debug behavior and first errors pass 158 controls. Historical reports naming the old scope remain as evidence of their original source. +``RuntimeImageRequest`` and ``RuntimeFunctionInvocationRequest`` are removed. +``CellProfilerImageRequest`` owns the image domain and actual callable kwargs +through execution and output recording, eliminating the five-field copy-back +step. Original aliases survive object-driven changes in image shape/count. +The shared ``RuntimeImageExecutionContext`` remains the parent for genuine +batch and measurement consumers. This lane is merged into main as PR #538, +closing #537. The clean main candidate ``7e2f97f80`` passes all 780 tests in +the 17-file execution, library, recording, binding, axis and stream gate. +The pre-existing retained-plane shape fixture now explicitly declares its +object plane domain; its Area and true-volume assertions remain intact. + +The parsed-module contract boundary now uses the existing prepared-callable +factory, matching authored compilation. The original Crop equality failure +reproduces before the carrier deletion. Commit ``5c3e12ecd`` fixes its actual +production boundary; all 641 execution/library/binding controls pass without +removing signature fields from equality or changing the original assertion. +Two additional declaration fixtures failed identically before that fix: they +omitted the existing seven QA outputs and the step-owned Costes runtime input. +Their updated assertions retain original ABI counts and add complete diagnostic +names/roles and runtime parameter checks. The final six-module contract gate +passes all 100 tests. After merging PR #538, the exact retained-plane and alias +controls pass all ten tests; an identical test duplicated by Git's merge is +removed, leaving its original definition intact. + Publication reader consolidation uses the existing document handler and source projection builder. Required projection-only viewer consumers remain. Its 94 reader controls pass; four saved-operand replays remove 151ms median locally diff --git a/tests/unit/test_cellprofiler_module_execution.py b/tests/unit/test_cellprofiler_module_execution.py index 5354331a7c..40067b9881 100644 --- a/tests/unit/test_cellprofiler_module_execution.py +++ b/tests/unit/test_cellprofiler_module_execution.py @@ -9568,58 +9568,6 @@ def test_invocation_request_keeps_original_aliases_when_labels_change_image_doma assert original.payload.shape == (6, 7) -def test_invocation_request_keeps_original_aliases_when_labels_change_image_domain( - monkeypatch, -): - contract = _compiled_callable_contract(IdentifyTertiaryObjectsModule.require_callable()) - executor = _module_executor(contract) - larger = ObjectLabelSet( - name="Larger", - variant_data=ObjectLabelVariantData(labels=np.ones((2, 3), dtype=np.int32)), - ) - smaller = ObjectLabelSet( - name="Smaller", - variant_data=ObjectLabelVariantData(labels=np.ones((4, 5), dtype=np.int32)), - ) - monkeypatch.setattr( - CellProfilerModuleExecutor, - "_runtime_input_kwargs", - lambda self, *args, **kwargs: { - "secondary_labels": larger, - "primary_labels": smaller, - }, - ) - original = CellProfilerImageRequest( - payload=np.zeros((6, 7), dtype=np.float32), - source_image_name="Carrier", - source_aliases=("CarrierA", "CarrierB"), - image_count=2, - ) - marker = [] - authored_kwargs = {"secondary_labels": "authored", "marker": marker} - invocation = executor._invocation_request( - image_request=original, - adapter=_FakeCellProfilerRuntime({}, callable_contract=contract), - current_image=original.payload, - kwargs=authored_kwargs, - module_type=IdentifyTertiaryObjectsModule, - ) - - assert isinstance(invocation, CellProfilerImageRequest) - assert invocation.source_aliases is original.source_aliases - assert invocation.source_image_name is None - assert invocation.image_count == 1 - assert image_payload_data(invocation.payload).shape == (2, 3) - assert invocation.kwargs["secondary_labels"] is larger - assert invocation.kwargs["primary_labels"] is smaller - assert invocation.kwargs["marker"] is marker - assert authored_kwargs == {"secondary_labels": "authored", "marker": marker} - assert original.source_image_name == "Carrier" - assert original.image_count == 2 - assert original.kwargs == {} - assert original.payload.shape == (6, 7) - - def test_cellprofiler_main_flow_output_preserves_input_source_planes(): input_image = ImagePayloadMetadata( plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, diff --git a/tests/unit/test_cellprofiler_special_io_declarations.py b/tests/unit/test_cellprofiler_special_io_declarations.py index a7dc1764a2..16b2c84c1f 100644 --- a/tests/unit/test_cellprofiler_special_io_declarations.py +++ b/tests/unit/test_cellprofiler_special_io_declarations.py @@ -40,6 +40,7 @@ runtime_bound_parameter_names_from_callable, ) from openhcs.core.runtime_relationships import DirectedObjectRelationshipPayload +from openhcs.core.runtime_plane_projection import RuntimeSliceInvariantValue from openhcs.core.runtime_tabular_values import ColumnarRows from openhcs.interop.cellprofiler.module_artifact_declarations import ( MeasurementArtifactOutputModule, @@ -85,6 +86,9 @@ IdentifyPrimaryObjectsModule, identify_primary_objects, ) +from openhcs.processing.backends.cellprofiler.primary_object_diagnostics import ( + PrimaryObjectDiagnosticPlanes, +) from openhcs.processing.backends.cellprofiler.secondary import ( IdentifySecondaryObjectsModule, IdentifyTertiaryObjectsModule, @@ -603,7 +607,11 @@ def test_fixed_return_slots_follow_nominal_contract_output_order() -> None: main_flow=(dna,), ), identify_primary_objects, - (MeasurementsArtifactType, ObjectLabelsArtifactType), + ( + MeasurementsArtifactType, + ObjectLabelsArtifactType, + *((ImageArtifactType,) * len(PrimaryObjectDiagnosticPlanes._fields)), + ), ), ( _contract( @@ -707,6 +715,22 @@ def test_fixed_return_slots_follow_nominal_contract_output_order() -> None: len(contract.trailing_return_output_specs) + 1 ) + primary_contract = next( + contract + for contract, func, _artifact_types in contracts_and_functions + if func is identify_primary_objects + ) + diagnostic_outputs = primary_contract.trailing_return_output_specs[2:] + diagnostic_prefix = ArtifactSidecarRole.QA_CHECKPOINT.name_for(nuclei.name) + assert tuple(spec.name for spec in diagnostic_outputs) == tuple( + f"{diagnostic_prefix}__{stage}" + for stage in PrimaryObjectDiagnosticPlanes._fields + ) + assert all( + spec.sidecar_role is ArtifactSidecarRole.QA_CHECKPOINT + for spec in diagnostic_outputs + ) + def test_contract_order_owns_guided_grid_and_tertiary_input_mapping() -> None: grid = ArtifactSpec.output("Grid", SpatialGridArtifactType) @@ -803,9 +827,19 @@ def test_object_colocalization_rank_provider_is_runtime_bound() -> None: assert parameter.kind is inspect.Parameter.KEYWORD_ONLY assert runtime_bound_parameter_names_from_callable( measure_colocalization_objects - ) == ("rank_provider", "threshold_mask_outputs") - assert CallableContract.from_callable( - measure_colocalization_objects - ).config_bound_parameter_names == ("dtype_config",) + ) == ("rank_provider", "threshold_mask_outputs", "costes_threshold_batch") + contract = CallableContract.from_callable(measure_colocalization_objects) + assert contract.config_bound_parameter_names == ("dtype_config",) assert parameters["dtype_config"].annotation is LazyDtypeConfig assert "rank_provider" in parameter_exclusions(measure_colocalization_objects) + assert parameters["costes_threshold_batch"].kind is inspect.Parameter.KEYWORD_ONLY + assert parameters["costes_threshold_batch"].default is None + assert "costes_threshold_batch" in parameter_exclusions( + measure_colocalization_objects + ) + costes_declaration = next( + declaration + for declaration in contract.metadata.runtime_bound_parameters + if declaration.require_parameter_name() == "costes_threshold_batch" + ) + assert costes_declaration.annotation_type is RuntimeSliceInvariantValue From 0618e544f127acd32410614945e4680d8050c1ca Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 11:27:29 -0400 Subject: [PATCH 141/235] refactor(cellprofiler): specialize output recording from the input binding owner Addresses #542. Preserve output-first admission, live input read epochs, exact endpoint identity and reference broadcast semantics. Remove temporary binding holders and migrate record kwargs to one inherited field. --- .../cellprofiler/runtime/module_execution.py | 2 +- .../runtime/output_record_request.py | 36 ++-- .../cellprofiler/runtime/output_recording.py | 2 +- .../runtime/relationship_measurement_rows.py | 2 +- .../backends/cellprofiler/neighbors.py | 4 +- .../backends/cellprofiler/relationships.py | 6 +- ...profiler_measurement_source_group_scope.py | 4 +- .../test_cellprofiler_module_execution.py | 98 +++++----- .../test_cellprofiler_neighbors_contract.py | 2 +- ...t_cellprofiler_output_record_occurrence.py | 168 +++++++++++++++++- .../test_cellprofiler_output_recording.py | 6 +- ...st_child_measurement_producer_selection.py | 2 +- 12 files changed, 242 insertions(+), 90 deletions(-) diff --git a/openhcs/interop/cellprofiler/runtime/module_execution.py b/openhcs/interop/cellprofiler/runtime/module_execution.py index 1d551b94e0..f88956140b 100644 --- a/openhcs/interop/cellprofiler/runtime/module_execution.py +++ b/openhcs/interop/cellprofiler/runtime/module_execution.py @@ -1155,7 +1155,7 @@ def _run_per_image_measurement( output_plan=matched_plan, output_value=matched_value, source=measurement_image, - call_kwargs=invocation_kwargs, + kwargs=invocation_kwargs, current_image=measurement_image.payload, declared_only_outputs=CellProfilerOutputRecorder.transient_output_values( callable_contract=self.callable_contract, diff --git a/openhcs/interop/cellprofiler/runtime/output_record_request.py b/openhcs/interop/cellprofiler/runtime/output_record_request.py index afcd9e4825..2f20f721fb 100644 --- a/openhcs/interop/cellprofiler/runtime/output_record_request.py +++ b/openhcs/interop/cellprofiler/runtime/output_record_request.py @@ -5,7 +5,6 @@ from collections.abc import Mapping from dataclasses import dataclass, field from types import MappingProxyType -from typing import TYPE_CHECKING from openhcs.core.artifacts import ( ArtifactInputPlan, @@ -20,7 +19,6 @@ SourcePayloadPlaneIdentitySequence, SourcePlaneIdentitySequenceAlignment, ) -from openhcs.core.runtime_array_values import RuntimeArrayData from openhcs.core.runtime_image_values import ( ImagePayloadMetadata, image_payload_metadata, @@ -37,26 +35,19 @@ ) from openhcs.core.steps.function_runtime import ( RuntimeCallableArgument, - RuntimeCallableKwargs, ) -if TYPE_CHECKING: - from openhcs.interop.cellprofiler.runtime.adapter import CellProfilerRuntimeAdapter - -@dataclass(frozen=True, slots=True) -class CellProfilerOutputRecordRequest: +@dataclass(frozen=True, slots=True, kw_only=True) +class CellProfilerOutputRecordRequest(RuntimeInputBindingRequest): """Inputs and semantic authorities for recording one CellProfiler output.""" callable_contract: CallableContract active_input_edges: tuple[InvocationArtifactInputEdgePlan, ...] - adapter: CellProfilerRuntimeAdapter spec: ArtifactSpec output_plan: ArtifactOutputPlan output_value: RuntimeCallableArgument source: CellProfilerImageRequest | CellProfilerMeasurementImage - call_kwargs: RuntimeCallableKwargs - current_image: RuntimeArrayData declared_only_outputs: Mapping[ ArtifactSpecRef, RuntimeCallableArgument, @@ -95,6 +86,8 @@ def __post_init__(self) -> None: f"{declared.ref()!r}." ) previous_input_index = input_index + if self.selected_object_inputs is not None: + RuntimeInputBindingRequest.__post_init__(self) def artifact_output_value( self, @@ -127,7 +120,7 @@ def artifact_output_value( "nor present in the current declared-only return." ) - def artifact_value( + def declared_artifact_value( self, spec: ArtifactSpec, ) -> RuntimeCallableArgument: @@ -176,11 +169,10 @@ def artifact_input_value( ) -> RuntimeCallableArgument: """Return one exact compiled input occurrence in invocation scope.""" - return RuntimeInputBindingRequest( - adapter=self.adapter, - kwargs=self.call_kwargs, - current_image=self.current_image, - ).runtime_value( + # Recording admits output declarations at construction. Input selection + # remains live and is admitted only when that input is actually read. + RuntimeInputBindingRequest.__post_init__(self) + return self.runtime_value( edge, parameter_name=edge.spec.parameter_name, ) @@ -193,7 +185,7 @@ def measurement_source_metadata( if not specs: raise ValueError("Measurement source context requires declared artifacts.") - artifact_values = tuple(self.artifact_value(spec) for spec in specs) + artifact_values = tuple(self.declared_artifact_value(spec) for spec in specs) metadata = tuple(image_payload_metadata(value) for value in artifact_values) source_group_component = self.output_plan.group_component identity_policy = SourceImageSetIdentityPolicy( @@ -228,16 +220,12 @@ def artifact_source_payload( """Resolve one declared input in the callable invocation's exact scope.""" spec = edge.spec - binding_request = RuntimeInputBindingRequest( - adapter=self.adapter, - kwargs=self.call_kwargs, - current_image=self.current_image, - ) + RuntimeInputBindingRequest.__post_init__(self) payload = RuntimeArtifactTypeStrategy.for_artifact_type( spec.artifact_type ).source_image_payload( spec, - binding_request.runtime_value( + self.runtime_value( edge, parameter_name=spec.parameter_name, ), diff --git a/openhcs/interop/cellprofiler/runtime/output_recording.py b/openhcs/interop/cellprofiler/runtime/output_recording.py index 650d7bf03f..66cdd0e0bf 100644 --- a/openhcs/interop/cellprofiler/runtime/output_recording.py +++ b/openhcs/interop/cellprofiler/runtime/output_recording.py @@ -158,7 +158,7 @@ def record_module_outputs( output_plan=output_plan, output_value=output_value, source=invocation, - call_kwargs=invocation.kwargs, + kwargs=invocation.kwargs, current_image=current_image, declared_only_outputs=declared_only_outputs, ) diff --git a/openhcs/interop/cellprofiler/runtime/relationship_measurement_rows.py b/openhcs/interop/cellprofiler/runtime/relationship_measurement_rows.py index 5849251569..6155856329 100644 --- a/openhcs/interop/cellprofiler/runtime/relationship_measurement_rows.py +++ b/openhcs/interop/cellprofiler/runtime/relationship_measurement_rows.py @@ -559,4 +559,4 @@ def unprojected_object_labels( self, spec: ArtifactSpec, ) -> RuntimeCallableArgument: - return self.request.artifact_value(spec) + return self.request.declared_artifact_value(spec) diff --git a/openhcs/processing/backends/cellprofiler/neighbors.py b/openhcs/processing/backends/cellprofiler/neighbors.py index 11b18b4a1e..2c67bc19a8 100644 --- a/openhcs/processing/backends/cellprofiler/neighbors.py +++ b/openhcs/processing/backends/cellprofiler/neighbors.py @@ -1431,13 +1431,13 @@ class MeasurementRows(ModuleOwnedResultMeasurementRows): @classmethod def for_request(cls, module_type, request): planner = NeighborDistancePlanner.for_method( - request.call_kwargs["distance_method"] + request.kwargs["distance_method"] ) return cls( request.output_value, module_type=module_type, measurement_scale=planner.measurement_scale( - int(request.call_kwargs["neighbor_distance"]) + int(request.kwargs["neighbor_distance"]) ), ) diff --git a/openhcs/processing/backends/cellprofiler/relationships.py b/openhcs/processing/backends/cellprofiler/relationships.py index ced3fe559d..46ff64d888 100644 --- a/openhcs/processing/backends/cellprofiler/relationships.py +++ b/openhcs/processing/backends/cellprofiler/relationships.py @@ -1238,7 +1238,7 @@ def distance_method(self) -> RelateObjectsDistanceMethod: func = module_type.require_callable(callable_contract.function_name) call_kwargs = { **runtime_callable_defaults(func), - **self.request.call_kwargs, + **self.request.kwargs, } return call_kwargs["calculate_distances"] @@ -1251,8 +1251,8 @@ def distance_measurements_declared(self) -> bool: def per_parent_means_enabled(self) -> bool: value = ( - self.request.call_kwargs["calculate_per_parent_means"] - if "calculate_per_parent_means" in self.request.call_kwargs + self.request.kwargs["calculate_per_parent_means"] + if "calculate_per_parent_means" in self.request.kwargs else False ) return bool(value) diff --git a/tests/unit/test_cellprofiler_measurement_source_group_scope.py b/tests/unit/test_cellprofiler_measurement_source_group_scope.py index 142e37def2..10e8661e6f 100644 --- a/tests/unit/test_cellprofiler_measurement_source_group_scope.py +++ b/tests/unit/test_cellprofiler_measurement_source_group_scope.py @@ -43,7 +43,7 @@ class _PayloadBackedOutputRecordRequest(CellProfilerOutputRecordRequest): artifact_values: ClassVar[dict[ArtifactSpecRef, ObjectLabelSet]] = {} - def artifact_value(self, spec: ArtifactSpec) -> ObjectLabelSet: + def declared_artifact_value(self, spec: ArtifactSpec) -> ObjectLabelSet: return self.artifact_values[spec.ref()] @@ -134,7 +134,7 @@ def combine_objects(object_labels): source_image_name=None, image_count=1, ), - call_kwargs={}, + kwargs={}, current_image=first_value, ) diff --git a/tests/unit/test_cellprofiler_module_execution.py b/tests/unit/test_cellprofiler_module_execution.py index 40067b9881..b48491bd99 100644 --- a/tests/unit/test_cellprofiler_module_execution.py +++ b/tests/unit/test_cellprofiler_module_execution.py @@ -2574,7 +2574,7 @@ def test_track_objects_retained_image_uses_tracked_object_source_payload() -> No execution_mode=ImagePayloadExecutionMode.FULL_STACK, ), current_image=current_payload, - call_kwargs={}, + kwargs={}, ) source_payload = _module_type_for_contract( @@ -2738,7 +2738,7 @@ def test_object_label_output_source_preserves_matching_input_object_plane_contex spec=output, output_value=np.zeros((2, 4, 5), dtype=np.int32), source_image_name=None, - call_kwargs={}, + kwargs={}, current_image=current_image, ) @@ -2813,7 +2813,7 @@ def test_resize_objects_output_source_policy_uses_input_object_context() -> None spec=output, output_value=np.zeros((4, 5), dtype=np.int32), source_image_name="ReferenceImage", - call_kwargs={}, + kwargs={}, source_image_payload=image_payload, current_image=image_payload, ) @@ -2855,7 +2855,7 @@ def test_watershed_output_source_policy_uses_declared_image_as_parent() -> None: spec=output, output_value=np.zeros((3, 4, 5), dtype=np.int32), source_image_name="DNA", - call_kwargs={}, + kwargs={}, source_aliases=("DNA",), source_image_payload=image_payload, current_image=image_payload, @@ -2957,7 +2957,7 @@ def add_objects(name, labels, **kwargs): domain=ObjectLabelDomain(declared_object_count=0), ), source_image_name="ReferenceImage", - call_kwargs={}, + kwargs={}, source_image_payload=image_payload, current_image=image_payload, ) @@ -3019,7 +3019,7 @@ def add_objects(name, labels, **kwargs): spec=output, output_value=output_labels, source_image_name="Memb", - call_kwargs={}, + kwargs={}, source_aliases=("Memb",), source_image_payload=image_payload, current_image=image_payload, @@ -3099,7 +3099,7 @@ def test_measure_object_size_shape_record_builder_keeps_shape_features_unqualifi declared_field_names=("object_label", "Area"), ), source_image_name="BF_image", - call_kwargs={}, + kwargs={}, source_image_payload=ImagePayloadMetadata( source_path="/input/A01_s001_w1.tif" ).payload_with(np.zeros((3, 3), dtype=np.float32), None), @@ -3182,7 +3182,7 @@ def test_object_output_measurements_derive_count_and_locations_from_output_label source_image_name="Mask", source_image_payload=mask_payload, current_image=mask_payload, - call_kwargs={}, + kwargs={}, ) CellProfilerOutputRecorder.for_artifact_type(ObjectLabelsArtifactType).record( replace( @@ -3345,7 +3345,7 @@ def test_compiled_measurement_output_preserves_image_and_object_row_ownership() source_image_name="Mask", source_image_payload=source_image, current_image=source_image, - call_kwargs={}, + kwargs={}, ) adapter.add_objects(object_spec.name, labels) @@ -3442,7 +3442,7 @@ def test_object_label_output_source_payload_uses_primary_object_input() -> None: spec=output, output_value=np.zeros((2, 2), dtype=np.int32), source_image_name=None, - call_kwargs={}, + kwargs={}, source_image_payload=object(), current_image=None, ) @@ -3504,7 +3504,7 @@ def test_object_label_output_source_payload_uses_declared_primary_object_input() spec=output, output_value=np.zeros((2, 2), dtype=np.int32), source_image_name=None, - call_kwargs={}, + kwargs={}, source_image_payload=source_payload, current_image=current_image, ) @@ -4389,7 +4389,7 @@ def test_object_label_output_domain_scope_preserves_declared_source_stack() -> N source_image_name="Threshold", ), current_image=np.ones((3, 4, 5), dtype=np.float32), - call_kwargs={}, + kwargs={}, ) assert request.object_label_output_domain_scope() is None @@ -4427,7 +4427,7 @@ def test_object_label_output_domain_scope_uses_declared_group_lineage() -> None: source_image_name="Threshold", ), current_image=np.ones((3, 4, 5), dtype=np.float32), - call_kwargs={}, + kwargs={}, ) assert request.object_label_output_domain_scope() is ObjectLabelDomainScope.PAYLOAD @@ -8407,7 +8407,7 @@ def test_illumination_apply_image_output_uses_original_input_source_payload() -> source_aliases=("OrigMito", "IllumMito"), source_image_payload=stale_invocation_payload, current_image=np.zeros((2, 3, 4), dtype=np.float32), - call_kwargs={}, + kwargs={}, ) ) @@ -8509,7 +8509,7 @@ def test_image_output_projection_uses_exact_invocation_projection( plane_projection=plane_projection, ), current_image=source_payload, - call_kwargs={}, + kwargs={}, ) ) @@ -8593,7 +8593,7 @@ def test_declared_output_source_uses_projected_object_input_value() -> None: plane_projection=None, ), current_image=current_image, - call_kwargs={}, + kwargs={}, ) projected_source = MeasureObjectNeighborsModule.source_payload(request) @@ -8676,7 +8676,7 @@ def test_image_output_recording_projects_masked_singleton_rgb_payload() -> None: execution_mode=ImagePayloadExecutionMode.FULL_STACK, ), current_image=source_payload, - call_kwargs={}, + kwargs={}, ) ) @@ -8751,7 +8751,7 @@ def test_illumination_apply_image_output_preserves_declared_stack_value() -> Non source_aliases=("OrigMito", "IllumMito"), source_image_payload=duplicate_output, current_image=np.zeros((5, 6), dtype=np.float32), - call_kwargs={}, + kwargs={}, ) ) @@ -8826,7 +8826,7 @@ def test_illumination_apply_image_output_preserves_singleton_volume_value() -> N source_aliases=("OrigMito", "IllumMito"), source_image_payload=singleton_output, current_image=np.zeros((5, 6), dtype=np.float32), - call_kwargs={}, + kwargs={}, ) ) @@ -11383,7 +11383,7 @@ def test_image_measurement_table_retains_exact_declared_current_image() -> None: output_value=MeasurementSparseColumnarRows.from_rows((), fields=()), source_image_name="OrigGreen", source_image_payload=np.zeros((4, 5), dtype=np.float32), - call_kwargs={}, + kwargs={}, ) module_type = _module_type_for_contract(executor.callable_contract) @@ -11512,7 +11512,7 @@ def measure_colocalization( FieldSpec("manders_m1", float), ), ), - call_kwargs={}, + kwargs={}, source_aliases=("DNA", "ER"), ) ) @@ -11588,7 +11588,7 @@ def measure_neighbors(image: np.ndarray) -> tuple[np.ndarray, dict[str, float]]: row_type=NeighborMeasurements, ), source_image_name="OrigBlue", - call_kwargs={ + kwargs={ "distance_method": DistanceMethod.WITHIN, "neighbor_distance": 4, }, @@ -11679,7 +11679,7 @@ def test_track_objects_record_builder_uses_nominal_image_table_ownership() -> No ) ), source_image_name="OrigColor", - call_kwargs={"pixel_radius": 50}, + kwargs={"pixel_radius": 50}, ) ) @@ -11782,7 +11782,7 @@ def test_object_label_output_recorder_uses_output_label_domain() -> None: spec=_output_from_input("ExpandedObjects", "InputObjects"), output_value=output_payload, source_image_name=None, - call_kwargs={}, + kwargs={}, ) ) @@ -11946,7 +11946,7 @@ def align_function( row_type=AlignShiftMeasurement, ), source_image_name="Stain1Raw__Stain2Raw", - call_kwargs={}, + kwargs={}, ) _seed_align_output_provenance( request, @@ -12018,7 +12018,7 @@ def test_align_measurement_builder_records_additional_output_shifts() -> None: row_type=AlignShiftMeasurement, ), source_image_name=None, - call_kwargs={}, + kwargs={}, ) _seed_align_output_provenance( request, @@ -12099,7 +12099,7 @@ def classify_like(image: np.ndarray) -> tuple[np.ndarray, ClassificationResult]: classify_like(np.zeros((2, 2), dtype=np.float32))[1] ), source_image_name=None, - call_kwargs={}, + kwargs={}, ) ).rows.row_mappings() @@ -12218,7 +12218,7 @@ def test_untangle_measurement_object_names_use_compiled_relations( rows=MeasurementSparseColumnarRows.from_rows((), fields=()), subject=MeasurementSubject(MeasurementScope.IMAGE, "Image"), ), - call_kwargs={}, + kwargs={}, ) assert ( @@ -12280,7 +12280,7 @@ def test_untangle_object_measurements_do_not_inherit_input_image_identity() -> N ), source_image_name="WormObjectsBinary", source_image_payload=np.zeros((8, 8), dtype=np.uint8), - call_kwargs={"overlap_style": OverlapStyle.BOTH}, + kwargs={"overlap_style": OverlapStyle.BOTH}, ) source_image_name = UntangleWormsModule.measurement_record_source_image_name( @@ -12549,7 +12549,7 @@ def test_relationship_measurements_preserve_pure_2d_slice_indices() -> None: spec=executor.callable_contract.artifact_outputs.specs[0], output_value=payload, source_image_name=None, - call_kwargs={}, + kwargs={}, ) _record_output(request, request.spec, payload) @@ -12628,7 +12628,7 @@ def reject_ref_reconstruction(*_args: object, **_kwargs: object) -> None: spec=relationship_spec, output_value=payload, source_image_name=None, - call_kwargs={}, + kwargs={}, ) _record_output(request, relationship_spec, payload) @@ -12672,7 +12672,7 @@ def test_output_record_request_rejects_ref_equivalent_endpoint_reconstruction() spec=relationship_spec, output_value=DirectedObjectRelationshipPayload(source_ids=(), target_ids=()), source_image_name=None, - call_kwargs={}, + kwargs={}, ) ref_equivalent_spec = replace(endpoint_spec) @@ -12713,7 +12713,7 @@ def test_output_record_request_rejects_ambiguous_exact_endpoint_occurrence() -> spec=relationship_spec, output_value=DirectedObjectRelationshipPayload(source_ids=(), target_ids=()), source_image_name=None, - call_kwargs={}, + kwargs={}, ) with pytest.raises(RuntimeError, match="multiple exact compiled input edges"): @@ -12804,7 +12804,7 @@ def test_relationship_rows_project_temporal_endpoints_to_their_declared_slices() output_value=payload, source_image_name=None, source_image_payload=np.zeros((2, 5, 5), dtype=np.float32), - call_kwargs={}, + kwargs={}, ) _record_output(request, relationship_spec, payload) @@ -12917,7 +12917,7 @@ def test_relationship_measurements_reject_singleton_label_domain_broadcast() -> spec=executor.callable_contract.artifact_outputs.specs[0], output_value=payload, source_image_name=None, - call_kwargs={}, + kwargs={}, ) _record_output(request, request.spec, payload) @@ -12978,7 +12978,7 @@ def test_relationship_rows_use_declared_relationship_outputs_for_measurement_onl spec=measurement_spec, output_value=MeasurementSparseColumnarRows.from_rows((), fields=()), source_image_name=None, - call_kwargs={}, + kwargs={}, declared_only_outputs=MappingProxyType({relationship_spec.ref(): payload}), ) @@ -13108,7 +13108,7 @@ def test_object_measurement_table_uses_provenance_without_image_ownership() -> N "test-axis", component=None, value=None ), ), - call_kwargs={}, + kwargs={}, ) empty_rows = MeasurementSparseColumnarRows.from_rows((), fields=()) @@ -13203,7 +13203,7 @@ def test_object_output_table_uses_provenance_without_image_ownership() -> None: source_image_name=None, source_image_payload=np.zeros((2, 5, 5), dtype=np.float32), adapter=runtime, - call_kwargs={}, + kwargs={}, ) rows = MeasurementSparseColumnarRows.from_rows((), fields=()) @@ -13306,7 +13306,7 @@ def test_relate_objects_measurement_table_rejects_distinct_image_set_axes() -> N "test-axis", component=None, value=None ), ), - call_kwargs={}, + kwargs={}, ) with pytest.raises(ValueError, match="source image-set axis"): @@ -13347,7 +13347,7 @@ def test_object_lineage_measurement_table_preserves_current_payload_metadata() - output_value=MeasurementSparseColumnarRows.from_rows((), fields=()), source_image_payload=carrier, adapter=_FakeCellProfilerRuntime({}), - call_kwargs={}, + kwargs={}, ) source_metadata = MaskObjectsModule.measurement_record_source_metadata( @@ -13401,7 +13401,7 @@ def test_relationship_rows_do_not_slice_payload_scoped_3d_lineage_by_z_plane() - spec=relationship_spec, output_value=payload, source_image_name=None, - call_kwargs={}, + kwargs={}, ) CellProfilerOutputRecorder.for_artifact_type(ObjectLabelsArtifactType).record( @@ -13487,7 +13487,7 @@ def test_object_lineage_transform_measurement_record_includes_parent_rows() -> N source_image_name="Carrier", source_image_payload=np.ones((2, 2), dtype=np.float32), current_image=np.ones((2, 2), dtype=np.float32), - call_kwargs={}, + kwargs={}, ) payload = DirectedObjectRelationshipPayload(source_ids=(1, 2), target_ids=(1, 2)) @@ -13554,7 +13554,7 @@ def test_relateobjects_relationship_rows_project_distances_nominally() -> None: spec=executor.callable_contract.artifact_outputs.specs[0], output_value=MeasurementSparseColumnarRows.from_rows((), fields=()), source_image_name=None, - call_kwargs={}, + kwargs={}, ) _record_output(request, request.spec, payload) @@ -13623,7 +13623,7 @@ def test_relateobjects_relationship_rows_project_parent_mean_distances() -> None spec=executor.callable_contract.artifact_outputs.specs[0], output_value=MeasurementSparseColumnarRows.from_rows((), fields=()), source_image_name=None, - call_kwargs={"calculate_per_parent_means": True}, + kwargs={"calculate_per_parent_means": True}, ) _record_output(request, request.spec, payload) @@ -13806,7 +13806,7 @@ def test_relateobjects_parent_means_align_scoped_child_tables_by_source_plane() spec=executor.callable_contract.artifact_outputs.specs[0], output_value=MeasurementSparseColumnarRows.from_rows((), fields=()), source_image_name=None, - call_kwargs={"calculate_per_parent_means": True}, + kwargs={"calculate_per_parent_means": True}, ) _record_output(request, request.spec, payload) @@ -13934,7 +13934,7 @@ def test_relateobjects_relationship_rows_project_distances_from_slice_measuremen spec=executor.callable_contract.artifact_outputs.specs[0], output_value=MeasurementSparseColumnarRows.from_rows((), fields=()), source_image_name=None, - call_kwargs={}, + kwargs={}, ) _record_output(request, request.spec, payload) @@ -14046,7 +14046,7 @@ def test_spatial_grid_output_recorder_accepts_pure_2d_grid_sequence() -> None: adapter=runtime, spec=grid_spec, output_value=None, - call_kwargs={}, + kwargs={}, ) grids = [ SpatialGrid( @@ -14365,7 +14365,7 @@ def test_identify_objects_in_grid_location_rows_preserve_empty_grid_slots() -> N spec=measurement_spec, output_value=stats, source_image_name=None, - call_kwargs={ + kwargs={ "topology_inputs": (grid, guide_labels), "shape_choice": "natural_shape_and_location", }, @@ -14497,7 +14497,7 @@ def test_identify_objects_in_grid_location_rows_use_slice_aligned_grid() -> None spec=measurement_spec, output_value=RuntimeSliceAlignedValues(tuple(output[1] for output in outputs)), source_image_name=None, - call_kwargs={ + kwargs={ "topology_inputs": ( RuntimeSliceAlignedValues((grid, grid)), np.stack((first_guides, second_guides), axis=0), diff --git a/tests/unit/test_cellprofiler_neighbors_contract.py b/tests/unit/test_cellprofiler_neighbors_contract.py index 2a2d824df9..46cefbd2a0 100644 --- a/tests/unit/test_cellprofiler_neighbors_contract.py +++ b/tests/unit/test_cellprofiler_neighbors_contract.py @@ -588,7 +588,7 @@ def test_compiled_neighbor_distance_contract_drives_runtime_rows() -> None: ) projected = MeasureObjectNeighborsModule.MeasurementRows.for_request( MeasureObjectNeighborsModule, - SimpleNamespace(output_value=rows, call_kwargs=invocation.kwargs_dict), + SimpleNamespace(output_value=rows, kwargs=invocation.kwargs_dict), ).rows() assert len(projected) == 2 diff --git a/tests/unit/test_cellprofiler_output_record_occurrence.py b/tests/unit/test_cellprofiler_output_record_occurrence.py index 3f33d40f8f..0e66b8cb3f 100644 --- a/tests/unit/test_cellprofiler_output_record_occurrence.py +++ b/tests/unit/test_cellprofiler_output_record_occurrence.py @@ -1,10 +1,13 @@ from types import SimpleNamespace +from dataclasses import FrozenInstanceError, fields, replace +import numpy as np import pytest from openhcs.core.artifacts import ( ArtifactOutputPlan, ArtifactSpec, + ImageArtifactType, ObjectLabelsArtifactType, ) from openhcs.core.callable_contract import CallableContract, CallableMetadata @@ -13,8 +16,11 @@ FunctionInvocationKey, InvocationArtifactInputEdgePlan, InvocationArtifactInputProjectionKey, + MainFlowInputProjection, ) from openhcs.core.runtime_adapters import RuntimeAdapterRequest +from openhcs.core.runtime_image_values import image_payload_data, image_payload_metadata +from openhcs.interop.cellprofiler.runtime.artifact_binding import RuntimeInputBindingRequest from openhcs.interop.cellprofiler.runtime.output_record_request import ( CellProfilerOutputRecordRequest, ) @@ -73,7 +79,7 @@ def _output_record_request( output_plan=output_plan, output_value=object(), source=SimpleNamespace(), - call_kwargs={}, + kwargs={}, current_image=object(), ), edges, @@ -150,7 +156,7 @@ def test_output_source_uses_compiled_runtime_occurrence_for_repeated_roles( output_plan=output_plan, output_value=object(), source=SimpleNamespace(), - call_kwargs={}, + kwargs={}, current_image=object(), ) @@ -246,3 +252,161 @@ def test_output_record_request_rejects_out_of_range_index() -> None: (declared,), ((1, declared),), ) + + +def _image_record_request() -> tuple[CellProfilerOutputRecordRequest, ArtifactSpec]: + spec = ArtifactSpec.input("Original", ImageArtifactType) + request, (edge,) = _output_record_request((spec,), ((0, spec),)) + edge = replace(edge, main_flow_projection=MainFlowInputProjection.COMPLETE_PAYLOAD) + adapter = SimpleNamespace(request=replace( + request.adapter.request, artifact_inputs={edge.key: edge} + )) + return replace( + request, adapter=adapter, active_input_edges=(edge,), + current_image=np.zeros((3, 4), dtype=np.float32), + ), spec + + +def test_record_binding_keeps_output_admission_before_live_input_validation( + monkeypatch, +) -> None: + request, _spec = _image_record_request() + error = RuntimeError("live input declaration failed") + + def fail_selection(_self): + raise error + + monkeypatch.setattr(RuntimeAdapterRequest, "selected_artifact_input_specs", fail_selection) + # Output-only recording does not observe input declaration callbacks. + assert replace(request).output_value is request.output_value + with pytest.raises(ValueError, match="does not match active output"): + replace(request, output_plan=replace(request.output_plan, name="Wrong")) + # Both source-read operations preserve the original first exception identity. + for read in (request.artifact_input_value, request.artifact_source_payload): + with pytest.raises(RuntimeError) as caught: + read(request.active_input_edges[0]) + assert caught.value is error + + +def test_record_binding_reads_current_pixels_and_mutable_kwargs_without_holder_aliases() -> None: + request, spec = _image_record_request() + assert isinstance(request, RuntimeInputBindingRequest) + assert "call_kwargs" not in {item.name for item in fields(request)} + assert not hasattr(request, "__dict__") + with pytest.raises(TypeError): + CellProfilerOutputRecordRequest( + request.callable_contract, request.active_input_edges, + adapter=request.adapter, kwargs=request.kwargs, + current_image=request.current_image, spec=request.spec, + output_plan=request.output_plan, output_value=request.output_value, + source=request.source, + ) + with pytest.raises(FrozenInstanceError): + request.kwargs = {} + request.kwargs["after_call"] = [1] + copied = replace(request) + assert copied.kwargs is request.kwargs + request.kwargs["after_call"].append(2) + request.current_image[:] = 0.5 + value = request.declared_artifact_value(spec) + assert image_payload_data(value) is request.current_image + assert copied.kwargs["after_call"] == [1, 2] + np.testing.assert_array_equal(image_payload_data(value), 0.5) + request.current_image[:] = 0.75 + source = request.artifact_source_payload(request.active_input_edges[0]) + assert image_payload_data(source) is request.current_image + np.testing.assert_array_equal(image_payload_data(source), 0.75) + assert image_payload_metadata(source).source_image_names == (spec.name,) + + +def test_record_endpoint_identity_does_not_change_reference_broadcast_selection() -> None: + request, spec = _image_record_request() + equivalent = replace(spec) + with pytest.raises(RuntimeError, match="has no exact compiled input edge"): + request.declared_artifact_value(equivalent) + # The parent's edge API and its reference-only broadcast keep their domains. + edge = request.active_input_edges[0] + assert request.artifact_value(edge) is request.current_image + assert request.input_edge_for_spec(equivalent) is edge + broadcast = request.stack_broadcast_source_value(equivalent.ref()) + assert image_payload_data(broadcast) is request.current_image + + +def test_record_input_selection_effects_remain_at_each_read_epoch(monkeypatch) -> None: + request, spec = _image_record_request() + original_selection = RuntimeAdapterRequest.selected_artifact_input_specs + events = [] + + def observe_selection(adapter_request): + events.append(request.kwargs["epoch"]) + return original_selection(adapter_request) + + monkeypatch.setattr( + RuntimeAdapterRequest, "selected_artifact_input_specs", observe_selection + ) + # The original binding operation is an independent reference for callback + # ordering; the record specialization must not move it to construction. + for epoch in ("first", "after_raw_mutation"): + request.kwargs["epoch"] = epoch + events.clear() + original = RuntimeInputBindingRequest( + adapter=request.adapter, kwargs=request.kwargs, + current_image=request.current_image, + ).runtime_value(request.active_input_edges[0]) + expected_events = tuple(events) + events.clear() + actual = request.declared_artifact_value(spec) + assert tuple(events) == expected_events + assert image_payload_data(actual) is image_payload_data(original) + assert image_payload_metadata(actual) == image_payload_metadata(original) + + +def test_record_explicit_object_subset_preserves_binding_admission() -> None: + first = ArtifactSpec.input("First", ObjectLabelsArtifactType) + second = ArtifactSpec.input("Second", ObjectLabelsArtifactType) + request, _edges = _output_record_request((first, second), ((0, first), (1, second))) + standalone = RuntimeInputBindingRequest( + adapter=request.adapter, kwargs=request.kwargs, current_image=request.current_image, + ) + for subset in ((first,), (replace(second),), ()): + actual = request.with_object_inputs(subset) + expected = standalone.with_object_inputs(subset) + assert actual.object_inputs == expected.object_inputs + assert actual.active_input_edges is request.active_input_edges + assert actual.output_value is request.output_value + assert actual.kwargs is request.kwargs + undeclared = (ArtifactSpec.input("Unknown", ObjectLabelsArtifactType),) + with pytest.raises(ValueError) as expected: + standalone.with_object_inputs(undeclared) + with pytest.raises(ValueError) as actual: + request.with_object_inputs(undeclared) + assert str(actual.value) == str(expected.value) + + +def test_record_subset_validation_runs_after_output_admission_and_preserves_error_identity( + monkeypatch, +) -> None: + request, _spec = _image_record_request() + error = RuntimeError("subset declaration failure") + + def fail_selection(_self): + raise error + + monkeypatch.setattr(RuntimeAdapterRequest, "selected_artifact_input_specs", fail_selection) + for change in ( + lambda: RuntimeInputBindingRequest( + adapter=request.adapter, kwargs=request.kwargs, + current_image=request.current_image, selected_object_inputs=(), + ), + lambda: request.with_object_inputs(()), + ): + with pytest.raises(RuntimeError) as caught: + change() + assert caught.value is error + with pytest.raises(ValueError, match="does not match active output"): + replace( + request, selected_object_inputs=(), + output_plan=replace(request.output_plan, name="Wrong"), + ) + # The ordinary post-call record still observes no input selector at birth. + assert replace(request).selected_object_inputs is None diff --git a/tests/unit/test_cellprofiler_output_recording.py b/tests/unit/test_cellprofiler_output_recording.py index 24bbc0bb5e..6a03139bde 100644 --- a/tests/unit/test_cellprofiler_output_recording.py +++ b/tests/unit/test_cellprofiler_output_recording.py @@ -152,7 +152,7 @@ def test_relationship_recording_uses_exact_artifact_relation() -> None: target_ids=(2,), ), source=source, - call_kwargs={}, + kwargs={}, current_image=source.payload, ) @@ -233,7 +233,7 @@ def source_payload(cls, request: CellProfilerOutputRecordRequest) -> object: execution_mode=ImagePayloadExecutionMode.NATURAL, plane_projection=plane_projection, ), - call_kwargs={}, + kwargs={}, current_image=source_slice, ) ) @@ -288,7 +288,7 @@ def test_output_recording_carries_exact_invocation_plane_projection( request = recorder.record.call_args.args[0] assert request.source is invocation - assert request.call_kwargs is call_kwargs + assert request.kwargs is call_kwargs assert request.source.payload is image assert request.source.source_aliases == ("OriginalCarrier",) assert request.source.image_count == 2 diff --git a/tests/unit/test_child_measurement_producer_selection.py b/tests/unit/test_child_measurement_producer_selection.py index 0f544a3f2d..43a0c28fe2 100644 --- a/tests/unit/test_child_measurement_producer_selection.py +++ b/tests/unit/test_child_measurement_producer_selection.py @@ -504,7 +504,7 @@ def _upstream_rows(*, sites=("1", "2"), well="A01", time="1"): image_count=2, payload=image, ), - call_kwargs={}, + kwargs={}, current_image=image, ) return RelateObjectsRelationshipMeasurementRows(request), child, contract From fcdca8c82650ae2df04600fa01684a97ef65cf2f Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 12:31:48 -0400 Subject: [PATCH 142/235] docs(runtime): record merged binding ownership and fresh paired performance --- .../README.rst | 34 +++++++++++++++++++ .../runtime-owner-consolidation-20261003.rst | 20 +++++++++++ 2 files changed, 54 insertions(+) create mode 100644 benchmark/results/perf_invocation_owner_paired_20261003/README.rst diff --git a/benchmark/results/perf_invocation_owner_paired_20261003/README.rst b/benchmark/results/perf_invocation_owner_paired_20261003/README.rst new file mode 100644 index 0000000000..ad7037234f --- /dev/null +++ b/benchmark/results/perf_invocation_owner_paired_20261003/README.rst @@ -0,0 +1,34 @@ +Fresh invocation-owner paired checkpoint +======================================== + +Source ``c59eaa55fe3d3e49f3ecb727a14a91e56e2957f6`` contains the six +carrier removals and checkpoint correction. It precedes the output-record +binding specialization merged as PR #544. Both ordinary repetitions and two +fresh measured native repetitions are retained in ``timing_summary.json``. + +3D mean OpenHCS execution is 9.021577s, total 11.020390s, and native warm +invocation 14.793260s: 1.63976x execution and 1.34235x total. Speckles means +are 1.554196s execution, 2.433450s total and 1.964025s native: 1.26369x +execution and 0.807095x total. The two 3D execution observations are 9.616797s +and 8.426356s; Speckles observations are 1.810366s and 1.298026s. This +spread and two observations do not establish causal improvement or regression. + +All eight cross-comparisons pass the existing strict scientific image, +measurement, nonempty-domain, correlation and complete-inventory gates. Native +CP 3.9/JVM 11 import, loading and full warmup are outside its invocation clock. +Mandatory OpenHCS READY preparation and server startup/shutdown are outside +pipeline clocks. OpenHCS execution excludes compilation; total includes it +and ordinary OUTCOMES completion. One well, one inline worker and one native +CPU thread are used, with the ordinary memory observer. + +``qualification.json`` pins the original controller, source/dependency/input +joins, full strict-science report and verified lossless scientific custody +archive. The archive is 1,766,472 bytes for all 377 scientific files; its +SHA-256 is ``1cf533efca21da3638ae43931b5f5685a8790e3c45ae812622d10efcbcbfa6e8``. +Full strict report SHA-256 is +``08710272d7e7cbb14d6d12ab184532573267c777b68d86bfddd3d226a4270e3b``. +All original outputs remain retained. The local artifact paths are custody +references, not a claim that this directory is a standalone runnable bundle. + +This is a current two-case descriptive checkpoint. Full30, scaling, fresh +scaling figures and the performance goal remain unfinished. diff --git a/docs/refactor/receipts/runtime-owner-consolidation-20261003.rst b/docs/refactor/receipts/runtime-owner-consolidation-20261003.rst index 01398b8f1a..62cb0b824e 100644 --- a/docs/refactor/receipts/runtime-owner-consolidation-20261003.rst +++ b/docs/refactor/receipts/runtime-owner-consolidation-20261003.rst @@ -62,6 +62,18 @@ passes all 100 tests. After merging PR #538, the exact retained-plane and alias controls pass all ten tests; an identical test duplicated by Git's merge is removed, leaving its original definition intact. +``CellProfilerOutputRecordRequest`` now specializes the existing input-binding +owner. Adapter, kwargs and current image are inherited once, and two temporary +binding-holder constructions are removed. The binding edge operation remains +``artifact_value(edge)``; the record's distinct declaration operation is +``declared_artifact_value(spec)``. Output-plan admission, exact endpoints, +reference broadcasts, live per-read validation and mutation epochs remain +separate and tested. Explicit subsets satisfy the inherited admission law; +record construction is keyword-only. PR #544 is merged, closing #542. After +merging latest main ``8ab4a0df7``, its clean candidate ``a23978e3c`` passes +all 470 tests in the seven-file CP gate. No pipeline timing gain is claimed +for this lane. + Publication reader consolidation uses the existing document handler and source projection builder. Required projection-only viewer consumers remain. Its 94 reader controls pass; four saved-operand replays remove 151ms median locally @@ -95,6 +107,14 @@ family projection and method comparison support ownership inspection, not complete runtime equivalence. Unchanged method bodies do not prove constructor or effect behavior; those have separate executed controls. +Fresh paired evidence at ``c59eaa55f`` is retained under +``benchmark/results/perf_invocation_owner_paired_20261003``. All eight strict +scientific comparisons pass. Mean 3D execution is 9.021577s versus native +14.793260s (1.640x); Speckles is 1.554196s versus 1.964025s (1.264x). +Two observations have substantial spread and do not establish a causal gain. +This source precedes the output-record specialization. Its remaining execution +gap to 2x is 1.624947s for 3D and 0.572183s for Speckles. + PR #394 remains draft. Its original whole-branch R0/R1 obligations, installed consumer acceptance, full-catalog parity and fresh full30/scaling figures remain open. Startup and required library/kernel preparation remain outside From dca7d9a53a4afe1ad6d51b9632747baa17cda3dd Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 12:31:59 -0400 Subject: [PATCH 143/235] docs(benchmark): retain exact paired timing and scientific custody records --- .../qualification.json | 64 ++++++++++++++++ .../timing_summary.json | 73 +++++++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 benchmark/results/perf_invocation_owner_paired_20261003/qualification.json create mode 100644 benchmark/results/perf_invocation_owner_paired_20261003/timing_summary.json diff --git a/benchmark/results/perf_invocation_owner_paired_20261003/qualification.json b/benchmark/results/perf_invocation_owner_paired_20261003/qualification.json new file mode 100644 index 0000000000..fd439f7f15 --- /dev/null +++ b/benchmark/results/perf_invocation_owner_paired_20261003/qualification.json @@ -0,0 +1,64 @@ +{ + "status": "PASS", + "source_head": "c59eaa55fe3d3e49f3ecb727a14a91e56e2957f6", + "source": "/var/tmp/openhcs-owner-benchmark-c59eaa55f", + "source_clean": true, + "source_freeze": { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/invocation-owner-c59-paired-v2/source-freeze.json", + "sha256": "71d4df68c1755f372b4096c468dddc4c03acf60e538e64c63a1272aef61b606f" + }, + "science": { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/invocation-owner-c59-paired-v2/strict-science.json", + "sha256": "08710272d7e7cbb14d6d12ab184532573267c777b68d86bfddd3d226a4270e3b", + "comparisons": 8, + "gate": "Existing strict runtime measurement/image/correlation/inventory authorities; exact3D images, originalCPnumericpolicy,nonvacuousCSVdomain." + }, + "timing_summary": { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/invocation-owner-c59-paired-v2/timing_summary.json", + "sha256": "2b8caa5b22cc877f1c77a7ab882d6fdd18dc2efec7c6727a032d87093cc46ede" + }, + "custody_archive": { + "manifest": "/home/ts/.local/state/openhcs-maintenance/20261003/invocation-owner-c59-paired-v2-preparation/scientific-custody-archive-v1.json", + "manifest_sha256": "fca4765c1bc339f707010d31a9acb145e04b18927a2a1928eed3292277ae5ec2", + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/invocation-owner-c59-paired-v2.scientific-custody.tar.gz", + "sha256": "1cf533efca21da3638ae43931b5f5685a8790e3c45ae812622d10efcbcbfa6e8", + "bytes": 1766472, + "files": 377, + "original_output_bytes": 37850708 + }, + "controller": { + "path": "/home/ts/.local/state/openhcs-maintenance/20261003/invocation-owner-c59-paired-v2-preparation/controller.py", + "sha256": "7a2d2265a391c2e8ebdbbb057fdebaab2b46f39024d6d9ad8ce3602f30bd56ec", + "baseline": "/home/ts/.local/state/openhcs-maintenance/20261003/current-3d-speckles-paired-preparation-v2/controller.py", + "scope": "Reviewed original driver/native/science implementation retained; changes only detachedsource,custodyactualeditableimportowner discovery and currentmissingGitblobdocumentation supplement." + }, + "source_input_refs": { + "cp_tutorial_3d_monolayer": 180, + "ExampleSpeckles": 2 + }, + "physical_inputs_sha256": "980311696da713abd37ba7588dc21d06e0552ec4e5e76a72e7fcf2e636ce255e", + "all_handles_terminal": { + "prepare": { + "session": 87187, + "code": 0 + }, + "ordinary": { + "session": 23481, + "code": 0 + }, + "native": { + "session": 82103, + "code": 0 + }, + "compare": { + "session": 19497, + "code": 0 + } + }, + "retained_failure": { + "session": 26868, + "code": 1, + "reason": "Oldsparsecustody lacked7currentunexecuteddocs; no driver launched; V1 source/controller/log/output preserved." + }, + "scope": "Fresh descriptive2repetitions1well1workerCPU5/defaultOUTCOMES/RSS,READY/startupoutsideclocks; nativeCP39/JVM11 import/load/warmup excluded. No causal gain/full30/scalingclaim. Mutable root source notexecuted; only frozen detachedc59 and exactinstalled8editables used. Scientificoutputs retainedimmutable; no captures/newenv." +} diff --git a/benchmark/results/perf_invocation_owner_paired_20261003/timing_summary.json b/benchmark/results/perf_invocation_owner_paired_20261003/timing_summary.json new file mode 100644 index 0000000000..9de0ca741f --- /dev/null +++ b/benchmark/results/perf_invocation_owner_paired_20261003/timing_summary.json @@ -0,0 +1,73 @@ +{ + "status": "QUALIFIED_DESCRIPTIVE_TWO_REPEATS", + "source_head": "c59eaa55fe3d3e49f3ecb727a14a91e56e2957f6", + "cases": { + "cp_tutorial_3d_monolayer": { + "ordinary_observations": { + "compile_seconds": [ + 1.1835861206054688, + 1.1569459438323975 + ], + "execute_seconds": [ + 9.616796970367432, + 8.426356315612793 + ], + "total_seconds": [ + 11.650369129143655, + 10.390410478692502 + ] + }, + "native_invocation_observations": [ + 14.520403317175806, + 15.06611712090671 + ], + "means": { + "compile_seconds": 1.170266032218933, + "execute_seconds": 9.021576642990112, + "total_seconds": 11.020389803918079, + "native_invocation_seconds": 14.793260219041258 + }, + "execution_speedup": 1.6397644008861603, + "total_speedup": 1.3423536265279665, + "remaining_2x_execution_gap": 1.6249465334694833, + "source_head": "c59eaa55fe3d3e49f3ecb727a14a91e56e2957f6", + "well_count": 1, + "image_sets": 1, + "science": "ALL4_PAIR_COMPARISONS_PASS" + }, + "ExampleSpeckles": { + "ordinary_observations": { + "compile_seconds": [ + 0.5536556243896484, + 0.5043470859527588 + ], + "execute_seconds": [ + 1.810366153717041, + 1.2980256080627441 + ], + "total_seconds": [ + 2.7351605952717364, + 2.1317388960160315 + ] + }, + "native_invocation_observations": [ + 1.9850073368288577, + 1.9430433670058846 + ], + "means": { + "compile_seconds": 0.5290013551712036, + "execute_seconds": 1.5541958808898926, + "total_seconds": 2.433449745643884, + "native_invocation_seconds": 1.9640253519173712 + }, + "execution_speedup": 1.2636922900560132, + "total_speedup": 0.8070950942928536, + "remaining_2x_execution_gap": 0.572183204931207, + "source_head": "c59eaa55fe3d3e49f3ecb727a14a91e56e2957f6", + "well_count": 1, + "image_sets": 1, + "science": "ALL4_PAIR_COMPARISONS_PASS" + } + }, + "scope": "Two ordinary observations versus two fresh measured native warm observations. READY/serverstartup/shutdown and nativeimports/JVM/pipelineload/fullwarmup excluded by existing clocks. DefaultOUTCOMES/RSS. No causal optimization/full30/scaling claim. Native invocation clock covers full selected well; no division by plane/image-set count." +} From 61ab83842ce23e3c2c67d76d68167b5084d856c8 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 12:50:10 -0400 Subject: [PATCH 144/235] refactor(runtime): make loaded cohort the execution scope Remove FunctionRuntimeScope, its execution bridge, adapter factories, and artifact-load forwarding. Derive invocation requests from the existing complete PatternGroupData owner. Preserve initial capture and live plan semantics. Refs #496, #479. Validation: 844 passing tests; additional 167 passes and three identical retained baseline producer-record failures. --- .../runtime-owner-consolidation-20261003.rst | 15 + openhcs/core/aligned_image_payload.py | 74 +++- openhcs/core/runtime_adapters.py | 79 ---- openhcs/core/steps/function_runtime.py | 378 +++++++----------- .../test_cellprofiler_module_execution.py | 22 +- ...test_cellprofiler_static_deletion_gates.py | 3 +- tests/unit/test_debug_runtime.py | 9 +- tests/unit/test_framework_execution_scope.py | 2 +- tests/unit/test_function_artifact_outputs.py | 11 +- .../test_function_core_executor_ownership.py | 19 +- ...test_function_runtime_source_projection.py | 54 +-- .../test_loaded_pattern_group_ownership.py | 254 ++++++++++++ .../test_opaque_main_flow_image_domain.py | 19 +- .../unit/test_path_planner_materialization.py | 8 +- .../unit/test_runtime_context_declaration.py | 8 +- 15 files changed, 574 insertions(+), 381 deletions(-) create mode 100644 tests/unit/test_loaded_pattern_group_ownership.py diff --git a/docs/refactor/receipts/runtime-owner-consolidation-20261003.rst b/docs/refactor/receipts/runtime-owner-consolidation-20261003.rst index 62cb0b824e..de8047deb0 100644 --- a/docs/refactor/receipts/runtime-owner-consolidation-20261003.rst +++ b/docs/refactor/receipts/runtime-owner-consolidation-20261003.rst @@ -74,6 +74,21 @@ merging latest main ``8ab4a0df7``, its clean candidate ``a23978e3c`` passes all 470 tests in the seven-file CP gate. No pipeline timing gain is claimed for this lane. +``PatternGroupData`` now specializes ``PatternGroupExecutionScope`` and owns +one complete loaded cohort. ``FunctionRuntimeScope`` and its execution bridge +are deleted. ``FunctionCoreExecutor`` consumes the cohort directly; the two +adapter factories and the artifact-input forwarding helper are removed. +Image composition moves to the existing ``ImagePayloadStackComposition`` owner. +Initial source coordinates and plane count remain captured at their original +load boundary; mutable plan selectors remain live, and later image/memory +progression remains separate from that initial cohort. Frozen slots, transport +serialization, first errors, debug events and alias/mutation controls pass. +The exact migration has 844 wholly passing tests. An additional consumer shard +has 167 passes and three dynamic producer-record failures reproduced unchanged +with the three original production modules. Those failures remain open consumer +work, not waived tests. The bounded NRA census changes from 164 to 163 classes. +No pipeline timing gain is claimed; this is part of a larger semantic batch. + Publication reader consolidation uses the existing document handler and source projection builder. Required projection-only viewer consumers remain. Its 94 reader controls pass; four saved-operand replays remove 151ms median locally diff --git a/openhcs/core/aligned_image_payload.py b/openhcs/core/aligned_image_payload.py index 8203db5ad4..4625facf2d 100644 --- a/openhcs/core/aligned_image_payload.py +++ b/openhcs/core/aligned_image_payload.py @@ -6,7 +6,7 @@ from collections.abc import Iterator, Sequence from dataclasses import dataclass, replace from enum import Enum -from typing import Any, ClassVar, Mapping +from typing import Any, ClassVar, Mapping, TYPE_CHECKING import numpy as np from metaclass_registry import AutoRegisterMeta @@ -58,6 +58,15 @@ ) +if TYPE_CHECKING: + from openhcs.core.compiled_step_plan import CompiledStepPlan + from openhcs.core.steps.function_output_manifest import ProducedOutputSemantics + from openhcs.core.source_workspace_projection import ( + VirtualWorkspaceSourceProjection, + VirtualWorkspacePathLookup, + ) + + @dataclass(frozen=True, slots=True) class ImagePayloadSourceSpatialDomainAdapter(SourceSpatialDomainAdapter): """Source-domain adapter for image payload data and masks.""" @@ -423,6 +432,69 @@ def composition_payloads(self) -> tuple[Any, ...]: ... @abstractmethod def composition_metadata_mode(self) -> ImagePayloadMetadataCompositionMode: ... + @staticmethod + def validate_main_flow_cohort( + producer_records: Sequence[ProducedOutputSemantics] | None, + ) -> None: + """One input cohort has one declared whole-image composition domain.""" + if producer_records and len( + {record.main_flow_plane_axis for record in producer_records} + ) != 1: + raise ValueError( + "One main-flow cohort cannot combine different declared image axes." + ) + + @staticmethod + def from_loaded_images( + payloads: Sequence[RuntimeArrayData], + *, + producer_records: Sequence[ProducedOutputSemantics] | None, + execution_plan: CompiledStepPlan, + source_projection: VirtualWorkspaceSourceProjection | None, + workspace_source_lookups: Sequence[VirtualWorkspacePathLookup], + ) -> RuntimeArrayData: + """Compose a selected admissible input cohort in its declared image domain.""" + if ( + producer_records + and len(producer_records) == 1 + and producer_records[0].main_flow_plane_axis + is image_payload_metadata(payloads[0]).plane_axis + ): + main_data_stack = ImagePayloadStackComposition.copy_whole_image( + payloads[0], + memory_type=execution_plan.input_memory_type, + device_id=execution_plan.device_id_for(execution_plan.input_memory_type), + ) + else: + metadata_mode = ImagePayloadMetadataCompositionMode.STACK + if producer_records: + declared_axis = producer_records[0].main_flow_plane_axis + metadata_mode = ( + ImagePayloadMetadataCompositionMode.BUNDLE + if declared_axis is None + else ImagePayloadMetadataCompositionMode.for_plane_axis( + declared_axis + ) + ) + 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: + main_data_stack = stack_runtime_slices( + tuple(image_payload_data(payload) for payload in payloads), + execution_plan.input_memory_type, + execution_plan.device_id_for(execution_plan.input_memory_type), + ) + main_data_stack = stack_image_payload_context( + payloads, main_data_stack, metadata_mode=metadata_mode, + ) + elif metadata_mode is ImagePayloadMetadataCompositionMode.BUNDLE: + main_data_stack = ImagePayloadBundleContext.from_payloads( + tuple(payloads), metadata_mode=metadata_mode, + ).compose() + return main_data_stack + @staticmethod def copy_whole_image( value: RuntimeArrayData, diff --git a/openhcs/core/runtime_adapters.py b/openhcs/core/runtime_adapters.py index 26dadc2c89..4dbe9601f4 100644 --- a/openhcs/core/runtime_adapters.py +++ b/openhcs/core/runtime_adapters.py @@ -62,7 +62,6 @@ if TYPE_CHECKING: from openhcs.core.callable_contract import CallableContract from openhcs.core.context.processing_context import ProcessingContext - from openhcs.core.steps.function_runtime import FunctionRuntimeScope from openhcs.core.runtime_stores import RuntimeArtifactInput @@ -361,84 +360,6 @@ def source_artifact_payload(self, ref: ArtifactSpecRef) -> object: ), ) - @classmethod - def from_source_context( - cls, - *, - context: "ProcessingContext", - source_payload: object | None, - artifact_inputs: Mapping[ - "InvocationArtifactInputProjectionKey", - "InvocationArtifactInputEdgePlan", - ], - artifact_outputs: Mapping[ArtifactSpecRef, ArtifactOutputPlan], - source_binding_plan: CompiledSourceBindingPlan, - source_binding_context: SourceBindingRuntimeContext, - group_key: str | None = None, - axis_scope: RuntimeExecutionAxisScope | None = None, - plane_projection: RuntimePlaneProjection | None = None, - variable_components: tuple[VariableComponents, ...] | None = None, - source_load_plan: SourceLoadPlan | None = None, - callable_contract: "CallableContract | None" = None, - ) -> "RuntimeAdapterRequest": - """Project a source-binding runtime context into an adapter request.""" - return cls( - context=context, - callable_contract=callable_contract, - source_payload=source_payload, - artifact_inputs=artifact_inputs, - artifact_outputs=artifact_outputs, - source_binding_plan=source_binding_plan, - source_binding_context=source_binding_context, - group_key=group_key, - axis_scope=( - axis_scope - if axis_scope is not None - else RuntimeExecutionAxisScope.from_context(context) - ), - plane_projection=( - plane_projection - if plane_projection is not None - else RuntimePlaneProjection.stack() - ), - variable_components=( - tuple(variable_components) if variable_components is not None else () - ), - source_load_plan=( - source_load_plan if source_load_plan is not None else SourceLoadPlan() - ), - ) - - @classmethod - def from_runtime_scope( - cls, - *, - runtime_scope: "FunctionRuntimeScope", - artifact_inputs: Mapping[ - "InvocationArtifactInputProjectionKey", - "InvocationArtifactInputEdgePlan", - ], - artifact_outputs: Mapping[ArtifactSpecRef, ArtifactOutputPlan], - group_key: str | None, - plane_projection: RuntimePlaneProjection, - source_payload: object, - callable_contract: "CallableContract | None" = None, - ) -> "RuntimeAdapterRequest": - """Project an invocation runtime scope into an adapter request.""" - return cls.from_source_context( - context=runtime_scope.context, - callable_contract=callable_contract, - source_payload=source_payload, - artifact_inputs=artifact_inputs, - artifact_outputs=artifact_outputs, - source_binding_plan=runtime_scope.source_binding_plan, - source_binding_context=runtime_scope.source_binding_context, - group_key=group_key, - axis_scope=runtime_scope.axis_scope, - plane_projection=plane_projection, - variable_components=tuple(runtime_scope.execution_plan.variable_components), - source_load_plan=runtime_scope.execution_plan.source_load_plan, - ) RuntimeAdapterFactory = Callable[[RuntimeAdapterRequest], object] diff --git a/openhcs/core/steps/function_runtime.py b/openhcs/core/steps/function_runtime.py index 0a458a424a..b2bdd42428 100644 --- a/openhcs/core/steps/function_runtime.py +++ b/openhcs/core/steps/function_runtime.py @@ -9,7 +9,7 @@ import logging import os import time -from dataclasses import dataclass, field, replace +from dataclasses import dataclass, replace from threading import Lock from pathlib import Path from types import MappingProxyType @@ -1384,20 +1384,33 @@ def axis_scope(self) -> RuntimeExecutionAxisScope: @dataclass(frozen=True, slots=True, kw_only=True) -class FunctionRuntimeScope(PatternGroupExecutionScope): - """Generic runtime scope shared by chain, invocation, adapter, and debug code.""" +class PatternGroupExecutionRequest(PatternGroupExecutionScope): + """All runtime data needed to process one pattern group.""" + + pattern_group_info: JsonValue + component_index: int + component_count: int + + +@dataclass(frozen=True, slots=True, kw_only=True) +class PatternGroupData(PatternGroupExecutionScope): + """Complete loaded cohort and its original execution coordinates.""" artifacts: ComponentArtifactPlans[ArtifactSpecRef, ArtifactInputPlan] source_binding_context: SourceBindingRuntimeContext runtime_plane_index: int runtime_plane_count: int + matching_files: list[str] + main_data_stack: RuntimeArrayData @classmethod - def from_pattern_group( + def from_loaded_group( cls, request: "PatternGroupExecutionRequest", - loaded: "PatternGroupData", - ) -> "FunctionRuntimeScope": + matching_files: list[str], + main_data_stack: RuntimeArrayData, + source_binding_context: SourceBindingRuntimeContext, + ) -> "PatternGroupData": artifacts = ComponentArtifactPlans.from_step_component( request.execution_plan, request.component_key, @@ -1408,7 +1421,7 @@ def from_pattern_group( artifacts.outputs, ) source_provenance = image_payload_metadata( - loaded.main_data_stack + main_data_stack ).source_provenance.with_common_scalar_identity_from_planes() common_source_metadata = source_provenance.source_component_metadata or {} variable_components = ComponentSet.coerce( @@ -1430,13 +1443,15 @@ def from_pattern_group( is not None ) return cls( + matching_files=matching_files, + main_data_stack=main_data_stack, context=request.context, execution_plan=request.execution_plan, compiled_group=request.compiled_group, artifacts=artifacts, - source_binding_context=loaded.source_binding_context, + source_binding_context=source_binding_context, runtime_plane_index=request.component_index, - runtime_plane_count=len(loaded.matching_files), + runtime_plane_count=len(matching_files), component_value=request.component_value, fixed_component_values=source_provenance.require_common_component_values( fixed_components @@ -1450,171 +1465,6 @@ def require_invocations(self) -> None: f"Compiled function group {self.compiled_group.group_key} has no invocations." ) - def execute_chain( - self, initial_data_stack: RuntimeArrayData - ) -> RuntimeArrayData | NoMainFlowOutput: - self.require_invocations() - current_stack: RuntimeArrayData | NoMainFlowOutput = initial_data_stack - current_memory_type = self.execution_plan.input_memory_type - debug_sink = debug_event_sink_from_context(self.context) - declared_source_bindings = self.execution_plan.source_binding_plan - active_main_flow_bindings = self.active_main_flow_source_binding_plan( - initial_data_stack - ) - for invocation in self.compiled_group.invocations: - group_key = invocation.key.runtime_group_key(self.component_value) - artifacts = self.artifacts.select_for_invocation( - invocation, - execution_scope=self.execution_plan.execution_group_scope, - component_key=self.component_key, - ) - if artifacts is None: - continue - artifacts = artifacts.select_source_bound_inputs( - declared_source_bindings=declared_source_bindings, - active_source_bindings=active_main_flow_bindings, - ) - runtime_invocation = invocation.for_runtime_outputs( - output_plans=tuple(artifacts.outputs.values()), - ) - executor = FunctionCoreExecutor( - main_data_arg=current_stack, - source_memory_type=current_memory_type, - runtime_scope=self, - invocation=runtime_invocation, - artifacts=artifacts, - group_key=group_key, - plane_projection=RuntimePlaneProjection.stack(self.runtime_plane_count), - ) - captures_debug = debug_sink.captures_invocation_events() - if captures_debug and debug_sink.should_skip_invocation( - executor.debug_cursor() - ): - continue - - invocation_started_at = time.perf_counter() - try: - current_stack = executor.execute( - debug_sink=debug_sink if captures_debug else None, - ) - except Exception as exc: - if captures_debug: - debug_sink.record( - executor.debug_event( - DebugEventType.EXCEPTION, - exception=exc, - ) - ) - raise - invocation_seconds = time.perf_counter() - invocation_started_at - if captures_debug: - after_event = executor.debug_event( - DebugEventType.AFTER_INVOCATION, - timing_seconds=invocation_seconds, - ) - debug_sink.record(after_event) - if debug_sink.should_stop_after_invocation(after_event): - break - RuntimeProfileSink.record( - "invocation_total", - invocation_seconds, - function=invocation.key.function_name, - group=invocation.key.group_key, - position=invocation.key.position, - ) - if isinstance(current_stack, NoMainFlowOutput): - return current_stack - current_memory_type = executor.memory_types().output_type - if self.compiled_group.preserves_input_main_flow() and all( - invocation.contract.artifact_output_policy.records_outputs - for invocation in self.compiled_group.invocations - ): - return NoMainFlowOutput() - return current_stack - - -@dataclass(frozen=True, slots=True, kw_only=True) -class PatternGroupExecutionRequest(PatternGroupExecutionScope): - """All runtime data needed to process one pattern group.""" - - pattern_group_info: JsonValue - component_index: int - component_count: int - - -@dataclass(frozen=True, kw_only=True) -class PatternGroupData: - """Loaded image data for one pattern group.""" - - matching_files: list[str] - main_data_stack: RuntimeArrayData - source_binding_context: SourceBindingRuntimeContext = field( - default_factory=SourceBindingRuntimeContext.empty - ) - - - @staticmethod - def validate_main_flow_cohort( - producer_records: Sequence[ProducedOutputSemantics] | None, - ) -> None: - """One input cohort has one declared whole-image composition domain.""" - if producer_records and len({record.main_flow_plane_axis for record in producer_records}) != 1: - raise ValueError("One main-flow cohort cannot combine different declared image axes.") - - @classmethod - def from_loaded_images( - cls, - matching_files: list[str], - payloads: Sequence[RuntimeArrayData], - *, - producer_records: Sequence[ProducedOutputSemantics] | None, - source_binding_context: SourceBindingRuntimeContext, - execution_plan: CompiledStepPlan, - source_projection: VirtualWorkspaceSourceProjection | None, - workspace_source_lookups: Sequence[VirtualWorkspacePathLookup], - ) -> "PatternGroupData": - """Compose a selected admissible input cohort in its declared image domain.""" - if ( - producer_records - and len(producer_records) == 1 - and producer_records[0].main_flow_plane_axis is image_payload_metadata(payloads[0]).plane_axis - ): - main_data_stack = ImagePayloadStackComposition.copy_whole_image( - payloads[0], memory_type=execution_plan.input_memory_type, - device_id=execution_plan.device_id_for(execution_plan.input_memory_type), - ) - else: - metadata_mode = ImagePayloadMetadataCompositionMode.STACK - if producer_records: - declared_axis = producer_records[0].main_flow_plane_axis - metadata_mode = ( - ImagePayloadMetadataCompositionMode.BUNDLE - if declared_axis is None - else ImagePayloadMetadataCompositionMode.for_plane_axis(declared_axis) - ) - 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: - main_data_stack = stack_runtime_slices( - tuple(image_payload_data(payload) for payload in payloads), - execution_plan.input_memory_type, - execution_plan.device_id_for(execution_plan.input_memory_type), - ) - main_data_stack = stack_image_payload_context( - payloads, main_data_stack, metadata_mode=metadata_mode, - ) - elif metadata_mode is ImagePayloadMetadataCompositionMode.BUNDLE: - main_data_stack = ImagePayloadBundleContext.from_payloads( - tuple(payloads), metadata_mode=metadata_mode, - ).compose() - return cls( - matching_files=matching_files, - main_data_stack=main_data_stack, - source_binding_context=source_binding_context, - ) - @dataclass(frozen=True, slots=True) class OutputPathBatchEntry: @@ -1714,20 +1564,6 @@ def _save_artifact_value( return runtime_value.data -def _load_artifact_input_values( - runtime_scope: FunctionRuntimeScope, - input_plan: InvocationArtifactInputEdgePlan, -) -> tuple[RuntimeValue, ...]: - """Project producer-owned runtime records into one consumer invocation.""" - context = runtime_scope.context - return RuntimeArtifactInput( - edge_plan=input_plan, - axis_scope=runtime_scope.axis_scope, - backend=Backend.MEMORY.value, - source_binding_plan=runtime_scope.source_binding_plan, - ).projected_values(context.runtime_value_store) - - def prepare_compiled_function_group(group: CompiledFunctionGroup) -> None: """Run optional preparation hooks for each callable in a compiled group.""" for invocation in group.invocations: @@ -1783,7 +1619,7 @@ def prepare_compiled_context_callables( class FunctionCoreExecutor: """Execute one scoped callable invocation and route declared artifact I/O.""" - runtime_scope: FunctionRuntimeScope + group_data: PatternGroupData invocation: CompiledFunctionInvocation artifacts: ComponentArtifactPlans[ InvocationArtifactInputProjectionKey, @@ -1806,8 +1642,8 @@ def runtime_adapter_request( self, source_payload: RuntimePayload, ) -> RuntimeAdapterRequest: - return RuntimeAdapterRequest.from_runtime_scope( - runtime_scope=self.runtime_scope, + return RuntimeAdapterRequest( + context=self.group_data.context, callable_contract=self.invocation.contract, artifact_inputs={ edge.key: edge for edge in self.selected_artifact_input_edges @@ -1816,6 +1652,13 @@ def runtime_adapter_request( group_key=self.group_key, plane_projection=self.plane_projection, source_payload=source_payload, + source_binding_plan=self.group_data.source_binding_plan, + source_binding_context=self.group_data.source_binding_context, + axis_scope=self.group_data.axis_scope, + variable_components=tuple( + self.group_data.execution_plan.variable_components + ), + source_load_plan=self.group_data.execution_plan.source_load_plan, ) @property @@ -1837,7 +1680,7 @@ def declared_source_payload( ) stored_payload = loaded_artifact_payloads.get(source_ref) source_binding = ( - self.runtime_scope.source_binding_plan.binding_for_artifact_ref(source_ref) + self.group_data.source_binding_plan.binding_for_artifact_ref(source_ref) ) main_flow_edges = tuple( edge @@ -1914,7 +1757,7 @@ def load_artifact_inputs( RuntimeValue.from_spec( input_plan.spec, input_plan.resolve_unstored_payload(self, source_payload), - execution_scope=self.runtime_scope.axis_scope, + execution_scope=self.group_data.axis_scope, ), ) ) @@ -1948,10 +1791,12 @@ def load_artifact_input( ) load_started_at = time.perf_counter() try: - loaded_values = _load_artifact_input_values( - self.runtime_scope, - edge_plan, - ) + loaded_values = RuntimeArtifactInput( + edge_plan=edge_plan, + axis_scope=self.group_data.axis_scope, + backend=Backend.MEMORY.value, + source_binding_plan=self.group_data.source_binding_plan, + ).projected_values(self.group_data.context.runtime_value_store) except Exception as exc: logger.error( f"Failed to load artifact input '{arg_name}' from " @@ -1971,10 +1816,10 @@ def load_artifact_input( def debug_cursor(self) -> DebugCursor: return DebugCursor.from_invocation( - step_index=self.runtime_scope.execution_plan.step_index, - step_scope_id=self.runtime_scope.execution_plan.step_scope_id, + step_index=self.group_data.execution_plan.step_index, + step_scope_id=self.group_data.execution_plan.step_scope_id, invocation=self.invocation, - pattern_group_identity=str(self.runtime_scope.runtime_plane_index), + pattern_group_identity=str(self.group_data.runtime_plane_index), ) def debug_artifacts( @@ -2000,9 +1845,9 @@ def debug_event( return DebugEvent.for_invocation( event_type=event_type, cursor=self.debug_cursor(), - step_name=self.runtime_scope.execution_plan.step_name, + step_name=self.group_data.execution_plan.step_name, callable_name=self.invocation.key.function_name, - axis_id=self.runtime_scope.execution_plan.axis_id, + axis_id=self.group_data.execution_plan.axis_id, input_artifacts=self.debug_artifacts( { edge.storage_plan.ref(): edge.storage_plan @@ -2045,7 +1890,7 @@ def execute( payload=self.main_data_arg, source_type=self.source_memory_type, target_type=memory_types.input_type, - target_device_id=self.runtime_scope.execution_plan.device_id_for( + target_device_id=self.group_data.execution_plan.device_id_for( memory_types.input_type ), ).converted_payload() @@ -2103,7 +1948,7 @@ def execution_group_source_payload( source_payload: RuntimePayload, ) -> RuntimePayload: """Return source payload metadata carrying the current grouped identity.""" - component = self.runtime_scope.execution_plan.execution_group_scope.component + component = self.group_data.execution_plan.execution_group_scope.component if component is None or self.group_key is None: return source_payload metadata = image_payload_metadata(source_payload) @@ -2123,7 +1968,7 @@ def bind_runtime_owned_parameters( ) -> None: context_parameter_name = self.invocation.contract.runtime_context_parameter if context_parameter_name is not None: - final_kwargs[context_parameter_name] = self.runtime_scope.context + final_kwargs[context_parameter_name] = self.group_data.context def bind_compiled_runtime_parameters( self, @@ -2192,7 +2037,7 @@ def invoke( ) call_started_at = time.perf_counter() try: - with self.runtime_scope.execution_plan.memory_device_scope( + with self.group_data.execution_plan.memory_device_scope( contract.execution_memory_type ): raw_output = func_callable( @@ -2215,7 +2060,7 @@ def invoke( ) raise type(exc)( f"{exc} Invocation boundary: step_index={cursor.step_index}; " - f"step_name={self.runtime_scope.execution_plan.step_name!r}; " + f"step_name={self.group_data.execution_plan.step_name!r}; " f"function_invocation_key={self.invocation.key!r}; " f"module={contract.module_name!r}; " f"callable={contract.function_name!r}; " @@ -2351,11 +2196,11 @@ def save_artifact_output( materialization_source_payload ) saved_value = _save_artifact_value( - self.runtime_scope.context, + self.group_data.context, output_plan, value, output_source_payload, - execution_scope=self.runtime_scope.axis_scope, + execution_scope=self.group_data.axis_scope, group_key=self.group_key, materialization_source_metadata=materialization_source_metadata, plane_projector=self.plane_projection, @@ -2495,7 +2340,9 @@ def run(self) -> None: try: load_started_at = time.perf_counter() - loaded = self._load_input_stack() + matching_files, main_data_stack, source_binding_context = ( + self._load_input_stack() + ) except NoStepOutputManifestMatch: logger.debug( "Skipping stale pattern group %s for step %s (%s); no files " @@ -2514,7 +2361,10 @@ def run(self) -> None: pattern=self.pattern_repr, ) execute_started_at = time.perf_counter() - processed_stack = self._execute_pattern(loaded) + loaded = PatternGroupData.from_loaded_group( + self.request, matching_files, main_data_stack, source_binding_context, + ) + processed_stack = self.execute_chain(loaded) RuntimeProfileSink.record( "pattern_execute_chain", time.perf_counter() - execute_started_at, @@ -2611,7 +2461,9 @@ def _producer_output_contexts( self.request.context.microscope_handler.parser, ) - def _load_input_stack(self) -> PatternGroupData: + def _load_input_stack( + self, + ) -> tuple[list[str], RuntimeArrayData, SourceBindingRuntimeContext]: context = self.request.context plan = self.request.execution_plan request = self.request @@ -2702,7 +2554,7 @@ def _load_input_stack(self) -> PatternGroupData: if plan.main_input_dependency.kind is StepInputDependencyKind.STEP_OUTPUT else None ) - PatternGroupData.validate_main_flow_cohort(producer_records) + ImagePayloadStackComposition.validate_main_flow_cohort(producer_records) cached_stack = context.runtime_image_stack_cache.get( tuple(full_file_paths), memory_type=plan.input_memory_type, @@ -2739,11 +2591,9 @@ def _load_input_stack(self) -> PatternGroupData: f"Check file integrity and format compatibility." ) - return PatternGroupData.from_loaded_images( - matching_files, + main_data_stack = ImagePayloadStackComposition.from_loaded_images( raw_slices, producer_records=producer_records, - source_binding_context=source_binding_context, execution_plan=plan, source_projection=source_projection, workspace_source_lookups=workspace_source_lookups, @@ -2751,11 +2601,7 @@ def _load_input_stack(self) -> PatternGroupData: else: main_data_stack = cached_stack - return PatternGroupData( - matching_files=matching_files, - main_data_stack=main_data_stack, - source_binding_context=source_binding_context, - ) + return matching_files, main_data_stack, source_binding_context def _workspace_source_binding_lookups( self, @@ -3030,13 +2876,91 @@ def _apply_source_binding_payload( ) return apply_source_binding_payload(payload, binding, source_context) - def _execute_pattern( - self, - loaded: PatternGroupData, + @staticmethod + def execute_chain( + group_data: PatternGroupData ) -> RuntimeArrayData | NoMainFlowOutput: - request = self.request - runtime_scope = FunctionRuntimeScope.from_pattern_group(request, loaded) - return runtime_scope.execute_chain(loaded.main_data_stack) + group_data.require_invocations() + current_stack: RuntimeArrayData | NoMainFlowOutput = group_data.main_data_stack + current_memory_type = group_data.execution_plan.input_memory_type + debug_sink = debug_event_sink_from_context(group_data.context) + declared_source_bindings = group_data.execution_plan.source_binding_plan + active_main_flow_bindings = group_data.active_main_flow_source_binding_plan( + group_data.main_data_stack + ) + for invocation in group_data.compiled_group.invocations: + group_key = invocation.key.runtime_group_key(group_data.component_value) + artifacts = group_data.artifacts.select_for_invocation( + invocation, + execution_scope=group_data.execution_plan.execution_group_scope, + component_key=group_data.component_key, + ) + if artifacts is None: + continue + artifacts = artifacts.select_source_bound_inputs( + declared_source_bindings=declared_source_bindings, + active_source_bindings=active_main_flow_bindings, + ) + runtime_invocation = invocation.for_runtime_outputs( + output_plans=tuple(artifacts.outputs.values()), + ) + executor = FunctionCoreExecutor( + main_data_arg=current_stack, + source_memory_type=current_memory_type, + group_data=group_data, + invocation=runtime_invocation, + artifacts=artifacts, + group_key=group_key, + plane_projection=RuntimePlaneProjection.stack( + group_data.runtime_plane_count + ), + ) + captures_debug = debug_sink.captures_invocation_events() + if captures_debug and debug_sink.should_skip_invocation( + executor.debug_cursor() + ): + continue + + invocation_started_at = time.perf_counter() + try: + current_stack = executor.execute( + debug_sink=debug_sink if captures_debug else None, + ) + except Exception as exc: + if captures_debug: + debug_sink.record( + executor.debug_event( + DebugEventType.EXCEPTION, + exception=exc, + ) + ) + raise + invocation_seconds = time.perf_counter() - invocation_started_at + if captures_debug: + after_event = executor.debug_event( + DebugEventType.AFTER_INVOCATION, + timing_seconds=invocation_seconds, + ) + debug_sink.record(after_event) + if debug_sink.should_stop_after_invocation(after_event): + break + RuntimeProfileSink.record( + "invocation_total", + invocation_seconds, + function=invocation.key.function_name, + group=invocation.key.group_key, + position=invocation.key.position, + ) + if isinstance(current_stack, NoMainFlowOutput): + return current_stack + current_memory_type = executor.memory_types().output_type + if group_data.compiled_group.preserves_input_main_flow() and all( + invocation.contract.artifact_output_policy.records_outputs + for invocation in group_data.compiled_group.invocations + ): + return NoMainFlowOutput() + return current_stack + def _project_output_slices( self, diff --git a/tests/unit/test_cellprofiler_module_execution.py b/tests/unit/test_cellprofiler_module_execution.py index b48491bd99..d8c5517e07 100644 --- a/tests/unit/test_cellprofiler_module_execution.py +++ b/tests/unit/test_cellprofiler_module_execution.py @@ -179,7 +179,6 @@ from openhcs.core.source_spatial_domain import SourceSpatialDomain from openhcs.core.steps.function_runtime import ( FunctionOutputContextStrategy, - PatternGroupData, PatternGroupRuntime, ) from openhcs.core.steps.stream_component_semantics import ( @@ -9418,13 +9417,10 @@ def test_pattern_group_runtime_unstacks_aligned_image_stack_output(): source_image_names=("OrigRNA",), ).payload_with(np.full((4, 5), 2, dtype=np.float32), None) aligned = AlignedImageStack(slices=(dna_payload, rna_payload)) - loaded = PatternGroupData( - matching_files=["dna.tif", "rna.tif"], - main_data_stack=np.zeros((2, 4, 5), dtype=np.float32), - ) + matching_files = ["dna.tif", "rna.tif"] runtime = _pattern_group_runtime_for_output_memory("numpy") - output = runtime._project_output_slices(aligned, loaded.matching_files) + output = runtime._project_output_slices(aligned, matching_files) stack_payload = aligned.copy_projected_output_stack( output, memory_type="numpy", device_id=None, ) @@ -9476,13 +9472,10 @@ def test_pattern_group_runtime_does_not_invent_nested_axes_from_image_rank(): None, ) aligned = AlignedImageStack(slices=(first_site, second_site)) - loaded = PatternGroupData( - matching_files=["A01_s001_w2_z001_t001.tif"], - main_data_stack=np.zeros((4, 5), dtype=np.float32), - ) + matching_files = ["A01_s001_w2_z001_t001.tif"] runtime = _pattern_group_runtime_for_output_memory("numpy") - projected = runtime._project_output_slices(aligned, loaded.matching_files) + projected = runtime._project_output_slices(aligned, matching_files) output_slices = tuple(payload for payload, _context in projected) assert len(output_slices) == 2 @@ -9503,13 +9496,10 @@ def test_pattern_group_runtime_leaves_variable_shape_aligned_outputs_uncached(): first = np.ones((4, 5), dtype=np.float32) second = np.ones((3, 4), dtype=np.float32) aligned = AlignedImageStack(slices=(first, second)) - loaded = PatternGroupData( - matching_files=["first.tif", "second.tif"], - main_data_stack=np.zeros((2, 4, 5), dtype=np.float32), - ) + matching_files = ["first.tif", "second.tif"] runtime = _pattern_group_runtime_for_output_memory("numpy") - output = runtime._project_output_slices(aligned, loaded.matching_files) + output = runtime._project_output_slices(aligned, matching_files) assert [payload for payload, _context in output] == [first, second] assert aligned.copy_projected_output_stack( output, memory_type="numpy", device_id=None, diff --git a/tests/unit/test_cellprofiler_static_deletion_gates.py b/tests/unit/test_cellprofiler_static_deletion_gates.py index 1c48cb7972..82afc7ca84 100644 --- a/tests/unit/test_cellprofiler_static_deletion_gates.py +++ b/tests/unit/test_cellprofiler_static_deletion_gates.py @@ -828,7 +828,8 @@ def test_runtime_adapter_state_has_one_nominal_owner() -> None: and node.target.id == "source_binding_context" for node in source_context.body ) - for class_name in ("FunctionRuntimeScope", "PatternGroupData"): + assert "FunctionRuntimeScope" not in classes + for class_name in ("PatternGroupData",): assert all( ast.unparse(base) != "SourceBindingRuntimeContext" for base in classes[class_name].bases diff --git a/tests/unit/test_debug_runtime.py b/tests/unit/test_debug_runtime.py index be71a846b7..0197481dce 100644 --- a/tests/unit/test_debug_runtime.py +++ b/tests/unit/test_debug_runtime.py @@ -135,7 +135,8 @@ from openhcs.core.source_load_plan import SourceLoadPlan from openhcs.core.steps.function_runtime import ( ComponentArtifactPlans, - FunctionRuntimeScope, + PatternGroupData, + PatternGroupRuntime, ) from openhcs.core.runtime_stores import RuntimeValueStore from openhcs.processing.backends.lib_registry.unified_registry import ProcessingContract @@ -342,7 +343,9 @@ def execute_function_chain( ): resolved_artifact_inputs = {} if artifact_inputs is None else artifact_inputs resolved_artifact_outputs = {} if artifact_outputs is None else artifact_outputs - runtime_scope = FunctionRuntimeScope( + runtime_scope = PatternGroupData( + matching_files=[f"input-{index}.tif" for index in range(runtime_plane_count)], + main_data_stack=initial_data_stack, context=context, execution_plan=cls.execution_plan( artifact_inputs=resolved_artifact_inputs, @@ -364,7 +367,7 @@ def execute_function_chain( runtime_plane_index=runtime_plane_index, runtime_plane_count=runtime_plane_count, ) - return runtime_scope.execute_chain(initial_data_stack) + return PatternGroupRuntime.execute_chain(runtime_scope) @classmethod def cursor(cls) -> DebugCursor: diff --git a/tests/unit/test_framework_execution_scope.py b/tests/unit/test_framework_execution_scope.py index c5e341073b..56227eef55 100644 --- a/tests/unit/test_framework_execution_scope.py +++ b/tests/unit/test_framework_execution_scope.py @@ -30,7 +30,7 @@ def memory_device_scope(declaration): events.append(("exit", declaration)) executor = FunctionCoreExecutor( - runtime_scope=SimpleNamespace( + group_data=SimpleNamespace( execution_plan=SimpleNamespace( memory_device_scope=memory_device_scope, ) diff --git a/tests/unit/test_function_artifact_outputs.py b/tests/unit/test_function_artifact_outputs.py index 6ddc20f5ce..cf86b7d7a5 100644 --- a/tests/unit/test_function_artifact_outputs.py +++ b/tests/unit/test_function_artifact_outputs.py @@ -79,9 +79,8 @@ ComponentArtifactPlans, FunctionCoreExecutor, FunctionOutputContextStrategy, - FunctionRuntimeScope, - ImageFunctionOutputContextStrategy, PatternGroupData, + ImageFunctionOutputContextStrategy, PatternGroupRuntime, UnchangedFunctionOutputContextStrategy, ) @@ -265,7 +264,9 @@ def _execute_function_core(request: CoreExecutionRequest): component_value = ( None if request.execution_group_scope.is_ungrouped else request.group_key ) - runtime_scope = FunctionRuntimeScope( + runtime_scope = PatternGroupData( + matching_files=[f"input-{index}.tif" for index in range(request.runtime_plane_count)], + main_data_stack=request.main_data_arg, context=request.context, execution_plan=execution_plan, compiled_group=CompiledFunctionGroup( @@ -285,7 +286,7 @@ def _execute_function_core(request: CoreExecutionRequest): return FunctionCoreExecutor( main_data_arg=request.main_data_arg, source_memory_type=MEMORY_TYPE_NUMPY, - runtime_scope=runtime_scope, + group_data=runtime_scope, invocation=invocation, artifacts=runtime_scope.artifacts.select_for_invocation( invocation, @@ -2895,7 +2896,7 @@ def declared_source_origin(image, *, image_to_save=None): ) invocation = invocation.with_artifact_input_edges((edge,)) return FunctionCoreExecutor( - runtime_scope=SimpleNamespace( + group_data=SimpleNamespace( context=ContextStub(), source_binding_plan=source_binding_plan, source_binding_context=SourceBindingRuntimeContext.empty(), diff --git a/tests/unit/test_function_core_executor_ownership.py b/tests/unit/test_function_core_executor_ownership.py index edfd6b1d89..0c61087947 100644 --- a/tests/unit/test_function_core_executor_ownership.py +++ b/tests/unit/test_function_core_executor_ownership.py @@ -23,7 +23,7 @@ from openhcs.core.steps.function_runtime import ( ComponentArtifactPlans, FunctionCoreExecutor, - FunctionRuntimeScope, + PatternGroupData, ) @@ -64,7 +64,10 @@ def _executor(): source_binding_plan=CompiledSourceBindingPlan.empty(), ) artifacts = ComponentArtifactPlans(inputs={edge.key: edge for edge in edges}, outputs={}) - scope = FunctionRuntimeScope( + initial_image = np.arange(6, dtype=np.float32).reshape(1, 2, 3) + scope = PatternGroupData( + matching_files=["input.tif"], + main_data_stack=initial_image, context=SimpleNamespace(axis_id="A01"), execution_plan=plan, compiled_group=replace(pattern.default_group, invocations=(invocation,)), @@ -75,13 +78,13 @@ def _executor(): ) return FunctionCoreExecutor( scope, invocation, artifacts, None, RuntimePlaneProjection.stack(), - np.arange(6, dtype=np.float32).reshape(1, 2, 3), "numpy", + initial_image, "numpy", ) -def test_executor_retains_original_positional_constructor_and_frozen_slots(): +def test_executor_retains_positional_layout_with_loaded_group_owner_and_frozen_slots(): expected = ( - "runtime_scope", "invocation", "artifacts", "group_key", "plane_projection", + "group_data", "invocation", "artifacts", "group_key", "plane_projection", "main_data_arg", "source_memory_type", ) declaration = fields(FunctionCoreExecutor) @@ -95,7 +98,7 @@ def test_executor_retains_original_positional_constructor_and_frozen_slots(): executor.source_memory_type = "other" replaced = replace(executor, group_key="changed") assert replaced.group_key == "changed" - assert replaced.runtime_scope is executor.runtime_scope + assert replaced.group_data is executor.group_data assert replaced.artifacts is executor.artifacts @@ -103,11 +106,11 @@ def test_executor_pickle_retains_real_scope_edges_and_ordered_field_state(): executor = _executor() original_state = executor.__getstate__() assert len(original_state) == len(fields(FunctionCoreExecutor)) - assert original_state[0] is executor.runtime_scope + assert original_state[0] is executor.group_data assert original_state[-1] == "numpy" restored = pickle.loads(pickle.dumps(executor)) assert type(restored) is FunctionCoreExecutor - assert restored.runtime_scope.execution_plan.step_scope_id == "metadata-binding" + assert restored.group_data.execution_plan.step_scope_id == "metadata-binding" assert tuple(edge.spec.ref() for edge in restored.selected_artifact_input_edges) == (_FIRST.ref(), _SECOND.ref()) assert all(isinstance(edge, CompiledMetadataArtifactInputEdgePlan) for edge in restored.selected_artifact_input_edges) assert restored.invocation.kwargs_dict == executor.invocation.kwargs_dict diff --git a/tests/unit/test_function_runtime_source_projection.py b/tests/unit/test_function_runtime_source_projection.py index 7b75d20ffc..57109fdcdb 100644 --- a/tests/unit/test_function_runtime_source_projection.py +++ b/tests/unit/test_function_runtime_source_projection.py @@ -2082,7 +2082,7 @@ def test_first_step_prepares_raw_source_anchors_under_semantic_binding_groups( ) loaded = PatternGroupRuntime(request)._load_input_stack() - assert loaded.matching_files == ["A01_s001_w1_z001_t001.tif"] + assert loaded[0] == ["A01_s001_w1_z001_t001.tif"] def test_source_bound_artifact_managed_step_keeps_source_anchors() -> None: @@ -2228,13 +2228,13 @@ def test_dict_callable_runtime_scope_projects_bindings_to_selected_group() -> No def test_grouped_runtime_adapter_receives_component_selected_source_bindings() -> None: from openhcs.core.runtime_adapters import ( - RuntimeAdapterRequest, RuntimePlaneProjection, ) from openhcs.core.source_load_plan import SourceLoadPlan from openhcs.core.steps.function_runtime import ( ComponentArtifactPlans, - FunctionRuntimeScope, + PatternGroupData, + FunctionCoreExecutor, ) source_binding_plan = CompiledSourceBindingPlan( @@ -2265,7 +2265,9 @@ def test_grouped_runtime_adapter_receives_component_selected_source_bindings() - variable_components=(VariableComponents.SITE,), source_load_plan=SourceLoadPlan(), ) - scope = FunctionRuntimeScope( + scope = PatternGroupData( + matching_files=["first.tif", "second.tif"], + main_data_stack=np.zeros((2, 3, 4), dtype=np.uint16), context=SimpleNamespace(), execution_plan=execution_plan, compiled_group=compiled_pattern.default_group, @@ -2276,14 +2278,14 @@ def test_grouped_runtime_adapter_receives_component_selected_source_bindings() - runtime_plane_count=2, ) - request = RuntimeAdapterRequest.from_runtime_scope( - runtime_scope=scope, - artifact_inputs={}, - artifact_outputs={}, - group_key="1", - plane_projection=RuntimePlaneProjection.stack(), - source_payload=np.zeros((2, 3, 4), dtype=np.uint16), + executor = FunctionCoreExecutor( + group_data=scope, + invocation=compiled_pattern.default_group.invocations[0], + artifacts=ComponentArtifactPlans(inputs={}, outputs={}), + group_key="1", plane_projection=RuntimePlaneProjection.stack(), + main_data_arg=scope.main_data_stack, source_memory_type="numpy", ) + request = executor.runtime_adapter_request(scope.main_data_stack) assert tuple( binding.alias for binding in scope.source_binding_plan.binding_declarations @@ -2310,7 +2312,7 @@ def test_runtime_invocation_uses_only_active_source_bound_main_flow_edges( from openhcs.core.steps import function_runtime from openhcs.core.steps.function_runtime import ( ComponentArtifactPlans, - FunctionRuntimeScope, + PatternGroupData, ) source_binding_plan = CompiledSourceBindingPlan( @@ -2390,7 +2392,9 @@ def consume_channel_source(image): artifact_inputs={}, artifact_outputs=output_plans, ) - scope = FunctionRuntimeScope( + scope = PatternGroupData( + matching_files=["input.tif"], + main_data_stack=np.zeros((1, 3, 4), dtype=np.uint16), context=SimpleNamespace(), execution_plan=execution_plan, compiled_group=compiled_group, @@ -2420,7 +2424,8 @@ def execute(self, *, debug_sink=None): active_payload = ImagePayloadMetadata( source_image_names=("OrigStain1",), ).payload_with(np.zeros((1, 3, 4), dtype=np.uint16)) - result = scope.execute_chain(active_payload) + scope = replace(scope, main_data_stack=active_payload) + result = function_runtime.PatternGroupRuntime.execute_chain(scope) assert isinstance(result, NoMainFlowOutput) assert tuple( @@ -2451,7 +2456,7 @@ def test_runtime_chain_skips_adapter_invocation_without_component_outputs( from openhcs.core.steps import function_runtime from openhcs.core.steps.function_runtime import ( ComponentArtifactPlans, - FunctionRuntimeScope, + PatternGroupData, ) first_spec = ArtifactSpec.output("FirstLabels", ObjectLabelsArtifactType) @@ -2529,7 +2534,9 @@ def record_second_labels(image, *, runtime): artifact_inputs={}, artifact_outputs=output_plans, ) - scope = FunctionRuntimeScope( + scope = PatternGroupData( + matching_files=["input.tif"], + main_data_stack=np.zeros((1, 3, 4), dtype=np.uint16), context=SimpleNamespace(), execution_plan=execution_plan, compiled_group=compiled_group, @@ -2560,7 +2567,7 @@ def memory_types(self): lambda context: SimpleNamespace(captures_invocation_events=lambda: False), ) - scope.execute_chain(np.zeros((1, 3, 4), dtype=np.uint16)) + function_runtime.PatternGroupRuntime.execute_chain(scope) assert executed == ["record_first_labels"] @@ -3093,7 +3100,6 @@ def test_pipeline_start_main_flow_survives_prior_producer_image_input( def test_runtime_plane_count_comes_from_loaded_slices_not_dispatch_groups() -> None: from openhcs.core.steps.function_runtime import ( - FunctionRuntimeScope, PatternGroupData, PatternGroupExecutionRequest, ) @@ -3119,7 +3125,8 @@ def test_runtime_plane_count_comes_from_loaded_slices_not_dispatch_groups() -> N component_index=0, component_count=1, ) - loaded = PatternGroupData( + scope = PatternGroupData.from_loaded_group( + request, matching_files=[ "A01_s001_w1_z001_t001.tif", "A01_s002_w1_z001_t001.tif", @@ -3150,10 +3157,9 @@ def test_runtime_plane_count_comes_from_loaded_slices_not_dispatch_groups() -> N ), ), ).payload_with(np.zeros((2, 4, 5), dtype=np.float32)), + source_binding_context=SourceBindingRuntimeContext.empty(), ) - scope = FunctionRuntimeScope.from_pattern_group(request, loaded) - assert scope.runtime_plane_count == 2 assert scope.axis_scope.fixed_component_values == ( (AllComponents.Z_INDEX, "3"), @@ -3897,8 +3903,8 @@ def resolve_address(backend_address, backend, *, base_path): loaded = runtime._load_input_stack() - data = image_payload_data(loaded.main_data_stack) - metadata = image_payload_metadata(loaded.main_data_stack) + data = image_payload_data(loaded[1]) + metadata = image_payload_metadata(loaded[1]) 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 @@ -4012,7 +4018,7 @@ def load_batch(self, paths, backend): loaded = runtime._load_input_stack() provenance_planes = image_payload_metadata( - loaded.main_data_stack + loaded[1] ).source_image_provenance_planes assert provenance_planes.count == 2 assert provenance_planes.contributor_count == 0 diff --git a/tests/unit/test_loaded_pattern_group_ownership.py b/tests/unit/test_loaded_pattern_group_ownership.py new file mode 100644 index 0000000000..9e07614538 --- /dev/null +++ b/tests/unit/test_loaded_pattern_group_ownership.py @@ -0,0 +1,254 @@ +"""Loaded cohorts retain original input facts while plans and call images remain live.""" + +from dataclasses import FrozenInstanceError, fields, replace +import inspect +import pickle +from types import SimpleNamespace + +import numpy as np +import pytest + +from openhcs.constants.constants import AllComponents, VariableComponents +from openhcs.core.compiled_step_plan import CompiledStepPlan +from openhcs.core.component_group_scope import ComponentGroupScope +from openhcs.core.context.processing_context import ProcessingContext +from openhcs.core.function_patterns import compile_function_pattern +from openhcs.core.runtime_image_values import ImagePayloadMetadata, image_payload_data +from openhcs.core.source_bindings import CompiledSourceBindingPlan, SourceBindingRuntimeContext +from openhcs.core.source_image_provenance import SourceImageProvenancePlanes +from openhcs.core.steps import function_runtime +from openhcs.core.steps.function_runtime import ( + ComponentArtifactPlans, + FunctionCoreExecutor, + PatternGroupData, + PatternGroupExecutionRequest, + PatternGroupExecutionScope, + PatternGroupRuntime, + RuntimeProfileSink, +) +from openhcs.core.steps.function_output_manifest import NoStepOutputManifestMatch + + +def _identity(image): + return image + + +def _fixture(): + pattern = compile_function_pattern(_identity, {}, {}) + plan = CompiledStepPlan( + step_index=0, step_type="FunctionStep", step_name="Loaded", axis_id="A01", + step_scope_id="loaded-cohort", input_memory_type="numpy", output_memory_type="numpy", + execution_group_scope=ComponentGroupScope.dynamic(AllComponents.CHANNEL), + variable_components=(VariableComponents.SITE,), + source_binding_plan=CompiledSourceBindingPlan.empty(), + ) + context = ProcessingContext(axis_id="A01") + request = PatternGroupExecutionRequest( + context=context, execution_plan=plan, compiled_group=pattern.default_group, + component_value="1", pattern_group_info="A01_s{iii}_w1_z003_t002.tif", + component_index=7, component_count=99, + fixed_component_values=((AllComponents.Z_INDEX, "dispatch-value"),), + ) + paths = ["A01_s001_w1_z003_t002.tif", "A01_s002_w1_z003_t002.tif"] + payload = ImagePayloadMetadata( + source_image_provenance_planes=SourceImageProvenancePlanes.from_components( + paths=tuple(paths), + component_metadata=tuple( + {"well": "A01", "site": str(site), "channel": "1", "z_index": "3", "timepoint": "2"} + for site in (1, 2) + ), + ), + ).payload_with(np.arange(24, dtype=np.float32).reshape(2, 3, 4)) + source_context = SourceBindingRuntimeContext.empty() + return request, paths, payload, source_context + + +def test_complete_loaded_owner_has_one_frozen_eleven_field_contract_and_pickle(): + request, paths, payload, source_context = _fixture() + loaded = PatternGroupData.from_loaded_group(request, paths, payload, source_context) + expected = ( + "context", "execution_plan", "compiled_group", "component_value", "fixed_component_values", + "artifacts", "source_binding_context", "runtime_plane_index", "runtime_plane_count", + "matching_files", "main_data_stack", + ) + assert PatternGroupData.__bases__ == (PatternGroupExecutionScope,) + assert tuple(item.name for item in fields(loaded)) == expected + assert set(inspect.signature(PatternGroupData).parameters) == set(expected) + assert all(item.kw_only for item in fields(loaded)) + assert not hasattr(loaded, "__dict__") + assert loaded.context is request.context + assert loaded.execution_plan is request.execution_plan + assert loaded.compiled_group is request.compiled_group + assert loaded.source_binding_context is source_context + assert loaded.matching_files is paths + assert loaded.main_data_stack is payload + with pytest.raises(FrozenInstanceError): + loaded.runtime_plane_count = 0 + replaced = replace(loaded, component_value="2") + assert replaced.main_data_stack is payload + assert replaced.matching_files is paths + from openhcs.core.orchestrator.execution_result import RuntimeExecutionTransportSerialization + + RuntimeExecutionTransportSerialization.register() + restored = pickle.loads(pickle.dumps(loaded)) + assert type(restored) is PatternGroupData + assert restored.runtime_plane_index == 7 + assert restored.runtime_plane_count == 2 + assert restored.execution_plan.step_scope_id == "loaded-cohort" + assert restored.fixed_component_values == loaded.fixed_component_values + np.testing.assert_array_equal(image_payload_data(restored.main_data_stack), image_payload_data(payload)) + + +def test_initial_coordinates_stay_captured_while_plan_selectors_follow_mutation(): + request, paths, payload, source_context = _fixture() + loaded = PatternGroupData.from_loaded_group(request, paths, payload, source_context) + assert loaded.runtime_plane_count == 2 + assert loaded.fixed_component_values == ((AllComponents.Z_INDEX, "3"), (AllComponents.TIMEPOINT, "2")) + paths.append("later-mutation.tif") + image_payload_data(payload)[:] = -1 + assert loaded.runtime_plane_count == 2 + assert len(loaded.matching_files) == 3 + request.execution_plan.axis_id = "B02" + request.execution_plan.execution_group_scope = ComponentGroupScope.dynamic(AllComponents.SITE) + assert loaded.axis_scope.axis_id == "B02" + assert loaded.axis_component == "site" + assert loaded.fixed_component_values == ((AllComponents.Z_INDEX, "3"), (AllComponents.TIMEPOINT, "2")) + + +def test_chain_shares_cohort_but_advances_only_current_image_and_memory(monkeypatch): + request, paths, payload, source_context = _fixture() + invocation = request.compiled_group.invocations[0] + group = replace(request.compiled_group, invocations=(invocation, invocation)) + request = replace(request, compiled_group=group) + loaded = PatternGroupData.from_loaded_group(request, paths, payload, source_context) + first_output = np.ones((2, 3, 4), dtype=np.float32) + second_output = np.full((2, 3, 4), 2, dtype=np.float32) + seen = [] + + def execute(executor, *, debug_sink=None): + assert debug_sink is None + seen.append((executor.group_data, executor.main_data_arg, executor.source_memory_type)) + if len(seen) == 1: + paths.append("during-call.tif") + image_payload_data(payload)[:] = -3 + return first_output + return second_output + + monkeypatch.setattr(FunctionCoreExecutor, "execute", execute) + monkeypatch.setattr(FunctionCoreExecutor, "memory_types", lambda _executor: SimpleNamespace(output_type="next-memory")) + monkeypatch.setattr(function_runtime, "debug_event_sink_from_context", lambda _context: SimpleNamespace(captures_invocation_events=lambda: False)) + assert PatternGroupRuntime.execute_chain(loaded) is second_output + assert seen[0][0] is loaded and seen[1][0] is loaded + assert seen[0][1] is payload and seen[1][1] is first_output + assert seen[0][2] == "numpy" and seen[1][2] == "next-memory" + assert loaded.main_data_stack is payload + assert loaded.runtime_plane_count == 2 + assert loaded.fixed_component_values == ((AllComponents.Z_INDEX, "3"), (AllComponents.TIMEPOINT, "2")) + + +def test_scope_admission_follows_load_profile_inside_execution_error_boundary(monkeypatch): + request, paths, payload, source_context = _fixture() + runtime = PatternGroupRuntime(request) + events = [] + failure = RuntimeError("cohort admission failed") + monkeypatch.setattr(runtime, "_load_input_stack", lambda: (events.append("load") or (paths, payload, source_context))) + monkeypatch.setattr(RuntimeProfileSink, "record", lambda label, *_args, **_kwargs: events.append(label)) + + def fail_capture(cls, observed_request, observed_paths, observed_payload, observed_context): + assert observed_request is request + assert observed_paths is paths + assert observed_payload is payload + assert observed_context is source_context + events.append("capture") + raise failure + + monkeypatch.setattr(PatternGroupData, "from_loaded_group", classmethod(fail_capture)) + with pytest.raises(ValueError, match="Failed to process pattern group") as exc: + runtime.run() + assert exc.value.__cause__ is failure + assert events == ["load", "pattern_load_stack", "capture"] + + +@pytest.mark.parametrize("failure", [RuntimeError("load failed"), NoStepOutputManifestMatch("stale")]) +def test_load_errors_and_stale_skip_precede_capture_and_execution_wrapping(monkeypatch, failure): + request, _paths, _payload, _source_context = _fixture() + runtime = PatternGroupRuntime(request) + events = [] + + def failed_load(): + events.append("load") + raise failure + + monkeypatch.setattr(runtime, "_load_input_stack", failed_load) + monkeypatch.setattr(RuntimeProfileSink, "record", lambda *_args, **_kwargs: pytest.fail("failed load must not reach profile/capture")) + if isinstance(failure, NoStepOutputManifestMatch): + assert runtime.run() is None + else: + with pytest.raises(RuntimeError) as exc: + runtime.run() + assert exc.value is failure + assert events == ["load"] + + +def test_empty_chain_is_rejected_after_full_capture_and_load_profile(monkeypatch): + request, paths, payload, source_context = _fixture() + request = replace(request, compiled_group=replace(request.compiled_group, invocations=())) + runtime = PatternGroupRuntime(request) + labels = [] + monkeypatch.setattr(runtime, "_load_input_stack", lambda: (paths, payload, source_context)) + monkeypatch.setattr(RuntimeProfileSink, "record", lambda label, *_args, **_kwargs: labels.append(label)) + with pytest.raises(ValueError, match="has no invocations") as exc: + runtime.run() + assert isinstance(exc.value.__cause__, ValueError) + assert labels == ["pattern_load_stack"] + + +def test_adapter_request_projects_live_fields_and_preserves_current_payload_epoch(): + request, paths, initial, source_context = _fixture() + loaded = PatternGroupData.from_loaded_group(request, paths, initial, source_context) + current = np.ones((2, 3, 4), dtype=np.float32) + invocation = loaded.compiled_group.invocations[0] + executor = FunctionCoreExecutor( + loaded, invocation, ComponentArtifactPlans(inputs={}, outputs={}), "1", + function_runtime.RuntimePlaneProjection.stack(2), current, "numpy", + ) + adapter = executor.runtime_adapter_request(current) + assert adapter.context is loaded.context + assert adapter.callable_contract is invocation.contract + assert adapter.source_payload is current + assert adapter.source_payload is not loaded.main_data_stack + assert adapter.source_binding_context is source_context + assert adapter.plane_projection is executor.plane_projection + assert adapter.source_load_plan is request.execution_plan.source_load_plan + assert adapter.variable_components == (VariableComponents.SITE,) + assert adapter.axis_scope.fixed_component_values == loaded.fixed_component_values + request.execution_plan.axis_id = "B02" + request.execution_plan.variable_components = (VariableComponents.Z_INDEX,) + updated = executor.runtime_adapter_request(current) + assert updated.axis_scope.axis_id == "B02" + assert updated.variable_components == (VariableComponents.Z_INDEX,) + assert updated.source_binding_context is source_context + + +def test_adapter_tuple_admission_precedes_output_map_validation(): + request, paths, initial, source_context = _fixture() + loaded = PatternGroupData.from_loaded_group(request, paths, initial, source_context) + invocation = loaded.compiled_group.invocations[0] + executor = FunctionCoreExecutor( + loaded, invocation, ComponentArtifactPlans(inputs={}, outputs={"invalid": object()}), "1", + function_runtime.RuntimePlaneProjection.stack(2), initial, "numpy", + ) + request.execution_plan.variable_components = None + with pytest.raises(TypeError, match="NoneType.*not iterable"): + executor.runtime_adapter_request(initial) + request.execution_plan.variable_components = () + with pytest.raises(TypeError, match="Runtime adapter output"): + executor.runtime_adapter_request(initial) + + +def test_component_artifact_admission_precedes_source_provenance_capture(monkeypatch): + request, paths, initial, source_context = _fixture() + request.execution_plan.artifact_inputs = {"invalid": object()} + monkeypatch.setattr(function_runtime, "image_payload_metadata", lambda _payload: pytest.fail("source projection must follow artifact-plan admission")) + with pytest.raises(TypeError, match="Component artifact input"): + PatternGroupData.from_loaded_group(request, paths, initial, source_context) diff --git a/tests/unit/test_opaque_main_flow_image_domain.py b/tests/unit/test_opaque_main_flow_image_domain.py index acb8dd3a9f..f49e3a3347 100644 --- a/tests/unit/test_opaque_main_flow_image_domain.py +++ b/tests/unit/test_opaque_main_flow_image_domain.py @@ -19,7 +19,7 @@ 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 PatternGroupData, PatternGroupRuntime +from openhcs.core.steps.function_runtime import PatternGroupRuntime from openhcs.core.aligned_image_payload import AlignedImageStack, ImageOutputBundle, AlignedImageSliceContext, ImagePayloadStackComposition from openhcs.core.context.processing_context import ProcessingContext from openhcs.core.steps.function_output_manifest import step_output_manifest, StepOutputManifestStore @@ -221,7 +221,7 @@ def test_save_and_next_load_preserve_domain_and_independent_cache(tmp_path, monk )) monkeypatch.setattr(consumer, "source_workspace_projection_authority", lambda: SimpleNamespace(projection_if_available=lambda: None)) monkeypatch.setattr(SourceBindingRuntimeContextRequest, "from_context", classmethod(lambda cls, **kwargs: SimpleNamespace(runtime_context=SourceBindingRuntimeContext.empty))) - loaded = consumer._load_input_stack().main_data_stack + loaded = consumer._load_input_stack()[1] assert image_payload_data(loaded).shape == pixels.shape assert image_payload_metadata(loaded).plane_axis is axis np.testing.assert_array_equal(image_payload_data(loaded), pixels) @@ -351,7 +351,6 @@ def test_saved_mixed_named_cohort_rejects_joint_load_and_preserves_selected_doma context.microscope_handler = SimpleNamespace(parser=SourceSchemaFilenameParser()) producer.request.context = context producer.pattern_repr = "mixed" - loaded = PatternGroupData(matching_files=[source, source], main_data_stack=planes) # A prior homogeneous producer caches the same physical paths. Mixed replacement # must invalidate that cache rather than leave a false joint runtime domain. @@ -381,7 +380,7 @@ def consumer_for(compiled): consumer_for(pattern)._load_input_stack() for name, expected, axis in (("Planes", image_payload_data(planes), RuntimePlaneAxis.RUNTIME_SLICE), ("Volume", opaque_pixels, None)): selected = consumer_for(_selected_pattern(name)) - actual = selected._load_input_stack().main_data_stack + actual = selected._load_input_stack()[1] np.testing.assert_array_equal(image_payload_data(actual), expected) assert image_payload_metadata(actual).plane_axis is axis if not cache_hit: @@ -389,7 +388,7 @@ def consumer_for(compiled): else: selected_path = next(r.output_path for r in records if r.producer_identity.output_key == name) context.runtime_image_stack_cache.store((selected_path,), memory_type=MEMORY_TYPE_NUMPY, stack=actual) - repeated = selected._load_input_stack().main_data_stack + repeated = selected._load_input_stack()[1] np.testing.assert_array_equal(image_payload_data(repeated), expected) assert image_payload_metadata(repeated).plane_axis is axis @@ -436,10 +435,10 @@ def unexpected_device_resolution(self, memory_type): raise AssertionError("BUNDLE composition must retain its own memory allocation authority") monkeypatch.setattr(CompiledStepPlan, "device_id_for", unexpected_device_resolution) - output = PatternGroupData.from_loaded_images( - [r.output_path for r in records], payloads, - producer_records=records, source_binding_context=SourceBindingRuntimeContext.empty(), + output = ImagePayloadStackComposition.from_loaded_images( + payloads, + producer_records=records, execution_plan=plan, source_projection=None, workspace_source_lookups=(), ) - assert image_payload_metadata(output.main_data_stack).plane_axis is RuntimePlaneAxis.SOURCE_BINDING - np.testing.assert_array_equal(image_payload_data(output.main_data_stack), np.stack([image_payload_data(p) for p in payloads])) + assert image_payload_metadata(output).plane_axis is RuntimePlaneAxis.SOURCE_BINDING + np.testing.assert_array_equal(image_payload_data(output), np.stack([image_payload_data(p) for p in payloads])) diff --git a/tests/unit/test_path_planner_materialization.py b/tests/unit/test_path_planner_materialization.py index 79edf7b1eb..54c2887ff8 100644 --- a/tests/unit/test_path_planner_materialization.py +++ b/tests/unit/test_path_planner_materialization.py @@ -111,7 +111,7 @@ from openhcs.core.steps.function_runtime import ( ComponentArtifactPlans, FunctionCoreExecutor, - FunctionRuntimeScope, + PatternGroupData, ) from openhcs.microscopes.microscope_interfaces import MetadataArtifactProvider from openhcs.microscopes.openhcs import OpenHCSMetadataHandler @@ -144,7 +144,9 @@ def _execute_compiled_metadata_pattern(compiled, input_plans=None, stored_output ), path=producer.path, backend="memory", ) - scope = FunctionRuntimeScope( + scope = PatternGroupData( + matching_files=["input.tif"], + main_data_stack=np.zeros((1, 3, 4), dtype=np.uint16), context=context, execution_plan=plan, compiled_group=compiled.default_group, @@ -159,7 +161,7 @@ def _execute_compiled_metadata_pattern(compiled, input_plans=None, stored_output result = FunctionCoreExecutor( main_data_arg=ImagePayloadMetadata().payload_with(source), source_memory_type=MEMORY_TYPE_NUMPY, - runtime_scope=scope, + group_data=scope, invocation=invocation, artifacts=scope.artifacts.select_for_invocation( invocation, execution_scope=plan.execution_group_scope, component_key=None diff --git a/tests/unit/test_runtime_context_declaration.py b/tests/unit/test_runtime_context_declaration.py index 25da4722a8..2bd7f4fef8 100644 --- a/tests/unit/test_runtime_context_declaration.py +++ b/tests/unit/test_runtime_context_declaration.py @@ -27,7 +27,7 @@ from openhcs.core.steps.function_runtime import ( ComponentArtifactPlans, FunctionCoreExecutor, - FunctionRuntimeScope, + PatternGroupData, ) from openhcs.processing.backends.cellprofiler.save_images import ( save_images, @@ -129,7 +129,9 @@ def test_runtime_binding_consumes_captured_selection(func, parameter): pattern = compile_function_pattern(func, {}, {}) context = ProcessingContext(axis_id="A01") artifacts = ComponentArtifactPlans(inputs={}, outputs={}) - scope = FunctionRuntimeScope( + scope = PatternGroupData( + matching_files=["input.tif"], + main_data_stack=np.zeros((1, 3, 4), dtype=np.uint16), context=context, execution_plan=CompiledStepPlan( step_index=0, step_name="Context", step_type="FunctionStep", axis_id="A01" @@ -141,7 +143,7 @@ def test_runtime_binding_consumes_captured_selection(func, parameter): runtime_plane_count=1, ) executor = FunctionCoreExecutor( - runtime_scope=scope, + group_data=scope, invocation=next(pattern.iter_invocations()), artifacts=artifacts, group_key=None, From 74693f589736e87a4b7414048580f49873b8a4ed Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 13:14:24 -0400 Subject: [PATCH 145/235] refactor(runtime): consolidate producer and projection authorities Move producer identity onto compiled plans, derive paths and publication metadata from original produced records, and select cohorts at their manifest owner. Remove stream request and memory path authorities; group correlated stream occurrences in one pass. Fold the parallel aligned-argument strategy registry into the existing runtime projection family. Preserve distinct operation contracts, derive member scopes and share MRO selection. Fix the obsolete aligned-value bridge through its actual capability owner. Refs #384, #496, #134. Validation: 1247 tests pass in the combined 22-file gate. No pipeline speedup claim; removed extension APIs migrate to the canonical registry. --- .../runtime-owner-consolidation-20261003.rst | 43 ++ openhcs/core/aligned_image_payload.py | 176 +------- openhcs/core/compiled_step_plan.py | 48 ++- openhcs/core/registry_strategies.py | 28 +- openhcs/core/runtime_execution_validation.py | 11 +- openhcs/core/runtime_slice_projection.py | 150 ++++++- .../function_artifact_materialization.py | 11 +- .../core/steps/function_output_manifest.py | 106 ++--- openhcs/core/steps/function_outputs.py | 245 ++++------- openhcs/core/steps/function_runtime.py | 2 - .../pipelines/czi_brain_axon_cellbody.py | 6 +- .../loose_operaphenix_neurite_outgrowth.py | 6 +- ...uroncyto_ii_crossover_neurite_outgrowth.py | 6 +- .../test_aligned_image_payload_registry.py | 393 +++++++++++++----- .../unit/test_cellprofiler_runtime_adapter.py | 31 +- ...st_function_output_manifest_input_scope.py | 117 +++++- tests/unit/test_function_outputs.py | 106 ++++- ...test_function_runtime_source_projection.py | 84 +++- 18 files changed, 976 insertions(+), 593 deletions(-) diff --git a/docs/refactor/receipts/runtime-owner-consolidation-20261003.rst b/docs/refactor/receipts/runtime-owner-consolidation-20261003.rst index de8047deb0..779060c9d4 100644 --- a/docs/refactor/receipts/runtime-owner-consolidation-20261003.rst +++ b/docs/refactor/receipts/runtime-owner-consolidation-20261003.rst @@ -89,6 +89,49 @@ with the three original production modules. Those failures remain open consumer work, not waived tests. The bounded NRA census changes from 164 to 163 classes. No pipeline timing gain is claimed; this is part of a larger semantic batch. +The next coherent batch removes four producer/stream representations: +``FunctionStepOutputProducerIdentityRequest``, +``FunctionStepOutputProducerIdentityAuthority``, ``ProducedMemoryPathsAuthority`` +and ``StreamOutputProjectionRequest``. ``CompiledStepPlan`` derives producer +identity from the original main-flow/artifact declaration; ``ProducedOutputSemantics`` +derives memory paths and current source metadata; ``StepOutputManifestStore`` +selects current image cohorts. Save, materialization, viewer validation, +publication and all three preset consumers migrate together. ``StreamOutputBatch`` +owns admission and projection. Correlated one-pass grouping replaces a complete +record scan for each producer projection, preserving first-seen group order, +within-group order, frozen input-list structure and original projector errors. +Actual writer reloads and persisted-format decisions retain their original epochs. +The producer/source gate passes 112 tests, stream/output gate 70 and +artifact/viewer gate 79. Three stale CP fixtures now declare their producer-owned +locations; all 248 related tests pass, including missing/wrong-location rejection. +The production input-selection law is unchanged. + +The same batch removes ``AlignedImageStackKwargResolutionStrategy`` and its +seven concrete leaves. Their operation now belongs to the existing +``RuntimeSliceProjectionStrategy`` family. Ordinary projection and argument +alignment remain distinct operations: tuple recursion versus list passthrough, +whole image spatial alignment, label projection before spatial alignment, and +nested outer-stack selection keep their original laws. Operation participation +and nominal members derive from existing declarations and method ownership, +with one shared MRO ordering algorithm and cached type selection. Narrower child +declarations and dynamic extensions are covered by the 517-test family/CP gate. +External extensions must use the existing projection family; equal-distance +virtual-ABC ties follow its stable registration order. The separate registry's +independent tie order and removed APIs have no compatibility aliases. +A genuine old aligned-value bug also closes: the removed strategy called a +nonexistent ``projection_axis.aligned_value``. The merged leaf invokes the +existing value owner's ``value_for_aligned_slice`` capability, with exact outer +count/index controls. Original ``AttributeError`` evidence is retained. + +The final combined 22-file execution, projection, image topology, save, +publication, artifact, debug, framework, path and CP gate passes all 1,247 tests +in 18.97 seconds. This validates the structural batch together. No new ordinary +pipeline timing has yet been measured for this revision. An isolated main +cherry-pick of the loaded-cohort change was rejected because it depends on the +earlier executor/output consolidation; that temporary operation was aborted, +its conflict evidence retained, and the empty branch removed. The coherent +implementation remains in the formally linked draft PR #394. + Publication reader consolidation uses the existing document handler and source projection builder. Required projection-only viewer consumers remain. Its 94 reader controls pass; four saved-operand replays remove 151ms median locally diff --git a/openhcs/core/aligned_image_payload.py b/openhcs/core/aligned_image_payload.py index 4625facf2d..4aa3cfc9ea 100644 --- a/openhcs/core/aligned_image_payload.py +++ b/openhcs/core/aligned_image_payload.py @@ -9,7 +9,6 @@ from typing import Any, ClassVar, Mapping, TYPE_CHECKING import numpy as np -from metaclass_registry import AutoRegisterMeta from openhcs.core.image_payload_execution_mode import ( ImagePayloadExecutionMode, @@ -24,9 +23,6 @@ detect_memory_type, stack_runtime_slices, ) -from openhcs.core.registry_strategies import ( - NominalTypeKeyedStrategyMixin, -) from openhcs.core.runtime_image_values import ( ImagePayloadMetadata, ImagePayloadSliceProjector, @@ -49,7 +45,6 @@ RuntimePlaneAxis, RuntimePlaneAxisProjector, RuntimePlaneAxisValueProjection, - RuntimeSliceProjectableValue, ) from openhcs.core.runtime_slice_alignment import RuntimeSliceAlignedValueSet from openhcs.core.source_spatial_domain import ( @@ -382,11 +377,9 @@ class AlignedImageStackKwargResolver: reference_payload: Any | None = None def resolve(self, value: Any) -> Any: - strategy = AlignedImageStackKwargResolutionStrategy.require_nominal_value( - value, - context="Aligned image-stack kwarg resolution", - ) - return strategy.resolve(value, self) + from openhcs.core.runtime_slice_projection import RuntimeSliceProjectionStrategy + + return RuntimeSliceProjectionStrategy.aligned_kwarg_value(value, self) def resolve_source_spatial_value(self, value: Any) -> Any: """Project a nominal value into the declared reference payload domain.""" @@ -704,149 +697,6 @@ def unstack_image_payload_context( return projector.payloads_for_slices(slices) -class AlignedImageStackKwargResolutionStrategy( - NominalTypeKeyedStrategyMixin, - ABC, - metaclass=AutoRegisterMeta, -): - """Nominal strategy for resolving one slice-aligned runtime kwarg.""" - - __registry_key__ = "value_type_label" - __skip_if_no_key__ = True - __registry__: ClassVar[ - dict[str, type["AlignedImageStackKwargResolutionStrategy"]] - ] = {} - value_type: ClassVar[type[Any] | None] = None - value_type_label: ClassVar[str | None] = None - - @abstractmethod - def resolve( - self, - value: Any, - resolver: AlignedImageStackKwargResolver, - ) -> Any: - """Return the value in the current aligned slice context.""" - - -class TupleAlignedKwargResolutionStrategy(AlignedImageStackKwargResolutionStrategy): - """Resolve tuple-valued kwargs elementwise while preserving tuple structure.""" - - value_type = tuple - - def resolve( - self, - value: Any, - resolver: AlignedImageStackKwargResolver, - ) -> Any: - return tuple(resolver.resolve(item) for item in value) - - -class ImagePayloadAlignedKwargResolutionStrategy( - AlignedImageStackKwargResolutionStrategy -): - """Resolve image payloads without discarding metadata or masks.""" - - value_type = ImagePayloadMetadataCarrier - - def resolve( - self, - value: Any, - resolver: AlignedImageStackKwargResolver, - ) -> Any: - return resolver.resolve_source_spatial_value(value) - - -class ObjectLabelAlignedKwargResolutionStrategy( - AlignedImageStackKwargResolutionStrategy -): - """Resolve object labels by runtime-slice and source-spatial contracts.""" - - value_type = ObjectLabelValue - - def resolve( - self, - value: Any, - resolver: AlignedImageStackKwargResolver, - ) -> Any: - if not isinstance(value, ObjectLabelValue): - raise TypeError( - "Object-label aligned kwarg resolution requires ObjectLabelValue." - ) - slice_count = value.runtime_slice_plane_count() - if slice_count is not None: - if slice_count != resolver.projection_axis.axis_size: - raise ValueError( - "Runtime-slice object-label cardinality must exactly match the " - f"declared projection axis: {slice_count} != " - f"{resolver.projection_axis.axis_size}." - ) - from openhcs.core.runtime_slice_projection import RuntimeSliceProjection - - projected = RuntimeSliceProjection.value_for_slice( - value, - resolver.projection_axis, - ) - else: - projected = value - return resolver.resolve_source_spatial_value(projected) - - -class RuntimeSliceAlignedValueKwargResolutionStrategy( - AlignedImageStackKwargResolutionStrategy -): - """Select non-image values that explicitly declare runtime-slice alignment.""" - - value_type = RuntimeSliceAlignedValueSet - - def resolve( - self, - value: Any, - resolver: AlignedImageStackKwargResolver, - ) -> Any: - if not isinstance(value, RuntimeSliceAlignedValueSet): - raise TypeError( - "RuntimeSliceAlignedValueKwargResolutionStrategy requires " - "RuntimeSliceAlignedValueSet." - ) - return resolver.projection_axis.aligned_value(value) - - -class RuntimeSliceProjectableAlignedKwargResolutionStrategy( - AlignedImageStackKwargResolutionStrategy -): - """Project values through their declared runtime-slice hook.""" - - value_type = RuntimeSliceProjectableValue - - def resolve( - self, - value: Any, - resolver: AlignedImageStackKwargResolver, - ) -> Any: - from openhcs.core.runtime_slice_projection import RuntimeSliceProjection - - return RuntimeSliceProjection.value_for_slice( - value, - resolver.projection_axis, - ) - - -class PassThroughAlignedKwargResolutionStrategy( - AlignedImageStackKwargResolutionStrategy, -): - """Leave non-slice-aligned kwargs in their native runtime domain.""" - - value_type = object - - def resolve( - self, - value: Any, - resolver: AlignedImageStackKwargResolver, - ) -> Any: - del resolver - return value - - @dataclass(frozen=True, slots=True) class ImagePayloadComposition: """Resolved image payload plus its execution mode.""" @@ -1515,26 +1365,6 @@ def pack_aligned_image_outputs( return AlignedImageStack(packed) -class NestedAlignedImageStackKwargResolutionStrategy( - AlignedImageStackKwargResolutionStrategy -): - """Select matching slices from kwargs that are already aligned stacks.""" - - value_type = AlignedImageStack - - def resolve( - self, - value: Any, - resolver: AlignedImageStackKwargResolver, - ) -> Any: - return resolver.resolve( - value.aligned_slice( - resolver.projection_axis.require_plane_index(), - resolver.projection_axis.axis_size, - ) - ) - - def compose_aligned_image_payload( owner_name: str, image_payloads: tuple[Any, ...], diff --git a/openhcs/core/compiled_step_plan.py b/openhcs/core/compiled_step_plan.py index 0282c2d04e..15f35284e4 100644 --- a/openhcs/core/compiled_step_plan.py +++ b/openhcs/core/compiled_step_plan.py @@ -6,7 +6,9 @@ from contextlib import AbstractContextManager, nullcontext from dataclasses import dataclass, field from pathlib import Path -from typing import TYPE_CHECKING, Any, Mapping, Sequence +from typing import TYPE_CHECKING, Any, ClassVar, Mapping, Sequence + +from polystore.streaming.identity import StreamProducerIdentity from openhcs.constants.constants import ( GPU_MEMORY_TYPES, @@ -30,6 +32,7 @@ from openhcs.core.step_dependencies import StepInputDependency if TYPE_CHECKING: + from openhcs.core.aligned_image_payload import AlignedImageSliceContext from openhcs.core.config import StreamingConfig else: StreamingConfig = Any @@ -254,6 +257,8 @@ class CompiledStepPlan: mutate fields on this dataclass rather than writing string-keyed dicts. """ + ARTIFACT_OUTPUT_KIND: ClassVar[str] = "artifact" + step_index: int step_name: str step_type: str @@ -315,6 +320,47 @@ class CompiledStepPlan: chainbreaker: bool = False error: str | None = None + def producer_identity( + self, + *, + output_kind: str, + output_key: str, + projection_key: str, + artifact_kind: str | None = None, + ) -> StreamProducerIdentity: + """Project this step's identity onto one declared output surface.""" + return StreamProducerIdentity.pipeline_output( + output_kind=output_kind, + output_key=output_key, + projection_key=projection_key, + step_name=self.step_name, + pipeline_position=self.pipeline_position, + step_scope_id=self.step_scope_id, + artifact_kind=artifact_kind, + ) + + def producer_identity_for_main_flow( + self, output_context: AlignedImageSliceContext + ) -> StreamProducerIdentity: + """Retain the main-flow surface's declared kind, key and projection.""" + return self.producer_identity( + output_kind=output_context.output_kind, + output_key=output_context.output_key, + projection_key=output_context.projection_key, + artifact_kind=output_context.artifact_kind, + ) + + def producer_identity_for_artifact( + self, output_plan: ArtifactOutputPlan + ) -> StreamProducerIdentity: + """Derive a named artifact producer from its original compiled plan.""" + return self.producer_identity( + output_kind=self.ARTIFACT_OUTPUT_KIND, + output_key=output_plan.name, + projection_key=output_plan.name, + artifact_kind=output_plan.artifact_type.value, + ) + @property def requires_terminal_source_projection(self) -> bool: """Require an exact source anchor when declared bindings have no predecessor.""" diff --git a/openhcs/core/registry_strategies.py b/openhcs/core/registry_strategies.py index c2776e5b8e..3558578685 100644 --- a/openhcs/core/registry_strategies.py +++ b/openhcs/core/registry_strategies.py @@ -3,7 +3,7 @@ from __future__ import annotations from abc import ABC, abstractmethod -from collections.abc import Mapping, MutableMapping +from collections.abc import Iterable, Mapping, MutableMapping from dataclasses import dataclass, field from enum import Enum from functools import lru_cache @@ -185,10 +185,32 @@ def strategy_types_for_nominal_type( value_type: type[object], ) -> tuple[type[_TypeStrategyT], ...]: """Return registered strategy classes ordered by type MRO specificity.""" + return cls.order_nominal_strategy_members( + value_type, cls.nominal_strategy_members(), + ) + + @classmethod + def nominal_strategy_members( + cls: type[_TypeStrategyT], + ) -> Iterable[ + tuple[type[_TypeStrategyT], type[object] | tuple[type[object], ...] | None] + ]: + """Derive nominal members from the existing registered declarations.""" + for strategy_type in cls.registered_strategy_types(): + yield strategy_type, strategy_type.value_type + + @classmethod + def order_nominal_strategy_members( + cls: type[_TypeStrategyT], + value_type: type[object], + members: Iterable[ + tuple[type[_TypeStrategyT], type[object] | tuple[type[object], ...] | None] + ], + ) -> tuple[type[_TypeStrategyT], ...]: + """Order derived operation members using the family's existing MRO law.""" value_mro = value_type.mro() matches: list[tuple[int, type[_TypeStrategyT]]] = [] - for strategy_type in cls.registered_strategy_types(): - member = strategy_type.value_type + for strategy_type, member in members: if _is_nominal_type_member(member) and issubclass(value_type, member): distance = cls.nominal_type_distance(value_mro, member) matches.append((distance, strategy_type)) diff --git a/openhcs/core/runtime_execution_validation.py b/openhcs/core/runtime_execution_validation.py index b47296cc06..ac0408d267 100644 --- a/openhcs/core/runtime_execution_validation.py +++ b/openhcs/core/runtime_execution_validation.py @@ -26,10 +26,6 @@ source_component_metadata_items, ) from openhcs.core.source_spatial_domain import SourceSpatialDomain -from openhcs.core.steps.function_output_manifest import ( - FunctionStepOutputProducerIdentityAuthority, - FunctionStepOutputProducerIdentityRequest, -) from openhcs.processing.materialization import Output RuntimeArtifactViewerComponentIdentity = tuple[tuple[str, str], ...] @@ -253,12 +249,7 @@ def runtime_artifact_viewer_expectations( viewer_outputs = materialization.viewer_outputs(plan, context) if not viewer_outputs: continue - producer = FunctionStepOutputProducerIdentityAuthority.build( - FunctionStepOutputProducerIdentityRequest.from_artifact( - plan, - output_plan, - ) - ) + producer = plan.producer_identity_for_artifact(output_plan) payloads = expected_by_producer.setdefault(producer, []) payloads.extend( _runtime_artifact_viewer_output_payloads( diff --git a/openhcs/core/runtime_slice_projection.py b/openhcs/core/runtime_slice_projection.py index 283b9a9e84..a78a23607f 100644 --- a/openhcs/core/runtime_slice_projection.py +++ b/openhcs/core/runtime_slice_projection.py @@ -6,7 +6,8 @@ from collections.abc import Iterable, Mapping, Sequence from dataclasses import dataclass, field from enum import Enum -from typing import ClassVar, TypeAlias, cast, overload +from functools import lru_cache +from typing import Any, ClassVar, TypeAlias, cast, overload import numpy as np import numpy.typing as npt @@ -14,7 +15,11 @@ from metaclass_registry import AutoRegisterMeta from openhcs.constants.constants import VariableComponents -from openhcs.core.aligned_image_payload import AlignedImageStack, ImageOutputBundle +from openhcs.core.aligned_image_payload import ( + AlignedImageStack, + AlignedImageStackKwargResolver, + ImageOutputBundle, +) from openhcs.core.measurement_row_materialization import MeasurementRowsAxisProjection from openhcs.core.registry_strategies import ( EnumKeyedStrategyMixin, @@ -28,6 +33,7 @@ from openhcs.core.runtime_image_values import ( ImageMetadataPayload, ImagePayloadMetadata, + ImagePayloadMetadataCarrier, MaskedImagePayload, image_payload_data, image_payload_geometry, @@ -405,6 +411,54 @@ def strategy_for_value( f"{type(value).__name__}." ) + @classmethod + def aligned_kwarg_member(cls) -> type | tuple[type, ...] | None: + """Derive aligned participation from this class's operation implementation.""" + return ( + None + if cls.resolve_aligned_kwarg + is RuntimeSliceProjectionStrategy.resolve_aligned_kwarg + else cls.value_type + ) + + @classmethod + def aligned_strategy_members( + cls, + ) -> Iterable[ + tuple[type[RuntimeSliceProjectionStrategy], type | tuple[type, ...] | None] + ]: + """Project aligned operation ownership from the single nominal family.""" + for strategy_type in cls.registered_strategy_types(): + yield strategy_type, strategy_type.aligned_kwarg_member() + + @classmethod + def aligned_kwarg_value( + cls, value: Any, resolver: AlignedImageStackKwargResolver, + ) -> Any: + """Apply aligned semantics without declaring unknown values projectable.""" + strategies = cls.strategy_types_for_aligned_kwarg_type(type(value)) + if not strategies: + return value + return strategies[0]().resolve_aligned_kwarg(value, resolver) + + @classmethod + @lru_cache(maxsize=None) + def strategy_types_for_aligned_kwarg_type( + cls, + value_type: type, + ) -> tuple[type[RuntimeSliceProjectionStrategy], ...]: + """Cache the aligned view under the same nominal registry mutation contract.""" + return cls.order_nominal_strategy_members( + value_type, cls.aligned_strategy_members(), + ) + + def resolve_aligned_kwarg( + self, value: Any, resolver: AlignedImageStackKwargResolver, + ) -> Any: + """Preserve families with no declared aligned-argument operation.""" + del resolver + return value + def value_for_slice( self, value: RuntimeProjectionData, @@ -466,6 +520,21 @@ class ImagePayloadRuntimeSliceProjectionStrategy(RuntimeSliceProjectionStrategy) value_type = (ImageMetadataPayload, MaskedImagePayload) + @classmethod + def aligned_kwarg_member(cls) -> type | tuple[type, ...] | None: + return ( + ImagePayloadMetadataCarrier + if cls.value_type is ImagePayloadRuntimeSliceProjectionStrategy.value_type + else cls.value_type + ) + + def resolve_aligned_kwarg( + self, + value: Any, + resolver: AlignedImageStackKwargResolver, + ) -> Any: + return resolver.resolve_source_spatial_value(value) + def slice_count_for_value( self, value: RuntimeProjectionData, @@ -510,6 +579,18 @@ class AlignedImageStackRuntimeSliceProjectionStrategy(RuntimeSliceProjectionStra value_type = AlignedImageStack + def resolve_aligned_kwarg( + self, + value: Any, + resolver: AlignedImageStackKwargResolver, + ) -> Any: + return resolver.resolve( + value.aligned_slice( + resolver.projection_axis.require_plane_index(), + resolver.projection_axis.axis_size, + ) + ) + def full_stack_value(self, value: RuntimeProjectionData) -> RuntimeProjectionData: return cast(AlignedImageStack, value).compose() @@ -644,6 +725,21 @@ class RuntimeSliceAlignedValueProjectionStrategy( value_type = RuntimeSliceAlignedValueSet + def resolve_aligned_kwarg( + self, + value: Any, + resolver: AlignedImageStackKwargResolver, + ) -> Any: + if not isinstance(value, RuntimeSliceAlignedValueSet): + raise TypeError( + "Aligned kwarg resolution requires " + "RuntimeSliceAlignedValueSet." + ) + return value.value_for_aligned_slice( + resolver.projection_axis.require_plane_index(), + resolver.projection_axis.axis_size, + ) + def value_for_slice( self, value: RuntimeProjectionData, @@ -734,6 +830,16 @@ class RuntimeSliceProjectableValueProjectionStrategy( value_type = RuntimeSliceProjectableValue + def resolve_aligned_kwarg( + self, + value: Any, + resolver: AlignedImageStackKwargResolver, + ) -> Any: + return RuntimeSliceProjection.value_for_slice( + value, + resolver.projection_axis, + ) + def value_for_slice( self, value: RuntimeProjectionData, @@ -798,6 +904,31 @@ class ObjectLabelValueRuntimeSliceProjectionStrategy( value_type = ObjectLabelValue + def resolve_aligned_kwarg( + self, + value: Any, + resolver: AlignedImageStackKwargResolver, + ) -> Any: + if not isinstance(value, ObjectLabelValue): + raise TypeError( + "Object-label aligned kwarg resolution requires ObjectLabelValue." + ) + slice_count = value.runtime_slice_plane_count() + if slice_count is not None: + if slice_count != resolver.projection_axis.axis_size: + raise ValueError( + "Runtime-slice object-label cardinality must exactly match the " + f"declared projection axis: {slice_count} != " + f"{resolver.projection_axis.axis_size}." + ) + projected = RuntimeSliceProjection.value_for_slice( + value, + resolver.projection_axis, + ) + else: + projected = value + return resolver.resolve_source_spatial_value(projected) + def slice_count_for_value( self, value: RuntimeProjectionData, @@ -855,6 +986,21 @@ class SequenceRuntimeSliceProjectionStrategy(RuntimeSliceProjectionStrategy): value_type = (tuple, list) + @classmethod + def aligned_kwarg_member(cls) -> type | tuple[type, ...] | None: + return ( + tuple + if cls.value_type is SequenceRuntimeSliceProjectionStrategy.value_type + else cls.value_type + ) + + def resolve_aligned_kwarg( + self, + value: Any, + resolver: AlignedImageStackKwargResolver, + ) -> Any: + return tuple(resolver.resolve(item) for item in value) + def value_for_slice( self, value: RuntimeProjectionData, diff --git a/openhcs/core/steps/function_artifact_materialization.py b/openhcs/core/steps/function_artifact_materialization.py index e91198c959..e6c9770dcc 100644 --- a/openhcs/core/steps/function_artifact_materialization.py +++ b/openhcs/core/steps/function_artifact_materialization.py @@ -59,10 +59,6 @@ FunctionOutputPathAuthority, IncompleteFunctionOutputFilenameIdentityError, ) -from openhcs.core.steps.function_output_manifest import ( - FunctionStepOutputProducerIdentityAuthority, - FunctionStepOutputProducerIdentityRequest, -) from openhcs.core.steps.stream_component_semantics import ( StreamComponentMessageExtraAuthority, StreamSourceComponentMetadataItems, @@ -1440,12 +1436,7 @@ def materialize_artifact_outputs( data=data, fallback_source_identity=materialization.source_identity, producer_identity=( - FunctionStepOutputProducerIdentityAuthority.build( - FunctionStepOutputProducerIdentityRequest.from_artifact( - plan, - materialization.output_plan, - ) - ) + plan.producer_identity_for_artifact(materialization.output_plan) ), context=context, filemanager=filemanager, diff --git a/openhcs/core/steps/function_output_manifest.py b/openhcs/core/steps/function_output_manifest.py index daef36dea2..9d0b86bb29 100644 --- a/openhcs/core/steps/function_output_manifest.py +++ b/openhcs/core/steps/function_output_manifest.py @@ -4,7 +4,7 @@ from dataclasses import dataclass, field, replace from pathlib import Path -from typing import ClassVar, Iterator, Sequence +from typing import Iterator, Sequence from weakref import WeakKeyDictionary from polystore.streaming.identity import StreamProducerIdentity @@ -24,6 +24,7 @@ image_payload_metadata, ) from openhcs.core.runtime_plane_projection import RuntimePlaneAxis +from openhcs.core.source_metadata import SourceMetadataValue from openhcs.core.step_dependencies import StepInputDependencyKind from openhcs.core.steps.function_output_identity import ( @@ -47,65 +48,6 @@ class NoStepOutputManifestMatch(RuntimeError): """Raised when a stale directory pattern is not in producer lineage.""" -@dataclass(frozen=True, slots=True) -class FunctionStepOutputProducerIdentityRequest: - """Declared producer identity facts for a FunctionStep output surface.""" - - ARTIFACT_OUTPUT_KIND: ClassVar[str] = "artifact" - - plan: CompiledStepPlan - output_kind: str - output_key: str - projection_key: str - artifact_kind: str | None = None - - @classmethod - def from_main_flow( - cls, - plan: CompiledStepPlan, - output_context: AlignedImageSliceContext, - ) -> "FunctionStepOutputProducerIdentityRequest": - return cls( - plan=plan, - output_kind=output_context.output_kind, - output_key=output_context.output_key, - projection_key=output_context.projection_key, - artifact_kind=output_context.artifact_kind, - ) - - @classmethod - def from_artifact( - cls, - plan: CompiledStepPlan, - output_plan: ArtifactOutputPlan, - ) -> "FunctionStepOutputProducerIdentityRequest": - return cls( - plan=plan, - output_kind=cls.ARTIFACT_OUTPUT_KIND, - output_key=output_plan.name, - projection_key=output_plan.name, - artifact_kind=output_plan.artifact_type.value, - ) - - -class FunctionStepOutputProducerIdentityAuthority: - """Build stable producer identities for all FunctionStep output surfaces.""" - - @staticmethod - def build( - request: FunctionStepOutputProducerIdentityRequest, - ) -> StreamProducerIdentity: - return StreamProducerIdentity.pipeline_output( - output_kind=request.output_kind, - output_key=request.output_key, - projection_key=request.projection_key, - step_name=request.plan.step_name, - pipeline_position=request.plan.pipeline_position, - step_scope_id=request.plan.step_scope_id, - artifact_kind=request.artifact_kind, - ) - - @dataclass(frozen=True, slots=True) class ProducedOutputSemantics(FunctionOutputIdentity): """Semantic record for one output file produced by a FunctionStep.""" @@ -120,11 +62,7 @@ def passed_through(self, plan: CompiledStepPlan) -> "ProducedOutputSemantics": """Retain the image domain and physical identity under the next producer.""" return replace( self, - producer_identity=FunctionStepOutputProducerIdentityAuthority.build( - FunctionStepOutputProducerIdentityRequest.from_main_flow( - plan, self.output_context, - ) - ), + producer_identity=plan.producer_identity_for_main_flow(self.output_context), relative_output_path=source_path_identity(self.output_path).name, ) @@ -144,6 +82,21 @@ def path_under(self, output_dir: str | Path) -> str: return str(Path(output_dir) / self.relative_output_path) + def memory_path(self, plan: CompiledStepPlan) -> str: + """Resolve this original saved occurrence in its step's memory namespace.""" + path = Path(self.output_path) + return str(path) if path.is_absolute() else self.path_under(plan.output_dir) + + def source_metadata_for_projection( + self, metadata: ImagePayloadMetadata, destination: str + ) -> dict[str, SourceMetadataValue]: + """Combine this occurrence's coordinates with current persisted image facts.""" + source_metadata = dict( + self.component_metadata(metadata.source_component_metadata) + ) + metadata.source_voxel_spacing.merge_into(source_metadata, path=destination) + return source_metadata + def owns_persisted_artifact( self, output_plan: ArtifactOutputPlan, @@ -215,12 +168,7 @@ def from_output( if output_context is None: output_context = AlignedImageSliceContext.anonymous_main_flow() return cls( - producer_identity=FunctionStepOutputProducerIdentityAuthority.build( - FunctionStepOutputProducerIdentityRequest.from_main_flow( - plan, - output_context, - ) - ), + producer_identity=plan.producer_identity_for_main_flow(output_context), component_values=output_identity.component_values, extension=output_identity.extension, source=output_identity.source, @@ -260,12 +208,7 @@ def from_existing_main_flow_path( source="existing main-flow path", ) return cls( - producer_identity=FunctionStepOutputProducerIdentityAuthority.build( - FunctionStepOutputProducerIdentityRequest.from_main_flow( - plan, - output_context, - ) - ), + producer_identity=plan.producer_identity_for_main_flow(output_context), component_values=identity.component_values, extension=identity.extension, source=identity.source, @@ -435,6 +378,15 @@ def produced_paths_for( record.relative_output_path for record in self.produced_records_for(plan) ) + def image_records_for( + self, plan: CompiledStepPlan + ) -> tuple[ProducedOutputSemantics, ...]: + """Select image occurrences directly from this step's current producer cohort.""" + return tuple( + record for record in self.produced_records_for(plan) + if record.is_image_payload + ) + def producer_output_contexts_for_paths( self, plan: CompiledStepPlan, diff --git a/openhcs/core/steps/function_outputs.py b/openhcs/core/steps/function_outputs.py index 6f2c590ab9..5dc73d4cfe 100644 --- a/openhcs/core/steps/function_outputs.py +++ b/openhcs/core/steps/function_outputs.py @@ -5,7 +5,7 @@ import logging import time from abc import ABC, abstractmethod -from collections.abc import Callable, Mapping +from collections.abc import Callable, Mapping, Sequence from dataclasses import dataclass, field, replace from pathlib import Path from types import MappingProxyType @@ -109,32 +109,6 @@ def stream_payload_summary(payload: StreamPayload) -> str: return f"{summary} min={data.min()} max={data.max()}" -class ProducedMemoryPathsAuthority: - """Resolve absolute memory paths produced by the current step execution.""" - - @classmethod - def paths( - cls, - context: ProcessingContext, - plan: CompiledStepPlan, - ) -> list[str]: - return [ - cls.memory_path(record, plan) - for record in step_output_manifest(context).produced_records_for(plan) - if record.is_image_payload - ] - - @staticmethod - def memory_path( - record: ProducedOutputSemantics, - plan: CompiledStepPlan, - ) -> str: - path = Path(record.output_path) - if path.is_absolute(): - return str(path) - return str(plan.output_dir / record.relative_output_path) - - def finalize_function_step_outputs( context: ProcessingContext, plan: CompiledStepPlan, @@ -216,15 +190,11 @@ def write_if_needed( if plan.write_backend == Backend.MEMORY.value: return - produced_outputs = tuple( - record - for record in step_output_manifest(context).produced_records_for(plan) - if record.is_image_payload - ) + produced_outputs = step_output_manifest(context).image_records_for(plan) if not produced_outputs: return memory_paths = [ - ProducedMemoryPathsAuthority.memory_path(record, plan) + record.memory_path(plan) for record in produced_outputs ] memory_data = context.filemanager.load_batch( @@ -275,13 +245,9 @@ def write_if_needed( if materialized_output is None: return - produced_outputs = tuple( - record - for record in step_output_manifest(context).produced_records_for(plan) - if record.is_image_payload - ) + produced_outputs = step_output_manifest(context).image_records_for(plan) memory_paths = [ - ProducedMemoryPathsAuthority.memory_path(record, plan) + record.memory_path(plan) for record in produced_outputs ] if not produced_outputs: @@ -316,91 +282,6 @@ def write_if_needed( ) -@dataclass(frozen=True, slots=True) -class StreamOutputProjectionRequest: - """Nominal source of truth for projecting produced outputs into viewer streams.""" - - parser: FilenameParser - payloads: tuple[StreamPayload, ...] - paths: tuple[str, ...] - produced_outputs: tuple[ProducedOutputSemantics, ...] - - @classmethod - def from_sequences( - cls, - *, - parser: FilenameParser, - payloads: list[StreamPayload], - paths: list[str], - produced_outputs: tuple[ProducedOutputSemantics, ...], - ) -> StreamOutputProjectionRequest: - return cls( - parser=parser, - payloads=tuple(payloads), - paths=tuple(paths), - produced_outputs=produced_outputs, - ) - - def __post_init__(self) -> None: - if len(self.payloads) != len(self.paths): - raise ValueError( - "Streaming payload/path cardinality mismatch: " - f"{len(self.payloads)} payloads for {len(self.paths)} paths." - ) - if len(self.payloads) != len(self.produced_outputs): - raise ValueError( - "Streaming payload/output-record cardinality mismatch: " - f"{len(self.payloads)} payloads for " - f"{len(self.produced_outputs)} output records." - ) - if not self.produced_outputs: - raise ValueError("Streaming requires at least one produced output record.") - - def require_single_projection(self) -> tuple[str, ...]: - projection = self.produced_outputs[0].producer_identity.route_parts() - for produced_output in self.produced_outputs[1:]: - if produced_output.producer_identity.route_parts() != projection: - raise ValueError( - "A viewer stream batch cannot mix producer projections." - ) - return projection - - def runtime_projection_request( - self, - payload: StreamPayload, - path: str, - produced_output: ProducedOutputSemantics, - ) -> RuntimeProjectionSourceIdentityRequest: - return RuntimeProjectionSourceIdentityRequest( - value=produced_output.contextualize_image_payload(payload), - source_description=path, - ) - - def for_projection( - self, - projection: tuple[str, ...], - ) -> StreamOutputProjectionRequest: - payloads: list[StreamPayload] = [] - paths: list[str] = [] - produced_outputs: list[ProducedOutputSemantics] = [] - for payload, path, produced_output in zip( - self.payloads, - self.paths, - self.produced_outputs, - strict=True, - ): - if produced_output.producer_identity.route_parts() == projection: - payloads.append(payload) - paths.append(path) - produced_outputs.append(produced_output) - return StreamOutputProjectionRequest.from_sequences( - parser=self.parser, - payloads=payloads, - paths=paths, - produced_outputs=tuple(produced_outputs), - ) - - @dataclass(frozen=True, slots=True) class StreamOutputItem: """One projected image payload and its stream-visible output path.""" @@ -429,23 +310,55 @@ class StreamOutputBatch: items: tuple[StreamOutputItem, ...] producer: ViewerStreamProducer + @staticmethod + def _validate_inputs( + payloads: Sequence[StreamPayload], + paths: Sequence[str], + produced_outputs: tuple[ProducedOutputSemantics, ...], + ) -> None: + if len(payloads) != len(paths): + raise ValueError( + "Streaming payload/path cardinality mismatch: " + f"{len(payloads)} payloads for {len(paths)} paths." + ) + if len(payloads) != len(produced_outputs): + raise ValueError( + "Streaming payload/output-record cardinality mismatch: " + f"{len(payloads)} payloads for " + f"{len(produced_outputs)} output records." + ) + if not produced_outputs: + raise ValueError("Streaming requires at least one produced output record.") + @classmethod def from_projection( cls, - request: StreamOutputProjectionRequest, + *, + parser: FilenameParser, + payloads: Sequence[StreamPayload], + paths: Sequence[str], + produced_outputs: tuple[ProducedOutputSemantics, ...], ) -> StreamOutputBatch: - request.require_single_projection() + payloads = tuple(payloads) + paths = tuple(paths) + cls._validate_inputs(payloads, paths, produced_outputs) + projection = produced_outputs[0].producer_identity.route_parts() + for produced_output in produced_outputs[1:]: + if produced_output.producer_identity.route_parts() != projection: + raise ValueError( + "A viewer stream batch cannot mix producer projections." + ) items: list[StreamOutputItem] = [] for payload, path, produced_output in zip( - request.payloads, - request.paths, - request.produced_outputs, - strict=True, + payloads, paths, produced_outputs, strict=True, ): projected_items = tuple( cls.project_item( - request.runtime_projection_request(payload, path, produced_output) + RuntimeProjectionSourceIdentityRequest( + value=produced_output.contextualize_image_payload(payload), + source_description=path, + ) ) ) for projected_item in projected_items: @@ -453,7 +366,7 @@ def from_projection( projected_item, produced_path=path, produced_output=produced_output, - parser=request.parser, + parser=parser, projected_item_count=len(projected_items), ) source_metadata = projected_item.require_source_component_metadata() @@ -481,18 +394,33 @@ def from_projection( @classmethod def from_projection_groups( cls, - request: StreamOutputProjectionRequest, + *, + parser: FilenameParser, + payloads: Sequence[StreamPayload], + paths: Sequence[str], + produced_outputs: tuple[ProducedOutputSemantics, ...], ) -> tuple[StreamOutputBatch, ...]: - projections = tuple( - dict.fromkeys( - produced_output.producer_identity.route_parts() - for produced_output in request.produced_outputs + payloads = tuple(payloads) + paths = tuple(paths) + cls._validate_inputs(payloads, paths, produced_outputs) + projections: dict[ + tuple[str, ...], list[tuple[StreamPayload, str, ProducedOutputSemantics]] + ] = {} + for payload, path, produced_output in zip( + payloads, paths, produced_outputs, strict=True, + ): + projection = produced_output.producer_identity.route_parts() + projections.setdefault(projection, []).append( + (payload, path, produced_output) ) - ) - return tuple( - cls.from_projection(request.for_projection(projection)) - for projection in projections + cls.from_projection( + parser=parser, + payloads=tuple(payload for payload, _path, _record in members), + paths=tuple(path for _payload, path, _record in members), + produced_outputs=tuple(record for _payload, _path, record in members), + ) + for members in projections.values() ) @property @@ -607,13 +535,9 @@ def stream_outputs( context.axis_id, ) continue - produced_outputs = tuple( - record - for record in step_output_manifest(context).produced_records_for(plan) - if record.is_image_payload - ) + produced_outputs = step_output_manifest(context).image_records_for(plan) memory_paths = [ - ProducedMemoryPathsAuthority.memory_path(record, plan) + record.memory_path(plan) for record in produced_outputs ] if not memory_paths: @@ -637,12 +561,10 @@ def stream_outputs( ) ) stream_batches = StreamOutputBatch.from_projection_groups( - StreamOutputProjectionRequest.from_sequences( - parser=context.microscope_handler.parser, - payloads=streaming_payloads, - paths=list(streaming_paths), - produced_outputs=produced_outputs, - ) + parser=context.microscope_handler.parser, + payloads=streaming_payloads, + paths=streaming_paths, + produced_outputs=produced_outputs, ) stream_batches = tuple( stream_batch @@ -866,7 +788,7 @@ def produced_projection_metadata( payloads = ( context.filemanager.load_batch( [ - ProducedMemoryPathsAuthority.memory_path(record, plan) + record.memory_path(plan) for record in records ], Backend.MEMORY.value, @@ -888,11 +810,8 @@ def produced_projection_metadata( destination=destination, payload=payload, ) - source_metadata = dict( - record.component_metadata(metadata.source_component_metadata) - ) - metadata.source_voxel_spacing.merge_into( - source_metadata, path=destination + source_metadata = record.source_metadata_for_projection( + metadata, destination ) address = record.filename_address if address not in declared_addresses: @@ -1115,11 +1034,7 @@ class ProducedImageMetadataCapability: def produced_records( self, context: ProcessingContext, plan: CompiledStepPlan ) -> tuple[ProducedOutputSemantics, ...]: - return tuple( - record - for record in step_output_manifest(context).produced_records_for(plan) - if record.is_image_payload - ) + return step_output_manifest(context).image_records_for(plan) class PrimaryImageMetadataTarget( @@ -1133,7 +1048,7 @@ class PrimaryImageMetadataTarget( def from_execution( cls, context: ProcessingContext, plan: CompiledStepPlan ) -> PrimaryImageMetadataTarget | None: - if not ProducedMemoryPathsAuthority.paths(context, plan): + if not step_output_manifest(context).image_records_for(plan): return None return cls.from_plan(plan) diff --git a/openhcs/core/steps/function_runtime.py b/openhcs/core/steps/function_runtime.py index b2bdd42428..0b28125213 100644 --- a/openhcs/core/steps/function_runtime.py +++ b/openhcs/core/steps/function_runtime.py @@ -71,7 +71,6 @@ from openhcs.core.aligned_image_payload import ( AlignedImageStack, AlignedImageSliceContext, - ImagePayloadBundleContext, ImagePayloadStackComposition, ImageOutputBundle, flatten_aligned_image_payload_slices, @@ -81,7 +80,6 @@ from openhcs.core.memory import ( MemoryType, convert_memory, - stack_runtime_slices, unstack_runtime_slices, ) from openhcs.core.measurement_row_materialization import ( diff --git a/openhcs/processing/presets/pipelines/czi_brain_axon_cellbody.py b/openhcs/processing/presets/pipelines/czi_brain_axon_cellbody.py index a9896b3930..5c23e04a39 100644 --- a/openhcs/processing/presets/pipelines/czi_brain_axon_cellbody.py +++ b/openhcs/processing/presets/pipelines/czi_brain_axon_cellbody.py @@ -51,9 +51,7 @@ SourceSelector, ) from openhcs.core.source_projection import SourceCandidate, SourcePlaneDataset -from openhcs.core.steps.function_output_manifest import ( - FunctionStepOutputProducerIdentityRequest, -) +from openhcs.core.compiled_step_plan import CompiledStepPlan from openhcs.core.steps.function_step import FunctionStep from openhcs.microscopes.bioformats import BioFormatsHandler, BioFormatsMetadataHandler from openhcs.microscopes.bioformats_adapter import BIOFORMATS_MANIFEST_FILENAME @@ -478,7 +476,7 @@ def artifact_presentation( ) -> StreamProducerIdentity: return StreamProducerIdentity.pipeline_output( output_kind=( - FunctionStepOutputProducerIdentityRequest.ARTIFACT_OUTPUT_KIND + CompiledStepPlan.ARTIFACT_OUTPUT_KIND ), output_key=output_spec.name, projection_key=output_spec.name, diff --git a/openhcs/processing/presets/pipelines/loose_operaphenix_neurite_outgrowth.py b/openhcs/processing/presets/pipelines/loose_operaphenix_neurite_outgrowth.py index 3434bf4d51..7aea13ca99 100644 --- a/openhcs/processing/presets/pipelines/loose_operaphenix_neurite_outgrowth.py +++ b/openhcs/processing/presets/pipelines/loose_operaphenix_neurite_outgrowth.py @@ -47,9 +47,7 @@ SourceFilterSubject, SourceSelector, ) -from openhcs.core.steps.function_output_manifest import ( - FunctionStepOutputProducerIdentityRequest, -) +from openhcs.core.compiled_step_plan import CompiledStepPlan from openhcs.core.steps.function_step import FunctionStep from openhcs.processing.backends.analysis.neurite_outgrowth import ( CELLPROFILER_NEURITE_ENGINE_PROFILE, @@ -433,7 +431,7 @@ def prepare() -> None: pipeline_steps=tuple(pipeline_steps), presentation_identity=StreamProducerIdentity.pipeline_output( output_kind=( - FunctionStepOutputProducerIdentityRequest.ARTIFACT_OUTPUT_KIND + CompiledStepPlan.ARTIFACT_OUTPUT_KIND ), output_key=secondary_output_name, projection_key=secondary_output_name, diff --git a/openhcs/processing/presets/pipelines/neuroncyto_ii_crossover_neurite_outgrowth.py b/openhcs/processing/presets/pipelines/neuroncyto_ii_crossover_neurite_outgrowth.py index f97743b068..d7acf246a8 100644 --- a/openhcs/processing/presets/pipelines/neuroncyto_ii_crossover_neurite_outgrowth.py +++ b/openhcs/processing/presets/pipelines/neuroncyto_ii_crossover_neurite_outgrowth.py @@ -44,9 +44,7 @@ SourceFilterSubject, SourceSelector, ) -from openhcs.core.steps.function_output_manifest import ( - FunctionStepOutputProducerIdentityRequest, -) +from openhcs.core.compiled_step_plan import CompiledStepPlan from openhcs.core.steps.function_step import FunctionStep from openhcs.processing.backends.analysis.neurite_outgrowth import ( NEURITE_MORPHOLOGY_OUTPUT, @@ -295,7 +293,7 @@ def prepare() -> None: pipeline_steps=tuple(pipeline_steps), presentation_identity=StreamProducerIdentity.pipeline_output( output_kind=( - FunctionStepOutputProducerIdentityRequest.ARTIFACT_OUTPUT_KIND + CompiledStepPlan.ARTIFACT_OUTPUT_KIND ), output_key=NEURITE_MORPHOLOGY_OUTPUT.name, projection_key=NEURITE_MORPHOLOGY_OUTPUT.name, diff --git a/tests/unit/test_aligned_image_payload_registry.py b/tests/unit/test_aligned_image_payload_registry.py index d86066c90b..67411bad17 100644 --- a/tests/unit/test_aligned_image_payload_registry.py +++ b/tests/unit/test_aligned_image_payload_registry.py @@ -1,100 +1,86 @@ from __future__ import annotations +from abc import ABC from collections.abc import Iterator -from typing import Any +import numpy as np import pytest from openhcs.core.aligned_image_payload import ( - AlignedImageStackKwargResolutionStrategy, AlignedImageStackKwargResolver, - ImagePayloadAlignedKwargResolutionStrategy, - NestedAlignedImageStackKwargResolutionStrategy, - ObjectLabelAlignedKwargResolutionStrategy, - PassThroughAlignedKwargResolutionStrategy, - RuntimeSliceAlignedValueKwargResolutionStrategy, - RuntimeSliceProjectableAlignedKwargResolutionStrategy, - TupleAlignedKwargResolutionStrategy, + AlignedImageSliceContext, + ImageOutputBundle, ) +from openhcs.core.runtime_image_values import ( + ImagePayloadMetadata, + ImagePayloadMetadataCarrier, + ImageMetadataPayload, + image_payload_data, +) +from openhcs.core.runtime_object_labels import ObjectLabelSet, ObjectLabelVariantData +from openhcs.core.runtime_object_label_domains import ObjectLabelDomain, ObjectLabelDomainScope +from openhcs.core.runtime_plane_projection import ( + RuntimePlaneAxis, + RuntimePlaneAxisValueProjection, + RuntimeSliceProjectableValue, +) +from openhcs.core.runtime_slice_alignment import RuntimeSliceAlignedValues +from openhcs.core.runtime_slice_projection import ( + RuntimeSliceProjection, + RuntimeSliceProjectionDeclarationError, + RuntimeSliceProjectionStrategy, + ImagePayloadRuntimeSliceProjectionStrategy, + SequenceRuntimeSliceProjectionStrategy, +) + + +def _clear_strategy_caches() -> None: + RuntimeSliceProjectionStrategy.registered_strategy_types.cache_clear() + RuntimeSliceProjectionStrategy.strategy_types_for_nominal_type.cache_clear() + RuntimeSliceProjectionStrategy.strategy_types_for_aligned_kwarg_type.cache_clear() @pytest.fixture -def isolated_strategy_registry() -> ( - Iterator[dict[str, type[AlignedImageStackKwargResolutionStrategy]]] -): - registry = AlignedImageStackKwargResolutionStrategy.__registry__ +def isolated_strategy_registry() -> Iterator[dict[str, type[RuntimeSliceProjectionStrategy]]]: + registry = RuntimeSliceProjectionStrategy.__registry__ snapshot = registry.copy() - AlignedImageStackKwargResolutionStrategy.registered_strategy_types.cache_clear() - AlignedImageStackKwargResolutionStrategy.strategy_types_for_nominal_type.cache_clear() + _clear_strategy_caches() try: yield registry finally: registry.clear() registry.update(snapshot) - AlignedImageStackKwargResolutionStrategy.registered_strategy_types.cache_clear() - AlignedImageStackKwargResolutionStrategy.strategy_types_for_nominal_type.cache_clear() - - -def test_existing_aligned_kwarg_strategies_register_automatically() -> None: - expected = { - TupleAlignedKwargResolutionStrategy, - ImagePayloadAlignedKwargResolutionStrategy, - ObjectLabelAlignedKwargResolutionStrategy, - RuntimeSliceAlignedValueKwargResolutionStrategy, - RuntimeSliceProjectableAlignedKwargResolutionStrategy, - PassThroughAlignedKwargResolutionStrategy, - NestedAlignedImageStackKwargResolutionStrategy, - } - - assert type(AlignedImageStackKwargResolutionStrategy.__registry__) is dict - assert ( - set(AlignedImageStackKwargResolutionStrategy.__registry__.values()) == expected + _clear_strategy_caches() + + +def _resolver(*, axis=RuntimePlaneAxis.RUNTIME_SLICE, size=2, index=1): + return AlignedImageStackKwargResolver( + RuntimePlaneAxisValueProjection.from_selected_plane( + axis=axis, plane_index=index, axis_size=size, + ), ) - assert len(AlignedImageStackKwargResolutionStrategy.__registry__) == len(expected) -def test_dynamic_aligned_kwarg_strategy_registers_at_class_definition( - isolated_strategy_registry: dict[ - str, type[AlignedImageStackKwargResolutionStrategy] - ], -) -> None: +def test_dynamic_aligned_operation_registers_on_existing_projection_family(isolated_strategy_registry): class DynamicKwarg: pass - class DynamicKwargResolutionStrategy(AlignedImageStackKwargResolutionStrategy): + marker = object() + + class DynamicProjectionStrategy(RuntimeSliceProjectionStrategy): value_type = DynamicKwarg - def resolve( - self, - value: Any, - resolver: AlignedImageStackKwargResolver, - ) -> Any: - del resolver - return value - - assert DynamicKwargResolutionStrategy.value_type_label is not None - assert ( - isolated_strategy_registry[DynamicKwargResolutionStrategy.value_type_label] - is DynamicKwargResolutionStrategy - ) - assert ( - type( - AlignedImageStackKwargResolutionStrategy.require_nominal_value( - DynamicKwarg(), - context="dynamic aligned kwarg", - ) - ) - is DynamicKwargResolutionStrategy - ) + def resolve_aligned_kwarg(self, value, resolver): + assert isinstance(value, DynamicKwarg) + assert resolver.projection_axis.axis_size == 2 + return marker + assert DynamicProjectionStrategy.value_type_label is not None + assert isolated_strategy_registry[DynamicProjectionStrategy.value_type_label] is DynamicProjectionStrategy + assert _resolver().resolve(DynamicKwarg()) is marker -def test_aligned_kwarg_strategy_resolution_follows_exact_value_mro( - isolated_strategy_registry: dict[ - str, type[AlignedImageStackKwargResolutionStrategy] - ], -) -> None: - del isolated_strategy_registry +def test_aligned_operation_follows_exact_value_mro(isolated_strategy_registry): class BaseKwarg: pass @@ -104,41 +90,250 @@ class DerivedKwarg(BaseKwarg): class MostDerivedKwarg(DerivedKwarg): pass - class BaseKwargResolutionStrategy(AlignedImageStackKwargResolutionStrategy): + base_marker, derived_marker = object(), object() + + class BaseProjectionStrategy(RuntimeSliceProjectionStrategy): value_type = BaseKwarg - def resolve( - self, - value: Any, - resolver: AlignedImageStackKwargResolver, - ) -> Any: - del resolver - return value + def resolve_aligned_kwarg(self, value, resolver): + return base_marker - class DerivedKwargResolutionStrategy(AlignedImageStackKwargResolutionStrategy): + class DerivedProjectionStrategy(BaseProjectionStrategy): value_type = DerivedKwarg - def resolve( - self, - value: Any, - resolver: AlignedImageStackKwargResolver, - ) -> Any: - del resolver - return value - - assert AlignedImageStackKwargResolutionStrategy.strategy_types_for_nominal_type( - MostDerivedKwarg - ) == ( - DerivedKwargResolutionStrategy, - BaseKwargResolutionStrategy, - PassThroughAlignedKwargResolutionStrategy, + def resolve_aligned_kwarg(self, value, resolver): + return derived_marker + + assert RuntimeSliceProjectionStrategy.strategy_types_for_aligned_kwarg_type(MostDerivedKwarg) == ( + DerivedProjectionStrategy, BaseProjectionStrategy, ) - assert ( - type( - AlignedImageStackKwargResolutionStrategy.require_nominal_value( - MostDerivedKwarg(), - context="MRO-aligned kwarg", - ) - ) - is DerivedKwargResolutionStrategy + assert _resolver().resolve(MostDerivedKwarg()) is derived_marker + + +def test_default_aligned_hook_does_not_steal_projectable_operation_through_mi(isolated_strategy_registry): + class OrdinaryOwner: + pass + + class MixedKwarg(OrdinaryOwner, RuntimeSliceProjectableValue): + def project_runtime_slice(self, slice_index): + pytest.fail("ordinary nominal owner must determine the inner projection") + + marker = object() + + class OrdinaryOnlyProjectionStrategy(RuntimeSliceProjectionStrategy): + value_type = OrdinaryOwner + + def value_for_slice(self, value, context): + assert isinstance(value, MixedKwarg) + assert context.require_plane_index() == 1 + return marker + + ordinary = RuntimeSliceProjectionStrategy.strategy_types_for_nominal_type(MixedKwarg) + aligned = RuntimeSliceProjectionStrategy.strategy_types_for_aligned_kwarg_type(MixedKwarg) + assert ordinary[0] is OrdinaryOnlyProjectionStrategy + assert OrdinaryOnlyProjectionStrategy not in aligned + assert _resolver().resolve(MixedKwarg()) is marker + + +def test_virtual_abc_ties_follow_single_canonical_registry_order(isolated_strategy_registry): + class FirstMember(ABC): + pass + + class SecondMember(ABC): + pass + + class VirtualValue: + pass + + FirstMember.register(VirtualValue) + SecondMember.register(VirtualValue) + first, second = object(), object() + + class FirstProjectionStrategy(RuntimeSliceProjectionStrategy): + value_type = FirstMember + + def value_for_slice(self, value, context): + return first + + def resolve_aligned_kwarg(self, value, resolver): + return first + + class SecondProjectionStrategy(RuntimeSliceProjectionStrategy): + value_type = SecondMember + + def value_for_slice(self, value, context): + return second + + def resolve_aligned_kwarg(self, value, resolver): + return second + + value, resolver = VirtualValue(), _resolver() + assert resolver.resolve(value) is first + assert RuntimeSliceProjection.value_for_slice(value, resolver.projection_axis) is first + key = FirstProjectionStrategy.value_type_label + isolated_strategy_registry[key] = isolated_strategy_registry.pop(key) + _clear_strategy_caches() + assert resolver.resolve(value) is second + assert RuntimeSliceProjection.value_for_slice(value, resolver.projection_axis) is second + + +def test_tuple_recurses_while_list_stays_in_its_original_domain(): + projection = _resolver().projection_axis + elements = [object(), object()] + # A projection declaration has its own meaningful projectable capability. + values = (projection, elements) + result = _resolver().resolve(values) + assert type(result) is tuple + assert result[0].require_plane_index() == 1 + assert result[1] is elements + assert _resolver().resolve(elements) is elements + + +def test_unknown_aligned_value_passes_through_but_ordinary_projection_stays_strict(): + class Unknown: + pass + + value = Unknown() + assert _resolver().resolve(value) is value + with pytest.raises(RuntimeSliceProjectionDeclarationError, match="no nominal strategy"): + RuntimeSliceProjection.value_for_slice(value, _resolver().projection_axis) + + +def test_broad_image_carrier_aligned_operation_preserves_declared_stack(): + class BroadCarrier(ImagePayloadMetadataCarrier): + def __init__(self): + self.pixels = np.ones((2, 3, 4), dtype=np.float32) + self._metadata = ImagePayloadMetadata(plane_axis=RuntimePlaneAxis.RUNTIME_SLICE) + + def image_data(self): + return self.pixels + + @property + def metadata(self): + return self._metadata + + value = BroadCarrier() + assert _resolver().resolve(value) is value + assert value.image_data().shape == (2, 3, 4) + with pytest.raises(RuntimeSliceProjectionDeclarationError, match="no nominal strategy"): + RuntimeSliceProjection.value_for_slice(value, _resolver().projection_axis) + + +def test_image_aligned_operation_does_not_remove_runtime_plane_axis(): + value = ImagePayloadMetadata(plane_axis=RuntimePlaneAxis.RUNTIME_SLICE).payload_with( + np.arange(24, dtype=np.float32).reshape(2, 3, 4), + ) + resolver = _resolver() + assert resolver.resolve(value) is value + ordinary = RuntimeSliceProjection.value_for_slice(value, resolver.projection_axis) + np.testing.assert_array_equal(image_payload_data(ordinary), image_payload_data(value)[1]) + assert image_payload_data(ordinary).shape == (3, 4) + + +def test_image_output_bundle_aligned_operation_selects_outer_member_then_recurses(): + first, second = tuple( + ImagePayloadMetadata(plane_axis=RuntimePlaneAxis.RUNTIME_SLICE).payload_with( + np.full((2, 3, 4), index, dtype=np.float32), + ) for index in (1, 2) + ) + bundle = ImageOutputBundle( + (first, second), + tuple(AlignedImageSliceContext.independent_main_flow(name) for name in ("First", "Second")), + ) + resolver = _resolver() + assert resolver.resolve(bundle) is second + assert image_payload_data(resolver.resolve(bundle)).shape == (2, 3, 4) + ordinary = RuntimeSliceProjection.value_for_slice(bundle, resolver.projection_axis) + assert isinstance(ordinary, ImageOutputBundle) + assert [image_payload_data(value).shape for value in ordinary.slices] == [(3, 4), (3, 4)] + + +@pytest.mark.parametrize("axis", (RuntimePlaneAxis.RUNTIME_SLICE, RuntimePlaneAxis.SOURCE_BINDING)) +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: + 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) + + +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, + domain=ObjectLabelDomain( + declared_object_id_domains=((1,), (1,)), + scope=ObjectLabelDomainScope.PLANE, + ), ) + monkeypatch.setattr(RuntimeSliceProjection, "value_for_slice", lambda *_args: pytest.fail("cardinality comes first")) + monkeypatch.setattr(AlignedImageStackKwargResolver, "resolve_source_spatial_value", lambda *_args: pytest.fail("cardinality comes before spatial alignment")) + with pytest.raises(ValueError, match="cardinality must exactly match"): + _resolver(size=3).resolve(labels) + + +def test_narrower_image_owner_inherits_operation_scope_from_its_own_declaration(isolated_strategy_registry): + class DerivedImage(ImageMetadataPayload): + pass + + marker = object() + + class DerivedImageProjectionStrategy(ImagePayloadRuntimeSliceProjectionStrategy): + value_type = DerivedImage + + def resolve_aligned_kwarg(self, value, resolver): + return marker + + value = DerivedImage( + data=np.arange(24, dtype=np.float32).reshape(2, 3, 4), + metadata=ImagePayloadMetadata(plane_axis=RuntimePlaneAxis.RUNTIME_SLICE), + ) + assert RuntimeSliceProjectionStrategy.strategy_types_for_aligned_kwarg_type(DerivedImage)[0] is DerivedImageProjectionStrategy + assert RuntimeSliceProjectionStrategy.strategy_types_for_nominal_type(DerivedImage)[0] is DerivedImageProjectionStrategy + assert _resolver().resolve(value) is marker + ordinary = RuntimeSliceProjection.value_for_slice(value, _resolver().projection_axis) + np.testing.assert_array_equal(image_payload_data(ordinary), value.data[1]) + + +def test_narrower_sequence_owner_can_declare_aligned_list_capability(isolated_strategy_registry): + class DeclaredList(list): + pass + + class DeclaredListProjectionStrategy(SequenceRuntimeSliceProjectionStrategy): + value_type = DeclaredList + + resolver = _resolver() + projection = resolver.projection_axis + original = DeclaredList([projection, object()]) + assert RuntimeSliceProjectionStrategy.strategy_types_for_aligned_kwarg_type(DeclaredList)[0] is DeclaredListProjectionStrategy + aligned = resolver.resolve(original) + assert isinstance(aligned, tuple) + assert aligned[0].require_plane_index() == 1 + assert aligned[1] is original[1] + plain = list(original) + assert resolver.resolve(plain) is plain + + +def test_derived_dispatch_cache_uses_the_same_explicit_registry_mutation_boundary(isolated_strategy_registry): + class DynamicKwarg: + pass + + resolver, value = _resolver(), DynamicKwarg() + assert resolver.resolve(value) is value + marker = object() + + class DynamicProjectionStrategy(RuntimeSliceProjectionStrategy): + value_type = DynamicKwarg + + def resolve_aligned_kwarg(self, value, resolver): + return marker + + _clear_strategy_caches() + assert resolver.resolve(value) is marker + before = RuntimeSliceProjectionStrategy.strategy_types_for_aligned_kwarg_type.cache_info() + assert resolver.resolve(value) is marker + after = RuntimeSliceProjectionStrategy.strategy_types_for_aligned_kwarg_type.cache_info() + assert after.hits == before.hits + 1 diff --git a/tests/unit/test_cellprofiler_runtime_adapter.py b/tests/unit/test_cellprofiler_runtime_adapter.py index c5a86f3d1b..ee29bdc6ce 100644 --- a/tests/unit/test_cellprofiler_runtime_adapter.py +++ b/tests/unit/test_cellprofiler_runtime_adapter.py @@ -3140,6 +3140,7 @@ def test_cellprofiler_adapter_discovers_single_realized_dynamic_grouped_input(): artifact_type=ObjectLabelsArtifactType, group_keys=(None,), group_component=AllComponents.CHANNEL, + paths_by_group={"1": realized_path}, ), invocation_scope=ComponentGroupScope.ungrouped(), producer_selection_scope=ComponentGroupScope.dynamic( @@ -3172,6 +3173,22 @@ def test_cellprofiler_adapter_discovers_single_realized_dynamic_grouped_input(): assert objects.domain.declared_object_id_domains == ((1,),) + # A dynamic component never grants access to another compiled producer path. + (edge,) = _compiled_artifact_inputs.values() + for undeclared_paths in (None, {"1": "/other/Nuclei_s1.pkl"}): + mismatched_edge = replace( + edge, storage_plan=replace(edge.storage_plan, paths_by_group=undeclared_paths) + ) + mismatched_consumer = CellProfilerRuntimeAdapter( + request=replace( + consumer.request, artifact_inputs={edge.key: mismatched_edge} + ), + backend=consumer.backend, + ) + with pytest.raises(RuntimeError, match="Missing dynamic grouped artifact input"): + mismatched_consumer.get_objects(NUCLEI) + + def test_cellprofiler_adapter_discovers_realized_dynamic_grouped_object_inputs(): filemanager = FileManagerStub() store = RuntimeValueStore() @@ -3225,6 +3242,7 @@ def test_cellprofiler_adapter_discovers_realized_dynamic_grouped_object_inputs() artifact_type=ObjectLabelsArtifactType, group_keys=(None,), group_component=AllComponents.CHANNEL, + paths_by_group=group_paths, ), invocation_scope=ComponentGroupScope.ungrouped(), producer_selection_scope=ComponentGroupScope.dynamic( @@ -3549,6 +3567,10 @@ def test_cellprofiler_adapter_preserves_ungrouped_runtime_slice_output_stack(): def test_cellprofiler_adapter_stacks_dynamic_compiled_grouped_images(): filemanager = FileManagerStub() store = RuntimeValueStore() + group_paths = { + "1": "/memory/DNA_s1.pkl", + "2": "/memory/DNA_s2.pkl", + } first = cellprofiler_runtime_adapter_for_test( runtime_value_store=store, axis_scope=runtime_axis_scope(AXIS_ID), @@ -3559,11 +3581,11 @@ def test_cellprofiler_adapter_stacks_dynamic_compiled_grouped_images(): ImageArtifactType, plan=ArtifactOutputPlan( name=DNA_IMAGE, - path="/memory/DNA_s1.pkl", + path=group_paths["1"], artifact_type=ImageArtifactType, group_keys=("1",), group_component=AllComponents.SITE, - paths_by_group={"1": "/memory/DNA_s1.pkl"}, + paths_by_group={"1": group_paths["1"]}, ), ), ), @@ -3579,11 +3601,11 @@ def test_cellprofiler_adapter_stacks_dynamic_compiled_grouped_images(): ImageArtifactType, plan=ArtifactOutputPlan( name=DNA_IMAGE, - path="/memory/DNA_s2.pkl", + path=group_paths["2"], artifact_type=ImageArtifactType, group_keys=("2",), group_component=AllComponents.SITE, - paths_by_group={"2": "/memory/DNA_s2.pkl"}, + paths_by_group={"2": group_paths["2"]}, ), ), ), @@ -3601,6 +3623,7 @@ def test_cellprofiler_adapter_stacks_dynamic_compiled_grouped_images(): artifact_type=ImageArtifactType, group_keys=(None,), group_component=AllComponents.SITE, + paths_by_group=group_paths, ), invocation_scope=ComponentGroupScope.ungrouped(), producer_selection_scope=ComponentGroupScope.dynamic( diff --git a/tests/unit/test_function_output_manifest_input_scope.py b/tests/unit/test_function_output_manifest_input_scope.py index d2b55b26fd..bdcc32f4c2 100644 --- a/tests/unit/test_function_output_manifest_input_scope.py +++ b/tests/unit/test_function_output_manifest_input_scope.py @@ -7,12 +7,19 @@ from openhcs.core.aligned_image_payload import AlignedImageSliceContext from openhcs.core.artifacts import ( ArtifactInputPlan, + ArtifactOutputPlan, ArtifactInputProjectionPlan, ArtifactSpec, ImageArtifactType, MeasurementsArtifactType, ObjectLabelsArtifactType, ) +from openhcs.core.compiled_step_plan import CompiledStepPlan +from openhcs.core.runtime_image_values import ImagePayloadMetadata +from openhcs.core.source_metadata import ( + SOURCE_VOXEL_SPACING_FIELD, + SourceVoxelSpacing, +) from openhcs.core.function_patterns import ( InvocationArtifactInputEdgePlan, InvocationArtifactInputProjectionKey, @@ -22,22 +29,98 @@ from openhcs.core.step_dependencies import StepInputDependency from openhcs.core.steps.function_output_identity import FunctionOutputIdentity from openhcs.core.steps.function_output_manifest import ( - FunctionStepOutputProducerIdentityRequest, ProducedOutputSemantics, StepOutputManifestStore, ) from openhcs.microscopes.source_schema import SourceSchemaFilenameParser -def test_artifact_output_kind_is_declared_by_producer_identity_request() -> None: - request = FunctionStepOutputProducerIdentityRequest.from_artifact( - SimpleNamespace(), - SimpleNamespace(name="cells", artifact_type=ObjectLabelsArtifactType), +def test_artifact_output_kind_is_owned_by_original_compiled_plan() -> None: + plan = CompiledStepPlan( + step_index=3, + step_type="FunctionStep", + step_name="IdentifyCells", + step_scope_id="identify-cells", + pipeline_position=3, + axis_id="A01", ) + output = ArtifactOutputPlan( + name="cells", path="cells.pkl", artifact_type=ObjectLabelsArtifactType, + ) + identity = plan.producer_identity_for_artifact(output) + + assert identity.output_kind == CompiledStepPlan.ARTIFACT_OUTPUT_KIND + assert identity.output_key == identity.projection_key == "cells" + assert identity.artifact_kind == ObjectLabelsArtifactType.value + assert identity.step_name == "IdentifyCells" + assert identity.step_scope_id == "identify-cells" + assert identity.pipeline_position == 3 - assert request.output_kind == ( - FunctionStepOutputProducerIdentityRequest.ARTIFACT_OUTPUT_KIND + +def test_producer_identity_reads_current_plan_and_original_surface() -> None: + plan = CompiledStepPlan( + step_index=1, step_type="FunctionStep", step_name="Original", + step_scope_id="original", pipeline_position=1, axis_id="A01", ) + surface = AlignedImageSliceContext.main_flow("corrected") + original = plan.producer_identity_for_main_flow(surface) + plan.step_name = "Replacement" + plan.step_scope_id = "replacement" + plan.pipeline_position = 2 + replacement = plan.producer_identity_for_main_flow(surface) + + assert original.step_name == "Original" + assert original.step_scope_id == "original" + assert original.pipeline_position == 1 + assert replacement.step_name == "Replacement" + assert replacement.step_scope_id == "replacement" + assert replacement.pipeline_position == 2 + assert replacement.output_kind == surface.output_kind + assert replacement.output_key == surface.output_key + assert replacement.projection_key == surface.projection_key + assert replacement.artifact_kind == surface.artifact_kind + + +@pytest.mark.parametrize("saved_path", ("/saved/image.tif", "image.tif")) +def test_produced_occurrence_owns_memory_path_and_current_source_projection( + saved_path: str, +) -> None: + plan = CompiledStepPlan( + step_index=1, step_type="FunctionStep", step_name="Producer", axis_id="A01", + step_scope_id="producer", pipeline_position=1, output_dir=Path("/memory"), + ) + components = {"well": "A01", "channel": 2} + record = ProducedOutputSemantics( + producer_identity=plan.producer_identity_for_main_flow( + AlignedImageSliceContext.anonymous_main_flow() + ), + component_values=components, + filename_component_values={"well": "A01", "channel": 9}, + extension=".tif", source="test", output_path=saved_path, + relative_output_path="nested/image.tif", + ) + assert record.memory_path(plan) == ( + saved_path if saved_path.startswith("/") else "/memory/nested/image.tif" + ) + metadata = ImagePayloadMetadata( + source_component_metadata={"acquisition": "first", "channel": 1}, + source_voxel_spacing=SourceVoxelSpacing((0.5, 0.5)), + ) + original = record.source_metadata_for_projection(metadata, "/disk/output.tif") + assert original["channel"] == "2" + assert original["acquisition"] == "first" + assert original[SOURCE_VOXEL_SPACING_FIELD] == "0.5,0.5" + components["channel"] = 3 + metadata.source_component_metadata = {"acquisition": "second", "channel": 1} + current = record.source_metadata_for_projection(metadata, "/disk/output.tif") + assert current["channel"] == "3" + assert current["acquisition"] == "second" + assert original["channel"] == "2" + assert record.filename_values["channel"] == 9 + + metadata.source_component_metadata = {SOURCE_VOXEL_SPACING_FIELD: "1,1"} + with pytest.raises(RuntimeError, match="Conflicting source voxel spacing.*output.tif"): + record.source_metadata_for_projection(metadata, "/disk/output.tif") def _compiled_pattern_with_input_edges( @@ -124,7 +207,9 @@ def test_foreign_artifact_inputs_do_not_filter_lifecycle_producer( foreign_inputs: tuple[tuple[ArtifactSpec, str], ...], ) -> None: output_dir = tmp_path / "images" - producer = SimpleNamespace( + producer = CompiledStepPlan( + step_index=1, + step_type="FunctionStep", step_scope_id=dependency_scope, step_name="LifecycleProducer", pipeline_position=1, @@ -183,7 +268,9 @@ def test_compiled_main_flow_edge_selects_exact_producer_identity( tmp_path: Path, ) -> None: output_dir = tmp_path / "images" - producer = SimpleNamespace( + producer = CompiledStepPlan( + step_index=1, + step_type="FunctionStep", step_scope_id="align", step_name="Align", pipeline_position=1, @@ -264,7 +351,9 @@ def test_storage_backed_primary_input_selects_exact_lifecycle_output( tmp_path: Path, ) -> None: output_dir = tmp_path / "images" - producer = SimpleNamespace( + producer = CompiledStepPlan( + step_index=1, + step_type="FunctionStep", step_scope_id="color_to_gray", step_name="ColorToGray", pipeline_position=1, @@ -361,7 +450,9 @@ def test_storage_backed_input_does_not_reclassify_lifecycle_output( tmp_path: Path, ) -> None: output_dir = tmp_path / "images" - producer = SimpleNamespace( + producer = CompiledStepPlan( + step_index=1, + step_type="FunctionStep", step_scope_id="artifact_producer", step_name="ArtifactProducer", pipeline_position=1, @@ -436,7 +527,9 @@ def test_same_scope_parameter_bound_input_does_not_select_lifecycle_output( parameter_name: str, ) -> None: output_dir = tmp_path / "images" - producer = SimpleNamespace( + producer = CompiledStepPlan( + step_index=1, + step_type="FunctionStep", step_scope_id="artifact_producer", step_name="ArtifactProducer", pipeline_position=1, diff --git a/tests/unit/test_function_outputs.py b/tests/unit/test_function_outputs.py index 98d2cad9f9..36caefe3a0 100644 --- a/tests/unit/test_function_outputs.py +++ b/tests/unit/test_function_outputs.py @@ -77,11 +77,11 @@ MaterializedImageOutputWriter, MemoryOutputWriter, OpenHCSMetadataWriter, - ProducedMemoryPathsAuthority, RuntimeArtifactMaterializationAuthority, MaterializedImageMetadataTarget, RuntimeArtifactMetadataTarget, StreamOutputsAuthority, + StreamOutputBatch, finalize_function_step_outputs, ) from openhcs.core.streaming_config_declarations import ViewerType @@ -94,6 +94,7 @@ MetadataWriteError, VirtualWorkspaceSourceProjectionEntries, ) +from openhcs.microscopes.imagexpress import ImageXpressFilenameParser from openhcs.microscopes.microscope_interfaces import MetadataHandler from openhcs.microscopes.openhcs import OpenHCSMetadataHandler from openhcs.microscopes.source_schema import SourceSchemaFilenameParser @@ -1378,7 +1379,7 @@ def test_image_persistence_skips_object_label_main_flow_payloads(): ), ) - assert ProducedMemoryPathsAuthority.paths(context, plan) == [] + assert step_output_manifest(context).image_records_for(plan) == () def test_metadata_target_family_discovers_new_declaration_without_consumer_edits( @@ -2521,3 +2522,104 @@ def test_actual_array_exports_only_publish_declared_raster_inventory( raster.image_metadata.source_voxel_spacing.native_coordinate_unit == "micrometer" ) + + +def _stream_batch_sources(projections): + plan = function_step_plan("StreamOwned") + arrays = [np.full((2, 3), index, dtype=np.float32) for index in range(len(projections))] + paths = [str(plan.output_dir / f"A01_s1_w{index}.tif") for index in range(len(projections))] + records = tuple( + ProducedOutputSemantics.from_output( + plan, + path, + FunctionOutputIdentity( + component_values={"well": "A01", "site": "1", "channel": str(index)}, + extension=".tif", + source="stream source", + ), + output_context=AlignedImageSliceContext.main_flow( + output_key=f"image-{index}", projection_key=projection, + artifact_kind=ImageArtifactType.value, + ), + ) + for index, (path, projection) in enumerate(zip(paths, projections, strict=True)) + ) + return arrays, paths, records + + +def test_stream_batch_owns_correlated_projection_order_and_frozen_input_lists(monkeypatch): + arrays, paths, records = _stream_batch_sources(("second", "first", "second", "first")) + expected_arrays, expected_paths = tuple(arrays), tuple(paths) + observed = [] + original_project = StreamOutputBatch.project_item + + def project(request): + observed.append(request.source_description) + arrays.clear() + paths.clear() + return original_project(request) + + monkeypatch.setattr(StreamOutputBatch, "project_item", staticmethod(project)) + batches = StreamOutputBatch.from_projection_groups( + parser=ImageXpressFilenameParser(), payloads=arrays, paths=paths, + produced_outputs=records, + ) + assert observed == [expected_paths[index] for index in (0, 2, 1, 3)] + assert len(batches) == 2 + assert [item.output_path for batch in batches for item in batch.items] == observed + assert [item.producer_identity for batch in batches for item in batch.items] == [ + records[index].producer_identity for index in (0, 2, 1, 3) + ] + for item, index in zip( + (item for batch in batches for item in batch.items), (0, 2, 1, 3), strict=True, + ): + assert item.data is expected_arrays[index] + assert item.source_component_metadata["channel"] == str(index) + + +def test_stream_batch_keeps_first_projection_error_and_prior_callback_effects(monkeypatch): + arrays, paths, records = _stream_batch_sources(("first", "second", "first", "second")) + observed = [] + failure = ValueError("actual projector failure") + original_project = StreamOutputBatch.project_item + + def project(request): + observed.append(request.source_description) + if request.source_description == paths[1]: + raise failure + return original_project(request) + + monkeypatch.setattr(StreamOutputBatch, "project_item", staticmethod(project)) + with pytest.raises(ValueError) as raised: + StreamOutputBatch.from_projection_groups( + parser=ImageXpressFilenameParser(), payloads=arrays, paths=paths, + produced_outputs=records, + ) + assert raised.value is failure + assert observed == [paths[index] for index in (0, 2, 1)] + + +def test_stream_batch_validates_cardinality_and_routes_before_projecting(monkeypatch): + arrays, paths, records = _stream_batch_sources(("first", "second")) + + def forbidden_project(request): + raise AssertionError("invalid inputs reached projection") + + monkeypatch.setattr(StreamOutputBatch, "project_item", staticmethod(forbidden_project)) + parser = ImageXpressFilenameParser() + with pytest.raises(ValueError, match="payload/path cardinality mismatch"): + StreamOutputBatch.from_projection_groups( + parser=parser, payloads=arrays, paths=paths[:1], produced_outputs=(), + ) + with pytest.raises(ValueError, match="payload/output-record cardinality mismatch"): + StreamOutputBatch.from_projection_groups( + parser=parser, payloads=arrays, paths=paths, produced_outputs=records[:1], + ) + with pytest.raises(ValueError, match="at least one produced output record"): + StreamOutputBatch.from_projection_groups( + parser=parser, payloads=(), paths=(), produced_outputs=(), + ) + with pytest.raises(ValueError, match="cannot mix producer projections"): + StreamOutputBatch.from_projection( + parser=parser, payloads=arrays, paths=paths, produced_outputs=records, + ) diff --git a/tests/unit/test_function_runtime_source_projection.py b/tests/unit/test_function_runtime_source_projection.py index 57109fdcdb..d262923767 100644 --- a/tests/unit/test_function_runtime_source_projection.py +++ b/tests/unit/test_function_runtime_source_projection.py @@ -112,7 +112,9 @@ def _source_manifest(plan, paths_and_components): - producer = SimpleNamespace( + producer = CompiledStepPlan( + step_index=plan.main_input_dependency.source_step_index, + step_type="FunctionStep", step_scope_id=plan.main_input_dependency.source_step_scope_id, step_name="producer", pipeline_position=plan.main_input_dependency.source_step_index, @@ -758,7 +760,9 @@ def test_payload_slices_do_not_infer_alignment_from_source_provenance() -> None: def test_step_output_manifest_scopes_previous_step_inputs(tmp_path: Path) -> None: output_dir = tmp_path / "images" - producer = SimpleNamespace( + producer = CompiledStepPlan( + step_index=1, + step_type="FunctionStep", step_scope_id="enhance", step_name="Enhance", pipeline_position=1, @@ -845,7 +849,9 @@ def test_step_output_manifest_pattern_lookup_returns_producer_memory_paths( tmp_path: Path, ) -> None: output_dir = tmp_path / "images" - producer = SimpleNamespace( + producer = CompiledStepPlan( + step_index=4, + step_type="FunctionStep", step_scope_id="mask_image", step_name="MaskImage", pipeline_position=4, @@ -915,14 +921,18 @@ def test_step_output_manifest_does_not_treat_artifact_inputs_as_main_flow( tmp_path: Path, ) -> None: output_dir = tmp_path / "images" - cells_producer = SimpleNamespace( + cells_producer = CompiledStepPlan( + step_index=6, + step_type="FunctionStep", step_scope_id="identify_cells", step_name="IdentifySecondaryObjects", pipeline_position=6, axis_id="A14", output_dir=output_dir, ) - nuclei_producer = SimpleNamespace( + nuclei_producer = CompiledStepPlan( + step_index=5, + step_type="FunctionStep", step_scope_id="identify_nuclei", step_name="IdentifyPrimaryObjects", pipeline_position=5, @@ -1019,14 +1029,18 @@ def test_step_output_manifest_uses_declared_artifact_producer_scope( tmp_path: Path, ) -> None: output_dir = tmp_path / "images" - requested_producer = SimpleNamespace( + requested_producer = CompiledStepPlan( + step_index=2, + step_type="FunctionStep", step_scope_id="crop_blue", step_name="CropBlue", pipeline_position=2, axis_id="A01", output_dir=output_dir, ) - previous_producer = SimpleNamespace( + previous_producer = CompiledStepPlan( + step_index=3, + step_type="FunctionStep", step_scope_id="crop_red", step_name="CropRed", pipeline_position=3, @@ -1121,7 +1135,9 @@ def test_special_artifact_input_does_not_narrow_step_output_main_flow( tmp_path: Path, ) -> None: output_dir = tmp_path / "images" - producer = SimpleNamespace( + producer = CompiledStepPlan( + step_index=2, + step_type="FunctionStep", step_scope_id="image_set", step_name="ImageSet", pipeline_position=2, @@ -4093,7 +4109,9 @@ def test_step_output_manifest_does_not_filter_main_flow_by_artifact_input( tmp_path: Path, ) -> None: output_dir = tmp_path / "images" - producer = SimpleNamespace( + producer = CompiledStepPlan( + step_index=1, + step_type="FunctionStep", step_scope_id="correct_illumination", step_name="CorrectIlluminationApply", pipeline_position=1, @@ -4180,7 +4198,9 @@ def test_step_output_manifest_filters_declared_main_flow_contract_identity( tmp_path: Path, ) -> None: output_dir = tmp_path / "images" - producer = SimpleNamespace( + producer = CompiledStepPlan( + step_index=2, + step_type="FunctionStep", step_scope_id="align", step_name="Align", pipeline_position=2, @@ -4274,14 +4294,18 @@ def test_step_output_manifest_updates_selected_slot_and_preserves_other_componen tmp_path: Path, ) -> None: output_dir = tmp_path / "images" - producer = SimpleNamespace( + producer = CompiledStepPlan( + step_index=1, + step_type="FunctionStep", step_scope_id="producer", step_name="Producer", pipeline_position=1, axis_id="A01", output_dir=output_dir, ) - update = SimpleNamespace( + update = CompiledStepPlan( + step_index=2, + step_type="FunctionStep", step_scope_id="update", step_name="Update", pipeline_position=2, @@ -4367,14 +4391,18 @@ def test_step_output_manifest_collapsed_domain_replaces_inherited_components( collapsed_input_domain: bool, ) -> None: output_dir = tmp_path / "images" - producer = SimpleNamespace( + producer = CompiledStepPlan( + step_index=1, + step_type="FunctionStep", step_scope_id="producer", step_name="Producer", pipeline_position=1, axis_id="A01", output_dir=output_dir, ) - collapse = SimpleNamespace( + collapse = CompiledStepPlan( + step_index=2, + step_type="FunctionStep", step_scope_id="collapse", step_name="Collapse", pipeline_position=2, @@ -4436,14 +4464,18 @@ def test_step_output_manifest_new_output_address_replaces_inherited_components( tmp_path: Path, ) -> None: output_dir = tmp_path / "images" - producer = SimpleNamespace( + producer = CompiledStepPlan( + step_index=1, + step_type="FunctionStep", step_scope_id="producer", step_name="Producer", pipeline_position=1, axis_id="A01", output_dir=output_dir, ) - replacement = SimpleNamespace( + replacement = CompiledStepPlan( + step_index=2, + step_type="FunctionStep", step_scope_id="replacement", step_name="Replacement", pipeline_position=2, @@ -4518,14 +4550,18 @@ def test_step_output_manifest_grouped_subset_replaces_inherited_components( tmp_path: Path, ) -> None: output_dir = tmp_path / "images" - producer = SimpleNamespace( + producer = CompiledStepPlan( + step_index=1, + step_type="FunctionStep", step_scope_id="producer", step_name="Producer", pipeline_position=1, axis_id="A01", output_dir=output_dir, ) - subset = SimpleNamespace( + subset = CompiledStepPlan( + step_index=2, + step_type="FunctionStep", step_scope_id="subset", step_name="Subset", pipeline_position=2, @@ -4589,7 +4625,9 @@ def test_step_output_manifest_preserves_anonymous_side_effect_main_flow( tmp_path: Path, ) -> None: output_dir = tmp_path / "images" - producer = SimpleNamespace( + producer = CompiledStepPlan( + step_index=2, + step_type="FunctionStep", step_scope_id="identify_primary", step_name="IdentifyPrimaryObjects", pipeline_position=2, @@ -4649,7 +4687,9 @@ def test_step_output_manifest_accepts_source_anchor_for_qualified_output( tmp_path: Path, ) -> None: output_dir = tmp_path / "images" - producer = SimpleNamespace( + producer = CompiledStepPlan( + step_index=4, + step_type="FunctionStep", step_scope_id="correct_illumination_apply", step_name="CorrectIlluminationApply", pipeline_position=4, @@ -5389,7 +5429,9 @@ def save_batch( @pytest.fixture def qualified_producer_manifest(tmp_path): parser = SourceSchemaFilenameParser() - producer = SimpleNamespace( + producer = CompiledStepPlan( + step_index=0, + step_type="FunctionStep", step_scope_id="producer", step_name="Producer", pipeline_position=0, From 22b74a622a4c6144e6ca3c3810a91c42eab0b918 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 14:21:10 -0400 Subject: [PATCH 146/235] Keep publication and output identity on their nominal owners Remove incoming projection encode/decode, repeated source binding, five detached identity facades and the unused parallel progress emitter family. Preserve atomic admission, live input epochs and existing saved-buffer semantics. Related: #134, #384, #435, #433. Coupled execution, recording, publication and materialization checks pass; no new timing gain claimed. --- .../runtime-owner-consolidation-20261003.rst | 63 ++++- .../progress_runtime_projection_system.rst | 4 +- openhcs/core/progress/__init__.py | 17 -- openhcs/core/progress/emitters.py | 167 ------------ openhcs/core/source_projection.py | 19 +- .../function_artifact_materialization.py | 11 +- .../core/steps/function_output_identity.py | 248 +++++++----------- .../core/steps/function_output_manifest.py | 9 +- openhcs/core/steps/function_outputs.py | 43 +-- openhcs/core/steps/function_runtime.py | 9 +- openhcs/core/virtual_workspace_metadata.py | 133 +++++++--- .../cellprofiler/runtime/artifact_binding.py | 17 +- .../runtime/output_record_request.py | 2 +- openhcs/processing/materialization/core.py | 14 +- ...utomatic_image_role_materialization_435.py | 20 +- .../test_cellprofiler_module_execution.py | 2 +- ...t_cellprofiler_output_record_occurrence.py | 77 +++++- ...test_collapsed_persisted_occurrence_435.py | 17 +- .../unit/test_derived_role_publication_435.py | 8 +- tests/unit/test_function_outputs.py | 24 +- ...test_function_runtime_source_projection.py | 156 +++++------ .../test_materialization_source_extension.py | 10 +- tests/unit/test_persisted_image_metadata.py | 161 +++++++++++- 23 files changed, 650 insertions(+), 581 deletions(-) delete mode 100644 openhcs/core/progress/emitters.py diff --git a/docs/refactor/receipts/runtime-owner-consolidation-20261003.rst b/docs/refactor/receipts/runtime-owner-consolidation-20261003.rst index 779060c9d4..641df4c39a 100644 --- a/docs/refactor/receipts/runtime-owner-consolidation-20261003.rst +++ b/docs/refactor/receipts/runtime-owner-consolidation-20261003.rst @@ -123,10 +123,10 @@ nonexistent ``projection_axis.aligned_value``. The merged leaf invokes the existing value owner's ``value_for_aligned_slice`` capability, with exact outer count/index controls. Original ``AttributeError`` evidence is retained. -The final combined 22-file execution, projection, image topology, save, +The combined 22-file execution, projection, image topology, save, publication, artifact, debug, framework, path and CP gate passes all 1,247 tests -in 18.97 seconds. This validates the structural batch together. No new ordinary -pipeline timing has yet been measured for this revision. An isolated main +in 18.97 seconds. This validates the structural batch together. The subsequent +paired measurement below does not establish a material speedup. An isolated main cherry-pick of the loaded-cohort change was rejected because it depends on the earlier executor/output consolidation; that temporary operation was aborted, its conflict evidence retained, and the empty branch removed. The coherent @@ -156,6 +156,52 @@ pass, including unchanged 48-file inventories, metadata reopening, yeast, labels, table passthrough and opaque domains. The original saver reproduces the new unwrapped-path control failure. +Typed publication and input source resolution +-------------------------------------------- + +Produced output projections now stay typed through the atomic publication +transaction. The existing ``VirtualWorkspaceSourceProjectionEntries`` admits +incoming projection/path pairs, merges them with the current durable document, +and supplies serialization at the disk boundary. Producer ``metadata_dict`` +construction and the atomic writer's raw-field merge are deleted. Incoming +projections are not decoded; unreplaced durable records are decoded once. +The shared owner retains duplicate-identity admission, replacement repair, +path order, concurrent-well retention and final reconciliation pruning. +Metadata-disabled transactions retain opaque existing wire annotations and +orphan mapping entries. The internal incoming API intentionally requires typed +entries; persisted JSON remains the external format. + +Input source-context queries now consume the value already bound and projected +by ``RuntimeInputBindingRequest``. They no longer repeat image aliasing, +intensity normalization or object-label binding. Each post-call source read +still resolves the current input, including callback mutations. + +One output identity owner +------------------------- + +``FunctionOutputIdentity`` now owns construction, component and extension +normalization, qualifiers, filenames and paths. ``ProducedOutputSemantics`` +inherits these operations. The five detached identity/path/component/extension/ +qualifier facades are deleted and their production consumers migrated. +Semantic coordinates remain distinct from storage filename coordinates; +the external parser and execution-local caches retain their own roles. + +The unused five-class ``core.progress.emitters`` family and its public +reexports are deleted. Actual emission still uses the required progress queue, +the execution server's emitter, and the GUI's event registry. + +The publication, source-binding, identity and progress changes pass one coupled +22-file gate covering execution, recording, masks, live mutations, materialization, +persisted reopening, projection and progress (971 controls, 10.92s). This is a +behavioral check of the combined source, not evidence of speedup. + +The proposed metadata-only input lane was withdrawn before publication. A real +compile-only inventory admitted seven 3D groups and no Speckles groups; existing +cache hits already avoid assembly. The proposed broader lazy composition was +also rejected: eliminating source snapshots changes in-place callable isolation, +while preserving them retains the copying cost. Neither route is promoted as +an unconsumed capability or a dominant performance fix. + Evidence scope -------------- @@ -173,6 +219,17 @@ Two observations have substantial spread and do not establish a causal gain. This source precedes the output-record specialization. Its remaining execution gap to 2x is 1.624947s for 3D and 0.572183s for Speckles. +The subsequent ordinary paired run at ``74693f589`` measures mean 3D execution +8.841s, total 10.823s and native warm invocation 15.230s: 1.723x execution and +1.407x total. Speckles measures execution 1.415s, total 2.216s and native +1.896s: 1.340x execution and 0.856x total. All eight scientific comparisons +pass. The 0.180s difference from the previous 3D execution mean lies within +the observed spread and is not a causal performance claim. The execution gap +to 2x native remains 1.227s. Local timing and qualification records are retained +under ``owner-consolidation-746-paired-v1`` and its preparation sibling in the +20261003 maintenance evidence root. The typed publication, input binding and +identity changes described above follow this measurement and remain unmeasured. + PR #394 remains draft. Its original whole-branch R0/R1 obligations, installed consumer acceptance, full-catalog parity and fresh full30/scaling figures remain open. Startup and required library/kernel preparation remain outside diff --git a/docs/source/architecture/progress_runtime_projection_system.rst b/docs/source/architecture/progress_runtime_projection_system.rst index 1f3c43b3bb..c0baab81aa 100644 --- a/docs/source/architecture/progress_runtime_projection_system.rst +++ b/docs/source/architecture/progress_runtime_projection_system.rst @@ -23,7 +23,9 @@ OpenHCS-specific modules: - ``openhcs.core.progress.projection`` and ``openhcs.core.progress.runtime_tree`` own the OpenHCS execution and tree projections. -- ``openhcs.core.progress.emitters`` owns orchestrator and step event emission. +- ``openhcs.core.progress`` emits worker events through the configured progress + queue. ``openhcs.runtime.zmq_progress`` constructs execution-server events and + owns the compiler queue adapter. Consumption path: diff --git a/openhcs/core/progress/__init__.py b/openhcs/core/progress/__init__.py index fda37b2df9..5cf0260176 100644 --- a/openhcs/core/progress/__init__.py +++ b/openhcs/core/progress/__init__.py @@ -8,7 +8,6 @@ - ProgressPhase, ProgressStatus: Type-safe enums - emit(): Emit progress (convenience wrapper) - registry(): Get per-process registry - - ProgressEmitter: Emitter base class - ProgressRegistry: Registry singleton Usage Examples: @@ -35,13 +34,6 @@ def on_progress(execution_id, event): print(f"{event.phase.value}: {event.percent}%") registry().add_listener(on_progress) - - Testing (mock emitter): - from openhcs.core.progress import NoopEmitter, ProgressEvent - - emitter = NoopEmitter() - event = ProgressEvent(...) - emitter.emit(event) """ # Public API - Types @@ -70,15 +62,6 @@ def on_progress(execution_id, event): # Public API - Registry from .registry import registry -# Public API - Emitters -from .emitters import ( - ProgressEmitter, - NoopEmitter, - CallbackEmitter, - ZMQProgressEmitter, - LoggingEmitter, -) - # Public API - Exceptions from .exceptions import ( ProgressError, diff --git a/openhcs/core/progress/emitters.py b/openhcs/core/progress/emitters.py deleted file mode 100644 index 66ecfdef4c..0000000000 --- a/openhcs/core/progress/emitters.py +++ /dev/null @@ -1,167 +0,0 @@ -"""Progress emitter interface and implementations.""" - -import logging -from abc import ABC, abstractmethod -from typing import Callable -import json - -from .types import ProgressEvent - -logger = logging.getLogger(__name__) - - -# ============================================================================= -# ProgressEmitter Interface (ABC) -# ============================================================================= - - -class ProgressEmitter(ABC): - """Abstract base class for progress emitters. - - Follows OpenHCS pattern: explicit ABC with single abstract method. - Emitter implementations decide WHERE progress goes (callback, ZMQ, noop). - - Design: - - ABC enforces implementation (no duck typing) - - Single abstract method (minimal surface area) - - Subclasses handle transport details - """ - - @abstractmethod - def emit(self, event: ProgressEvent) -> None: - """Emit a progress event. - - Args: - event: ProgressEvent to emit - - Raises: - ProgressError: If emission fails - """ - pass - - -# ============================================================================= -# NoopEmitter - For Testing/Disabled Progress -# ============================================================================= - - -class NoopEmitter(ProgressEmitter): - """No-op emitter for testing or disabled progress tracking. - - Does nothing on emit() - useful for: - - Unit tests (no side effects) - - Production runs with progress disabled - - Dry-run modes - """ - - def emit(self, event: ProgressEvent) -> None: - """Do nothing (intentionally no-op).""" - pass - - -# ============================================================================= -# CallbackEmitter - For In-Process Use -# ============================================================================= - - -class CallbackEmitter(ProgressEmitter): - """Callback-based emitter for in-process use. - - Useful when: - - No ZMQ required (same process) - - Direct UI updates needed - - Testing with mock callbacks - """ - - def __init__(self, callback: Callable[[ProgressEvent], None]): - """Initialize with callback. - - Args: - callback: Function to call on each emit - """ - self.callback = callback - - def emit(self, event: ProgressEvent) -> None: - """Call callback with event. - - Wraps in try/except to prevent progress failures from - crashing the pipeline (fail-safe). - """ - try: - self.callback(event) - except Exception as e: - logger.exception(f"Progress callback failed: {e}") - # Don't re-raise - progress failure shouldn't stop pipeline - - -# ============================================================================= -# ZMQProgressEmitter - For Cross-Process Communication -# ============================================================================= - - -class ZMQProgressEmitter(ProgressEmitter): - """ZMQ-based emitter for cross-process communication. - - Serializes ProgressEvent to JSON and sends via ZMQ socket. - Used for worker processes to communicate progress to main process. - """ - - def __init__(self, socket): - """Initialize with ZMQ socket. - - Args: - socket: ZMQ socket (REQ/REP pair) or None - """ - self.socket = socket - - def emit(self, event: ProgressEvent) -> None: - """Emit event via ZMQ socket. - - Serializes to JSON dict for ZMQ transport. - Handles closed socket gracefully (no crash). - - Args: - event: ProgressEvent to emit - """ - if self.socket is None: - logger.debug(f"Progress emit (no socket): {event}") - return - - try: - # Serialize to dict (enums → strings for JSON) - message = event.to_dict() - json_str = json.dumps(message) - self.socket.send_string(json_str) - except Exception as e: - logger.exception(f"ZMQ emit failed: {e}") - # Don't re-raise - progress failure shouldn't stop pipeline - - -# ============================================================================= -# LoggingEmitter - For Debugging -# ============================================================================= - - -class LoggingEmitter(ProgressEmitter): - """Logging emitter for debugging. - - Logs every progress event at INFO level. - Useful for development and troubleshooting. - """ - - def __init__(self, prefix: str = "PROGRESS"): - """Initialize with log prefix. - - Args: - prefix: Prefix for log messages (e.g., "PROGRESS") - """ - self.prefix = prefix - - def emit(self, event: ProgressEvent) -> None: - """Log event at INFO level.""" - logger.info( - f"[{self.prefix}] {event.execution_id} | " - f"{event.phase.value} | {event.status.value} | " - f"{event.step_name} | {event.axis_id} | " - f"{event.percent:.1f}% ({event.completed}/{event.total})" - ) diff --git a/openhcs/core/source_projection.py b/openhcs/core/source_projection.py index d6c270b616..382bfe43a0 100644 --- a/openhcs/core/source_projection.py +++ b/openhcs/core/source_projection.py @@ -1179,23 +1179,24 @@ def component_metadata( } return metadata + @classmethod def projection_fields( - self, + cls, projection_paths: tuple[tuple[SourceProjection, str], ...], ) -> dict[str, Any]: """Serialize the coherent path-keyed projection fields as one unit.""" return { - self.WORKSPACE_MAPPING_FIELD: { + cls.WORKSPACE_MAPPING_FIELD: { path: projection.ref.to_workspace_mapping() for projection, path in projection_paths }, - self.SOURCE_METADATA_FIELD: { - path: self._source_metadata(projection) + cls.SOURCE_METADATA_FIELD: { + path: cls._source_metadata(projection) for projection, path in projection_paths }, - self.SOURCE_PROJECTION_FIELD: [ - self._source_projection_payload(projection, path) + cls.SOURCE_PROJECTION_FIELD: [ + cls._source_projection_payload(projection, path) for projection, path in projection_paths ], } @@ -1298,8 +1299,9 @@ def _available_backends( projection.ref.backend: True for projection in projection_set.projections } + @classmethod def _source_metadata( - self, + cls, projection: SourceProjection, ) -> dict[str, SourceMetadataValue]: metadata = source_metadata_dict(projection.source_metadata) @@ -1349,8 +1351,9 @@ def _source_metadata( projection.extend_source_metadata(metadata) return metadata + @classmethod def _source_projection_payload( - self, + cls, projection: SourceProjection, path: str, ) -> dict[str, Any]: diff --git a/openhcs/core/steps/function_artifact_materialization.py b/openhcs/core/steps/function_artifact_materialization.py index e6c9770dcc..5f02fe7b64 100644 --- a/openhcs/core/steps/function_artifact_materialization.py +++ b/openhcs/core/steps/function_artifact_materialization.py @@ -54,9 +54,7 @@ from openhcs.core.steps.abstract import StepExecutionObservation from openhcs.core.steps.function_output_identity import ( FunctionOutputIdentity, - FunctionOutputIdentityAuthority, FunctionOutputParserContext, - FunctionOutputPathAuthority, IncompleteFunctionOutputFilenameIdentityError, ) from openhcs.core.steps.stream_component_semantics import ( @@ -799,7 +797,7 @@ def record_source_descriptor( record.value.data ) if use_filename_identity or exact_fixed_scope: - identity = FunctionOutputIdentityAuthority.filename_identity_from_metadata( + identity = FunctionOutputIdentity.from_filename_metadata( parser_context.parser, metadata, ) @@ -810,7 +808,7 @@ def record_source_descriptor( "its declared source metadata has no addressable identity." ) else: - identity = FunctionOutputIdentityAuthority.identity_from_metadata( + identity = FunctionOutputIdentity.from_metadata( parser_context.parser, metadata, fallback_identity_path=record.path, @@ -822,10 +820,7 @@ def record_source_descriptor( ) try: filename = Path( - FunctionOutputPathAuthority.filename_for_identity( - parser_context.parser, - identity, - ) + identity.filename(parser_context.parser) ).name except IncompleteFunctionOutputFilenameIdentityError as exc: if cls.missing_component_is_aggregated( diff --git a/openhcs/core/steps/function_output_identity.py b/openhcs/core/steps/function_output_identity.py index a9775369ec..84e58d7c84 100644 --- a/openhcs/core/steps/function_output_identity.py +++ b/openhcs/core/steps/function_output_identity.py @@ -5,7 +5,7 @@ from dataclasses import dataclass, field, replace from pathlib import Path import re -from typing import TYPE_CHECKING, Mapping, Sequence, TypeAlias +from typing import TYPE_CHECKING, ClassVar, Mapping, Sequence, TypeAlias from openhcs.core.source_path_identity import source_path_identity from openhcs.constants.constants import AllComponents, VariableComponents @@ -163,159 +163,82 @@ def filename_component_metadata(self) -> SourceComponentMetadata: def with_filename_qualifier(self, qualifier: str) -> "FunctionOutputIdentity": """Return this identity qualified by a declared output surface name.""" - return replace(self, filename_qualifier=FilenameQualifier.from_value(qualifier)) + return replace( + self, filename_qualifier=self._normalize_filename_qualifier(qualifier), + ) def without_filename_qualifier(self) -> "FunctionOutputIdentity": """Return this identity without its output-surface filename qualifier.""" return replace(self, filename_qualifier=None) - -class FilenameQualifier: - """Normalize declared output surface names for filename disambiguation.""" - - _unsafe_pattern = re.compile(r"[^A-Za-z0-9_.-]+") + _unsafe_qualifier_pattern: ClassVar[re.Pattern[str]] = re.compile(r"[^A-Za-z0-9_.-]+") @classmethod - def from_value(cls, value: str) -> str: - qualifier = cls._unsafe_pattern.sub("_", str(value).strip()) + def _normalize_filename_qualifier(cls, value: str) -> str: + qualifier = cls._unsafe_qualifier_pattern.sub("_", str(value).strip()) qualifier = qualifier.strip("_.-") if not qualifier: raise ValueError("Filename qualifier cannot be empty.") return qualifier - -@dataclass(frozen=True, slots=True) -class FunctionOutputParserContext: - """Parser-facing microscope context for FunctionStep output finalization.""" - - parser: FilenameParser - microscope_type: str - - @classmethod - def from_processing_context( - cls, - context: "ProcessingContext", - ) -> "FunctionOutputParserContext": - handler = context.microscope_handler - return cls( - parser=handler.parser, - microscope_type=handler.microscope_type, - ) - - @property - def parser_name(self) -> str: - return self.parser.__class__.__name__ - - def parse_path_metadata(self, path: str | Path) -> SourceComponentMetadata | None: - parsed = self.parser.parse_filename(source_path_identity(str(path)).name) - return None if parsed is None else parsed.wire_mapping() - - -class FunctionOutputPathAuthority: - """Construct output filenames from semantic identity through the parser.""" - - @classmethod - def output_path(cls, request: FunctionOutputPathRequest) -> Path: - return cls.output_path_for_identity( - request, - FunctionOutputIdentityAuthority.identity(request), - ) - - @classmethod - def output_filename(cls, request: FunctionOutputPathRequest) -> str: - return cls.output_filename_for_identity( - request, - FunctionOutputIdentityAuthority.identity(request), + def path_for_request(self, request: FunctionOutputPathRequest) -> Path: + """Construct this identity's path in the request's output directory.""" + return Path(request.output_dir) / self.cached_filename( + request.parser, request.identity_cache, ) @classmethod - def output_path_for_identity( - cls, - request: FunctionOutputPathRequest, - identity: FunctionOutputIdentity, - ) -> Path: - return Path(request.output_dir) / cls.output_filename_for_identity( - request, - identity, - ) + def path_from_request(cls, request: FunctionOutputPathRequest) -> Path: + """Resolve one runtime slice identity and construct its output path.""" + return cls.from_request(request).path_for_request(request) - @classmethod - def output_filename_for_identity( - cls, - request: FunctionOutputPathRequest, - identity: FunctionOutputIdentity, - ) -> str: - return cls.cached_filename_for_identity( - request.parser, - identity, - request.identity_cache, - ) - - @classmethod - def cached_filename_for_identity( - cls, + def cached_filename( + self, parser: FilenameParser, - identity: FunctionOutputIdentity, identity_cache: FunctionOutputIdentityCache, ) -> str: - cache_key = cls._filename_cache_key(parser, identity) + cache_key = self._filename_cache_key(parser) cached = identity_cache.filenames_by_identity.get(cache_key) if cached is not None: return cached - filename = cls.filename_for_identity(parser, identity) + filename = self.filename(parser) identity_cache.filenames_by_identity[cache_key] = filename return filename - @classmethod - def filename_for_identity( - cls, - parser: FilenameParser, - identity: FunctionOutputIdentity, - ) -> str: - component_values = dict(identity.filename_values) - extension = identity.extension + def filename(self, parser: FilenameParser) -> str: + """Construct this value's storage filename through the external parser.""" + component_values = dict(self.filename_values) + extension = self.extension try: - bound = parser.bind_component_values( - component_values, - extension=extension, - ) + bound = parser.bind_component_values(component_values, extension=extension) filename = parser.construct_filename(bound) - return cls._qualified_filename( - filename, identity.filename_qualifier, extension=bound.extension + return self._qualified_filename( + filename, self.filename_qualifier, extension=bound.extension, ) except MissingFilenameComponentError as exc: raise IncompleteFunctionOutputFilenameIdentityError( exc.component_name, - cls._construction_error_message( - identity, - component_values, - extension, - ), + self._construction_error_message(component_values, extension), ) from exc except Exception as exc: raise ValueError( - cls._construction_error_message( - identity, - component_values, - extension, - ) + self._construction_error_message(component_values, extension), ) from exc - @staticmethod def _construction_error_message( - identity: FunctionOutputIdentity, + self, component_values: FunctionOutputComponentValues, extension: str | None, ) -> str: return ( "Cannot construct FunctionStep output filename from " - f"{identity.source} identity. Components={component_values!r}, " + f"{self.source} identity. Components={component_values!r}, " f"extension={extension!r}." ) @staticmethod def _qualified_filename( - filename: str, qualifier: str | None, *, extension: str + filename: str, qualifier: str | None, *, extension: str, ) -> str: if qualifier is None: return filename @@ -323,43 +246,34 @@ def _qualified_filename( raise ValueError("Constructed filename does not retain its declared extension.") return f"{filename[:-len(extension)]}_{qualifier}{extension}" - @staticmethod - def _filename_cache_key( - parser: FilenameParser, - identity: FunctionOutputIdentity, - ) -> FunctionOutputFilenameCacheKey: - component_values = identity.filename_values + def _filename_cache_key(self, parser: FilenameParser) -> FunctionOutputFilenameCacheKey: return FunctionOutputFilenameCacheKey( parser_id=id(parser), component_values=tuple( - sorted((str(key), value) for key, value in component_values.items()) + sorted((str(key), value) for key, value in self.filename_values.items()) ), - extension=identity.extension, - filename_qualifier=identity.filename_qualifier, + extension=self.extension, + filename_qualifier=self.filename_qualifier, ) - -class FunctionOutputExtensionAuthority: - """Resolve filename extensions from source metadata or paths.""" - @classmethod - def first(cls, *extensions: str | None) -> str | None: + def _first_extension(cls, *extensions: str | None) -> str | None: for extension in extensions: if extension is not None: return extension return None @classmethod - def from_metadata( + def _extension_from_metadata( cls, metadata: SourceComponentMetadata | None, ) -> str | None: - return cls.from_raw( + return cls._extension_from_raw( SourceImageIdentity(component_metadata=metadata).filename_extension ) @classmethod - def from_path( + def _extension_from_path( cls, path: str | None, *, @@ -368,17 +282,17 @@ def from_path( ) -> str | None: if path is None: return None - identity = FunctionOutputIdentityAuthority._parsed_path_identity_with_cache( + identity = cls._parsed_path_identity_with_cache( parser, path, source="source extension", identity_cache=identity_cache ) if identity is not None: return identity.extension # Physical source paths need not have a canonical plane address. Their # terminal file suffix is not a chain of dotted source identity tokens. - return cls.from_raw(Path(path).suffix) + return cls._extension_from_raw(Path(path).suffix) @classmethod - def from_source( + def _extension_from_source( cls, metadata: SourceComponentMetadata | None, path: str | None, @@ -387,13 +301,13 @@ def from_source( identity_cache: FunctionOutputIdentityCache, ) -> str | None: """Honor retained extension declarations before external path decoding.""" - extension = cls.from_metadata(metadata) + extension = cls._extension_from_metadata(metadata) if extension is not None: return extension - return cls.from_path(path, parser=parser, identity_cache=identity_cache) + return cls._extension_from_path(path, parser=parser, identity_cache=identity_cache) @staticmethod - def from_raw(raw_extension: ParsedFilenameValue) -> str | None: + def _extension_from_raw(raw_extension: ParsedFilenameValue) -> str | None: if raw_extension is None: return None extension = str(raw_extension) @@ -401,12 +315,8 @@ def from_raw(raw_extension: ParsedFilenameValue) -> str | None: return None return extension - -class FunctionOutputComponentIdentityAuthority: - """Normalize parser/source component values for output identity.""" - @classmethod - def from_source_metadata( + def _component_values_from_source_metadata( cls, metadata: SourceComponentMetadata | None, ) -> dict[str, FunctionOutputComponentValue]: @@ -418,7 +328,7 @@ def from_source_metadata( } @classmethod - def from_parsed( + def component_values_from_parsed( cls, parsed: FilenameParseResult, ) -> dict[str, FunctionOutputComponentValue]: @@ -438,12 +348,8 @@ def coerce_component_value( value_text = str(value) return int(value_text) if value_text.isdecimal() else value_text - -class FunctionOutputIdentityAuthority: - """Resolve the single source of output identity for a runtime slice.""" - @classmethod - def identity(cls, request: FunctionOutputPathRequest) -> FunctionOutputIdentity: + def from_request(cls, request: FunctionOutputPathRequest) -> FunctionOutputIdentity: metadata = image_payload_metadata(request.output_payload) payload_identity = cls._identity_from_metadata_with_cache( request.parser, @@ -464,7 +370,7 @@ def identity(cls, request: FunctionOutputPathRequest) -> FunctionOutputIdentity: return cls._input_aligned_identity(request) @classmethod - def identity_from_metadata( + def from_metadata( cls, parser: FilenameParser, metadata: ImagePayloadMetadata, @@ -484,7 +390,7 @@ def identity_from_metadata( ) @classmethod - def identity_from_metadata_with_cache( + def from_metadata_with_cache( cls, parser: FilenameParser, metadata: ImagePayloadMetadata, @@ -585,7 +491,7 @@ def _identity_from_metadata_uncached( identity = cls._identity_from_metadata( metadata.source_component_metadata, - extension=FunctionOutputExtensionAuthority.from_source( + extension=cls._extension_from_source( metadata.source_component_metadata, metadata.source_path, parser=parser, identity_cache=identity_cache, ), @@ -593,7 +499,7 @@ def _identity_from_metadata_uncached( ) if identity is not None: if source_plane_count == 0 and len(represented_source_identities) > 1: - filename_identity = cls.filename_identity_from_metadata( + filename_identity = cls.from_filename_metadata( parser, metadata, fallback_identity_path=fallback_identity_path, @@ -610,7 +516,7 @@ def _identity_from_metadata_uncached( return replace( identity, component_values=semantic_component_values, - extension=FunctionOutputExtensionAuthority.first( + extension=cls._first_extension( identity.extension, filename_identity.extension, ), @@ -640,7 +546,7 @@ def _identity_from_metadata_uncached( source_identity = represented_source_identities[0] identity = cls._identity_from_metadata( source_identity.component_metadata, - extension=FunctionOutputExtensionAuthority.from_source( + extension=cls._extension_from_source( source_identity.component_metadata, source_identity.path, parser=parser, identity_cache=identity_cache, ), @@ -680,7 +586,7 @@ def _identity_from_metadata_uncached( return None @classmethod - def filename_identity_from_metadata( + def from_filename_metadata( cls, parser: FilenameParser, metadata: ImagePayloadMetadata, @@ -699,7 +605,7 @@ def filename_identity_from_metadata( ) identity = cls._identity_from_metadata( metadata.source_component_metadata, - extension=FunctionOutputExtensionAuthority.from_source( + extension=cls._extension_from_source( metadata.source_component_metadata, metadata.source_path, parser=parser, identity_cache=identity_cache, ), @@ -783,7 +689,7 @@ def _complete_identity_from_paths( component_values.update(identity.component_values) return FunctionOutputIdentity( component_values=component_values, - extension=FunctionOutputExtensionAuthority.first( + extension=cls._first_extension( identity.extension, parsed_identity.extension, ), @@ -823,7 +729,7 @@ def _parsed_path_identity( if parsed is None: return None return FunctionOutputIdentity( - component_values=FunctionOutputComponentIdentityAuthority.from_parsed( + component_values=cls.component_values_from_parsed( parsed ), extension=parsed.extension, @@ -899,7 +805,7 @@ def _source_stack_source_identity( ) -> FunctionOutputIdentity: identity = cls._identity_from_metadata( source_identity.component_metadata, - extension=FunctionOutputExtensionAuthority.from_source( + extension=cls._extension_from_source( source_identity.component_metadata, source_identity.path, parser=parser, identity_cache=identity_cache, ), @@ -987,8 +893,9 @@ def _source_stack_semantic_component_keys( extra_keys = tuple(sorted(keys - frozenset(ordered_keys))) return (*ordered_keys, *extra_keys) - @staticmethod + @classmethod def _source_stack_extension( + cls, plane_identities: tuple[FunctionOutputIdentity, ...], identity_component_values: frozenset[str], *, @@ -996,7 +903,7 @@ def _source_stack_extension( parser: FilenameParser, identity_cache: FunctionOutputIdentityCache, ) -> str | None: - fallback_extension = FunctionOutputExtensionAuthority.from_path( + fallback_extension = cls._extension_from_path( fallback_identity_path, parser=parser, identity_cache=identity_cache ) if fallback_extension is not None: @@ -1093,7 +1000,7 @@ def _input_aligned_payload_identity( return replace( identity, component_values=component_values, - extension=FunctionOutputExtensionAuthority.first( + extension=cls._first_extension( identity.extension, parsed_identity.extension, ), @@ -1105,7 +1012,7 @@ def _input_aligned_payload_identity( return replace( identity, component_values=component_values, - extension=FunctionOutputExtensionAuthority.first( + extension=cls._first_extension( identity.extension, parsed_identity.extension, ), @@ -1121,7 +1028,7 @@ def _identity_from_metadata( source: str, ) -> FunctionOutputIdentity | None: component_values = ( - FunctionOutputComponentIdentityAuthority.from_source_metadata(metadata) + cls._component_values_from_source_metadata(metadata) ) if not component_values: return None @@ -1130,3 +1037,30 @@ def _identity_from_metadata( extension=extension, source=source, ) + + +@dataclass(frozen=True, slots=True) +class FunctionOutputParserContext: + """Parser-facing microscope context for FunctionStep output finalization.""" + + parser: FilenameParser + microscope_type: str + + @classmethod + def from_processing_context( + cls, + context: "ProcessingContext", + ) -> "FunctionOutputParserContext": + handler = context.microscope_handler + return cls( + parser=handler.parser, + microscope_type=handler.microscope_type, + ) + + @property + def parser_name(self) -> str: + return self.parser.__class__.__name__ + + def parse_path_metadata(self, path: str | Path) -> SourceComponentMetadata | None: + parsed = self.parser.parse_filename(source_path_identity(str(path)).name) + return None if parsed is None else parsed.wire_mapping() diff --git a/openhcs/core/steps/function_output_manifest.py b/openhcs/core/steps/function_output_manifest.py index 9d0b86bb29..3736c97d42 100644 --- a/openhcs/core/steps/function_output_manifest.py +++ b/openhcs/core/steps/function_output_manifest.py @@ -28,9 +28,7 @@ from openhcs.core.step_dependencies import StepInputDependencyKind from openhcs.core.steps.function_output_identity import ( - FunctionOutputComponentIdentityAuthority, FunctionOutputIdentity, - FunctionOutputPathAuthority, ) from openhcs.microscopes.microscope_interfaces import FilenameParser from openhcs.core.compiled_step_plan import CompiledStepPlan @@ -200,7 +198,7 @@ def from_existing_main_flow_path( extension = parsed.extension if parsed is not None else path.suffix or None identity = FunctionOutputIdentity( component_values=( - FunctionOutputComponentIdentityAuthority.from_parsed(parsed) + FunctionOutputIdentity.component_values_from_parsed(parsed) if parsed is not None else {} ), @@ -636,10 +634,7 @@ def from_records( tokens.add(source_path_identity(value).name) if record.filename_qualifier is not None: tokens.add( - FunctionOutputPathAuthority.filename_for_identity( - parser, - record.without_filename_qualifier(), - ) + record.without_filename_qualifier().filename(parser) ) return cls(frozenset(tokens)) diff --git a/openhcs/core/steps/function_outputs.py b/openhcs/core/steps/function_outputs.py index 5dc73d4cfe..54cff5625d 100644 --- a/openhcs/core/steps/function_outputs.py +++ b/openhcs/core/steps/function_outputs.py @@ -55,7 +55,6 @@ SourcePlaneProjection, SourceProjection, SourceProjectionMetadataSerializer, - SourceProjectionSet, ) from openhcs.core.steps.abstract import StepExecutionObservation from openhcs.core.steps.function_artifact_materialization import ( @@ -70,9 +69,8 @@ zarr_output_batch_layout, ) from openhcs.core.steps.function_output_identity import ( - FunctionOutputIdentityAuthority, + FunctionOutputIdentity, FunctionOutputParserContext, - FunctionOutputPathAuthority, ) from openhcs.core.steps.function_output_manifest import ( ProducedOutputSemantics, @@ -86,6 +84,7 @@ from openhcs.core.virtual_workspace_metadata import ( METADATA_CONFIG, AtomicMetadataWriter, + VirtualWorkspaceSourceProjectionEntries, ) from openhcs.microscopes.microscope_interfaces import FilenameParser @@ -495,7 +494,7 @@ def stream_path_for_projected_item( """Return the stream-visible path for one projected payload item.""" if projected_item_count <= 1: return produced_path - identity = FunctionOutputIdentityAuthority.identity_from_metadata( + identity = FunctionOutputIdentity.from_metadata( parser, projected_item.metadata, fallback_identity_path=produced_path, @@ -506,10 +505,7 @@ def stream_path_for_projected_item( identity = identity.with_filename_qualifier( produced_output.filename_qualifier ) - filename = FunctionOutputPathAuthority.filename_for_identity( - parser, - identity, - ) + filename = identity.filename(parser) return str(Path(produced_path).parent / filename) @@ -738,7 +734,7 @@ def write( raise ValueError( "Produced metadata requires declared component labels." ) - structured_metadata = self.produced_projection_metadata( + projection_entries = self.produced_projection_entries( context, produced_plan ) parser_context = FunctionOutputParserContext.from_processing_context( @@ -753,7 +749,7 @@ def write( AtomicMetadataWriter().publish_source_projection_metadata( METADATA_CONFIG.metadata_path(self.plate_root), self.sub_dir, - structured_metadata, + projection_entries, serializer=SourceProjectionMetadataSerializer(parser_context.parser), saved_image_paths=saved_image_paths, microscope_handler_name=parser_context.microscope_type, @@ -767,11 +763,11 @@ def write( ), ) - def produced_projection_metadata( + def produced_projection_entries( self, context: ProcessingContext, plan: CompiledStepPlan | None, - ) -> Mapping[str, object] | None: + ) -> VirtualWorkspaceSourceProjectionEntries | None: """Project the current saved plan while its typed memory outputs remain.""" if plan is None: return None # Plate reconciliation must not reload cleaned step memory. @@ -799,9 +795,6 @@ def produced_projection_metadata( projection_paths = [] produced_projections = {} declared_addresses: set[OpenHCSPlaneAddress] = set() - parser_context = FunctionOutputParserContext.from_processing_context( - context - ) for record, payload in zip(records, payloads, strict=True): destination = record.path_under(self.output_dir) virtual_path = str(Path(destination).relative_to(self.plate_root)) @@ -855,18 +848,8 @@ def produced_projection_metadata( ) if not projection_paths: return None - projection_set = SourceProjectionSet( - tuple(projection for projection, _path in projection_paths) - ) - return SourceProjectionMetadataSerializer( - parser_context.parser - ).metadata_dict( - projection_set, - microscope_handler_name=parser_context.microscope_type, - source_filename_parser_name=parser_context.parser_name, - grid_dimensions=[], - pixel_size=1.0, - projection_paths=tuple(projection_paths), + return VirtualWorkspaceSourceProjectionEntries.from_projection_paths( + projection_paths ) def project_runtime_artifacts( @@ -994,13 +977,13 @@ def write( context, plan, artifact_materializations=artifact_materializations ): if not plan.create_openhcs_metadata: - structured_metadata = target.produced_projection_metadata(context, plan) - if structured_metadata is None: + projection_entries = target.produced_projection_entries(context, plan) + if projection_entries is None: continue AtomicMetadataWriter().merge_source_projection_metadata( METADATA_CONFIG.metadata_path(target.plate_root), target.sub_dir, - structured_metadata, + projection_entries, ) else: target.write(context, produced_plan=plan) diff --git a/openhcs/core/steps/function_runtime.py b/openhcs/core/steps/function_runtime.py index 0b28125213..7fb7c13ae1 100644 --- a/openhcs/core/steps/function_runtime.py +++ b/openhcs/core/steps/function_runtime.py @@ -177,8 +177,6 @@ ) from openhcs.core.steps.function_output_identity import ( FunctionOutputIdentity, - FunctionOutputIdentityAuthority, - FunctionOutputPathAuthority, FunctionOutputPathRequest, ) from openhcs.core.compiled_step_plan import CompiledStepPlan @@ -3172,7 +3170,7 @@ def plane_axis_for_output( identity_cache=context.runtime_function_output_identity_cache, ) try: - output_identity = FunctionOutputIdentityAuthority.identity( + output_identity = FunctionOutputIdentity.from_request( output_path_request ) except ValueError as exc: @@ -3195,10 +3193,7 @@ def plane_axis_for_output( output_identity = output_identity.with_filename_qualifier( output_context.output_key ) - output_path = FunctionOutputPathAuthority.output_path_for_identity( - output_path_request, - output_identity, - ) + output_path = output_identity.path_for_request(output_path_request) output_path_text = str(output_path) output_path_entries.append( OutputPathBatchEntry( diff --git a/openhcs/core/virtual_workspace_metadata.py b/openhcs/core/virtual_workspace_metadata.py index fc0c25cee3..89742142e1 100644 --- a/openhcs/core/virtual_workspace_metadata.py +++ b/openhcs/core/virtual_workspace_metadata.py @@ -134,7 +134,7 @@ def merge_source_projection_metadata( self, metadata_path: str | Path, subdirectory_name: str, - projection_metadata: Mapping[str, Any] | None = None, + projection_entries: VirtualWorkspaceSourceProjectionEntries, ) -> None: """Merge exact produced paths under one lock, preserving other wells.""" @@ -143,46 +143,20 @@ def update(data): subdirectory = data[METADATA_CONFIG.SUBDIRECTORIES_KEY].setdefault( subdirectory_name, {} ) - self._merge_source_projection_fields(subdirectory, projection_metadata) + entries = projection_entries.merge_into_subdirectory(subdirectory) self._update_projection_geometry( subdirectory, - VirtualWorkspaceSourceProjectionEntries.from_subdirectory( - subdirectory - ).entries.values(), + entries.entries.values(), ) return data self._execute_update(metadata_path, update) - @staticmethod - def _merge_source_projection_fields( - subdirectory: dict[str, Any], - projection_metadata: Mapping[str, Any] | None, - ) -> None: - """Merge the one durable projection store; shared by both transactions.""" - for key in (FIELDS.WORKSPACE_MAPPING, FIELDS.SOURCE_METADATA): - subdirectory[key] = { - **subdirectory.get(key, {}), - **({} if projection_metadata is None else projection_metadata[key]), - } - entries = { - record["virtual_path"]: record - for record in subdirectory.get(FIELDS.SOURCE_PROJECTION, []) - } - if projection_metadata is not None: - entries.update( - { - record["virtual_path"]: record - for record in projection_metadata[FIELDS.SOURCE_PROJECTION] - } - ) - subdirectory[FIELDS.SOURCE_PROJECTION] = list(entries.values()) - def publish_source_projection_metadata( self, metadata_path: str | Path, subdirectory_name: str, - projection_metadata: Mapping[str, Any] | None, + projection_entries: VirtualWorkspaceSourceProjectionEntries | None, *, serializer: SourceProjectionMetadataSerializer, saved_image_paths: Sequence[str], @@ -207,12 +181,12 @@ def update(data): subdirectory = data[METADATA_CONFIG.SUBDIRECTORIES_KEY].setdefault( subdirectory_name, {} ) - self._merge_source_projection_fields(subdirectory, projection_metadata) - entries = VirtualWorkspaceSourceProjectionEntries.from_subdirectory( - subdirectory - ).entries + entries = ( + VirtualWorkspaceSourceProjectionEntries(MappingProxyType({})) + if projection_entries is None else projection_entries + ).merged_with_subdirectory(subdirectory).entries missing = saved_set.difference(entries) - if missing and projection_metadata is None: + if missing and projection_entries is None: raise MetadataWriteError( f"Saved images lack typed produced addresses: {sorted(missing)!r}." ) @@ -226,7 +200,7 @@ def update(data): (projection, path) for path, projection in entries.items() if ( - projection_metadata is not None + projection_entries is not None or path in saved_set or Path(path).parent != Path(subdirectory_name) ) @@ -454,6 +428,93 @@ class VirtualWorkspaceSourceProjectionEntries: entries: Mapping[str, SourceProjection] + @property + def projection_paths(self) -> tuple[tuple[SourceProjection, str], ...]: + """Expose serialization order directly from the admitted path owner.""" + return tuple((projection, path) for path, projection in self.entries.items()) + + @classmethod + def from_projection_paths( + cls, + projection_paths: Sequence[tuple[SourceProjection, str]], + ) -> "VirtualWorkspaceSourceProjectionEntries": + """Admit a producer's typed updates in persisted path order.""" + SourceProjectionSet( + tuple(projection for projection, _path in projection_paths) + ) + return cls( + MappingProxyType( + {path: projection for projection, path in projection_paths} + ) + ) + + @staticmethod + def _records_by_path( + subdirectory: OpenHCSSubdirectoryPayload, + ) -> dict[str, JsonValue]: + for key in (FIELDS.WORKSPACE_MAPPING, FIELDS.SOURCE_METADATA): + if not isinstance(subdirectory.get(key, {}), Mapping): + raise TypeError(f"virtual_workspace {key} must be a mapping.") + # Transactions have always replaced duplicate paths before admitting + # records. In particular, a new producer can repair an invalid old record. + return { + record["virtual_path"]: record + for record in subdirectory.get(FIELDS.SOURCE_PROJECTION, []) + } + + def merged_with_subdirectory( + self, + subdirectory: OpenHCSSubdirectoryPayload, + ) -> "VirtualWorkspaceSourceProjectionEntries": + """Admit current durable records, retaining the actual typed replacements.""" + records = self._records_by_path(subdirectory) + return self._admit_retained_records(records) + + def _admit_retained_records( + self, + records: Mapping[str, JsonValue], + ) -> "VirtualWorkspaceSourceProjectionEntries": + entries: dict[str, SourceProjection] = {} + for path, record in records.items(): + virtual_path, projection = ( + (path, self.entries[path]) + if path in self.entries else self._projection_record(record) + ) + if virtual_path in entries: + raise RuntimeError( + "virtual_workspace source_projection contains duplicate path " + f"{virtual_path!r}." + ) + entries[virtual_path] = projection + for path, projection in self.entries.items(): + if path not in records: + if path in entries: + raise RuntimeError( + "virtual_workspace source_projection contains duplicate path " + f"{path!r}." + ) + entries[path] = projection + return type(self)(MappingProxyType(entries)) + + def merge_into_subdirectory( + self, + subdirectory: dict[str, Any], + ) -> "VirtualWorkspaceSourceProjectionEntries": + """Merge producer fields without normalizing opaque retained wire fields.""" + records = self._records_by_path(subdirectory) + admitted = self._admit_retained_records(records) + fields = SourceProjectionMetadataSerializer.projection_fields( + self.projection_paths + ) + for key in (FIELDS.WORKSPACE_MAPPING, FIELDS.SOURCE_METADATA): + subdirectory[key] = {**subdirectory.get(key, {}), **fields[key]} + records.update( + (record["virtual_path"], record) + for record in fields[FIELDS.SOURCE_PROJECTION] + ) + subdirectory[FIELDS.SOURCE_PROJECTION] = list(records.values()) + return admitted + @classmethod def from_subdirectory( cls, diff --git a/openhcs/interop/cellprofiler/runtime/artifact_binding.py b/openhcs/interop/cellprofiler/runtime/artifact_binding.py index 2408ce01ee..bb346b64bc 100644 --- a/openhcs/interop/cellprofiler/runtime/artifact_binding.py +++ b/openhcs/interop/cellprofiler/runtime/artifact_binding.py @@ -167,12 +167,17 @@ def source_image_name_from_value( del value return None - def source_image_payload( + def source_image_payload_from_runtime_value( self, spec: ArtifactSpec, value: RuntimeCallableArgument, ) -> RuntimeCallableArgument | None: - """Return an image payload that carries this artifact's source paths.""" + """Read source context from an already bound, invocation-scoped value. + + Input naming, intensity conversion and plane selection belong to + ``RuntimeInputBindingRequest.runtime_value``. A source-context query + must consume that result without binding the input a second time. + """ return None def published_main_flow_output( @@ -252,12 +257,12 @@ def source_image_name_from_value( image_payload_metadata(value).source_provenance.represented_source_image_names ) - def source_image_payload( + def source_image_payload_from_runtime_value( self, spec: ArtifactSpec, value: RuntimeCallableArgument, ) -> RuntimeCallableArgument | None: - return self.raw_runtime_input_value(spec, value) + return value def published_main_flow_output( self, @@ -338,12 +343,12 @@ def source_image_name_from_value( ) -> str | None: return cast(ObjectLabelSet, value).source_image_name - def source_image_payload( + def source_image_payload_from_runtime_value( self, spec: ArtifactSpec, value: RuntimeCallableArgument, ) -> RuntimeCallableArgument | None: - return self.object_labels(spec, value) + return value class MeasurementsArtifactTypeStrategy(RuntimeArtifactTypeStrategy): diff --git a/openhcs/interop/cellprofiler/runtime/output_record_request.py b/openhcs/interop/cellprofiler/runtime/output_record_request.py index 2f20f721fb..e767f22ea6 100644 --- a/openhcs/interop/cellprofiler/runtime/output_record_request.py +++ b/openhcs/interop/cellprofiler/runtime/output_record_request.py @@ -223,7 +223,7 @@ def artifact_source_payload( RuntimeInputBindingRequest.__post_init__(self) payload = RuntimeArtifactTypeStrategy.for_artifact_type( spec.artifact_type - ).source_image_payload( + ).source_image_payload_from_runtime_value( spec, self.runtime_value( edge, diff --git a/openhcs/processing/materialization/core.py b/openhcs/processing/materialization/core.py index 7a6f9aee00..a24f5b86b0 100644 --- a/openhcs/processing/materialization/core.py +++ b/openhcs/processing/materialization/core.py @@ -76,8 +76,7 @@ from openhcs.core.source_metadata import SourceVoxelSpacing from openhcs.core.source_spatial_domain import SourceSpatialDomain from openhcs.core.steps.function_output_identity import ( - FunctionOutputIdentityAuthority, - FunctionOutputPathAuthority, + FunctionOutputIdentity, ) from openhcs.core.steps.stream_component_semantics import ( StreamImagePayloadMetadataProjector, @@ -1626,7 +1625,7 @@ def projected_outputs(self, outputs: Sequence[Output]) -> tuple[Output, ...]: continue output_path = Path(output.path) for item in items: - identity = FunctionOutputIdentityAuthority.identity_from_metadata( + identity = FunctionOutputIdentity.from_metadata( parser, item.metadata, fallback_identity_path=output.path, @@ -1634,10 +1633,7 @@ def projected_outputs(self, outputs: Sequence[Output]) -> tuple[Output, ...]: if identity is None: raise ValueError("Projected viewer output requires a filename identity.") identity = replace(identity, extension=output_path.suffix) - filename = FunctionOutputPathAuthority.filename_for_identity( - parser, - identity.with_filename_qualifier(output_path.stem), - ) + filename = identity.with_filename_qualifier(output_path.stem).filename(parser) result.append( replace( output, @@ -1793,13 +1789,13 @@ def named_source_filename( if qualifier is None: return None parser = SourceStemAuthoritySelection.from_processing_context(self.context).required_parser() - identity = FunctionOutputIdentityAuthority.filename_identity_from_metadata(parser, metadata) + identity = FunctionOutputIdentity.from_filename_metadata(parser, metadata) if identity is None: raise ValueError("Retained image output has no addressable source filename identity.") identity = self.materialization_spec.filename_identity_for_output( replace(identity, extension=extension), self.output_plan, ) - return FunctionOutputPathAuthority.filename_for_identity(parser, identity) + return identity.filename(parser) def paths(self, options: FileOutputOptions) -> PathHelper: return PathHelper(self.base_path, options) diff --git a/tests/unit/test_automatic_image_role_materialization_435.py b/tests/unit/test_automatic_image_role_materialization_435.py index 58f6253db9..8e501c928e 100644 --- a/tests/unit/test_automatic_image_role_materialization_435.py +++ b/tests/unit/test_automatic_image_role_materialization_435.py @@ -21,9 +21,10 @@ from openhcs.core.runtime_image_values import ImageMetadataPayload, ImagePayloadMetadata, ImagePayloadMetadataCompositionMode from openhcs.core.runtime_stores import RuntimeValueStore from openhcs.core.source_metadata import SourceVoxelSpacing +from openhcs.core.source_projection import SourceProjectionMetadataSerializer from openhcs.core.source_workspace_projection import VirtualWorkspaceSourceProjection from openhcs.core.steps.function_output_identity import ( - FunctionOutputIdentityAuthority, FunctionOutputPathAuthority, + FunctionOutputIdentity, ) from openhcs.core.steps.function_outputs import ( PrimaryImageMetadataTarget, RuntimeArtifactMaterializationAuthority, @@ -97,12 +98,10 @@ def test_automatic_two_role_images_publish_their_actual_saved_occurrences(tmp_pa ) expected[output.name] = [] for payload in payloads: - identity = FunctionOutputIdentityAuthority.filename_identity_from_metadata( + identity = FunctionOutputIdentity.from_filename_metadata( context.microscope_handler.parser, payload.metadata, ).with_filename_qualifier(output_context.output_key) - destination = plan.output_dir / FunctionOutputPathAuthority.filename_for_identity( - context.microscope_handler.parser, identity, - ) + destination = plan.output_dir / identity.filename(context.microscope_handler.parser) filemanager.ensure_directory(plan.output_dir, Backend.MEMORY.value) filemanager.ensure_directory(plan.output_dir, Backend.DISK.value) filemanager.save(payload, str(destination), Backend.MEMORY.value) @@ -116,7 +115,10 @@ def test_automatic_two_role_images_publish_their_actual_saved_occurrences(tmp_pa target = replace(PrimaryImageMetadataTarget.from_plan(plan), artifact_materializations=saved) # The original implementation raises its real duplicate projection guard # here: both automatic materializations used the same unqualified path. - structured = target.produced_projection_metadata(context, plan) + structured = target.produced_projection_entries(context, plan) + structured = SourceProjectionMetadataSerializer.projection_fields( + structured.projection_paths + ) assert {item["source_alias"] for item in structured["source_projection"]} == set(expected) assert len(structured["source_projection"]) == 2 * plane_count for materialized in saved: @@ -253,12 +255,10 @@ def test_retained_image_qualifier_respects_complete_writer_suffix(tmp_path, suff ).outputs(plan, context) assert len(outputs) == plane_count for index, (output, payload) in enumerate(zip(outputs, payloads, strict=True), 1): - identity = FunctionOutputIdentityAuthority.filename_identity_from_metadata( + identity = FunctionOutputIdentity.from_filename_metadata( context.microscope_handler.parser, payload.metadata, ).with_filename_qualifier(output_plan.name) - expected = FunctionOutputPathAuthority.filename_for_identity( - context.microscope_handler.parser, replace(identity, extension=suffix), - ) + expected = replace(identity, extension=suffix).filename(context.microscope_handler.parser) assert Path(output.path).name == expected assert Path(output.path).name.endswith("_Role_Name" + suffix) assert output.metadata.source_component_metadata["channel"] == "2" diff --git a/tests/unit/test_cellprofiler_module_execution.py b/tests/unit/test_cellprofiler_module_execution.py index d8c5517e07..fdbb125ae7 100644 --- a/tests/unit/test_cellprofiler_module_execution.py +++ b/tests/unit/test_cellprofiler_module_execution.py @@ -1874,7 +1874,7 @@ def test_object_artifact_source_payload_uses_native_object_provenance() -> None: ) payload = RuntimeArtifactTypeStrategy.for_artifact_type( ObjectLabelsArtifactType - ).source_image_payload(object_spec, objects) + ).source_image_payload_from_runtime_value(object_spec, objects) assert payload is objects assert image_payload_metadata(payload).source_image_provenance_planes.paths == ( diff --git a/tests/unit/test_cellprofiler_output_record_occurrence.py b/tests/unit/test_cellprofiler_output_record_occurrence.py index 0e66b8cb3f..36bf1a39ea 100644 --- a/tests/unit/test_cellprofiler_output_record_occurrence.py +++ b/tests/unit/test_cellprofiler_output_record_occurrence.py @@ -19,8 +19,17 @@ MainFlowInputProjection, ) from openhcs.core.runtime_adapters import RuntimeAdapterRequest -from openhcs.core.runtime_image_values import image_payload_data, image_payload_metadata -from openhcs.interop.cellprofiler.runtime.artifact_binding import RuntimeInputBindingRequest +from openhcs.core.runtime_image_values import ( + ImagePayloadMetadata, + image_payload_data, + image_payload_mask, + image_payload_metadata, +) +from openhcs.core.source_image_provenance import SourceImageProvenancePlanes +from openhcs.interop.cellprofiler.runtime.artifact_binding import ( + ImageArtifactTypeStrategy, + RuntimeInputBindingRequest, +) from openhcs.interop.cellprofiler.runtime.output_record_request import ( CellProfilerOutputRecordRequest, ) @@ -319,6 +328,70 @@ def test_record_binding_reads_current_pixels_and_mutable_kwargs_without_holder_a assert image_payload_metadata(source).source_image_names == (spec.name,) +def test_record_source_binds_current_image_once_and_preserves_intensity_mask_lineage( + monkeypatch, +) -> None: + request, spec = _image_record_request() + pixels = np.full((3, 4), 65535, dtype=np.uint16) + mask = np.ones((3, 4), dtype=bool) + payload = ImagePayloadMetadata( + source_image_names=("Acquired",), + source_image_provenance_planes=SourceImageProvenancePlanes.from_components( + paths=("/input/actual.tif",), + ), + ).payload_with(pixels, mask) + request = replace(request, current_image=payload) + original_bind = ImageArtifactTypeStrategy.raw_runtime_input_value + binding_inputs = [] + + def observe_bind(strategy, input_spec, value): + binding_inputs.append(value) + return original_bind(strategy, input_spec, value) + + monkeypatch.setattr( + ImageArtifactTypeStrategy, "raw_runtime_input_value", observe_bind + ) + for pixel_value in (65535, 32768): + pixels[:] = pixel_value + binding_inputs.clear() + source = request.artifact_source_payload(request.active_input_edges[0]) + assert binding_inputs == [payload] + assert image_payload_data(source).dtype == np.float32 + np.testing.assert_allclose( + image_payload_data(source), np.float32(pixel_value) / 65535, + ) + assert image_payload_mask(source) is mask + metadata = image_payload_metadata(source) + assert metadata.source_image_names == (spec.name,) + assert metadata.source_image_provenance_planes.paths == ("/input/actual.tif",) + assert set(metadata.source_provenance.represented_source_image_names) == { + "Acquired", spec.name, + } + + +def test_record_source_returns_the_actual_bound_value_without_rebinding(monkeypatch) -> None: + request, _spec = _image_record_request() + bound = ImagePayloadMetadata( + source_image_names=("Original",), + ).payload_with(np.ones((3, 4), dtype=np.float32)) + reads = [] + + def runtime_value(binding, edge, parameter_name=None): + reads.append((edge, parameter_name)) + return bound + + def reject_rebinding(*_args, **_kwargs): + raise AssertionError("Source context rebound an already bound runtime image") + + monkeypatch.setattr(RuntimeInputBindingRequest, "runtime_value", runtime_value) + monkeypatch.setattr( + ImageArtifactTypeStrategy, "raw_runtime_input_value", reject_rebinding + ) + edge = request.active_input_edges[0] + assert request.artifact_source_payload(edge) is bound + assert reads == [(edge, edge.spec.parameter_name)] + + def test_record_endpoint_identity_does_not_change_reference_broadcast_selection() -> None: request, spec = _image_record_request() equivalent = replace(spec) diff --git a/tests/unit/test_collapsed_persisted_occurrence_435.py b/tests/unit/test_collapsed_persisted_occurrence_435.py index 564af6fce3..11a52555cf 100644 --- a/tests/unit/test_collapsed_persisted_occurrence_435.py +++ b/tests/unit/test_collapsed_persisted_occurrence_435.py @@ -161,7 +161,10 @@ def _actual_saved_occurrence(tmp_path, scenario): ) target = PrimaryImageMetadataTarget.from_plan(plan) # This is the existing accepted main-flow publication, before a mirror is added. - original = target.produced_projection_metadata(context, plan) + original = target.produced_projection_entries(context, plan) + original = SourceProjectionMetadataSerializer.projection_fields( + original.projection_paths + ) assert len(original["source_projection"]) == 1 if scenario == "collapsed_site": row = original["source_projection"][0] @@ -215,7 +218,7 @@ def test_real_same_path_join_observes_original_rejected_pair(tmp_path, scenario) with pytest.raises( ValueError, match="Conflicting metadata for persisted image occurrence" ) as failure: - target.produced_projection_metadata(context, plan) + target.produced_projection_entries(context, plan) pair = _rejected_original_pair(failure.value) projection = pair["projection"] metadata = pair["metadata"] @@ -261,7 +264,10 @@ def test_real_scalar_same_path_mirror_remains_admissible(tmp_path): context, plan, target, _output_plan, _pixels = _actual_saved_occurrence( tmp_path, "scalar" ) - structured = target.produced_projection_metadata(context, plan) + structured = target.produced_projection_entries(context, plan) + structured = SourceProjectionMetadataSerializer.projection_fields( + structured.projection_paths + ) assert len(structured["source_projection"]) == 1 assert structured["source_projection"][0]["source_alias"] == "Mosaic" @@ -294,7 +300,10 @@ def test_collapsed_same_path_mirror_retains_filename_and_semantic_views(tmp_path context, plan, target, _output_plan, pixels = _actual_saved_occurrence( tmp_path, "collapsed_site" ) - structured = target.produced_projection_metadata(context, plan) + structured = target.produced_projection_entries(context, plan) + structured = SourceProjectionMetadataSerializer.projection_fields( + structured.projection_paths + ) (row,) = structured["source_projection"] assert row["address"]["site"] == "1" assert row["source_alias"] == "Mosaic" diff --git a/tests/unit/test_derived_role_publication_435.py b/tests/unit/test_derived_role_publication_435.py index 4a0155c0f1..ebb0089b34 100644 --- a/tests/unit/test_derived_role_publication_435.py +++ b/tests/unit/test_derived_role_publication_435.py @@ -23,6 +23,7 @@ from openhcs.core.runtime_stores import RuntimeValueStore from openhcs.core.runtime_object_label_building import SourceImageObjectLabelBuildRequest from openhcs.core.source_metadata import SourceVoxelSpacing +from openhcs.core.source_projection import SourceProjectionMetadataSerializer from openhcs.core.source_workspace_projection import VirtualWorkspaceSourceProjection from openhcs.core.steps.function_outputs import ( PrimaryImageMetadataTarget, RuntimeArtifactMaterializationAuthority, @@ -139,11 +140,14 @@ def test_saved_roles_publish_once_per_persisted_occurrence(tmp_path, scenario): if scenario != "same_occurrence": message = "Conflicting metadata for persisted image" if scenario == "conflicting_metadata" else "Duplicate source projection address" with pytest.raises(ValueError, match=message) as rejection: - target.produced_projection_metadata(context, plan) + target.produced_projection_entries(context, plan) receipt["rejection"] = str(rejection.value) (tmp_path / "persisted-occurrence-witness.json").write_text(json.dumps(receipt, indent=2)) return - structured = target.produced_projection_metadata(context, plan) + structured = target.produced_projection_entries(context, plan) + structured = SourceProjectionMetadataSerializer.projection_fields( + structured.projection_paths + ) assert len(structured["source_projection"]) == 2 assert {item["source_alias"] for item in structured["source_projection"]} == {"RawRole", "CappedRole"} target.write(context, produced_plan=plan) diff --git a/tests/unit/test_function_outputs.py b/tests/unit/test_function_outputs.py index 36caefe3a0..3af656fd44 100644 --- a/tests/unit/test_function_outputs.py +++ b/tests/unit/test_function_outputs.py @@ -61,8 +61,6 @@ from openhcs.core.step_dependencies import StepInputDependency from openhcs.core.steps.function_output_identity import ( FunctionOutputIdentity, - FunctionOutputIdentityAuthority, - FunctionOutputPathAuthority, FunctionOutputPathRequest, ) from openhcs.core.steps.function_output_manifest import ( @@ -1408,7 +1406,7 @@ def from_plan(cls, plan): assert OpenHCSMetadataWriter.OutputTarget.for_execution(context, plan) == ( target, ) - assert target.produced_projection_metadata(context, plan) is None + assert target.produced_projection_entries(context, plan) is None finally: for key in set(registry) - original_keys: del registry[key] @@ -1680,7 +1678,10 @@ def test_runtime_image_artifact_projects_persisted_source_binding( assert projection.image_metadata is not None assert projection.image_metadata.source_dtype == "uint8" assert projection.ref == SourcePixelRef(Backend.DISK.value, virtual_path) - structured = target.produced_projection_metadata(context, plan) + structured = target.produced_projection_entries(context, plan) + structured = SourceProjectionMetadataSerializer.projection_fields( + structured.projection_paths + ) assert structured is not None [record] = structured[FIELDS.SOURCE_PROJECTION] assert record["virtual_path"] == virtual_path @@ -1810,7 +1811,10 @@ def test_runtime_multiplane_label_artifact_projects_persisted_source_binding( ] for index in range(2) ) == ("1", "2") - structured = target.produced_projection_metadata(context, plan) + structured = target.produced_projection_entries(context, plan) + structured = SourceProjectionMetadataSerializer.projection_fields( + structured.projection_paths + ) assert structured is not None [record] = structured[FIELDS.SOURCE_PROJECTION] assert record["artifact_kind"] == ObjectLabelsArtifactType.value @@ -1994,13 +1998,13 @@ def test_produced_address_publication_never_parses_generated_filenames( }, source_voxel_spacing=SourceVoxelSpacing((0.5, 0.5)), ) - identity = FunctionOutputIdentityAuthority.identity_from_metadata( + identity = FunctionOutputIdentity.from_metadata( parser, metadata ) assert identity is not None assert identity.extension == extension identity = identity.with_filename_qualifier("centre_dots") - filename = FunctionOutputPathAuthority.filename_for_identity(parser, identity) + filename = identity.filename(parser) assert filename == f"{well}_s001_w1_z{z_index:03d}_t001_centre_dots{extension}" # Readback is an external boundary; publication below must not parse. parsed = parser.parse_filename(filename) @@ -2099,14 +2103,12 @@ def test_produced_stacked_dotted_identity_keeps_declared_extension( input_path=Path(source_paths[0]).name, variable_components=(VariableComponents.Z_INDEX,), ) - identity = FunctionOutputIdentityAuthority.identity(request) + identity = FunctionOutputIdentity.from_request(request) assert identity.extension == extension assert "z_index" not in identity.component_values assert identity.filename_address.value_for(AllComponents.Z_INDEX) == "3" assert identity.filename_address.value_for(AllComponents.WELL) == well - filename = FunctionOutputPathAuthority.filename_for_identity( - parser, identity.with_filename_qualifier("centre_dots") - ) + filename = identity.with_filename_qualifier("centre_dots").filename(parser) assert filename == f"{well}_s001_w1_z003_t001_centre_dots{extension}" diff --git a/tests/unit/test_function_runtime_source_projection.py b/tests/unit/test_function_runtime_source_projection.py index d262923767..cd8481bb90 100644 --- a/tests/unit/test_function_runtime_source_projection.py +++ b/tests/unit/test_function_runtime_source_projection.py @@ -96,9 +96,7 @@ ) from openhcs.core.steps.function_output_identity import ( FunctionOutputIdentity, - FunctionOutputIdentityAuthority, FunctionOutputIdentityCache, - FunctionOutputPathAuthority, FunctionOutputPathRequest, ) from openhcs.core.steps.function_output_manifest import ( @@ -4764,14 +4762,12 @@ def test_function_output_path_uses_payload_identity_over_input_carrier( }, ).payload_with(np.zeros((4, 5), dtype=np.float32), None) - output_path = FunctionOutputPathAuthority.output_path( - FunctionOutputPathRequest( + output_path = FunctionOutputIdentity.path_from_request(FunctionOutputPathRequest( parser=SourceSchemaFilenameParser(), output_dir=tmp_path, output_payload=payload, input_path="A14_s001_w1_z001_t001.tif", - ) - ) + )) assert output_path.name == "A14_s002_w3_z001_t001.tif" @@ -4790,14 +4786,12 @@ def test_function_output_path_uses_payload_identity_without_input_path( }, ).payload_with(np.zeros((4, 5), dtype=np.float32), None) - output_path = FunctionOutputPathAuthority.output_path( - FunctionOutputPathRequest( + output_path = FunctionOutputIdentity.path_from_request(FunctionOutputPathRequest( parser=SourceSchemaFilenameParser(), output_dir=tmp_path, output_payload=payload, input_path=None, - ) - ) + )) assert output_path.name == "A14_s001_w5_z001_t001.tif" @@ -4816,7 +4810,7 @@ def test_function_output_identity_completes_partial_payload_metadata_from_fallba }, ) - identity = FunctionOutputIdentityAuthority.identity_from_metadata( + identity = FunctionOutputIdentity.from_metadata( parser, metadata, fallback_identity_path="Sequence1_s001_w1_z001_t000.tif", @@ -4824,7 +4818,7 @@ def test_function_output_identity_completes_partial_payload_metadata_from_fallba assert identity is not None assert ( - FunctionOutputPathAuthority.filename_for_identity(parser, identity) + identity.filename(parser) == "Sequence1_s001_w1_z001_t000.tif" ) @@ -4843,7 +4837,7 @@ def test_function_output_identity_uses_fallback_path_extension_for_payload_ident }, ) - identity = FunctionOutputIdentityAuthority.identity_from_metadata( + identity = FunctionOutputIdentity.from_metadata( parser, metadata, fallback_identity_path="A01_s001_w1_z001_t001.png", @@ -4851,7 +4845,7 @@ def test_function_output_identity_uses_fallback_path_extension_for_payload_ident assert identity is not None assert ( - FunctionOutputPathAuthority.filename_for_identity(parser, identity) + identity.filename(parser) == "A01_s001_w2_z001_t001.png" ) @@ -4879,14 +4873,12 @@ def test_function_output_path_uses_input_identity_for_multi_plane_carrier( ), ).payload_with(np.zeros((2, 4, 5), dtype=np.float32), None) - output_path = FunctionOutputPathAuthority.output_path( - FunctionOutputPathRequest( + output_path = FunctionOutputIdentity.path_from_request(FunctionOutputPathRequest( parser=SourceSchemaFilenameParser(), output_dir=tmp_path, output_payload=payload, input_path="A14_s001_w1_z001_t001.tif", - ) - ) + )) assert output_path.name == "A14_s001_w1_z001_t001.tif" @@ -4904,14 +4896,12 @@ def test_function_output_path_rejects_multi_plane_carrier_without_input_path( ).payload_with(np.zeros((2, 4, 5), dtype=np.float32), None) with pytest.raises(ValueError, match="multi-plane source provenance"): - FunctionOutputPathAuthority.output_path( - FunctionOutputPathRequest( + FunctionOutputIdentity.path_from_request(FunctionOutputPathRequest( parser=SourceSchemaFilenameParser(), output_dir=tmp_path, output_payload=payload, input_path=None, - ) - ) + )) def test_function_output_path_uses_variable_component_identity( @@ -4949,11 +4939,8 @@ def test_function_output_path_uses_variable_component_identity( variable_components=(VariableComponents.Z_INDEX,), ) - identity = FunctionOutputIdentityAuthority.identity(request) - output_path = FunctionOutputPathAuthority.output_path_for_identity( - request, - identity, - ) + identity = FunctionOutputIdentity.from_request(request) + output_path = identity.path_for_request(request) assert output_path.name == "A14_s001_w1_z001_t001.tif" assert identity.component_values == { @@ -5007,15 +4994,12 @@ def test_collapsed_output_identity_uses_retained_source_contributors( variable_components=(VariableComponents.SITE,), ) - identity = FunctionOutputIdentityAuthority.identity(request) - filename_identity = FunctionOutputIdentityAuthority.filename_identity_from_metadata( + identity = FunctionOutputIdentity.from_request(request) + filename_identity = FunctionOutputIdentity.from_filename_metadata( request.parser, collapsed_metadata, ) - output_path = FunctionOutputPathAuthority.output_path_for_identity( - request, - identity, - ) + output_path = identity.path_for_request(request) assert collapsed_metadata.source_image_provenance_planes.count == 0 assert collapsed_metadata.source_image_provenance_planes.contributor_count == 2 @@ -5030,10 +5014,7 @@ def test_collapsed_output_identity_uses_retained_source_contributors( assert identity.filename_component_values["site"] == 1 assert filename_identity is not None assert ( - FunctionOutputPathAuthority.filename_for_identity( - request.parser, - filename_identity, - ) + filename_identity.filename(request.parser) == "A14_s001_w1_z001_t001.tif" ) @@ -5085,11 +5066,8 @@ def test_composite_then_z_collapse_uses_current_scalar_identity( variable_components=(VariableComponents.Z_INDEX,), ) - identity = FunctionOutputIdentityAuthority.identity(request) - output_path = FunctionOutputPathAuthority.output_path_for_identity( - request, - identity, - ) + identity = FunctionOutputIdentity.from_request(request) + output_path = identity.path_for_request(request) assert z_stack_metadata.source_provenance.source_plane_count == 3 assert projected_metadata.source_provenance.source_plane_count == 0 @@ -5125,11 +5103,8 @@ def test_variable_component_identity_uses_fallback_path_extension( variable_components=(VariableComponents.CHANNEL,), ) - identity = FunctionOutputIdentityAuthority.identity(request) - output_path = FunctionOutputPathAuthority.output_path_for_identity( - request, - identity, - ) + identity = FunctionOutputIdentity.from_request(request) + output_path = identity.path_for_request(request) assert output_path.name == "A01_s001_w1_z001_t001.png" assert identity.extension == ".png" @@ -5153,16 +5128,10 @@ def test_declared_main_flow_output_context_qualifies_output_filename( output_payload=payload, input_path="A01_s001_w1_z001_t001.jpg", ) - identity = FunctionOutputIdentityAuthority.identity(request) + identity = FunctionOutputIdentity.from_request(request) - red_path = FunctionOutputPathAuthority.output_path_for_identity( - request, - identity.with_filename_qualifier("CorrRed"), - ) - green_path = FunctionOutputPathAuthority.output_path_for_identity( - request, - identity.with_filename_qualifier("CorrGreen"), - ) + red_path = identity.with_filename_qualifier("CorrRed").path_for_request(request) + green_path = identity.with_filename_qualifier("CorrGreen").path_for_request(request) assert red_path.name == "A01_s001_w1_z001_t001_CorrRed.jpg" assert green_path.name == "A01_s001_w1_z001_t001_CorrGreen.jpg" @@ -5197,7 +5166,7 @@ def test_function_output_path_rejects_variation_outside_identity_components( ) with pytest.raises(ValueError, match="varies outside identity components"): - FunctionOutputIdentityAuthority.identity( + FunctionOutputIdentity.from_request( FunctionOutputPathRequest( parser=SourceSchemaFilenameParser(), output_dir=tmp_path, @@ -5221,7 +5190,7 @@ def test_function_output_path_rejects_group_by_component_stack_variation( ), ).payload_with(np.zeros((3, 4, 5), dtype=np.float32), None) with pytest.raises(ValueError, match="varies outside identity components"): - FunctionOutputIdentityAuthority.identity( + FunctionOutputIdentity.from_request( FunctionOutputPathRequest( parser=SourceSchemaFilenameParser(), output_dir=tmp_path, @@ -5252,11 +5221,8 @@ def test_input_aligned_stack_output_uses_input_filename_identity( input_aligned_output=True, ) - identity = FunctionOutputIdentityAuthority.identity(request) - output_path = FunctionOutputPathAuthority.output_path_for_identity( - request, - identity, - ) + identity = FunctionOutputIdentity.from_request(request) + output_path = identity.path_for_request(request) assert output_path.name == "A01_s002_w1_z001_t001.png" assert identity.component_values["site"] == 2 @@ -5299,11 +5265,8 @@ def test_function_output_path_keeps_payload_split_axis_over_input_alignment( input_aligned_output=True, ) - identity = FunctionOutputIdentityAuthority.identity(request) - output_path = FunctionOutputPathAuthority.output_path_for_identity( - request, - identity, - ) + identity = FunctionOutputIdentity.from_request(request) + output_path = identity.path_for_request(request) assert output_path.name == "A01_s001_w1_z001_t001.tif" assert identity.component_values["site"] == 1 @@ -5458,7 +5421,7 @@ def qualified_producer_manifest(tmp_path): ProducedOutputSemantics.from_output( producer, tmp_path - / FunctionOutputPathAuthority.filename_for_identity(parser, identity), + / identity.filename(parser), identity, output_context=AlignedImageSliceContext.main_flow( output_key=f"Output{plane}", @@ -5478,14 +5441,14 @@ def test_step_output_manifest_batch_lookup_preserves_aliases_order_and_duplicate ): store, _producer, consumer, records, parser = qualified_producer_manifest calls = [] - original = FunctionOutputPathAuthority.filename_for_identity + original = FunctionOutputIdentity.filename - def count_filename(parser, identity): + def count_filename(identity, parser): calls.append(identity) - return original(parser, identity) + return original(identity, parser) monkeypatch.setattr( - FunctionOutputPathAuthority, "filename_for_identity", count_filename + FunctionOutputIdentity, "filename", count_filename ) paths = ( records[1].output_path, @@ -5503,6 +5466,53 @@ def count_filename(parser, identity): assert len(calls) == len(records) +def test_produced_identity_owns_filename_and_live_storage_coordinate_cache( + qualified_producer_manifest, + tmp_path, + monkeypatch, +): + _store, _producer, _consumer, records, parser = qualified_producer_manifest + storage_components = dict(records[0].component_values) + storage_components["channel"] = 1 + semantic_components = {**storage_components, "well": "B02", "channel": 9} + record = replace( + records[0], + component_values=semantic_components, + filename_component_values=storage_components, + ).with_filename_qualifier(" ./Corrected signal?!.. ") + cache = FunctionOutputIdentityCache() + calls = [] + original_construct = parser.construct_filename + + def construct(bound): + calls.append(bound) + return original_construct(bound) + + monkeypatch.setattr(parser, "construct_filename", construct) + first = record.cached_filename(parser, cache) + assert first == "A01_s001_w1_z001_t001_Corrected_signal.tif" + assert record.cached_filename(parser, cache) == first + assert len(calls) == 1 + # The inherited formatter follows the original live storage mapping; + # semantic producer coordinates and its recorded path are independent. + storage_components["channel"] = 2 + second = record.cached_filename(parser, cache) + assert second == "A01_s001_w2_z001_t001_Corrected_signal.tif" + assert len(calls) == 2 + assert record.component_metadata()["well"] == "B02" + assert record.component_metadata()["channel"] == "9" + assert record.output_path == records[0].output_path + request = FunctionOutputPathRequest( + parser=parser, + output_dir=tmp_path / "other", + output_payload=np.zeros((3, 4), dtype=np.float32), + input_path=None, + identity_cache=cache, + ) + assert record.path_for_request(request) == tmp_path / "other" / second + assert len(calls) == 2 + + def test_step_output_manifest_batch_lookup_template_deduplicates_record_aliases( qualified_producer_manifest, ): diff --git a/tests/unit/test_materialization_source_extension.py b/tests/unit/test_materialization_source_extension.py index 5fd4e2d79f..7130b502d2 100644 --- a/tests/unit/test_materialization_source_extension.py +++ b/tests/unit/test_materialization_source_extension.py @@ -25,8 +25,7 @@ VirtualWorkspaceSourceProjection, ) from openhcs.core.steps.function_output_identity import ( - FunctionOutputIdentityAuthority, - FunctionOutputPathAuthority, + FunctionOutputIdentity, ) from openhcs.microscopes.source_schema import SourceSchemaFilenameParser from openhcs.processing.materialization import ImageFileOptions, MaterializationSpec @@ -275,13 +274,10 @@ def test_real_source_schema_composition_to_named_output_and_image_materializer( # suffixes before the named main/checkpoint writer saw the source identity. composed = ImagePayloadMetadata.compose((payload,), mode=mode) scalar = composed.for_leading_source_plane(0) - identity = FunctionOutputIdentityAuthority.filename_identity_from_metadata( + identity = FunctionOutputIdentity.from_filename_metadata( parser, scalar ) - destination = FunctionOutputPathAuthority.filename_for_identity( - parser, - replace(identity, filename_qualifier="fixture_image"), - ) + destination = replace(identity, filename_qualifier="fixture_image").filename(parser) assert destination == f"{well}_s001_w1_z001_t001_fixture_image{extension}" assert destination.count("_s001_w1_z001_t001") == 1 assert identity.extension == extension diff --git a/tests/unit/test_persisted_image_metadata.py b/tests/unit/test_persisted_image_metadata.py index 6dd325823b..a7a4566d87 100644 --- a/tests/unit/test_persisted_image_metadata.py +++ b/tests/unit/test_persisted_image_metadata.py @@ -173,17 +173,9 @@ def merge(well): writer.merge_source_projection_metadata( path, ".", - { - FIELDS.WORKSPACE_MAPPING: { - virtual_path: {"backend": "disk", "backend_address": virtual_path} - }, - FIELDS.SOURCE_METADATA: {virtual_path: {"well": well}}, - FIELDS.SOURCE_PROJECTION: [ - SourceProjectionMetadataSerializer(None)._source_projection_payload( - projection, virtual_path - ) - ], - }, + VirtualWorkspaceSourceProjectionEntries.from_projection_paths( + ((projection, virtual_path),) + ), ) with ThreadPoolExecutor(max_workers=4) as pool: @@ -242,7 +234,12 @@ def test_atomic_projection_merge_refreshes_geometry_from_current_document( writer.merge_subdirectory_metadata(path, {"images": second_fields}) expected_pixel_size = 2.0 else: - writer.merge_source_projection_metadata(path, "images", second_fields) + writer.merge_source_projection_metadata( + path, "images", + VirtualWorkspaceSourceProjectionEntries.from_projection_paths( + ((second, "images/second.tif"),) + ), + ) expected_pixel_size = 1.0 # Conflicting calibration has no physical scalar. subdir = json.loads(path.read_text())[FIELDS.SUBDIRECTORIES]["images"] assert subdir[FIELDS.PIXEL_SIZE] == expected_pixel_size @@ -261,7 +258,8 @@ def test_final_publication_prunes_geometry_and_preserves_other_directories(tmp_p (elsewhere, "other/retained.tif"), ) writer.merge_source_projection_metadata( - path, "images", serializer.projection_fields(projection_paths) + path, "images", + VirtualWorkspaceSourceProjectionEntries.from_projection_paths(projection_paths) ) before = json.loads(path.read_text())[FIELDS.SUBDIRECTORIES]["images"] assert before[FIELDS.PIXEL_SIZE] == 1.0 @@ -294,7 +292,9 @@ def publish(item): publish_projection_inventory( writer, path, - serializer.projection_fields(((projection, image_path),)), + VirtualWorkspaceSourceProjectionEntries.from_projection_paths( + ((projection, image_path),) + ), (image_path,), ) @@ -311,6 +311,139 @@ def publish(item): assert subdir["wells"] == {f"A0{index + 1}": None for index in range(4)} +@pytest.mark.parametrize("publish", (False, True)) +def test_typed_projection_update_admits_only_retained_wire_records( + tmp_path, monkeypatch, publish +): + path = tmp_path / "openhcs_metadata.json" + writer = AtomicMetadataWriter() + retained = calibrated_projection("A01", "images/retained.tif", 0.5) + replacement = calibrated_projection("A02", "images/replaced.tif", 0.5) + serializer = SourceProjectionMetadataSerializer(SourceSchemaFilenameParser()) + fields = serializer.projection_fields(((retained, "images/retained.tif"),)) + fields[FIELDS.SOURCE_PROJECTION].append( + {"virtual_path": "images/replaced.tif", "projection_role": "broken"} + ) + writer.replace_subdirectory_metadata(path, "images", fields) + updates = VirtualWorkspaceSourceProjectionEntries.from_projection_paths( + ((replacement, "images/replaced.tif"),) + ) + admitted_paths = [] + serialized_paths = [] + decode = VirtualWorkspaceSourceProjectionEntries._projection_record + encode = SourceProjectionMetadataSerializer._source_projection_payload + + def record(cls, wire): + admitted_paths.append(wire["virtual_path"]) + return decode(wire) + + def payload(cls, projection, virtual_path): + serialized_paths.append(virtual_path) + return encode(projection, virtual_path) + + monkeypatch.setattr( + VirtualWorkspaceSourceProjectionEntries, + "_projection_record", + classmethod(record), + ) + monkeypatch.setattr( + SourceProjectionMetadataSerializer, + "_source_projection_payload", + classmethod(payload), + ) + merged = updates.merged_with_subdirectory(fields) + assert merged.entries["images/replaced.tif"] is replacement + assert tuple(merged.entries) == ("images/retained.tif", "images/replaced.tif") + admitted_paths.clear() + if publish: + publish_projection_inventory( + writer, path, updates, ("images/retained.tif", "images/replaced.tif") + ) + else: + writer.merge_source_projection_metadata(path, "images", updates) + assert admitted_paths == ["images/retained.tif"] + assert serialized_paths == ( + ["images/retained.tif", "images/replaced.tif"] + if publish else ["images/replaced.tif"] + ) + subdir = json.loads(path.read_text())[FIELDS.SUBDIRECTORIES]["images"] + restored = VirtualWorkspaceSourceProjectionEntries.from_subdirectory(subdir) + assert ( + restored.entries["images/replaced.tif"].image_metadata + == replacement.image_metadata + ) + + +@pytest.mark.parametrize("publish", (False, True)) +def test_invalid_unreplaced_projection_aborts_typed_transaction(tmp_path, publish): + path = tmp_path / "openhcs_metadata.json" + writer = AtomicMetadataWriter() + writer.replace_subdirectory_metadata( + path, "images", + {FIELDS.SOURCE_PROJECTION: [{"virtual_path": "images/broken.tif"}]}, + ) + replacement = calibrated_projection("A02", "images/new.tif", 0.5) + updates = VirtualWorkspaceSourceProjectionEntries.from_projection_paths( + ((replacement, "images/new.tif"),) + ) + before = path.read_bytes() + with pytest.raises(MetadataWriteError, match="projection_role"): + if publish: + publish_projection_inventory(writer, path, updates, ("images/new.tif",)) + else: + writer.merge_source_projection_metadata(path, "images", updates) + assert path.read_bytes() == before + + +def test_typed_merge_retains_opaque_fields_and_orphan_mappings(tmp_path): + path = tmp_path / "openhcs_metadata.json" + writer = AtomicMetadataWriter() + original = calibrated_projection("A01", "images/original.tif", 0.5) + fields = SourceProjectionMetadataSerializer.projection_fields( + ((original, "images/original.tif"),) + ) + fields[FIELDS.SOURCE_PROJECTION][0]["external_annotation"] = {"version": 7} + fields[FIELDS.WORKSPACE_MAPPING]["legacy.tif"] = {"opaque": "mapping"} + fields[FIELDS.SOURCE_METADATA]["legacy.tif"] = {"opaque": "metadata"} + writer.replace_subdirectory_metadata(path, "images", fields) + new = calibrated_projection("A02", "images/new.tif", 0.5) + writer.merge_source_projection_metadata( + path, "images", + VirtualWorkspaceSourceProjectionEntries.from_projection_paths( + ((new, "images/new.tif"),) + ), + ) + subdir = json.loads(path.read_text())[FIELDS.SUBDIRECTORIES]["images"] + assert subdir[FIELDS.SOURCE_PROJECTION][0]["external_annotation"] == {"version": 7} + assert subdir[FIELDS.WORKSPACE_MAPPING]["legacy.tif"] == {"opaque": "mapping"} + assert subdir[FIELDS.SOURCE_METADATA]["legacy.tif"] == {"opaque": "metadata"} + + +def test_typed_transaction_normalizes_legacy_paths_and_rejects_collisions(): + original = calibrated_projection("A01", "7", 0.5) + fields = SourceProjectionMetadataSerializer.projection_fields(((original, "7"),)) + fields[FIELDS.SOURCE_PROJECTION][0]["virtual_path"] = 7 + updates = VirtualWorkspaceSourceProjectionEntries.from_projection_paths( + ((calibrated_projection("A02", "new.tif", 0.5), "new.tif"),) + ) + assert tuple(updates.merged_with_subdirectory(fields).entries) == ("7", "new.tif") + colliding = VirtualWorkspaceSourceProjectionEntries.from_projection_paths( + ((calibrated_projection("A02", "7", 0.5), "7"),) + ) + with pytest.raises(RuntimeError, match="duplicate path '7'"): + colliding.merged_with_subdirectory(fields) + + +@pytest.mark.parametrize("field", (FIELDS.WORKSPACE_MAPPING, FIELDS.SOURCE_METADATA)) +def test_typed_transaction_preserves_durable_mapping_validation(field): + projection = calibrated_projection("A01", "images/current.tif", 0.5) + updates = VirtualWorkspaceSourceProjectionEntries.from_projection_paths( + ((projection, "images/current.tif"),) + ) + with pytest.raises(TypeError, match="must be a mapping"): + updates.merged_with_subdirectory({field: None}) + + def test_metadata_codec_rejects_unknown_declarations(): with pytest.raises(ValueError, match="undeclared"): ImagePayloadMetadata.from_mapping({"imagined_tile_axis": 9}) From 497e553a793cd61fadc648aa134cbb8b2ae2530e Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 15:08:08 -0400 Subject: [PATCH 147/235] Preserve measurement module rosters through CellProfiler import Keep complete image/object selections on the original measurement module so the existing runtime Cartesian batch owns their execution. Remove the scalar splitting overrides and unused helper. Declare every measured label in the object-only invocation domain, and derive grouping before projecting active runtime source bindings. Existing nominal family hooks preserve full composed image operands and mixed original/produced measurement subjects. Validation: 56 focused import/source-scope/family controls pass. Actual 3D and Speckles cppipes import through production and roundtrip through public source byte-for-byte. The 3D intensity and shape modules each retain one complete ungrouped invocation; separately authored Speckles calls remain distinct. Runtime/native parity and end-to-end performance are separate pending gates. Refs #536, #479. --- .../cellprofiler/module_artifact_contracts.py | 34 ----- .../module_artifact_declarations.py | 69 +++++---- .../cellprofiler/module_callable_abi.py | 2 +- .../cellprofiler/module_declarations.py | 48 ++++++- .../interop/cellprofiler/pipeline_import.py | 26 +--- .../unit/test_cellprofiler_pipeline_import.py | 135 +++++++++++++++--- 6 files changed, 202 insertions(+), 112 deletions(-) diff --git a/openhcs/interop/cellprofiler/module_artifact_contracts.py b/openhcs/interop/cellprofiler/module_artifact_contracts.py index 276c2c7b12..3cf653e6f1 100644 --- a/openhcs/interop/cellprofiler/module_artifact_contracts.py +++ b/openhcs/interop/cellprofiler/module_artifact_contracts.py @@ -28,7 +28,6 @@ normalize_cellprofiler_setting_name, ) from openhcs.interop.cellprofiler.setting_names import ( - setting_name_matches, setting_names, setting_values, ) @@ -123,39 +122,6 @@ def artifact_names_for_binding( for name in split_symbol_names(value) ) - @classmethod - def split_invocation_blocks_for_binding( - cls, - modules: tuple["ModuleBlock", ...], - binding: SettingToKeywordBinding, - ) -> tuple["ModuleBlock", ...]: - """Split repeated artifact selections into exact scalar invocations.""" - - split_blocks: list[ModuleBlock] = [] - for module in modules: - names = cls.artifact_names_for_binding(module, binding) - if len(names) <= 1: - split_blocks.append(module) - continue - retained_records = tuple( - record - for record in module.iter_settings() - if not setting_name_matches(record.name, binding.setting_name) - ) - split_blocks.extend( - replace( - module, - setting_records=[ - *retained_records, - *binding.records_from_kwargs( - {binding.require_parameter_name(): name} - ), - ], - ) - for name in names - ) - return tuple(split_blocks) - @classmethod def main_flow_output_specs( cls, diff --git a/openhcs/interop/cellprofiler/module_artifact_declarations.py b/openhcs/interop/cellprofiler/module_artifact_declarations.py index f7534336f8..5142146a0c 100644 --- a/openhcs/interop/cellprofiler/module_artifact_declarations.py +++ b/openhcs/interop/cellprofiler/module_artifact_declarations.py @@ -12,6 +12,7 @@ ClassVar, ) +from openhcs.constants.input_source import InputSource from openhcs.core.artifacts import ( ArtifactInputPlan, ArtifactOutputPlan, @@ -1188,6 +1189,29 @@ class ImageMeasurementInputModule( image_measurement_binding, ) + @classmethod + def input_source_for_contract( + cls, + callable_contract: CallableContract, + *, + step_context: "ArtifactDeclarationStepContext", + ) -> InputSource: + """Keep original measurement images alongside produced image subjects.""" + produced_refs = frozenset( + producer.spec.ref().for_plan_type(ArtifactInputPlan) + for producer in step_context.available_artifact_producers + ) + if any( + spec.ref() not in produced_refs + and step_context.source_bindings.binding_for_artifact_ref(spec.ref()) is not None + for spec in callable_contract.artifact_inputs.of_artifact_type(ImageArtifactType) + ): + return InputSource.PIPELINE_START + return super().input_source_for_contract( + callable_contract, + step_context=step_context, + ) + @classmethod def measurement_output_relations( cls, @@ -1218,29 +1242,6 @@ def measurement_output_relations( *image_subjects, ) - @classmethod - def invocation_module_blocks( - cls, - module: "ModuleBlock", - ) -> tuple["ModuleBlock", ...]: - """Expose each natural measurement image as one public invocation.""" - - blocks = super().invocation_module_blocks(module) - if ( - CallableContract.from_callable( - cls.require_callable() - ).image_payload_consumption - is ImagePayloadConsumption.COMPOSED - ): - return blocks - (binding,) = cls.declared_artifact_bindings( - plan_type=ArtifactInputPlan, artifact_type=ImageArtifactType - ) - return cls.split_invocation_blocks_for_binding( - blocks, - binding, - ) - @classmethod def executes_per_image_measurements( cls, @@ -1312,15 +1313,21 @@ class ObjectMeasurementInputModule( ) @classmethod - def invocation_module_blocks( + def processing_group_scope_inputs( cls, - module: "ModuleBlock", - ) -> tuple["ModuleBlock", ...]: - """Expose each measured object set as one scalar-label invocation.""" - - return cls.split_invocation_blocks_for_binding( - super().invocation_module_blocks(module), - cls.object_measurement_binding, + callable_contract: CallableContract, + ) -> tuple[ArtifactSpec, ...]: + """Keep all declared measurement subjects in one logical module batch.""" + source_refs = frozenset( + spec.ref() for spec in ( + *super().processing_group_scope_inputs(callable_contract), + *callable_contract.group_scope_inputs, + ) + ) + return tuple( + spec + for spec in callable_contract.artifact_inputs + if spec.ref() in source_refs ) diff --git a/openhcs/interop/cellprofiler/module_callable_abi.py b/openhcs/interop/cellprofiler/module_callable_abi.py index 78a23086ea..cce856f9a6 100644 --- a/openhcs/interop/cellprofiler/module_callable_abi.py +++ b/openhcs/interop/cellprofiler/module_callable_abi.py @@ -200,7 +200,7 @@ def invocation_domain_inputs( ObjectLabelsArtifactType ) if cls.executes_per_object_measurements(object_inputs): - return object_inputs[:1] + return object_inputs return tuple( artifact_input for artifact_input in declared_inputs diff --git a/openhcs/interop/cellprofiler/module_declarations.py b/openhcs/interop/cellprofiler/module_declarations.py index 196f030113..ab4b519c7e 100644 --- a/openhcs/interop/cellprofiler/module_declarations.py +++ b/openhcs/interop/cellprofiler/module_declarations.py @@ -27,7 +27,10 @@ FunctionStepExecutionScope, ) from openhcs.core.config import ProcessingConfig, StepSourceBindingsConfig -from openhcs.core.invocation_artifacts import ArtifactDeclarationStepContext +from openhcs.core.invocation_artifacts import ( + ArtifactDeclarationStepContext, + ArtifactInputPlan, +) from openhcs.core.source_bindings import ( SourceBindingsConfig, ) @@ -881,6 +884,31 @@ def resolve_function( del module, contract, source_bindings return cls.require_callable() + @classmethod + def input_source_for_contract( + cls, + callable_contract: CallableContract, + *, + step_context: ArtifactDeclarationStepContext, + ) -> InputSource: + """Resolve the main-flow anchor independently of auxiliary source inputs.""" + produced_refs = frozenset( + producer.spec.ref().for_plan_type(ArtifactInputPlan) + for producer in step_context.available_artifact_producers + ) + if any( + step_context.main_flow_artifacts.by_ref(spec.ref()) is not None + and spec.ref() in produced_refs + for spec in callable_contract.artifact_inputs + ): + return InputSource.PREVIOUS_STEP + if any( + step_context.source_bindings.binding_for_artifact_ref(spec.ref()) is not None + for spec in callable_contract.artifact_inputs + ): + return InputSource.PIPELINE_START + return InputSource.PREVIOUS_STEP + @classmethod def processing_config( cls, @@ -919,11 +947,7 @@ def processing_config( grouping_component = AllComponents.from_value(group_by.value) source_anchor_group_keys = step_context.source_bindings.component_group_keys_for_artifact_specs( grouping_component, - tuple( - spec - for spec in callable_contract.artifact_inputs - if spec.parameter_name is None - ), + cls.processing_group_scope_inputs(callable_contract), step_context.available_artifacts, ) if len(source_anchor_group_keys) > 1: @@ -934,3 +958,15 @@ def processing_config( group_by=group_by, input_source=input_source, ) + + @classmethod + def processing_group_scope_inputs( + cls, + callable_contract: CallableContract, + ) -> tuple[ArtifactSpec, ...]: + """Keep the complete unbound payload domain available for an invocation.""" + return tuple( + spec + for spec in callable_contract.artifact_inputs + if spec.parameter_name is None + ) diff --git a/openhcs/interop/cellprofiler/pipeline_import.py b/openhcs/interop/cellprofiler/pipeline_import.py index c40de79690..b9785bcdfa 100644 --- a/openhcs/interop/cellprofiler/pipeline_import.py +++ b/openhcs/interop/cellprofiler/pipeline_import.py @@ -492,25 +492,9 @@ def _parsed_target_units( raise ValueError( f"CellProfiler module {module.name} resolved a noncanonical callable." ) - follows_produced_main_flow = any( - declaration_context.main_flow_artifacts.by_ref(spec.ref()) is not None - and any( - producer.spec.ref().for_plan_type(ArtifactInputPlan) == spec.ref() - for producer in declaration_context.available_artifact_producers - ) - for spec in contract.artifact_inputs - ) - input_source = ( - InputSource.PREVIOUS_STEP - if follows_produced_main_flow - else ( - InputSource.PIPELINE_START - if any( - source_bindings.binding_for_artifact_ref(spec.ref()) is not None - for spec in contract.artifact_inputs - ) - else InputSource.PREVIOUS_STEP - ) + input_source = module_type.input_source_for_contract( + contract, + step_context=declaration_context, ) public_source_bindings = _public_step_source_bindings( source_bindings, @@ -528,7 +512,7 @@ def _parsed_target_units( inherited_processing_config, input_source=input_source, ), - step_context=processing_context, + step_context=declaration_context, ) public_source_bindings = _public_step_source_bindings( source_bindings, @@ -585,7 +569,7 @@ def _parsed_target_units( inherited_processing_config, input_source=input_source, ), - step_context=resolved_context, + step_context=declaration_context, ) public_source_bindings = _public_step_source_bindings( source_bindings, diff --git a/tests/unit/test_cellprofiler_pipeline_import.py b/tests/unit/test_cellprofiler_pipeline_import.py index d687c1079b..947486fbd0 100644 --- a/tests/unit/test_cellprofiler_pipeline_import.py +++ b/tests/unit/test_cellprofiler_pipeline_import.py @@ -16,7 +16,9 @@ ArtifactInputPlan, ArtifactSpec, ArtifactSpecCollection, + ArtifactSpecRelation, ImageArtifactType, + MeasurementsArtifactType, ObjectLabelsArtifactType, ) from openhcs.core.callable_contract import CallableContract, FunctionStepExecutionScope @@ -35,6 +37,7 @@ ComponentSelector, NamedSourceBinding, SourceBindingMatchMethod, + SourceBindingsConfig, SourceProjectionRole, StepSourceBindingsConfig, ) @@ -43,14 +46,16 @@ from openhcs.interop.cellprofiler.module_declarations import ( CellProfilerModule, ) -from openhcs.interop.cellprofiler.parser import ModuleBlock, ModuleSetting +from openhcs.interop.cellprofiler.parser import CPPipeParser, ModuleBlock, ModuleSetting from openhcs.interop.cellprofiler.pipeline_import import ( + _parsed_pipeline_declaration, _ParsedTargetUnit, _public_kwargs_for_target, _public_step_source_bindings, _SelectedInputBindingOccurrence, import_cellprofiler_pipeline, ) +from openhcs.interop.cellprofiler.settings_binder import SettingsBinder from openhcs.interop.cellprofiler.setting_names import ( SettingNameFamily, setting_values, @@ -1180,7 +1185,7 @@ def test_active_channel_outputs_preserve_declared_identity_without_consumers() - ) == ("CorrectedStain1", "CorrectedStain2") -def test_repeated_natural_measurement_images_use_group_local_source_identity() -> None: +def test_repeated_measurement_images_preserve_one_complete_module_roster() -> None: cppipe_path = Path("pipelines/grouped-measurements.cppipe") filemanager = _MemoryFileManager( {cppipe_path: """CellProfiler Pipeline: https://cellprofiler.org @@ -1214,28 +1219,27 @@ def test_repeated_natural_measurement_images_use_group_local_source_identity() - step for step in pipeline_steps if step.name == "MeasureObjectIntensity" ) invocations = tuple(normalize_function_pattern(measurement_step.func).iter_items()) - assert tuple(invocation.key.group_key for invocation in invocations) == ("1", "2") - assert all( - "select_images_to_measure" not in invocation.kwargs_dict - for invocation in invocations - ) - assert tuple(invocation.kwargs_dict for invocation in invocations) == ( - {}, - {"select_object_sets_to_measure": "Nuclei"}, + assert tuple(invocation.key.group_key for invocation in invocations) == ( + DEFAULT_GROUP_KEY, ) + assert invocations[0].kwargs_dict["select_images_to_measure"] == ("DNA", "PH3") with config_context(pipeline_config): - assert measurement_step.processing_config.group_by is GroupBy.CHANNEL + assert measurement_step.processing_config.group_by is GroupBy.NONE + assert measurement_step.processing_config.input_source is InputSource.PIPELINE_START -def test_repeated_object_measurements_retain_each_scalar_object_selection() -> None: +def test_measurement_rosters_preserve_exact_subjects_across_public_transport() -> None: cppipe_path = Path("pipelines/repeated-object-measurements.cppipe") filemanager = _MemoryFileManager( {cppipe_path: """CellProfiler Pipeline: https://cellprofiler.org NamesAndTypes:[module_num:1|enabled:True] - Assignments count:1 + Assignments count:2 Select the image type:Grayscale image Name to assign these images:DNA Select the rule criteria:and (metadata does channel "1") + Select the image type:Grayscale image + Name to assign these images:RNA + Select the rule criteria:and (metadata does channel "2") MedianFilter:[module_num:2|enabled:True] Select the input image:DNA Name the output image:CorrDNA @@ -1252,7 +1256,7 @@ def test_repeated_object_measurements_retain_each_scalar_object_selection() -> N Select the smaller identified objects:Nuclei Name the tertiary objects to be identified:Cytoplasm MeasureObjectIntensity:[module_num:6|enabled:True] - Select images to measure:CorrDNA + Select images to measure:CorrDNA, RNA Select objects to measure:Nuclei, Cells, Cytoplasm """} ) @@ -1270,10 +1274,11 @@ def test_repeated_object_measurements_retain_each_scalar_object_selection() -> N measurement_invocations = tuple( normalize_function_pattern(measurement_steps[0].func).iter_items() ) - assert tuple( - invocation.kwargs_dict["select_object_sets_to_measure"] - for invocation in measurement_invocations - ) == ("Nuclei", "Cells", "Cytoplasm") + assert len(measurement_invocations) == 1 + assert measurement_invocations[0].key.group_key == DEFAULT_GROUP_KEY + assert measurement_invocations[0].kwargs_dict["select_images_to_measure"] == ( + "CorrDNA", "RNA", + ) source = FunctionStepTransportAuthority.source_from_pipeline(pipeline_steps) namespace: dict[str, object] = {} @@ -1286,12 +1291,104 @@ def test_repeated_object_measurements_retain_each_scalar_object_selection() -> N ) assert FunctionStepTransportAuthority.source_from_pipeline(reconstructed) == source assert tuple( - invocation.kwargs_dict["select_object_sets_to_measure"] + invocation.key for step in reconstructed_measurements for invocation in normalize_function_pattern(step.func).iter_items() + ) == (measurement_invocations[0].key,) + + parser = CPPipeParser() + modules = tuple( + parser.parse(cppipe_path, filemanager=filemanager, backend=Backend.MEMORY) + ) + declaration = _parsed_pipeline_declaration( + modules, + CellProfilerModule.source_bindings_for_modules( + modules, + SourceBindingsConfig(image_plane_sources=parser.image_plane_sources), + ), + binder=SettingsBinder(source_root=cppipe_path.parent), + ) + target = next( + unit for unit in declaration.target_units + if unit.module.name == "MeasureObjectIntensity" + ) + module_type = CellProfilerModule.require_module("MeasureObjectIntensity") + reconstructed_invocation = next( + normalize_function_pattern(reconstructed_measurements[0].func).iter_items() + ) + blocks, consumed = module_type.module_blocks_for_invocation( + invocation=reconstructed_invocation, step_context=target.context, + ) + assert len(blocks) == 1 + numbered, _next_module_num = CellProfilerModule.number_step_invocation_blocks( + (blocks,), first_module_num=1, + ) + contract, _consumed = module_type.invocation_callable_contract( + invocation=reconstructed_invocation, + numbered_module_blocks=numbered[0], + consumed_kwarg_names=consumed, + step_context=target.context, + ) + assert tuple(spec.ref() for spec in contract.artifact_inputs) == tuple( + spec.ref() for spec in target.contract.artifact_inputs + ) + assert tuple(spec.name for spec in contract.artifact_inputs) == ( + "CorrDNA", "RNA", "Nuclei", "Cells", "Cytoplasm", + ) + (measurement_output,) = contract.artifact_outputs.of_artifact_type( + MeasurementsArtifactType, + ) + assert tuple( + subject.name + for subject in ArtifactSpecRelation.measurement_subjects_for_output( + measurement_output, + ) ) == ("Nuclei", "Cells", "Cytoplasm") +def test_object_measurement_subjects_across_channels_share_one_invocation() -> None: + cppipe_path = Path("pipelines/cross-channel-object-measurements.cppipe") + filemanager = _MemoryFileManager( + {cppipe_path: """CellProfiler Pipeline: https://cellprofiler.org +NamesAndTypes:[module_num:1|enabled:True] + Assignments count:2 + Select the image type:Grayscale image + Name to assign these images:DNA + Select the rule criteria:and (metadata does channel "1") + Select the image type:Grayscale image + Name to assign these images:RNA + Select the rule criteria:and (metadata does channel "2") +IdentifyPrimaryObjects:[module_num:2|enabled:True] + Select the input image:DNA + Name the primary objects to be identified:Nuclei +IdentifyPrimaryObjects:[module_num:3|enabled:True] + Select the input image:RNA + Name the primary objects to be identified:Cells +IdentifyPrimaryObjects:[module_num:4|enabled:True] + Select the input image:RNA + Name the primary objects to be identified:Unmeasured +MeasureObjectSizeShape:[module_num:5|enabled:True] + Select object sets to measure:Nuclei, Cells +"""} + ) + steps, config = import_cellprofiler_pipeline( + cppipe_path, filemanager=filemanager, backend=Backend.MEMORY, + ) + (measurement_step,) = tuple( + step for step in steps if step.name == "MeasureObjectSizeShape" + ) + (invocation,) = tuple( + normalize_function_pattern(measurement_step.func).iter_items() + ) + assert invocation.key.group_key == DEFAULT_GROUP_KEY + assert invocation.kwargs_dict["select_object_sets_to_measure"] == ( + "Nuclei", "Cells", + ) + with config_context(config): + assert measurement_step.processing_config.group_by is GroupBy.NONE + assert measurement_step.processing_config.input_source is InputSource.PREVIOUS_STEP + + def test_mixed_source_and_produced_measurements_share_one_step() -> None: cppipe_path = Path("pipelines/mixed-source-produced-measurements.cppipe") filemanager = _MemoryFileManager( From 9cb5e6518d871f445ec02c7e1fc90759da87f1e3 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 15:47:10 -0400 Subject: [PATCH 148/235] Execute producer cohorts without intermediate image checkpoints Preserve exact storage independently of primary image role, derive checkpoint demand from consumers, and admit isolated pixel/mask buffers with correlated producer coordinates. Consolidate artifact context extraction and resolve natural measurement rosters once. Remove redundant transport flag and coordinate reconstruction. Refs #479, #536, #450. Existing changed-source controls pass after completing API fixture migrations; fresh end-to-end/native comparison follows separately. --- .../runtime-owner-consolidation-20261003.rst | 25 ++ openhcs/core/aligned_image_payload.py | 25 ++ openhcs/core/artifacts.py | 13 + openhcs/core/compiled_step_plan.py | 67 ++++- openhcs/core/component_group_scope.py | 33 +++ openhcs/core/function_patterns.py | 129 +++++++--- openhcs/core/pipeline/path_planner.py | 32 ++- openhcs/core/runtime_adapters.py | 4 +- openhcs/core/runtime_stores.py | 166 +++++++++++-- openhcs/core/steps/function_execution.py | 229 ++++++++++++++---- .../core/steps/function_output_manifest.py | 2 +- openhcs/core/steps/function_runtime.py | 172 ++++++++++--- .../cellprofiler/runtime/artifact_binding.py | 24 +- .../cellprofiler/runtime/module_execution.py | 73 ++++-- .../test_chained_callable_lineage_journey.py | 23 +- .../unit/test_aligned_image_payload_device.py | 30 +++ .../test_cellprofiler_module_execution.py | 173 ++++++++++++- ...cellprofiler_module_execution_semantics.py | 3 +- ...t_cellprofiler_output_record_occurrence.py | 4 +- .../unit/test_cellprofiler_runtime_adapter.py | 3 +- ...st_child_measurement_producer_selection.py | 2 +- tests/unit/test_function_artifact_outputs.py | 4 +- ...st_function_execution_lifecycle_anchors.py | 2 +- ...st_function_output_manifest_input_scope.py | 3 +- tests/unit/test_function_patterns.py | 53 +++- ...test_function_runtime_source_projection.py | 15 +- .../unit/test_function_step_execution_plan.py | 4 +- .../test_function_step_execution_scope.py | 59 +++++ .../test_implicit_main_flow_compilation.py | 2 +- ...nvocation_input_source_context_identity.py | 8 +- .../test_loaded_pattern_group_ownership.py | 200 ++++++++++++++- .../test_opaque_main_flow_image_domain.py | 10 +- .../unit/test_path_planner_materialization.py | 4 +- ...test_primary_image_carrier_compile_gate.py | 3 +- tests/unit/test_runtime_value_store.py | 145 ++++++++++- 35 files changed, 1499 insertions(+), 245 deletions(-) diff --git a/docs/refactor/receipts/runtime-owner-consolidation-20261003.rst b/docs/refactor/receipts/runtime-owner-consolidation-20261003.rst index 641df4c39a..22d8bda179 100644 --- a/docs/refactor/receipts/runtime-owner-consolidation-20261003.rst +++ b/docs/refactor/receipts/runtime-owner-consolidation-20261003.rst @@ -202,6 +202,31 @@ also rejected: eliminating source snapshots changes in-place callable isolation, while preserving them retains the copying cost. Neither route is promoted as an unconsumed capability or a dominant performance fix. +Canonical producer transport and measurement rosters +--------------------------------------------------- + +An input edge now retains its exact producer storage independently of its +semantic primary-image projection. The redundant ``consumes_main_flow`` flag +is removed. Artifact-owned execution admits an independent pixel/mask buffer +directly from those producers instead of unstacking, saving, discovering and +reloading an unnamed checkpoint. ``CompiledStepPlan`` derives checkpoint demand +from actual consumers and propagates that demand through preserved main flow. +Path-based conversion, sequential filters, implicit raw inputs and unavailable +same-step producers retain their required transport. + +``RuntimeArtifactInput`` owns producer selection and candidate coordinates; +runtime scope joins retain whole correlated rows and permit compatible partial +contexts. Original filename metadata does not replace producer coordinates. +The existing artifact type owns image-context extraction, and the existing +stack composition owns independent buffer admission. No address or data cache +is introduced. + +The importer preserves a native measurement module's full image/object roster. +The existing measurement executor owns its Cartesian measurement traversal and +batch kernels. NATURAL object measurements resolve that roster once, without +building an unused composed image request; COMPOSED and selected-plane source +identity retain their declared behavior. + Evidence scope -------------- diff --git a/openhcs/core/aligned_image_payload.py b/openhcs/core/aligned_image_payload.py index 4aa3cfc9ea..1445ab0a54 100644 --- a/openhcs/core/aligned_image_payload.py +++ b/openhcs/core/aligned_image_payload.py @@ -496,6 +496,10 @@ def copy_whole_image( device_id: int | None, ) -> RuntimeArrayData: """Copy pixels, mask and metadata without adding a composition axis.""" + if isinstance(value, ImagePayloadStackComposition): + return value.copy_input_cohort( + memory_type=memory_type, device_id=device_id, + ) copied_data = stack_runtime_slices( (image_payload_data(value),), memory_type, device_id, )[0] @@ -508,6 +512,14 @@ def copy_whole_image( copied_data, copied_mask, ) + def copy_input_cohort( + self, *, memory_type: str, device_id: int | None, + ) -> RuntimeArrayData: + """Copy the dense image domain owned by this composition.""" + return self.copy_whole_image( + self.compose(), memory_type=memory_type, device_id=device_id, + ) + @staticmethod def with_saved_output_context( stack_payload: RuntimeArrayData, @@ -1163,6 +1175,19 @@ def with_slices(self, slices: Sequence[Any]) -> "AlignedImageStack": return type(self)(tuple(slices), self.slice_contexts) + def copy_input_cohort( + self, *, memory_type: str, device_id: int | None, + ) -> "AlignedImageStack": + """Retain aligned member domains while admitting independent buffers.""" + return self.with_slices( + tuple( + self.copy_whole_image( + payload, memory_type=memory_type, device_id=device_id, + ) + for payload in self.slices + ) + ) + def projected_output_slices( self, ) -> Iterator[tuple[Any, AlignedImageSliceContext | None]]: diff --git a/openhcs/core/artifacts.py b/openhcs/core/artifacts.py index 2d11eee696..72e5e23257 100644 --- a/openhcs/core/artifacts.py +++ b/openhcs/core/artifacts.py @@ -242,6 +242,11 @@ def normalize_source_payload( return data, channel_axis + @classmethod + def source_image_payload_from_runtime_value(cls, value: object) -> object | None: + """Read image context from this artifact's currently resolved payload.""" + return None + @classmethod def compose_runtime_values( cls, @@ -391,6 +396,10 @@ class ImageArtifactType(ArtifactType): participates_in_main_flow_output = True carries_source_image_context = True + @classmethod + def source_image_payload_from_runtime_value(cls, value: object) -> object: + return value + @classmethod def retained_filename_qualifier(cls, artifact_name: str) -> str: """Distinguish named retained images sharing one physical source plane.""" @@ -559,6 +568,10 @@ class ObjectLabelsArtifactType(ArtifactType): carries_source_image_context = True payload_description = "object_labels payload" + @classmethod + def source_image_payload_from_runtime_value(cls, value: object) -> object: + return value + @classmethod def runtime_parameter_types(cls) -> tuple[type, ...]: from openhcs.core.runtime_object_labels import ObjectLabelValue diff --git a/openhcs/core/compiled_step_plan.py b/openhcs/core/compiled_step_plan.py index 15f35284e4..8c6192693f 100644 --- a/openhcs/core/compiled_step_plan.py +++ b/openhcs/core/compiled_step_plan.py @@ -11,6 +11,7 @@ from polystore.streaming.identity import StreamProducerIdentity from openhcs.constants.constants import ( + Backend, GPU_MEMORY_TYPES, MemoryType, SequentialComponents, @@ -23,7 +24,11 @@ ) from openhcs.core.callable_contract import FunctionStepExecutionScope from openhcs.core.component_group_scope import ComponentGroupScope -from openhcs.core.function_patterns import CompiledFunctionPattern +from openhcs.core.function_patterns import ( + CompiledFunctionGroup, + CompiledFunctionPattern, + InvocationArtifactInputEdgePlan, +) from openhcs.core.source_bindings import ( CompiledSourceBindingPlan, CompiledSourceUniversePlan, @@ -320,6 +325,66 @@ class CompiledStepPlan: chainbreaker: bool = False error: str | None = None + def stored_primary_input_edges_for_group( + self, group: CompiledFunctionGroup, component_key: str | None, + ) -> tuple[InvocationArtifactInputEdgePlan, ...] | None: + """Admit exact producer transport only without path-based transforms.""" + if self.input_conversion is not None or self.sequential_filter_plan.enabled: + return None + edges = group.stored_primary_input_edges_for_component( + self.execution_group_scope, component_key, + ) + if edges and any( + edge.storage_plan.source_step_scope_id == self.step_scope_id + if edge.storage_plan.source_step_scope_id is not None + else edge.storage_plan.source_step_id in ( + self.step_index, self.step_scope_id, + ) + for edge in edges + ): + return None + return edges + + def requires_main_flow_checkpoint( + self, plans: Mapping[int, "CompiledStepPlan"], + ) -> bool: + """Derive pixel transport demand, including preserved input lifetimes.""" + if not plans or self.step_index not in plans: + return True + demanded: set[int] = set() + for plan in plans.values(): + if ( + plan.write_backend != Backend.MEMORY.value + or plan.materialized_output is not None + or plan.streaming_configs + or plan.visualize + ): + demanded.add(plan.step_index) + pattern = plan.compiled_function_pattern + if pattern is None or any( + (group := pattern.group_for_component(component_key)) is None + or plan.stored_primary_input_edges_for_group( + group, component_key, + ) is None + for component_key in plan.execution_group_scope.keys + ): + predecessor = plan.main_input_dependency.predecessor_step_index() + if predecessor is not None: + demanded.add(predecessor) + pending = list(demanded) + while pending: + current = plans.get(pending.pop()) + if current is None: + continue + pattern = current.compiled_function_pattern + if pattern is None or not pattern.preserves_input_main_flow(): + continue + predecessor = current.main_input_dependency.predecessor_step_index() + if predecessor is not None and predecessor not in demanded: + demanded.add(predecessor) + pending.append(predecessor) + return self.step_index in demanded + def producer_identity( self, *, diff --git a/openhcs/core/component_group_scope.py b/openhcs/core/component_group_scope.py index 9a5ca314f5..8a424610f5 100644 --- a/openhcs/core/component_group_scope.py +++ b/openhcs/core/component_group_scope.py @@ -387,6 +387,39 @@ def value_text_for_component( def has_fixed_components(self) -> bool: return bool(self.fixed_component_values) + def join_execution_cohort( + self, + other: "RuntimeExecutionAxisScope", + ) -> "RuntimeExecutionAxisScope | None": + """Join complete producer coordinates within one consumer execution group. + + An absent fixed coordinate leaves that axis unconstrained. Shared + coordinates must agree before either correlated row contributes its + remaining coordinates; fields from conflicting rows are never combined. + """ + + if not isinstance(other, RuntimeExecutionAxisScope): + raise TypeError("Execution cohorts require RuntimeExecutionAxisScope values.") + if ( + self.axis_id != other.axis_id + or self.component is not other.component + or self.value_text != other.value_text + ): + return None + own_fixed = dict(self.fixed_component_values) + other_fixed = dict(other.fixed_component_values) + if any( + own_fixed[component] != other_fixed[component] + for component in own_fixed.keys() & other_fixed.keys() + ): + return None + return type(self).from_raw( + self.axis_id, + component=self.component, + value=self.value_text, + fixed_component_values=tuple((own_fixed | other_fixed).items()), + ) + @property def source_component_values(self) -> RuntimeFixedComponentValues: """Return every typed source coordinate represented by this scope.""" diff --git a/openhcs/core/function_patterns.py b/openhcs/core/function_patterns.py index ab57008e0b..eff68598ae 100644 --- a/openhcs/core/function_patterns.py +++ b/openhcs/core/function_patterns.py @@ -229,9 +229,24 @@ class InvocationArtifactInputEdgePlan: spec: ArtifactSpec storage_plan: ArtifactInputPlan | None = field(compare=False) projection: ArtifactInputProjectionPlan | None - consumes_main_flow: bool = False main_flow_projection: MainFlowInputProjection | None = None + @staticmethod + def source_projection( + spec: ArtifactSpec, + main_flow_artifacts: ArtifactSpecCollection, + invocation_sources: ArtifactSpecCollection, + ) -> MainFlowInputProjection | None: + """Derive the primary source role independently of its pixel producer.""" + main_flow_refs = main_flow_artifacts.ref_set() + if spec.ref() not in main_flow_refs or spec.ref() not in invocation_sources.ref_set(): + return None + return ( + MainFlowInputProjection.COMPLETE_PAYLOAD + if len(main_flow_refs) == 1 + else MainFlowInputProjection.DECLARED_SOURCE_IMAGE + ) + @classmethod def from_source_declarations( cls, @@ -245,13 +260,12 @@ def from_source_declarations( ) -> "InvocationArtifactInputEdgePlan": """Project one unstored source occurrence from its exact declarations.""" - main_flow_refs = main_flow_artifacts.ref_set() - consumes_main_flow = ( - spec.ref() in main_flow_refs and spec.ref() in invocation_sources.ref_set() + main_flow_projection = cls.source_projection( + spec, main_flow_artifacts, invocation_sources, ) edge_type = ( CompiledMetadataArtifactInputEdgePlan - if metadata_available and not (consumes_main_flow or source_binding_available) + if metadata_available and main_flow_projection is None and not source_binding_available else cls ) return edge_type( @@ -259,16 +273,7 @@ def from_source_declarations( spec=spec, storage_plan=None, projection=None, - consumes_main_flow=consumes_main_flow, - main_flow_projection=( - MainFlowInputProjection.COMPLETE_PAYLOAD - if consumes_main_flow and len(main_flow_refs) == 1 - else ( - MainFlowInputProjection.DECLARED_SOURCE_IMAGE - if consumes_main_flow - else None - ) - ), + main_flow_projection=main_flow_projection, ) def uses_runtime_storage(self) -> bool: @@ -277,7 +282,9 @@ def uses_runtime_storage(self) -> bool: def requires_callable_binding(self) -> bool: """Admit parameter-bearing sources and validate every stored argument.""" - return self.uses_runtime_storage() or self.spec.binds_callable_parameter() + return self.spec.binds_callable_parameter() or ( + self.uses_runtime_storage() and self.main_flow_projection is None + ) def resolve_unstored_payload( self, @@ -290,22 +297,6 @@ def resolve_unstored_payload( ) def __post_init__(self) -> None: - if type(self.consumes_main_flow) is not bool: - raise TypeError( - "Invocation artifact input edge consumes_main_flow must be bool, " - f"got {type(self.consumes_main_flow).__name__}." - ) - if self.consumes_main_flow and self.main_flow_projection is None: - object.__setattr__( - self, - "main_flow_projection", - MainFlowInputProjection.DECLARED_SOURCE_IMAGE, - ) - elif not self.consumes_main_flow and self.main_flow_projection is not None: - raise ValueError( - "Invocation artifact input edge cannot declare a main-flow " - "projection when it does not consume main flow." - ) if self.main_flow_projection is not None and not isinstance( self.main_flow_projection, MainFlowInputProjection, @@ -315,11 +306,6 @@ def __post_init__(self) -> None: "MainFlowInputProjection or None, got " f"{type(self.main_flow_projection).__name__}." ) - if self.consumes_main_flow and self.storage_plan is not None: - raise ValueError( - "Invocation artifact input edge cannot consume main flow when an " - "exact storage plan owns the input." - ) if (self.storage_plan is None) != (self.projection is None): raise ValueError( "Invocation artifact input edge storage and projection must be " @@ -745,7 +731,7 @@ def _main_flow_input_refs( edge.spec.ref() for _invocation, edges in invocations_and_edges for edge in edges - if edge.consumes_main_flow + if edge.main_flow_projection is not None ) ) if main_flow_refs: @@ -756,6 +742,73 @@ def _main_flow_input_refs( ) return None if has_implicit_image_argument else () + def stored_primary_input_edges_for_component( + self, + execution_scope: ComponentGroupScope, + component_key: str | None, + ) -> tuple[InvocationArtifactInputEdgePlan, ...] | None: + """Select exact producer edges supplying an artifact-owned cohort. + + Artifact-owning invocations admit declared semantic primary sources. + Context-only inputs can supply a cohort when no raw primary image is + consumed. Unknown raw adapters retain source-path transport. + """ + active = tuple( + projected + for invocation in self.invocations + if ( + projected := invocation.for_component_execution( + execution_scope, component_key, + ) + ) is not None + ) + if not active or any( + projected.invocation.contract.accepts_implicit_main_flow_input + and not projected.invocation.adapter_manages_artifact_inputs + for projected in active + ): + return None + primary_edges = tuple( + edge + for projected in active + for edge in projected.artifact_input_edges + if edge.main_flow_projection is not None + and edge.spec.artifact_type.carries_source_image_context + ) + if not primary_edges: + if any( + projected.invocation.contract.accepts_implicit_main_flow_input + for projected in active + ): + return None + primary_edges = tuple( + edge + for projected in active + for edge in projected.artifact_input_edges + if edge.spec.ref() + in projected.invocation.contract.group_scope_inputs.ref_set() + and edge.spec.artifact_type.carries_source_image_context + ) + if not primary_edges or any( + edge.storage_plan is None for edge in primary_edges + ): + return None + selected: dict[ArtifactSpecRef, InvocationArtifactInputEdgePlan] = {} + for edge in primary_edges: + previous = selected.get(edge.spec.ref()) + if previous is not None and ( + edge.spec != previous.spec + or edge.storage_plan != previous.storage_plan + or edge.projection != previous.projection + or edge.main_flow_projection != previous.main_flow_projection + ): + raise ValueError( + f"Artifact-owned cohort source {edge.spec.ref()!r} has " + "conflicting compiled producer authorities." + ) + selected.setdefault(edge.spec.ref(), edge) + return tuple(selected.values()) + def resulting_main_flow_output_plans(self) -> tuple[ArtifactOutputPlan, ...]: """Return exact named output plans carried after this callable chain.""" diff --git a/openhcs/core/pipeline/path_planner.py b/openhcs/core/pipeline/path_planner.py index 3a1f469928..ad730c60e5 100644 --- a/openhcs/core/pipeline/path_planner.py +++ b/openhcs/core/pipeline/path_planner.py @@ -35,6 +35,7 @@ FunctionPatternSyntax, InvocationArtifactInputEdgePlan, InvocationArtifactInputProjectionKey, + MainFlowInputProjection, RuntimeParameterBinding, compile_function_pattern, inject_artifact_input_values, @@ -1259,14 +1260,10 @@ def compile_invocation_input_edges( ) -> CompiledFunctionPattern: """Compile exact invocation-to-input projections from nominal contracts.""" - main_flow_refs = main_flow_artifacts.ref_set() groups: list[CompiledFunctionGroup] = [] for group in compiled_pattern.groups: invocations: list[CompiledFunctionInvocation] = [] for invocation in group.invocations: - relation_owned_main_flow_refs = main_flow_refs.intersection( - invocation.contract.output_group_scope_sources - ) if compiled_pattern.is_grouped: if execution_group_scope.is_ungrouped: raise ValueError( @@ -1298,17 +1295,24 @@ def compile_invocation_input_edges( len(invocation.contract.artifact_inputs), ) selected_plans_by_ref = {plan.ref(): plan for plan in selected_plans} + adapter = invocation.contract.runtime_adapter + owns_artifact_inputs = ( + adapter is not None and adapter.manages_artifact_inputs + ) + raw_main_flow_refs = ( + main_flow_artifacts.ref_set() + .intersection(invocation.contract.output_group_scope_sources) + if not owns_artifact_inputs + else frozenset() + ) edges: list[InvocationArtifactInputEdgePlan] = [] for input_edge_key, input_spec in zip( input_edge_keys, invocation.contract.artifact_inputs, strict=True, ): - # A declared lineage source already travels in the primary - # payload; its producer storage is not a second ABI argument. storage_plan = ( - None - if input_spec.ref() in relation_owned_main_flow_refs + None if input_spec.ref() in raw_main_flow_refs else selected_plans_by_ref.get(input_spec.ref()) ) if storage_plan is None: @@ -1334,7 +1338,11 @@ def compile_invocation_input_edges( consumer_variable_components=consumer_variable_components, source_bindings=source_bindings, available_artifacts=available_artifacts, - consumes_main_flow=False, + main_flow_projection=InvocationArtifactInputEdgePlan.source_projection( + input_spec, + main_flow_artifacts, + invocation.contract.group_scope_inputs, + ) if owns_artifact_inputs else None, ) ) invocations.append(invocation.with_artifact_input_edges(tuple(edges))) @@ -1356,7 +1364,7 @@ def invocation_input_edge( consumer_variable_components: ComponentSet, source_bindings: StepSourceBindingsConfig, available_artifacts: ArtifactSpecCollection, - consumes_main_flow: bool, + main_flow_projection: MainFlowInputProjection | None, ) -> InvocationArtifactInputEdgePlan: """Compile one exact relation-owned invocation input edge.""" @@ -1381,7 +1389,7 @@ def invocation_input_edge( spec=input_spec, storage_plan=storage_plan, projection=projection, - consumes_main_flow=consumes_main_flow, + main_flow_projection=main_flow_projection, ) relation_scopes = tuple( @@ -1439,7 +1447,7 @@ def invocation_input_edge( spec=input_spec, storage_plan=storage_plan, projection=projection, - consumes_main_flow=consumes_main_flow, + main_flow_projection=main_flow_projection, ) @staticmethod diff --git a/openhcs/core/runtime_adapters.py b/openhcs/core/runtime_adapters.py index 4dbe9601f4..9ea47a8113 100644 --- a/openhcs/core/runtime_adapters.py +++ b/openhcs/core/runtime_adapters.py @@ -227,8 +227,8 @@ def require_artifact_input_edge( first = matches[0] if any( edge.spec != first.spec - or (edge.storage_plan, edge.projection, edge.consumes_main_flow) - != (first.storage_plan, first.projection, first.consumes_main_flow) + or (edge.storage_plan, edge.projection, edge.main_flow_projection) + != (first.storage_plan, first.projection, first.main_flow_projection) for edge in matches[1:] ): raise ValueError( diff --git a/openhcs/core/runtime_stores.py b/openhcs/core/runtime_stores.py index 93aa3ce0e9..5d270aad09 100644 --- a/openhcs/core/runtime_stores.py +++ b/openhcs/core/runtime_stores.py @@ -44,6 +44,7 @@ SourceAxisMetadataScope, SourceImageSetIdentityCompatibility, SourceImageSetIdentityPolicy, + semantic_source_metadata_value, ) from openhcs.core.source_bindings import CompiledSourceBindingPlan from openhcs.serialization.json import to_jsonable @@ -304,19 +305,7 @@ def records( if producer_scope.is_ungrouped: return self._records(store, producer_scope, None) selection_scope = projection.producer_selection_scope - if projection.selects_declared_complete_producer( - self.edge_plan.spec, storage_plan - ): - return self.all_records(store) - if storage_plan.composes_producer_groups( - ComponentSet.coerce(projection.consumer_variable_components) - ): - return self.all_records(store) - if ( - selection_scope == producer_scope - and not selection_scope.is_dynamic - and len(selection_scope.keys) > 1 - ): + if self.selects_complete_producer(): return self.all_records(store) runtime_key = self.axis_scope.value_text_for_component( @@ -327,6 +316,136 @@ def records( ) return self._records(store, producer_scope, selected_key) + def selects_complete_producer(self) -> bool: + """Derive whole-producer selection from this exact compiled projection.""" + + storage_plan = self.edge_plan.storage_plan + projection = self.edge_plan.projection + producer_scope = storage_plan.producer_group_scope() + selection_scope = projection.producer_selection_scope + return ( + projection.selects_declared_complete_producer(self.edge_plan.spec, storage_plan) + or storage_plan.composes_producer_groups( + ComponentSet.coerce(projection.consumer_variable_components) + ) + or ( + selection_scope == producer_scope + and not selection_scope.is_dynamic + and len(selection_scope.keys) > 1 + ) + ) + + def candidate_execution_scopes( + self, + store: "RuntimeValueStore", + execution_scope: ComponentGroupScope, + *, + variable_components: ComponentSet, + ) -> dict[RuntimeExecutionAxisScope, str]: + """Discover correlated consumer coordinates from exact producer records. + + Discovery retains addresses for diagnostics; admission through records() + subsequently validates and resolves the current producer value. + """ + + storage_plan = self.edge_plan.storage_plan + producer_scope = storage_plan.producer_group_scope() + projection = self.edge_plan.projection + selection_scope = projection.producer_selection_scope + selects_complete_producer = self.selects_complete_producer() + projected_components = projection.projected_variable_components(storage_plan) + identity_policy = self._source_context_identity_policy() + candidates = tuple( + record + for group_key in ((None,) if producer_scope.is_dynamic else producer_scope.keys) + for record in store.find_matching( + RuntimeArtifactQuery.from_input_plan( + storage_plan, + axis_id=self.axis_scope.axis_id, + backend=self.backend, + group_key=group_key, + ) + ) + ) + scopes = {} + for record in candidates: + record_scope = record.key.scope + if record_scope.component is not producer_scope.component: + continue + if not selects_complete_producer and not selection_scope.contains_runtime_key( + record_scope.value_text + ): + continue + record_coordinates = dict(record_scope.source_component_values) + if selects_complete_producer: + record_coordinates.pop(producer_scope.component, None) + if projected_components: + provenance = image_payload_metadata(record.value.data).source_provenance + plane_metadata = provenance.source_image_provenance_planes.runtime_component_metadata + metadata_rows = plane_metadata or ( + (provenance.source_component_metadata,) + if provenance.source_component_metadata is not None else () + ) + else: + metadata_rows = (None,) + for metadata in metadata_rows: + if metadata is None and projected_components: + continue + coordinates = dict(record_coordinates) + for component in projected_components: + component_scope = projection.component_scope(component) + if component_scope.is_dynamic: + component_value = semantic_source_metadata_value( + metadata, component.value, + ) + if component_value is None: + break + component_value = str(component_value) + else: + component_value = component_scope.select_runtime_key(None) + if not SourceAxisMetadataScope.constraint_matches_metadata( + metadata, component.value, component_value, + ): + break + existing_value = coordinates.get(component) + if existing_value is not None and not SourceAxisMetadataScope.constraint_matches_metadata( + metadata, component.value, existing_value, + ): + break + coordinates.setdefault(component, component_value) + else: + runtime_value = coordinates.get(execution_scope.component) + if execution_scope.is_dynamic and runtime_value is None: + continue + component_keys = execution_scope.runtime_keys((runtime_value,)) + for component_key in component_keys: + if ( + not selects_complete_producer + and selection_scope.is_dynamic + and selection_scope.component is execution_scope.component + and record_scope.value_text != component_key + ): + continue + fixed_group_value = coordinates.get(execution_scope.component) + if fixed_group_value is not None and fixed_group_value != component_key: + continue + fixed_values = tuple( + (component, value) + for component, value in coordinates.items() + if component is not execution_scope.component + and not component.is_multiprocessing_axis() + and component not in variable_components + and identity_policy.is_identity_component(component) + ) + scope = RuntimeExecutionAxisScope.from_raw( + self.axis_scope.axis_id, + component=execution_scope.component, + value=component_key, + fixed_component_values=fixed_values, + ) + scopes.setdefault(scope, record.path) + return scopes + def all_records( self, store: "RuntimeValueStore", @@ -585,16 +704,7 @@ def _matches_execution_scope( (component for component, _value in self.axis_scope.fixed_component_values), (component for component, _value in record_scope.fixed_component_values), ).excluding(projected_components) - # Only a declared source-context relation supplies plane membership. - # Unrelated visible bindings must not relax an exact artifact input. - context_sources = self.edge_plan.spec.source_context_sources() - identity_policy = ( - SourceImageSetIdentityPolicy.from_source_bindings( - self.source_binding_plan.for_artifact_refs(context_sources) - ) - if context_sources - else SourceImageSetIdentityPolicy() - ) + identity_policy = self._source_context_identity_policy() context_components = ( context_components .intersection(record_scope.source_components) @@ -615,6 +725,16 @@ def _matches_execution_scope( ), ).matches() + def _source_context_identity_policy(self) -> SourceImageSetIdentityPolicy: + """Derive plane membership only from this edge's context declarations.""" + + context_sources = self.edge_plan.spec.source_context_sources() + if not context_sources: + return SourceImageSetIdentityPolicy() + return SourceImageSetIdentityPolicy.from_source_bindings( + self.source_binding_plan.for_artifact_refs(context_sources) + ) + @dataclass(frozen=True, slots=True) class RuntimeArtifactBatch(RuntimeParameterDeclarationABC): diff --git a/openhcs/core/steps/function_execution.py b/openhcs/core/steps/function_execution.py index 6cd8cf743c..81dedd7ae3 100644 --- a/openhcs/core/steps/function_execution.py +++ b/openhcs/core/steps/function_execution.py @@ -18,12 +18,16 @@ LOADABLE_IMAGE_EXTENSIONS, Backend, ) +from openhcs.core.component_set import ComponentSet +from openhcs.core.component_group_scope import ComponentGroupScope, RuntimeExecutionAxisScope +from openhcs.core.runtime_stores import RuntimeArtifactInput from openhcs.core.callable_contract import ImagePayloadConsumption from openhcs.core.compiled_step_plan import CompiledStepPlan from openhcs.core.context.processing_context import ProcessingContext from openhcs.core.function_patterns import ( CompiledFunctionGroup, FunctionGroupKey, + InvocationArtifactInputEdgePlan, RuntimeInvocationDomain, ) from openhcs.core.progress import ProgressPhase, ProgressStatus, emit @@ -55,6 +59,7 @@ from openhcs.core.steps.abstract import StepExecutionObservation from openhcs.core.steps.function_outputs import finalize_function_step_outputs from openhcs.core.steps.function_runtime import ( + ArtifactPatternGroupExecutionRequest, PatternGroupExecutionRequest, _process_single_pattern_group, ) @@ -203,6 +208,141 @@ def map_groups( } ) + def execution_requests( + self, + context: ProcessingContext, + plan: CompiledStepPlan, + ) -> tuple[PatternGroupExecutionRequest, ...]: + """Admit source anchors to their complete compiled execution coordinates.""" + + requests = [] + for component_index, (component_value, patterns) in enumerate(self.items()): + compiled_group = plan.compiled_function_pattern.group_for_component( + component_value + ) + if compiled_group is None: + raise ValueError( + f"No compiled function group for component {component_value!r}." + ) + requests.extend( + PatternGroupExecutionRequest( + context=context, + execution_plan=plan, + pattern_group_info=pattern, + compiled_group=compiled_group, + component_value=component_value, + component_index=component_index, + component_count=len(self), + ) + for pattern in patterns + ) + return tuple(requests) + + @classmethod + def artifact_execution_requests( + cls, + context: ProcessingContext, + plan: CompiledStepPlan, + ) -> tuple[ArtifactPatternGroupExecutionRequest, ...] | None: + """Discover complete correlated cohorts from their compiled producers.""" + + pattern = plan.compiled_function_pattern + scope = plan.execution_group_scope + + def candidate_scopes( + edge: InvocationArtifactInputEdgePlan, + execution_scope: ComponentGroupScope, + ) -> dict[RuntimeExecutionAxisScope, str]: + return RuntimeArtifactInput( + edge_plan=edge, + axis_scope=RuntimeExecutionAxisScope.from_raw( + plan.axis_id, component=None, value=None, + ), + backend=Backend.MEMORY.value, + source_binding_plan=plan.source_binding_plan, + ).candidate_execution_scopes( + context.runtime_value_store, + execution_scope, + variable_components=ComponentSet.coerce(plan.variable_components), + ) + + if scope.is_dynamic: + discovered_keys = [] + for group in pattern.groups: + for invocation in group.invocations: + group_refs = invocation.contract.group_scope_inputs.ref_set() + for edge in invocation.artifact_input_edges: + if edge.storage_plan is None or not ( + edge.main_flow_projection is not None + or edge.spec.ref() in group_refs + ): + continue + discovered_keys.extend( + candidate.value_text + for candidate in candidate_scopes(edge, scope) + ) + if not any(key is not None for key in discovered_keys): + return None + component_keys = scope.runtime_keys(discovered_keys) + else: + component_keys = scope.runtime_keys(()) + + selected_groups = [] + for component_key in component_keys: + group = pattern.group_for_component(component_key) + if group is None: + raise ValueError( + f"No compiled function group for component {component_key!r}." + ) + edges = plan.stored_primary_input_edges_for_group(group, component_key) + if edges is None: + return None + selected_groups.append((component_key, group, edges)) + + requests = [] + for component_index, (component_key, group, edges) in enumerate(selected_groups): + cohorts = None + for edge in edges: + edge_cohorts = candidate_scopes( + edge, + ComponentGroupScope.from_raw((component_key,), component=scope.component), + ) + if not edge_cohorts: + raise ValueError( + f"Step {plan.step_index} ({plan.step_name}) has no exact " + f"producer cohort for {edge.spec.ref()!r} on axis {plan.axis_id}." + ) + cohorts = ( + edge_cohorts + if cohorts is None + else { + joined: anchor + for coordinates, anchor in cohorts.items() + for candidate in edge_cohorts + if (joined := coordinates.join_execution_cohort(candidate)) + is not None + } + ) + if not cohorts: + raise ValueError( + f"Step {plan.step_index} ({plan.step_name}) producer inputs " + "do not share a complete execution cohort." + ) + requests.extend( + ArtifactPatternGroupExecutionRequest( + context=context, + execution_plan=plan, + pattern_group_info=anchor, + compiled_group=group, + component_value=component_key, + fixed_component_values=coordinates.fixed_component_values, + component_index=component_index, + component_count=len(selected_groups), + ) + for coordinates, anchor in cohorts.items() + ) + return tuple(requests) + def _single_execution_group_patterns( patterns: DiscoveredPatternCollection, @@ -725,37 +865,49 @@ def run(self) -> StepExecutionObservation: ) phase_started_at = time.perf_counter() - patterns_by_axis = self._detect_patterns() + execution_requests = PatternGroups.artifact_execution_requests( + self.context, plan, + ) + patterns_by_axis = ( + self._detect_patterns() if execution_requests is None else None + ) self.record_runtime_profile( "step_detect_patterns", time.perf_counter() - phase_started_at, ) - self._log_discovered_patterns(patterns_by_axis) + if patterns_by_axis is not None: + self._log_discovered_patterns(patterns_by_axis) phase_started_at = time.perf_counter() self._convert_input_if_needed() self.record_runtime_profile( "step_convert_input", time.perf_counter() - phase_started_at, ) - self._require_patterns(patterns_by_axis) - self._apply_sequential_filter(patterns_by_axis) - - phase_started_at = time.perf_counter() - grouped_patterns = self._prepare_groups(patterns_by_axis) - self.record_runtime_profile( - "step_prepare_groups", - time.perf_counter() - phase_started_at, - ) - phase_started_at = time.perf_counter() - self._preload_inputs_if_needed(grouped_patterns) - self.record_runtime_profile( - "step_preload_inputs", - time.perf_counter() - phase_started_at, - ) - total_groups = grouped_patterns.total_count() + if patterns_by_axis is not None: + self._require_patterns(patterns_by_axis) + self._apply_sequential_filter(patterns_by_axis) + phase_started_at = time.perf_counter() + grouped_patterns = self._prepare_groups(patterns_by_axis) + execution_requests = grouped_patterns.execution_requests(self.context, plan) + self.record_runtime_profile( + "step_prepare_groups", + time.perf_counter() - phase_started_at, + ) + phase_started_at = time.perf_counter() + self._preload_inputs_if_needed(grouped_patterns) + self.record_runtime_profile( + "step_preload_inputs", + time.perf_counter() - phase_started_at, + ) + if not execution_requests: + raise ValueError( + f"No execution cohorts found for step {plan.step_index} " + f"({plan.step_name}) on axis {plan.axis_id}." + ) + total_groups = len(execution_requests) execution_started_at = time.perf_counter() self._execute_pattern_groups( - grouped_patterns, + execution_requests, total_groups, ) execution_elapsed = time.perf_counter() - execution_started_at @@ -1084,40 +1236,17 @@ def _preload_inputs_if_needed( def _execute_pattern_groups( self, - grouped_patterns: PatternGroups, + execution_requests: Sequence[PatternGroupExecutionRequest], total_groups: int, ) -> None: - completed_groups = 0 - for component_index, (component_value, current_pattern_list) in enumerate( - grouped_patterns.items() - ): - compiled_group = self.plan.compiled_function_pattern.group_for_component( - component_value + for completed_groups, request in enumerate(execution_requests, start=1): + _process_single_pattern_group(request) + self._emit_pattern_progress( + completed_groups, + total_groups, + request.component_value, + request.pattern_group_info, ) - if compiled_group is None: - raise ValueError( - f"No compiled function group for component {component_value!r}." - ) - - for pattern_item in current_pattern_list: - _process_single_pattern_group( - PatternGroupExecutionRequest( - context=self.context, - execution_plan=self.plan, - pattern_group_info=pattern_item, - compiled_group=compiled_group, - component_value=component_value, - component_index=component_index, - component_count=len(grouped_patterns), - ) - ) - completed_groups += 1 - self._emit_pattern_progress( - completed_groups, - total_groups, - component_value, - pattern_item, - ) def _emit_pattern_progress( self, diff --git a/openhcs/core/steps/function_output_manifest.py b/openhcs/core/steps/function_output_manifest.py index 3736c97d42..5bf4ff72b2 100644 --- a/openhcs/core/steps/function_output_manifest.py +++ b/openhcs/core/steps/function_output_manifest.py @@ -561,7 +561,7 @@ def _requested_producer_outputs( for invocation in plan.compiled_function_pattern.iter_invocations() for edge in invocation.artifact_input_edges if ( - edge.consumes_main_flow + edge.main_flow_projection is not None or ( edge.spec.parameter_name is None and edge.spec.ref() diff --git a/openhcs/core/steps/function_runtime.py b/openhcs/core/steps/function_runtime.py index 7fb7c13ae1..0caa0f1418 100644 --- a/openhcs/core/steps/function_runtime.py +++ b/openhcs/core/steps/function_runtime.py @@ -41,6 +41,7 @@ ) from openhcs.core.callable_contract import ( CallableRuntimeCacheKey, + ImagePayloadConsumption, ) from openhcs.core.component_group_scope import ( ComponentGroupScope, @@ -104,6 +105,7 @@ RuntimeSliceAlignedValues, ) from openhcs.core.runtime_slice_projection import ( + RuntimeSliceProjection, RuntimeSliceProjectionDeclarationError, ) from openhcs.core.source_image_semantics import apply_source_binding_payload @@ -168,7 +170,6 @@ RuntimePlaneAxisValueProjection, RuntimePlaneProjection, ) -from openhcs.core.runtime_slice_projection import RuntimeSliceProjection from openhcs.core.step_dependencies import StepInputDependencyKind from openhcs.core.steps.function_output_manifest import ( NoStepOutputManifestMatch, @@ -1238,7 +1239,7 @@ def select_source_bound_inputs( edge_key: edge for edge_key, edge in self.inputs.items() if not ( - edge.consumes_main_flow + edge.main_flow_projection is not None and declared_source_bindings.declares_artifact_ref(edge.spec.ref()) and not active_source_bindings.declares_artifact_ref( edge.spec.ref() @@ -1387,6 +1388,134 @@ class PatternGroupExecutionRequest(PatternGroupExecutionScope): component_index: int component_count: int + def load_input_stack( + self, runtime: "PatternGroupRuntime", + ) -> tuple[list[str], RuntimeArrayData, SourceBindingRuntimeContext]: + """Admit the filename-selected source cohort.""" + return runtime._load_input_stack() + + def loaded_plane_count( + self, matching_files: Sequence[str], payload: RuntimeArrayData, + ) -> int: + """The source path roster declares the initial runtime plane count.""" + return len(matching_files) + + def loaded_fixed_component_values( + self, payload: RuntimeArrayData, + ) -> RuntimeFixedComponentValues: + """Capture the source-selected cohort's common fixed coordinates.""" + source_provenance = image_payload_metadata( + payload + ).source_provenance.with_common_scalar_identity_from_planes() + common_source_metadata = source_provenance.source_component_metadata or {} + variable_components = ComponentSet.coerce( + self.execution_plan.variable_components or () + ) + execution_group_component = ( + self.execution_plan.execution_group_scope.component + ) + fixed_components = tuple( + component + for component in AllComponents + if not component.is_multiprocessing_axis() + and component is not execution_group_component + and component not in variable_components + and source_component_metadata_value( + common_source_metadata, + component, + ) + is not None + ) + return source_provenance.require_common_component_values(fixed_components) + + def passthrough_producer_records( + self, matching_files: Sequence[str], + ) -> tuple[ProducedOutputSemantics, ...] | None: + """Resolve the physical source paths selected by this request.""" + return step_output_manifest(self.context).producer_output_records_for_paths( + self.execution_plan, + matching_files, + self.context.microscope_handler.parser, + ) + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ArtifactPatternGroupExecutionRequest(PatternGroupExecutionRequest): + """Admit exact canonical producer values into an independent cohort.""" + + def load_input_stack( + self, runtime: "PatternGroupRuntime", + ) -> tuple[list[str], RuntimeArrayData, SourceBindingRuntimeContext]: + edges = self.execution_plan.stored_primary_input_edges_for_group( + self.compiled_group, self.component_key, + ) + if not edges: + raise ValueError("Artifact cohort requires complete stored primary inputs.") + payloads = [] + matching_files = [] + for edge in edges: + artifact_input = RuntimeArtifactInput( + edge_plan=edge, + axis_scope=self.axis_scope, + backend=Backend.MEMORY.value, + source_binding_plan=self.source_binding_plan, + ) + records = artifact_input.records(self.context.runtime_value_store) + source_payload = edge.spec.artifact_type.source_image_payload_from_runtime_value( + artifact_input.composed_value(records), + ) + if source_payload is None: + raise ValueError( + f"Artifact cohort source {edge.spec.ref()!r} has no image context." + ) + payloads.append( + ImagePayloadStackComposition.copy_whole_image( + source_payload, + memory_type=self.execution_plan.input_memory_type, + device_id=self.execution_plan.device_id_for( + self.execution_plan.input_memory_type, + ), + ) + ) + matching_files.extend(record.location.path for record in records) + main_data_stack = ImagePayloadConsumption.NATURAL.compose_image_payload( + self.execution_plan.step_name, tuple(payloads), + ).payload + source_projection = ( + runtime.source_workspace_projection_authority().projection_if_available() + ) + source_binding_context = SourceBindingRuntimeContextRequest.from_context( + context=self.context, + plan=self.execution_plan, + matching_files=matching_files, + source_projection=source_projection, + ).runtime_context() + return matching_files, main_data_stack, source_binding_context + + def loaded_plane_count( + self, matching_files: Sequence[str], payload: RuntimeArrayData, + ) -> int: + """Canonical payloads declare their runtime axis independently of paths.""" + count = RuntimeSliceProjection.slice_count_from_values((payload,)) + return 1 if count is None else count + + def loaded_fixed_component_values( + self, payload: RuntimeArrayData, + ) -> RuntimeFixedComponentValues: + """Retain exact producer coordinates admitted by typed discovery.""" + return self.fixed_component_values + + def passthrough_producer_records( + self, matching_files: Sequence[str], + ) -> tuple[ProducedOutputSemantics, ...]: + """Preserve input transport independently of artifact context admission.""" + records = step_output_manifest(self.context).producer_records_for( + self.execution_plan, + ) + if records is None: + raise ValueError("Artifact input passthrough requires producer lineage.") + return records + @dataclass(frozen=True, slots=True, kw_only=True) class PatternGroupData(PatternGroupExecutionScope): @@ -1416,28 +1545,6 @@ def from_loaded_group( request.component_key, artifacts.outputs, ) - source_provenance = image_payload_metadata( - main_data_stack - ).source_provenance.with_common_scalar_identity_from_planes() - common_source_metadata = source_provenance.source_component_metadata or {} - variable_components = ComponentSet.coerce( - request.execution_plan.variable_components or () - ) - execution_group_component = ( - request.execution_plan.execution_group_scope.component - ) - fixed_components = tuple( - component - for component in AllComponents - if not component.is_multiprocessing_axis() - and component is not execution_group_component - and component not in variable_components - and source_component_metadata_value( - common_source_metadata, - component, - ) - is not None - ) return cls( matching_files=matching_files, main_data_stack=main_data_stack, @@ -1447,11 +1554,11 @@ def from_loaded_group( artifacts=artifacts, source_binding_context=source_binding_context, runtime_plane_index=request.component_index, - runtime_plane_count=len(matching_files), - component_value=request.component_value, - fixed_component_values=source_provenance.require_common_component_values( - fixed_components + runtime_plane_count=request.loaded_plane_count( + matching_files, main_data_stack, ), + component_value=request.component_value, + fixed_component_values=request.loaded_fixed_component_values(main_data_stack), ) def require_invocations(self) -> None: @@ -1681,7 +1788,7 @@ def declared_source_payload( main_flow_edges = tuple( edge for edge in self.selected_artifact_input_edges - if edge.spec.ref() == source_ref and edge.consumes_main_flow + if edge.spec.ref() == source_ref and edge.main_flow_projection is not None ) uses_main_flow = bool( stored_payload is None and source_binding is None and main_flow_edges @@ -1749,6 +1856,7 @@ def load_artifact_inputs( projected_values = ( self.load_artifact_input(input_plan.spec.name, input_plan) if input_plan.uses_runtime_storage() + and input_plan.main_flow_projection is None else ( RuntimeValue.from_spec( input_plan.spec, @@ -2337,7 +2445,7 @@ def run(self) -> None: try: load_started_at = time.perf_counter() matching_files, main_data_stack, source_binding_context = ( - self._load_input_stack() + self.request.load_input_stack(self) ) except NoStepOutputManifestMatch: logger.debug( @@ -2384,6 +2492,8 @@ def run(self) -> None: plan.step_name, ) return + if not plan.requires_main_flow_checkpoint(self.request.context.step_plans): + return output_records = self._save_outputs(processed_stack, loaded.matching_files) output_paths = [record.output_path for record in output_records] cleanup_started_at = time.perf_counter() @@ -2429,7 +2539,7 @@ def _record_main_flow_passthrough(self, matching_files: Sequence[str]) -> None: plan = self.request.execution_plan parser = self.request.context.microscope_handler.parser manifest = step_output_manifest(self.request.context) - producer_records = manifest.producer_output_records_for_paths(plan, matching_files, parser) + producer_records = self.request.passthrough_producer_records(matching_files) if producer_records is None: records = tuple( ProducedOutputSemantics.from_existing_main_flow_path( diff --git a/openhcs/interop/cellprofiler/runtime/artifact_binding.py b/openhcs/interop/cellprofiler/runtime/artifact_binding.py index bb346b64bc..b9cb78aaad 100644 --- a/openhcs/interop/cellprofiler/runtime/artifact_binding.py +++ b/openhcs/interop/cellprofiler/runtime/artifact_binding.py @@ -178,7 +178,7 @@ def source_image_payload_from_runtime_value( ``RuntimeInputBindingRequest.runtime_value``. A source-context query must consume that result without binding the input a second time. """ - return None + return spec.artifact_type.source_image_payload_from_runtime_value(value) def published_main_flow_output( self, @@ -257,13 +257,6 @@ def source_image_name_from_value( image_payload_metadata(value).source_provenance.represented_source_image_names ) - def source_image_payload_from_runtime_value( - self, - spec: ArtifactSpec, - value: RuntimeCallableArgument, - ) -> RuntimeCallableArgument | None: - return value - def published_main_flow_output( self, input_value: RuntimeCallableArgument, @@ -343,13 +336,6 @@ def source_image_name_from_value( ) -> str | None: return cast(ObjectLabelSet, value).source_image_name - def source_image_payload_from_runtime_value( - self, - spec: ArtifactSpec, - value: RuntimeCallableArgument, - ) -> RuntimeCallableArgument | None: - return value - class MeasurementsArtifactTypeStrategy(RuntimeArtifactTypeStrategy): """Resolve measurement payloads and lineage.""" @@ -635,20 +621,20 @@ def artifact_value( ) source_plan = self.adapter.request.source_binding_plan source_binding = source_plan.binding_for_artifact_ref(spec.ref()) - consumes_main_flow = edge.consumes_main_flow + primary_projection = edge.main_flow_projection source_artifact_binding = ( source_binding - if edge.storage_plan is None and not consumes_main_flow + if edge.storage_plan is None and primary_projection is None else None ) runtime_edge = ( - edge if edge.storage_plan is not None and not consumes_main_flow else None + edge if edge.storage_plan is not None and primary_projection is None else None ) authority_count = sum( ( source_artifact_binding is not None, runtime_edge is not None, - consumes_main_flow, + primary_projection is not None, ) ) if authority_count != 1: diff --git a/openhcs/interop/cellprofiler/runtime/module_execution.py b/openhcs/interop/cellprofiler/runtime/module_execution.py index f88956140b..18045d725c 100644 --- a/openhcs/interop/cellprofiler/runtime/module_execution.py +++ b/openhcs/interop/cellprofiler/runtime/module_execution.py @@ -295,7 +295,11 @@ def __call__( module_type=module_type, ) image_request = None - if primary_image_inputs: + if ( + primary_image_inputs + and self.callable_contract.image_payload_consumption + is ImagePayloadConsumption.COMPOSED + ): image_request = self._image_request( image, cellprofiler_runtime, @@ -321,6 +325,7 @@ def __call__( adapter=cellprofiler_runtime, kwargs=kwargs, image_request=image_request, + image_inputs=primary_image_inputs, module_type=module_type, active_input_specs=active_inputs.specs, active_output_plans=active_outputs, @@ -616,13 +621,9 @@ def _measurement_image_inputs( current_image: RuntimeCallableArgument, image_request: CellProfilerImageRequest | None, *, + image_inputs: tuple[ArtifactSpec, ...], module_type: type[CellProfilerModule], ) -> tuple[CellProfilerMeasurementImage, ...]: - image_inputs = self._primary_image_inputs( - current_image, - adapter, - module_type=module_type, - ) if not image_inputs: return () if ( @@ -720,22 +721,21 @@ def _resolved_measurement_image( ImageArtifactType, ).raw_runtime_input_value(spec, value) ) - metadata = image_payload_metadata(payload) - plane_axis = metadata.plane_axis + plane_projection = ( + preserved_image_plane_projection(payload, adapter, source_aliases) + if image_payload_metadata(payload).plane_axis is not None + else None + ) return CellProfilerMeasurementImage( source_image_name=spec.name, source_aliases=source_aliases, payload=payload, plane_projection=( replace( - preserved_image_plane_projection( - payload, - adapter, - source_aliases, - ), + plane_projection, source_aliases=source_aliases, ) - if plane_axis is not None + if plane_projection is not None else None ), reference_domain=reference_domain, @@ -776,6 +776,7 @@ def _run_per_object_measurement( adapter: CellProfilerRuntimeAdapter, kwargs: RuntimeCallableKwargs, image_request: "CellProfilerImageRequest | None", + image_inputs: tuple[ArtifactSpec, ...], module_type: type[CellProfilerModule], active_input_specs: tuple[ArtifactSpec, ...], active_output_plans: tuple[ArtifactOutputPlan, ...], @@ -784,9 +785,6 @@ def _run_per_object_measurement( current_image = image input_image = image cellprofiler_runtime = adapter - source_image_name = ( - None if image_request is None else image_request.source_image_name - ) kwargs = dict(kwargs) function_name = self.callable_contract.function_name profiler = CellProfilerRuntimeProfiler( @@ -805,8 +803,49 @@ def _run_per_object_measurement( cellprofiler_runtime, current_image, image_request, + image_inputs=image_inputs, module_type=module_type, ) + if image_request is not None: + source_image_name = image_request.source_image_name + elif not image_inputs: + source_image_name = None + else: + image_strategy = RuntimeArtifactTypeStrategy.for_artifact_type(ImageArtifactType) + runtime_projection = RuntimePlaneAxisValueProjection.from_projector( + adapter, RuntimePlaneAxis.RUNTIME_SLICE, (), + ) + projected_binding = ( + RuntimeInputBindingRequest( + adapter=adapter, kwargs={}, + current_image=RuntimeSliceProjection.value_for_slice( + current_image, runtime_projection, + ), + ) + if runtime_projection is not None and runtime_projection.plane_index is not None + else None + ) + source_names = [] + for spec, measurement_image in zip(image_inputs, measurement_images, strict=True): + edge = ( + adapter.request.require_artifact_input_edge(spec.ref()) + if projected_binding is not None else None + ) + if edge is not None and edge.main_flow_projection is not None: + source_name = image_strategy.source_image_name( + spec, + projected_binding.main_flow_value( + spec, projection=edge.main_flow_projection, + ), + ) + else: + source_name = image_strategy.source_image_name_from_value( + measurement_image.payload + ) + source_names.append(source_name) + source_image_name = self._primary_image_source_name_from_sources( + image_inputs, tuple(source_names), + ) if not measurement_images: measurement_images = tuple( self._object_label_measurement_image( diff --git a/tests/integration/test_chained_callable_lineage_journey.py b/tests/integration/test_chained_callable_lineage_journey.py index 59c840578c..7afb82b34b 100644 --- a/tests/integration/test_chained_callable_lineage_journey.py +++ b/tests/integration/test_chained_callable_lineage_journey.py @@ -1,8 +1,10 @@ """Canonical callable twice through document, compiler, execution and publication.""" from csv import DictReader +from inspect import unwrap import numpy as np +import pytest import tifffile from openhcs.agent.services.execution_session_service import ( @@ -35,15 +37,32 @@ SourceSelector, ) from openhcs.core.steps.function_step import FunctionStep +from openhcs.processing.custom_functions.runtime_registry import ( + CustomFunctionRuntimeRegistry, + register_custom_function, +) from polystore.roi import load_rois_from_zip # Reuse the public reference's authoritative code block, not a copied callable. from tests.unit.agent.test_callable_artifact_reference import reference_namespace # noqa: F401 +@pytest.fixture +def registered_reference_callable(reference_namespace): + """Give the executable public example its actual custom catalog lifetime.""" + function = reference_namespace["inspect_label_fixture"] + registered = register_custom_function(function) + try: + assert unwrap(registered) is unwrap(function) + yield function + finally: + CustomFunctionRuntimeRegistry.remove(function.__name__) + + def test_chained_public_callable_uses_declared_main_flow_not_storage_argument( - tmp_path, reference_namespace, + tmp_path, reference_namespace, registered_reference_callable, ): + assert reference_namespace["inspect_label_fixture"] is registered_reference_callable plate = tmp_path / "plate" plate.mkdir() fixture = np.zeros((8, 8), dtype=np.uint16) @@ -106,7 +125,7 @@ def test_chained_public_callable_uses_declared_main_flow_not_storage_argument( assert edge.spec.ref() == incoming_ref assert edge.spec.parameter_name is None assert second_invocation.contract.group_scope_inputs.ref_set() == {incoming_ref} - assert edge.consumes_main_flow + assert edge.main_flow_projection is not None assert edge.main_flow_projection is MainFlowInputProjection.COMPLETE_PAYLOAD assert edge.storage_plan is None assert edge.projection is None diff --git a/tests/unit/test_aligned_image_payload_device.py b/tests/unit/test_aligned_image_payload_device.py index 4e0d4c2bb9..db18f9fa59 100644 --- a/tests/unit/test_aligned_image_payload_device.py +++ b/tests/unit/test_aligned_image_payload_device.py @@ -37,3 +37,33 @@ def test_image_bundle_stacks_on_the_payload_framework_device(monkeypatch) -> Non assert context.compose_unmasked(payloads) == "stacked" assert observed == [(payloads, MemoryType.CUPY.value, 7)] + + +def test_independent_cohort_copy_retains_nested_named_bundle_domains(): + from openhcs.core.aligned_image_payload import ( + AlignedImageSliceContext, AlignedImageStack, ImageOutputBundle, + ImagePayloadStackComposition, + ) + from openhcs.core.runtime_image_values import ( + ImagePayloadMetadata, image_payload_data, image_payload_mask, + ) + + pixels = np.arange(12, dtype=np.float32).reshape(3, 4) + mask = np.ones((3, 4), dtype=bool) + payload = ImagePayloadMetadata(source_image_names=("Canonical",)).payload_with(pixels, mask) + context = AlignedImageSliceContext.main_flow("Canonical") + named = ImageOutputBundle((payload,), (context,)) + nested = AlignedImageStack((named,)) + copied = ImagePayloadStackComposition.copy_whole_image( + nested, memory_type="numpy", device_id=None, + ) + assert type(copied) is AlignedImageStack + assert type(copied.slices[0]) is ImageOutputBundle + assert copied.slices[0].slice_contexts == (context,) + member = copied.slices[0].slices[0] + assert not np.shares_memory(image_payload_data(member), pixels) + assert not np.shares_memory(image_payload_mask(member), mask) + image_payload_data(member)[:] = -1 + image_payload_mask(member)[:] = False + np.testing.assert_array_equal(pixels, np.arange(12).reshape(3, 4)) + assert mask.all() diff --git a/tests/unit/test_cellprofiler_module_execution.py b/tests/unit/test_cellprofiler_module_execution.py index fdbb125ae7..80bd9b59b3 100644 --- a/tests/unit/test_cellprofiler_module_execution.py +++ b/tests/unit/test_cellprofiler_module_execution.py @@ -65,7 +65,10 @@ from openhcs.core.config import DtypeConfig from openhcs.core.compiled_step_plan import CompiledStepPlan from openhcs.core.equivalence.keys import RuntimeMeasurementSourcePair -from openhcs.core.function_patterns import InvocationArtifactInputEdgePlan +from openhcs.core.function_patterns import ( + InvocationArtifactInputEdgePlan, + MainFlowInputProjection, +) from openhcs.core.measurement_image_alignment import ( MeasurementImageLabelAlignmentStrategy, MeasurementLabelSourceAlignmentStrategy, @@ -1427,7 +1430,7 @@ def test_main_flow_image_artifact_selects_declared_alias_plane() -> None: artifact_input_edges=( replace( _artifact_input_edge_for_test(first_spec, stored=False), - consumes_main_flow=True, + main_flow_projection=MainFlowInputProjection.DECLARED_SOURCE_IMAGE, ), ), ) @@ -1482,7 +1485,7 @@ def test_single_main_flow_image_artifact_selects_declared_source_binding_plane() artifact_input_edges=( replace( _artifact_input_edge_for_test(spec, stored=False), - consumes_main_flow=True, + main_flow_projection=MainFlowInputProjection.DECLARED_SOURCE_IMAGE, ), ), ), @@ -1533,7 +1536,7 @@ def test_main_flow_image_artifact_projects_named_provenance_plane() -> None: artifact_input_edges=( replace( _artifact_input_edge_for_test(first_spec, stored=False), - consumes_main_flow=True, + main_flow_projection=MainFlowInputProjection.DECLARED_SOURCE_IMAGE, ), ), ), @@ -1578,7 +1581,7 @@ def test_main_flow_image_artifact_preserves_declared_runtime_slice_stack() -> No artifact_input_edges=( replace( _artifact_input_edge_for_test(first_spec, stored=False), - consumes_main_flow=True, + main_flow_projection=MainFlowInputProjection.DECLARED_SOURCE_IMAGE, ), ), ), @@ -1638,7 +1641,7 @@ def test_main_flow_projection_binds_singleton_broadcast_artifact_as_2d( artifact_input_edges=( replace( _artifact_input_edge_for_test(original_spec, stored=False), - consumes_main_flow=True, + main_flow_projection=MainFlowInputProjection.DECLARED_SOURCE_IMAGE, ), _artifact_input_edge_for_test( illumination_spec, @@ -1711,6 +1714,7 @@ def test_composed_measurement_images_keep_declared_aliases_and_resolved_payload( runtime, image, image_request, + image_inputs=(image_spec,), module_type=MeasureObjectIntensityModule, ) @@ -1724,6 +1728,7 @@ def test_composed_measurement_images_keep_declared_aliases_and_resolved_payload( runtime, image, None, + image_inputs=(image_spec,), module_type=MeasureObjectIntensityModule, ) @@ -1781,7 +1786,7 @@ def test_repeated_broadcast_inputs_consume_their_declared_source_group_axes() -> *( replace( _artifact_input_edge_for_test(spec, stored=False), - consumes_main_flow=True, + main_flow_projection=MainFlowInputProjection.DECLARED_SOURCE_IMAGE, ) for spec in original_specs ), @@ -1846,7 +1851,7 @@ def test_single_main_flow_image_preserves_runtime_slice_axis() -> None: artifact_input_edges=( replace( _artifact_input_edge_for_test(spec, stored=False), - consumes_main_flow=True, + main_flow_projection=MainFlowInputProjection.DECLARED_SOURCE_IMAGE, ), ), ), @@ -3160,7 +3165,7 @@ def test_object_output_measurements_derive_count_and_locations_from_output_label ArtifactSpec.input("Mask", ImageArtifactType), stored=False, ), - consumes_main_flow=True, + main_flow_projection=MainFlowInputProjection.DECLARED_SOURCE_IMAGE, ), ), output_plans=tuple( @@ -3323,7 +3328,7 @@ def test_compiled_measurement_output_preserves_image_and_object_row_ownership() artifact_input_edges=( replace( _artifact_input_edge_for_test(source_spec, stored=False), - consumes_main_flow=True, + main_flow_projection=MainFlowInputProjection.DECLARED_SOURCE_IMAGE, ), ), output_plans=tuple( @@ -4376,7 +4381,7 @@ def test_object_label_output_domain_scope_preserves_declared_source_stack() -> N artifact_input_edges=( replace( _artifact_input_edge_for_test(source_spec, stored=False), - consumes_main_flow=True, + main_flow_projection=MainFlowInputProjection.DECLARED_SOURCE_IMAGE, ), ), output_plans=tuple(_artifact_output_plan(item) for item in ((output_spec,))), @@ -4414,7 +4419,7 @@ def test_object_label_output_domain_scope_uses_declared_group_lineage() -> None: artifact_input_edges=( replace( _artifact_input_edge_for_test(source_spec, stored=False), - consumes_main_flow=True, + main_flow_projection=MainFlowInputProjection.DECLARED_SOURCE_IMAGE, ), ), output_plans=tuple(_artifact_output_plan(item) for item in ((output_spec,))), @@ -18100,3 +18105,147 @@ def test_object_measurement_execution_policy_keeps_declared_full_stack_for_2d_la ) assert mode is ImagePayloadExecutionMode.FULL_STACK + + +@pytest.mark.parametrize("image_count", (1, 2)) +def test_natural_object_measurement_resolves_one_roster_without_eager_composition( + monkeypatch, image_count +) -> None: + image_specs = tuple( + ArtifactSpec.input(name, ImageArtifactType) + for name in ("DNA", "ER")[:image_count] + ) + object_spec = ArtifactSpec.input("Cells", ObjectLabelsArtifactType) + measurement_spec = ArtifactSpec.output( + "MeasureObjectIntensity_roster", MeasurementsArtifactType + ) + labels = ObjectLabelSet( + name="Cells", + variant_data=ObjectLabelVariantData(labels=np.ones((3, 4), dtype=np.int32)), + domain=ObjectLabelDomain(declared_object_ids=(1,)), + ) + images = { + spec.name: ImagePayloadMetadata( + source_image_names=(f"Original{spec.name}",) + ).payload_with(np.full((3, 4), value, dtype=np.float32)) + for spec, value in zip(image_specs, (1, 2)[:image_count], strict=True) + } + runtime = _FakeCellProfilerRuntime( + images, + {"Cells": labels}, + artifact_input_edges=tuple( + _artifact_input_edge_for_test(spec) + for spec in (*image_specs, object_spec) + ), + artifact_output_bindings=( + (measurement_spec, _artifact_output_plan(measurement_spec)), + ), + ) + contract = _compiled_callable_contract( + MeasureObjectIntensityModule.require_callable(), + artifact_inputs=(*image_specs, object_spec), + artifact_outputs=(measurement_spec,), + ) + executor = _module_executor(contract) + monkeypatch.setattr( + CellProfilerModuleExecutor, + "_image_request", + lambda *_args, **_kwargs: pytest.fail( + "NATURAL measurements do not compose the image roster" + ), + ) + strategy_type = type( + RuntimeArtifactTypeStrategy.for_artifact_type(ImageArtifactType) + ) + original_resolution = strategy_type.raw_runtime_input_value + resolutions = [] + + def resolve(self, spec, value): + resolutions.append(spec.name) + return original_resolution(self, spec, value) + + monkeypatch.setattr(strategy_type, "raw_runtime_input_value", resolve) + policy_type = type( + MeasureObjectIntensityModule.runtime_object_measurement_row_policy() + ) + original_table_name = policy_type.table_source_image_name + fallbacks = [] + + def table_name(self, measurement_images, source_image_name): + fallbacks.append(source_image_name) + return original_table_name(self, measurement_images, source_image_name) + + monkeypatch.setattr(policy_type, "table_source_image_name", table_name) + current = np.zeros((3, 4), dtype=np.float32) + assert _run_module(executor, current, cellprofiler_runtime=runtime) is current + assert resolutions == ["DNA", "ER"][:image_count] + assert fallbacks == [None if image_count == 1 else "dna__er"] + table = next( + table for table in runtime.measurements + if table.subject == MeasurementSubject(MeasurementScope.OBJECT, "Cells") + ) + rows = table.rows.row_mappings() + assert [ + row[f"Intensity_IntegratedIntensity_{spec.name}"] + for row, spec in zip(rows, image_specs, strict=True) + ] == [12.0, 24.0][:image_count] + + +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) + output_spec = ArtifactSpec.output("SelectedPlaneIntensity", MeasurementsArtifactType) + planes = tuple( + ImagePayloadMetadata(source_image_names=(name,)).payload_with( + np.ones((3, 4), dtype=np.float32) + ) for name in ("DNA", "ER") + ) + image = ImagePayloadMetadata.compose( + planes, mode=ImagePayloadMetadataCompositionMode.STACK + ).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, + domain=ObjectLabelDomain( + scope=ObjectLabelDomainScope.PLANE, + declared_object_id_domains=((1,), (1,)), + ), + ) + runtime = _FakeCellProfilerRuntime( + {}, {"Cells": labels}, + artifact_input_edges=( + replace( + _artifact_input_edge_for_test(image_spec, stored=False), + main_flow_projection=MainFlowInputProjection.COMPLETE_PAYLOAD, + ), + _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), + ) + executor = _module_executor(_compiled_callable_contract( + MeasureObjectIntensityModule.require_callable(), + artifact_inputs=(image_spec, object_spec), artifact_outputs=(output_spec,), + )) + _activate_runtime_contract(executor.callable_contract, runtime) + original_fallback = executor._image_request( + image, runtime, module_type=MeasureObjectIntensityModule, + active_input_specs=(image_spec, object_spec), + ).source_image_name + assert original_fallback == "DNA" + monkeypatch.setattr( + CellProfilerModuleExecutor, "_image_request", + lambda *_args, **_kwargs: pytest.fail("unused composed request"), + ) + policy_type = type(MeasureObjectIntensityModule.runtime_object_measurement_row_policy()) + original_table_name = policy_type.table_source_image_name + fallbacks = [] + def table_name(self, measurement_images, source_image_name): + fallbacks.append(source_image_name) + return original_table_name(self, measurement_images, source_image_name) + monkeypatch.setattr(policy_type, "table_source_image_name", table_name) + assert _run_module(executor, image, cellprofiler_runtime=runtime) is image + assert fallbacks == [original_fallback] diff --git a/tests/unit/test_cellprofiler_module_execution_semantics.py b/tests/unit/test_cellprofiler_module_execution_semantics.py index 6a5209eaaf..70bee71ff8 100644 --- a/tests/unit/test_cellprofiler_module_execution_semantics.py +++ b/tests/unit/test_cellprofiler_module_execution_semantics.py @@ -25,6 +25,7 @@ from openhcs.core.callable_contract import CallableContract from openhcs.core.component_group_scope import RuntimeExecutionAxisScope from openhcs.core.function_patterns import ( + MainFlowInputProjection, DEFAULT_GROUP_KEY, FunctionInvocationKey, InvocationArtifactInputEdgePlan, @@ -109,7 +110,7 @@ def _runtime_with_outputs( spec=spec, storage_plan=None, projection=None, - consumes_main_flow=True, + main_flow_projection=MainFlowInputProjection.DECLARED_SOURCE_IMAGE, ) for input_index, spec in enumerate(main_flow_inputs) ) diff --git a/tests/unit/test_cellprofiler_output_record_occurrence.py b/tests/unit/test_cellprofiler_output_record_occurrence.py index 36bf1a39ea..f326dd6eef 100644 --- a/tests/unit/test_cellprofiler_output_record_occurrence.py +++ b/tests/unit/test_cellprofiler_output_record_occurrence.py @@ -63,7 +63,7 @@ def _output_record_request( spec=spec, storage_plan=None, projection=None, - consumes_main_flow=True, + main_flow_projection=MainFlowInputProjection.DECLARED_SOURCE_IMAGE, ) for input_index, spec in active_occurrences ) @@ -133,7 +133,7 @@ def test_output_source_uses_compiled_runtime_occurrence_for_repeated_roles( spec=spec, storage_plan=None, projection=None, - consumes_main_flow=True, + main_flow_projection=MainFlowInputProjection.DECLARED_SOURCE_IMAGE, ) for input_index, spec in enumerate(contract.artifact_inputs) ) diff --git a/tests/unit/test_cellprofiler_runtime_adapter.py b/tests/unit/test_cellprofiler_runtime_adapter.py index ee29bdc6ce..c4c3ebc19e 100644 --- a/tests/unit/test_cellprofiler_runtime_adapter.py +++ b/tests/unit/test_cellprofiler_runtime_adapter.py @@ -34,6 +34,7 @@ ) from openhcs.core.config import DtypeConfig from openhcs.core.function_patterns import ( + MainFlowInputProjection, DEFAULT_GROUP_KEY, FunctionInvocationKey, InvocationArtifactInputEdgePlan, @@ -829,7 +830,7 @@ def _executor_for_contract( spec=spec, storage_plan=None, projection=None, - consumes_main_flow=spec.ref() in main_flow_input_refs, + main_flow_projection=(MainFlowInputProjection.DECLARED_SOURCE_IMAGE if spec.ref() in main_flow_input_refs else None), ) ) continue diff --git a/tests/unit/test_child_measurement_producer_selection.py b/tests/unit/test_child_measurement_producer_selection.py index 43a0c28fe2..084987280c 100644 --- a/tests/unit/test_child_measurement_producer_selection.py +++ b/tests/unit/test_child_measurement_producer_selection.py @@ -101,7 +101,7 @@ def _compiled_edge(*, dynamic=False, declared=True): consumer_variable_components=ComponentSet((AllComponents.SITE,)), source_bindings=EMPTY_SOURCE_BINDINGS, available_artifacts=ArtifactSpecCollection(()), - consumes_main_flow=False, + main_flow_projection=None, ) return edge, child diff --git a/tests/unit/test_function_artifact_outputs.py b/tests/unit/test_function_artifact_outputs.py index cf86b7d7a5..2a1e632bfb 100644 --- a/tests/unit/test_function_artifact_outputs.py +++ b/tests/unit/test_function_artifact_outputs.py @@ -2891,7 +2891,7 @@ def declared_source_origin(image, *, image_to_save=None): spec=spec, storage_plan=None, projection=None, - consumes_main_flow=True, + main_flow_projection=main_flow_projection, ) invocation = invocation.with_artifact_input_edges((edge,)) @@ -3090,7 +3090,7 @@ def test_missing_source_origin_is_not_satisfied_by_an_authored_kwarg(): spec = ArtifactSpec.input("MissingImage", ImageArtifactType, parameter_name="image_to_save") executor = _declared_source_executor(spec) (original_edge,) = executor.invocation.artifact_input_edges - edge = replace(original_edge, consumes_main_flow=False, main_flow_projection=None) + edge = replace(original_edge, main_flow_projection=None) primary = ImagePayloadMetadata().payload_with(np.zeros((1, 2, 3), dtype=np.uint16)) invocation = replace(executor.invocation, kwargs=(("image_to_save", primary),)) executor = replace(executor, invocation=invocation, diff --git a/tests/unit/test_function_execution_lifecycle_anchors.py b/tests/unit/test_function_execution_lifecycle_anchors.py index 288c3eecc3..dc5e711ef5 100644 --- a/tests/unit/test_function_execution_lifecycle_anchors.py +++ b/tests/unit/test_function_execution_lifecycle_anchors.py @@ -93,7 +93,7 @@ def consume_measurements(image, *, runtime): invocation = next(compiled_pattern.iter_invocations()) assert invocation.runtime_domain is RuntimeInvocationDomain.ARTIFACT_MANAGED assert all( - edge.storage_plan is not None and not edge.consumes_main_flow + edge.storage_plan is not None and not (edge.main_flow_projection is not None) for edge in invocation.artifact_input_edges ) diff --git a/tests/unit/test_function_output_manifest_input_scope.py b/tests/unit/test_function_output_manifest_input_scope.py index bdcc32f4c2..0613070e19 100644 --- a/tests/unit/test_function_output_manifest_input_scope.py +++ b/tests/unit/test_function_output_manifest_input_scope.py @@ -21,6 +21,7 @@ SourceVoxelSpacing, ) from openhcs.core.function_patterns import ( + MainFlowInputProjection, InvocationArtifactInputEdgePlan, InvocationArtifactInputProjectionKey, compile_function_pattern, @@ -295,7 +296,7 @@ def consume(image): spec=input_spec, storage_plan=None, projection=None, - consumes_main_flow=True, + main_flow_projection=MainFlowInputProjection.DECLARED_SOURCE_IMAGE, ), ) ) diff --git a/tests/unit/test_function_patterns.py b/tests/unit/test_function_patterns.py index e9da0944c6..88b4104a3f 100644 --- a/tests/unit/test_function_patterns.py +++ b/tests/unit/test_function_patterns.py @@ -27,6 +27,7 @@ from openhcs.core.context.processing_context import ProcessingContext from openhcs.core.function_contract_metadata import FunctionContractAttribute from openhcs.core.function_patterns import ( + MainFlowInputProjection, FunctionInvocationKey, InvocationArtifactInputEdgePlan, InvocationArtifactInputProjectionKey, @@ -632,7 +633,7 @@ def consume_source(image): spec=source_spec, storage_plan=None, projection=None, - consumes_main_flow=True, + main_flow_projection=MainFlowInputProjection.DECLARED_SOURCE_IMAGE, ) invocation = invocation.with_artifact_input_edges((edge,)) group = replace(compiled.default_group, invocations=(invocation,)) @@ -669,7 +670,7 @@ def consume_source(image): spec=source, storage_plan=None, projection=None, - consumes_main_flow=False, + ) group = replace( compiled.default_group, @@ -727,7 +728,7 @@ def measure_channels(image, *, runtime): spec=source, storage_plan=None, projection=None, - consumes_main_flow=True, + main_flow_projection=MainFlowInputProjection.DECLARED_SOURCE_IMAGE, ) for edge_key, source in zip( InvocationArtifactInputProjectionKey.for_input_count( @@ -812,7 +813,7 @@ def measure_with_aggregate(image, *, runtime): spec=source, storage_plan=None, projection=None, - consumes_main_flow=True, + main_flow_projection=MainFlowInputProjection.DECLARED_SOURCE_IMAGE, ) invocation = invocation.with_artifact_input_edges((edge,)) @@ -1715,3 +1716,47 @@ def needs_grid(image, grid_dimensions): pattern, {"grid_dimensions": (3, 4)}, ) == (needs_grid, {"grid_dimensions": (3, 4), "sigma": 2}) + + +def test_artifact_binding_owner_does_not_erase_undeclared_raw_image_demand(): + source = ArtifactSpec.input("Source", ImageArtifactType) + + @artifact_inputs(source) + @runtime_adapter("runtime", lambda _request: object(), manages_artifact_inputs=True) + def consume(image, *, runtime): + return image + + compiled = compile_function_pattern(consume, {}, {}) + invocation = compiled.default_group.invocations[0] + edge = exact_input_edge( + invocation, input_index=0, spec=source, + storage_plan=ArtifactInputPlan(source.name, "/memory/source", artifact_type=ImageArtifactType), + parameter_name=None, + ) + invocation = invocation.with_artifact_input_edges((edge,)) + group = replace(compiled.default_group, invocations=(invocation,)) + assert invocation.adapter_manages_artifact_inputs + assert invocation.contract.accepts_implicit_main_flow_input + assert group.stored_primary_input_edges_for_component(ComponentGroupScope.ungrouped(), None) is None + primary = replace(edge, main_flow_projection=MainFlowInputProjection.COMPLETE_PAYLOAD) + group = replace(group, invocations=(invocation.with_artifact_input_edges((primary,)),)) + assert group.stored_primary_input_edges_for_component(ComponentGroupScope.ungrouped(), None) == (primary,) + + +def test_table_context_cannot_stand_in_for_an_image_cohort(): + table = ArtifactSpec.input("Measurements", MeasurementsArtifactType) + + @artifact_inputs(table) + @runtime_adapter("runtime", lambda _request: object(), manages_artifact_inputs=True) + def consume(*, runtime): + return None + + compiled = compile_function_pattern(consume, {}, {}) + invocation = compiled.default_group.invocations[0] + edge = exact_input_edge( + invocation, input_index=0, spec=table, + storage_plan=ArtifactInputPlan(table.name, "/memory/table", artifact_type=MeasurementsArtifactType), + parameter_name=None, + ) + group = replace(compiled.default_group, invocations=(invocation.with_artifact_input_edges((edge,)),)) + assert group.stored_primary_input_edges_for_component(ComponentGroupScope.ungrouped(), None) is None diff --git a/tests/unit/test_function_runtime_source_projection.py b/tests/unit/test_function_runtime_source_projection.py index cd8481bb90..bc74cf766e 100644 --- a/tests/unit/test_function_runtime_source_projection.py +++ b/tests/unit/test_function_runtime_source_projection.py @@ -33,6 +33,7 @@ ) from openhcs.core.component_set import ComponentSet from openhcs.core.function_patterns import ( + MainFlowInputProjection, InvocationArtifactInputEdgePlan, InvocationArtifactInputProjectionKey, RuntimeInvocationDomain, @@ -2376,7 +2377,7 @@ def consume_channel_source(image): spec=spec, storage_plan=None, projection=None, - consumes_main_flow=True, + main_flow_projection=MainFlowInputProjection.DECLARED_SOURCE_IMAGE, ) for edge_key, spec in zip( InvocationArtifactInputProjectionKey.for_input_count( @@ -2662,7 +2663,7 @@ def main_flow_with_auxiliary_source(image): spec=spec, storage_plan=None, projection=None, - consumes_main_flow=input_index == 0, + main_flow_projection=(MainFlowInputProjection.DECLARED_SOURCE_IMAGE if input_index == 0 else None), ) for input_index, (edge_key, spec) in enumerate( zip( @@ -2821,7 +2822,7 @@ def consume_channel_stack(image): spec=spec, storage_plan=None, projection=None, - consumes_main_flow=True, + main_flow_projection=MainFlowInputProjection.DECLARED_SOURCE_IMAGE, ) for edge_key, spec in zip( InvocationArtifactInputProjectionKey.for_input_count( @@ -2893,7 +2894,7 @@ def consume_selected_channels(image): spec=spec, storage_plan=None, projection=None, - consumes_main_flow=True, + main_flow_projection=MainFlowInputProjection.DECLARED_SOURCE_IMAGE, ) for edge_key, spec in zip( InvocationArtifactInputProjectionKey.for_input_count( @@ -2966,7 +2967,7 @@ def measure_neurites(image, pixel_size=1.0): spec=pixel_size_spec, storage_plan=None, projection=None, - consumes_main_flow=False, + ), ) ) @@ -3103,7 +3104,7 @@ def test_pipeline_start_main_flow_survives_prior_producer_image_input( ) == ("Primary",) assert mask_edge.spec == _MAIN_FLOW_MASK_INPUT assert mask_edge.storage_plan is not None - assert mask_edge.consumes_main_flow is False + assert (mask_edge.main_flow_projection is not None) is False assert invocation.contract.accepts_implicit_main_flow_input is True assert compiled_pattern.default_group.main_flow_input_refs is None assert tuple( @@ -4223,7 +4224,7 @@ def identify_stain_1(image): spec=input_spec, storage_plan=None, projection=None, - consumes_main_flow=True, + main_flow_projection=MainFlowInputProjection.DECLARED_SOURCE_IMAGE, ), ) ) diff --git a/tests/unit/test_function_step_execution_plan.py b/tests/unit/test_function_step_execution_plan.py index 1af5495d04..68c1894829 100644 --- a/tests/unit/test_function_step_execution_plan.py +++ b/tests/unit/test_function_step_execution_plan.py @@ -276,7 +276,7 @@ def begin_step(self, plan, input_records=()): class ExecutionStub(function_execution.FunctionStepExecutor): def __init__(self): self.context = SimpleNamespace() - self.plan = SimpleNamespace( + self.plan = _compiled_plan( step_index=3, step_name="prepared-at-compile", axis_id="A01", @@ -303,7 +303,7 @@ def _apply_sequential_filter(self, patterns_by_axis): def _prepare_groups(self, patterns_by_axis): events.append(("groups", tuple(patterns_by_axis))) return function_execution.PatternGroups.from_prepared( - {"default": ["image.tif"]} + {None: ["image.tif"]} ) def _preload_inputs_if_needed(self, grouped_patterns): diff --git a/tests/unit/test_function_step_execution_scope.py b/tests/unit/test_function_step_execution_scope.py index 9836d217d2..8299bb91f4 100644 --- a/tests/unit/test_function_step_execution_scope.py +++ b/tests/unit/test_function_step_execution_scope.py @@ -129,6 +129,65 @@ def test_runtime_axis_scope_returns_none_only_without_declared_component_axis() ) +def test_runtime_axis_scope_joins_correlated_rows_without_inventing_states() -> None: + rows = tuple( + RuntimeExecutionAxisScope.from_raw( + "A01", component=None, value=None, + fixed_component_values=((AllComponents.SITE, site), (AllComponents.TIMEPOINT, timepoint)), + ) + for site, timepoint in (("1", "3"), ("2", "4")) + ) + time_three = RuntimeExecutionAxisScope.from_raw( + "A01", component=None, value=None, + fixed_component_values=((AllComponents.TIMEPOINT, "3"),), + ) + joined = tuple( + result for row in rows + if (result := row.join_execution_cohort(time_three)) is not None + ) + assert joined == (rows[0],) + assert rows[0].join_execution_cohort(rows[1]) is None + + +def test_runtime_axis_scope_empty_context_broadcasts_to_actual_correlated_row() -> None: + empty = RuntimeExecutionAxisScope.from_raw("A01", component=None, value=None) + source = RuntimeExecutionAxisScope.from_raw( + "A01", component=None, value=None, + fixed_component_values=((AllComponents.SITE, "1"), (AllComponents.TIMEPOINT, "3")), + ) + assert empty.join_execution_cohort(source) == source + assert source.join_execution_cohort(empty) == source + assert empty.join_execution_cohort(empty) == empty + + +@pytest.mark.parametrize("axis,component,value", [ + ("B01", AllComponents.CHANNEL, "1"), + ("A01", AllComponents.SITE, "1"), + ("A01", AllComponents.CHANNEL, "2"), +]) +def test_runtime_axis_scope_cannot_join_other_execution_group(axis, component, value) -> None: + own = RuntimeExecutionAxisScope.from_raw("A01", component=AllComponents.CHANNEL, value="1") + other = RuntimeExecutionAxisScope.from_raw(axis, component=component, value=value) + assert own.join_execution_cohort(other) is None + + +def test_runtime_axis_scope_join_derives_partial_fixed_context_union() -> None: + left = RuntimeExecutionAxisScope.from_raw( + "A01", component=None, value=None, + fixed_component_values=((AllComponents.SITE, "1"), (AllComponents.TIMEPOINT, "3")), + ) + right = RuntimeExecutionAxisScope.from_raw( + "A01", component=None, value=None, + fixed_component_values=((AllComponents.TIMEPOINT, "3"), (AllComponents.Z_INDEX, "2")), + ) + expected = RuntimeExecutionAxisScope.from_raw( + "A01", component=None, value=None, + fixed_component_values=((AllComponents.SITE, "1"), (AllComponents.TIMEPOINT, "3"), (AllComponents.Z_INDEX, "2")), + ) + assert left.join_execution_cohort(right) == expected + assert right.join_execution_cohort(left) == expected + + def test_function_step_execution_scope_is_exact_closed_enum() -> None: assert [(member.name, member.value) for member in FunctionStepExecutionScope] == [ ("AXIS", "axis"), diff --git a/tests/unit/test_implicit_main_flow_compilation.py b/tests/unit/test_implicit_main_flow_compilation.py index 4027933d56..5fc5263d3f 100644 --- a/tests/unit/test_implicit_main_flow_compilation.py +++ b/tests/unit/test_implicit_main_flow_compilation.py @@ -94,6 +94,6 @@ def test_native_grouped_main_flow_compiles_into_cellprofiler_consumer(tmp_path): assert threshold_plan.compiled_function_pattern is not None invocation = next(threshold_plan.compiled_function_pattern.iter_invocations()) (edge,) = invocation.artifact_input_edges - assert edge.consumes_main_flow + assert (edge.main_flow_projection is not None) assert edge.storage_plan is None assert invocation.contract.execution_scope is FunctionStepExecutionScope.AXIS diff --git a/tests/unit/test_invocation_input_source_context_identity.py b/tests/unit/test_invocation_input_source_context_identity.py index 95c31656ce..7404681b68 100644 --- a/tests/unit/test_invocation_input_source_context_identity.py +++ b/tests/unit/test_invocation_input_source_context_identity.py @@ -98,7 +98,7 @@ def test_compiled_input_edges_own_exact_main_flow_membership() -> None: assert tuple(edge.spec for edge in edges) == (plate_template, combined_image) assert tuple(edge.storage_plan for edge in edges) == (None, None) - assert tuple(edge.consumes_main_flow for edge in edges) == (False, True) + assert tuple((edge.main_flow_projection is not None) for edge in edges) == (False, True) assert tuple(edge.main_flow_projection for edge in edges) == ( None, MainFlowInputProjection.COMPLETE_PAYLOAD, @@ -149,7 +149,7 @@ def threshold_like(image: np.ndarray) -> np.ndarray: current_image=source_payload, ).artifact_value(edge) - assert edge.consumes_main_flow is True + assert (edge.main_flow_projection is not None) is True assert edge.main_flow_projection is MainFlowInputProjection.COMPLETE_PAYLOAD assert request is source_payload assert image_payload_metadata(request).source_image_names == ("Hoechst",) @@ -235,7 +235,7 @@ def source_artifact_payload(_request, ref): request.artifact_value( replace( edges_by_ref[plate_template.ref()], - consumes_main_flow=True, + main_flow_projection=MainFlowInputProjection.DECLARED_SOURCE_IMAGE, ) ) @@ -260,7 +260,7 @@ def test_storage_backed_input_keeps_exact_runtime_authority() -> None: edge = next(edge for edge in edges if edge.spec == combined_image) assert edge.storage_plan is not None - assert edge.consumes_main_flow is False + assert (edge.main_flow_projection is not None) is False stored_payload = ImagePayloadMetadata( source_image_names=(combined_image.name,), diff --git a/tests/unit/test_loaded_pattern_group_ownership.py b/tests/unit/test_loaded_pattern_group_ownership.py index 9e07614538..f3f1421c21 100644 --- a/tests/unit/test_loaded_pattern_group_ownership.py +++ b/tests/unit/test_loaded_pattern_group_ownership.py @@ -13,7 +13,7 @@ from openhcs.core.component_group_scope import ComponentGroupScope from openhcs.core.context.processing_context import ProcessingContext from openhcs.core.function_patterns import compile_function_pattern -from openhcs.core.runtime_image_values import ImagePayloadMetadata, image_payload_data +from openhcs.core.runtime_image_values import ImagePayloadMetadata, image_payload_data, image_payload_metadata from openhcs.core.source_bindings import CompiledSourceBindingPlan, SourceBindingRuntimeContext from openhcs.core.source_image_provenance import SourceImageProvenancePlanes from openhcs.core.steps import function_runtime @@ -252,3 +252,201 @@ def test_component_artifact_admission_precedes_source_provenance_capture(monkeyp monkeypatch.setattr(function_runtime, "image_payload_metadata", lambda _payload: pytest.fail("source projection must follow artifact-plan admission")) with pytest.raises(TypeError, match="Component artifact input"): PatternGroupData.from_loaded_group(request, paths, initial, source_context) + + +def _stored_primary_fixture(*, preserves_main_flow=False): + from openhcs.constants.constants import Backend + from openhcs.core.artifacts import ( + ArtifactInputPlan, ArtifactOutputPlan, ArtifactSpec, + ArtifactSpecCollection, GroupLineageSourceRelation, ImageArtifactType, SpecialArtifactType, + ) + from openhcs.core.component_set import ComponentSet + from openhcs.core.pipeline.function_contracts import artifact_inputs, artifact_outputs + 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, source_context = _fixture() + source = ArtifactSpec.input("Canonical", ImageArtifactType) + output = ArtifactSpec.output( + "Processed", SpecialArtifactType if preserves_main_flow else ImageArtifactType, + relations=(GroupLineageSourceRelation(source=source.ref()),), + ) + + @artifact_inputs(source) + @artifact_outputs(output) + @runtime_adapter("runtime", lambda _request: object(), manages_artifact_inputs=True) + def consume(image, *, runtime): + return image + + output_plan = ArtifactOutputPlan( + name=output.name, path="/memory/processed", artifact_type=output.artifact_type, + relations=output.relations, + ) + input_plan = ArtifactInputPlan( + name=source.name, path="/memory/canonical", artifact_type=ImageArtifactType, + source_step_id=0, + ) + pattern = compile_function_pattern(consume, {}, {output_plan.ref(): output_plan}) + pattern = PathPlannerArtifactStage(PathPlanner.__new__(PathPlanner)).compile_invocation_input_edges( + pattern, + artifact_inputs={input_plan.ref(): input_plan}, + relation_source_scopes={}, + execution_group_scope=ComponentGroupScope.ungrouped(), + consumer_variable_components=ComponentSet((AllComponents.SITE,)), + main_flow_artifacts=ArtifactSpecCollection((source,)), + ) + request.execution_plan.step_index = 1 + request.execution_plan.execution_group_scope = ComponentGroupScope.ungrouped() + request.execution_plan.artifact_inputs = {input_plan.ref(): input_plan} + request.execution_plan.artifact_outputs = {output_plan.ref(): output_plan} + request.execution_plan.compiled_function_pattern = pattern + request = function_runtime.ArtifactPatternGroupExecutionRequest( + context=request.context, execution_plan=request.execution_plan, + compiled_group=pattern.default_group, pattern_group_info=input_plan.path, + component_index=7, component_count=99, + fixed_component_values=((AllComponents.Z_INDEX, "3"), (AllComponents.TIMEPOINT, "2")), + ) + payload = image_payload_metadata(payload).replace_fields( + plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + ).payload_with(image_payload_data(payload), np.ones((2, 3, 4), dtype=bool)) + value = RuntimeValue.from_spec( + source, payload, + execution_scope=RuntimeExecutionAxisScope.from_raw("A01", component=None, value=None, fixed_component_values=request.fixed_component_values), + ) + request.context.runtime_value_store.replace( + value, path=input_plan.path, backend=Backend.MEMORY.value, + ) + return request, value, source_context + + +def test_stored_primary_admission_owns_independent_buffer_and_live_primary_binding(monkeypatch): + from openhcs.constants.constants import Backend + from openhcs.core.runtime_image_values import image_payload_mask + from openhcs.interop.cellprofiler.runtime.artifact_binding import RuntimeInputBindingRequest + from tests.unit.cellprofiler_runtime_test_support import cellprofiler_runtime_adapter_for_test + + request, canonical, source_context = _stored_primary_fixture() + runtime = PatternGroupRuntime(request) + monkeypatch.setattr( + runtime, "source_workspace_projection_authority", + lambda: SimpleNamespace(projection_if_available=lambda: None), + ) + monkeypatch.setattr( + function_runtime.SourceBindingRuntimeContextRequest, "from_context", + classmethod(lambda cls, **kwargs: SimpleNamespace(runtime_context=lambda: source_context)), + ) + paths, admitted, admitted_context = request.load_input_stack(runtime) + assert paths == ["/memory/canonical"] + assert admitted_context is source_context + assert not np.shares_memory(image_payload_data(admitted), image_payload_data(canonical.data)) + assert not np.shares_memory(image_payload_mask(admitted), image_payload_mask(canonical.data)) + loaded = PatternGroupData.from_loaded_group(request, paths, admitted, admitted_context) + assert loaded.runtime_plane_count == 2 + assert loaded.runtime_plane_index == 7 + assert loaded.fixed_component_values == ((AllComponents.Z_INDEX, "3"), (AllComponents.TIMEPOINT, "2")) + replacement = replace(canonical, data=image_payload_metadata(canonical.data).payload_with( + np.full((2, 3, 4), 31, dtype=np.float32), np.zeros((2, 3, 4), dtype=bool), + )) + request.context.runtime_value_store.replace( + replacement, path=paths[0], backend=Backend.MEMORY.value, + ) + image_payload_data(admitted)[:] = 17 + image_payload_mask(admitted)[:] = False + invocation = request.compiled_group.invocations[0] + edge = invocation.artifact_input_edges[0] + adapter = cellprofiler_runtime_adapter_for_test( + runtime_value_store=request.context.runtime_value_store, + callable_contract=invocation.contract, + artifact_inputs={edge.key: edge}, axis_scope=request.axis_scope, + ) + bound = RuntimeInputBindingRequest(adapter=adapter, kwargs={}, current_image=admitted).artifact_value(edge) + assert bound is admitted + np.testing.assert_array_equal(image_payload_data(bound), 17) + np.testing.assert_array_equal(image_payload_data(replacement.data), 31) + _, next_admitted, _ = request.load_input_stack(runtime) + np.testing.assert_array_equal(image_payload_data(next_admitted), 31) + assert not np.shares_memory(image_payload_data(next_admitted), image_payload_data(replacement.data)) + assert loaded.runtime_plane_count == 2 + + +def test_checkpoint_demand_follows_preserved_input_to_native_reader(): + from openhcs.constants.constants import Backend + from openhcs.core.step_dependencies import StepInputDependency + + request, _, _ = _stored_primary_fixture() + source = replace(request.execution_plan, step_index=0, step_scope_id="source", write_backend=Backend.MEMORY.value) + preserving_request, _, _ = _stored_primary_fixture(preserves_main_flow=True) + preserving_pattern = preserving_request.execution_plan.compiled_function_pattern + preserving = replace( + source, step_index=1, step_scope_id="preserving", compiled_function_pattern=preserving_pattern, + main_input_dependency=StepInputDependency.step_output(source_step_index=0, source_step_scope_id="source"), + ) + native = replace( + source, step_index=2, step_scope_id="native", compiled_function_pattern=compile_function_pattern(_identity, {}, {}), + main_input_dependency=StepInputDependency.step_output(source_step_index=1, source_step_scope_id="preserving"), + ) + plans = {0: source, 1: preserving, 2: native} + assert source.requires_main_flow_checkpoint(plans) + assert preserving.requires_main_flow_checkpoint(plans) + native.compiled_function_pattern = source.compiled_function_pattern + assert not source.requires_main_flow_checkpoint(plans) + source.streaming_configs = {"viewer": object()} + assert source.requires_main_flow_checkpoint(plans) + + +def test_artifact_loaded_coordinates_keep_producer_authority_over_original_source(): + request, canonical, source_context = _stored_primary_fixture() + exact_coordinates = ( + (AllComponents.CHANNEL, "ProducedChannel"), + (AllComponents.Z_INDEX, "3"), + (AllComponents.TIMEPOINT, "2"), + ) + request = replace(request, fixed_component_values=exact_coordinates) + assert image_payload_metadata(canonical.data).source_provenance.with_common_scalar_identity_from_planes().source_component_metadata["channel"] == "1" + loaded = PatternGroupData.from_loaded_group( + request, ["/memory/canonical"], canonical.data, source_context, + ) + assert loaded.fixed_component_values == exact_coordinates + canonical.data.metadata.source_provenance = canonical.data.metadata.source_provenance.with_source_component_metadata({"channel": "9"}) + assert loaded.fixed_component_values == exact_coordinates + + +def test_conversion_and_sequential_filter_use_the_same_transport_admission(): + from pathlib import Path + from openhcs.constants.constants import Backend, SequentialComponents + from openhcs.core.compiled_step_plan import ( + InputConversionPlan, SequentialRuntimeFilter, SequentialRuntimeFilterPlan, + ) + + request, _, _ = _stored_primary_fixture() + plan = request.execution_plan + assert plan.stored_primary_input_edges_for_group(request.compiled_group, None) + plan.input_conversion = InputConversionPlan( + output_dir=Path("/memory/input"), backend=Backend.MEMORY.value, + uses_virtual_workspace=True, original_subdir="images", + ) + assert plan.stored_primary_input_edges_for_group(request.compiled_group, None) is None + plan.input_conversion = None + plan.sequential_filter_plan = SequentialRuntimeFilterPlan( + filters=(SequentialRuntimeFilter(SequentialComponents.TIMEPOINT, "2"),), + ) + assert plan.stored_primary_input_edges_for_group(request.compiled_group, None) is None + + +def test_same_step_primary_producer_is_not_queried_before_its_callable_runs(): + request, _, _ = _stored_primary_fixture() + plan = request.execution_plan + assert plan.stored_primary_input_edges_for_group(request.compiled_group, None) + invocation = request.compiled_group.invocations[0] + edge = invocation.artifact_input_edges[0] + later_edge = replace(edge, storage_plan=replace( + edge.storage_plan, source_step_id=plan.step_index, + source_step_scope_id=plan.step_scope_id, + )) + group = replace(request.compiled_group, invocations=( + invocation.with_artifact_input_edges((later_edge,)), + )) + assert plan.stored_primary_input_edges_for_group(group, None) is None diff --git a/tests/unit/test_opaque_main_flow_image_domain.py b/tests/unit/test_opaque_main_flow_image_domain.py index f49e3a3347..ad19e2add7 100644 --- a/tests/unit/test_opaque_main_flow_image_domain.py +++ b/tests/unit/test_opaque_main_flow_image_domain.py @@ -9,7 +9,7 @@ from openhcs.constants.constants import MEMORY_TYPE_NUMPY from openhcs.core.compiled_step_plan import CompiledStepPlan from openhcs.core.component_group_scope import ComponentGroupScope -from openhcs.core.function_patterns import compile_function_pattern +from openhcs.core.function_patterns import MainFlowInputProjection, compile_function_pattern from openhcs.core.runtime_image_values import ( ImagePayloadMetadata, image_payload_data, @@ -19,7 +19,7 @@ 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 PatternGroupRuntime +from openhcs.core.steps.function_runtime import PatternGroupExecutionRequest, PatternGroupRuntime from openhcs.core.aligned_image_payload import AlignedImageStack, ImageOutputBundle, AlignedImageSliceContext, ImagePayloadStackComposition from openhcs.core.context.processing_context import ProcessingContext from openhcs.core.steps.function_output_manifest import step_output_manifest, StepOutputManifestStore @@ -214,9 +214,9 @@ def test_save_and_next_load_preserve_domain_and_independent_cache(tmp_path, monk plan, step_index=1, step_scope_id="whole-consumer", input_dir=tmp_path, main_input_dependency=StepInputDependency.step_output(source_step_index=0, source_step_scope_id=plan.step_scope_id), ) - consumer = PatternGroupRuntime(SimpleNamespace( + consumer = PatternGroupRuntime(PatternGroupExecutionRequest( context=context, execution_plan=consumer_plan, compiled_group=pattern.default_group, - source_binding_plan=CompiledSourceBindingPlan.empty(), component_key=None, + component_index=0, component_count=1, pattern_group_info="A01_s001_w1_z{iii}_t001.tif", )) monkeypatch.setattr(consumer, "source_workspace_projection_authority", lambda: SimpleNamespace(projection_if_available=lambda: None)) @@ -323,7 +323,7 @@ def consume(image): invocation = pattern.default_group.invocations[0] invocation = invocation.with_artifact_input_edges((InvocationArtifactInputEdgePlan( key=InvocationArtifactInputProjectionKey(invocation_key=invocation.key, input_index=0), - spec=spec, storage_plan=None, projection=None, consumes_main_flow=True, + spec=spec, storage_plan=None, projection=None, main_flow_projection=MainFlowInputProjection.DECLARED_SOURCE_IMAGE, ),)) return replace(pattern, groups=(replace(pattern.default_group, invocations=(invocation,)),)) diff --git a/tests/unit/test_path_planner_materialization.py b/tests/unit/test_path_planner_materialization.py index 54c2887ff8..96a7cc729d 100644 --- a/tests/unit/test_path_planner_materialization.py +++ b/tests/unit/test_path_planner_materialization.py @@ -1141,7 +1141,7 @@ def combine_sources(image, secondary=None): edges = next(compiled.iter_invocations()).artifact_input_edges assert tuple(edge.spec for edge in edges) == source_specs - assert tuple(edge.consumes_main_flow for edge in edges) == (True, False, False) + assert tuple((edge.main_flow_projection is not None) for edge in edges) == (True, False, False) assert edges[0].storage_plan is None assert edges[0].projection is None assert edges[1].storage_plan is stored_inputs.get(source_specs[1].ref()) @@ -1311,7 +1311,7 @@ def test_implicit_native_main_flow_provenance_drives_artifact_owned_scope(): edge = next(compiled_consumer.iter_invocations()).artifact_input_edges[0] assert edge.spec == cursor assert edge.storage_plan is None - assert edge.consumes_main_flow + assert (edge.main_flow_projection is not None) def test_artifact_output_source_uses_compiled_plan_across_parameter_occurrences(): diff --git a/tests/unit/test_primary_image_carrier_compile_gate.py b/tests/unit/test_primary_image_carrier_compile_gate.py index 5056b6dd0c..c7da86749e 100644 --- a/tests/unit/test_primary_image_carrier_compile_gate.py +++ b/tests/unit/test_primary_image_carrier_compile_gate.py @@ -19,6 +19,7 @@ from openhcs.core.component_group_scope import ComponentGroupScope from openhcs.core.context.processing_context import ProcessingContext from openhcs.core.function_patterns import ( + MainFlowInputProjection, CompiledFunctionGroup, CompiledFunctionInvocation, CompiledFunctionPattern, @@ -74,7 +75,7 @@ def _compiled_pattern( ), storage_plan=None, projection=None, - consumes_main_flow=True, + main_flow_projection=MainFlowInputProjection.DECLARED_SOURCE_IMAGE, ), ) ) diff --git a/tests/unit/test_runtime_value_store.py b/tests/unit/test_runtime_value_store.py index d5535ffb0b..77669ee5c7 100644 --- a/tests/unit/test_runtime_value_store.py +++ b/tests/unit/test_runtime_value_store.py @@ -818,7 +818,13 @@ def test_runtime_artifact_input_projection_transposes_producer_stack_axis() -> N None, ) store.record( - RuntimeValue.normalize(group_plan, payload, axis_id="A01"), + RuntimeValue.normalize_for_execution_scope( + group_plan, + payload, + execution_scope=RuntimeExecutionAxisScope.from_raw( + "A01", component=AllComponents.CHANNEL, value=channel, + ), + ), path=group_plan.path, backend="memory", ) @@ -848,6 +854,14 @@ def test_runtime_artifact_input_projection_transposes_producer_stack_axis() -> N backend="memory", ) + candidates = runtime_input.candidate_execution_scopes( + store, + ComponentGroupScope.dynamic(AllComponents.SITE), + variable_components=ComponentSet((AllComponents.CHANNEL,)), + ) + assert tuple(scope.value_text for scope in candidates) == ("1", "2", "3") + assert all(scope.fixed_component_values == () for scope in candidates) + payload = runtime_input.composed_value(runtime_input.records(store)) np.testing.assert_array_equal( @@ -861,6 +875,135 @@ def test_runtime_artifact_input_projection_transposes_producer_stack_axis() -> N ) +def test_artifact_candidate_scopes_preserve_projected_site_time_correlation() -> None: + path = "/memory/processed_pixels.pkl" + variables = (AllComponents.SITE, AllComponents.TIMEPOINT) + output_plan = ArtifactOutputPlan( + name="image", path=path, artifact_type=ImageArtifactType, + variable_components=variables, + ) + storage_plan = ArtifactInputPlan( + name="image", path=path, artifact_type=ImageArtifactType, + variable_components=variables, + ) + payload = ImagePayloadMetadata( + plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + source_image_provenance_planes=SourceImageProvenancePlanes.from_components( + paths=("/source/original_first.tif", "/source/original_second.tif"), + component_metadata=( + {"site": "1", "timepoint": "3"}, + {"site": "2", "timepoint": "4"}, + ), + ), + ).payload_with( + np.stack((np.full((2, 2), 17.0), np.full((2, 2), 29.0))), + np.asarray([[[True, False], [False, True]], [[False, True], [True, False]]]), + ) + store = RuntimeValueStore() + store.record(RuntimeValue.normalize(output_plan, payload, axis_id="A01"), + path=path, backend="memory") + edge = _runtime_input_edge( + storage_plan, + invocation_scope=ComponentGroupScope.dynamic(AllComponents.SITE), + producer_selection_scope=ComponentGroupScope.ungrouped(), + component_scopes=( + ComponentGroupScope.dynamic(AllComponents.SITE), + ComponentGroupScope.from_raw(("3",), component=AllComponents.TIMEPOINT), + ), + consumer_variable_components=(), + ) + runtime_input = RuntimeArtifactInput( + edge_plan=edge, backend="memory", + axis_scope=RuntimeExecutionAxisScope.from_raw("A01", component=None, value=None), + ) + candidates = runtime_input.candidate_execution_scopes( + store, ComponentGroupScope.dynamic(AllComponents.SITE), + variable_components=ComponentSet(), + ) + (coordinates,) = tuple(candidates) + assert coordinates.value_text == "1" + assert coordinates.fixed_component_values == ((AllComponents.TIMEPOINT, "3"),) + assert candidates[coordinates] == path + selected = replace(runtime_input, axis_scope=coordinates).resolve_value(store) + np.testing.assert_array_equal(image_payload_data(selected), np.full((2, 2), 17.0)) + np.testing.assert_array_equal(selected.mask, payload.mask[0]) + + +def test_artifact_candidate_scopes_keep_actual_producer_group_as_fixed_context() -> None: + path = "/memory/produced_pixels.pkl" + output_plan = ArtifactOutputPlan( + name="image", path=path, artifact_type=ImageArtifactType, + group_component=AllComponents.CHANNEL, group_keys=("1",), + ) + storage_plan = ArtifactInputPlan( + name="image", path=path, artifact_type=ImageArtifactType, + group_component=AllComponents.CHANNEL, group_keys=("1",), + ) + payload = ImagePayloadMetadata( + source_path="/source/old_filename_w9.tif", + source_component_metadata={"site": "2", "timepoint": "3", "channel": "1", "z_index": "1"}, + ).payload_with(np.full((2, 2), 17.0), None) + store = RuntimeValueStore() + record = store.record( + RuntimeValue.normalize_for_execution_scope( + output_plan, payload, + execution_scope=RuntimeExecutionAxisScope.from_raw( + "A01", component=AllComponents.CHANNEL, value="1", + fixed_component_values=((AllComponents.SITE, "2"), (AllComponents.TIMEPOINT, "3"), (AllComponents.Z_INDEX, "1")), + ), + ), + path=path, backend="memory", + ) + runtime_input = RuntimeArtifactInput( + edge_plan=_runtime_input_edge( + storage_plan, + invocation_scope=ComponentGroupScope.dynamic(AllComponents.SITE), + producer_selection_scope=storage_plan.producer_group_scope(), + component_scopes=(ComponentGroupScope.dynamic(AllComponents.SITE),), + consumer_variable_components=(), + ), + axis_scope=RuntimeExecutionAxisScope.from_raw("A01", component=None, value=None), + backend="memory", + ) + candidates = runtime_input.candidate_execution_scopes( + store, ComponentGroupScope.dynamic(AllComponents.SITE), + variable_components=ComponentSet(), + ) + (coordinates,) = tuple(candidates) + assert coordinates.value_text == "2" + assert dict(coordinates.fixed_component_values) == { + AllComponents.CHANNEL: "1", AllComponents.TIMEPOINT: "3", AllComponents.Z_INDEX: "1", + } + selected = replace(runtime_input, axis_scope=coordinates).records(store) + assert len(selected) == 1 + assert selected[0] is record + + second_payload = ImagePayloadMetadata( + source_path="/source/another_original.tif", + source_component_metadata={"site": "2", "timepoint": "3", "channel": "1", "z_index": "2"}, + ).payload_with(np.full((2, 2), 29.0), None) + second_record = store.record( + RuntimeValue.normalize_for_execution_scope( + output_plan, second_payload, + execution_scope=RuntimeExecutionAxisScope.from_raw( + "A01", component=AllComponents.CHANNEL, value="1", + fixed_component_values=((AllComponents.SITE, "2"), (AllComponents.TIMEPOINT, "3"), (AllComponents.Z_INDEX, "2")), + ), + ), + path=path, backend="memory", + ) + candidates = runtime_input.candidate_execution_scopes( + store, ComponentGroupScope.dynamic(AllComponents.SITE), + variable_components=ComponentSet(), + ) + assert len(candidates) == 2 + selected_records = tuple( + replace(runtime_input, axis_scope=scope).records(store) + for scope in candidates + ) + assert selected_records == ((record,), (second_record,)) + + def test_runtime_artifact_input_projection_ignores_scalar_pixel_contributors() -> None: path = "/memory/scalar_image.pkl" output_plan = ArtifactOutputPlan( From 71497e22887efc5275471a73d677f6c22490bc06 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 16:17:19 -0400 Subject: [PATCH 149/235] Respect adapter-owned measurement rosters at compilation boundaries Refs #547. Preserve explicit object selection and let the recording owner validate named row domains. --- openhcs/core/artifacts.py | 8 ++-- openhcs/core/callable_contract.py | 4 ++ .../module_artifact_declarations.py | 23 ++++++++++ .../test_measurement_declaration_journey.py | 24 +++++++++-- ..._artifact_input_parameter_normalization.py | 25 ++++++++++- tests/unit/test_artifact_output_ownership.py | 43 ++++++++++++++++++- 6 files changed, 118 insertions(+), 9 deletions(-) diff --git a/openhcs/core/artifacts.py b/openhcs/core/artifacts.py index 72e5e23257..6f1a8844f5 100644 --- a/openhcs/core/artifacts.py +++ b/openhcs/core/artifacts.py @@ -737,13 +737,13 @@ def require_output_subject( def validate_output_declaration(cls, spec: "ArtifactSpec") -> None: """Reject incompatible row domains before payload-owner validation. - Source-qualified image rows share one image-set domain. Their recording - owner determines whether multiple subjects can occupy the output; native - and ordinary recorded tables still require one exact subject. + The payload owner determines whether distinct named subjects can occupy + the output. Native and ordinary recorded tables require one exact + subject; row-owning recorders preserve each subject's identity. """ subjects = ArtifactSpecRelation.measurement_subjects_for_output(spec) - if len({subject.row_identity_domain for subject in subjects}) > 1: + if len({(subject.scope, subject.id_field) for subject in subjects}) > 1: ArtifactSpecRelation.measurement_subject_for_output(spec) @classmethod diff --git a/openhcs/core/callable_contract.py b/openhcs/core/callable_contract.py index d3096e6eb8..4e39256c4a 100644 --- a/openhcs/core/callable_contract.py +++ b/openhcs/core/callable_contract.py @@ -1442,6 +1442,10 @@ def validate_artifact_input_parameter_bindings(self) -> None: continue if ( len(bound_specs) > 1 + and not ( + self.runtime_adapter is not None + and self.runtime_adapter.manages_artifact_inputs + ) and not special_input_parameter_accepts_sequence(parameter) and any( spec.artifact_type is not ImageArtifactType for spec in bound_specs diff --git a/openhcs/interop/cellprofiler/module_artifact_declarations.py b/openhcs/interop/cellprofiler/module_artifact_declarations.py index 5142146a0c..3d0bd9b9a7 100644 --- a/openhcs/interop/cellprofiler/module_artifact_declarations.py +++ b/openhcs/interop/cellprofiler/module_artifact_declarations.py @@ -65,6 +65,7 @@ from openhcs.core.function_patterns import ( FunctionInvocationKey, + NormalizedFunctionItem, ) from openhcs.core.runtime_image_values import ImagePayloadMetadata from openhcs.core.runtime_tabular_values import ColumnarRows @@ -1312,6 +1313,28 @@ class ObjectMeasurementInputModule( object_measurement_binding, ) + @classmethod + def finalize_module_blocks_for_invocation( + cls, + blocks: tuple["ModuleBlock", ...], + *, + invocation: "NormalizedFunctionItem", + step_context: "ArtifactDeclarationStepContext", + ) -> tuple["ModuleBlock", ...]: + """Require selection intent before reconstructing multiple object subjects.""" + + blocks = super().finalize_module_blocks_for_invocation( + blocks, invocation=invocation, step_context=step_context, + ) + binding = cls.object_measurement_binding + selector = binding.require_parameter_name() + if selector not in invocation.kwargs_dict: + for block in blocks: + subjects = cls.artifact_names_for_binding(block, binding) + if len(subjects) > 1: + return () + return blocks + @classmethod def processing_group_scope_inputs( cls, diff --git a/tests/integration/test_measurement_declaration_journey.py b/tests/integration/test_measurement_declaration_journey.py index bbcf1dd880..55c9a61775 100644 --- a/tests/integration/test_measurement_declaration_journey.py +++ b/tests/integration/test_measurement_declaration_journey.py @@ -4,6 +4,7 @@ from dataclasses import dataclass, replace from csv import DictReader +from inspect import unwrap import numpy as np import pytest @@ -82,6 +83,10 @@ CellProfilerThresholdMethod, ) from openhcs.processing.backends.lib_registry.unified_registry import ProcessingContract +from openhcs.processing.custom_functions.runtime_registry import ( + CustomFunctionRuntimeRegistry, + register_custom_function, +) from openhcs.processing.materialization import CsvOptions, MaterializationSpec @@ -136,6 +141,17 @@ def count_with_image_subject(image): ) +@pytest.fixture +def registered_count_callable(valid): + function = count_with_image_subject if valid else count_without_subject + registered = register_custom_function(function) + try: + assert unwrap(registered) is unwrap(function) + yield registered + finally: + CustomFunctionRuntimeRegistry.remove(function.__name__) + + def _binding_name(module, plan_type, artifact_type): (binding,) = module.declared_artifact_bindings( plan_type=plan_type, artifact_type=artifact_type @@ -329,7 +345,7 @@ def test_exact_secondary_selector_survives_authoring_compile_and_execution(tmp_p def test_omitted_secondary_selector_still_fails_closed(tmp_path): _write_plate(tmp_path) - with pytest.raises(ValueError, match="labels.*multiple exact artifact occurrences"): + with pytest.raises(ValueError, match="cannot reconstruct an exact module block"): _compile( tmp_path, _document(selected=False), @@ -355,7 +371,9 @@ def test_omitted_selector_remains_valid_for_one_label_producer(tmp_path): @pytest.mark.parametrize("valid", [False, True]) -def test_headless_entrypoint_requires_subject_and_executes_corrected_rows(tmp_path, valid): +def test_headless_entrypoint_requires_subject_and_executes_corrected_rows( + tmp_path, valid, registered_count_callable, +): _write_plate(tmp_path) document = PipelineDocumentAuthority.from_values( pipeline_config=PipelineConfig( @@ -365,7 +383,7 @@ def test_headless_entrypoint_requires_subject_and_executes_corrected_rows(tmp_pa ), ), pipeline_steps=[_step( - count_with_image_subject if valid else count_without_subject, + registered_count_callable, "Count pixels", {}, )], ) diff --git a/tests/unit/test_artifact_input_parameter_normalization.py b/tests/unit/test_artifact_input_parameter_normalization.py index b2fe6c88ac..45ce2d4f71 100644 --- a/tests/unit/test_artifact_input_parameter_normalization.py +++ b/tests/unit/test_artifact_input_parameter_normalization.py @@ -4,13 +4,14 @@ import pytest -from openhcs.core.artifacts import ArtifactSpec, SpecialArtifactType +from openhcs.core.artifacts import ArtifactSpec, ObjectLabelsArtifactType, SpecialArtifactType from openhcs.core.callable_contract import CallableContract from openhcs.core.function_patterns import compile_function_pattern from openhcs.core.invocation_artifacts import ( InvocationContractPlan, InvocationContractProvider, ) +from openhcs.core.runtime_adapters import runtime_adapter from openhcs.core.pipeline.function_contracts import ( artifact_inputs, special_input_names_from_callable, @@ -61,6 +62,28 @@ def consume(image, *, labels=None): assert received == [None] +@pytest.mark.parametrize("managed", [False, True]) +def test_scalar_artifact_roster_cardinality_belongs_to_its_runtime_binder(managed) -> None: + inputs = tuple( + ArtifactSpec.input(name, ObjectLabelsArtifactType, parameter_name="labels") + for name in ("Nuclei", "Cells") + ) + + @runtime_adapter("runtime", lambda request: object(), manages_artifact_inputs=managed) + @artifact_inputs(*inputs) + def consume(image, *, labels, runtime): + raise AssertionError("Contract admission must not execute the callable") + + if not managed: + with pytest.raises(ValueError, match="labels.*multiple exact artifact occurrences"): + compile_function_pattern(consume, {}, {}) + return + compiled = compile_function_pattern(consume, {}, {}) + (invocation,) = compiled.default_group.invocations + assert invocation.contract.artifact_inputs.specs == inputs + assert invocation.contract.artifact_input_parameter_names == ("labels",) + + def test_artifact_spec_only_parameter_declaration_compiles() -> None: labels = ArtifactSpec.input( "StoredLabels", diff --git a/tests/unit/test_artifact_output_ownership.py b/tests/unit/test_artifact_output_ownership.py index 34dd5785d4..ea1f9d71d1 100644 --- a/tests/unit/test_artifact_output_ownership.py +++ b/tests/unit/test_artifact_output_ownership.py @@ -192,7 +192,12 @@ def measure(image, labels=None, *, runtime): compile_function_pattern(measure, {}, {}) # Output obligations remain mandatory once input selection is unambiguous. - with pytest.raises(ValueError, match="multiple measurement subjects"): + obligation = ( + "declared measurement_feature_owner" + if output_policy is CellProfilerRecordedArtifactOutputPolicy + else "multiple measurement subjects" + ) + with pytest.raises(ValueError, match=obligation): CallableContract.from_callable(measure).validate_artifact_output_declarations() @@ -295,6 +300,42 @@ def measure(image, *, runtime): assert compiled is not None +@pytest.mark.parametrize("policy", [ + NativeReturnArtifactOutputPolicy, + AdapterRecordedArtifactOutputPolicy, + CellProfilerRecordedArtifactOutputPolicy, +]) +def test_measurement_row_owner_controls_named_object_roster_admission(policy): + from openhcs.core.artifacts import ArtifactSpecRelation + from openhcs.processing.backends.cellprofiler.intensity import MeasureObjectIntensityModule + + objects = tuple( + ArtifactSpec.input(name, ObjectLabelsArtifactType) + for name in ("Nuclei", "Cells") + ) + measurements = ArtifactSpec.output( + "Measurements", MeasurementsArtifactType, + relations=tuple(ObjectMeasurementSubjectRelation(spec.ref()) for spec in objects), + measurement_feature_owner=MeasureObjectIntensityModule, + ) + + @runtime_adapter("runtime", lambda request: object(), artifact_output_policy=policy) + @artifact_inputs(*objects) + @artifact_outputs(measurements) + def measure(image, *, runtime): + raise AssertionError("Compile admission must not execute a callable") + + if policy is not CellProfilerRecordedArtifactOutputPolicy: + with pytest.raises(ValueError, match="multiple measurement subjects"): + compile_function_pattern(measure, {}, {}) + return + compiled = compile_function_pattern(measure, {}, {}) + subjects = ArtifactSpecRelation.measurement_subjects_for_output(measurements) + assert tuple(subject.name for subject in subjects) == ("Nuclei", "Cells") + assert len({subject.row_identity_domain for subject in subjects}) == 2 + assert compiled.default_group.invocations[0].contract.artifact_outputs.specs == (measurements,) + + def test_real_cellprofiler_declaration_compiles_without_a_table_wide_subject(): """The real module/provider declares the owner, not a compiler exemption.""" from openhcs.core.compiled_step_plan import CompiledStepPlan From 27b2aa1bcbb4ed9b9251c6b00c301220c959c7af Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 16:17:19 -0400 Subject: [PATCH 150/235] Consolidate runtime ownership and save preserved outputs to current destinations Refs #479 and #536. Delete single-use runtime carriers, place producer discovery on the executor, and separate original memory addresses from physical publication destinations. --- openhcs/core/steps/function_execution.py | 213 +++++++++++------------ openhcs/core/steps/function_outputs.py | 8 +- openhcs/core/steps/function_runtime.py | 53 ++---- tests/unit/test_function_outputs.py | 44 +++++ 4 files changed, 168 insertions(+), 150 deletions(-) diff --git a/openhcs/core/steps/function_execution.py b/openhcs/core/steps/function_execution.py index 81dedd7ae3..28fb32c4a5 100644 --- a/openhcs/core/steps/function_execution.py +++ b/openhcs/core/steps/function_execution.py @@ -238,111 +238,6 @@ def execution_requests( ) return tuple(requests) - @classmethod - def artifact_execution_requests( - cls, - context: ProcessingContext, - plan: CompiledStepPlan, - ) -> tuple[ArtifactPatternGroupExecutionRequest, ...] | None: - """Discover complete correlated cohorts from their compiled producers.""" - - pattern = plan.compiled_function_pattern - scope = plan.execution_group_scope - - def candidate_scopes( - edge: InvocationArtifactInputEdgePlan, - execution_scope: ComponentGroupScope, - ) -> dict[RuntimeExecutionAxisScope, str]: - return RuntimeArtifactInput( - edge_plan=edge, - axis_scope=RuntimeExecutionAxisScope.from_raw( - plan.axis_id, component=None, value=None, - ), - backend=Backend.MEMORY.value, - source_binding_plan=plan.source_binding_plan, - ).candidate_execution_scopes( - context.runtime_value_store, - execution_scope, - variable_components=ComponentSet.coerce(plan.variable_components), - ) - - if scope.is_dynamic: - discovered_keys = [] - for group in pattern.groups: - for invocation in group.invocations: - group_refs = invocation.contract.group_scope_inputs.ref_set() - for edge in invocation.artifact_input_edges: - if edge.storage_plan is None or not ( - edge.main_flow_projection is not None - or edge.spec.ref() in group_refs - ): - continue - discovered_keys.extend( - candidate.value_text - for candidate in candidate_scopes(edge, scope) - ) - if not any(key is not None for key in discovered_keys): - return None - component_keys = scope.runtime_keys(discovered_keys) - else: - component_keys = scope.runtime_keys(()) - - selected_groups = [] - for component_key in component_keys: - group = pattern.group_for_component(component_key) - if group is None: - raise ValueError( - f"No compiled function group for component {component_key!r}." - ) - edges = plan.stored_primary_input_edges_for_group(group, component_key) - if edges is None: - return None - selected_groups.append((component_key, group, edges)) - - requests = [] - for component_index, (component_key, group, edges) in enumerate(selected_groups): - cohorts = None - for edge in edges: - edge_cohorts = candidate_scopes( - edge, - ComponentGroupScope.from_raw((component_key,), component=scope.component), - ) - if not edge_cohorts: - raise ValueError( - f"Step {plan.step_index} ({plan.step_name}) has no exact " - f"producer cohort for {edge.spec.ref()!r} on axis {plan.axis_id}." - ) - cohorts = ( - edge_cohorts - if cohorts is None - else { - joined: anchor - for coordinates, anchor in cohorts.items() - for candidate in edge_cohorts - if (joined := coordinates.join_execution_cohort(candidate)) - is not None - } - ) - if not cohorts: - raise ValueError( - f"Step {plan.step_index} ({plan.step_name}) producer inputs " - "do not share a complete execution cohort." - ) - requests.extend( - ArtifactPatternGroupExecutionRequest( - context=context, - execution_plan=plan, - pattern_group_info=anchor, - compiled_group=group, - component_value=component_key, - fixed_component_values=coordinates.fixed_component_values, - component_index=component_index, - component_count=len(selected_groups), - ) - for coordinates, anchor in cohorts.items() - ) - return tuple(requests) - def _single_execution_group_patterns( patterns: DiscoveredPatternCollection, @@ -854,6 +749,110 @@ def execute( ) raise + def artifact_execution_requests( + self, + ) -> tuple[ArtifactPatternGroupExecutionRequest, ...] | None: + """Discover complete correlated cohorts from their compiled producers.""" + + context = self.context + plan = self.plan + pattern = plan.compiled_function_pattern + scope = plan.execution_group_scope + + def candidate_scopes( + edge: InvocationArtifactInputEdgePlan, + execution_scope: ComponentGroupScope, + ) -> dict[RuntimeExecutionAxisScope, str]: + return RuntimeArtifactInput( + edge_plan=edge, + axis_scope=RuntimeExecutionAxisScope.from_raw( + plan.axis_id, component=None, value=None, + ), + backend=Backend.MEMORY.value, + source_binding_plan=plan.source_binding_plan, + ).candidate_execution_scopes( + context.runtime_value_store, + execution_scope, + variable_components=ComponentSet.coerce(plan.variable_components), + ) + + if scope.is_dynamic: + discovered_keys = [] + for group in pattern.groups: + for invocation in group.invocations: + group_refs = invocation.contract.group_scope_inputs.ref_set() + for edge in invocation.artifact_input_edges: + if edge.storage_plan is None or not ( + edge.main_flow_projection is not None + or edge.spec.ref() in group_refs + ): + continue + discovered_keys.extend( + candidate.value_text + for candidate in candidate_scopes(edge, scope) + ) + if not any(key is not None for key in discovered_keys): + return None + component_keys = scope.runtime_keys(discovered_keys) + else: + component_keys = scope.runtime_keys(()) + + selected_groups = [] + for component_key in component_keys: + group = pattern.group_for_component(component_key) + if group is None: + raise ValueError( + f"No compiled function group for component {component_key!r}." + ) + edges = plan.stored_primary_input_edges_for_group(group, component_key) + if edges is None: + return None + selected_groups.append((component_key, group, edges)) + + requests = [] + for component_index, (component_key, group, edges) in enumerate(selected_groups): + cohorts = None + for edge in edges: + edge_cohorts = candidate_scopes( + edge, + ComponentGroupScope.from_raw((component_key,), component=scope.component), + ) + if not edge_cohorts: + raise ValueError( + f"Step {plan.step_index} ({plan.step_name}) has no exact " + f"producer cohort for {edge.spec.ref()!r} on axis {plan.axis_id}." + ) + cohorts = ( + edge_cohorts + if cohorts is None + else { + joined: anchor + for coordinates, anchor in cohorts.items() + for candidate in edge_cohorts + if (joined := coordinates.join_execution_cohort(candidate)) + is not None + } + ) + if not cohorts: + raise ValueError( + f"Step {plan.step_index} ({plan.step_name}) producer inputs " + "do not share a complete execution cohort." + ) + requests.extend( + ArtifactPatternGroupExecutionRequest( + context=context, + execution_plan=plan, + pattern_group_info=anchor, + compiled_group=group, + component_value=component_key, + fixed_component_values=coordinates.fixed_component_values, + component_index=component_index, + component_count=len(selected_groups), + ) + for coordinates, anchor in cohorts.items() + ) + return tuple(requests) + def run(self) -> StepExecutionObservation: plan = self.plan step_started_at = time.perf_counter() @@ -865,9 +864,7 @@ def run(self) -> StepExecutionObservation: ) phase_started_at = time.perf_counter() - execution_requests = PatternGroups.artifact_execution_requests( - self.context, plan, - ) + execution_requests = self.artifact_execution_requests() patterns_by_axis = ( self._detect_patterns() if execution_requests is None else None ) diff --git a/openhcs/core/steps/function_outputs.py b/openhcs/core/steps/function_outputs.py index 54cff5625d..1e57e0ff4b 100644 --- a/openhcs/core/steps/function_outputs.py +++ b/openhcs/core/steps/function_outputs.py @@ -200,6 +200,10 @@ def write_if_needed( memory_paths, Backend.MEMORY.value, ) + output_paths = [ + record.path_under(plan.output_dir) + for record in produced_outputs + ] parser_context = FunctionOutputParserContext.from_processing_context(context) row, col = parser_context.parser.extract_component_coordinates(plan.axis_id) context.filemanager.ensure_directory( @@ -207,8 +211,8 @@ def write_if_needed( plan.write_backend, ) context.filemanager.save_batch( - cls.payloads(memory_data, memory_paths, plan), - memory_paths, + cls.payloads(memory_data, output_paths, plan), + output_paths, plan.write_backend, chunk_name=plan.axis_id, zarr_config=plan.zarr_config, diff --git a/openhcs/core/steps/function_runtime.py b/openhcs/core/steps/function_runtime.py index 0caa0f1418..2888357413 100644 --- a/openhcs/core/steps/function_runtime.py +++ b/openhcs/core/steps/function_runtime.py @@ -1990,14 +1990,16 @@ def execute( debug_sink: DebugEventSink | None = None, ) -> RuntimePayload | NoMainFlowOutput: memory_types = self.memory_types() - source_payload = MainFlowMemoryConversion( - payload=self.main_data_arg, + target_device_id = self.group_data.execution_plan.device_id_for( + memory_types.input_type + ) + converted_data = convert_memory( + data=image_payload_data(self.main_data_arg), source_type=self.source_memory_type, target_type=memory_types.input_type, - target_device_id=self.group_data.execution_plan.device_id_for( - memory_types.input_type - ), - ).converted_payload() + gpu_id=target_device_id, + ) + source_payload = with_image_payload_data(self.main_data_arg, converted_data) main_data_arg = self.main_flow_call_argument(source_payload) final_kwargs = dict(self.base_kwargs) self.bind_compiled_runtime_parameters(final_kwargs) @@ -2357,39 +2359,6 @@ def from_invocation( return cls(invocation.input_memory_type, invocation.output_memory_type) -@dataclass(frozen=True, slots=True) -class VariableComponentNames: - """Microscope parser variable-component names for pattern lookup.""" - - components: Sequence[VariableComponents] - - @property - def value(self) -> list[str] | None: - if not self.components: - return None - return [component.value for component in self.components] - - -@dataclass(frozen=True, slots=True) -class MainFlowMemoryConversion: - """Main-flow image memory conversion preserving payload context.""" - - payload: RuntimeArrayData - source_type: str - target_type: str - target_device_id: int | None - - def converted_payload(self) -> RuntimeArrayData: - data = image_payload_data(self.payload) - converted = convert_memory( - data=data, - source_type=self.source_type, - target_type=self.target_type, - gpu_id=self.target_device_id, - ) - return with_image_payload_data(self.payload, converted) - - class PatternGroupRuntime: """Staged runtime for one pattern group.""" @@ -2592,7 +2561,11 @@ def _load_input_stack( request.pattern_group_info, context.filemanager, Backend.MEMORY.value, - VariableComponentNames(plan.variable_components).value, + ( + [component.value for component in plan.variable_components] + if plan.variable_components + else None + ), ) matching_files = output_manifest.filter_to_producer_paths( plan, diff --git a/tests/unit/test_function_outputs.py b/tests/unit/test_function_outputs.py index 3af656fd44..42074077e2 100644 --- a/tests/unit/test_function_outputs.py +++ b/tests/unit/test_function_outputs.py @@ -433,6 +433,50 @@ def record_output_path( ) +def test_memory_output_writer_materializes_preserved_source_at_current_destination( + tmp_path, +): + source_dir = tmp_path / "source" + source_dir.mkdir() + source_path = source_dir / "A01_s1_w1.tif" + original_pixels = np.full((4, 5), 7, dtype=np.uint16) + tifffile.imwrite(source_path, original_pixels) + memory_pixels = np.full((4, 5), 3, dtype=np.uint16) + filemanager = FileManager( + { + Backend.DISK.value: DiskStorageBackend(), + Backend.MEMORY.value: MemoryStorageBackend(), + } + ) + filemanager.ensure_directory(source_dir, Backend.MEMORY.value) + filemanager.save(memory_pixels, str(source_path), Backend.MEMORY.value) + context = context_stub(filemanager) + source_plan = function_step_plan("Source", pipeline_position=0) + source_plan.output_dir = source_dir + record_output_path(context, source_plan, source_path) + + plan = function_step_plan("Preserve image", pipeline_position=1) + plan.output_plate_root = str(tmp_path / "output_plate") + plan.output_dir = Path(plan.output_plate_root) / "images" + plan.sub_dir = "images" + plan.analysis_results_dir = str(Path(plan.output_plate_root) / "results") + plan.write_backend = Backend.DISK.value + manifest = step_output_manifest(context) + (source_record,) = manifest.image_records_for(source_plan) + manifest.record_outputs(plan, (source_record.passed_through(plan),)) + + MemoryOutputWriter.write_if_needed(context, plan) + + destination = plan.output_dir / source_path.name + np.testing.assert_array_equal(tifffile.imread(destination), memory_pixels) + np.testing.assert_array_equal(tifffile.imread(source_path), original_pixels) + np.testing.assert_array_equal( + filemanager.load(str(source_path), Backend.MEMORY.value), memory_pixels + ) + (target,) = OpenHCSMetadataWriter.OutputTarget.for_execution(context, plan) + assert target.produced_projection_entries(context, plan) is not None + + def test_function_output_identity_preserves_non_axis_source_metadata() -> None: identity = FunctionOutputIdentity( component_values={"channel": "2", "site": "1"}, From c50a947f42393914d37ee3bbe5854f22f13a9237 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 16:27:30 -0400 Subject: [PATCH 151/235] Keep exact producer group selection distinct from image-set context Refs #479 and #547. A channel-0 labels producer can feed a channel-2 image consumer through its exact declared edge. Preserve site, time, fixed Z, address and genuine fixed-channel constraints. --- openhcs/core/runtime_stores.py | 7 +- tests/unit/test_runtime_value_store.py | 159 +++++++++++++++++++++++++ 2 files changed, 165 insertions(+), 1 deletion(-) diff --git a/openhcs/core/runtime_stores.py b/openhcs/core/runtime_stores.py index 5d270aad09..bd3059d90b 100644 --- a/openhcs/core/runtime_stores.py +++ b/openhcs/core/runtime_stores.py @@ -703,7 +703,12 @@ def _matches_execution_scope( context_components = ComponentSet.collect( (component for component, _value in self.axis_scope.fixed_component_values), (component for component, _value in record_scope.fixed_component_values), - ).excluding(projected_components) + ).excluding( + ComponentSet.collect( + projected_components, + (self.edge_plan.storage_plan.producer_group_scope().component,), + ) + ) identity_policy = self._source_context_identity_policy() context_components = ( context_components diff --git a/tests/unit/test_runtime_value_store.py b/tests/unit/test_runtime_value_store.py index 77669ee5c7..851a0efcd0 100644 --- a/tests/unit/test_runtime_value_store.py +++ b/tests/unit/test_runtime_value_store.py @@ -1703,6 +1703,165 @@ def test_declared_paired_channel_label_input_matches_image_set_context(): assert runtime_input.resolve_value(store).name == "Nuclei" +def _grouped_label_input_with_distinct_consumer_channel(): + path = "/memory/Cells_channel_0.pkl" + storage_plan = ArtifactInputPlan( + name="Cells", + path=path, + artifact_type=ObjectLabelsArtifactType, + group_component=AllComponents.CHANNEL, + group_keys=("0",), + paths_by_group={"0": path}, + source_step_id=20, + ) + output_plan = ArtifactOutputPlan( + name=storage_plan.name, + path=path, + artifact_type=storage_plan.artifact_type, + group_component=storage_plan.group_component, + group_keys=storage_plan.group_keys, + paths_by_group=storage_plan.paths_by_group, + ).for_group("0") + value = RuntimeValue.normalize_for_execution_scope( + output_plan, + ObjectLabelSet( + name="Cells", + variant_data=ObjectLabelVariantData(labels=np.ones((2, 2), dtype=np.uint16)), + ), + execution_scope=RuntimeExecutionAxisScope.from_raw( + "W001", component=AllComponents.CHANNEL, value="0", + fixed_component_values=( + (AllComponents.SITE, "1"), (AllComponents.TIMEPOINT, "1"), + ), + ), + ) + store = RuntimeValueStore() + record = store.record(value, path=path, backend="memory") + runtime_input = RuntimeArtifactInput( + edge_plan=_runtime_input_edge( + storage_plan, + invocation_scope=ComponentGroupScope.ungrouped(), + producer_selection_scope=storage_plan.producer_group_scope(), + component_scopes=(), + consumer_variable_components=(AllComponents.Z_INDEX,), + ), + axis_scope=RuntimeExecutionAxisScope.from_raw( + "W001", component=None, value=None, + fixed_component_values=( + (AllComponents.SITE, "1"), (AllComponents.CHANNEL, "2"), + (AllComponents.TIMEPOINT, "1"), + ), + ), + backend="memory", + ) + return store, record, runtime_input + + +def test_exact_producer_group_does_not_constrain_consumer_source_channel(): + store, record, runtime_input = _grouped_label_input_with_distinct_consumer_channel() + + assert runtime_input.records(store) == (record,) + assert runtime_input.resolve_value(store) is record.value.data + candidates = runtime_input.candidate_execution_scopes( + store, ComponentGroupScope.ungrouped(), + variable_components=ComponentSet((AllComponents.Z_INDEX,)), + ) + (scope,) = candidates + # Discovery still owns the producer's semantic channel, rather than adopting + # a later consumer's independently selected source-image channel. + assert dict(scope.fixed_component_values) == { + AllComponents.SITE: "1", AllComponents.CHANNEL: "0", + AllComponents.TIMEPOINT: "1", + } + + +@pytest.mark.parametrize("component", [AllComponents.SITE, AllComponents.TIMEPOINT]) +def test_exact_producer_group_preserves_shared_fixed_context_constraints(component): + store, _record, runtime_input = _grouped_label_input_with_distinct_consumer_channel() + fixed_values = dict(runtime_input.axis_scope.fixed_component_values) + fixed_values[component] = "2" + wrong_context = replace( + runtime_input, + axis_scope=replace( + runtime_input.axis_scope, fixed_component_values=tuple(fixed_values.items()), + ), + ) + with pytest.raises(RuntimeError, match="Missing RuntimeValueStore record"): + wrong_context.records(store) + + +@pytest.mark.parametrize("mismatch", ["group", "well", "path", "fixed-z"]) +def test_exact_producer_group_rejects_other_producer_or_fixed_plane(mismatch): + _store, record, runtime_input = _grouped_label_input_with_distinct_consumer_channel() + scope = record.key.scope + path = record.path + if mismatch == "group": + scope = replace(scope, value="1") + elif mismatch == "well": + scope = replace(scope, axis_id="W002") + elif mismatch == "path": + path = "/memory/other/Cells_channel_0.pkl" + else: + scope = RuntimeExecutionAxisScope.from_raw( + scope.axis_id, component=scope.component, value=scope.value, + fixed_component_values=(*scope.fixed_component_values, (AllComponents.Z_INDEX, "2")), + ) + runtime_input = replace( + runtime_input, + axis_scope=RuntimeExecutionAxisScope.from_raw( + runtime_input.axis_scope.axis_id, + component=runtime_input.axis_scope.component, + value=runtime_input.axis_scope.value, + fixed_component_values=( + *runtime_input.axis_scope.fixed_component_values, + (AllComponents.Z_INDEX, "1"), + ), + ), + ) + store = RuntimeValueStore() + store.record( + replace(record.value, key=replace(record.key, scope=scope)), + path=path, backend=record.backend, + ) + with pytest.raises(RuntimeError, match="Missing RuntimeValueStore record"): + runtime_input.records(store) + + +def test_different_producer_group_axis_preserves_fixed_source_channel_constraint(): + _store, record, runtime_input = _grouped_label_input_with_distinct_consumer_channel() + # SITE is now the selected producer group. CHANNEL remains a genuine fixed + # image context and cannot use the selected-group exemption. + scope = RuntimeExecutionAxisScope.from_raw( + "W001", component=AllComponents.SITE, value="1", + fixed_component_values=( + (AllComponents.CHANNEL, "0"), (AllComponents.TIMEPOINT, "1"), + ), + ) + storage_plan = replace( + runtime_input.edge_plan.storage_plan, + group_component=AllComponents.SITE, group_keys=("1",), + paths_by_group={"1": record.path}, + ) + runtime_input = replace( + runtime_input, + edge_plan=replace( + runtime_input.edge_plan, + storage_plan=storage_plan, + projection=replace( + runtime_input.edge_plan.projection, + producer_selection_scope=storage_plan.producer_group_scope(), + ), + ), + ) + store = RuntimeValueStore() + store.record( + replace(record.value, key=replace(record.key, scope=scope)), + path=record.path, backend=record.backend, + ) + with pytest.raises(RuntimeError, match="Missing RuntimeValueStore record"): + runtime_input.records(store) + + def test_paired_channel_declaration_reaches_both_adapter_input_consumers(): store, record, runtime_input = _paired_channel_label_input() edge = runtime_input.edge_plan From e699d5b401cf0388086502d968df1f88f2ea2037 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 16:48:22 -0400 Subject: [PATCH 152/235] Resolve complete adapter source rosters in the existing invocation selector Refs #547. Retain explicit original aliases outside the loaded anchor through their exact source binding. Preserve represented payload epochs and refuse missing stored primaries. Delete the redundant second source-selection pass. --- openhcs/core/steps/function_runtime.py | 50 +++++++--------- .../test_measurement_declaration_journey.py | 57 +++++++++++++++++++ ...test_function_runtime_source_projection.py | 37 ++++++++++++ 3 files changed, 116 insertions(+), 28 deletions(-) diff --git a/openhcs/core/steps/function_runtime.py b/openhcs/core/steps/function_runtime.py index 2888357413..f9935ee84f 100644 --- a/openhcs/core/steps/function_runtime.py +++ b/openhcs/core/steps/function_runtime.py @@ -1213,6 +1213,8 @@ def select_for_invocation( *, execution_scope: ComponentGroupScope, component_key: str | None, + declared_source_bindings: CompiledSourceBindingPlan = CompiledSourceBindingPlan(), + active_source_bindings: CompiledSourceBindingPlan = CompiledSourceBindingPlan(), ) -> "ComponentArtifactPlans[InvocationArtifactInputProjectionKey, InvocationArtifactInputEdgePlan] | None": projected = invocation.for_component_execution( execution_scope, @@ -1220,34 +1222,28 @@ def select_for_invocation( ) if projected is None: return None + inputs = {} + for edge_key, edge in projected.select_inputs(self.inputs).items(): + if ( + edge.main_flow_projection is not None + and declared_source_bindings.declares_artifact_ref(edge.spec.ref()) + and not active_source_bindings.declares_artifact_ref(edge.spec.ref()) + ): + if not invocation.adapter_manages_artifact_inputs: + continue + if edge.storage_plan is not None: + raise ValueError( + f"Stored primary input {edge.spec.ref()!r} is not represented " + "by this main-flow payload; its producer cannot substitute " + "for the current payload epoch." + ) + edge = replace(edge, main_flow_projection=None) + inputs[edge_key] = edge return ComponentArtifactPlans( - inputs=projected.select_inputs(self.inputs), + inputs=inputs, outputs=projected.select_outputs(self.outputs), ) - def select_source_bound_inputs( - self: "ComponentArtifactPlans[InvocationArtifactInputProjectionKey, InvocationArtifactInputEdgePlan]", - *, - declared_source_bindings: CompiledSourceBindingPlan, - active_source_bindings: CompiledSourceBindingPlan, - ) -> "ComponentArtifactPlans[InvocationArtifactInputProjectionKey, InvocationArtifactInputEdgePlan]": - """Keep only source-bound main-flow occurrences active on this component.""" - - return replace( - self, - inputs={ - edge_key: edge - for edge_key, edge in self.inputs.items() - if not ( - edge.main_flow_projection is not None - and declared_source_bindings.declares_artifact_ref(edge.spec.ref()) - and not active_source_bindings.declares_artifact_ref( - edge.spec.ref() - ) - ) - }, - ) - @classmethod def _select_output_plans_for_component( cls, @@ -2973,13 +2969,11 @@ def execute_chain( invocation, execution_scope=group_data.execution_plan.execution_group_scope, component_key=group_data.component_key, - ) - if artifacts is None: - continue - artifacts = artifacts.select_source_bound_inputs( declared_source_bindings=declared_source_bindings, active_source_bindings=active_main_flow_bindings, ) + if artifacts is None: + continue runtime_invocation = invocation.for_runtime_outputs( output_plans=tuple(artifacts.outputs.values()), ) diff --git a/tests/integration/test_measurement_declaration_journey.py b/tests/integration/test_measurement_declaration_journey.py index 55c9a61775..ccf0738828 100644 --- a/tests/integration/test_measurement_declaration_journey.py +++ b/tests/integration/test_measurement_declaration_journey.py @@ -58,6 +58,8 @@ SourceFilterMatchType, SourceFilterSubject, SourceSelector, + SourceBindingMatchMethod, + SourceBindingMatchPlan, StepSourceBindingsConfig, ) from openhcs.core.steps.function_step import FunctionStep @@ -353,6 +355,61 @@ def test_omitted_secondary_selector_still_fails_closed(tmp_path): ) +def test_explicit_measurement_rosters_survive_one_matched_source_anchor(tmp_path): + from openhcs.core.steps.function_execution import FunctionStepExecutor + + _write_plate(tmp_path) + original = _document() + for producer in original.pipeline_steps[:-1]: + producer.processing_config = replace(producer.processing_config, group_by=GroupBy.CHANNEL) + original.pipeline_steps[-1] = _step( + measure_object_intensity, + "Measure both source images and object sets", + { + MeasureObjectIntensityModule.image_measurement_binding.require_parameter_name(): ("DNA", "Actin"), + MeasureObjectIntensityModule.object_measurement_binding.require_parameter_name(): ("Nuclei", "Cells"), + }, + ) + document = PipelineDocumentAuthority.from_values( + pipeline_config=replace( + original.pipeline_config, + source_bindings_config=LazySourceBindingsConfig( + bindings=(_source("DNA", "1"), _source("Actin", "2")), + match_plan=SourceBindingMatchPlan(method=SourceBindingMatchMethod.ORDER), + ), + ), + pipeline_steps=original.pipeline_steps, + ) + document = PipelineDocumentAuthority.from_source(PipelineDocumentAuthority.render(document)) + bundle = _compile(tmp_path, document, GlobalPipelineConfig(num_workers=1, use_threading=True)) + context = bundle.runtime_contexts["A01"] + executor = FunctionStepExecutor(context, 2) + prepared = executor._prepare_groups(executor._detect_patterns()) + assert prepared.total_count() == 1 + invocation = next(executor.plan.compiled_function_pattern.iter_invocations()) + assert tuple( + spec.name for spec in invocation.contract.artifact_inputs.of_artifact_type(ImageArtifactType) + ) == ("DNA", "Actin") + results = _execute(tmp_path, document, bundle) + assert results["A01"].is_success(), results["A01"].error_message + (output,) = invocation.artifact_output_plans + (measurement,) = context.runtime_value_store.find( + name=output.name, artifact_type=MeasurementsArtifactType, axis_id="A01", + ) + for object_name in ("Nuclei", "Cells"): + (labels,) = context.runtime_value_store.find(name=object_name, axis_id="A01") + area = np.count_nonzero(object_label_dense_array(labels.value.data)) + for image_name, expected in (("DNA", 4.0), ("Actin", float(area))): + values = measurement_values_for_feature( + (measurement.value.data,), + f"Intensity_IntegratedIntensity_{image_name}", + object_count=1, + object_name=object_name, + dialect=CELLPROFILER_MEASUREMENT_LOOKUP_DIALECT, + ) + assert tuple(values) == (expected,) + + def test_omitted_selector_remains_valid_for_one_label_producer(tmp_path): _write_plate(tmp_path) bundle = _compile( diff --git a/tests/unit/test_function_runtime_source_projection.py b/tests/unit/test_function_runtime_source_projection.py index bc74cf766e..90b0f9e18b 100644 --- a/tests/unit/test_function_runtime_source_projection.py +++ b/tests/unit/test_function_runtime_source_projection.py @@ -2463,6 +2463,43 @@ def execute(self, *, debug_sink=None): assert request.selected_artifact_input_specs().names() == ("OrigStain1",) +def test_source_roster_selection_cannot_replace_missing_stored_primary_epoch() -> None: + from openhcs.core.artifacts import ArtifactInputProjectionPlan + from openhcs.core.steps.function_runtime import ComponentArtifactPlans + + bindings = CompiledSourceBindingPlan(bindings=(NamedSourceBinding(alias="Original"),)) + (source_spec,) = tuple(binding.input_spec() for binding in bindings.binding_declarations) + + @runtime_adapter("runtime", lambda _request: object(), manages_artifact_inputs=True) + @artifact_inputs(source_spec) + def consume_original(image, *, runtime): + return image + + storage = ArtifactInputPlan( + name=source_spec.name, artifact_type=source_spec.artifact_type, + path="/memory/Original.pkl", source_step_id=0, + ) + invocation = compile_function_pattern(consume_original, {storage.ref(): storage}, {}).default_group.invocations[0] + (key,) = InvocationArtifactInputProjectionKey.for_input_count(invocation.key, 1) + invocation = invocation.with_artifact_input_edges(( + InvocationArtifactInputEdgePlan( + key=key, spec=source_spec, storage_plan=storage, + projection=ArtifactInputProjectionPlan( + invocation_scope=ComponentGroupScope.ungrouped(), + producer_selection_scope=storage.producer_group_scope(), + ), + main_flow_projection=MainFlowInputProjection.COMPLETE_PAYLOAD, + ), + )) + plans = ComponentArtifactPlans(inputs={storage.ref(): storage}, outputs={}) + with pytest.raises(ValueError, match="producer cannot substitute.*current payload epoch"): + plans.select_for_invocation( + invocation, execution_scope=ComponentGroupScope.ungrouped(), component_key=None, + declared_source_bindings=bindings, + active_source_bindings=CompiledSourceBindingPlan.empty(), + ) + + def test_runtime_chain_skips_adapter_invocation_without_component_outputs( monkeypatch: pytest.MonkeyPatch, ) -> None: From 356c47ed01e6dfe737992662764c01d924f0e5a0 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 16:56:52 -0400 Subject: [PATCH 153/235] Declare exact measurement image-set context on shared artifact inputs Refs #547. Extend the existing source-context relation family with an input-only image-set relation. Derive label context from the selected measurement image roster; retain strict edge-local matching without group or broadcast changes. --- .../runtime-owner-consolidation-20261003.rst | 26 +++++++++ openhcs/core/artifacts.py | 26 +++++++++ .../module_artifact_declarations.py | 30 +++++++++++ .../test_measurement_declaration_journey.py | 34 ++++++++---- tests/unit/test_artifact_spec_collection.py | 40 ++++++++++++++ ...test_cellprofiler_artifact_declarations.py | 54 +++++++++++++++++++ 6 files changed, 199 insertions(+), 11 deletions(-) diff --git a/docs/refactor/receipts/runtime-owner-consolidation-20261003.rst b/docs/refactor/receipts/runtime-owner-consolidation-20261003.rst index 22d8bda179..77107d92a0 100644 --- a/docs/refactor/receipts/runtime-owner-consolidation-20261003.rst +++ b/docs/refactor/receipts/runtime-owner-consolidation-20261003.rst @@ -227,6 +227,23 @@ batch kernels. NATURAL object measurements resolve that roster once, without building an unused composed image request; COMPOSED and selected-plane source identity retain their declared behavior. +Explicit roster compilation belongs to the adapter and declaration owners, +while raw scalar argument multiplicity remains on the raw binder. Omitted +object selection still fails when more than one subject is available. Recorded +measurement payload owners validate named subject multiplicity after the common +row scope/id-field checks. + +Invocation selection now also owns the source-origin view: represented inputs +retain the current payload epoch, and unrepresented explicit original images +resolve through their exact matched source binding. The separate source-input +selection pass is removed. An absent stored primary cannot be replaced by its +producer's older value. Exact producer-group selection is distinct from +additional image-set fixed coordinates; discovery retains actual produced +coordinates, while site, time, Z and genuinely fixed-channel constraints remain. +Physical publication loads original memory addresses and writes manifest-owned +destinations under the current output root; preserved outputs no longer write +back into the input plate. + Evidence scope -------------- @@ -255,6 +272,15 @@ under ``owner-consolidation-746-paired-v1`` and its preparation sibling in the 20261003 maintenance evidence root. The typed publication, input binding and identity changes described above follow this measurement and remain unmeasured. +The original-pipeline campaigns under ``canonical-producer-transport-paired-v1`` +and ``v2`` failed at compilation and runtime respectively; their clocks are not +qualified performance evidence. The ``v3`` campaign at ``c50a947f4`` completed +both ordinary sweeps and fresh native runs but failed strict comparison with +42 missing origMemb intensity features. Its faster timings are also unqualified. +The missing explicit source-roster edge is repaired by the invocation selector; +fresh complete scientific comparison remains required. All failed campaigns +are retained, and no output exclusion or tolerance change is admitted. + PR #394 remains draft. Its original whole-branch R0/R1 obligations, installed consumer acceptance, full-catalog parity and fresh full30/scaling figures remain open. Startup and required library/kernel preparation remain outside diff --git a/openhcs/core/artifacts.py b/openhcs/core/artifacts.py index 6f1a8844f5..328ec55a26 100644 --- a/openhcs/core/artifacts.py +++ b/openhcs/core/artifacts.py @@ -1437,6 +1437,31 @@ class InputGroupLineageSourceRelation(ArtifactGroupScopeSourceRelation): relation_key: ClassVar[str] = "input_group_lineage_source" +class InputImageSetContextSourceRelation(ArtifactSourceContextSourceRelation): + """Consume an artifact in the image-set context of a declared input image.""" + + relation_key: ClassVar[str] = "input_image_set_context_source" + + def __post_init__(self) -> None: + super().__post_init__() + if ( + self.source.plan_type is not ArtifactInputPlan + or self.source.artifact_type is not ImageArtifactType + ): + raise ValueError( + f"{type(self).__name__} requires an input image source, got " + f"{self.source!r}." + ) + + def require_target_spec(self, spec: "ArtifactSpec") -> None: + super().require_target_spec(spec) + if not spec.artifact_type.carries_source_image_context: + raise ValueError( + f"{type(self).__name__} requires a target carrying source-image " + f"context, got {spec.artifact_type.value!r}." + ) + + class InputStackBroadcastSourceRelation(ArtifactSourceContextSourceRelation): """Input artifact may broadcast across one declared source input stack.""" @@ -3355,6 +3380,7 @@ def validate_complete_producer_projection( ArtifactSpecRelation.target_plan_type = ArtifactOutputPlan InputGroupLineageSourceRelation.target_plan_type = ArtifactInputPlan +InputImageSetContextSourceRelation.target_plan_type = ArtifactInputPlan InputStackBroadcastSourceRelation.target_plan_type = ArtifactInputPlan InputStackBroadcastSourceRelation.target_artifact_type = ImageArtifactType InputObjectMeasurementSourceRelation.target_plan_type = ArtifactInputPlan diff --git a/openhcs/interop/cellprofiler/module_artifact_declarations.py b/openhcs/interop/cellprofiler/module_artifact_declarations.py index 3d0bd9b9a7..9c2b323f03 100644 --- a/openhcs/interop/cellprofiler/module_artifact_declarations.py +++ b/openhcs/interop/cellprofiler/module_artifact_declarations.py @@ -24,6 +24,7 @@ ImageArtifactType, ImageMeasurementSubjectRelation, InputGroupLineageSourceRelation, + InputImageSetContextSourceRelation, MeasurementsArtifactType, ObjectLabelsArtifactType, ObjectLineageArtifactType, @@ -1190,6 +1191,35 @@ class ImageMeasurementInputModule( image_measurement_binding, ) + @classmethod + def finalize_artifact_contract_inputs( + cls, + module: "ModuleBlock", + *, + invocation_key: "FunctionInvocationKey", + step_context: "ArtifactDeclarationStepContext", + artifact_inputs: ArtifactSpecCollection, + ) -> tuple[ArtifactSpec, ...]: + """Bind measured object sets to exactly the selected input image contexts.""" + inputs = ArtifactSpecCollection( + super().finalize_artifact_contract_inputs( + module, + invocation_key=invocation_key, + step_context=step_context, + artifact_inputs=artifact_inputs, + ) + ) + image_contexts = tuple( + InputImageSetContextSourceRelation(spec.ref()) + for spec in inputs.of_artifact_type(ImageArtifactType) + ) + return tuple( + replace(spec, relations=(*spec.relations, *image_contexts)) + if spec.artifact_type is ObjectLabelsArtifactType and image_contexts + else spec + for spec in inputs + ) + @classmethod def input_source_for_contract( cls, diff --git a/tests/integration/test_measurement_declaration_journey.py b/tests/integration/test_measurement_declaration_journey.py index ccf0738828..112b787ab7 100644 --- a/tests/integration/test_measurement_declaration_journey.py +++ b/tests/integration/test_measurement_declaration_journey.py @@ -355,13 +355,18 @@ def test_omitted_secondary_selector_still_fails_closed(tmp_path): ) -def test_explicit_measurement_rosters_survive_one_matched_source_anchor(tmp_path): +@pytest.mark.parametrize("producer_group_by", [GroupBy.NONE, GroupBy.CHANNEL]) +def test_explicit_measurement_rosters_survive_one_matched_source_anchor( + tmp_path, producer_group_by, +): from openhcs.core.steps.function_execution import FunctionStepExecutor _write_plate(tmp_path) original = _document() for producer in original.pipeline_steps[:-1]: - producer.processing_config = replace(producer.processing_config, group_by=GroupBy.CHANNEL) + producer.processing_config = replace( + producer.processing_config, group_by=producer_group_by, + ) original.pipeline_steps[-1] = _step( measure_object_intensity, "Measure both source images and object sets", @@ -399,15 +404,22 @@ def test_explicit_measurement_rosters_survive_one_matched_source_anchor(tmp_path for object_name in ("Nuclei", "Cells"): (labels,) = context.runtime_value_store.find(name=object_name, axis_id="A01") area = np.count_nonzero(object_label_dense_array(labels.value.data)) - for image_name, expected in (("DNA", 4.0), ("Actin", float(area))): - values = measurement_values_for_feature( - (measurement.value.data,), - f"Intensity_IntegratedIntensity_{image_name}", - object_count=1, - object_name=object_name, - dialect=CELLPROFILER_MEASUREMENT_LOOKUP_DIALECT, - ) - assert tuple(values) == (expected,) + for image_name, integrated in (("DNA", 4.0), ("Actin", float(area))): + expected_features = { + "IntegratedIntensity": integrated, + "MeanIntensity": integrated / area, + "MinIntensity": 1.0 if integrated == area else 0.0, + "MaxIntensity": 1.0, + } + for feature, expected in expected_features.items(): + values = measurement_values_for_feature( + (measurement.value.data,), + f"Intensity_{feature}_{image_name}", + object_count=1, + object_name=object_name, + dialect=CELLPROFILER_MEASUREMENT_LOOKUP_DIALECT, + ) + assert tuple(values) == pytest.approx((expected,)) def test_omitted_selector_remains_valid_for_one_label_producer(tmp_path): diff --git a/tests/unit/test_artifact_spec_collection.py b/tests/unit/test_artifact_spec_collection.py index 829bc2bcab..6b7daab570 100644 --- a/tests/unit/test_artifact_spec_collection.py +++ b/tests/unit/test_artifact_spec_collection.py @@ -11,10 +11,12 @@ ArtifactSpecCollection, GroupLineageSourceRelation, ImageArtifactType, + InputImageSetContextSourceRelation, InputStackBroadcastSourceRelation, MainFlowPlaneProjectionOutputSpec, MeasurementsArtifactType, ObjectLabelsArtifactType, + SpatialGraphArtifactType, ) from openhcs.core.runtime_measurements import RuntimeMeasurementFeatureOwner @@ -93,6 +95,44 @@ def test_stack_broadcast_indices_use_exact_ordered_occurrences() -> None: ).stack_broadcast_source_indices() +@pytest.mark.parametrize( + "artifact_type", (ImageArtifactType, ObjectLabelsArtifactType, SpatialGraphArtifactType) +) +def test_input_image_set_context_does_not_regroup_or_broadcast(artifact_type) -> None: + image = ArtifactSpec.input("Actin", ImageArtifactType) + relation = InputImageSetContextSourceRelation(image.ref()) + target = ArtifactSpec.input("Subject", artifact_type, relations=(relation,)) + + assert target.source_context_sources() == (image.ref(),) + assert target.group_scope_sources() == () + assert target.source_stack_scope_sources() == () + assert target.stack_broadcast_sources() == () + assert relation.dependency_refs() == (image.ref(),) + assert target.for_plan_type(ArtifactOutputPlan).relations == () + + +@pytest.mark.parametrize( + "source", + ( + ArtifactSpec.output("Actin", ImageArtifactType), + ArtifactSpec.input("Cells", ObjectLabelsArtifactType), + ), +) +def test_input_image_set_context_requires_an_input_image_source(source) -> None: + with pytest.raises(ValueError, match="requires an input image source"): + InputImageSetContextSourceRelation(source.ref()) + + +def test_input_image_set_context_rejects_output_and_contextless_targets() -> None: + relation = InputImageSetContextSourceRelation( + ArtifactSpec.input("Actin", ImageArtifactType).ref() + ) + with pytest.raises(ValueError, match="target plan role"): + ArtifactSpec.output("Cells", ObjectLabelsArtifactType, relations=(relation,)) + with pytest.raises(ValueError, match="carrying source-image context"): + ArtifactSpec.input("Measurements", MeasurementsArtifactType, relations=(relation,)) + + def test_artifact_spec_collection_deduplicates_or_fails_loudly() -> None: image = ArtifactSpec.output("DNA", ImageArtifactType) diff --git a/tests/unit/test_cellprofiler_artifact_declarations.py b/tests/unit/test_cellprofiler_artifact_declarations.py index 1515156c70..fc1a0df8bb 100644 --- a/tests/unit/test_cellprofiler_artifact_declarations.py +++ b/tests/unit/test_cellprofiler_artifact_declarations.py @@ -21,6 +21,7 @@ ArtifactSpecRelation, GroupLineageSourceRelation, ImageArtifactType, + InputImageSetContextSourceRelation, InputGroupLineageSourceRelation, MeasurementsArtifactType, ObjectLabelsArtifactType, @@ -444,6 +445,59 @@ def test_measurement_output_separates_provenance_from_invocation_group_scope() - assert output.source_stack_scope_sources() == () +def test_object_measurement_context_uses_only_selected_image_inputs() -> None: + available = ArtifactSpecCollection( + ( + ArtifactSpec.output("DNA", ImageArtifactType), + ArtifactSpec.output("Actin", ImageArtifactType), + ArtifactSpec.output("Unselected", ImageArtifactType), + ArtifactSpec.output("Nuclei", ObjectLabelsArtifactType), + ArtifactSpec.output("Cells", ObjectLabelsArtifactType), + ) + ) + contract = _callable_contract( + _module( + 3, + "MeasureObjectIntensity", + { + "Select images to measure": "DNA,Actin", + "Select objects to measure": "Nuclei,Cells", + }, + ), + step_index=2, + available_artifacts=available, + main_flow_artifacts=ArtifactSpecCollection(()), + ) + images = contract.artifact_inputs.of_artifact_type(ImageArtifactType) + objects = contract.artifact_inputs.of_artifact_type(ObjectLabelsArtifactType) + assert tuple(spec.name for spec in images) == ("DNA", "Actin") + assert tuple(spec.name for spec in objects) == ("Nuclei", "Cells") + for spec in objects: + assert spec.source_context_sources() == tuple(image.ref() for image in images) + assert spec.relations == tuple( + InputImageSetContextSourceRelation(image.ref()) for image in images + ) + assert spec.group_scope_sources() == () + assert spec.source_stack_scope_sources() == () + assert spec.stack_broadcast_sources() == () + + +def test_object_only_measurement_does_not_inherit_visible_image_context() -> None: + contract = _callable_contract( + _module(3, "MeasureObjectSizeShape", {"Select objects to measure": "Cells"}), + step_index=2, + available_artifacts=ArtifactSpecCollection( + ( + ArtifactSpec.input("DNA", ImageArtifactType), + ArtifactSpec.output("Cells", ObjectLabelsArtifactType), + ) + ), + main_flow_artifacts=ArtifactSpecCollection(()), + ) + (objects,) = contract.artifact_inputs.of_artifact_type(ObjectLabelsArtifactType) + assert objects.source_context_sources() == () + + def test_prior_measurement_selects_its_declared_producer_group_scope() -> None: green = ArtifactSpec.input("OrigGreen", ImageArtifactType) blue = ArtifactSpec.input("OrigBlue", ImageArtifactType) From 102d6e4911eed09ddaf7ad958589c6dd87755a32 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 17:11:45 -0400 Subject: [PATCH 154/235] Derive active source bindings from the current invocation payload Refs #547. Earlier outputs can add represented source aliases to the current named image bundle. Resolve the source-origin view at each invocation boundary instead of retaining the initial cohort view. --- openhcs/core/steps/function_runtime.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/openhcs/core/steps/function_runtime.py b/openhcs/core/steps/function_runtime.py index f9935ee84f..39b2824e14 100644 --- a/openhcs/core/steps/function_runtime.py +++ b/openhcs/core/steps/function_runtime.py @@ -2960,9 +2960,6 @@ def execute_chain( current_memory_type = group_data.execution_plan.input_memory_type debug_sink = debug_event_sink_from_context(group_data.context) declared_source_bindings = group_data.execution_plan.source_binding_plan - active_main_flow_bindings = group_data.active_main_flow_source_binding_plan( - group_data.main_data_stack - ) for invocation in group_data.compiled_group.invocations: group_key = invocation.key.runtime_group_key(group_data.component_value) artifacts = group_data.artifacts.select_for_invocation( @@ -2970,7 +2967,9 @@ def execute_chain( execution_scope=group_data.execution_plan.execution_group_scope, component_key=group_data.component_key, declared_source_bindings=declared_source_bindings, - active_source_bindings=active_main_flow_bindings, + active_source_bindings=group_data.active_main_flow_source_binding_plan( + current_stack + ), ) if artifacts is None: continue From 7836532a623d4273a8c3145a4fef4a223aa22855 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 17:39:41 -0400 Subject: [PATCH 155/235] docs(perf): qualify complete producer transport against native CP Refs #479, #547. Record the corrected original-pipeline comparison and retain prior failed campaigns without promoting their clocks. --- .../runtime-owner-consolidation-20261003.rst | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/docs/refactor/receipts/runtime-owner-consolidation-20261003.rst b/docs/refactor/receipts/runtime-owner-consolidation-20261003.rst index 77107d92a0..075f608a5c 100644 --- a/docs/refactor/receipts/runtime-owner-consolidation-20261003.rst +++ b/docs/refactor/receipts/runtime-owner-consolidation-20261003.rst @@ -278,8 +278,33 @@ qualified performance evidence. The ``v3`` campaign at ``c50a947f4`` completed both ordinary sweeps and fresh native runs but failed strict comparison with 42 missing origMemb intensity features. Its faster timings are also unqualified. The missing explicit source-roster edge is repaired by the invocation selector; -fresh complete scientific comparison remains required. All failed campaigns -are retained, and no output exclusion or tolerance change is admitted. +the corrected scientific comparison below covers both original pipelines. +All failed campaigns are retained, and no output exclusion or tolerance change +is admitted. + +The corrected ``canonical-producer-transport-paired-v5`` campaign at +``102d6e491`` passes all eight strict scientific comparisons, including complete +measurement inventories and images with existing CP tolerances. Active source +aliases derive from the current payload for each invocation, preserving outputs +and mutations within a function chain. The existing artifact source-relation +family supplies the explicitly selected image-set context to object inputs; +exact producer addresses and unrelated fixed coordinates remain strict. + +Means of two independent ordinary and two fresh native observations are: + +* 3D: compile 1.112773s, execution 7.401238s, total 9.284819s, native invocation + 14.204229s; 1.919169x execution and 1.529834x total. +* Speckles: compile 0.677950s, execution 1.290954s, total 2.292555s, native + invocation 1.930783s; 1.495625x execution and 0.842197x total. + +These observations do not establish a causal gain or full30/scaling behavior. +Speckles still loses on total. The existing benchmark figure owner produces +fresh PNG/SVG runtime and speedup figures in the campaign's ``figures-v2`` +directory. Native invocation includes its pre-first-module work; startup, +imports/JVM setup and the excluded warmup observation are outside this clock. +Ordinary total includes compilation and normal OUTCOMES/RSS completion. +The ``v4`` namespace is explicitly aborted before measurement, after independent +review found stale binding selection within a multi-function chain. PR #394 remains draft. Its original whole-branch R0/R1 obligations, installed consumer acceptance, full-catalog parity and fresh full30/scaling figures From 9d79c428988b3989a6cd5d9242f3a54a39aad1bd Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 18:00:02 -0400 Subject: [PATCH 156/235] fix(runtime): preserve prepared plans through transport derivation Refs #558. Derive separate transport plan snapshots on the existing bundle owner. Fork, inline and thread resources consume their prepared runtime graph directly; remove repeated worker normalization. --- openhcs/core/compiled_execution.py | 17 +++-- openhcs/core/function_step_transport.py | 19 +++-- openhcs/core/orchestrator/worker_execution.py | 59 ++++++++------- tests/unit/test_compiled_execution.py | 13 +++- tests/unit/test_function_step_transport.py | 74 +++++++++++++++++++ tests/unit/test_orchestrator_lane_planning.py | 38 +++++++++- 6 files changed, 175 insertions(+), 45 deletions(-) diff --git a/openhcs/core/compiled_execution.py b/openhcs/core/compiled_execution.py index 986a7157af..5cdc325c20 100644 --- a/openhcs/core/compiled_execution.py +++ b/openhcs/core/compiled_execution.py @@ -121,7 +121,10 @@ class CompiledExecutionBundle: ``runtime_contexts`` preserve the rich in-process compiled state used for runtime facts and fork inheritance. ``transport_contexts`` are the - pickle-safe contexts submitted through worker queues. + pickle-safe plan snapshots derived at bundle construction and submitted + through worker queues. Runtime services and author configuration objects + retain their existing shared lifetimes; later runtime plan replacement does + not update this transport snapshot. """ pipeline_definition: Sequence[AbstractStep] @@ -178,8 +181,10 @@ def from_runtime_contexts( worker_assignments: Mapping[str, list[str]], runtime_environment: CompiledRuntimeEnvironmentPlan, ) -> "CompiledExecutionBundle": - transport_contexts = resolve_lazy_configurations_for_serialization( - dict(runtime_contexts.items()) + from openhcs.core.function_step_transport import FunctionStepTransportAuthority + + transport_contexts = FunctionStepTransportAuthority.normalize_contexts( + resolve_lazy_configurations_for_serialization(dict(runtime_contexts.items())) ) return cls( pipeline_definition=pipeline_definition, @@ -213,14 +218,10 @@ def for_transport_serialization(self) -> "CompiledExecutionBundle": from openhcs.core.function_step_transport import FunctionStepTransportAuthority - transport_contexts = FunctionStepTransportAuthority.normalize_contexts( - dict(self.transport_contexts) - ) return replace( self, pipeline_definition=FunctionStepTransportAuthority.normalize_pipeline( list(self.pipeline_definition) ), - runtime_contexts=transport_contexts, - transport_contexts=transport_contexts, + runtime_contexts=self.transport_contexts, ) diff --git a/openhcs/core/function_step_transport.py b/openhcs/core/function_step_transport.py index 4ced68b3b5..57d05c7722 100644 --- a/openhcs/core/function_step_transport.py +++ b/openhcs/core/function_step_transport.py @@ -2,6 +2,7 @@ from __future__ import annotations +from copy import copy from dataclasses import replace from types import ModuleType from collections.abc import Mapping, Sequence @@ -87,13 +88,19 @@ def normalize_contexts(cls, contexts: Mapping[str, Any]) -> dict[str, Any]: @classmethod def normalize_context(cls, context: Any) -> Any: - step_plans = context.step_plans - for step_plan in step_plans.values(): - step_plan.func = cls.normalize_function_spec(step_plan.func) - step_plan.compiled_function_pattern = cls.normalize_compiled_pattern( - step_plan.compiled_function_pattern + """Derive transport plans without replacing prepared runtime contracts.""" + transport_context = copy(context) + transport_context.step_plans = { + step_id: replace( + step_plan, + func=cls.normalize_function_spec(step_plan.func), + compiled_function_pattern=cls.normalize_compiled_pattern( + step_plan.compiled_function_pattern + ), ) - return context + for step_id, step_plan in context.step_plans.items() + } + return transport_context @classmethod def normalize_step(cls, step: Any) -> Any: diff --git a/openhcs/core/orchestrator/worker_execution.py b/openhcs/core/orchestrator/worker_execution.py index fe7776e554..0527b43547 100644 --- a/openhcs/core/orchestrator/worker_execution.py +++ b/openhcs/core/orchestrator/worker_execution.py @@ -120,23 +120,16 @@ def plan_worker_lanes( fork_inherited_execution=self.uses_fork_inherited_contexts, ).plan(contexts_snapshot, worker_assignments) - def contexts_snapshot( - self, - execution_bundle: CompiledExecutionBundle, - ) -> Dict[str, ProcessingContext]: - raw_contexts = self.raw_contexts_snapshot(execution_bundle) - return FunctionStepTransportAuthority.normalize_contexts(dict(raw_contexts)) - @property @abstractmethod def uses_fork_inherited_contexts(self) -> bool: """Whether lane planning receives fork-inherited runtime context keys.""" @abstractmethod - def raw_contexts_snapshot( + def contexts_snapshot( self, execution_bundle: CompiledExecutionBundle, - ) -> Mapping[str, ProcessingContext]: + ) -> Dict[str, ProcessingContext]: """Return the context map consumed by lane planning for this mode.""" @abstractmethod @@ -169,11 +162,11 @@ class InlineWorkerExecutorResources(WorkerExecutorResources): def uses_fork_inherited_contexts(self) -> bool: return False - def raw_contexts_snapshot( + def contexts_snapshot( self, execution_bundle: CompiledExecutionBundle, - ) -> Mapping[str, ProcessingContext]: - return execution_bundle.transport_contexts + ) -> Dict[str, ProcessingContext]: + return dict(execution_bundle.runtime_contexts) def run_worker_lanes( self, @@ -200,11 +193,11 @@ class ForkInheritedWorkerExecutorResources(WorkerExecutorResources): def uses_fork_inherited_contexts(self) -> bool: return True - def raw_contexts_snapshot( + def contexts_snapshot( self, execution_bundle: CompiledExecutionBundle, - ) -> Mapping[str, ProcessingContext]: - return execution_bundle.runtime_contexts + ) -> Dict[str, ProcessingContext]: + return dict(execution_bundle.runtime_contexts) def install_execution_bundle( self, execution_bundle: CompiledExecutionBundle @@ -244,11 +237,11 @@ def uses_fork_inherited_contexts(self) -> bool: def execution_context(self): return self._executor - def raw_contexts_snapshot( + def contexts_snapshot( self, execution_bundle: CompiledExecutionBundle, - ) -> Mapping[str, ProcessingContext]: - return execution_bundle.transport_contexts + ) -> Dict[str, ProcessingContext]: + return dict(execution_bundle.transport_contexts) def run_worker_lanes( self, @@ -292,6 +285,17 @@ def shutdown_executor(self) -> None: logger.warning(f"ORCHESTRATOR: Executor shutdown failed: {exc}") +@dataclass(frozen=True, slots=True) +class ThreadedWorkerExecutorResources(PooledWorkerExecutorResources): + """Thread workers share the prepared in-process execution graph.""" + + def contexts_snapshot( + self, + execution_bundle: CompiledExecutionBundle, + ) -> Dict[str, ProcessingContext]: + return dict(execution_bundle.runtime_contexts) + + class WorkerExecutorFactory: """Create the worker resources matching the effective runtime config.""" @@ -337,18 +341,21 @@ def create( executor = concurrent.futures.ThreadPoolExecutor( max_workers=actual_max_workers ) - else: - executor = self._process_pool_executor( - multiprocessing_context, - actual_max_workers, + return ThreadedWorkerExecutorResources( + multiprocessing_context=multiprocessing_context, + use_multiprocessing=False, + _executor=executor, + cancellation=self._cancellation, ) + executor = self._process_pool_executor( + multiprocessing_context, + actual_max_workers, + ) return PooledWorkerExecutorResources( multiprocessing_context=multiprocessing_context, - use_multiprocessing=not runtime_environment.use_threading, + use_multiprocessing=True, _executor=executor, - cancellation=( - self._cancellation if runtime_environment.use_threading else None - ), + cancellation=None, ) def _process_pool_executor( diff --git a/tests/unit/test_compiled_execution.py b/tests/unit/test_compiled_execution.py index 6ab25aa560..fea077e130 100644 --- a/tests/unit/test_compiled_execution.py +++ b/tests/unit/test_compiled_execution.py @@ -62,13 +62,13 @@ def _runtime_environment() -> CompiledRuntimeEnvironmentPlan: def test_compiled_execution_bundle_owns_transport_context_resolution(monkeypatch): - runtime_context = object() - transport_context = object() + runtime_context = ProcessingContext(axis_id="A01") + resolved_context = ProcessingContext(axis_id="A01") resolver_calls = [] def resolve_for_transport(contexts): resolver_calls.append(contexts) - return {"A01": transport_context} + return {"A01": resolved_context} monkeypatch.setattr( "openhcs.core.compiled_execution.resolve_lazy_configurations_for_serialization", @@ -84,7 +84,10 @@ def resolve_for_transport(contexts): assert resolver_calls == [{"A01": runtime_context}] assert bundle.runtime_contexts == {"A01": runtime_context} - assert bundle.transport_contexts == {"A01": transport_context} + transport_context = bundle.transport_contexts["A01"] + assert transport_context is not resolved_context + assert transport_context.runtime_value_store is resolved_context.runtime_value_store + assert transport_context.step_plans is not resolved_context.step_plans assert bundle.worker_assignments == {"worker_0": ["A01"]} @@ -364,8 +367,10 @@ def execute_current_run( context, _lane_context, *, + context_key, cancellation=None, ): + assert context_key == "A01" current_records.append( context.runtime_value_store.replace( measurement_value(2), diff --git a/tests/unit/test_function_step_transport.py b/tests/unit/test_function_step_transport.py index 1125c36db3..f3cb9c1448 100644 --- a/tests/unit/test_function_step_transport.py +++ b/tests/unit/test_function_step_transport.py @@ -391,6 +391,80 @@ def test_module_objects_are_rejected_as_function_specs() -> None: FunctionStepTransportAuthority.normalize_function_spec(crop_module) +def test_bundle_transport_preserves_prepared_runtime_contract_identity() -> None: + from polystore.filemanager import FileManager + from polystore.memory import MemoryStorageBackend + from openhcs.constants.constants import Backend + from openhcs.core.compiled_execution import ( + CompiledExecutionBundle, + CompiledRuntimeEnvironmentPlan, + ) + from openhcs.core.compiled_step_plan import CompiledStepPlan + from openhcs.core.context.processing_context import ProcessingContext + from openhcs.core.function_patterns import compile_function_pattern + from openhcs.core.orchestrator.execution_result import ( + RuntimeExecutionTransportSerialization, + ) + from openhcs.core.steps.function_runtime import ( + FunctionInvocationCallableCache, + FunctionInvocationCallableResolver, + ) + + reference = FunctionReferenceTransportAuthority.function_reference( + cellprofiler_backend.crop + ) + reference = replace( + reference, metadata=replace(reference.metadata, prepare=lambda: None) + ) + pattern = compile_function_pattern(reference, {}, {}) + (invocation,) = tuple(pattern.iter_invocations()) + prepared_callable = FunctionInvocationCallableResolver.resolve(invocation) + plan = CompiledStepPlan( + step_index=0, step_name="Crop", step_type="FunctionStep", axis_id="A01", + func=reference, compiled_function_pattern=pattern, + ) + context = ProcessingContext( + axis_id="A01", step_plans={0: plan}, + filemanager=FileManager({Backend.MEMORY.value: MemoryStorageBackend()}), + ) + context.freeze() + bundle = CompiledExecutionBundle.from_runtime_contexts( + pipeline_definition=(), runtime_contexts={"A01": context}, + worker_assignments={"worker_0": ["A01"]}, + runtime_environment=CompiledRuntimeEnvironmentPlan.from_global_config( + GlobalPipelineConfig(), compiled_contexts={"A01": context}, server_mode=False, + ), + ) + transport_context = bundle.transport_contexts["A01"] + transport_plan = transport_context.step_plans[0] + assert transport_context is not context + assert transport_plan is not plan + assert transport_plan.func.metadata.prepare is None + assert plan.func is reference and reference.metadata.prepare is not None + assert plan.compiled_function_pattern is pattern + assert FunctionInvocationCallableResolver.resolve(invocation) is prepared_callable + assert transport_context.runtime_value_store is context.runtime_value_store + assert transport_context.runtime_image_stack_cache is context.runtime_image_stack_cache + assert transport_context.filemanager is context.filemanager + transport_plan.step_name = "Transport-only name" + assert plan.step_name == "Crop" + + serialized_bundle = bundle.for_transport_serialization() + assert serialized_bundle.transport_contexts is bundle.transport_contexts + assert serialized_bundle.runtime_contexts is bundle.transport_contexts + assert context.step_plans[0] is plan + assert ( + FunctionInvocationCallableCache.process_cache().get_bound(invocation.contract) + is prepared_callable + ) + assert FunctionInvocationCallableResolver.resolve(invocation) is prepared_callable + RuntimeExecutionTransportSerialization.register() + restored = pickle.loads(pickle.dumps(serialized_bundle)) + restored_plan = restored.runtime_contexts["A01"].step_plans[0] + assert restored_plan.func.metadata.prepare is None + assert restored_plan.func.resolve() is reference.resolve() + + def test_generated_source_resolves_catalog_owned_cellprofiler_callable() -> None: source = FunctionStepTransportAuthority.source_from_pipeline( [FunctionStep(func=cellprofiler_backend.crop, name="Crop")] diff --git a/tests/unit/test_orchestrator_lane_planning.py b/tests/unit/test_orchestrator_lane_planning.py index b26ec32794..653a365581 100644 --- a/tests/unit/test_orchestrator_lane_planning.py +++ b/tests/unit/test_orchestrator_lane_planning.py @@ -49,6 +49,7 @@ ForkInheritedWorkerLaneRunner, InlineWorkerExecutorResources, PooledWorkerExecutorResources, + ThreadedWorkerExecutorResources, PooledWorkerLaneRunner, WorkerExecutorFactory, ) @@ -388,6 +389,41 @@ def test_executor_factory_uses_inline_lane_for_single_threaded_worker(monkeypatc assert resources.use_multiprocessing is False +def test_resource_modes_select_prepared_runtime_or_serial_transport_contexts(): + runtime_context = _compiled_context("A01") + transport_context = _compiled_context("A01") + bundle = CompiledExecutionBundle( + pipeline_definition=(), runtime_contexts={"A01": runtime_context}, + transport_contexts={"A01": transport_context}, worker_assignments={}, + runtime_environment=_runtime_environment( + use_threading=False, start_method=MultiprocessingStartMethod.SPAWN, + ), + ) + cancellation = ExecutionCancellationSignal() + resources = ( + InlineWorkerExecutorResources( + multiprocessing_context=None, use_multiprocessing=False, + cancellation=cancellation, + ), + ForkInheritedWorkerExecutorResources( + multiprocessing_context=None, use_multiprocessing=True, + ), + ThreadedWorkerExecutorResources( + multiprocessing_context=None, use_multiprocessing=False, + _executor=None, cancellation=cancellation, + ), + ) + for resource in resources: + contexts = resource.contexts_snapshot(bundle) + assert contexts is not bundle.runtime_contexts + assert contexts["A01"] is runtime_context + serial = PooledWorkerExecutorResources( + multiprocessing_context=None, use_multiprocessing=True, + _executor=None, cancellation=None, + ) + assert serial.contexts_snapshot(bundle)["A01"] is transport_context + + def test_executor_factory_uses_inline_lane_for_single_fork_worker(monkeypatch): context = object() monkeypatch.setattr( @@ -450,7 +486,7 @@ def __init__(self, *, max_workers): assert isinstance(resources.executor, FakeThreadPoolExecutor) assert created == {"max_workers": 3} - assert isinstance(resources, PooledWorkerExecutorResources) + assert isinstance(resources, ThreadedWorkerExecutorResources) assert resources.uses_fork_inherited_contexts is False assert resources.use_multiprocessing is False From ad4670f201290e55462a4d9d0e53f49671a5ec47 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 18:03:59 -0400 Subject: [PATCH 157/235] refactor(runtime): retain producer records and own loaded source semantics Refs #384, #547. Remove detached path-set and pattern-selector authorities and the source semantics helper module. Load from one current producer index, preserving ambiguity and source epochs; existing NamedSourceBinding owns transformations with declared axis restoration at the physical metadata boundary. --- openhcs/core/runtime_adapters.py | 4 +- openhcs/core/runtime_image_loading.py | 7 +- openhcs/core/source_bindings.py | 89 ++++++++ openhcs/core/source_image_semantics.py | 107 --------- .../core/steps/function_output_manifest.py | 206 +++++++----------- openhcs/core/steps/function_runtime.py | 44 ++-- .../unit/test_cellprofiler_source_bindings.py | 22 +- ...test_function_runtime_source_projection.py | 140 +++++++++--- tests/unit/test_persisted_image_metadata.py | 14 +- .../test_runtime_adapter_source_artifacts.py | 4 +- tests/unit/test_runtime_values.py | 52 ++--- 11 files changed, 353 insertions(+), 336 deletions(-) delete mode 100644 openhcs/core/source_image_semantics.py diff --git a/openhcs/core/runtime_adapters.py b/openhcs/core/runtime_adapters.py index 9ea47a8113..6540007698 100644 --- a/openhcs/core/runtime_adapters.py +++ b/openhcs/core/runtime_adapters.py @@ -47,7 +47,6 @@ SourceImageIdentity, SourceImageProvenance, ) -from openhcs.core.source_image_semantics import apply_source_binding_payload from openhcs.core.source_load_plan import SourceLoadPlan from openhcs.core.runtime_plane_projection import ( RuntimePlaneAxis, @@ -339,9 +338,8 @@ def source_artifact_payload(self, ref: ArtifactSpecRef) -> object: ) projected = projection.project_payload(lookup, payload) projected_payloads.append( - apply_source_binding_payload( + binding.apply_loaded_payload( projected, - binding, ImagePayloadSourceMetadataContext( SourceImageIdentity( member, diff --git a/openhcs/core/runtime_image_loading.py b/openhcs/core/runtime_image_loading.py index a29901c1c2..ac70edff4d 100644 --- a/openhcs/core/runtime_image_loading.py +++ b/openhcs/core/runtime_image_loading.py @@ -130,9 +130,14 @@ def metadata( image_shape_yx=source_spatial_shape_yx, ) ) - return metadata.with_source_context_from( + metadata = metadata.with_source_context_from( existing_metadata ).with_missing_intensity_from(existing_metadata) + if source_binding is not None: + # A declared absent channel axis must not be refilled from the + # previous carrier while merging missing source context. + metadata.source_channel_axis = source_channel_axis + return metadata @staticmethod def source_channel_axis( diff --git a/openhcs/core/source_bindings.py b/openhcs/core/source_bindings.py index 9b00e410dd..dea7994bf0 100644 --- a/openhcs/core/source_bindings.py +++ b/openhcs/core/source_bindings.py @@ -52,6 +52,9 @@ if TYPE_CHECKING: from openhcs.core.component_group_scope import RuntimeExecutionAxisScope + from openhcs.core.runtime_array_values import RuntimeArrayData + from openhcs.core.runtime_image_loading import ImagePayloadSourceMetadataContext + from openhcs.core.runtime_image_values import ImagePayloadMetadata SourceMetadataIdentity = tuple[tuple[str, SourceMetadataIdentityItems], ...] SOURCE_ALIAS_PART_SEPARATOR = "__" @@ -1038,6 +1041,92 @@ def input_spec(self) -> ArtifactSpec: required=self.required, ) + def apply_loaded_payload( + self, + payload: RuntimeArrayData, + source_context: ImagePayloadSourceMetadataContext | None, + ) -> RuntimeArrayData: + """Apply this declaration to one freshly loaded source payload.""" + import numpy as np + + from openhcs.core.runtime_image_values import ( + ImagePayloadMetadata, + ImageUnitIntervalIntensityMetadata, + image_payload_data, + image_payload_mask, + image_payload_metadata, + ) + + if source_context is None: + existing = image_payload_metadata(payload) + metadata = ( + existing + if existing.has_values + else ImagePayloadMetadata.for_array_payload(payload) + ) + else: + # The physical context needs the declared channel axis before it + # admits source geometry. It resolves this binding's axis once. + metadata = source_context.metadata(payload, source_binding=self) + metadata = metadata.replace_fields( + source_provenance=metadata.source_provenance.with_source_image_names( + (self.alias,) + ) + ) + if source_context is None: + metadata = metadata.replace_fields( + source_channel_axis=self.source_channel_axis_for_shape( + np.shape(image_payload_data(payload)), + observed_axis=metadata.source_channel_axis, + ), + ) + metadata.normalized_source_channel_axis(payload) + + data, source_channel_axis = self.artifact_kind.normalize_source_payload( + image_payload_data(payload), + metadata.source_channel_axis, + ) + if self.load_as_monochrome and source_channel_axis is not None: + data = self._monochrome_source_data(data, source_channel_axis, metadata) + source_channel_axis = None + metadata = metadata.replace_fields( + unit_interval_intensity=ImageUnitIntervalIntensityMetadata(), + intensity_scale=ImagePayloadMetadata.for_array(data).intensity_scale, + ) + if self.load_as_mask: + data = np.asarray(data, dtype=bool) + + metadata = metadata.replace_fields(source_channel_axis=source_channel_axis) + return metadata.payload_with(data, image_payload_mask(payload)) + + @staticmethod + def _monochrome_source_data( + data: RuntimeArrayData, + channel_axis: int, + metadata: ImagePayloadMetadata, + ) -> RuntimeArrayData: + """Convert declared RGB channels using the source intensity domain.""" + import numpy as np + + from openhcs.core.runtime_image_values import ( + image_payload_data, + normalize_image_payload_intensity, + ) + + normalized = normalize_image_payload_intensity( + metadata.payload_with(data), + dtype=np.float32, + ) + rgb = np.moveaxis( + np.asarray(image_payload_data(normalized)), channel_axis, -1 + )[..., :3] + if np.all(rgb == rgb[..., :1]): + return np.ascontiguousarray(rgb[..., 0]) + + from skimage.color import rgb2gray + + return rgb2gray(rgb) + def component_values( self, component: AllComponents, diff --git a/openhcs/core/source_image_semantics.py b/openhcs/core/source_image_semantics.py deleted file mode 100644 index b12f30692f..0000000000 --- a/openhcs/core/source_image_semantics.py +++ /dev/null @@ -1,107 +0,0 @@ -"""Generic source payload transforms declared by source bindings.""" - -from __future__ import annotations - -from dataclasses import replace -import numpy as np -from openhcs.core.runtime_image_values import ( - ImageMetadataPayload, - ImagePayloadMetadata, - MaskedImagePayload, - image_payload_data, - image_payload_mask, - image_payload_metadata, - normalize_image_payload_intensity, -) -from openhcs.core.runtime_image_loading import ImagePayloadSourceMetadataContext -from openhcs.core.runtime_array_values import RuntimeArrayData -from openhcs.core.source_bindings import NamedSourceBinding - - -def _rgb_to_monochrome(data: np.ndarray) -> np.ndarray: - rgb = np.asarray(data)[..., :3] - if np.all(rgb == rgb[..., :1]): - return np.ascontiguousarray(rgb[..., 0]) - - from skimage.color import rgb2gray - - return rgb2gray(rgb) - - -def _monochrome_source_data( - data: RuntimeArrayData, - channel_axis: int | None, - metadata: ImagePayloadMetadata, -) -> RuntimeArrayData: - if channel_axis is None: - return data - normalized = normalize_image_payload_intensity( - metadata.payload_with(data), - dtype=np.float32, - ) - channel_last = np.moveaxis( - np.asarray(image_payload_data(normalized)), - channel_axis, - -1, - ) - return _rgb_to_monochrome(channel_last) - - -def _loaded_source_payload_metadata( - payload: RuntimeArrayData, - binding: NamedSourceBinding, - source_context: ImagePayloadSourceMetadataContext | None, -) -> ImagePayloadMetadata: - existing = image_payload_metadata(payload) - if source_context is None: - metadata = ( - existing - if existing.has_values - else ImagePayloadMetadata.for_array_payload(payload) - ) - else: - metadata = source_context.metadata(payload, source_binding=binding) - metadata = metadata.replace_fields( - source_provenance=metadata.source_provenance.with_source_image_names( - (binding.alias,) - ) - ) - source_channel_axis = binding.source_channel_axis_for_shape( - np.shape(image_payload_data(payload)), - observed_axis=metadata.source_channel_axis, - ) - metadata = replace(metadata, source_channel_axis=source_channel_axis) - metadata.normalized_source_channel_axis(payload) - return metadata - - -def apply_source_binding_payload( - payload: RuntimeArrayData, - binding: NamedSourceBinding, - source_context: ImagePayloadSourceMetadataContext | None, -) -> RuntimeArrayData: - """Apply one resolved source binding to a loaded runtime payload.""" - metadata = _loaded_source_payload_metadata(payload, binding, source_context) - data = image_payload_data(payload) - source_channel_axis = metadata.source_channel_axis - data, source_channel_axis = binding.artifact_kind.normalize_source_payload( - data, - source_channel_axis, - ) - if binding.load_as_monochrome and source_channel_axis is not None: - data = _monochrome_source_data(data, source_channel_axis, metadata) - source_channel_axis = None - metadata = replace( - metadata.without_unit_interval_intensity_scale(), - intensity_scale=ImagePayloadMetadata.for_array(data).intensity_scale, - ) - if binding.load_as_mask: - data = np.asarray(data, dtype=bool) - - mask = image_payload_mask(payload) - metadata = replace(metadata, source_channel_axis=source_channel_axis) - if mask is not None: - return MaskedImagePayload(data, mask, metadata) - if metadata.has_values: - return ImageMetadataPayload(data, metadata) - return data diff --git a/openhcs/core/steps/function_output_manifest.py b/openhcs/core/steps/function_output_manifest.py index 5bf4ff72b2..a4aedb8c03 100644 --- a/openhcs/core/steps/function_output_manifest.py +++ b/openhcs/core/steps/function_output_manifest.py @@ -56,6 +56,20 @@ class ProducedOutputSemantics(FunctionOutputIdentity): image_metadata: ImagePayloadMetadata | None = None main_flow_plane_axis: RuntimePlaneAxis | None = RuntimePlaneAxis.RUNTIME_SLICE + def path_tokens(self, parser: FilenameParser) -> frozenset[str]: + """Expose this occurrence's exact storage and unqualified address aliases.""" + tokens = { + token + for value in (self.relative_output_path, self.output_path) + for token in ( + source_path_identity(value).as_posix(), + source_path_identity(value).name, + ) + } + if self.filename_qualifier is not None: + tokens.add(self.without_filename_qualifier().filename(parser)) + return frozenset(tokens) + def passed_through(self, plan: CompiledStepPlan) -> "ProducedOutputSemantics": """Retain the image domain and physical identity under the next producer.""" return replace( @@ -229,10 +243,6 @@ class StepOutputManifestStore: tuple[int, int], tuple[ProducedOutputSemantics, ...] | None, ] = field(default_factory=dict) - producer_paths_by_pattern: dict[ - tuple[int, int, str, int], - tuple[str, ...], - ] = field(default_factory=dict) filtered_paths_by_plan: dict[ tuple[int, int, tuple[str, ...], int], tuple[str, ...], @@ -292,7 +302,6 @@ def record_outputs( def _invalidate_record_selection_caches(self) -> None: self.records_revision += 1 self.selected_records_by_plan.clear() - self.producer_paths_by_pattern.clear() self.filtered_paths_by_plan.clear() def producer_records_for( @@ -346,18 +355,14 @@ def producer_patterns_by_execution_group( key = plan.execution_group_scope.normalize_key(value) if plan.execution_group_scope.contains_runtime_key(key): records_by_group.setdefault(key, []).append(record) - selectors = tuple( - (pattern, ProducedPathPatternSelector.from_pattern(pattern)) - for pattern in dict.fromkeys(patterns) - ) - return { - key: tuple( - pattern - for pattern, selector in selectors - if selector.matches(ProducedPathSet.from_records(group_records, parser)) + patterns = tuple(dict.fromkeys(patterns)) + groups = {} + for key, group_records in records_by_group.items(): + index = ProducedPathRecordIndex.from_records(group_records, parser) + groups[key] = tuple( + pattern for pattern in patterns if index.contains(pattern) ) - for key, group_records in records_by_group.items() - } + return groups def produced_records_for( self, @@ -408,21 +413,18 @@ def producer_output_records_for_paths( ) -> tuple[ProducedOutputSemantics, ...] | None: """Resolve exact producer declarations in the requested physical path order.""" - producer_records = self._selected_unique_producer_records_for(plan) - if producer_records is None: - return None + index = self.producer_record_index_for(plan, parser) + return None if index is None else index.records_for_paths(paths) - path_index = ProducedPathRecordIndex.from_records(producer_records, parser) - records: list[ProducedOutputSemantics] = [] - for path in paths: - matching_records = path_index.matching_records(path) - if len(matching_records) != 1: - raise NoStepOutputManifestMatch( - "Expected one producer output context for input path " - f"{path!r}, found {len(matching_records)}." - ) - records.append(matching_records[0]) - return tuple(records) + def producer_record_index_for( + self, plan: CompiledStepPlan, parser: FilenameParser, + ) -> ProducedPathRecordIndex | None: + """Admit one current producer cohort with its correlated address aliases.""" + records = self._selected_unique_producer_records_for(plan) + return ( + None if records is None + else ProducedPathRecordIndex.from_records(records, parser) + ) def filter_to_producer_paths( self, @@ -440,11 +442,10 @@ def filter_to_producer_paths( if cached is not None: return list(cached) - producer_records = self._selected_unique_producer_records_for(plan) - if producer_records is None: + index = self.producer_record_index_for(plan, parser) + if index is None: return list(paths) - allowed = ProducedPathSet.from_records(producer_records, parser) - selected = [path for path in paths if allowed.contains(path)] + selected = [path for path in paths if index.contains(path)] if selected: self.filtered_paths_by_plan[cache_key] = tuple(selected) return selected @@ -453,30 +454,6 @@ def filter_to_producer_paths( self.filtered_paths_by_plan[cache_key] = () return [] - def producer_paths_matching_pattern( - self, - plan: CompiledStepPlan, - pattern: str, - parser: FilenameParser, - ) -> list[str]: - """Return requested producer paths addressed by a detected pattern.""" - cache_key = (self.records_revision, id(plan), str(pattern), id(parser)) - cached = self.producer_paths_by_pattern.get(cache_key) - if cached is not None: - return list(cached) - - producer_records = self._selected_unique_producer_records_for(plan) - if producer_records is None: - return [] - selector = ProducedPathPatternSelector.from_pattern(pattern) - selected = tuple( - record.output_path - for record in producer_records - if selector.matches(ProducedPathSet.from_records((record,), parser)) - ) - self.producer_paths_by_pattern[cache_key] = selected - return list(selected) - def _selected_unique_producer_records_for( self, plan: CompiledStepPlan, @@ -580,13 +557,6 @@ def records_for_key( return () return self.records_by_key[key] - @staticmethod - def allowed_path_tokens( - records: Sequence[ProducedOutputSemantics], - parser: FilenameParser, - ) -> set[str]: - return ProducedPathSet.from_records(records, parser).tokens - @staticmethod def key_for_producer( plan: CompiledStepPlan, @@ -616,35 +586,8 @@ def step_output_manifest(context: ProcessingContext) -> StepOutputManifestStore: @dataclass(frozen=True, slots=True) -class ProducedPathSet: - """Path membership authority for concrete and template producer anchors.""" - - tokens: frozenset[str] - - @classmethod - def from_records( - cls, - records: Sequence[ProducedOutputSemantics], - parser: FilenameParser, - ) -> "ProducedPathSet": - tokens: set[str] = set() - for record in records: - for value in (record.relative_output_path, record.output_path): - tokens.add(source_path_identity(value).as_posix()) - tokens.add(source_path_identity(value).name) - if record.filename_qualifier is not None: - tokens.add( - record.without_filename_qualifier().filename(parser) - ) - return cls(frozenset(tokens)) - - def contains(self, path: str) -> bool: - return ProducedPathPatternSelector.from_pattern(path).matches(self) - - -@dataclass(frozen=True, slots=True) -class ProducedPathRecordIndex(ProducedPathSet): - """Batch lookup of original producer records through their owned path tokens.""" +class ProducedPathRecordIndex: + """Correlated producer occurrences and their concrete/template address aliases.""" records: tuple[ProducedOutputSemantics, ...] record_indices_by_token: dict[str, tuple[int, ...]] @@ -654,52 +597,59 @@ def from_records( cls, records: Sequence[ProducedOutputSemantics], parser: FilenameParser, - ) -> "ProducedPathRecordIndex": + ) -> ProducedPathRecordIndex: indices_by_token: dict[str, list[int]] = {} for index, record in enumerate(records): - for token in ProducedPathSet.from_records((record,), parser).tokens: + for token in record.path_tokens(parser): indices_by_token.setdefault(token, []).append(index) return cls( - tokens=frozenset(indices_by_token), records=tuple(records), record_indices_by_token={ token: tuple(indices) for token, indices in indices_by_token.items() }, ) + def matching_tokens(self, path: str) -> Iterator[str]: + """Resolve exact and template address aliases through the existing matcher.""" + path = source_path_identity(path).as_posix() + tokens = self.record_indices_by_token + matcher = PathPatternTemplateMatcher.from_pattern(path) + if path in tokens: + yield path + if matcher is not None: + for token in tokens: + if token != path and matcher.matches(token): + yield token + + def contains(self, path: str) -> bool: + return next(self.matching_tokens(path), None) is not None + def matching_records(self, path: str) -> tuple[ProducedOutputSemantics, ...]: - selector = ProducedPathPatternSelector.from_pattern(path) - indices = sorted( - { - index - for token in selector.matching_tokens(self) - for index in self.record_indices_by_token[token] - } - ) + indices = sorted({ + index + for token in self.matching_tokens(path) + for index in self.record_indices_by_token[token] + }) return tuple(self.records[index] for index in indices) + def record_for_path(self, path: str) -> ProducedOutputSemantics: + """Require one original occurrence for one admitted physical address.""" + records = self.matching_records(path) + if len(records) != 1: + raise NoStepOutputManifestMatch( + "Expected one producer output context for input path " + f"{path!r}, found {len(records)}." + ) + return records[0] -@dataclass(frozen=True, slots=True) -class ProducedPathPatternSelector: - """Prepared matcher for concrete and template producer path membership.""" - - path: str - matcher: PathPatternTemplateMatcher | None + def records_for_paths( + self, paths: Sequence[str], + ) -> tuple[ProducedOutputSemantics, ...]: + return tuple(self.record_for_path(path) for path in paths) - @classmethod - def from_pattern(cls, path: str) -> "ProducedPathPatternSelector": - path_text = source_path_identity(path).as_posix() - return cls(path_text, PathPatternTemplateMatcher.from_pattern(path_text)) - - def matches(self, path_set: ProducedPathSet) -> bool: - return next(self.matching_tokens(path_set), None) is not None - - def matching_tokens(self, path_set: ProducedPathSet) -> Iterator[str]: - """Yield exact and template aliases through one matching policy.""" - tokens = path_set.tokens - if self.path in tokens: - yield self.path - if self.matcher is not None: - for token in tokens: - if token != self.path and self.matcher.matches(token): - yield token + def validate_input_records( + self, records: Sequence[ProducedOutputSemantics], + ) -> None: + """Validate address cardinality while retaining the already-selected cohort.""" + for record in records: + self.record_for_path(record.output_path) diff --git a/openhcs/core/steps/function_runtime.py b/openhcs/core/steps/function_runtime.py index 39b2824e14..f415a680d7 100644 --- a/openhcs/core/steps/function_runtime.py +++ b/openhcs/core/steps/function_runtime.py @@ -108,7 +108,6 @@ RuntimeSliceProjection, RuntimeSliceProjectionDeclarationError, ) -from openhcs.core.source_image_semantics import apply_source_binding_payload from openhcs.core.source_image_provenance import ( SourceImageIdentity, SourceImageProvenance, @@ -2542,10 +2541,16 @@ def _load_input_stack( raise RuntimeError("MicroscopeHandler not available in context.") output_manifest = step_output_manifest(context) - producer_matching_files = output_manifest.producer_paths_matching_pattern( - plan, - str(request.pattern_group_info), - context.microscope_handler.parser, + producer_index = output_manifest.producer_record_index_for( + plan, context.microscope_handler.parser, + ) + producer_records = ( + None if producer_index is None + else producer_index.matching_records(str(request.pattern_group_info)) + ) + producer_matching_files = ( + () if producer_records is None + else tuple(record.output_path for record in producer_records) ) matching_files = list(producer_matching_files) source_projection = ( @@ -2563,11 +2568,13 @@ def _load_input_stack( else None ), ) - matching_files = output_manifest.filter_to_producer_paths( - plan, - matching_files, - context.microscope_handler.parser, - ) + if producer_index is not None and not producer_matching_files: + selected_paths = [ + path for path in matching_files if producer_index.contains(path) + ] + if matching_files and not selected_paths: + raise NoStepOutputManifestMatch + matching_files = selected_paths if not matching_files: raise ValueError( @@ -2622,13 +2629,14 @@ def _load_input_stack( matching_files=matching_files, source_projection=source_projection, ).runtime_context() - producer_records = ( - output_manifest.producer_output_records_for_paths( - plan, matching_files, context.microscope_handler.parser, - ) - if plan.main_input_dependency.kind is StepInputDependencyKind.STEP_OUTPUT - else None - ) + if producer_index is not None: + producer_records = tuple(sorted( + producer_records, key=lambda record: record.output_path, + )) + if matching_files != [record.output_path for record in producer_records]: + producer_records = producer_index.records_for_paths(matching_files) + else: + producer_index.validate_input_records(producer_records) ImagePayloadStackComposition.validate_main_flow_cohort(producer_records) cached_stack = context.runtime_image_stack_cache.get( tuple(full_file_paths), @@ -2949,7 +2957,7 @@ def _apply_source_binding_payload( f"Source-bound payload {source_path!r} declares unknown alias " f"{alias!r}." ) - return apply_source_binding_payload(payload, binding, source_context) + return binding.apply_loaded_payload(payload, source_context) @staticmethod def execute_chain( diff --git a/tests/unit/test_cellprofiler_source_bindings.py b/tests/unit/test_cellprofiler_source_bindings.py index c5b44a9f57..6271adcd21 100644 --- a/tests/unit/test_cellprofiler_source_bindings.py +++ b/tests/unit/test_cellprofiler_source_bindings.py @@ -21,9 +21,10 @@ PipelineConfig, ) from openhcs.core.source_metadata import SourceVoxelSpacing, SourceVoxelSpacingUnit -from openhcs.core.source_image_semantics import apply_source_binding_payload from openhcs.core.runtime_image_loading import ImagePayloadSourceMetadataContext from openhcs.core.runtime_image_values import ( + ImageMetadataPayload, + ImagePayloadMetadata, image_payload_data, image_payload_metadata, ) @@ -843,9 +844,8 @@ def test_names_and_types_contributes_payload_loading_semantics( multiband_data = np.zeros((5, 6, 2), dtype=np.float32) multiband_path = tmp_path / "multiband.tiff" tifffile.imwrite(multiband_path, multiband_data) - multiband = apply_source_binding_payload( + multiband = color.apply_loaded_payload( tifffile.imread(multiband_path), - color, ImagePayloadSourceMetadataContext( SourceImageIdentity(str(multiband_path)), ), @@ -853,12 +853,20 @@ def test_names_and_types_contributes_payload_loading_semantics( multiband_metadata = image_payload_metadata(multiband) assert multiband_metadata.source_channel_axis == -1 assert multiband_metadata.source_spatial_shape_yx == (5, 6) - grayscale = apply_source_binding_payload( + grayscale = color.apply_loaded_payload( np.zeros((512, 512), dtype=np.uint8), - color, None, ) assert image_payload_metadata(grayscale).source_channel_axis is None + grayscale = color.apply_loaded_payload( + ImageMetadataPayload( + np.zeros((512, 512), dtype=np.uint8), + ImagePayloadMetadata(source_channel_axis=-1), + ), + ImagePayloadSourceMetadataContext(SourceImageIdentity("grayscale.tif")), + ) + assert image_payload_metadata(grayscale).source_channel_axis is None + assert image_payload_metadata(grayscale).source_spatial_shape_yx == (512, 512) assert mask.artifact_kind is ImageArtifactType assert mask.load_as_mask is True assert objects.artifact_kind is ObjectLabelsArtifactType @@ -882,7 +890,7 @@ def test_monochrome_source_normalizes_before_collapsing_rgb_channels() -> None: source_channel_counts=frozenset((3, 4)), ) - observed = apply_source_binding_payload(rgb, binding, None) + observed = binding.apply_loaded_payload(rgb, None) expected = codes.astype(np.float32) / np.float32(255) np.testing.assert_array_equal(image_payload_data(observed), expected) @@ -909,7 +917,7 @@ def test_monochrome_source_uses_rgb_luminance_for_distinct_channels() -> None: source_channel_counts=frozenset((3, 4)), ) - observed = apply_source_binding_payload(rgb, binding, None) + observed = binding.apply_loaded_payload(rgb, None) expected = rgb2gray(rgb.astype(np.float32) / np.float32(255)) np.testing.assert_array_equal(image_payload_data(observed), expected) diff --git a/tests/unit/test_function_runtime_source_projection.py b/tests/unit/test_function_runtime_source_projection.py index 90b0f9e18b..76878d4fb7 100644 --- a/tests/unit/test_function_runtime_source_projection.py +++ b/tests/unit/test_function_runtime_source_projection.py @@ -906,11 +906,10 @@ def test_step_output_manifest_pattern_lookup_returns_producer_memory_paths( ), ) - assert store.producer_paths_matching_pattern( - consumer, - "A14_s{iii}_w3_z001_t001.tif", - SourceSchemaFilenameParser(), - ) == [ + index = store.producer_record_index_for(consumer, SourceSchemaFilenameParser()) + assert [record.output_path for record in index.matching_records( + "A14_s{iii}_w3_z001_t001.tif" + )] == [ str(output_dir / "A14_s001_w3_z001_t001.tif"), str(output_dir / "A14_s002_w3_z001_t001.tif"), ] @@ -1014,14 +1013,8 @@ def test_step_output_manifest_does_not_treat_artifact_inputs_as_main_flow( ), ) - assert ( - store.producer_paths_matching_pattern( - consumer, - "A14_s{iii}_w1_z001_t001.tif", - SourceSchemaFilenameParser(), - ) - == [] - ) + index = store.producer_record_index_for(consumer, SourceSchemaFilenameParser()) + assert index is None def test_step_output_manifest_uses_declared_artifact_producer_scope( @@ -3862,15 +3855,6 @@ def test_producer_anchored_pipeline_start_paths_use_exact_source_projection_bund ) ) - class SourceManifest: - @staticmethod - def producer_paths_matching_pattern(*_args): - return list(virtual_paths) - - @staticmethod - def filter_to_producer_paths(_plan, paths, _parser): - return list(paths) - class SourceFileManager: @staticmethod def load_batch(paths, backend): @@ -3892,7 +3876,7 @@ def resolve_address(backend_address, backend, *, base_path): monkeypatch.setattr( function_runtime, "step_output_manifest", - lambda _context: SourceManifest(), + lambda _context: StepOutputManifestStore(), ) monkeypatch.setattr( function_runtime.SourceBindingRuntimeContextRequest, @@ -3996,13 +3980,6 @@ def test_step_output_load_preserves_producer_stack_plane_provenance( ), ).payload_with(np.zeros((2, 4, 5), dtype=np.float32), None) - class ProducerManifest: - def producer_paths_matching_pattern(self, *_args): - return [str(output_path)] - - def filter_to_producer_paths(self, _plan, paths, _parser): - return list(paths) - class MemoryFileManager: def load_batch(self, paths, backend): assert paths == [str(output_path)] @@ -4013,7 +3990,7 @@ def load_batch(self, paths, backend): monkeypatch.setattr( function_runtime, "step_output_manifest", - lambda _context: ProducerManifest(), + lambda _context: producer_manifest, ) monkeypatch.setattr( function_runtime.SourceBindingRuntimeContextRequest, @@ -4049,7 +4026,26 @@ def load_batch(self, paths, backend): input_dir=tmp_path, input_memory_type="numpy", variable_components=(VariableComponents.Z_INDEX,), + compiled_function_pattern=compile_function_pattern(lambda image: image, {}, {}), + main_input_dependency=StepInputDependency.step_output( + source_step_index=0, source_step_scope_id="producer", + ), ) + producer = CompiledStepPlan( + step_index=0, step_name="producer", step_type="FunctionStep", + step_scope_id="producer", axis_id="A01", output_dir=tmp_path, + ) + producer_manifest = StepOutputManifestStore() + producer_manifest.begin_step(producer) + producer_manifest.record_outputs(producer, ( + ProducedOutputSemantics.from_output( + producer, output_path, + FunctionOutputIdentity( + component_values={"well": "A01", "site": "1", "channel": "2", "z_index": "1", "timepoint": "1"}, + extension=".tif", source="test", + ), + ).with_filename_qualifier("RescaledDNA"), + )) context = SimpleNamespace( microscope_handler=SimpleNamespace(parser=SourceSchemaFilenameParser()), filemanager=MemoryFileManager(), @@ -5602,3 +5598,85 @@ def test_step_output_manifest_batch_lookup_rejects_shared_basename( assert store.producer_output_contexts_for_paths( consumer, (records[0].output_path, other.output_path), parser ) == (records[0].output_context, other.output_context) + + +def test_producer_admission_rederives_live_storage_aliases_each_epoch( + qualified_producer_manifest, +): + store, producer, consumer, records, parser = qualified_producer_manifest + storage_components = dict(records[0].filename_values) + record = replace(records[0], filename_component_values=storage_components) + store.record_outputs(producer, (record,)) + old_index = store.producer_record_index_for(consumer, parser) + storage_components["channel"] = 9 + new_alias = record.without_filename_qualifier().filename(parser) + + assert old_index.matching_records(new_alias) == () + current_index = store.producer_record_index_for(consumer, parser) + assert current_index.matching_records(new_alias) == (record,) + assert current_index.record_for_path(record.output_path) is record + + +def test_producer_loader_validates_ambiguity_after_source_context_before_cache( + monkeypatch, +): + from openhcs.core.steps import function_runtime + + producer = CompiledStepPlan( + step_index=0, step_name="producer", step_type="FunctionStep", + step_scope_id="producer", axis_id="A01", output_dir=Path("/memory"), + ) + plan = CompiledStepPlan( + step_index=1, step_name="consumer", step_type="FunctionStep", + axis_id="A01", input_dir=Path("/memory"), input_memory_type="numpy", + compiled_function_pattern=compile_function_pattern(lambda image: image, {}, {}), + main_input_dependency=StepInputDependency.step_output( + source_step_index=0, source_step_scope_id="producer", + ), + ) + manifest = StepOutputManifestStore() + manifest.begin_step(producer) + manifest.record_outputs(producer, ( + ProducedOutputSemantics.from_existing_main_flow_path( + producer, "A01_s001_w1_z001_t001.tif", SourceSchemaFilenameParser(), + ), + ProducedOutputSemantics.from_output( + producer, "/memory/other/A01_s001_w1_z001_t001.tif", + FunctionOutputIdentity( + component_values={"well": "A01", "site": 2, "channel": 1, "z_index": 1, "timepoint": 1}, + extension=".tif", source="test", + ), + ), + )) + events = [] + + def source_context(): + events.append("source_context") + return SourceBindingRuntimeContext.empty() + + class RejectImageCache: + def get(self, *_args, **_kwargs): + pytest.fail("Ambiguous producer admission must precede cached pixels") + + context = SimpleNamespace( + microscope_handler=SimpleNamespace(parser=SourceSchemaFilenameParser()), + runtime_image_stack_cache=RejectImageCache(), + ) + runtime = function_runtime.PatternGroupRuntime( + function_runtime.PatternGroupExecutionRequest( + context=context, execution_plan=plan, + compiled_group=plan.compiled_function_pattern.default_group, + pattern_group_info="A01_s001_w1_z001_t001.tif", component_index=0, component_count=1, + ) + ) + monkeypatch.setattr(function_runtime, "step_output_manifest", lambda _context: manifest) + monkeypatch.setattr(runtime, "source_workspace_projection_authority", lambda: SimpleNamespace( + projection_if_available=lambda: None, + )) + monkeypatch.setattr(function_runtime.SourceBindingRuntimeContextRequest, "from_context", classmethod( + lambda cls, **_kwargs: SimpleNamespace(runtime_context=source_context), + )) + + with pytest.raises(NoStepOutputManifestMatch, match="found 2"): + runtime._load_input_stack() + assert events == ["source_context"] diff --git a/tests/unit/test_persisted_image_metadata.py b/tests/unit/test_persisted_image_metadata.py index a7a4566d87..cd1ddc68ff 100644 --- a/tests/unit/test_persisted_image_metadata.py +++ b/tests/unit/test_persisted_image_metadata.py @@ -481,11 +481,11 @@ def test_native_header_reload_preserves_declared_crop_and_alias(tmp_path, origin context = ImagePayloadSourceMetadataContext(SourceImageIdentity(str(path), {})) from openhcs.core.runtime_image_values import image_payload_metadata from openhcs.core.source_bindings import NamedSourceBinding - from openhcs.core.source_image_semantics import apply_source_binding_payload current = image_payload_metadata( - apply_source_binding_payload( - reloaded, NamedSourceBinding(alias="neurite"), context + NamedSourceBinding(alias="neurite").apply_loaded_payload( + reloaded, + context, ) ) assert current.source_dtype == "float32" @@ -503,7 +503,6 @@ def test_native_mosaic_replaces_incompatible_old_tile_domain_without_losing_sour ): from openhcs.core.runtime_image_values import image_payload_metadata from openhcs.core.source_bindings import NamedSourceBinding - from openhcs.core.source_image_semantics import apply_source_binding_payload metadata = metadata_fixture().replace_fields( source_spatial_domain=SourceSpatialDomain((0, 0), (4, 6)) @@ -516,9 +515,8 @@ def test_native_mosaic_replaces_incompatible_old_tile_domain_without_losing_sour image_format.read(path), image_format.persisted_metadata(path, payload) ) current = image_payload_metadata( - apply_source_binding_payload( + NamedSourceBinding(alias="neurite").apply_loaded_payload( reloaded, - NamedSourceBinding(alias="neurite"), ImagePayloadSourceMetadataContext(SourceImageIdentity(str(path), {})), ) ) @@ -616,7 +614,6 @@ def test_native_headerless_hwc_replay_retains_declared_channel_semantics( from openhcs.core.runtime_image_values import image_payload_metadata from openhcs.core.source_bindings import NamedSourceBinding - from openhcs.core.source_image_semantics import apply_source_binding_payload pixels = np.arange(60, dtype=np.uint8).reshape(4, 5, 3) metadata = metadata_fixture().replace_fields( @@ -652,9 +649,8 @@ def test_native_headerless_hwc_replay_retains_declared_channel_semantics( json.loads(json.dumps(to_jsonable(restored))) ) assert restored.source_channel_axis == -1 - bound = apply_source_binding_payload( + bound = NamedSourceBinding(alias="neurite").apply_loaded_payload( ImageMetadataPayload(loaded, restored), - NamedSourceBinding(alias="neurite"), context, ) current = image_payload_metadata(bound) diff --git a/tests/unit/test_runtime_adapter_source_artifacts.py b/tests/unit/test_runtime_adapter_source_artifacts.py index c2bea0bcff..cb0b5a6f76 100644 --- a/tests/unit/test_runtime_adapter_source_artifacts.py +++ b/tests/unit/test_runtime_adapter_source_artifacts.py @@ -33,7 +33,6 @@ StepSourceBindingsConfig, ) from openhcs.core.source_image_provenance import SourceImageIdentity -from openhcs.core.source_image_semantics import apply_source_binding_payload from openhcs.core.source_matching import SourceImageSetIdentityPolicy from openhcs.core.source_projection import SourcePlaneProjection from openhcs.core.source_workspace_projection import ( @@ -178,9 +177,8 @@ def binding( Backend.VIRTUAL_WORKSPACE.value, ), ) - primary_payload = apply_source_binding_payload( + primary_payload = primary_binding.apply_loaded_payload( primary_payload, - primary_binding, ImagePayloadSourceMetadataContext( SourceImageIdentity( primary_virtual_path, diff --git a/tests/unit/test_runtime_values.py b/tests/unit/test_runtime_values.py index d89c1bde66..2108cad6ee 100644 --- a/tests/unit/test_runtime_values.py +++ b/tests/unit/test_runtime_values.py @@ -112,7 +112,6 @@ SourceImageIdentity, SourceImageProvenancePlanes, ) -from openhcs.core.source_image_semantics import apply_source_binding_payload from openhcs.core.source_metadata import ( SOURCE_PLANE_COUNT_FIELD, SOURCE_PLANE_INDEX_FIELD, @@ -2142,9 +2141,8 @@ def test_source_aligned_object_labels_replace_stale_plane_provenance() -> None: def test_source_image_loading_semantics_attaches_component_metadata() -> None: image = np.zeros((4, 5), dtype=np.uint16) - payload = apply_source_binding_payload( + payload = NamedSourceBinding(alias="DNA").apply_loaded_payload( image, - NamedSourceBinding(alias="DNA"), ImagePayloadSourceMetadataContext( SourceImageIdentity( "01_POS002_D.TIF", @@ -2301,13 +2299,12 @@ def test_declared_source_pixel_channel_axis_owns_source_spatial_domain() -> None ), ) - bound = apply_source_binding_payload( + bound = NamedSourceBinding( + alias="Color", + source_channel_axis=-1, + source_channel_counts=frozenset((3, 4)), + ).apply_loaded_payload( payload, - NamedSourceBinding( - alias="Color", - source_channel_axis=-1, - source_channel_counts=frozenset((3, 4)), - ), ImagePayloadSourceMetadataContext(SourceImageIdentity("color.tif")), ) @@ -2325,15 +2322,14 @@ def test_object_label_source_image_semantics_treats_rgb_image_as_label_plane() - ImagePayloadMetadata(source_channel_axis=-1), ) - payload = apply_source_binding_payload( + payload = NamedSourceBinding( + alias="Objects", + artifact_kind=ObjectLabelsArtifactType, + projection_role=SourceProjectionRole.SOURCE_ARTIFACT, + source_channel_axis=-1, + source_channel_counts=frozenset((3, 4)), + ).apply_loaded_payload( source, - NamedSourceBinding( - alias="Objects", - artifact_kind=ObjectLabelsArtifactType, - projection_role=SourceProjectionRole.SOURCE_ARTIFACT, - source_channel_axis=-1, - source_channel_counts=frozenset((3, 4)), - ), ImagePayloadSourceMetadataContext(SourceImageIdentity("objects.png")), ) @@ -2353,13 +2349,12 @@ def test_source_file_pixel_semantics_declare_ingestion_channel_axis( image = np.zeros((4, 5, 3), dtype=np.uint8) iio.imwrite(path, image) - payload = apply_source_binding_payload( + payload = NamedSourceBinding( + alias="Color", + source_channel_axis=-1, + source_channel_counts=frozenset((3, 4)), + ).apply_loaded_payload( iio.imread(path), - NamedSourceBinding( - alias="Color", - source_channel_axis=-1, - source_channel_counts=frozenset((3, 4)), - ), ImagePayloadSourceMetadataContext(SourceImageIdentity(str(path))), ) @@ -2394,13 +2389,12 @@ def test_source_file_pixel_semantics_allow_monochrome_without_declared_channel_a path = tmp_path / "source.png" iio.imwrite(path, np.zeros((4, 5), dtype=np.uint8)) - payload = apply_source_binding_payload( + payload = NamedSourceBinding( + alias="Color", + source_channel_axis=-1, + source_channel_counts=frozenset((3, 4)), + ).apply_loaded_payload( iio.imread(path), - NamedSourceBinding( - alias="Color", - source_channel_axis=-1, - source_channel_counts=frozenset((3, 4)), - ), ImagePayloadSourceMetadataContext(SourceImageIdentity(str(path))), ) From 65a76d7e989999d3f78a179e46411baa38a7c883 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 18:08:36 -0400 Subject: [PATCH 158/235] fix(transport): remove process-local preparation from contract wire views Refs #558. Normalize both reference and contract preparation metadata while preserving the warmed runtime identity. Strengthen the existing real pickle control against both fields. --- openhcs/core/function_step_transport.py | 3 ++- tests/unit/test_function_step_transport.py | 13 +++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/openhcs/core/function_step_transport.py b/openhcs/core/function_step_transport.py index 57d05c7722..2ea347cc2b 100644 --- a/openhcs/core/function_step_transport.py +++ b/openhcs/core/function_step_transport.py @@ -226,9 +226,10 @@ def normalize_callable_contract( if ( normalized_func is contract.func and normalized_raw is contract.raw_processing_function + and contract.metadata.prepare is None ): return contract - metadata = contract.metadata + metadata = contract.metadata.without_prepare() if normalized_raw is not contract.raw_processing_function: metadata = metadata.with_raw_processing_function(normalized_raw) return replace( diff --git a/tests/unit/test_function_step_transport.py b/tests/unit/test_function_step_transport.py index f3cb9c1448..3d0bf7398d 100644 --- a/tests/unit/test_function_step_transport.py +++ b/tests/unit/test_function_step_transport.py @@ -418,6 +418,16 @@ def test_bundle_transport_preserves_prepared_runtime_contract_identity() -> None ) pattern = compile_function_pattern(reference, {}, {}) (invocation,) = tuple(pattern.iter_invocations()) + invocation = replace( + invocation, + contract=replace( + invocation.contract, + metadata=replace(invocation.contract.metadata, prepare=lambda: None), + ), + ) + pattern = replace( + pattern, groups=(replace(pattern.groups[0], invocations=(invocation,)),) + ) prepared_callable = FunctionInvocationCallableResolver.resolve(invocation) plan = CompiledStepPlan( step_index=0, step_name="Crop", step_type="FunctionStep", axis_id="A01", @@ -440,6 +450,9 @@ def test_bundle_transport_preserves_prepared_runtime_contract_identity() -> None assert transport_context is not context assert transport_plan is not plan assert transport_plan.func.metadata.prepare is None + (transport_invocation,) = tuple(transport_plan.compiled_function_pattern.iter_invocations()) + assert transport_invocation.contract.metadata.prepare is None + assert invocation.contract.metadata.prepare is not None assert plan.func is reference and reference.metadata.prepare is not None assert plan.compiled_function_pattern is pattern assert FunctionInvocationCallableResolver.resolve(invocation) is prepared_callable From 7b7f6414d07b85300d127459bdcd07a235bd39ce Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 18:09:21 -0400 Subject: [PATCH 159/235] docs(perf): record runtime boundary cleanup and derive figure target labels Refs #384, #558. Record qualified semantics separately from measured source. Render target labels from their actual declared values; corrected fresh figures retain the original qualified timing samples. --- benchmark/reports/cppipe_figures.py | 11 +++--- .../runtime-owner-consolidation-20261003.rst | 35 ++++++++++++++++++- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/benchmark/reports/cppipe_figures.py b/benchmark/reports/cppipe_figures.py index 02eb7781b5..7ea906b70f 100644 --- a/benchmark/reports/cppipe_figures.py +++ b/benchmark/reports/cppipe_figures.py @@ -1192,7 +1192,7 @@ def _plot_average_speedup_points( alpha=0.86, ) axis.annotate( - "4x target", + f"{SPEEDUP_TARGET:g}x target", xy=(0.995, SPEEDUP_TARGET), xycoords=("axes fraction", "data"), xytext=(-2, 3), @@ -1423,7 +1423,7 @@ def plot_cdf(self, *, log_x: bool) -> tuple[Path, ...]: alpha=0.86, ) axis.annotate( - "4x target", + f"{SPEEDUP_TARGET:g}x target", xy=(SPEEDUP_TARGET, 99.0), xycoords=("data", "data"), xytext=(3, -2), @@ -1626,7 +1626,10 @@ def _draw_reference_lines(axis, *, metric: FigureMetricSpec, log_y: bool) -> Non linestyle="--", alpha=0.86, ) - label = "4x target" if metric.key == SPEEDUP_METRIC_KEY else "target" + label = ( + f"{metric.target_line:g}x target" + if metric.key == SPEEDUP_METRIC_KEY else "target" + ) axis.annotate( label, xy=(0.995, metric.target_line), @@ -1689,7 +1692,7 @@ def _write_benchmark_figure_index(path: Path, outputs: Sequence[Path]) -> None: "- `cppipe_accuracy_zoom.*`: broken-axis parity view for tiny numeric drift near 100%.", "- `cppipe_raw_seconds.*`: single-thread execution runtime in seconds.", "- `cppipe_raw_seconds_log.*`: runtime on a log scale for mixed short and long pipelines.", - "- `cppipe_speedup.*`: execution speedup versus native CellProfiler with the 4x target line.", + f"- `cppipe_speedup.*`: execution speedup versus native CellProfiler with the {SPEEDUP_TARGET:g}x target line.", "- `cppipe_speedup_log.*`: speedup on a log scale for wide dynamic range.", "- `cppipe_speedup_summary_statistics.*`: min, max, median, and mean speedup tables.", "- `cppipe_speedup_cumulative_distribution.*`: percent of datasets at or above each speedup threshold.", diff --git a/docs/refactor/receipts/runtime-owner-consolidation-20261003.rst b/docs/refactor/receipts/runtime-owner-consolidation-20261003.rst index 075f608a5c..1139d5b28f 100644 --- a/docs/refactor/receipts/runtime-owner-consolidation-20261003.rst +++ b/docs/refactor/receipts/runtime-owner-consolidation-20261003.rst @@ -244,6 +244,39 @@ Physical publication loads original memory addresses and writes manifest-owned destinations under the current output root; preserved outputs no longer write back into the input plate. +Current runtime and transport boundaries +---------------------------------------- + +Commit ``9d79c4289`` repairs the declared runtime/transport ownership boundary +(#558). Objectstate serialization resolution returns ordinary processing +contexts unchanged; the two bundle maps therefore previously shared plans, +and transport normalization rewrote the prepared runtime graph. The existing +bundle now derives separate transport context/plan snapshots. Inline, fork and +thread resources consume rich contexts directly; queued process workers and +public serialization consume transport snapshots. Stores, pixels, configuration +and service lifetimes retain their existing sharing. This is not a claim that +all compiled plans have been sealed. Actual warmed callable identity and pickle +roundtrip controls pass. Commit ``65a76d7e9`` also strips process-local hooks +from the compiled contract metadata on its transport view, preserving the +rich contract identity; both callable-reference and contract fields are +covered by the same real pickle control. + +Commit ``ad4670f20`` folds producer path admission into +``ProducedPathRecordIndex``. ``ProducedPathSet``, ``ProducedPathPatternSelector`` +and the detached producer-pattern path cache are removed. A loader resolves one +current record cohort and derives its paths, retaining cardinality admission +after source-context construction and before cache/pixel access. Source-only +and pipeline-start requests do not acquire producer records. + +The same batch puts source transformations on ``NamedSourceBinding`` and deletes +``source_image_semantics.py``. The physical metadata context retains header, +calibration and crop geometry; a binding's already-resolved channel axis remains +authoritative after missing-context merge, including declared grayscale absence. +Mutable scalar provenance remains independent. One coupled source, producer, +calibration and original measurement journey gate passes 236 controls. This +later source is not included in the paired performance evidence below; no +additional speedup is claimed before its scientific comparison. + Evidence scope -------------- @@ -300,7 +333,7 @@ Means of two independent ordinary and two fresh native observations are: These observations do not establish a causal gain or full30/scaling behavior. Speckles still loses on total. The existing benchmark figure owner produces fresh PNG/SVG runtime and speedup figures in the campaign's ``figures-v2`` -directory. Native invocation includes its pre-first-module work; startup, +directory (corrected target labels in ``figures-v3``). Native invocation includes its pre-first-module work; startup, imports/JVM setup and the excluded warmup observation are outside this clock. Ordinary total includes compilation and normal OUTCOMES/RSS completion. The ``v4`` namespace is explicitly aborted before measurement, after independent From 3c42ed32f2d93149d6222cf603e8f4319aba489b Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 18:24:16 -0400 Subject: [PATCH 160/235] perf(runtime): follow live consumers and preserve retained projection wire records --- openhcs/core/compiled_step_plan.py | 56 +++++------ openhcs/core/source_projection.py | 26 +++++- openhcs/core/virtual_workspace_metadata.py | 92 +++++++++++++++---- .../test_loaded_pattern_group_ownership.py | 75 +++++++++++++++ tests/unit/test_persisted_image_metadata.py | 49 +++++++--- 5 files changed, 235 insertions(+), 63 deletions(-) diff --git a/openhcs/core/compiled_step_plan.py b/openhcs/core/compiled_step_plan.py index 8c6192693f..e89c619ee5 100644 --- a/openhcs/core/compiled_step_plan.py +++ b/openhcs/core/compiled_step_plan.py @@ -348,42 +348,44 @@ def stored_primary_input_edges_for_group( def requires_main_flow_checkpoint( self, plans: Mapping[int, "CompiledStepPlan"], ) -> bool: - """Derive pixel transport demand, including preserved input lifetimes.""" + """Follow this value's consumers through preserved input lifetimes.""" if not plans or self.step_index not in plans: return True - demanded: set[int] = set() + consumers: dict[int, list[CompiledStepPlan]] = {} for plan in plans.values(): + predecessor = plan.main_input_dependency.predecessor_step_index() + if predecessor is not None: + consumers.setdefault(predecessor, []).append(plan) + pending = [self.step_index] + visited: set[int] = set() + while pending: + step_index = pending.pop() + if step_index in visited: + continue + visited.add(step_index) + plan = plans.get(step_index) + if plan is None: + continue if ( plan.write_backend != Backend.MEMORY.value or plan.materialized_output is not None or plan.streaming_configs or plan.visualize ): - demanded.add(plan.step_index) - pattern = plan.compiled_function_pattern - if pattern is None or any( - (group := pattern.group_for_component(component_key)) is None - or plan.stored_primary_input_edges_for_group( - group, component_key, - ) is None - for component_key in plan.execution_group_scope.keys - ): - predecessor = plan.main_input_dependency.predecessor_step_index() - if predecessor is not None: - demanded.add(predecessor) - pending = list(demanded) - while pending: - current = plans.get(pending.pop()) - if current is None: - continue - pattern = current.compiled_function_pattern - if pattern is None or not pattern.preserves_input_main_flow(): - continue - predecessor = current.main_input_dependency.predecessor_step_index() - if predecessor is not None and predecessor not in demanded: - demanded.add(predecessor) - pending.append(predecessor) - return self.step_index in demanded + return True + for consumer in consumers.get(step_index, ()): + pattern = consumer.compiled_function_pattern + if pattern is None or any( + (group := pattern.group_for_component(component_key)) is None + or consumer.stored_primary_input_edges_for_group( + group, component_key, + ) is None + for component_key in consumer.execution_group_scope.keys + ): + return True + if pattern.preserves_input_main_flow(): + pending.append(consumer.step_index) + return False def producer_identity( self, diff --git a/openhcs/core/source_projection.py b/openhcs/core/source_projection.py index 382bfe43a0..5ec10a7be1 100644 --- a/openhcs/core/source_projection.py +++ b/openhcs/core/source_projection.py @@ -1186,6 +1186,28 @@ def projection_fields( ) -> dict[str, Any]: """Serialize the coherent path-keyed projection fields as one unit.""" + return { + **cls.workspace_fields(projection_paths), + cls.SOURCE_PROJECTION_FIELD: cls.projection_records(projection_paths), + } + + @classmethod + def projection_records( + cls, + projection_paths: tuple[tuple[SourceProjection, str], ...], + ) -> list[dict[str, Any]]: + """Encode producer records independently of normalized workspace views.""" + return [ + cls._source_projection_payload(projection, path) + for projection, path in projection_paths + ] + + @classmethod + def workspace_fields( + cls, + projection_paths: tuple[tuple[SourceProjection, str], ...], + ) -> dict[str, Any]: + """Derive normalized workspace views without encoding image records.""" return { cls.WORKSPACE_MAPPING_FIELD: { path: projection.ref.to_workspace_mapping() @@ -1195,10 +1217,6 @@ def projection_fields( path: cls._source_metadata(projection) for projection, path in projection_paths }, - cls.SOURCE_PROJECTION_FIELD: [ - cls._source_projection_payload(projection, path) - for projection, path in projection_paths - ], } def projection_paths( diff --git a/openhcs/core/virtual_workspace_metadata.py b/openhcs/core/virtual_workspace_metadata.py index 89742142e1..bf6f4927a2 100644 --- a/openhcs/core/virtual_workspace_metadata.py +++ b/openhcs/core/virtual_workspace_metadata.py @@ -174,7 +174,6 @@ def publish_source_projection_metadata( inventing coordinates from filenames or the input label cache. """ saved_paths = tuple(saved_image_paths) - saved_set = frozenset(saved_paths) def update(data): data = self._ensure_subdirectories_structure(data) @@ -184,28 +183,17 @@ def update(data): entries = ( VirtualWorkspaceSourceProjectionEntries(MappingProxyType({})) if projection_entries is None else projection_entries - ).merged_with_subdirectory(subdirectory).entries - missing = saved_set.difference(entries) - if missing and projection_entries is None: - raise MetadataWriteError( - f"Saved images lack typed produced addresses: {sorted(missing)!r}." - ) + ).publish_into_subdirectory( + subdirectory, + saved_image_paths=saved_paths, + reconcile_directory=( + subdirectory_name if projection_entries is None else None + ), + ).entries # Concurrent axes may persist their pixels before publishing their # own producer records. A step publishes known saved addresses only; # completed-plate reconciliation requires the entire saved inventory. published_paths = tuple(path for path in saved_paths if path in entries) - # Only final reconciliation can prune deleted images: a step's file - # snapshot may precede another axis's concurrent publication. - retained_paths = tuple( - (projection, path) - for path, projection in entries.items() - if ( - projection_entries is not None - or path in saved_set - or Path(path).parent != Path(subdirectory_name) - ) - ) - subdirectory.update(serializer.projection_fields(retained_paths)) if published_paths: projections = SourceProjectionSet( tuple(entries[path] for path in published_paths) @@ -228,7 +216,7 @@ def update(data): subdirectory[serializer.RESULTS_DIR_FIELD] = results_dir self._update_projection_geometry( subdirectory, - (projection for projection, _path in retained_paths), + entries.values(), ) return data @@ -515,6 +503,70 @@ def merge_into_subdirectory( subdirectory[FIELDS.SOURCE_PROJECTION] = list(records.values()) return admitted + def publish_into_subdirectory( + self, + subdirectory: dict[str, Any], + *, + saved_image_paths: Sequence[str], + reconcile_directory: str | None, + ) -> "VirtualWorkspaceSourceProjectionEntries": + """Publish current path views while retaining admitted durable records. + + Step snapshots cannot prune another axis's publication. Only completed + directory reconciliation requires complete inventory and removes deleted + paths. Retained records keep their original wire annotations; the two + workspace maps are independently derived normalized views. + """ + records = self._records_by_path(subdirectory) + admitted = self._admit_retained_records(records) + saved_set = frozenset(saved_image_paths) + missing = saved_set.difference(admitted.entries) + if missing and reconcile_directory is not None: + raise MetadataWriteError( + f"Saved images lack typed produced addresses: {sorted(missing)!r}." + ) + retained = type(self)( + MappingProxyType( + { + path: projection + for path, projection in admitted.entries.items() + if ( + reconcile_directory is None + or path in saved_set + or Path(path).parent != Path(reconcile_directory) + ) + } + ) + ) + workspace_fields = SourceProjectionMetadataSerializer.workspace_fields( + retained.projection_paths + ) + retained_records = {} + for path, record in records.items(): + if path in self.entries: + continue # Replacements can repair an invalid durable record. + canonical_path = self._required_text(record, "virtual_path") + if canonical_path in retained.entries: + retained_records[canonical_path] = ( + record if path == canonical_path + else {**record, "virtual_path": canonical_path} + ) + retained_records.update( + (record["virtual_path"], record) + for record in SourceProjectionMetadataSerializer.projection_records( + tuple( + (projection, path) + for path, projection in self.entries.items() + if path in retained.entries + ) + ) + ) + subdirectory.update(workspace_fields) + subdirectory[FIELDS.SOURCE_PROJECTION] = [ + retained_records[path] for path in retained.entries + ] + return retained + @classmethod def from_subdirectory( cls, diff --git a/tests/unit/test_loaded_pattern_group_ownership.py b/tests/unit/test_loaded_pattern_group_ownership.py index f3f1421c21..c20d1bc167 100644 --- a/tests/unit/test_loaded_pattern_group_ownership.py +++ b/tests/unit/test_loaded_pattern_group_ownership.py @@ -397,6 +397,81 @@ def test_checkpoint_demand_follows_preserved_input_to_native_reader(): assert source.requires_main_flow_checkpoint(plans) +@pytest.mark.parametrize("pattern_kind", ("unknown", "missing", "malformed")) +def test_checkpoint_demand_preserves_dependent_group_admission(pattern_kind): + from openhcs.constants.constants import Backend + from openhcs.core.step_dependencies import StepInputDependency + + request, _, _ = _stored_primary_fixture() + source = replace(request.execution_plan, step_index=0, step_scope_id="source", write_backend=Backend.MEMORY.value) + consumer = replace( + source, step_index=1, step_scope_id="consumer", + compiled_function_pattern=( + None if pattern_kind == "unknown" else replace( + source.compiled_function_pattern, groups=(), + is_grouped=pattern_kind == "missing", + ) + ), + main_input_dependency=StepInputDependency.step_output(source_step_index=0, source_step_scope_id="source"), + ) + plans = {0: source, 1: consumer} + if pattern_kind == "malformed": + with pytest.raises(ValueError, match="no 'default' group"): + source.requires_main_flow_checkpoint(plans) + else: + assert source.requires_main_flow_checkpoint(plans) + consumer.main_input_dependency = StepInputDependency.step_output(source_step_index=99, source_step_scope_id="absent") + assert not source.requires_main_flow_checkpoint(plans) + assert source.requires_main_flow_checkpoint({}) + assert source.requires_main_flow_checkpoint({1: consumer}) + + +def test_checkpoint_demand_propagates_only_through_preserved_outputs(): + from openhcs.constants.constants import Backend + from openhcs.core.step_dependencies import StepInputDependency + + request, _, _ = _stored_primary_fixture() + source = replace(request.execution_plan, step_index=0, step_scope_id="source", write_backend=Backend.MEMORY.value) + consumer = replace( + source, step_index=1, step_scope_id="consumer", visualize=True, + main_input_dependency=StepInputDependency.step_output(source_step_index=0, source_step_scope_id="source"), + ) + plans = {0: source, 1: consumer} + assert consumer.requires_main_flow_checkpoint(plans) + assert not source.requires_main_flow_checkpoint(plans) + preserving, _, _ = _stored_primary_fixture(preserves_main_flow=True) + consumer.compiled_function_pattern = preserving.execution_plan.compiled_function_pattern + assert source.requires_main_flow_checkpoint(plans) + + +def test_checkpoint_demand_terminates_on_preserving_cycles_and_self_producers(): + from openhcs.constants.constants import Backend + from openhcs.core.step_dependencies import StepInputDependency + + request, _, _ = _stored_primary_fixture(preserves_main_flow=True) + first = replace( + request.execution_plan, step_index=1, step_scope_id="first", write_backend=Backend.MEMORY.value, + main_input_dependency=StepInputDependency.step_output(source_step_index=2, source_step_scope_id="second"), + ) + second = replace( + first, step_index=2, step_scope_id="second", + main_input_dependency=StepInputDependency.step_output(source_step_index=1, source_step_scope_id="first"), + ) + plans = {1: first, 2: second} + assert not first.requires_main_flow_checkpoint(plans) + assert not second.requires_main_flow_checkpoint(plans) + second.streaming_configs = {"viewer": object()} + assert first.requires_main_flow_checkpoint(plans) + assert second.requires_main_flow_checkpoint(plans) + first.main_input_dependency = StepInputDependency.step_output(source_step_index=1, source_step_scope_id="first") + assert not first.requires_main_flow_checkpoint({1: first}) + self_producer = replace( + first, step_index=0, step_scope_id="self", + main_input_dependency=StepInputDependency.step_output(source_step_index=0, source_step_scope_id="self"), + ) + assert self_producer.requires_main_flow_checkpoint({0: self_producer}) + + def test_artifact_loaded_coordinates_keep_producer_authority_over_original_source(): request, canonical, source_context = _stored_primary_fixture() exact_coordinates = ( diff --git a/tests/unit/test_persisted_image_metadata.py b/tests/unit/test_persisted_image_metadata.py index cd1ddc68ff..07a1ca4606 100644 --- a/tests/unit/test_persisted_image_metadata.py +++ b/tests/unit/test_persisted_image_metadata.py @@ -362,10 +362,7 @@ def payload(cls, projection, virtual_path): else: writer.merge_source_projection_metadata(path, "images", updates) assert admitted_paths == ["images/retained.tif"] - assert serialized_paths == ( - ["images/retained.tif", "images/replaced.tif"] - if publish else ["images/replaced.tif"] - ) + assert serialized_paths == ["images/replaced.tif"] subdir = json.loads(path.read_text())[FIELDS.SUBDIRECTORIES]["images"] restored = VirtualWorkspaceSourceProjectionEntries.from_subdirectory(subdir) assert ( @@ -395,7 +392,10 @@ def test_invalid_unreplaced_projection_aborts_typed_transaction(tmp_path, publis assert path.read_bytes() == before -def test_typed_merge_retains_opaque_fields_and_orphan_mappings(tmp_path): +@pytest.mark.parametrize("publish", (False, True)) +def test_typed_transaction_retains_wire_fields_and_derives_workspace_views( + tmp_path, publish +): path = tmp_path / "openhcs_metadata.json" writer = AtomicMetadataWriter() original = calibrated_projection("A01", "images/original.tif", 0.5) @@ -403,20 +403,45 @@ def test_typed_merge_retains_opaque_fields_and_orphan_mappings(tmp_path): ((original, "images/original.tif"),) ) fields[FIELDS.SOURCE_PROJECTION][0]["external_annotation"] = {"version": 7} + fields[FIELDS.SOURCE_METADATA]["images/original.tif"] = { + "manual_override": "obsolete" + } fields[FIELDS.WORKSPACE_MAPPING]["legacy.tif"] = {"opaque": "mapping"} fields[FIELDS.SOURCE_METADATA]["legacy.tif"] = {"opaque": "metadata"} writer.replace_subdirectory_metadata(path, "images", fields) new = calibrated_projection("A02", "images/new.tif", 0.5) - writer.merge_source_projection_metadata( - path, "images", - VirtualWorkspaceSourceProjectionEntries.from_projection_paths( - ((new, "images/new.tif"),) - ), + updates = VirtualWorkspaceSourceProjectionEntries.from_projection_paths( + ((new, "images/new.tif"),) ) + if publish: + publish_projection_inventory( + writer, path, updates, ("images/original.tif", "images/new.tif") + ) + else: + writer.merge_source_projection_metadata(path, "images", updates) subdir = json.loads(path.read_text())[FIELDS.SUBDIRECTORIES]["images"] assert subdir[FIELDS.SOURCE_PROJECTION][0]["external_annotation"] == {"version": 7} - assert subdir[FIELDS.WORKSPACE_MAPPING]["legacy.tif"] == {"opaque": "mapping"} - assert subdir[FIELDS.SOURCE_METADATA]["legacy.tif"] == {"opaque": "metadata"} + if publish: + assert "legacy.tif" not in subdir[FIELDS.WORKSPACE_MAPPING] + assert "legacy.tif" not in subdir[FIELDS.SOURCE_METADATA] + assert subdir[FIELDS.SOURCE_METADATA]["images/original.tif"]["well"] == "A01" + assert "manual_override" not in subdir[FIELDS.SOURCE_METADATA][ + "images/original.tif" + ] + assert "well" not in subdir[FIELDS.SOURCE_PROJECTION][0]["source_metadata"] + publish_projection_inventory( + writer, path, None, ("images/original.tif", "images/new.tif") + ) + reconciled = json.loads(path.read_text())[FIELDS.SUBDIRECTORIES]["images"] + assert reconciled[FIELDS.SOURCE_PROJECTION][0]["external_annotation"] == { + "version": 7 + } + else: + assert subdir[FIELDS.WORKSPACE_MAPPING]["legacy.tif"] == {"opaque": "mapping"} + assert subdir[FIELDS.SOURCE_METADATA]["legacy.tif"] == {"opaque": "metadata"} + assert subdir[FIELDS.SOURCE_METADATA]["images/original.tif"] == { + "manual_override": "obsolete" + } def test_typed_transaction_normalizes_legacy_paths_and_rejects_collisions(): From 27f63585d5e0b6abca43f193c397e05e38d5618c Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 18:41:05 -0400 Subject: [PATCH 161/235] refactor(runtime): delete unused source context transport and duplicate request owners --- openhcs/core/runtime_adapters.py | 4 - openhcs/core/runtime_source_binding_cache.py | 51 +-- openhcs/core/source_binding_selection.py | 214 +++------- openhcs/core/source_bindings.py | 373 +----------------- openhcs/core/steps/function_runtime.py | 58 ++- tests/unit/test_cellprofiler_adapter.py | 1 - .../test_cellprofiler_module_execution.py | 9 - .../unit/test_cellprofiler_runtime_adapter.py | 87 ---- ...test_cellprofiler_static_deletion_gates.py | 4 +- tests/unit/test_debug_runtime.py | 7 - tests/unit/test_function_artifact_outputs.py | 3 - .../test_function_core_executor_ownership.py | 3 +- tests/unit/test_function_io.py | 4 +- ...test_function_runtime_source_projection.py | 101 +++-- .../test_loaded_pattern_group_ownership.py | 78 ++-- .../test_opaque_main_flow_image_domain.py | 5 +- .../unit/test_path_planner_materialization.py | 2 - .../unit/test_persisted_semantic_metadata.py | 11 +- .../test_runtime_adapter_source_artifacts.py | 9 - .../unit/test_runtime_context_declaration.py | 2 - ...test_runtime_source_resolution_snapshot.py | 4 - tests/unit/test_source_bindings.py | 36 -- tests/unit/test_source_metadata_owner.py | 4 - 23 files changed, 189 insertions(+), 881 deletions(-) diff --git a/openhcs/core/runtime_adapters.py b/openhcs/core/runtime_adapters.py index 6540007698..2de8fa2244 100644 --- a/openhcs/core/runtime_adapters.py +++ b/openhcs/core/runtime_adapters.py @@ -38,7 +38,6 @@ from openhcs.core.source_bindings import ( CompiledSourceBindingPlan, NamedSourceBinding, - SourceBindingRuntimeContext, ) from openhcs.core.source_binding_selection import ( SourceBindingMatchedImageSet, @@ -91,9 +90,6 @@ class RuntimeAdapterRequest: default_factory=dict ) source_binding_plan: CompiledSourceBindingPlan = CompiledSourceBindingPlan.empty() - source_binding_context: SourceBindingRuntimeContext = field( - default_factory=SourceBindingRuntimeContext.empty - ) group_key: str | None = None axis_scope: RuntimeExecutionAxisScope plane_projection: RuntimePlaneProjection = field( diff --git a/openhcs/core/runtime_source_binding_cache.py b/openhcs/core/runtime_source_binding_cache.py index 8b074a55c7..9e6a311a47 100644 --- a/openhcs/core/runtime_source_binding_cache.py +++ b/openhcs/core/runtime_source_binding_cache.py @@ -8,7 +8,6 @@ from typing import Any, Hashable, TYPE_CHECKING from openhcs.core.source_bindings import ( - SourceBindingRuntimeContext, SourceBindingRuntimeMetadataNormalizer, MetadataExtractionRule, ) @@ -94,10 +93,6 @@ class RuntimeSourceBindingContextCache: int, Mapping[str, SourceMetadataMapping], ] = field(default_factory=dict) - runtime_context_by_request_identity: dict[ - tuple[int, tuple[str, ...], object, int | None], - SourceBindingRuntimeContext, - ] = field(default_factory=dict) runtime_universe_state_by_request_identity: dict[ tuple[int, tuple[str, ...], object, int | None], "SourceUniverseRuntimeState", @@ -145,24 +140,6 @@ def normalized_source_metadata( self.source_metadata_by_mapping_identity[cache_key] = cached return cached - def runtime_context( - self, - *, - plan: Any, - matching_files: tuple[str, ...], - source_backend: object, - source_projection: object | None, - ) -> SourceBindingRuntimeContext | None: - """Return a cached runtime context for one immutable request identity.""" - return self.runtime_context_by_request_identity.get( - self.runtime_context_key( - plan=plan, - matching_files=matching_files, - source_backend=source_backend, - source_projection=source_projection, - ) - ) - def runtime_universe_state( self, *, @@ -173,7 +150,7 @@ def runtime_universe_state( ) -> "SourceUniverseRuntimeState | None": """Return cached source-universe state for one request identity.""" return self.runtime_universe_state_by_request_identity.get( - self.runtime_context_key( + self.runtime_universe_state_key( plan=plan, matching_files=matching_files, source_backend=source_backend, @@ -192,7 +169,7 @@ def store_runtime_universe_state( ) -> "SourceUniverseRuntimeState": """Cache source-universe state for one request identity.""" self.runtime_universe_state_by_request_identity[ - self.runtime_context_key( + self.runtime_universe_state_key( plan=plan, matching_files=matching_files, source_backend=source_backend, @@ -201,35 +178,15 @@ def store_runtime_universe_state( ] = runtime_state return runtime_state - def store_runtime_context( - self, - runtime_context: SourceBindingRuntimeContext, - *, - plan: Any, - matching_files: tuple[str, ...], - source_backend: object, - source_projection: object | None, - ) -> SourceBindingRuntimeContext: - """Cache a runtime context for one immutable request identity.""" - self.runtime_context_by_request_identity[ - self.runtime_context_key( - plan=plan, - matching_files=matching_files, - source_backend=source_backend, - source_projection=source_projection, - ) - ] = runtime_context - return runtime_context - @staticmethod - def runtime_context_key( + def runtime_universe_state_key( *, plan: Any, matching_files: tuple[str, ...], source_backend: object, source_projection: object | None, ) -> tuple[int, tuple[str, ...], object, int | None]: - """Return the process-local identity for one source runtime context.""" + """Return the process-local identity for one source-universe request.""" return ( id(plan), tuple(matching_files), diff --git a/openhcs/core/source_binding_selection.py b/openhcs/core/source_binding_selection.py index 329813c31f..4ded7df016 100644 --- a/openhcs/core/source_binding_selection.py +++ b/openhcs/core/source_binding_selection.py @@ -24,7 +24,6 @@ NamedSourceBinding, SourceBindingMatchMethod, SourceBindingMatchPlan, - SourceBindingRuntimeContext, SourceSetRole, ) from openhcs.core.source_image_provenance import ( @@ -208,23 +207,6 @@ def from_sources( metadata_rules=metadata_rules, ) - @classmethod - def from_runtime_context( - cls, - *, - parser: "FilenameParser", - runtime_context: SourceBindingRuntimeContext, - metadata_rules: tuple[MetadataExtractionRule, ...] = (), - ) -> "SourcePatternResolutionContext": - """Build the generic selector context from compiled runtime source state.""" - - return cls.from_sources( - parser=parser, - source_paths_by_virtual_path=runtime_context.step_input_source_paths, - source_metadata_by_path=runtime_context.source_metadata_by_path, - metadata_rules=metadata_rules, - ) - @classmethod def from_projection( cls, @@ -1538,8 +1520,6 @@ class SourceFileUniverse: class SourceUniverseRuntimeState: """Resolved source universes assembled from the registered request family.""" - step_input_universe: SourceFileUniverse | None = None - pipeline_start_universe: SourceFileUniverse | None = None load_universe: SourceFileUniverse | None = None step_input_source_paths: Mapping[str, str] = field( default_factory=lambda: MappingProxyType({}) @@ -1547,7 +1527,6 @@ class SourceUniverseRuntimeState: source_metadata_by_path: Mapping[str, SourceMetadataMapping] = field( default_factory=lambda: MappingProxyType({}) ) - pipeline_source_candidate_files: tuple[str, ...] = () def with_source_metadata( self, @@ -1566,50 +1545,11 @@ def with_source_metadata( merged.update(source_metadata_by_path) return replace(self, source_metadata_by_path=MappingProxyType(merged)) - def require_step_input_universe(self) -> SourceFileUniverse: - if self.step_input_universe is None: - raise RuntimeError( - "Source universe runtime state has no step-input universe." - ) - return self.step_input_universe - - def require_pipeline_start_universe(self) -> SourceFileUniverse: - if self.pipeline_start_universe is None: - raise RuntimeError( - "Source universe runtime state has no pipeline-start universe." - ) - return self.pipeline_start_universe - def require_load_universe(self) -> SourceFileUniverse: if self.load_universe is None: raise RuntimeError("Source universe runtime state has no load universe.") return self.load_universe - def runtime_context( - self, - request: "SourceBindingRuntimeContextRequest", - source_metadata_by_path: Mapping[str, SourceMetadataMapping], - ) -> SourceBindingRuntimeContext: - """Build the runtime context from source-universe contributions.""" - step_input_universe = self.require_step_input_universe() - pipeline_source_universe = self.require_pipeline_start_universe() - return SourceBindingRuntimeContext( - step_input_files=step_input_universe.files, - current_step_input_files=request.current_step_input_files( - step_input_universe - ), - current_image_files=request.matching_files, - step_input_dir=str(request.plan.input_dir), - step_input_source_backend=request.plan.read_backend, - step_input_storage_backend=Backend.MEMORY.value, - step_input_source_paths=self.step_input_source_paths, - source_metadata_by_path=source_metadata_by_path, - source_metadata_is_normalized=True, - pipeline_input_files=pipeline_source_universe.files, - pipeline_source_candidate_files=self.pipeline_source_candidate_files, - pipeline_input_backend=pipeline_source_universe.backend.value, - ) - @dataclass(frozen=True, slots=True) class SourceUniverseRequest(metaclass=AutoRegisterMeta): @@ -1625,6 +1565,54 @@ class SourceUniverseRequest(metaclass=AutoRegisterMeta): source_backend: Backend source_projection: VirtualWorkspaceSourceProjection | None + @classmethod + def from_context( + cls, + *, + context: "ProcessingContext", + plan: "CompiledStepPlan", + matching_files: Sequence[str], + source_projection: VirtualWorkspaceSourceProjection | None, + ) -> "SourceUniverseRequest": + if not isinstance(plan, CompiledStepPlan): + raise TypeError( + "SourceUniverseRequest requires CompiledStepPlan, got " + f"{type(plan).__name__}." + ) + plan.require_function_execution_ready() + source_backend = Backend( + context.microscope_handler.get_primary_backend( + context.input_dir, + context.filemanager, + ) + ) + return cls( + context=context, + plan=plan, + matching_files=tuple(matching_files), + source_backend=source_backend, + source_projection=source_projection, + ) + + def runtime_universe_state(self) -> SourceUniverseRuntimeState: + """Return cached source-universe state for this request.""" + cache = self.context.runtime_source_binding_context_cache + cached = cache.runtime_universe_state( + plan=self.plan, + matching_files=self.matching_files, + source_backend=self.source_backend, + source_projection=self.source_projection, + ) + if cached is not None: + return cached + return cache.store_runtime_universe_state( + SourceUniverseRequest.runtime_state(self), + plan=self.plan, + matching_files=self.matching_files, + source_backend=self.source_backend, + source_projection=self.source_projection, + ) + @classmethod def registered_request_types(cls) -> tuple[type["SourceUniverseRequest"], ...]: """Return registered concrete runtime plan request classes.""" @@ -1635,11 +1623,11 @@ def registered_request_types(cls) -> tuple[type["SourceUniverseRequest"], ...]: return tuple(request_types) @classmethod - def from_runtime_context( + def from_request( cls, - request: "SourceBindingRuntimeContextRequest", + request: "SourceUniverseRequest", ) -> "SourceUniverseRequest": - """Build this registered request type from the runtime context request.""" + """Build a registered source-universe role from its parent request.""" return cls( context=request.context, plan=request.plan, @@ -1651,12 +1639,12 @@ def from_runtime_context( @classmethod def runtime_state( cls, - request: "SourceBindingRuntimeContextRequest", + request: "SourceUniverseRequest", ) -> SourceUniverseRuntimeState: """Resolve every registered source-universe request into runtime state.""" state = SourceUniverseRuntimeState() for request_type in cls.registered_request_types(): - universe_request = request_type.from_runtime_context(request) + universe_request = request_type.from_request(request) universe = SourceUniverseStrategy.universe(universe_request) state = universe_request.contribute_runtime_state(state, universe) return state @@ -1759,7 +1747,6 @@ def contribute_runtime_state( ) -> SourceUniverseRuntimeState: state = replace( state, - step_input_universe=universe, load_universe=state.load_universe or universe, step_input_source_paths=self.step_input_source_paths, ) @@ -1780,8 +1767,6 @@ def contribute_runtime_state( load_universe = self.load_universe() state = replace( state, - pipeline_start_universe=universe, - pipeline_source_candidate_files=universe.files, load_universe=( state.load_universe if load_universe is None else load_universe ), @@ -2058,96 +2043,3 @@ def source_universe(self, request: SourceUniverseRequest) -> SourceFileUniverse: ), backend=universe_backend, ) - - -@dataclass(frozen=True, slots=True) -class SourceBindingRuntimeContextRequest: - """Build the source-binding runtime context from one resolved source universe.""" - - context: "ProcessingContext" - plan: "CompiledStepPlan" - matching_files: tuple[str, ...] - source_backend: Backend - source_projection: VirtualWorkspaceSourceProjection | None - - def __post_init__(self) -> None: - if not isinstance(self.plan, CompiledStepPlan): - raise TypeError( - "SourceBindingRuntimeContextRequest requires CompiledStepPlan, got " - f"{type(self.plan).__name__}." - ) - self.plan.require_function_execution_ready() - - @classmethod - def from_context( - cls, - *, - context: "ProcessingContext", - plan: "CompiledStepPlan", - matching_files: Sequence[str], - source_projection: VirtualWorkspaceSourceProjection | None, - ) -> "SourceBindingRuntimeContextRequest": - source_backend = Backend( - context.microscope_handler.get_primary_backend( - context.input_dir, - context.filemanager, - ) - ) - return cls( - context=context, - plan=plan, - matching_files=tuple(matching_files), - source_backend=source_backend, - source_projection=source_projection, - ) - - def runtime_context(self) -> SourceBindingRuntimeContext: - cache = self.context.runtime_source_binding_context_cache - cached = cache.runtime_context( - plan=self.plan, - matching_files=self.matching_files, - source_backend=self.source_backend, - source_projection=self.source_projection, - ) - if cached is not None: - return cached - universe_state = self.runtime_universe_state() - source_metadata_by_path = cache.normalized_source_metadata( - universe_state.source_metadata_by_path - ) - return cache.store_runtime_context( - universe_state.runtime_context( - self, - source_metadata_by_path, - ), - plan=self.plan, - matching_files=self.matching_files, - source_backend=self.source_backend, - source_projection=self.source_projection, - ) - - def runtime_universe_state(self) -> SourceUniverseRuntimeState: - """Return cached source-universe state for this request.""" - cache = self.context.runtime_source_binding_context_cache - cached = cache.runtime_universe_state( - plan=self.plan, - matching_files=self.matching_files, - source_backend=self.source_backend, - source_projection=self.source_projection, - ) - if cached is not None: - return cached - return cache.store_runtime_universe_state( - SourceUniverseRequest.runtime_state(self), - plan=self.plan, - matching_files=self.matching_files, - source_backend=self.source_backend, - source_projection=self.source_projection, - ) - - def current_step_input_files( - self, - step_input_universe: SourceFileUniverse, - ) -> tuple[str, ...]: - del step_input_universe - return self.matching_files diff --git a/openhcs/core/source_bindings.py b/openhcs/core/source_bindings.py index dea7994bf0..2307dc7740 100644 --- a/openhcs/core/source_bindings.py +++ b/openhcs/core/source_bindings.py @@ -10,7 +10,7 @@ import urllib.request from abc import ABC, abstractmethod from collections.abc import Hashable, Iterable, Mapping, Sequence -from dataclasses import InitVar, dataclass, field, replace +from dataclasses import dataclass, field, replace from dataclasses import fields as dataclass_fields from enum import Enum from functools import lru_cache @@ -37,7 +37,6 @@ from openhcs.core.components.validation import convert_enum_by_value from openhcs.core.runtime_tabular_values import FieldSpec from openhcs.core.source_metadata import ( - SourceMetadataIdentityItems, SourceMetadataFields, ResolvedSourceMetadataRecord, SourceMetadataMapping, @@ -47,7 +46,6 @@ source_metadata_dict, source_metadata_scalar, ) -from openhcs.core.source_path_identity import source_path_identity_key from openhcs.core.xdg_paths import get_openhcs_cache_dir if TYPE_CHECKING: @@ -56,31 +54,11 @@ from openhcs.core.runtime_image_loading import ImagePayloadSourceMetadataContext from openhcs.core.runtime_image_values import ImagePayloadMetadata -SourceMetadataIdentity = tuple[tuple[str, SourceMetadataIdentityItems], ...] SOURCE_ALIAS_PART_SEPARATOR = "__" SOURCE_BINDING_ALIAS_METADATA_FIELD = "source_alias" SourceBindingValue = TypeVar("SourceBindingValue") -@dataclass(frozen=True, slots=True) -class SourceBindingRuntimeContextProcessIdentity: - """Hash-stable semantic identity for process-local source caches.""" - - source_order_identity: tuple[Hashable, ...] - source_metadata_identity: SourceMetadataIdentity - _hash: int = field(init=False, repr=False, compare=False) - - def __post_init__(self) -> None: - object.__setattr__( - self, - "_hash", - hash((self.source_order_identity, self.source_metadata_identity)), - ) - - def __hash__(self) -> int: - return self._hash - - class SourceBindingPlanMeta(EnableableMeta, AutoRegisterMeta): """Auto-register source-binding plans while preserving Enableable semantics.""" @@ -2457,355 +2435,6 @@ def normalized(self) -> Mapping[str, SourceMetadataMapping]: ) -@dataclass(frozen=True) -class SourceBindingRuntimeContext: - """Execution-local file universe for selector-bearing source bindings.""" - - step_input_files: tuple[str, ...] = () - current_step_input_files: tuple[str, ...] = () - current_image_files: tuple[str, ...] = () - step_input_dir: str | None = None - step_input_source_backend: str | None = None - step_input_storage_backend: str | None = None - step_input_source_paths: Mapping[str, str] = field( - default_factory=lambda: MappingProxyType({}) - ) - source_metadata_by_path: Mapping[str, SourceMetadataMapping] = field( - default_factory=lambda: MappingProxyType({}) - ) - pipeline_input_files: tuple[str, ...] = () - pipeline_source_candidate_files: tuple[str, ...] = () - pipeline_input_backend: str | None = None - source_metadata_is_normalized: InitVar[bool] = False - _source_metadata_identity: SourceMetadataIdentity | None = field( - default=None, - init=False, - repr=False, - compare=False, - ) - _pipeline_input_files_identity: tuple[str, ...] | None = field( - default=None, - init=False, - repr=False, - compare=False, - ) - _source_order_identity: tuple[Hashable, ...] | None = field( - default=None, - init=False, - repr=False, - compare=False, - ) - _process_semantic_identity: SourceBindingRuntimeContextProcessIdentity | None = ( - field( - default=None, - init=False, - repr=False, - compare=False, - ) - ) - _virtual_source_paths_by_identity: Mapping[str, tuple[str, ...]] | None = field( - default=None, - init=False, - repr=False, - compare=False, - ) - _source_metadata_by_runtime_lookup_key: ( - Mapping[ - str, - SourceMetadataMapping, - ] - | None - ) = field( - default=None, - init=False, - repr=False, - compare=False, - ) - - @classmethod - def empty(cls) -> SourceBindingRuntimeContext: - return cls() - - def __post_init__( - self, - source_metadata_is_normalized: bool, - ) -> None: - object.__setattr__(self, "step_input_files", tuple(self.step_input_files)) - object.__setattr__( - self, - "current_step_input_files", - tuple(self.current_step_input_files or self.step_input_files), - ) - object.__setattr__( - self, - "current_image_files", - tuple(self.current_image_files or self.current_step_input_files), - ) - if self.step_input_dir is not None: - object.__setattr__(self, "step_input_dir", str(self.step_input_dir)) - if self.step_input_source_backend is not None: - object.__setattr__( - self, - "step_input_source_backend", - str(self.step_input_source_backend), - ) - if self.step_input_storage_backend is not None: - object.__setattr__( - self, - "step_input_storage_backend", - str(self.step_input_storage_backend), - ) - step_input_source_paths = self.step_input_source_paths - if not isinstance(step_input_source_paths, MappingProxyType): - step_input_source_paths = MappingProxyType( - { - str(path): str(source) - for path, source in step_input_source_paths.items() - } - ) - object.__setattr__(self, "step_input_source_paths", step_input_source_paths) - - if source_metadata_is_normalized: - if not isinstance(self.source_metadata_by_path, MappingProxyType): - raise TypeError( - "Normalized SourceBindingRuntimeContext metadata must be " - "MappingProxyType." - ) - else: - object.__setattr__( - self, - "source_metadata_by_path", - SourceBindingRuntimeMetadataNormalizer( - self.source_metadata_by_path - ).normalized(), - ) - object.__setattr__( - self, - "pipeline_input_files", - tuple(self.pipeline_input_files), - ) - object.__setattr__( - self, - "pipeline_source_candidate_files", - tuple( - self.pipeline_source_candidate_files - or self.pipeline_input_files - or self.step_input_files - ), - ) - if self.pipeline_input_backend is not None: - object.__setattr__( - self, - "pipeline_input_backend", - str(self.pipeline_input_backend), - ) - - @property - def source_metadata_identity( - self, - ) -> SourceMetadataIdentity: - """Stable identity for the complete source-metadata universe.""" - - cached = self._source_metadata_identity - if cached is None: - cached = tuple( - (path, SourceMetadataFields.identity_items(metadata)) - for path, metadata in sorted(self.source_metadata_by_path.items()) - ) - object.__setattr__(self, "_source_metadata_identity", cached) - return cached - - @property - def process_semantic_identity(self) -> SourceBindingRuntimeContextProcessIdentity: - """Return the source context identity used by process-local caches.""" - cached = self._process_semantic_identity - if cached is None: - cached = SourceBindingRuntimeContextProcessIdentity( - source_order_identity=self.source_order_identity, - source_metadata_identity=self.source_metadata_identity, - ) - object.__setattr__(self, "_process_semantic_identity", cached) - return cached - - @property - def source_metadata_by_runtime_lookup_key( - self, - ) -> Mapping[str, SourceMetadataMapping]: - """Return source metadata indexed by every runtime path spelling.""" - cached = self._source_metadata_by_runtime_lookup_key - if cached is None: - indexed: dict[str, SourceMetadataMapping] = {} - for path, metadata in self.source_metadata_by_path.items(): - for key in _source_runtime_path_lookup_keys( - str(path), - self.step_input_dir, - ): - indexed.setdefault(key, metadata) - indexed.setdefault(_source_runtime_native_path(str(path)), metadata) - cached = MappingProxyType(indexed) - object.__setattr__( - self, - "_source_metadata_by_runtime_lookup_key", - cached, - ) - return cached - - def source_metadata_for_runtime_path( - self, - path: str, - ) -> SourceMetadataMapping | None: - """Return source metadata for one runtime path spelling, if known.""" - lookup = self.source_metadata_by_runtime_lookup_key - for key in _source_runtime_path_lookup_keys(str(path), self.step_input_dir): - metadata = lookup.get(key) - if metadata is not None: - return metadata - return lookup.get(_source_runtime_native_path(str(path))) - - @property - def pipeline_input_files_identity(self) -> tuple[str, ...]: - """Return sorted pipeline input files for source-order cache identities.""" - cached = self._pipeline_input_files_identity - if cached is None: - cached = tuple(sorted(self.pipeline_input_files)) - object.__setattr__(self, "_pipeline_input_files_identity", cached) - return cached - - @property - def source_order_identity(self) -> tuple[Hashable, ...]: - """Return source-order mapping identity shared by runtime source caches.""" - cached = self._source_order_identity - if cached is None: - cached = ( - self.step_input_dir, - tuple(sorted(self.pipeline_source_candidate_files)), - tuple(sorted(self.step_input_source_paths.items())), - tuple(sorted(self.virtual_source_paths_by_identity.items())), - ) - object.__setattr__(self, "_source_order_identity", cached) - return cached - - @property - def virtual_source_paths_by_identity(self) -> Mapping[str, tuple[str, ...]]: - """Return virtual source paths grouped by normalized physical identity.""" - - cached = self._virtual_source_paths_by_identity - if cached is None: - grouped: dict[str, list[str]] = {} - for virtual_path, source_path in self.step_input_source_paths.items(): - for identity in self.source_path_identities(source_path): - paths = grouped.get(identity) - if paths is None: - grouped[identity] = [virtual_path] - continue - paths.append(virtual_path) - cached = MappingProxyType( - { - identity: tuple(dict.fromkeys(paths)) - for identity, paths in grouped.items() - } - ) - object.__setattr__(self, "_virtual_source_paths_by_identity", cached) - return cached - - @staticmethod - @lru_cache(maxsize=8192) - def source_path_identities(source_path: str) -> tuple[str, ...]: - """Return path identities for stored and resolved source-path spellings.""" - path = Path(source_path) - return tuple( - dict.fromkeys( - ( - source_path_identity_key(source_path), - source_path_identity_key(str(path.resolve(strict=False))), - ) - ) - ) - - def path_spellings(self, paths: Sequence[str]) -> tuple[str, ...]: - """Return virtual and physical spellings represented by selected paths.""" - - selected: list[str] = [] - for path in paths: - selected.append(str(path)) - mapped = self.step_input_source_paths.get(str(path)) - if mapped is not None: - selected.append(mapped) - for identity in self.source_path_identities(str(path)): - selected.extend(self.virtual_source_paths_by_identity.get(identity, ())) - return tuple(dict.fromkeys(selected)) - - def metadata_identity_for_paths( - self, - paths: tuple[str, ...], - ) -> SourceMetadataIdentity: - """Return the stable metadata identity for a selected source subset.""" - - identity: list[tuple[str, SourceMetadataIdentityItems]] = [] - for path in paths: - if path in self.source_metadata_by_path: - metadata = SourceMetadataFields.identity_items( - self.source_metadata_by_path[path] - ) - else: - metadata = () - identity.append((path, metadata)) - return tuple(identity) - - def source_candidate_file_universes(self) -> tuple[tuple[str, ...], ...]: - """Return distinct non-empty file universes that may be source-parsed.""" - return tuple( - dict.fromkeys( - files - for files in ( - self.step_input_files, - self.current_step_input_files, - self.pipeline_source_candidate_files, - ) - if files - ) - ) - - def __reduce__( - self, - ) -> tuple[ - object, - tuple[ - tuple[str, ...], - tuple[str, ...], - tuple[str, ...], - str | None, - str | None, - str | None, - dict[str, str], - dict[str, dict[str, SourceMetadataValue]], - tuple[str, ...], - tuple[str, ...], - str | None, - ], - ]: - """Serialize mappingproxy-backed provenance as a plain dict.""" - return ( - self.__class__, - ( - self.step_input_files, - self.current_step_input_files, - self.current_image_files, - self.step_input_dir, - self.step_input_source_backend, - self.step_input_storage_backend, - dict(self.step_input_source_paths), - { - path: source_metadata_dict(metadata) - for path, metadata in self.source_metadata_by_path.items() - }, - self.pipeline_input_files, - self.pipeline_source_candidate_files, - self.pipeline_input_backend, - ), - ) - - EMPTY_SOURCE_BINDINGS = StepSourceBindingsConfig() diff --git a/openhcs/core/steps/function_runtime.py b/openhcs/core/steps/function_runtime.py index f415a680d7..ccdef926c6 100644 --- a/openhcs/core/steps/function_runtime.py +++ b/openhcs/core/steps/function_runtime.py @@ -121,7 +121,7 @@ from openhcs.core.source_binding_selection import ( SourceBindingCandidateMatcher, SourceBindingMatchedImageSet, - SourceBindingRuntimeContextRequest, + SourceUniverseRequest, SourcePatternResolutionContext, ) from openhcs.core.source_matching import ( @@ -132,7 +132,6 @@ from openhcs.core.source_bindings import ( CompiledSourceBindingPlan, SOURCE_BINDING_ALIAS_METADATA_FIELD, - SourceBindingRuntimeContext, SourceProjectionRole, ) from openhcs.core.runtime_image_values import ( @@ -1385,7 +1384,7 @@ class PatternGroupExecutionRequest(PatternGroupExecutionScope): def load_input_stack( self, runtime: "PatternGroupRuntime", - ) -> tuple[list[str], RuntimeArrayData, SourceBindingRuntimeContext]: + ) -> tuple[list[str], RuntimeArrayData]: """Admit the filename-selected source cohort.""" return runtime._load_input_stack() @@ -1440,7 +1439,7 @@ class ArtifactPatternGroupExecutionRequest(PatternGroupExecutionRequest): def load_input_stack( self, runtime: "PatternGroupRuntime", - ) -> tuple[list[str], RuntimeArrayData, SourceBindingRuntimeContext]: + ) -> tuple[list[str], RuntimeArrayData]: edges = self.execution_plan.stored_primary_input_edges_for_group( self.compiled_group, self.component_key, ) @@ -1476,16 +1475,7 @@ def load_input_stack( main_data_stack = ImagePayloadConsumption.NATURAL.compose_image_payload( self.execution_plan.step_name, tuple(payloads), ).payload - source_projection = ( - runtime.source_workspace_projection_authority().projection_if_available() - ) - source_binding_context = SourceBindingRuntimeContextRequest.from_context( - context=self.context, - plan=self.execution_plan, - matching_files=matching_files, - source_projection=source_projection, - ).runtime_context() - return matching_files, main_data_stack, source_binding_context + return matching_files, main_data_stack def loaded_plane_count( self, matching_files: Sequence[str], payload: RuntimeArrayData, @@ -1517,7 +1507,6 @@ class PatternGroupData(PatternGroupExecutionScope): """Complete loaded cohort and its original execution coordinates.""" artifacts: ComponentArtifactPlans[ArtifactSpecRef, ArtifactInputPlan] - source_binding_context: SourceBindingRuntimeContext runtime_plane_index: int runtime_plane_count: int matching_files: list[str] @@ -1529,7 +1518,6 @@ def from_loaded_group( request: "PatternGroupExecutionRequest", matching_files: list[str], main_data_stack: RuntimeArrayData, - source_binding_context: SourceBindingRuntimeContext, ) -> "PatternGroupData": artifacts = ComponentArtifactPlans.from_step_component( request.execution_plan, @@ -1547,7 +1535,6 @@ def from_loaded_group( execution_plan=request.execution_plan, compiled_group=request.compiled_group, artifacts=artifacts, - source_binding_context=source_binding_context, runtime_plane_index=request.component_index, runtime_plane_count=request.loaded_plane_count( matching_files, main_data_stack, @@ -1751,7 +1738,6 @@ def runtime_adapter_request( plane_projection=self.plane_projection, source_payload=source_payload, source_binding_plan=self.group_data.source_binding_plan, - source_binding_context=self.group_data.source_binding_context, axis_scope=self.group_data.axis_scope, variable_components=tuple( self.group_data.execution_plan.variable_components @@ -2408,7 +2394,7 @@ def run(self) -> None: try: load_started_at = time.perf_counter() - matching_files, main_data_stack, source_binding_context = ( + matching_files, main_data_stack = ( self.request.load_input_stack(self) ) except NoStepOutputManifestMatch: @@ -2430,7 +2416,7 @@ def run(self) -> None: ) execute_started_at = time.perf_counter() loaded = PatternGroupData.from_loaded_group( - self.request, matching_files, main_data_stack, source_binding_context, + self.request, matching_files, main_data_stack, ) processed_stack = self.execute_chain(loaded) RuntimeProfileSink.record( @@ -2533,7 +2519,7 @@ def _producer_output_contexts( def _load_input_stack( self, - ) -> tuple[list[str], RuntimeArrayData, SourceBindingRuntimeContext]: + ) -> tuple[list[str], RuntimeArrayData]: context = self.request.context plan = self.request.execution_plan request = self.request @@ -2623,12 +2609,6 @@ def _load_input_stack( if source_projection is not None else () ) - source_binding_context = SourceBindingRuntimeContextRequest.from_context( - context=self.request.context, - plan=self.request.execution_plan, - matching_files=matching_files, - source_projection=source_projection, - ).runtime_context() if producer_index is not None: producer_records = tuple(sorted( producer_records, key=lambda record: record.output_path, @@ -2661,7 +2641,6 @@ def _load_input_stack( raw_slices, workspace_path_lookups, workspace_source_lookups, - source_binding_context, source_projection, ) @@ -2684,7 +2663,7 @@ def _load_input_stack( else: main_data_stack = cached_stack - return matching_files, main_data_stack, source_binding_context + return matching_files, main_data_stack def _workspace_source_binding_lookups( self, @@ -2795,7 +2774,7 @@ def _source_binding_load_universe(self) -> tuple[str, ...]: source_projection = ( self.source_workspace_projection_authority().projection_if_available() ) - request = SourceBindingRuntimeContextRequest.from_context( + request = SourceUniverseRequest.from_context( context=self.request.context, plan=self.request.execution_plan, matching_files=(), @@ -2819,7 +2798,6 @@ def _apply_source_image_loading_semantics( raw_slices: Sequence[RuntimeArrayData], workspace_path_lookups: Sequence[VirtualWorkspacePathLookup], workspace_source_lookups: Sequence[VirtualWorkspacePathLookup], - source_binding_context: SourceBindingRuntimeContext, source_projection: VirtualWorkspaceSourceProjection | None, ) -> list[RuntimeArrayData]: if source_projection is not None: @@ -2845,10 +2823,22 @@ def _apply_source_image_loading_semantics( ) ] + universe_state = SourceUniverseRequest.from_context( + context=self.request.context, + plan=self.request.execution_plan, + matching_files=tuple( + lookup.virtual_path for lookup in workspace_path_lookups + ), + source_projection=None, + ).runtime_universe_state() + cache = self.request.context.runtime_source_binding_context_cache + source_metadata = cache.normalized_source_metadata( + universe_state.source_metadata_by_path + ) source_context = SourcePatternResolutionContext.from_sources( parser=self.request.context.microscope_handler.parser, - source_paths_by_virtual_path=source_binding_context.step_input_source_paths, - source_metadata_by_path=source_binding_context.source_metadata_by_path, + source_paths_by_virtual_path=universe_state.step_input_source_paths, + source_metadata_by_path=source_metadata, metadata_rules=self.request.source_binding_plan.metadata_rules, ) return [ @@ -2861,7 +2851,7 @@ def _apply_source_image_loading_semantics( ) ), source_path=source_context.source_path_for(lookup.full_virtual_path), - read_backend=source_binding_context.step_input_source_backend, + read_backend=self.request.execution_plan.read_backend, ) for payload, lookup in zip( raw_slices, diff --git a/tests/unit/test_cellprofiler_adapter.py b/tests/unit/test_cellprofiler_adapter.py index afe8d73571..a4f01925bc 100644 --- a/tests/unit/test_cellprofiler_adapter.py +++ b/tests/unit/test_cellprofiler_adapter.py @@ -38,7 +38,6 @@ from openhcs.core.runtime_stores import RuntimeValueStore from openhcs.core.source_bindings import ( ImportedMetadataTable, - SourceBindingRuntimeContext, ) from tests.unit.cellprofiler_runtime_test_support import ( cellprofiler_runtime_adapter_for_test, diff --git a/tests/unit/test_cellprofiler_module_execution.py b/tests/unit/test_cellprofiler_module_execution.py index 80bd9b59b3..7f9204dabc 100644 --- a/tests/unit/test_cellprofiler_module_execution.py +++ b/tests/unit/test_cellprofiler_module_execution.py @@ -170,7 +170,6 @@ CompiledSourceBindingPlan, ComponentSelector, NamedSourceBinding, - SourceBindingRuntimeContext, ) from openhcs.core.source_image_provenance import ( SourceImageProvenancePlanes, @@ -4008,7 +4007,6 @@ def __init__( tuple[ArtifactSpec, ArtifactOutputPlan], ... ] = (), source_bindings: tuple[NamedSourceBinding, ...] = (), - ordered_pipeline_image_paths: tuple[str, ...] = (), variable_components: tuple[VariableComponents, ...] = (), plane_projection: RuntimePlaneProjection = RuntimePlaneProjection.stack(), axis_scope: RuntimeExecutionAxisScope | None = None, @@ -4023,11 +4021,6 @@ def __init__( value=None, ) source_binding_plan = CompiledSourceBindingPlan(bindings=source_bindings) - source_binding_context = SourceBindingRuntimeContext( - step_input_files=ordered_pipeline_image_paths, - current_step_input_files=ordered_pipeline_image_paths, - pipeline_input_files=ordered_pipeline_image_paths, - ) processing_context = SimpleNamespace( microscope_handler=SimpleNamespace( parser=SimpleNamespace(semantic_identity=lambda: ()), @@ -4050,7 +4043,6 @@ def __init__( artifact_inputs={edge.key: edge for edge in indexed_input_edges}, artifact_output_bindings=artifact_output_bindings, source_binding_plan=source_binding_plan, - source_binding_context=source_binding_context, microscope_handler=processing_context.microscope_handler, variable_components=variable_components, plane_projection=plane_projection, @@ -4091,7 +4083,6 @@ def __init__( table, ) ) - self.ordered_pipeline_image_paths = ordered_pipeline_image_paths self.measurements: list[MeasurementTable] = [] self.objects: list[tuple[str, np.ndarray, dict[str, object]]] = [] self.spatial_grids: dict[str, SpatialGrid] = {} diff --git a/tests/unit/test_cellprofiler_runtime_adapter.py b/tests/unit/test_cellprofiler_runtime_adapter.py index c4c3ebc19e..0a5478c560 100644 --- a/tests/unit/test_cellprofiler_runtime_adapter.py +++ b/tests/unit/test_cellprofiler_runtime_adapter.py @@ -123,7 +123,6 @@ CompiledSourceBindingPlan, NamedSourceBinding, SourceBindingOrigin, - SourceBindingRuntimeContext, SourceFilterClause, SourceFilterMatchType, SourceFilterSubject, @@ -650,7 +649,6 @@ def _adapter( source_bindings=StepSourceBindingsConfig( bindings=(NamedSourceBinding(alias=DNA_IMAGE),) ), - source_binding_context=SourceBindingRuntimeContext.empty(), processing_context=None, plane_projection=RuntimePlaneProjection.stack(1), callable_contract=None, @@ -661,7 +659,6 @@ def _adapter( axis_scope=runtime_axis_scope(AXIS_ID), artifact_output_bindings=output_bindings, source_binding_plan=_compiled_source_binding_plan(source_bindings), - source_binding_context=source_binding_context, plane_projection=plane_projection, callable_contract=callable_contract, microscope_handler=( @@ -692,7 +689,6 @@ def _pipeline_start_contains_binding(alias): def _source_bound_image_adapter(output_bindings, images): filemanager = FileManagerStub() - paths = tuple(f"/src/{alias}.tif" for alias in images) for alias, image in images.items(): filemanager.saved[("memory", f"/src/{alias}.tif")] = image context = ContextStub(filemanager) @@ -708,12 +704,6 @@ def _source_bound_image_adapter(output_bindings, images): ) ) ), - source_binding_context=SourceBindingRuntimeContext( - step_input_files=paths, - step_input_dir="/src", - pipeline_input_files=paths, - pipeline_input_backend="memory", - ), microscope_handler=(context).microscope_handler, filemanager=filemanager, ) @@ -1215,11 +1205,6 @@ def test_cellprofiler_adapter_does_not_cache_current_image_object_selection(): "/src/A01_s001_w1_z001_t001.tif", "/src/A01_s002_w1_z001_t001.tif", ) - source_binding_context = SourceBindingRuntimeContext( - step_input_files=source_paths, - step_input_dir="/src", - pipeline_input_files=source_paths, - ) filemanager = FileManagerStub() processing_context = ContextStub(filemanager) @@ -1231,7 +1216,6 @@ def test_cellprofiler_adapter_does_not_cache_current_image_object_selection(): runtime_value_store=store, axis_scope=runtime_axis_scope(AXIS_ID), artifact_output_bindings=output_bindings, - source_binding_context=source_binding_context, group_key=group_key, microscope_handler=( processing_context.microscope_handler @@ -1281,7 +1265,6 @@ def test_cellprofiler_adapter_does_not_cache_current_image_object_selection(): ) }, variable_components=(VariableComponents.SITE,), - source_binding_context=source_binding_context, microscope_handler=( processing_context.microscope_handler if processing_context is not None @@ -1563,9 +1546,6 @@ def test_cellprofiler_adapter_does_not_source_scope_default_image_records(): axis_scope=runtime_axis_scope(AXIS_ID), artifact_inputs=_compiled_artifact_inputs, variable_components=(VariableComponents.SITE,), - source_binding_context=SourceBindingRuntimeContext( - step_input_files=("/src/A01_s003_w1.tif",), - ), filemanager=filemanager, callable_contract=_compiled_callable_contract( calculate_math, @@ -1627,11 +1607,6 @@ def test_cellprofiler_adapter_stacks_declared_default_image_input_runtime_groups ).payload_with(np.full((2, 2), value, dtype=np.float32), None), ) - source_binding_context = SourceBindingRuntimeContext( - step_input_files=source_paths, - current_step_input_files=source_paths, - pipeline_input_files=source_paths, - ) _compiled_artifact_inputs = { edge.key: edge for edge in ( @@ -1663,7 +1638,6 @@ def test_cellprofiler_adapter_stacks_declared_default_image_input_runtime_groups runtime_value_store=store, axis_scope=runtime_axis_scope(AXIS_ID, "well", AXIS_ID), artifact_inputs=_compiled_artifact_inputs, - source_binding_context=source_binding_context, microscope_handler=(context).microscope_handler, filemanager=filemanager, variable_components=(VariableComponents.SITE,), @@ -1760,12 +1734,6 @@ def test_cellprofiler_adapter_keeps_multisource_current_image_grouped_when_files runtime_value_store=store, axis_scope=runtime_axis_scope(AXIS_ID), artifact_inputs=_compiled_artifact_inputs, - source_binding_context=SourceBindingRuntimeContext( - step_input_files=source_paths, - current_step_input_files=(source_paths[0],), - pipeline_input_files=source_paths, - source_metadata_by_path=source_metadata_by_path, - ), microscope_handler=(context).microscope_handler, filemanager=filemanager, variable_components=(VariableComponents.SITE,), @@ -1832,11 +1800,6 @@ def test_cellprofiler_adapter_stacks_declared_image_input_for_pattern_group(): ).payload_with(np.full((2, 2), value, dtype=np.float32), None), ) - source_binding_context = SourceBindingRuntimeContext( - step_input_files=source_paths, - current_step_input_files=source_paths, - pipeline_input_files=source_paths, - ) _compiled_artifact_inputs = { edge.key: edge for edge in ( @@ -1862,7 +1825,6 @@ def test_cellprofiler_adapter_stacks_declared_image_input_for_pattern_group(): axis_scope=runtime_axis_scope(AXIS_ID), group_key="A01_s{iii}_w1_z001_t001.tif", artifact_inputs=_compiled_artifact_inputs, - source_binding_context=source_binding_context, microscope_handler=(context).microscope_handler, filemanager=filemanager, variable_components=(VariableComponents.SITE,), @@ -1897,7 +1859,6 @@ def test_cellprofiler_adapter_records_output_in_declared_invocation_group(): filemanager = FileManagerStub() output_name = "MembMasked" source_path = "/plate/Images/3d_monolayer_xy1_ch3.tif" - mask_path = "/plate/Images/3d_monolayer_xy1_ch1.tif" output_plan = ArtifactOutputPlan( name=output_name, path=f"/memory/{output_name}.pkl", @@ -1924,15 +1885,6 @@ def test_cellprofiler_adapter_records_output_in_declared_invocation_group(): artifact_output_bindings=( _output_binding(output_name, ImageArtifactType, plan=output_plan), ), - source_binding_context=SourceBindingRuntimeContext( - step_input_files=(mask_path,), - current_step_input_files=(mask_path,), - pipeline_input_files=( - "/plate/Images/3d_monolayer_xy1_ch0.tif", - mask_path, - source_path, - ), - ), microscope_handler=(ContextStub(filemanager)).microscope_handler, filemanager=filemanager, ) @@ -2042,11 +1994,6 @@ def test_cellprofiler_adapter_projects_source_bound_runtime_image_to_group_plane group_key="2", plane_projection=RuntimePlaneProjection.selected(1, 2), artifact_inputs=_compiled_artifact_inputs, - source_binding_context=SourceBindingRuntimeContext( - source_metadata_by_path=dict( - zip(source_paths, source_metadata, strict=True) - ), - ), filemanager=filemanager, callable_contract=_compiled_callable_contract( calculate_math, @@ -2146,11 +2093,6 @@ def test_cellprofiler_adapter_deduplicates_grouped_runtime_image_input_locations axis_scope=runtime_axis_scope(AXIS_ID, "site", "2"), plane_projection=RuntimePlaneProjection.selected(1, 2), artifact_inputs=_compiled_artifact_inputs, - source_binding_context=SourceBindingRuntimeContext( - source_metadata_by_path=dict( - zip(source_paths, source_metadata, strict=True) - ), - ), filemanager=filemanager, callable_contract=_compiled_callable_contract( calculate_math, @@ -2725,9 +2667,6 @@ def test_cellprofiler_adapter_keeps_template_scoped_object_records_grouped(): axis_scope=runtime_axis_scope(AXIS_ID), artifact_inputs=_compiled_artifact_inputs, variable_components=(VariableComponents.SITE,), - source_binding_context=SourceBindingRuntimeContext( - step_input_files=("/src/A01_s{iii}_w1.tif",), - ), filemanager=filemanager, callable_contract=_compiled_callable_contract( calculate_math, @@ -3414,22 +3353,6 @@ def test_cellprofiler_adapter_does_not_resolve_object_input_from_source_context( ), ) - source_binding_context = SourceBindingRuntimeContext( - step_input_files=( - "/plate/Images/A01_s002_w1_z001_t001.tif", - "/plate/Images/A01_s002_w2_z001_t001.tif", - ), - pipeline_input_files=( - "/plate/Images/A01_s001_w1_z001_t001.tif", - "/plate/Images/A01_s001_w2_z001_t001.tif", - "/plate/Images/A01_s002_w1_z001_t001.tif", - "/plate/Images/A01_s002_w2_z001_t001.tif", - ), - current_step_input_files=( - "/plate/Images/A01_s002_w1_z001_t001.tif", - "/plate/Images/A01_s002_w2_z001_t001.tif", - ), - ) _compiled_artifact_inputs = { edge.key: edge for edge in ( @@ -3459,7 +3382,6 @@ def test_cellprofiler_adapter_does_not_resolve_object_input_from_source_context( runtime_value_store=store, axis_scope=runtime_axis_scope(AXIS_ID), artifact_inputs=_compiled_artifact_inputs, - source_binding_context=source_binding_context, microscope_handler=(ContextStub(filemanager)).microscope_handler, filemanager=filemanager, variable_components=(VariableComponents.SITE,), @@ -4382,14 +4304,6 @@ def test_cellprofiler_adapter_does_not_select_measurement_record_from_current_so ) ) - source_binding_context = SourceBindingRuntimeContext( - step_input_files=("/plate/Images/A01_s002_w1_z001_t001.tif",), - pipeline_input_files=( - "/plate/Images/A01_s001_w1_z001_t001.tif", - "/plate/Images/A01_s002_w1_z001_t001.tif", - ), - current_step_input_files=("/plate/Images/A01_s002_w1_z001_t001.tif",), - ) _compiled_artifact_inputs = { edge.key: edge for edge in ( @@ -4419,7 +4333,6 @@ def test_cellprofiler_adapter_does_not_select_measurement_record_from_current_so runtime_value_store=store, axis_scope=runtime_axis_scope(AXIS_ID), artifact_inputs=_compiled_artifact_inputs, - source_binding_context=source_binding_context, microscope_handler=(ContextStub(filemanager)).microscope_handler, filemanager=filemanager, variable_components=(VariableComponents.SITE,), diff --git a/tests/unit/test_cellprofiler_static_deletion_gates.py b/tests/unit/test_cellprofiler_static_deletion_gates.py index 82afc7ca84..d97c78c59c 100644 --- a/tests/unit/test_cellprofiler_static_deletion_gates.py +++ b/tests/unit/test_cellprofiler_static_deletion_gates.py @@ -794,7 +794,7 @@ def test_runtime_adapter_state_has_one_nominal_owner() -> None: } request = classes["RuntimeAdapterRequest"] adapter = classes["CellProfilerRuntimeAdapter"] - source_context = classes["SourceBindingRuntimeContext"] + assert "SourceBindingRuntimeContext" not in classes request_fields = { node.target.id @@ -826,7 +826,7 @@ def test_runtime_adapter_state_has_one_nominal_owner() -> None: isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name) and node.target.id == "source_binding_context" - for node in source_context.body + for node in request.body ) assert "FunctionRuntimeScope" not in classes for class_name in ("PatternGroupData",): diff --git a/tests/unit/test_debug_runtime.py b/tests/unit/test_debug_runtime.py index 0197481dce..3274195fb1 100644 --- a/tests/unit/test_debug_runtime.py +++ b/tests/unit/test_debug_runtime.py @@ -130,7 +130,6 @@ from openhcs.core.pipeline.function_contracts import artifact_inputs, artifact_outputs from openhcs.core.source_bindings import ( CompiledSourceBindingPlan, - SourceBindingRuntimeContext, ) from openhcs.core.source_load_plan import SourceLoadPlan from openhcs.core.steps.function_runtime import ( @@ -339,7 +338,6 @@ def execute_function_chain( artifact_outputs=None, runtime_plane_index: int = 0, runtime_plane_count: int = 1, - source_binding_context: SourceBindingRuntimeContext | None = None, ): resolved_artifact_inputs = {} if artifact_inputs is None else artifact_inputs resolved_artifact_outputs = {} if artifact_outputs is None else artifact_outputs @@ -359,11 +357,6 @@ def execute_function_chain( inputs=resolved_artifact_inputs, outputs=resolved_artifact_outputs, ), - source_binding_context=( - SourceBindingRuntimeContext.empty() - if source_binding_context is None - else source_binding_context - ), runtime_plane_index=runtime_plane_index, runtime_plane_count=runtime_plane_count, ) diff --git a/tests/unit/test_function_artifact_outputs.py b/tests/unit/test_function_artifact_outputs.py index 2a1e632bfb..977eab0089 100644 --- a/tests/unit/test_function_artifact_outputs.py +++ b/tests/unit/test_function_artifact_outputs.py @@ -59,7 +59,6 @@ from openhcs.core.source_bindings import ( CompiledSourceBindingPlan, NamedSourceBinding, - SourceBindingRuntimeContext, ) from openhcs.core.source_load_plan import SourceLoadPlan from openhcs.core.source_metadata import ( @@ -278,7 +277,6 @@ def _execute_function_core(request: CoreExecutionRequest): execution_plan, component_value, ), - source_binding_context=SourceBindingRuntimeContext.empty(), runtime_plane_index=0, runtime_plane_count=request.runtime_plane_count, ) @@ -2899,7 +2897,6 @@ def declared_source_origin(image, *, image_to_save=None): group_data=SimpleNamespace( context=ContextStub(), source_binding_plan=source_binding_plan, - source_binding_context=SourceBindingRuntimeContext.empty(), axis_scope=RuntimeExecutionAxisScope.from_raw( "A01", component=None, diff --git a/tests/unit/test_function_core_executor_ownership.py b/tests/unit/test_function_core_executor_ownership.py index 0c61087947..f53a51991c 100644 --- a/tests/unit/test_function_core_executor_ownership.py +++ b/tests/unit/test_function_core_executor_ownership.py @@ -19,7 +19,7 @@ ) from openhcs.core.pipeline.function_contracts import artifact_inputs from openhcs.core.runtime_plane_projection import RuntimePlaneProjection -from openhcs.core.source_bindings import CompiledSourceBindingPlan, SourceBindingRuntimeContext +from openhcs.core.source_bindings import CompiledSourceBindingPlan from openhcs.core.steps.function_runtime import ( ComponentArtifactPlans, FunctionCoreExecutor, @@ -72,7 +72,6 @@ def _executor(): execution_plan=plan, compiled_group=replace(pattern.default_group, invocations=(invocation,)), artifacts=ComponentArtifactPlans(inputs={}, outputs={}), - source_binding_context=SourceBindingRuntimeContext.empty(), runtime_plane_index=0, runtime_plane_count=1, ) diff --git a/tests/unit/test_function_io.py b/tests/unit/test_function_io.py index b539238248..70c56a8adb 100644 --- a/tests/unit/test_function_io.py +++ b/tests/unit/test_function_io.py @@ -469,8 +469,8 @@ def test_virtual_pipeline_source_universe_does_not_mix_physical_paths( universe, ) - assert state.pipeline_source_candidate_files == universe.files - assert not set(state.pipeline_source_candidate_files).intersection( + assert state.require_load_universe().files == universe.files + assert not set(state.require_load_universe().files).intersection( str(source_path) for source_path in source_paths ) diff --git a/tests/unit/test_function_runtime_source_projection.py b/tests/unit/test_function_runtime_source_projection.py index 76878d4fb7..a85191e0c0 100644 --- a/tests/unit/test_function_runtime_source_projection.py +++ b/tests/unit/test_function_runtime_source_projection.py @@ -66,7 +66,6 @@ SourceBindingMatchMethod, SourceBindingMatchPlan, SourceBindingOrigin, - SourceBindingRuntimeContext, SourceFilterClause, SourceFilterMatchType, SourceFilterSubject, @@ -533,6 +532,69 @@ def resolve_address(backend_address, backend, *, base_path): assert metadata.source_image_names == ("OrigBlue",) + +def test_physical_source_loading_preserves_tiff_calibration_and_live_buffers( + tmp_path: Path, +) -> None: + import tifffile + from polystore.base import ensure_storage_registry, storage_registry + from polystore.filemanager import FileManager + + from openhcs.constants.constants import Backend + from openhcs.core.runtime_image_values import image_payload_mask + from openhcs.core.runtime_source_binding_cache import RuntimeSourceBindingContextCache + from openhcs.core.steps.function_runtime import PatternGroupRuntime + from openhcs.microscopes.source_schema import SourceSchemaFilenameParser + + source_path = tmp_path / "A01_s002_w1_z003_t004.tif" + pixels = np.array([[0, 4095]], dtype=np.uint16) + mask = np.array([[True, False]]) + tifffile.imwrite(source_path, pixels, extratags=((281, "H", 1, 4095, False),)) + ensure_storage_registry() + filemanager = FileManager(dict(storage_registry)) + bindings = CompiledSourceBindingPlan.empty() + plan = CompiledStepPlan( + step_index=0, step_name="Physical source", step_type="FunctionStep", + axis_id="A01", input_dir=tmp_path, output_dir=tmp_path / "outputs", + output_plate_root=tmp_path / "outputs", sub_dir="images", + read_backend=Backend.DISK.value, write_backend=Backend.MEMORY.value, + pipeline_position=0, variable_components=(), source_binding_plan=bindings, + compiled_function_pattern=compile_function_pattern(lambda image: image, {}, {}), + ) + context = SimpleNamespace( + input_dir=tmp_path, filemanager=filemanager, + microscope_handler=SimpleNamespace( + parser=SourceSchemaFilenameParser(), + get_primary_backend=lambda *_args: Backend.DISK.value, + ), + runtime_source_binding_context_cache=RuntimeSourceBindingContextCache(), + ) + runtime = PatternGroupRuntime.__new__(PatternGroupRuntime) + runtime.request = SimpleNamespace( + context=context, execution_plan=plan, source_binding_plan=bindings, + ) + payload = ImagePayloadMetadata().payload_with(pixels, mask) + lookup = VirtualWorkspacePathLookup.from_paths(source_path.name, str(source_path)) + + (loaded,) = runtime._apply_source_image_loading_semantics( + (payload,), (lookup,), (), None, + ) + + metadata = image_payload_metadata(loaded) + assert metadata.intensity_scale == 4095.0 + assert metadata.source_dtype == "uint16" + assert metadata.source_path == str(source_path) + assert metadata.source_component_metadata["site"] == 2 + assert metadata.source_component_metadata["z_index"] == 3 + assert metadata.source_component_metadata["timepoint"] == 4 + assert image_payload_data(loaded) is pixels + assert image_payload_mask(loaded) is mask + np.testing.assert_array_equal(image_payload_data(loaded), [[0, 4095]]) + pixels[0, 0] = 17 + mask[0, 0] = False + assert image_payload_data(loaded)[0, 0] == 17 + assert not image_payload_mask(loaded)[0, 0] + def test_virtual_workspace_source_filters_use_persisted_candidate_identity( tmp_path: Path, ) -> None: @@ -2281,7 +2343,6 @@ def test_grouped_runtime_adapter_receives_component_selected_source_bindings() - compiled_group=compiled_pattern.default_group, component_value="1", artifacts=ComponentArtifactPlans(inputs={}, outputs={}), - source_binding_context=SourceBindingRuntimeContext.empty(), runtime_plane_index=0, runtime_plane_count=2, ) @@ -2301,7 +2362,6 @@ def test_grouped_runtime_adapter_receives_component_selected_source_bindings() - assert tuple( binding.alias for binding in request.source_binding_plan.binding_declarations ) == ("OrigStain1",) - assert request.source_binding_context is scope.source_binding_context from openhcs.interop.cellprofiler.runtime.module_execution import ( cellprofiler_runtime_adapter_factory, @@ -2408,7 +2468,6 @@ def consume_channel_source(image): compiled_group=compiled_group, component_value="1", artifacts=ComponentArtifactPlans.from_step_component(execution_plan, "1"), - source_binding_context=SourceBindingRuntimeContext.empty(), runtime_plane_index=0, runtime_plane_count=1, ) @@ -2587,7 +2646,6 @@ def record_second_labels(image, *, runtime): compiled_group=compiled_group, component_value="1", artifacts=ComponentArtifactPlans.from_step_component(execution_plan, "1"), - source_binding_context=SourceBindingRuntimeContext.empty(), runtime_plane_index=0, runtime_plane_count=1, ) @@ -3202,7 +3260,6 @@ def test_runtime_plane_count_comes_from_loaded_slices_not_dispatch_groups() -> N ), ), ).payload_with(np.zeros((2, 4, 5), dtype=np.float32)), - source_binding_context=SourceBindingRuntimeContext.empty(), ) assert scope.runtime_plane_count == 2 @@ -3707,7 +3764,6 @@ def resolve_address(backend_address, backend, *, base_path): (payload,), (lookup,), workspace_source_lookups, - SourceBindingRuntimeContext.empty(), projection, ) @@ -3878,15 +3934,6 @@ def resolve_address(backend_address, backend, *, base_path): "step_output_manifest", lambda _context: StepOutputManifestStore(), ) - monkeypatch.setattr( - function_runtime.SourceBindingRuntimeContextRequest, - "from_context", - classmethod( - lambda cls, **_kwargs: SimpleNamespace( - runtime_context=SourceBindingRuntimeContext.empty - ) - ), - ) monkeypatch.setattr( function_runtime.PatternGroupRuntime, "source_workspace_projection_authority", @@ -3986,21 +4033,11 @@ def load_batch(self, paths, backend): assert backend == "memory" return [producer_payload] - source_context = SourceBindingRuntimeContext() monkeypatch.setattr( function_runtime, "step_output_manifest", lambda _context: producer_manifest, ) - monkeypatch.setattr( - function_runtime.SourceBindingRuntimeContextRequest, - "from_context", - classmethod( - lambda cls, **_kwargs: SimpleNamespace( - runtime_context=lambda: source_context - ) - ), - ) monkeypatch.setattr( function_runtime.PatternGroupRuntime, "source_workspace_projection_authority", @@ -5617,7 +5654,7 @@ def test_producer_admission_rederives_live_storage_aliases_each_epoch( assert current_index.record_for_path(record.output_path) is record -def test_producer_loader_validates_ambiguity_after_source_context_before_cache( +def test_producer_loader_validates_ambiguity_before_cache( monkeypatch, ): from openhcs.core.steps import function_runtime @@ -5648,12 +5685,6 @@ def test_producer_loader_validates_ambiguity_after_source_context_before_cache( ), ), )) - events = [] - - def source_context(): - events.append("source_context") - return SourceBindingRuntimeContext.empty() - class RejectImageCache: def get(self, *_args, **_kwargs): pytest.fail("Ambiguous producer admission must precede cached pixels") @@ -5673,10 +5704,6 @@ def get(self, *_args, **_kwargs): monkeypatch.setattr(runtime, "source_workspace_projection_authority", lambda: SimpleNamespace( projection_if_available=lambda: None, )) - monkeypatch.setattr(function_runtime.SourceBindingRuntimeContextRequest, "from_context", classmethod( - lambda cls, **_kwargs: SimpleNamespace(runtime_context=source_context), - )) with pytest.raises(NoStepOutputManifestMatch, match="found 2"): runtime._load_input_stack() - assert events == ["source_context"] diff --git a/tests/unit/test_loaded_pattern_group_ownership.py b/tests/unit/test_loaded_pattern_group_ownership.py index c20d1bc167..067097b50e 100644 --- a/tests/unit/test_loaded_pattern_group_ownership.py +++ b/tests/unit/test_loaded_pattern_group_ownership.py @@ -14,7 +14,7 @@ from openhcs.core.context.processing_context import ProcessingContext from openhcs.core.function_patterns import compile_function_pattern from openhcs.core.runtime_image_values import ImagePayloadMetadata, image_payload_data, image_payload_metadata -from openhcs.core.source_bindings import CompiledSourceBindingPlan, SourceBindingRuntimeContext +from openhcs.core.source_bindings import CompiledSourceBindingPlan from openhcs.core.source_image_provenance import SourceImageProvenancePlanes from openhcs.core.steps import function_runtime from openhcs.core.steps.function_runtime import ( @@ -59,16 +59,15 @@ def _fixture(): ), ), ).payload_with(np.arange(24, dtype=np.float32).reshape(2, 3, 4)) - source_context = SourceBindingRuntimeContext.empty() - return request, paths, payload, source_context + return request, paths, payload -def test_complete_loaded_owner_has_one_frozen_eleven_field_contract_and_pickle(): - request, paths, payload, source_context = _fixture() - loaded = PatternGroupData.from_loaded_group(request, paths, payload, source_context) +def test_complete_loaded_owner_has_one_frozen_ten_field_contract_and_pickle(): + request, paths, payload = _fixture() + loaded = PatternGroupData.from_loaded_group(request, paths, payload) expected = ( "context", "execution_plan", "compiled_group", "component_value", "fixed_component_values", - "artifacts", "source_binding_context", "runtime_plane_index", "runtime_plane_count", + "artifacts", "runtime_plane_index", "runtime_plane_count", "matching_files", "main_data_stack", ) assert PatternGroupData.__bases__ == (PatternGroupExecutionScope,) @@ -79,7 +78,6 @@ def test_complete_loaded_owner_has_one_frozen_eleven_field_contract_and_pickle() assert loaded.context is request.context assert loaded.execution_plan is request.execution_plan assert loaded.compiled_group is request.compiled_group - assert loaded.source_binding_context is source_context assert loaded.matching_files is paths assert loaded.main_data_stack is payload with pytest.raises(FrozenInstanceError): @@ -100,8 +98,8 @@ def test_complete_loaded_owner_has_one_frozen_eleven_field_contract_and_pickle() def test_initial_coordinates_stay_captured_while_plan_selectors_follow_mutation(): - request, paths, payload, source_context = _fixture() - loaded = PatternGroupData.from_loaded_group(request, paths, payload, source_context) + request, paths, payload = _fixture() + loaded = PatternGroupData.from_loaded_group(request, paths, payload) assert loaded.runtime_plane_count == 2 assert loaded.fixed_component_values == ((AllComponents.Z_INDEX, "3"), (AllComponents.TIMEPOINT, "2")) paths.append("later-mutation.tif") @@ -116,11 +114,11 @@ def test_initial_coordinates_stay_captured_while_plan_selectors_follow_mutation( def test_chain_shares_cohort_but_advances_only_current_image_and_memory(monkeypatch): - request, paths, payload, source_context = _fixture() + request, paths, payload = _fixture() invocation = request.compiled_group.invocations[0] group = replace(request.compiled_group, invocations=(invocation, invocation)) request = replace(request, compiled_group=group) - loaded = PatternGroupData.from_loaded_group(request, paths, payload, source_context) + loaded = PatternGroupData.from_loaded_group(request, paths, payload) first_output = np.ones((2, 3, 4), dtype=np.float32) second_output = np.full((2, 3, 4), 2, dtype=np.float32) seen = [] @@ -147,18 +145,17 @@ def execute(executor, *, debug_sink=None): def test_scope_admission_follows_load_profile_inside_execution_error_boundary(monkeypatch): - request, paths, payload, source_context = _fixture() + request, paths, payload = _fixture() runtime = PatternGroupRuntime(request) events = [] failure = RuntimeError("cohort admission failed") - monkeypatch.setattr(runtime, "_load_input_stack", lambda: (events.append("load") or (paths, payload, source_context))) + monkeypatch.setattr(runtime, "_load_input_stack", lambda: (events.append("load") or (paths, payload))) monkeypatch.setattr(RuntimeProfileSink, "record", lambda label, *_args, **_kwargs: events.append(label)) - def fail_capture(cls, observed_request, observed_paths, observed_payload, observed_context): + def fail_capture(cls, observed_request, observed_paths, observed_payload): assert observed_request is request assert observed_paths is paths assert observed_payload is payload - assert observed_context is source_context events.append("capture") raise failure @@ -171,7 +168,7 @@ def fail_capture(cls, observed_request, observed_paths, observed_payload, observ @pytest.mark.parametrize("failure", [RuntimeError("load failed"), NoStepOutputManifestMatch("stale")]) def test_load_errors_and_stale_skip_precede_capture_and_execution_wrapping(monkeypatch, failure): - request, _paths, _payload, _source_context = _fixture() + request, _paths, _payload = _fixture() runtime = PatternGroupRuntime(request) events = [] @@ -191,11 +188,11 @@ def failed_load(): def test_empty_chain_is_rejected_after_full_capture_and_load_profile(monkeypatch): - request, paths, payload, source_context = _fixture() + request, paths, payload = _fixture() request = replace(request, compiled_group=replace(request.compiled_group, invocations=())) runtime = PatternGroupRuntime(request) labels = [] - monkeypatch.setattr(runtime, "_load_input_stack", lambda: (paths, payload, source_context)) + monkeypatch.setattr(runtime, "_load_input_stack", lambda: (paths, payload)) monkeypatch.setattr(RuntimeProfileSink, "record", lambda label, *_args, **_kwargs: labels.append(label)) with pytest.raises(ValueError, match="has no invocations") as exc: runtime.run() @@ -204,8 +201,8 @@ def test_empty_chain_is_rejected_after_full_capture_and_load_profile(monkeypatch def test_adapter_request_projects_live_fields_and_preserves_current_payload_epoch(): - request, paths, initial, source_context = _fixture() - loaded = PatternGroupData.from_loaded_group(request, paths, initial, source_context) + request, paths, initial = _fixture() + loaded = PatternGroupData.from_loaded_group(request, paths, initial) current = np.ones((2, 3, 4), dtype=np.float32) invocation = loaded.compiled_group.invocations[0] executor = FunctionCoreExecutor( @@ -217,7 +214,6 @@ def test_adapter_request_projects_live_fields_and_preserves_current_payload_epoc assert adapter.callable_contract is invocation.contract assert adapter.source_payload is current assert adapter.source_payload is not loaded.main_data_stack - assert adapter.source_binding_context is source_context assert adapter.plane_projection is executor.plane_projection assert adapter.source_load_plan is request.execution_plan.source_load_plan assert adapter.variable_components == (VariableComponents.SITE,) @@ -227,12 +223,11 @@ def test_adapter_request_projects_live_fields_and_preserves_current_payload_epoc updated = executor.runtime_adapter_request(current) assert updated.axis_scope.axis_id == "B02" assert updated.variable_components == (VariableComponents.Z_INDEX,) - assert updated.source_binding_context is source_context def test_adapter_tuple_admission_precedes_output_map_validation(): - request, paths, initial, source_context = _fixture() - loaded = PatternGroupData.from_loaded_group(request, paths, initial, source_context) + request, paths, initial = _fixture() + loaded = PatternGroupData.from_loaded_group(request, paths, initial) invocation = loaded.compiled_group.invocations[0] executor = FunctionCoreExecutor( loaded, invocation, ComponentArtifactPlans(inputs={}, outputs={"invalid": object()}), "1", @@ -247,11 +242,11 @@ def test_adapter_tuple_admission_precedes_output_map_validation(): def test_component_artifact_admission_precedes_source_provenance_capture(monkeypatch): - request, paths, initial, source_context = _fixture() + request, paths, initial = _fixture() request.execution_plan.artifact_inputs = {"invalid": object()} monkeypatch.setattr(function_runtime, "image_payload_metadata", lambda _payload: pytest.fail("source projection must follow artifact-plan admission")) with pytest.raises(TypeError, match="Component artifact input"): - PatternGroupData.from_loaded_group(request, paths, initial, source_context) + PatternGroupData.from_loaded_group(request, paths, initial) def _stored_primary_fixture(*, preserves_main_flow=False): @@ -268,7 +263,7 @@ def _stored_primary_fixture(*, preserves_main_flow=False): from openhcs.core.runtime_plane_projection import RuntimePlaneAxis from openhcs.core.component_group_scope import RuntimeExecutionAxisScope - request, _, payload, source_context = _fixture() + request, _, payload = _fixture() source = ArtifactSpec.input("Canonical", ImageArtifactType) output = ArtifactSpec.output( "Processed", SpecialArtifactType if preserves_main_flow else ImageArtifactType, @@ -319,7 +314,7 @@ def consume(image, *, runtime): request.context.runtime_value_store.replace( value, path=input_plan.path, backend=Backend.MEMORY.value, ) - return request, value, source_context + return request, value def test_stored_primary_admission_owns_independent_buffer_and_live_primary_binding(monkeypatch): @@ -328,22 +323,17 @@ def test_stored_primary_admission_owns_independent_buffer_and_live_primary_bindi from openhcs.interop.cellprofiler.runtime.artifact_binding import RuntimeInputBindingRequest from tests.unit.cellprofiler_runtime_test_support import cellprofiler_runtime_adapter_for_test - request, canonical, source_context = _stored_primary_fixture() + request, canonical = _stored_primary_fixture() runtime = PatternGroupRuntime(request) monkeypatch.setattr( runtime, "source_workspace_projection_authority", lambda: SimpleNamespace(projection_if_available=lambda: None), ) - monkeypatch.setattr( - function_runtime.SourceBindingRuntimeContextRequest, "from_context", - classmethod(lambda cls, **kwargs: SimpleNamespace(runtime_context=lambda: source_context)), - ) - paths, admitted, admitted_context = request.load_input_stack(runtime) + paths, admitted = request.load_input_stack(runtime) assert paths == ["/memory/canonical"] - assert admitted_context is source_context assert not np.shares_memory(image_payload_data(admitted), image_payload_data(canonical.data)) assert not np.shares_memory(image_payload_mask(admitted), image_payload_mask(canonical.data)) - loaded = PatternGroupData.from_loaded_group(request, paths, admitted, admitted_context) + loaded = PatternGroupData.from_loaded_group(request, paths, admitted) assert loaded.runtime_plane_count == 2 assert loaded.runtime_plane_index == 7 assert loaded.fixed_component_values == ((AllComponents.Z_INDEX, "3"), (AllComponents.TIMEPOINT, "2")) @@ -366,7 +356,7 @@ def test_stored_primary_admission_owns_independent_buffer_and_live_primary_bindi assert bound is admitted np.testing.assert_array_equal(image_payload_data(bound), 17) np.testing.assert_array_equal(image_payload_data(replacement.data), 31) - _, next_admitted, _ = request.load_input_stack(runtime) + _, next_admitted = request.load_input_stack(runtime) np.testing.assert_array_equal(image_payload_data(next_admitted), 31) assert not np.shares_memory(image_payload_data(next_admitted), image_payload_data(replacement.data)) assert loaded.runtime_plane_count == 2 @@ -376,9 +366,9 @@ def test_checkpoint_demand_follows_preserved_input_to_native_reader(): from openhcs.constants.constants import Backend from openhcs.core.step_dependencies import StepInputDependency - request, _, _ = _stored_primary_fixture() + request, _ = _stored_primary_fixture() source = replace(request.execution_plan, step_index=0, step_scope_id="source", write_backend=Backend.MEMORY.value) - preserving_request, _, _ = _stored_primary_fixture(preserves_main_flow=True) + preserving_request, _ = _stored_primary_fixture(preserves_main_flow=True) preserving_pattern = preserving_request.execution_plan.compiled_function_pattern preserving = replace( source, step_index=1, step_scope_id="preserving", compiled_function_pattern=preserving_pattern, @@ -473,7 +463,7 @@ def test_checkpoint_demand_terminates_on_preserving_cycles_and_self_producers(): def test_artifact_loaded_coordinates_keep_producer_authority_over_original_source(): - request, canonical, source_context = _stored_primary_fixture() + request, canonical = _stored_primary_fixture() exact_coordinates = ( (AllComponents.CHANNEL, "ProducedChannel"), (AllComponents.Z_INDEX, "3"), @@ -482,7 +472,7 @@ def test_artifact_loaded_coordinates_keep_producer_authority_over_original_sourc request = replace(request, fixed_component_values=exact_coordinates) assert image_payload_metadata(canonical.data).source_provenance.with_common_scalar_identity_from_planes().source_component_metadata["channel"] == "1" loaded = PatternGroupData.from_loaded_group( - request, ["/memory/canonical"], canonical.data, source_context, + request, ["/memory/canonical"], canonical.data, ) assert loaded.fixed_component_values == exact_coordinates canonical.data.metadata.source_provenance = canonical.data.metadata.source_provenance.with_source_component_metadata({"channel": "9"}) @@ -496,7 +486,7 @@ def test_conversion_and_sequential_filter_use_the_same_transport_admission(): InputConversionPlan, SequentialRuntimeFilter, SequentialRuntimeFilterPlan, ) - request, _, _ = _stored_primary_fixture() + request, _ = _stored_primary_fixture() plan = request.execution_plan assert plan.stored_primary_input_edges_for_group(request.compiled_group, None) plan.input_conversion = InputConversionPlan( @@ -512,7 +502,7 @@ def test_conversion_and_sequential_filter_use_the_same_transport_admission(): def test_same_step_primary_producer_is_not_queried_before_its_callable_runs(): - request, _, _ = _stored_primary_fixture() + request, _ = _stored_primary_fixture() plan = request.execution_plan assert plan.stored_primary_input_edges_for_group(request.compiled_group, None) invocation = request.compiled_group.invocations[0] diff --git a/tests/unit/test_opaque_main_flow_image_domain.py b/tests/unit/test_opaque_main_flow_image_domain.py index ad19e2add7..7f02a14a48 100644 --- a/tests/unit/test_opaque_main_flow_image_domain.py +++ b/tests/unit/test_opaque_main_flow_image_domain.py @@ -23,8 +23,7 @@ from openhcs.core.aligned_image_payload import AlignedImageStack, ImageOutputBundle, AlignedImageSliceContext, ImagePayloadStackComposition from openhcs.core.context.processing_context import ProcessingContext from openhcs.core.steps.function_output_manifest import step_output_manifest, StepOutputManifestStore -from openhcs.core.source_bindings import CompiledSourceBindingPlan, SourceBindingRuntimeContext -from openhcs.core.steps.function_runtime import SourceBindingRuntimeContextRequest +from openhcs.core.source_bindings import CompiledSourceBindingPlan from openhcs.core.step_dependencies import StepInputDependency from openhcs.microscopes.source_schema import SourceSchemaFilenameParser from openhcs.core.source_image_provenance import SourceImageProvenancePlanes @@ -220,7 +219,6 @@ def test_save_and_next_load_preserve_domain_and_independent_cache(tmp_path, monk pattern_group_info="A01_s001_w1_z{iii}_t001.tif", )) monkeypatch.setattr(consumer, "source_workspace_projection_authority", lambda: SimpleNamespace(projection_if_available=lambda: None)) - monkeypatch.setattr(SourceBindingRuntimeContextRequest, "from_context", classmethod(lambda cls, **kwargs: SimpleNamespace(runtime_context=SourceBindingRuntimeContext.empty))) loaded = consumer._load_input_stack()[1] assert image_payload_data(loaded).shape == pixels.shape assert image_payload_metadata(loaded).plane_axis is axis @@ -364,7 +362,6 @@ def test_saved_mixed_named_cohort_rejects_joint_load_and_preserves_selected_doma 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 context.runtime_image_stack_cache.get(tuple(r.output_path for r in records), memory_type=MEMORY_TYPE_NUMPY) is None - monkeypatch.setattr(SourceBindingRuntimeContextRequest, "from_context", classmethod(lambda cls, **kwargs: SimpleNamespace(runtime_context=SourceBindingRuntimeContext.empty))) def consumer_for(compiled): consumer_plan = replace(plan, step_index=1, step_scope_id="mixed-consumer", input_dir=tmp_path, diff --git a/tests/unit/test_path_planner_materialization.py b/tests/unit/test_path_planner_materialization.py index 96a7cc729d..8642a81bbd 100644 --- a/tests/unit/test_path_planner_materialization.py +++ b/tests/unit/test_path_planner_materialization.py @@ -101,7 +101,6 @@ SourceBindingOrigin, SourceBindingsConfig, SourceProjectionRole, - SourceBindingRuntimeContext, StepSourceBindingsConfig, ) from openhcs.core.step_dependencies import StepInputDependency @@ -152,7 +151,6 @@ def _execute_compiled_metadata_pattern(compiled, input_plans=None, stored_output compiled_group=compiled.default_group, component_value=None, artifacts=ComponentArtifactPlans.from_step_component(plan, None), - source_binding_context=SourceBindingRuntimeContext.empty(), runtime_plane_index=0, runtime_plane_count=1, ) diff --git a/tests/unit/test_persisted_semantic_metadata.py b/tests/unit/test_persisted_semantic_metadata.py index 9dd251927c..6f9535c173 100644 --- a/tests/unit/test_persisted_semantic_metadata.py +++ b/tests/unit/test_persisted_semantic_metadata.py @@ -17,7 +17,6 @@ image_payload_metadata, ) from openhcs.core.source_binding_selection import SourcePatternResolutionContext -from openhcs.core.source_bindings import SourceBindingRuntimeContext from openhcs.core.source_image_provenance import ( SourceImageProvenance, SourceImageProvenancePlanes, @@ -94,14 +93,10 @@ def _resolve_persisted_nested_metadata_in_spawned_runtime( projection = VirtualWorkspaceSourceProjection.from_openhcs_metadata( Path("/plate"), document ) - runtime_context = SourceBindingRuntimeContext( - step_input_files=(VIRTUAL_PATH,), - step_input_source_paths={VIRTUAL_PATH: VIRTUAL_PATH}, - source_metadata_by_path=projection.source_metadata_by_path, - ) - selection_context = SourcePatternResolutionContext.from_runtime_context( + selection_context = SourcePatternResolutionContext.from_sources( parser=SourceSchemaFilenameParser(), - runtime_context=runtime_context, + source_paths_by_virtual_path={VIRTUAL_PATH: VIRTUAL_PATH}, + source_metadata_by_path=projection.source_metadata_by_path, ) metadata = selection_context.metadata_for_path(VIRTUAL_PATH) if metadata is None: diff --git a/tests/unit/test_runtime_adapter_source_artifacts.py b/tests/unit/test_runtime_adapter_source_artifacts.py index cb0b5a6f76..9b2f159ea0 100644 --- a/tests/unit/test_runtime_adapter_source_artifacts.py +++ b/tests/unit/test_runtime_adapter_source_artifacts.py @@ -23,7 +23,6 @@ NamedSourceBinding, SourceBindingMatchMethod, SourceBindingMatchPlan, - SourceBindingRuntimeContext, SourceBindingsConfig, SourceFilterClause, SourceFilterMatchType, @@ -189,13 +188,6 @@ def binding( primary_projection.ref.backend_address, ), ) - runtime_context = SourceBindingRuntimeContext( - step_input_source_paths={ - virtual_path: source_ref.backend_address - for virtual_path, source_ref in projection.source_refs_by_virtual_path.items() - }, - source_metadata_by_path=projection.source_metadata_by_path, - ) def request() -> RuntimeAdapterRequest: return RuntimeAdapterRequest( @@ -208,7 +200,6 @@ def request() -> RuntimeAdapterRequest: enabled=True, ), ), - source_binding_context=runtime_context, axis_scope=RuntimeExecutionAxisScope.from_raw( "A01", component=None, diff --git a/tests/unit/test_runtime_context_declaration.py b/tests/unit/test_runtime_context_declaration.py index 2bd7f4fef8..2ac6962887 100644 --- a/tests/unit/test_runtime_context_declaration.py +++ b/tests/unit/test_runtime_context_declaration.py @@ -23,7 +23,6 @@ from openhcs.core.pipeline.function_contracts import runtime_context_parameter from openhcs.core.runtime_image_values import ImagePayloadMetadata from openhcs.core.runtime_plane_projection import RuntimePlaneProjection -from openhcs.core.source_bindings import SourceBindingRuntimeContext from openhcs.core.steps.function_runtime import ( ComponentArtifactPlans, FunctionCoreExecutor, @@ -138,7 +137,6 @@ def test_runtime_binding_consumes_captured_selection(func, parameter): ), compiled_group=pattern.default_group, artifacts=artifacts, - source_binding_context=SourceBindingRuntimeContext.empty(), runtime_plane_index=0, runtime_plane_count=1, ) diff --git a/tests/unit/test_runtime_source_resolution_snapshot.py b/tests/unit/test_runtime_source_resolution_snapshot.py index 5d3660d75c..eaa1322d23 100644 --- a/tests/unit/test_runtime_source_resolution_snapshot.py +++ b/tests/unit/test_runtime_source_resolution_snapshot.py @@ -17,7 +17,6 @@ from openhcs.core.source_bindings import ( MetadataExtractionRule, MetadataSource, - SourceBindingRuntimeContext, ) from openhcs.core.source_matching import SourceImageSetIdentityPolicy from openhcs.core.source_metadata import ( @@ -284,9 +283,6 @@ def test_warmed_cache_transport_reconstructs_all_derived_defaults(context_owned) source_projection = projection({ORIGINAL_SOURCE_METADATA_FIELD: {"Plate": "A"}}) context = snapshot(cache, source_projection, parser) cache.normalized_source_metadata(source_projection.source_metadata_by_path) - cache.runtime_context_by_request_identity[(1, (), "disk", None)] = ( - SourceBindingRuntimeContext() - ) assert cache.source_resolution_snapshots assert cache.source_metadata_by_mapping_identity restored_owner = pickle.loads( diff --git a/tests/unit/test_source_bindings.py b/tests/unit/test_source_bindings.py index 34d6667e5a..f66ce533d4 100644 --- a/tests/unit/test_source_bindings.py +++ b/tests/unit/test_source_bindings.py @@ -57,7 +57,6 @@ SourceBindingMatchMethod, SourceBindingMatchPlan, SourceBindingOrigin, - SourceBindingRuntimeContext, SourceBindingsConfig, SourceFilterClause, SourceFilterMatchType, @@ -1542,38 +1541,3 @@ def test_compiled_source_binding_plan_round_trips_through_pickle(): assert restored.metadata_rules == plan.metadata_rules assert restored.match_plan == plan.match_plan assert restored.binding_for_alias("OrigBlue") == plan.binding_for_alias("OrigBlue") - - -def test_source_binding_runtime_context_preserves_source_provenance_through_pickle(): - context = SourceBindingRuntimeContext( - step_input_files=("A01_s001_w1_z001_t001.tif",), - step_input_dir="/workspace", - step_input_source_paths={ - "A01_s001_w1_z001_t001.tif": "/real/source_C20_w1.tif", - }, - source_metadata_by_path={ - "A01_s001_w1_z001_t001.tif": { - "Compound": "DMSO", - ORIGINAL_SOURCE_METADATA_FIELD: { - "Plate": "PlateA", - "ChannelNumber": "1", - }, - }, - }, - pipeline_input_files=("/real/source_C20_w1.tif",), - pipeline_input_backend="disk", - ) - - restored = pickle.loads(pickle.dumps(context)) - - assert restored == context - assert dict(restored.step_input_source_paths) == { - "A01_s001_w1_z001_t001.tif": "/real/source_C20_w1.tif", - } - assert dict(restored.source_metadata_by_path["A01_s001_w1_z001_t001.tif"]) == { - "Compound": "DMSO", - ORIGINAL_SOURCE_METADATA_FIELD: { - "Plate": "PlateA", - "ChannelNumber": "1", - }, - } diff --git a/tests/unit/test_source_metadata_owner.py b/tests/unit/test_source_metadata_owner.py index f204c09c61..217278289d 100644 --- a/tests/unit/test_source_metadata_owner.py +++ b/tests/unit/test_source_metadata_owner.py @@ -19,7 +19,6 @@ from openhcs.core.source_binding_selection import DeclaredSourceMetadataRecord from openhcs.core.source_bindings import ( SOURCE_BINDING_ALIAS_METADATA_FIELD, - SourceBindingRuntimeContext, ) from openhcs.core.source_image_provenance import ( SourceImageIdentity, @@ -508,9 +507,6 @@ def test_mapping_and_ordered_record_equality_namespaces_stay_separate(): lambda metadata: VirtualWorkspaceSourceProjection({}, {"plane.tif": metadata}), lambda metadata: SourceImageProvenancePlaneRecord(component_metadata=metadata), lambda metadata: SourcePlaneIndexedMetadata(metadata, 0, 1), - lambda metadata: SourceBindingRuntimeContext( - source_metadata_by_path={"plane.tif": metadata} - ), lambda metadata: SourcePlaneProjection( address=OpenHCSPlaneAddress.from_values( well="A01", site="1", channel="1", z_index="1", timepoint="1" From 8dedb07c7838ad4693c057a75930b159c48cd558 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 18:43:42 -0400 Subject: [PATCH 162/235] test(runtime): migrate checkpoint controls to pair-only cohort fixture --- tests/unit/test_loaded_pattern_group_ownership.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/unit/test_loaded_pattern_group_ownership.py b/tests/unit/test_loaded_pattern_group_ownership.py index 067097b50e..2019672a2e 100644 --- a/tests/unit/test_loaded_pattern_group_ownership.py +++ b/tests/unit/test_loaded_pattern_group_ownership.py @@ -392,7 +392,7 @@ def test_checkpoint_demand_preserves_dependent_group_admission(pattern_kind): from openhcs.constants.constants import Backend from openhcs.core.step_dependencies import StepInputDependency - request, _, _ = _stored_primary_fixture() + request, _ = _stored_primary_fixture() source = replace(request.execution_plan, step_index=0, step_scope_id="source", write_backend=Backend.MEMORY.value) consumer = replace( source, step_index=1, step_scope_id="consumer", @@ -420,7 +420,7 @@ def test_checkpoint_demand_propagates_only_through_preserved_outputs(): from openhcs.constants.constants import Backend from openhcs.core.step_dependencies import StepInputDependency - request, _, _ = _stored_primary_fixture() + request, _ = _stored_primary_fixture() source = replace(request.execution_plan, step_index=0, step_scope_id="source", write_backend=Backend.MEMORY.value) consumer = replace( source, step_index=1, step_scope_id="consumer", visualize=True, @@ -429,7 +429,7 @@ def test_checkpoint_demand_propagates_only_through_preserved_outputs(): plans = {0: source, 1: consumer} assert consumer.requires_main_flow_checkpoint(plans) assert not source.requires_main_flow_checkpoint(plans) - preserving, _, _ = _stored_primary_fixture(preserves_main_flow=True) + preserving, _ = _stored_primary_fixture(preserves_main_flow=True) consumer.compiled_function_pattern = preserving.execution_plan.compiled_function_pattern assert source.requires_main_flow_checkpoint(plans) @@ -438,7 +438,7 @@ def test_checkpoint_demand_terminates_on_preserving_cycles_and_self_producers(): from openhcs.constants.constants import Backend from openhcs.core.step_dependencies import StepInputDependency - request, _, _ = _stored_primary_fixture(preserves_main_flow=True) + request, _ = _stored_primary_fixture(preserves_main_flow=True) first = replace( request.execution_plan, step_index=1, step_scope_id="first", write_backend=Backend.MEMORY.value, main_input_dependency=StepInputDependency.step_output(source_step_index=2, source_step_scope_id="second"), From 581ae6b9cddd43dc6f52d98845c35edd09de6853 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 18:47:54 -0400 Subject: [PATCH 163/235] perf(bench): sample descendants through exact process identity Refs OpenHCSDev/ZMQRuntime#15. Preserve genuine child RSS and limit callbacks without repeated host-wide process scans. --- benchmark/metrics/memory.py | 6 ++++- tests/unit/test_metrics_memory.py | 40 +++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/benchmark/metrics/memory.py b/benchmark/metrics/memory.py index 12aacb17f1..ebcb728d71 100644 --- a/benchmark/metrics/memory.py +++ b/benchmark/metrics/memory.py @@ -8,6 +8,7 @@ from openhcs.core.alias_property import AliasProperty import psutil +from zmqruntime.messages import ProcessIdentity from benchmark.contracts.metric import MetricCollector @@ -42,6 +43,9 @@ def __init__( self._peak_rss = 0 self._thread: threading.Thread | None = None self._process = psutil.Process() + self._process_identity = ProcessIdentity( + self._process.pid, self._process.create_time() + ) self._started = False self._sampling_error: Exception | None = None self._limit_exceeded = False @@ -101,7 +105,7 @@ def _sample_process_tree_rss(self) -> tuple[int, tuple[psutil.Process, ...]]: if not self.include_children: return rss, () try: - children = tuple(self._process.children(recursive=True)) + children = self._process_identity.descendants() except psutil.NoSuchProcess as exc: self._sampling_error = exc self._running = False diff --git a/tests/unit/test_metrics_memory.py b/tests/unit/test_metrics_memory.py index cde163ef2f..9346a58703 100644 --- a/tests/unit/test_metrics_memory.py +++ b/tests/unit/test_metrics_memory.py @@ -1,11 +1,51 @@ from __future__ import annotations import time +import subprocess +import sys + +import psutil +import pytest from benchmark.metrics import memory as memory_module from benchmark.metrics.memory import MemoryMetric +@pytest.mark.skipif(sys.platform != "linux", reason="Linux descendant discovery") +def test_memory_sampler_retains_real_child_rss_and_limit_callback_without_host_scan( + monkeypatch, +) -> None: + child = subprocess.Popen( + [sys.executable, "-u", "-c", + "import sys; pixels = bytearray(32 * 1024 * 1024); print('ready'); sys.stdin.readline()"], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + text=True, + ) + try: + assert child.stdout.readline().strip() == "ready" + callbacks = [] + metric = MemoryMetric( + max_memory_mb=1, + on_limit_exceeded=lambda _peak, children: callbacks.append(children), + ) + parent_rss = metric._process.memory_info().rss + child_rss = psutil.Process(child.pid).memory_info().rss + + def reject_host_scan(*args, **kwargs): + pytest.fail("Memory sampling scanned every host process") + + monkeypatch.setattr(psutil.Process, "children", reject_host_scan) + rss, children = metric._sample_process_tree_rss() + assert child.pid in {process.pid for process in children} + assert rss >= parent_rss + child_rss - 1024 * 1024 + metric._peak_rss = rss + metric._enforce_limit(rss, children) + assert callbacks == [children] + finally: + child.communicate("stop\n", timeout=5) + + def test_memory_metric_reenforces_limit_while_rss_remains_high(monkeypatch) -> None: callbacks: list[float] = [] metric = MemoryMetric( From 8ff84f3af87c96232e6a581b3b9b5612f3decdff Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 18:50:37 -0400 Subject: [PATCH 164/235] build(runtime): use latest ZMQRuntime descendant observation and shutdown fixes --- external/zmqruntime | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/zmqruntime b/external/zmqruntime index 668edafcb5..f7b80c871e 160000 --- a/external/zmqruntime +++ b/external/zmqruntime @@ -1 +1 @@ -Subproject commit 668edafcb5ee530a163377c936431ae1e8334e85 +Subproject commit f7b80c871eb4773757368bb21f108920bf461881 From fc7cb5497b4be0fd49ad46337453d6f0ab10ad63 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 18:58:24 -0400 Subject: [PATCH 165/235] build(runtime): sync installed shared dependencies and pins to latest main --- external/PolyStore | 2 +- external/pycodify | 2 +- external/pyqt-reactive | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/external/PolyStore b/external/PolyStore index 84f322e468..fc8aa56f30 160000 --- a/external/PolyStore +++ b/external/PolyStore @@ -1 +1 @@ -Subproject commit 84f322e46871de5ed47e7fd20976ad03e533c1c4 +Subproject commit fc8aa56f305eaaa8dd49a61aa02aa17f7d5d768b diff --git a/external/pycodify b/external/pycodify index 792b034133..77c27a0bed 160000 --- a/external/pycodify +++ b/external/pycodify @@ -1 +1 @@ -Subproject commit 792b03413331fdf107e369c924c257a2f0b16126 +Subproject commit 77c27a0bed97f70878e6eb26d4fe7cdfae4e8683 diff --git a/external/pyqt-reactive b/external/pyqt-reactive index ad4948775a..b64ed2eca9 160000 --- a/external/pyqt-reactive +++ b/external/pyqt-reactive @@ -1 +1 @@ -Subproject commit ad4948775ab81180a354d4b793d17ee5ddff3972 +Subproject commit b64ed2eca96fcdad29ba90beb545caa669a4d3d8 From 7d7e12493060ee1a85ada7faddfbbb798626524f Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 19:00:33 -0400 Subject: [PATCH 166/235] test(runtime): preserve prepared reference signature in batch transport control --- tests/unit/test_callable_contract.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_callable_contract.py b/tests/unit/test_callable_contract.py index 35591dd9e3..cc9e0f0dd9 100644 --- a/tests/unit/test_callable_contract.py +++ b/tests/unit/test_callable_contract.py @@ -553,7 +553,9 @@ def test_function_reference_preserves_batching_through_compiler_and_transport( contract = normalized.groups[0].items[0].contract restored = pickle.loads(pickle.dumps(contract)) assert restored.func == reference - assert restored.metadata == reference.metadata + expected_metadata = reference.metadata.with_prepared_signatures(func, func) + assert restored.metadata.canonical_signature == expected_metadata.canonical_signature + assert restored.metadata == expected_metadata assert restored.runtime_batch_executor( RuntimeBatchExecutionDomain.PURE_2D_SLICES ) is _batch_executor From 3601e34807453085a6e5cd8de00a7854371f6e5b Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 19:14:52 -0400 Subject: [PATCH 167/235] refactor(runtime): delete component invocation projection and duplicate declaration clones --- openhcs/core/function_patterns.py | 113 +++++++----------- openhcs/core/steps/function_runtime.py | 19 +-- tests/unit/test_function_patterns.py | 64 ++++++++-- .../unit/test_path_planner_materialization.py | 2 +- tests/unit/test_runtime_output_matching.py | 12 +- 5 files changed, 112 insertions(+), 98 deletions(-) diff --git a/openhcs/core/function_patterns.py b/openhcs/core/function_patterns.py index eff68598ae..3842a49a6c 100644 --- a/openhcs/core/function_patterns.py +++ b/openhcs/core/function_patterns.py @@ -466,24 +466,12 @@ def kwargs_dict(self) -> dict: """Return user-authored callable kwargs as a runtime dict.""" return dict(self.kwargs) - def for_runtime_outputs( - self, - *, - output_plans: Sequence[ArtifactOutputPlan], - ) -> "CompiledFunctionInvocation": - """Select component-scoped storage plans without changing the callable ABI.""" - - return replace( - self, - artifact_output_plans=tuple(output_plans), - ) - - def for_component_execution( + def output_plans_for_component( self, execution_scope: ComponentGroupScope, component_key: str | None, - ) -> ComponentFunctionInvocationProjection | None: - """Project this invocation and its exact edges through output lineage.""" + ) -> tuple[ArtifactOutputPlan, ...] | None: + """Select output lineage, or reject an inactive adapter invocation.""" active_outputs = [] for output_plan in self.artifact_output_plans: @@ -505,6 +493,13 @@ def for_component_execution( active_outputs.append(projected) if self.adapter_records_artifact_outputs and not active_outputs: return None + return tuple(active_outputs) + + def input_edges_for_outputs( + self, + active_outputs: Sequence[ArtifactOutputPlan], + ) -> tuple[InvocationArtifactInputEdgePlan, ...]: + """Derive sparse occurrences while retaining their original ABI indices.""" compiled_group_scope_sources = frozenset( source_ref for output_plan in self.artifact_output_plans @@ -534,17 +529,15 @@ def for_component_execution( ) ) ) - return ComponentFunctionInvocationProjection( - invocation=self, - artifact_output_plans=tuple(active_outputs), - artifact_input_edges=active_edges, - ) + return active_edges def select_inputs( self, input_plans: Mapping[ArtifactSpecRef, ArtifactInputPlan], + *, + active_output_plans: Sequence[ArtifactOutputPlan] | None = None, ) -> dict[InvocationArtifactInputProjectionKey, InvocationArtifactInputEdgePlan]: - """Return exact compiled input occurrences after storage validation.""" + """Validate the complete input owner before selecting active occurrences.""" ArtifactInputPlan.require_exact_map( input_plans, boundary=f"Compiled invocation {self.key!r} received input plan", @@ -557,7 +550,12 @@ def select_inputs( f"Compiled invocation {self.key!r} input plan " f"{edge.storage_plan.ref()!r} is unavailable in this step." ) - return {edge.key: edge for edge in self.artifact_input_edges} + active_edges = ( + self.artifact_input_edges + if active_output_plans is None + else self.input_edges_for_outputs(active_output_plans) + ) + return {edge.key: edge for edge in active_edges} def with_artifact_input_edges( self, @@ -570,14 +568,23 @@ def with_artifact_input_edges( def select_outputs( self, output_plans: Mapping[ArtifactSpecRef, ArtifactOutputPlan], + *, + compiled_output_plans: Sequence[ArtifactOutputPlan] | None = None, ) -> dict[ArtifactSpecRef, ArtifactOutputPlan]: - """Select exact runtime projections of this invocation's output plans.""" + """Select runtime storage against this invocation's active output owner.""" ArtifactOutputPlan.require_exact_map( output_plans, boundary=f"Compiled invocation {self.key!r} received output plan", ) + active_outputs = ( + self.artifact_output_plans + if compiled_output_plans is None + else compiled_output_plans + ) + if any(not isinstance(plan, ArtifactOutputPlan) for plan in active_outputs): + raise TypeError("Compiled artifact outputs must be ArtifactOutputPlan values.") selected: dict[ArtifactSpecRef, ArtifactOutputPlan] = {} - for compiled_plan in self.artifact_output_plans: + for compiled_plan in active_outputs: runtime_plan = output_plans.get(compiled_plan.ref()) if runtime_plan is None: raise ValueError( @@ -642,36 +649,6 @@ def proves_primary_image_carrier( return transition is not None and transition.proves(requirement) -@dataclass(frozen=True, slots=True) -class ComponentFunctionInvocationProjection: - """Sparse component view preserving the compiled invocation's ABI positions.""" - - invocation: CompiledFunctionInvocation - artifact_output_plans: tuple[ArtifactOutputPlan, ...] - artifact_input_edges: tuple[InvocationArtifactInputEdgePlan, ...] - - def select_inputs( - self, - input_plans: Mapping[ArtifactSpecRef, ArtifactInputPlan], - ) -> dict[InvocationArtifactInputProjectionKey, InvocationArtifactInputEdgePlan]: - """Select sparse active occurrences from the complete compiled mapping.""" - - compiled_inputs = self.invocation.select_inputs(input_plans) - return { - edge.key: compiled_inputs[edge.key] for edge in self.artifact_input_edges - } - - def select_outputs( - self, - output_plans: Mapping[ArtifactSpecRef, ArtifactOutputPlan], - ) -> dict[ArtifactSpecRef, ArtifactOutputPlan]: - """Select outputs through the complete invocation's compiled owner.""" - - return self.invocation.for_runtime_outputs( - output_plans=self.artifact_output_plans, - ).select_outputs(output_plans) - - @dataclass(frozen=True, slots=True) class CompiledFunctionGroup: """Compiled callable chain for one function-pattern group.""" @@ -703,10 +680,10 @@ def main_flow_input_refs_for_component( """Return main-flow refs active for one compiled component execution.""" invocations_and_edges = tuple( - (projected.invocation, projected.artifact_input_edges) + (invocation, invocation.input_edges_for_outputs(active_outputs)) for invocation in self.invocations if ( - projected := invocation.for_component_execution( + active_outputs := invocation.output_plans_for_component( execution_scope, component_key, ) @@ -754,39 +731,39 @@ def stored_primary_input_edges_for_component( consumed. Unknown raw adapters retain source-path transport. """ active = tuple( - projected + (invocation, invocation.input_edges_for_outputs(active_outputs)) for invocation in self.invocations if ( - projected := invocation.for_component_execution( + active_outputs := invocation.output_plans_for_component( execution_scope, component_key, ) ) is not None ) if not active or any( - projected.invocation.contract.accepts_implicit_main_flow_input - and not projected.invocation.adapter_manages_artifact_inputs - for projected in active + invocation.contract.accepts_implicit_main_flow_input + and not invocation.adapter_manages_artifact_inputs + for invocation, edges in active ): return None primary_edges = tuple( edge - for projected in active - for edge in projected.artifact_input_edges + for invocation, edges in active + for edge in edges if edge.main_flow_projection is not None and edge.spec.artifact_type.carries_source_image_context ) if not primary_edges: if any( - projected.invocation.contract.accepts_implicit_main_flow_input - for projected in active + invocation.contract.accepts_implicit_main_flow_input + for invocation, edges in active ): return None primary_edges = tuple( edge - for projected in active - for edge in projected.artifact_input_edges + for invocation, edges in active + for edge in edges if edge.spec.ref() - in projected.invocation.contract.group_scope_inputs.ref_set() + in invocation.contract.group_scope_inputs.ref_set() and edge.spec.artifact_type.carries_source_image_context ) if not primary_edges or any( diff --git a/openhcs/core/steps/function_runtime.py b/openhcs/core/steps/function_runtime.py index ccdef926c6..c66585c9fe 100644 --- a/openhcs/core/steps/function_runtime.py +++ b/openhcs/core/steps/function_runtime.py @@ -1214,14 +1214,16 @@ def select_for_invocation( declared_source_bindings: CompiledSourceBindingPlan = CompiledSourceBindingPlan(), active_source_bindings: CompiledSourceBindingPlan = CompiledSourceBindingPlan(), ) -> "ComponentArtifactPlans[InvocationArtifactInputProjectionKey, InvocationArtifactInputEdgePlan] | None": - projected = invocation.for_component_execution( + active_outputs = invocation.output_plans_for_component( execution_scope, component_key, ) - if projected is None: + if active_outputs is None: return None inputs = {} - for edge_key, edge in projected.select_inputs(self.inputs).items(): + for edge_key, edge in invocation.select_inputs( + self.inputs, active_output_plans=active_outputs, + ).items(): if ( edge.main_flow_projection is not None and declared_source_bindings.declares_artifact_ref(edge.spec.ref()) @@ -1239,7 +1241,9 @@ def select_for_invocation( inputs[edge_key] = edge return ComponentArtifactPlans( inputs=inputs, - outputs=projected.select_outputs(self.outputs), + outputs=invocation.select_outputs( + self.outputs, compiled_output_plans=active_outputs, + ), ) @classmethod @@ -2143,7 +2147,7 @@ def invoke( ) artifact_refs = ( *(edge.spec.ref() for edge in self.selected_artifact_input_edges), - *(plan.ref() for plan in self.invocation.artifact_output_plans), + *self.artifacts.outputs, ) raise type(exc)( f"{exc} Invocation boundary: step_index={cursor.step_index}; " @@ -2971,14 +2975,11 @@ def execute_chain( ) if artifacts is None: continue - runtime_invocation = invocation.for_runtime_outputs( - output_plans=tuple(artifacts.outputs.values()), - ) executor = FunctionCoreExecutor( main_data_arg=current_stack, source_memory_type=current_memory_type, group_data=group_data, - invocation=runtime_invocation, + invocation=invocation, artifacts=artifacts, group_key=group_key, plane_projection=RuntimePlaneProjection.stack( diff --git a/tests/unit/test_function_patterns.py b/tests/unit/test_function_patterns.py index 88b4104a3f..f3dc771ba0 100644 --- a/tests/unit/test_function_patterns.py +++ b/tests/unit/test_function_patterns.py @@ -752,12 +752,52 @@ def measure_channels(image, *, runtime): assert group.main_flow_input_refs_for_component(execution_scope, "2") == ( green.ref(), ) - green_projection = invocation.for_component_execution(execution_scope, "2") - assert green_projection is not None + green_outputs = invocation.output_plans_for_component(execution_scope, "2") + assert green_outputs is not None assert tuple( - edge.key.input_index for edge in green_projection.artifact_input_edges + edge.key.input_index + for edge in invocation.input_edges_for_outputs(green_outputs) ) == (1,) + storage_plans = { + source.ref(): ArtifactInputPlan( + name=source.name, + artifact_type=source.artifact_type, + path=f"/memory/{source.name}.pkl", + source_step_id=0, + ) + for source in (blue, green) + } + stored_invocation = invocation.with_artifact_input_edges(tuple( + replace( + edge, + storage_plan=storage_plans[edge.spec.ref()], + projection=ArtifactInputProjectionPlan( + invocation_scope=execution_scope, + producer_selection_scope=storage_plans[edge.spec.ref()].producer_group_scope(), + ), + ) + for edge in invocation.artifact_input_edges + )) + artifacts = ComponentArtifactPlans( + inputs=storage_plans, outputs={plan.ref(): plan for plan in green_outputs}, + ) + selected = artifacts.select_for_invocation( + stored_invocation, execution_scope=execution_scope, component_key="2", + ) + assert selected is not None + assert tuple(key.input_index for key in selected.inputs) == (1,) + assert tuple(selected.outputs.values()) == green_outputs + assert stored_invocation.artifact_output_plans == (output_plan,) + + # Sparse admission still validates the complete compiled producer owner at + # this epoch, including a producer whose output lineage is inactive here. + storage_plans.pop(blue.ref()) + with pytest.raises(ValueError, match="input plan.*unavailable"): + artifacts.select_for_invocation( + stored_invocation, execution_scope=execution_scope, component_key="2", + ) + def test_unscoped_active_output_retains_complete_compiled_invocation_inputs() -> None: source = ArtifactSpec.input("OrigBlue", ImageArtifactType) @@ -817,7 +857,7 @@ def measure_with_aggregate(image, *, runtime): ) invocation = invocation.with_artifact_input_edges((edge,)) - projection = invocation.for_component_execution( + active_outputs = invocation.output_plans_for_component( ComponentGroupScope.from_raw( ("1", "2"), component=AllComponents.CHANNEL, @@ -825,11 +865,9 @@ def measure_with_aggregate(image, *, runtime): "2", ) - assert projection is not None - assert tuple(plan.name for plan in projection.artifact_output_plans) == ( - "Aggregate", - ) - assert projection.artifact_input_edges == (edge,) + assert active_outputs is not None + assert tuple(plan.name for plan in active_outputs) == ("Aggregate",) + assert invocation.input_edges_for_outputs(active_outputs) == (edge,) def test_shared_output_plan_uses_each_invocation_declared_group_lineage() -> None: @@ -890,10 +928,10 @@ def measure_green(image, *, runtime): component=AllComponents.CHANNEL, ) - assert blue_invocation.for_component_execution(execution_scope, "1") is not None - assert blue_invocation.for_component_execution(execution_scope, "2") is None - assert green_invocation.for_component_execution(execution_scope, "1") is None - assert green_invocation.for_component_execution(execution_scope, "2") is not None + assert blue_invocation.output_plans_for_component(execution_scope, "1") is not None + assert blue_invocation.output_plans_for_component(execution_scope, "2") is None + assert green_invocation.output_plans_for_component(execution_scope, "1") is None + assert green_invocation.output_plans_for_component(execution_scope, "2") is not None def test_artifact_only_group_preserves_empty_explicit_main_flow_refs() -> None: diff --git a/tests/unit/test_path_planner_materialization.py b/tests/unit/test_path_planner_materialization.py index 8642a81bbd..95f207c078 100644 --- a/tests/unit/test_path_planner_materialization.py +++ b/tests/unit/test_path_planner_materialization.py @@ -2703,7 +2703,7 @@ def measure_green(image, *, runtime): channel: tuple( invocation.key.function_name for invocation in compiled.default_group.invocations - if invocation.for_component_execution(execution_scope, channel) is not None + if invocation.output_plans_for_component(execution_scope, channel) is not None ) for channel in execution_scope.keys } == { diff --git a/tests/unit/test_runtime_output_matching.py b/tests/unit/test_runtime_output_matching.py index 968ecb23ec..bb5cf64e9a 100644 --- a/tests/unit/test_runtime_output_matching.py +++ b/tests/unit/test_runtime_output_matching.py @@ -194,9 +194,7 @@ def test_runtime_invocation_selects_storage_without_truncating_callable_abi() -> ), artifact_output_plans=(first_plan, second_plan), ) - runtime = compiled.for_runtime_outputs( - output_plans=(second_plan,), - ) + runtime_output_plans = (second_plan,) first_value = np.zeros((4, 5), dtype=np.float32) second_value = np.ones((4, 5), dtype=np.float32) returned_stack = AlignedImageStack( @@ -214,12 +212,12 @@ def test_runtime_invocation_selects_storage_without_truncating_callable_abi() -> ) resolved, matched = RuntimeReturnedOutputMatcher( - callable_contract=runtime.contract, + callable_contract=compiled.contract, returned_output=returned_stack, - ).resolve_plan_values(runtime.artifact_output_plans) + ).resolve_plan_values(runtime_output_plans) - assert runtime.contract.canonical_return_output_specs.specs == (first, second) - assert runtime.contract.artifact_inputs.specs == (first_input, second_input) + assert compiled.contract.canonical_return_output_specs.specs == (first, second) + assert compiled.contract.artifact_inputs.specs == (first_input, second_input) assert resolved == { first.ref(): first_value, second.ref(): second_value, From d7214684346024ec3e880f13216c6843f68c8170 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 19:51:58 -0400 Subject: [PATCH 168/235] Update PolyStore for declared TIFF pixel axes (PolyStore#23) --- external/PolyStore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/PolyStore b/external/PolyStore index fc8aa56f30..1d72165e06 160000 --- a/external/PolyStore +++ b/external/PolyStore @@ -1 +1 @@ -Subproject commit fc8aa56f305eaaa8dd49a61aa02aa17f7d5d768b +Subproject commit 1d72165e065b7f2d69d6dcbd8e8a1ef54f13b0d3 From b8867a2f985093587c7d8330d6bfe00873a28cb3 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 19:56:28 -0400 Subject: [PATCH 169/235] Preserve declared intrinsic volumes across storage and publication Refs #573. Derive whole-volume materialization from the nominal spatial domain while retaining runtime plane projection, exact producer scopes and independent pixel/mask buffers. Declare TIFF axes through the generic codec boundary. Validated with 99 joined controls, including actual saved-page and prepared CP resize behavior; end-to-end qualification remains pending. --- openhcs/core/aligned_image_payload.py | 21 +- openhcs/core/image_file_serialization.py | 83 ++++++- openhcs/core/runtime_adapters.py | 22 +- openhcs/core/runtime_image_values.py | 24 +- openhcs/core/source_bindings.py | 15 ++ openhcs/core/source_projection.py | 2 + openhcs/core/source_spatial_domain.py | 216 ++++++++++++++---- openhcs/core/steps/function_io.py | 12 +- .../core/steps/function_output_manifest.py | 23 ++ openhcs/core/steps/function_outputs.py | 71 ++++-- openhcs/core/steps/function_runtime.py | 34 ++- .../backends/cellprofiler/infrastructure.py | 4 + openhcs/processing/materialization/core.py | 65 +++++- ...lprofiler_geometry_prepared_payload_abi.py | 35 ++- .../unit/test_cellprofiler_source_bindings.py | 3 + tests/unit/test_function_outputs.py | 110 ++++++++- ...test_function_runtime_source_projection.py | 39 +++- tests/unit/test_image_file_serialization.py | 28 +++ tests/unit/test_materialization_core.py | 134 +++++++++++ tests/unit/test_pipeline_document.py | 29 +++ 20 files changed, 859 insertions(+), 111 deletions(-) diff --git a/openhcs/core/aligned_image_payload.py b/openhcs/core/aligned_image_payload.py index 1445ab0a54..df2aa90988 100644 --- a/openhcs/core/aligned_image_payload.py +++ b/openhcs/core/aligned_image_payload.py @@ -52,7 +52,6 @@ SourceSpatialDomainAdapter, ) - if TYPE_CHECKING: from openhcs.core.compiled_step_plan import CompiledStepPlan from openhcs.core.steps.function_output_manifest import ProducedOutputSemantics @@ -447,11 +446,14 @@ def from_loaded_images( workspace_source_lookups: Sequence[VirtualWorkspacePathLookup], ) -> RuntimeArrayData: """Compose a selected admissible input cohort in its declared image domain.""" - if ( - producer_records - and len(producer_records) == 1 - and producer_records[0].main_flow_plane_axis - is image_payload_metadata(payloads[0]).plane_axis + if len(payloads) == 1 and ( + image_payload_metadata(payloads[0]).persists_whole_image() + or ( + producer_records + and len(producer_records) == 1 + and producer_records[0].main_flow_plane_axis + is image_payload_metadata(payloads[0]).plane_axis + ) ): main_data_stack = ImagePayloadStackComposition.copy_whole_image( payloads[0], @@ -1194,8 +1196,11 @@ def projected_output_slices( """Project each output once together with its declaration-owned context.""" contexts = self.slice_contexts or (None,) * len(self.slices) for payload, context in zip(self.slices, contexts, strict=True): - for output_slice in payload_slices_for_alignment(payload): - yield output_slice, context + if image_payload_metadata(payload).persists_whole_image(): + yield payload, context + else: + for output_slice in payload_slices_for_alignment(payload): + yield output_slice, context def copy_projected_output_stack( self, diff --git a/openhcs/core/image_file_serialization.py b/openhcs/core/image_file_serialization.py index 0b2593fa72..d330a45be6 100644 --- a/openhcs/core/image_file_serialization.py +++ b/openhcs/core/image_file_serialization.py @@ -4,13 +4,14 @@ import logging from abc import ABC, abstractmethod -from dataclasses import dataclass +from dataclasses import dataclass, replace from functools import lru_cache from pathlib import Path from typing import Any, Callable, ClassVar, Sequence import numpy as np from metaclass_registry import AutoRegisterMeta +from polystore.config import TiffConfig, TiffPhotometric, TiffPlanarConfig from openhcs.constants.constants import FileFormat from openhcs.core.registry_strategies import NominalTypeStrategyFamilyMixin @@ -251,6 +252,42 @@ def require_path(cls, path: str | Path) -> "ImageFileFormat": def prepare(self, payload: Any) -> Any: """Return a payload suitable for this file format.""" + def storage_config( + self, payload: Any, configured: TiffConfig | None + ) -> TiffConfig | None: + """Non-TIFF formats retain their existing backend writer configuration.""" + return None + + @classmethod + def storage_write_batches( + cls, + payloads: Sequence[Any], + paths: Sequence[str | Path], + configured: TiffConfig | None, + ) -> tuple[tuple[tuple[int, ...], TiffConfig | None], ...]: + """Batch compatible declared image codecs without changing output order.""" + if len(payloads) != len(paths): + raise ValueError("Image storage payload/path cardinality mismatch.") + batches = [] + for index, (payload, path) in enumerate(zip(payloads, paths, strict=True)): + config = ( + cls.require_path(path).storage_config( + payload, + ( + configured + if configured is not None and configured.applies_to_path(path) + else None + ), + ) + if cls.is_image_path(path) + else None + ) + if batches and batches[-1][1] == config: + batches[-1][0].append(index) + else: + batches.append(([index], config)) + return tuple((tuple(indices), config) for indices, config in batches) + def read(self, path: str | Path) -> np.ndarray: """Read pixels through this exact registered image-file format.""" import imageio.v3 as iio @@ -413,6 +450,50 @@ def preserves_pixel_values(self, source_dtype: Any) -> bool: def prepare(self, payload: Any) -> Any: return image_payload_data(payload) + def storage_config( + self, payload: Any, configured: TiffConfig | None + ) -> TiffConfig | None: + metadata = image_payload_metadata(payload) + if not metadata.persists_whole_image(): + return configured + data = image_payload_data(payload) + channel_axis = metadata.normalized_source_channel_axis(data) + axes = list("ZYX") + planarconfig = None + photometric = TiffPhotometric.MINISBLACK + if channel_axis is not None: + if channel_axis < 0: + channel_axis += data.ndim + if channel_axis not in (data.ndim - 1, data.ndim - 3): + raise ValueError( + "Intrinsic TIFF channels must be contiguous or planar before Y/X." + ) + axes.insert(channel_axis, "S") + planarconfig = ( + TiffPlanarConfig.CONTIG + if channel_axis == data.ndim - 1 + else TiffPlanarConfig.SEPARATE + ) + photometric = TiffPhotometric.RGB + if len(axes) != data.ndim: + raise ValueError( + "Intrinsic TIFF pixels must retain exactly their declared Z/Y/X and channel axes." + ) + return replace( + configured if configured is not None else TiffConfig(), + photometric=photometric, + axes="".join(axes), + planarconfig=planarconfig, + ) + + def write(self, path: str | Path, payload: Any) -> None: + config = self.storage_config(payload, None) + if config is None: + return super().write(path, payload) + import tifffile + + tifffile.imwrite(path, self.prepare(payload), **config.tifffile_write_kwargs()) + def requires_plane_store_decoder(self, path: Path) -> bool: import tifffile diff --git a/openhcs/core/runtime_adapters.py b/openhcs/core/runtime_adapters.py index 2de8fa2244..c7770d7aa1 100644 --- a/openhcs/core/runtime_adapters.py +++ b/openhcs/core/runtime_adapters.py @@ -347,13 +347,23 @@ def source_artifact_payload(self, ref: ArtifactSpecRef) -> object: ), ) ) - return stack_image_payloads( - projected_payloads, - metadata_mode=ImagePayloadMetadataCompositionMode.for_plane_axis( - RuntimePlaneAxis.RUNTIME_SLICE - ), + payload = ( + projected_payloads[0] + if len(projected_payloads) == 1 + and image_payload_metadata(projected_payloads[0]).persists_whole_image() + else stack_image_payloads( + projected_payloads, + metadata_mode=ImagePayloadMetadataCompositionMode.for_plane_axis( + RuntimePlaneAxis.RUNTIME_SLICE + ), + ) ) - + metadata = image_payload_metadata(payload) + domain = self.source_binding_plan.source_spatial_domain.admit_source_cohort( + metadata.source_spatial_domain, + depth=len(members), + ) + return metadata.replace_fields(source_spatial_domain=domain).attach_to(payload) RuntimeAdapterFactory = Callable[[RuntimeAdapterRequest], object] diff --git a/openhcs/core/runtime_image_values.py b/openhcs/core/runtime_image_values.py index 661367cdef..2fe22fd97c 100644 --- a/openhcs/core/runtime_image_values.py +++ b/openhcs/core/runtime_image_values.py @@ -195,6 +195,10 @@ def from_mapping(cls, values: Mapping[str, object]) -> "ImagePayloadMetadata": decoded["source_provenance"] = SourceImageProvenance.from_mapping( decoded["source_provenance"] ) + if "source_spatial_domain" in decoded: + decoded["source_spatial_domain"] = SourceSpatialDomain.from_mapping( + decoded["source_spatial_domain"] + ) return dataclass_from_mapping(cls, decoded) def retained_plane_component_values( @@ -609,6 +613,13 @@ def without_source_channel_axis(self) -> "ImagePayloadMetadata": """Return metadata after an operation collapses the source channel axis.""" return self.replace_fields(source_channel_axis=None) + 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 + ) + def for_leading_source_plane(self, plane_index: int) -> "ImagePayloadMetadata": """Project metadata after explicitly removing its leading plane axis.""" return LeadingSourcePlaneMetadataProjection(self, plane_index).project() @@ -633,6 +644,10 @@ 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: + projected.source_spatial_domain = ( + projected.source_spatial_domain.for_intrinsic_plane() + ) projected.plane_axis = None projected.source_channel_axis = source_channel_axis projected.source_plane_intensity_scales = () @@ -1763,13 +1778,8 @@ def compose(self) -> ImagePayloadMetadata: unit_interval_intensity=self.composed_unit_interval_intensity( source_plane_metadata_records ), - source_spatial_domain=SourceSpatialDomain( - origin_yx=self.common_metadata_value( - metadata.spatial_origin_yx for metadata in metadata_by_payload - ), - source_shape_yx=self.common_metadata_value( - metadata.source_spatial_shape_yx for metadata in metadata_by_payload - ), + source_spatial_domain=SourceSpatialDomain.common_from_domains( + metadata.source_spatial_domain for metadata in metadata_by_payload ), source_voxel_spacing=common_source_voxel_spacing, physical_border_edges_yx=self.common_metadata_value( diff --git a/openhcs/core/source_bindings.py b/openhcs/core/source_bindings.py index 2307dc7740..1cf4eae158 100644 --- a/openhcs/core/source_bindings.py +++ b/openhcs/core/source_bindings.py @@ -47,6 +47,7 @@ source_metadata_scalar, ) from openhcs.core.xdg_paths import get_openhcs_cache_dir +from openhcs.core.source_spatial_domain import SourceSpatialDomain if TYPE_CHECKING: from openhcs.core.component_group_scope import RuntimeExecutionAxisScope @@ -1231,6 +1232,7 @@ class SourceBindingDeclarationsMixin: bindings: tuple[NamedSourceBinding, ...] | None source_stack_components: tuple[AllComponents, ...] + source_spatial_domain: SourceSpatialDomain @property def binding_declarations(self) -> tuple[NamedSourceBinding, ...]: @@ -1790,6 +1792,11 @@ class SourceBindingsConfig(SourceBindingDeclarationsMixin, _SourceBindingPlanBas source_stack_components: tuple[AllComponents, ...] = () """Ordered plate components that form one logical source image stack.""" + source_spatial_domain: SourceSpatialDomain = field( + default_factory=SourceSpatialDomain + ) + """Declared intrinsic image dimensions, independent of stack transport.""" + grouping_metadata_fields: tuple[str, ...] = () """Metadata field names used to partition matched sources into execution groups.""" @@ -2207,6 +2214,9 @@ class CompiledSourceBindingPlan(SourceBindingDeclarationsMixin, _SourceBindingPl registry_key: ClassVar[str] = "compiled" bindings: tuple[NamedSourceBinding, ...] = () source_stack_components: tuple[AllComponents, ...] = () + source_spatial_domain: SourceSpatialDomain = field( + default_factory=SourceSpatialDomain + ) @classmethod def empty(cls) -> CompiledSourceBindingPlan: @@ -2232,6 +2242,7 @@ def from_config( realized_source_metadata ), source_stack_components=config.source_stack_components, + source_spatial_domain=config.source_spatial_domain, ) def __post_init__(self) -> None: @@ -2294,6 +2305,7 @@ def __reduce__( SourceBindingMatchPlan | None, tuple[FieldSpec, ...], tuple[AllComponents, ...], + SourceSpatialDomain, ], ]: """Serialize source-binding plan state for multiprocessing.""" @@ -2305,6 +2317,7 @@ def __reduce__( self.match_plan, self.metadata_fields, self.source_stack_components, + self.source_spatial_domain, ), ) @@ -2326,6 +2339,7 @@ def _from_pickled_state( match_plan: SourceBindingMatchPlan | None, metadata_fields: tuple[FieldSpec, ...], source_stack_components: tuple[AllComponents, ...], + source_spatial_domain: SourceSpatialDomain, ) -> CompiledSourceBindingPlan: return cls( bindings=bindings, @@ -2333,6 +2347,7 @@ def _from_pickled_state( match_plan=match_plan, metadata_fields=metadata_fields, source_stack_components=source_stack_components, + source_spatial_domain=source_spatial_domain, ) diff --git a/openhcs/core/source_projection.py b/openhcs/core/source_projection.py index 5ec10a7be1..1349e94927 100644 --- a/openhcs/core/source_projection.py +++ b/openhcs/core/source_projection.py @@ -835,6 +835,8 @@ def scalar_address_for_image_metadata( ) -> OpenHCSPlaneAddress | None: """Return a plane address only when the whole artifact is scalar.""" + if metadata.persists_whole_image(): + return None return OpenHCSPlaneAddress.from_complete_source_metadata( metadata.source_component_metadata ) diff --git a/openhcs/core/source_spatial_domain.py b/openhcs/core/source_spatial_domain.py index c61d8b41aa..8723c6a4ee 100644 --- a/openhcs/core/source_spatial_domain.py +++ b/openhcs/core/source_spatial_domain.py @@ -4,7 +4,7 @@ from abc import ABC, abstractmethod from collections.abc import Callable, Iterable, Mapping, Sequence -from dataclasses import dataclass, field +from dataclasses import dataclass, field, fields, replace from typing import Any, ClassVar, Generic, Self, TypeVar from metaclass_registry import AutoRegisterMeta, RegistryFamily, RegistryKeyAttribute @@ -18,6 +18,7 @@ ) from openhcs.core.registry_strategies import NominalTypeKeyedStrategyMixin +from openhcs.serialization.json import to_jsonable SourceSpatialAliasValueT = TypeVar("SourceSpatialAliasValueT") @@ -61,7 +62,7 @@ def as_tuple(self) -> tuple[int, int]: @dataclass(frozen=True, slots=True) -class SourceSpatialDomain: +class SourceSpatialDomain(metaclass=AutoRegisterMeta): """Dense XY placement contract for a source-image coordinate domain.""" origin_yx: tuple[int, int] | None = None @@ -69,6 +70,54 @@ class SourceSpatialDomain: fill_value: Any = 0 value_name: str = "Dense array" + __registry_family__ = RegistryFamily("spatial_rank") + spatial_rank: ClassVar[int] = 2 + + @classmethod + def from_mapping(cls, values: Mapping[str, Any]) -> "SourceSpatialDomain": + """Decode the declared spatial owner, retaining its intrinsic dimensions.""" + payload = dict(values) + dimensions = payload.pop("spatial_dimensions", 2) + owner = cls.__registry__.get(dimensions) + if owner is None: + raise ValueError(f"Unsupported source spatial dimensions {dimensions!r}.") + return owner(**payload).normalized() + + def to_mapping(self) -> dict[str, Any]: + """Serialize placement fields with the nominal spatial declaration.""" + return { + **{ + item.name: to_jsonable(getattr(self, item.name)) + for item in fields(self) + }, + "spatial_dimensions": self.spatial_rank, + } + + def for_intrinsic_plane(self) -> "SourceSpatialDomain": + """An ordinary source plane retains its XY placement owner.""" + return self + + def admit_source_cohort( + self, + domain: "SourceSpatialDomain", + *, + depth: int, + ) -> "SourceSpatialDomain": + """Ordinary source stacks retain their existing spatial domain.""" + return domain + + def intrinsic_plane_count( + self, + shape: Sequence[int], + fallback: int, + ) -> int: + """Return the execution plane count independently of storage layout.""" + return fallback + + def persists_whole_image(self) -> bool: + """An ordinary XY domain does not consume a runtime-slice axis.""" + return False + @property def has_values(self) -> bool: """Return whether this domain carries source-image placement metadata.""" @@ -104,7 +153,9 @@ def to_viewer_wire_mapping(self) -> dict[str, ViewerWireValue]: def with_missing_from(self, fallback: "SourceSpatialDomain") -> Self: """Fill missing spatial placement values from another source domain.""" - return type(self)( + owner = fallback if fallback.spatial_rank > self.spatial_rank else self + return replace( + owner, origin_yx=( self.origin_yx if self.origin_yx is not None else fallback.origin_yx ), @@ -147,7 +198,7 @@ def with_native_image_context( try: candidate.require_image_window(image_shape_yx) except ValueError: - return native_domain + return native_domain.with_missing_from(self) return candidate @classmethod @@ -163,6 +214,18 @@ def common_from_domains( domains_tuple = tuple(domains) if not domains_tuple: return cls(fill_value=fill_value, value_name=value_name) + owner = type(domains_tuple[0]) + if ( + cls is SourceSpatialDomain + and owner is not cls + and all(type(domain) is owner for domain in domains_tuple) + ): + return owner.common_from_domains( + domains_tuple, + expand_varying_domains=expand_varying_domains, + fill_value=fill_value, + value_name=value_name, + ) source_shape = CommonRuntimeValue.from_values( domain.source_shape_yx for domain in domains_tuple @@ -201,42 +264,22 @@ def domains_have_varying_complete_placement( def with_origin_yx(self, origin_yx: tuple[int, int] | None) -> Self: """Return this domain with a replacement source-image origin.""" - return type(self)( - origin_yx=origin_yx, - source_shape_yx=self.source_shape_yx, - fill_value=self.fill_value, - value_name=self.value_name, - ) + return replace(self, origin_yx=origin_yx) def with_source_shape_yx( self, source_shape_yx: tuple[int, int] | None, ) -> Self: """Return this domain with a replacement full source-image shape.""" - return type(self)( - origin_yx=self.origin_yx, - source_shape_yx=source_shape_yx, - fill_value=self.fill_value, - value_name=self.value_name, - ) + return replace(self, source_shape_yx=source_shape_yx) def with_value_name(self, value_name: str) -> Self: """Return this domain with a consumer-specific value label.""" - return type(self)( - origin_yx=self.origin_yx, - source_shape_yx=self.source_shape_yx, - fill_value=self.fill_value, - value_name=value_name, - ) + return replace(self, value_name=value_name) def with_fill_value(self, fill_value: Any) -> Self: """Return this domain with a replacement dense materialization fill.""" - return type(self)( - origin_yx=self.origin_yx, - source_shape_yx=self.source_shape_yx, - fill_value=fill_value, - value_name=self.value_name, - ) + return replace(self, fill_value=fill_value) def normalized(self) -> Self: """Return this domain with canonical tuple metadata values.""" @@ -253,11 +296,10 @@ def normalized(self) -> Self: "source_spatial_shape_yx", ) ) - return type(self)( + return replace( + self, origin_yx=origin_yx, source_shape_yx=source_shape_yx, - fill_value=self.fill_value, - value_name=self.value_name, ) def physical_border_edges_for_shape( @@ -292,14 +334,13 @@ def with_spatial_crop( source_shape = ( self.source_shape_yx if self.source_shape_yx is not None else input_shape ) - return type(self)( + return replace( + self, origin_yx=( int(parent_origin[0]) + int(offset_yx[0]), int(parent_origin[1]) + int(offset_yx[1]), ), source_shape_yx=source_shape, - fill_value=self.fill_value, - value_name=self.value_name, ) def with_spatial_resize( @@ -309,11 +350,10 @@ def with_spatial_resize( """Return the local coordinate domain established by a spatial resize.""" output_shape = self._shape_yx(output_shape_yx, "output_shape_yx") - return type(self)( + return replace( + self, origin_yx=(0, 0), source_shape_yx=output_shape, - fill_value=self.fill_value, - value_name=self.value_name, ) def as_materialized_source_domain( @@ -326,11 +366,10 @@ def as_materialized_source_domain( raise ValueError( f"{self.value_name} source-domain materialization requires shape." ) - return type(self)( + return replace( + self, origin_yx=(0, 0), source_shape_yx=source_shape, - fill_value=self.fill_value, - value_name=self.value_name, ) @staticmethod @@ -354,6 +393,91 @@ def materialize( ) +@dataclass(frozen=True, slots=True) +class VolumeSourceSpatialDomain(SourceSpatialDomain): + """Intrinsic Z/Y/X image placement, independent of runtime slice transport.""" + + spatial_rank: ClassVar[int] = 3 + source_depth: int | None = None + origin_z: int | None = 0 + + @property + def has_values(self) -> bool: + """The intrinsic Z dimension remains semantic without XY placement.""" + return True + + def normalized(self) -> Self: + domain = super(VolumeSourceSpatialDomain, self).normalized() + if domain.source_depth is not None and domain.source_depth <= 0: + raise ValueError("Intrinsic source volume depth must be positive.") + if domain.origin_z is not None and domain.origin_z < 0: + raise ValueError("Intrinsic source volume origin must be nonnegative.") + return domain + + def admit_source_cohort( + self, + domain: SourceSpatialDomain, + *, + depth: int, + ) -> "VolumeSourceSpatialDomain": + if domain.spatial_rank >= self.spatial_rank: + return domain + return replace( + self, + origin_yx=domain.origin_yx, + source_shape_yx=domain.source_shape_yx, + fill_value=domain.fill_value, + value_name=domain.value_name, + source_depth=depth, + ).normalized() + + def intrinsic_plane_count(self, shape: Sequence[int], fallback: int) -> int: + if len(shape) < 3 or shape[0] <= 0: + raise ValueError("An intrinsic volume must retain its Z pixel axis.") + return int(shape[0]) + + def persists_whole_image(self) -> bool: + return True + + def for_intrinsic_plane(self) -> SourceSpatialDomain: + """Explicit Z selection consumes the intrinsic volume dimension.""" + return SourceSpatialDomain( + origin_yx=self.origin_yx, + source_shape_yx=self.source_shape_yx, + fill_value=self.fill_value, + value_name=self.value_name, + ) + + def require_image_window(self, image_shape_yx: Sequence[int]) -> None: + # Source depth describes original placement, independently of current + # resampled pixels. XY placement retains its existing crop validation. + shape = tuple(image_shape_yx) + super(VolumeSourceSpatialDomain, self).require_image_window(shape[-2:]) + + @classmethod + def common_from_domains( + cls, + domains: Iterable[SourceSpatialDomain], + **kwargs: Any, + ) -> "VolumeSourceSpatialDomain": + values = tuple(domains) + domain = super(VolumeSourceSpatialDomain, cls).common_from_domains( + values, **kwargs + ) + return replace( + domain, + source_depth=CommonRuntimeValue.from_values( + value.source_depth for value in values + ).single, + origin_z=CommonRuntimeValue.from_values( + value.origin_z for value in values + ).single, + ) + + +to_jsonable.register(SourceSpatialDomain, SourceSpatialDomain.to_mapping) + + @dataclass(frozen=True, slots=True) class SourceSpatialDomainAlias(Generic[SourceSpatialAliasValueT]): """Descriptor for scalar aliases backed by a source-spatial domain.""" @@ -540,10 +664,14 @@ def common_source_domain( source_shape = cls.common_source_shape_yx(adapters) if source_shape is None: return None - return SourceSpatialDomain( - source_shape_yx=source_shape, - fill_value=fill_value, - value_name=value_name, + return ( + SourceSpatialDomain.common_from_domains( + (adapter.domain for adapter in adapters), + fill_value=fill_value, + value_name=value_name, + ) + .with_origin_yx(None) + .with_source_shape_yx(source_shape) ) @classmethod diff --git a/openhcs/core/steps/function_io.py b/openhcs/core/steps/function_io.py index d2e5e5e38b..f1510b60f2 100644 --- a/openhcs/core/steps/function_io.py +++ b/openhcs/core/steps/function_io.py @@ -10,7 +10,6 @@ from typing import TYPE_CHECKING, Callable, ClassVar, Mapping, Sequence, TypeAlias from metaclass_registry import AutoRegisterMeta -from polystore.config import tiff_write_batches from polystore.zarr_batch import ZarrBatchAxis, ZarrBatchAxisRole, ZarrBatchLayout from openhcs.constants.constants import ( @@ -19,7 +18,10 @@ Backend, ) from openhcs.core.components.parser_metaprogramming import FilenameParseResult -from openhcs.core.image_file_serialization import prepare_disk_image_payloads +from openhcs.core.image_file_serialization import ( + ImageFileFormat, + prepare_disk_image_payloads, +) from openhcs.core.registry_strategies import EnumKeyedStrategyMixin from openhcs.core.runtime_array_values import RuntimeArrayData from openhcs.core.runtime_image_loading import ImagePayloadSourceMetadataContext @@ -384,7 +386,11 @@ def save_materialized_data( tiff_config = ( context.tiff_config if materialized_backend == Backend.DISK.value else None ) - for indices, batch_config in tiff_write_batches(materialized_paths, tiff_config): + for indices, batch_config in ImageFileFormat.storage_write_batches( + memory_data, + materialized_paths, + tiff_config, + ): filemanager.save_batch( [payloads[index] for index in indices], [materialized_paths[index] for index in indices], diff --git a/openhcs/core/steps/function_output_manifest.py b/openhcs/core/steps/function_output_manifest.py index a4aedb8c03..9ceb44b9c2 100644 --- a/openhcs/core/steps/function_output_manifest.py +++ b/openhcs/core/steps/function_output_manifest.py @@ -32,6 +32,8 @@ ) from openhcs.microscopes.microscope_interfaces import FilenameParser from openhcs.core.compiled_step_plan import CompiledStepPlan +from openhcs.core.component_group_scope import RuntimeExecutionAxisScope +from openhcs.constants import AllComponents @dataclass(frozen=True, slots=True) @@ -109,6 +111,27 @@ def source_metadata_for_projection( metadata.source_voxel_spacing.merge_into(source_metadata, path=destination) return source_metadata + def execution_scope(self, plan: CompiledStepPlan) -> RuntimeExecutionAxisScope: + """Retain the exact producer coordinates of a whole intrinsic image.""" + group_component = plan.execution_group_scope.component + group_value = ( + None + if group_component is None + else self.component_values.get(group_component.value) + ) + return RuntimeExecutionAxisScope.from_raw( + plan.axis_id, + component=group_component if group_value is not None else None, + value=group_value, + fixed_component_values=tuple( + (component, str(value)) + for component in AllComponents + if not component.is_multiprocessing_axis() + and component is not group_component + and (value := self.component_values.get(component.value)) is not None + ), + ) + def owns_persisted_artifact( self, output_plan: ArtifactOutputPlan, diff --git a/openhcs/core/steps/function_outputs.py b/openhcs/core/steps/function_outputs.py index 1e57e0ff4b..1692605457 100644 --- a/openhcs/core/steps/function_outputs.py +++ b/openhcs/core/steps/function_outputs.py @@ -192,10 +192,7 @@ def write_if_needed( produced_outputs = step_output_manifest(context).image_records_for(plan) if not produced_outputs: return - memory_paths = [ - record.memory_path(plan) - for record in produced_outputs - ] + memory_paths = [record.memory_path(plan) for record in produced_outputs] memory_data = context.filemanager.load_batch( memory_paths, Backend.MEMORY.value, @@ -210,18 +207,30 @@ def write_if_needed( plan.output_dir, plan.write_backend, ) - context.filemanager.save_batch( - cls.payloads(memory_data, output_paths, plan), - output_paths, - plan.write_backend, - chunk_name=plan.axis_id, - zarr_config=plan.zarr_config, - batch_layout=zarr_output_batch_layout(produced_outputs), - row=row, - col=col, - parser_name=parser_context.parser_name, - microscope_type=parser_context.microscope_type, + payloads = cls.payloads(memory_data, output_paths, plan) + batches = ( + ImageFileFormat.storage_write_batches( + memory_data, output_paths, context.tiff_config + ) + if plan.write_backend == Backend.DISK.value + else ((tuple(range(len(output_paths))), None),) ) + for indices, config in batches: + context.filemanager.save_batch( + [payloads[index] for index in indices], + [output_paths[index] for index in indices], + plan.write_backend, + chunk_name=plan.axis_id, + zarr_config=plan.zarr_config, + batch_layout=zarr_output_batch_layout( + tuple(produced_outputs[index] for index in indices) + ), + row=row, + col=col, + parser_name=parser_context.parser_name, + microscope_type=parser_context.microscope_type, + **({"tiff_config": config} if config is not None else {}), + ) @staticmethod def payloads( @@ -811,6 +820,25 @@ def produced_projection_entries( metadata, destination ) address = record.filename_address + if metadata.persists_whole_image(): + projection_paths.append( + ( + SourceArtifactProjection( + address=None, + ref=SourcePixelRef(self.backend, virtual_path), + source_alias=record.producer_identity.output_key, + artifact_kind=ImageArtifactType, + source_metadata=source_metadata, + image_metadata=metadata, + execution_scope=record.execution_scope(plan), + ), + virtual_path, + ) + ) + produced_projections[Path(destination)] = ( + record, projection_paths[-1][0] + ) + continue if address not in declared_addresses: declared_addresses.add(address) projection_paths.append( @@ -902,9 +930,20 @@ def project_runtime_artifacts( # A collapsed image retains filename coordinates while # its semantic source address can be absent. Validate # the occurrence against its producer's filename view. + whole_image = metadata.persists_whole_image() + expected_address = ( + None if whole_image else record.filename_address + ) if ( - projection.address != record.filename_address + projection.address != expected_address or projection.image_metadata != metadata + or ( + whole_image + and projection.execution_scope + != record.execution_scope( + materialization.output_plan + ) + ) ): raise ValueError( "Conflicting metadata for persisted image " diff --git a/openhcs/core/steps/function_runtime.py b/openhcs/core/steps/function_runtime.py index c66585c9fe..c163985948 100644 --- a/openhcs/core/steps/function_runtime.py +++ b/openhcs/core/steps/function_runtime.py @@ -439,7 +439,6 @@ def contextualize_from_projector( ) - class ImageFunctionOutputContextStrategy(ProjectedFunctionOutputContextStrategy): """Preserve source-image metadata for image outputs derived from the main input.""" @@ -1393,10 +1392,17 @@ def load_input_stack( return runtime._load_input_stack() def loaded_plane_count( - self, matching_files: Sequence[str], payload: RuntimeArrayData, + self, + matching_files: Sequence[str], + payload: RuntimeArrayData, ) -> int: """The source path roster declares the initial runtime plane count.""" - return len(matching_files) + return image_payload_metadata( + payload + ).source_spatial_domain.intrinsic_plane_count( + np.shape(image_payload_data(payload)), + len(matching_files), + ) def loaded_fixed_component_values( self, payload: RuntimeArrayData, @@ -2664,6 +2670,15 @@ def _load_input_stack( source_projection=source_projection, workspace_source_lookups=workspace_source_lookups, ) + if not producer_records: + metadata = image_payload_metadata(main_data_stack) + domain = request.source_binding_plan.source_spatial_domain.admit_source_cohort( + metadata.source_spatial_domain, + depth=len(matching_files), + ) + main_data_stack = metadata.replace_fields( + source_spatial_domain=domain, + ).attach_to(main_data_stack) else: main_data_stack = cached_stack @@ -3032,16 +3047,15 @@ def execute_chain( return NoMainFlowOutput() return current_stack - def _project_output_slices( self, processed_stack: RuntimeArrayData, matching_files: Sequence[str], ) -> tuple[tuple[RuntimeArrayData, AlignedImageSliceContext | None], ...]: """Project the original output through its nominal image topology.""" - if ( - isinstance(processed_stack, ImagePayloadMetadataCarrier) - and processed_stack.metadata.plane_axis is None + if isinstance(processed_stack, ImagePayloadMetadataCarrier) and ( + processed_stack.metadata.plane_axis is None + or processed_stack.metadata.persists_whole_image() ): output_context = self._unwrapped_main_flow_output_context() contexts = ( @@ -3168,9 +3182,9 @@ def _save_outputs( memory_type=plan.output_memory_type, device_id=plan.device_id_for(plan.output_memory_type), ) - elif ( - isinstance(processed_stack, ImagePayloadMetadataCarrier) - and processed_stack.metadata.plane_axis is None + elif isinstance(processed_stack, ImagePayloadMetadataCarrier) and ( + processed_stack.metadata.plane_axis is None + or processed_stack.metadata.persists_whole_image() ): stack_payload = ImagePayloadStackComposition.copy_whole_image( processed_stack, diff --git a/openhcs/processing/backends/cellprofiler/infrastructure.py b/openhcs/processing/backends/cellprofiler/infrastructure.py index 8fc913576d..809171fe43 100644 --- a/openhcs/processing/backends/cellprofiler/infrastructure.py +++ b/openhcs/processing/backends/cellprofiler/infrastructure.py @@ -41,6 +41,7 @@ ) from openhcs.core.source_matching import source_metadata_component from openhcs.core.source_metadata import SourceVoxelSpacing +from openhcs.core.source_spatial_domain import VolumeSourceSpatialDomain from openhcs.core.runtime_tabular_values import FieldSpec from openhcs.interop.cellprofiler.cellprofiler_literals import ( decode_cellprofiler_setting_literal, @@ -1408,10 +1409,12 @@ def contribute_source_bindings( explicit_sources.append(explicit_source) source_stack_components = config.source_stack_components + source_spatial_domain = config.source_spatial_domain if module.get_setting("Process as 3D?", "No").strip().casefold() == "yes": source_stack_components = tuple( dict.fromkeys((*source_stack_components, AllComponents.Z_INDEX)) ) + source_spatial_domain = VolumeSourceSpatialDomain() voxel_spacing = SourceVoxelSpacing.from_cellprofiler_xyz( x=_positive_float_setting( module, @@ -1436,6 +1439,7 @@ def contribute_source_bindings( dict.fromkeys((*config.image_plane_sources, *explicit_sources)) ), source_stack_components=source_stack_components, + source_spatial_domain=source_spatial_domain, source_voxel_spacing=voxel_spacing, ) diff --git a/openhcs/processing/materialization/core.py b/openhcs/processing/materialization/core.py index a24f5b86b0..64ccc9fd1c 100644 --- a/openhcs/processing/materialization/core.py +++ b/openhcs/processing/materialization/core.py @@ -23,7 +23,7 @@ import numpy as np import pandas as pd from metaclass_registry import AutoRegisterMeta -from polystore.config import TiffConfig, tiff_write_batches +from polystore.config import TiffConfig from polystore.streaming.viewer_transport import ( PathMappedViewerStreamSourceMetadata, ViewerStreamBackendKwargs, @@ -200,8 +200,17 @@ def filemanager_batches( **({"tiff_config": config} if config is not None else {}), }, ) - for indices, config in tiff_write_batches( - tuple(output.path for output in outputs), self.tiff_config + for indices, config in ImageFileFormat.storage_write_batches( + tuple( + ( + output.metadata.attach_to(output.content) + if output.metadata is not None + else output.content + ) + for output in outputs + ), + tuple(output.path for output in outputs), + self.tiff_config, ) ) @@ -1306,6 +1315,47 @@ def from_runtime_slice_projected_value( ), ) + @classmethod + def from_image_value( + cls, + value: MaterializationValue, + options: SourceOptions, + ) -> "MaterializationInput": + """Preserve intrinsic images while projecting ordinary runtime planes.""" + unprojected = cls.from_value(value, options) + items = [] + for image in unprojected.items: + if image.metadata.persists_whole_image(): + items.append(image) + else: + items.extend( + MaterializationInputItem( + value=item.value, + source_description=item.source_description, + runtime_plane_metadata=item.runtime_plane_metadata, + ) + for item in ( + RuntimeProjectionSourceIdentityRequirement.OPTIONAL + ).project_payload_items( + RuntimeProjectionSourceIdentityRequest( + value=image.value, + source_description=image.source_description, + plane_projection=( + image.value.declared_plane_projection() + if isinstance(image.value, ObjectLabelValue) + else None + ), + ) + ) + ) + return replace( + unprojected, + items=tuple(items), + source_plane_projection=SourcePlaneProjectionContract.from_payloads( + tuple(item.value for item in unprojected.items) + ), + ) + @property def data(self) -> MaterializationValue: data_items = [item.data for item in self.items] @@ -2544,7 +2594,7 @@ def write_image_file( context: MaterializationContext, ) -> list[Output]: """Write image payloads through the nominal image serialization format.""" - materialization_input = MaterializationInput.from_runtime_slice_projected_value( + materialization_input = MaterializationInput.from_image_value( data, options, ) @@ -3465,12 +3515,7 @@ def image_file_emits_variable_component_planes( or _image_template_uses_sequence_index(options.relative_path_template) ): return False - return ( - len( - MaterializationInput.from_runtime_slice_projected_value(data, options).items - ) - > 1 - ) + return len(MaterializationInput.from_image_value(data, options).items) > 1 @materialization_emits_variable_component_planes.register(ROIOptions) diff --git a/tests/unit/test_cellprofiler_geometry_prepared_payload_abi.py b/tests/unit/test_cellprofiler_geometry_prepared_payload_abi.py index 933738e65e..a0396a3de5 100644 --- a/tests/unit/test_cellprofiler_geometry_prepared_payload_abi.py +++ b/tests/unit/test_cellprofiler_geometry_prepared_payload_abi.py @@ -16,7 +16,10 @@ image_payload_metadata, ) from openhcs.core.runtime_plane_projection import RuntimePlaneAxis -from openhcs.core.source_spatial_domain import SourceSpatialDomain +from openhcs.core.source_spatial_domain import ( + SourceSpatialDomain, + VolumeSourceSpatialDomain, +) from openhcs.interop.cellprofiler.runtime.adapter import CellProfilerRuntimeAdapter from openhcs.interop.cellprofiler.runtime.function_contract_execution import ( CellProfilerFunctionContractExecutor, @@ -53,7 +56,14 @@ def test_prepared_resize_preserves_typed_primary_and_bare_array(func): @pytest.mark.parametrize("axis", (None, RuntimePlaneAxis.RUNTIME_SLICE)) @pytest.mark.parametrize("z_factor", (1.0, 2.0)) -def test_prepared_full_stack_resize_preserves_existing_metadata_and_mask(axis, z_factor): +@pytest.mark.parametrize( + "domain_type", (SourceSpatialDomain, VolumeSourceSpatialDomain) +) +def test_prepared_full_stack_resize_preserves_existing_metadata_and_mask( + axis, + z_factor, + domain_type, +): planes = tuple( ImagePayloadMetadata( source_path=f"/images/z{index}.tif", @@ -66,6 +76,15 @@ def test_prepared_full_stack_resize_preserves_existing_metadata_and_mask(axis, z for index in (3, 1, 2) ) metadata = ImagePayloadMetadata.compose(planes).replace_fields(plane_axis=axis) + if domain_type is VolumeSourceSpatialDomain: + metadata = metadata.replace_fields( + source_spatial_domain=VolumeSourceSpatialDomain( + source_depth=3 + ).admit_source_cohort( + metadata.source_spatial_domain, + depth=3, + ) + ) pixels = np.stack(tuple(image_payload_data(plane) for plane in planes)) mask = np.ones(pixels.shape, dtype=bool) mask[:, 0, 0] = False @@ -82,13 +101,21 @@ def test_prepared_full_stack_resize_preserves_existing_metadata_and_mask(axis, z execution_mode=ImagePayloadExecutionMode.FULL_STACK, ) - np.testing.assert_array_equal(image_payload_data(result), image_payload_data(expected)) - np.testing.assert_array_equal(image_payload_mask(result), image_payload_mask(expected)) + np.testing.assert_array_equal( + image_payload_data(result), image_payload_data(expected) + ) + np.testing.assert_array_equal( + image_payload_mask(result), image_payload_mask(expected) + ) assert image_payload_metadata(result) == image_payload_metadata(expected) assert image_payload_metadata(result).plane_axis is axis assert image_payload_metadata(result).source_image_paths == metadata.source_image_paths assert image_payload_metadata(result).source_provenance == metadata.source_provenance assert image_payload_metadata(result).source_spatial_domain.source_shape_yx == (2, 3) + assert isinstance(image_payload_metadata(result).source_spatial_domain, domain_type) + if domain_type is VolumeSourceSpatialDomain: + assert image_payload_metadata(result).source_spatial_domain.source_depth == 3 + assert image_payload_data(result).shape[0] == int(3 * z_factor) assert image_payload_metadata(source) is metadata np.testing.assert_array_equal(image_payload_data(source), pixels) np.testing.assert_array_equal(image_payload_mask(source), mask) diff --git a/tests/unit/test_cellprofiler_source_bindings.py b/tests/unit/test_cellprofiler_source_bindings.py index 6271adcd21..1a6a5fc48e 100644 --- a/tests/unit/test_cellprofiler_source_bindings.py +++ b/tests/unit/test_cellprofiler_source_bindings.py @@ -960,6 +960,9 @@ def test_names_and_types_contributes_3d_axis_and_voxel_spacing() -> None: config = _fold_setup_modules(module) + from openhcs.core.source_spatial_domain import VolumeSourceSpatialDomain + + assert isinstance(config.source_spatial_domain, VolumeSourceSpatialDomain) assert config.source_stack_components == (AllComponents.Z_INDEX,) assert config.source_voxel_spacing.values_zyx == (2.0, 1.0, 0.5) assert config.source_voxel_spacing.unit is SourceVoxelSpacingUnit.RELATIVE diff --git a/tests/unit/test_function_outputs.py b/tests/unit/test_function_outputs.py index 42074077e2..0028c03fb2 100644 --- a/tests/unit/test_function_outputs.py +++ b/tests/unit/test_function_outputs.py @@ -34,7 +34,7 @@ ) from openhcs.core.component_group_scope import RuntimeExecutionAxisScope from openhcs.core.components.parser_metaprogramming import FilenameParseResult -from openhcs.core.config import WellFilterMode +from openhcs.core.config import TiffConfig, WellFilterMode from openhcs.core.function_patterns import compile_function_pattern from openhcs.core.runtime_image_loading import ImagePayloadSourceMetadataContext from openhcs.core.runtime_image_values import ( @@ -347,6 +347,7 @@ def context_stub(filemanager, parser=None): context.input_dir = Path("/tmp/plate/images") context.execution_runtime = SimpleNamespace(execution_axis_values=("A01",)) context.axis_id = "A01" + context.tiff_config = TiffConfig() context.step_axis_filters = {} return context @@ -2669,3 +2670,110 @@ def forbidden_project(request): StreamOutputBatch.from_projection( parser=parser, payloads=arrays, paths=paths, produced_outputs=records, ) + + +def test_whole_volume_checkpoint_publication_retains_exact_producer_scope(tmp_path): + from openhcs.core.source_spatial_domain import VolumeSourceSpatialDomain + from openhcs.core.source_projection import SourceArtifactProjection + + plate_root = tmp_path / "plate" + output_dir = plate_root / "images" + output_dir.mkdir(parents=True) + path = output_dir / "A01_s001_w2_z001_t001.tif" + pixels = np.arange(5 * 6 * 7, dtype=np.uint16).reshape(5, 6, 7) + components = tuple( + { + "well": "A01", + "site": 1, + "channel": 2, + "z_index": z, + "timepoint": 1, + "extension": ".tif", + } + for z in range(1, 6) + ) + metadata = ImagePayloadMetadata( + plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + source_spatial_domain=VolumeSourceSpatialDomain( + source_depth=5, + origin_yx=(0, 0), + source_shape_yx=(6, 7), + ), + source_component_metadata={ + "well": "A01", + "site": 1, + "channel": 2, + "timepoint": 1, + "extension": ".tif", + }, + source_image_provenance_planes=SourceImageProvenancePlanes.from_components( + paths=tuple(f"/input/A01_s001_w2_z{z:03d}_t001.tif" for z in range(1, 6)), + component_metadata=components, + ), + ) + manager = FileManager( + {"disk": DiskStorageBackend(), "memory": MemoryStorageBackend()} + ) + manager.ensure_directory(output_dir, "memory") + manager.save(metadata.payload_with(pixels), str(path), "memory") + context = context_stub(manager, parser=SourceSchemaFilenameParser()) + plan = function_step_plan( + "Volume checkpoint", variable_components=(VariableComponents.Z_INDEX,) + ) + plan.output_dir = output_dir + plan.output_plate_root = str(plate_root) + plan.sub_dir = "images" + plan.analysis_results_dir = str(plate_root / "results") + plan.write_backend = "disk" + identity = FunctionOutputIdentity( + component_values={"well": "A01", "site": 1, "channel": 2, "timepoint": 1}, + filename_component_values=components[0], + extension=".tif", + source="declared volume", + ) + record_output_path( + context, + plan, + str(path), + image_metadata=metadata, + identity=identity, + output_context=AlignedImageSliceContext.main_flow( + output_key="DNA", artifact_kind=ImageArtifactType.value + ), + ) + MemoryOutputWriter.write_if_needed(context, plan) + np.testing.assert_array_equal(tifffile.imread(path), pixels) + (target,) = OpenHCSMetadataWriter.OutputTarget.for_execution(context, plan) + entries = target.produced_projection_entries(context, plan) + assert entries is not None + (projection,) = entries.entries.values() + assert isinstance(projection, SourceArtifactProjection) + assert projection.address is None + assert projection.source_alias == "DNA" + assert ( + projection.execution_scope.value_text_for_component(AllComponents.CHANNEL) + == "2" + ) + assert ( + projection.execution_scope.value_text_for_component(AllComponents.SITE) == "1" + ) + assert ( + projection.execution_scope.value_text_for_component(AllComponents.TIMEPOINT) + == "1" + ) + assert ( + projection.execution_scope.value_text_for_component(AllComponents.Z_INDEX) + is None + ) + assert ( + projection.image_metadata.source_image_provenance_planes.component_metadata + == metadata.source_image_provenance_planes.component_metadata + ) + document = {} + entries.merge_into_subdirectory(document) + decoded = VirtualWorkspaceSourceProjectionEntries.from_subdirectory( + json.loads(json.dumps(document)) + ) + restored = decoded.entries["images/A01_s001_w2_z001_t001.tif"] + assert restored.execution_scope == projection.execution_scope + assert restored.image_metadata.source_spatial_domain.source_depth == 5 diff --git a/tests/unit/test_function_runtime_source_projection.py b/tests/unit/test_function_runtime_source_projection.py index a85191e0c0..7bf8eddcdd 100644 --- a/tests/unit/test_function_runtime_source_projection.py +++ b/tests/unit/test_function_runtime_source_projection.py @@ -532,7 +532,6 @@ def resolve_address(backend_address, backend, *, base_path): assert metadata.source_image_names == ("OrigBlue",) - def test_physical_source_loading_preserves_tiff_calibration_and_live_buffers( tmp_path: Path, ) -> None: @@ -595,6 +594,7 @@ def test_physical_source_loading_preserves_tiff_calibration_and_live_buffers( assert image_payload_data(loaded)[0, 0] == 17 assert not image_payload_mask(loaded)[0, 0] + def test_virtual_workspace_source_filters_use_persisted_candidate_identity( tmp_path: Path, ) -> None: @@ -5707,3 +5707,40 @@ def get(self, *_args, **_kwargs): with pytest.raises(NoStepOutputManifestMatch, match="found 2"): runtime._load_input_stack() + + +def test_whole_volume_checkpoint_load_preserves_depth_and_independent_buffers(): + from openhcs.core.aligned_image_payload import ImagePayloadStackComposition + from openhcs.core.runtime_image_values import image_payload_mask + from openhcs.core.source_spatial_domain import VolumeSourceSpatialDomain + + pixels = np.arange(3 * 4 * 5, dtype=np.float32).reshape(3, 4, 5) + mask = pixels % 2 == 0 + metadata = ImagePayloadMetadata( + plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + source_spatial_domain=VolumeSourceSpatialDomain(source_depth=3), + ) + source = metadata.payload_with(pixels, mask) + plan = CompiledStepPlan( + step_index=0, + step_name="Volume", + step_type="FunctionStep", + axis_id="A01", + step_scope_id="volume", + input_memory_type="numpy", + ) + loaded = ImagePayloadStackComposition.from_loaded_images( + (source,), + producer_records=None, + execution_plan=plan, + source_projection=None, + workspace_source_lookups=(), + ) + assert image_payload_data(loaded).shape == pixels.shape + assert loaded.metadata.source_spatial_domain.source_depth == 3 + assert not np.shares_memory(image_payload_data(loaded), pixels) + assert not np.shares_memory(image_payload_mask(loaded), mask) + pixels[:] = -1 + mask[:] = False + assert np.all(image_payload_data(loaded) >= 0) + assert np.any(image_payload_mask(loaded)) diff --git a/tests/unit/test_image_file_serialization.py b/tests/unit/test_image_file_serialization.py index 5646baefa8..fee02e3021 100644 --- a/tests/unit/test_image_file_serialization.py +++ b/tests/unit/test_image_file_serialization.py @@ -507,3 +507,31 @@ def test_png_image_format_uses_registered_png_leaf() -> None: def test_unknown_image_serialization_suffix_fails_loudly(path) -> None: with pytest.raises(ValueError, match="image|suffix|format"): ImageFileFormat.require_path(path) + + +@pytest.mark.parametrize("channel_axis", (None, -1, 1)) +def test_intrinsic_tiff_write_uses_declared_axes_not_rgb_shaped_dimensions( + tmp_path, channel_axis +): + from openhcs.core.source_spatial_domain import VolumeSourceSpatialDomain + + shape = ( + (3, 4, 4) + if channel_axis is None + else ((3, 4, 4, 3) if channel_axis == -1 else (3, 3, 4, 4)) + ) + pixels = np.arange(np.prod(shape), dtype=np.uint16).reshape(shape) + metadata = ImagePayloadMetadata( + plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + source_channel_axis=channel_axis, + source_spatial_domain=VolumeSourceSpatialDomain(source_depth=3), + ) + path = tmp_path / "volume.tif" + image_format = TiffImageFileFormat() + image_format.write(path, metadata.payload_with(pixels)) + np.testing.assert_array_equal(image_format.read(path), pixels) + header = image_format.require_source_metadata(path) + assert header.source_frame_shape == ((3,) if channel_axis != 1 else None) + assert header.pixel_semantics.channel_axis == channel_axis + assert header.pixel_semantics.channel_count == (None if channel_axis is None else 3) + assert header.image_shape_yx == (4, 4) diff --git a/tests/unit/test_materialization_core.py b/tests/unit/test_materialization_core.py index 39022ad340..1d4fc2cc1a 100644 --- a/tests/unit/test_materialization_core.py +++ b/tests/unit/test_materialization_core.py @@ -1,4 +1,5 @@ import json +from pathlib import Path from functools import partial from multiprocessing.shared_memory import SharedMemory from types import SimpleNamespace @@ -2431,3 +2432,136 @@ def test_roi_materialization_treats_non_spatial_label_payload_as_empty() -> None assert out == "/tmp/A01_Worms_step3_segmentation_summary.txt" assert "No ROIs extracted" in fm.load(out, "memory") + + +@pytest.mark.parametrize("intrinsic", (False, True)) +def test_declared_volume_save_preserves_pixels_and_exact_plane_selection( + tmp_path, intrinsic +): + from openhcs.core.artifacts import ImageArtifactType + from openhcs.core.component_group_scope import RuntimeExecutionAxisScope + from openhcs.core.image_file_serialization import TiffImageFileFormat + from openhcs.core.runtime_image_values import image_payload_data, image_payload_mask + from openhcs.core.runtime_slice_projection import RuntimeSliceProjection + from openhcs.core.source_projection import SourceArtifactProjection, SourcePixelRef + from openhcs.core.source_spatial_domain import VolumeSourceSpatialDomain + from openhcs.core.source_workspace_projection import ( + VirtualWorkspaceImagePayloadProjection, + ) + from openhcs.core.virtual_workspace_metadata import ( + VirtualWorkspaceSourceProjectionEntries, + ) + from openhcs.processing.materialization import prepare_materialization + import tifffile + + pixels = np.arange(3 * 4 * 5, dtype=np.uint16).reshape(3, 4, 5) + mask = pixels % 3 != 0 + paths = tuple(f"/source/A01_s001_w2_z{z:03d}_t001.tif" for z in (1, 2, 3)) + components = tuple( + {"well": "A01", "site": 1, "channel": 2, "z_index": z, "timepoint": 1} + for z in (1, 2, 3) + ) + domain = ( + VolumeSourceSpatialDomain( + source_depth=3, source_shape_yx=(4, 5), origin_yx=(0, 0) + ) + if intrinsic + else SourceSpatialDomain(source_shape_yx=(4, 5), origin_yx=(0, 0)) + ) + metadata = ImagePayloadMetadata( + plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + source_spatial_domain=domain, + source_voxel_spacing=SourceVoxelSpacing((2.0, 1.0, 0.5)), + source_image_names=("DNA",), + source_component_metadata={ + "well": "A01", + "site": 1, + "channel": 2, + "timepoint": 1, + "extension": ".tif", + }, + source_image_provenance_planes=SourceImageProvenancePlanes.from_components( + paths=paths, + component_metadata=components, + ), + ) + payload = metadata.payload_with(pixels, mask) + manager = FileManager({"disk": DiskStorageBackend()}) + spec = MaterializationSpec(ImageFileOptions(filename_suffix=".tif")) + batch = prepare_materialization( + spec, + payload, + str(tmp_path / ("A01_s001_w2_z001_t001.tif" if intrinsic else "output")), + manager, + ("disk",), + context=_SourceSchemaProcessingContext(), + variable_components=(VariableComponents.Z_INDEX,), + ) + batch.save() + assert len(batch.outputs) == (1 if intrinsic else 3) + if not intrinsic: + for index, output in enumerate(batch.outputs): + np.testing.assert_array_equal(tifffile.imread(output.path), pixels[index]) + return + + (output,) = batch.outputs + np.testing.assert_array_equal(tifffile.imread(output.path), pixels) + saved_metadata = TiffImageFileFormat().persisted_metadata( + Path(output.path), payload + ) + projection = SourceArtifactProjection( + address=SourceArtifactProjection.scalar_address_for_image_metadata( + saved_metadata + ), + ref=SourcePixelRef("disk", output.path), + source_alias="DNA", + artifact_kind=ImageArtifactType, + image_metadata=saved_metadata, + execution_scope=RuntimeExecutionAxisScope.from_raw( + "A01", + component="channel", + value="2", + fixed_component_values=( + (AllComponents.SITE, "1"), + (AllComponents.TIMEPOINT, "1"), + ), + ), + ) + assert projection.address is None + entries = VirtualWorkspaceSourceProjectionEntries.from_projection_paths( + ((projection, "images/volume.tif"),) + ) + document = {} + entries.merge_into_subdirectory(document) + decoded = VirtualWorkspaceSourceProjectionEntries.from_subdirectory( + json.loads(json.dumps(document)) + ) + restored = decoded.entries["images/volume.tif"] + assert restored.execution_scope == projection.execution_scope + assert isinstance( + restored.image_metadata.source_spatial_domain, VolumeSourceSpatialDomain + ) + assert restored.image_metadata.source_spatial_domain.source_depth == 3 + reloaded = VirtualWorkspaceImagePayloadProjection( + persisted_metadata=restored.image_metadata + ).apply(metadata.payload_with(tifffile.imread(output.path), mask.copy())) + for index in range(3): + physical_plane = SourcePixelRef( + "disk", output.path, source_axis_indices=(index,) + ).load( + {"disk": manager._get_backend("disk")}, + base_path=tmp_path, + ) + np.testing.assert_array_equal(physical_plane, pixels[index]) + plane = RuntimeSliceProjection.value_for_slice( + reloaded, + RuntimePlaneAxisValueProjection.preserve( + axis=RuntimePlaneAxis.RUNTIME_SLICE, axis_size=3 + ).selected_plane(index), + ) + np.testing.assert_array_equal(image_payload_data(plane), pixels[index]) + np.testing.assert_array_equal(image_payload_mask(plane), mask[index]) + assert plane.metadata.source_path == paths[index] + assert plane.metadata.source_component_metadata["z_index"] == index + 1 + assert type(plane.metadata.source_spatial_domain) is SourceSpatialDomain + assert plane.metadata.source_voxel_spacing == metadata.source_voxel_spacing diff --git a/tests/unit/test_pipeline_document.py b/tests/unit/test_pipeline_document.py index 4b58a23201..1760d52baa 100644 --- a/tests/unit/test_pipeline_document.py +++ b/tests/unit/test_pipeline_document.py @@ -158,3 +158,32 @@ def test_render_revalidates_direct_pipeline_document_instances() -> None: with pytest.raises(TypeError, match="PipelineConfig"): PipelineDocumentAuthority.render(invalid) + + +def test_pipeline_document_preserves_intrinsic_volume_source_declaration(): + import pickle + from openhcs.core.config import LazySourceBindingsConfig + from openhcs.core.source_bindings import ( + CompiledSourceBindingPlan, + StepSourceBindingsConfig, + ) + from openhcs.core.source_spatial_domain import VolumeSourceSpatialDomain + + domain = VolumeSourceSpatialDomain() + document = PipelineDocumentAuthority.from_values( + pipeline_config=PipelineConfig( + source_bindings_config=LazySourceBindingsConfig( + source_spatial_domain=domain + ) + ), + pipeline_steps=[], + ) + source = PipelineDocumentAuthority.render(document) + restored = PipelineDocumentAuthority.from_source(source) + assert ( + restored.pipeline_config.source_bindings_config.source_spatial_domain == domain + ) + plan = CompiledSourceBindingPlan.from_config( + StepSourceBindingsConfig(source_spatial_domain=domain) + ) + assert pickle.loads(pickle.dumps(plan)).source_spatial_domain == domain From 511b962480ecfc14d426d3692cd48dfd50c0d69a Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 20:04:24 -0400 Subject: [PATCH 170/235] Reuse admitted compiled declarations without rebuilding source inventory Execute an explicitly selected artifact through its retained declaration and source domain after exact request signature admission. Keep current runtime auxiliary options, live backend reset, fresh cancellation and execution identity, and independent compiled runtime/transport graphs. Validation: 33 focused compilation/server/inspection controls passed; the fresh runtime virtual-workspace/reset/cancellation/source-availability control also passed with an actual backend read. End-to-end performance remains to be measured in the joined runtime campaign. Refs #394. --- openhcs/core/orchestrator/orchestrator.py | 63 +++++++ openhcs/runtime/zmq_compilation.py | 42 +++-- openhcs/runtime/zmq_execution_server.py | 49 ++++- .../test_compiled_artifact_inspection.py | 5 + tests/unit/test_artifact_plan_view.py | 3 + tests/unit/test_zmq_compilation.py | 6 + tests/unit/test_zmq_execution_server.py | 177 +++++++++++++++++- 7 files changed, 322 insertions(+), 23 deletions(-) diff --git a/openhcs/core/orchestrator/orchestrator.py b/openhcs/core/orchestrator/orchestrator.py index 2169f6ab89..90a73e9f34 100644 --- a/openhcs/core/orchestrator/orchestrator.py +++ b/openhcs/core/orchestrator/orchestrator.py @@ -480,6 +480,69 @@ def initialize( logger.error(f"Failed to initialize orchestrator: {e}") raise + def adopt_compiled_execution( + self, + execution_bundle: CompiledExecutionBundle, + ) -> "PipelineOrchestrator": + """Bind an admitted compiled source domain to this fresh runtime owner.""" + if self._initialized or self.state is not OrchestratorState.CREATED: + raise RuntimeError( + "Compiled execution can only be adopted before initialization." + ) + contexts = tuple(execution_bundle.runtime_contexts.values()) + if not contexts: + raise ValueError("Compile artifact missing compiled_contexts") + source = contexts[0] + if ( + source.microscope_handler is None + or source.input_dir is None + or source.filemanager is None + ): + raise ValueError("Compiled execution lacks its admitted source workspace.") + source_registry = source.filemanager.registry + for context in contexts: + if ( + context.plate_path != self.plate_path + or context.input_dir != source.input_dir + or context.workspace_path != source.workspace_path + or context.microscope_handler is not source.microscope_handler + or context.filemanager is None + or context.filemanager.registry.keys() != source_registry.keys() + or any( + backend is not source_registry[name] + for name, backend in context.filemanager.registry.items() + ) + ): + raise ValueError( + "Compiled execution source domain does not match this runtime." + ) + if source_registry.get(Backend.MEMORY.value) is not self.registry.get( + Backend.MEMORY.value + ): + raise ValueError( + "Compiled execution memory backend does not match this runtime." + ) + source.microscope_handler.source_selection_role().require_available_source( + self.plate_path + ) + self.registry = dict(source_registry) + self.registry[Backend.ZARR.value] = self.filemanager.registry[ + Backend.ZARR.value + ] + self.filemanager = FileManager(self.registry) + self.microscope_handler = source.microscope_handler + self.input_dir = source.input_dir + self.workspace_path = source.workspace_path + self.default_pipeline_definition = list(execution_bundle.pipeline_definition) + from openhcs.constants import MULTIPROCESSING_AXIS + + self._component_keys_cache[MULTIPROCESSING_AXIS] = list( + execution_bundle.axis_ids + ) + self._initialized = True + self._state = OrchestratorState.READY + return self + def is_initialized(self) -> bool: return self._initialized diff --git a/openhcs/runtime/zmq_compilation.py b/openhcs/runtime/zmq_compilation.py index f20ef7f6d5..69949296a5 100644 --- a/openhcs/runtime/zmq_compilation.py +++ b/openhcs/runtime/zmq_compilation.py @@ -16,6 +16,7 @@ ZMQCompileProgressHeartbeat, ZMQProgressEmitter, ) +from openhcs.runtime.zmq_execution_signature import OpenHCSExecutionConfigBundle if TYPE_CHECKING: from openhcs.core.orchestrator.orchestrator import PipelineOrchestrator @@ -101,25 +102,11 @@ def reuse_artifact(self) -> ZMQCompilationResult: if self.retain_compile_artifact else self.compilation_signature ) - artifact_signature = artifact.signature_for_retain_policy( - self.retain_compile_artifact + artifact.require_compatible_request( + plate_id=self.plate_id, + signature=expected_signature, + retain_compile_artifact=self.retain_compile_artifact, ) - if artifact_signature != expected_signature: - logger.error( - "[%s] Compile artifact signature mismatch: artifact_id=%s artifact_sig=%s request_sig=%s", - self.execution_id, - self.compile_artifact_id, - artifact_signature[:12], - expected_signature[:12], - ) - raise ValueError( - f"Compile artifact '{self.compile_artifact_id}' does not match execution request" - ) - if artifact.plate_id != str(self.plate_id): - raise ValueError( - f"Compile artifact '{self.compile_artifact_id}' is for plate " - f"{artifact.plate_id}, not {self.plate_id}" - ) execution_bundle = artifact.compilation.execution_bundle compiled_contexts = execution_bundle.runtime_contexts @@ -224,9 +211,28 @@ class ZMQCompileArtifactRecord: compilation_signature: str debug_replay_signature: str compilation: ZMQCompilationResult + configs: OpenHCSExecutionConfigBundle created_at: float = field(default_factory=time.time) def signature_for_retain_policy(self, retain_compile_artifact: bool) -> str: if retain_compile_artifact: return self.debug_replay_signature return self.compilation_signature + + def require_compatible_request( + self, + *, + plate_id: str, + signature: str, + retain_compile_artifact: bool, + ) -> None: + """Admit the exact compiled declaration before evaluating or preparing work.""" + if self.signature_for_retain_policy(retain_compile_artifact) != signature: + raise ValueError( + f"Compile artifact '{self.execution_id}' does not match execution request" + ) + if self.plate_id != str(plate_id): + raise ValueError( + f"Compile artifact '{self.execution_id}' is for plate " + f"{self.plate_id}, not {plate_id}" + ) diff --git a/openhcs/runtime/zmq_execution_server.py b/openhcs/runtime/zmq_execution_server.py index e981bccf96..96d15ca836 100644 --- a/openhcs/runtime/zmq_execution_server.py +++ b/openhcs/runtime/zmq_execution_server.py @@ -410,6 +410,39 @@ def _execute_pipeline( ): logger.info("[%s] Starting plate %s", execution_id, request_payload.plate_id) + request_payload.compile_control.validate() + self._cleanup_compiled_artifacts() + if request_payload.compile_artifact_id is not None: + artifact = self._compiled_artifacts.get(request_payload.compile_artifact_id) + if artifact is None: + raise ValueError( + f"Missing compile artifact '{request_payload.compile_artifact_id}'. " + "Re-run compilation before execution." + ) + auxiliary = ZMQAuxiliaryExecutionParams.from_transport( + request_payload.config_params + ) + retain = self._retain_compile_artifact(auxiliary.debug_execution_config) + artifact.require_compatible_request( + plate_id=request_payload.plate_id, + signature=( + request_payload.debug_replay_signature + if retain + else request_payload.compilation_signature + ), + retain_compile_artifact=retain, + ) + return self._execute_with_orchestrator( + ZMQExecutionContext( + execution_id=execution_id, + request_payload=request_payload, + pipeline_steps=list( + artifact.compilation.execution_bundle.pipeline_definition + ), + configs=artifact.configs, + ) + ) + import openhcs.processing.func_registry as func_registry_module if func_registry_module.pipeline_source_requires_import_projection( @@ -417,8 +450,6 @@ def _execute_pipeline( ): func_registry_module.initialize_registry() - self._cleanup_compiled_artifacts() - module_name = f"openhcs_zmq_pipeline_{request_payload.pipeline_sha}" module = ModuleType(module_name) module.__file__ = f"<{module_name}>" @@ -524,6 +555,13 @@ def _execute_with_orchestrator( plate_path_str, request_context.pipeline_config, selected_pipeline_path=request_context.request_payload.selected_pipeline_path, + execution_bundle=( + self._compiled_artifacts[ + request_context.compile_artifact_id + ].compilation.execution_bundle + if request_context.compile_artifact_id is not None + else None + ), ) self._raise_if_cancelled(request_context.execution_id, "initialization") wells = self._wells_for_execution( @@ -595,6 +633,7 @@ def _initialize_orchestrator( plate_path_str: str, pipeline_config, selected_pipeline_path: str | None = None, + execution_bundle=None, ): from pathlib import Path @@ -608,7 +647,10 @@ def _initialize_orchestrator( transport_config=self.config, ) orchestrator.execution_id = execution_id - orchestrator.initialize() + if execution_bundle is None: + orchestrator.initialize() + else: + orchestrator.adopt_compiled_execution(execution_bundle) self.active_executions[execution_id].set_extra("orchestrator", orchestrator) return orchestrator @@ -817,6 +859,7 @@ def _store_compile_artifact( compilation_signature=request_context.compilation_signature, debug_replay_signature=request_context.debug_replay_signature, compilation=compilation, + configs=request_context.configs, ) ) logger.info( diff --git a/tests/integration/test_compiled_artifact_inspection.py b/tests/integration/test_compiled_artifact_inspection.py index 317bf0469a..7f1a7402fd 100644 --- a/tests/integration/test_compiled_artifact_inspection.py +++ b/tests/integration/test_compiled_artifact_inspection.py @@ -13,6 +13,8 @@ from zmqruntime import TcpDataControlPortPairAuthority from zmqruntime.config import TransportMode +from openhcs.core.config import GlobalPipelineConfig, PipelineConfig +from openhcs.runtime.zmq_execution_signature import OpenHCSExecutionConfigBundle from openhcs.core.artifact_inspection import ( CompiledArtifactInspectionControlPayload, CompiledArtifactInspectionRequest, @@ -238,6 +240,9 @@ def produce(image): ) return ( ZMQCompileArtifactRecord( + configs=OpenHCSExecutionConfigBundle( + GlobalPipelineConfig(), PipelineConfig() + ), execution_id="compile-1", plate_id="/plates/one", compilation_signature="request", diff --git a/tests/unit/test_artifact_plan_view.py b/tests/unit/test_artifact_plan_view.py index 464d91dd1f..1b076f7d77 100644 --- a/tests/unit/test_artifact_plan_view.py +++ b/tests/unit/test_artifact_plan_view.py @@ -10,6 +10,8 @@ from PyQt6.QtWidgets import QApplication +from openhcs.core.config import GlobalPipelineConfig, PipelineConfig +from openhcs.runtime.zmq_execution_signature import OpenHCSExecutionConfigBundle from openhcs.core.artifact_inspection import ( CompiledArtifactInspection, CompiledArtifactInspectionControlPayload, @@ -217,6 +219,7 @@ def test_compiled_inspection_preserves_exact_contract_edges_and_plans() -> None: def test_registered_control_router_reads_retained_compile_artifact() -> None: expected, _output_plan, bundle = _compiled_fixture() record = ZMQCompileArtifactRecord( + configs=OpenHCSExecutionConfigBundle(GlobalPipelineConfig(), PipelineConfig()), execution_id="compile-1", plate_id="/plates/one", compilation_signature="request", diff --git a/tests/unit/test_zmq_compilation.py b/tests/unit/test_zmq_compilation.py index 252c76b642..f86a280c87 100644 --- a/tests/unit/test_zmq_compilation.py +++ b/tests/unit/test_zmq_compilation.py @@ -5,6 +5,8 @@ import pytest +from openhcs.core.config import GlobalPipelineConfig, PipelineConfig +from openhcs.runtime.zmq_execution_signature import OpenHCSExecutionConfigBundle from openhcs.core.compiled_execution import ( CompiledExecutionBundle, CompiledRuntimeEnvironmentPlan, @@ -166,6 +168,9 @@ def test_reused_compile_artifact_reads_step_names_from_compiled_plans() -> None: ) artifacts = { "artifact-1": ZMQCompileArtifactRecord( + configs=OpenHCSExecutionConfigBundle( + GlobalPipelineConfig(), PipelineConfig() + ), execution_id="compile-1", plate_id="/tmp/plate", compilation_signature="signature", @@ -271,6 +276,7 @@ def test_rejected_reuse_preserves_artifact_for_a_valid_request(retain, invalid): runtime_environment=_runtime_environment(), ) artifact = ZMQCompileArtifactRecord( + configs=OpenHCSExecutionConfigBundle(GlobalPipelineConfig(), PipelineConfig()), execution_id="compile-1", plate_id="/plate", compilation_signature="signature", diff --git a/tests/unit/test_zmq_execution_server.py b/tests/unit/test_zmq_execution_server.py index 85599621bf..ca6eb60aea 100644 --- a/tests/unit/test_zmq_execution_server.py +++ b/tests/unit/test_zmq_execution_server.py @@ -1,5 +1,6 @@ import sys from pathlib import Path +from dataclasses import replace from queue import SimpleQueue from types import ModuleType, SimpleNamespace @@ -226,8 +227,10 @@ def test_server_exports_outcomes_without_projecting_compiled_values( assert record.get_extra("runtime_observation_export_scope") == "outcomes" -def test_zmq_server_reconstructs_pipeline_and_configs_for_artifact_execution( +@pytest.mark.parametrize("changed", (None, "pipeline", "config", "plate")) +def test_zmq_server_admits_compiled_declaration_without_reevaluating_source( monkeypatch, + changed, ) -> None: import openhcs.processing.func_registry as func_registry_module @@ -263,8 +266,72 @@ def test_zmq_server_reconstructs_pipeline_and_configs_for_artifact_execution( compile_control=ZMQExecutionCompileControl(compile_artifact_id="compile-1"), ) - context = server._execute_pipeline("exec-1", request_payload) + from openhcs.core.compiled_execution import ( + CompiledExecutionBundle, + CompiledRuntimeEnvironmentPlan, + ) + from openhcs.runtime.zmq_compilation import ( + ZMQCompileArtifactRecord, + ZMQCompilationResult, + ) + configs = OpenHCSExecutionConfigBundle(GlobalPipelineConfig(), PipelineConfig()) + bundle = CompiledExecutionBundle( + pipeline_definition=[], + runtime_contexts={}, + transport_contexts={}, + worker_assignments={}, + runtime_environment=CompiledRuntimeEnvironmentPlan.from_global_config( + configs.global_pipeline, + compiled_contexts={}, + server_mode=True, + ), + ) + server._compiled_artifacts["compile-1"] = ZMQCompileArtifactRecord( + execution_id="compile-1", + plate_id=request_payload.plate_id, + compilation_signature=request_payload.compilation_signature, + debug_replay_signature=request_payload.debug_replay_signature, + compilation=ZMQCompilationResult(bundle, []), + configs=configs, + ) + monkeypatch.setattr( + server, + "_resolve_request_config", + lambda *args: pytest.fail("Artifact executed config source"), + ) + monkeypatch.setattr( + zmq_execution_server_module.PipelineDocumentAuthority, + "from_namespace", + lambda *args: pytest.fail("Artifact rebuilt pipeline declaration"), + ) + if changed == "pipeline": + request_payload = replace( + request_payload, + pipeline_code=request_payload.pipeline_code + + "\nraise AssertionError('must not execute')\n", + ) + elif changed == "config": + request_payload = replace( + request_payload, + config_transport=replace( + request_payload.config_transport, + config_code="raise AssertionError('must not execute')", + ), + ) + elif changed == "plate": + request_payload = replace( + request_payload, + identity=replace(request_payload.identity, plate_id="/different/source"), + ) + if changed is not None: + with pytest.raises(ValueError, match="does not match execution request"): + server._execute_pipeline("exec-1", request_payload) + assert "compile-1" in server._compiled_artifacts + return + context = server._execute_pipeline("exec-1", request_payload) + assert context.execution_id == "exec-1" + assert context.configs is configs assert type(context.pipeline_steps) is list assert context.pipeline_steps == [] assert isinstance(context.configs, OpenHCSExecutionConfigBundle) @@ -422,3 +489,109 @@ def fail_transport_stop(_server) -> None: server.stop() assert events == ["transport", "catalog", ("process_resources", True)] + + +def test_compiled_source_adoption_owns_fresh_runtime_services_and_live_source_gate( + tmp_path, +): + import json + from polystore.base import reset_memory_backend + from polystore.virtual_workspace import VirtualWorkspaceBackend + from openhcs.constants.constants import Backend + from openhcs.core.compiled_execution import ( + CompiledExecutionBundle, + CompiledRuntimeEnvironmentPlan, + ) + from openhcs.core.context.processing_context import ProcessingContext + from openhcs.core.orchestrator.cancellation import ExecutionCancelledError + from openhcs.core.orchestrator.orchestrator import PipelineOrchestrator + from openhcs.microscopes.openhcs import OpenHCSMicroscopeHandler + + previous = PipelineOrchestrator( + plate_path=tmp_path, pipeline_config=PipelineConfig() + ) + metadata_path = tmp_path / "polystore_metadata.json" + metadata_path.write_text( + json.dumps( + { + "subdirectories": { + "images": { + "workspace_mapping": { + "image.tif": { + "backend": Backend.MEMORY.value, + "backend_address": str(tmp_path / "source.tif"), + "source_axis_indices": [], + } + }, + } + } + } + ) + ) + virtual_workspace = VirtualWorkspaceBackend(plate_root=tmp_path) + previous.filemanager.register_backend( + Backend.VIRTUAL_WORKSPACE.value, virtual_workspace + ) + previous._execution_cancellation.request() + previous.filemanager.ensure_directory(str(tmp_path), Backend.MEMORY.value) + previous.filemanager.save("stale", str(tmp_path / "old.tif"), Backend.MEMORY.value) + handler = OpenHCSMicroscopeHandler(previous.filemanager) + context = ProcessingContext(axis_id="A01", filemanager=previous.filemanager) + context.plate_path = tmp_path + context.input_dir = tmp_path + context.microscope_handler = handler + transport = ProcessingContext(axis_id="A01") + bundle = CompiledExecutionBundle( + pipeline_definition=[], + runtime_contexts={"A01": context}, + transport_contexts={"A01": transport}, + worker_assignments={}, + runtime_environment=CompiledRuntimeEnvironmentPlan.from_global_config( + GlobalPipelineConfig(), + compiled_contexts={"A01": context}, + server_mode=True, + ), + ) + reset_memory_backend() + runtime = PipelineOrchestrator( + plate_path=tmp_path, pipeline_config=PipelineConfig() + ) + runtime.execution_id = "next-execution" + runtime.adopt_compiled_execution(bundle) + assert runtime.filemanager is not previous.filemanager + assert ( + runtime.filemanager.registry[Backend.VIRTUAL_WORKSPACE.value] + is virtual_workspace + ) + assert not runtime.filemanager.exists( + str(tmp_path / "old.tif"), Backend.MEMORY.value + ) + assert not context.filemanager.exists( + str(tmp_path / "old.tif"), Backend.MEMORY.value + ) + runtime.filemanager.save("live", str(tmp_path / "source.tif"), Backend.MEMORY.value) + assert ( + runtime.filemanager.load( + str(tmp_path / "image.tif"), + Backend.VIRTUAL_WORKSPACE.value, + ) + == "live" + ) + assert bundle.runtime_contexts["A01"] is context + assert bundle.transport_contexts["A01"] is transport + assert runtime.microscope_handler is handler + assert runtime.execution_id == "next-execution" + signal = runtime._execution_cancellation.begin() + signal.raise_if_requested("new execution") + runtime._execution_cancellation.request() + with pytest.raises(ExecutionCancelledError): + signal.raise_if_requested("active execution") + runtime._execution_cancellation.finish(signal) + metadata_path.unlink() + tmp_path.rmdir() + unavailable = PipelineOrchestrator( + plate_path=tmp_path, pipeline_config=PipelineConfig() + ) + with pytest.raises(FileNotFoundError): + unavailable.adopt_compiled_execution(bundle) + assert not unavailable.is_initialized() From 68892a9fbedbe58e2ccdf654e4b2fd33069a7cf5 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 20:06:10 -0400 Subject: [PATCH 171/235] Use the existing execution visualizer contract without a GUI import fallback --- openhcs/core/orchestrator/orchestrator.py | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/openhcs/core/orchestrator/orchestrator.py b/openhcs/core/orchestrator/orchestrator.py index 90a73e9f34..f898fac200 100644 --- a/openhcs/core/orchestrator/orchestrator.py +++ b/openhcs/core/orchestrator/orchestrator.py @@ -20,6 +20,7 @@ from openhcs.constants import Microscope from openhcs.core.compiled_execution import CompiledExecutionBundle from openhcs.core.config import GlobalPipelineConfig +from openhcs.core.execution_visualizer import ExecutionVisualizerABC from objectstate.object_state import ObjectState from openhcs.runtime.zmq_config import OPENHCS_ZMQ_CONFIG, OpenHCSZMQConfig @@ -63,16 +64,6 @@ # Import generic component system - required for orchestrator functionality -# Optional napari import for visualization -try: - from openhcs.runtime.napari_stream_visualizer import NapariStreamVisualizer - - NapariVisualizerType = NapariStreamVisualizer -except ImportError: - # Create a placeholder type for type hints when napari is not available - NapariStreamVisualizer = None - NapariVisualizerType = Any # Use Any for type hints when napari is not available - logger = logging.getLogger(__name__) @@ -733,7 +724,7 @@ def execute_compiled_plate( self, execution_bundle: CompiledExecutionBundle, max_workers: Optional[int] = None, - visualizer: Optional[NapariVisualizerType] = None, + visualizer: ExecutionVisualizerABC | None = None, log_file_base: Optional[str] = None, progress_queue=None, progress_context=None, @@ -748,8 +739,7 @@ def execute_compiled_plate( compiled_contexts: Dict of axis_id to its compiled, frozen ProcessingContext. Obtained from `compile_plate_for_processing`. max_workers: Maximum number of worker threads for parallel execution. - visualizer: Optional instance of NapariStreamVisualizer for real-time visualization - (requires napari to be installed; must be initialized with orchestrator's filemanager by the caller). + visualizer: Viewer implementing the compiled execution lifecycle. log_file_base: Base path for worker process log files (without extension). Each worker will create its own log file: {log_file_base}_worker_{pid}.log From b7c1724776c130aec1dcb204f7eb730ad0d13c53 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 20:08:45 -0400 Subject: [PATCH 172/235] Keep intrinsic source coordinates through alignment and materialization Refs #573. Preserve the aligned value spatial owner during XY-only projection. Delegate materialization source fallback to the existing metadata owner, which excludes physical filename coordinates that actually vary across admitted correlated source planes while retaining missing fixed facts. Validate whole-image overlap against its compiled step. Four actual alignment/save/publication controls pass, including same-producer checkpoint and named artifact sharing a physical volume. --- openhcs/core/aligned_image_payload.py | 10 ++-- openhcs/core/runtime_image_values.py | 18 +++++- openhcs/core/steps/function_outputs.py | 4 +- openhcs/processing/materialization/core.py | 4 +- tests/unit/test_function_outputs.py | 67 +++++++++++++++++++++- tests/unit/test_source_spatial_domain.py | 33 +++++++++++ 6 files changed, 123 insertions(+), 13 deletions(-) diff --git a/openhcs/core/aligned_image_payload.py b/openhcs/core/aligned_image_payload.py index df2aa90988..ab5aa3cda5 100644 --- a/openhcs/core/aligned_image_payload.py +++ b/openhcs/core/aligned_image_payload.py @@ -212,11 +212,10 @@ def value_in_payload_domain( ) -> RuntimeArrayData: """Project this image payload into another declared payload domain.""" materialized = self.payload_in_source_domain(self.value, target.domain) - target_domain = SourceSpatialDomain( + target_domain = replace( + self.domain, origin_yx=target.payload_domain.origin_yx, source_shape_yx=target.payload_domain.source_shape_yx, - fill_value=self.domain.fill_value, - value_name=self.domain.value_name, ) metadata = replace( image_payload_metadata(materialized), @@ -346,11 +345,10 @@ def value_in_payload_domain( target: SourceSpatialDomainAdapter, ) -> ObjectLabelValue: """Project every label variant into another declared payload domain.""" - target_domain = SourceSpatialDomain( + target_domain = replace( + self.domain, origin_yx=target.payload_domain.origin_yx, source_shape_yx=target.payload_domain.source_shape_yx, - fill_value=self.domain.fill_value, - value_name=self.domain.value_name, ) variants = self.value.variant_data.project( lambda labels: target.extract_source_array( diff --git a/openhcs/core/runtime_image_values.py b/openhcs/core/runtime_image_values.py index 2fe22fd97c..b2076db2a6 100644 --- a/openhcs/core/runtime_image_values.py +++ b/openhcs/core/runtime_image_values.py @@ -992,8 +992,24 @@ def with_source_context_from( source: "ImagePayloadMetadata", ) -> "ImagePayloadMetadata": """Fill missing source-image identity and spatial context from a source.""" + fallback_provenance = source.source_provenance + if self.persists_whole_image(): + # Physical filename coordinates must not collapse the correlated + # source coordinates represented by an intrinsic image. + varying = self.source_provenance.varying_plane_component_values( + tuple(AllComponents) + ) + fallback_provenance = fallback_provenance.with_source_component_metadata( + { + key: value + for key, value in ( + fallback_provenance.source_component_metadata or {} + ).items() + if key not in varying + } + ) source_provenance = self.source_provenance.with_missing_from( - source.source_provenance + fallback_provenance ) source_channel_axis = self.source_channel_axis if source_channel_axis is None: diff --git a/openhcs/core/steps/function_outputs.py b/openhcs/core/steps/function_outputs.py index 1692605457..0758567b81 100644 --- a/openhcs/core/steps/function_outputs.py +++ b/openhcs/core/steps/function_outputs.py @@ -940,9 +940,7 @@ def project_runtime_artifacts( or ( whole_image and projection.execution_scope - != record.execution_scope( - materialization.output_plan - ) + != record.execution_scope(plan) ) ): raise ValueError( diff --git a/openhcs/processing/materialization/core.py b/openhcs/processing/materialization/core.py index 64ccc9fd1c..6d98533bc3 100644 --- a/openhcs/processing/materialization/core.py +++ b/openhcs/processing/materialization/core.py @@ -3725,8 +3725,8 @@ def metadata_with_artifact_source_identity( source_path=self.artifact_source_identity.path, source_component_metadata=self.artifact_source_identity.component_metadata, ) - return metadata.with_source_provenance( - metadata.source_provenance.with_missing_from(artifact_provenance) + return metadata.with_source_context_from( + ImagePayloadMetadata(source_provenance=artifact_provenance) ) def ordered_axes(self) -> tuple[str, ...]: diff --git a/tests/unit/test_function_outputs.py b/tests/unit/test_function_outputs.py index 0028c03fb2..0c6c118337 100644 --- a/tests/unit/test_function_outputs.py +++ b/tests/unit/test_function_outputs.py @@ -2672,7 +2672,10 @@ def forbidden_project(request): ) -def test_whole_volume_checkpoint_publication_retains_exact_producer_scope(tmp_path): +@pytest.mark.parametrize("with_artifact", (False, True)) +def test_whole_volume_checkpoint_publication_retains_exact_producer_scope( + tmp_path, with_artifact +): from openhcs.core.source_spatial_domain import VolumeSourceSpatialDomain from openhcs.core.source_projection import SourceArtifactProjection @@ -2693,6 +2696,7 @@ def test_whole_volume_checkpoint_publication_retains_exact_producer_scope(tmp_pa for z in range(1, 6) ) metadata = ImagePayloadMetadata( + source_image_names=("DNA",), plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, source_spatial_domain=VolumeSourceSpatialDomain( source_depth=5, @@ -2711,6 +2715,22 @@ def test_whole_volume_checkpoint_publication_retains_exact_producer_scope(tmp_pa component_metadata=components, ), ) + metadata = metadata.with_source_provenance( + metadata.source_provenance.with_derived_source_image_names(("DNA",)) + ) + incomplete = metadata.replace_fields( + source_component_metadata={ + key: value + for key, value in metadata.source_component_metadata.items() + if key != "timepoint" + } + ) + contextualized = incomplete.with_source_context_from( + ImagePayloadMetadata(source_component_metadata=components[0]) + ) + assert str(contextualized.source_component_metadata["timepoint"]) == "1" + assert "z_index" not in contextualized.source_component_metadata + assert contextualized.source_image_provenance_planes == metadata.source_image_provenance_planes manager = FileManager( {"disk": DiskStorageBackend(), "memory": MemoryStorageBackend()} ) @@ -2744,6 +2764,51 @@ def test_whole_volume_checkpoint_publication_retains_exact_producer_scope(tmp_pa MemoryOutputWriter.write_if_needed(context, plan) np.testing.assert_array_equal(tifffile.imread(path), pixels) (target,) = OpenHCSMetadataWriter.OutputTarget.for_execution(context, plan) + materializations = () + if with_artifact: + plan.streaming_configs = {} + context.runtime_value_store = RuntimeValueStore() + plan.runtime_artifact_materialization = RuntimeArtifactMaterializationPlan( + persistent_enabled=True, + persistent_backend=Backend.DISK.value, + ) + output_plan = ArtifactOutputPlan( + name="DNA", + path="/memory/DNA.pkl", + artifact_type=ImageArtifactType, + producer_step_scope_id=plan.step_scope_id, + materialization=MaterializationSpec( + ImageFileOptions(relative_path_template=path.name) + ), + ) + plan.artifact_outputs[output_plan.ref()] = output_plan + context.runtime_value_store.record( + RuntimeValue.normalize_for_execution_scope( + output_plan, + metadata.payload_with(pixels), + execution_scope=RuntimeExecutionAxisScope.from_raw( + "A01", + component=None, + value=None, + fixed_component_values=( + (AllComponents.CHANNEL, "2"), + (AllComponents.SITE, "1"), + (AllComponents.TIMEPOINT, "1"), + ), + ), + ), + path=output_plan.path, + backend=Backend.MEMORY.value, + ) + materializations = RuntimeArtifactMaterializationAuthority.materialize( + context, plan + ) + assert { + Path(output.path) + for artifact in materializations + for output in artifact.outputs_for_backend(Backend.DISK.value) + } == {path} + target = replace(target, artifact_materializations=materializations) entries = target.produced_projection_entries(context, plan) assert entries is not None (projection,) = entries.entries.values() diff --git a/tests/unit/test_source_spatial_domain.py b/tests/unit/test_source_spatial_domain.py index 269f5e6a09..f460fd08bd 100644 --- a/tests/unit/test_source_spatial_domain.py +++ b/tests/unit/test_source_spatial_domain.py @@ -1,4 +1,5 @@ import numpy as np +import pytest from openhcs.core.aligned_image_payload import ( ObjectLabelPayloadSourceSpatialDomainAdapter, @@ -16,6 +17,7 @@ from openhcs.core.runtime_sparse_labels import SparseIJVLabelRows from openhcs.core.source_spatial_domain import ( SourceSpatialDomain, + VolumeSourceSpatialDomain, SourceSpatialDomainAdapter, dense_array_in_source_spatial_domain, ) @@ -116,3 +118,34 @@ def test_sparse_object_label_projection_preserves_all_declared_variants(): dtype=np.int32, ), ) + + +@pytest.mark.parametrize("as_labels", (False, True)) +def test_xy_alignment_preserves_intrinsic_volume_owner(as_labels): + pixels = np.arange(3 * 6 * 7, dtype=np.int32).reshape(3, 6, 7) + mask = np.ones(pixels.shape, dtype=bool) + mask[:, 0, 0] = False + source = ImagePayloadMetadata( + source_spatial_domain=VolumeSourceSpatialDomain( + source_depth=3, origin_yx=(0, 0), source_shape_yx=(6, 7) + ), + ).payload_with(pixels, mask) + value = ( + SourceImageObjectLabelBuildRequest(image=source, labels=pixels).label_set(name="Objects") + if as_labels else source + ) + target = ImagePayloadMetadata( + source_spatial_domain=SourceSpatialDomain( + origin_yx=(1, 2), source_shape_yx=(6, 7) + ), + ).payload_with(np.zeros((3, 2, 3)), None) + result = SourceSpatialDomainAdapter.for_value(value).value_in_payload_domain( + SourceSpatialDomainAdapter.for_value(target) + ) + adapter = SourceSpatialDomainAdapter.for_value(result) + assert isinstance(adapter.domain, VolumeSourceSpatialDomain) + assert adapter.domain.source_depth == 3 + assert adapter.domain.origin_yx == (1, 2) + np.testing.assert_array_equal(adapter.array, pixels[:, 1:3, 2:5]) + if not as_labels: + np.testing.assert_array_equal(result.mask, mask[:, 1:3, 2:5]) From 13bb65b10888ec49f184b0b234fdaded9c15e6c5 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 20:15:36 -0400 Subject: [PATCH 173/235] Preserve native source calibration through viewer receipt replay Refs #134. Retain the existing SourceVoxelSpacing owner in payload summaries and replayed source projections. Move axis calibration projection from NapariAxisPresentation to SourceVoxelSpacing and use it for both native presentation and receipt scale validation. Legacy unspecified-calibration receipts remain identity-only. Actual full TIFF reopen and public receipt codecs preserve physical/relative units and correlated plane order; 54 controls pass with crop, translation, axis and scale conflict guards. --- openhcs/agent/dto/viewer.py | 14 +++- .../agent/services/plate_streaming_service.py | 1 + openhcs/core/source_metadata.py | 24 ++++++ openhcs/runtime/napari_streaming_handlers.py | 25 ++---- openhcs/runtime/napari_viewer_server.py | 1 + openhcs/runtime/viewer_protocol.py | 8 ++ tests/unit/test_viewer_receipt_replay.py | 78 +++++++++++++++++-- 7 files changed, 124 insertions(+), 27 deletions(-) diff --git a/openhcs/agent/dto/viewer.py b/openhcs/agent/dto/viewer.py index c5f0f38a98..bf5b68f0f4 100644 --- a/openhcs/agent/dto/viewer.py +++ b/openhcs/agent/dto/viewer.py @@ -1242,12 +1242,24 @@ def image_payload_binding_for( len(transform.scale) != self.viewer_ndim or len(transform.translate) != self.viewer_ndim or not transform.scale - or any(value != 1.0 for value in transform.scale) or any(value != 0.0 for value in transform.translate) ): raise ValueError( "Image receipt requires explicit identity pixel placement." ) + spacing = summary.voxel_spacing + if spacing.has_values: + if len(layer.axis_labels) != self.viewer_ndim: + raise ValueError( + "Calibrated image receipt requires its complete semantic axes." + ) + expected_scale = spacing.layer_coordinate_kwargs(layer.axis_labels)["scale"] + else: + expected_scale = (1.0,) * self.viewer_ndim + if transform.scale != expected_scale: + raise ValueError( + "Image receipt transform conflicts with its declared source calibration." + ) summary.require_full_image_window() return ( ViewerWindowPayloadRecord( diff --git a/openhcs/agent/services/plate_streaming_service.py b/openhcs/agent/services/plate_streaming_service.py index f915edf043..7d55a613a3 100644 --- a/openhcs/agent/services/plate_streaming_service.py +++ b/openhcs/agent/services/plate_streaming_service.py @@ -429,6 +429,7 @@ def _receipt_source_projection( origin_yx=domain.origin_yx, source_shape_yx=domain.source_shape_yx, ), + source_voxel_spacing=record.summary.voxel_spacing, ), ) builder.record_workspace_source_path(image_path, projection.ref) diff --git a/openhcs/core/source_metadata.py b/openhcs/core/source_metadata.py index 06fc651d4a..d13f23bb34 100644 --- a/openhcs/core/source_metadata.py +++ b/openhcs/core/source_metadata.py @@ -966,6 +966,30 @@ def spacing_for_ndim(self, ndim: int) -> tuple[float, ...]: ) return self.values_zyx[-ndim:] + def layer_coordinate_kwargs( + self, axis_labels: Sequence[str] + ) -> dict[str, tuple[float, ...] | tuple[str, ...]]: + """Project source calibration onto semantic layer axes and payload bands.""" + labels = tuple(axis_labels) + if ( + len(labels) < 2 + or labels[-2:] != ("y", "x") + or len(set(labels)) != len(labels) + ): + raise ValueError( + "Source calibration requires unique semantic axes ending in Y/X." + ) + scale = [1.0] * len(labels) + units = ["dimensionless"] * len(labels) + scale[-2:] = self.spacing_for_ndim(2) + units[-2:] = (self.native_coordinate_unit,) * 2 + z_component = AllComponents.Z_INDEX.value + if len(self.values_zyx) == 3 and z_component in labels: + z_axis = labels.index(z_component) + scale[z_axis] = self.spacing_for_ndim(3)[0] + units[z_axis] = self.native_coordinate_unit + return {"scale": tuple(scale), "units": tuple(units)} + @property def native_coordinate_unit(self) -> str: """Unknown calibration is pixels, never an inferred physical unit.""" diff --git a/openhcs/runtime/napari_streaming_handlers.py b/openhcs/runtime/napari_streaming_handlers.py index aee44ed577..08035a1003 100644 --- a/openhcs/runtime/napari_streaming_handlers.py +++ b/openhcs/runtime/napari_streaming_handlers.py @@ -1511,28 +1511,13 @@ def spatial_layer_kwargs( raise ValueError( "A native viewer route requires consistent source voxel spacing." ) - component_scales = [1.0] * len(self.display_axis_components) - component_units = ["dimensionless"] * len(self.display_axis_components) - z_component = AllComponents.Z_INDEX.value - if ( - len(spacing.values_zyx) == 3 - and z_component in self.display_axis_components - ): - z_axis = self.display_axis_components.index(z_component) - component_scales[z_axis] = spacing.spacing_for_ndim(3)[0] - component_units[z_axis] = spacing.native_coordinate_unit - scale = ( - *component_scales, - *(1.0 for _ in payload_axis_labels), - *spacing.spacing_for_ndim(2), + coordinate_kwargs = spacing.layer_coordinate_kwargs( + (*self.display_axis_components, *payload_axis_labels, "y", "x") ) return { - "scale": scale, - "translate": self.translate(payload_axis_labels, scale=scale), - "units": ( - *component_units, - *("dimensionless" for _ in payload_axis_labels), - *(spacing.native_coordinate_unit for _ in range(2)), + **coordinate_kwargs, + "translate": self.translate( + payload_axis_labels, scale=coordinate_kwargs["scale"] ), } diff --git a/openhcs/runtime/napari_viewer_server.py b/openhcs/runtime/napari_viewer_server.py index 94becdb54b..8c43b87691 100644 --- a/openhcs/runtime/napari_viewer_server.py +++ b/openhcs/runtime/napari_viewer_server.py @@ -4146,6 +4146,7 @@ def payload_summary( ViewerPayloadField.PATH.value: item.address.path, ViewerPayloadField.COMPONENTS.value: dict(components), "payload_type": type(data).__name__, + "source_voxel_spacing": to_jsonable(item.image_metadata.source_voxel_spacing), } summary.update( item.image_metadata.source_spatial_domain.to_viewer_wire_mapping() diff --git a/openhcs/runtime/viewer_protocol.py b/openhcs/runtime/viewer_protocol.py index 2404c162b4..6f2c8ee260 100644 --- a/openhcs/runtime/viewer_protocol.py +++ b/openhcs/runtime/viewer_protocol.py @@ -65,6 +65,7 @@ ViewerTransportEndpoint, ) +from openhcs.core.source_metadata import SourceVoxelSpacing from openhcs.core.execution_visualizer import ExecutionVisualizerABC from openhcs.core.streaming_config_declarations import ViewerType from openhcs.core.streaming_config_factory import ( @@ -326,9 +327,16 @@ def wire_overrides(self) -> JsonObject: @dataclass(frozen=True, kw_only=True) class ViewerSourceSpatialSummary(ViewerProjectionRecord): + source_voxel_spacing: SourceVoxelSpacing | None | ViewerFieldAbsent = VIEWER_FIELD_ABSENT + spatial_origin_yx: tuple[StrictInt, StrictInt] | None | ViewerFieldAbsent = VIEWER_FIELD_ABSENT source_spatial_shape_yx: tuple[StrictInt, StrictInt] | None | ViewerFieldAbsent = VIEWER_FIELD_ABSENT + @property + def voxel_spacing(self) -> SourceVoxelSpacing: + """Retain declared calibration, with unspecified spacing for legacy receipts.""" + return self.optional(self.source_voxel_spacing) or SourceVoxelSpacing() + @property def source_domain(self) -> ViewerSourceSpatialDomainPayload: return ViewerSourceSpatialDomainPayload( diff --git a/tests/unit/test_viewer_receipt_replay.py b/tests/unit/test_viewer_receipt_replay.py index b8b5a21e74..4db21ace9f 100644 --- a/tests/unit/test_viewer_receipt_replay.py +++ b/tests/unit/test_viewer_receipt_replay.py @@ -35,7 +35,7 @@ from openhcs.constants.constants import AllComponents from openhcs.core.runtime_image_values import image_payload_metadata, image_payload_data from openhcs.core.source_image_provenance import SourceImageProvenancePlanes -from openhcs.core.source_metadata import SourceVoxelSpacing +from openhcs.core.source_metadata import SourceVoxelSpacing, SourceVoxelSpacingUnit from openhcs.core.streaming_config_declarations import ViewerType from openhcs.core.viewer_streaming_service import ViewerStreamingSource from openhcs.core.plate_file_inventory import PlateFileKind @@ -173,7 +173,14 @@ def test_observed_and_declared_wire_domains_have_distinct_order_contracts(): @pytest.mark.parametrize("historical_dtype", [None, "int32"]) -def test_native_persisted_aggregate_source_projection_preserves_order(tmp_path, historical_dtype): +@pytest.mark.parametrize("spacing", ( + SourceVoxelSpacing(), + SourceVoxelSpacing((2.0, 0.65, 0.65)), + SourceVoxelSpacing((2.0, 1.0, 0.75), SourceVoxelSpacingUnit.RELATIVE), +)) +def test_native_persisted_aggregate_source_projection_preserves_order( + tmp_path, historical_dtype, spacing +): path = tmp_path / "aggregate.labels.tif" expected = np.arange(2 * 8 * 9, dtype=np.int64).reshape(2, 8, 9) manager = FileManager({"disk": DiskStorageBackend()}) @@ -197,9 +204,12 @@ def test_native_persisted_aggregate_source_projection_preserves_order(tmp_path, components=fixture.layers[0].payload_summaries[0].components, path=str(path), stream_layer_data_type=StreamingDataType.IMAGE, ), - image_metadata=ImagePayloadMetadata(source_spatial_domain=SourceSpatialDomain( - origin_yx=(0, 0), source_shape_yx=(8, 9), - )), + image_metadata=ImagePayloadMetadata( + source_voxel_spacing=spacing, + source_spatial_domain=SourceSpatialDomain( + origin_yx=(0, 0), source_shape_yx=(8, 9), + ), + ), plane_component_domain=ViewerComponentValueDomainPayload.from_ordered_wire_mapping( {"channel": [2, 1]}, context="synthetic native receipt", ), @@ -222,7 +232,36 @@ def test_native_persisted_aggregate_source_projection_preserves_order(tmp_path, assert summary.require_plane_components() == {"channel": (2, 1)} if historical_dtype is not None: summary = replace(summary, dtype=historical_dtype) + from openhcs.runtime.napari_streaming_handlers import NapariAxisPresentation + from openhcs.runtime.viewer_component_system import ViewerLayerAxisProjection + + presentation = NapariAxisPresentation( + entries=native_semantics.entries, + layout=native_semantics.layout, + route_key=fixture.layers[0].route_key, + projection=ViewerLayerAxisProjection( + projected_axis_components=("channel",), + component_values={"channel": [1, 2]}, + routed_component_values={"channel": [1, 2]}, + axis_offsets=(0,), + scalar_component_values={}, + ), + ) + coordinate_kwargs = presentation.spatial_layer_kwargs((native_item,)) state = receipt_state(path, summary=summary) + state = replace( + state, + viewer_ndim=len(presentation.axis_labels), + layers=(replace( + state.layers[0], + axis_labels=presentation.axis_labels, + native_transform=ViewerNativeLayerTransform( + scale=coordinate_kwargs["scale"], + translate=coordinate_kwargs["translate"], + ), + ),), + ) + assert summary.voxel_spacing == spacing data = json.dumps(to_jsonable(state), sort_keys=True).encode() resource_path = tmp_path / "canonical-viewer-state.json" resource_path.write_bytes(data) @@ -380,7 +419,7 @@ def test_native_persisted_aggregate_source_projection_preserves_order(tmp_path, displayed = _build_nd_image_array([item], display_projection, bindings) np.testing.assert_array_equal(displayed[0], expected[1]) np.testing.assert_array_equal(displayed[1], expected[0]) - assert not metadata.source_voxel_spacing.has_values + assert metadata.source_voxel_spacing == spacing assert metadata.source_channel_axis is None with pytest.raises(ValueError, match="window conflicts"): source.require_projected_image_window( @@ -498,3 +537,30 @@ def stream_files(self, request, *, ui_bridge_connection): assert admitted_request.source_receipt == receipt assert admitted_request.connection.port == 5631 assert admitted_connection.descriptor_file_path == str(tmp_path / "exact-ui.json") + + +@pytest.mark.parametrize("change", ("scale", "missing_axes", "duplicate_axes", "translation", "crop")) +def test_calibrated_receipt_rejects_conflicting_native_placement(tmp_path, change): + spacing = SourceVoxelSpacing((2.0, 0.65, 0.65)) + state = receipt_state(tmp_path / "aggregate.labels.tif") + summary = replace(state.layers[0].payload_summaries[0], source_voxel_spacing=spacing) + axes = ("channel", "z_index", "timepoint", "site", "well", "y", "x") + transform = ViewerNativeLayerTransform( + scale=spacing.layer_coordinate_kwargs(axes)["scale"], translate=(0,) * 7 + ) + if change == "scale": + transform = replace(transform, scale=(1,) * 7) + elif change == "translation": + transform = replace(transform, translate=(0, 0, 0, 0, 0, 1, 0)) + elif change == "missing_axes": + axes = () + elif change == "duplicate_axes": + axes = ("z_index", "z_index", "timepoint", "site", "well", "y", "x") + else: + summary = replace(summary, spatial_origin_yx=(1, 0)) + state = replace(state, layers=(replace( + state.layers[0], axis_labels=axes, + payload_summaries=(summary,), native_transform=transform, + ),)) + with pytest.raises(ValueError): + state.image_payload_binding_for(str(tmp_path / "aggregate.labels.tif")) From 48257317395f712e3a9678bc0c1f8a8df5f2377a Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 20:21:07 -0400 Subject: [PATCH 174/235] Compose runtime source provenance on its existing owner Refs #384; remove measurement metadata round trips and duplicate image-composer provenance algorithms. --- openhcs/core/runtime_image_values.py | 160 +++--------------- openhcs/core/source_image_provenance.py | 136 +++++++++++++++ .../cellprofiler/runtime/invocation.py | 112 +++++++++--- .../test_cellprofiler_module_execution.py | 125 ++++++++++++++ 4 files changed, 372 insertions(+), 161 deletions(-) diff --git a/openhcs/core/runtime_image_values.py b/openhcs/core/runtime_image_values.py index b2076db2a6..19feb18eb0 100644 --- a/openhcs/core/runtime_image_values.py +++ b/openhcs/core/runtime_image_values.py @@ -10,7 +10,6 @@ from dataclasses import dataclass, field, fields from enum import Enum from pathlib import Path -from types import MappingProxyType from typing import Any, TypeVar import numpy as np @@ -36,20 +35,13 @@ RuntimePlaneAxisValueProjection, ) from openhcs.core.source_image_provenance import ( - RuntimeSourceImageProvenancePlane, SourceComponentMetadata, - SourceImageIdentity, SourceImageProvenance, SourceImageProvenanceFields, - SourceImageProvenancePlanes, SourcePlaneIndexedProvenanceExpansion, - common_source_component_metadata, ) from openhcs.core.source_metadata import ( SourceMetadataScalar, - OwnedSourceMetadataFields, - SourceMetadataFields, - SourceMetadataValue, SourceVoxelSpacing, SourceVoxelSpacingFields, ) @@ -723,7 +715,7 @@ def source_plane_metadata_count(self) -> int: len(self.source_plane_unit_interval_intensity_scales), ) - def source_plane_metadata_records(self) -> tuple["ImagePayloadMetadata", ...]: + def source_metadata_by_payload(self) -> tuple["ImagePayloadMetadata", ...]: """Return one scalar metadata record per represented source plane.""" if ( self.source_plane_metadata_count == 1 @@ -1754,9 +1746,24 @@ def compose(self) -> ImagePayloadMetadata: self.source_plane_metadata_for_payload(metadata) for metadata in metadata_by_payload ) - source_plane_metadata_records = source_metadata_by_payload - composed_source_provenance_planes = self.composed_source_provenance_planes( - source_plane_metadata_records + compose_provenance = ( + SourceImageProvenance.stack + if self.mode is ImagePayloadMetadataCompositionMode.STACK + else SourceImageProvenance.bundle + ) + source_provenance = compose_provenance( + tuple( + metadata.source_provenance for metadata in source_metadata_by_payload + ), + scalar_sources=tuple( + metadata.source_provenance for metadata in metadata_by_payload + ), + preserve_single_topology=( + len(source_metadata_by_payload) == 1 + and self.mode.preserves_plane_topology( + source_metadata_by_payload[0].plane_axis + ) + ), ) common_source_voxel_spacing = self.common_metadata_value( metadata.source_voxel_spacing @@ -1765,34 +1772,16 @@ def compose(self) -> ImagePayloadMetadata: ) if common_source_voxel_spacing is None: common_source_voxel_spacing = SourceVoxelSpacing() - source_component_metadata_by_payload = tuple( - ( - metadata - if metadata.source_component_metadata is not None - else source_metadata - ) - for metadata, source_metadata in zip( - metadata_by_payload, - source_metadata_by_payload, - strict=True, - ) - ) return self.metadata_type( - source_path=self.common_metadata_value( - metadata.source_path for metadata in source_metadata_by_payload - ), - source_component_metadata=self.common_source_component_metadata( - source_component_metadata_by_payload - ), + source_provenance=source_provenance, source_plane_intensity_scales=tuple( - metadata.intensity_scale for metadata in source_plane_metadata_records + metadata.intensity_scale for metadata in source_metadata_by_payload ), source_plane_dtypes=tuple( - metadata.source_dtype for metadata in source_plane_metadata_records + metadata.source_dtype for metadata in source_metadata_by_payload ), - source_image_provenance_planes=composed_source_provenance_planes, unit_interval_intensity=self.composed_unit_interval_intensity( - source_plane_metadata_records + source_metadata_by_payload ), source_spatial_domain=SourceSpatialDomain.common_from_domains( metadata.source_spatial_domain for metadata in metadata_by_payload @@ -1804,33 +1793,10 @@ def compose(self) -> ImagePayloadMetadata: mask_defines_border=self.common_metadata_value( metadata.mask_defines_border for metadata in metadata_by_payload ), - source_image_names=( - composed_source_provenance_planes.runtime_source_image_names - ), source_channel_axis=self.composed_source_channel_axis(metadata_by_payload), plane_axis=self.mode.plane_axis, ) - def composed_source_provenance_planes( - self, - metadata_records: tuple[ImagePayloadMetadata, ...], - ) -> SourceImageProvenancePlanes: - """Compose projectable planes without replacing compatible topology.""" - - if len(metadata_records) == 1: - metadata = metadata_records[0] - provenance_planes = metadata.source_image_provenance_planes - if provenance_planes.count > 1 and self.mode.preserves_plane_topology( - metadata.plane_axis - ): - return provenance_planes - return SourceImageProvenancePlanes( - tuple( - self.runtime_source_provenance_plane(metadata) - for metadata in metadata_records - ) - ) - @staticmethod def composed_unit_interval_intensity( metadata_records: Sequence[ImagePayloadMetadata], @@ -1848,86 +1814,6 @@ def composed_unit_interval_intensity( ) ) - @staticmethod - def runtime_source_provenance_plane( - metadata: ImagePayloadMetadata, - ) -> RuntimeSourceImageProvenancePlane: - """Return one projectable plane with nested non-projectable contributors.""" - source_image_names = metadata.source_image_names - contributors = metadata.source_image_provenance_planes.as_contributors( - source_image_names - ).planes - if len(source_image_names) > 1: - raise ValueError( - "Composed image payload provenance permits at most one " - f"source alias per scalar plane, got {source_image_names!r}." - ) - source_image_name = source_image_names[0] if source_image_names else None - return RuntimeSourceImageProvenancePlane( - SourceImageIdentity( - metadata.source_path, - metadata.source_component_metadata, - ), - contributors, - source_image_name, - ) - - def common_source_component_metadata( - self, - values: Iterable[ImagePayloadMetadata], - ) -> SourceComponentMetadata | None: - """Return source metadata shared by the composed payload.""" - metadata_values = tuple(values) - metadata_by_plane = tuple( - SourceMetadataFields.composition_snapshot( - metadata.source_component_metadata - ) - for metadata in metadata_values - if metadata.source_component_metadata is not None - ) - if self.mode is ImagePayloadMetadataCompositionMode.BUNDLE: - field_names = set().union( - *(metadata.keys() for metadata in metadata_by_plane) - ) - common_metadata: dict[str, SourceMetadataValue] = {} - for field_name in field_names: - values_for_field = tuple( - metadata[field_name] - for metadata in metadata_by_plane - if field_name in metadata - ) - if values_for_field and all( - value == values_for_field[0] for value in values_for_field - ): - common_metadata[field_name] = values_for_field[0] - else: - common_metadata = dict( - common_source_component_metadata( - tuple( - metadata.source_component_metadata - for metadata in metadata_values - ) - ) - or {} - ) - if not common_metadata: - return None - owner = ( - metadata_by_plane[0] - if metadata_by_plane - and all( - isinstance(metadata, OwnedSourceMetadataFields) - for metadata in metadata_by_plane - ) - else {} - ) - derived = SourceMetadataFields.derived_mapping(owner, common_metadata) - return ( - derived - if isinstance(derived, OwnedSourceMetadataFields) - else MappingProxyType(derived) - ) - @staticmethod def common_metadata_value( values: Iterable[MetadataValueT | None], diff --git a/openhcs/core/source_image_provenance.py b/openhcs/core/source_image_provenance.py index 025d77b87f..dc3da05141 100644 --- a/openhcs/core/source_image_provenance.py +++ b/openhcs/core/source_image_provenance.py @@ -702,6 +702,142 @@ def __init__( self.source_image_names, ) + def as_runtime_plane(self) -> RuntimeSourceImageProvenancePlane: + """Compose one scalar source, retaining its non-projectable contributors.""" + names = self.source_image_names + contributors = self.source_image_provenance_planes.as_contributors(names).planes + if len(names) > 1: + raise ValueError( + "Composed image payload provenance permits at most one " + f"source alias per scalar plane, got {names!r}." + ) + return RuntimeSourceImageProvenancePlane( + SourceImageIdentity(self.source_path, self.source_component_metadata), + contributors, + names[0] if names else None, + ) + + @classmethod + def stack( + cls, + sources: Sequence["SourceImageProvenance"], + *, + scalar_sources: Sequence["SourceImageProvenance"] | None = None, + preserve_single_topology: bool = True, + ) -> Self: + """Compose ordered runtime sources with their common scalar coordinates.""" + return cls._compose( + tuple(sources), + scalar_sources, + preserve_single_topology=preserve_single_topology, + bundle=False, + ) + + @classmethod + def bundle( + cls, + sources: Sequence["SourceImageProvenance"], + *, + scalar_sources: Sequence["SourceImageProvenance"] | None = None, + preserve_single_topology: bool = False, + ) -> Self: + """Compose same-slice sources, retaining nonconflicting partial coordinates.""" + return cls._compose( + tuple(sources), + scalar_sources, + preserve_single_topology=preserve_single_topology, + bundle=True, + ) + + @staticmethod + def bundle_component_metadata( + sources: Sequence["SourceImageProvenance"], + *, + scalar_sources: Sequence["SourceImageProvenance"] | None = None, + ) -> SourceComponentMetadata | None: + metadata = tuple( + SourceMetadataFields.composition_snapshot(fields) + for fields in SourceImageProvenance._composition_component_metadata( + sources, scalar_sources + ) + if fields is not None + ) + common: dict[str, SourceMetadataValue] = {} + for name in set().union(*(fields.keys() for fields in metadata)): + values = tuple(fields[name] for fields in metadata if name in fields) + if values and all(value == values[0] for value in values): + common[name] = values[0] + if not common: + return None + owner = ( + metadata[0] + if metadata + and all( + isinstance(fields, OwnedSourceMetadataFields) for fields in metadata + ) + else {} + ) + derived = SourceMetadataFields.derived_mapping(owner, common) + return ( + derived + if isinstance(derived, OwnedSourceMetadataFields) + else MappingProxyType(derived) + ) + + @staticmethod + def _composition_component_metadata( + sources: Sequence["SourceImageProvenance"], + scalar_sources: Sequence["SourceImageProvenance"] | None, + ) -> tuple[SourceComponentMetadata | None, ...]: + """Retain authored scalar coordinates before inheriting selected plane facts.""" + if scalar_sources is None: + scalar_sources = sources + return tuple( + ( + scalar.source_component_metadata + if scalar.source_component_metadata is not None + else selected.source_component_metadata + ) + for scalar, selected in zip(scalar_sources, sources, strict=True) + ) + + @classmethod + def _compose( + cls, + sources: tuple["SourceImageProvenance", ...], + scalar_sources: Sequence["SourceImageProvenance"] | None, + *, + preserve_single_topology: bool, + bundle: bool, + ) -> Self: + if not sources: + raise ValueError("Source provenance composition cannot be empty.") + planes = ( + sources[0].source_image_provenance_planes + if preserve_single_topology + and len(sources) == 1 + and sources[0].source_plane_count > 1 + else SourceImageProvenancePlanes( + tuple(source.as_runtime_plane() for source in sources) + ) + ) + component_metadata = ( + cls.bundle_component_metadata(sources, scalar_sources=scalar_sources) + if bundle + else common_source_component_metadata( + cls._composition_component_metadata(sources, scalar_sources) + ) + ) + present_paths = tuple( + source.source_path for source in sources if source.source_path is not None + ) + return cls( + source_path=common_source_path(present_paths), + source_component_metadata=component_metadata, + source_image_provenance_planes=planes, + source_image_names=planes.runtime_source_image_names, + ) + @classmethod def from_init_values( cls, diff --git a/openhcs/interop/cellprofiler/runtime/invocation.py b/openhcs/interop/cellprofiler/runtime/invocation.py index fff2312c33..413ae81b4a 100644 --- a/openhcs/interop/cellprofiler/runtime/invocation.py +++ b/openhcs/interop/cellprofiler/runtime/invocation.py @@ -11,7 +11,6 @@ from openhcs.core.alias_property import AliasProperty from openhcs.core.aligned_image_payload import ( AlignedImageStack, - payload_slices_for_alignment, ) from openhcs.core.equivalence.keys import RuntimeMeasurementSourcePair from openhcs.core.measurement_image_alignment import ( @@ -36,8 +35,12 @@ from openhcs.core.runtime_plane_projection import ( RuntimePlaneAxisProjector, ) -from openhcs.core.runtime_slice_alignment import RuntimeSliceAlignedValues +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 @@ -135,35 +138,96 @@ def composed_source_metadata( return None if mode is None: mode = cls.source_metadata_composition_mode(sources) - source_payloads = tuple( - source_payload + scalar_sources = tuple( + provenance for source in sources - for source_payload in payload_slices_for_alignment(source.payload) + for provenance in cls.source_provenances(source.payload) ) - source_metadata = tuple( - ImagePayloadMetadata( - source_provenance=( - ImagePayloadMetadata.compose( - payload_slices_for_alignment(payload), - mode=ImagePayloadMetadataCompositionMode.BUNDLE, - ) - .collapse_leading_plane_axis() - .source_provenance - if isinstance(payload, AlignedImageStack) - else image_payload_metadata(payload).source_provenance - ) + if not scalar_sources: + raise ValueError("Image metadata composition payloads cannot be empty.") + provenances = tuple( + ( + provenance.for_source_plane(0) + if provenance.source_plane_count == 1 + else provenance ) - for payload in source_payloads + for provenance in scalar_sources ) - metadata = ImagePayloadMetadata.compose( - source_payloads, - mode=mode, - source_metadata=source_metadata, + 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 + ) + ) + metadata = ImagePayloadMetadata( + source_provenance=provenance, + source_plane_intensity_scales=(None,) * len(provenances), + source_plane_dtypes=(None,) * len(provenances), + plane_axis=mode.plane_axis, ) - if not metadata.has_values: - return None return metadata + @classmethod + 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(payload.value_for_slice(index)) + for index in range(payload.slice_count) + ) + count = RuntimeSliceProjection.slice_count_from_values((payload,)) + metadata = image_payload_metadata(payload) + provenance = metadata.source_provenance + if count is None: + return (provenance,) + if metadata.source_channel_axis == 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 image_payload_metadata(payload).source_provenance + metadata = tuple(image_payload_metadata(value) 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() + ) + @classmethod def source_metadata_composition_mode( cls, diff --git a/tests/unit/test_cellprofiler_module_execution.py b/tests/unit/test_cellprofiler_module_execution.py index 7f9204dabc..437752d088 100644 --- a/tests/unit/test_cellprofiler_module_execution.py +++ b/tests/unit/test_cellprofiler_module_execution.py @@ -97,6 +97,7 @@ ImagePayloadSliceProjector, ImageMetadataPayload, ImagePayloadMetadata, + ImagePayloadMetadataCompositionMode, MaskedImagePayload, image_payload_data, image_payload_mask, @@ -172,6 +173,7 @@ NamedSourceBinding, ) from openhcs.core.source_image_provenance import ( + SourceImageProvenance, SourceImageProvenancePlanes, ) from openhcs.core.source_matching import ( @@ -9268,6 +9270,13 @@ def test_measurement_images_flatten_aligned_runtime_slices_for_source_metadata() {"well": "A01", "site": "1"}, {"well": "A01", "site": "2"}, ) + assert tuple( + tuple(contributor.path for contributor in plane.contributors) + for plane in metadata.source_image_provenance_planes.planes + ) == ( + ("/plate/A01_s001_w1.tif", "/plate/A01_s001_w2.tif"), + ("/plate/A01_s002_w1.tif", "/plate/A01_s002_w2.tif"), + ) def test_measurement_images_preserve_runtime_slice_axis_across_source_aliases() -> None: @@ -9308,6 +9317,122 @@ 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), + ), +) +def test_image_source_composition_preserves_only_compatible_incoming_topology( + incoming_axis, mode, expected_planes +) -> None: + paths = ("/input/A01_s1_w1.tif", "/input/A01_s1_w2.tif") + metadata = ImagePayloadMetadata( + source_image_provenance_planes=SourceImageProvenancePlanes.from_components( + paths=paths, + component_metadata=({"channel": "1"}, {"channel": "2"}), + ), + plane_axis=incoming_axis, + ) + payload = metadata.payload_with(np.zeros((2, 4, 5), dtype=np.float32)) + + composed = ImagePayloadMetadata.compose((payload,), mode=mode) + + planes = composed.source_image_provenance_planes + assert planes.count == expected_planes + if expected_planes == 2: + assert planes.paths == paths + else: + assert tuple(contributor.path for contributor in planes.plane(0).contributors) == paths + + +def test_measurement_source_composition_reads_dense_plane_facts_without_pixel_projection( + monkeypatch, +) -> None: + paths = tuple(f"/input/A01_s1_w2_z{index + 1}.tif" for index in range(60)) + + def provenance(channel, alias): + return SourceImageProvenance( + source_image_provenance_planes=SourceImageProvenancePlanes.from_components( + paths=paths, + component_metadata=tuple( + { + "well": "A01", + "site": "1", + "channel": str(channel), + "z_index": str(index + 1), + } + for index in range(60) + ), + ), + source_image_names=(alias,), + ) + + metadata = ImagePayloadMetadata( + source_provenance=provenance(2, "OrigBlue"), + plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, + ) + source = CellProfilerMeasurementImage( + source_image_name="OrigBlue", + payload=metadata.payload_with(np.zeros((60, 4, 5), dtype=np.float32)), + ) + + def reject_image_projection(*args, **kwargs): + raise AssertionError( + "Source facts must not require image or metadata projection" + ) + + monkeypatch.setattr( + RuntimeSliceProjection, "value_for_slice", reject_image_projection + ) + monkeypatch.setattr(ImagePayloadMetadata, "compose", reject_image_projection) + monkeypatch.setattr( + ImagePayloadMetadata, "for_leading_source_plane", reject_image_projection + ) + + first = CellProfilerMeasurementImage.composed_source_metadata((source,)) + assert first.source_image_provenance_planes.paths == paths + assert first.source_image_names == ("OrigBlue",) * 60 + assert tuple( + values["z_index"] + for values in first.source_image_provenance_planes.component_metadata + ) == tuple(str(index + 1) for index in range(60)) + + metadata.source_provenance = provenance(3, "OrigGreen") + second = CellProfilerMeasurementImage.composed_source_metadata((source,)) + assert second.source_image_names == ("OrigGreen",) * 60 + assert second.source_component_metadata["channel"] == "3" + assert first.source_component_metadata["channel"] == "2" + + +def test_measurement_source_composition_retains_authored_scalar_coordinates() -> None: + metadata = ImagePayloadMetadata( + source_component_metadata={"well": "A01"}, + source_image_provenance_planes=SourceImageProvenancePlanes.from_components( + paths=("/input/A01_s1_w2.tif",), + component_metadata=({"well": "A01", "site": "1", "channel": "2"},), + ), + source_image_names=("OrigBlue",), + ) + source = CellProfilerMeasurementImage( + source_image_name="OrigBlue", + payload=metadata.payload_with(np.zeros((4, 5), dtype=np.float32)), + ) + + composed = CellProfilerMeasurementImage.composed_source_metadata((source,)) + + assert dict(composed.source_component_metadata) == {"well": "A01"} + assert composed.source_image_provenance_planes.paths == ("/input/A01_s1_w2.tif",) + assert composed.source_image_provenance_planes.component_metadata == ( + {"well": "A01", "site": "1", "channel": "2"}, + ) + + def test_measurement_tables_compose_provenance_after_object_rows_clear_source() -> None: first_metadata = ImagePayloadMetadata( source_image_provenance_planes=SourceImageProvenancePlanes.from_components( From 31211161401b3dfaa79ac628e43f618f6ce0b827 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 20:39:03 -0400 Subject: [PATCH 175/235] Load selected runtime cohorts through their source-file owner Refs #419 and #394. Remove the separate whole-axis preload stage, file-set carrier and callback getter. PatternGroupRuntime selects the exact input paths and SourceFileUniverse loads missing source images once into the execution memory backend, returning those payloads directly. Producer records retain their actual memory owner. Source metadata stays with ImagePayloadSourceMetadataContext. Existing BioFormats, source binding, Zarr/layout, compiled-plan and producer controls pass; the actual first-step source binding control confirms the unselected channel is not loaded. No end-to-end timing attribution yet. --- openhcs/core/runtime_image_loading.py | 8 + openhcs/core/source_binding_selection.py | 46 +++++ openhcs/core/steps/function_execution.py | 58 ------ openhcs/core/steps/function_io.py | 169 +----------------- openhcs/core/steps/function_runtime.py | 11 +- .../test_bioformats_handler_runtime.py | 15 +- tests/unit/test_function_io.py | 32 +--- ...test_function_runtime_source_projection.py | 10 +- .../unit/test_function_step_execution_plan.py | 3 - 9 files changed, 83 insertions(+), 269 deletions(-) diff --git a/openhcs/core/runtime_image_loading.py b/openhcs/core/runtime_image_loading.py index ac70edff4d..aac6f04af4 100644 --- a/openhcs/core/runtime_image_loading.py +++ b/openhcs/core/runtime_image_loading.py @@ -17,6 +17,7 @@ ImagePayloadMetadataCarrier, image_payload_data, image_payload_metadata, + image_payload_mask, ) from openhcs.core.runtime_plane_projection import RuntimePlaneAxis from openhcs.core.source_image_provenance import ( @@ -51,6 +52,13 @@ def source_path(self) -> str: raise ValueError("ImagePayloadSourceMetadataContext requires source path.") return self.source_identity.path + def payload(self, image: Any) -> ImagePayloadMetadataCarrier: + """Keep loader metadata, deriving source facts only for bare pixels.""" + metadata = image_payload_metadata(image) + if not metadata.has_values: + metadata = self.metadata(image) + return metadata.payload_with(image_payload_data(image), image_payload_mask(image)) + def metadata( self, image: Any, diff --git a/openhcs/core/source_binding_selection.py b/openhcs/core/source_binding_selection.py index 4ded7df016..a7124d9763 100644 --- a/openhcs/core/source_binding_selection.py +++ b/openhcs/core/source_binding_selection.py @@ -58,9 +58,12 @@ from openhcs.core.source_projection import SourceProjection from openhcs.core.source_workspace_projection import VirtualWorkspaceSourceProjection 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 if TYPE_CHECKING: + from polystore.filemanager import FileManager from openhcs.core.context.processing_context import ProcessingContext from openhcs.microscopes.microscope_interfaces import FilenameParser @@ -1515,6 +1518,49 @@ class SourceFileUniverse: files: tuple[str, ...] backend: Backend + def load_images( + self, + filemanager: "FileManager", + *, + zarr_config: Mapping[str, object] | None = None, + ) -> list[RuntimeArrayData]: + """Load this exact source cohort, retaining its execution-local memory copy.""" + if self.backend is Backend.MEMORY: + return filemanager.load_batch(list(self.files), self.backend.value) + missing = tuple(dict.fromkeys( + path for path in self.files + if not filemanager.exists(path, Backend.MEMORY.value) + )) + loaded_by_path = {} + if missing: + pixels = filemanager.load_batch( + list(missing), self.backend.value, + **({"zarr_config": zarr_config} if self.backend is Backend.ZARR else {}), + ) + loaded_by_path.update( + (path, ImagePayloadSourceMetadataContext( + SourceImageIdentity(path), + read_backend=self.backend.value, + filemanager=filemanager, + ).payload(image)) + for path, image in zip(missing, pixels, strict=True) + ) + for parent in dict.fromkeys(str(Path(path).parent) for path in missing): + filemanager.ensure_directory(parent, Backend.MEMORY.value) + filemanager.save_batch( + list(loaded_by_path.values()), list(loaded_by_path), Backend.MEMORY.value, + ) + retained = tuple(dict.fromkeys( + path for path in self.files if path not in loaded_by_path + )) + if retained: + loaded_by_path.update(zip( + retained, + filemanager.load_batch(list(retained), Backend.MEMORY.value), + strict=True, + )) + return [loaded_by_path[path] for path in self.files] + @dataclass(frozen=True, slots=True) class SourceUniverseRuntimeState: diff --git a/openhcs/core/steps/function_execution.py b/openhcs/core/steps/function_execution.py index 28fb32c4a5..6a3c9b07c0 100644 --- a/openhcs/core/steps/function_execution.py +++ b/openhcs/core/steps/function_execution.py @@ -11,8 +11,6 @@ from itertools import zip_longest from typing import TYPE_CHECKING -import psutil - from openhcs.constants import MULTIPROCESSING_AXIS from openhcs.constants.constants import ( LOADABLE_IMAGE_EXTENSIONS, @@ -45,7 +43,6 @@ ) from openhcs.core.step_dependencies import StepInputDependencyKind from openhcs.core.steps.function_io import ( - bulk_preload_step_images, generate_materialized_paths, get_all_image_paths, save_materialized_data, @@ -890,12 +887,6 @@ def run(self) -> StepExecutionObservation: "step_prepare_groups", time.perf_counter() - phase_started_at, ) - phase_started_at = time.perf_counter() - self._preload_inputs_if_needed(grouped_patterns) - self.record_runtime_profile( - "step_preload_inputs", - time.perf_counter() - phase_started_at, - ) if not execution_requests: raise ValueError( f"No execution cohorts found for step {plan.step_index} " @@ -1182,55 +1173,6 @@ def _prepare_groups( ) return grouped_patterns - def _preload_inputs_if_needed( - self, - grouped_patterns: PatternGroups, - ) -> None: - plan = self.plan - if plan.read_backend == Backend.MEMORY.value: - return - - process = psutil.Process(os.getpid()) - mem_before_mb = process.memory_info().rss / 1024 / 1024 - logger.debug("Memory before preload: %.1f MB RSS", mem_before_mb) - - if plan.sequential_filter_plan.enabled: - patterns_to_preload = [ - pattern - for pattern_list in grouped_patterns.values() - for pattern in pattern_list - ] - logger.info( - "Sequential mode: preloading %s filtered patterns", - len(patterns_to_preload), - ) - bulk_preload_step_images( - plan.input_dir, - plan.axis_id, - plan.read_backend, - self.context.filemanager, - self.context.microscope_handler, - plan.zarr_config, - patterns_to_preload=patterns_to_preload, - variable_components=plan.variable_component_values, - ) - else: - bulk_preload_step_images( - plan.input_dir, - plan.axis_id, - plan.read_backend, - self.context.filemanager, - self.context.microscope_handler, - plan.zarr_config, - ) - - mem_after_mb = process.memory_info().rss / 1024 / 1024 - logger.debug( - "Memory after preload: %.1f MB RSS (+%.1f MB)", - mem_after_mb, - mem_after_mb - mem_before_mb, - ) - def _execute_pattern_groups( self, execution_requests: Sequence[PatternGroupExecutionRequest], diff --git a/openhcs/core/steps/function_io.py b/openhcs/core/steps/function_io.py index f1510b60f2..caa50aaba6 100644 --- a/openhcs/core/steps/function_io.py +++ b/openhcs/core/steps/function_io.py @@ -7,7 +7,7 @@ from abc import ABC from dataclasses import dataclass, replace from pathlib import Path, PurePosixPath -from typing import TYPE_CHECKING, Callable, ClassVar, Mapping, Sequence, TypeAlias +from typing import TYPE_CHECKING, ClassVar, Mapping, Sequence, TypeAlias from metaclass_registry import AutoRegisterMeta from polystore.zarr_batch import ZarrBatchAxis, ZarrBatchAxisRole, ZarrBatchLayout @@ -24,13 +24,9 @@ ) from openhcs.core.registry_strategies import EnumKeyedStrategyMixin from openhcs.core.runtime_array_values import RuntimeArrayData -from openhcs.core.runtime_image_loading import ImagePayloadSourceMetadataContext from openhcs.core.runtime_image_values import ( image_payload_data, - image_payload_mask, - image_payload_metadata, ) -from openhcs.core.source_image_provenance import SourceImageIdentity from openhcs.core.steps.function_output_identity import FunctionOutputIdentity if TYPE_CHECKING: @@ -65,79 +61,6 @@ def prepare_storage_image_payloads( return [image_payload_data(payload) for payload in payloads] -@dataclass(frozen=True, slots=True) -class StepPreloadFileSet: - """Execution-scoped source files that must be available in memory backend.""" - - paths: tuple[str, ...] - - @classmethod - def from_paths(cls, paths: Sequence[str | Path]) -> "StepPreloadFileSet": - """Return a deterministic file set while preserving first-seen order.""" - return cls(tuple(dict.fromkeys(str(path) for path in paths))) - - def missing_memory_paths( - self, - filemanager: FileManager, - ) -> tuple[str, ...]: - """Return source paths not already copied into the execution memory backend.""" - return tuple( - path - for path in self.paths - if not filemanager.exists(path, Backend.MEMORY.value) - ) - - def load_missing_payloads( - self, - *, - filemanager: FileManager, - read_backend: str, - zarr_config: ZarrBackendConfig | None, - ) -> tuple[tuple[str, ...], list[RuntimeArrayData]]: - """Load missing source payloads and wrap them with source metadata.""" - missing_paths = self.missing_memory_paths(filemanager) - if not missing_paths: - return (), [] - if read_backend == Backend.ZARR.value: - raw_images = filemanager.load_batch( - list(missing_paths), - read_backend, - zarr_config=zarr_config, - ) - else: - raw_images = filemanager.load_batch(list(missing_paths), read_backend) - return missing_paths, [ - _preloaded_image_payload( - image, - source_path=file_path, - read_backend=read_backend, - filemanager=filemanager, - ) - for image, file_path in zip(raw_images, missing_paths, strict=True) - ] - - -def _preloaded_image_payload( - image: RuntimeArrayData, - *, - source_path: str, - read_backend: str, - filemanager: FileManager, -) -> RuntimeArrayData: - """Preserve loader-owned metadata or derive it through the source backend.""" - metadata = image_payload_metadata(image) - if not metadata.has_values: - metadata = ImagePayloadSourceMetadataContext( - SourceImageIdentity(source_path), - read_backend=read_backend, - filemanager=filemanager, - ).metadata(image) - return metadata.payload_with( - image_payload_data(image), - image_payload_mask(image), - ) - - def generate_materialized_paths( memory_paths: Sequence[str], step_output_dir: Path, @@ -447,96 +370,6 @@ def get_all_image_paths( return full_file_paths -def create_image_path_getter( - axis_id: str, - filemanager: FileManager, - microscope_handler: MicroscopeHandler, -) -> Callable[[str | Path, str], list[str]]: - """Create a path getter bound to one multiprocessing axis value.""" - - def get_paths_for_axis(input_dir: str | Path, backend: str) -> list[str]: - return get_all_image_paths( - input_dir=input_dir, - axis_id=axis_id, - backend=backend, - filemanager=filemanager, - microscope_handler=microscope_handler, - ) - - return get_paths_for_axis - - -def bulk_preload_step_images( - step_input_dir: Path, - axis_id: str, - read_backend: str, - filemanager: FileManager, - microscope_handler: MicroscopeHandler, - zarr_config: ZarrBackendConfig | None = None, - patterns_to_preload: Sequence[str] | None = None, - variable_components: Sequence[str] | None = None, -) -> None: - """Preload this step's images from the source backend into the memory backend.""" - if patterns_to_preload is not None: - all_files = ( - file_path - for pattern in patterns_to_preload - for file_path in microscope_handler.path_list_from_pattern( - str(step_input_dir), - pattern, - filemanager, - read_backend, - variable_components, - ) - ) - full_file_paths = ( - ( - str(step_input_dir / file_path) - if not Path(file_path).is_absolute() - else str(file_path) - ) - for file_path in all_files - ) - else: - get_paths_for_axis = create_image_path_getter( - axis_id, filemanager, microscope_handler - ) - full_file_paths = get_paths_for_axis(step_input_dir, read_backend) - - preload_file_set = StepPreloadFileSet.from_paths(full_file_paths) - - if not preload_file_set.paths: - raise RuntimeError( - f"Bulk preload found no files for axis {axis_id} in {step_input_dir} " - f"with backend {read_backend}." - ) - - filemanager.ensure_directory(str(step_input_dir), Backend.MEMORY.value) - for parent in dict.fromkeys( - str(Path(path).parent) for path in preload_file_set.paths - ): - filemanager.ensure_directory(parent, Backend.MEMORY.value) - missing_paths, raw_images = preload_file_set.load_missing_payloads( - filemanager=filemanager, - read_backend=read_backend, - zarr_config=zarr_config, - ) - if not missing_paths: - logger.debug( - "Bulk preload reused %s memory-backed files for axis %s", - len(preload_file_set.paths), - axis_id, - ) - return - logger.debug( - "Bulk preload loading %s/%s files for axis %s", - len(missing_paths), - len(preload_file_set.paths), - axis_id, - ) - filemanager.save_batch(list(raw_images), list(missing_paths), Backend.MEMORY.value) - - def update_metadata_for_zarr_conversion( plate_root: Path, original_subdir: str, diff --git a/openhcs/core/steps/function_runtime.py b/openhcs/core/steps/function_runtime.py index c163985948..a93b49574b 100644 --- a/openhcs/core/steps/function_runtime.py +++ b/openhcs/core/steps/function_runtime.py @@ -121,6 +121,7 @@ from openhcs.core.source_binding_selection import ( SourceBindingCandidateMatcher, SourceBindingMatchedImageSet, + SourceFileUniverse, SourceUniverseRequest, SourcePatternResolutionContext, ) @@ -2557,7 +2558,7 @@ def _load_input_stack( str(plan.input_dir), request.pattern_group_info, context.filemanager, - Backend.MEMORY.value, + plan.read_backend, ( [component.value for component in plan.variable_components] if plan.variable_components @@ -2642,10 +2643,10 @@ def _load_input_stack( memory_type=plan.input_memory_type, ) if cached_stack is None: - raw_slices = context.filemanager.load_batch( - full_file_paths, - Backend.MEMORY.value, - ) + raw_slices = SourceFileUniverse( + tuple(full_file_paths), + Backend.MEMORY if producer_matching_files else Backend(plan.read_backend), + ).load_images(context.filemanager, zarr_config=plan.zarr_config) if source_projection is not None or not producer_matching_files: raw_slices = self._apply_source_image_loading_semantics( raw_slices, diff --git a/tests/integration/test_bioformats_handler_runtime.py b/tests/integration/test_bioformats_handler_runtime.py index 2dbe39259c..e4f6f3409a 100644 --- a/tests/integration/test_bioformats_handler_runtime.py +++ b/tests/integration/test_bioformats_handler_runtime.py @@ -3,7 +3,7 @@ import numpy as np from openhcs.constants.constants import Backend -from openhcs.core.steps.function_io import bulk_preload_step_images +from openhcs.core.source_binding_selection import SourceFileUniverse from openhcs.microscopes.bioformats import BioFormatsHandler from tests.unit.bioformats_fixture import ( bioformats_filemanager, @@ -11,7 +11,7 @@ ) -def test_bioformats_handler_preloads_planes_through_runtime_path( +def test_bioformats_handler_loads_selected_planes_through_runtime_path( tmp_path: Path, ) -> None: stack = write_bioformats_manifest_fixture(tmp_path) @@ -19,14 +19,9 @@ def test_bioformats_handler_preloads_planes_through_runtime_path( handler = BioFormatsHandler(filemanager) handler.initialize_workspace(tmp_path, filemanager) - bulk_preload_step_images( - step_input_dir=tmp_path, - axis_id="A01", - read_backend=Backend.VIRTUAL_WORKSPACE.value, - filemanager=filemanager, - microscope_handler=handler, - patterns_to_preload=("A01_s001_w1_z001_t001.tif",), - ) + SourceFileUniverse( + (str(tmp_path / "A01_s001_w1_z001_t001.tif"),), Backend.VIRTUAL_WORKSPACE, + ).load_images(filemanager) loaded = filemanager.load_batch( [str(tmp_path / "A01_s001_w1_z001_t001.tif")], diff --git a/tests/unit/test_function_io.py b/tests/unit/test_function_io.py index 70c56a8adb..e752311c34 100644 --- a/tests/unit/test_function_io.py +++ b/tests/unit/test_function_io.py @@ -31,7 +31,6 @@ ) from openhcs.core.source_workspace_projection import VirtualWorkspaceSourceProjection from openhcs.core.steps.function_io import ( - bulk_preload_step_images, get_all_image_paths, zarr_batch_layout, zarr_output_batch_layout, @@ -306,20 +305,16 @@ def test_bulk_preload_preserves_nested_virtual_workspace_paths(tmp_path: Path) - ] assert virtual_paths == expected_paths - bulk_preload_step_images( - workspace_root, - "A01", - Backend.VIRTUAL_WORKSPACE.value, - filemanager, - handler, - ) + SourceFileUniverse( + tuple(expected_paths), Backend.VIRTUAL_WORKSPACE, + ).load_images(filemanager) loaded = filemanager.load_batch(expected_paths, Backend.MEMORY.value) for payload, labels in zip(loaded, sources.values(), strict=True): np.testing.assert_array_equal(payload.data, labels) -def test_object_only_source_anchors_match_compiled_pattern_after_preload( +def test_object_only_source_anchors_match_compiled_pattern_after_loading( tmp_path: Path, ) -> None: source_root = tmp_path / "source" @@ -374,13 +369,10 @@ def test_object_only_source_anchors_match_compiled_pattern_after_preload( "A01_s001_w1_z001_t001.tif", "A01_s001_w2_z001_t001.tif", ) - bulk_preload_step_images( - workspace_root, - "A01", - Backend.VIRTUAL_WORKSPACE.value, - filemanager, - handler, - ) + SourceFileUniverse( + tuple(str(workspace_root / path) for path in materialization.artifact_mappings), + Backend.VIRTUAL_WORKSPACE, + ).load_images(filemanager) assert handler.path_list_from_pattern( workspace_root, @@ -534,13 +526,7 @@ def test_bulk_preload_loads_matlab_pixels_through_declared_vfs_backend( str(workspace_root / virtual_path) for virtual_path in materialization.plane_mappings ) - bulk_preload_step_images( - workspace_root, - "A01", - Backend.VIRTUAL_WORKSPACE.value, - filemanager, - SourceBindingsHandler(filemanager, source_bindings), - ) + SourceFileUniverse(virtual_paths, Backend.VIRTUAL_WORKSPACE).load_images(filemanager) loaded = filemanager.load_batch( list(virtual_paths), diff --git a/tests/unit/test_function_runtime_source_projection.py b/tests/unit/test_function_runtime_source_projection.py index 7bf8eddcdd..5b1c67ac06 100644 --- a/tests/unit/test_function_runtime_source_projection.py +++ b/tests/unit/test_function_runtime_source_projection.py @@ -8,7 +8,7 @@ from objectstate.global_config import GlobalContextValues from polystore.virtual_workspace import SourcePixelRef -from openhcs.constants.constants import AllComponents, GroupBy, VariableComponents +from openhcs.constants.constants import AllComponents, Backend, GroupBy, VariableComponents from openhcs.core.aligned_image_payload import ( AlignedImageSliceContext, ImagePayloadBundleContext, @@ -2139,7 +2139,6 @@ def test_first_step_prepares_raw_source_anchors_under_semantic_binding_groups( "MCP_DNA": ("A01_s{iii}_w1_z001_t001.tif",), "MCP_AGP": ("A01_s{iii}_w2_z001_t001.tif",), } - executor._preload_inputs_if_needed(grouped_patterns) request = PatternGroupExecutionRequest( context=context, @@ -2153,6 +2152,12 @@ def test_first_step_prepares_raw_source_anchors_under_semantic_binding_groups( loaded = PatternGroupRuntime(request)._load_input_stack() assert loaded[0] == ["A01_s001_w1_z001_t001.tif"] + assert context.filemanager.exists( + str(executor.plan.input_dir / "A01_s001_w1_z001_t001.tif"), Backend.MEMORY.value, + ) + assert not context.filemanager.exists( + str(executor.plan.input_dir / "A01_s001_w2_z001_t001.tif"), Backend.MEMORY.value, + ) def test_source_bound_artifact_managed_step_keeps_source_anchors() -> None: @@ -3950,6 +3955,7 @@ def resolve_address(backend_address, backend, *, base_path): step_type="FunctionStep", axis_id="A01", input_dir=tmp_path, + read_backend="memory", input_memory_type="numpy", variable_components=(), main_input_dependency=StepInputDependency.pipeline_start(), diff --git a/tests/unit/test_function_step_execution_plan.py b/tests/unit/test_function_step_execution_plan.py index 68c1894829..baeab80d3f 100644 --- a/tests/unit/test_function_step_execution_plan.py +++ b/tests/unit/test_function_step_execution_plan.py @@ -306,9 +306,6 @@ def _prepare_groups(self, patterns_by_axis): {None: ["image.tif"]} ) - def _preload_inputs_if_needed(self, grouped_patterns): - events.append(("preload", tuple(grouped_patterns.groups))) - def _prepare_callables(self, grouped_patterns): raise AssertionError("callable warmup belongs to compilation") From 2b9bb5649d60d5b98612922a7b0c746009466f54 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 20:43:27 -0400 Subject: [PATCH 176/235] Use ObjectState nominal subtype-preserving config merges Refs #573 and OpenHCSDev/ObjectState#14. Pin merged ObjectState PR15, preserving the authored volume domain and all sibling source bindings during lazy nested configuration resolution. Actual original 3D import retains its 31 steps and three source bindings. --- external/ObjectState | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/ObjectState b/external/ObjectState index a01f8939b9..dbc3c64c5f 160000 --- a/external/ObjectState +++ b/external/ObjectState @@ -1 +1 @@ -Subproject commit a01f8939b9bf4659502a33fe3febe62cab0f3c9a +Subproject commit dbc3c64c5f7ebde88c1fedf03cb8aff4dbe1f4c1 From 18231c62903d5e8bcb07d178bfff5f421c218e8c Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 20:51:13 -0400 Subject: [PATCH 177/235] Derive source load backend from the declared input dependency Refs #419 and #394. Pipeline-start source selection can expand a prior anchor to a different declared channel whose pixels have not yet been loaded. A producer manifest entry is anchor bookkeeping and does not make that selected source memory-resident. Use the compiled input dependency to select the source backend; only actual step-output inputs are owned by memory. Found on fresh complete Beginner execution; full pipeline qualification remains pending. --- openhcs/core/steps/function_runtime.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openhcs/core/steps/function_runtime.py b/openhcs/core/steps/function_runtime.py index a93b49574b..9c62a09321 100644 --- a/openhcs/core/steps/function_runtime.py +++ b/openhcs/core/steps/function_runtime.py @@ -2645,7 +2645,9 @@ def _load_input_stack( if cached_stack is None: raw_slices = SourceFileUniverse( tuple(full_file_paths), - Backend.MEMORY if producer_matching_files else Backend(plan.read_backend), + Backend.MEMORY + if plan.main_input_dependency.kind is StepInputDependencyKind.STEP_OUTPUT + else Backend(plan.read_backend), ).load_images(context.filemanager, zarr_config=plan.zarr_config) if source_projection is not None or not producer_matching_files: raw_slices = self._apply_source_image_loading_semantics( From 0bca84a48577a2bea9f0e65fd18770ccb2a2dcea Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 20:50:06 -0400 Subject: [PATCH 178/235] Consolidate callable invocation preparation on existing declaration owners Normalize and compile through the existing function item/group owners; retain executable resolution on CallableContract with the existing identity-bound process cache. Remove parallel invocation, compilation, resolution, and memory-type carriers while preserving authored versus resolved declaration epochs and transport ownership. Validation: 93 focused tests passed, including custom runtime wrappers, prepared image metadata, and compiled transport identity. No independent performance attribution. Refs #384. Refs #394. --- openhcs/core/callable_contract.py | 36 +- openhcs/core/function_patterns.py | 341 ++++++++---------- openhcs/core/steps/function_runtime.py | 87 +---- .../backends/cellprofiler/classification.py | 6 +- tests/unit/test_callable_abi_preparation.py | 8 +- ...st_callable_contract_runtime_resolution.py | 51 +++ .../test_cellprofiler_prepared_payload_abi.py | 4 +- ...t_function_invocation_callable_resolver.py | 62 ---- tests/unit/test_function_step_transport.py | 13 +- 9 files changed, 254 insertions(+), 354 deletions(-) create mode 100644 tests/unit/test_callable_contract_runtime_resolution.py delete mode 100644 tests/unit/test_function_invocation_callable_resolver.py diff --git a/openhcs/core/callable_contract.py b/openhcs/core/callable_contract.py index 6fc8a5fed5..8c5acfc8e7 100644 --- a/openhcs/core/callable_contract.py +++ b/openhcs/core/callable_contract.py @@ -14,6 +14,7 @@ from enum import Enum from functools import wraps from pathlib import Path +from threading import Lock from types import MappingProxyType from typing import ( TYPE_CHECKING, @@ -58,6 +59,7 @@ from openhcs.core.variable_component_stack_requirement import ( VariableComponentStackRequirement, ) +from openhcs.core.process_local_cache import IdentityBoundProcessCache if TYPE_CHECKING: from openhcs.core.aligned_image_payload import ( @@ -79,7 +81,12 @@ CallableNamespace = Mapping[str, Any] _EnumT = TypeVar("_EnumT", bound=Enum) -CallableRuntimeCacheKey = int + +class CallableContractRuntimeCache(IdentityBoundProcessCache): + """Retain executable callables for their exact compiled declaration owners.""" + + registry_key = "function_invocation_callable" + resolution_lock = Lock() @dataclass(frozen=True, slots=True) @@ -1218,21 +1225,28 @@ def runtime_batch_executor( return None return self.runtime_batch_executors.get(domain) - def runtime_callable_cache_identity( - self, - ) -> CallableRuntimeCacheKey: - """Return the process-local cache identity for this callable contract.""" - if not _is_function_reference(self.func) and not callable(self.func): - raise TypeError(f"Invalid callable contract function: {self.func}") - return id(self) + def require_memory_types(self) -> tuple[str, str]: + """Admit both declared memory domains before runtime conversion.""" + if self.input_memory_type is None or self.output_memory_type is None: + raise ValueError( + f"Callable {self.function_name!r} is missing memory types." + ) + return self.input_memory_type, self.output_memory_type def resolve_runtime_callable(self) -> Callable[..., object]: """Return the executable callable for this contract.""" + cache = CallableContractRuntimeCache.process_cache() + with CallableContractRuntimeCache.resolution_lock: + cached = cache.get_bound(self) + if cached is not None: + return cached resolved = _resolve_declared_callable(self.func) runtime_adapter = self.runtime_adapter - if runtime_adapter is None: - return resolved - return runtime_adapter.executable_callable(resolved, self) + if runtime_adapter is not None: + resolved = runtime_adapter.executable_callable(resolved, self) + with CallableContractRuntimeCache.resolution_lock: + cache.put_bound(self, resolved) + return resolved def resolve_canonical_raw_callable(self) -> Callable[..., object]: """Return the declaration-owned semantic target.""" diff --git a/openhcs/core/function_patterns.py b/openhcs/core/function_patterns.py index 3842a49a6c..9c6f92341f 100644 --- a/openhcs/core/function_patterns.py +++ b/openhcs/core/function_patterns.py @@ -334,19 +334,6 @@ def resolve_unstored_payload( return scope.invocation.artifact_parameter_value(self.spec) -@dataclass(frozen=True) -class FunctionInvocation: - """One enabled callable extracted from a FunctionStep pattern.""" - - contract: CallableContract - key: FunctionInvocationKey - - @property - def func(self) -> FunctionPatternCallable: - """Callable reference used by the runtime invocation.""" - return self.contract.func - - @dataclass(frozen=True, slots=True) class NormalizedFunctionItem: """Compiler-normalized callable item with stable invocation identity.""" @@ -388,6 +375,38 @@ def group_key(self) -> str: return str(self.source_group_key) + @classmethod + def from_pattern( + cls, + group_key: FunctionGroupKey, + pattern: FunctionPatternSyntax, + ) -> NormalizedFunctionGroup: + items = pattern if isinstance(pattern, list) else [pattern] + normalized_items: list[NormalizedFunctionItem] = [] + + for item in items: + func, kwargs = _split_function_item(item) + if RUNTIME_CALLABLE_KWARG_POLICY.item_is_disabled(kwargs): + continue + contract = CallableContract.from_prepared_callable(func) + position = len(normalized_items) + normalized_items.append( + NormalizedFunctionItem( + key=FunctionInvocationKey.from_contract( + contract, + group_key, + position, + ), + contract=contract, + kwargs=_freeze_runtime_kwargs(kwargs), + ) + ) + + return cls( + source_group_key=group_key, + items=tuple(normalized_items), + ) + @dataclass(frozen=True, slots=True) class NormalizedFunctionPattern: @@ -461,11 +480,6 @@ def execution_memory_type(self) -> str | None: """Declared execution memory type from the callable contract.""" return self.contract.execution_memory_type - @property - def kwargs_dict(self) -> dict: - """Return user-authored callable kwargs as a runtime dict.""" - return dict(self.kwargs) - def output_plans_for_component( self, execution_scope: ComponentGroupScope, @@ -648,6 +662,83 @@ def proves_primary_image_carrier( transition = self.contract.primary_image_carrier_transition return transition is not None and transition.proves(requirement) + @classmethod + def from_normalized_item( + cls, + item: NormalizedFunctionItem, + input_plans: Mapping[ArtifactSpecRef, ArtifactInputPlan], + output_plans: Mapping[ArtifactSpecRef, ArtifactOutputPlan], + declaration_provider: InvocationArtifactDeclarationProviderLike, + invocation_contract_provider: InvocationContractProvider, + step_context: ArtifactDeclarationStepContext, + runtime_parameter_bindings: Sequence[RuntimeParameterBinding], + path_resolver: "CompilationPathResolver | None", + ) -> "CompiledFunctionInvocation": + contract_plan = invocation_contract_provider(item, step_context) + if contract_plan is None: + invocation_kwargs = item.kwargs + else: + invocation_kwargs = contract_plan.consume_authored_kwargs( + item, + step_context, + ) + item = replace(item, contract=contract_plan.contract) + artifact_selector = declaration_provider(item, step_context) + item.contract.validate_artifact_input_parameter_bindings() + artifact_selector.validate_artifact_output_declarations() + artifact_input_plans = artifact_selector.select_plans( + ArtifactInputPlan, + input_plans, + ) + user_kwargs, compiled_runtime_bindings = _compile_runtime_parameter_bindings( + invocation_kwargs, + runtime_parameter_bindings, + ( + *item.contract.runtime_bound_parameters, + *item.contract.config_bound_parameter_names, + ), + ) + public_kwargs = dict(user_kwargs) + if path_resolver is not None: + public_kwargs = item.contract.resolve_declared_paths( + public_kwargs, + path_resolver, + ) + else: + relative_parameters = tuple( + parameter_name + for parameter_name, (_declaration, value) in ( + item.contract.declared_path_values(public_kwargs).items() + ) + if isinstance(value, (str, Path)) and not Path(value).is_absolute() + ) + if relative_parameters: + raise ValueError( + f"Callable {item.contract.function_name!r} has relative declared " + f"paths {relative_parameters!r} but compilation supplied no " + "CompilationPathResolver." + ) + runtime_loaded_input_refs = frozenset(plan.ref() for plan in artifact_input_plans) + validated_kwargs = item.contract.validate_public_kwargs( + public_kwargs, + runtime_loaded_artifact_parameter_names=( + spec.parameter_name + for spec in item.contract.artifact_inputs + if spec.parameter_name is not None + and spec.ref() in runtime_loaded_input_refs + ), + ) + return cls( + key=item.key, + contract=item.contract, + kwargs=validated_kwargs, + artifact_output_plans=artifact_selector.select_plans( + ArtifactOutputPlan, + output_plans, + ), + runtime_parameter_bindings=compiled_runtime_bindings, + ) + @dataclass(frozen=True, slots=True) class CompiledFunctionGroup: @@ -868,6 +959,37 @@ def primary_image_carrier_proof( return CreatedPrimaryImageCarrierProof() return InheritedPrimaryImageCarrierProof() + @classmethod + def from_normalized_group( + cls, + normalized_group: NormalizedFunctionGroup, + *, + input_plans: Mapping[ArtifactSpecRef, ArtifactInputPlan], + output_plans: Mapping[ArtifactSpecRef, ArtifactOutputPlan], + declaration_provider: InvocationArtifactDeclarationProviderLike, + invocation_contract_provider: InvocationContractProvider, + step_context: ArtifactDeclarationStepContext, + runtime_parameter_bindings: Sequence[RuntimeParameterBinding], + path_resolver: "CompilationPathResolver | None", + ) -> "CompiledFunctionGroup": + """Compile an authored group through its declaration-owned invocation values.""" + return cls( + group_key=normalized_group.group_key, + invocations=tuple( + CompiledFunctionInvocation.from_normalized_item( + item=item, + input_plans=input_plans, + output_plans=output_plans, + declaration_provider=declaration_provider, + invocation_contract_provider=invocation_contract_provider, + step_context=step_context, + runtime_parameter_bindings=runtime_parameter_bindings, + path_resolver=path_resolver, + ) + for item in normalized_group.items + ), + ) + class PrimaryImageCarrierProof(ABC): """One group-owned proof result, not another carrier metadata authority.""" @@ -1078,17 +1200,13 @@ def prepare_grouped_patterns( def iter_enabled_function_invocations( pattern: FunctionPatternSyntax, -) -> Iterator[FunctionInvocation]: +) -> Iterator[NormalizedFunctionItem]: """Yield enabled callable invocations from any supported function pattern. Positions are renumbered after disabled functions are filtered out, matching the current runtime behavior for list chains and dict-pattern branches. """ - for item in normalize_function_pattern(pattern).iter_items(): - yield FunctionInvocation( - contract=item.contract, - key=item.key, - ) + yield from normalize_function_pattern(pattern).iter_items() def get_core_callable( @@ -1128,10 +1246,9 @@ def normalize_function_pattern( """Lower raw FunctionStep.func syntax into typed grouped callable items.""" if isinstance(pattern, NormalizedFunctionPattern): return pattern - normalizer = NormalizeFunctionGroupAuthority() if isinstance(pattern, dict): groups = tuple( - normalizer.normalize(group_key=group_key, pattern=value) + NormalizedFunctionGroup.from_pattern(group_key=group_key, pattern=value) for group_key, value in pattern.items() ) return NormalizedFunctionPattern( @@ -1141,7 +1258,7 @@ def normalize_function_pattern( return NormalizedFunctionPattern( groups=( - normalizer.normalize( + NormalizedFunctionGroup.from_pattern( group_key=DEFAULT_GROUP_KEY, pattern=pattern, ), @@ -1198,17 +1315,21 @@ def compile_function_pattern( ) -> CompiledFunctionPattern: """Compile raw FunctionStep.func syntax into the runtime source of truth.""" normalized = normalize_function_pattern(pattern) - compiler = CompileFunctionGroupAuthority( - input_plans=input_plans, - output_plans=output_plans, - declaration_provider=declaration_provider, - invocation_contract_provider=invocation_contract_provider, - step_context=step_context, - runtime_parameter_bindings=tuple(runtime_parameter_bindings), - path_resolver=path_resolver, - ) + runtime_parameter_bindings = tuple(runtime_parameter_bindings) return CompiledFunctionPattern( - groups=tuple(compiler.compile(group) for group in normalized.groups), + groups=tuple( + CompiledFunctionGroup.from_normalized_group( + group, + input_plans=input_plans, + output_plans=output_plans, + declaration_provider=declaration_provider, + invocation_contract_provider=invocation_contract_provider, + step_context=step_context, + runtime_parameter_bindings=runtime_parameter_bindings, + path_resolver=path_resolver, + ) + for group in normalized.groups + ), is_grouped=normalized.is_grouped, ) @@ -1327,152 +1448,6 @@ def merge_replacing_existing( return self.merge(pattern) -@dataclass(frozen=True, slots=True) -class NormalizeFunctionGroupAuthority: - """Normalize one function-pattern group into callable invocation items.""" - - def normalize( - self, - group_key: FunctionGroupKey, - pattern: FunctionPatternSyntax, - ) -> NormalizedFunctionGroup: - items = pattern if isinstance(pattern, list) else [pattern] - normalized_items: list[NormalizedFunctionItem] = [] - - for item in items: - func, kwargs = _split_function_item(item) - if RUNTIME_CALLABLE_KWARG_POLICY.item_is_disabled(kwargs): - continue - contract = CallableContract.from_prepared_callable(func) - position = len(normalized_items) - normalized_items.append( - NormalizedFunctionItem( - key=FunctionInvocationKey.from_contract( - contract, - group_key, - position, - ), - contract=contract, - kwargs=_freeze_runtime_kwargs(kwargs), - ) - ) - - return NormalizedFunctionGroup( - source_group_key=group_key, - items=tuple(normalized_items), - ) - - -@dataclass(frozen=True, slots=True, kw_only=True) -class CompileFunctionGroupAuthority: - """Compile normalized function-pattern groups into invocation plans.""" - - input_plans: Mapping[ArtifactSpecRef, ArtifactInputPlan] - output_plans: Mapping[ArtifactSpecRef, ArtifactOutputPlan] - declaration_provider: InvocationArtifactDeclarationProviderLike - invocation_contract_provider: InvocationContractProvider - step_context: ArtifactDeclarationStepContext - runtime_parameter_bindings: tuple[RuntimeParameterBinding, ...] = () - path_resolver: "CompilationPathResolver | None" = None - - def compile( - self, normalized_group: NormalizedFunctionGroup - ) -> CompiledFunctionGroup: - invocations = tuple( - _compile_invocation( - item=item, - input_plans=self.input_plans, - output_plans=self.output_plans, - declaration_provider=self.declaration_provider, - invocation_contract_provider=self.invocation_contract_provider, - step_context=self.step_context, - runtime_parameter_bindings=self.runtime_parameter_bindings, - path_resolver=self.path_resolver, - ) - for item in normalized_group.items - ) - return CompiledFunctionGroup( - group_key=normalized_group.group_key, - invocations=invocations, - ) - - -def _compile_invocation( - item: NormalizedFunctionItem, - input_plans: Mapping[ArtifactSpecRef, ArtifactInputPlan], - output_plans: Mapping[ArtifactSpecRef, ArtifactOutputPlan], - declaration_provider: InvocationArtifactDeclarationProviderLike, - invocation_contract_provider: InvocationContractProvider, - step_context: ArtifactDeclarationStepContext, - runtime_parameter_bindings: Sequence[RuntimeParameterBinding], - path_resolver: "CompilationPathResolver | None", -) -> CompiledFunctionInvocation: - contract_plan = invocation_contract_provider(item, step_context) - if contract_plan is None: - invocation_kwargs = item.kwargs - else: - invocation_kwargs = contract_plan.consume_authored_kwargs( - item, - step_context, - ) - item = replace(item, contract=contract_plan.contract) - artifact_selector = declaration_provider(item, step_context) - item.contract.validate_artifact_input_parameter_bindings() - artifact_selector.validate_artifact_output_declarations() - artifact_input_plans = artifact_selector.select_plans( - ArtifactInputPlan, - input_plans, - ) - user_kwargs, compiled_runtime_bindings = _compile_runtime_parameter_bindings( - invocation_kwargs, - runtime_parameter_bindings, - ( - *item.contract.runtime_bound_parameters, - *item.contract.config_bound_parameter_names, - ), - ) - public_kwargs = dict(user_kwargs) - if path_resolver is not None: - public_kwargs = item.contract.resolve_declared_paths( - public_kwargs, - path_resolver, - ) - else: - relative_parameters = tuple( - parameter_name - for parameter_name, (_declaration, value) in ( - item.contract.declared_path_values(public_kwargs).items() - ) - if isinstance(value, (str, Path)) and not Path(value).is_absolute() - ) - if relative_parameters: - raise ValueError( - f"Callable {item.contract.function_name!r} has relative declared " - f"paths {relative_parameters!r} but compilation supplied no " - "CompilationPathResolver." - ) - runtime_loaded_input_refs = frozenset(plan.ref() for plan in artifact_input_plans) - validated_kwargs = item.contract.validate_public_kwargs( - public_kwargs, - runtime_loaded_artifact_parameter_names=( - spec.parameter_name - for spec in item.contract.artifact_inputs - if spec.parameter_name is not None - and spec.ref() in runtime_loaded_input_refs - ), - ) - return CompiledFunctionInvocation( - key=item.key, - contract=item.contract, - kwargs=validated_kwargs, - artifact_output_plans=artifact_selector.select_plans( - ArtifactOutputPlan, - output_plans, - ), - runtime_parameter_bindings=compiled_runtime_bindings, - ) - - def _compile_runtime_parameter_bindings( kwargs: RuntimeKwargItems, runtime_parameter_bindings: Sequence[RuntimeParameterBinding], diff --git a/openhcs/core/steps/function_runtime.py b/openhcs/core/steps/function_runtime.py index 9c62a09321..d8eaf7bee0 100644 --- a/openhcs/core/steps/function_runtime.py +++ b/openhcs/core/steps/function_runtime.py @@ -10,7 +10,6 @@ import os import time from dataclasses import dataclass, replace -from threading import Lock from pathlib import Path from types import MappingProxyType from typing import ( @@ -40,7 +39,6 @@ ArtifactSpecRef, ) from openhcs.core.callable_contract import ( - CallableRuntimeCacheKey, ImagePayloadConsumption, ) from openhcs.core.component_group_scope import ( @@ -87,7 +85,6 @@ MeasurementRowOwnership, measurement_rows_with_source_provenance, ) -from openhcs.core.process_local_cache import IdentityBoundProcessCache from openhcs.core.runtime_stores import ( RuntimeArtifactInput, RuntimeArtifactLocation, @@ -217,52 +214,6 @@ EMPTY_ARTIFACT_PLANS: ArtifactOutputPlans = MappingProxyType({}) -class FunctionInvocationCallableCache(IdentityBoundProcessCache): - """Bound resolved callables to their exact compiled contract owners.""" - - registry_key = "function_invocation_callable" - - -class FunctionInvocationCallableResolver: - """Process-local resolver for compiled invocation callables. - - The compiler stores picklable ``FunctionReference`` objects in compiled - invocations. Runtime execution needs actual callables. Resolving them during - compiler preparation lets fork workers inherit the resolved callable cache, - while spawn workers still resolve lazily in their own process. - """ - - _lock = Lock() - - @classmethod - def prepare(cls, invocation: CompiledFunctionInvocation) -> None: - """Resolve and cache one invocation callable before timed execution.""" - cls.resolve(invocation) - - @classmethod - def resolve(cls, invocation: CompiledFunctionInvocation) -> Callable: - """Return the callable for a compiled invocation.""" - cache = FunctionInvocationCallableCache.process_cache() - with cls._lock: - cached = cache.get_bound(invocation.contract) - if cached is not None: - return cached - - resolved = invocation.contract.resolve_runtime_callable() - - with cls._lock: - cache.put_bound(invocation.contract, resolved) - return resolved - - @classmethod - def cache_key( - cls, - invocation: CompiledFunctionInvocation, - ) -> CallableRuntimeCacheKey: - """Return process-local callable cache key for one compiled invocation.""" - return invocation.contract.runtime_callable_cache_identity() - - class RuntimeProfileSink: """Runtime-profile output authority backed by explicit environment settings.""" @@ -1663,7 +1614,7 @@ def _save_artifact_value( def prepare_compiled_function_group(group: CompiledFunctionGroup) -> None: """Run optional preparation hooks for each callable in a compiled group.""" for invocation in group.invocations: - FunctionInvocationCallableResolver.prepare(invocation) + invocation.contract.resolve_runtime_callable() def prepare_compiled_context_callables( @@ -1909,7 +1860,6 @@ def load_artifact_input( ) return loaded_values - def debug_cursor(self) -> DebugCursor: return DebugCursor.from_invocation( step_index=self.group_data.execution_plan.step_index, @@ -1960,7 +1910,7 @@ def debug_event( @property def func_callable(self) -> Callable: - return FunctionInvocationCallableResolver.resolve(self.invocation) + return self.invocation.contract.resolve_runtime_callable() @property def base_kwargs(self) -> RuntimeCallableKwargs: @@ -1981,14 +1931,14 @@ def execute( *, debug_sink: DebugEventSink | None = None, ) -> RuntimePayload | NoMainFlowOutput: - memory_types = self.memory_types() + input_memory_type, _ = self.invocation.contract.require_memory_types() target_device_id = self.group_data.execution_plan.device_id_for( - memory_types.input_type + input_memory_type ) converted_data = convert_memory( data=image_payload_data(self.main_data_arg), source_type=self.source_memory_type, - target_type=memory_types.input_type, + target_type=input_memory_type, gpu_id=target_device_id, ) source_payload = with_image_payload_data(self.main_data_arg, converted_data) @@ -2038,9 +1988,6 @@ def main_flow_call_argument( return self.invocation.contract.main_flow_call_argument(source_payload) - def memory_types(self) -> "FunctionChainInvocationMemoryTypes": - return FunctionChainInvocationMemoryTypes.from_invocation(self.invocation) - def execution_group_source_payload( self, source_payload: RuntimePayload, @@ -2329,28 +2276,6 @@ def artifact_output_source_payload( ) -@dataclass(frozen=True, slots=True) -class FunctionChainInvocationMemoryTypes: - """Validated memory types for one compiled invocation.""" - - input_type: str - output_type: str - - @classmethod - def from_invocation( - cls, - invocation: CompiledFunctionInvocation, - ) -> "FunctionChainInvocationMemoryTypes": - if ( - invocation.input_memory_type is None - or invocation.output_memory_type is None - ): - raise ValueError( - f"Compiled invocation {invocation.key} is missing memory types." - ) - return cls(invocation.input_memory_type, invocation.output_memory_type) - - class PatternGroupRuntime: """Staged runtime for one pattern group.""" @@ -3042,7 +2967,7 @@ def execute_chain( ) if isinstance(current_stack, NoMainFlowOutput): return current_stack - current_memory_type = executor.memory_types().output_type + current_memory_type = executor.invocation.contract.output_memory_type if group_data.compiled_group.preserves_input_main_flow() and all( invocation.contract.artifact_output_policy.records_outputs for invocation in group_data.compiled_group.invocations diff --git a/openhcs/processing/backends/cellprofiler/classification.py b/openhcs/processing/backends/cellprofiler/classification.py index e7e22bd10a..d265f9fa3d 100644 --- a/openhcs/processing/backends/cellprofiler/classification.py +++ b/openhcs/processing/backends/cellprofiler/classification.py @@ -74,7 +74,7 @@ ) if TYPE_CHECKING: - from openhcs.core.function_patterns import FunctionInvocation + from openhcs.core.function_patterns import NormalizedFunctionItem from openhcs.interop.cellprofiler.parser import ModuleBlock from openhcs.interop.cellprofiler.settings_binder import SettingsBinder @@ -1317,7 +1317,7 @@ def classified_image_outputs( @abstractmethod def finalize_mode_blocks( self, module_type: type[ClassifyObjectsSingleMeasurementModule], - blocks: tuple[ModuleBlock, ...], invocation: FunctionInvocation, + blocks: tuple[ModuleBlock, ...], invocation: NormalizedFunctionItem, ) -> tuple[ModuleBlock, ...]: """Complete this mode after the shared template emits its literal.""" @@ -1472,7 +1472,7 @@ def classified_image_outputs( def finalize_module_blocks( self, module_type: type[ClassifyObjectsSingleMeasurementModule], - blocks: tuple[ModuleBlock, ...], invocation: FunctionInvocation, + blocks: tuple[ModuleBlock, ...], invocation: NormalizedFunctionItem, ) -> tuple[ModuleBlock, ...]: blocks = tuple( replace( diff --git a/tests/unit/test_callable_abi_preparation.py b/tests/unit/test_callable_abi_preparation.py index dd3f1aa8e9..046995f880 100644 --- a/tests/unit/test_callable_abi_preparation.py +++ b/tests/unit/test_callable_abi_preparation.py @@ -19,7 +19,7 @@ attach_callable_contract_metadata, attach_processing_prepare, ) from openhcs.core.function_contract_metadata import FunctionContractAttribute -from openhcs.core.function_patterns import NormalizeFunctionGroupAuthority +from openhcs.core.function_patterns import NormalizedFunctionGroup from openhcs.core.function_reference import ImportableFunctionReference from openhcs.core.processing_preparation import CallablePreparation, PreparationCacheBatch from openhcs.core.runtime_image_values import ImagePayloadMetadata, image_payload_data @@ -175,8 +175,8 @@ def prepare(owner): raw.__annotations__["image"] = desired_annotations.pop(0) monkeypatch.setattr(CallablePreparation, "prepare", prepare) - first = NormalizeFunctionGroupAuthority().normalize("group", raw).items[0].contract - second = NormalizeFunctionGroupAuthority().normalize("group", raw).items[0].contract + first = NormalizedFunctionGroup.from_pattern("group", raw).items[0].contract + second = NormalizedFunctionGroup.from_pattern("group", raw).items[0].contract assert first.canonical_parameter_annotations["image"] is ArrayPayload assert second.canonical_parameter_annotations["image"] is np.ndarray assert FunctionContractAttribute.canonical_signature not in vars(raw) @@ -203,7 +203,7 @@ def test_unprepared_function_reference_compilation_reads_prepared_live_declarati import_identity=CallableImportIdentity.from_callable(raw_numeric), composite_key="test:raw_numeric", ) - contract = NormalizeFunctionGroupAuthority().normalize("group", reference).items[0].contract + contract = NormalizedFunctionGroup.from_pattern("group", reference).items[0].contract assert contract.func is reference assert contract.canonical_parameter_annotations["image"] is np.ndarray assert fake_preparation.call_count == 1 diff --git a/tests/unit/test_callable_contract_runtime_resolution.py b/tests/unit/test_callable_contract_runtime_resolution.py new file mode 100644 index 0000000000..c6283be88f --- /dev/null +++ b/tests/unit/test_callable_contract_runtime_resolution.py @@ -0,0 +1,51 @@ +from dataclasses import replace + +from openhcs.core.artifacts import ArtifactSpec, ImageArtifactType +from openhcs.core.callable_contract import CallableContract, CallableMetadata +from openhcs.core.runtime_adapters import RuntimeAdapterSpec + + +def test_custom_runtime_wrapper_is_retained_for_its_exact_contract(): + prepared = [] + + def raw(image): + return image + + def runtime_callable(registered_func, contract): + prepared.append(contract) + + def execute(image): + return contract.artifact_outputs[0].name, registered_func(image) + + return execute + + first = CallableContract( + func=raw, + function_name=raw.__name__, + module_name=raw.__module__, + metadata=CallableMetadata( + input_memory_type="python", + output_memory_type="python", + artifact_outputs=(ArtifactSpec.output("First", ImageArtifactType),), + runtime_adapter=RuntimeAdapterSpec( + parameter_name="runtime", + factory=lambda request: request, + runtime_callable_factory=runtime_callable, + ), + ), + ) + second = replace( + first, + metadata=replace( + first.metadata, + artifact_outputs=(ArtifactSpec.output("Second", ImageArtifactType),), + ), + ) + first_callable = first.resolve_runtime_callable() + second_callable = second.resolve_runtime_callable() + assert first_callable(7) == ("First", 7) + assert second_callable(9) == ("Second", 9) + assert first.resolve_runtime_callable() is first_callable + assert second.resolve_runtime_callable() is second_callable + assert len(prepared) == 2 + assert prepared[0] is first and prepared[1] is second diff --git a/tests/unit/test_cellprofiler_prepared_payload_abi.py b/tests/unit/test_cellprofiler_prepared_payload_abi.py index bf0f122d11..30becbea9b 100644 --- a/tests/unit/test_cellprofiler_prepared_payload_abi.py +++ b/tests/unit/test_cellprofiler_prepared_payload_abi.py @@ -6,7 +6,7 @@ from scipy import ndimage from openhcs.core.aligned_image_payload import ImagePayloadExecutionMode -from openhcs.core.function_patterns import NormalizeFunctionGroupAuthority +from openhcs.core.function_patterns import NormalizedFunctionGroup from openhcs.core.runtime_image_values import ( ImagePayloadMetadata, image_payload_data, @@ -30,7 +30,7 @@ def _execute_prepared(func, source, kwargs, mode=ImagePayloadExecutionMode.NATURAL): - contract = NormalizeFunctionGroupAuthority().normalize("image", func).items[0].contract + contract = NormalizedFunctionGroup.from_pattern("image", func).items[0].contract contract = replace( contract, metadata=replace( diff --git a/tests/unit/test_function_invocation_callable_resolver.py b/tests/unit/test_function_invocation_callable_resolver.py deleted file mode 100644 index 0198cf7446..0000000000 --- a/tests/unit/test_function_invocation_callable_resolver.py +++ /dev/null @@ -1,62 +0,0 @@ -from openhcs.core.artifacts import ArtifactSpec, ImageArtifactType -from openhcs.core.callable_contract import ( - CallableContract, - CallableImportIdentity, - CallableMetadata, -) -from openhcs.core.function_patterns import ( - CompiledFunctionInvocation, - FunctionInvocationKey, -) -from openhcs.core.function_reference import ( - FunctionReference, - RegistryFunctionReference, -) -from openhcs.core.steps.function_runtime import FunctionInvocationCallableResolver - - -def _callable_contract_for_reference( - reference: FunctionReference, - output_spec: ArtifactSpec, -) -> CallableContract: - return CallableContract( - func=reference, - function_name=reference.function_name, - module_name=reference.original_module, - metadata=CallableMetadata( - input_memory_type="python", - output_memory_type="python", - artifact_outputs=(output_spec,), - ), - ) - - -def test_function_reference_cache_key_distinguishes_compiled_callable_contracts(): - reference = RegistryFunctionReference( - import_identity=CallableImportIdentity( - module_name="openhcs.processing.backends.cellprofiler", - function_name="image_math", - ), - composite_key="cellprofiler:image_math", - ) - first_contract = _callable_contract_for_reference( - reference, - ArtifactSpec.output("CombinedImage", ImageArtifactType), - ) - second_contract = _callable_contract_for_reference( - reference, - ArtifactSpec.output("SubtractedRed", ImageArtifactType), - ) - - first_invocation = CompiledFunctionInvocation( - key=FunctionInvocationKey.from_contract(first_contract, "default", 0), - contract=first_contract, - ) - second_invocation = CompiledFunctionInvocation( - key=FunctionInvocationKey.from_contract(second_contract, "default", 0), - contract=second_contract, - ) - - assert FunctionInvocationCallableResolver.cache_key( - first_invocation - ) != FunctionInvocationCallableResolver.cache_key(second_invocation) diff --git a/tests/unit/test_function_step_transport.py b/tests/unit/test_function_step_transport.py index 3d0bf7398d..8b6b0e634a 100644 --- a/tests/unit/test_function_step_transport.py +++ b/tests/unit/test_function_step_transport.py @@ -405,10 +405,7 @@ def test_bundle_transport_preserves_prepared_runtime_contract_identity() -> None from openhcs.core.orchestrator.execution_result import ( RuntimeExecutionTransportSerialization, ) - from openhcs.core.steps.function_runtime import ( - FunctionInvocationCallableCache, - FunctionInvocationCallableResolver, - ) + from openhcs.core.callable_contract import CallableContractRuntimeCache reference = FunctionReferenceTransportAuthority.function_reference( cellprofiler_backend.crop @@ -428,7 +425,7 @@ def test_bundle_transport_preserves_prepared_runtime_contract_identity() -> None pattern = replace( pattern, groups=(replace(pattern.groups[0], invocations=(invocation,)),) ) - prepared_callable = FunctionInvocationCallableResolver.resolve(invocation) + prepared_callable = invocation.contract.resolve_runtime_callable() plan = CompiledStepPlan( step_index=0, step_name="Crop", step_type="FunctionStep", axis_id="A01", func=reference, compiled_function_pattern=pattern, @@ -455,7 +452,7 @@ def test_bundle_transport_preserves_prepared_runtime_contract_identity() -> None assert invocation.contract.metadata.prepare is not None assert plan.func is reference and reference.metadata.prepare is not None assert plan.compiled_function_pattern is pattern - assert FunctionInvocationCallableResolver.resolve(invocation) is prepared_callable + assert invocation.contract.resolve_runtime_callable() is prepared_callable assert transport_context.runtime_value_store is context.runtime_value_store assert transport_context.runtime_image_stack_cache is context.runtime_image_stack_cache assert transport_context.filemanager is context.filemanager @@ -467,10 +464,10 @@ def test_bundle_transport_preserves_prepared_runtime_contract_identity() -> None assert serialized_bundle.runtime_contexts is bundle.transport_contexts assert context.step_plans[0] is plan assert ( - FunctionInvocationCallableCache.process_cache().get_bound(invocation.contract) + CallableContractRuntimeCache.process_cache().get_bound(invocation.contract) is prepared_callable ) - assert FunctionInvocationCallableResolver.resolve(invocation) is prepared_callable + assert invocation.contract.resolve_runtime_callable() is prepared_callable RuntimeExecutionTransportSerialization.register() restored = pickle.loads(pickle.dumps(serialized_bundle)) restored_plan = restored.runtime_contexts["A01"].step_plans[0] From 7f47103483a503c703343bcd86651c2df421dfba Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 20:54:24 -0400 Subject: [PATCH 179/235] Make stored runtime artifacts inherit their value Remove nested RuntimeValue wrapping and derived field forwarding throughout runtime, materialization, export and equivalence consumers. Preserve real stored locations and return transient values when pixels or semantic identity change. Refs #384 --- .../equivalence/object_label_measurements.py | 8 +- openhcs/core/progress/live_measurements.py | 2 +- openhcs/core/runtime_artifact_queries.py | 8 +- openhcs/core/runtime_artifact_values.py | 36 +-- openhcs/core/runtime_equivalence.py | 10 +- openhcs/core/runtime_exports.py | 6 +- openhcs/core/runtime_stores.py | 42 ++- .../function_artifact_materialization.py | 44 ++-- .../interop/cellprofiler/analyst_export.py | 12 +- .../interop/cellprofiler/runtime/adapter.py | 6 +- .../cellprofiler/runtime/artifact_binding.py | 2 +- .../runtime/object_label_measurements.py | 2 +- .../runtime/object_measurement_vectors.py | 2 +- .../backends/cellprofiler/relationships.py | 2 +- .../cellprofiler/spreadsheet_export.py | 4 +- tests/diagnostics/owned_bootstrap_readback.py | 6 +- .../test_cellprofiler_generated_pipeline.py | 40 +-- .../test_chained_callable_lineage_journey.py | 2 +- .../test_measurement_declaration_journey.py | 18 +- ...rimary_segmentation_diagnostics_journey.py | 20 +- .../agent/test_callable_artifact_reference.py | 6 +- ..._display_manual_projection_declarations.py | 2 +- .../test_cellprofiler_module_execution.py | 18 +- .../unit/test_cellprofiler_runtime_adapter.py | 40 +-- ..._cellprofiler_runtime_cache_authorities.py | 2 +- .../test_cellprofiler_spreadsheet_export.py | 16 +- ...st_child_measurement_producer_selection.py | 20 +- .../test_function_artifact_materialization.py | 8 +- tests/unit/test_function_artifact_outputs.py | 48 ++-- tests/unit/test_function_outputs.py | 4 +- .../test_function_step_execution_scope.py | 2 +- .../test_persisted_roi_only_publication.py | 13 +- tests/unit/test_returned_output_abi.py | 2 +- tests/unit/test_runtime_equivalence.py | 246 ++++++++---------- tests/unit/test_runtime_exports.py | 14 +- tests/unit/test_runtime_value_store.py | 97 ++++--- 36 files changed, 415 insertions(+), 395 deletions(-) diff --git a/openhcs/core/equivalence/object_label_measurements.py b/openhcs/core/equivalence/object_label_measurements.py index dc141546fe..ee5cea564a 100644 --- a/openhcs/core/equivalence/object_label_measurements.py +++ b/openhcs/core/equivalence/object_label_measurements.py @@ -314,7 +314,7 @@ def from_object_label_records( identifier_subjects: set[RuntimeMeasurementSubjectKey] = set() location_subjects: set[RuntimeMeasurementSubjectKey] = set() for record in records: - object_labels = cast(ObjectLabelSet, record.value.data) + object_labels = cast(ObjectLabelSet, record.data) subject = RuntimeMeasurementSubjectKey( MeasurementScope.OBJECT, object_labels.name, @@ -401,7 +401,7 @@ def from_records( counts: dict[RuntimeMeasurementSubjectKey, int] = {} named_plane_domains: list[tuple[str, tuple[tuple[int, ...], ...]]] = [] for record in records: - object_labels = cast(ObjectLabelSet, record.value.data) + object_labels = cast(ObjectLabelSet, record.data) subject = RuntimeMeasurementSubjectKey( MeasurementScope.OBJECT, object_labels.name, @@ -508,7 +508,7 @@ def facts_for_records( facts: RuntimeMeasurementFactList = [] for record in records: context = ObjectLabelMeasurementContext.from_runtime_value( - record.value, + record, self.policy, self.object_identifier_subjects, self.object_location_subjects, @@ -885,7 +885,7 @@ def object_label_measurement_values_for_name( values_by_feature: RuntimeObjectValuesByLabel = {} for record in records: - object_labels = cast(ObjectLabelSet, record.value.data) + object_labels = cast(ObjectLabelSet, record.data) if normalize_runtime_identifier(object_labels.name) != subject.name: continue for key, values in object_label_location_values_by_label( diff --git a/openhcs/core/progress/live_measurements.py b/openhcs/core/progress/live_measurements.py index 45c1f0eacb..dc00b533b4 100644 --- a/openhcs/core/progress/live_measurements.py +++ b/openhcs/core/progress/live_measurements.py @@ -75,7 +75,7 @@ def from_record( if record.key.artifact_type is not MeasurementsArtifactType: return None - table = cast(MeasurementTable, record.value.data) + table = cast(MeasurementTable, record.data) row_preview = _measurement_row_preview( table.rows, row_limit, diff --git a/openhcs/core/runtime_artifact_queries.py b/openhcs/core/runtime_artifact_queries.py index 260fd32914..fa2f5ff37e 100644 --- a/openhcs/core/runtime_artifact_queries.py +++ b/openhcs/core/runtime_artifact_queries.py @@ -383,7 +383,7 @@ def resolve( def runtime_record_locations(records: Sequence[StoredRuntimeValue]) -> tuple[str, ...]: """Return compact runtime-record identities without formatting payload data.""" return tuple( - f"{record.key.scope.value_text or ''}@{record.backend}:{record.path}" + f"{record.key.scope.value_text or ''}@{record.location.backend}:{record.location.path}" for record in records ) @@ -443,7 +443,7 @@ def runtime_measurement_tables( if cached is not None: return cached tables = tuple( - cast(MeasurementTable, record.value.data) + cast(MeasurementTable, record.data) for record in context.find(artifact_type=MeasurementsArtifactType) ) return store_cache.store_value(cache_key, tables) @@ -482,7 +482,7 @@ def runtime_relationship( artifact_type=RelationshipsArtifactType, purpose="relationship artifact", ) - return cast(ObjectRelationship, record.value.data) + return cast(ObjectRelationship, record.data) def runtime_spatial_grid( @@ -495,7 +495,7 @@ def runtime_spatial_grid( artifact_type=SpatialGridArtifactType, purpose="spatial grid artifact", ) - return cast(SpatialGrid, record.value.data) + return cast(SpatialGrid, record.data) def _measurement_table_may_declare_object_name(table: MeasurementTable) -> bool: diff --git a/openhcs/core/runtime_artifact_values.py b/openhcs/core/runtime_artifact_values.py index deb4343b71..f619f7af65 100644 --- a/openhcs/core/runtime_artifact_values.py +++ b/openhcs/core/runtime_artifact_values.py @@ -4,7 +4,7 @@ from collections.abc import Sequence from dataclasses import dataclass, replace -from typing import TYPE_CHECKING, Any, Self +from typing import TYPE_CHECKING, Any from openhcs.core.artifacts import ArtifactOutputPlan, ArtifactSpec, ArtifactType from openhcs.core.component_group_scope import ( @@ -48,16 +48,15 @@ class RuntimeValue: data: Any materialization_source_metadata: "ImagePayloadMetadata | None" = None - @classmethod + @staticmethod def from_output_plan( - cls, output_plan: ArtifactOutputPlan, data: Any, *, execution_scope: RuntimeExecutionAxisScope, materialization_source_metadata: "ImagePayloadMetadata | None" = None, - ) -> Self: - """Construct one exact runtime value from already-normalized data.""" + ) -> "RuntimeValue": + """Construct a transient value from already-normalized data and identity.""" if not isinstance(execution_scope, RuntimeExecutionAxisScope): raise TypeError( @@ -68,24 +67,23 @@ def from_output_plan( output_plan.group_component if group_key is not None else None, group_key, ) - return cls.from_spec( + return RuntimeValue.from_spec( output_plan, data, execution_scope=artifact_scope, materialization_source_metadata=materialization_source_metadata, ) - @classmethod + @staticmethod def from_spec( - cls, spec: ArtifactSpec, data: Any, *, execution_scope: RuntimeExecutionAxisScope, materialization_source_metadata: "ImagePayloadMetadata | None" = None, - ) -> Self: - """Derive transient or stored runtime identity from one declared artifact.""" - return cls( + ) -> "RuntimeValue": + """Derive transient runtime identity without declaring a stored location.""" + return RuntimeValue( key=ArtifactKey( name=spec.name, artifact_type=spec.artifact_type, @@ -151,9 +149,8 @@ def compose( producer_group_scope=producer_group_scope, ) - @classmethod + @staticmethod def normalize( - cls, output_plan: ArtifactOutputPlan, value: Any, *, @@ -162,16 +159,15 @@ def normalize( ) -> "RuntimeValue": """Normalize a raw artifact return exactly once through its compiled plan.""" - return cls.normalize_for_execution_scope( + return RuntimeValue.normalize_for_execution_scope( output_plan, value, execution_scope=RuntimeExecutionAxisScope(axis_id=str(axis_id)), materialization_source_metadata=materialization_source_metadata, ) - @classmethod + @staticmethod def normalize_for_execution_scope( - cls, output_plan: ArtifactOutputPlan, value: Any, *, @@ -209,7 +205,7 @@ def normalize_for_execution_scope( axis_id=execution_scope.axis_id, ) - runtime_value = cls.from_output_plan( + runtime_value = RuntimeValue.from_output_plan( output_plan, normalized, execution_scope=execution_scope, @@ -259,5 +255,9 @@ def validated_for_output_plan( self.artifact_type.validate_runtime_payload(output_plan.name, self.data) semantic_id = self.artifact_type.runtime_semantic_id(self.data) if semantic_id != self.key.semantic_id: - return replace(self, key=replace(self.key, semantic_id=semantic_id)) + return RuntimeValue( + key=replace(self.key, semantic_id=semantic_id), + data=self.data, + materialization_source_metadata=self.materialization_source_metadata, + ) return self diff --git a/openhcs/core/runtime_equivalence.py b/openhcs/core/runtime_equivalence.py index 4fbbbe3d2f..bb1bf87b4b 100644 --- a/openhcs/core/runtime_equivalence.py +++ b/openhcs/core/runtime_equivalence.py @@ -639,8 +639,8 @@ def accept_measurement_table( ) -> None: self.measurement_tables.append( RuntimeScopedMeasurementTable( - cast(MeasurementTable, record.value.data), - record_identity=record.path, + cast(MeasurementTable, record.data), + record_identity=record.location.path, execution_scope=record.key.scope, ) ) @@ -654,7 +654,7 @@ def accept_relationship_record( record: StoredRuntimeValue, ) -> None: """Record a relationship artifact observed on this axis.""" - relationship = cast(ObjectRelationship, record.value.data) + relationship = cast(ObjectRelationship, record.data) relationship_identity = RuntimeObjectRelationshipIdentity.from_relationship( relationship ) @@ -879,7 +879,7 @@ def measurement_fact_counter_for_projected_key( def record_spatial_grid(self, record: StoredRuntimeValue) -> None: record_measurement_facts( self.measurement_fact_counts, - spatial_grid_measurement_facts(record.value, self.policy), + spatial_grid_measurement_facts(record, self.policy), required_keys=self.required_measurement_keys, ) @@ -3220,7 +3220,7 @@ def _measurement_source_names_from_artifact_execution( for record in records: if record.key.artifact_type is not MeasurementsArtifactType: continue - table = cast(MeasurementTable, record.value.data) + table = cast(MeasurementTable, record.data) if table.source_image_name is not None: source_names.update(_source_name_aliases(table.source_image_name)) for row in iter_measurement_rows((table,)): diff --git a/openhcs/core/runtime_exports.py b/openhcs/core/runtime_exports.py index ee5f64cc05..8712395dfd 100644 --- a/openhcs/core/runtime_exports.py +++ b/openhcs/core/runtime_exports.py @@ -389,7 +389,7 @@ def _file_bundle_failures( ) -> tuple[str, ...]: failures: list[str] = [] for record in _runtime_records_for_specs(output_specs, runtime_records_by_axis): - payload = record.value.materialization_payload() + payload = record.materialization_payload() if type(payload) is not dict: failures.append( f"materialized file-bundle artifact {record.key.name!r} has " @@ -441,7 +441,7 @@ def _table_schema_field_failures( if record.key.artifact_type is not MeasurementsArtifactType: return () expected_fields = tuple( - field.name for field in cast(MeasurementTable, record.value.data).rows.fields + field.name for field in cast(MeasurementTable, record.data).rows.fields ) if not expected_fields: return () @@ -467,7 +467,7 @@ def _table_row_count_failures( table_outputs: tuple[Path, ...], row_counts_by_path: Mapping[Path, int], ) -> tuple[str, ...]: - if materialization_is_empty(record.value.materialization_payload()): + if materialization_is_empty(record.materialization_payload()): return () return tuple( f"table output {path} has no data rows" diff --git a/openhcs/core/runtime_stores.py b/openhcs/core/runtime_stores.py index bd3059d90b..afdd0b1a28 100644 --- a/openhcs/core/runtime_stores.py +++ b/openhcs/core/runtime_stores.py @@ -4,7 +4,7 @@ from collections import OrderedDict from collections.abc import Mapping, Sequence -from dataclasses import dataclass, field as dataclass_field, replace +from dataclasses import dataclass, field as dataclass_field import inspect from pathlib import Path from types import MappingProxyType @@ -242,25 +242,12 @@ def matches(self, record: "StoredRuntimeValue") -> bool: return True -@dataclass(frozen=True, slots=True) -class StoredRuntimeValue: +@dataclass(frozen=True, slots=True, kw_only=True) +class StoredRuntimeValue(RuntimeValue): """A validated runtime value with its persistence boundary.""" - value: RuntimeValue location: RuntimeArtifactLocation - @property - def key(self) -> ArtifactKey: - return self.value.key - - @property - def path(self) -> str: - return self.location.path - - @property - def backend(self) -> str: - return self.location.backend - @dataclass(frozen=True, slots=True) class RuntimeArtifactInput: @@ -380,7 +367,7 @@ def candidate_execution_scopes( if selects_complete_producer: record_coordinates.pop(producer_scope.component, None) if projected_components: - provenance = image_payload_metadata(record.value.data).source_provenance + provenance = image_payload_metadata(record.data).source_provenance plane_metadata = provenance.source_image_provenance_planes.runtime_component_metadata metadata_rows = plane_metadata or ( (provenance.source_component_metadata,) @@ -443,7 +430,7 @@ def candidate_execution_scopes( value=component_key, fixed_component_values=fixed_values, ) - scopes.setdefault(scope, record.path) + scopes.setdefault(scope, record.location.path) return scopes def all_records( @@ -504,7 +491,7 @@ def composed_value( """Compose exact records after projecting the runtime-axis coordinate.""" return RuntimeValue.compose( - tuple(self._axis_value(record.value) for record in records), + tuple(self._axis_value(record) for record in records), self._producer_group_composition_scope(), ) @@ -514,7 +501,7 @@ def projected_values( ) -> tuple[RuntimeValue, ...]: """Return exact producer values projected into this consumer scope.""" - return tuple(self._axis_value(record.value) for record in self.records(store)) + return tuple(self._axis_value(record) for record in self.records(store)) def resolve_value(self, store: "RuntimeValueStore") -> Any: """Resolve this compiled input to its invocation value.""" @@ -607,8 +594,8 @@ def _axis_value(self, value: RuntimeValue) -> RuntimeValue: f"{metadata_scope.component_values!r} selects multiple producer " f"runtime slices {matching_indices!r}." ) - return replace( - value, + return RuntimeValue( + key=value.key, data=RuntimeSliceProjection.value_for_slice( payload, RuntimePlaneAxisValueProjection.from_selected_plane( @@ -617,6 +604,7 @@ def _axis_value(self, value: RuntimeValue) -> RuntimeValue: axis_size=slice_count, ), ), + materialization_source_metadata=value.materialization_source_metadata, ) def _consumer_component_value(self, component: AllComponents) -> str: @@ -885,7 +873,7 @@ def from_record(cls, record: StoredRuntimeValue) -> "RuntimeArtifactAddress": return cls( key=record.key, location=record.location, - value_type=type(record.value.data).__qualname__, + value_type=type(record.data).__qualname__, ) @classmethod @@ -1026,7 +1014,9 @@ def record( ) -> StoredRuntimeValue: """Record a validated value and its persistence location.""" record = StoredRuntimeValue( - value=value, + key=value.key, + data=value.data, + materialization_source_metadata=value.materialization_source_metadata, location=RuntimeArtifactLocation(path=path, backend=backend), ) existing = self._current_record(value.key) @@ -1052,7 +1042,9 @@ def replace( keeping record() strict for accidental duplicate writes. """ record = StoredRuntimeValue( - value=value, + key=value.key, + data=value.data, + materialization_source_metadata=value.materialization_source_metadata, location=RuntimeArtifactLocation(path=path, backend=backend), ) self._records_by_location[(value.key, record.location)] = record diff --git a/openhcs/core/steps/function_artifact_materialization.py b/openhcs/core/steps/function_artifact_materialization.py index 5f02fe7b64..e0aa18c3e3 100644 --- a/openhcs/core/steps/function_artifact_materialization.py +++ b/openhcs/core/steps/function_artifact_materialization.py @@ -140,7 +140,7 @@ def records_for_group( record_locations = tuple( ( record.key.semantic_id, - record.path, + record.location.path, ) for record in records ) @@ -175,7 +175,7 @@ def records_for_output( list[StoredRuntimeValue], ] = {} for record in records: - payload = output_plan.materialization_payload(record.value) + payload = output_plan.materialization_payload(record) metadata = image_payload_metadata(payload) address = OpenHCSPlaneAddress.from_complete_source_metadata( metadata.source_component_metadata @@ -229,9 +229,9 @@ def _record_for_scalar_address( ) owner_record = owner_records[0] - owner_payload = output_plan.materialization_payload(owner_record.value) + owner_payload = output_plan.materialization_payload(owner_record) for record in records: - payload = output_plan.materialization_payload(record.value) + payload = output_plan.materialization_payload(record) if not cls._payloads_are_equivalent(owner_payload, payload): raise ValueError( "Conflicting scalar image materialization payloads for source " @@ -271,7 +271,7 @@ def records_for_group( if len(records) <= 1: return records group_plan = output_plan.for_group(group_key) - tables = tuple(cast(MeasurementTable, record.value.data) for record in records) + tables = tuple(cast(MeasurementTable, record.data) for record in records) table = MeasurementTableUnion(output_plan.name, tables).as_artifact_table() value = RuntimeValue.normalize_for_execution_scope( group_plan, @@ -280,7 +280,9 @@ def records_for_group( ) return ( StoredRuntimeValue( - value=value, + key=value.key, + data=value.data, + materialization_source_metadata=value.materialization_source_metadata, location=RuntimeArtifactLocation( path=group_plan.path, backend=Backend.MEMORY.value, @@ -637,7 +639,7 @@ def build( "Artifact record descriptor requires a materialization spec." ) if record.key.artifact_type.uses_aggregate_materialization_identity( - record.value.data + record.data ): metadata = cls.record_payload_metadata(record) aggregate_provenance = ( @@ -731,11 +733,11 @@ def record_payload_metadata( ) -> ImagePayloadMetadata | None: if record is None: return None - if isinstance(record.value.data, SourceImageProvenanceFields): + if isinstance(record.data, SourceImageProvenanceFields): return ImagePayloadMetadata( - source_provenance=record.value.data.source_provenance, + source_provenance=record.data.source_provenance, ) - return image_payload_metadata(record.value.data) + return image_payload_metadata(record.data) @classmethod def record_metadata_with_runtime_scope( @@ -787,14 +789,14 @@ def record_source_descriptor( and output_plan.materialization_source() != output_plan.source_context_source() ): - metadata = output_plan.materialization_metadata(record.value) + metadata = output_plan.materialization_metadata(record) metadata = cls.record_metadata_with_runtime_scope( record, metadata, ) parser_context = FunctionOutputParserContext.from_processing_context(context) use_filename_identity = materialization_spec.uses_filename_source_identity( - record.value.data + record.data ) if use_filename_identity or exact_fixed_scope: identity = FunctionOutputIdentity.from_filename_metadata( @@ -811,7 +813,7 @@ def record_source_descriptor( identity = FunctionOutputIdentity.from_metadata( parser_context.parser, metadata, - fallback_identity_path=record.path, + fallback_identity_path=record.location.path, variable_components=plan.variable_components, ) if identity is not None: @@ -950,7 +952,7 @@ def actual_materialization_records( axis_id=plan.axis_id, ) if record.key.scope.value_text is not None - and record.backend == Backend.MEMORY.value + and record.location.backend == Backend.MEMORY.value and store.get(record.key).location == record.location ) if dynamic_records: @@ -1003,7 +1005,7 @@ def actual_materialization_records( group_key=group_key, match_group=True, ) - if record.backend == Backend.MEMORY.value + if record.location.backend == Backend.MEMORY.value and store.get(record.key).location == record.location ) if not records: @@ -1028,8 +1030,8 @@ def actual_materialization_records( candidate_locations = tuple( ( candidate.key.scope.value_text, - candidate.backend, - candidate.path, + candidate.location.backend, + candidate.location.path, ) for candidate in candidates ) @@ -1037,7 +1039,7 @@ def actual_materialization_records( candidate for candidate in candidates if candidate.key.scope.value_text is None - and candidate.backend == Backend.MEMORY.value + and candidate.location.backend == Backend.MEMORY.value and store.get(candidate.key).location == candidate.location ) if identity_records: @@ -1088,7 +1090,7 @@ def from_record( f"Artifact output {output_plan.name!r} declares unsupported " f"materialization {type(spec).__name__}." ) - data = output_plan.materialization_payload(record.value) + data = output_plan.materialization_payload(record) emits_projected_planes = spec.emits_variable_component_planes(data) if ( output_plan.materialization_uses_source_identity_filename() @@ -1127,7 +1129,7 @@ def from_record( plan, context, record.key.scope.value_text, - artifact_path=record.path, + artifact_path=record.location.path, record=record, materialization_spec=spec, output_plan=output_plan, @@ -1409,7 +1411,7 @@ def materialize_artifact_outputs( backend_plan = target_plan.backend_plan(plan, context, materialization) record = materialization.record data = materialization.data - filemanager.ensure_directory(Path(record.path).parent, record.backend) + filemanager.ensure_directory(Path(record.location.path).parent, record.location.backend) stream_output_paths = materialization.spec.candidate_paths( str(materialization.base_path) ) diff --git a/openhcs/interop/cellprofiler/analyst_export.py b/openhcs/interop/cellprofiler/analyst_export.py index db6c084f78..7c2bbb8fcc 100644 --- a/openhcs/interop/cellprofiler/analyst_export.py +++ b/openhcs/interop/cellprofiler/analyst_export.py @@ -991,7 +991,7 @@ def collect_recorded_image_provenance( record for record in records if record.key.name == channel.alias ) for record in channel_records: - provenance = image_payload_metadata(record.value.data).source_provenance + provenance = image_payload_metadata(record.data).source_provenance self.collect_image_provenance( provenance, scope=record.key.scope, @@ -1285,7 +1285,7 @@ def build( ), ) all_measurement_tables = tuple( - cast(MeasurementTable, record.value.data) + cast(MeasurementTable, record.data) for records in measurement_records.values() for record in records ) @@ -1398,7 +1398,7 @@ def _collect_measurements( ], ) -> tuple[tuple[FieldSpec, ...], tuple[FieldSpec, ...]]: for record in records: - table = cast(MeasurementTable, record.value.data) + table = cast(MeasurementTable, record.data) image_columns, experiment_columns = self._collect_measurement_table( table=table, scope=record.key.scope, @@ -1558,7 +1558,7 @@ def _collect_relationships( ], ) -> None: for record in records: - relationship = cast(ObjectRelationship, record.value.data) + relationship = cast(ObjectRelationship, record.data) relationship_declaration = relationship.declaration declaration = ( relationship_declaration.relationship_type, @@ -1591,7 +1591,7 @@ def _collect_measurement_provenance( ) -> None: channel_aliases = frozenset(channel.alias for channel in image_channels) for record in records: - table = cast(MeasurementTable, record.value.data) + table = cast(MeasurementTable, record.data) provenance = table.source_provenance for source_image_name in provenance.represented_source_image_names: if source_image_name not in channel_aliases: @@ -1646,7 +1646,7 @@ def _collect_image_thumbnails( for record in records: if record.key.name != image_name: continue - payload = record.value.data + payload = record.data provenance = image_payload_metadata(payload).source_provenance image_numbers = row_projection.image_numbers_for_provenance( provenance, diff --git a/openhcs/interop/cellprofiler/runtime/adapter.py b/openhcs/interop/cellprofiler/runtime/adapter.py index a50c05656a..78ef97fcf0 100644 --- a/openhcs/interop/cellprofiler/runtime/adapter.py +++ b/openhcs/interop/cellprofiler/runtime/adapter.py @@ -218,7 +218,7 @@ def artifact_output_value(self, output_plan: ArtifactOutputPlan) -> object: f"CellProfiler output {output_plan.ref()!r} requires exactly one " f"invocation record, got {len(records)}." ) - return RuntimeValue.compose((records[0].value,)) + return RuntimeValue.compose((records[0],)) def require_artifact_available( self, @@ -438,7 +438,7 @@ def get_measurements( ) return MeasurementTableUnion( name, - tuple(cast(MeasurementTable, record.value.data) for record in records), + tuple(cast(MeasurementTable, record.data) for record in records), ).as_table() def measurement_tables( @@ -452,7 +452,7 @@ def measurement_tables( group_key=group_key, match_group=match_group, ) - return tuple(cast(MeasurementTable, record.value.data) for record in records) + return tuple(cast(MeasurementTable, record.data) for record in records) def declared_measurement_input_records( self, diff --git a/openhcs/interop/cellprofiler/runtime/artifact_binding.py b/openhcs/interop/cellprofiler/runtime/artifact_binding.py index b9cb78aaad..6c8cbdcc53 100644 --- a/openhcs/interop/cellprofiler/runtime/artifact_binding.py +++ b/openhcs/interop/cellprofiler/runtime/artifact_binding.py @@ -528,7 +528,7 @@ def declared_measurement_tables(self) -> tuple[MeasurementTable, ...]: slice_axis = MeasurementRowAxisField.SLICE_INDEX for spec in measurement_specs: spec_tables = tuple( - cast(MeasurementTable, record.value.data) + cast(MeasurementTable, record.data) for record in self.adapter.artifact_input_records( spec.name, MeasurementsArtifactType, diff --git a/openhcs/interop/cellprofiler/runtime/object_label_measurements.py b/openhcs/interop/cellprofiler/runtime/object_label_measurements.py index 19c2890811..ae4ef4ae76 100644 --- a/openhcs/interop/cellprofiler/runtime/object_label_measurements.py +++ b/openhcs/interop/cellprofiler/runtime/object_label_measurements.py @@ -81,7 +81,7 @@ def measurement_tables( if not records: return () object_table_index = ObjectMeasurementTableIndex.from_tables( - tuple(cast(MeasurementTable, record.value.data) for record in records) + tuple(cast(MeasurementTable, record.data) for record in records) ) tables = object_table_index.for_object_feature( self.object_name, diff --git a/openhcs/interop/cellprofiler/runtime/object_measurement_vectors.py b/openhcs/interop/cellprofiler/runtime/object_measurement_vectors.py index 858e1e7ce8..7f43dc84fe 100644 --- a/openhcs/interop/cellprofiler/runtime/object_measurement_vectors.py +++ b/openhcs/interop/cellprofiler/runtime/object_measurement_vectors.py @@ -252,7 +252,7 @@ def runtime_feature_tables( ) table_records = [] for record in source_records: - table = cast(MeasurementTable, record.value.data) + table = cast(MeasurementTable, record.data) if ( query.table_may_carry_feature(table) or query.optional_value_index((table,)) is not None diff --git a/openhcs/processing/backends/cellprofiler/relationships.py b/openhcs/processing/backends/cellprofiler/relationships.py index 46ff64d888..5d990208f1 100644 --- a/openhcs/processing/backends/cellprofiler/relationships.py +++ b/openhcs/processing/backends/cellprofiler/relationships.py @@ -1576,7 +1576,7 @@ def upstream_child_measurement_values( spec.name, MeasurementsArtifactType, ): - table = record.value.data + table = record.data if not isinstance(table, MeasurementTable): raise TypeError( f"Declared measurement input {spec.name!r} carries " diff --git a/openhcs/processing/backends/cellprofiler/spreadsheet_export.py b/openhcs/processing/backends/cellprofiler/spreadsheet_export.py index 3c0745c822..d4a862020a 100644 --- a/openhcs/processing/backends/cellprofiler/spreadsheet_export.py +++ b/openhcs/processing/backends/cellprofiler/spreadsheet_export.py @@ -468,7 +468,7 @@ def _measurement_tables( for axis_records in records_by_axis.values() for record in axis_records ) - tables = tuple(cast(MeasurementTable, record.value.data) for record in records) + tables = tuple(cast(MeasurementTable, record.data) for record in records) all_tables.extend(tables) slice_axis = MeasurementRowAxisField.SLICE_INDEX row_domains = tuple( @@ -598,7 +598,7 @@ def _relationship_rows( artifact_batch, RelationshipsArtifactType, ): - relationship = cast(ObjectRelationship, record.value.data) + relationship = cast(ObjectRelationship, record.data) image_numbers_by_slice = image_numbers.for_source_slices( scope=record.key.scope, provenance=relationship.source_provenance, diff --git a/tests/diagnostics/owned_bootstrap_readback.py b/tests/diagnostics/owned_bootstrap_readback.py index c9d4956509..a90c30258c 100644 --- a/tests/diagnostics/owned_bootstrap_readback.py +++ b/tests/diagnostics/owned_bootstrap_readback.py @@ -107,7 +107,7 @@ def record(name): expected_runtime_paths.add(matched.path) return matched - image = record(image_name).value.data + image = record(image_name).data np.testing.assert_array_equal(image_payload_data(image), expected) assert addresses(image_payload_metadata(image)) == expected_addresses flow = summary.main_flow_materialization @@ -132,8 +132,8 @@ def record(name): result['steps'].append(step_result) if summary.step_index % 3 == 0: continue - labels = record('volume_fixture_labels_v2').value.data - rows = record('volume_fixture_rows_v2').value.data + labels = record('volume_fixture_labels_v2').data + rows = record('volume_fixture_rows_v2').data np.testing.assert_array_equal(object_label_dense_array(labels), expected.astype(np.int32)) assert addresses(image_payload_metadata(labels)) == expected_addresses assert rows.subject.object_name == plans['volume_fixture_labels_v2'].name diff --git a/tests/integration/test_cellprofiler_generated_pipeline.py b/tests/integration/test_cellprofiler_generated_pipeline.py index f1b9b35131..57d3eba777 100644 --- a/tests/integration/test_cellprofiler_generated_pipeline.py +++ b/tests/integration/test_cellprofiler_generated_pipeline.py @@ -287,10 +287,10 @@ def _runtime_export_has_semantic_record( "Structured endpoint identity is only valid for relationship records." ) return any( - isinstance(record.value.data, ObjectRelationship) + isinstance(record.data, ObjectRelationship) and ( - record.value.data.declaration.source.name, - record.value.data.declaration.target.name, + record.data.declaration.source.name, + record.data.declaration.target.name, ) == name for record in _runtime_records( @@ -466,7 +466,7 @@ def test_cppipe_import_executes_through_canonical_zmq_path( axis_id="A01", ) assert len(nuclei_records) == 1 - assert object_label_dense_array(nuclei_records[0].value.data).max() > 0 + assert object_label_dense_array(nuclei_records[0].data).max() > 0 def test_bbbc021_cppipe_executes_named_channel_bindings_through_zmq( @@ -495,7 +495,7 @@ def test_bbbc021_cppipe_executes_named_channel_bindings_through_zmq( axis_id="A01", ) assert len(nuclei_records) == 1 - assert object_label_dense_array(nuclei_records[0].value.data).max() > 0 + assert object_label_dense_array(nuclei_records[0].data).max() > 0 assert len(composite_records) == 1 @@ -565,7 +565,7 @@ def test_loadimages_cppipe_preserves_source_artifact_bindings_through_zmq( axis_id="A01", ) assert len(corrected_records) == 1 - assert np.asarray(corrected_records[0].value.data).shape[-2:] == (64, 64) + assert np.asarray(corrected_records[0].data).shape[-2:] == (64, 64) def test_legacy_examplefly_load_data_cppipe_fails_at_nominal_import_boundary() -> None: @@ -607,7 +607,7 @@ def test_cppipe_relationship_outputs_are_compiler_derived_over_zmq( ) assert relationship_records assert measurement_records - relationship = cast(ObjectRelationship, relationship_records[0].value.data) + relationship = cast(ObjectRelationship, relationship_records[0].data) assert relationship.declaration.source.name == "Nuclei" assert relationship.declaration.target.name == "Cells" assert relationship.declaration.relationship_type == "Parent" @@ -675,10 +675,10 @@ def test_percent_positive_cppipe_executes_measurement_consumers_over_zmq( export, artifact_type=MeasurementsArtifactType, ) - if isinstance(record.value.data, MeasurementTable) + if isinstance(record.data, MeasurementTable) if any( row_mapping["output_name"] == "PercentPositive" - for row in record.value.data.rows.iter_row_mappings() + for row in record.data.rows.iter_row_mappings() for row_mapping in (measurement_row_mapping(row),) if "output_name" in row_mapping ) @@ -686,7 +686,7 @@ def test_percent_positive_cppipe_executes_measurement_consumers_over_zmq( assert calculate_math_records calculate_math_table = cast( MeasurementTable, - calculate_math_records[0].value.data, + calculate_math_records[0].data, ) calculate_math_rows = tuple( measurement_row_mapping(row) @@ -722,7 +722,7 @@ def test_examplehuman_cppipe_executes_source_bound_objects_over_zmq( axis_id=axis_id, ) assert len(cytoplasm_records) == 1 - cytoplasm = object_label_dense_array(cytoplasm_records[0].value.data) + cytoplasm = object_label_dense_array(cytoplasm_records[0].data) assert cytoplasm.ndim == 2 or cytoplasm.ndim == 3 and cytoplasm.shape[0] == 1 assert _runtime_export_has_semantic_record( export, @@ -760,7 +760,7 @@ def test_official_untangleworms_preserves_overlay_shape_over_zmq( axis_id=axis_id, ) assert len(overlay_records) == 1 - overlay = np.asarray(overlay_records[0].value.data) + overlay = np.asarray(overlay_records[0].data) assert overlay.ndim == 4 assert overlay.shape[0] == 2 assert overlay.shape[-1] == 3 @@ -832,8 +832,8 @@ def test_official_colocalization_preserves_relationships_and_configured_export( ( record.key.scope.component, record.key.scope.value_text, - object_label_dense_array(record.value.data).shape, - cast(ObjectLabelValue, record.value.data).plane_axis, + object_label_dense_array(record.data).shape, + cast(ObjectLabelValue, record.data).plane_axis, ) for record in records ) @@ -871,8 +871,8 @@ def test_official_colocalization_preserves_relationships_and_configured_export( ) assert { ( - record.value.data.declaration.source.name, - record.value.data.declaration.target.name, + record.data.declaration.source.name, + record.data.declaration.target.name, ) for record in relationship_records } == { @@ -888,8 +888,8 @@ def test_official_colocalization_preserves_relationships_and_configured_export( ) assert { ( - record.value.data.declaration.source.name, - record.value.data.declaration.target.name, + record.data.declaration.source.name, + record.data.declaration.target.name, ) for record in lineage_records } == { @@ -897,7 +897,7 @@ def test_official_colocalization_preserves_relationships_and_configured_export( ("Objects1", "ColocalizedRegion"), } relationships = tuple( - cast(ObjectRelationship, record.value.data) for record in relationship_records + cast(ObjectRelationship, record.data) for record in relationship_records ) assert { ( @@ -947,7 +947,7 @@ def test_official_neighbors_preserves_measurement_and_image_exports_over_zmq( axis_id=axis_id, ) assert cells_records - assert object_label_dense_array(cells_records[0].value.data).max() > 0 + assert object_label_dense_array(cells_records[0].data).max() > 0 assert _runtime_export_has_semantic_record( export, name="MeasureObjectNeighbors_6_measurements", diff --git a/tests/integration/test_chained_callable_lineage_journey.py b/tests/integration/test_chained_callable_lineage_journey.py index 7afb82b34b..8960728d8c 100644 --- a/tests/integration/test_chained_callable_lineage_journey.py +++ b/tests/integration/test_chained_callable_lineage_journey.py @@ -159,7 +159,7 @@ def test_chained_public_callable_uses_declared_main_flow_not_storage_argument( ) ) records.append(record) - image, labels, rows = (record.value.data for record in records) + image, labels, rows = (record.data for record in records) np.testing.assert_array_equal(np.squeeze(image_payload_data(image)), fixture) np.testing.assert_array_equal(np.squeeze(object_label_dense_array(labels)), fixture) assert rows.subject.object_name == labels_plan.name diff --git a/tests/integration/test_measurement_declaration_journey.py b/tests/integration/test_measurement_declaration_journey.py index 112b787ab7..c1efc52742 100644 --- a/tests/integration/test_measurement_declaration_journey.py +++ b/tests/integration/test_measurement_declaration_journey.py @@ -325,15 +325,15 @@ def test_exact_secondary_selector_survives_authoring_compile_and_execution(tmp_p assert secondary.key.scope.value_text_for_component(AllComponents.CHANNEL) == ( "1" if same_source else "2" ) - primary_area = np.count_nonzero(object_label_dense_array(primary.value.data)) - secondary_area = np.count_nonzero(object_label_dense_array(secondary.value.data)) + primary_area = np.count_nonzero(object_label_dense_array(primary.data)) + secondary_area = np.count_nonzero(object_label_dense_array(secondary.data)) assert secondary_area > primary_area > 0 [output] = invocation.artifact_output_plans [measurement] = store.find(name=output.name, artifact_type=MeasurementsArtifactType, axis_id="A01") - assert measurement.value.data.subject.object_name == "Cells" + assert measurement.data.subject.object_name == "Cells" image_name = "DNA" if same_source else "Actin" values = measurement_values_for_feature( - (measurement.value.data,), + (measurement.data,), f"Intensity_IntegratedIntensity_{image_name}", object_count=1, object_name="Cells", @@ -403,7 +403,7 @@ def test_explicit_measurement_rosters_survive_one_matched_source_anchor( ) for object_name in ("Nuclei", "Cells"): (labels,) = context.runtime_value_store.find(name=object_name, axis_id="A01") - area = np.count_nonzero(object_label_dense_array(labels.value.data)) + area = np.count_nonzero(object_label_dense_array(labels.data)) for image_name, integrated in (("DNA", 4.0), ("Actin", float(area))): expected_features = { "IntegratedIntensity": integrated, @@ -413,7 +413,7 @@ def test_explicit_measurement_rosters_survive_one_matched_source_anchor( } for feature, expected in expected_features.items(): values = measurement_values_for_feature( - (measurement.value.data,), + (measurement.data,), f"Intensity_{feature}_{image_name}", object_count=1, object_name=object_name, @@ -496,8 +496,8 @@ def test_headless_entrypoint_requires_subject_and_executes_corrected_rows( [counts] = store.find(name="PixelCounts", axis_id="A01") [image] = store.find(name="CountedImage", axis_id="A01") assert image.key.artifact_type is ImageArtifactType - assert counts.value.data.subject.source_image_name == "CountedImage" - assert tuple(counts.value.data.rows.column_values("pixel_count")) == (4,) + assert counts.data.subject.source_image_name == "CountedImage" + assert tuple(counts.data.rows.column_values("pixel_count")) == (4,) step_plan = bundle.runtime_contexts["A01"].step_plans[0] [csv_path] = step_plan.artifact_analysis_output_dir.glob( f"*_{counts.key.name}_step{step_plan.step_index}_details.csv" @@ -505,4 +505,4 @@ def test_headless_entrypoint_requires_subject_and_executes_corrected_rows( with csv_path.open(newline="") as stream: [persisted] = DictReader(stream) assert persisted["pixel_count"] == "4" - assert persisted["source_image_name"] == counts.value.data.subject.source_image_name + assert persisted["source_image_name"] == counts.data.subject.source_image_name diff --git a/tests/integration/test_primary_segmentation_diagnostics_journey.py b/tests/integration/test_primary_segmentation_diagnostics_journey.py index c73c3577a2..cc114b48d4 100644 --- a/tests/integration/test_primary_segmentation_diagnostics_journey.py +++ b/tests/integration/test_primary_segmentation_diagnostics_journey.py @@ -307,14 +307,14 @@ def step(func, name, kwargs): store = context.runtime_value_store (primary_record,) = store.find(name="Nuclei", axis_id="A01") (secondary_record,) = store.find(name="Cells", axis_id="A01") - source_metadata = primary_record.value.data.metadata + source_metadata = primary_record.data.metadata assert ( - np.count_nonzero(object_label_dense_array(secondary_record.value.data)) - >= np.count_nonzero(object_label_dense_array(primary_record.value.data)) + np.count_nonzero(object_label_dense_array(secondary_record.data)) + >= np.count_nonzero(object_label_dense_array(primary_record.data)) > 0 ) - primary_labels = object_label_dense_array(primary_record.value.data) - secondary_labels = object_label_dense_array(secondary_record.value.data) + primary_labels = object_label_dense_array(primary_record.data) + secondary_labels = object_label_dense_array(secondary_record.data) np.testing.assert_array_equal( np.unique(primary_labels), np.unique(secondary_labels) ) @@ -337,10 +337,10 @@ def step(func, name, kwargs): ) (options,) = spec.materialization.outputs (plane,) = MaterializationInput.from_runtime_slice_projected_value( - record.value.data, options + record.data, options ).items np.testing.assert_array_equal(tifffile.imread(saved_path), plane.data) - metadata = image_payload_metadata(record.value.data) + metadata = image_payload_metadata(record.data) # Sidecars have their own runtime alias, retaining the same source # address and pixel contributors as the primary object payload. assert metadata.source_provenance.scalar_source_identity == ( @@ -349,14 +349,14 @@ def step(func, name, kwargs): assert metadata.source_image_provenance_planes.as_contributors().identity == ( source_metadata.source_image_provenance_planes.as_contributors().identity ) - persisted.append(record.value.data) + persisted.append(record.data) diagnostics = PrimaryObjectDiagnosticPlanes(*persisted) np.testing.assert_array_equal( - diagnostics.unedited_objects.data, primary_record.value.data.unedited_labels + diagnostics.unedited_objects.data, primary_record.data.unedited_labels ) np.testing.assert_array_equal( diagnostics.small_removed_objects.data, - primary_record.value.data.small_removed_labels, + primary_record.data.small_removed_labels, ) # A memory-backed first step still owns persistent image artifacts. Later diff --git a/tests/unit/agent/test_callable_artifact_reference.py b/tests/unit/agent/test_callable_artifact_reference.py index 97e8911296..2fa56d7c41 100644 --- a/tests/unit/agent/test_callable_artifact_reference.py +++ b/tests/unit/agent/test_callable_artifact_reference.py @@ -306,8 +306,8 @@ def test_plate_reference_prepares_compiles_and_runs_in_original_parent( context = plate["_plate_context"](axis_id, (plan,)) plate["_record_measurements"](context, name=source.name, path=path, count=0) record = context.runtime_value_store.values()[0] - assert isinstance(record.value.data, MeasurementTable) - record.value.data.rows = reference_namespace["inspect_label_fixture"](fixture)[2] + assert isinstance(record.data, MeasurementTable) + record.data.rows = reference_namespace["inspect_label_fixture"](fixture)[2] plate["_record_measurements"]( context, name="unrelated_rows", path=f"/memory/{axis_id}/unrelated", count=99, ) @@ -319,7 +319,7 @@ def test_plate_reference_prepares_compiles_and_runs_in_original_parent( if record.key.name == output.name ) assert len(summaries) == 1 # one parent result, not one execution per axis - table = summaries[0].value.data + table = summaries[0].data assert summaries[0].key.artifact_type is SpecialArtifactType assert isinstance(table, DataclassMeasurementColumnarRows) assert table.row_mappings() == ( diff --git a/tests/unit/test_cellprofiler_display_manual_projection_declarations.py b/tests/unit/test_cellprofiler_display_manual_projection_declarations.py index 32f5048dd3..3c69d8f79e 100644 --- a/tests/unit/test_cellprofiler_display_manual_projection_declarations.py +++ b/tests/unit/test_cellprofiler_display_manual_projection_declarations.py @@ -329,7 +329,7 @@ def test_histogram_float_range_reconstructs_as_one_cellprofiler_setting_row() -> assert consumed == () assert len(blocks) == 1 range_rows = tuple( - record.value + record for record in blocks[0].setting_records if record.name == "Minimum/maximum values for the X-axis" ) diff --git a/tests/unit/test_cellprofiler_module_execution.py b/tests/unit/test_cellprofiler_module_execution.py index 437752d088..886a0f50d5 100644 --- a/tests/unit/test_cellprofiler_module_execution.py +++ b/tests/unit/test_cellprofiler_module_execution.py @@ -3363,7 +3363,7 @@ def test_compiled_measurement_output_preserves_image_and_object_row_ownership() artifact_type=MeasurementsArtifactType, axis_id="A01_s001", ) - tables = tuple(cast(MeasurementTable, record.value.data) for record in records) + tables = tuple(cast(MeasurementTable, record.data) for record in records) assert len(tables) == 1 table = tables[0] assert table.subject is not None @@ -3743,24 +3743,24 @@ def test_cellprofiler_adapter_preserves_object_label_source_component_metadata() record = adapter.add_objects("Nuclei", labels) - assert isinstance(record.value.data, ObjectLabelSet) - assert record.value.data.source_path == "/input/01_POS002_D.TIF" - assert dict(record.value.data.source_component_metadata) == { + assert isinstance(record.data, ObjectLabelSet) + assert record.data.source_path == "/input/01_POS002_D.TIF" + assert dict(record.data.source_component_metadata) == { "well": "01", "site": "POS002", "channel": "D", } - assert record.value.data.source_image_provenance_planes.paths == ( + assert record.data.source_image_provenance_planes.paths == ( "/input/01_POS002_D.TIF", ) assert tuple( dict(metadata) - for metadata in record.value.data.source_image_provenance_planes.component_metadata + for metadata in record.data.source_image_provenance_planes.component_metadata if metadata is not None ) == ( {"well": "01", "site": "POS002", "channel": "D"}, ) - assert filemanager.saved[0][0] is record.value.data + assert filemanager.saved[0][0] is record.data def test_default_row_policy_accepts_multi_source_image_row_ownership() -> None: @@ -4201,7 +4201,7 @@ def get_objects( artifact_type=ObjectLabelsArtifactType, purpose="test object-label artifact", ) - return cast(ObjectLabelSet, record.value.data) + return cast(ObjectLabelSet, record.data) def get_objects_across_groups(self, name: str) -> ObjectLabelSet: return self.runtime_objects[name] @@ -4333,7 +4333,7 @@ def add_spatial_grid( record = super().add_spatial_grid(name, grid) self.spatial_grids[name] = cast( SpatialGrid | RuntimeSliceAlignedValues, - record.value.data, + record.data, ) return record diff --git a/tests/unit/test_cellprofiler_runtime_adapter.py b/tests/unit/test_cellprofiler_runtime_adapter.py index 0a5478c560..cb930464bc 100644 --- a/tests/unit/test_cellprofiler_runtime_adapter.py +++ b/tests/unit/test_cellprofiler_runtime_adapter.py @@ -283,7 +283,7 @@ def _output_objects(adapter: CellProfilerRuntimeAdapter, name: str) -> ObjectLab _selected_output_plan(adapter, name, ObjectLabelsArtifactType) ) assert len(records) == 1 - value = records[0].value.data + value = records[0].data assert isinstance(value, ObjectLabelSet) return value @@ -295,7 +295,7 @@ def _output_measurements( records = adapter.artifact_output_records( _selected_output_plan(adapter, name, MeasurementsArtifactType) ) - tables = tuple(record.value.data for record in records) + tables = tuple(record.data for record in records) assert all(isinstance(table, MeasurementTable) for table in tables) return MeasurementTableUnion(name, tables).as_table() @@ -307,7 +307,7 @@ def _output_relationship( records = adapter.artifact_output_records( _selected_output_plan(adapter, name, RelationshipsArtifactType) ) - relationships = tuple(record.value.data for record in records) + relationships = tuple(record.data for record in records) assert all(isinstance(value, ObjectRelationship) for value in relationships) if len(relationships) == 1: return relationships[0] @@ -321,7 +321,7 @@ def _output_spatial_grid( records = adapter.artifact_output_records( _selected_output_plan(adapter, name, SpatialGridArtifactType) ) - value = RuntimeValue.compose(tuple(record.value for record in records)) + value = RuntimeValue.compose(tuple(record for record in records)) assert isinstance(value, (SpatialGrid, RuntimeSliceAlignedValues)) return value @@ -1010,8 +1010,8 @@ def test_cellprofiler_adapter_adds_and_reads_objects_through_runtime_store(): ) objects = _output_objects(adapter, NUCLEI) - assert isinstance(record.value.data, ObjectLabelSet) - assert record.value.data.name == NUCLEI + assert isinstance(record.data, ObjectLabelSet) + assert record.data.name == NUCLEI assert objects.labels is labels assert objects.source_image_name == DNA_IMAGE assert objects.dimensions == ("y", "x") @@ -1053,14 +1053,14 @@ def test_cellprofiler_adapter_contextualizes_source_aligned_object_label_stack() source_image_payload=source_image, ) - assert isinstance(record.value.data, ObjectLabelSet) - assert record.value.data.source_image_provenance_planes.paths == ( + assert isinstance(record.data, ObjectLabelSet) + assert record.data.source_image_provenance_planes.paths == ( "/src/A01_s001_w1_z001_t001.tif", "/src/A01_s002_w1_z001_t001.tif", ) assert tuple( dict(metadata) - for metadata in record.value.data.source_image_provenance_planes.component_metadata + for metadata in record.data.source_image_provenance_planes.component_metadata if metadata is not None ) == ( {"well": "A01", "site": "1", "channel": "1"}, @@ -1070,7 +1070,7 @@ def test_cellprofiler_adapter_contextualizes_source_aligned_object_label_stack() assert isinstance(saved_payload, ObjectLabelSet) assert ( saved_payload.source_image_provenance_planes.paths - == record.value.data.source_image_provenance_planes.paths + == record.data.source_image_provenance_planes.paths ) @@ -1110,7 +1110,7 @@ def test_cellprofiler_adapter_contextualizes_single_source_aligned_label_plane() source_image_payload=source_image, ) - object_labels = record.value.data + object_labels = record.data assert isinstance(object_labels, ObjectLabelSet) object_labels.validate_source_alignment(NUCLEI) assert object_labels.source_image_provenance_planes.paths == (source_path,) @@ -1142,9 +1142,9 @@ def test_cellprofiler_adapter_preserves_sparse_ijv_object_value_representation() record = adapter.add_objects(NUCLEI, labels) - assert isinstance(record.value.data, ObjectLabelSet) - assert record.value.data.representation is ObjectLabelRepresentation.SPARSE_IJV - assert record.value.data.labels is sparse_rows + assert isinstance(record.data, ObjectLabelSet) + assert record.data.representation is ObjectLabelRepresentation.SPARSE_IJV + assert record.data.labels is sparse_rows objects = _output_objects(adapter, NUCLEI) assert objects.representation is ObjectLabelRepresentation.SPARSE_IJV assert objects.labels is sparse_rows @@ -3706,8 +3706,8 @@ def test_cellprofiler_adapter_declared_relationship_allows_pruned_child_endpoint ) assert relationship.value.artifact_type is RelationshipsArtifactType - assert isinstance(relationship.value.data, ObjectRelationship) - assert relationship.value.data.declaration.target.name == "FilteredCells" + assert isinstance(relationship.data, ObjectRelationship) + assert relationship.data.declaration.target.name == "FilteredCells" def test_cellprofiler_adapter_relationships_accept_grouped_parent_inputs(): @@ -3989,8 +3989,8 @@ def test_cellprofiler_adapter_replaces_existing_payload_with_latest_binding(): NUCLEI, ObjectLabelPayload(variant_data=ObjectLabelVariantData(labels=second)) ) - assert isinstance(record.value.data, ObjectLabelSet) - assert record.value.data.labels is second + assert isinstance(record.data, ObjectLabelSet) + assert record.data.labels is second assert filemanager.deleted == [("memory", "/memory/Nuclei.pkl")] saved_objects = filemanager.saved[("memory", "/memory/Nuclei.pkl")] assert isinstance(saved_objects, ObjectLabelSet) @@ -4214,7 +4214,7 @@ def test_cellprofiler_adapter_preserves_same_artifact_measurement_subjects(): ) tables = tuple( - record.value.data + record.data for record in adapter.artifact_output_records(measurement_output_plan) ) @@ -4559,7 +4559,7 @@ def test_cellprofiler_adapter_hides_undeclared_same_name_object_table_occurrence NUCLEI_MEASUREMENTS, ), ) - ).value.data + ).data ) _compiled_artifact_inputs = { edge.key: edge diff --git a/tests/unit/test_cellprofiler_runtime_cache_authorities.py b/tests/unit/test_cellprofiler_runtime_cache_authorities.py index 108d2cb0e1..1a40152c9a 100644 --- a/tests/unit/test_cellprofiler_runtime_cache_authorities.py +++ b/tests/unit/test_cellprofiler_runtime_cache_authorities.py @@ -329,7 +329,7 @@ def get_relationship( axis_id=AXIS_ID, ) assert len(records) == 1 - relationship = records[0].value.data + relationship = records[0].data assert isinstance(relationship, ObjectRelationship) return relationship diff --git a/tests/unit/test_cellprofiler_spreadsheet_export.py b/tests/unit/test_cellprofiler_spreadsheet_export.py index 80ed53ca20..5136fda7f7 100644 --- a/tests/unit/test_cellprofiler_spreadsheet_export.py +++ b/tests/unit/test_cellprofiler_spreadsheet_export.py @@ -1645,9 +1645,11 @@ def _measurement_record( axis_id=axis_id, ) return StoredRuntimeValue( - value, - RuntimeArtifactLocation(path=output_plan.path, backend="memory"), - ) + key=value.key, + data=value.data, + materialization_source_metadata=value.materialization_source_metadata, + location=RuntimeArtifactLocation(path=output_plan.path, backend="memory"), + ) def _fixture_field_dtype( @@ -1694,6 +1696,8 @@ def _relationship_record(name: str, *, axis_id: str) -> StoredRuntimeValue: axis_id=axis_id, ) return StoredRuntimeValue( - value, - RuntimeArtifactLocation(path=output_plan.path, backend="memory"), - ) + key=value.key, + data=value.data, + materialization_source_metadata=value.materialization_source_metadata, + location=RuntimeArtifactLocation(path=output_plan.path, backend="memory"), + ) diff --git a/tests/unit/test_child_measurement_producer_selection.py b/tests/unit/test_child_measurement_producer_selection.py index 084987280c..07885613b8 100644 --- a/tests/unit/test_child_measurement_producer_selection.py +++ b/tests/unit/test_child_measurement_producer_selection.py @@ -217,7 +217,7 @@ def test_declared_child_measurements_gather_all_exact_producer_channels(dynamic) ) children = ObjectMeasurementTableIndex.from_tables( - tuple(record.value.data for record in records) + tuple(record.data for record in records) ).for_object("Children") assert [table.source_image_name for table in children] == [ "Orig1", @@ -571,9 +571,9 @@ def test_actual_parent_means_include_each_channel_without_merging_site_ids(): def test_upstream_tables_without_source_coordinates_are_rejected(): rows, child, _ = _upstream_rows() table = next( - record.value.data + record.data for record in rows.request.adapter.request.context.runtime_value_store.values() - if isinstance(record.value.data, MeasurementTable) + if isinstance(record.data, MeasurementTable) ) table.source_image_provenance_planes = SourceImageProvenancePlanes() with pytest.raises(ValueError, match="complete source image-set identity"): @@ -732,9 +732,9 @@ def test_complete_dynamic_input_rejects_foreign_producer_location(): foreign = RuntimeValueStore() for record in _measurement_store(edge).values(): foreign.record( - record.value, - path=record.path.replace("/memory/", "/another-producer/"), - backend=record.backend, + record, + path=record.location.path.replace("/memory/", "/another-producer/"), + backend=record.location.backend, ) with pytest.raises(RuntimeError, match="Missing dynamic grouped artifact input"): _runtime_input(edge).records(foreign) @@ -744,7 +744,7 @@ def test_complete_dynamic_input_rejects_foreign_producer_backend(): edge, _ = _compiled_edge(dynamic=True) foreign = RuntimeValueStore() for record in _measurement_store(edge).values(): - foreign.record(record.value, path=record.path, backend="disk") + foreign.record(record, path=record.location.path, backend="disk") with pytest.raises(RuntimeError, match="Missing dynamic grouped artifact input"): _runtime_input(edge).records(foreign) @@ -755,8 +755,8 @@ def test_complete_dynamic_input_selects_compiled_producer_after_workspace_rebind original = store.values() for record in original: store.replace( - record.value, - path=record.path.replace("/memory/", "/later-producer/"), - backend=record.backend, + record, + path=record.location.path.replace("/memory/", "/later-producer/"), + backend=record.location.backend, ) assert _runtime_input(edge).records(store) == original diff --git a/tests/unit/test_function_artifact_materialization.py b/tests/unit/test_function_artifact_materialization.py index 2a889182ae..c72b1e1c66 100644 --- a/tests/unit/test_function_artifact_materialization.py +++ b/tests/unit/test_function_artifact_materialization.py @@ -2031,7 +2031,7 @@ def test_fixed_component_scopes_materialize_distinct_measurement_paths() -> None materializations = runtime_artifact_materializations(plan, context) assert len(records) == 2 - assert tuple(record.value.data.rows.row_count() for record in records) == (2, 1) + assert tuple(record.data.rows.row_count() for record in records) == (2, 1) assert tuple( record.key.scope.value_text_for_component(AllComponents.Z_INDEX) for record in records @@ -2503,7 +2503,7 @@ def test_actual_materialization_records_uses_dynamic_runtime_groups(): ) assert tuple(record.key.scope.value_text for record in records) == ("1", "2") - assert tuple(record.path for record in records) == ( + assert tuple(record.location.path for record in records) == ( "/memory/A01_w1_segmentation_masks_step7.pkl", "/memory/A01_w2_segmentation_masks_step7.pkl", ) @@ -3046,7 +3046,9 @@ def test_tile_positions_runtime_materializes_native_json_without_changing_payloa (retained,) = tuple((tmp_path / "results").rglob("*.json")) assert json.loads(retained.read_text()) == [list(pair) for pair in positions] assert positions == [(-1.25, 0.000000001), (921.600000001, -3.5), (1.0, 1843.2)] - assert record.value is value + assert record.key is value.key + assert record.data is value.data + assert record.materialization_source_metadata is value.materialization_source_metadata def test_materialize_artifact_outputs_skips_explicitly_disabled_artifact_without_record( diff --git a/tests/unit/test_function_artifact_outputs.py b/tests/unit/test_function_artifact_outputs.py index 977eab0089..4bb3763771 100644 --- a/tests/unit/test_function_artifact_outputs.py +++ b/tests/unit/test_function_artifact_outputs.py @@ -1045,7 +1045,7 @@ def analyze(image): axis_id="A01", ) assert len(stored) == 1 - assert tuple(stored[0].value.data.rows) == ({"count": 2},) + assert tuple(stored[0].data.rows) == ({"count": 2},) def test_trailing_object_labels_do_not_replace_canonical_image_output(): @@ -1114,7 +1114,7 @@ def count(_image): name=labels_spec.name, axis_id=context.axis_id, ) - assert isinstance(stored_labels.value.data, ObjectLabelSet) + assert isinstance(stored_labels.data, ObjectLabelSet) def test_execute_function_core_attaches_execution_group_identity_to_artifact(): @@ -1173,7 +1173,7 @@ def segment(image): match_group=True, ) assert len(stored) == 1 - metadata = image_payload_metadata(stored[0].value.data) + metadata = image_payload_metadata(stored[0].data) assert dict(metadata.source_component_metadata) == { "well": "A01", "site": "1", @@ -1243,7 +1243,7 @@ def segment(image): ) assert len(stored) == 1 assert stored[0].path == "/memory/A01_w2_segmentation_masks.pkl" - metadata = image_payload_metadata(stored[0].value.data) + metadata = image_payload_metadata(stored[0].data) assert dict(metadata.source_component_metadata) == { "well": "A01", "site": "1", @@ -1340,8 +1340,8 @@ def corrected_images(image): green_records = context.runtime_value_store.find(name="Green", axis_id="A01") assert len(red_records) == 1 assert len(green_records) == 1 - assert image_payload_data(red_records[0].value.data)[0, 0] == 1 - assert image_payload_data(green_records[0].value.data)[0, 0] == 2 + assert image_payload_data(red_records[0].data)[0, 0] == 1 + assert image_payload_data(green_records[0].data)[0, 0] == 2 def test_execute_function_core_keeps_image_sidecar_out_of_main_flow(): @@ -1394,7 +1394,7 @@ def crop_outputs(source): ) assert len(mask_records) == 1 np.testing.assert_array_equal( - image_payload_data(mask_records[0].value.data), + image_payload_data(mask_records[0].data), mask, ) @@ -1445,8 +1445,8 @@ def produce_corrected_image(image): ) stored = context.runtime_value_store.find(name="CorrectedImage", axis_id="A01") assert len(stored) == 1 - np.testing.assert_array_equal(image_payload_data(stored[0].value.data), output) - assert image_payload_metadata(stored[0].value.data).source_image_names == ( + np.testing.assert_array_equal(image_payload_data(stored[0].data), output) + assert image_payload_metadata(stored[0].data).source_image_names == ( "CorrectedImage", ) @@ -1502,7 +1502,7 @@ def produce_image(image): name=output_spec.name, axis_id=context.axis_id, ) - stored_data = stored.value.data + stored_data = stored.data assert isinstance(stored_data, RuntimeSliceAlignedValues) for index, source_payload in enumerate(source_slices): output_payload = stored_data.value_for_slice(index) @@ -1956,10 +1956,10 @@ def calculate_illumination(image): ) assert len(stored) == 1 np.testing.assert_array_equal( - image_payload_data(stored[0].value.data), + image_payload_data(stored[0].data), source_data + 1, ) - stored_metadata = image_payload_metadata(stored[0].value.data) + stored_metadata = image_payload_metadata(stored[0].data) assert stored_metadata.source_image_names == (illumination_spec.name,) assert stored_metadata.source_provenance.represented_source_image_names == ( illumination_spec.name, @@ -2161,7 +2161,7 @@ def labels_to_image(image, labels: ObjectLabelValue): stored = context.runtime_value_store.find(name="label_image", axis_id="A01") assert len(stored) == 1 - metadata = image_payload_metadata(stored[0].value.data) + metadata = image_payload_metadata(stored[0].data) assert ( tuple( dict(item) @@ -2215,10 +2215,10 @@ def segment(image): stored = context.runtime_value_store.find(name="nuclei", axis_id="A01") assert len(stored) == 1 - assert isinstance(stored[0].value.data, ObjectLabelSet) - assert stored[0].value.data.name == "nuclei" - assert stored[0].value.data.source_path == "/input/01_POS002_D.TIF" - assert dict(stored[0].value.data.source_component_metadata) == { + assert isinstance(stored[0].data, ObjectLabelSet) + assert stored[0].data.name == "nuclei" + assert stored[0].data.source_path == "/input/01_POS002_D.TIF" + assert dict(stored[0].data.source_component_metadata) == { "well": "01", "site": "POS002", "channel": "D", @@ -2281,7 +2281,7 @@ def segment(image): name=output_spec.name, axis_id=context.axis_id, ) - label_set = stored.value.data + 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 @@ -2363,11 +2363,11 @@ def count(image): ) ) [stored] = context.runtime_value_store.find(name=rows.name, axis_id=context.axis_id) - assert isinstance(stored.value.data, MeasurementTable) - assert stored.value.data.subject == MeasurementSubject( + assert isinstance(stored.data, MeasurementTable) + assert stored.data.subject == MeasurementSubject( MeasurementScope.IMAGE, image.name ) - assert tuple(stored.value.data.rows.column_values("cell_count")) == (2,) + assert tuple(stored.data.rows.column_values("cell_count")) == (2,) def test_compile_rejects_conflicting_measurement_subject_relations(): @@ -2428,7 +2428,7 @@ def count(image): name=measurement_spec.name, axis_id=context.axis_id, ) - table = stored.value.data + table = stored.data assert isinstance(table, MeasurementTable) assert table.name == measurement_spec.name assert table.subject == MeasurementSubject(MeasurementScope.ARTIFACT) @@ -2498,7 +2498,7 @@ def count(image): name=measurement_spec.name, axis_id=context.axis_id, ) - table = stored.value.data + table = stored.data assert isinstance(table, MeasurementTable) expected_rows = ( { @@ -2703,7 +2703,7 @@ def analyze(image): name="match_results", axis_id=context.axis_id, ) - assert stored.value.data is match_results + assert stored.data is match_results def test_execute_function_core_requires_store_record_even_when_vfs_payload_exists(): diff --git a/tests/unit/test_function_outputs.py b/tests/unit/test_function_outputs.py index 0c6c118337..a238e2431a 100644 --- a/tests/unit/test_function_outputs.py +++ b/tests/unit/test_function_outputs.py @@ -2327,8 +2327,8 @@ def test_runtime_image_metadata_target_requires_persisted_images(tmp_path, conte Backend.DISK.value: ( Output.from_metadata( path=str(directory / "A01_s001_w2_z001_t001.tif"), - content=record.value.data, - metadata=image_payload_metadata(record.value.data), + content=record.data, + metadata=image_payload_metadata(record.data), ), ) }, diff --git a/tests/unit/test_function_step_execution_scope.py b/tests/unit/test_function_step_execution_scope.py index 8299bb91f4..cb960d00d4 100644 --- a/tests/unit/test_function_step_execution_scope.py +++ b/tests/unit/test_function_step_execution_scope.py @@ -942,7 +942,7 @@ def export(*, artifact_batch: RuntimeArtifactBatch): assert len(batches) == 1 selected = batches[0].records(measurement_spec.ref())["A01"] assert len(selected) == 2 - tables = tuple(record.value.data for record in selected) + tables = tuple(record.data for record in selected) assert all(isinstance(table, MeasurementTable) for table in tables) assert {table.subject.object_name for table in tables} == { "Nuclei", diff --git a/tests/unit/test_persisted_roi_only_publication.py b/tests/unit/test_persisted_roi_only_publication.py index 216042a27d..4ec049524a 100644 --- a/tests/unit/test_persisted_roi_only_publication.py +++ b/tests/unit/test_persisted_roi_only_publication.py @@ -116,12 +116,15 @@ def forbidden_render(*args, **kwargs): artifact_type=MetadataArtifactType, materialization=spec, ) + value = RuntimeValue.from_output_plan( + artifact_plan, + {}, + execution_scope=RuntimeExecutionAxisScope(axis_id="A01"), + ) record = StoredRuntimeValue( - value=RuntimeValue.from_output_plan( - artifact_plan, - {}, - execution_scope=RuntimeExecutionAxisScope(axis_id="A01"), - ), + key=value.key, + data=value.data, + materialization_source_metadata=value.materialization_source_metadata, location=RuntimeArtifactLocation( path=artifact_plan.path, backend=Backend.MEMORY.value ), diff --git a/tests/unit/test_returned_output_abi.py b/tests/unit/test_returned_output_abi.py index a62970c746..7cac5c73e6 100644 --- a/tests/unit/test_returned_output_abi.py +++ b/tests/unit/test_returned_output_abi.py @@ -279,6 +279,6 @@ def process(_image: np.ndarray) -> tuple[ImageOutputBundle, np.ndarray]: records = context.runtime_value_store.find(name=spec.name, axis_id="A01") assert len(records) == 1 np.testing.assert_array_equal( - image_payload_data(records[0].value.data), + image_payload_data(records[0].data), expected, ) diff --git a/tests/unit/test_runtime_equivalence.py b/tests/unit/test_runtime_equivalence.py index 9d6c4040f0..b00af0e456 100644 --- a/tests/unit/test_runtime_equivalence.py +++ b/tests/unit/test_runtime_equivalence.py @@ -8756,15 +8756,13 @@ def test_runtime_measurement_snapshot_preserves_plane_local_single_row_aggregate axis_id = f"A01_s00{site}" records_by_axis[axis_id] = ( StoredRuntimeValue( - RuntimeValue( - key=ArtifactKey( - name="Crop", - artifact_type=MeasurementsArtifactType, - scope=RuntimeExecutionAxisScope(axis_id=axis_id), - ), - data=table, + key=ArtifactKey( + name="Crop", + artifact_type=MeasurementsArtifactType, + scope=RuntimeExecutionAxisScope(axis_id=axis_id), ), - RuntimeArtifactLocation( + data=table, + location=RuntimeArtifactLocation( path=f"/memory/Crop_site{site}.pkl", backend="memory", ), @@ -10732,15 +10730,13 @@ def test_runtime_reference_artifact_equivalence_ignores_duplicate_measurement_ar ) records = tuple( StoredRuntimeValue( - RuntimeValue( - key=ArtifactKey( - name="MeasureTexture", - artifact_type=MeasurementsArtifactType, - scope=RuntimeExecutionAxisScope(axis_id="A01"), - ), - data=table, + key=ArtifactKey( + name="MeasureTexture", + artifact_type=MeasurementsArtifactType, + scope=RuntimeExecutionAxisScope(axis_id="A01"), ), - RuntimeArtifactLocation( + data=table, + location=RuntimeArtifactLocation( path=f"/memory/MeasureTexture_{index}.pkl", backend="memory", ), @@ -10792,15 +10788,13 @@ def test_runtime_reference_artifact_equivalence_ignores_duplicate_image_feature_ ) records = tuple( StoredRuntimeValue( - RuntimeValue( - key=ArtifactKey( - name=table.name, - artifact_type=MeasurementsArtifactType, - scope=RuntimeExecutionAxisScope(axis_id="A01"), - ), - data=table, + key=ArtifactKey( + name=table.name, + artifact_type=MeasurementsArtifactType, + scope=RuntimeExecutionAxisScope(axis_id="A01"), ), - RuntimeArtifactLocation( + data=table, + location=RuntimeArtifactLocation( path=f"/memory/{table.name}_{index}.pkl", backend="memory", ), @@ -10852,19 +10846,17 @@ def test_runtime_reference_artifact_equivalence_ignores_grouped_duplicate_image_ ) records = tuple( StoredRuntimeValue( - RuntimeValue( - key=ArtifactKey( - name=table.name, - artifact_type=MeasurementsArtifactType, - scope=RuntimeExecutionAxisScope( - axis_id="A01", - component=AllComponents.CHANNEL, - value=group_key, - ), + key=ArtifactKey( + name=table.name, + artifact_type=MeasurementsArtifactType, + scope=RuntimeExecutionAxisScope( + axis_id="A01", + component=AllComponents.CHANNEL, + value=group_key, ), - data=table, ), - RuntimeArtifactLocation( + data=table, + location=RuntimeArtifactLocation( path=f"/memory/{table.name}_{group_key}.pkl", backend="memory", ), @@ -10927,15 +10919,13 @@ def test_runtime_reference_artifact_equivalence_applies_tolerance_after_same_pat ) records = tuple( StoredRuntimeValue( - RuntimeValue( - key=ArtifactKey( - name=table.name, - artifact_type=MeasurementsArtifactType, - scope=RuntimeExecutionAxisScope(axis_id="A01"), - ), - data=table, + key=ArtifactKey( + name=table.name, + artifact_type=MeasurementsArtifactType, + scope=RuntimeExecutionAxisScope(axis_id="A01"), ), - RuntimeArtifactLocation( + data=table, + location=RuntimeArtifactLocation( path=f"/memory/{table.name}.pkl", backend="memory", ), @@ -11055,15 +11045,13 @@ def test_runtime_reference_artifact_equivalence_preserves_distinct_source_image_ ) observation_records.append( StoredRuntimeValue( - RuntimeValue( - key=ArtifactKey( - name=table.name, - artifact_type=MeasurementsArtifactType, - scope=RuntimeExecutionAxisScope(axis_id="A01"), - ), - data=table, + key=ArtifactKey( + name=table.name, + artifact_type=MeasurementsArtifactType, + scope=RuntimeExecutionAxisScope(axis_id="A01"), ), - RuntimeArtifactLocation( + data=table, + location=RuntimeArtifactLocation( path=f"/memory/{Path(source_path).stem}.pkl", backend="memory", ), @@ -11112,15 +11100,13 @@ def test_runtime_reference_artifact_equivalence_ignores_duplicate_aggregate_tabl records_by_axis = { axis: ( StoredRuntimeValue( - RuntimeValue( - key=ArtifactKey( - name="IdentifyPrimaryObjects", - artifact_type=MeasurementsArtifactType, - scope=RuntimeExecutionAxisScope(axis_id=axis), - ), - data=table, + key=ArtifactKey( + name="IdentifyPrimaryObjects", + artifact_type=MeasurementsArtifactType, + scope=RuntimeExecutionAxisScope(axis_id=axis), ), - RuntimeArtifactLocation( + data=table, + location=RuntimeArtifactLocation( path=f"/memory/IdentifyPrimaryObjects_{axis}.pkl", backend="memory", ), @@ -11154,42 +11140,40 @@ def test_runtime_reference_artifact_equivalence_ignores_group_replayed_image_tab ) records = tuple( StoredRuntimeValue( - RuntimeValue( - key=ArtifactKey( - name="CalculateMath", - artifact_type=MeasurementsArtifactType, - scope=RuntimeExecutionAxisScope( - axis_id="A01", - component=AllComponents.CHANNEL, - value=channel, - ), + key=ArtifactKey( + name="CalculateMath", + artifact_type=MeasurementsArtifactType, + scope=RuntimeExecutionAxisScope( + axis_id="A01", + component=AllComponents.CHANNEL, + value=channel, ), - data=MeasurementTable( - name="CalculateMath", - rows=MeasurementSparseColumnarRows.from_rows( - ( - { - "slice_index": 0, - "feature_name": "Math_Stain1Colocalized", - "result_value": 0.25, - }, - { - "slice_index": 1, - "feature_name": "Math_Stain1Colocalized", - "result_value": 0.75, - }, - ), - fields=( - FieldSpec("slice_index", int), - FieldSpec("feature_name", str), - FieldSpec("result_value", float), - ), + ), + data=MeasurementTable( + name="CalculateMath", + rows=MeasurementSparseColumnarRows.from_rows( + ( + { + "slice_index": 0, + "feature_name": "Math_Stain1Colocalized", + "result_value": 0.25, + }, + { + "slice_index": 1, + "feature_name": "Math_Stain1Colocalized", + "result_value": 0.75, + }, + ), + fields=( + FieldSpec("slice_index", int), + FieldSpec("feature_name", str), + FieldSpec("result_value", float), ), - source_path=f"/source/channel_{channel}.tif", - subject=MeasurementSubject(MeasurementScope.IMAGE, "Image"), ), + source_path=f"/source/channel_{channel}.tif", + subject=MeasurementSubject(MeasurementScope.IMAGE, "Image"), ), - RuntimeArtifactLocation( + location=RuntimeArtifactLocation( path=f"/memory/CalculateMath_{channel}.pkl", backend="memory", ), @@ -11222,37 +11206,35 @@ def test_runtime_reference_artifact_equivalence_preserves_local_group_image_rows ) records = tuple( StoredRuntimeValue( - RuntimeValue( - key=ArtifactKey( - name="CalculateMath", - artifact_type=MeasurementsArtifactType, - scope=RuntimeExecutionAxisScope( - axis_id="A01", - component=AllComponents.CHANNEL, - value=channel, - ), + key=ArtifactKey( + name="CalculateMath", + artifact_type=MeasurementsArtifactType, + scope=RuntimeExecutionAxisScope( + axis_id="A01", + component=AllComponents.CHANNEL, + value=channel, ), - data=MeasurementTable( - name="CalculateMath", - rows=MeasurementSparseColumnarRows.from_rows( - ( - { - "slice_index": 0, - "feature_name": "Math_Stain1Colocalized", - "result_value": 0.25, - }, - ), - fields=( - FieldSpec("slice_index", int), - FieldSpec("feature_name", str), - FieldSpec("result_value", float), - ), + ), + data=MeasurementTable( + name="CalculateMath", + rows=MeasurementSparseColumnarRows.from_rows( + ( + { + "slice_index": 0, + "feature_name": "Math_Stain1Colocalized", + "result_value": 0.25, + }, + ), + fields=( + FieldSpec("slice_index", int), + FieldSpec("feature_name", str), + FieldSpec("result_value", float), ), - source_path=f"/source/channel_{channel}.tif", - subject=MeasurementSubject(MeasurementScope.IMAGE, "Image"), ), + source_path=f"/source/channel_{channel}.tif", + subject=MeasurementSubject(MeasurementScope.IMAGE, "Image"), ), - RuntimeArtifactLocation( + location=RuntimeArtifactLocation( path=f"/memory/CalculateMath_{channel}.pkl", backend="memory", ), @@ -11303,15 +11285,13 @@ def test_runtime_reference_artifact_equivalence_preserves_same_value_wide_image_ { "A01": ( StoredRuntimeValue( - RuntimeValue( - key=ArtifactKey( - name="IdentifyPrimaryObjects", - artifact_type=MeasurementsArtifactType, - scope=RuntimeExecutionAxisScope(axis_id="A01"), - ), - data=table, + key=ArtifactKey( + name="IdentifyPrimaryObjects", + artifact_type=MeasurementsArtifactType, + scope=RuntimeExecutionAxisScope(axis_id="A01"), ), - RuntimeArtifactLocation( + data=table, + location=RuntimeArtifactLocation( path="/memory/IdentifyPrimaryObjects_A01.pkl", backend="memory", ), @@ -11385,15 +11365,13 @@ def test_runtime_reference_artifact_equivalence_ignores_duplicate_object_rows( ) records.append( StoredRuntimeValue( - RuntimeValue( - key=ArtifactKey( - name="MeasureTexture", - artifact_type=MeasurementsArtifactType, - scope=RuntimeExecutionAxisScope(axis_id="A01"), - ), - data=table, + key=ArtifactKey( + name="MeasureTexture", + artifact_type=MeasurementsArtifactType, + scope=RuntimeExecutionAxisScope(axis_id="A01"), ), - RuntimeArtifactLocation( + data=table, + location=RuntimeArtifactLocation( path=f"/memory/MeasureTexture_{index}.pkl", backend="memory", ), diff --git a/tests/unit/test_runtime_exports.py b/tests/unit/test_runtime_exports.py index 3e14b1eb64..ba205b4974 100644 --- a/tests/unit/test_runtime_exports.py +++ b/tests/unit/test_runtime_exports.py @@ -176,12 +176,14 @@ def _stored_measurements_record( ), ) return StoredRuntimeValue( - value, - RuntimeArtifactLocation( - path=f"results/{axis_id}_Measurements_step1.csv", - backend="disk", - ), - ) + key=value.key, + data=value.data, + materialization_source_metadata=value.materialization_source_metadata, + location=RuntimeArtifactLocation( + path=f"results/{axis_id}_Measurements_step1.csv", + backend="disk", + ), + ) def _table_export_expectation() -> RuntimeExportExpectation: diff --git a/tests/unit/test_runtime_value_store.py b/tests/unit/test_runtime_value_store.py index 851a0efcd0..d8c00482c5 100644 --- a/tests/unit/test_runtime_value_store.py +++ b/tests/unit/test_runtime_value_store.py @@ -215,7 +215,7 @@ def test_store_transport_excludes_derived_caches_and_retains_records(): assert restored.revision == store.revision assert len(restored) == len(store) == 1 assert ( - restored.observed_values[0].value.data.row_mappings() + restored.observed_values[0].data.row_mappings() == value.data.row_mappings() ) assert ( @@ -572,9 +572,12 @@ def test_runtime_artifact_query_from_dynamic_input_plan_matches_discovered_group ) assert isinstance(query.target, RuntimeArtifactDynamicComponentTarget) + value = _runtime_value(path="/memory/measurements_wDAPI.pkl") assert query.matches( StoredRuntimeValue( - value=_runtime_value(path="/memory/measurements_wDAPI.pkl"), + key=value.key, + data=value.data, + materialization_source_metadata=value.materialization_source_metadata, location=RuntimeArtifactLocation( path="/memory/measurements_wDAPI.pkl", backend="memory", @@ -1150,6 +1153,10 @@ def test_runtime_artifact_input_projection_collapses_excluded_singleton_axis() - backend="memory", ) + projected_values = runtime_input.projected_values(store) + assert all(type(value) is RuntimeValue for value in projected_values) + assert all(image_payload_data(value.data).shape == (2, 2) for value in projected_values) + assert all(image_payload_data(record.data).shape == (1, 2, 2) for record in store.values()) payload = runtime_input.resolve_value(store) assert image_payload_data(payload).shape == (2, 2, 2) @@ -1362,7 +1369,7 @@ def test_runtime_artifact_input_projection_keeps_equal_keys_component_typed(): assert len(records) == 1 assert records[0].key.scope.component is AllComponents.SITE - np.testing.assert_array_equal(records[0].value.data, np.full((2, 2), 1.0)) + np.testing.assert_array_equal(records[0].data, np.full((2, 2), 1.0)) def test_runtime_artifact_input_projection_uses_compiled_group_for_plane_scope(): @@ -1761,7 +1768,7 @@ def test_exact_producer_group_does_not_constrain_consumer_source_channel(): store, record, runtime_input = _grouped_label_input_with_distinct_consumer_channel() assert runtime_input.records(store) == (record,) - assert runtime_input.resolve_value(store) is record.value.data + assert runtime_input.resolve_value(store) is record.data candidates = runtime_input.candidate_execution_scopes( store, ComponentGroupScope.ungrouped(), variable_components=ComponentSet((AllComponents.Z_INDEX,)), @@ -1794,7 +1801,7 @@ def test_exact_producer_group_preserves_shared_fixed_context_constraints(compone def test_exact_producer_group_rejects_other_producer_or_fixed_plane(mismatch): _store, record, runtime_input = _grouped_label_input_with_distinct_consumer_channel() scope = record.key.scope - path = record.path + path = record.location.path if mismatch == "group": scope = replace(scope, value="1") elif mismatch == "well": @@ -1820,8 +1827,8 @@ def test_exact_producer_group_rejects_other_producer_or_fixed_plane(mismatch): ) store = RuntimeValueStore() store.record( - replace(record.value, key=replace(record.key, scope=scope)), - path=path, backend=record.backend, + replace(record, key=replace(record.key, scope=scope)), + path=path, backend=record.location.backend, ) with pytest.raises(RuntimeError, match="Missing RuntimeValueStore record"): runtime_input.records(store) @@ -1840,7 +1847,7 @@ def test_different_producer_group_axis_preserves_fixed_source_channel_constraint storage_plan = replace( runtime_input.edge_plan.storage_plan, group_component=AllComponents.SITE, group_keys=("1",), - paths_by_group={"1": record.path}, + paths_by_group={"1": record.location.path}, ) runtime_input = replace( runtime_input, @@ -1855,8 +1862,8 @@ def test_different_producer_group_axis_preserves_fixed_source_channel_constraint ) store = RuntimeValueStore() store.record( - replace(record.value, key=replace(record.key, scope=scope)), - path=record.path, backend=record.backend, + replace(record, key=replace(record.key, scope=scope)), + path=record.location.path, backend=record.location.backend, ) with pytest.raises(RuntimeError, match="Missing RuntimeValueStore record"): runtime_input.records(store) @@ -1900,7 +1907,7 @@ def consume_labels(image, labels): request = RuntimeInputBindingRequest( adapter=adapter, kwargs={}, current_image=np.zeros((2, 2)) ) - assert request.artifact_value(edge) is record.value.data + assert request.artifact_value(edge) is record.data @pytest.mark.parametrize("component", [AllComponents.SITE, AllComponents.Z_INDEX, AllComponents.TIMEPOINT]) @@ -1973,14 +1980,14 @@ def test_paired_channel_projection_rejects_a_different_producer_site_plane(): store, record, runtime_input = _paired_channel_label_input() edge = runtime_input.edge_plan payload = replace( - record.value.data, + record.data, source_image_provenance_planes=SourceImageProvenancePlanes.from_components( paths=("/source/site2_DNA.tif",), component_metadata=({"site": "2", "channel": "1"},), ), ) store.replace( - replace(record.value, data=payload), path=record.path, backend=record.backend + replace(record, data=payload), path=record.location.path, backend=record.location.backend ) projected_input = replace( runtime_input, @@ -2003,7 +2010,7 @@ def test_paired_channel_input_rejects_ambiguous_address_matched_contexts(): _other_store, other_record, _other_input = _paired_channel_label_input( producer_values=((AllComponents.CHANNEL, "3"),), ) - store.record(other_record.value, path=other_record.path, backend=other_record.backend) + store.record(other_record, path=other_record.location.path, backend=other_record.location.backend) with pytest.raises(RuntimeError, match="Ambiguous RuntimeValueStore records"): runtime_input.records(store) @@ -2097,8 +2104,12 @@ def test_exact_input_admits_no_shared_projected_context_constraints( ) store = RuntimeValueStore() record = store.record( - replace(original.value, key=replace(original.key, scope=producer_scope)), - path=original.path, backend=original.backend, + RuntimeValue( + key=replace(original.key, scope=producer_scope), + data=original.data, + materialization_source_metadata=original.materialization_source_metadata, + ), + path=original.location.path, backend=original.location.backend, ) source = NamedSourceBinding( alias="Reference", @@ -2253,12 +2264,14 @@ def test_runtime_value_store_merges_observed_records_from_worker_boundary(): def test_runtime_measurement_observation_axis_accepts_table_record_once(): value = _runtime_value() record = StoredRuntimeValue( - value=value, - location=RuntimeArtifactLocation( + key=value.key, + data=value.data, + materialization_source_metadata=value.materialization_source_metadata, + location=RuntimeArtifactLocation( path="/memory/measurements.pkl", backend="memory", ), - ) + ) axis = RuntimeMeasurementObservationAxis("A01") axis.accept_measurement_table(record) @@ -2266,7 +2279,7 @@ def test_runtime_measurement_observation_axis_accepts_table_record_once(): assert len(axis.measurement_tables) == 1 scoped_table = axis.measurement_tables[0] assert scoped_table.table is value.data - assert scoped_table.record_identity == record.path + assert scoped_table.record_identity == record.location.path assert scoped_table.execution_scope == record.key.scope @@ -2292,10 +2305,10 @@ def test_dynamic_input_query_distinguishes_compiled_paths_with_same_backend(): RuntimeArtifactQuery.from_input_plan( ArtifactInputPlan( name="measurements", - path=record.path, + path=record.location.path, artifact_type=MeasurementsArtifactType, group_component=AllComponents.CHANNEL, - paths_by_group={None: record.path, "DAPI": record.path}, + paths_by_group={None: record.location.path, "DAPI": record.location.path}, ), axis_id="A01", backend="memory", @@ -2360,7 +2373,7 @@ def test_dynamic_query_snapshots_address_mapping_without_mutating_source_plan(): assert store.find_matching(old_query) == (first,) paths["DAPI"] = "/second/measurements.pkl" assert plan.paths_by_group is paths - assert old_query.target.input_plan.paths_by_group["DAPI"] == first.path + assert old_query.target.input_plan.paths_by_group["DAPI"] == first.location.path assert hash(old_query) == old_hash assert old_query == same_query assert store.find_matching(old_query) == (first,) @@ -2430,10 +2443,10 @@ def test_store_transport_excludes_all_derived_lookup_caches(): query = RuntimeArtifactQuery.from_input_plan( ArtifactInputPlan( name="measurements", - path=record.path, + path=record.location.path, artifact_type=MeasurementsArtifactType, group_component=AllComponents.CHANNEL, - paths_by_group={"DAPI": record.path}, + paths_by_group={"DAPI": record.location.path}, ), axis_id="A01", backend="memory", @@ -2453,13 +2466,14 @@ def test_store_transport_excludes_all_derived_lookup_caches(): assert restored._query_caches == {} assert restored.values()[0].key == record.key assert restored.values()[0].location == record.location - assert restored.values()[0].value.data.row_mappings() == value.data.row_mappings() + assert restored.values()[0].data.row_mappings() == value.data.row_mappings() assert restored.find_matching(query) == (restored.values()[0],) assert restored.get(record.key) is restored.values()[1] assert restored.get(record.key).location == latest.location assert restored.observed_values == restored.values() assert restored.values()[0] is restored.observed_values[0] - assert restored.values()[0].value is restored.values()[1].value + assert restored.values()[0].data is restored.values()[1].data + assert restored.values()[0].key is restored.values()[1].key def test_unified_store_cache_keeps_both_query_domains_and_empty_results(): @@ -2470,7 +2484,7 @@ def test_unified_store_cache_keeps_both_query_domains_and_empty_results(): query = RuntimeArtifactQuery.from_input_plan( ArtifactInputPlan( name="measurements", - path=record.path, + path=record.location.path, artifact_type=MeasurementsArtifactType, group_component=AllComponents.CHANNEL, ), @@ -2506,7 +2520,7 @@ def test_unified_store_cache_eviction_recomputes_order_without_changing_record_a query = RuntimeArtifactQuery.from_output_plan( ArtifactOutputPlan( name="first", - path=first.path, + path=first.location.path, artifact_type=MeasurementsArtifactType, group_component=AllComponents.CHANNEL, group_keys=("DAPI",), @@ -2522,8 +2536,31 @@ def test_unified_store_cache_eviction_recomputes_order_without_changing_record_a assert recomputed == all_records assert recomputed is not all_records assert recomputed[0] is first and recomputed[1] is second - replacement = store.replace(first_value, path=first.path, backend="memory") + replacement = store.replace(first_value, path=first.location.path, backend="memory") assert cache.entries == {} assert store.find_matching(query)[0] is replacement assert store.find() == (replacement, second) assert store.find()[1] is second + + +def test_changed_stored_measurement_subject_requires_a_transient_value(): + value = _runtime_value() + store = RuntimeValueStore() + record = store.record(value, path="/memory/measurements.pkl", backend="memory") + plan = ArtifactOutputPlan( + name=value.name, artifact_type=value.artifact_type, + path="/memory/measurements.pkl", + ) + record.data.subject = MeasurementSubject(MeasurementScope.IMAGE, "NextImage") + + admitted = record.validated_for_output_plan(plan, axis_id="A01") + + assert type(admitted) is RuntimeValue + assert admitted.key.semantic_id == record.data.runtime_semantic_id + assert admitted.key != record.key + assert store.get(record.key) is record + assert record.location.path == "/memory/measurements.pkl" + assert admitted.data is record.data + assert type(StoredRuntimeValue.from_output_plan( + plan, admitted.data, execution_scope=record.key.scope, + )) is RuntimeValue From e7190b99f7e5e9e53776ae150553c12c2408651a Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 20:59:56 -0400 Subject: [PATCH 180/235] Use admitted artifact context for adapter-owned execution cohorts Refs #419 and #394. The compiled group already rejects raw implicit image inputs unless their adapter owns artifact input resolution. Remove a later contradictory signature-only rejection that forced label-driven adapters back through unrelated main-flow files. Existing group-scope edges and artifact source-context capability now supply their actual stored cohorts. Found on the complete fresh Beginner tertiary-objects consumer; full replay remains pending. --- openhcs/core/function_patterns.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/openhcs/core/function_patterns.py b/openhcs/core/function_patterns.py index 9c6f92341f..d6947af154 100644 --- a/openhcs/core/function_patterns.py +++ b/openhcs/core/function_patterns.py @@ -844,11 +844,6 @@ def stored_primary_input_edges_for_component( and edge.spec.artifact_type.carries_source_image_context ) if not primary_edges: - if any( - invocation.contract.accepts_implicit_main_flow_input - for invocation, edges in active - ): - return None primary_edges = tuple( edge for invocation, edges in active From 29339fc261fc9517012935d88035889d00e52193 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 21:04:46 -0400 Subject: [PATCH 181/235] Apply compiled checkpoint demand to artifact-only passthrough publication Artifact-owned invocations can execute from labels without a physical main-flow producer. Skip physical passthrough publication when the existing compiled demand graph proves no checkpoint consumer; required checkpoint routes retain strict producer-lineage admission. Validation: direct original Beginner pipeline replay completed all 13 steps with all ten original source references, 44 runtime records, two site overlays and seven CSVs. Registry/kernel preparation completed before compilation. No timing or native numerical-parity claim. Refs #384. Refs #394. --- openhcs/core/steps/function_runtime.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openhcs/core/steps/function_runtime.py b/openhcs/core/steps/function_runtime.py index d8eaf7bee0..6d09165489 100644 --- a/openhcs/core/steps/function_runtime.py +++ b/openhcs/core/steps/function_runtime.py @@ -2423,6 +2423,8 @@ def _record_main_flow_passthrough(self, matching_files: Sequence[str]) -> None: if not matching_files: return plan = self.request.execution_plan + if not plan.requires_main_flow_checkpoint(self.request.context.step_plans): + return parser = self.request.context.microscope_handler.parser manifest = step_output_manifest(self.request.context) producer_records = self.request.passthrough_producer_records(matching_files) From e4b5916b96231e05cf9acecff81d3755bc5e2927 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 21:06:38 -0400 Subject: [PATCH 182/235] Preserve concrete source-domain owners across reconstruction and serialization Use merged ObjectState#17 and pycodify#2 fixes for actual nominal nested dataclass owners and non-elidable subtype constructors. Public original 3D pipeline import, ConfigDocument roundtrip, saved ObjectState reconstruction and all 31 compiled steps passed together. Refs #573; OpenHCSDev/ObjectState#16; OpenHCSDev/pycodify#1. --- external/ObjectState | 2 +- external/pycodify | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/external/ObjectState b/external/ObjectState index dbc3c64c5f..16aa704ec1 160000 --- a/external/ObjectState +++ b/external/ObjectState @@ -1 +1 @@ -Subproject commit dbc3c64c5f7ebde88c1fedf03cb8aff4dbe1f4c1 +Subproject commit 16aa704ec1f80315b4496813277127f10f99d210 diff --git a/external/pycodify b/external/pycodify index 77c27a0bed..108a8edbf2 160000 --- a/external/pycodify +++ b/external/pycodify @@ -1 +1 @@ -Subproject commit 77c27a0bed97f70878e6eb26d4fe7cdfae4e8683 +Subproject commit 108a8edbf25de258168756bac22c7572f018fa90 From 2a0688132837d2d4fa86eaa936e4fa70be8d00f3 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 21:16:53 -0400 Subject: [PATCH 183/235] Read source coordinates through owned plane identities (#384) --- .../core/measurement_row_materialization.py | 5 +- openhcs/core/source_image_provenance.py | 81 ++++++++++++------- .../test_measurement_row_materialization.py | 29 +++++-- tests/unit/test_source_image_provenance.py | 48 +++++++++++ 4 files changed, 129 insertions(+), 34 deletions(-) diff --git a/openhcs/core/measurement_row_materialization.py b/openhcs/core/measurement_row_materialization.py index ac6c9bf6fa..1579e9e0a9 100644 --- a/openhcs/core/measurement_row_materialization.py +++ b/openhcs/core/measurement_row_materialization.py @@ -2159,15 +2159,16 @@ def metadata_for_row(row_index: int): f"slice_index={slice_index}, but provenance declares " f"{source_provenance.source_plane_count} runtime plane(s)." ) - return source_provenance.for_source_plane(slice_index).source_component_metadata + return source_provenance.component_metadata_for_plane(slice_index) + metadata_by_row = tuple(metadata_for_row(index) for index in range(row_count)) for component in AllComponents: values = tuple( ( MEASUREMENT_SPARSE_CELL if ( value := source_component_metadata_value( - metadata_for_row(row_index) or {}, + metadata_by_row[row_index] or {}, component, ) ) diff --git a/openhcs/core/source_image_provenance.py b/openhcs/core/source_image_provenance.py index dc3da05141..b23f9afe55 100644 --- a/openhcs/core/source_image_provenance.py +++ b/openhcs/core/source_image_provenance.py @@ -134,6 +134,22 @@ def with_missing_from( component_metadata=self.component_metadata_with_missing_from(fallback), ) + def image_set_identities( + self, + policy: SourceImageSetIdentityPolicy, + ) -> frozenset[SourceImageSetIdentity]: + """Project this scalar source address into its declared image-set domain.""" + identity = SourceImageSetIdentity.from_metadata( + self.component_metadata or {}, + fallback_source_path=self.path or "", + policy=policy, + ) + return ( + frozenset() + if identity.components == (("source_path", ""),) + else frozenset((identity,)) + ) + def component_metadata_with_missing_from( self, fallback: "SourceImageIdentity", @@ -956,14 +972,7 @@ def image_set_identities( policy: SourceImageSetIdentityPolicy, ) -> frozenset[SourceImageSetIdentity]: """Return the image-set identity represented by scalar provenance.""" - identity = SourceImageSetIdentity.from_metadata( - self.source_component_metadata or {}, - fallback_source_path=self.source_path or "", - policy=policy, - ) - if identity.components == (("source_path", ""),): - return frozenset() - return frozenset((identity,)) + return self.source_identity.image_set_identities(policy) def image_set_axis( self, @@ -981,10 +990,24 @@ def image_set_plane_identities( ) -> tuple[frozenset[SourceImageSetIdentity], ...]: """Return one image-set identity entry for every runtime source plane.""" return tuple( - self.for_source_plane(index).image_set_identities(policy) + self.source_image_provenance_planes.plane( + index + ).source_identity.with_missing_from( + self.source_identity + ).image_set_identities(policy) for index in range(self.source_plane_count) ) + def component_metadata_for_plane( + self, plane_index: int + ) -> SourceComponentMetadata | None: + """Read current plane coordinates with their scalar source fallback.""" + if not self.source_plane_count: + return self.source_component_metadata + return self.source_image_provenance_planes.plane( + plane_index + ).source_identity.component_metadata_with_missing_from(self.source_identity) + def for_source_plane(self, plane_index: int) -> "SourceImageProvenance": if self.source_plane_count == 0: return type(self)( @@ -1108,16 +1131,20 @@ def varying_plane_component_values( components: Sequence[AllComponents], ) -> dict[str, tuple[SourceMetadataScalar, ...]]: """Return exact component values that vary across declared source planes.""" - if self.source_plane_count <= 1: + if not components or self.source_plane_count <= 1: return {} + metadata_by_plane = tuple( + self.component_metadata_for_plane(index) or {} + for index in range(self.source_plane_count) + ) values_by_component: dict[str, tuple[SourceMetadataScalar, ...]] = {} for component in components: values = tuple( source_component_metadata_raw_value( - self.for_source_plane(plane_index).source_component_metadata or {}, + metadata, component, ) - for plane_index in range(self.source_plane_count) + for metadata in metadata_by_plane ) if any(value is None for value in values): continue @@ -1131,24 +1158,24 @@ def require_common_component_values( ) -> tuple[tuple[AllComponents, str], ...]: """Return fixed component values shared by every represented source plane.""" + if not components: + return () + metadata_by_plane = ( + tuple( + self.component_metadata_for_plane(index) or {} + for index in range(self.source_plane_count) + ) + if self.source_plane_count + else (self.source_component_metadata or {},) + ) values: list[tuple[AllComponents, str]] = [] for component in components: - metadata_values = ( - tuple( - source_component_metadata_value( - self.for_source_plane(plane_index).source_component_metadata - or {}, - component, - ) - for plane_index in range(self.source_plane_count) - ) - if self.source_plane_count - else ( - source_component_metadata_value( - self.source_component_metadata or {}, - component, - ), + metadata_values = tuple( + source_component_metadata_value( + metadata, + component, ) + for metadata in metadata_by_plane ) if any(value is None for value in metadata_values): raise ValueError( diff --git a/tests/unit/test_measurement_row_materialization.py b/tests/unit/test_measurement_row_materialization.py index 3a3a2b054b..152db68198 100644 --- a/tests/unit/test_measurement_row_materialization.py +++ b/tests/unit/test_measurement_row_materialization.py @@ -133,9 +133,9 @@ def test_source_provenance_preserves_producer_image_name_and_fills_coordinates() ): rows = MeasurementProjectedColumnarRows( { - "slice_index": (0,), - "source_image_name": ("IllumActinAvg",), - "mean_intensity": (0.25,), + "slice_index": (1, 0, 1), + "source_image_name": ("IllumActinAvg",) * 3, + "mean_intensity": (0.5, 0.25, 0.75), }, fields=( FieldSpec("slice_index", int), @@ -146,14 +146,25 @@ def test_source_provenance_preserves_producer_image_name_and_fills_coordinates() provenance = SourceImageProvenance( source_image_names=("IllumActin",), source_image_provenance_planes=SourceImageProvenancePlanes.from_components( - paths=("/input/A01_s1_w2.tif",), - component_metadata=({"well": "A01", "site": "1", "channel": "2"},), + paths=("/input/A01_s1_w2.tif", "/input/A01_s2_w2.tif"), + component_metadata=( + {"well": "A01", "site": "1", "channel": "2"}, + {"well": "A01", "site": "2", "channel": "2"}, + ), ), ) projected = measurement_rows_with_source_provenance(rows, provenance) assert projected.row_mappings() == ( + { + "slice_index": 1, + "source_image_name": "IllumActinAvg", + "mean_intensity": 0.5, + "well": "A01", + "site": "2", + "channel": "2", + }, { "slice_index": 0, "source_image_name": "IllumActinAvg", @@ -162,6 +173,14 @@ def test_source_provenance_preserves_producer_image_name_and_fills_coordinates() "site": "1", "channel": "2", }, + { + "slice_index": 1, + "source_image_name": "IllumActinAvg", + "mean_intensity": 0.75, + "well": "A01", + "site": "2", + "channel": "2", + }, ) diff --git a/tests/unit/test_source_image_provenance.py b/tests/unit/test_source_image_provenance.py index eae9118787..7f65e326ea 100644 --- a/tests/unit/test_source_image_provenance.py +++ b/tests/unit/test_source_image_provenance.py @@ -67,6 +67,54 @@ def test_source_provenance_preserves_plane_positions_before_axis_deduplication() assert axis == (plane_identities[0],) +def test_plane_coordinate_reads_preserve_runtime_scope_and_scalar_fallback() -> None: + provenance = SourceImageProvenance( + source_path="/input/fallback.tif", + source_component_metadata={ + "well": "A01", "site": "99", "channel": "9", "timepoint": "3", + }, + source_image_provenance_planes=SourceImageProvenancePlanes(( + SourceImageProvenanceContributor( + SourceImageIdentity(component_metadata={"site": "88"}), + source_image_name="Ancestor", + ), + RuntimeSourceImageProvenancePlane( + SourceImageIdentity(component_metadata={"site": "1", "channel": "1"}), + contributors=(SourceImageProvenanceContributor( + SourceImageIdentity(component_metadata={"site": "77"}), + source_image_name="DNA", + ),), + ), + RuntimeSourceImageProvenancePlane( + SourceImageIdentity(component_metadata={"site": "2", "channel": "2"}), + ), + )), + ) + assert dict(provenance.component_metadata_for_plane(0)) == { + "well": "A01", "site": "1", "channel": "1", "timepoint": "3", + } + assert provenance.varying_plane_component_values(tuple(AllComponents)) == { + "site": ("1", "2"), "channel": ("1", "2"), + } + assert provenance.require_common_component_values( + (AllComponents.WELL, AllComponents.TIMEPOINT) + ) == ((AllComponents.WELL, "A01"), (AllComponents.TIMEPOINT, "3")) + with pytest.raises(ValueError, match="'site' is not fixed"): + provenance.require_common_component_values((AllComponents.SITE,)) + identities = provenance.image_set_plane_identities( + SourceImageSetIdentityPolicy(frozenset((AllComponents.CHANNEL,))) + ) + assert len(identities) == 2 + assert tuple(next(iter(value)).components for value in identities) == ( + (("site", "1"), ("timepoint", "3"), ("well", "A01")), + (("site", "2"), ("timepoint", "3"), ("well", "A01")), + ) + provenance.source_identity.component_metadata = {"well": "B02", "timepoint": "4"} + assert provenance.require_common_component_values( + (AllComponents.WELL, AllComponents.TIMEPOINT) + ) == ((AllComponents.WELL, "B02"), (AllComponents.TIMEPOINT, "4")) + + def test_source_provenance_axis_retains_distinct_image_sets() -> None: provenance = SourceImageProvenance( source_image_provenance_planes=SourceImageProvenancePlanes.from_components( From 46c169535ff389893a876051dcd10ef58c158514 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 21:18:32 -0400 Subject: [PATCH 184/235] refactor: let measurement tables own joins and provenance --- openhcs/core/artifacts.py | 9 +- openhcs/core/runtime_artifact_queries.py | 215 ------------------ openhcs/core/runtime_measurements.py | 213 +++++++++++++++++ openhcs/core/runtime_slice_projection.py | 5 +- .../function_artifact_materialization.py | 3 +- .../interop/cellprofiler/runtime/adapter.py | 10 +- .../cellprofiler/runtime/artifact_binding.py | 3 +- .../cellprofiler/runtime/module_execution.py | 11 +- .../cellprofiler/spreadsheet_export.py | 3 +- tests/unit/test_cellprofiler_image_quality.py | 8 +- .../test_cellprofiler_module_execution.py | 3 +- .../unit/test_cellprofiler_runtime_adapter.py | 3 +- .../test_function_artifact_materialization.py | 1 + tests/unit/test_runtime_artifact_queries.py | 55 +++-- 14 files changed, 274 insertions(+), 268 deletions(-) diff --git a/openhcs/core/artifacts.py b/openhcs/core/artifacts.py index 328ec55a26..e259c5150b 100644 --- a/openhcs/core/artifacts.py +++ b/openhcs/core/artifacts.py @@ -787,12 +787,11 @@ def compose_runtime_values( """Concatenate exact producer-group measurement tables.""" del producer_group_scope - from openhcs.core.runtime_artifact_queries import MeasurementTableUnion + from openhcs.core.runtime_measurements import MeasurementTable - return MeasurementTableUnion( - values[0].name, - tuple(value.data for value in values), - ).as_table() + return MeasurementTable.join( + values[0].name, tuple(value.data for value in values) + ) @classmethod def materialization_payload(cls, value: "RuntimeValue") -> object: diff --git a/openhcs/core/runtime_artifact_queries.py b/openhcs/core/runtime_artifact_queries.py index fa2f5ff37e..c4c75fa673 100644 --- a/openhcs/core/runtime_artifact_queries.py +++ b/openhcs/core/runtime_artifact_queries.py @@ -16,10 +16,7 @@ SpatialGridArtifactType, ) from openhcs.core.measurement_row_materialization import ( - ConcatenatedColumnarRows, MeasurementColumnarRowsView, - MeasurementRowOwnership, - MeasurementRowsAxisProjection, columnar_row_values, measurement_row_object_name, measurement_rows, @@ -64,10 +61,6 @@ from openhcs.core.runtime_measurements import ( MeasurementTable, ) -from openhcs.core.runtime_image_values import ( - ImagePayloadMetadata, - ImagePayloadMetadataCompositionMode, -) from openhcs.core.runtime_object_labels import ( ObjectLabelValue, ) @@ -122,214 +115,6 @@ class MeasurementLabelSliceFeatureBatchQueryCache( max_entries = 1024 -@dataclass(frozen=True, slots=True) -class MeasurementTableUnion: - """Lossless row-owned view over same-artifact measurement subject tables.""" - - name: str - tables: tuple[MeasurementTable, ...] - - def __post_init__(self) -> None: - if not self.name: - raise ValueError("MeasurementTableUnion.name cannot be empty.") - if not self.tables: - raise ValueError("MeasurementTableUnion.tables cannot be empty.") - - def as_table(self) -> MeasurementTable: - if len(self.tables) == 1: - return self.tables[0] - subjects = tuple(dict.fromkeys(table.subject for table in self.tables)) - if len(subjects) != 1: - raise ValueError( - "Measurement table unions require one exact nominal subject; " - f"got {subjects!r}." - ) - owners = tuple( - dict.fromkeys(table.measurement_feature_owner for table in self.tables) - ) - if len(owners) != 1: - raise ValueError( - "Measurement table unions require one exact nominal measurement " - f"feature owner; got {owners!r}." - ) - source_names = tuple( - dict.fromkeys(table.source_image_name for table in self.tables) - ) - if len(source_names) != 1: - raise ValueError( - "Measurement table unions require one exact source-image owner; " - f"got {source_names!r}." - ) - return MeasurementTable( - name=self.name, - rows=self.rows(), - source_image_name=source_names[0], - subject=subjects[0], - measurement_feature_owner=owners[0], - source_provenance=self.source_metadata().source_provenance, - ) - - def as_artifact_table(self) -> MeasurementTable: - """Re-own mixed subject rows as one artifact-level export table.""" - - return MeasurementTable( - name=self.name, - rows=ConcatenatedColumnarRows( - tuple( - cast( - ColumnarRows, - MeasurementRowOwnership( - object_name=table.subject.object_name, - source_image_name=( - table.source_image_name - or table.subject.source_image_name - ), - ).annotate_rows(table.rows), - ) - for table in self.tables - ) - ), - subject=MeasurementSubject(MeasurementScope.ARTIFACT, self.name), - source_provenance=self.source_metadata().source_provenance, - ) - - def rows(self) -> ColumnarRows: - return ConcatenatedColumnarRows(tuple(table.rows for table in self.tables)) - - def source_metadata(self) -> ImagePayloadMetadata: - """Compose table provenance on its declared runtime-slice axis.""" - - if len(self.tables) == 1: - return ImagePayloadMetadata( - source_provenance=self.tables[0].source_provenance, - ) - - slice_axis = MeasurementRowAxisField.SLICE_INDEX - axis_domain = self.row_axis_domain(slice_axis) - if axis_domain is None: - return ImagePayloadMetadata.compose( - tuple( - ImagePayloadMetadata( - source_provenance=table.source_provenance, - ).payload_with((0,)) - for table in self.tables - ), - mode=ImagePayloadMetadataCompositionMode.STACK, - ) - - table_domains = tuple( - MeasurementRowsAxisProjection.from_rows(table.rows).present_axis_values( - slice_axis.value - ) - for table in self.tables - ) - declared_plane_counts = tuple( - table.source_provenance.source_plane_count - for table in self.tables - if table.source_provenance.source_plane_count > 0 - ) - distinct_plane_counts = tuple(dict.fromkeys(declared_plane_counts)) - if len(distinct_plane_counts) > 1: - raise ValueError( - f"Measurement table union {self.name!r} cannot align declared " - f"source-plane counts {distinct_plane_counts!r}." - ) - axis_size = ( - distinct_plane_counts[0] if distinct_plane_counts else max(axis_domain) + 1 - ) - if max(axis_domain) >= axis_size: - raise ValueError( - f"Measurement table union {self.name!r} declares " - f"{slice_axis.value}={max(axis_domain)} beyond its source-plane " - f"axis of size {axis_size}." - ) - - for table, domain in zip(self.tables, table_domains, strict=True): - provenance = table.source_provenance - if ( - provenance.source_plane_count == 0 - and provenance.has_values - and len(domain) > 1 - ): - raise ValueError( - f"Measurement table {table.name!r} carries scalar source " - f"provenance for multiple {slice_axis.value} values {domain!r}." - ) - - plane_metadata: list[ImagePayloadMetadata] = [] - for plane_index in range(axis_size): - plane_provenance = tuple( - ( - table.source_provenance.for_source_plane(plane_index) - if table.source_provenance.source_plane_count > 0 - else table.source_provenance - ) - for table, domain in zip(self.tables, table_domains, strict=True) - if table.source_provenance.source_plane_count > 0 - or domain == (plane_index,) - ) - source_metadata = tuple( - ImagePayloadMetadata(source_provenance=provenance) - for provenance in plane_provenance - ) - if not source_metadata: - raise ValueError( - f"Measurement table union {self.name!r} has no declared " - f"source provenance for {slice_axis.value}={plane_index}." - ) - if len(source_metadata) == 1: - plane_metadata.append(source_metadata[0]) - else: - plane_metadata.append( - ImagePayloadMetadata.compose( - tuple( - metadata.payload_with((0,)) for metadata in source_metadata - ), - mode=ImagePayloadMetadataCompositionMode.BUNDLE, - ).without_leading_plane_axis() - ) - - return ImagePayloadMetadata.compose( - tuple(metadata.payload_with((0,)) for metadata in plane_metadata), - mode=ImagePayloadMetadataCompositionMode.STACK, - ) - - def row_axis_domain( - self, - axis: MeasurementRowAxisField, - ) -> tuple[int, ...] | None: - """Return one exact row-axis domain, or ``None`` for an axisless union.""" - - projections = tuple( - MeasurementRowsAxisProjection.from_rows(table.rows) for table in self.tables - ) - declarations = tuple( - projection.declares_axis_field(axis) - or any(field.name == axis.value for field in table.rows.fields) - for table, projection in zip(self.tables, projections, strict=True) - ) - if not any(declarations): - return None - domains = tuple( - projection.present_axis_values(axis.value) for projection in projections - ) - if not any(domains): - return None - for table, projection, domain in zip( - self.tables, - projections, - domains, - strict=True, - ): - if projection.has_rows and not domain: - raise ValueError( - f"Measurement table union {self.name!r} mixes declared and " - f"axisless {axis.value!r} row domains; table {table.name!r} " - "declares no concrete axis value." - ) - return tuple(sorted({value for domain in domains for value in domain})) - - @dataclass(frozen=True, slots=True) class RuntimeArtifactQueryContext: """Execution-scope view over a RuntimeValueStore.""" diff --git a/openhcs/core/runtime_measurements.py b/openhcs/core/runtime_measurements.py index a213bd62d8..2988915889 100644 --- a/openhcs/core/runtime_measurements.py +++ b/openhcs/core/runtime_measurements.py @@ -15,6 +15,7 @@ MeasurementObjectRowIdentity, ) from openhcs.core.source_image_provenance import ( + SourceImageProvenance, SourceImageProvenanceFields, ) @@ -135,6 +136,218 @@ def runtime_semantic_id(self) -> str: ) ) + @classmethod + def join(cls, name: str, tables: tuple[MeasurementTable, ...]) -> MeasurementTable: + """Concatenate tables with one exact subject, feature owner and image owner.""" + cls._require_join(name, tables) + if len(tables) == 1: + return tables[0] + subjects = tuple(dict.fromkeys(table.subject for table in tables)) + if len(subjects) != 1: + raise ValueError( + "Measurement table unions require one exact nominal subject; " + f"got {subjects!r}." + ) + owners = tuple( + dict.fromkeys(table.measurement_feature_owner for table in tables) + ) + if len(owners) != 1: + raise ValueError( + "Measurement table unions require one exact nominal measurement " + f"feature owner; got {owners!r}." + ) + source_names = tuple(dict.fromkeys(table.source_image_name for table in tables)) + if len(source_names) != 1: + raise ValueError( + "Measurement table unions require one exact source-image owner; " + f"got {source_names!r}." + ) + from openhcs.core.measurement_row_materialization import ( + ConcatenatedColumnarRows, + ) + + return cls( + name=name, + rows=ConcatenatedColumnarRows(tuple(table.rows for table in tables)), + source_image_name=source_names[0], + subject=subjects[0], + measurement_feature_owner=owners[0], + source_provenance=cls.joined_source_provenance(name, tables), + ) + + @classmethod + def join_artifact( + cls, name: str, tables: tuple[MeasurementTable, ...] + ) -> MeasurementTable: + """Re-own mixed subject rows as one lossless artifact-level export table.""" + cls._require_join(name, tables) + from openhcs.core.measurement_row_materialization import ( + ConcatenatedColumnarRows, + MeasurementRowOwnership, + ) + + return cls( + name=name, + rows=ConcatenatedColumnarRows( + tuple( + MeasurementRowOwnership( + object_name=table.subject.object_name, + source_image_name=( + table.source_image_name or table.subject.source_image_name + ), + ).annotate_rows(table.rows) + for table in tables + ) + ), + subject=MeasurementSubject(MeasurementScope.ARTIFACT, name), + source_provenance=cls.joined_source_provenance(name, tables), + ) + + @staticmethod + def _require_join(name: str, tables: tuple[MeasurementTable, ...]) -> None: + if not name: + raise ValueError("MeasurementTable join name cannot be empty.") + if not tables: + raise ValueError("MeasurementTable join tables cannot be empty.") + + @classmethod + def joined_source_provenance( + cls, name: str, tables: tuple[MeasurementTable, ...] + ) -> SourceImageProvenance: + """Compose exact table provenance on its declared runtime-slice axis.""" + cls._require_join(name, tables) + if len(tables) == 1: + return tables[0].source_provenance + slice_axis = MeasurementRowAxisField.SLICE_INDEX + table_domains = cls._row_axis_domains(name, tables, slice_axis) + if table_domains is None: + sources = tuple(table.source_provenance for table in tables) + if not any(source.has_values for source in sources): + return SourceImageProvenance() + return SourceImageProvenance.stack( + tuple( + ( + source.for_source_plane(0) + if source.source_plane_count == 1 + else source + ) + for source in sources + ), + scalar_sources=sources, + ) + axis_domain = tuple( + sorted({value for domain in table_domains for value in domain}) + ) + declared_plane_counts = tuple( + table.source_provenance.source_plane_count + for table in tables + if table.source_provenance.source_plane_count > 0 + ) + distinct_plane_counts = tuple(dict.fromkeys(declared_plane_counts)) + if len(distinct_plane_counts) > 1: + raise ValueError( + f"Measurement table union {name!r} cannot align declared " + f"source-plane counts {distinct_plane_counts!r}." + ) + axis_size = ( + distinct_plane_counts[0] if distinct_plane_counts else max(axis_domain) + 1 + ) + if max(axis_domain) >= axis_size: + raise ValueError( + f"Measurement table union {name!r} declares " + f"{slice_axis.value}={max(axis_domain)} beyond its source-plane " + f"axis of size {axis_size}." + ) + for table, domain in zip(tables, table_domains, strict=True): + provenance = table.source_provenance + if ( + provenance.source_plane_count == 0 + and provenance.has_values + and len(domain) > 1 + ): + raise ValueError( + f"Measurement table {table.name!r} carries scalar source " + f"provenance for multiple {slice_axis.value} values {domain!r}." + ) + planes: list[SourceImageProvenance] = [] + for plane_index in range(axis_size): + sources = tuple( + ( + table.source_provenance.for_source_plane(plane_index) + if table.source_provenance.source_plane_count > 0 + else table.source_provenance + ) + for table, domain in zip(tables, table_domains, strict=True) + if table.source_provenance.source_plane_count > 0 + or domain == (plane_index,) + ) + if not sources: + raise ValueError( + f"Measurement table union {name!r} has no declared " + f"source provenance for {slice_axis.value}={plane_index}." + ) + planes.append( + sources[0] + if len(sources) == 1 + else ( + SourceImageProvenance.bundle( + sources + ).with_runtime_planes_as_contributors() + if any(source.has_values for source in sources) + else SourceImageProvenance() + ) + ) + return ( + SourceImageProvenance.stack(tuple(planes)) + if any(plane.has_values for plane in planes) + else SourceImageProvenance() + ) + + @classmethod + def shared_row_axis_domain( + cls, + name: str, + tables: tuple[MeasurementTable, ...], + axis: MeasurementRowAxisField, + ) -> tuple[int, ...] | None: + """Return the exact joined row-axis domain, or None for axisless rows.""" + cls._require_join(name, tables) + domains = cls._row_axis_domains(name, tables, axis) + if domains is None: + return None + return tuple(sorted({value for domain in domains for value in domain})) + + @staticmethod + def _row_axis_domains( + name: str, tables: tuple[MeasurementTable, ...], axis: MeasurementRowAxisField + ) -> tuple[tuple[int, ...], ...] | None: + from openhcs.core.measurement_row_materialization import ( + MeasurementRowsAxisProjection, + ) + + projections = tuple( + MeasurementRowsAxisProjection.from_rows(table.rows) for table in tables + ) + declarations = tuple( + projection.declares_axis_field(axis) + or any(field.name == axis.value for field in table.rows.fields) + for table, projection in zip(tables, projections, strict=True) + ) + if not any(declarations): + return None + domains = tuple( + projection.present_axis_values(axis.value) for projection in projections + ) + if not any(domains): + return None + for table, projection, domain in zip(tables, projections, domains, strict=True): + if projection.has_rows and not domain: + raise ValueError( + f"Measurement table union {name!r} mixes declared and " + f"axisless {axis.value!r} row domains; table {table.name!r} " + "declares no concrete axis value." + ) + return domains @dataclass(frozen=True, slots=True) class ObjectLabelMeasurementValues: diff --git a/openhcs/core/runtime_slice_projection.py b/openhcs/core/runtime_slice_projection.py index a78a23607f..5cfc599d49 100644 --- a/openhcs/core/runtime_slice_projection.py +++ b/openhcs/core/runtime_slice_projection.py @@ -28,7 +28,6 @@ from openhcs.core.runtime_array_values import RuntimeArrayData, is_array_payload from openhcs.core.runtime_artifact_queries import ( MeasurementTableAxisProjection, - MeasurementTableUnion, ) from openhcs.core.runtime_image_values import ( ImageMetadataPayload, @@ -801,8 +800,8 @@ def slice_count_for_value( @staticmethod def row_axis_domain(table: MeasurementTable) -> tuple[int, ...] | None: - return MeasurementTableUnion(table.name, (table,)).row_axis_domain( - MeasurementRowAxisField.SLICE_INDEX + return MeasurementTable.shared_row_axis_domain( + table.name, (table,), MeasurementRowAxisField.SLICE_INDEX ) diff --git a/openhcs/core/steps/function_artifact_materialization.py b/openhcs/core/steps/function_artifact_materialization.py index e0aa18c3e3..e18f94bc89 100644 --- a/openhcs/core/steps/function_artifact_materialization.py +++ b/openhcs/core/steps/function_artifact_materialization.py @@ -27,7 +27,6 @@ from openhcs.core.component_group_scope import RuntimeExecutionAxisScope from openhcs.core.component_set import ComponentSet from openhcs.core.registry_strategies import MostDerivedContextStrategyMixin -from openhcs.core.runtime_artifact_queries import MeasurementTableUnion from openhcs.core.runtime_artifact_values import RuntimeValue from openhcs.core.runtime_image_values import ( ImagePayloadMetadata, @@ -272,7 +271,7 @@ def records_for_group( return records group_plan = output_plan.for_group(group_key) tables = tuple(cast(MeasurementTable, record.data) for record in records) - table = MeasurementTableUnion(output_plan.name, tables).as_artifact_table() + table = MeasurementTable.join_artifact(output_plan.name, tables) value = RuntimeValue.normalize_for_execution_scope( group_plan, table, diff --git a/openhcs/interop/cellprofiler/runtime/adapter.py b/openhcs/interop/cellprofiler/runtime/adapter.py index 78ef97fcf0..631d102106 100644 --- a/openhcs/interop/cellprofiler/runtime/adapter.py +++ b/openhcs/interop/cellprofiler/runtime/adapter.py @@ -23,9 +23,6 @@ SpatialGridArtifactType, ) from openhcs.core.runtime_adapters import RuntimeAdapterRequest -from openhcs.core.runtime_artifact_queries import ( - MeasurementTableUnion, -) from openhcs.core.runtime_artifact_values import ( RuntimeValue, ) @@ -436,10 +433,9 @@ def get_measurements( MeasurementsArtifactType, group_key=group_key, ) - return MeasurementTableUnion( - name, - tuple(cast(MeasurementTable, record.data) for record in records), - ).as_table() + return MeasurementTable.join( + name, tuple(cast(MeasurementTable, record.data) for record in records) + ) def measurement_tables( self, diff --git a/openhcs/interop/cellprofiler/runtime/artifact_binding.py b/openhcs/interop/cellprofiler/runtime/artifact_binding.py index 6c8cbdcc53..3dd7cefec3 100644 --- a/openhcs/interop/cellprofiler/runtime/artifact_binding.py +++ b/openhcs/interop/cellprofiler/runtime/artifact_binding.py @@ -34,7 +34,6 @@ from openhcs.core.pipeline.function_contracts import ( object_label_input_execution_mode_from_callable, ) -from openhcs.core.runtime_artifact_queries import MeasurementTableUnion from openhcs.core.runtime_array_values import RuntimeArrayData from openhcs.core.runtime_image_values import ( image_payload_data, @@ -534,7 +533,7 @@ def declared_measurement_tables(self) -> tuple[MeasurementTable, ...]: MeasurementsArtifactType, ) ) - MeasurementTableUnion(spec.name, spec_tables).row_axis_domain(slice_axis) + MeasurementTable.shared_row_axis_domain(spec.name, spec_tables, slice_axis) tables.extend(spec_tables) return tuple(tables) diff --git a/openhcs/interop/cellprofiler/runtime/module_execution.py b/openhcs/interop/cellprofiler/runtime/module_execution.py index 18045d725c..d1662fe700 100644 --- a/openhcs/interop/cellprofiler/runtime/module_execution.py +++ b/openhcs/interop/cellprofiler/runtime/module_execution.py @@ -41,9 +41,9 @@ from openhcs.core.runtime_adapters import ( RuntimeAdapterRequest, ) -from openhcs.core.runtime_artifact_queries import MeasurementTableUnion from openhcs.core.runtime_batch_contracts import RuntimeBatchExecutionDomain from openhcs.core.runtime_image_values import ( + ImagePayloadMetadata, image_payload_metadata, preserved_image_plane_projection, ) @@ -1247,10 +1247,11 @@ def _run_per_image_measurement( ) if rows_only_declare_object_name: image_measurement_source_name = None - source_metadata = MeasurementTableUnion( - measurement_output_plan.name, - tuple(combined_tables), - ).source_metadata() + source_metadata = ImagePayloadMetadata( + source_provenance=MeasurementTable.joined_source_provenance( + measurement_output_plan.name, tuple(combined_tables) + ) + ) record_started_at = time.perf_counter() cellprofiler_runtime.add_measurements( module_type.build_measurement_table( diff --git a/openhcs/processing/backends/cellprofiler/spreadsheet_export.py b/openhcs/processing/backends/cellprofiler/spreadsheet_export.py index d4a862020a..aa56037fcd 100644 --- a/openhcs/processing/backends/cellprofiler/spreadsheet_export.py +++ b/openhcs/processing/backends/cellprofiler/spreadsheet_export.py @@ -35,7 +35,6 @@ execution_scope, runtime_bound_parameters, ) -from openhcs.core.runtime_artifact_queries import MeasurementTableUnion from openhcs.core.runtime_tabular_values import ( FieldSpec, ) @@ -474,7 +473,7 @@ def _measurement_tables( row_domains = tuple( MeasurementRowsAxisProjection.from_rows(table.rows) for table in tables ) - MeasurementTableUnion(spec.name, tables).row_axis_domain(slice_axis) + MeasurementTable.shared_row_axis_domain(spec.name, tables, slice_axis) for record, table, row_domain in zip( records, tables, diff --git a/tests/unit/test_cellprofiler_image_quality.py b/tests/unit/test_cellprofiler_image_quality.py index 64165a0639..97a7b55813 100644 --- a/tests/unit/test_cellprofiler_image_quality.py +++ b/tests/unit/test_cellprofiler_image_quality.py @@ -31,7 +31,6 @@ ) from openhcs.core.runtime_image_values import ImagePayloadMetadata from openhcs.core.runtime_measurements import MeasurementTable -from openhcs.core.runtime_artifact_queries import MeasurementTableUnion from openhcs.core.runtime_tabular_values import ( FieldSpec, ) @@ -506,13 +505,14 @@ def test_quality_control_measurements_keep_distinct_site_image_identities() -> N ) for channel in range(1, 6) ) - union = MeasurementTableUnion("quality_metrics", tables) table = MeasureImageQualityModule.build_measurement_table( name="quality_metrics", - rows=union.rows(), + rows=ConcatenatedColumnarRows(tuple(table.rows for table in tables)), object_name=None, source_image_name=None, - source_metadata=union.source_metadata(), + source_metadata=ImagePayloadMetadata( + source_provenance=MeasurementTable.joined_source_provenance("quality_metrics", tables), + ), ) projected_rows = CellProfilerImageSetNumbering( diff --git a/tests/unit/test_cellprofiler_module_execution.py b/tests/unit/test_cellprofiler_module_execution.py index 886a0f50d5..2593b6549f 100644 --- a/tests/unit/test_cellprofiler_module_execution.py +++ b/tests/unit/test_cellprofiler_module_execution.py @@ -88,7 +88,6 @@ ) from openhcs.core.runtime_artifact_queries import ( MeasurementTableAxisProjection, - MeasurementTableUnion, RuntimeArtifactQueryContext, ) from openhcs.core.runtime_artifact_values import RuntimeValue @@ -9477,7 +9476,7 @@ def test_measurement_tables_compose_provenance_after_object_rows_clear_source() ), ) - combined = MeasurementTableUnion("Locations", tables).as_table() + combined = MeasurementTable.join("Locations", tables) assert all(table.source_image_name is None for table in tables) assert tuple( diff --git a/tests/unit/test_cellprofiler_runtime_adapter.py b/tests/unit/test_cellprofiler_runtime_adapter.py index cb930464bc..221a984b9b 100644 --- a/tests/unit/test_cellprofiler_runtime_adapter.py +++ b/tests/unit/test_cellprofiler_runtime_adapter.py @@ -60,7 +60,6 @@ from openhcs.core.runtime_artifact_queries import ( MeasurementLabelSliceFeatureQuery, MeasurementTableAxisProjection, - MeasurementTableUnion, ) from openhcs.core.runtime_artifact_values import RuntimeValue from openhcs.core.runtime_batch_contracts import SliceIndexRuntimeParameter @@ -297,7 +296,7 @@ def _output_measurements( ) tables = tuple(record.data for record in records) assert all(isinstance(table, MeasurementTable) for table in tables) - return MeasurementTableUnion(name, tables).as_table() + return MeasurementTable.join(name, tables) def _output_relationship( diff --git a/tests/unit/test_function_artifact_materialization.py b/tests/unit/test_function_artifact_materialization.py index c72b1e1c66..a52ef4a9fc 100644 --- a/tests/unit/test_function_artifact_materialization.py +++ b/tests/unit/test_function_artifact_materialization.py @@ -1934,6 +1934,7 @@ def test_materialize_artifact_outputs_unions_measurement_subject_records( materialized = [] def fake_materialize(_spec, data, path, *_args, **_kwargs): + assert not data.source_provenance.has_values materialized.append((tuple(data.iter_row_mappings()), path)) return SimpleNamespace(save=lambda: SavedMaterializationOutputs({})) diff --git a/tests/unit/test_runtime_artifact_queries.py b/tests/unit/test_runtime_artifact_queries.py index 105607d76d..78d21dc473 100644 --- a/tests/unit/test_runtime_artifact_queries.py +++ b/tests/unit/test_runtime_artifact_queries.py @@ -29,7 +29,6 @@ MeasurementLabelSliceFeatureBatchQuery, MeasurementLabelSliceFeatureQuery, MeasurementTableAxisProjection, - MeasurementTableUnion, measurement_table_axis_values, measurement_row_mapping, runtime_measurement_tables_for_object, @@ -1416,7 +1415,7 @@ def test_measurement_table_union_preserves_compatible_schema() -> None: subject=subject, ) - union = MeasurementTableUnion("CellMeasurements", (first, second)).as_table() + union = MeasurementTable.join("CellMeasurements", (first, second)) assert union.rows.fields == (FieldSpec("cell_id", int), FieldSpec("area", float)) assert union.subject.object_name == "Cells" @@ -1446,7 +1445,7 @@ def test_measurement_table_union_composes_ordered_source_provenance() -> None: for index, path in enumerate(paths, start=1) ) - union = MeasurementTableUnion("CellMeasurements", tables).as_table() + union = MeasurementTable.join("CellMeasurements", tables) assert union.rows.row_mappings() == ( {"cell_id": 1, "area": 10.0}, @@ -1504,19 +1503,17 @@ def test_measurement_table_union_bundles_sources_per_declared_runtime_slice() -> for channel in (1, 2) ) - metadata = MeasurementTableUnion("quality_metrics", tables).source_metadata() + provenance = MeasurementTable.joined_source_provenance("quality_metrics", tables) - assert metadata.source_provenance.source_plane_count == 2 + assert provenance.source_plane_count == 2 assert tuple( - metadata.source_provenance.for_source_plane(index).source_component_metadata[ - "site" - ] + provenance.for_source_plane(index).source_component_metadata["site"] for index in range(2) ) == ("1", "2") assert tuple( tuple( contributor.path - for contributor in metadata.source_provenance.for_source_plane( + for contributor in provenance.for_source_plane( index ).source_image_provenance_planes.contributors ) @@ -1526,6 +1523,29 @@ def test_measurement_table_union_bundles_sources_per_declared_runtime_slice() -> ("/plate/A01_s002_w1.tif", "/plate/A01_s002_w2.tif"), ) + # A later export reads the tables' current provenance, not the prior join epoch. + first = tables[0] + first.source_provenance = first.source_provenance.with_source_image_provenance_planes( + SourceImageProvenancePlanes.from_components( + paths=("/changed/site-1.tif", "/changed/site-2.tif"), + component_metadata=( + {"well": "A01", "site": "1"}, + {"well": "A01", "site": "2"}, + ), + ) + ) + current = MeasurementTable.joined_source_provenance("quality_metrics", tables) + assert ( + current.for_source_plane(0).source_image_provenance_planes.contributors[0].path + == "/changed/site-1.tif" + ) + assert ( + provenance.for_source_plane(0) + .source_image_provenance_planes.contributors[0] + .path + == "/plate/A01_s001_w1.tif" + ) + def test_measurement_table_union_accepts_axisless_payload_domain() -> None: table = MeasurementTable( @@ -1541,8 +1561,8 @@ def test_measurement_table_union_accepts_axisless_payload_domain() -> None: ) assert ( - MeasurementTableUnion("CellMeasurements", (table,)).row_axis_domain( - MeasurementRowAxisField.SLICE_INDEX + MeasurementTable.shared_row_axis_domain( + "CellMeasurements", (table,), MeasurementRowAxisField.SLICE_INDEX ) is None ) @@ -1568,11 +1588,8 @@ def test_measurement_table_union_preserves_payload_rows_in_axis_declaring_table( subject=MeasurementSubject(MeasurementScope.OBJECT, "Cells", "cell_id"), ) - assert MeasurementTableUnion( - "CellMeasurements", - (table,), - ).row_axis_domain( - MeasurementRowAxisField.SLICE_INDEX + assert MeasurementTable.shared_row_axis_domain( + "CellMeasurements", (table,), MeasurementRowAxisField.SLICE_INDEX ) == (0,) @@ -1607,8 +1624,8 @@ def test_measurement_table_union_rejects_mixed_slice_domains() -> None: ValueError, match="mixes declared and axisless 'slice_index' row domains", ): - MeasurementTableUnion("CellMeasurements", tables).row_axis_domain( - MeasurementRowAxisField.SLICE_INDEX + MeasurementTable.shared_row_axis_domain( + "CellMeasurements", tables, MeasurementRowAxisField.SLICE_INDEX ) @@ -1640,7 +1657,7 @@ def test_measurement_table_union_drops_incompatible_schema_facts() -> None: ValueError, match="require one exact nominal subject", ): - MeasurementTableUnion("MixedMeasurements", (first, second)).as_table() + MeasurementTable.join("MixedMeasurements", (first, second)) def test_measurement_table_axis_query_projects_table_sequences() -> None: From 46d8c7585c27c47d67d9895b0b0f3e8045ba4474 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 21:21:10 -0400 Subject: [PATCH 185/235] Unify CellProfiler artifact-kind behavior on existing recorders Remove the parallel runtime artifact strategy family and its duplicate preparation registry. Existing nominal recorders now own input normalization, naming, recording and main-flow publication; source payload extraction uses the artifact type directly. Preserve live source reads, image and mask independence, output ordering and nominal input-only kinds. Relevant semantic controls: 68 passed in the focused gate, then the one stale required-argument fixture passed after migration. No isolated timing claim. Refs #384, #394 --- .../cellprofiler/runtime/artifact_binding.py | 326 +----------------- .../cellprofiler/runtime/module_execution.py | 19 +- .../runtime/output_record_request.py | 6 +- .../cellprofiler/runtime/output_recording.py | 254 +++++++++++++- ...t_cellprofiler_measurement_source_names.py | 6 +- .../test_cellprofiler_module_execution.py | 33 +- ...t_cellprofiler_output_record_occurrence.py | 10 +- .../test_gray_to_color_binding_axis_432.py | 6 +- .../test_runtime_adapter_source_artifacts.py | 6 +- ...t_runtime_artifact_input_value_contract.py | 10 +- .../test_saved_source_label_plane_domain.py | 8 +- 11 files changed, 308 insertions(+), 376 deletions(-) diff --git a/openhcs/interop/cellprofiler/runtime/artifact_binding.py b/openhcs/interop/cellprofiler/runtime/artifact_binding.py index 3dd7cefec3..5d8019d80a 100644 --- a/openhcs/interop/cellprofiler/runtime/artifact_binding.py +++ b/openhcs/interop/cellprofiler/runtime/artifact_binding.py @@ -2,30 +2,21 @@ from __future__ import annotations -from abc import ABC from collections.abc import Callable, Sequence from dataclasses import dataclass, replace -from functools import lru_cache import inspect -from typing import TypeAlias, cast, get_origin +from typing import cast, get_origin from openhcs.core.artifacts import ( - ArtifactOutputPlan, ArtifactSpec, ArtifactSpecCollection, ArtifactSpecRef, - ArtifactType, - ArtifactTypeStrategyMatchMixin, ImageArtifactType, ObjectLabelsArtifactType, - ObjectLineageArtifactType, MeasurementsArtifactType, - SpatialGridArtifactType, ) from openhcs.core.aligned_image_payload import ( - AlignedImageSliceContext, AlignedImageStack, - ImageOutputBundle, ) from openhcs.core.function_patterns import ( InvocationArtifactInputEdgePlan, @@ -36,20 +27,14 @@ ) from openhcs.core.runtime_array_values import RuntimeArrayData from openhcs.core.runtime_image_values import ( - image_payload_data, - image_payload_mask, image_payload_metadata, ) from openhcs.core.runtime_measurements import ( MeasurementTable, ) from openhcs.core.runtime_object_labels import ( - ObjectLabelSet, ObjectLabelValue, ) -from openhcs.core.runtime_object_label_building import ( - SourceImageObjectLabelBuildRequest, -) from openhcs.core.runtime_slice_alignment import ( RuntimeSliceAlignedValues, ) @@ -70,310 +55,17 @@ from openhcs.core.variable_component_stack_requirement import ( VariableComponentStackRequirementRequest, ) -from openhcs.interop.cellprofiler.image_normalization import ( - normalize_cellprofiler_image_payload, -) from openhcs.interop.cellprofiler.runtime.adapter import ( CellProfilerRuntimeAdapter, ) -from openhcs.interop.cellprofiler.runtime.measurement_source_names import ( - single_source_name, -) -from openhcs.interop.cellprofiler.runtime.main_flow import ( - cellprofiler_main_flow_output, +from openhcs.interop.cellprofiler.runtime.output_recording import ( + CellProfilerOutputRecorder, ) from openhcs.core.steps.function_runtime import ( RuntimeCallableArgument, RuntimeCallableKwargs, RuntimeFunctionOutput, ) -from openhcs.core.registry_strategies import ( - MostDerivedContextStrategyMixin, -) - -RuntimeMainFlowArtifactOutput: TypeAlias = tuple[ - ArtifactOutputPlan, - ArtifactSpec, - RuntimeCallableArgument, -] - - -class RuntimeArtifactTypeStrategy( - ArtifactTypeStrategyMatchMixin, - MostDerivedContextStrategyMixin[type[ArtifactType]], - ABC, -): - """Nominal strategy family for ArtifactType-specific runtime semantics.""" - - @classmethod - @lru_cache(maxsize=None) - def for_artifact_type( - cls, - artifact_type: ArtifactType, - ) -> "RuntimeArtifactTypeStrategy": - return cls.for_context( - ArtifactType.coerce(artifact_type), - error_subject="CellProfiler runtime artifact type strategy", - ) - - @classmethod - def for_main_flow_outputs( - cls, - outputs: tuple[RuntimeMainFlowArtifactOutput, ...], - ) -> "RuntimeArtifactTypeStrategy": - """Select one nominal strategy from the complete exact output set.""" - - artifact_types = frozenset( - spec.artifact_type for _plan, spec, _value in outputs - ) - if not artifact_types: - raise ValueError("CellProfiler main-flow publication requires an output.") - if len(artifact_types) != 1: - raise TypeError( - "CellProfiler main-flow outputs require one exact artifact type; " - f"got {tuple(sorted(kind.require_value() for kind in artifact_types))!r}." - ) - (artifact_type,) = artifact_types - return cls.for_artifact_type(artifact_type) - - def runtime_input_value( - self, spec: ArtifactSpec, value: RuntimeCallableArgument - ) -> RuntimeCallableArgument: - """Return the runtime payload bound into absorbed function kwargs.""" - - return value - - def raw_runtime_input_value( - self, spec: ArtifactSpec, value: RuntimeCallableArgument - ) -> RuntimeCallableArgument: - """Return the runtime payload before CellProfiler intensity coercion.""" - return self.runtime_input_value(spec, value) - - def source_image_name( - self, - spec: ArtifactSpec, - value: RuntimeCallableArgument, - ) -> str | None: - """Return the transitive source image name for one artifact input.""" - del spec, value - return None - - def source_image_name_from_value( - self, - value: RuntimeCallableArgument, - ) -> str | None: - """Project a source name from an already resolved artifact value.""" - del value - return None - - def source_image_payload_from_runtime_value( - self, - spec: ArtifactSpec, - value: RuntimeCallableArgument, - ) -> RuntimeCallableArgument | None: - """Read source context from an already bound, invocation-scoped value. - - Input naming, intensity conversion and plane selection belong to - ``RuntimeInputBindingRequest.runtime_value``. A source-context query - must consume that result without binding the input a second time. - """ - return spec.artifact_type.source_image_payload_from_runtime_value(value) - - def published_main_flow_output( - self, - input_value: RuntimeCallableArgument, - outputs: tuple[RuntimeMainFlowArtifactOutput, ...], - plane_projection: RuntimePlaneAxisValueProjection | None, - ) -> RuntimeCallableArgument: - """Publish one recorded artifact through the canonical OpenHCS flow.""" - - del input_value, plane_projection - self.validate_main_flow_outputs(outputs) - if len(outputs) != 1: - raise ValueError( - f"{type(self).__name__} requires exactly one main-flow output, " - f"got {len(outputs)}." - ) - return outputs[0][2] - - def validate_main_flow_outputs( - self, - outputs: tuple[RuntimeMainFlowArtifactOutput, ...], - ) -> None: - """Require every published output to belong to this nominal strategy.""" - - artifact_type = type(self).artifact_type - mismatched = tuple( - spec.ref() - for _plan, spec, _value in outputs - if spec.artifact_type is not artifact_type - ) - if mismatched: - raise TypeError( - f"{type(self).__name__} cannot publish outputs {mismatched!r}." - ) - - -class ImageArtifactTypeStrategy(RuntimeArtifactTypeStrategy): - """Resolve image artifact payloads and source-image lineage.""" - - artifact_type = ImageArtifactType - - def raw_runtime_input_value( - self, spec: ArtifactSpec, value: RuntimeCallableArgument - ) -> RuntimeCallableArgument: - payload = value - metadata = image_payload_metadata(payload) - metadata = metadata.with_source_provenance( - metadata.source_provenance.with_derived_source_image_names( - (spec.name,) - ) - ) - return metadata.payload_with( - image_payload_data(payload), - mask=image_payload_mask(payload), - ) - - def runtime_input_value( - self, spec: ArtifactSpec, value: RuntimeCallableArgument - ) -> RuntimeCallableArgument: - return normalize_cellprofiler_image_payload( - self.raw_runtime_input_value(spec, value) - ) - - def source_image_name( - self, - spec: ArtifactSpec, - value: RuntimeCallableArgument, - ) -> str | None: - return self.source_image_name_from_value(self.raw_runtime_input_value(spec, value)) - - def source_image_name_from_value( - self, - value: RuntimeCallableArgument, - ) -> str | None: - return single_source_name( - image_payload_metadata(value).source_provenance.represented_source_image_names - ) - - def published_main_flow_output( - self, - input_value: RuntimeCallableArgument, - outputs: tuple[RuntimeMainFlowArtifactOutput, ...], - plane_projection: RuntimePlaneAxisValueProjection | None, - ) -> RuntimeCallableArgument: - """Publish one or more named image outputs with exact plane context.""" - - self.validate_main_flow_outputs(outputs) - if not outputs: - raise ValueError("Image main-flow publication requires an output.") - return ImageOutputBundle( - tuple( - cellprofiler_main_flow_output( - input_value, - output_value, - plane_projection, - ) - for _plan, _spec, output_value in outputs - ), - AlignedImageSliceContext.main_flow_for_output_plans( - tuple(plan for plan, _spec, _value in outputs) - ), - ) - - -class ObjectLabelsArtifactTypeStrategy(RuntimeArtifactTypeStrategy): - """Resolve object-label payloads and lineage.""" - - artifact_type = ObjectLabelsArtifactType - - def object_labels( - self, - spec: ArtifactSpec, - value: RuntimeCallableArgument, - ) -> ObjectLabelSet: - """Return the native object value carrying its source-image provenance.""" - - if isinstance(value, ObjectLabelSet): - return value - metadata = image_payload_metadata(value) - return SourceImageObjectLabelBuildRequest( - image=value, - labels=image_payload_data(value), - plane_projection=RuntimePlaneAxisValueProjection.from_source_declaration( - metadata.plane_axis, metadata.source_provenance, - ), - ).label_set( - name=spec.name, - source_image_name=spec.name, - ) - - def runtime_input_value( - self, spec: ArtifactSpec, value: RuntimeCallableArgument - ) -> RuntimeCallableArgument: - return self.object_labels(spec, value) - - def raw_runtime_input_value( - self, - spec: ArtifactSpec, - value: RuntimeCallableArgument, - ) -> RuntimeCallableArgument: - """Return the nominal label set in the invocation's component scope.""" - - return self.object_labels(spec, value) - - def source_image_name( - self, - spec: ArtifactSpec, - value: RuntimeCallableArgument, - ) -> str | None: - return self.source_image_name_from_value(self.object_labels(spec, value)) - - def source_image_name_from_value( - self, - value: RuntimeCallableArgument, - ) -> str | None: - return cast(ObjectLabelSet, value).source_image_name - - -class MeasurementsArtifactTypeStrategy(RuntimeArtifactTypeStrategy): - """Resolve measurement payloads and lineage.""" - - artifact_type = MeasurementsArtifactType - - def runtime_input_value( - self, spec: ArtifactSpec, value: RuntimeCallableArgument - ) -> RuntimeCallableArgument: - if not isinstance(value, MeasurementTable): - raise TypeError( - f"Measurement artifact {spec.name!r} requires a " - f"MeasurementTable, got {type(value).__name__}." - ) - return value.rows - - def source_image_name( - self, - spec: ArtifactSpec, - value: RuntimeCallableArgument, - ) -> str | None: - if not isinstance(value, MeasurementTable): - raise TypeError( - f"Measurement artifact {spec.name!r} requires a " - f"MeasurementTable, got {type(value).__name__}." - ) - return value.source_image_name - - -class RelationshipsArtifactTypeStrategy(RuntimeArtifactTypeStrategy): - """Resolve relationship payloads.""" - - artifact_type = ObjectLineageArtifactType - - -class SpatialGridArtifactTypeStrategy(RuntimeArtifactTypeStrategy): - """Resolve spatial-grid payloads.""" - - artifact_type = SpatialGridArtifactType @dataclass(frozen=True, slots=True, kw_only=True) @@ -585,7 +277,7 @@ def stack_broadcast_source_value( if source_ref in self.declared_inputs.ref_set(): source_spec = self.declared_inputs.by_ref(source_ref) source_edge = self.input_edge_for_spec(source_spec) - return RuntimeArtifactTypeStrategy.for_artifact_type( + return CellProfilerOutputRecorder.for_artifact_type( ImageArtifactType ).runtime_input_value(source_edge.spec, self.artifact_value(source_edge)) @@ -682,7 +374,9 @@ def artifact_value_for_spec( return self.artifact_value(self.input_edge_for_spec(spec)) def label_payload_for(self, spec: ArtifactSpec) -> ObjectLabelValue: - return ObjectLabelsArtifactTypeStrategy().object_labels( + return CellProfilerOutputRecorder.for_artifact_type( + ObjectLabelsArtifactType + ).object_labels( spec, self.artifact_value_for_spec(spec) ) @@ -814,13 +508,15 @@ def runtime_value( spec = edge.spec if spec.artifact_type is ObjectLabelsArtifactType: - payload = ObjectLabelsArtifactTypeStrategy().object_labels( + payload = CellProfilerOutputRecorder.for_artifact_type( + ObjectLabelsArtifactType + ).object_labels( spec, self.artifact_value(edge) ) if parameter_name is not None: return self.project_label_argument(payload) return payload - value = RuntimeArtifactTypeStrategy.for_artifact_type( + value = CellProfilerOutputRecorder.for_artifact_type( spec.artifact_type ).runtime_input_value(spec, self.artifact_value(edge)) if spec.artifact_type is not ImageArtifactType: diff --git a/openhcs/interop/cellprofiler/runtime/module_execution.py b/openhcs/interop/cellprofiler/runtime/module_execution.py index d1662fe700..244d71215f 100644 --- a/openhcs/interop/cellprofiler/runtime/module_execution.py +++ b/openhcs/interop/cellprofiler/runtime/module_execution.py @@ -74,7 +74,6 @@ from openhcs.interop.cellprofiler.runtime.adapter import CellProfilerRuntimeAdapter from openhcs.interop.cellprofiler.runtime.artifact_binding import ( RuntimeInputBindingRequest, - RuntimeArtifactTypeStrategy, ) from openhcs.interop.cellprofiler.runtime.function_contract_execution import ( _CELLPROFILER_FUNCTION_CONTRACT_EXECUTOR, @@ -240,12 +239,8 @@ def measurement_output_plan( def prepare(self) -> None: """Resolve nominal policies used by this executor before timed execution.""" - for strategy_type in RuntimeArtifactTypeStrategy.registered_strategy_types(): - RuntimeArtifactTypeStrategy.for_artifact_type(strategy_type.artifact_type) - for artifact_type in frozenset( - output.artifact_type for output in self.callable_contract.artifact_outputs - ): - CellProfilerOutputRecorder.for_artifact_type(artifact_type) + for strategy_type in CellProfilerOutputRecorder.registered_strategy_types(): + CellProfilerOutputRecorder.for_artifact_type(strategy_type.artifact_type) def __call__( self, @@ -533,7 +528,7 @@ def _replacement_main_flow_output( ) for output_plan, spec in outputs ) - replacement = RuntimeArtifactTypeStrategy.for_main_flow_outputs( + replacement = CellProfilerOutputRecorder.for_main_flow_outputs( output_values ).published_main_flow_output( invocation_image, @@ -717,7 +712,7 @@ def _resolved_measurement_image( current_image=current_image, ).artifact_value_for_spec(spec) payload = normalize_cellprofiler_image_payload( - RuntimeArtifactTypeStrategy.for_artifact_type( + CellProfilerOutputRecorder.for_artifact_type( ImageArtifactType, ).raw_runtime_input_value(spec, value) ) @@ -811,7 +806,7 @@ def _run_per_object_measurement( elif not image_inputs: source_image_name = None else: - image_strategy = RuntimeArtifactTypeStrategy.for_artifact_type(ImageArtifactType) + image_strategy = CellProfilerOutputRecorder.for_artifact_type(ImageArtifactType) runtime_projection = RuntimePlaneAxisValueProjection.from_projector( adapter, RuntimePlaneAxis.RUNTIME_SLICE, (), ) @@ -1364,7 +1359,7 @@ def _image_request( image_input_specs = ArtifactSpecCollection(image_inputs) payloads = [] source_names: list[str | None] = [] - image_strategy = RuntimeArtifactTypeStrategy.for_artifact_type( + image_strategy = CellProfilerOutputRecorder.for_artifact_type( ImageArtifactType ) input_binding = replace(input_binding, current_image=current_runtime_payload) @@ -1434,7 +1429,7 @@ def _input_source_image_name( for spec in active_input_specs: if not spec.artifact_type.carries_source_image_context: continue - source_name = RuntimeArtifactTypeStrategy.for_artifact_type( + source_name = CellProfilerOutputRecorder.for_artifact_type( spec.artifact_type ).source_image_name(spec, input_binding.artifact_value_for_spec(spec)) if source_name is not None: diff --git a/openhcs/interop/cellprofiler/runtime/output_record_request.py b/openhcs/interop/cellprofiler/runtime/output_record_request.py index e767f22ea6..bc1f1a5342 100644 --- a/openhcs/interop/cellprofiler/runtime/output_record_request.py +++ b/openhcs/interop/cellprofiler/runtime/output_record_request.py @@ -30,7 +30,6 @@ CellProfilerMeasurementImage, ) from openhcs.interop.cellprofiler.runtime.artifact_binding import ( - RuntimeArtifactTypeStrategy, RuntimeInputBindingRequest, ) from openhcs.core.steps.function_runtime import ( @@ -221,10 +220,7 @@ def artifact_source_payload( spec = edge.spec RuntimeInputBindingRequest.__post_init__(self) - payload = RuntimeArtifactTypeStrategy.for_artifact_type( - spec.artifact_type - ).source_image_payload_from_runtime_value( - spec, + payload = spec.artifact_type.source_image_payload_from_runtime_value( self.runtime_value( edge, parameter_name=spec.parameter_name, diff --git a/openhcs/interop/cellprofiler/runtime/output_recording.py b/openhcs/interop/cellprofiler/runtime/output_recording.py index 66cdd0e0bf..ea34cfd448 100644 --- a/openhcs/interop/cellprofiler/runtime/output_recording.py +++ b/openhcs/interop/cellprofiler/runtime/output_recording.py @@ -8,10 +8,11 @@ from functools import lru_cache from graphlib import TopologicalSorter from types import MappingProxyType -from typing import ClassVar +from typing import TYPE_CHECKING, ClassVar, cast from openhcs.core.artifacts import ( ArtifactOutputPlan, + ArtifactSpec, ArtifactSpecCollection, ArtifactSpecRef, ArtifactType, @@ -26,8 +27,27 @@ from openhcs.core.callable_contract import CallableContract from openhcs.core.function_patterns import InvocationArtifactInputEdgePlan from openhcs.core.registry_strategies import MostDerivedContextStrategyMixin -from openhcs.core.runtime_image_values import image_payload_metadata +from openhcs.core.aligned_image_payload import ( + AlignedImageSliceContext, + ImageOutputBundle, +) +from openhcs.core.runtime_image_values import ( + image_payload_data, + image_payload_mask, + image_payload_metadata, +) +from openhcs.core.runtime_measurements import MeasurementTable +from openhcs.core.runtime_object_label_building import SourceImageObjectLabelBuildRequest +from openhcs.core.runtime_plane_projection import RuntimePlaneAxisValueProjection +from openhcs.interop.cellprofiler.image_normalization import ( + normalize_cellprofiler_image_payload, +) +from openhcs.interop.cellprofiler.runtime.main_flow import cellprofiler_main_flow_output +from openhcs.interop.cellprofiler.runtime.measurement_source_names import ( + single_source_name, +) from openhcs.core.runtime_object_labels import ( + ObjectLabelSet, ObjectLabelValue, ) from openhcs.core.runtime_output_matching import RuntimeMatchedOutput @@ -45,9 +65,6 @@ from openhcs.interop.cellprofiler.runtime.measurement_recording import ( measurement_table_for_module, ) -from openhcs.interop.cellprofiler.runtime.output_record_request import ( - CellProfilerOutputRecordRequest, -) from openhcs.core.steps.function_runtime import RuntimeCallableArgument from openhcs.interop.cellprofiler.runtime.profile_fields import ( cellprofiler_profile_payload_fields, @@ -57,12 +74,18 @@ ) +if TYPE_CHECKING: + from openhcs.interop.cellprofiler.runtime.output_record_request import ( + CellProfilerOutputRecordRequest, + ) + + class CellProfilerOutputRecorder( ArtifactTypeStrategyMatchMixin, MostDerivedContextStrategyMixin[type[ArtifactType]], ABC, ): - """Nominal output writer selected by artifact type.""" + """Own CellProfiler input binding, output recording and publication by artifact kind.""" artifact_type: ClassVar[type[ArtifactType] | None] = None @@ -77,6 +100,90 @@ def for_artifact_type( error_subject="CellProfiler output recorder", ) + @classmethod + def for_main_flow_outputs( + cls, + outputs: tuple[RuntimeMatchedOutput, ...], + ) -> "CellProfilerOutputRecorder": + """Select one nominal strategy from the complete exact output set.""" + + artifact_types = frozenset( + spec.artifact_type for _plan, spec, _value in outputs + ) + if not artifact_types: + raise ValueError("CellProfiler main-flow publication requires an output.") + if len(artifact_types) != 1: + raise TypeError( + "CellProfiler main-flow outputs require one exact artifact type; " + f"got {tuple(sorted(kind.require_value() for kind in artifact_types))!r}." + ) + (artifact_type,) = artifact_types + return cls.for_artifact_type(artifact_type) + + def runtime_input_value( + self, spec: ArtifactSpec, value: RuntimeCallableArgument + ) -> RuntimeCallableArgument: + """Return the runtime payload bound into absorbed function kwargs.""" + + return value + + def raw_runtime_input_value( + self, spec: ArtifactSpec, value: RuntimeCallableArgument + ) -> RuntimeCallableArgument: + """Return the runtime payload before CellProfiler intensity coercion.""" + return self.runtime_input_value(spec, value) + + def source_image_name( + self, + spec: ArtifactSpec, + value: RuntimeCallableArgument, + ) -> str | None: + """Return the transitive source image name for one artifact input.""" + del spec, value + return None + + def source_image_name_from_value( + self, + value: RuntimeCallableArgument, + ) -> str | None: + """Project a source name from an already resolved artifact value.""" + del value + return None + + def published_main_flow_output( + self, + input_value: RuntimeCallableArgument, + outputs: tuple[RuntimeMatchedOutput, ...], + plane_projection: RuntimePlaneAxisValueProjection | None, + ) -> RuntimeCallableArgument: + """Publish one recorded artifact through the canonical OpenHCS flow.""" + + del input_value, plane_projection + self.validate_main_flow_outputs(outputs) + if len(outputs) != 1: + raise ValueError( + f"{type(self).__name__} requires exactly one main-flow output, " + f"got {len(outputs)}." + ) + return outputs[0][2] + + def validate_main_flow_outputs( + self, + outputs: tuple[RuntimeMatchedOutput, ...], + ) -> None: + """Require every published output to belong to this nominal strategy.""" + + artifact_type = type(self).artifact_type + mismatched = tuple( + spec.ref() + for _plan, spec, _value in outputs + if spec.artifact_type is not artifact_type + ) + if mismatched: + raise TypeError( + f"{type(self).__name__} cannot publish outputs {mismatched!r}." + ) + @classmethod def transient_output_values( cls, @@ -113,6 +220,9 @@ def record_module_outputs( current_image: RuntimeCallableArgument, ) -> Mapping[ArtifactSpecRef, RuntimeCallableArgument]: """Record one module invocation's returned artifacts.""" + from openhcs.interop.cellprofiler.runtime.output_record_request import ( + CellProfilerOutputRecordRequest, + ) function_name = callable_contract.function_name active_output_plans = tuple(plan for plan, _spec, _value in matched_outputs) active_output_refs = frozenset(plan.ref() for plan in active_output_plans) @@ -186,6 +296,68 @@ class ImageOutputRecorder(CellProfilerOutputRecorder): artifact_type = ImageArtifactType + def raw_runtime_input_value( + self, spec: ArtifactSpec, value: RuntimeCallableArgument + ) -> RuntimeCallableArgument: + payload = value + metadata = image_payload_metadata(payload) + metadata = metadata.with_source_provenance( + metadata.source_provenance.with_derived_source_image_names( + (spec.name,) + ) + ) + return metadata.payload_with( + image_payload_data(payload), + mask=image_payload_mask(payload), + ) + + def runtime_input_value( + self, spec: ArtifactSpec, value: RuntimeCallableArgument + ) -> RuntimeCallableArgument: + return normalize_cellprofiler_image_payload( + self.raw_runtime_input_value(spec, value) + ) + + def source_image_name( + self, + spec: ArtifactSpec, + value: RuntimeCallableArgument, + ) -> str | None: + return self.source_image_name_from_value(self.raw_runtime_input_value(spec, value)) + + def source_image_name_from_value( + self, + value: RuntimeCallableArgument, + ) -> str | None: + return single_source_name( + image_payload_metadata(value).source_provenance.represented_source_image_names + ) + + def published_main_flow_output( + self, + input_value: RuntimeCallableArgument, + outputs: tuple[RuntimeMatchedOutput, ...], + plane_projection: RuntimePlaneAxisValueProjection | None, + ) -> RuntimeCallableArgument: + """Publish one or more named image outputs with exact plane context.""" + + self.validate_main_flow_outputs(outputs) + if not outputs: + raise ValueError("Image main-flow publication requires an output.") + return ImageOutputBundle( + tuple( + cellprofiler_main_flow_output( + input_value, + output_value, + plane_projection, + ) + for _plan, _spec, output_value in outputs + ), + AlignedImageSliceContext.main_flow_for_output_plans( + tuple(plan for plan, _spec, _value in outputs) + ), + ) + def record(self, request: CellProfilerOutputRecordRequest) -> None: module_type = CellProfilerModule.require_callable_contract_owner( request.callable_contract @@ -212,6 +384,54 @@ class ObjectLabelsOutputRecorder(CellProfilerOutputRecorder): artifact_type = ObjectLabelsArtifactType + def object_labels( + self, + spec: ArtifactSpec, + value: RuntimeCallableArgument, + ) -> ObjectLabelSet: + """Return the native object value carrying its source-image provenance.""" + + if isinstance(value, ObjectLabelSet): + return value + metadata = image_payload_metadata(value) + return SourceImageObjectLabelBuildRequest( + image=value, + labels=image_payload_data(value), + plane_projection=RuntimePlaneAxisValueProjection.from_source_declaration( + metadata.plane_axis, metadata.source_provenance, + ), + ).label_set( + name=spec.name, + source_image_name=spec.name, + ) + + def runtime_input_value( + self, spec: ArtifactSpec, value: RuntimeCallableArgument + ) -> RuntimeCallableArgument: + return self.object_labels(spec, value) + + def raw_runtime_input_value( + self, + spec: ArtifactSpec, + value: RuntimeCallableArgument, + ) -> RuntimeCallableArgument: + """Return the nominal label set in the invocation's component scope.""" + + return self.object_labels(spec, value) + + def source_image_name( + self, + spec: ArtifactSpec, + value: RuntimeCallableArgument, + ) -> str | None: + return self.source_image_name_from_value(self.object_labels(spec, value)) + + def source_image_name_from_value( + self, + value: RuntimeCallableArgument, + ) -> str | None: + return cast(ObjectLabelSet, value).source_image_name + def record(self, request: CellProfilerOutputRecordRequest) -> None: module_type = CellProfilerModule.require_callable_contract_owner( request.callable_contract @@ -244,6 +464,28 @@ class MeasurementsOutputRecorder(CellProfilerOutputRecorder): artifact_type = MeasurementsArtifactType + def runtime_input_value( + self, spec: ArtifactSpec, value: RuntimeCallableArgument + ) -> RuntimeCallableArgument: + if not isinstance(value, MeasurementTable): + raise TypeError( + f"Measurement artifact {spec.name!r} requires a " + f"MeasurementTable, got {type(value).__name__}." + ) + return value.rows + + def source_image_name( + self, + spec: ArtifactSpec, + value: RuntimeCallableArgument, + ) -> str | None: + if not isinstance(value, MeasurementTable): + raise TypeError( + f"Measurement artifact {spec.name!r} requires a " + f"MeasurementTable, got {type(value).__name__}." + ) + return value.source_image_name + def record(self, request: CellProfilerOutputRecordRequest) -> None: module_type = CellProfilerModule.require_callable_contract_owner( request.callable_contract diff --git a/tests/unit/test_cellprofiler_measurement_source_names.py b/tests/unit/test_cellprofiler_measurement_source_names.py index fa1fcc1c84..73db9d361e 100644 --- a/tests/unit/test_cellprofiler_measurement_source_names.py +++ b/tests/unit/test_cellprofiler_measurement_source_names.py @@ -80,7 +80,7 @@ def source_image_name(spec: ArtifactSpec, value: SimpleNamespace) -> str: ) monkeypatch.setattr( module_execution, - "RuntimeArtifactTypeStrategy", + "CellProfilerOutputRecorder", SimpleNamespace(for_artifact_type=lambda _artifact_type: ArtifactStrategy()), ) @@ -110,9 +110,6 @@ def test_composed_measurement_caller_preserves_ordered_source_aliases() -> None: callable_contract=SimpleNamespace( image_payload_consumption=ImagePayloadConsumption.COMPOSED ), - _primary_image_inputs=lambda current_image, adapter, *, module_type: ( - image_inputs - ), ) measurement_images = CellProfilerModuleExecutor._measurement_image_inputs( @@ -120,6 +117,7 @@ def test_composed_measurement_caller_preserves_ordered_source_aliases() -> None: adapter=SimpleNamespace(), current_image=payload, image_request=image_request, + image_inputs=image_inputs, module_type=SimpleNamespace(), ) diff --git a/tests/unit/test_cellprofiler_module_execution.py b/tests/unit/test_cellprofiler_module_execution.py index 2593b6549f..a0b056e023 100644 --- a/tests/unit/test_cellprofiler_module_execution.py +++ b/tests/unit/test_cellprofiler_module_execution.py @@ -204,7 +204,6 @@ ) from openhcs.interop.cellprofiler.runtime.artifact_binding import ( RuntimeInputBindingRequest, - RuntimeArtifactTypeStrategy, ) from openhcs.interop.cellprofiler.runtime.function_contract_execution import ( CellProfilerFunctionContractExecutor, @@ -1374,7 +1373,7 @@ def test_image_artifact_resolution_uses_declared_artifact_alias( ), source_image_names=("OrigStain1",), ).payload_with(np.ones((4, 5), dtype=np.float32), None) - payload = RuntimeArtifactTypeStrategy.for_artifact_type( + payload = CellProfilerOutputRecorder.for_artifact_type( ImageArtifactType ).raw_runtime_input_value( spec=spec, value=source @@ -1384,12 +1383,12 @@ def test_image_artifact_resolution_uses_declared_artifact_alias( assert metadata.source_image_names == ("IllumStain1",) assert metadata.source_image_provenance_planes.paths == ("/input/A01_s001_w1.tif",) assert ( - RuntimeArtifactTypeStrategy.for_artifact_type( + CellProfilerOutputRecorder.for_artifact_type( ImageArtifactType ).source_image_name(spec=spec, value=source) is None ) - strategy = RuntimeArtifactTypeStrategy.for_artifact_type(ImageArtifactType) + strategy = CellProfilerOutputRecorder.for_artifact_type(ImageArtifactType) def reject_second_resolution(*args, **kwargs): raise AssertionError("Source-name projection resolved the image again") @@ -1435,7 +1434,7 @@ def test_main_flow_image_artifact_selects_declared_alias_plane() -> None: ), ) - payload = RuntimeArtifactTypeStrategy.for_artifact_type( + payload = CellProfilerOutputRecorder.for_artifact_type( ImageArtifactType ).raw_runtime_input_value( first_spec, RuntimeInputBindingRequest( @@ -1475,7 +1474,7 @@ def test_single_main_flow_image_artifact_selects_declared_source_binding_plane() None, ) - payload = RuntimeArtifactTypeStrategy.for_artifact_type( + payload = CellProfilerOutputRecorder.for_artifact_type( ImageArtifactType ).raw_runtime_input_value( spec, RuntimeInputBindingRequest( @@ -1526,7 +1525,7 @@ def test_main_flow_image_artifact_projects_named_provenance_plane() -> None: None, ) - payload = RuntimeArtifactTypeStrategy.for_artifact_type( + payload = CellProfilerOutputRecorder.for_artifact_type( ImageArtifactType ).raw_runtime_input_value( first_spec, RuntimeInputBindingRequest( @@ -1571,7 +1570,7 @@ def test_main_flow_image_artifact_preserves_declared_runtime_slice_stack() -> No None, ) - payload = RuntimeArtifactTypeStrategy.for_artifact_type( + payload = CellProfilerOutputRecorder.for_artifact_type( ImageArtifactType ).raw_runtime_input_value( first_spec, RuntimeInputBindingRequest( @@ -1652,7 +1651,7 @@ def test_main_flow_projection_binds_singleton_broadcast_artifact_as_2d( _activate_runtime_contract(executor.callable_contract, runtime) strategy_type = type( - RuntimeArtifactTypeStrategy.for_artifact_type(ImageArtifactType) + CellProfilerOutputRecorder.for_artifact_type(ImageArtifactType) ) resolve_image = strategy_type.raw_runtime_input_value resolved_specs: list[ArtifactSpec] = [] @@ -1841,7 +1840,7 @@ def test_single_main_flow_image_preserves_runtime_slice_axis() -> None: plane_axis=RuntimePlaneAxis.RUNTIME_SLICE, ).payload_with(np.ones((1, 4, 5), dtype=np.float32), None) - payload = RuntimeArtifactTypeStrategy.for_artifact_type( + payload = CellProfilerOutputRecorder.for_artifact_type( ImageArtifactType ).raw_runtime_input_value( spec, RuntimeInputBindingRequest( @@ -1877,7 +1876,7 @@ def test_object_artifact_source_payload_uses_native_object_provenance() -> None: paths=("/input/A01_s001_w1.tif",), ), ) - payload = RuntimeArtifactTypeStrategy.for_artifact_type( + payload = CellProfilerOutputRecorder.for_artifact_type( ObjectLabelsArtifactType ).source_image_payload_from_runtime_value(object_spec, objects) @@ -3700,7 +3699,7 @@ def test_main_flow_strategy_rejects_mixed_artifact_types_in_either_order() -> No for outputs in (values, tuple(reversed(values))): with pytest.raises(TypeError, match="require one exact artifact type"): - RuntimeArtifactTypeStrategy.for_main_flow_outputs(outputs) + CellProfilerOutputRecorder.for_main_flow_outputs(outputs) def test_cellprofiler_adapter_preserves_object_label_source_component_metadata() -> ( @@ -16521,7 +16520,7 @@ def test_object_label_payload_preserves_source_metadata_for_measurements() -> No ) _activate_runtime_contract(executor.callable_contract, runtime) - payload = RuntimeArtifactTypeStrategy.for_artifact_type( + payload = CellProfilerOutputRecorder.for_artifact_type( spec.artifact_type ).raw_runtime_input_value( spec, RuntimeInputBindingRequest( @@ -16579,7 +16578,7 @@ def test_runtime_object_label_payload_ignores_measurement_image_as_selector() -> measurement_image = ImagePayloadMetadata( source_component_metadata={"well": "A01"}, ).payload_with(image, None) - payload = RuntimeArtifactTypeStrategy.for_artifact_type( + payload = CellProfilerOutputRecorder.for_artifact_type( spec.artifact_type ).raw_runtime_input_value( spec, RuntimeInputBindingRequest( @@ -16630,7 +16629,7 @@ def test_full_stack_object_measurement_resolves_complete_label_artifact() -> Non ) _activate_runtime_contract(executor.callable_contract, runtime) - label_payload = RuntimeArtifactTypeStrategy.for_artifact_type( + label_payload = CellProfilerOutputRecorder.for_artifact_type( object_spec.artifact_type ).raw_runtime_input_value( object_spec, RuntimeInputBindingRequest( @@ -16710,7 +16709,7 @@ def test_object_label_payload_for_measurement_image_projects_source_spatial_crop ) _activate_runtime_contract(executor.callable_contract, runtime) - label_payload = RuntimeArtifactTypeStrategy.for_artifact_type( + label_payload = CellProfilerOutputRecorder.for_artifact_type( spec.artifact_type ).raw_runtime_input_value( spec, RuntimeInputBindingRequest( @@ -18270,7 +18269,7 @@ def test_natural_object_measurement_resolves_one_roster_without_eager_compositio ), ) strategy_type = type( - RuntimeArtifactTypeStrategy.for_artifact_type(ImageArtifactType) + CellProfilerOutputRecorder.for_artifact_type(ImageArtifactType) ) original_resolution = strategy_type.raw_runtime_input_value resolutions = [] diff --git a/tests/unit/test_cellprofiler_output_record_occurrence.py b/tests/unit/test_cellprofiler_output_record_occurrence.py index f326dd6eef..29f0320b63 100644 --- a/tests/unit/test_cellprofiler_output_record_occurrence.py +++ b/tests/unit/test_cellprofiler_output_record_occurrence.py @@ -27,9 +27,11 @@ ) from openhcs.core.source_image_provenance import SourceImageProvenancePlanes from openhcs.interop.cellprofiler.runtime.artifact_binding import ( - ImageArtifactTypeStrategy, RuntimeInputBindingRequest, ) +from openhcs.interop.cellprofiler.runtime.output_recording import ( + ImageOutputRecorder, +) from openhcs.interop.cellprofiler.runtime.output_record_request import ( CellProfilerOutputRecordRequest, ) @@ -341,7 +343,7 @@ def test_record_source_binds_current_image_once_and_preserves_intensity_mask_lin ), ).payload_with(pixels, mask) request = replace(request, current_image=payload) - original_bind = ImageArtifactTypeStrategy.raw_runtime_input_value + original_bind = ImageOutputRecorder.raw_runtime_input_value binding_inputs = [] def observe_bind(strategy, input_spec, value): @@ -349,7 +351,7 @@ def observe_bind(strategy, input_spec, value): return original_bind(strategy, input_spec, value) monkeypatch.setattr( - ImageArtifactTypeStrategy, "raw_runtime_input_value", observe_bind + ImageOutputRecorder, "raw_runtime_input_value", observe_bind ) for pixel_value in (65535, 32768): pixels[:] = pixel_value @@ -385,7 +387,7 @@ def reject_rebinding(*_args, **_kwargs): monkeypatch.setattr(RuntimeInputBindingRequest, "runtime_value", runtime_value) monkeypatch.setattr( - ImageArtifactTypeStrategy, "raw_runtime_input_value", reject_rebinding + ImageOutputRecorder, "raw_runtime_input_value", reject_rebinding ) edge = request.active_input_edges[0] assert request.artifact_source_payload(edge) is bound 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 a247b65a6f..321cc8ba0b 100644 --- a/tests/unit/test_gray_to_color_binding_axis_432.py +++ b/tests/unit/test_gray_to_color_binding_axis_432.py @@ -29,7 +29,9 @@ CellProfilerFunctionContractExecutor, ) from openhcs.interop.cellprofiler.runtime.module_execution import CellProfilerModuleExecutor -from openhcs.interop.cellprofiler.runtime.artifact_binding import RuntimeArtifactTypeStrategy +from openhcs.interop.cellprofiler.runtime.output_recording import ( + CellProfilerOutputRecorder, +) from openhcs.processing.backends.cellprofiler.color import GrayToColorModule, gray_to_color from test_cellprofiler_generic_special_input_binding import _compile_public_step from tests.unit.cellprofiler_runtime_test_support import cellprofiler_runtime_adapter_for_test @@ -193,7 +195,7 @@ def test_integer_primary_binding_units_are_owned_before_stack_rescale_flag(): pixels = np.arange(20, dtype=np.uint16).reshape(4, 5) * 1000 source = _source_plane(pixels, "FITC") spec = ArtifactSpec.input("FITC", ImageArtifactType) - binding_owner = RuntimeArtifactTypeStrategy.for_artifact_type(ImageArtifactType) + binding_owner = CellProfilerOutputRecorder.for_artifact_type(ImageArtifactType) bound = binding_owner.runtime_input_value(spec, source) # This conversion happens in the original input owner, before GrayToColor's # rescale_intensity argument. Do not claim a raw-unit-preserving four-step fix diff --git a/tests/unit/test_runtime_adapter_source_artifacts.py b/tests/unit/test_runtime_adapter_source_artifacts.py index 9b2f159ea0..af3d5ca2a0 100644 --- a/tests/unit/test_runtime_adapter_source_artifacts.py +++ b/tests/unit/test_runtime_adapter_source_artifacts.py @@ -39,8 +39,8 @@ VirtualWorkspaceSourceProjectionAuthority, VirtualWorkspaceSourceProjectionCache, ) -from openhcs.interop.cellprofiler.runtime.artifact_binding import ( - RuntimeArtifactTypeStrategy, +from openhcs.interop.cellprofiler.runtime.output_recording import ( + CellProfilerOutputRecorder, ) from openhcs.microscopes import create_microscope_handler from openhcs.microscopes.openhcs import OpenHCSMicroscopeHandler @@ -227,7 +227,7 @@ def request() -> RuntimeAdapterRequest: labels_request = request() labels_spec = labels_binding.input_spec() labels_payload = labels_request.source_artifact_payload(labels_spec.ref()) - label_set = RuntimeArtifactTypeStrategy.for_artifact_type( + label_set = CellProfilerOutputRecorder.for_artifact_type( ObjectLabelsArtifactType ).raw_runtime_input_value( spec=labels_spec, value=labels_payload diff --git a/tests/unit/test_runtime_artifact_input_value_contract.py b/tests/unit/test_runtime_artifact_input_value_contract.py index a99da528e9..504dd7de60 100644 --- a/tests/unit/test_runtime_artifact_input_value_contract.py +++ b/tests/unit/test_runtime_artifact_input_value_contract.py @@ -9,14 +9,16 @@ image_payload_data, image_payload_metadata, ) -from openhcs.interop.cellprofiler.runtime.artifact_binding import RuntimeArtifactTypeStrategy +from openhcs.interop.cellprofiler.runtime.output_recording import ( + CellProfilerOutputRecorder, +) def test_integer_call_mutation_does_not_replace_post_call_source(): spec = ArtifactSpec.input("DNA", ImageArtifactType) pixels = np.arange(12, dtype=np.uint8).reshape(3, 4) source = ImagePayloadMetadata(source_image_names=("Original",)).payload_with(pixels) - strategy = RuntimeArtifactTypeStrategy.for_artifact_type(spec.artifact_type) + strategy = CellProfilerOutputRecorder.for_artifact_type(spec.artifact_type) raw = strategy.raw_runtime_input_value(spec, source) assert image_payload_data(raw) is pixels assert image_payload_metadata(raw) is not image_payload_metadata(source) @@ -37,7 +39,7 @@ def test_raw_source_derivation_preserves_live_nested_source_mapping(): source = ImagePayloadMetadata( source_component_metadata={"nested": {"selected": "before"}}, ).payload_with(np.zeros((2, 3), dtype=np.uint8)) - strategy = RuntimeArtifactTypeStrategy.for_artifact_type(spec.artifact_type) + strategy = CellProfilerOutputRecorder.for_artifact_type(spec.artifact_type) first = strategy.raw_runtime_input_value(spec, source) nested = image_payload_metadata(source).source_component_metadata["nested"] assert image_payload_metadata(first) is not image_payload_metadata(source) @@ -50,7 +52,7 @@ def test_raw_source_derivation_preserves_live_nested_source_mapping(): def test_measurement_type_errors_remain_at_value_consumption(): spec = ArtifactSpec.input("Intensity", MeasurementsArtifactType) - strategy = RuntimeArtifactTypeStrategy.for_artifact_type(spec.artifact_type) + strategy = CellProfilerOutputRecorder.for_artifact_type(spec.artifact_type) with pytest.raises(TypeError, match="Measurement artifact 'Intensity' requires a MeasurementTable, got object"): strategy.runtime_input_value(spec, object()) with pytest.raises(TypeError, match="Measurement artifact 'Intensity' requires a MeasurementTable, got object"): diff --git a/tests/unit/test_saved_source_label_plane_domain.py b/tests/unit/test_saved_source_label_plane_domain.py index ea6d9eedc9..58cad9bb89 100644 --- a/tests/unit/test_saved_source_label_plane_domain.py +++ b/tests/unit/test_saved_source_label_plane_domain.py @@ -16,8 +16,8 @@ from openhcs.core.runtime_plane_projection import RuntimePlaneAxis, RuntimePlaneAxisValueProjection from openhcs.core.source_image_provenance import SourceImageProvenancePlanes from openhcs.core.source_metadata import SourceVoxelSpacing -from openhcs.interop.cellprofiler.runtime.artifact_binding import ( - RuntimeArtifactTypeStrategy, +from openhcs.interop.cellprofiler.runtime.output_recording import ( + CellProfilerOutputRecorder, ) from openhcs.interop.cellprofiler.runtime.function_contract_execution import CellProfilerFunctionContractExecutor from openhcs.processing.backends.cellprofiler.shape import MeasureObjectSizeShapeModule, measure_object_size_shape @@ -38,7 +38,7 @@ def _source(labels, *, axis=RuntimePlaneAxis.RUNTIME_SLICE, plane_count=None, sp def _saved_label_input(source): spec = ArtifactSpec.input("saved_labels", ObjectLabelsArtifactType, parameter_name="labels") - return RuntimeArtifactTypeStrategy.for_artifact_type(spec.artifact_type).runtime_input_value( + return CellProfilerOutputRecorder.for_artifact_type(spec.artifact_type).runtime_input_value( spec=spec, value=source ) @@ -79,7 +79,7 @@ def test_original_artifact_admission_and_full_stack_shape_keep_2d_planes(axis): source = _source(labels, axis=axis) source_bytes = labels.tobytes() spec = ArtifactSpec.input("saved_labels", ObjectLabelsArtifactType, parameter_name="labels") - value = RuntimeArtifactTypeStrategy.for_artifact_type(spec.artifact_type).runtime_input_value( + value = CellProfilerOutputRecorder.for_artifact_type(spec.artifact_type).runtime_input_value( spec=spec, value=source ) assert value.domain.scope is ObjectLabelDomainScope.PLANE From 7792fa41bfed42e8513774742bc874e31a101b6e Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 21:31:20 -0400 Subject: [PATCH 186/235] Migrate existing runtime fixtures to their direct declaration and location owners Keep the same relationship, stored measurement location and callable-chain memory progression assertions after StoredRuntimeValue inheritance and the memory-holder deletion. Six affected semantic controls pass. Refs #384. --- tests/unit/test_cellprofiler_runtime_adapter.py | 10 +++++----- tests/unit/test_loaded_pattern_group_ownership.py | 6 +++++- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/tests/unit/test_cellprofiler_runtime_adapter.py b/tests/unit/test_cellprofiler_runtime_adapter.py index 221a984b9b..a47d088018 100644 --- a/tests/unit/test_cellprofiler_runtime_adapter.py +++ b/tests/unit/test_cellprofiler_runtime_adapter.py @@ -3673,7 +3673,7 @@ def test_cellprofiler_adapter_relationships_validate_declared_inputs_by_location ) ) - assert relationship.value.artifact_type is RelationshipsArtifactType + assert relationship.artifact_type is RelationshipsArtifactType def test_cellprofiler_adapter_declared_relationship_allows_pruned_child_endpoint(): @@ -3704,7 +3704,7 @@ def test_cellprofiler_adapter_declared_relationship_allows_pruned_child_endpoint ) ) - assert relationship.value.artifact_type is RelationshipsArtifactType + assert relationship.artifact_type is RelationshipsArtifactType assert isinstance(relationship.data, ObjectRelationship) assert relationship.data.declaration.target.name == "FilteredCells" @@ -3803,7 +3803,7 @@ def test_cellprofiler_adapter_relationships_accept_grouped_parent_inputs(): ) ) - assert relationship.value.artifact_type is RelationshipsArtifactType + assert relationship.artifact_type is RelationshipsArtifactType def test_cellprofiler_adapter_relationships_allow_same_invocation_child_output(): @@ -3880,7 +3880,7 @@ def test_cellprofiler_adapter_relationships_allow_same_invocation_child_output() ) ) - assert relationship.value.artifact_type is RelationshipsArtifactType + assert relationship.artifact_type is RelationshipsArtifactType def test_cellprofiler_adapter_adds_and_reads_spatial_grid_artifacts(): @@ -4084,7 +4084,7 @@ def test_cellprofiler_adapter_records_ungrouped_measurements_once(): ) assert len(records) == 1 - assert records[0].path == "/memory/A01_Measurements.pkl" + assert records[0].location.path == "/memory/A01_Measurements.pkl" def test_cellprofiler_adapter_uses_static_output_scope(): diff --git a/tests/unit/test_loaded_pattern_group_ownership.py b/tests/unit/test_loaded_pattern_group_ownership.py index 2019672a2e..bc6a55ce28 100644 --- a/tests/unit/test_loaded_pattern_group_ownership.py +++ b/tests/unit/test_loaded_pattern_group_ownership.py @@ -116,6 +116,11 @@ def test_initial_coordinates_stay_captured_while_plan_selectors_follow_mutation( def test_chain_shares_cohort_but_advances_only_current_image_and_memory(monkeypatch): request, paths, payload = _fixture() invocation = request.compiled_group.invocations[0] + invocation = replace(invocation, contract=replace( + invocation.contract, metadata=replace( + invocation.contract.metadata, output_memory_type="next-memory", + ), + )) group = replace(request.compiled_group, invocations=(invocation, invocation)) request = replace(request, compiled_group=group) loaded = PatternGroupData.from_loaded_group(request, paths, payload) @@ -133,7 +138,6 @@ def execute(executor, *, debug_sink=None): return second_output monkeypatch.setattr(FunctionCoreExecutor, "execute", execute) - monkeypatch.setattr(FunctionCoreExecutor, "memory_types", lambda _executor: SimpleNamespace(output_type="next-memory")) monkeypatch.setattr(function_runtime, "debug_event_sink_from_context", lambda _context: SimpleNamespace(captures_invocation_events=lambda: False)) assert PatternGroupRuntime.execute_chain(loaded) is second_output assert seen[0][0] is loaded and seen[1][0] is loaded From 1a9d45ec80aa183156e09ee9caaed98b7e906238 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 21:31:47 -0400 Subject: [PATCH 187/235] Admit callable cohorts through their nominal adapter input domain The existing CP module declaration selects actual primary images or object-only input domains. A substitutable adapter-spec specialization exposes that selection to the generic compiled cohort owner, avoiding secondary-label context images being loaded and composed as primary operands. Rendered intrinsic object volumes inherit their existing spatial domain without acquiring the outer invocation axis. Original 31-step/180-reference 3D replay compiled with both OverlayObjects selecting only their primary images and executed through both overlays; whole-volume filename publication remains a separate known follow-up. Existing runtime ownership controls and strict legacy label-plane drift admission pass. Refs #384; Refs #573. --- openhcs/core/callable_contract.py | 10 +++++++++ openhcs/core/function_patterns.py | 3 ++- openhcs/core/runtime_adapters.py | 6 +++++ openhcs/core/steps/function_runtime.py | 5 +++++ .../interop/cellprofiler/runtime/adapter.py | 22 ++++++++++++++++--- 5 files changed, 42 insertions(+), 4 deletions(-) diff --git a/openhcs/core/callable_contract.py b/openhcs/core/callable_contract.py index 8c5acfc8e7..b2d61b24fc 100644 --- a/openhcs/core/callable_contract.py +++ b/openhcs/core/callable_contract.py @@ -948,6 +948,16 @@ def preserves_input_main_flow(self) -> bool: return self.artifact_output_policy.preserves_input_main_flow(self) + @property + def invocation_domain_inputs(self) -> ArtifactSpecCollection: + """Derive carrier inputs through the declared adapter's nominal owner.""" + adapter = self.runtime_adapter + return ( + self.group_scope_inputs + if adapter is None + else adapter.invocation_domain_inputs(self) + ) + @property def runtime_adapter(self) -> RuntimeAdapterSpec | None: """Declared runtime adapter.""" diff --git a/openhcs/core/function_patterns.py b/openhcs/core/function_patterns.py index d6947af154..10a5d9fd45 100644 --- a/openhcs/core/function_patterns.py +++ b/openhcs/core/function_patterns.py @@ -847,9 +847,10 @@ def stored_primary_input_edges_for_component( primary_edges = tuple( edge for invocation, edges in active + for domain_refs in (invocation.contract.invocation_domain_inputs.ref_set(),) for edge in edges if edge.spec.ref() - in invocation.contract.group_scope_inputs.ref_set() + in domain_refs and edge.spec.artifact_type.carries_source_image_context ) if not primary_edges or any( diff --git a/openhcs/core/runtime_adapters.py b/openhcs/core/runtime_adapters.py index c7770d7aa1..0872921da7 100644 --- a/openhcs/core/runtime_adapters.py +++ b/openhcs/core/runtime_adapters.py @@ -385,6 +385,12 @@ class RuntimeAdapterSpec: ) runtime_callable_factory: RuntimeCallableFactory | None = None + def invocation_domain_inputs( + self, contract: "CallableContract", + ) -> ArtifactSpecCollection: + """Return declared inputs supplying this adapter's invocation carrier.""" + return contract.group_scope_inputs + def __post_init__(self) -> None: if not self.parameter_name: raise ValueError("RuntimeAdapterSpec.parameter_name cannot be empty.") diff --git a/openhcs/core/steps/function_runtime.py b/openhcs/core/steps/function_runtime.py index 6d09165489..31811847f8 100644 --- a/openhcs/core/steps/function_runtime.py +++ b/openhcs/core/steps/function_runtime.py @@ -918,6 +918,7 @@ def requires_plane_contextualization( plane_projection is not None and plane_projection.plane_index is None and image_payload_metadata(source_payload).plane_axis is None + and not image_payload_metadata(source_payload).persists_whole_image() and np.ndim(object_label_dense_array(source_payload)) >= 3 ) @@ -933,6 +934,10 @@ def contextualize( f"{type(source_payload).__name__}." ) source_metadata = image_payload_metadata(source_payload) + if source_metadata.persists_whole_image(): + return source_metadata.derive_payload( + source_payload, output_value, plane_projection=None, + ) if plane_projection is None or plane_projection.plane_index is not None: return source_metadata.derive_payload( source_payload, diff --git a/openhcs/interop/cellprofiler/runtime/adapter.py b/openhcs/interop/cellprofiler/runtime/adapter.py index 631d102106..67ef08331a 100644 --- a/openhcs/interop/cellprofiler/runtime/adapter.py +++ b/openhcs/interop/cellprofiler/runtime/adapter.py @@ -14,6 +14,7 @@ from openhcs.core.artifacts import ( ArtifactOutputPlan, ArtifactSpec, + ArtifactSpecCollection, ArtifactType, ImageArtifactType, MeasurementsArtifactType, @@ -22,7 +23,7 @@ RelationshipsArtifactType, SpatialGridArtifactType, ) -from openhcs.core.runtime_adapters import RuntimeAdapterRequest +from openhcs.core.runtime_adapters import RuntimeAdapterRequest, RuntimeAdapterSpec from openhcs.core.runtime_artifact_values import ( RuntimeValue, ) @@ -87,6 +88,22 @@ def validate_measurement_subject(cls, spec: ArtifactSpec) -> None: spec.require_measurement_feature_owner() +class CellProfilerRuntimeAdapterSpec(RuntimeAdapterSpec): + """Derive CP invocation carriers from the module's declared input domain.""" + + __slots__ = () + + def invocation_domain_inputs( + self, contract: CallableContract, + ) -> ArtifactSpecCollection: + from openhcs.interop.cellprofiler.module_declarations import CellProfilerModule + + module = CellProfilerModule.require_callable_contract_owner(contract) + return ArtifactSpecCollection(module.invocation_domain_inputs( + contract.resolve_canonical_raw_callable(), contract.artifact_inputs.specs, + )) + + @dataclass(slots=True) class CellProfilerRuntimeAdapter(RuntimePlaneAxisProjector): """CellProfiler-like API backed by typed OpenHCS runtime state. @@ -106,13 +123,12 @@ def require_parameter_name(cls) -> str: def runtime_adapter_spec(cls): """Return the sole compiled CellProfiler runtime-adapter declaration.""" - from openhcs.core.runtime_adapters import RuntimeAdapterSpec from openhcs.interop.cellprofiler.runtime.module_execution import ( cellprofiler_runtime_adapter_factory, cellprofiler_runtime_callable_factory, ) - return RuntimeAdapterSpec( + return CellProfilerRuntimeAdapterSpec( parameter_name=cls.require_parameter_name(), factory=cellprofiler_runtime_adapter_factory, manages_artifact_inputs=True, From 6927a02a15324be4700c0c3274e24edad5aba156 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 21:33:16 -0400 Subject: [PATCH 188/235] Reuse compiled CellProfiler contracts through runtime declaration policies Image-domain and primary-image policies receive the existing CallableContract instead of rebuilding it from the raw callable. Module ownership checks resolve the current exact declaration and validate current exclusions against that supplied contract; authored help and signature preparation stays at require_callable admission. Remove the executor duplicate require_callable ownership check. Preserve canonical declaration replacement rejection, current unowned exclusions, transported function references and input/source epochs. One bounded semantic gate: 13 passed. No isolated timing claim. Refs #384, #394 --- .../cellprofiler/module_artifact_contracts.py | 6 ++-- .../module_artifact_declarations.py | 4 ++- .../cellprofiler/module_callable_abi.py | 11 +++--- .../cellprofiler/module_declarations.py | 36 +++++++++++++++---- .../runtime/measurement_recording.py | 2 +- .../cellprofiler/runtime/module_execution.py | 14 ++------ .../runtime/primary_image_input_policies.py | 11 +++--- .../backends/cellprofiler/primary_objects.py | 6 +++- .../test_cellprofiler_backend_public_api.py | 29 +++++++++++++++ ...filer_conditional_topology_declarations.py | 2 +- .../test_cellprofiler_module_execution.py | 34 +++++++++++++++++- ...st_cellprofiler_special_io_declarations.py | 4 +-- 12 files changed, 120 insertions(+), 39 deletions(-) diff --git a/openhcs/interop/cellprofiler/module_artifact_contracts.py b/openhcs/interop/cellprofiler/module_artifact_contracts.py index 3cf653e6f1..14ccb568b1 100644 --- a/openhcs/interop/cellprofiler/module_artifact_contracts.py +++ b/openhcs/interop/cellprofiler/module_artifact_contracts.py @@ -1033,7 +1033,9 @@ def artifact_output_relations( del module, binding, name, step_context, output_position primary_images = cls.primary_image_inputs( - cls.require_callable(invocation_key.function_name), + CallableContract.from_prepared_callable( + cls.require_callable(invocation_key.function_name) + ), artifact_inputs.specs, ) lineage_inputs = ArtifactSpecCollection( @@ -1171,7 +1173,7 @@ def callable_contract( artifact_inputs=ArtifactSpecCollection(inputs), ) remaining_inputs = list(inputs) - primary_image_inputs = cls.primary_image_inputs(func, inputs) + primary_image_inputs = cls.primary_image_inputs(callable_contract, inputs) for primary_image_input in primary_image_inputs: remaining_inputs.remove(primary_image_input) artifact_inputs = ArtifactSpecCollection( diff --git a/openhcs/interop/cellprofiler/module_artifact_declarations.py b/openhcs/interop/cellprofiler/module_artifact_declarations.py index 9c2b323f03..143e485998 100644 --- a/openhcs/interop/cellprofiler/module_artifact_declarations.py +++ b/openhcs/interop/cellprofiler/module_artifact_declarations.py @@ -729,7 +729,9 @@ def measurement_output_relations( for artifact_input in artifact_inputs.specs ) invocation_domain_inputs = cls.invocation_domain_inputs( - cls.require_callable(invocation_key.function_name), + CallableContract.from_prepared_callable( + cls.require_callable(invocation_key.function_name) + ), artifact_inputs.specs, ) return ( diff --git a/openhcs/interop/cellprofiler/module_callable_abi.py b/openhcs/interop/cellprofiler/module_callable_abi.py index cce856f9a6..4f7bbe6d63 100644 --- a/openhcs/interop/cellprofiler/module_callable_abi.py +++ b/openhcs/interop/cellprofiler/module_callable_abi.py @@ -166,15 +166,12 @@ def source_context( @classmethod def primary_image_inputs( cls, - func: "Callable[..., RuntimeFunctionOutput]", + contract: CallableContract, declared_inputs: tuple[ArtifactSpec, ...], ) -> tuple[ArtifactSpec, ...]: """Return non-special image inputs that drive invocation slices.""" - if ( - CallableContract.from_callable(func).execution_scope - is FunctionStepExecutionScope.PLATE - ): + if contract.execution_scope is FunctionStepExecutionScope.PLATE: return () image_inputs = ArtifactSpecCollection(declared_inputs).of_artifact_type( ImageArtifactType @@ -188,12 +185,12 @@ def primary_image_inputs( @classmethod def invocation_domain_inputs( cls, - func: "Callable[..., RuntimeFunctionOutput]", + contract: CallableContract, declared_inputs: tuple[ArtifactSpec, ...], ) -> tuple[ArtifactSpec, ...]: """Return the inputs whose component scope owns one invocation.""" - primary_images = cls.primary_image_inputs(func, declared_inputs) + primary_images = cls.primary_image_inputs(contract, declared_inputs) if primary_images: return primary_images object_inputs = ArtifactSpecCollection(declared_inputs).of_artifact_type( diff --git a/openhcs/interop/cellprofiler/module_declarations.py b/openhcs/interop/cellprofiler/module_declarations.py index ab4b519c7e..62c99d4dd7 100644 --- a/openhcs/interop/cellprofiler/module_declarations.py +++ b/openhcs/interop/cellprofiler/module_declarations.py @@ -492,13 +492,16 @@ def for_callable_contract( module_type = cls.for_callable_import_identity(identity) if module_type is None: return None - canonical_callable = module_type.require_callable(identity.function_name) + canonical_callable = module_type._require_declared_callable( + identity.function_name + ) if raw_callable is not canonical_callable: raise ValueError( f"Callable import identity {identity.import_path!r} claims " f"CellProfiler module {module_type.__name__}, but its object is " "not the declaration-owned canonical callable." ) + module_type._validate_callable_parameter_exclusions(raw_callable, contract) return module_type @classmethod @@ -534,11 +537,11 @@ def require_module_name(cls) -> str: return _required_string(cls.module_name, "module_name", cls.__name__) @classmethod - def require_callable( + def _require_declared_callable( cls, function_name: str | None = None, ) -> Callable[..., Any]: - """Load one raw backend callable declared by this module class.""" + """Resolve the current exact callable without repeating authored preparation.""" selected_name = cls.function_name if function_name is None else function_name selected_name = _required_string( selected_name, @@ -557,10 +560,32 @@ def require_callable( f"CellProfiler module {cls.module_name!r} declares missing " f"callable {selected_name!r} in {cls.__module__!r}." ) + return implementation + + @classmethod + def require_callable( + cls, + function_name: str | None = None, + ) -> Callable[..., Any]: + """Prepare and admit one raw callable at the authored declaration boundary.""" + + implementation = cls._require_declared_callable(function_name) cls._install_callable_parameter_help(implementation) + cls._validate_callable_parameter_exclusions( + implementation, CallableContract.from_callable(implementation), + ) + return implementation + + @classmethod + def _validate_callable_parameter_exclusions( + cls, + implementation: Callable[..., Any], + contract: CallableContract, + ) -> None: + """Keep current exclusion admission while reusing the owned declaration.""" + from python_introspect import parameter_exclusions - contract = CallableContract.from_callable(implementation) excluded_names = parameter_exclusions(implementation) unowned_exclusions = tuple( sorted(excluded_names - contract.runtime_owned_parameter_names) @@ -568,11 +593,10 @@ def require_callable( if unowned_exclusions: raise ValueError( f"CellProfiler module {cls.module_name!r} callable " - f"{selected_name!r} excludes parameters without a runtime-owned " + f"{contract.function_name!r} excludes parameters without a runtime-owned " "callable contract declaration: " f"{unowned_exclusions!r}." ) - return implementation @classmethod def _install_callable_parameter_help( diff --git a/openhcs/interop/cellprofiler/runtime/measurement_recording.py b/openhcs/interop/cellprofiler/runtime/measurement_recording.py index 63cacedd72..fd545fb56d 100644 --- a/openhcs/interop/cellprofiler/runtime/measurement_recording.py +++ b/openhcs/interop/cellprofiler/runtime/measurement_recording.py @@ -247,7 +247,7 @@ def measurement_record_source_image_name( request.callable_contract ) primary_image_inputs = module_type.primary_image_inputs( - request.callable_contract.resolve_canonical_raw_callable(), + request.callable_contract, request.callable_contract.artifact_inputs.specs, ) current_source_name = request.source.source_image_name diff --git a/openhcs/interop/cellprofiler/runtime/module_execution.py b/openhcs/interop/cellprofiler/runtime/module_execution.py index 244d71215f..4c7223ad6f 100644 --- a/openhcs/interop/cellprofiler/runtime/module_execution.py +++ b/openhcs/interop/cellprofiler/runtime/module_execution.py @@ -179,15 +179,7 @@ def __post_init__(self) -> None: f"{self.callable_contract.function_name!r} does not match resolved " f"raw callable {self.raw_func.__name__!r}." ) - module_type = self.module_type() - if self.raw_func is not module_type.require_callable( - self.callable_contract.function_name - ): - raise ValueError( - f"CellProfiler callable {self.callable_contract.function_name!r} " - f"is not the declaration-owned callable for module " - f"{module_type.require_module_name()!r}." - ) + self.module_type() def module_type(self) -> type[CellProfilerModule]: """Return the nominal module declaration that owns this callable.""" @@ -606,7 +598,7 @@ def _primary_image_inputs( current_image=current_image, ) return module_type.primary_image_inputs( - self.raw_func, + self.callable_contract, request.primary_image_inputs, ) @@ -1303,7 +1295,7 @@ def _image_request( current_image=current_image, ) image_inputs = module_type.primary_image_inputs( - self.raw_func, + self.callable_contract, input_binding.primary_image_inputs, ) runtime_projection = RuntimePlaneAxisValueProjection.from_projector( diff --git a/openhcs/interop/cellprofiler/runtime/primary_image_input_policies.py b/openhcs/interop/cellprofiler/runtime/primary_image_input_policies.py index 40a9e40801..93d2cf83f9 100644 --- a/openhcs/interop/cellprofiler/runtime/primary_image_input_policies.py +++ b/openhcs/interop/cellprofiler/runtime/primary_image_input_policies.py @@ -2,16 +2,15 @@ from __future__ import annotations -from collections.abc import Callable from dataclasses import replace from openhcs.core.artifacts import ( ArtifactSpec, ) +from openhcs.core.callable_contract import CallableContract from openhcs.core.runtime_object_labels import ObjectLabelValue from openhcs.core.steps.function_runtime import ( RuntimeCallableKwargs, - RuntimeFunctionOutput, ) from openhcs.interop.cellprofiler.runtime.invocation import CellProfilerImageRequest @@ -22,21 +21,21 @@ class ObjectLabelDrivenPrimaryImageInputPolicy: @classmethod def primary_image_inputs( cls, - func: Callable[..., RuntimeFunctionOutput], + contract: CallableContract, declared_inputs: tuple[ArtifactSpec, ...], ) -> tuple[ArtifactSpec, ...]: - del cls, func, declared_inputs + del cls, contract, declared_inputs return () @classmethod def invocation_domain_inputs( cls, - func: Callable[..., RuntimeFunctionOutput], + contract: CallableContract, declared_inputs: tuple[ArtifactSpec, ...], ) -> tuple[ArtifactSpec, ...]: """Return the exact object-label input that owns one invocation.""" - del func + del contract binding = cls.primary_image_domain_input_binding() parameter_name = binding.require_runtime_parameter_name() artifact_type = binding.require_artifact_type() diff --git a/openhcs/processing/backends/cellprofiler/primary_objects.py b/openhcs/processing/backends/cellprofiler/primary_objects.py index 9f51d8e0ba..897f377167 100644 --- a/openhcs/processing/backends/cellprofiler/primary_objects.py +++ b/openhcs/processing/backends/cellprofiler/primary_objects.py @@ -90,6 +90,7 @@ from openhcs.processing.backends.cellprofiler.enum_attributes import ( CellProfilerEnumAttributeMixin, ) +from openhcs.core.callable_contract import CallableContract from openhcs.core.artifacts import ( ArtifactSpecCollection, ImageArtifactType, @@ -646,7 +647,10 @@ def finalize_artifact_contract_outputs( artifact_outputs=artifact_outputs, ) source = cls.primary_image_inputs( - cls.require_callable(invocation_key.function_name), artifact_inputs.specs + CallableContract.from_prepared_callable( + cls.require_callable(invocation_key.function_name) + ), + artifact_inputs.specs, ) if len(source) != 1: raise ValueError("IdentifyPrimaryObjects diagnostics require one source image.") diff --git a/tests/unit/test_cellprofiler_backend_public_api.py b/tests/unit/test_cellprofiler_backend_public_api.py index 9e78196358..e71d99fd6f 100644 --- a/tests/unit/test_cellprofiler_backend_public_api.py +++ b/tests/unit/test_cellprofiler_backend_public_api.py @@ -284,6 +284,35 @@ def test_callable_ownership_uses_complete_import_identity() -> None: assert CellProfilerModule.for_callable_contract(numpy_contract) is None + +@pytest.mark.parametrize("mutation", ("replacement", "unowned_exclusion")) +def test_compiled_callable_owner_keeps_current_declaration_admission( + monkeypatch: pytest.MonkeyPatch, + mutation: str, +) -> None: + module = CellProfilerModule.require_module("Crop") + raw = module.require_callable() + contract = CallableContract.from_prepared_callable(raw) + if mutation == "replacement": + implementation_module = importlib.import_module(module.__module__) + monkeypatch.setattr( + implementation_module, contract.function_name, lambda image: image + ) + with pytest.raises( + ValueError, match="not the declaration-owned canonical callable" + ): + CellProfilerModule.require_callable_contract_owner(contract) + else: + from python_introspect import parameter_exclusions, set_parameter_exclusions + + original = parameter_exclusions(raw) + try: + set_parameter_exclusions(raw, (*original, "undeclared_runtime_input")) + with pytest.raises(ValueError, match="undeclared_runtime_input"): + CellProfilerModule.require_callable_contract_owner(contract) + finally: + set_parameter_exclusions(raw, original) + def test_duplicate_function_ownership_fails_during_module_declaration() -> None: existing_owner = CellProfilerModule.require_module("Crop") diff --git a/tests/unit/test_cellprofiler_conditional_topology_declarations.py b/tests/unit/test_cellprofiler_conditional_topology_declarations.py index c07c1b331b..118dd97e6a 100644 --- a/tests/unit/test_cellprofiler_conditional_topology_declarations.py +++ b/tests/unit/test_cellprofiler_conditional_topology_declarations.py @@ -583,7 +583,7 @@ def test_watershed_contract_uses_active_roles_and_preserves_reuse() -> None: "Input", ) assert WatershedModule.primary_image_inputs( - watershed_library, + contracts[0], contracts[0].artifact_inputs, ) == (contracts[0].artifact_inputs[0],) assert _contract_input_names(contracts[0]) == ("Input",) diff --git a/tests/unit/test_cellprofiler_module_execution.py b/tests/unit/test_cellprofiler_module_execution.py index a0b056e023..57669a2eac 100644 --- a/tests/unit/test_cellprofiler_module_execution.py +++ b/tests/unit/test_cellprofiler_module_execution.py @@ -449,6 +449,38 @@ def _module_executor( return executor + +def test_compiled_image_request_does_not_repeat_authored_callable_preparation( + monkeypatch: pytest.MonkeyPatch, +) -> None: + module = CellProfilerModule.require_module("Crop") + raw = module.require_callable() + source_spec = ArtifactSpec.input("Input", ImageArtifactType) + contract = _compiled_callable_contract(raw, artifact_inputs=(source_spec,)) + image = np.full((4, 5), 0.25, dtype=np.float32) + adapter = _FakeCellProfilerRuntime( + {source_spec.name: image}, + callable_contract=contract, + artifact_input_edges=(_artifact_input_edge_for_test(source_spec),), + ) + + def reject_authored_preparation(*args, **kwargs): + raise AssertionError("Runtime re-entered authored callable preparation") + + monkeypatch.setattr(CallableContract, "from_callable", reject_authored_preparation) + monkeypatch.setattr( + module, "_install_callable_parameter_help", reject_authored_preparation + ) + executor = CellProfilerModuleExecutor(raw, contract) + image_request = executor._image_request( + image, adapter, + module_type=executor.module_type(), + active_input_specs=contract.artifact_inputs.specs, + ) + + assert image_request.source_aliases == (source_spec.name,) + np.testing.assert_array_equal(image_payload_data(image_request.payload), image) + def test_default_invocation_keeps_compiled_source_bindings_outside_anchor_group() -> ( None ): @@ -9953,7 +9985,7 @@ def test_mask_objects_uses_object_labels_as_primary_execution_domain() -> None: module_type = MaskObjectsModule assert ( module_type.primary_image_inputs( - mask_objects, + contract, contract.artifact_inputs, ) == () diff --git a/tests/unit/test_cellprofiler_special_io_declarations.py b/tests/unit/test_cellprofiler_special_io_declarations.py index 16b2c84c1f..df42f66599 100644 --- a/tests/unit/test_cellprofiler_special_io_declarations.py +++ b/tests/unit/test_cellprofiler_special_io_declarations.py @@ -431,11 +431,11 @@ def test_optional_object_special_input_does_not_consume_primary_image() -> None: objects = ArtifactSpec.input("Nuclei", ObjectLabelsArtifactType) assert MeasureImageIntensityModule.primary_image_inputs( - measure_image_intensity, + CallableContract.from_prepared_callable(measure_image_intensity), (image,), ) == (image,) assert MeasureImageIntensityModule.primary_image_inputs( - measure_image_intensity_objects, + CallableContract.from_prepared_callable(measure_image_intensity_objects), (image, objects), ) == (image,) From ea87af352e2fd4b4ffff26daf61bd21e46312c97 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 21:33:59 -0400 Subject: [PATCH 189/235] Pass the existing compiled contract to adapter cohort-domain selection Complete the migrated nominal ABI at the shared cohort adapter declaration. Refs #384; Refs #573. --- openhcs/interop/cellprofiler/runtime/adapter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openhcs/interop/cellprofiler/runtime/adapter.py b/openhcs/interop/cellprofiler/runtime/adapter.py index 67ef08331a..0a9f3909b7 100644 --- a/openhcs/interop/cellprofiler/runtime/adapter.py +++ b/openhcs/interop/cellprofiler/runtime/adapter.py @@ -100,7 +100,7 @@ def invocation_domain_inputs( module = CellProfilerModule.require_callable_contract_owner(contract) return ArtifactSpecCollection(module.invocation_domain_inputs( - contract.resolve_canonical_raw_callable(), contract.artifact_inputs.specs, + contract, contract.artifact_inputs.specs, )) From 9b27aa0576adb9e30ce0ed8e29ea512aa48bf49a Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 21:34:28 -0400 Subject: [PATCH 190/235] Use retained storage filename identity for whole-image materialization Refs #573. Forward the existing materialization filename identity to the shared source-stem owner, preserving aggregate source Z provenance while naming the physical image. Extend the real TIFF volume save/projection control to exercise source-identity naming. --- openhcs/processing/materialization/core.py | 4 +++- tests/unit/test_materialization_core.py | 19 ++++++++++++++++--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/openhcs/processing/materialization/core.py b/openhcs/processing/materialization/core.py index 6d98533bc3..9ceb743937 100644 --- a/openhcs/processing/materialization/core.py +++ b/openhcs/processing/materialization/core.py @@ -2637,7 +2637,9 @@ def write_image_file( filename = context.named_source_filename(item.metadata, options.primary_output_suffix) if filename is None: filename = ( - source_stem_authority.required_source_stem(item.metadata) + source_stem_authority.required_source_stem( + item.metadata, context.artifact_filename_identity, + ) + options.primary_output_suffix ) path = str(paths.parent / filename) diff --git a/tests/unit/test_materialization_core.py b/tests/unit/test_materialization_core.py index 1d4fc2cc1a..3be3e5a87c 100644 --- a/tests/unit/test_materialization_core.py +++ b/tests/unit/test_materialization_core.py @@ -2439,9 +2439,12 @@ def test_declared_volume_save_preserves_pixels_and_exact_plane_selection( tmp_path, intrinsic ): from openhcs.core.artifacts import ImageArtifactType + from openhcs.processing.materialization.options import MaterializedFilenameIdentity from openhcs.core.component_group_scope import RuntimeExecutionAxisScope from openhcs.core.image_file_serialization import TiffImageFileFormat - from openhcs.core.runtime_image_values import image_payload_data, image_payload_mask + from openhcs.core.runtime_image_values import ( + image_payload_data, image_payload_mask, image_payload_metadata, + ) from openhcs.core.runtime_slice_projection import RuntimeSliceProjection from openhcs.core.source_projection import SourceArtifactProjection, SourcePixelRef from openhcs.core.source_spatial_domain import VolumeSourceSpatialDomain @@ -2487,15 +2490,21 @@ def test_declared_volume_save_preserves_pixels_and_exact_plane_selection( ) payload = metadata.payload_with(pixels, mask) manager = FileManager({"disk": DiskStorageBackend()}) - spec = MaterializationSpec(ImageFileOptions(filename_suffix=".tif")) + spec = MaterializationSpec(ImageFileOptions( + filename_suffix=".tif", + filename_identity=MaterializedFilenameIdentity.SOURCE_IDENTITY, + )) batch = prepare_materialization( spec, payload, - str(tmp_path / ("A01_s001_w2_z001_t001.tif" if intrinsic else "output")), + str(tmp_path / "output"), manager, ("disk",), context=_SourceSchemaProcessingContext(), variable_components=(VariableComponents.Z_INDEX,), + artifact_filename_identity=SourceImageIdentity( + component_metadata={**components[0], "extension": ".tif"}, + ), ) batch.save() assert len(batch.outputs) == (1 if intrinsic else 3) @@ -2505,6 +2514,10 @@ def test_declared_volume_save_preserves_pixels_and_exact_plane_selection( return (output,) = batch.outputs + assert Path(output.path).name == "A01_s001_w2_z001_t001.tif" + assert image_payload_metadata(payload).source_component_metadata.get("z_index") is None + assert output.metadata.source_component_metadata.get("z_index") is None + assert output.metadata.source_provenance.source_plane_count == 3 np.testing.assert_array_equal(tifffile.imread(output.path), pixels) saved_metadata = TiffImageFileFormat().persisted_metadata( Path(output.path), payload From 0adb681089ac89794dec4b0ddbf7623fad8fbaf3 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 21:36:57 -0400 Subject: [PATCH 191/235] refactor: compile directly from the resolved pipeline owner --- openhcs/core/pipeline/compilation_session.py | 98 ++---- openhcs/core/pipeline/compiler.py | 179 +++++----- openhcs/core/pipeline/path_planner.py | 311 ++++++++---------- openhcs/core/pipeline/step_snapshot.py | 44 --- .../cellprofiler/compile_time_contracts.py | 30 +- .../pyqt_gui/test_plate_manager_widget.py | 17 +- tests/unit/test_artifact_output_ownership.py | 10 +- ...test_cellprofiler_artifact_declarations.py | 33 +- ...ellprofiler_combineobjects_source_scope.py | 4 +- ...ellprofiler_morphology_public_contracts.py | 19 +- .../test_cellprofiler_neighbors_contract.py | 57 ++-- ...profiler_runtime_callable_introspection.py | 16 +- tests/unit/test_compilation_session.py | 197 +++++------ .../test_function_step_execution_scope.py | 7 +- .../unit/test_invocation_contract_provider.py | 93 ++---- ...test_materialization_filter_inheritance.py | 11 +- .../unit/test_path_planner_materialization.py | 287 ++++++++-------- ...y => test_resolved_pipeline_definition.py} | 37 +-- tests/unit/test_source_bindings.py | 41 +-- 19 files changed, 598 insertions(+), 893 deletions(-) delete mode 100644 openhcs/core/pipeline/step_snapshot.py rename tests/unit/{test_step_snapshot.py => test_resolved_pipeline_definition.py} (59%) diff --git a/openhcs/core/pipeline/compilation_session.py b/openhcs/core/pipeline/compilation_session.py index 785b2ef9f3..2751314ba6 100644 --- a/openhcs/core/pipeline/compilation_session.py +++ b/openhcs/core/pipeline/compilation_session.py @@ -2,7 +2,6 @@ from __future__ import annotations -from collections.abc import Iterator from dataclasses import dataclass, fields, is_dataclass, replace from pathlib import Path from typing import TYPE_CHECKING, Any, Mapping, MutableMapping, Sequence, get_type_hints @@ -11,10 +10,6 @@ from openhcs.core.compiled_step_plan import CompiledStepPlan from openhcs.core.context.processing_context import ProcessingContext -from openhcs.core.pipeline.step_snapshot import ( - StepSnapshot, - build_step_snapshots, -) from openhcs.core.source_metadata import SourceMetadataMapping from openhcs.core.source_workspace_projection import VirtualWorkspaceSourceProjection from openhcs.core.steps.abstract import AbstractStep @@ -156,21 +151,36 @@ def resolve_declared_dataclass_paths( return replace(value, **replacements) if replacements else value +@dataclass(frozen=True, slots=True) +class ResolvedPipelineDefinition: + """ObjectState-resolved pipeline declaration shared by all axis sessions.""" + + steps: Sequence[AbstractStep] + step_state_map: Mapping[int, "ObjectState"] + + def __post_init__(self) -> None: + missing_states = [ + index for index in range(len(self.steps)) if index not in self.step_state_map + ] + if missing_states: + raise ValueError( + f"Resolved pipeline missing ObjectState entries for steps {missing_states}." + ) + + @dataclass(slots=True) class CompilationSession: """Compiler boundary for one ProcessingContext. The session is not a dict wrapper. It owns the invariants tying together the - resolved step list, ObjectState map, StepSnapshot tuple, context, and mutable + resolved pipeline declaration, context, and mutable compiled-plan map for one axis or sequential-combination context. """ context: ProcessingContext - steps: Sequence[AbstractStep] + pipeline: ResolvedPipelineDefinition orchestrator: "PipelineOrchestrator" global_config: "GlobalPipelineConfig" - step_state_map: Mapping[int, "ObjectState"] - snapshots: tuple[StepSnapshot, ...] plans: MutableMapping[int, CompiledStepPlan] source_workspace_projection: VirtualWorkspaceSourceProjection path_resolver: CompilationPathResolver | None = None @@ -183,11 +193,9 @@ def from_context( cls, *, context: ProcessingContext, - steps: Sequence[AbstractStep], + pipeline: ResolvedPipelineDefinition, orchestrator: "PipelineOrchestrator", global_config: "GlobalPipelineConfig", - step_state_map: Mapping[int, "ObjectState"], - snapshots: tuple[StepSnapshot, ...] | None = None, source_workspace_projection: VirtualWorkspaceSourceProjection | None = None, path_resolver: CompilationPathResolver | None = None, metadata_writer: bool = False, @@ -196,15 +204,11 @@ def from_context( ) -> "CompilationSession": if context.step_plans is None: raise ValueError("CompilationSession requires context.step_plans.") - if snapshots is None: - snapshots = build_step_snapshots(steps, step_state_map) return cls( context=context, - steps=steps, + pipeline=pipeline, orchestrator=orchestrator, global_config=global_config, - step_state_map=step_state_map, - snapshots=snapshots, plans=context.step_plans, source_workspace_projection=( VirtualWorkspaceSourceProjection.empty(context.plate_path) @@ -224,34 +228,6 @@ def from_context( def __post_init__(self) -> None: if self.plate_scope is None and self.context.plate_path is not None: self.plate_scope = CompilationPlateScope.from_context(self.context) - if len(self.steps) != len(self.snapshots): - raise ValueError( - "CompilationSession requires one StepSnapshot per step: " - f"{len(self.snapshots)} snapshots for {len(self.steps)} steps." - ) - missing_states = [ - index - for index in range(len(self.steps)) - if index not in self.step_state_map - ] - if missing_states: - raise ValueError( - f"CompilationSession missing ObjectState entries for steps " - f"{missing_states}." - ) - for expected_index, (snapshot, step) in enumerate( - zip(self.snapshots, self.steps, strict=True) - ): - if snapshot.index != expected_index: - raise ValueError( - f"StepSnapshot index mismatch: expected {expected_index}, " - f"got {snapshot.index}." - ) - if snapshot.step is not step: - raise ValueError( - f"StepSnapshot {expected_index} does not reference its resolved step." - ) - @property def axis_id(self) -> str: return self.context.axis_id @@ -273,35 +249,16 @@ def realized_source_metadata( ) return metadata or None - def step(self, index: int) -> AbstractStep: - return self.steps[index] - - def snapshot(self, index: int) -> StepSnapshot: - return self.snapshots[index] - @property def step_count(self) -> int: - return len(self.snapshots) - - def indexed_snapshots(self) -> Iterator[tuple[int, StepSnapshot]]: - return iter(enumerate(self.snapshots)) - - def reverse_snapshot_indices(self) -> range: - return range(self.step_count - 1, -1, -1) - - def step_state(self, index: int) -> "ObjectState": - try: - return self.step_state_map[index] - except KeyError as exc: - raise ValueError(f"Missing ObjectState for step {index}.") from exc + return len(self.pipeline.steps) def plan(self, index: int) -> CompiledStepPlan: try: return self.plans[index] except KeyError as exc: - snapshot = self.snapshot(index) raise ValueError( - f"Missing compiled plan for step {index} ({snapshot.step.name})." + f"Missing compiled plan for step {index} ({self.pipeline.steps[index].name})." ) from exc def main_flow_plan_ancestry( @@ -325,12 +282,3 @@ def main_flow_plan_ancestry( if source_step_index is None: return tuple(ancestry) current_index = source_step_index - - -@dataclass(frozen=True, slots=True) -class ResolvedPipelineDefinition: - """ObjectState-resolved pipeline declaration shared by all axis sessions.""" - - steps: Sequence[AbstractStep] - step_state_map: Mapping[int, "ObjectState"] - snapshots: tuple[StepSnapshot, ...] diff --git a/openhcs/core/pipeline/compiler.py b/openhcs/core/pipeline/compiler.py index 46619c1b74..47396871d3 100644 --- a/openhcs/core/pipeline/compiler.py +++ b/openhcs/core/pipeline/compiler.py @@ -7,7 +7,7 @@ CONFIGURATION ACCESS PATTERN: ============================ The compiler resolves ObjectState once and replaces the submitted pipeline with -the resolved steps before creating compiler snapshots: +the resolved steps retained by ResolvedPipelineDefinition: CORRECT: # Steps are registered in ObjectState with parent hierarchy: step → orchestrator → global @@ -15,8 +15,7 @@ ObjectStateRegistry.register(step_state) resolved_step = step_state.to_saved_resolved_object() - snapshot = StepSnapshot(index=0, scope_id=step_state.scope_id, step=resolved_step) - var_comps = snapshot.step.processing_config.variable_components + var_comps = resolved_step.processing_config.variable_components ✅ CORRECT (LIVE VALUES FOR UI): # For UI: use get_resolved_value() to get current values with unsaved edits @@ -34,7 +33,7 @@ WHY: - ObjectState.to_saved_resolved_object() provides the resolved compiler step -- StepSnapshot carries only compiler index, scope identity, and that resolved step +- ResolvedPipelineDefinition owns ordered resolved steps and their registered ObjectStates - get_resolved_value() provides live state with unsaved edits (for UI) - parameters.get() returns raw local value only, NO inheritance - No cross-step pollution - each step only sees its own config hierarchy @@ -113,10 +112,6 @@ PathPlannerExecutionGroups, PipelinePathPlanner, ) -from openhcs.core.pipeline.step_snapshot import ( - StepSnapshot, - build_step_snapshots, -) from openhcs.core.source_matching import SourceImageSetIdentityPolicy from openhcs.core.source_workspace_projection import ( VirtualWorkspaceSourceProjectionAuthority, @@ -234,11 +229,9 @@ class PipelineCompiler: @staticmethod def initialize_step_plans_for_context( context: ProcessingContext, - steps_definition: List[AbstractStep], + pipeline: ResolvedPipelineDefinition, orchestrator, global_config: "GlobalPipelineConfig", - step_state_map: Mapping[int, "ObjectState"], - step_snapshots: tuple[StepSnapshot, ...], metadata_writer: bool = False, plate_path: Optional[Path] = None, path_resolver: CompilationPathResolver | None = None, @@ -252,13 +245,11 @@ def initialize_step_plans_for_context( Args: context: ProcessingContext to initialize step plans for - steps_definition: List of AbstractStep objects defining the pipeline + pipeline: Ordered resolved steps and their registered ObjectStates orchestrator: Orchestrator instance for well filter resolution metadata_writer: If True, this well is responsible for creating OpenHCS metadata files plate_path: Path to plate root for zarr conversion detection global_config: Resolved global compiler configuration - step_state_map: ObjectState mapping from pipeline-level resolution - step_snapshots: Snapshots of the same resolved pipeline steps Returns: The axis-scoped compilation session used by all downstream stages. @@ -268,11 +259,9 @@ def initialize_step_plans_for_context( logger.debug("Using pipeline-resolved steps for context %s", context.axis_id) session = CompilationSession.from_context( context=context, - steps=steps_definition, + pipeline=pipeline, orchestrator=orchestrator, global_config=global_config, - step_state_map=step_state_map, - snapshots=step_snapshots, source_workspace_projection=( orchestrator.source_workspace_projection().filtered_by_axis( axis_id=context.axis_id, @@ -287,7 +276,7 @@ def initialize_step_plans_for_context( PipelineCompiler._ensure_initial_step_plans(session) PipelineCompiler._configure_input_conversion_if_needed( context, - session.steps, + session.pipeline.steps, orchestrator, plate_path, ) @@ -311,7 +300,7 @@ def _register_object_state( scope_id: str, parent_state: Optional["ObjectState"], ) -> "ObjectState": - """Create and register an ObjectState with the compiler's snapshot policy.""" + """Create and register an ObjectState with the compiler's step policy.""" state = ObjectState( object_instance=object_instance, scope_id=scope_id, @@ -351,12 +340,12 @@ def _missing_plan_fields( def _ensure_initial_step_plans( session: CompilationSession, ) -> None: - for step_index, snapshot in enumerate(session.snapshots): + for step_index, step in enumerate(session.pipeline.steps): if step_index not in session.plans: session.plans[step_index] = CompiledStepPlan( step_index=step_index, - step_name=snapshot.step.name, - step_type=type(snapshot.step).__name__, + step_name=step.name, + step_type=type(step).__name__, axis_id=session.axis_id, ) @@ -421,17 +410,17 @@ def _plan_context_paths( @staticmethod def _supplement_step_plans(session: CompilationSession) -> None: - for step_index, snapshot in enumerate(session.snapshots): + for step_index, step in enumerate(session.pipeline.steps): if step_index not in session.plans: logger.error( "Critical error: Step %s (index: %s) not found in step_plans after path planning phase.", - snapshot.step.name, + step.name, step_index, ) session.plans[step_index] = CompiledStepPlan( step_index=step_index, - step_name=snapshot.step.name, - step_type=type(snapshot.step).__name__, + step_name=step.name, + step_type=type(step).__name__, axis_id=session.axis_id, error="Missing from path planning phase by PipelinePathPlanner", create_openhcs_metadata=session.metadata_writer, @@ -439,29 +428,29 @@ def _supplement_step_plans(session: CompilationSession) -> None: continue current_plan = session.plans[step_index] - current_plan.step_scope_id = snapshot.scope_id - current_plan.step_name = snapshot.step.name - current_plan.step_type = type(snapshot.step).__name__ + current_plan.step_scope_id = session.pipeline.step_state_map[ + step_index + ].scope_id + current_plan.step_name = step.name + current_plan.step_type = type(step).__name__ current_plan.axis_id = session.axis_id current_plan.create_openhcs_metadata = session.metadata_writer - current_plan.variable_components = ( - snapshot.step.processing_config.variable_components - ) + current_plan.variable_components = step.processing_config.variable_components current_plan.group_by = PathPlannerExecutionGroups.normalized_group_by( - snapshot, + step, ) - declared_group_by = snapshot.step.processing_config.group_by + declared_group_by = step.processing_config.group_by if current_plan.group_by is not declared_group_by: logger.warning( "Step %r uses a non-routed function pattern with group_by=%s " "also present in variable_components; compiled group_by is " "GroupBy.NONE because variable_components owns the runtime " "stack axis.", - snapshot.step.name, + step.name, declared_group_by.name, ) - current_plan.input_source = snapshot.step.processing_config.input_source - current_plan.sequential_processing = snapshot.step.processing_config + current_plan.input_source = step.processing_config.input_source + current_plan.sequential_processing = step.processing_config current_plan.sequential_filter_plan = ( PipelineCompiler._compile_sequential_runtime_filter_plan(session) ) @@ -513,13 +502,13 @@ def _collect_streaming_configs( session: CompilationSession, ) -> None: streaming_config_types = tuple(StreamingConfig.__registry__.values()) - for step_index, snapshot in enumerate(session.snapshots): + for step_index, step in enumerate(session.pipeline.steps): step_plan = session.plans[step_index] for config_type in streaming_config_types: PipelineCompiler._collect_streaming_config( session, step_index, - snapshot, + step, step_plan, config_type, ) @@ -528,19 +517,19 @@ def _collect_streaming_configs( def _collect_streaming_config( session: CompilationSession, step_index: int, - snapshot: StepSnapshot, + step: AbstractStep, step_plan: CompiledStepPlan, config_type: type[StreamingConfig], ) -> None: resolved_config_type = get_base_type_for_lazy(config_type) or config_type configs = tuple( value - for value in vars(snapshot.step).values() + for value in vars(step).values() if isinstance(value, resolved_config_type) ) if not configs: return - defaults_enabled = snapshot.step.streaming_defaults.enabled + defaults_enabled = step.streaming_defaults.enabled for config_obj in configs: per_stream_enabled = config_obj.enabled enabled = True if defaults_enabled is True else per_stream_enabled @@ -606,12 +595,12 @@ def declare_zarr_stores(session: CompilationSession) -> None: # Access config from merged config (pipeline + global) for proper inheritance vfs_config = orchestrator.get_effective_config().vfs_config - for step_index, snapshot in enumerate(session.snapshots): + for step_index, step in enumerate(session.pipeline.steps): step_plan = session.plan(step_index) will_use_zarr = ( vfs_config.materialization_backend == MaterializationBackend.ZARR - and step_index == len(session.steps) - 1 + and step_index == len(session.pipeline.steps) - 1 ) if will_use_zarr: @@ -620,7 +609,7 @@ def declare_zarr_stores(session: CompilationSession) -> None: "needs_initialization": True, } logger.debug( - f"Step '{snapshot.step.name}' will use zarr backend for axis {context.axis_id}" + f"Step '{step.name}' will use zarr backend for axis {context.axis_id}" ) else: step_plan.zarr_config = None @@ -647,7 +636,7 @@ def plan_materialization_flags(session: CompilationSession) -> None: # CRITICAL: Pass merged config (not raw pipeline_config) for proper global config inheritance MaterializationFlagPlanner.prepare_pipeline_flags( context, - session.steps, + session.pipeline.steps, session.orchestrator.plate_path, session.global_config, # Use merged config instead of raw pipeline_config available_axis_values=session.orchestrator.get_component_keys( @@ -656,11 +645,11 @@ def plan_materialization_flags(session: CompilationSession) -> None: ) # Post-check (optional, but good for ensuring contracts are met by the planner) - for step_index, snapshot in enumerate(session.snapshots): + for step_index, step in enumerate(session.pipeline.steps): if step_index not in context.step_plans: # This should not happen if prepare_pipeline_flags guarantees plans for all steps logger.error( - f"Step {snapshot.step.name} (index: {step_index}) missing from step_plans after materialization planning." + f"Step {step.name} (index: {step_index}) missing from step_plans after materialization planning." ) continue @@ -672,7 +661,7 @@ def plan_materialization_flags(session: CompilationSession) -> None: ) if missing_keys: logger.error( - f"Materialization flag planning incomplete for step {snapshot.step.name} (index: {step_index}). " + f"Materialization flag planning incomplete for step {step.name} (index: {step_index}). " f"Missing required keys: {missing_keys}." ) PipelineCompiler._compile_runtime_artifact_materialization_plans(session) @@ -714,14 +703,14 @@ def _compile_runtime_artifact_materialization_plans( @staticmethod def validate_sequential_components_compatibility( - step_snapshots: Sequence[StepSnapshot], + steps: Sequence[AbstractStep], sequential_components: List, ) -> None: """ Validate that no step's variable_components overlap with pipeline's sequential_components. Args: - step_snapshots: ObjectState-resolved compiler snapshots + steps: ObjectState-resolved pipeline steps sequential_components: List of SequentialComponents from pipeline config Raises: @@ -732,10 +721,10 @@ def validate_sequential_components_compatibility( seq_comp_values = {sc.value for sc in sequential_components} - for snapshot in step_snapshots: - if not isinstance(snapshot.step, FunctionStep): + for step in steps: + if not isinstance(step, FunctionStep): continue - var_comps = snapshot.step.processing_config.variable_components + var_comps = step.processing_config.variable_components if not var_comps: continue var_comp_values = {vc.value for vc in var_comps} @@ -743,7 +732,7 @@ def validate_sequential_components_compatibility( if overlap: raise ValueError( - f"Step '{snapshot.step.name}' has variable_components {sorted(overlap)} that conflict with " + f"Step '{step.name}' has variable_components {sorted(overlap)} that conflict with " f"pipeline's sequential_components {sorted(seq_comp_values)}. " f"A component cannot be both sequential (pipeline-level) and variable (step-level). " f"Either remove {sorted(overlap)} from step's variable_components or from " @@ -850,17 +839,17 @@ def validate_memory_contracts(session: CompilationSession) -> None: ) FuncStepContractValidator.validate_pipeline( - steps=session.steps, + steps=session.pipeline.steps, pipeline_context=context, # Pass context so validator can access step plans for memory type overrides orchestrator=session.orchestrator, # Pass orchestrator for dict pattern key validation ) - for step_index, step in enumerate(session.steps): + for step_index, step in enumerate(session.pipeline.steps): if not isinstance(step, FunctionStep): continue if step_index not in context.step_plans: raise AssertionError( - f"Memory validation requires a compiled plan for FunctionStep {session.snapshot(step_index).step.name} (index: {step_index})." + f"Memory validation requires a compiled plan for FunctionStep {session.pipeline.steps[step_index].name} (index: {step_index})." ) step_plan = context.step_plans[step_index] compiled_pattern = step_plan.compiled_function_pattern @@ -879,7 +868,7 @@ def validate_memory_contracts(session: CompilationSession) -> None: ) # Apply memory type override: Any step with disk output must use numpy for disk writing - for step_index, step in enumerate(session.steps): + for step_index, step in enumerate(session.pipeline.steps): if isinstance(step, FunctionStep): if step_index in context.step_plans: step_plan = context.step_plans[step_index] @@ -894,7 +883,7 @@ def validate_memory_contracts(session: CompilationSession) -> None: if write_backend == "disk": logger.debug( - f"Step {session.snapshot(step_index).step.name} has disk output, overriding output_memory_type to numpy" + f"Step {session.pipeline.steps[step_index].name} has disk output, overriding output_memory_type to numpy" ) step_plan.output_memory_type = "numpy" @@ -956,7 +945,7 @@ def resolve_lazy_dataclasses(session: CompilationSession) -> None: # Configs are already resolved via ObjectState.to_object() in initialize_step_plans_for_context # No additional resolution needed - step plans already contain resolved configs logger.debug( - f"Step plans already resolved via ObjectState for {len(session.steps)} steps" + f"Step plans already resolved via ObjectState for {len(session.pipeline.steps)} steps" ) @staticmethod @@ -1106,7 +1095,7 @@ def _register_and_resolve_pipeline_once( len(pipeline_definition), ) - step_state_map, snapshots = PipelineCompiler._filter_enabled_steps( + pipeline = PipelineCompiler._filter_enabled_steps( pipeline_definition, step_state_map, ) @@ -1118,11 +1107,7 @@ def _register_and_resolve_pipeline_once( return ( compiler_scope_id, pipeline_config_state, - ResolvedPipelineDefinition( - steps=pipeline_definition, - step_state_map=step_state_map, - snapshots=snapshots, - ), + pipeline, ) @staticmethod @@ -1206,23 +1191,20 @@ def _replace_pipeline_with_resolved_steps( def _filter_enabled_steps( pipeline_definition: List[AbstractStep], step_state_map: Mapping[int, "ObjectState"], - ) -> tuple[Dict[int, "ObjectState"], tuple[StepSnapshot, ...]]: - snapshots = build_step_snapshots(pipeline_definition, step_state_map) + ) -> ResolvedPipelineDefinition: + pipeline = ResolvedPipelineDefinition(pipeline_definition, step_state_map) enabled_pairs = [ - (step, step_state_map[snapshot.index]) - for snapshot, step in zip(snapshots, pipeline_definition) + (step, pipeline.step_state_map[index]) + for index, step in enumerate(pipeline.steps) if step.enabled ] pipeline_definition.clear() pipeline_definition.extend(step for step, _state in enabled_pairs) - enabled_state_map = { - new_index: state for new_index, (_step, state) in enumerate(enabled_pairs) - } - if not pipeline_definition: - return enabled_state_map, () - return ( - enabled_state_map, - build_step_snapshots(pipeline_definition, enabled_state_map), + return ResolvedPipelineDefinition( + steps=pipeline_definition, + step_state_map={ + index: state for index, (_step, state) in enumerate(enabled_pairs) + }, ) @staticmethod @@ -1240,13 +1222,11 @@ def _capture_pipeline_config( @staticmethod def _resolve_global_step_axis_filters( orchestrator, - step_snapshots: tuple[StepSnapshot, ...], - step_state_map: Mapping[int, "ObjectState"], + pipeline: ResolvedPipelineDefinition, ) -> StepAxisFilterMap: temp_context = orchestrator.create_context("temp") _resolve_step_axis_filters( - step_snapshots, - step_state_map, + pipeline, temp_context, orchestrator, ) @@ -1319,11 +1299,9 @@ def build_initialize_axis_session( ) -> CompilationSession: return PipelineCompiler.initialize_step_plans_for_context( context, - list(request.pipeline.steps), + request.pipeline, request.orchestrator, request.global_config, - dict(request.pipeline.step_state_map), - request.pipeline.snapshots, metadata_writer=metadata_writer, plate_path=request.orchestrator.plate_path, path_resolver=request.path_resolver, @@ -1708,7 +1686,7 @@ def _validate_sequential_components_for_session( seq_config = session.global_config.sequential_processing_config if seq_config and seq_config.sequential_components: PipelineCompiler.validate_sequential_components_compatibility( - session.snapshots, + session.pipeline.steps, seq_config.sequential_components, ) @@ -1918,21 +1896,14 @@ def compile_pipelines( pipeline_inputs = replace( pipeline_inputs, steps=resolved_steps, - snapshots=build_step_snapshots( - resolved_steps, - pipeline_inputs.step_state_map, - ), ) PipelineCompiler.validate_backend_compatibility( orchestrator, effective_config.vfs_config, ) - global_step_axis_filters = ( - PipelineCompiler._resolve_global_step_axis_filters( - orchestrator, - pipeline_inputs.snapshots, - pipeline_inputs.step_state_map, - ) + global_step_axis_filters = PipelineCompiler._resolve_global_step_axis_filters( + orchestrator, + pipeline_inputs, ) axis_request = AxisCompilationRequest( orchestrator=orchestrator, @@ -2017,8 +1988,7 @@ def _write_compilation_debug_bundle_if_configured( def _resolve_step_axis_filters( - step_snapshots: tuple[StepSnapshot, ...], - step_state_map: Mapping[int, "ObjectState"], + pipeline: ResolvedPipelineDefinition, context, orchestrator, ): @@ -2030,8 +2000,7 @@ def _resolve_step_axis_filters( It processes ALL WellFilterConfig instances (materialization, streaming, etc.) uniformly. Args: - step_snapshots: ObjectState-resolved compiler snapshots - step_state_map: ObjectState owners retaining inherited-field provenance + pipeline: Resolved steps and ObjectState owners retaining inherited-field provenance context: Processing context for the current axis value orchestrator: Orchestrator instance with access to available axis values """ @@ -2043,10 +2012,10 @@ def _resolve_step_axis_filters( logger.warning("No available axis values found for axis filter resolution") return - for snapshot in step_snapshots: + for step_index, step in enumerate(pipeline.steps): step_filters: dict[type[WellFilterConfig], StepAxisFilterResolution] = {} - step_state = step_state_map[snapshot.index] - for field_name, config in vars(snapshot.step).items(): + step_state = pipeline.step_state_map[step_index] + for field_name, config in vars(step).items(): if not isinstance(config, WellFilterConfig) or config.well_filter is None: continue provenance = step_state.get_provenance(f"{field_name}.well_filter") @@ -2073,7 +2042,7 @@ def _resolve_step_axis_filters( ) if step_filters: - context.step_axis_filters[snapshot.index] = StepAxisFilterSet(step_filters) + context.step_axis_filters[step_index] = StepAxisFilterSet(step_filters) total_filters = sum(len(filters) for filters in context.step_axis_filters.values()) logger.debug( diff --git a/openhcs/core/pipeline/path_planner.py b/openhcs/core/pipeline/path_planner.py index ad730c60e5..ed20b81ec1 100644 --- a/openhcs/core/pipeline/path_planner.py +++ b/openhcs/core/pipeline/path_planner.py @@ -71,9 +71,6 @@ CompilationPlateScope, CompilationSession, ) -from openhcs.core.pipeline.step_snapshot import ( - StepSnapshot, -) from openhcs.core.source_bindings import ( CompiledSourceBindingPlan, CompiledSourceUniversePlan, @@ -85,6 +82,7 @@ StepInputDependency, StepInputDependencyKind, ) +from openhcs.core.steps.abstract import AbstractStep from openhcs.core.steps.function_step import FunctionStep logger = logging.getLogger(__name__) @@ -215,11 +213,11 @@ def scope_for_group_by( def output_after( self, - snapshot: StepSnapshot, + step: AbstractStep, execution_scope: PathPlannerGroupScope, compiled_pattern: CompiledFunctionPattern | None, ) -> "PathPlannerComponentScopes": - if not isinstance(snapshot.step, FunctionStep): + if not isinstance(step, FunctionStep): return self if compiled_pattern is None: raise TypeError( @@ -230,15 +228,13 @@ def output_after( return self scopes = dict(self.scopes) - variable_components = tuple( - snapshot.step.processing_config.variable_components or () - ) + variable_components = tuple(step.processing_config.variable_components or ()) for component in variable_components: scopes.pop(component, None) if compiled_pattern.collapses_input_plane_axis(): scopes[component] = PathPlannerGroupScope.ungrouped() - group_by = PathPlannerExecutionGroups.normalized_group_by(snapshot) + group_by = PathPlannerExecutionGroups.normalized_group_by(step) group_by_component = self.component_from_group_by(group_by) if ( group_by_component is not None @@ -296,24 +292,24 @@ def context_producer_scope( def get_execution_groups( self, - snapshot: StepSnapshot, + step: AbstractStep, input_component_scopes: PathPlannerComponentScopes | None = None, *, source_bindings: StepSourceBindingsConfig | None = None, contracts: Sequence[CallableContract] = (), ) -> PathPlannerGroupScope: """Determine which component groups this step will execute for.""" - if not isinstance(snapshot.step, FunctionStep): + if not isinstance(step, FunctionStep): return PathPlannerGroupScope.ungrouped() - func_pattern = snapshot.step.func - group_by = self.normalized_group_by(snapshot) + func_pattern = step.func + group_by = self.normalized_group_by(step) if isinstance(func_pattern, dict): scope = PathPlannerGroupScope.from_raw( func_pattern.keys(), component=self.execution_component_for_dict_pattern( group_by, - snapshot.step.name, + step.name, ), ) logger.debug("Dict function pattern groups: %s", scope.keys) @@ -329,46 +325,42 @@ def get_execution_groups( ) if artifact_owned: source_bindings = ( - self.planner.source_bindings_for_snapshot(snapshot) - if source_bindings is None - else source_bindings + step.source_bindings if source_bindings is None else source_bindings ).for_artifact_refs( - spec.ref() - for contract in contracts - for spec in contract.group_scope_inputs + spec.ref() for contract in contracts for spec in contract.group_scope_inputs ) scope = component_scopes.scope_for_group_by( group_by, ) if scope is None: source_scope = self.source_binding_scope_for_group_by( - snapshot, + step, group_by, source_bindings=source_bindings, ) scope = ( - self.dynamic_execution_scope_for_group_by(snapshot, group_by) + self.dynamic_execution_scope_for_group_by(step, group_by) if source_scope.is_ungrouped else source_scope ) if artifact_owned: scope = self.artifact_owned_execution_scope( - snapshot, + step, contracts, consumer_scope=scope, ) logger.debug( "Artifact-managed FunctionStep groups for %s: %s", - snapshot.step.name, + step.name, scope.keys, ) return scope - logger.debug("FunctionStep groups for %s: %s", snapshot.step.name, scope.keys) + logger.debug("FunctionStep groups for %s: %s", step.name, scope.keys) return scope def artifact_owned_execution_scope( self, - snapshot: StepSnapshot, + step: AbstractStep, contracts: Sequence[CallableContract], *, consumer_scope: PathPlannerGroupScope, @@ -382,13 +374,13 @@ def artifact_owned_execution_scope( ) if not owner_specs: raise ValueError( - f"Artifact-owned FunctionStep {snapshot.step.name!r} declares no " + f"Artifact-owned FunctionStep {step.name!r} declares no " "artifact group-scope owner." ) available_artifacts = self.planner.artifact_context.available_artifacts - contract_source_bindings = self.planner.source_bindings_for_snapshot(snapshot) - group_by = self.normalized_group_by(snapshot) + contract_source_bindings = step.source_bindings + group_by = self.normalized_group_by(step) group_component = PathPlannerComponentScopes.component_from_group_by(group_by) normalized_group_component = ( None @@ -421,8 +413,7 @@ def artifact_owned_execution_scope( continue has_source_lineage = ( - contract_source_bindings.binding_for_artifact_ref(spec.ref()) - is not None + contract_source_bindings.binding_for_artifact_ref(spec.ref()) is not None or available_artifacts.by_name_and_artifact_type( spec.name, spec.artifact_type, @@ -436,7 +427,7 @@ def artifact_owned_execution_scope( continue if group_by is not GroupBy.NONE: raise ValueError( - f"Artifact-owned FunctionStep {snapshot.step.name!r} cannot " + f"Artifact-owned FunctionStep {step.name!r} cannot " f"resolve group scope for {spec.ref()!r}." ) scopes.append(PathPlannerGroupScope.ungrouped()) @@ -464,7 +455,7 @@ def artifact_owned_execution_scope( ) if source_scope.is_ungrouped and group_by is not GroupBy.NONE: raise ValueError( - f"Artifact-owned FunctionStep {snapshot.step.name!r} cannot " + f"Artifact-owned FunctionStep {step.name!r} cannot " f"resolve group scope for {spec.ref()!r}." ) scopes.append(source_scope) @@ -473,7 +464,7 @@ def artifact_owned_execution_scope( return consumer_scope consumer_variable_components = ComponentSet.from_enum_values( - snapshot.step.processing_config.variable_components or () + step.processing_config.variable_components or () ) projected_scopes = tuple( scope.output_lineage_scope( @@ -485,26 +476,23 @@ def artifact_owned_execution_scope( execution_scope = PathPlannerGroupScope.union_compatible(projected_scopes) if execution_scope is None: raise ValueError( - f"Artifact-owned FunctionStep {snapshot.step.name!r} has " + f"Artifact-owned FunctionStep {step.name!r} has " f"incompatible declared owner scopes {projected_scopes!r}." ) return execution_scope def dynamic_execution_scope_for_group_by( self, - snapshot: StepSnapshot, + step: AbstractStep, group_by: GroupBy | None, ) -> PathPlannerGroupScope: """Return a typed runtime-discovered scope for a concrete group axis.""" - group_by_component = PathPlannerComponentScopes.component_from_group_by( - group_by - ) + group_by_component = PathPlannerComponentScopes.component_from_group_by(group_by) if group_by_component is None: return PathPlannerGroupScope.ungrouped() source_keys = ( tuple(self.planner.orchestrator.get_component_keys(group_by_component)) - if snapshot.step.processing_config.input_source - is InputSource.PIPELINE_START + if step.processing_config.input_source is InputSource.PIPELINE_START else () ) if source_keys: @@ -518,20 +506,18 @@ def dynamic_execution_scope_for_group_by( def source_binding_scope_for_group_by( self, - snapshot: StepSnapshot, + step: AbstractStep, group_by: GroupBy | None, *, source_bindings: StepSourceBindingsConfig | None = None, ) -> PathPlannerGroupScope: """Derive execution groups declared by source-binding component identity.""" - group_by_component = PathPlannerComponentScopes.component_from_group_by( - group_by - ) + group_by_component = PathPlannerComponentScopes.component_from_group_by(group_by) if group_by_component is None: return PathPlannerGroupScope.ungrouped() if source_bindings is None: - source_bindings = self.planner.source_bindings_for_snapshot(snapshot) + source_bindings = step.source_bindings if not source_bindings.binding_declarations: return PathPlannerGroupScope.ungrouped() component = ComponentSet.coerce_component(group_by_component) @@ -559,17 +545,17 @@ def execution_component_for_dict_pattern( return AllComponents.from_value(group_by.value) @staticmethod - def normalized_group_by(snapshot: StepSnapshot) -> GroupBy: + def normalized_group_by(step: AbstractStep) -> GroupBy: """Use the same group_by normalization as compiled execution plans.""" from openhcs.core.pipeline.funcstep_contract_validator import ( FuncStepContractValidator, ) return FuncStepContractValidator.normalized_group_by( - snapshot.step.processing_config.group_by, - snapshot.step.processing_config.variable_components, - snapshot.step.name, - normalize_function_pattern(snapshot.step.func), + step.processing_config.group_by, + step.processing_config.variable_components, + step.name, + normalize_function_pattern(step.func), ) @@ -610,7 +596,8 @@ class PathPlannerArtifactStage(PathPlannerMetadataArtifactInjection): def prepare_step_declarations( self, - snapshot: StepSnapshot, + step: AbstractStep, + step_index: int, ) -> tuple[ ArtifactGraph, FunctionPatternSyntax | None, @@ -618,7 +605,7 @@ def prepare_step_declarations( tuple[CallableContract, ...], ]: """Normalize a step's function pattern and collect artifact declarations.""" - if not isinstance(snapshot.step, FunctionStep): + if not isinstance(step, FunctionStep): return ( ArtifactGraph.empty(), None, @@ -626,10 +613,11 @@ def prepare_step_declarations( (), ) - func_pattern = strip_disabled_functions(snapshot.step.func) - source_bindings = self.planner.source_bindings_for_snapshot(snapshot) + func_pattern = strip_disabled_functions(step.func) + source_bindings = step.source_bindings declaration_context = self.artifact_declaration_context( - snapshot, + step, + step_index, source_bindings=source_bindings, ) contracts = resolve_function_pattern_contracts( @@ -653,25 +641,22 @@ def prepare_step_declarations( def source_bindings_for_contracts( self, - snapshot: StepSnapshot, + step: AbstractStep, contracts: Iterable[CallableContract], main_input_dependency: StepInputDependency, ) -> StepSourceBindingsConfig: """Project bindings to implicit main flow and exact public source inputs.""" contracts = tuple(contracts) - source_bindings = self.planner.source_bindings_for_snapshot(snapshot) + source_bindings = step.source_bindings available_artifacts = self.planner.artifact_context.available_artifacts implicit_main_flow_specs = ( tuple( - binding.input_spec() - for binding in source_bindings.primary_plane_bindings + binding.input_spec() for binding in source_bindings.primary_plane_bindings ) if ( main_input_dependency.kind is StepInputDependencyKind.PIPELINE_START - and any( - contract.accepts_implicit_main_flow_input for contract in contracts - ) + and any(contract.accepts_implicit_main_flow_input for contract in contracts) ) else () ) @@ -684,8 +669,7 @@ def source_bindings_for_contracts( for contract in contracts for spec in contract.artifact_inputs if ( - source_bindings.binding_for_artifact_ref(spec.ref()) - is not None + source_bindings.binding_for_artifact_ref(spec.ref()) is not None or available_artifacts.by_name_and_artifact_type( spec.name, spec.artifact_type, @@ -737,7 +721,7 @@ def source_binding_component_domains( def compile_source_plans( self, - snapshot: StepSnapshot, + step: AbstractStep, source_bindings: StepSourceBindingsConfig, ) -> tuple[CompiledSourceBindingPlan, CompiledSourceUniversePlan]: """Freeze invocation-scoped source declarations into runtime plans.""" @@ -747,9 +731,7 @@ def compile_source_plans( else: binding_plan = CompiledSourceBindingPlan.from_config( source_bindings, - realized_source_metadata=( - self.planner.session.realized_source_metadata - ), + realized_source_metadata=(self.planner.session.realized_source_metadata), ) return ( binding_plan, @@ -796,7 +778,7 @@ def namespace_grouped_outputs_for_runtime_consumers( def compile_plan_maps( self, - snapshot: StepSnapshot, + step: AbstractStep, step_index: int, declarations: ArtifactGraph, group_scope: PathPlannerGroupScope, @@ -804,14 +786,14 @@ def compile_plan_maps( source_bindings: StepSourceBindingsConfig = EMPTY_SOURCE_BINDINGS, ) -> ArtifactPlanMaps: """Compile artifact declarations into runtime I/O maps.""" - step_name = snapshot.step.name - group_by = PathPlannerExecutionGroups.normalized_group_by(snapshot) + step_name = step.name + group_by = PathPlannerExecutionGroups.normalized_group_by(step) source_binding_plan, source_universe_plan = self.compile_source_plans( - snapshot, + step, source_bindings, ) consumer_variable_components = ComponentSet.from_enum_values( - snapshot.step.processing_config.variable_components or () + step.processing_config.variable_components or () ) artifact_inputs = self.process_artifact_inputs( declarations, @@ -1159,7 +1141,8 @@ def relation_source_scopes_by_ref( def build_step_compiled_function_pattern( self, - snapshot: StepSnapshot, + step: AbstractStep, + step_index: int, is_function_step: bool, func_pattern: FunctionPatternSyntax | None, artifact_inputs: Mapping[ArtifactSpecRef, ArtifactInputPlan], @@ -1174,7 +1157,7 @@ def build_step_compiled_function_pattern( if not is_function_step or not func_pattern: return None - step_context = self.artifact_declaration_context(snapshot) + step_context = self.artifact_declaration_context(step, step_index) contracts = resolve_function_pattern_contracts( func_pattern, self.planner.invocation_contract_provider, @@ -1186,11 +1169,11 @@ def build_step_compiled_function_pattern( prior = config_parameters.setdefault(parameter.name, parameter) if prior.annotation is not parameter.annotation: raise TypeError( - f"FunctionStep {snapshot.step.name!r} callable pattern " + f"FunctionStep {step.name!r} callable pattern " f"declares incompatible config parameter {parameter.name!r}: " f"{prior.annotation!r} and {parameter.annotation!r}." ) - step_values = vars(snapshot.step) + step_values = vars(step) pipeline_values = vars(self.planner.session.global_config) runtime_parameter_bindings: list[RuntimeParameterBinding] = [] for parameter_name, parameter in config_parameters.items(): @@ -1204,7 +1187,7 @@ def build_step_compiled_function_pattern( ) if not isinstance(provider, parameter_type): raise TypeError( - f"FunctionStep {snapshot.step.name!r} config parameter " + f"FunctionStep {step.name!r} config parameter " f"{parameter_name!r} requires {parameter_type.__name__}, got " f"{type(provider).__name__}." ) @@ -1234,7 +1217,7 @@ def build_step_compiled_function_pattern( relation_source_scopes=relation_source_scopes, execution_group_scope=execution_group_scope, consumer_variable_components=ComponentSet.from_enum_values( - snapshot.step.processing_config.variable_components or () + step.processing_config.variable_components or () ), source_bindings=step_context.source_bindings, available_artifacts=available_artifacts, @@ -1536,23 +1519,24 @@ def exact_component_scopes( def artifact_declaration_context( self, - snapshot: StepSnapshot, + step: AbstractStep, + step_index: int, *, source_bindings: StepSourceBindingsConfig | None = None, ) -> ArtifactDeclarationStepContext: """Return compile-time context for invocation artifact providers.""" if source_bindings is None: - source_bindings = self.planner.source_bindings_for_snapshot(snapshot) + source_bindings = step.source_bindings return replace( self.planner.artifact_context, - step_name=snapshot.step.name, - step_index=snapshot.index, + step_name=step.name, + step_index=step_index, ).with_source_binding_scope( source_bindings=source_bindings, - group_by=PathPlannerExecutionGroups.normalized_group_by(snapshot), - input_source=snapshot.step.processing_config.input_source, + group_by=PathPlannerExecutionGroups.normalized_group_by(step), + input_source=step.processing_config.input_source, source_groups=self.planner.execution_groups.dynamic_execution_scope_for_group_by( - snapshot, PathPlannerExecutionGroups.normalized_group_by(snapshot) + step, PathPlannerExecutionGroups.normalized_group_by(step) ).keys, ) @@ -2018,23 +2002,22 @@ class PathPlannerMaterializationStage: def materialized_output_dir_for_step( self, - snapshot: StepSnapshot, + step: AbstractStep, + step_index: int, ) -> Optional[Path]: """Resolve optional per-step materialization output directory.""" - materialization_config = snapshot.step.step_materialization_config + materialization_config = step.step_materialization_config if not materialization_config or not materialization_config.enabled: return None step_axis_filters = self.planner.ctx.step_axis_filters.get( - snapshot.index, + step_index, StepAxisFilterSet.empty(), ) - if not step_axis_filters.allows( - materialization_config, self.planner.ctx.axis_id - ): + if not step_axis_filters.allows(materialization_config, self.planner.ctx.axis_id): logger.debug( "Skipping materialization for step %s, axis %s (filtered out)", - snapshot.step.name, + step.name, self.planner.ctx.axis_id, ) return None @@ -2064,7 +2047,7 @@ def input_conversion_plan_for_step( def apply_materialization_plan( self, - snapshot: StepSnapshot, + step: AbstractStep, step_index: int, materialized_output_dir: Optional[Path], ) -> None: @@ -2072,7 +2055,7 @@ def apply_materialization_plan( if not materialized_output_dir: return - materialization_config = snapshot.step.step_materialization_config + materialization_config = step.step_materialization_config materialized_plate_root = self.planner.paths.build_output_plate_root( self.planner.plate_path, materialization_config, @@ -2110,7 +2093,7 @@ class PathPlannerValidationStage: def validate(self) -> None: """Validate connectivity and materialization paths.""" for i in range(1, self.planner.session.step_count): - curr = self.planner.session.snapshot(i) + curr = self.planner.session.pipeline.steps[i] dependency = self.planner.plans[i].main_input_dependency if dependency.kind in ( StepInputDependencyKind.NO_MAIN_FLOW, @@ -2118,13 +2101,11 @@ def validate(self) -> None: ): continue if dependency.kind is not StepInputDependencyKind.STEP_OUTPUT: - raise ValueError( - f"Step {curr.step.name} has unresolved main input dependency." - ) + raise ValueError(f"Step {curr.name} has unresolved main input dependency.") source_step_index = dependency.source_step_index if source_step_index is None: raise ValueError( - f"Step {curr.step.name} main input dependency is missing source_step_index." + f"Step {curr.name} main input dependency is missing source_step_index." ) curr_in = self.planner.plans[i].input_dir source_out = self.planner.plans[source_step_index].output_dir @@ -2135,10 +2116,10 @@ def validate(self) -> None: for inp in self.planner.plans[i].artifact_inputs.values() ) if not has_artifact_bridge: - producer_name = self.planner.session.snapshot( + producer_name = self.planner.session.pipeline.steps[ source_step_index - ).step.name - raise ValueError(f"Disconnect: {producer_name} -> {curr.step.name}") + ].name + raise ValueError(f"Disconnect: {producer_name} -> {curr.name}") self.validate_materialization_paths() @@ -2148,39 +2129,36 @@ def validate_materialization_paths(self) -> None: mat_steps = [ ( - snapshot, + step, self.planner.plans[i].pipeline_position or i, - self.planner.paths.build_output_path( - snapshot.step.step_materialization_config - ), + self.planner.paths.build_output_path(step.step_materialization_config), ) - for i, snapshot in self.planner.session.indexed_snapshots() - if snapshot.step.step_materialization_config - and snapshot.step.step_materialization_config.enabled + for i, step in enumerate(self.planner.session.pipeline.steps) + if step.step_materialization_config and step.step_materialization_config.enabled ] path_groups = defaultdict(list) - for snapshot, pos, path in mat_steps: + for step, pos, path in mat_steps: if path == global_path: - self.resolve_and_update_paths(snapshot, pos, path, "main flow") + self.resolve_and_update_paths(step, pos, path, "main flow") else: - path_groups[str(path)].append((snapshot, pos, path)) + path_groups[str(path)].append((step, pos, path)) for path_key, step_list in path_groups.items(): if len(step_list) > 1: - for snapshot, pos, path in step_list: - self.resolve_and_update_paths(snapshot, pos, path, f"pos {pos}") + for step, pos, path in step_list: + self.resolve_and_update_paths(step, pos, path, f"pos {pos}") def resolve_and_update_paths( self, - snapshot: StepSnapshot, + step: AbstractStep, position: int, original_path: Path, conflict_type: str, ) -> None: """Resolve path conflict by updating the compiled plan only.""" del original_path, conflict_type - materialization_config = snapshot.step.step_materialization_config + materialization_config = step.step_materialization_config original_sub_dir = materialization_config.sub_dir new_sub_dir = f"{original_sub_dir}_step{position}" @@ -2345,14 +2323,12 @@ def prime_future_artifact_inputs(self) -> None: set() for _ in range(self.planner.session.step_count) ] - for i in self.planner.session.reverse_snapshot_indices(): + for i in range(self.planner.session.step_count - 1, -1, -1): self.planner.future_artifact_inputs[i] = set(future_inputs) - snapshot = self.planner.session.snapshot(i) - if isinstance(snapshot.step, FunctionStep): - pattern = self.planner.artifacts.stripped_declaration_pattern( - snapshot.step.func - ) + step = self.planner.session.pipeline.steps[i] + if isinstance(step, FunctionStep): + pattern = self.planner.artifacts.stripped_declaration_pattern(step.func) declarations = extract_artifact_declarations( pattern, declaration_provider=self.planner.declaration_provider, @@ -2360,7 +2336,7 @@ def prime_future_artifact_inputs(self) -> None: self.planner.invocation_contract_provider ), step_context=self.planner.artifacts.artifact_declaration_context( - snapshot + step, i ), ) step_inputs = { @@ -2375,27 +2351,30 @@ def prime_future_artifact_inputs(self) -> None: future_inputs.update(step_inputs) - def plan_step(self, snapshot: StepSnapshot, step_index: int) -> None: + def plan_step(self, step: AbstractStep, step_index: int) -> None: """Plan one step's directories, artifacts, and executable pattern.""" - self.planner.plans[step_index].step_scope_id = snapshot.scope_id - self.planner.artifact_context = ( - self.planner.artifacts.artifact_declaration_context(snapshot) + self.planner.plans[step_index].step_scope_id = ( + self.planner.session.pipeline.step_state_map[step_index].scope_id + ) + self.planner.artifact_context = self.planner.artifacts.artifact_declaration_context( + step, step_index ) declarations, func_pattern, execution_scope, contracts = ( self.planner.artifacts.prepare_step_declarations( - snapshot, + step, + step_index, ) ) - resolved_source_bindings = self.planner.source_bindings_for_snapshot(snapshot) + resolved_source_bindings = step.source_bindings main_input_dependency = self.main_input_dependency( - snapshot, + step, step_index, declarations=declarations, execution_scope=execution_scope, source_bindings=resolved_source_bindings, ) contract_source_bindings = self.planner.artifacts.source_bindings_for_contracts( - snapshot, + step, contracts, main_input_dependency, ) @@ -2413,7 +2392,7 @@ def plan_step(self, snapshot: StepSnapshot, step_index: int) -> None: PathPlannerGroupScope.ungrouped() if execution_scope is FunctionStepExecutionScope.PLATE else self.planner.execution_groups.get_execution_groups( - snapshot, + step, input_component_scopes, source_bindings=execution_source_bindings, contracts=contracts, @@ -2427,7 +2406,7 @@ def plan_step(self, snapshot: StepSnapshot, step_index: int) -> None: ) ) artifact_maps = self.planner.artifacts.compile_plan_maps( - snapshot, + step, step_index, declarations, group_scope, @@ -2435,7 +2414,7 @@ def plan_step(self, snapshot: StepSnapshot, step_index: int) -> None: contract_source_bindings, ) - if isinstance(snapshot.step, FunctionStep): + if isinstance(step, FunctionStep): func_pattern = self.planner.artifacts.inject_metadata( func_pattern, declarations.inputs, @@ -2443,8 +2422,9 @@ def plan_step(self, snapshot: StepSnapshot, step_index: int) -> None: self.planner.plans[step_index].func = func_pattern compiled_pattern = self.planner.artifacts.build_step_compiled_function_pattern( - snapshot, - isinstance(snapshot.step, FunctionStep), + step, + step_index, + isinstance(step, FunctionStep), func_pattern, artifact_maps.inputs, artifact_maps.outputs, @@ -2459,7 +2439,7 @@ def plan_step(self, snapshot: StepSnapshot, step_index: int) -> None: ) ) self.update_core_step_plan( - snapshot, + step, step_index, main_input_dependency, input_dir, @@ -2468,9 +2448,9 @@ def plan_step(self, snapshot: StepSnapshot, step_index: int) -> None: compiled_pattern, ) self.planner.materialization.apply_materialization_plan( - snapshot, + step, step_index, - self.planner.materialization.materialized_output_dir_for_step(snapshot), + self.planner.materialization.materialized_output_dir_for_step(step, step_index), ) self.planner.materialization.apply_input_conversion_plan( step_index, @@ -2481,7 +2461,7 @@ def plan_step(self, snapshot: StepSnapshot, step_index: int) -> None: ) self.planner.main_flow_component_scopes[step_index] = ( input_component_scopes.output_after( - snapshot, + step, artifact_maps.group_scope, compiled_pattern, ) @@ -2504,7 +2484,7 @@ def input_component_scopes( def update_core_step_plan( self, - snapshot: StepSnapshot, + step: AbstractStep, step_index: int, main_input_dependency: StepInputDependency, input_dir: Path, @@ -2519,15 +2499,14 @@ def update_core_step_plan( is_per_step_materialization=False, ) step_plan = self.planner.plans[step_index] - step_plan.step_scope_id = snapshot.scope_id step_plan.input_dir = input_dir step_plan.output_dir = output_dir step_plan.output_plate_root = str(main_plate_root) step_plan.sub_dir = self.planner.cfg.sub_dir step_plan.analysis_results_dir = str(self.planner.paths.results_path()) step_plan.pipeline_position = step_index - step_plan.input_source = self.input_source(snapshot) - step_plan.group_by = PathPlannerExecutionGroups.normalized_group_by(snapshot) + step_plan.input_source = self.input_source(step) + step_plan.group_by = PathPlannerExecutionGroups.normalized_group_by(step) step_plan.main_input_dependency = main_input_dependency step_plan.artifact_inputs = artifact_maps.inputs step_plan.artifact_outputs = artifact_maps.outputs @@ -2538,7 +2517,7 @@ def update_core_step_plan( def main_input_dependency( self, - snapshot: StepSnapshot, + step: AbstractStep, step_index: int, *, declarations: ArtifactGraph = ArtifactGraph(), @@ -2547,22 +2526,18 @@ def main_input_dependency( ) -> StepInputDependency: """Resolve the explicit main-input edge for one step.""" existing_plan = self.planner.plans.get(step_index) - if ( - existing_plan is not None - and existing_plan.main_input_dependency.is_resolved - ): + if existing_plan is not None and existing_plan.main_input_dependency.is_resolved: return existing_plan.main_input_dependency if ( - isinstance(snapshot.step, FunctionStep) + isinstance(step, FunctionStep) and execution_scope is FunctionStepExecutionScope.PLATE ): return StepInputDependency.no_main_flow() if ( step_index == 0 - or snapshot.step.processing_config.input_source - == InputSource.PIPELINE_START + or step.processing_config.input_source == InputSource.PIPELINE_START ): return StepInputDependency.pipeline_start() @@ -2609,7 +2584,7 @@ def main_input_dependency( raise MissingArtifactInputError( step_id=step_index, artifact_key=producer_ref.name, - step_name=snapshot.step.name, + step_name=step.name, ) if len(candidate_indices) > 1: raise ValueError( @@ -2621,14 +2596,14 @@ def main_input_dependency( producer_step_indices = tuple(dict.fromkeys(producer_step_indices)) if len(producer_step_indices) > 1: raise ValueError( - f"Step {snapshot.step.name!r} declares main-flow inputs from multiple " + f"Step {step.name!r} declares main-flow inputs from multiple " f"producer steps {producer_step_indices!r}: {main_input_specs!r}." ) if producer_step_indices: producer_index = producer_step_indices[0] if not isinstance(producer_index, int): raise TypeError( - f"Main-flow artifact producer for step {snapshot.step.name!r} has " + f"Main-flow artifact producer for step {step.name!r} has " f"non-integer step identity {producer_index!r}." ) producer_scope_id = self.planner.plans[producer_index].step_scope_id @@ -2645,10 +2620,7 @@ def main_input_dependency( producer_index = step_index - 1 producer_plan = self.planner.plans[producer_index] compiled_pattern = producer_plan.compiled_function_pattern - if ( - compiled_pattern is not None - and compiled_pattern.preserves_input_main_flow() - ): + if compiled_pattern is not None and compiled_pattern.preserves_input_main_flow(): if not producer_plan.main_input_dependency.is_resolved: raise RuntimeError( f"Main-flow-preserving step {producer_index} has no resolved " @@ -2656,7 +2628,7 @@ def main_input_dependency( ) return producer_plan.main_input_dependency - producer_scope_id = self.planner.session.snapshot(producer_index).scope_id + producer_scope_id = self.planner.plans[producer_index].step_scope_id return StepInputDependency.step_output( source_step_index=producer_index, source_step_scope_id=producer_scope_id, @@ -2700,9 +2672,9 @@ def step_io_dirs( return input_dir, output_dir @staticmethod - def input_source(snapshot: StepSnapshot) -> str: + def input_source(step: AbstractStep) -> str: """Get input source string.""" - if snapshot.step.processing_config.input_source == InputSource.PIPELINE_START: + if step.processing_config.input_source == InputSource.PIPELINE_START: return "PIPELINE_START" return "PREVIOUS_STEP" @@ -2749,18 +2721,11 @@ def __init__( self.plate_scope = CompilationPlateScope.from_context(self.ctx) self.plate_path = self.plate_scope.path - def source_bindings_for_snapshot( - self, - snapshot: StepSnapshot, - ) -> StepSourceBindingsConfig: - """Return the ObjectState-resolved source declarations from the snapshot.""" - return snapshot.step.source_bindings - def plan(self) -> dict[int, CompiledStepPlan]: """Plan all paths with zero duplication.""" self.steps.prime_future_artifact_inputs() - for i, snapshot in self.session.indexed_snapshots(): - self.steps.plan_step(snapshot, i) + for i, step in enumerate(self.session.pipeline.steps): + self.steps.plan_step(step, i) self.validation.validate() diff --git a/openhcs/core/pipeline/step_snapshot.py b/openhcs/core/pipeline/step_snapshot.py deleted file mode 100644 index 621373625d..0000000000 --- a/openhcs/core/pipeline/step_snapshot.py +++ /dev/null @@ -1,44 +0,0 @@ -"""Compiler identity for already-resolved pipeline steps.""" - -from __future__ import annotations - -from dataclasses import dataclass -from typing import TYPE_CHECKING, Mapping, Sequence - -from openhcs.core.steps.abstract import AbstractStep - -if TYPE_CHECKING: - from objectstate import ObjectState - - -@dataclass(frozen=True, slots=True) -class StepSnapshot: - """Bind compiler identity to one already-resolved pipeline step. - - The normal compiler path has already converted ObjectState to a resolved step - object before this snapshot is built. ObjectState contributes only its scope - identity; all step semantics remain owned by the resolved step. - """ - - index: int - scope_id: str - step: AbstractStep - - -def build_step_snapshots( - steps: Sequence[AbstractStep], - step_state_map: Mapping[int, "ObjectState"], -) -> tuple[StepSnapshot, ...]: - """Build compiler snapshots for already-resolved steps.""" - snapshots: list[StepSnapshot] = [] - for index, step in enumerate(steps): - try: - step_state = step_state_map[index] - except KeyError as exc: - raise ValueError( - f"Missing ObjectState for resolved step {index} ({step.name})." - ) from exc - snapshots.append( - StepSnapshot(index=index, scope_id=step_state.scope_id, step=step) - ) - return tuple(snapshots) diff --git a/openhcs/interop/cellprofiler/compile_time_contracts.py b/openhcs/interop/cellprofiler/compile_time_contracts.py index f1f52a4912..10a76e3515 100644 --- a/openhcs/interop/cellprofiler/compile_time_contracts.py +++ b/openhcs/interop/cellprofiler/compile_time_contracts.py @@ -147,29 +147,25 @@ def provider_for_session( ] = {} forward_context = ArtifactDeclarationStepContext.empty() next_module_num = 1 - for snapshot in session.snapshots: - if ( - not snapshot.step.enabled - or not isinstance(snapshot.step, FunctionStep) - or snapshot.step.func is None - ): + for step_index, step in enumerate(session.pipeline.steps): + if not step.enabled or not isinstance(step, FunctionStep) or step.func is None: continue - source_bindings = snapshot.step.source_bindings + source_bindings = step.source_bindings effective_source_bindings = source_bindings.for_input_source( - snapshot.step.processing_config.input_source + step.processing_config.input_source ) forward_context = replace( forward_context, - step_name=snapshot.step.name, - step_index=snapshot.index, + step_name=step.name, + step_index=step_index, ).with_source_binding_scope( source_bindings=effective_source_bindings, - group_by=snapshot.step.processing_config.group_by, - input_source=snapshot.step.processing_config.input_source, + group_by=step.processing_config.group_by, + input_source=step.processing_config.input_source, ) step_context = forward_context - normalized_pattern = normalize_function_pattern(snapshot.step.func) + normalized_pattern = normalize_function_pattern(step.func) first_step_module_num = next_module_num step_invocations = [] group_contexts: list[ArtifactDeclarationStepContext] = [] @@ -210,7 +206,7 @@ def provider_for_session( ( ArtifactSpec.input( unnamed_main_flow_artifact_name( - snapshot.index, + step_index, invocation.key, ), ImageArtifactType, @@ -255,7 +251,7 @@ def provider_for_session( except (TypeError, ValueError) as exc: raise type(exc)( "CellProfiler contract compilation failed for step " - f"{snapshot.index} ({snapshot.step.name!r}), invocation " + f"{step_index} ({step.name!r}), invocation " f"{invocation.key!r}, module {module_type.__name__}: {exc}" ) from exc compiled_contract = replace( @@ -272,11 +268,11 @@ def provider_for_session( ), ), ) - key = (snapshot.index, invocation.key) + key = (step_index, invocation.key) if key in plans: raise ValueError( f"Duplicate CellProfiler invocation contract for step " - f"{snapshot.index} ({snapshot.step.name!r}), invocation " + f"{step_index} ({step.name!r}), invocation " f"{invocation.key!r}, module {module_type.__name__}: " f"{key!r}." ) diff --git a/tests/unit/pyqt_gui/test_plate_manager_widget.py b/tests/unit/pyqt_gui/test_plate_manager_widget.py index 44e9097966..f90c0362a7 100644 --- a/tests/unit/pyqt_gui/test_plate_manager_widget.py +++ b/tests/unit/pyqt_gui/test_plate_manager_widget.py @@ -46,7 +46,6 @@ ) from openhcs.core.input_workspace import InputWorkspacePreparationResult from openhcs.core.orchestrator.orchestrator import PipelineOrchestrator -from openhcs.core.pipeline.step_snapshot import StepSnapshot from openhcs.core.pipeline_document import PipelineDocumentAuthority from openhcs.core.progress import ( ProgressEvent, @@ -1085,27 +1084,19 @@ def test_cellprofiler_import_applies_pipeline_config_to_orchestrator_state( ) plate_state = ObjectStateRegistry.get_by_scope(plate_scope) - editor_state = PipelineObjectStateBinding.editor_state_for_plate( - plate_scope - ) + editor_state = PipelineObjectStateBinding.editor_state_for_plate(plate_scope) assert plate_state is not None assert widget.plate_configs[plate_scope] == pipeline_config assert ( - plate_state.get_saved_resolved_value( - "source_bindings_config.match_plan" - ) + plate_state.get_saved_resolved_value("source_bindings_config.match_plan") == match_plan ) step_scope_id = editor_state.step_scope_ids[0] step_state = ObjectStateRegistry.get_by_scope(step_scope_id) assert step_state is not None - snapshot = StepSnapshot( - index=0, - scope_id=step_state.scope_id, - step=step_state.to_saved_resolved_object(), - ) - assert snapshot.step.source_bindings.match_plan == match_plan + snapshot = step_state.to_saved_resolved_object() + assert snapshot.source_bindings.match_plan == match_plan finally: close_widget(widget) ObjectStateRegistry.clear() diff --git a/tests/unit/test_artifact_output_ownership.py b/tests/unit/test_artifact_output_ownership.py index ea1f9d71d1..04a0491855 100644 --- a/tests/unit/test_artifact_output_ownership.py +++ b/tests/unit/test_artifact_output_ownership.py @@ -1,5 +1,7 @@ """Compile invariants apply independently of artifact recording ownership.""" +from openhcs.core.pipeline.compilation_session import ResolvedPipelineDefinition + from dataclasses import replace from types import SimpleNamespace @@ -343,7 +345,6 @@ def test_real_cellprofiler_declaration_compiles_without_a_table_wide_subject(): from openhcs.core.context.processing_context import ProcessingContext from openhcs.core.invocation_artifacts import ArtifactDeclarationStepContext from openhcs.core.pipeline.compilation_session import CompilationSession - from openhcs.core.pipeline.step_snapshot import StepSnapshot from openhcs.core.source_bindings import ( NamedSourceBinding, StepSourceBindingsConfig, @@ -397,11 +398,12 @@ def test_real_cellprofiler_declaration_compiles_without_a_table_wide_subject(): ), }, ), - steps=[step], orchestrator=SimpleNamespace(pipeline_config=PipelineConfig()), global_config=GlobalPipelineConfig(), - step_state_map={0: object()}, - snapshots=(StepSnapshot(index=0, scope_id="test::cp-output-owner", step=step),), + pipeline=ResolvedPipelineDefinition( + steps=(step,), + step_state_map={0: SimpleNamespace(scope_id="plate::functionstep_0")}, + ), ) provider = CellProfilerInvocationContractProviderFactory.provider_for_session( session diff --git a/tests/unit/test_cellprofiler_artifact_declarations.py b/tests/unit/test_cellprofiler_artifact_declarations.py index fc1a0df8bb..8331ca4488 100644 --- a/tests/unit/test_cellprofiler_artifact_declarations.py +++ b/tests/unit/test_cellprofiler_artifact_declarations.py @@ -2,6 +2,8 @@ from __future__ import annotations +from openhcs.core.pipeline.compilation_session import ResolvedPipelineDefinition + from dataclasses import replace from pathlib import Path from types import SimpleNamespace @@ -48,7 +50,6 @@ ArtifactProducer, artifact_producers_for_outputs, ) -from openhcs.core.pipeline.step_snapshot import StepSnapshot from openhcs.core.source_bindings import ( ComponentSelector, NamedSourceBinding, @@ -158,7 +159,6 @@ def _compiler_contracts( ObjectStateRegistry.register(pipeline_state, _skip_snapshot=True) resolved_steps: list[FunctionStep] = [] step_states: dict[int, ObjectState] = {} - snapshots: list[StepSnapshot] = [] for index, step in enumerate(steps): step_state = ObjectState( step, @@ -170,13 +170,6 @@ def _compiler_contracts( assert isinstance(resolved_step, FunctionStep) resolved_steps.append(resolved_step) step_states[index] = step_state - snapshots.append( - StepSnapshot( - index=index, - scope_id=step_state.scope_id, - step=resolved_step, - ) - ) context = ProcessingContext( step_plans={ @@ -192,31 +185,29 @@ def _compiler_contracts( ) session = CompilationSession.from_context( context=context, - steps=resolved_steps, orchestrator=SimpleNamespace( pipeline_config=pipeline_state.to_object(), ), global_config=global_config, - step_state_map=step_states, - snapshots=tuple(snapshots), + pipeline=ResolvedPipelineDefinition( + steps=resolved_steps, step_state_map=step_states + ), ) provider = PipelineInvocationContractProviderAuthority.provider_for_session( session, ) contracts: list[CallableContract] = [] - for snapshot in snapshots: - invocations = tuple( - normalize_function_pattern(snapshot.step.func).iter_items() - ) + for index, snapshot in enumerate(resolved_steps): + invocations = tuple(normalize_function_pattern(snapshot.func).iter_items()) assert len(invocations) == 1 plan = provider( invocations[0], ArtifactDeclarationStepContext( - step_name=snapshot.step.name, - step_index=snapshot.index, - source_bindings=snapshot.step.source_bindings, - group_by=snapshot.step.processing_config.group_by, - input_source=snapshot.step.processing_config.input_source, + step_name=snapshot.name, + step_index=index, + source_bindings=snapshot.source_bindings, + group_by=snapshot.processing_config.group_by, + input_source=snapshot.processing_config.input_source, ), ) assert plan is not None diff --git a/tests/unit/test_cellprofiler_combineobjects_source_scope.py b/tests/unit/test_cellprofiler_combineobjects_source_scope.py index 08323bf0a5..2328a2c0f3 100644 --- a/tests/unit/test_cellprofiler_combineobjects_source_scope.py +++ b/tests/unit/test_cellprofiler_combineobjects_source_scope.py @@ -30,7 +30,6 @@ PathPlannerExecutionGroups, PathPlannerGroupScope, ) -from openhcs.core.pipeline.step_snapshot import StepSnapshot from openhcs.core.progress import set_progress_queue from openhcs.core.source_bindings import ( NamedSourceBinding, @@ -103,7 +102,7 @@ def combine(object_labels): ), source_bindings=source_bindings, ) - snapshot = StepSnapshot(index=0, scope_id="combine", step=step) + snapshot = step planner = SimpleNamespace( declared={}, artifact_context=ArtifactDeclarationStepContext( @@ -112,7 +111,6 @@ def combine(object_labels): session=SimpleNamespace( realized_source_metadata=({"source_alias": source.name, "channel": "2"},), ), - source_bindings_for_snapshot=lambda _snapshot: source_bindings, ) scope = PathPlannerExecutionGroups(planner).artifact_owned_execution_scope( diff --git a/tests/unit/test_cellprofiler_morphology_public_contracts.py b/tests/unit/test_cellprofiler_morphology_public_contracts.py index 904e5852b5..a0aef06dad 100644 --- a/tests/unit/test_cellprofiler_morphology_public_contracts.py +++ b/tests/unit/test_cellprofiler_morphology_public_contracts.py @@ -2,6 +2,8 @@ from __future__ import annotations +from openhcs.core.pipeline.compilation_session import ResolvedPipelineDefinition + from dataclasses import dataclass from types import SimpleNamespace @@ -32,7 +34,6 @@ artifact_outputs, special_input_names_from_callable, ) -from openhcs.core.pipeline.step_snapshot import StepSnapshot from openhcs.core.source_bindings import ( NamedSourceBinding, SourceProjectionRole, @@ -189,14 +190,7 @@ def fixture_producer(image): producer_step = FunctionStep(func=fixture_producer, name="FixtureProducer") steps = (producer_step, step) - snapshots = tuple( - StepSnapshot( - index=index, - scope_id=f"test::morphology::{index}", - step=current_step, - ) - for index, current_step in enumerate(steps) - ) + snapshots = tuple(current_step for index, current_step in enumerate(steps)) session = CompilationSession.from_context( context=ProcessingContext( step_plans={ @@ -210,11 +204,12 @@ def fixture_producer(image): }, axis_id="A01", ), - steps=steps, orchestrator=SimpleNamespace(pipeline_config=PipelineConfig()), global_config=GlobalPipelineConfig(), - step_state_map={index: object() for index in range(len(steps))}, - snapshots=snapshots, + pipeline=ResolvedPipelineDefinition( + steps=snapshots, + step_state_map={index: object() for index in range(len(steps))}, + ), ) provider = CellProfilerInvocationContractProviderFactory.provider_for_session( session diff --git a/tests/unit/test_cellprofiler_neighbors_contract.py b/tests/unit/test_cellprofiler_neighbors_contract.py index 46cefbd2a0..20c2b77933 100644 --- a/tests/unit/test_cellprofiler_neighbors_contract.py +++ b/tests/unit/test_cellprofiler_neighbors_contract.py @@ -2,6 +2,8 @@ from __future__ import annotations +from openhcs.core.pipeline.compilation_session import ResolvedPipelineDefinition + import inspect from types import SimpleNamespace from typing import get_type_hints @@ -30,7 +32,6 @@ from openhcs.core.pipeline.artifact_planning import artifact_producers_for_outputs from openhcs.core.pipeline.function_contracts import artifact_outputs from openhcs.core.memory.decorators import numpy -from openhcs.core.pipeline.step_snapshot import StepSnapshot from openhcs.core.runtime_object_labels import ( ObjectLabelSet, ObjectLabelPayload, @@ -102,11 +103,7 @@ def test_relationship_module_number_is_derived_after_public_transport() -> None: (restored_step,) = FunctionStepTransportAuthority.pipeline_steps_from_namespace( namespace ) - snapshot = StepSnapshot( - index=0, - scope_id="test::neighbors-module-order", - step=restored_step, - ) + snapshot = restored_step session = CompilationSession.from_context( context=ProcessingContext( step_plans={ @@ -119,11 +116,12 @@ def test_relationship_module_number_is_derived_after_public_transport() -> None: }, axis_id="A01", ), - steps=[restored_step], orchestrator=SimpleNamespace(pipeline_config=PipelineConfig()), global_config=GlobalPipelineConfig(), - step_state_map={0: object()}, - snapshots=(snapshot,), + pipeline=ResolvedPipelineDefinition( + steps=(snapshot,), + step_state_map={0: SimpleNamespace(scope_id="plate::functionstep_0")}, + ), ) provider = CellProfilerInvocationContractProviderFactory.provider_for_session( @@ -142,7 +140,7 @@ def test_relationship_module_number_is_derived_after_public_transport() -> None: def _compiled_neighbor_invocation(): step = _reconstructed_neighbor_step() - snapshot = StepSnapshot(index=0, scope_id="test::neighbors", step=step) + snapshot = step context = ProcessingContext( step_plans={ 0: CompiledStepPlan( @@ -156,11 +154,12 @@ def _compiled_neighbor_invocation(): ) session = CompilationSession.from_context( context=context, - steps=[step], orchestrator=SimpleNamespace(pipeline_config=PipelineConfig()), global_config=GlobalPipelineConfig(), - step_state_map={0: object()}, - snapshots=(snapshot,), + pipeline=ResolvedPipelineDefinition( + steps=(snapshot,), + step_state_map={0: SimpleNamespace(scope_id="plate::functionstep_0")}, + ), ) provider = CellProfilerInvocationContractProviderFactory.provider_for_session( session @@ -363,14 +362,7 @@ def fixture_producer(image): name="LaterNeighbors", ), ) - snapshots = tuple( - StepSnapshot( - index=index, - scope_id=f"test::numbered-neighbors::{index}", - step=step, - ) - for index, step in enumerate(steps) - ) + snapshots = tuple(step for index, step in enumerate(steps)) session = CompilationSession.from_context( context=ProcessingContext( step_plans={ @@ -384,11 +376,12 @@ def fixture_producer(image): }, axis_id="A01", ), - steps=steps, orchestrator=SimpleNamespace(pipeline_config=PipelineConfig()), global_config=GlobalPipelineConfig(), - step_state_map={index: object() for index in range(len(steps))}, - snapshots=snapshots, + pipeline=ResolvedPipelineDefinition( + steps=snapshots, + step_state_map={index: object() for index in range(len(steps))}, + ), ) provider = CellProfilerInvocationContractProviderFactory.provider_for_session( @@ -511,14 +504,7 @@ def fixture_producer(image): FunctionStep(func=neighbor_pattern, name="MeasureObjectNeighbors"), FunctionStep(func=relate_pattern, name="RelateObjects"), ) - snapshots = tuple( - StepSnapshot( - index=index, - scope_id=f"test::advanced-module-numbering::{index}", - step=step, - ) - for index, step in enumerate(steps) - ) + snapshots = tuple(step for index, step in enumerate(steps)) session = CompilationSession.from_context( context=ProcessingContext( step_plans={ @@ -532,11 +518,12 @@ def fixture_producer(image): }, axis_id="A01", ), - steps=steps, orchestrator=SimpleNamespace(pipeline_config=PipelineConfig()), global_config=GlobalPipelineConfig(), - step_state_map={index: object() for index in range(len(steps))}, - snapshots=snapshots, + pipeline=ResolvedPipelineDefinition( + steps=snapshots, + step_state_map={index: object() for index in range(len(steps))}, + ), ) provider = CellProfilerInvocationContractProviderFactory.provider_for_session( session diff --git a/tests/unit/test_cellprofiler_runtime_callable_introspection.py b/tests/unit/test_cellprofiler_runtime_callable_introspection.py index e435baed1f..d0078ba9e5 100644 --- a/tests/unit/test_cellprofiler_runtime_callable_introspection.py +++ b/tests/unit/test_cellprofiler_runtime_callable_introspection.py @@ -1,5 +1,7 @@ """CellProfiler runtime callable introspection behavior.""" +from openhcs.core.pipeline.compilation_session import ResolvedPipelineDefinition + import subprocess import sys from types import SimpleNamespace @@ -28,7 +30,6 @@ from openhcs.core.function_reference import FunctionReferenceTransportAuthority from openhcs.core.invocation_artifacts import ArtifactDeclarationStepContext from openhcs.core.pipeline.compilation_session import CompilationSession -from openhcs.core.pipeline.step_snapshot import StepSnapshot from openhcs.core.source_bindings import ( ComponentSelector, NamedSourceBinding, @@ -51,16 +52,9 @@ ) -def _snapshot_for_step(index: int, step: FunctionStep) -> StepSnapshot: - return StepSnapshot(index=index, scope_id=f"test::functionstep_{index}", step=step) - - def _compilation_session_for_steps( steps: list[FunctionStep], ) -> CompilationSession: - snapshots = tuple( - _snapshot_for_step(index, step) for index, step in enumerate(steps) - ) return CompilationSession.from_context( context=ProcessingContext( step_plans={ @@ -74,11 +68,11 @@ def _compilation_session_for_steps( }, axis_id="A01", ), - steps=steps, orchestrator=SimpleNamespace(pipeline_config=PipelineConfig()), global_config=GlobalPipelineConfig(), - step_state_map={index: object() for index in range(len(steps))}, - snapshots=snapshots, + pipeline=ResolvedPipelineDefinition( + steps=steps, step_state_map={index: object() for index in range(len(steps))} + ), ) diff --git a/tests/unit/test_compilation_session.py b/tests/unit/test_compilation_session.py index d2656e432f..3177ca0f0c 100644 --- a/tests/unit/test_compilation_session.py +++ b/tests/unit/test_compilation_session.py @@ -1,3 +1,4 @@ +from openhcs.core.pipeline.compilation_session import ResolvedPipelineDefinition from inspect import signature from types import SimpleNamespace @@ -33,7 +34,7 @@ PathPlannerArtifactStage, PathPlannerExecutionGroups, ) -from openhcs.core.pipeline.step_snapshot import StepSnapshot +from openhcs.core.steps.abstract import AbstractStep from openhcs.core.source_bindings import ( EMPTY_SOURCE_BINDINGS, ComponentSelector, @@ -92,8 +93,9 @@ def test_axis_session_initialization_requires_pipeline_resolved_state() -> None: PipelineCompiler.initialize_step_plans_for_context ).parameters - assert "step_state_map" in parameters - assert "step_snapshots" in parameters + assert "pipeline" in parameters + assert "step_state_map" not in parameters + assert "step_snapshots" not in parameters assert "steps_already_resolved" not in parameters assert "_resolve_steps_for_context" not in vars(PipelineCompiler) @@ -180,13 +182,12 @@ def _previous_step_and_external_source(image, dna): return image, dna -def _snapshot( +def _resolved_step( step: FunctionStep, - index: int, variable_components=(VariableComponents.SITE,), source_bindings=EMPTY_SOURCE_BINDINGS, input_source: InputSource = InputSource.PREVIOUS_STEP, -) -> StepSnapshot: +) -> AbstractStep: step.source_bindings = source_bindings step.processing_config = ProcessingConfig( variable_components=list(variable_components), @@ -194,11 +195,7 @@ def _snapshot( input_source=input_source, ) step.step_materialization_config = StepMaterializationConfig(enabled=False) - return StepSnapshot( - index=index, - scope_id=f"plate::functionstep_{index}", - step=step, - ) + return step def _context() -> SimpleNamespace: @@ -223,14 +220,13 @@ def _orchestrator(pipeline_config: PipelineConfig | None = None) -> SimpleNamesp def _compile_source_plans_for_contract( session: CompilationSession, - snapshot: StepSnapshot, + snapshot: AbstractStep, func, main_input_dependency: StepInputDependency = StepInputDependency.pipeline_start(), ): planner = SimpleNamespace( session=session, artifact_context=ArtifactDeclarationStepContext.empty(), - source_bindings_for_snapshot=(lambda value: value.step.source_bindings), ) stage = PathPlannerArtifactStage(planner) execution_bindings = stage.source_bindings_for_contracts( @@ -271,67 +267,36 @@ def test_axis_compilation_request_preserves_effective_auto_add_flag(): ) -def test_compilation_session_owns_step_snapshot_plan_invariants(): +def test_compilation_session_shares_resolved_pipeline_and_owns_axis_plans(): step = FunctionStep(func=_identity, name="step") - step_state = object() + step_state = SimpleNamespace(scope_id="plate::functionstep_0") session = CompilationSession.from_context( context=_context(), - steps=[step], orchestrator=_orchestrator(), global_config=GlobalPipelineConfig(), - step_state_map={0: step_state}, - snapshots=(_snapshot(step, 0),), + pipeline=ResolvedPipelineDefinition( + steps=(_resolved_step(step),), step_state_map={0: step_state} + ), ) assert session.axis_id == "A01" - assert session.step(0) is step - assert session.step_state(0) is step_state - assert session.snapshot(0).step.name == "step" + assert session.pipeline.steps[0] is step + assert session.pipeline.step_state_map[0] is step_state + assert session.pipeline.steps[0].name == "step" assert session.plan(0).step_name == "step" -def test_compilation_session_rejects_missing_snapshot(): - step = FunctionStep(func=_identity, name="step") - - with pytest.raises(ValueError, match="one StepSnapshot per step"): - CompilationSession.from_context( - context=_context(), - steps=[step], - orchestrator=_orchestrator(), - global_config=GlobalPipelineConfig(), - step_state_map={0: object()}, - snapshots=(), - ) - - -def test_compilation_session_rejects_non_contiguous_snapshot_index(): - step = FunctionStep(func=_identity, name="step") - - with pytest.raises(ValueError, match="index mismatch"): - CompilationSession.from_context( - context=_context(), - steps=[step], - orchestrator=_orchestrator(), - global_config=GlobalPipelineConfig(), - step_state_map={0: object()}, - snapshots=(_snapshot(step, 1),), - ) - - def test_compiler_keeps_variable_components_as_stack_source(): step = FunctionStep(func=_identity, name="step") session = CompilationSession.from_context( context=_context(), - steps=[step], orchestrator=_orchestrator(), global_config=GlobalPipelineConfig(), - step_state_map={0: object()}, - snapshots=( - _snapshot( - step, - 0, - variable_components=(VariableComponents.CHANNEL,), + pipeline=ResolvedPipelineDefinition( + steps=( + _resolved_step(step, variable_components=(VariableComponents.CHANNEL,)), ), + step_state_map={0: SimpleNamespace(scope_id="plate::functionstep_0")}, ), ) @@ -382,18 +347,14 @@ def test_path_planner_source_binding_plan_comes_from_objectstate_snapshot(): ) ObjectStateRegistry.register(step_state, _skip_snapshot=True) resolved_step = step_state.to_saved_resolved_object() - snapshot = StepSnapshot( - index=0, - scope_id=step_state.scope_id, - step=resolved_step, - ) + snapshot = resolved_step session = CompilationSession.from_context( context=_context(), - steps=[resolved_step], orchestrator=_orchestrator(pipeline_state.to_object()), global_config=GlobalPipelineConfig(), - step_state_map={0: step_state}, - snapshots=(snapshot,), + pipeline=ResolvedPipelineDefinition( + steps=(snapshot,), step_state_map={0: step_state} + ), ) execution_bindings, (source_binding_plan, _source_universe_plan) = ( @@ -406,9 +367,9 @@ def test_path_planner_source_binding_plan_comes_from_objectstate_snapshot(): finally: ObjectStateRegistry.clear() - assert snapshot.step.source_bindings.bindings == (binding,) - assert snapshot.step.source_bindings.metadata_rules == (metadata_rule,) - assert snapshot.step.source_bindings.match_plan == match_plan + assert snapshot.source_bindings.bindings == (binding,) + assert snapshot.source_bindings.metadata_rules == (metadata_rule,) + assert snapshot.source_bindings.match_plan == match_plan assert execution_bindings.bindings == (binding,) assert source_binding_plan.bindings == (binding,) assert source_binding_plan.metadata_rules == (metadata_rule,) @@ -443,20 +404,16 @@ def test_compiler_streaming_config_snapshot_preserves_inherited_port(): ) ObjectStateRegistry.register(step_state, _skip_snapshot=True) resolved_step = step_state.to_saved_resolved_object() - snapshot = StepSnapshot( - index=0, - scope_id=step_state.scope_id, - step=resolved_step, - ) + snapshot = resolved_step context = _context() context.required_visualizers = [] session = CompilationSession.from_context( context=context, - steps=[resolved_step], orchestrator=_orchestrator(pipeline_state.to_object()), global_config=global_config, - step_state_map={0: step_state}, - snapshots=(snapshot,), + pipeline=ResolvedPipelineDefinition( + steps=(snapshot,), step_state_map={0: step_state} + ), ) PipelineCompiler._collect_streaming_configs(session) @@ -474,18 +431,17 @@ def test_compiler_streaming_config_snapshot_preserves_inherited_port(): def test_compiler_disabled_source_bindings_stay_inert_without_contract_requirement(): binding = NamedSourceBinding(alias="DNA") step = FunctionStep(func=_identity, name="source-bound") - snapshot = _snapshot( - step, - 0, - source_bindings=StepSourceBindingsConfig(bindings=(binding,)), + snapshot = _resolved_step( + step, source_bindings=StepSourceBindingsConfig(bindings=(binding,)) ) session = CompilationSession.from_context( context=_context(), - steps=[step], orchestrator=_orchestrator(), global_config=GlobalPipelineConfig(), - step_state_map={0: object()}, - snapshots=(snapshot,), + pipeline=ResolvedPipelineDefinition( + steps=(snapshot,), + step_state_map={0: SimpleNamespace(scope_id="plate::functionstep_0")}, + ), ) PipelineCompiler._supplement_step_plans(session) @@ -496,19 +452,19 @@ def test_compiler_disabled_source_bindings_stay_inert_without_contract_requireme def test_path_planner_activates_declared_source_binding_for_pipeline_start(): binding = NamedSourceBinding(alias="DNA") step = FunctionStep(func=_external_source_consumer, name="source-bound") - snapshot = _snapshot( + snapshot = _resolved_step( step, - 0, source_bindings=StepSourceBindingsConfig(bindings=(binding,)), input_source=InputSource.PIPELINE_START, ) session = CompilationSession.from_context( context=_context(), - steps=[step], orchestrator=_orchestrator(), global_config=GlobalPipelineConfig(), - step_state_map={0: object()}, - snapshots=(snapshot,), + pipeline=ResolvedPipelineDefinition( + steps=(snapshot,), + step_state_map={0: SimpleNamespace(scope_id="plate::functionstep_0")}, + ), ) _execution_bindings, (source_binding_plan, _source_universe_plan) = ( @@ -530,9 +486,8 @@ def test_plate_export_contract_construction_projects_inputs_to_runtime_batch(): binding = NamedSourceBinding(alias="DNA") step = FunctionStep(func=export_to_database, name="ExportToDatabase") - snapshot = _snapshot( + snapshot = _resolved_step( step, - 0, variable_components=(), source_bindings=StepSourceBindingsConfig( enabled=True, @@ -542,11 +497,12 @@ def test_plate_export_contract_construction_projects_inputs_to_runtime_batch(): ) session = CompilationSession.from_context( context=_context(), - steps=[step], orchestrator=_orchestrator(), global_config=GlobalPipelineConfig(), - step_state_map={0: object()}, - snapshots=(snapshot,), + pipeline=ResolvedPipelineDefinition( + steps=(snapshot,), + step_state_map={0: SimpleNamespace(scope_id="plate::functionstep_0")}, + ), ) dependency_before_provider = session.plan(0).main_input_dependency @@ -575,19 +531,19 @@ def test_plate_export_contract_construction_projects_inputs_to_runtime_batch(): def test_path_planner_preserves_pipeline_start_bindings_for_implicit_main_flow(): binding = NamedSourceBinding(alias="DNA") step = FunctionStep(func=_identity, name="source-bound measurement") - snapshot = _snapshot( + snapshot = _resolved_step( step, - 0, source_bindings=StepSourceBindingsConfig(bindings=(binding,)), input_source=InputSource.PIPELINE_START, ) session = CompilationSession.from_context( context=_context(), - steps=[step], orchestrator=_orchestrator(), global_config=GlobalPipelineConfig(), - step_state_map={0: object()}, - snapshots=(snapshot,), + pipeline=ResolvedPipelineDefinition( + steps=(snapshot,), + step_state_map={0: SimpleNamespace(scope_id="plate::functionstep_0")}, + ), ) execution_bindings, (source_binding_plan, source_universe_plan) = ( _compile_source_plans_for_contract(session, snapshot, _identity) @@ -605,18 +561,17 @@ def test_path_planner_step_output_projects_only_exact_source_artifacts() -> None func=_previous_step_and_external_source, name="previous-step plus exact source", ) - snapshot = _snapshot( - step, - 0, - source_bindings=StepSourceBindingsConfig(bindings=(dna, unrelated)), + snapshot = _resolved_step( + step, source_bindings=StepSourceBindingsConfig(bindings=(dna, unrelated)) ) session = CompilationSession.from_context( context=_context(), - steps=[step], orchestrator=_orchestrator(), global_config=GlobalPipelineConfig(), - step_state_map={0: object()}, - snapshots=(snapshot,), + pipeline=ResolvedPipelineDefinition( + steps=(snapshot,), + step_state_map={0: SimpleNamespace(scope_id="plate::functionstep_0")}, + ), ) execution_bindings, (source_binding_plan, _source_universe_plan) = ( @@ -646,9 +601,8 @@ def test_path_planner_preserves_metaxpress_primary_source_order() -> None: NamedSourceBinding(alias="Hoechst"), ) step = FunctionStep(func=neurite_outgrowth_metaxpress, name="neurite") - snapshot = _snapshot( + snapshot = _resolved_step( step, - 0, variable_components=(VariableComponents.CHANNEL,), source_bindings=StepSourceBindingsConfig( enabled=True, @@ -661,11 +615,12 @@ def test_path_planner_preserves_metaxpress_primary_source_order() -> None: ) session = CompilationSession.from_context( context=_context(), - steps=[step], orchestrator=_orchestrator(pipeline_config), global_config=GlobalPipelineConfig(), - step_state_map={0: object()}, - snapshots=(snapshot,), + pipeline=ResolvedPipelineDefinition( + steps=(snapshot,), + step_state_map={0: SimpleNamespace(scope_id="plate::functionstep_0")}, + ), ) contract = CallableContract.from_callable(neurite_outgrowth_metaxpress) @@ -703,9 +658,8 @@ def test_path_planner_execution_groups_use_resolved_source_bindings(): component_identity=(ComponentSelector(AllComponents.CHANNEL, "2"),), ), ) - snapshot = _snapshot( + snapshot = _resolved_step( step, - 0, source_bindings=StepSourceBindingsConfig( enabled=True, bindings=bindings, @@ -717,7 +671,7 @@ def test_path_planner_execution_groups_use_resolved_source_bindings(): scope = PathPlannerExecutionGroups(planner).source_binding_scope_for_group_by( snapshot, GroupBy.CHANNEL, - source_bindings=snapshot.step.source_bindings, + source_bindings=snapshot.source_bindings, ) assert scope.keys == ("1", "2") @@ -742,9 +696,8 @@ def test_path_planner_execution_groups_preserve_declared_component_identity() -> component_identity=(ComponentSelector(AllComponents.CHANNEL, "MCP_AGP"),), ), ) - snapshot = _snapshot( + snapshot = _resolved_step( step, - 0, source_bindings=StepSourceBindingsConfig( enabled=True, bindings=bindings, @@ -761,7 +714,7 @@ def test_path_planner_execution_groups_preserve_declared_component_identity() -> scope = PathPlannerExecutionGroups(planner).source_binding_scope_for_group_by( snapshot, GroupBy.CHANNEL, - source_bindings=snapshot.step.source_bindings, + source_bindings=snapshot.source_bindings, ) assert scope.keys == ("MCP_DNA", "MCP_AGP") @@ -772,9 +725,8 @@ def test_path_planner_freezes_only_contract_selected_source_bindings(): binding = NamedSourceBinding(alias="DNA") unused_binding = NamedSourceBinding(alias="Unused") step = FunctionStep(func=_external_source_consumer, name="source-bound") - snapshot = _snapshot( + snapshot = _resolved_step( step, - 0, source_bindings=StepSourceBindingsConfig( bindings=(binding, unused_binding), enabled=True, @@ -782,11 +734,12 @@ def test_path_planner_freezes_only_contract_selected_source_bindings(): ) session = CompilationSession.from_context( context=_context(), - steps=[step], orchestrator=_orchestrator(), global_config=GlobalPipelineConfig(), - step_state_map={0: object()}, - snapshots=(snapshot,), + pipeline=ResolvedPipelineDefinition( + steps=(snapshot,), + step_state_map={0: SimpleNamespace(scope_id="plate::functionstep_0")}, + ), ) execution_bindings, (source_binding_plan, _source_universe_plan) = ( _compile_source_plans_for_contract( @@ -848,5 +801,5 @@ def test_compiler_pipeline_scope_prevents_cross_pipeline_source_binding_inherita ) assert first_scope != second_scope - assert first_resolved.snapshots[0].step.source_bindings.bindings == (binding,) - assert second_resolved.snapshots[0].step.source_bindings.is_empty + assert first_resolved.steps[0].source_bindings.bindings == (binding,) + assert second_resolved.steps[0].source_bindings.is_empty diff --git a/tests/unit/test_function_step_execution_scope.py b/tests/unit/test_function_step_execution_scope.py index cb960d00d4..6147a2ffb7 100644 --- a/tests/unit/test_function_step_execution_scope.py +++ b/tests/unit/test_function_step_execution_scope.py @@ -58,7 +58,6 @@ PathPlannerStepAssemblyStage, PathPlannerValidationStage, ) -from openhcs.core.pipeline.step_snapshot import StepSnapshot from openhcs.core.runtime_artifact_values import RuntimeValue from openhcs.core.runtime_batch_contracts import RuntimeBatchExecutionDomain from openhcs.core.measurement_row_materialization import ( @@ -222,11 +221,7 @@ def test_plate_scope_drives_no_main_flow_paths() -> None: output_dir = Path("/data/plate_processed/images") planner.paths = SimpleNamespace(build_output_path=lambda: output_dir) planner.steps = PathPlannerStepAssemblyStage(planner) - snapshot = StepSnapshot( - index=1, - scope_id="export", - step=FunctionStep(func=lambda image: image, name="export"), - ) + snapshot = FunctionStep(func=lambda image: image, name="export") dependency = planner.steps.main_input_dependency( snapshot, diff --git a/tests/unit/test_invocation_contract_provider.py b/tests/unit/test_invocation_contract_provider.py index b987982643..6172ab7bbb 100644 --- a/tests/unit/test_invocation_contract_provider.py +++ b/tests/unit/test_invocation_contract_provider.py @@ -2,6 +2,8 @@ from __future__ import annotations +from openhcs.core.pipeline.compilation_session import ResolvedPipelineDefinition + from dataclasses import fields, replace import importlib from types import MappingProxyType, SimpleNamespace @@ -642,7 +644,6 @@ def test_cellprofiler_provider_reconstructs_exact_contract_from_public_step() -> ) from openhcs.core.context.processing_context import ProcessingContext from openhcs.core.pipeline.compilation_session import CompilationSession - from openhcs.core.pipeline.step_snapshot import StepSnapshot from openhcs.core.source_bindings import ( NamedSourceBinding, StepSourceBindingsConfig, @@ -680,9 +681,7 @@ def test_cellprofiler_provider_reconstructs_exact_contract_from_public_step() -> ), ), ) - snapshot = StepSnapshot( - index=0, scope_id="provider-test::functionstep_0", step=step - ) + snapshot = step session = CompilationSession.from_context( context=ProcessingContext( step_plans={ @@ -695,11 +694,12 @@ def test_cellprofiler_provider_reconstructs_exact_contract_from_public_step() -> }, axis_id="A01", ), - steps=(step,), orchestrator=SimpleNamespace(pipeline_config=PipelineConfig()), global_config=GlobalPipelineConfig(), - step_state_map={0: object()}, - snapshots=(snapshot,), + pipeline=ResolvedPipelineDefinition( + steps=(snapshot,), + step_state_map={0: SimpleNamespace(scope_id="plate::functionstep_0")}, + ), ) callable_state = vars(color_to_gray).copy() @@ -738,7 +738,6 @@ def test_cellprofiler_provider_advances_native_artifacts_through_generic_graph() from openhcs.core.config import GlobalPipelineConfig, PipelineConfig from openhcs.core.context.processing_context import ProcessingContext from openhcs.core.pipeline.compilation_session import CompilationSession - from openhcs.core.pipeline.step_snapshot import StepSnapshot from openhcs.core.steps.function_step import FunctionStep from openhcs.interop.cellprofiler.compile_time_contracts import ( CellProfilerInvocationContractProvider, @@ -759,13 +758,7 @@ def test_cellprofiler_provider_advances_native_artifacts_through_generic_graph() name="ColorToGray", ) - def snapshot(index: int, step: FunctionStep) -> StepSnapshot: - return StepSnapshot( - index=index, scope_id=f"mixed-provider::functionstep_{index}", step=step - ) - steps = (native_step, cellprofiler_step) - snapshots = tuple(snapshot(index, step) for index, step in enumerate(steps)) plans = { index: CompiledStepPlan( step_index=index, @@ -777,11 +770,15 @@ def snapshot(index: int, step: FunctionStep) -> StepSnapshot: } session = CompilationSession.from_context( context=ProcessingContext(step_plans=plans, axis_id="A01"), - steps=steps, orchestrator=SimpleNamespace(pipeline_config=PipelineConfig()), global_config=GlobalPipelineConfig(), - step_state_map={0: object(), 1: object()}, - snapshots=snapshots, + pipeline=ResolvedPipelineDefinition( + steps=steps, + step_state_map={ + 0: SimpleNamespace(scope_id="plate::functionstep_0"), + 1: SimpleNamespace(scope_id="plate::functionstep_1"), + }, + ), ) provider = CellProfilerInvocationContractProviderFactory.provider_for_session( @@ -812,7 +809,6 @@ def test_cellprofiler_provider_leaves_native_same_name_callable_unclaimed() -> N from openhcs.core.config import GlobalPipelineConfig, PipelineConfig from openhcs.core.context.processing_context import ProcessingContext from openhcs.core.pipeline.compilation_session import CompilationSession - from openhcs.core.pipeline.step_snapshot import StepSnapshot from openhcs.core.steps.function_step import FunctionStep from openhcs.interop.cellprofiler.compile_time_contracts import ( CellProfilerInvocationContractProviderFactory, @@ -820,11 +816,7 @@ def test_cellprofiler_provider_leaves_native_same_name_callable_unclaimed() -> N from openhcs.processing.backends.processors.numpy_processor import crop step = FunctionStep(func=crop, name="Native NumPy crop") - snapshot = StepSnapshot( - index=0, - scope_id="native-crop-provider::functionstep_0", - step=step, - ) + snapshot = step session = CompilationSession.from_context( context=ProcessingContext( step_plans={ @@ -837,11 +829,12 @@ def test_cellprofiler_provider_leaves_native_same_name_callable_unclaimed() -> N }, axis_id="A01", ), - steps=(step,), orchestrator=SimpleNamespace(pipeline_config=PipelineConfig()), global_config=GlobalPipelineConfig(), - step_state_map={0: object()}, - snapshots=(snapshot,), + pipeline=ResolvedPipelineDefinition( + steps=(snapshot,), + step_state_map={0: SimpleNamespace(scope_id="plate::functionstep_0")}, + ), ) provider = CellProfilerInvocationContractProviderFactory.provider_for_session( @@ -1125,7 +1118,6 @@ def test_cellprofiler_provider_rejects_under_specified_one_image_align() -> None ) from openhcs.core.context.processing_context import ProcessingContext from openhcs.core.pipeline.compilation_session import CompilationSession - from openhcs.core.pipeline.step_snapshot import StepSnapshot from openhcs.core.source_bindings import ( NamedSourceBinding, StepSourceBindingsConfig, @@ -1152,11 +1144,7 @@ def test_cellprofiler_provider_rejects_under_specified_one_image_align() -> None ), ), ) - snapshot = StepSnapshot( - index=0, - scope_id="align-cardinality::functionstep_0", - step=step, - ) + snapshot = step session = CompilationSession.from_context( context=ProcessingContext( step_plans={ @@ -1169,11 +1157,12 @@ def test_cellprofiler_provider_rejects_under_specified_one_image_align() -> None }, axis_id="A01", ), - steps=(step,), orchestrator=SimpleNamespace(pipeline_config=PipelineConfig()), global_config=GlobalPipelineConfig(), - step_state_map={0: object()}, - snapshots=(snapshot,), + pipeline=ResolvedPipelineDefinition( + steps=(snapshot,), + step_state_map={0: SimpleNamespace(scope_id="plate::functionstep_0")}, + ), ) with pytest.raises( @@ -1255,7 +1244,6 @@ def test_calculate_math_provider_keeps_object_identity_and_output_name_public() from openhcs.core.config import GlobalPipelineConfig, PipelineConfig from openhcs.core.context.processing_context import ProcessingContext from openhcs.core.pipeline.compilation_session import CompilationSession - from openhcs.core.pipeline.step_snapshot import StepSnapshot from openhcs.core.source_bindings import ( NamedSourceBinding, SourceProjectionRole, @@ -1303,14 +1291,7 @@ def test_calculate_math_provider_keeps_object_identity_and_output_name_public() name="CalculateMath", source_bindings=measurement_step.source_bindings, ) - snapshots = tuple( - StepSnapshot( - index=index, - scope_id=f"calculate-math-provider::functionstep_{index}", - step=current_step, - ) - for index, current_step in enumerate((measurement_step, step)) - ) + steps = (measurement_step, step) session = CompilationSession.from_context( context=ProcessingContext( step_plans={ @@ -1324,11 +1305,15 @@ def test_calculate_math_provider_keeps_object_identity_and_output_name_public() }, axis_id="A01", ), - steps=(measurement_step, step), orchestrator=SimpleNamespace(pipeline_config=PipelineConfig()), global_config=GlobalPipelineConfig(), - step_state_map={0: object(), 1: object()}, - snapshots=snapshots, + pipeline=ResolvedPipelineDefinition( + steps=steps, + step_state_map={ + 0: SimpleNamespace(scope_id="plate::functionstep_0"), + 1: SimpleNamespace(scope_id="plate::functionstep_1"), + }, + ), ) provider = CellProfilerInvocationContractProviderFactory.provider_for_session( @@ -1387,7 +1372,6 @@ def test_native_unnamed_main_flow_remains_a_canonical_contract_input() -> None: ) from openhcs.core.context.processing_context import ProcessingContext from openhcs.core.pipeline.compilation_session import CompilationSession - from openhcs.core.pipeline.step_snapshot import StepSnapshot from openhcs.core.steps.function_step import FunctionStep from openhcs.interop.cellprofiler.compile_time_contracts import ( CellProfilerInvocationContractProviderFactory, @@ -1402,12 +1386,6 @@ def session_for( FunctionStep(func=_native_image, name="NativeImage"), cellprofiler_step, ) - snapshots = tuple( - StepSnapshot( - index=index, scope_id=f"native-cursor::functionstep_{index}", step=step - ) - for index, step in enumerate(steps) - ) return ( CompilationSession.from_context( context=ProcessingContext( @@ -1422,11 +1400,12 @@ def session_for( }, axis_id="A01", ), - steps=steps, orchestrator=SimpleNamespace(pipeline_config=PipelineConfig()), global_config=GlobalPipelineConfig(), - step_state_map={index: object() for index in range(len(steps))}, - snapshots=snapshots, + pipeline=ResolvedPipelineDefinition( + steps=steps, + step_state_map={index: object() for index in range(len(steps))}, + ), ), steps, ) diff --git a/tests/unit/test_materialization_filter_inheritance.py b/tests/unit/test_materialization_filter_inheritance.py index fb52726433..e79d6ada7b 100644 --- a/tests/unit/test_materialization_filter_inheritance.py +++ b/tests/unit/test_materialization_filter_inheritance.py @@ -67,8 +67,7 @@ def _resolved_checkpoint( ) filters = PipelineCompiler._resolve_global_step_axis_filters( orchestrator, - resolved.snapshots, - resolved.step_state_map, + resolved, ) return orchestrator, scope_id, pipeline_config_state, resolved, filters @@ -81,8 +80,8 @@ def test_path_zero_keeps_inherited_step_checkpoint_independent(tmp_path) -> None ) try: - snapshot = resolved.snapshots[0] - checkpoint_config = snapshot.step.step_materialization_config + snapshot = resolved.steps[0] + checkpoint_config = snapshot.step_materialization_config assert checkpoint_config.well_filter == 0 assert filters[0].allows(checkpoint_config, "A01") assert not filters[0].allows(checkpoint_config, "B02") @@ -97,7 +96,7 @@ def test_path_zero_keeps_inherited_step_checkpoint_independent(tmp_path) -> None ) ) assert ( - materialization_stage.materialized_output_dir_for_step(snapshot) + materialization_stage.materialized_output_dir_for_step(snapshot, 0) == checkpoint_dir ) @@ -154,7 +153,7 @@ def test_explicit_checkpoint_filter_overrides_inherited_workload(tmp_path) -> No ) try: - checkpoint_config = resolved.snapshots[0].step.step_materialization_config + checkpoint_config = resolved.steps[0].step_materialization_config assert checkpoint_config.well_filter == "B02" assert not filters[0].allows(checkpoint_config, "A01") assert filters[0].allows(checkpoint_config, "B02") diff --git a/tests/unit/test_path_planner_materialization.py b/tests/unit/test_path_planner_materialization.py index 95f207c078..9545224acb 100644 --- a/tests/unit/test_path_planner_materialization.py +++ b/tests/unit/test_path_planner_materialization.py @@ -1,3 +1,4 @@ +from openhcs.core.pipeline.compilation_session import ResolvedPipelineDefinition from dataclasses import dataclass, replace from pathlib import Path from types import SimpleNamespace @@ -87,7 +88,7 @@ PathPlannerStepAssemblyStage, PathPlannerValidationStage, ) -from openhcs.core.pipeline.step_snapshot import StepSnapshot +from openhcs.core.steps.abstract import AbstractStep from openhcs.core.artifact_key_selection import AdapterRecordedArtifactOutputPolicy from openhcs.core.runtime_adapters import runtime_adapter from openhcs.core.runtime_object_labels import ObjectLabelValue @@ -191,16 +192,28 @@ def resolve(self, handler, plate_path): def _compile_metadata_pattern(planner, pattern): - snapshot = _snapshot(func=pattern) - declarations, pattern, _, _ = planner.artifacts.prepare_step_declarations(snapshot) + snapshot = _resolved_step(func=pattern) + declarations, pattern, _, _ = planner.artifacts.prepare_step_declarations( + snapshot, 3 + ) input_plans = planner.artifacts.process_artifact_inputs( - declarations, snapshot.index, PathPlannerGroupScope.ungrouped(), - EMPTY_SOURCE_BINDINGS, ComponentSet(), snapshot.step.name, + declarations, + 3, + PathPlannerGroupScope.ungrouped(), + EMPTY_SOURCE_BINDINGS, + ComponentSet(), + snapshot.name, execution_scope=FunctionStepExecutionScope.AXIS, ) compiled = planner.artifacts.build_step_compiled_function_pattern( - snapshot, True, planner.artifacts.inject_metadata(pattern, declarations.inputs), - input_plans, {}, {}, PathPlannerGroupScope.ungrouped(), + snapshot, + 3, + True, + planner.artifacts.inject_metadata(pattern, declarations.inputs), + input_plans, + {}, + {}, + PathPlannerGroupScope.ungrouped(), ) return compiled, input_plans @@ -278,10 +291,8 @@ def process(self, context, step_index: int) -> None: del context, step_index -def _snapshot( +def _resolved_step( *, - index: int = 3, - scope_id: str | None = None, name: str = "step", is_function_step: bool = True, func=None, @@ -291,7 +302,7 @@ def _snapshot( input_source: InputSource = InputSource.PREVIOUS_STEP, processing_config: ProcessingConfig | None = None, step_materialization_config=None, -) -> StepSnapshot: +) -> AbstractStep: if func is None: def passthrough(image): @@ -316,11 +327,7 @@ def passthrough(image): if is_function_step else _NonFunctionStep(**step_kwargs) ) - return StepSnapshot( - index=index, - scope_id=scope_id or f"plate::functionstep_{index}", - step=step, - ) + return step def test_metadata_satisfied_artifact_input_compiles_without_runtime_plan(): @@ -341,7 +348,7 @@ def metadata_consumer(image, grid_dimensions): ) planner.ctx.plate_path = planner.plate_path pattern = (metadata_consumer, {"grid_dimensions": None}) - snapshot = _snapshot( + snapshot = _resolved_step( func=pattern, source_bindings=StepSourceBindingsConfig( enabled=True, @@ -349,7 +356,7 @@ def metadata_consumer(image, grid_dimensions): ), ) declarations, pattern, _, contracts = planner.artifacts.prepare_step_declarations( - snapshot + snapshot, 3 ) execution_bindings = planner.artifacts.source_bindings_for_contracts( snapshot, @@ -367,16 +374,17 @@ def metadata_consumer(image, grid_dimensions): ) runtime_input_plans = planner.artifacts.process_artifact_inputs( declarations, - snapshot.index, + 3, PathPlannerGroupScope.ungrouped(), execution_bindings, ComponentSet(), - snapshot.step.name, + snapshot.name, execution_scope=FunctionStepExecutionScope.AXIS, ) compiled = planner.artifacts.build_step_compiled_function_pattern( snapshot, + 3, True, planner.artifacts.inject_metadata(pattern, declarations.inputs), runtime_input_plans, @@ -587,7 +595,7 @@ def export_measurements(*, artifact_batch: RuntimeArtifactBatch): planner.artifact_context, available_artifacts=ArtifactSpecCollection((measurements,)), ) - snapshot = _snapshot( + snapshot = _resolved_step( func=export_measurements, name="PlateExporter", source_bindings=StepSourceBindingsConfig( @@ -603,7 +611,7 @@ def export_measurements(*, artifact_batch: RuntimeArtifactBatch): pattern, callable_scope, contracts, - ) = planner.artifacts.prepare_step_declarations(snapshot) + ) = planner.artifacts.prepare_step_declarations(snapshot, 3) execution_bindings = planner.artifacts.source_bindings_for_contracts( snapshot, contracts, @@ -611,7 +619,7 @@ def export_measurements(*, artifact_batch: RuntimeArtifactBatch): ) maps = planner.artifacts.compile_plan_maps( snapshot, - snapshot.index, + 3, declarations, PathPlannerGroupScope.ungrouped(), callable_scope, @@ -619,6 +627,7 @@ def export_measurements(*, artifact_batch: RuntimeArtifactBatch): ) compiled = planner.artifacts.build_step_compiled_function_pattern( snapshot, + 3, True, pattern, maps.inputs, @@ -626,8 +635,8 @@ def export_measurements(*, artifact_batch: RuntimeArtifactBatch): maps.relation_source_scopes, maps.group_scope, ) - plan = planner.plans[snapshot.index] - plan.step_name = snapshot.step.name + plan = planner.plans[3] + plan.step_name = snapshot.name plan.func = pattern plan.main_input_dependency = StepInputDependency.no_main_flow() plan.artifact_inputs = maps.inputs @@ -760,7 +769,8 @@ def __call__(self, invocation, step_context): match="Conflicting compiled invocation output artifact sidecar role", ): planner.artifacts.build_step_compiled_function_pattern( - _snapshot(func=pattern), + _resolved_step(func=pattern), + 0, True, pattern, {}, @@ -789,10 +799,8 @@ def test_materialization_collision_updates_results_dir_and_config(): materialization_config=PathConfigStub(sub_dir="images"), ) } - snapshot = _snapshot( - index=3, - name="materialize", - step_materialization_config=PathConfigStub(sub_dir="images"), + snapshot = _resolved_step( + name="materialize", step_materialization_config=PathConfigStub(sub_dir="images") ) planner.paths = PathPlannerPathAuthority(planner) @@ -805,7 +813,7 @@ def test_materialization_collision_updates_results_dir_and_config(): "main flow", ) - assert snapshot.step.step_materialization_config.sub_dir == "images" + assert snapshot.step_materialization_config.sub_dir == "images" materialized_output = planner.plans[3].materialized_output assert materialized_output.output_dir == Path("/data/plate1_processed/images_step3") assert materialized_output.sub_dir == "images_step3" @@ -855,8 +863,7 @@ def test_same_name_typed_artifacts_compile_through_producer_and_consumer_plans() def produce_shared(): return None - producer_snapshot = _snapshot( - index=2, + producer_snapshot = _resolved_step( name="produce_shared", func=produce_shared, group_by=GroupBy.NONE, @@ -868,10 +875,10 @@ def produce_shared(): producer_pattern, producer_scope, _producer_contracts, - ) = planner.artifacts.prepare_step_declarations(producer_snapshot) + ) = planner.artifacts.prepare_step_declarations(producer_snapshot, 2) producer_maps = planner.artifacts.compile_plan_maps( producer_snapshot, - producer_snapshot.index, + 2, producer_declarations, PathPlannerGroupScope.ungrouped(), producer_scope, @@ -879,6 +886,7 @@ def produce_shared(): ) producer_compiled = planner.artifacts.build_step_compiled_function_pattern( producer_snapshot, + 2, True, producer_pattern, producer_maps.inputs, @@ -920,8 +928,7 @@ def consume_shared(image, shared_image, shared_labels): del shared_image, shared_labels return image - consumer_snapshot = _snapshot( - index=3, + consumer_snapshot = _resolved_step( name="consume_shared", func=consume_shared, group_by=GroupBy.NONE, @@ -932,10 +939,10 @@ def consume_shared(image, shared_image, shared_labels): consumer_pattern, consumer_scope, _consumer_contracts, - ) = planner.artifacts.prepare_step_declarations(consumer_snapshot) + ) = planner.artifacts.prepare_step_declarations(consumer_snapshot, 3) consumer_maps = planner.artifacts.compile_plan_maps( consumer_snapshot, - consumer_snapshot.index, + 3, consumer_declarations, PathPlannerGroupScope.ungrouped(), consumer_scope, @@ -943,6 +950,7 @@ def consume_shared(image, shared_image, shared_labels): ) consumer_compiled = planner.artifacts.build_step_compiled_function_pattern( consumer_snapshot, + 3, True, consumer_pattern, consumer_maps.inputs, @@ -1181,14 +1189,6 @@ def test_implicit_native_main_flow_provenance_drives_artifact_owned_scope(): processing_config=processing_config, ), ) - snapshots = tuple( - StepSnapshot( - index=index, - scope_id=f"implicit-main-flow::functionstep_{index}", - step=step, - ) - for index, step in enumerate(steps) - ) session = CompilationSession.from_context( context=ProcessingContext( step_plans={ @@ -1202,11 +1202,11 @@ def test_implicit_native_main_flow_provenance_drives_artifact_owned_scope(): }, axis_id="A01", ), - steps=steps, orchestrator=SimpleNamespace(pipeline_config=PipelineConfig()), global_config=GlobalPipelineConfig(), - step_state_map={index: object() for index in range(len(steps))}, - snapshots=snapshots, + pipeline=ResolvedPipelineDefinition( + steps=steps, step_state_map={index: object() for index in range(len(steps))} + ), ) provider = CellProfilerInvocationContractProviderFactory.provider_for_session( session @@ -1274,7 +1274,7 @@ def test_implicit_native_main_flow_provenance_drives_artifact_owned_scope(): assert planner.declared == {} execution_scope = planner.execution_groups.get_execution_groups( - snapshots[1], + steps[1], PathPlannerComponentScopes.empty(), contracts=(consumer_contract,), ) @@ -1309,7 +1309,7 @@ def test_implicit_native_main_flow_provenance_drives_artifact_owned_scope(): edge = next(compiled_consumer.iter_invocations()).artifact_input_edges[0] assert edge.spec == cursor assert edge.storage_plan is None - assert (edge.main_flow_projection is not None) + assert edge.main_flow_projection is not None def test_artifact_output_source_uses_compiled_plan_across_parameter_occurrences(): @@ -1573,10 +1573,8 @@ def identify(main_image, Stain1): producer_step_name="Align", ), ) - snapshot = _snapshot( - name="IdentifyPrimaryObjects", - func=identify, - source_bindings=source_bindings, + snapshot = _resolved_step( + name="IdentifyPrimaryObjects", func=identify, source_bindings=source_bindings ) declarations = extract_artifact_declarations(identify) execution_scope = planner.execution_groups.get_execution_groups( @@ -1599,6 +1597,7 @@ def identify(main_image, Stain1): ) compiled = planner.artifacts.build_step_compiled_function_pattern( snapshot, + 3, True, identify, maps.inputs, @@ -1962,7 +1961,7 @@ def test_declared_group_lineage_scopes_outputs_without_rewriting_execution(): ) maps = planner.artifacts.compile_plan_maps( - _snapshot(name="FilterObjects"), + _resolved_step(name="FilterObjects"), 3, declarations, PathPlannerGroupScope.from_raw(("2",), component=AllComponents.CHANNEL), @@ -2014,7 +2013,7 @@ def test_declared_group_lineage_uses_main_flow_scope_without_artifact_plan(): ) maps = planner.artifacts.compile_plan_maps( - _snapshot(name="MeasureColocalization"), + _resolved_step(name="MeasureColocalization"), 3, declarations, group_scope, @@ -2065,7 +2064,7 @@ def test_prior_main_flow_artifact_scopes_output_without_rewriting_execution(): ) maps = planner.artifacts.compile_plan_maps( - _snapshot(name="IdentifyPrimaryObjects"), + _resolved_step(name="IdentifyPrimaryObjects"), 3, declarations, PathPlannerGroupScope.from_raw( @@ -2107,7 +2106,7 @@ def test_dict_invocation_lineage_uses_its_non_plan_input_group_scope(): ) maps = planner.artifacts.compile_plan_maps( - _snapshot(name="IdentifyPrimaryObjects"), + _resolved_step(name="IdentifyPrimaryObjects"), 3, declarations, PathPlannerGroupScope.from_raw( @@ -2177,7 +2176,7 @@ def test_measurement_output_scope_compiles_exact_cross_group_consumer_edge(): ) maps = planner.artifacts.compile_plan_maps( - _snapshot(name="MeasureObjectIntensity"), + _resolved_step(name="MeasureObjectIntensity"), 2, declarations, PathPlannerGroupScope.from_raw( @@ -2212,7 +2211,7 @@ def filter_objects(main_image, MeasureObjectIntensity_3_measurements): DEFAULT_GROUP_KEY, 0, ) - consumer_snapshot = _snapshot( + consumer_snapshot = _resolved_step( name="FilterObjects", func=filter_objects, group_by=GroupBy.CHANNEL, @@ -2236,6 +2235,7 @@ def filter_objects(main_image, MeasureObjectIntensity_3_measurements): ) compiled = planner.artifacts.build_step_compiled_function_pattern( consumer_snapshot, + 3, True, filter_objects, consumer_maps.inputs, @@ -2313,7 +2313,7 @@ def test_artifact_managed_lineage_keeps_exact_named_inputs_in_one_invocation(): ) maps = planner.artifacts.compile_plan_maps( - _snapshot(name="MeasureObjectSizeShape"), + _resolved_step(name="MeasureObjectSizeShape"), 3, declarations, PathPlannerGroupScope.ungrouped(), @@ -2366,7 +2366,7 @@ def test_artifact_managed_single_source_output_retains_source_group_scope(): ) maps = planner.artifacts.compile_plan_maps( - _snapshot(name="IdentifyPrimaryObjects"), + _resolved_step(name="IdentifyPrimaryObjects"), 3, declarations, PathPlannerGroupScope.ungrouped(), @@ -2432,7 +2432,7 @@ def test_declared_group_lineage_unions_compatible_source_groups(): ) maps = planner.artifacts.compile_plan_maps( - _snapshot(name="Measure"), + _resolved_step(name="Measure"), 3, declarations, PathPlannerGroupScope.from_raw( @@ -2516,7 +2516,7 @@ def test_collected_lineage_outputs_use_relation_owned_group_scope(): ) maps = planner.artifacts.compile_plan_maps( - _snapshot( + _resolved_step( name="MeasureColocalization", group_by=GroupBy.SITE, variable_components=(VariableComponents.CHANNEL,), @@ -2590,7 +2590,7 @@ def test_output_lineage_uses_input_qualified_consumer_scope(): ) maps = planner.artifacts.compile_plan_maps( - _snapshot(name="Calculate"), + _resolved_step(name="Calculate"), 3, declarations, PathPlannerGroupScope.from_raw( @@ -2618,12 +2618,18 @@ def test_planner_derived_group_lineage_selects_exact_managed_invocation(): blue_measurements = ArtifactSpec.output( "Measurements", MeasurementsArtifactType, - relations=(GroupLineageSourceRelation(blue.ref()), ArtifactMeasurementSubjectRelation()), + relations=( + GroupLineageSourceRelation(blue.ref()), + ArtifactMeasurementSubjectRelation(), + ), ) green_measurements = ArtifactSpec.output( "Measurements", MeasurementsArtifactType, - relations=(GroupLineageSourceRelation(green.ref()), ArtifactMeasurementSubjectRelation()), + relations=( + GroupLineageSourceRelation(green.ref()), + ArtifactMeasurementSubjectRelation(), + ), ) @artifact_inputs(blue) @@ -2662,7 +2668,7 @@ def measure_green(image, *, runtime): ), ) planner = _artifact_planner_stub() - snapshot = _snapshot( + snapshot = _resolved_step( name="MeasureChannels", func=functions, source_bindings=source_bindings, @@ -2703,7 +2709,8 @@ def measure_green(image, *, runtime): channel: tuple( invocation.key.function_name for invocation in compiled.default_group.invocations - if invocation.output_plans_for_component(execution_scope, channel) is not None + if invocation.output_plans_for_component(execution_scope, channel) + is not None ) for channel in execution_scope.keys } == { @@ -2748,11 +2755,11 @@ def test_real_object_measurement_preserves_selected_labels_group_scope( if measurement_group == DEFAULT_GROUP_KEY else {measurement_group: [invocation_pattern]} ) - snapshot = _snapshot(name="MeasureCells", func=pattern) + snapshot = _resolved_step(name="MeasureCells", func=pattern) labels_input = ArtifactSpec.input("Cells", ObjectLabelsArtifactType) step_context = ArtifactDeclarationStepContext( - step_name=snapshot.step.name, - step_index=snapshot.index, + step_name=snapshot.name, + step_index=3, group_by=GroupBy.CHANNEL, available_artifacts=ArtifactSpecCollection((labels_input,)), available_artifact_producers=( @@ -2772,22 +2779,28 @@ def test_real_object_measurement_preserves_selected_labels_group_scope( (numbered_blocks,), _ = MeasureObjectSizeShapeModule.number_step_invocation_blocks( (blocks,), first_module_num=4 ) - contract, consumed_names = MeasureObjectSizeShapeModule.invocation_callable_contract( - invocation=authored, - numbered_module_blocks=numbered_blocks, - consumed_kwarg_names=consumed_names, - step_context=step_context, + contract, consumed_names = ( + MeasureObjectSizeShapeModule.invocation_callable_contract( + invocation=authored, + numbered_module_blocks=numbered_blocks, + consumed_kwarg_names=consumed_names, + step_context=step_context, + ) ) provider = CellProfilerInvocationContractProvider( - {(snapshot.index, authored.key): InvocationContractPlan(contract, consumed_names)} + {(3, authored.key): InvocationContractPlan(contract, consumed_names)} ) declarations = extract_artifact_declarations( pattern, invocation_contract_provider=provider, step_context=step_context, ) - (measurement,) = contract.artifact_outputs.of_artifact_type(MeasurementsArtifactType) - (object_input,) = contract.artifact_inputs.of_artifact_type(ObjectLabelsArtifactType) + (measurement,) = contract.artifact_outputs.of_artifact_type( + MeasurementsArtifactType + ) + (object_input,) = contract.artifact_inputs.of_artifact_type( + ObjectLabelsArtifactType + ) assert object_input.ref() == labels_input.ref() assert object_input.parameter_name == "labels" assert measurement.measurement_feature_owner is MeasureObjectSizeShapeModule @@ -2892,7 +2905,7 @@ def test_declared_group_lineage_cannot_rewrite_scalar_step_execution_scope(): ) maps = planner.artifacts.compile_plan_maps( - _snapshot(name="MaskImage"), + _resolved_step(name="MaskImage"), 3, declarations, PathPlannerGroupScope.from_raw(("1", "3"), component=AllComponents.CHANNEL), @@ -2981,7 +2994,7 @@ def test_artifact_output_storage_scope_is_independent_of_execution_scope(): ) maps = planner.artifacts.compile_plan_maps( - _snapshot(name="CalculateMath"), + _resolved_step(name="CalculateMath"), 3, declarations, PathPlannerGroupScope.from_raw(("3",), component=AllComponents.CHANNEL), @@ -3049,7 +3062,7 @@ def test_each_output_storage_scope_is_independent_of_execution_scope(): ) maps = planner.artifacts.compile_plan_maps( - _snapshot(name="invocation-scoped-output"), + _resolved_step(name="invocation-scoped-output"), 3, declarations, PathPlannerGroupScope.from_raw( @@ -3125,7 +3138,7 @@ def test_dict_pattern_output_groups_do_not_drive_scalar_scope_narrowing(): ) maps = planner.artifacts.compile_plan_maps( - _snapshot(name="dict_pattern"), + _resolved_step(name="dict_pattern"), 3, declarations, PathPlannerGroupScope.from_raw(("1", "3"), component=AllComponents.CHANNEL), @@ -3169,7 +3182,7 @@ def test_source_binding_component_identity_narrows_declared_output_lineage(): ) maps = planner.artifacts.compile_plan_maps( - _snapshot(name="Watershed", source_bindings=source_bindings), + _resolved_step(name="Watershed", source_bindings=source_bindings), 3, declarations, PathPlannerGroupScope.from_raw( @@ -3216,7 +3229,7 @@ def test_source_binding_identity_scopes_outputs_without_execution_fanout(): ) maps = planner.artifacts.compile_plan_maps( - _snapshot(name="Watershed", source_bindings=source_bindings), + _resolved_step(name="Watershed", source_bindings=source_bindings), 3, declarations, PathPlannerGroupScope.ungrouped(), @@ -3282,7 +3295,7 @@ def test_image_object_outputs_keep_declared_image_execution_group_scope(): ) maps = planner.artifacts.compile_plan_maps( - _snapshot(name="MeasureObjectIntensity"), + _resolved_step(name="MeasureObjectIntensity"), 3, declarations, PathPlannerGroupScope.from_raw( @@ -3335,7 +3348,7 @@ def test_group_lineage_source_resolves_prior_main_flow_output_without_store_inpu ) maps = planner.artifacts.compile_plan_maps( - _snapshot(name="FilterObjects"), + _resolved_step(name="FilterObjects"), 3, declarations, PathPlannerGroupScope.from_raw( @@ -3370,13 +3383,12 @@ def declared_artifact_owner(image): planner = _artifact_planner_stub() planner.declaration_provider = declarations_for_invocation - snapshot = _snapshot( + snapshot = _resolved_step( is_function_step=True, func=(identify, {"artifact_name": "cells"}), group_by=GroupBy.NONE, variable_components=(VariableComponents.SITE,), name="identify_cells", - index=2, source_bindings=EMPTY_SOURCE_BINDINGS, input_source=InputSource.PREVIOUS_STEP, ) @@ -3388,6 +3400,7 @@ def declared_artifact_owner(image): _contracts, ) = planner.artifacts.prepare_step_declarations( snapshot, + 2, ) assert execution_scope is FunctionStepExecutionScope.AXIS output_plan = ArtifactOutputPlan( @@ -3397,6 +3410,7 @@ def declared_artifact_owner(image): ) compiled = planner.artifacts.build_step_compiled_function_pattern( snapshot, + 2, True, func_pattern, {}, @@ -3427,13 +3441,12 @@ def filter_objects(image, *, runtime): return image planner = _artifact_planner_stub() - snapshot = _snapshot( + snapshot = _resolved_step( is_function_step=True, func=filter_objects, group_by=GroupBy.CHANNEL, variable_components=(VariableComponents.SITE,), name="FilterObjects", - index=2, source_bindings=EMPTY_SOURCE_BINDINGS, input_source=InputSource.PREVIOUS_STEP, ) @@ -3449,6 +3462,7 @@ def filter_objects(image, *, runtime): _declarations, _pattern, execution_scope, _contracts = ( planner.artifacts.prepare_step_declarations( snapshot, + 2, ) ) execution_groups = planner.execution_groups.get_execution_groups( @@ -3462,13 +3476,14 @@ def filter_objects(image, *, runtime): ) assert execution_scope is FunctionStepExecutionScope.AXIS - snapshot.step.func = { + snapshot.func = { "1": [filter_objects], "2": [filter_objects], } _declarations, _pattern, execution_scope, _contracts = ( planner.artifacts.prepare_step_declarations( snapshot, + 2, ) ) execution_groups = planner.execution_groups.get_execution_groups( @@ -3566,13 +3581,12 @@ def identify_primary_objects(image, *, runtime): ), ), ) - snapshot = _snapshot( + snapshot = _resolved_step( is_function_step=True, func=identify_primary_objects, group_by=GroupBy.CHANNEL, variable_components=(VariableComponents.SITE,), name="IdentifyPrimaryObjects", - index=2, source_bindings=EMPTY_SOURCE_BINDINGS, input_source=InputSource.PREVIOUS_STEP, ) @@ -3623,13 +3637,12 @@ def relate_objects(image, *, runtime): group_component=AllComponents.CHANNEL, ), ) - snapshot = _snapshot( + snapshot = _resolved_step( is_function_step=True, func=relate_objects, group_by=GroupBy.CHANNEL, variable_components=(VariableComponents.SITE,), name="RelateObjects", - index=2, source_bindings=EMPTY_SOURCE_BINDINGS, input_source=InputSource.PREVIOUS_STEP, ) @@ -3678,13 +3691,12 @@ def measure_object_size_shape(image, *, runtime): group_component=AllComponents.SITE, ), ) - snapshot = _snapshot( + snapshot = _resolved_step( is_function_step=True, func=measure_object_size_shape, group_by=GroupBy.CHANNEL, variable_components=(VariableComponents.SITE,), name="MeasureObjectSizeShape", - index=2, source_bindings=EMPTY_SOURCE_BINDINGS, input_source=InputSource.PREVIOUS_STEP, ) @@ -3711,7 +3723,7 @@ def measure_object_size_shape(image, *, runtime): def test_execution_groups_resolve_non_grouped_variable_component_conflicts(): planner = _artifact_planner_stub() - snapshot = _snapshot( + snapshot = _resolved_step( is_function_step=True, func=lambda image: image, group_by=GroupBy.CHANNEL, @@ -3733,7 +3745,7 @@ def test_non_dict_group_by_declares_dynamic_scope_without_plate_key_lookup(): "non-dict group_by must not request plate component keys" ) ) - source_snapshot = _snapshot( + source_snapshot = _resolved_step( is_function_step=True, func=lambda image: image, group_by=GroupBy.CHANNEL, @@ -3755,7 +3767,7 @@ def test_non_dict_group_by_declares_dynamic_scope_without_plate_key_lookup(): def test_non_dict_group_by_uses_dynamic_source_scope_for_pipeline_start(): planner = _artifact_planner_stub() - source_snapshot = _snapshot( + source_snapshot = _resolved_step( is_function_step=True, func=lambda image: image, group_by=GroupBy.CHANNEL, @@ -3777,7 +3789,7 @@ def test_non_dict_group_by_uses_dynamic_source_scope_for_pipeline_start(): def test_dict_pattern_group_by_declares_execution_group_component(): planner = _artifact_planner_stub() - snapshot = _snapshot( + snapshot = _resolved_step( is_function_step=True, func={ "1": lambda image: image, @@ -3802,7 +3814,7 @@ def test_dict_pattern_group_by_declares_execution_group_component(): def test_dict_pattern_rejects_group_by_none_execution_component(): planner = _artifact_planner_stub() - snapshot = _snapshot( + snapshot = _resolved_step( is_function_step=True, func={ "1": lambda image: image, @@ -3827,7 +3839,7 @@ def test_dict_pattern_rejects_group_by_none_execution_component(): def test_execution_groups_reject_grouped_group_by_axis_conflict(): planner = _artifact_planner_stub() - composite_snapshot = _snapshot( + composite_snapshot = _resolved_step( is_function_step=True, func={"1": lambda image: image}, group_by=GroupBy.CHANNEL, @@ -3856,7 +3868,7 @@ def test_non_dict_group_by_preserves_explicitly_collapsed_input_axis(): ), } ) - snapshot = _snapshot( + snapshot = _resolved_step( is_function_step=True, func=lambda image: image, group_by=GroupBy.CHANNEL, @@ -3912,7 +3924,7 @@ def measurement_only(image, *, runtime): VariableComponents.SITE: PathPlannerGroupScope.ungrouped(), } ) - snapshot = _snapshot( + snapshot = _resolved_step( is_function_step=True, variable_components=(VariableComponents.CHANNEL,), group_by=GroupBy.SITE, @@ -3963,7 +3975,7 @@ def image_output(image): VariableComponents.SITE: PathPlannerGroupScope.ungrouped(), } ) - snapshot = _snapshot( + snapshot = _resolved_step( is_function_step=True, variable_components=(VariableComponents.CHANNEL,), group_by=GroupBy.SITE, @@ -3997,7 +4009,7 @@ def test_non_dict_group_by_namespaces_artifact_outputs_with_dynamic_component(): ), ) ) - snapshot = _snapshot( + snapshot = _resolved_step( is_function_step=True, func=lambda image: image, group_by=GroupBy.CHANNEL, @@ -4026,7 +4038,7 @@ def test_non_dict_group_by_namespaces_artifact_outputs_with_dynamic_component(): def test_non_dict_group_by_uses_source_binding_identity_for_pipeline_start_scope(): planner = _artifact_planner_stub() - snapshot = _snapshot( + snapshot = _resolved_step( is_function_step=True, func=lambda image: image, group_by=GroupBy.CHANNEL, @@ -4099,12 +4111,14 @@ def save_declared_payload(image): component_identity=(ComponentSelector(AllComponents.CHANNEL, "3"),), ) ) - snapshot = _snapshot( + snapshot = _resolved_step( is_function_step=True, func=save_declared_payload, group_by=GroupBy.CHANNEL, variable_components=(VariableComponents.SITE,), - source_bindings=StepSourceBindingsConfig(enabled=True, bindings=tuple(bindings)), + source_bindings=StepSourceBindingsConfig( + enabled=True, bindings=tuple(bindings) + ), input_source=InputSource.PREVIOUS_STEP, ) @@ -4139,7 +4153,7 @@ def test_main_flow_source_anchor_restricts_execution_to_its_exact_channel(): ), ), ) - snapshot = _snapshot( + snapshot = _resolved_step( is_function_step=True, func=lambda image: image, group_by=GroupBy.CHANNEL, @@ -4226,7 +4240,7 @@ def measure(image, CropBlue): producer_step_name="Crop", ), ) - snapshot = _snapshot( + snapshot = _resolved_step( name="Measure", func=measure, group_by=GroupBy.SITE, @@ -4246,6 +4260,7 @@ def measure(image, CropBlue): ) compiled = planner.artifacts.build_step_compiled_function_pattern( snapshot, + 3, True, measure, maps.inputs, @@ -4302,9 +4317,8 @@ def test_execution_anchor_ignores_source_artifact_lineage(): ), ), ) - snapshot = _snapshot( - source_bindings=source_bindings, - input_source=InputSource.PIPELINE_START, + snapshot = _resolved_step( + source_bindings=source_bindings, input_source=InputSource.PIPELINE_START ) source_contract = CallableContract.from_callable(lambda image: image) source_contract = replace( @@ -4554,7 +4568,7 @@ def test_artifact_graph_output_groups_validates_exact_partial_map() -> None: def test_non_dict_group_by_ignores_source_binding_identity_for_other_components(): planner = _artifact_planner_stub() - snapshot = _snapshot( + snapshot = _resolved_step( is_function_step=True, func=lambda image: image, group_by=GroupBy.SITE, @@ -4593,13 +4607,12 @@ def test_compiled_group_by_preserves_dynamic_execution_scope(): planner.plans[3].group_by = GroupBy.CHANNEL planner.plans[3].variable_components = (VariableComponents.SITE,) planner.plans[3].func = lambda image: image - snapshot = _snapshot( + snapshot = _resolved_step( is_function_step=True, func=lambda image: image, group_by=GroupBy.CHANNEL, variable_components=(VariableComponents.SITE,), name="measure_after_channel_collapse", - scope_id="plate::functionstep_3", input_source=InputSource.PREVIOUS_STEP, ) artifact_maps = ArtifactPlanMaps( @@ -4899,7 +4912,7 @@ def consume(main_image, image): ) ) - snapshot = _snapshot( + snapshot = _resolved_step( name="consumer", func=consume, group_by=GroupBy.NONE, @@ -4915,6 +4928,7 @@ def consume(main_image, image): with pytest.raises(ValueError, match="no exact relation-owned selection"): planner.artifacts.build_step_compiled_function_pattern( snapshot, + 3, True, consume, maps.inputs, @@ -4957,7 +4971,7 @@ def consume(main_image, image): ) ) - snapshot = _snapshot( + snapshot = _resolved_step( name="consumer", func=consume, group_by=GroupBy.NONE, @@ -4975,6 +4989,7 @@ def consume(main_image, image): assert producer_scope.component is AllComponents.CHANNEL compiled = planner.artifacts.build_step_compiled_function_pattern( snapshot, + 3, True, consume, maps.inputs, @@ -5075,8 +5090,7 @@ def produce(image, source_value): enabled=True, bindings=(NamedSourceBinding(alias=alias),), ) - snapshot = _snapshot( - index=step_index, + snapshot = _resolved_step( name=f"produce_{output_name}", func=produce, source_bindings=source_bindings, @@ -5137,7 +5151,7 @@ def consume(image, Nuclei, PH3): del Nuclei, PH3 return image - snapshot = _snapshot(index=4, name="consume", func=consume) + snapshot = _resolved_step(name="consume", func=consume) declarations = planner.artifacts.namespace_grouped_outputs_for_runtime_consumers( consume, extract_artifact_declarations(consume), @@ -5735,8 +5749,8 @@ def test_main_input_dependency_uses_scope_identity_for_step_output_edges(): ), } snapshots_by_index = { - 0: _snapshot(scope_id="plate::functionstep_0"), - 1: _snapshot(scope_id="plate::functionstep_1"), + 0: _resolved_step(), + 1: _resolved_step(), } planner.session = SimpleNamespace( snapshot=lambda index: snapshots_by_index[index], @@ -5744,7 +5758,7 @@ def test_main_input_dependency_uses_scope_identity_for_step_output_edges(): planner.steps = PathPlannerStepAssemblyStage(planner) dependency = planner.steps.main_input_dependency( - _snapshot(is_function_step=False), + _resolved_step(is_function_step=False), 1, ) @@ -5796,7 +5810,7 @@ def test_main_input_dependency_uses_declared_artifact_producer_not_previous_step ) dependency = planner.steps.main_input_dependency( - _snapshot( + _resolved_step( input_source=InputSource.PREVIOUS_STEP, is_function_step=True, name="Identify", @@ -5877,12 +5891,12 @@ def measure(image, *, runtime): ), } planner.session = SimpleNamespace( - snapshot=lambda index: _snapshot(scope_id=f"plate::functionstep_{index}"), + snapshot=lambda index: _resolved_step(), ) planner.steps = PathPlannerStepAssemblyStage(planner) dependency = planner.steps.main_input_dependency( - _snapshot(is_function_step=False), + _resolved_step(is_function_step=False), 2, ) @@ -5902,7 +5916,7 @@ def test_main_input_dependency_preserves_pipeline_start_edges(): } planner.initial_input = Path("/data/plate1/images") planner.session = SimpleNamespace( - snapshot=lambda index: {1: _snapshot(scope_id="plate::functionstep_1")}[index], + snapshot=lambda index: {1: _resolved_step()}[index], ) planner.paths = SimpleNamespace( build_output_path=lambda *_args, **_kwargs: Path( @@ -5912,10 +5926,7 @@ def test_main_input_dependency_preserves_pipeline_start_edges(): planner.steps = PathPlannerStepAssemblyStage(planner) dependency = planner.steps.main_input_dependency( - _snapshot( - input_source=InputSource.PIPELINE_START, - is_function_step=False, - ), + _resolved_step(input_source=InputSource.PIPELINE_START, is_function_step=False), 1, ) diff --git a/tests/unit/test_step_snapshot.py b/tests/unit/test_resolved_pipeline_definition.py similarity index 59% rename from tests/unit/test_step_snapshot.py rename to tests/unit/test_resolved_pipeline_definition.py index ddbf5ca4cb..acdf9a8983 100644 --- a/tests/unit/test_step_snapshot.py +++ b/tests/unit/test_resolved_pipeline_definition.py @@ -1,14 +1,9 @@ -from dataclasses import fields - +from openhcs.core.pipeline.compilation_session import ResolvedPipelineDefinition import pytest from openhcs.constants import VariableComponents from openhcs.constants.input_source import InputSource from openhcs.core.config import ProcessingConfig, StepMaterializationConfig -from openhcs.core.pipeline.step_snapshot import ( - StepSnapshot, - build_step_snapshots, -) from openhcs.core.source_bindings import ( ComponentSelector, NamedSourceBinding, @@ -27,10 +22,10 @@ def __init__(self, scope_id="plate::functionstep_0"): self.scope_id = scope_id def to_object(self): - raise AssertionError("StepSnapshot must not call ObjectState.to_object()") + raise AssertionError("Resolved pipeline must not call ObjectState.to_object()") -def test_step_snapshot_reads_semantics_from_resolved_step_without_object_conversion(): +def test_resolved_pipeline_reads_steps_without_object_conversion(): source_bindings = StepSourceBindingsConfig( bindings=( NamedSourceBinding( @@ -54,24 +49,20 @@ def test_step_snapshot_reads_semantics_from_resolved_step_without_object_convers ) state = StateStub() - snapshot = build_step_snapshots([step], {0: state})[0] - - assert [field.name for field in fields(StepSnapshot)] == [ - "index", - "scope_id", - "step", - ] - assert snapshot.scope_id == "plate::functionstep_0" - assert snapshot.step is step - assert snapshot.step.source_bindings is source_bindings - assert snapshot.step.processing_config.input_source is InputSource.PIPELINE_START - assert snapshot.step.processing_config.variable_components == [ + pipeline = ResolvedPipelineDefinition([step], {0: state}) + assert pipeline.steps[0] is step + assert pipeline.step_state_map[0] is state + assert pipeline.steps[0].source_bindings is source_bindings + assert ( + pipeline.steps[0].processing_config.input_source is InputSource.PIPELINE_START + ) + assert pipeline.steps[0].processing_config.variable_components == [ VariableComponents.SITE ] -def test_build_step_snapshots_requires_matching_objectstate(): +def test_resolved_pipeline_requires_matching_objectstate(): step = FunctionStep(func=_identity, name="missing") - with pytest.raises(ValueError, match="Missing ObjectState"): - build_step_snapshots([step], {}) + with pytest.raises(ValueError, match="missing ObjectState"): + ResolvedPipelineDefinition([step], {}) diff --git a/tests/unit/test_source_bindings.py b/tests/unit/test_source_bindings.py index f66ce533d4..12ad215366 100644 --- a/tests/unit/test_source_bindings.py +++ b/tests/unit/test_source_bindings.py @@ -32,7 +32,6 @@ LazyStepSourceBindingsConfig, PipelineConfig, ) -from openhcs.core.pipeline.step_snapshot import StepSnapshot from openhcs.core.runtime_tabular_values import FieldSpec from openhcs.core.source_binding_selection import ( SourceBindingCandidateMatcher, @@ -429,22 +428,16 @@ def test_step_source_bindings_inherit_plate_source_bindings_for_snapshot(): exclude_params=["func"], ) ObjectStateRegistry.register(step_state, _skip_snapshot=True) - snapshot = StepSnapshot( - index=0, - scope_id=step_state.scope_id, - step=step_state.to_saved_resolved_object(), - ) + snapshot = step_state.to_saved_resolved_object() finally: ObjectStateRegistry.clear() - assert snapshot.step.source_bindings.bindings == (binding,) - assert snapshot.step.source_bindings.source_stack_components == ( - AllComponents.Z_INDEX, - ) - assert snapshot.step.source_bindings.grouping_metadata_fields == ("Plate",) - assert snapshot.step.source_bindings.metadata_fields == metadata_fields + assert snapshot.source_bindings.bindings == (binding,) + assert snapshot.source_bindings.source_stack_components == (AllComponents.Z_INDEX,) + assert snapshot.source_bindings.grouping_metadata_fields == ("Plate",) + assert snapshot.source_bindings.metadata_fields == metadata_fields compiled = CompiledSourceBindingPlan.from_config( - snapshot.step.source_bindings, + snapshot.source_bindings, ) assert compiled.bindings == (binding,) assert compiled.source_stack_components == (AllComponents.Z_INDEX,) @@ -516,17 +509,13 @@ def test_enabled_step_source_bindings_compile_inherited_bindings(): exclude_params=["func"], ) ObjectStateRegistry.register(step_state, _skip_snapshot=True) - snapshot = StepSnapshot( - index=0, - scope_id=step_state.scope_id, - step=step_state.to_saved_resolved_object(), - ) + snapshot = step_state.to_saved_resolved_object() finally: ObjectStateRegistry.clear() - assert snapshot.step.source_bindings.bindings == (binding,) + assert snapshot.source_bindings.bindings == (binding,) assert CompiledSourceBindingPlan.from_config( - snapshot.step.source_bindings, + snapshot.source_bindings, ).bindings == (binding,) @@ -554,18 +543,14 @@ def test_pipeline_step_source_bindings_enabled_inherits_to_function_steps(): exclude_params=["func"], ) ObjectStateRegistry.register(step_state, _skip_snapshot=True) - snapshot = StepSnapshot( - index=0, - scope_id=step_state.scope_id, - step=step_state.to_saved_resolved_object(), - ) + snapshot = step_state.to_saved_resolved_object() finally: ObjectStateRegistry.clear() - assert snapshot.step.source_bindings.enabled is True - assert snapshot.step.source_bindings.bindings == (binding,) + assert snapshot.source_bindings.enabled is True + assert snapshot.source_bindings.bindings == (binding,) assert CompiledSourceBindingPlan.from_config( - snapshot.step.source_bindings, + snapshot.source_bindings, ).bindings == (binding,) From 15c99a53337a948c87d41ba91df82f812fb796d9 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 21:51:22 -0400 Subject: [PATCH 192/235] Resolve source universes on their existing nominal request owners Refs #384. Remove the twelve-class secondary strategy family and its registry. Existing step-input and pipeline-start request roles own their irreducible source scopes, sharing the current-axis resolution algorithm. Preserve source-backend selection, virtual positions, runtime-state contribution order and caching. Qualify the physical TIFF calibration/live-buffer and virtual selected-source consumers; no runtime improvement is claimed. --- ...erience_implementation_dry_run_20260629.md | 8 +- ..._artifact_source_coverage_plan_20260629.md | 10 +- openhcs/core/source_binding_selection.py | 257 +++--------------- tests/unit/test_function_io.py | 1 + 4 files changed, 50 insertions(+), 226 deletions(-) diff --git a/docs/plans/mcp_agent_experience_implementation_dry_run_20260629.md b/docs/plans/mcp_agent_experience_implementation_dry_run_20260629.md index f532b097cd..12b777b786 100644 --- a/docs/plans/mcp_agent_experience_implementation_dry_run_20260629.md +++ b/docs/plans/mcp_agent_experience_implementation_dry_run_20260629.md @@ -33,12 +33,11 @@ show("VirtualWorkspaceSourceProjection methods", tuple(name for name in ("pipeli from openhcs.core.compiled_step_plan import CompiledStepPlan from openhcs.core.source_bindings import CompiledSourceUniversePlan from openhcs.core.source_load_plan import SourceLoadPlan -from openhcs.core.source_binding_selection import SourceUniverseRequest, SourceUniverseStrategy +from openhcs.core.source_binding_selection import SourceUniverseRequest show("CompiledStepPlan source/artifact fields", tuple(f.name for f in fields(CompiledStepPlan) if f.name.startswith("source_") or f.name.startswith("artifact_"))) show("CompiledSourceUniversePlan fields", tuple(f.name for f in fields(CompiledSourceUniversePlan))) show("SourceLoadPlan fields", tuple(f.name for f in fields(SourceLoadPlan))) show("SourceUniverseRequest types", tuple(t.__name__ for t in SourceUniverseRequest.registered_request_types())) -show("SourceUniverseStrategy types", tuple(t.__name__ for t in SourceUniverseStrategy.registered_strategy_types())) from openhcs.core.artifacts import ArtifactSidecarRole, ArtifactSpec, ArtifactInputPlan, ArtifactOutputPlan show("ArtifactSidecarRole values", tuple(role.value for role in ArtifactSidecarRole)) @@ -105,10 +104,11 @@ Artifact/source coverage: richer source-load semantics until the compiler owns them. - `SourceUniverseRequest.registered_request_types()` returns `StepInputSourceUniverseRequest` and `PipelineStartSourceUniverseRequest`. -- `SourceUniverseStrategy.registered_strategy_types()` returns six concrete +- The historical independent source-universe strategy registry returned six concrete strategies covering current pattern, virtual workspace, physical axis, axis-scoped pipeline start, virtual workspace pipeline start, and physical - pipeline start. + pipeline start. These cases now belong directly to the registered request + owners through `source_universe()`. Runtime sidecars: diff --git a/docs/plans/mcp_artifact_source_coverage_plan_20260629.md b/docs/plans/mcp_artifact_source_coverage_plan_20260629.md index b70092767d..da6e9b7882 100644 --- a/docs/plans/mcp_artifact_source_coverage_plan_20260629.md +++ b/docs/plans/mcp_artifact_source_coverage_plan_20260629.md @@ -32,7 +32,6 @@ file counters in MCP to paper over ambiguity. - `_source_workspace_summary` - `openhcs/core/source_binding_selection.py` - `SourceUniverseRequest` - - `SourceUniverseStrategy` - step-input and pipeline-start universe strategies - `openhcs/core/source_workspace_projection.py` - `VirtualWorkspaceSourceProjection` @@ -89,8 +88,8 @@ The 2026-06-29 dry run showed `SourceLoadPlan` currently exposes only them to that plan. If implementation needs to enumerate source-universe strategy behavior, iterate -the nominal `SourceUniverseRequest`/`SourceUniverseStrategy` registration and -matching machinery. Do not mirror strategy names or physical/virtual/pipeline +the nominal `SourceUniverseRequest` registration and each request's +`source_universe()` operation. Do not mirror strategy names or physical/virtual/pipeline start categories in MCP. If implementation needs a generic compiled-context tree, iterate dataclass @@ -102,7 +101,8 @@ type. Do not use a string allowlist of important `CompiledStepPlan` fields. See `docs/plans/mcp_agent_experience_implementation_dry_run_20260629.md`. The dry run confirmed the compiled source/artifact fields on `CompiledStepPlan`, the three current `CompiledSourceUniversePlan` flags, the two -`SourceUniverseRequest` types, and six concrete `SourceUniverseStrategy` types. +`SourceUniverseRequest` types. Those request owners now resolve their universes +directly; the separate strategy family has been removed. It also confirmed `SourceLoadPlan` is currently only `zarr_config`. ## Implementation Steps @@ -146,7 +146,7 @@ Audit questions: - Does a generic compiled-context projection traverse dataclasses/types instead of maintaining a field allowlist? - Are source-universe facts projected from `CompiledSourceUniversePlan` and - `SourceUniverseRequest`/`SourceUniverseStrategy`, not recomputed in MCP? + `SourceUniverseRequest.source_universe()`, not recomputed in MCP? - Can an empty count be traced to the authority that was empty? Hard failures: diff --git a/openhcs/core/source_binding_selection.py b/openhcs/core/source_binding_selection.py index a7124d9763..89d30b0142 100644 --- a/openhcs/core/source_binding_selection.py +++ b/openhcs/core/source_binding_selection.py @@ -13,10 +13,7 @@ from metaclass_registry import AutoRegisterMeta from openhcs.constants.constants import Backend -from openhcs.core.registry_strategies import ( - EnumKeyedStrategyMixin, - MostDerivedContextStrategyMixin, -) +from openhcs.core.registry_strategies import EnumKeyedStrategyMixin from openhcs.core.path_pattern_matching import PathPatternTemplateMatcher from openhcs.core.source_bindings import ( CompiledSourceBindingPlan, @@ -1691,10 +1688,23 @@ def runtime_state( state = SourceUniverseRuntimeState() for request_type in cls.registered_request_types(): universe_request = request_type.from_request(request) - universe = SourceUniverseStrategy.universe(universe_request) + universe = universe_request.source_universe() state = universe_request.contribute_runtime_state(state, universe) return state + def source_universe(self) -> SourceFileUniverse: + """Resolve current-axis files through their declared source backend.""" + return SourceFileUniverse( + files=( + self.require_source_projection().pipeline_start_files( + axis_id=self.plan.axis_id, + ) + if self.uses_virtual_workspace_projection + else self.axis_files() + ), + backend=self.source_backend, + ) + def contribute_runtime_state( self, state: SourceUniverseRuntimeState, @@ -1786,6 +1796,12 @@ class StepInputSourceUniverseRequest(SourceUniverseRequest): universe_request_kind = "step_input" + def source_universe(self) -> SourceFileUniverse: + """Expand source selectors; otherwise retain the selected pattern files.""" + if not self.requires_step_input_selector_resolution: + return SourceFileUniverse(self.matching_files, self.source_backend) + return SourceUniverseRequest.source_universe(self) + def contribute_runtime_state( self, state: SourceUniverseRuntimeState, @@ -1805,6 +1821,25 @@ class PipelineStartSourceUniverseRequest(SourceUniverseRequest): universe_request_kind = "pipeline_start" + def source_universe(self) -> SourceFileUniverse: + """Resolve the declared original-source scope independently of step input.""" + if not self.requires_full_pipeline_source_universe: + return SourceUniverseRequest.source_universe(self) + if self.source_projection is not None: + return SourceFileUniverse( + self.source_projection.pipeline_start_files(), self.source_backend, + ) + backend = self.physical_full_universe_backend() + return SourceFileUniverse( + files=tuple( + str(path) + for path in self.context.filemanager.list_files( + str(self.context.input_dir), backend.value, recursive=True, + ) + ), + backend=backend, + ) + def contribute_runtime_state( self, state: SourceUniverseRuntimeState, @@ -1877,215 +1912,3 @@ class VirtualWorkspacePipelineStartListingBackendPolicy( """Virtual-workspace pipeline-start fan-out lists disk files.""" source_backend = Backend.VIRTUAL_WORKSPACE - - -class SourceUniverseStrategy( - MostDerivedContextStrategyMixin[SourceUniverseRequest], - ABC, -): - """Registered source-universe selection for source-binding runtime scopes.""" - - __registry_key__ = "strategy_key" - __skip_if_no_key__ = True - - strategy_key: ClassVar[str | None] = None - - @classmethod - def universe(cls, request: SourceUniverseRequest) -> SourceFileUniverse: - strategy = cls.for_context( - request, - error_subject="Source universe", - ) - if strategy is None: - raise ValueError("Source universe requires a strategy.") - return strategy.source_universe(request) - - @abstractmethod - def source_universe(self, request: SourceUniverseRequest) -> SourceFileUniverse: - """Return source files and backend for pipeline-start bindings.""" - - -class StepInputSourceUniverseStrategy(SourceUniverseStrategy, ABC): - """Source-universe strategy branch for current step input.""" - - def matches(self, request: SourceUniverseRequest) -> bool: - return isinstance( - request, - StepInputSourceUniverseRequest, - ) and self.matches_step_input(request) - - @abstractmethod - def matches_step_input(self, request: StepInputSourceUniverseRequest) -> bool: - """Return whether this strategy owns one step-input request.""" - - -class PipelineStartSourceUniverseStrategy(SourceUniverseStrategy, ABC): - """Source-universe strategy branch for original pipeline input.""" - - def matches(self, request: SourceUniverseRequest) -> bool: - return isinstance( - request, - PipelineStartSourceUniverseRequest, - ) and self.matches_pipeline_start(request) - - @abstractmethod - def matches_pipeline_start( - self, - request: PipelineStartSourceUniverseRequest, - ) -> bool: - """Return whether this strategy owns one pipeline-start request.""" - - -class AxisFilesSourceUniverseStrategy(SourceUniverseStrategy): - """Source-universe strategy that uses current-axis files from the source backend.""" - - def source_universe(self, request: SourceUniverseRequest) -> SourceFileUniverse: - return SourceFileUniverse( - files=request.axis_files(), - backend=request.source_backend, - ) - - -class StepInputAxisFilesSourceUniverseStrategy( - AxisFilesSourceUniverseStrategy, - StepInputSourceUniverseStrategy, - ABC, -): - """Axis-file universe strategy branch for current step input.""" - - -class PipelineStartAxisFilesSourceUniverseStrategy( - AxisFilesSourceUniverseStrategy, - PipelineStartSourceUniverseStrategy, - ABC, -): - """Axis-file universe strategy branch for original pipeline input.""" - - -class CurrentPatternStepInputSourceUniverseStrategy(StepInputSourceUniverseStrategy): - """Use the already-loaded pattern files when selectors do not need fan-out.""" - - strategy_key = "step_input_current_pattern" - - def matches_step_input(self, request: StepInputSourceUniverseRequest) -> bool: - return not request.requires_step_input_selector_resolution - - def source_universe(self, request: SourceUniverseRequest) -> SourceFileUniverse: - return SourceFileUniverse( - files=request.matching_files, - backend=request.source_backend, - ) - - -class VirtualWorkspaceStepInputSourceUniverseStrategy(StepInputSourceUniverseStrategy): - """Use source-schema virtual files when selector resolution must span sources.""" - - strategy_key = "step_input_virtual_workspace_source_projection" - - def matches_step_input(self, request: StepInputSourceUniverseRequest) -> bool: - return ( - request.requires_step_input_selector_resolution - and request.uses_virtual_workspace_projection - ) - - def source_universe(self, request: SourceUniverseRequest) -> SourceFileUniverse: - return SourceFileUniverse( - files=request.require_source_projection().pipeline_start_files( - axis_id=request.plan.axis_id - ), - backend=request.source_backend, - ) - - -class PhysicalAxisStepInputSourceUniverseStrategy( - StepInputAxisFilesSourceUniverseStrategy, -): - """Use physical axis files when source selectors need fan-out outside VWS.""" - - strategy_key = "step_input_physical_axis" - - def matches_step_input(self, request: StepInputSourceUniverseRequest) -> bool: - return ( - request.requires_step_input_selector_resolution - and not request.uses_virtual_workspace_projection - ) - - -class AxisScopedPipelineStartSourceUniverseStrategy( - PipelineStartAxisFilesSourceUniverseStrategy, -): - """Use the current axis source files when full pipeline fan-out is unnecessary.""" - - strategy_key = "axis_scoped" - - def matches_pipeline_start( - self, - request: PipelineStartSourceUniverseRequest, - ) -> bool: - return not request.requires_full_pipeline_source_universe - - def source_universe(self, request: SourceUniverseRequest) -> SourceFileUniverse: - if request.uses_virtual_workspace_projection: - return SourceFileUniverse( - files=request.require_source_projection().pipeline_start_files( - axis_id=request.plan.axis_id - ), - backend=request.source_backend, - ) - return SourceFileUniverse( - files=request.axis_files(), - backend=request.source_backend, - ) - - -class VirtualWorkspacePipelineStartSourceUniverseStrategy( - PipelineStartSourceUniverseStrategy, -): - """Use declared virtual-workspace source paths for pipeline-start fan-out.""" - - strategy_key = "virtual_workspace_source_projection" - - def matches_pipeline_start( - self, - request: PipelineStartSourceUniverseRequest, - ) -> bool: - return ( - request.requires_full_pipeline_source_universe - and request.source_projection is not None - ) - - def source_universe(self, request: SourceUniverseRequest) -> SourceFileUniverse: - projection = request.require_source_projection() - return SourceFileUniverse( - files=projection.pipeline_start_files(), - backend=request.source_backend, - ) - - -class PhysicalPipelineStartSourceUniverseStrategy(PipelineStartSourceUniverseStrategy): - """Use a file listing backend for full pipeline fan-out outside VWS.""" - - strategy_key = "physical_full_universe" - - def matches_pipeline_start( - self, - request: PipelineStartSourceUniverseRequest, - ) -> bool: - return ( - request.requires_full_pipeline_source_universe - and request.source_projection is None - ) - - def source_universe(self, request: SourceUniverseRequest) -> SourceFileUniverse: - universe_backend = request.physical_full_universe_backend() - return SourceFileUniverse( - files=tuple( - str(path) - for path in request.context.filemanager.list_files( - str(request.context.input_dir), - universe_backend.value, - recursive=True, - ) - ), - backend=universe_backend, - ) diff --git a/tests/unit/test_function_io.py b/tests/unit/test_function_io.py index e752311c34..91ec46ff1f 100644 --- a/tests/unit/test_function_io.py +++ b/tests/unit/test_function_io.py @@ -456,6 +456,7 @@ def test_virtual_pipeline_source_universe_does_not_mix_physical_paths( source_projection=projection, ) + universe = request.source_universe() state = request.contribute_runtime_state( SourceUniverseRuntimeState(), universe, From a0edbaf6bd3758ece0e3b09d47bbd57eaa51f82f Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 21:51:42 -0400 Subject: [PATCH 193/235] refactor: own anchor admission on the function step executor --- openhcs/core/steps/function_execution.py | 341 +++++++----------- ...st_function_execution_lifecycle_anchors.py | 54 ++- ...test_function_runtime_source_projection.py | 247 ++++++------- .../unit/test_function_step_execution_plan.py | 4 +- ..._source_bound_matched_image_set_anchors.py | 132 +++---- 5 files changed, 362 insertions(+), 416 deletions(-) diff --git a/openhcs/core/steps/function_execution.py b/openhcs/core/steps/function_execution.py index 6a3c9b07c0..ae9b13d3dd 100644 --- a/openhcs/core/steps/function_execution.py +++ b/openhcs/core/steps/function_execution.py @@ -6,8 +6,8 @@ import os import time import traceback -from collections.abc import Callable, Iterator, Mapping, Sequence -from dataclasses import dataclass, field +from collections.abc import Callable, Mapping, Sequence +from dataclasses import dataclass from itertools import zip_longest from typing import TYPE_CHECKING @@ -17,7 +17,10 @@ Backend, ) from openhcs.core.component_set import ComponentSet -from openhcs.core.component_group_scope import ComponentGroupScope, RuntimeExecutionAxisScope +from openhcs.core.component_group_scope import ( + ComponentGroupScope, + RuntimeExecutionAxisScope, +) from openhcs.core.runtime_stores import RuntimeArtifactInput from openhcs.core.callable_contract import ImagePayloadConsumption from openhcs.core.compiled_step_plan import CompiledStepPlan @@ -25,12 +28,12 @@ from openhcs.core.function_patterns import ( CompiledFunctionGroup, FunctionGroupKey, + GroupedPatternMap, InvocationArtifactInputEdgePlan, RuntimeInvocationDomain, ) from openhcs.core.progress import ProgressPhase, ProgressStatus, emit from openhcs.core.runtime_pattern_cache import RuntimePatternDiscoveryCacheKey -from openhcs.core.runtime_source_binding_cache import RuntimeSourceBindingContextCache from openhcs.core.source_binding_selection import ( SourceBoundAnchorPatternPolicy, SourceCandidatePath, @@ -39,7 +42,6 @@ from openhcs.core.source_bindings import CompiledSourceBindingPlan from openhcs.core.source_workspace_projection import ( VirtualWorkspaceSourceProjectionAuthority, - VirtualWorkspaceSourceProjectionCache, ) from openhcs.core.step_dependencies import StepInputDependencyKind from openhcs.core.steps.function_io import ( @@ -50,7 +52,6 @@ ) from openhcs.core.steps.function_output_manifest import ( NoStepOutputManifestMatch, - StepOutputManifestStore, step_output_manifest, ) from openhcs.core.steps.abstract import StepExecutionObservation @@ -162,58 +163,73 @@ def record_function_step_runtime_profile( ).emit() -@dataclass(frozen=True, slots=True) -class PatternGroups: - """Execution anchors grouped by compiled function-pattern component.""" +def _single_execution_group_patterns( + patterns: DiscoveredPatternCollection, +) -> Sequence[SourceCandidatePath]: + """Flatten discovered source groups when the planner chose no execution axis.""" + if not isinstance(patterns, dict): + return patterns + return tuple( + pattern for pattern_list in patterns.values() for pattern in pattern_list + ) - groups: Mapping[FunctionGroupKey, tuple[SourceCandidatePath, ...]] - @classmethod - def from_prepared( - cls, - grouped_patterns: Mapping[FunctionGroupKey, Sequence[SourceCandidatePath]], - ) -> PatternGroups: - return cls( - { - group_key: tuple(str(pattern) for pattern in pattern_list) - for group_key, pattern_list in grouped_patterns.items() - } - ) +def _filter_patterns_by_component( + patterns: DiscoveredPatternCollection, + component: str, + target_value: str, + parser: FilenameParser, +) -> DiscoveredPatternCollection: + """Filter pattern strings by a fixed parsed component value.""" - def items( - self, - ) -> Iterator[tuple[FunctionGroupKey, tuple[SourceCandidatePath, ...]]]: - return iter(self.groups.items()) + def filter_pattern_list( + pattern_list: Sequence[SourceCandidatePath], + ) -> list[SourceCandidatePath]: + filtered: list[SourceCandidatePath] = [] + component_declaration = parser.component_for_name(component) + for pattern in pattern_list: + metadata = parser.parse_filename(str(pattern)) + if metadata and str(metadata.value_for(component_declaration)) == str( + target_value + ): + filtered.append(pattern) + return filtered + + if isinstance(patterns, dict): + filtered_by_group = {} + for group_key, pattern_list in patterns.items(): + filtered_list = filter_pattern_list(pattern_list) + if filtered_list: + filtered_by_group[group_key] = filtered_list + return filtered_by_group - def values(self) -> Iterator[tuple[SourceCandidatePath, ...]]: - return iter(self.groups.values()) + return filter_pattern_list(patterns) - def __len__(self) -> int: - return len(self.groups) - def total_count(self) -> int: - return sum(len(pattern_list) for pattern_list in self.groups.values()) +class FunctionStepExecutor: + """Run one compiled FunctionStep plan for one multiprocessing axis.""" - def map_groups( - self, - selector: AnchorPatternSelector, - ) -> PatternGroups: - return PatternGroups.from_prepared( - { - group_key: selector(group_key, pattern_list) - for group_key, pattern_list in self.groups.items() - } - ) + def __init__(self, context: ProcessingContext, step_index: int) -> None: + self.context = context + compiled_plan = context.step_plans[step_index] + if not isinstance(compiled_plan, CompiledStepPlan): + raise TypeError( + f"FunctionStep {step_index} requires CompiledStepPlan, got " + f"{type(compiled_plan).__name__}." + ) + self.plan = compiled_plan.require_function_execution_ready() - def execution_requests( + def _execution_requests( self, - context: ProcessingContext, - plan: CompiledStepPlan, + grouped_patterns: GroupedPatternMap, ) -> tuple[PatternGroupExecutionRequest, ...]: """Admit source anchors to their complete compiled execution coordinates.""" + plan = self.plan requests = [] - for component_index, (component_value, patterns) in enumerate(self.items()): + for component_index, (component_value, patterns) in enumerate( + grouped_patterns.items() + ): compiled_group = plan.compiled_function_pattern.group_for_component( component_value ) @@ -223,113 +239,68 @@ def execution_requests( ) requests.extend( PatternGroupExecutionRequest( - context=context, + context=self.context, execution_plan=plan, pattern_group_info=pattern, compiled_group=compiled_group, component_value=component_value, component_index=component_index, - component_count=len(self), + component_count=len(grouped_patterns), ) for pattern in patterns ) return tuple(requests) - -def _single_execution_group_patterns( - patterns: DiscoveredPatternCollection, -) -> Sequence[SourceCandidatePath]: - """Flatten discovered source groups when the planner chose no execution axis.""" - if not isinstance(patterns, dict): - return patterns - return tuple( - pattern for pattern_list in patterns.values() for pattern in pattern_list - ) - - -@dataclass(frozen=True, slots=True) -class StepAnchorPatternFilter: - """Apply source, producer, and artifact-domain filtering to anchor groups.""" - - plan: CompiledStepPlan - parser: FilenameParser - output_manifest: StepOutputManifestStore - source_workspace_authority: VirtualWorkspaceSourceProjectionAuthority - source_workspace_projection_cache: VirtualWorkspaceSourceProjectionCache - source_binding_context_cache: RuntimeSourceBindingContextCache = field( - default_factory=RuntimeSourceBindingContextCache - ) - - @classmethod - def from_context( - cls, - context: ProcessingContext, - plan: CompiledStepPlan, - ) -> StepAnchorPatternFilter: - return cls( - plan=plan, - parser=context.microscope_handler.parser, - output_manifest=step_output_manifest(context), - source_workspace_authority=( - VirtualWorkspaceSourceProjectionAuthority.from_context( - context, - cache=context.runtime_source_workspace_projection_cache, - ) - ), - source_workspace_projection_cache=( - context.runtime_source_workspace_projection_cache - ), - source_binding_context_cache=context.runtime_source_binding_context_cache, - ) - - def filtered(self, grouped_patterns: PatternGroups) -> PatternGroups: + def _filter_anchor_patterns( + self, grouped_patterns: GroupedPatternMap + ) -> GroupedPatternMap: grouped_patterns = self.source_bound_anchor_patterns(grouped_patterns) grouped_patterns = self.producer_anchor_patterns(grouped_patterns) grouped_patterns = self.execution_group_anchor_patterns(grouped_patterns) - grouped_patterns = PatternGroups.from_prepared( - self.plan.compiled_function_pattern.prepare_grouped_patterns( - grouped_patterns.groups, - default_component=self.plan.execution_group_value, - ) + grouped_patterns = self.plan.compiled_function_pattern.prepare_grouped_patterns( + grouped_patterns, + default_component=self.plan.execution_group_value, ) return self.artifact_driven_anchor_patterns(grouped_patterns) def execution_group_anchor_patterns( self, - grouped_patterns: PatternGroups, - ) -> PatternGroups: + grouped_patterns: GroupedPatternMap, + ) -> GroupedPatternMap: """Map validated lifecycle anchors onto compiler-owned execution groups.""" pattern = self.plan.compiled_function_pattern source_owns_groups = ( pattern.runtime_domain is RuntimeInvocationDomain.SOURCE_ANCHORED ) - if not grouped_patterns.groups: + if not grouped_patterns: return grouped_patterns if source_owns_groups: - producer_groups = self.output_manifest.producer_patterns_by_execution_group( + producer_groups = step_output_manifest( + self.context + ).producer_patterns_by_execution_group( self.plan, tuple( pattern for patterns in grouped_patterns.values() for pattern in patterns ), - self.parser, + self.context.microscope_handler.parser, ) if producer_groups is not None: - return PatternGroups.from_prepared(producer_groups) - execution_scope = self.plan.execution_group_scope - return PatternGroups.from_prepared( - { - group_key: pattern_list - for group_key, pattern_list in grouped_patterns.items() - if execution_scope.contains_runtime_key(group_key) + return { + key: tuple(patterns) for key, patterns in producer_groups.items() } - ) + execution_scope = self.plan.execution_group_scope + return { + group_key: pattern_list + for group_key, pattern_list in grouped_patterns.items() + if execution_scope.contains_runtime_key(group_key) + } execution_group_keys = self.plan.execution_group_scope.runtime_keys( - grouped_patterns.groups + grouped_patterns ) canonical_anchors = next( ( @@ -339,17 +310,15 @@ def execution_group_anchor_patterns( ), (), ) - return PatternGroups.from_prepared( - { - group_key: grouped_patterns.groups.get(group_key) or canonical_anchors - for group_key in execution_group_keys - } - ) + return { + group_key: grouped_patterns.get(group_key) or canonical_anchors + for group_key in execution_group_keys + } def source_bound_anchor_patterns( self, - grouped_patterns: PatternGroups, - ) -> PatternGroups: + grouped_patterns: GroupedPatternMap, + ) -> GroupedPatternMap: """Restrict source-bound step anchors to compatible declared sources.""" if not self.plan.main_input_dependency.uses_pipeline_start_anchors(): @@ -416,8 +385,9 @@ def source_bound_anchor_patterns( selected_pattern = next(selected_patterns, exhausted) retained_by_group: dict[ FunctionGroupKey, + GroupedPatternMap, list[SourceCandidatePath], - ] = {component_value: [] for component_value in grouped_patterns.groups} + ] = {component_value: [] for component_value in grouped_patterns} for component_value, pattern in candidate_occurrences: if selected_pattern is exhausted: break @@ -484,15 +454,15 @@ def select_compatible( def default_source_bound_anchor_patterns( self, - grouped_patterns: PatternGroups, + grouped_patterns: GroupedPatternMap, *, policy: SourceBoundAnchorPatternPolicy, source_context: SourcePatternResolutionContext, - ) -> PatternGroups: + ) -> GroupedPatternMap: """Project raw source anchors onto compiler-owned semantic groups.""" execution_scope = self.plan.execution_group_scope - target_keys = execution_scope.runtime_keys(grouped_patterns.groups) + target_keys = execution_scope.runtime_keys(grouped_patterns) all_candidates = tuple( pattern for pattern_list in grouped_patterns.values() @@ -500,6 +470,7 @@ def default_source_bound_anchor_patterns( ) selected_groups: dict[ FunctionGroupKey, + GroupedPatternMap, Sequence[SourceCandidatePath], ] = {} for target_key in target_keys: @@ -507,7 +478,7 @@ def default_source_bound_anchor_patterns( self.plan.compiled_function_pattern.default_group, component_value=target_key, ) - local_candidates = grouped_patterns.groups.get(target_key) + local_candidates = grouped_patterns.get(target_key) requires_cross_group_resolution = ( execution_scope.component is not None and target_key is not None @@ -525,16 +496,18 @@ def default_source_bound_anchor_patterns( selected_groups[target_key] = ( () if bindings is None - else policy.select( - candidates, - bindings=bindings.binding_declarations, - source_context=source_context, + else tuple( + policy.select( + candidates, + bindings=bindings.binding_declarations, + source_context=source_context, + ) ) ) - filtered = PatternGroups.from_prepared(selected_groups) - before_count = grouped_patterns.total_count() - after_count = filtered.total_count() + filtered = selected_groups + before_count = sum(map(len, grouped_patterns.values())) + after_count = sum(map(len, filtered.values())) if before_count != after_count: record_function_step_runtime_profile( self.plan, @@ -577,8 +550,8 @@ def source_anchor_bindings( def producer_anchor_patterns( self, - grouped_patterns: PatternGroups, - ) -> PatternGroups: + grouped_patterns: GroupedPatternMap, + ) -> GroupedPatternMap: """Restrict previous-step anchors to the declared producer's files.""" def select_producer_paths( @@ -586,10 +559,10 @@ def select_producer_paths( pattern_list: tuple[SourceCandidatePath, ...], ) -> Sequence[SourceCandidatePath]: try: - return self.output_manifest.filter_to_producer_paths( + return step_output_manifest(self.context).filter_to_producer_paths( self.plan, tuple(pattern_list), - self.parser, + self.context.microscope_handler.parser, ) except NoStepOutputManifestMatch: return () @@ -602,8 +575,8 @@ def select_producer_paths( def artifact_driven_anchor_patterns( self, - grouped_patterns: PatternGroups, - ) -> PatternGroups: + grouped_patterns: GroupedPatternMap, + ) -> GroupedPatternMap: """Select the lifecycle anchors required by each compiled runtime domain.""" def select_lifecycle_anchors( @@ -626,12 +599,15 @@ def select_lifecycle_anchors( def apply( self, label: str, - grouped_patterns: PatternGroups, + grouped_patterns: GroupedPatternMap, selector: AnchorPatternSelector, - ) -> PatternGroups: - filtered = grouped_patterns.map_groups(selector) - before_count = grouped_patterns.total_count() - after_count = filtered.total_count() + ) -> GroupedPatternMap: + filtered = { + key: tuple(selector(key, patterns)) + for key, patterns in grouped_patterns.items() + } + before_count = sum(map(len, grouped_patterns.values())) + after_count = sum(map(len, filtered.values())) if before_count != after_count: record_function_step_runtime_profile( self.plan, @@ -647,62 +623,19 @@ def apply( def source_pattern_context(self) -> SourcePatternResolutionContext: """Return source-path context used to filter source-bound anchors.""" - projection = self.source_workspace_authority.projection_or_empty() - return self.source_binding_context_cache.source_pattern_context( - parser=self.parser, - projection=self.source_workspace_projection_cache.filtered_by_axis( + projection = VirtualWorkspaceSourceProjectionAuthority.from_context( + self.context, + cache=self.context.runtime_source_workspace_projection_cache, + ).projection_or_empty() + return self.context.runtime_source_binding_context_cache.source_pattern_context( + parser=self.context.microscope_handler.parser, + projection=self.context.runtime_source_workspace_projection_cache.filtered_by_axis( projection, axis_id=self.plan.axis_id, ), metadata_rules=self.plan.source_binding_plan.metadata_rules, ) - -def _filter_patterns_by_component( - patterns: DiscoveredPatternCollection, - component: str, - target_value: str, - parser: FilenameParser, -) -> DiscoveredPatternCollection: - """Filter pattern strings by a fixed parsed component value.""" - - def filter_pattern_list( - pattern_list: Sequence[SourceCandidatePath], - ) -> list[SourceCandidatePath]: - filtered: list[SourceCandidatePath] = [] - component_declaration = parser.component_for_name(component) - for pattern in pattern_list: - metadata = parser.parse_filename(str(pattern)) - if metadata and str(metadata.value_for(component_declaration)) == str( - target_value - ): - filtered.append(pattern) - return filtered - - if isinstance(patterns, dict): - filtered_by_group = {} - for group_key, pattern_list in patterns.items(): - filtered_list = filter_pattern_list(pattern_list) - if filtered_list: - filtered_by_group[group_key] = filtered_list - return filtered_by_group - - return filter_pattern_list(patterns) - - -class FunctionStepExecutor: - """Run one compiled FunctionStep plan for one multiprocessing axis.""" - - def __init__(self, context: ProcessingContext, step_index: int) -> None: - self.context = context - compiled_plan = context.step_plans[step_index] - if not isinstance(compiled_plan, CompiledStepPlan): - raise TypeError( - f"FunctionStep {step_index} requires CompiledStepPlan, got " - f"{type(compiled_plan).__name__}." - ) - self.plan = compiled_plan.require_function_execution_ready() - def record_runtime_profile( self, label: str, @@ -882,7 +815,7 @@ def run(self) -> StepExecutionObservation: self._apply_sequential_filter(patterns_by_axis) phase_started_at = time.perf_counter() grouped_patterns = self._prepare_groups(patterns_by_axis) - execution_requests = grouped_patterns.execution_requests(self.context, plan) + execution_requests = self._execution_requests(grouped_patterns) self.record_runtime_profile( "step_prepare_groups", time.perf_counter() - phase_started_at, @@ -1139,7 +1072,7 @@ def _apply_sequential_filter( def _prepare_groups( self, patterns_by_axis: Mapping[str, DiscoveredPatternCollection], - ) -> PatternGroups: + ) -> GroupedPatternMap: plan = self.plan axis_patterns = patterns_by_axis[plan.axis_id] execution_group_value = plan.execution_group_value @@ -1156,17 +1089,17 @@ def _prepare_groups( ): axis_patterns = _single_execution_group_patterns(axis_patterns) - pattern_filter = StepAnchorPatternFilter.from_context( - context=self.context, - plan=self.plan, - ) - grouped_patterns = PatternGroups.from_prepared( + discovered_groups = ( axis_patterns if isinstance(axis_patterns, Mapping) else {execution_group_value: axis_patterns} ) - grouped_patterns = pattern_filter.filtered(grouped_patterns) - if grouped_patterns.total_count() == 0: + grouped_patterns = { + key: tuple(str(pattern) for pattern in patterns) + for key, patterns in discovered_groups.items() + } + grouped_patterns = self._filter_anchor_patterns(grouped_patterns) + if sum(map(len, grouped_patterns.values())) == 0: raise ValueError( f"No pattern groups found for step {plan.step_index} " f"({plan.step_name}) in well {plan.axis_id}" diff --git a/tests/unit/test_function_execution_lifecycle_anchors.py b/tests/unit/test_function_execution_lifecycle_anchors.py index dc5e711ef5..d6b2693dba 100644 --- a/tests/unit/test_function_execution_lifecycle_anchors.py +++ b/tests/unit/test_function_execution_lifecycle_anchors.py @@ -2,7 +2,9 @@ from __future__ import annotations +from pathlib import Path from types import SimpleNamespace +from unittest.mock import Mock import pytest @@ -21,16 +23,40 @@ from openhcs.core.pipeline.function_contracts import artifact_inputs from openhcs.core.pipeline.path_planner import PathPlanner, PathPlannerArtifactStage from openhcs.core.runtime_adapters import runtime_adapter +from openhcs.core.runtime_source_binding_cache import RuntimeSourceBindingContextCache +from openhcs.core.steps.function_output_manifest import _STEP_OUTPUT_MANIFESTS from openhcs.core.source_bindings import CompiledSourceBindingPlan, NamedSourceBinding from openhcs.core.step_dependencies import StepInputDependency -from openhcs.core.steps.function_execution import PatternGroups, StepAnchorPatternFilter +from openhcs.core.steps.function_execution import FunctionStepExecutor + + +def _anchor_executor( + *, plan, parser, output_manifest, source_workspace_projection_cache +): + executor = object.__new__(FunctionStepExecutor) + executor.plan = plan + executor.context = Mock( + plate_path=Path("."), + microscope_handler=SimpleNamespace( + parser=parser, + metadata_handler=SimpleNamespace( + source_workspace_metadata_document=lambda _path: None + ), + ), + filemanager=SimpleNamespace(exists=lambda *_args: False), + runtime_source_workspace_projection_cache=source_workspace_projection_cache, + runtime_source_binding_context_cache=RuntimeSourceBindingContextCache(), + ) + if output_manifest is not None: + _STEP_OUTPUT_MANIFESTS[executor.context] = output_manifest + return executor + -def _anchor_filter(plan: object, output_manifest: object) -> StepAnchorPatternFilter: - return StepAnchorPatternFilter( +def _anchor_filter(plan: object, output_manifest: object) -> FunctionStepExecutor: + return _anchor_executor( plan=plan, parser=object(), output_manifest=output_manifest, - source_workspace_authority=None, source_workspace_projection_cache=None, ) @@ -120,17 +146,15 @@ def filter_to_producer_paths(self, _plan, paths, _parser): return [path for path in paths if "second" in path] manifest = ExactProducerManifest() - filtered = _anchor_filter(plan, manifest).filtered( - PatternGroups( - { - "1": ("first-anchor.tif",), - "2": ("second-anchor.tif",), - } - ) + filtered = _anchor_filter(plan, manifest)._filter_anchor_patterns( + { + "1": ("first-anchor.tif",), + "2": ("second-anchor.tif",), + } ) assert manifest.calls == 2 - assert filtered.groups == {"1": ("second-anchor.tif",)} + assert filtered == {"1": ("second-anchor.tif",)} def test_source_anchored_group_uses_exact_main_flow_producer_manifest() -> None: @@ -166,9 +190,9 @@ def producer_patterns_by_execution_group( return {None: patterns} manifest = ExactProducerManifest() - filtered = _anchor_filter(plan, manifest).filtered( - PatternGroups({None: ("unrelated-anchor.tif", "producer-anchor.tif")}) + filtered = _anchor_filter(plan, manifest)._filter_anchor_patterns( + {None: ("unrelated-anchor.tif", "producer-anchor.tif")} ) assert manifest.calls == 1 - assert filtered.groups == {None: ("producer-anchor.tif",)} + assert filtered == {None: ("producer-anchor.tif",)} diff --git a/tests/unit/test_function_runtime_source_projection.py b/tests/unit/test_function_runtime_source_projection.py index 5b1c67ac06..fab4092caf 100644 --- a/tests/unit/test_function_runtime_source_projection.py +++ b/tests/unit/test_function_runtime_source_projection.py @@ -2,13 +2,19 @@ from dataclasses import replace from pathlib import Path from types import SimpleNamespace +from unittest.mock import Mock import numpy as np import pytest from objectstate.global_config import GlobalContextValues from polystore.virtual_workspace import SourcePixelRef -from openhcs.constants.constants import AllComponents, Backend, GroupBy, VariableComponents +from openhcs.constants.constants import ( + AllComponents, + Backend, + GroupBy, + VariableComponents, +) from openhcs.core.aligned_image_payload import ( AlignedImageSliceContext, ImagePayloadBundleContext, @@ -58,6 +64,8 @@ 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 +from openhcs.core.steps.function_output_manifest import _STEP_OUTPUT_MANIFESTS from openhcs.core.source_bindings import ( SOURCE_BINDING_ALIAS_METADATA_FIELD, CompiledSourceBindingPlan, @@ -91,8 +99,6 @@ from openhcs.core.step_dependencies import StepInputDependency from openhcs.core.steps.function_execution import ( FunctionStepExecutor, - PatternGroups, - StepAnchorPatternFilter, ) from openhcs.core.steps.function_output_identity import ( FunctionOutputIdentity, @@ -109,6 +115,28 @@ from openhcs.microscopes.source_schema import SourceSchemaFilenameParser +def _anchor_executor( + *, plan, parser, output_manifest, source_workspace_projection_cache +): + executor = object.__new__(FunctionStepExecutor) + executor.plan = plan + executor.context = Mock( + plate_path=Path("."), + microscope_handler=SimpleNamespace( + parser=parser, + metadata_handler=SimpleNamespace( + source_workspace_metadata_document=lambda _path: None + ), + ), + filemanager=SimpleNamespace(exists=lambda *_args: False), + runtime_source_workspace_projection_cache=source_workspace_projection_cache, + runtime_source_binding_context_cache=RuntimeSourceBindingContextCache(), + ) + if output_manifest is not None: + _STEP_OUTPUT_MANIFESTS[executor.context] = output_manifest + return executor + + def _source_manifest(plan, paths_and_components): producer = CompiledStepPlan( step_index=plan.main_input_dependency.source_step_index, @@ -1325,12 +1353,11 @@ def test_step_output_anchor_filter_skips_source_binding_filter() -> None: bindings=(NamedSourceBinding(alias="OrigDNA"),) ), ) - grouped_patterns = PatternGroups({None: ("A14_s{iii}_w1_z001_t001.tif",)}) - pattern_filter = StepAnchorPatternFilter( + grouped_patterns = {None: ("A14_s{iii}_w1_z001_t001.tif",)} + pattern_filter = _anchor_executor( plan=plan, parser=None, output_manifest=StepOutputManifestStore(), - source_workspace_authority=None, source_workspace_projection_cache=VirtualWorkspaceSourceProjectionCache(), ) @@ -1353,18 +1380,17 @@ def test_step_output_anchor_uses_compiler_owned_component_scope() -> None: ), compiled_function_pattern=compile_function_pattern(lambda image: image, {}, {}), ) - grouped_patterns = PatternGroups({"2": ("A01_s{iii}_w2_z001_t001.tif",)}) - pattern_filter = StepAnchorPatternFilter( + grouped_patterns = {"2": ("A01_s{iii}_w2_z001_t001.tif",)} + pattern_filter = _anchor_executor( plan=plan, parser=SourceSchemaFilenameParser(), output_manifest=_source_manifest( plan, [("A01_s001_w2_z001_t001.tif", {"channel": 0})] ), - source_workspace_authority=None, source_workspace_projection_cache=VirtualWorkspaceSourceProjectionCache(), ) - assert pattern_filter.execution_group_anchor_patterns(grouped_patterns).groups == { + assert pattern_filter.execution_group_anchor_patterns(grouped_patterns) == { "0": ("A01_s{iii}_w2_z001_t001.tif",), } @@ -1396,27 +1422,19 @@ def identify_secondary(image): ), compiled_function_pattern=compiled_pattern, ) - pattern_filter = StepAnchorPatternFilter( + pattern_filter = _anchor_executor( plan=plan, parser=SourceSchemaFilenameParser(), output_manifest=_source_manifest( plan, [("A01_s001_w1_z001_t001.tif", {"channel": 2})] ), - source_workspace_authority=None, source_workspace_projection_cache=VirtualWorkspaceSourceProjectionCache(), ) - monkeypatch.setattr( - StepAnchorPatternFilter, - "from_context", - classmethod(lambda cls, context, plan: pattern_filter), - ) - executor = object.__new__(FunctionStepExecutor) - executor.context = SimpleNamespace() - executor.plan = plan + executor = pattern_filter grouped = executor._prepare_groups({"A01": {"1": ("A01_s{iii}_w1_z001_t001.tif",)}}) - assert grouped.groups == { + assert grouped == { "2": ("A01_s{iii}_w1_z001_t001.tif",), } @@ -1482,26 +1500,23 @@ def identify_primary_objects(image, *, runtime): def filter_to_producer_paths(_plan, paths, _parser): return tuple(path for path in paths if "_w2_" in path) - pattern_filter = StepAnchorPatternFilter( + pattern_filter = _anchor_executor( plan=plan, parser=SourceSchemaFilenameParser(), output_manifest=SimpleNamespace( filter_to_producer_paths=filter_to_producer_paths, ), - source_workspace_authority=None, source_workspace_projection_cache=VirtualWorkspaceSourceProjectionCache(), ) - filtered = pattern_filter.filtered( - PatternGroups( - { - "1": ("A01_s{iii}_w1_z001_t001.tif",), - "2": ("A01_s{iii}_w2_z001_t001.tif",), - } - ) + filtered = pattern_filter._filter_anchor_patterns( + { + "1": ("A01_s{iii}_w1_z001_t001.tif",), + "2": ("A01_s{iii}_w2_z001_t001.tif",), + } ) - assert filtered.groups == { + assert filtered == { "1": ("A01_s{iii}_w2_z001_t001.tif",), } @@ -1551,13 +1566,11 @@ def test_step_output_anchor_resolves_dynamic_component_scope_from_patterns() -> {}, ), ) - grouped_patterns = PatternGroups( - { - "1": ("A01_s001_w{iii}_z001_t001.tif",), - "2": ("A01_s002_w{iii}_z001_t001.tif",), - } - ) - pattern_filter = StepAnchorPatternFilter( + grouped_patterns = { + "1": ("A01_s001_w{iii}_z001_t001.tif",), + "2": ("A01_s002_w{iii}_z001_t001.tif",), + } + pattern_filter = _anchor_executor( plan=plan, parser=SourceSchemaFilenameParser(), output_manifest=_source_manifest( @@ -1567,13 +1580,12 @@ def test_step_output_anchor_resolves_dynamic_component_scope_from_patterns() -> ("A01_s002_w1_z001_t001.tif", {}), ], ), - source_workspace_authority=None, source_workspace_projection_cache=VirtualWorkspaceSourceProjectionCache(), ) assert ( - pattern_filter.execution_group_anchor_patterns(grouped_patterns).groups - == grouped_patterns.groups + pattern_filter.execution_group_anchor_patterns(grouped_patterns) + == grouped_patterns ) @@ -1590,22 +1602,19 @@ def test_source_anchor_uses_compiler_owned_static_component_scope() -> None: {}, ), ) - grouped_patterns = PatternGroups( - { - "1": ("A01_s{iii}_w1_z001_t001.tif",), - "2": ("A01_s{iii}_w2_z001_t001.tif",), - "3": ("A01_s{iii}_w3_z001_t001.tif",), - } - ) - pattern_filter = StepAnchorPatternFilter( + grouped_patterns = { + "1": ("A01_s{iii}_w1_z001_t001.tif",), + "2": ("A01_s{iii}_w2_z001_t001.tif",), + "3": ("A01_s{iii}_w3_z001_t001.tif",), + } + pattern_filter = _anchor_executor( plan=plan, parser=None, output_manifest=StepOutputManifestStore(), - source_workspace_authority=None, source_workspace_projection_cache=VirtualWorkspaceSourceProjectionCache(), ) - assert pattern_filter.execution_group_anchor_patterns(grouped_patterns).groups == { + assert pattern_filter.execution_group_anchor_patterns(grouped_patterns) == { "1": ("A01_s{iii}_w1_z001_t001.tif",), } @@ -1675,33 +1684,28 @@ def measure_source_set(image): {plan.ref(): plan for plan in (measurement_plan,)}, ), ) - pattern_filter = StepAnchorPatternFilter( + pattern_filter = _anchor_executor( plan=plan, parser=SourceSchemaFilenameParser(), output_manifest=StepOutputManifestStore(), - source_workspace_authority=SimpleNamespace( - projection_or_empty=lambda: VirtualWorkspaceSourceProjection.empty() - ), source_workspace_projection_cache=VirtualWorkspaceSourceProjectionCache(), ) filtered = pattern_filter.source_bound_anchor_patterns( - PatternGroups( - { - "1": ( - "A01_s001_w1_z001_t001.tif", - "A01_s002_w1_z001_t001.tif", - ), - "2": ( - "A01_s001_w2_z001_t001.tif", - "A01_s002_w2_z001_t001.tif", - ), - "3": ("A01_s001_w3_z001_t001.tif",), - } - ) + { + "1": ( + "A01_s001_w1_z001_t001.tif", + "A01_s002_w1_z001_t001.tif", + ), + "2": ( + "A01_s001_w2_z001_t001.tif", + "A01_s002_w2_z001_t001.tif", + ), + "3": ("A01_s001_w3_z001_t001.tif",), + } ) - assert filtered.groups == { + assert filtered == { "1": ( "A01_s001_w1_z001_t001.tif", "A01_s002_w1_z001_t001.tif", @@ -1842,28 +1846,23 @@ def save_image(image, *, image_to_save: np.ndarray): ), compiled_function_pattern=compiled_pattern, ) - pattern_filter = StepAnchorPatternFilter( + pattern_filter = _anchor_executor( plan=plan, parser=SourceSchemaFilenameParser(), output_manifest=StepOutputManifestStore(), - source_workspace_authority=SimpleNamespace( - projection_or_empty=lambda: VirtualWorkspaceSourceProjection.empty() - ), source_workspace_projection_cache=VirtualWorkspaceSourceProjectionCache(), ) - grouped_patterns = PatternGroups( - { - "1": ("A01_s001_w1_z001_t001.tif",), - "3": ("A01_s001_w3_z001_t001.tif",), - } - ) + grouped_patterns = { + "1": ("A01_s001_w1_z001_t001.tif",), + "3": ("A01_s001_w3_z001_t001.tif",), + } source_anchors = pattern_filter.source_bound_anchor_patterns(grouped_patterns) - assert source_anchors.groups == { + assert source_anchors == { "1": (), "3": ("A01_s001_w3_z001_t001.tif",), } - assert pattern_filter.execution_group_anchor_patterns(source_anchors).groups == { + assert pattern_filter.execution_group_anchor_patterns(source_anchors) == { "3": ("A01_s001_w3_z001_t001.tif",), } @@ -1905,29 +1904,24 @@ def exact_source_input(image): ), compiled_function_pattern=compiled_pattern, ) - pattern_filter = StepAnchorPatternFilter( + pattern_filter = _anchor_executor( plan=plan, parser=SourceSchemaFilenameParser(), output_manifest=StepOutputManifestStore(), - source_workspace_authority=SimpleNamespace( - projection_or_empty=lambda: VirtualWorkspaceSourceProjection.empty() - ), source_workspace_projection_cache=VirtualWorkspaceSourceProjectionCache(), ) - grouped_patterns = PatternGroups( - { - "1": ("A01_s001_w1_z001_t001.tif",), - "2": ("A01_s001_w2_z001_t001.tif",), - } - ) + grouped_patterns = { + "1": ("A01_s001_w1_z001_t001.tif",), + "2": ("A01_s001_w2_z001_t001.tif",), + } source_anchors = pattern_filter.source_bound_anchor_patterns(grouped_patterns) - assert source_anchors.groups == { + assert source_anchors == { "1": ("A01_s001_w1_z001_t001.tif",), "2": (), } - assert pattern_filter.execution_group_anchor_patterns(source_anchors).groups == { + assert pattern_filter.execution_group_anchor_patterns(source_anchors) == { "1": ("A01_s001_w1_z001_t001.tif",), } @@ -1964,27 +1958,22 @@ def exact_source_input(image): ), compiled_function_pattern=compile_function_pattern(exact_source_input, {}, {}), ) - pattern_filter = StepAnchorPatternFilter( + pattern_filter = _anchor_executor( plan=plan, parser=SourceSchemaFilenameParser(), output_manifest=StepOutputManifestStore(), - source_workspace_authority=SimpleNamespace( - projection_or_empty=lambda: VirtualWorkspaceSourceProjection.empty() - ), source_workspace_projection_cache=VirtualWorkspaceSourceProjectionCache(), ) filtered = pattern_filter.source_bound_anchor_patterns( - PatternGroups( - { - "1": ("A01_s001_w1_z001_t001.tif",), - "2": ("A01_s001_w2_z001_t001.tif",), - "3": ("A01_s001_w3_z001_t001.tif",), - } - ) + { + "1": ("A01_s001_w1_z001_t001.tif",), + "2": ("A01_s001_w2_z001_t001.tif",), + "3": ("A01_s001_w3_z001_t001.tif",), + } ) - assert filtered.groups == { + assert filtered == { "1": ("A01_s001_w1_z001_t001.tif",), } @@ -2022,26 +2011,21 @@ def test_pipeline_start_anchors_project_raw_selectors_to_semantic_groups() -> No {}, ), ) - pattern_filter = StepAnchorPatternFilter( + pattern_filter = _anchor_executor( plan=plan, parser=SourceSchemaFilenameParser(), output_manifest=StepOutputManifestStore(), - source_workspace_authority=SimpleNamespace( - projection_or_empty=lambda: VirtualWorkspaceSourceProjection.empty() - ), source_workspace_projection_cache=VirtualWorkspaceSourceProjectionCache(), ) filtered = pattern_filter.source_bound_anchor_patterns( - PatternGroups( - { - "1": ("A01_s001_w1_z001_t001.tif",), - "2": ("A01_s001_w2_z001_t001.tif",), - } - ) + { + "1": ("A01_s001_w1_z001_t001.tif",), + "2": ("A01_s001_w2_z001_t001.tif",), + } ) - assert filtered.groups == { + assert filtered == { "MCP_DNA": ("A01_s001_w1_z001_t001.tif",), "MCP_AGP": ("A01_s001_w2_z001_t001.tif",), } @@ -2135,7 +2119,7 @@ def test_first_step_prepares_raw_source_anchors_under_semantic_binding_groups( assert executor.plan.main_input_dependency == StepInputDependency.pipeline_start() assert executor.plan.source_binding_plan.has_primary_content - assert grouped_patterns.groups == { + assert grouped_patterns == { "MCP_DNA": ("A01_s{iii}_w1_z001_t001.tif",), "MCP_AGP": ("A01_s{iii}_w2_z001_t001.tif",), } @@ -2145,7 +2129,7 @@ def test_first_step_prepares_raw_source_anchors_under_semantic_binding_groups( execution_plan=executor.plan, compiled_group=executor.plan.compiled_function_pattern.default_group, component_value="MCP_DNA", - pattern_group_info=grouped_patterns.groups["MCP_DNA"][0], + pattern_group_info=grouped_patterns["MCP_DNA"][0], component_index=0, component_count=2, ) @@ -2153,10 +2137,12 @@ def test_first_step_prepares_raw_source_anchors_under_semantic_binding_groups( assert loaded[0] == ["A01_s001_w1_z001_t001.tif"] assert context.filemanager.exists( - str(executor.plan.input_dir / "A01_s001_w1_z001_t001.tif"), Backend.MEMORY.value, + str(executor.plan.input_dir / "A01_s001_w1_z001_t001.tif"), + Backend.MEMORY.value, ) assert not context.filemanager.exists( - str(executor.plan.input_dir / "A01_s001_w2_z001_t001.tif"), Backend.MEMORY.value, + str(executor.plan.input_dir / "A01_s001_w2_z001_t001.tif"), + Backend.MEMORY.value, ) @@ -2203,25 +2189,20 @@ def source_bound_module(image, *, runtime): execution_group_scope=ComponentGroupScope.ungrouped(), compiled_function_pattern=compiled_pattern, ) - pattern_filter = StepAnchorPatternFilter( + pattern_filter = _anchor_executor( plan=plan, parser=SourceSchemaFilenameParser(), output_manifest=StepOutputManifestStore(), - source_workspace_authority=SimpleNamespace( - projection_or_empty=lambda: VirtualWorkspaceSourceProjection.empty() - ), source_workspace_projection_cache=VirtualWorkspaceSourceProjectionCache(), ) - grouped_patterns = PatternGroups( - { - None: ( - "A01_s001_w1_z001_t001.tif", - "A01_s002_w1_z001_t001.tif", - ) - } - ) + grouped_patterns = { + None: ( + "A01_s001_w1_z001_t001.tif", + "A01_s002_w1_z001_t001.tif", + ) + } - assert pattern_filter.filtered(grouped_patterns).groups == grouped_patterns.groups + assert pattern_filter._filter_anchor_patterns(grouped_patterns) == grouped_patterns def test_default_callable_runtime_scope_projects_bindings_to_selected_group() -> None: diff --git a/tests/unit/test_function_step_execution_plan.py b/tests/unit/test_function_step_execution_plan.py index baeab80d3f..07c00a468a 100644 --- a/tests/unit/test_function_step_execution_plan.py +++ b/tests/unit/test_function_step_execution_plan.py @@ -302,9 +302,7 @@ def _apply_sequential_filter(self, patterns_by_axis): def _prepare_groups(self, patterns_by_axis): events.append(("groups", tuple(patterns_by_axis))) - return function_execution.PatternGroups.from_prepared( - {None: ["image.tif"]} - ) + return {None: ("image.tif",)} def _prepare_callables(self, grouped_patterns): raise AssertionError("callable warmup belongs to compilation") diff --git a/tests/unit/test_source_bound_matched_image_set_anchors.py b/tests/unit/test_source_bound_matched_image_set_anchors.py index f79d80899b..12f5e916f1 100644 --- a/tests/unit/test_source_bound_matched_image_set_anchors.py +++ b/tests/unit/test_source_bound_matched_image_set_anchors.py @@ -1,4 +1,6 @@ +from pathlib import Path from types import SimpleNamespace +from unittest.mock import Mock import pytest from polystore.virtual_workspace import SourcePixelRef @@ -23,6 +25,8 @@ execution_scope, ) from openhcs.core.source_binding_selection import SourcePatternResolutionContext +from openhcs.core.runtime_source_binding_cache import RuntimeSourceBindingContextCache +from openhcs.core.steps.function_output_manifest import _STEP_OUTPUT_MANIFESTS from openhcs.core.source_bindings import ( CompiledSourceBindingPlan, ComponentSelector, @@ -38,8 +42,7 @@ from openhcs.core.source_workspace_projection import VirtualWorkspaceSourceProjection from openhcs.core.step_dependencies import StepInputDependency from openhcs.core.steps.function_execution import ( - PatternGroups, - StepAnchorPatternFilter, + FunctionStepExecutor, ) from openhcs.microscopes.source_schema import SourceSchemaFilenameParser @@ -52,6 +55,28 @@ ) +def _anchor_executor( + *, plan, parser, output_manifest, source_workspace_projection_cache +): + executor = object.__new__(FunctionStepExecutor) + executor.plan = plan + executor.context = Mock( + plate_path=Path("."), + microscope_handler=SimpleNamespace( + parser=parser, + metadata_handler=SimpleNamespace( + source_workspace_metadata_document=lambda _path: None + ), + ), + filemanager=SimpleNamespace(exists=lambda *_args: False), + runtime_source_workspace_projection_cache=source_workspace_projection_cache, + runtime_source_binding_context_cache=RuntimeSourceBindingContextCache(), + ) + if output_manifest is not None: + _STEP_OUTPUT_MANIFESTS[executor.context] = output_manifest + return executor + + def test_artifact_only_group_declares_no_source_anchor_bindings() -> None: payload = ArtifactSpec.input( "payload", @@ -73,14 +98,13 @@ def consume(*, payload): ), ), ) - pattern_filter = StepAnchorPatternFilter( + pattern_filter = _anchor_executor( plan=SimpleNamespace( source_binding_plan=source_binding_plan, execution_group_scope=ComponentGroupScope.ungrouped(), ), parser=SourceSchemaFilenameParser(), output_manifest=None, - source_workspace_authority=None, source_workspace_projection_cache=None, ) @@ -129,7 +153,7 @@ def _filter_source_anchors( dimension_aliases: tuple[str, ...] | None = None, shared_output: bool = True, composed_image_set: bool = True, -) -> PatternGroups: +) -> dict: available_bindings = _source_bindings() aliases = tuple(binding.alias for binding in available_bindings) compiled_bindings = tuple( @@ -231,14 +255,10 @@ def measure_images(image): ), compiled_function_pattern=compiled_pattern, ) - patterns = PatternGroups( - { - channel: tuple( - f"A01_s{site:03d}_w{channel}_z001_t001.tif" for site in sites - ) - for _alias, channel in SOURCE_ALIASES - } - ) + patterns = { + channel: tuple(f"A01_s{site:03d}_w{channel}_z001_t001.tif" for site in sites) + for _alias, channel in SOURCE_ALIASES + } source_paths = { pattern: f"/source/{pattern}" for pattern_list in patterns.values() @@ -259,15 +279,14 @@ def measure_images(image): source_metadata_by_path=source_metadata, ) monkeypatch.setattr( - StepAnchorPatternFilter, + FunctionStepExecutor, "source_pattern_context", lambda _self: source_context, ) - pattern_filter = StepAnchorPatternFilter( + pattern_filter = _anchor_executor( plan=plan, parser=SourceSchemaFilenameParser(), output_manifest=None, - source_workspace_authority=None, source_workspace_projection_cache=None, ) return pattern_filter.source_bound_anchor_patterns(patterns) @@ -281,7 +300,7 @@ def test_all_loaded_contract_collapses_sibling_alias_groups_by_metadata( artifact_aliases=tuple(alias for alias, _channel in SOURCE_ALIASES), ) - assert filtered.groups == { + assert filtered == { "2": ("A01_s001_w2_z001_t001.tif",), "1": (), "5": (), @@ -299,7 +318,7 @@ def test_natural_callable_keeps_each_source_component_anchor( composed_image_set=False, ) - assert filtered.groups == { + assert filtered == { channel: (f"A01_s001_w{channel}_z001_t001.tif",) for _alias, channel in SOURCE_ALIASES } @@ -313,7 +332,7 @@ def test_selected_source_contract_keeps_only_its_exact_alias_anchor( artifact_aliases=("OrigHoechst",), ) - assert filtered.groups == { + assert filtered == { "1": ("A01_s001_w1_z001_t001.tif",), } @@ -327,7 +346,7 @@ def test_distinct_metadata_sets_each_keep_one_source_representative( sites=(1, 2), ) - assert filtered.groups == { + assert filtered == { "2": ( "A01_s001_w2_z001_t001.tif", "A01_s002_w2_z001_t001.tif", @@ -348,7 +367,7 @@ def test_independent_source_aligned_outputs_keep_each_execution_group( shared_output=False, ) - assert filtered.groups == { + assert filtered == { "2": ("A01_s001_w2_z001_t001.tif",), "1": ("A01_s001_w1_z001_t001.tif",), } @@ -408,13 +427,11 @@ def measure_hoechst(image): ), compiled_function_pattern=compiled_pattern, ) - patterns = PatternGroups( - { - "2": ("A01_s001_w2_z001_t001.tif",), - "1": ("A01_s001_w1_z001_t001.tif",), - "5": ("A01_s001_w5_z001_t001.tif",), - } - ) + patterns = { + "2": ("A01_s001_w2_z001_t001.tif",), + "1": ("A01_s001_w1_z001_t001.tif",), + "5": ("A01_s001_w5_z001_t001.tif",), + } source_context = SourcePatternResolutionContext.from_sources( parser=SourceSchemaFilenameParser(), source_paths_by_virtual_path={ @@ -429,21 +446,20 @@ def measure_hoechst(image): }, ) monkeypatch.setattr( - StepAnchorPatternFilter, + FunctionStepExecutor, "source_pattern_context", lambda _self: source_context, ) - pattern_filter = StepAnchorPatternFilter( + pattern_filter = _anchor_executor( plan=plan, parser=SourceSchemaFilenameParser(), output_manifest=None, - source_workspace_authority=None, source_workspace_projection_cache=None, ) filtered = pattern_filter.source_bound_anchor_patterns(patterns) - assert filtered.groups == { + assert filtered == { "2": ("A01_s001_w2_z001_t001.tif",), "1": ("A01_s001_w1_z001_t001.tif",), "5": (), @@ -510,28 +526,25 @@ def identify_bodies(image): ), ) monkeypatch.setattr( - StepAnchorPatternFilter, + FunctionStepExecutor, "source_pattern_context", lambda _self: source_context, ) - pattern_filter = StepAnchorPatternFilter( + pattern_filter = _anchor_executor( plan=plan, parser=SourceSchemaFilenameParser(), output_manifest=None, - source_workspace_authority=None, source_workspace_projection_cache=None, ) filtered = pattern_filter.source_bound_anchor_patterns( - PatternGroups( - { - "1": ("A01_s{iii}_w1_z001_t001.tif",), - "2": ("A01_s{iii}_w2_z001_t001.tif",), - } - ) + { + "1": ("A01_s{iii}_w1_z001_t001.tif",), + "2": ("A01_s{iii}_w2_z001_t001.tif",), + } ) - assert filtered.groups == { + assert filtered == { "2": ("A01_s{iii}_w2_z001_t001.tif",), } @@ -631,23 +644,20 @@ def align_source_set(image): ), ) monkeypatch.setattr( - StepAnchorPatternFilter, + FunctionStepExecutor, "source_pattern_context", lambda _self: source_context, ) - pattern_filter = StepAnchorPatternFilter( + pattern_filter = _anchor_executor( plan=plan, parser=SourceSchemaFilenameParser(), output_manifest=None, - source_workspace_authority=None, source_workspace_projection_cache=None, ) - patterns = PatternGroups( - { - "1": ("A01_s001_w{iii}_z001_t001.tif",), - "2": ("A01_s002_w{iii}_z001_t001.tif",), - } - ) + patterns = { + "1": ("A01_s001_w{iii}_z001_t001.tif",), + "2": ("A01_s002_w{iii}_z001_t001.tif",), + } assert pattern_filter.source_bound_anchor_patterns(patterns) == patterns @@ -683,7 +693,10 @@ def test_static_site_groups_do_not_cross_project_natural_source_set_templates( MeasurementsArtifactType, relations=( ArtifactMeasurementSubjectRelation(), - *(GroupLineageSourceRelation(source=spec.ref()) for spec in input_specs), + *( + GroupLineageSourceRelation(source=spec.ref()) + for spec in input_specs + ), ), ), ) @@ -759,22 +772,19 @@ def align_source_set(image): ), ) monkeypatch.setattr( - StepAnchorPatternFilter, + FunctionStepExecutor, "source_pattern_context", lambda _self: source_context, ) - pattern_filter = StepAnchorPatternFilter( + pattern_filter = _anchor_executor( plan=plan, parser=SourceSchemaFilenameParser(), output_manifest=None, - source_workspace_authority=None, source_workspace_projection_cache=None, ) - patterns = PatternGroups( - { - "1": ("A01_s001_w{iii}_z001_t001.tif",), - "2": ("A01_s002_w{iii}_z001_t001.tif",), - } - ) + patterns = { + "1": ("A01_s001_w{iii}_z001_t001.tif",), + "2": ("A01_s002_w{iii}_z001_t001.tif",), + } assert pattern_filter.source_bound_anchor_patterns(patterns) == patterns From e124b81177c219d699dcdfcef6839f9f84e04ca8 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 21:53:43 -0400 Subject: [PATCH 194/235] Sync the exact saved-text source capability for analysis reuse Refs #384. Pin merged PolyStore#26, closing PolyStore#25. Runtime analysis and warm debug reuse can consume exact saved writer text through FileManager rather than decoding or rerendering CSVs. --- external/PolyStore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/PolyStore b/external/PolyStore index 1d72165e06..89deeef366 160000 --- a/external/PolyStore +++ b/external/PolyStore @@ -1 +1 @@ -Subproject commit 1d72165e065b7f2d69d6dcbd8e8a1ef54f13b0d3 +Subproject commit 89deeef3662eabb11bc520fad9acd976698636bd From 19ec3f35a42c604fa3df83e391e16785f9c0bb49 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 21:54:24 -0400 Subject: [PATCH 195/235] Sync current ObjectState main dependency revision Refs #384. Refresh the branch pin and actual editable source to the latest main; the only new upstream change is its coverage badge. --- external/ObjectState | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/ObjectState b/external/ObjectState index 16aa704ec1..9fb5b7eeea 160000 --- a/external/ObjectState +++ b/external/ObjectState @@ -1 +1 @@ -Subproject commit 16aa704ec1f80315b4496813277127f10f99d210 +Subproject commit 9fb5b7eeea96f3b7bb0579de30718bbcc0018823 From 0add182ec0acdc87a79c22865bdd49c0b345874e Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 22:05:22 -0400 Subject: [PATCH 196/235] Carry saved analysis outputs through execution completion Remove completion-time artifact rediscovery and CSV rendering. Existing step observations carry the actual backend-accepted writer output text into worker, plate and consolidation consumers. Explicit debug reuse traverses historical materializations once and reads exact retained CSV text through FileManager.load_text. Keep source provenance on CSV outputs without treating them as image arrays during format batching. Preserve export exclusions, backend and well identity, record retention, and nonpersistent outputs. Refs #384, #394. Focused saved-output, debug, record retention, plate and raster consumer controls pass. No timing attribution: paired benchmarks disable consolidation. --- .../orchestrator/analysis_consolidation.py | 166 +++++++++++------- .../orchestrator/compiled_plate_execution.py | 21 ++- openhcs/core/orchestrator/execution_result.py | 8 +- openhcs/core/orchestrator/worker_execution.py | 27 +-- openhcs/core/steps/abstract.py | 17 +- .../function_artifact_materialization.py | 93 +++++----- openhcs/core/steps/function_outputs.py | 2 +- openhcs/processing/materialization/core.py | 1 + .../test_function_artifact_materialization.py | 82 ++++++--- tests/unit/test_function_outputs.py | 5 +- .../test_function_step_execution_scope.py | 32 +++- .../test_materialization_batch_outcomes.py | 7 +- 12 files changed, 288 insertions(+), 173 deletions(-) diff --git a/openhcs/core/orchestrator/analysis_consolidation.py b/openhcs/core/orchestrator/analysis_consolidation.py index dbcc0f1d68..13a4b4949a 100644 --- a/openhcs/core/orchestrator/analysis_consolidation.py +++ b/openhcs/core/orchestrator/analysis_consolidation.py @@ -3,9 +3,8 @@ from __future__ import annotations import logging -from collections.abc import Mapping +from collections.abc import Iterable, Mapping from dataclasses import dataclass -from functools import partial from pathlib import Path from typing import TYPE_CHECKING @@ -18,11 +17,6 @@ ExecutionResult, RuntimeExecutionObservation, ) -from openhcs.core.runtime_stores import StoredRuntimeValue -from openhcs.core.steps.function_artifact_materialization import ( - RuntimeArtifactMaterialization, - runtime_artifact_materializations_from_records, -) from openhcs.processing.backends.analysis.consolidate_analysis_results import ( AnalysisSummaryWriter, RuntimeAnalysisTableOutput, @@ -35,6 +29,12 @@ if TYPE_CHECKING: from polystore.filemanager import FileManager + from openhcs.core.compiled_step_plan import CompiledStepPlan + from openhcs.core.steps.function_artifact_materialization import ( + MaterializedRuntimeArtifact, + RuntimeArtifactMaterialization, + ) + from openhcs.processing.materialization.core import Output @dataclass(frozen=True, slots=True) @@ -67,55 +67,109 @@ def outputs_by_directory( return {directory: group.outputs for directory, group in self.groups.items()} @classmethod - def from_records( + def from_saved_outputs( + cls, + context: ProcessingContext, + plan: CompiledStepPlan, + saved: MaterializedRuntimeArtifact, + ) -> RuntimeAnalysisConsolidationInputs | None: + """Project the writer's actual saved CSV content without rendering again.""" + if not plan.runtime_artifact_materialization.has_persistent_target: + return None + backend = plan.runtime_artifact_materialization.require_persistent_backend() + return cls._from_table_contents( + context, plan, saved.materialization, + ( + (Path(output.path), output.require_text_content()) + for output in saved.outputs_for_backend(backend) + if analysis_file_path_is_included( + Path(output.path), + analysis_consolidation_config=context.analysis_consolidation_config, + ) + ), + ) + + @classmethod + def from_reused_outputs( cls, context: ProcessingContext, - records: tuple[StoredRuntimeValue, ...], + plan: CompiledStepPlan, + materialization: RuntimeArtifactMaterialization, + outputs: tuple[Output, ...], ) -> RuntimeAnalysisConsolidationInputs | None: - """Render only execution-owned tables while their payloads are available.""" - if not context.analysis_consolidation_config.enabled: + """Read exact historical CSV text for explicitly reused debug outputs.""" + if not plan.runtime_artifact_materialization.has_persistent_target: return None + backend = plan.runtime_artifact_materialization.require_persistent_backend() + return cls._from_table_contents( + context, plan, materialization, + ( + (Path(output.path), context.filemanager.load_text(output.path, backend)) + for output in outputs + if analysis_file_path_is_included( + Path(output.path), + analysis_consolidation_config=context.analysis_consolidation_config, + ) + ), + ) + + @classmethod + def _from_table_contents( + cls, + context: ProcessingContext, + plan: CompiledStepPlan, + materialization: RuntimeArtifactMaterialization, + contents: Iterable[tuple[Path, str]], + ) -> RuntimeAnalysisConsolidationInputs | None: + """Bind table identity and destinations from this one materialization.""" + if ( + not materialization.spec.participates_in_runtime_export_observation() + or not context.analysis_consolidation_config.enabled + ): + return None + backend = plan.runtime_artifact_materialization.require_persistent_backend() + destination = RuntimeAnalysisSummaryDestination( + backend=backend, images_dir=plan.artifact_images_dir, + ) + output_groups: dict[ + tuple[Path, RuntimeAnalysisSummaryDestination], list[RuntimeAnalysisTableOutput] + ] = {} + for output_path, content in contents: + output_groups.setdefault((output_path.parent, destination), []).append( + runtime_analysis_table_output( + materialization, + output_path=output_path, + csv_content=content, + pipeline_position=plan.pipeline_position, + ) + ) + return cls._from_groups( + output_groups, + {RuntimeAnalysisSummaryDestination(backend, str(plan.output_dir))}, + ) + + @classmethod + def combine( + cls, + inputs: Iterable[RuntimeAnalysisConsolidationInputs | None], + ) -> RuntimeAnalysisConsolidationInputs | None: + """Combine actual rendered projections without revisiting their payloads.""" output_groups: dict[ tuple[Path, RuntimeAnalysisSummaryDestination], list[RuntimeAnalysisTableOutput] ] = {} destinations: set[RuntimeAnalysisSummaryDestination] = set() seen_paths: set[tuple[str, Path]] = set() - for step_plan in context.step_plans.values(): - if (not step_plan.owns_runtime_outputs - or not step_plan.runtime_artifact_materialization.has_persistent_target): + for item in inputs: + if item is None: continue - for materialization in runtime_artifact_materializations_from_records( - step_plan, context, records, - ): - if not materialization.spec.participates_in_runtime_export_observation(): - continue - backend = step_plan.runtime_artifact_materialization.require_persistent_backend() - destination = RuntimeAnalysisSummaryDestination( - backend=backend, images_dir=step_plan.artifact_images_dir, - ) - for output in materialization.outputs( - step_plan, - context, - output_path_filter=partial( - analysis_file_path_is_included, - analysis_consolidation_config=context.analysis_consolidation_config, - ), - ): - output_path = Path(output.path) - if (backend, output_path) in seen_paths: + destinations.add(item.destination) + for directory, group in item.groups.items(): + for output in group.outputs: + identity = (group.destination.backend, output.path) + if identity in seen_paths: continue - seen_paths.add((backend, output_path)) - destinations.add(RuntimeAnalysisSummaryDestination( - backend=backend, images_dir=str(step_plan.output_dir), - )) - output_groups.setdefault((output_path.parent, destination), []).append( - runtime_analysis_table_output( - materialization, - output_path=output_path, - csv_content=output.require_text_content(), - pipeline_position=step_plan.pipeline_position, - ) - ) + seen_paths.add(identity) + output_groups.setdefault((directory, group.destination), []).append(output) return cls._from_groups(output_groups, destinations) @classmethod @@ -124,12 +178,8 @@ def from_observations( compiled_contexts: Mapping[str, ProcessingContext], observations: tuple[RuntimeExecutionObservation, ...], ) -> RuntimeAnalysisConsolidationInputs | None: - """Combine projected tables without revisiting worker artifact payloads.""" - output_groups: dict[ - tuple[Path, RuntimeAnalysisSummaryDestination], list[RuntimeAnalysisTableOutput] - ] = {} - destinations: set[RuntimeAnalysisSummaryDestination] = set() - seen_paths: set[tuple[str, Path]] = set() + """Admit execution contexts and combine their actual rendered outputs.""" + inputs = [] for observation in observations: for context_observation in observation.contexts: if context_observation.context_key not in compiled_contexts: @@ -137,18 +187,8 @@ def from_observations( "Runtime observation references unknown compiled context " f"{context_observation.context_key!r}." ) - inputs = context_observation.analysis_inputs - if inputs is None: - continue - destinations.add(inputs.destination) - for directory, group in inputs.groups.items(): - for output in group.outputs: - identity = (group.destination.backend, output.path) - if identity in seen_paths: - continue - seen_paths.add(identity) - output_groups.setdefault((directory, group.destination), []).append(output) - return cls._from_groups(output_groups, destinations) + inputs.append(context_observation.analysis_inputs) + return cls.combine(inputs) @classmethod def _from_groups( diff --git a/openhcs/core/orchestrator/compiled_plate_execution.py b/openhcs/core/orchestrator/compiled_plate_execution.py index 1633dd25a6..5e2cb76fcf 100644 --- a/openhcs/core/orchestrator/compiled_plate_execution.py +++ b/openhcs/core/orchestrator/compiled_plate_execution.py @@ -40,6 +40,7 @@ from openhcs.core.execution_visualizer import ExecutionVisualizerABC from openhcs.core.function_patterns import CompiledFunctionInvocation from openhcs.core.orchestrator.analysis_consolidation import ( + RuntimeAnalysisConsolidationInputs, consolidate_analysis_outputs, ) from openhcs.core.orchestrator.cancellation import ExecutionCancelledError @@ -77,7 +78,7 @@ replace_runtime_artifact_payload, ) from openhcs.core.source_bindings import CompiledSourceBindingPlan -from openhcs.core.steps.abstract import AbstractStep +from openhcs.core.steps.abstract import AbstractStep, StepExecutionObservation from openhcs.core.steps.function_outputs import ( OpenHCSMetadataWriter, RuntimeArtifactMaterializationAuthority, @@ -457,7 +458,7 @@ def execute_plate_scoped_steps( plate_step_indexes = validate_plate_scoped_contexts(compiled_contexts) total_steps = max(next(iter(compiled_contexts.values())).step_plans) + 1 records_by_axis = _runtime_record_snapshot(compiled_contexts) - runtime_export_paths_by_context = {key: [] for key in compiled_contexts} + observations_by_context = {key: [] for key in compiled_contexts} for step_index in plate_step_indexes: owner_context, owner_plan = _plate_output_owner( compiled_contexts, @@ -550,10 +551,10 @@ def execute_plate_scoped_steps( for key, context in compiled_contexts.items() if context is owner_context ) - runtime_export_paths_by_context[owner_context_key].extend( - path - for materialization in materializations - for path in materialization.observation(owner_plan).runtime_export_paths + observations_by_context[owner_context_key].append( + StepExecutionObservation.combine( + item.observation(owner_plan, owner_context) for item in materializations + ) ) _emit_execution_progress( progress_queue=progress_queue, @@ -573,9 +574,15 @@ def execute_plate_scoped_steps( context=context, records=records, runtime_export_paths=tuple( - dict.fromkeys(runtime_export_paths_by_context[context_key]) + dict.fromkeys( + path for item in observations_by_context[context_key] + for path in item.runtime_export_paths + ) ), runtime_observation_mode=RuntimeObservationMode.MERGE_INTO_PARENT, + analysis_inputs=RuntimeAnalysisConsolidationInputs.combine( + item.analysis_inputs for item in observations_by_context[context_key] + ), ) for context_key, context in compiled_contexts.items() if ( diff --git a/openhcs/core/orchestrator/execution_result.py b/openhcs/core/orchestrator/execution_result.py index bc8ccca839..60ddd28926 100644 --- a/openhcs/core/orchestrator/execution_result.py +++ b/openhcs/core/orchestrator/execution_result.py @@ -75,16 +75,14 @@ def from_context( records: tuple[StoredRuntimeValue, ...], runtime_observation_mode: "RuntimeObservationMode", runtime_export_paths: tuple[Path, ...], + analysis_inputs: "RuntimeAnalysisConsolidationInputs | None", ) -> "RuntimeContextObservation": - """Project completed outputs before releasing worker-owned payloads.""" - from openhcs.core.orchestrator.analysis_consolidation import ( - RuntimeAnalysisConsolidationInputs, - ) + """Retain requested records beside the completed output projections.""" return cls( context_key=context_key, records=runtime_observation_mode.retain_records(records, context), runtime_export_paths=tuple(dict.fromkeys(runtime_export_paths)), - analysis_inputs=RuntimeAnalysisConsolidationInputs.from_records(context, records), + analysis_inputs=analysis_inputs, ) diff --git a/openhcs/core/orchestrator/worker_execution.py b/openhcs/core/orchestrator/worker_execution.py index 066802c006..9257de1522 100644 --- a/openhcs/core/orchestrator/worker_execution.py +++ b/openhcs/core/orchestrator/worker_execution.py @@ -23,6 +23,9 @@ from openhcs.core.context.processing_context import ProcessingContext from openhcs.core.function_step_transport import FunctionStepTransportAuthority from openhcs.core.native_threading import configure_native_thread_count +from openhcs.core.orchestrator.analysis_consolidation import ( + RuntimeAnalysisConsolidationInputs, +) from openhcs.core.orchestrator.execution_result import ( ExecutionResult, RuntimeContextObservation, @@ -55,8 +58,7 @@ ) from openhcs.core.steps.abstract import AbstractStep, StepExecutionObservation from openhcs.core.steps.function_artifact_materialization import ( - preview_reused_materialized_artifact_locations, - preview_reused_runtime_export_paths, + preview_reused_step_outputs, ) from openhcs.utils.environment import OpenHCSProcessEnvironment @@ -818,6 +820,9 @@ def _execute_axis_with_sequential_combinations( records=observed_records, runtime_observation_mode=runtime_observation_mode, runtime_export_paths=runtime_export_paths, + analysis_inputs=RuntimeAnalysisConsolidationInputs.combine( + item.analysis_inputs for item in result.runtime_observation.contexts + ), ) finally: # This cache is context-local even when lanes share a process. @@ -956,6 +961,7 @@ def _execute_single_axis_static( lane_context.install_debug_sink(frozen_context) runtime_value_store = frozen_context.runtime_value_store runtime_export_paths = [] + analysis_inputs = [] for step_index, step in enumerate(pipeline_definition): if cancellation is not None: @@ -983,18 +989,15 @@ def _execute_single_axis_static( observed_records = runtime_value_store.observed_values_after( observation_cursor ) + reused_outputs = preview_reused_step_outputs( + step_plan, frozen_context, observed_records, + ) runtime_progress_context = _runtime_observation_progress_context( observed_records, - # Historical debug previews are not a new-save observation. - materialized_locations_by_address=preview_reused_materialized_artifact_locations( - step_plan, frozen_context, observed_records - ), - ) - runtime_export_paths.extend( - preview_reused_runtime_export_paths( - step_plan, frozen_context, observed_records - ) + materialized_locations_by_address=reused_outputs.materialized_locations_by_address, ) + runtime_export_paths.extend(reused_outputs.runtime_export_paths) + analysis_inputs.append(reused_outputs.analysis_inputs) emit( execution_id=lane_context.execution_id, plate_id=lane_context.plate_id, @@ -1029,6 +1032,7 @@ def _execute_single_axis_static( observation_cursor = runtime_value_store.observation_cursor() step_observation = step.process(frozen_context, step_index) runtime_export_paths.extend(step_observation.runtime_export_paths) + analysis_inputs.append(step_observation.analysis_inputs) observed_records = runtime_value_store.observed_values_after(observation_cursor) runtime_progress_context = _runtime_observation_progress_context( observed_records, @@ -1063,6 +1067,7 @@ def _execute_single_axis_static( context_key=context_key, records=(), runtime_export_paths=tuple(dict.fromkeys(runtime_export_paths)), + analysis_inputs=RuntimeAnalysisConsolidationInputs.combine(analysis_inputs), ), ) ), diff --git a/openhcs/core/steps/abstract.py b/openhcs/core/steps/abstract.py index 19402fa4d3..197a5687b4 100644 --- a/openhcs/core/steps/abstract.py +++ b/openhcs/core/steps/abstract.py @@ -39,11 +39,14 @@ # ProcessingContext is used in type hints if TYPE_CHECKING: from openhcs.core.context.processing_context import ProcessingContext + from openhcs.core.orchestrator.analysis_consolidation import ( + RuntimeAnalysisConsolidationInputs, + ) @dataclass(frozen=True, slots=True) class StepExecutionObservation: - """Execution facts emitted by one step's authoritative side effects.""" + """Saved outputs and explicitly reused historical outputs from one step.""" materialized_locations_by_address: Mapping[ RuntimeArtifactAddress, @@ -51,6 +54,7 @@ class StepExecutionObservation: ] runtime_export_paths: tuple[Path, ...] = field(default_factory=tuple) + analysis_inputs: "RuntimeAnalysisConsolidationInputs | None" = None @classmethod def empty(cls) -> "StepExecutionObservation": @@ -60,8 +64,13 @@ def empty(cls) -> "StepExecutionObservation": def combine( cls, observations: Iterable["StepExecutionObservation"] ) -> "StepExecutionObservation": + from openhcs.core.orchestrator.analysis_consolidation import ( + RuntimeAnalysisConsolidationInputs, + ) + locations = {} paths = [] + analysis_inputs = [] for observation in observations: for ( address, @@ -71,7 +80,11 @@ def combine( dict.fromkeys((*locations.get(address, ()), *values)) ) paths.extend(observation.runtime_export_paths) - return cls(MappingProxyType(locations), tuple(dict.fromkeys(paths))) + analysis_inputs.append(observation.analysis_inputs) + return cls( + MappingProxyType(locations), tuple(dict.fromkeys(paths)), + RuntimeAnalysisConsolidationInputs.combine(analysis_inputs), + ) # def get_step_id(step: 'AbstractStep') -> str: diff --git a/openhcs/core/steps/function_artifact_materialization.py b/openhcs/core/steps/function_artifact_materialization.py index e18f94bc89..3fca0ef912 100644 --- a/openhcs/core/steps/function_artifact_materialization.py +++ b/openhcs/core/steps/function_artifact_materialization.py @@ -1172,6 +1172,37 @@ def outputs( output_path_filter=output_path_filter, ) + def reused_observation( + self, plan: CompiledStepPlan, context: "ProcessingContext", + ) -> StepExecutionObservation: + """Report historical debug destinations and read their retained CSV text.""" + from openhcs.core.orchestrator.analysis_consolidation import ( + RuntimeAnalysisConsolidationInputs, + ) + + target = plan.runtime_artifact_materialization + if not target.has_persistent_target: + return StepExecutionObservation.empty() + backend = target.require_persistent_backend() + outputs = self.outputs(plan, context) + locations = ( + {RuntimeArtifactAddress.from_record(self.record): tuple( + RuntimeArtifactLocation(path=output.path, backend=backend) + for output in outputs + )} + if self.spec.participates_in_persistent_materialization() + else {} + ) + paths = ( + tuple(Path(output.path) for output in outputs) + if self.spec.participates_in_runtime_export_observation() + else () + ) + return StepExecutionObservation( + MappingProxyType(locations), paths, + RuntimeAnalysisConsolidationInputs.from_reused_outputs(context, plan, self, outputs), + ) + def viewer_outputs( self, plan: CompiledStepPlan, @@ -1258,56 +1289,21 @@ def runtime_artifact_materializations_from_records( return tuple(materializations) -def preview_reused_runtime_export_paths( +def preview_reused_step_outputs( plan: CompiledStepPlan, context: "ProcessingContext", records: tuple[StoredRuntimeValue, ...], -) -> tuple[Path, ...]: - """Predict historical debug-reuse exports, independently of new save outcomes.""" +) -> StepExecutionObservation: + """Project explicitly reused historical outputs once, independently of new saves.""" if not plan.runtime_artifact_materialization.has_persistent_target: - return () - return tuple( - Path(output.path) - for materialization in runtime_artifact_materializations_from_records( - plan, - context, - records, - ) - if materialization.spec.participates_in_runtime_export_observation() - for output in materialization.outputs(plan, context) + return StepExecutionObservation.empty() + return StepExecutionObservation.combine( + item.reused_observation(plan, context) + for item in runtime_artifact_materializations_from_records(plan, context, records) ) -def preview_reused_materialized_artifact_locations( - plan: CompiledStepPlan, - context: "ProcessingContext", - records: tuple[StoredRuntimeValue, ...], -) -> Mapping[RuntimeArtifactAddress, tuple[RuntimeArtifactLocation, ...]]: - """Predict historical debug-reuse destinations from retained logical values.""" - - if not plan.runtime_artifact_materialization.has_persistent_target: - return {} - backend = plan.runtime_artifact_materialization.require_persistent_backend() - locations_by_address: dict[ - RuntimeArtifactAddress, - tuple[RuntimeArtifactLocation, ...], - ] = {} - for materialization in runtime_artifact_materializations_from_records( - plan, - context, - records, - ): - if not materialization.spec.participates_in_persistent_materialization(): - continue - address = RuntimeArtifactAddress.from_record(materialization.record) - locations_by_address[address] = tuple( - RuntimeArtifactLocation(path=output.path, backend=backend) - for output in materialization.outputs(plan, context) - ) - return locations_by_address - - def planned_materialization_preview( *, context: "ProcessingContext", @@ -1375,7 +1371,9 @@ class MaterializedRuntimeArtifact(SavedMaterializationOutputs): materialization: RuntimeArtifactMaterialization - def observation(self, plan: CompiledStepPlan) -> StepExecutionObservation: + def observation( + self, plan: CompiledStepPlan, context: "ProcessingContext", + ) -> StepExecutionObservation: target = plan.runtime_artifact_materialization if not target.has_persistent_target: return StepExecutionObservation.empty() @@ -1393,7 +1391,14 @@ def observation(self, plan: CompiledStepPlan) -> StepExecutionObservation: if self.materialization.spec.participates_in_runtime_export_observation() else () ) - return StepExecutionObservation(MappingProxyType({address: locations}), paths) + from openhcs.core.orchestrator.analysis_consolidation import ( + RuntimeAnalysisConsolidationInputs, + ) + + return StepExecutionObservation( + MappingProxyType({address: locations}), paths, + RuntimeAnalysisConsolidationInputs.from_saved_outputs(context, plan, self), + ) def materialize_artifact_outputs( diff --git a/openhcs/core/steps/function_outputs.py b/openhcs/core/steps/function_outputs.py index 0758567b81..31be6d0720 100644 --- a/openhcs/core/steps/function_outputs.py +++ b/openhcs/core/steps/function_outputs.py @@ -152,7 +152,7 @@ def finalize_function_step_outputs( plan, ) return StepExecutionObservation.combine( - item.observation(plan) for item in materializations + item.observation(plan, context) for item in materializations ) diff --git a/openhcs/processing/materialization/core.py b/openhcs/processing/materialization/core.py index 9ceb743937..e282d23497 100644 --- a/openhcs/processing/materialization/core.py +++ b/openhcs/processing/materialization/core.py @@ -205,6 +205,7 @@ def filemanager_batches( ( output.metadata.attach_to(output.content) if output.metadata is not None + and ImageFileFormat.is_image_path(output.path) else output.content ) for output in outputs diff --git a/tests/unit/test_function_artifact_materialization.py b/tests/unit/test_function_artifact_materialization.py index 39595364c3..ef652afa05 100644 --- a/tests/unit/test_function_artifact_materialization.py +++ b/tests/unit/test_function_artifact_materialization.py @@ -116,13 +116,15 @@ SOURCE_PLANE_INDEX_FIELD, ) from openhcs.core.source_spatial_domain import SourceSpatialDomain +from openhcs.core.steps.abstract import StepExecutionObservation from openhcs.core.steps.function_artifact_materialization import ( ArtifactMaterializationBackendPlan, PersistentArtifactMaterializationTargetPlan, StreamingOnlyArtifactMaterializationTargetPlan, actual_materialization_records, materialize_artifact_outputs, - preview_reused_materialized_artifact_locations, + preview_reused_step_outputs, + RuntimeArtifactMaterialization, planned_materialization_preview, runtime_artifact_materializations, runtime_artifact_materializations_from_records, @@ -319,6 +321,14 @@ def ensure_directory(self, path, backend): def load(self, path, backend): return self.memory[path] + def load_text(self, path, backend): + for content, saved_path, saved_backend, _kwargs in reversed(self.saved): + if str(saved_path) == str(path) and saved_backend == backend: + if not isinstance(content, str): + raise TypeError("Saved output is not text") + return content + raise FileNotFoundError(path) + def save(self, content, path, backend, **kwargs): self.saved.append((content, path, backend, kwargs)) @@ -1530,9 +1540,10 @@ def test_multi_plane_roi_aggregate_defers_source_filenames_to_plane_writer(monke assert str(materialization.base_path) == ( "/analysis/A01_z_index-1_timepoint-1_segmentation_masks_step7.roi.zip" ) + roi_outputs = materialization.outputs(plan, context) assert tuple( output.path - for output in materialization.outputs(plan, context) + for output in roi_outputs if output.path.endswith(".roi.zip") ) == tuple( ( @@ -1555,8 +1566,8 @@ def test_multi_plane_roi_aggregate_defers_source_filenames_to_plane_writer(monke ), ) assert ( - RuntimeAnalysisConsolidationInputs.from_records( - context, context.runtime_value_store.observed_values, + RuntimeAnalysisConsolidationInputs.from_reused_outputs( + context, plan, materialization, roi_outputs, ) is None ) @@ -1709,11 +1720,24 @@ def record_for_site(site: str) -> None: current_execution_records = context.runtime_value_store.observed_values_after( current_execution_cursor ) + saved = materialize_artifact_outputs( + context.filemanager, plan, + PersistentArtifactMaterializationTargetPlan("disk"), context, + ) + selected_saved = tuple( + artifact for artifact in saved + if artifact.materialization.record.key == current_execution_records[0].key + ) + assert len(selected_saved) == 1 + selected_path = selected_saved[0].outputs_for_backend("disk")[0].path + historical_content = "cell_count\r\n99\r\n" + context.filemanager.save(historical_content, selected_path, "disk") + reused = preview_reused_step_outputs(plan, context, current_execution_records) + assert reused.analysis_inputs is not None + assert reused.analysis_inputs.outputs_by_directory[Path("/analysis")][0].csv_content == historical_content assert tuple( Path(location.path) - for locations in preview_reused_materialized_artifact_locations( - plan, context, current_execution_records - ).values() + for locations in reused.materialized_locations_by_address.values() for location in locations ) == ( Path( @@ -1721,11 +1745,7 @@ def record_for_site(site: str) -> None: "cell_counts_step7_details.csv" ), ) - assert preview_reused_materialized_artifact_locations( - plan, - context, - current_execution_records, - ) == { + assert reused.materialized_locations_by_address == { RuntimeArtifactAddress.from_record(current_execution_records[0]): ( RuntimeArtifactLocation( path=( @@ -1737,9 +1757,7 @@ def record_for_site(site: str) -> None: ), } context.step_plans = {plan.step_index: plan} - consolidation_inputs = RuntimeAnalysisConsolidationInputs.from_records( - context, current_execution_records, - ) + consolidation_inputs = selected_saved[0].observation(plan, context).analysis_inputs assert consolidation_inputs is not None runtime_output = consolidation_inputs.outputs_by_directory[Path("/analysis")][0] assert runtime_output.path == Path( @@ -1784,13 +1802,10 @@ def test_terminal_persistence_is_reported_without_becoming_declared_export() -> persistent_backend="disk", ) - locations = preview_reused_materialized_artifact_locations( - plan, - context, - (record,), - ) - - assert locations == { + reused = preview_reused_step_outputs(plan, context, (record,)) + assert reused.runtime_export_paths == () + assert reused.analysis_inputs is None + assert reused.materialized_locations_by_address == { RuntimeArtifactAddress.from_record(record): ( RuntimeArtifactLocation( path="/analysis/A01_cell_counts_step7_details.csv", @@ -1841,9 +1856,13 @@ def test_consolidation_preserves_export_bundle_and_text_tables(options, payload) persistent_backend="disk", ) context.step_plans = {plan.step_index: plan} - consolidation = RuntimeAnalysisConsolidationInputs.from_records( - context, context.runtime_value_store.observed_values, + saved = materialize_artifact_outputs( + context.filemanager, plan, + PersistentArtifactMaterializationTargetPlan("disk"), context, ) + consolidation = StepExecutionObservation.combine( + artifact.observation(plan, context) for artifact in saved + ).analysis_inputs assert consolidation is not None outputs = tuple( output @@ -1861,7 +1880,7 @@ def test_consolidation_preserves_export_bundle_and_text_tables(options, payload) (RuntimeObservationMode.OMIT, RuntimeObservationMode.MERGE_INTO_PARENT), ) def test_completed_observation_projects_tables_before_worker_payload_release( - observation_mode, + observation_mode, monkeypatch, ): image_plan = ArtifactOutputPlan( name="Corrected", path="/memory/Corrected.pkl", artifact_type=ImageArtifactType, @@ -1907,14 +1926,19 @@ def test_completed_observation_projects_tables_before_worker_payload_release( context.filemanager, table_step, PersistentArtifactMaterializationTargetPlan("disk"), context, ) + monkeypatch.setattr( + RuntimeArtifactMaterialization, "outputs", + lambda *_args, **_kwargs: pytest.fail("Saved tables must not render again"), + ) + step_observation = StepExecutionObservation.combine( + artifact.observation(table_step, context) for artifact in saved + ) observation = RuntimeContextObservation.from_context( context_key="A01", context=context, records=context.runtime_value_store.observed_values, runtime_observation_mode=observation_mode, - runtime_export_paths=tuple( - path for artifact in saved - for path in artifact.observation(table_step).runtime_export_paths - ), + runtime_export_paths=step_observation.runtime_export_paths, + analysis_inputs=step_observation.analysis_inputs, ) context.runtime_value_store.clear() del pixels diff --git a/tests/unit/test_function_outputs.py b/tests/unit/test_function_outputs.py index a238e2431a..cc2265740d 100644 --- a/tests/unit/test_function_outputs.py +++ b/tests/unit/test_function_outputs.py @@ -34,7 +34,7 @@ ) from openhcs.core.component_group_scope import RuntimeExecutionAxisScope from openhcs.core.components.parser_metaprogramming import FilenameParseResult -from openhcs.core.config import TiffConfig, WellFilterMode +from openhcs.core.config import AnalysisConsolidationConfig, TiffConfig, WellFilterMode from openhcs.core.function_patterns import compile_function_pattern from openhcs.core.runtime_image_loading import ImagePayloadSourceMetadataContext from openhcs.core.runtime_image_values import ( @@ -348,6 +348,7 @@ def context_stub(filemanager, parser=None): context.execution_runtime = SimpleNamespace(execution_axis_values=("A01",)) context.axis_id = "A01" context.tiff_config = TiffConfig() + context.analysis_consolidation_config = AnalysisConsolidationConfig() context.step_axis_filters = {} return context @@ -2526,7 +2527,7 @@ def test_actual_array_exports_only_publish_declared_raster_inventory( location for artifact in materializations for locations in ( - artifact.observation(plan).materialized_locations_by_address.values() + artifact.observation(plan, context).materialized_locations_by_address.values() ) for location in locations ) diff --git a/tests/unit/test_function_step_execution_scope.py b/tests/unit/test_function_step_execution_scope.py index 6147a2ffb7..246992ace6 100644 --- a/tests/unit/test_function_step_execution_scope.py +++ b/tests/unit/test_function_step_execution_scope.py @@ -23,7 +23,10 @@ SpecialArtifactType, ) from openhcs.core.callable_contract import CallableContract, FunctionStepExecutionScope -from openhcs.core.compiled_step_plan import CompiledStepPlan +from openhcs.core.compiled_step_plan import ( + CompiledStepPlan, + RuntimeArtifactMaterializationPlan, +) from openhcs.core.component_group_scope import RuntimeExecutionAxisScope from openhcs.core.component_group_scope import ComponentGroupScope from openhcs.core.component_set import ComponentSet @@ -80,6 +83,8 @@ StepInputDependencyKind, ) from openhcs.core.steps.function_step import FunctionStep +from openhcs.microscopes.source_schema import SourceSchemaFilenameParser +from openhcs.processing.materialization import FileBundleOptions, MaterializationSpec def test_runtime_step_values_share_within_step_and_release_between_steps() -> None: @@ -724,7 +729,7 @@ def export(*, artifact_batch: RuntimeArtifactBatch): assert events[-1].completed == events[-1].total == 2 -def test_plate_scope_observation_excludes_preexisting_runtime_history() -> None: +def test_plate_scope_observation_excludes_preexisting_runtime_history(tmp_path) -> None: output_spec = ArtifactSpec.output("ExportBundle", SpecialArtifactType) @execution_scope(FunctionStepExecutionScope.PLATE) @@ -732,7 +737,7 @@ def test_plate_scope_observation_excludes_preexisting_runtime_history() -> None: @artifact_outputs(output_spec) def export(*, artifact_batch: RuntimeArtifactBatch): del artifact_batch - return {"export.txt": b"data"} + return {"Image.csv": "ImageNumber,Count\r\n1,7\r\n"} plan = _plate_step_plan( axis_id="A01", @@ -743,10 +748,21 @@ def export(*, artifact_batch: RuntimeArtifactBatch): name=output_spec.name, path="/memory/plate/export", artifact_type=output_spec.artifact_type, + materialization=MaterializationSpec(FileBundleOptions()), ), metadata_writer=True, ) + plan.runtime_artifact_materialization = RuntimeArtifactMaterializationPlan( + persistent_enabled=True, persistent_backend=Backend.MEMORY.value, + ) + plan.output_plate_root = str(tmp_path) + plan.output_dir = tmp_path / "images" + plan.analysis_results_dir = str(tmp_path / "analysis") context = _plate_context("A01", (plan,)) + context.metadata_cache = {} + context.microscope_handler = SimpleNamespace( + parser=SourceSchemaFilenameParser(), microscope_type="test", + ) _record_measurements( context, name="prior_measurements", @@ -768,6 +784,16 @@ def export(*, artifact_batch: RuntimeArtifactBatch): assert {record.key.name for record in observation.contexts[0].records} == { output_spec.name } + analysis_inputs = observation.contexts[0].analysis_inputs + assert analysis_inputs is not None + assert analysis_inputs.destination.backend == Backend.MEMORY.value + (table,) = tuple( + output for group in analysis_inputs.outputs_by_directory.values() + for output in group + ) + assert table.csv_content == "ImageNumber,Count\r\n1,7\r\n" + assert table.well_id == "A01" + assert observation.contexts[0].runtime_export_paths == (table.path,) events = [ProgressEvent.from_dict(event) for event in progress_queue.events] assert events[-1].phase is ProgressPhase.STEP_COMPLETED diff --git a/tests/unit/test_materialization_batch_outcomes.py b/tests/unit/test_materialization_batch_outcomes.py index a24b092c58..ddb21fc62b 100644 --- a/tests/unit/test_materialization_batch_outcomes.py +++ b/tests/unit/test_materialization_batch_outcomes.py @@ -180,12 +180,7 @@ def forbidden_reconstruction(*_args, **_kwargs): monkeypatch.setattr( worker, - "preview_reused_materialized_artifact_locations", - forbidden_reconstruction, - ) - monkeypatch.setattr( - worker, - "preview_reused_runtime_export_paths", + "preview_reused_step_outputs", forbidden_reconstruction, ) result = _execute_axis_with_sequential_combinations( From 771950f652e5237b71bf0b21b0b8050bda48e011 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 22:05:50 -0400 Subject: [PATCH 197/235] refactor: own measurement roster execution and recording together --- .../runtime/measurement_execution_support.py | 230 ++++++++---------- .../cellprofiler/runtime/module_execution.py | 211 +++++++--------- .../test_cellprofiler_module_execution.py | 115 ++++++++- 3 files changed, 300 insertions(+), 256 deletions(-) diff --git a/openhcs/interop/cellprofiler/runtime/measurement_execution_support.py b/openhcs/interop/cellprofiler/runtime/measurement_execution_support.py index 92c93a7309..61c4adfc56 100644 --- a/openhcs/interop/cellprofiler/runtime/measurement_execution_support.py +++ b/openhcs/interop/cellprofiler/runtime/measurement_execution_support.py @@ -24,7 +24,10 @@ from openhcs.core.runtime_object_labels import ( ObjectLabelValue, ) -from openhcs.core.runtime_batch_contracts import RuntimeBatchInvocationRequest +from openhcs.core.runtime_batch_contracts import ( + RuntimeBatchExecutionDomain, + RuntimeBatchInvocationRequest, +) from openhcs.interop.cellprofiler.runtime.adapter import CellProfilerRuntimeAdapter from openhcs.interop.cellprofiler.runtime.function_contract_execution import ( _execute_runtime_batch_invocation, @@ -212,111 +215,6 @@ class PreparedObjectMeasurementInvocation(RuntimeBatchInvocationRequest): invocation: "ObjectMeasurementInvocation" completion_label_payload: RuntimeCallableArgument - def record_output( - self, - output_recorder: "ObjectMeasurementOutputRecorder", - raw_output: RuntimeCallableArgument, - ) -> None: - """Record one raw invocation output through the shared recorder.""" - output_recorder.record( - raw_output, - measurement_image=self.measurement_image, - object_spec=self.object_spec, - completion_label_payload=self.completion_label_payload, - invocation=self.invocation, - ) - - -@dataclass(frozen=True, slots=True) -class PreparedObjectMeasurementInvocationBatch: - """Execute prepared object-measurement invocations in declared batch order.""" - - callable_contract: CallableContract - func: Callable[..., RuntimeFunctionOutput] - function_name: str - invocations: tuple[PreparedObjectMeasurementInvocation, ...] - batch_executor: ObjectMeasurementBatchExecutor | None - - def execute( - self, - output_recorder: "ObjectMeasurementOutputRecorder", - ) -> float: - """Execute all invocations, record outputs, and return contract seconds.""" - if self.batch_executor is not None: - return self._execute_batched(output_recorder) - return self._execute_serial(output_recorder) - - def _execute_serial( - self, - output_recorder: "ObjectMeasurementOutputRecorder", - ) -> float: - contract_execute_seconds = 0.0 - for prepared_invocation in self.invocations: - contract_started_at = time.perf_counter() - raw_output = _execute_runtime_batch_invocation( - self.callable_contract, - self.func, - prepared_invocation, - ) - contract_execute_seconds += time.perf_counter() - contract_started_at - prepared_invocation.record_output(output_recorder, raw_output) - return contract_execute_seconds - - def _execute_batched( - self, - output_recorder: "ObjectMeasurementOutputRecorder", - ) -> float: - batch_requests = tuple( - invocation.batch_executor_request() for invocation in self.invocations - ) - if any(request is None for request in batch_requests): - return self._execute_serial(output_recorder) - executable_requests = tuple( - request for request in batch_requests if request is not None - ) - contract_started_at = time.perf_counter() - raw_outputs = tuple( - self.require_batch_executor()( - self.func, - executable_requests, - partial( - _execute_runtime_batch_invocation, - self.callable_contract, - ), - ) - ) - contract_execute_seconds = time.perf_counter() - contract_started_at - if len(raw_outputs) != len(self.invocations): - raise ValueError( - f"{self.function_name} measurement-image batch executor returned " - f"{len(raw_outputs)} outputs for {len(self.invocations)} requests." - ) - - ordered_batch_outputs = { - prepared_invocation.batch_index: ( - raw_output, - prepared_invocation, - ) - for raw_output, prepared_invocation in zip( - raw_outputs, - self.invocations, - strict=True, - ) - } - for order_index in range(len(ordered_batch_outputs)): - raw_output, prepared_invocation = ordered_batch_outputs[order_index] - prepared_invocation.record_output(output_recorder, raw_output) - return contract_execute_seconds - - def require_batch_executor(self) -> ObjectMeasurementBatchExecutor: - """Return the declared batch executor for the batched path.""" - if self.batch_executor is None: - raise RuntimeError( - "PreparedObjectMeasurementInvocationBatch requires a batch executor " - "for batched execution." - ) - return self.batch_executor - def object_label_stage_event( stage: str, @@ -381,45 +279,109 @@ def record_events( @dataclass(slots=True) -class ObjectMeasurementOutputTimings: - """Mutable timings for per-object measurement output handling.""" - - split_seconds: float = 0.0 - complete_rows_seconds: float = 0.0 - annotate_seconds: float = 0.0 - - -@dataclass(frozen=True, slots=True) class ObjectMeasurementOutputRecorder: - """Record one per-object CellProfiler measurement output.""" + """Execute and record an ordered object-measurement roster.""" callable_contract: CallableContract measurement_output_plan: ArtifactOutputPlan row_policy: "CellProfilerObjectMeasurementRowPolicy" module_type: type["CellProfilerModule"] - func: Callable[..., RuntimeFunctionOutput] - adapter: CellProfilerRuntimeAdapter measurement_images: tuple["CellProfilerMeasurementImage", ...] object_inputs: tuple[ArtifactSpec, ...] image_measurement_rows: list[ColumnarRows] columnar_rows: list[ColumnarRows] - timings: ObjectMeasurementOutputTimings + split_seconds: float = 0.0 + complete_rows_seconds: float = 0.0 + annotate_seconds: float = 0.0 + + def execute( + self, + invocations: tuple[PreparedObjectMeasurementInvocation, ...], + ) -> float: + """Execute all invocations, record outputs, and return contract seconds.""" + batch_executor = self.callable_contract.runtime_batch_executor( + RuntimeBatchExecutionDomain.MEASUREMENT_IMAGES + ) + if batch_executor is not None: + return self._execute_batched(invocations, batch_executor) + return self._execute_serial(invocations) + + def _execute_serial( + self, + invocations: tuple[PreparedObjectMeasurementInvocation, ...], + ) -> float: + func = self.callable_contract.resolve_canonical_raw_callable() + contract_execute_seconds = 0.0 + for prepared_invocation in invocations: + contract_started_at = time.perf_counter() + raw_output = _execute_runtime_batch_invocation( + self.callable_contract, + func, + prepared_invocation, + ) + contract_execute_seconds += time.perf_counter() - contract_started_at + self.record(raw_output, prepared_invocation) + return contract_execute_seconds + + def _execute_batched( + self, + invocations: tuple[PreparedObjectMeasurementInvocation, ...], + batch_executor: ObjectMeasurementBatchExecutor, + ) -> float: + func = self.callable_contract.resolve_canonical_raw_callable() + batch_requests = tuple( + invocation.batch_executor_request() for invocation in invocations + ) + if any(request is None for request in batch_requests): + return self._execute_serial(invocations) + executable_requests = tuple( + request for request in batch_requests if request is not None + ) + contract_started_at = time.perf_counter() + raw_outputs = tuple( + batch_executor( + func, + executable_requests, + partial( + _execute_runtime_batch_invocation, + self.callable_contract, + ), + ) + ) + contract_execute_seconds = time.perf_counter() - contract_started_at + if len(raw_outputs) != len(invocations): + raise ValueError( + f"{self.callable_contract.function_name} measurement-image batch executor returned " + f"{len(raw_outputs)} outputs for {len(invocations)} requests." + ) + + ordered_batch_outputs = { + prepared_invocation.batch_index: ( + raw_output, + prepared_invocation, + ) + for raw_output, prepared_invocation in zip( + raw_outputs, + invocations, + strict=True, + ) + } + for order_index in range(len(ordered_batch_outputs)): + raw_output, prepared_invocation = ordered_batch_outputs[order_index] + self.record(raw_output, prepared_invocation) + return contract_execute_seconds def record( self, raw_output: RuntimeCallableArgument, - *, - measurement_image: "CellProfilerMeasurementImage", - object_spec: ArtifactSpec, - completion_label_payload: RuntimeCallableArgument, - invocation: "ObjectMeasurementInvocation", + prepared_invocation: PreparedObjectMeasurementInvocation, ) -> None: split_started_at = time.perf_counter() _returned_values, matched_outputs = RuntimeReturnedOutputMatcher( callable_contract=self.callable_contract, returned_output=raw_output, ).resolve_plan_values((self.measurement_output_plan,)) - self.timings.split_seconds += time.perf_counter() - split_started_at + self.split_seconds += time.perf_counter() - split_started_at _output_plan, _output_spec, output_value = matched_outputs[0] emitted_measurement_rows = measurement_table_rows(output_value) CellProfilerRuntimeProfileLogger.log_module_profile( @@ -431,23 +393,23 @@ def record( ) raw_measurement_rows = self.row_policy.project_rows( emitted_measurement_rows, - invocation, + prepared_invocation.invocation, ) object_measurement_rows, non_object_measurement_rows = ( self.row_policy.split_scoped_rows(raw_measurement_rows) ) measurement_rows = self.completed_measurement_rows( object_measurement_rows, - completion_label_payload, + prepared_invocation.completion_label_payload, ) self.record_non_object_rows( non_object_measurement_rows, - measurement_image, + prepared_invocation.measurement_image, ) self.record_object_rows( measurement_rows, - object_spec, - measurement_image, + prepared_invocation.object_spec, + prepared_invocation.measurement_image, ) def completed_measurement_rows( @@ -460,9 +422,7 @@ def completed_measurement_rows( object_measurement_rows, label_payload=completion_label_payload, ) - self.timings.complete_rows_seconds += ( - time.perf_counter() - complete_rows_started_at - ) + self.complete_rows_seconds += time.perf_counter() - complete_rows_started_at return measurement_rows def project_owned_rows( @@ -500,7 +460,7 @@ def record_non_object_rows( row_object_name=None, ) self.image_measurement_rows.append(projected_rows) - self.timings.annotate_seconds += time.perf_counter() - annotate_started_at + self.annotate_seconds += time.perf_counter() - annotate_started_at def record_object_rows( self, @@ -515,4 +475,4 @@ def record_object_rows( row_object_name=object_spec.name, ) self.columnar_rows.append(projected_rows) - self.timings.annotate_seconds += time.perf_counter() - annotate_started_at + self.annotate_seconds += time.perf_counter() - annotate_started_at diff --git a/openhcs/interop/cellprofiler/runtime/module_execution.py b/openhcs/interop/cellprofiler/runtime/module_execution.py index 4c7223ad6f..e3ea0f9637 100644 --- a/openhcs/interop/cellprofiler/runtime/module_execution.py +++ b/openhcs/interop/cellprofiler/runtime/module_execution.py @@ -41,7 +41,6 @@ from openhcs.core.runtime_adapters import ( RuntimeAdapterRequest, ) -from openhcs.core.runtime_batch_contracts import RuntimeBatchExecutionDomain from openhcs.core.runtime_image_values import ( ImagePayloadMetadata, image_payload_metadata, @@ -86,9 +85,7 @@ from openhcs.interop.cellprofiler.runtime.measurement_execution_support import ( CellProfilerRuntimeProfiler, ObjectMeasurementOutputRecorder, - ObjectMeasurementOutputTimings, PreparedObjectMeasurementInvocation, - PreparedObjectMeasurementInvocationBatch, object_measurement_batch_group_key, object_measurement_runtime_inputs, ) @@ -798,31 +795,42 @@ def _run_per_object_measurement( elif not image_inputs: source_image_name = None else: - image_strategy = CellProfilerOutputRecorder.for_artifact_type(ImageArtifactType) + image_strategy = CellProfilerOutputRecorder.for_artifact_type( + ImageArtifactType + ) runtime_projection = RuntimePlaneAxisValueProjection.from_projector( - adapter, RuntimePlaneAxis.RUNTIME_SLICE, (), + adapter, + RuntimePlaneAxis.RUNTIME_SLICE, + (), ) projected_binding = ( RuntimeInputBindingRequest( - adapter=adapter, kwargs={}, + adapter=adapter, + kwargs={}, current_image=RuntimeSliceProjection.value_for_slice( - current_image, runtime_projection, + current_image, + runtime_projection, ), ) - if runtime_projection is not None and runtime_projection.plane_index is not None + if runtime_projection is not None + and runtime_projection.plane_index is not None else None ) source_names = [] - for spec, measurement_image in zip(image_inputs, measurement_images, strict=True): + for spec, measurement_image in zip( + image_inputs, measurement_images, strict=True + ): edge = ( adapter.request.require_artifact_input_edge(spec.ref()) - if projected_binding is not None else None + if projected_binding is not None + else None ) if edge is not None and edge.main_flow_projection is not None: source_name = image_strategy.source_image_name( spec, projected_binding.main_flow_value( - spec, projection=edge.main_flow_projection, + spec, + projection=edge.main_flow_projection, ), ) else: @@ -831,7 +839,8 @@ def _run_per_object_measurement( ) source_names.append(source_name) source_image_name = self._primary_image_source_name_from_sources( - image_inputs, tuple(source_names), + image_inputs, + tuple(source_names), ) if not measurement_images: measurement_images = tuple( @@ -842,28 +851,6 @@ def _run_per_object_measurement( ) for object_spec in object_inputs ) - measurement_object_pairs = tuple( - ( - measurement_image, - object_spec, - True, - ) - for measurement_image, object_spec in zip( - measurement_images, - object_inputs, - strict=True, - ) - ) - else: - measurement_object_pairs = tuple( - ( - measurement_image, - object_spec, - object_index == 0, - ) - for measurement_image in measurement_images - for object_index, object_spec in enumerate(object_inputs) - ) profile_events: list[CellProfilerRuntimeProfileEvent] = [] if profile_enabled: profile_events.append( @@ -883,108 +870,98 @@ def _run_per_object_measurement( label_payload_seconds = 0.0 label_align_seconds = 0.0 contract_execute_seconds = 0.0 - output_timings = ObjectMeasurementOutputTimings() columnar_rows: list[ColumnarRows] = [] - batch_executor = self.callable_contract.runtime_batch_executor( - RuntimeBatchExecutionDomain.MEASUREMENT_IMAGES - ) processing_contract = self.callable_contract.require_processing_contract() output_recorder = ObjectMeasurementOutputRecorder( callable_contract=self.callable_contract, measurement_output_plan=measurement_output_plan, row_policy=measurement_row_policy, module_type=module_type, - func=func, - adapter=cellprofiler_runtime, measurement_images=measurement_images, object_inputs=object_inputs, image_measurement_rows=image_measurement_rows, columnar_rows=columnar_rows, - timings=output_timings, ) measurement_invocations = tuple( measurement_row_policy.invocations(measurement_image, kwargs) - for ( - measurement_image, - _object_spec, - _include_image_measurements, - ) in measurement_object_pairs + for measurement_image in measurement_images ) total_measurement_batch_count = sum( - (len(invocations) for invocations in measurement_invocations) - ) + len(invocations) for invocations in measurement_invocations + ) * (len(object_inputs) if image_inputs else 1) prepared_invocations: list[PreparedObjectMeasurementInvocation] = [] - for ( - measurement_image, - object_spec, - include_image_measurements, - ), invocations in zip( - measurement_object_pairs, measurement_invocations, strict=True + for image_index, (measurement_image, invocations) in enumerate( + zip( + measurement_images, + measurement_invocations, + strict=True, + ) ): - label_payload = RuntimeInputBindingRequest( - adapter=cellprofiler_runtime, - kwargs=kwargs, - current_image=measurement_image.payload, - ).label_payload_for(object_spec) - ( - aligned_measurement_image, - executable_labels, - completion_label_payload, - execution_mode, - preparation_profile_events, - label_payload_elapsed, - label_align_elapsed, - ) = object_measurement_runtime_inputs( - object_label_execution=object_label_input_execution_mode_from_callable( - self.raw_func - ), - measurement_image=measurement_image, - object_spec=object_spec, - label_payload=label_payload, - adapter=cellprofiler_runtime, + measured_objects = ( + object_inputs if image_inputs else (object_inputs[image_index],) ) - profile_events.extend(preparation_profile_events) - label_payload_seconds += label_payload_elapsed - label_align_seconds += label_align_elapsed - for invocation in invocations: - invocation_kwargs = module_type.object_measurement_invocation_kwargs( - invocation.lowered_kwargs(), - include_image_measurements=include_image_measurements, + for object_index, object_spec in enumerate(measured_objects): + include_image_measurements = object_index == 0 + label_payload = RuntimeInputBindingRequest( + adapter=cellprofiler_runtime, + kwargs=kwargs, + current_image=measurement_image.payload, + ).label_payload_for(object_spec) + ( + aligned_measurement_image, + executable_labels, + completion_label_payload, + execution_mode, + preparation_profile_events, + label_payload_elapsed, + label_align_elapsed, + ) = object_measurement_runtime_inputs( + object_label_execution=object_label_input_execution_mode_from_callable( + self.raw_func + ), + measurement_image=measurement_image, + object_spec=object_spec, + label_payload=label_payload, + adapter=cellprofiler_runtime, ) - prepared_invocations.append( - PreparedObjectMeasurementInvocation( - source_image_name=aligned_measurement_image.source_image_name, - execution_mode=execution_mode, - plane_projection=aligned_measurement_image.plane_projection, - func=func, - image=aligned_measurement_image.payload, - kwargs={ - **invocation_kwargs, - **shared_runtime_kwargs, - **_execution_mode_semantic_control_kwargs( - processing_contract, - execution_mode, + profile_events.extend(preparation_profile_events) + label_payload_seconds += label_payload_elapsed + label_align_seconds += label_align_elapsed + for invocation in invocations: + invocation_kwargs = ( + module_type.object_measurement_invocation_kwargs( + invocation.lowered_kwargs(), + include_image_measurements=include_image_measurements, + ) + ) + prepared_invocations.append( + PreparedObjectMeasurementInvocation( + source_image_name=aligned_measurement_image.source_image_name, + execution_mode=execution_mode, + plane_projection=aligned_measurement_image.plane_projection, + func=func, + image=aligned_measurement_image.payload, + kwargs={ + **invocation_kwargs, + **shared_runtime_kwargs, + **_execution_mode_semantic_control_kwargs( + processing_contract, + execution_mode, + ), + "labels": executable_labels, + }, + batch_index=len(prepared_invocations), + batch_count=total_measurement_batch_count, + semantic_group_key=object_measurement_batch_group_key( + object_spec=object_spec, labels=completion_label_payload ), - "labels": executable_labels, - }, - batch_index=len(prepared_invocations), - batch_count=total_measurement_batch_count, - semantic_group_key=object_measurement_batch_group_key( - object_spec=object_spec, labels=completion_label_payload - ), - measurement_image=aligned_measurement_image, - object_spec=object_spec, - invocation=invocation, - completion_label_payload=completion_label_payload, + measurement_image=aligned_measurement_image, + object_spec=object_spec, + invocation=invocation, + completion_label_payload=completion_label_payload, + ) ) - ) - contract_execute_seconds = PreparedObjectMeasurementInvocationBatch( - callable_contract=self.callable_contract, - func=func, - function_name=function_name, - invocations=tuple(prepared_invocations), - batch_executor=batch_executor, - ).execute(output_recorder) + contract_execute_seconds = output_recorder.execute(tuple(prepared_invocations)) if profile_enabled: profile_events.extend( ( @@ -998,15 +975,15 @@ def _run_per_object_measurement( "cp_per_object_contract_execute", contract_execute_seconds ), CellProfilerRuntimeProfileEvent( - "cp_per_object_split_output", output_timings.split_seconds + "cp_per_object_split_output", output_recorder.split_seconds ), CellProfilerRuntimeProfileEvent( "cp_per_object_complete_rows", - output_timings.complete_rows_seconds, + output_recorder.complete_rows_seconds, ), CellProfilerRuntimeProfileEvent( "cp_per_object_annotate_rows", - output_timings.annotate_seconds, + output_recorder.annotate_seconds, ( ( "rows", diff --git a/tests/unit/test_cellprofiler_module_execution.py b/tests/unit/test_cellprofiler_module_execution.py index 57669a2eac..9803fb1ead 100644 --- a/tests/unit/test_cellprofiler_module_execution.py +++ b/tests/unit/test_cellprofiler_module_execution.py @@ -219,7 +219,6 @@ ) from openhcs.interop.cellprofiler.runtime.measurement_execution_support import ( ObjectMeasurementOutputRecorder, - ObjectMeasurementOutputTimings, object_measurement_runtime_inputs, ) from openhcs.interop.cellprofiler.runtime.measurement_recording import ( @@ -3968,13 +3967,10 @@ def test_object_measurement_output_recorder_completes_exact_columnar_rows() -> N ), row_policy=row_policy, module_type=MeasureObjectIntensityModule, - func=_synthetic_object_measurement_function, - adapter=cast(CellProfilerRuntimeAdapter, object()), measurement_images=(), object_inputs=(), image_measurement_rows=[], columnar_rows=[], - timings=ObjectMeasurementOutputTimings(), ) completed = recorder.completed_measurement_rows(source_rows, payload) @@ -18395,3 +18391,114 @@ def table_name(self, measurement_images, source_image_name): monkeypatch.setattr(policy_type, "table_source_image_name", table_name) assert _run_module(executor, image, cellprofiler_runtime=runtime) is image assert fallbacks == [original_fallback] + + +@pytest.mark.parametrize("batched", (False, True)) +def test_composed_measurement_roster_keeps_all_object_source_pairs( + monkeypatch, batched +): + from openhcs.core.runtime_batch_contracts import RuntimeBatchExecutionDomain + + image_specs = tuple( + ArtifactSpec.input(name, ImageArtifactType) for name in ("DNA", "ER", "RNA") + ) + object_specs = tuple( + ArtifactSpec.input(name, ObjectLabelsArtifactType) + for name in ("Cells", "Nuclei") + ) + output = ArtifactSpec.output("PairMeasurements", MeasurementsArtifactType) + pixels = np.arange(1, 13, dtype=np.float32).reshape(3, 4) / 12 + images = { + spec.name: ImagePayloadMetadata(source_image_names=(spec.name,)).payload_with( + pixels * scale + ) + for spec, scale in zip(image_specs, (1.0, 0.5, 0.25), strict=True) + } + objects = { + spec.name: ObjectLabelSet( + name=spec.name, + variant_data=ObjectLabelVariantData(labels=np.ones((3, 4), dtype=np.int32)), + domain=ObjectLabelDomain(declared_object_ids=(1,)), + ) + for spec in object_specs + } + contract = _compiled_callable_contract( + MeasureColocalizationModule.require_callable("measure_colocalization_objects"), + artifact_inputs=(*image_specs, *object_specs), + artifact_outputs=(output,), + ) + batch_executor = contract.runtime_batch_executor( + RuntimeBatchExecutionDomain.MEASUREMENT_IMAGES + ) + assert batch_executor is not None + batches = [] + + def execute_batch(func, requests, execute_one): + batches.append( + tuple( + (request.object_spec.name, request.batch_index, request.batch_count) + for request in requests + ) + ) + return batch_executor(func, requests, execute_one) + + contract = replace( + contract, + runtime_batch_executors={ + RuntimeBatchExecutionDomain.MEASUREMENT_IMAGES: execute_batch + } + if batched + else {}, + ) + runtime = _FakeCellProfilerRuntime( + images, + objects, + artifact_input_edges=tuple( + _artifact_input_edge_for_test(spec) + for spec in (*image_specs, *object_specs) + ), + artifact_output_bindings=((output, _artifact_output_plan(output)),), + ) + policy_type = type( + MeasureColocalizationModule.runtime_object_measurement_row_policy() + ) + original_invocations = policy_type.invocations + source_rosters = [] + + def invocations(policy, measurement_image, kwargs): + source_rosters.append(measurement_image.source_aliases) + return original_invocations(policy, measurement_image, kwargs) + + monkeypatch.setattr(policy_type, "invocations", invocations) + current = np.zeros((3, 4), dtype=np.float32) + assert ( + _run_module( + _module_executor(contract), + current, + cellprofiler_runtime=runtime, + do_costes=False, + do_manders=False, + do_rwc=False, + do_overlap=False, + ) + is current + ) + assert source_rosters == [("DNA", "ER", "RNA")] + assert batches == ( + [ + tuple( + (name, index, 6) + for index, name in enumerate(("Cells",) * 3 + ("Nuclei",) * 3) + ) + ] + if batched + else [] + ) + rows = tuple( + row for table in runtime.measurements for row in table.rows.iter_row_mappings() + ) + for name in ("Cells", "Nuclei"): + owned = tuple(row for row in rows if row.get("object_name") == name) + assert len(owned) == 3 + for row, pair in zip(owned, ("DNA_ER", "DNA_RNA", "ER_RNA"), strict=True): + assert row[f"Correlation_Correlation_{pair}"] == pytest.approx(1.0) From c067792819219cf5eb0ae7a20340c9a8b781bf0b Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 22:21:48 -0400 Subject: [PATCH 198/235] refactor: let measurement requests own paired alignment Delete the separate image and label alignment strategy authorities. Retain projected and aligned labels on the existing request so preparation consumes the validated source-domain result without projecting or aligning the same labels again. Preserve original source and completion label epochs, object-reference alignment, sparse plane domains, and mutable raw-call isolation. Refs #384. --- openhcs/core/measurement_image_alignment.py | 508 +++++++++--------- .../cellprofiler/runtime/invocation.py | 5 +- .../test_cellprofiler_module_execution.py | 70 +-- ...test_cellprofiler_static_deletion_gates.py | 4 +- .../unit/test_measurement_image_alignment.py | 65 +-- 5 files changed, 314 insertions(+), 338 deletions(-) diff --git a/openhcs/core/measurement_image_alignment.py b/openhcs/core/measurement_image_alignment.py index f8264e9d8e..010db0a305 100644 --- a/openhcs/core/measurement_image_alignment.py +++ b/openhcs/core/measurement_image_alignment.py @@ -265,237 +265,8 @@ def with_source_projected_labels(self) -> "MeasurementImageLabelAlignmentRequest label_payload=payload_projection, ) - -@dataclass(frozen=True, slots=True) -class PreparedMeasurementObjectLabels: - """Single-pass object-label preparation for measurement-image execution.""" - - request: MeasurementImageLabelAlignmentRequest - source_payload: ObjectLabelValue - source_projected_payload: ObjectLabelValue - source_projected_labels: ObjectLabelMeasurementSource - aligned_source: MeasurementImageAlignmentSource - measurement_labels: ObjectLabelMeasurementSource - completion_payload: ObjectLabelValue - - @property - def aligned_image(self) -> RuntimeArrayData | AlignedImageStack: - """Return the image from the source that owns its projected semantics.""" - - return self.aligned_source.alignment_image - - @classmethod - def from_source( - cls, - source: MeasurementImageAlignmentSource, - label_payload: ObjectLabelValue, - *, - plane_projector: RuntimePlaneAxisProjector | None = None, - align_image_to_labels: bool = True, - ) -> "PreparedMeasurementObjectLabels": - """Prepare labels from one measurement-image source and payload.""" - return cls.from_request( - request=source.object_label_alignment_request( - label_payload, - plane_projector=plane_projector, - align_image_to_labels=align_image_to_labels, - ), - ) - - @classmethod - def from_request( - cls, - request: MeasurementImageLabelAlignmentRequest, - ) -> "PreparedMeasurementObjectLabels": - """Prepare image, dense labels, and completion payload in one pass.""" - if request.label_payload is None: - raise TypeError( - "Measurement object-label preparation requires label_payload." - ) - profile_enabled = RuntimeProfileLogger.enabled() - source_payload = request.label_payload - source_projection_timer = RuntimeProfileTimer.start() - source_projected_request = request.with_source_projected_labels() - source_projected_labels = source_projected_request.labels - source_projection_payload = source_projected_request.label_payload - if source_projection_payload is None: - raise TypeError( - "Measurement object-label source projection lost label_payload." - ) - if profile_enabled: - RuntimeProfileLogger.log( - logger, - "measurement_object_labels_source_projection", - source_projection_timer.elapsed(), - reference_domain=request.reference_domain.value, - source_type=type(request.image).__name__, - labels_type=type(request.labels).__name__, - source_aliases=request.source_aliases, - ) - source_context_timer = RuntimeProfileTimer.start() - source_projected_payload = cls.source_context_payload( - source_projected_request, - source_projection_payload, - source_projected_labels, - ) - if profile_enabled: - RuntimeProfileLogger.log( - logger, - "measurement_object_labels_source_context", - source_context_timer.elapsed(), - reference_domain=request.reference_domain.value, - payload_reused=source_projected_payload is source_payload, - ) - image_request = replace( - source_projected_request, - labels=source_projected_labels, - label_payload=source_projected_payload, - ) - image_align_timer = RuntimeProfileTimer.start() - if request.align_image_to_labels: - aligned_image = MeasurementImageLabelAlignmentStrategy.align(image_request) - else: - aligned_image = request.image - aligned_source = image_request.source.with_alignment_image(aligned_image) - if profile_enabled: - RuntimeProfileLogger.log( - logger, - "measurement_object_labels_image_alignment", - image_align_timer.elapsed(), - reference_domain=request.reference_domain.value, - align_image_to_labels=request.align_image_to_labels, - aligned_type=type(aligned_image).__name__, - ) - label_align_timer = RuntimeProfileTimer.start() - aligned_measurement_labels = MeasurementLabelSourceAlignmentStrategy.align( - aligned_image, - source_projected_labels, - label_payload=source_projected_payload, - ) - measurement_labels = ( - object_label_dense_array(aligned_measurement_labels) - if isinstance(aligned_measurement_labels, ObjectLabelValue) - else aligned_measurement_labels - ) - if profile_enabled: - RuntimeProfileLogger.log( - logger, - "measurement_object_labels_label_alignment", - label_align_timer.elapsed(), - reference_domain=request.reference_domain.value, - labels_reused=aligned_measurement_labels is source_projected_labels, - ) - completion_timer = RuntimeProfileTimer.start() - completion_payload = source_projected_payload.with_measurement_labels( - measurement_labels - ) - if profile_enabled: - RuntimeProfileLogger.log( - logger, - "measurement_object_labels_completion_payload", - completion_timer.elapsed(), - reference_domain=request.reference_domain.value, - payload_reused=completion_payload is source_projected_payload, - ) - return cls( - request=request, - source_payload=source_payload, - source_projected_payload=source_projected_payload, - source_projected_labels=source_projected_labels, - aligned_source=aligned_source, - measurement_labels=measurement_labels, - completion_payload=completion_payload, - ) - @staticmethod - def source_context_payload( - request: MeasurementImageLabelAlignmentRequest, - source_payload: ObjectLabelValue, - source_projected_labels: ObjectLabelMeasurementSource, - ) -> ObjectLabelValue: - """Attach measurement-image source context to projected object labels.""" - source_variants = source_payload.variant_data - if isinstance(source_projected_labels, ObjectLabelValue): - variants = source_projected_labels.variant_data - elif source_projected_labels is request.labels: - variants = source_variants - else: - variants = ObjectLabelVariantData.compatible_replacement( - source_payload, - source_projected_labels, - ) - metadata = image_payload_metadata(request.image) - payload_domain = source_payload.object_label_source_spatial_domain() - source_spatial_domain = ( - metadata.object_label_source_spatial_domain() - .with_missing_from(payload_domain) - .with_fill_value(payload_domain.fill_value) - .with_value_name(payload_domain.value_name) - ) - if ( - variants.labels is source_variants.labels - and variants.unedited_labels is source_variants.unedited_labels - and variants.small_removed_labels is source_variants.small_removed_labels - and source_spatial_domain == payload_domain - ): - return source_payload - if ( - source_projected_labels is request.labels - and source_spatial_domain == payload_domain - ): - return source_payload.with_variants( - variants, - source_spatial_domain=source_spatial_domain, - ) - if source_spatial_domain != payload_domain: - contextual_payload = source_payload.with_variants( - variants, - source_spatial_domain=source_spatial_domain, - ) - image_source_domain = measurement_image_source_spatial_adapter( - request.image - ) - if image_source_domain is None: - raise ValueError( - "Measurement source-spatial projection requires an image source domain." - ) - contextual_source_domain = SourceSpatialDomainAdapter.for_value( - contextual_payload - ) - if contextual_source_domain is None: - raise TypeError( - "Measurement source-spatial projection requires an object-label " - "source domain adapter." - ) - variants = ObjectLabelVariantData.compatible_replacement( - contextual_payload, - image_source_domain.extract_source_array( - object_label_dense_array(contextual_payload), - spatial_axes_yx=contextual_source_domain.spatial_axes_yx, - ), - ) - return source_payload.with_variants( - variants, - representation=ObjectLabelRepresentation.DENSE_LABELS, - source_spatial_domain=source_spatial_domain, - ) - - -def measurement_image_source_spatial_adapter( - image: RuntimeArrayData | AlignedImageStack, -) -> SourceSpatialDomainAdapter | None: - """Return the nominal source-domain adapter for a measurement image.""" - if isinstance(image, AlignedImageStack): - return image.first_slice_source_spatial_adapter() - return SourceSpatialDomainAdapter.for_value(image) - - -class MeasurementLabelSourceAlignmentStrategy: - """Align labels through nominal image and source-spatial contracts.""" - - @classmethod - def align( - cls, + def labels_for_image( image: RuntimeArrayData | AlignedImageStack, labels: ObjectLabelMeasurementSource | RuntimeSliceAlignedValueSet, *, @@ -540,34 +311,29 @@ def align( return labels return label_domain_adapter.value_in_payload_domain(image_domain_adapter) - @classmethod - def align_request_labels_to_image_source( - cls, - request: MeasurementImageLabelAlignmentRequest, + def labels_in_image_source( + self, ) -> ObjectLabelMeasurementSource | RuntimeSliceAlignedValueSet: - """Return request labels projected into the request image source domain.""" - request = request.with_source_projected_labels() - return cls.align( + """Project labels into this request's declared image source domain.""" + request = self.with_source_projected_labels() + return self.labels_for_image( request.image, request.labels, label_payload=request.label_payload, ) + def aligned(self) -> "MeasurementImageLabelAlignmentRequest": + """Return projected image and labels from one coupled alignment pass.""" + return self.with_source_projected_labels()._with_aligned_source() -class MeasurementImageLabelAlignmentStrategy: - """Select measurement-image alignment from existing nominal contracts.""" - - @classmethod - def align( - cls, - request: MeasurementImageLabelAlignmentRequest, - ) -> RuntimeArrayData | AlignedImageStack: - request = request.with_source_projected_labels() + def _with_aligned_source(self) -> "MeasurementImageLabelAlignmentRequest": + """Align an already projected request without repeating label projection.""" + request = self if request.reference_domain is MeasurementImageReferenceDomain.SOURCE_IMAGE: request = request.with_source_projected_image() image = request.image elif request.reference_domain is MeasurementImageReferenceDomain.OBJECT_LABELS: - image = cls.object_label_reference_image(request) + image = request.object_label_reference_image() else: raise ValueError( "Measurement image alignment has no declared reference-domain behavior " @@ -589,7 +355,7 @@ def align( source_description="Payload-scoped object-label measurement image", ) aligned_labels = ( - MeasurementLabelSourceAlignmentStrategy.align( + self.labels_for_image( image, request.labels, label_payload=label_payload, @@ -597,19 +363,20 @@ def align( if request.reference_domain is MeasurementImageReferenceDomain.SOURCE_IMAGE else request.labels ) - cls.validate_alignment( + self.validate_alignment( image, aligned_labels, label_payload=label_payload, ) - return image + return replace( + request, + source=request.source.with_alignment_image(image), + labels=aligned_labels, + ) - @classmethod - def object_label_reference_image( - cls, - request: MeasurementImageLabelAlignmentRequest, - ) -> RuntimeArrayData | AlignedImageStack: + def object_label_reference_image(self) -> RuntimeArrayData | AlignedImageStack: """Return an image selected by declared object-label-domain semantics.""" + request = self label_payload = request.label_payload if label_payload is None: raise ValueError( @@ -758,6 +525,237 @@ def validate_dense_pair( ) +@dataclass(frozen=True, slots=True) +class PreparedMeasurementObjectLabels: + """Single-pass object-label preparation for measurement-image execution.""" + + request: MeasurementImageLabelAlignmentRequest + source_payload: ObjectLabelValue + source_projected_payload: ObjectLabelValue + source_projected_labels: ObjectLabelMeasurementSource + aligned_source: MeasurementImageAlignmentSource + measurement_labels: ObjectLabelMeasurementSource + completion_payload: ObjectLabelValue + + @property + def aligned_image(self) -> RuntimeArrayData | AlignedImageStack: + """Return the image from the source that owns its projected semantics.""" + + return self.aligned_source.alignment_image + + @classmethod + def from_source( + cls, + source: MeasurementImageAlignmentSource, + label_payload: ObjectLabelValue, + *, + plane_projector: RuntimePlaneAxisProjector | None = None, + align_image_to_labels: bool = True, + ) -> "PreparedMeasurementObjectLabels": + """Prepare labels from one measurement-image source and payload.""" + return cls.from_request( + request=source.object_label_alignment_request( + label_payload, + plane_projector=plane_projector, + align_image_to_labels=align_image_to_labels, + ), + ) + + @classmethod + def from_request( + cls, + request: MeasurementImageLabelAlignmentRequest, + ) -> "PreparedMeasurementObjectLabels": + """Prepare image, dense labels, and completion payload in one pass.""" + if request.label_payload is None: + raise TypeError( + "Measurement object-label preparation requires label_payload." + ) + profile_enabled = RuntimeProfileLogger.enabled() + source_payload = request.label_payload + source_projection_timer = RuntimeProfileTimer.start() + source_projected_request = request.with_source_projected_labels() + source_projected_labels = source_projected_request.labels + source_projection_payload = source_projected_request.label_payload + if source_projection_payload is None: + raise TypeError( + "Measurement object-label source projection lost label_payload." + ) + if profile_enabled: + RuntimeProfileLogger.log( + logger, + "measurement_object_labels_source_projection", + source_projection_timer.elapsed(), + reference_domain=request.reference_domain.value, + source_type=type(request.image).__name__, + labels_type=type(request.labels).__name__, + source_aliases=request.source_aliases, + ) + source_context_timer = RuntimeProfileTimer.start() + source_projected_payload = cls.source_context_payload( + source_projected_request, + source_projection_payload, + source_projected_labels, + ) + if profile_enabled: + RuntimeProfileLogger.log( + logger, + "measurement_object_labels_source_context", + source_context_timer.elapsed(), + reference_domain=request.reference_domain.value, + payload_reused=source_projected_payload is source_payload, + ) + image_request = replace( + source_projected_request, + labels=source_projected_labels, + label_payload=source_projected_payload, + ) + image_align_timer = RuntimeProfileTimer.start() + if request.align_image_to_labels: + aligned_request = image_request._with_aligned_source() + aligned_image = aligned_request.image + aligned_source = aligned_request.source + else: + aligned_image = request.image + aligned_source = image_request.source.with_alignment_image(aligned_image) + if profile_enabled: + RuntimeProfileLogger.log( + logger, + "measurement_object_labels_image_alignment", + image_align_timer.elapsed(), + reference_domain=request.reference_domain.value, + align_image_to_labels=request.align_image_to_labels, + aligned_type=type(aligned_image).__name__, + ) + label_align_timer = RuntimeProfileTimer.start() + aligned_measurement_labels = ( + aligned_request.labels + if request.align_image_to_labels + and request.reference_domain is MeasurementImageReferenceDomain.SOURCE_IMAGE + else image_request.labels_for_image( + aligned_image, + source_projected_labels, + label_payload=source_projected_payload, + ) + ) + measurement_labels = ( + object_label_dense_array(aligned_measurement_labels) + if isinstance(aligned_measurement_labels, ObjectLabelValue) + else aligned_measurement_labels + ) + if profile_enabled: + RuntimeProfileLogger.log( + logger, + "measurement_object_labels_label_alignment", + label_align_timer.elapsed(), + reference_domain=request.reference_domain.value, + labels_reused=aligned_measurement_labels is source_projected_labels, + ) + completion_timer = RuntimeProfileTimer.start() + completion_payload = source_projected_payload.with_measurement_labels( + measurement_labels + ) + if profile_enabled: + RuntimeProfileLogger.log( + logger, + "measurement_object_labels_completion_payload", + completion_timer.elapsed(), + reference_domain=request.reference_domain.value, + payload_reused=completion_payload is source_projected_payload, + ) + return cls( + request=request, + source_payload=source_payload, + source_projected_payload=source_projected_payload, + source_projected_labels=source_projected_labels, + aligned_source=aligned_source, + measurement_labels=measurement_labels, + completion_payload=completion_payload, + ) + + @staticmethod + def source_context_payload( + request: MeasurementImageLabelAlignmentRequest, + source_payload: ObjectLabelValue, + source_projected_labels: ObjectLabelMeasurementSource, + ) -> ObjectLabelValue: + """Attach measurement-image source context to projected object labels.""" + source_variants = source_payload.variant_data + if isinstance(source_projected_labels, ObjectLabelValue): + variants = source_projected_labels.variant_data + elif source_projected_labels is request.labels: + variants = source_variants + else: + variants = ObjectLabelVariantData.compatible_replacement( + source_payload, + source_projected_labels, + ) + metadata = image_payload_metadata(request.image) + payload_domain = source_payload.object_label_source_spatial_domain() + source_spatial_domain = ( + metadata.object_label_source_spatial_domain() + .with_missing_from(payload_domain) + .with_fill_value(payload_domain.fill_value) + .with_value_name(payload_domain.value_name) + ) + if ( + variants.labels is source_variants.labels + and variants.unedited_labels is source_variants.unedited_labels + and variants.small_removed_labels is source_variants.small_removed_labels + and source_spatial_domain == payload_domain + ): + return source_payload + if ( + source_projected_labels is request.labels + and source_spatial_domain == payload_domain + ): + return source_payload.with_variants( + variants, + source_spatial_domain=source_spatial_domain, + ) + if source_spatial_domain != payload_domain: + contextual_payload = source_payload.with_variants( + variants, + source_spatial_domain=source_spatial_domain, + ) + image_source_domain = measurement_image_source_spatial_adapter( + request.image + ) + if image_source_domain is None: + raise ValueError( + "Measurement source-spatial projection requires an image source domain." + ) + contextual_source_domain = SourceSpatialDomainAdapter.for_value( + contextual_payload + ) + if contextual_source_domain is None: + raise TypeError( + "Measurement source-spatial projection requires an object-label " + "source domain adapter." + ) + variants = ObjectLabelVariantData.compatible_replacement( + contextual_payload, + image_source_domain.extract_source_array( + object_label_dense_array(contextual_payload), + spatial_axes_yx=contextual_source_domain.spatial_axes_yx, + ), + ) + return source_payload.with_variants( + variants, + representation=ObjectLabelRepresentation.DENSE_LABELS, + source_spatial_domain=source_spatial_domain, + ) + + +def measurement_image_source_spatial_adapter( + image: RuntimeArrayData | AlignedImageStack, +) -> SourceSpatialDomainAdapter | None: + """Return the nominal source-domain adapter for a measurement image.""" + if isinstance(image, AlignedImageStack): + return image.first_slice_source_spatial_adapter() + return SourceSpatialDomainAdapter.for_value(image) + + class MeasurementImageReferenceDomain(str, Enum): """Semantic image domain used as the reference for object measurement.""" diff --git a/openhcs/interop/cellprofiler/runtime/invocation.py b/openhcs/interop/cellprofiler/runtime/invocation.py index 413ae81b4a..31876eec10 100644 --- a/openhcs/interop/cellprofiler/runtime/invocation.py +++ b/openhcs/interop/cellprofiler/runtime/invocation.py @@ -15,7 +15,6 @@ from openhcs.core.equivalence.keys import RuntimeMeasurementSourcePair from openhcs.core.measurement_image_alignment import ( MeasurementImageAlignmentSource, - MeasurementImageLabelAlignmentStrategy, MeasurementImageReferenceDomain, PreparedMeasurementObjectLabels, ) @@ -465,12 +464,14 @@ def align_image_to_labels( plane_projector: RuntimePlaneAxisProjector | None = None, ) -> RuntimeCallableArgument: """Project this measurement image payload into the supplied label domain.""" - return MeasurementImageLabelAlignmentStrategy.align( + return ( self.alignment_request( labels=labels, label_payload=label_payload, plane_projector=plane_projector, ) + .aligned() + .image ) diff --git a/tests/unit/test_cellprofiler_module_execution.py b/tests/unit/test_cellprofiler_module_execution.py index 9803fb1ead..c3c2e2e5c6 100644 --- a/tests/unit/test_cellprofiler_module_execution.py +++ b/tests/unit/test_cellprofiler_module_execution.py @@ -70,8 +70,7 @@ MainFlowInputProjection, ) from openhcs.core.measurement_image_alignment import ( - MeasurementImageLabelAlignmentStrategy, - MeasurementLabelSourceAlignmentStrategy, + MeasurementImageLabelAlignmentRequest, PreparedMeasurementObjectLabels, ) from openhcs.core.measurement_row_materialization import ( @@ -976,12 +975,15 @@ def _measurement_image_for_labels( payload=image, reference_domain=reference_domain, ) - return MeasurementImageLabelAlignmentStrategy.align( + return ( source.alignment_request( labels=labels, label_payload=label_payload, plane_projector=plane_projector, - ).with_source_projected_image() + ) + .with_source_projected_image() + .aligned() + .image ) @@ -15314,27 +15316,23 @@ def source_binding_axis_size( ), ).payload_with(image, None), ) - aligned_labels = ( - MeasurementLabelSourceAlignmentStrategy.align_request_labels_to_image_source( - measurement_image.alignment_request( - labels=label_planes, - label_payload=labels, - plane_projector=projector, - ) - ) - ) + aligned_labels = measurement_image.alignment_request( + labels=label_planes, + label_payload=labels, + plane_projector=projector, + ).labels_in_image_source() np.testing.assert_array_equal( object_label_dense_array(aligned_labels), label_planes[1], ) np.testing.assert_array_equal( - MeasurementImageLabelAlignmentStrategy.align( - measurement_image.alignment_request( - labels=aligned_labels, - plane_projector=projector, - ) - ), + measurement_image.alignment_request( + labels=aligned_labels, + plane_projector=projector, + ) + .aligned() + .image, image[1], ) @@ -15390,11 +15388,13 @@ def source_binding_axis_size( payload=payload, ) - aligned = MeasurementImageLabelAlignmentStrategy.align( + aligned = ( measurement_image.alignment_request( labels=np.ones((4, 5), dtype=np.int32), plane_projector=Projector(), ) + .aligned() + .image ) assert isinstance(aligned, MaskedImagePayload) @@ -15452,15 +15452,11 @@ def source_binding_axis_size( source_aliases=("rawDNA", "rawGFP"), payload=image, ) - aligned_labels = ( - MeasurementLabelSourceAlignmentStrategy.align_request_labels_to_image_source( - measurement_image.alignment_request( - labels=labels, - label_payload=labels, - plane_projector=Projector(), - ) - ) - ) + aligned_labels = measurement_image.alignment_request( + labels=labels, + label_payload=labels, + plane_projector=Projector(), + ).labels_in_image_source() assert isinstance(aligned_labels, ObjectLabelSet) np.testing.assert_array_equal( @@ -15605,7 +15601,9 @@ def test_measurement_labels_preserve_stack_for_object_domain_alignment() -> None image = np.ones((1, 4, 5), dtype=np.float32) labels = np.arange(2 * 4 * 5, dtype=np.int32).reshape(2, 4, 5) - measurement_labels = MeasurementLabelSourceAlignmentStrategy.align(image, labels) + measurement_labels = MeasurementImageLabelAlignmentRequest.labels_for_image( + image, labels + ) assert measurement_labels.shape == labels.shape np.testing.assert_array_equal(measurement_labels, labels) @@ -15641,7 +15639,7 @@ def test_measurement_label_alignment_preserves_runtime_slice_payload_for_aligned ), ) - measurement_labels = MeasurementLabelSourceAlignmentStrategy.align( + measurement_labels = MeasurementImageLabelAlignmentRequest.labels_for_image( AlignedImageStack((first_image, second_image)), labels, label_payload=label_payload, @@ -15666,7 +15664,7 @@ def test_measurement_label_alignment_preserves_runtime_slice_payload_for_dense_s ), ) - measurement_labels = MeasurementLabelSourceAlignmentStrategy.align( + measurement_labels = MeasurementImageLabelAlignmentRequest.labels_for_image( image, labels, label_payload=label_payload, @@ -15760,7 +15758,9 @@ def test_measurement_labels_do_not_infer_broadcast_from_equal_planes() -> None: label_plane = np.arange(4 * 5, dtype=np.int32).reshape(4, 5) labels = np.stack((label_plane, label_plane)) - measurement_labels = MeasurementLabelSourceAlignmentStrategy.align(image, labels) + measurement_labels = MeasurementImageLabelAlignmentRequest.labels_for_image( + image, labels + ) assert measurement_labels.shape == labels.shape np.testing.assert_array_equal(measurement_labels, labels) @@ -16017,7 +16017,9 @@ def source_binding_axis_plane_index( ) del measurement_image - measurement_labels = MeasurementLabelSourceAlignmentStrategy.align(image, labels) + measurement_labels = MeasurementImageLabelAlignmentRequest.labels_for_image( + image, labels + ) assert measurement_labels.shape == labels.shape np.testing.assert_array_equal(measurement_labels, labels) diff --git a/tests/unit/test_cellprofiler_static_deletion_gates.py b/tests/unit/test_cellprofiler_static_deletion_gates.py index d97c78c59c..8471b4ef75 100644 --- a/tests/unit/test_cellprofiler_static_deletion_gates.py +++ b/tests/unit/test_cellprofiler_static_deletion_gates.py @@ -1268,8 +1268,8 @@ def test_nominal_payloads_are_not_erased_before_semantic_projection() -> None: ) semantic_calls = frozenset( { - "MeasurementLabelSourceAlignmentStrategy.align", - "MeasurementLabelSourceAlignmentStrategy.align_request_labels_to_image_source", + "MeasurementImageLabelAlignmentRequest.labels_for_image", + "MeasurementImageLabelAlignmentRequest.labels_in_image_source", "RuntimeSliceProjection.value_for_slice", "SourceSpatialDomainAdapter.aligned_values", } diff --git a/tests/unit/test_measurement_image_alignment.py b/tests/unit/test_measurement_image_alignment.py index c45f956e8f..1260afabe1 100644 --- a/tests/unit/test_measurement_image_alignment.py +++ b/tests/unit/test_measurement_image_alignment.py @@ -9,9 +9,8 @@ ImageOutputBundle, ) from openhcs.core.measurement_image_alignment import ( + MeasurementImageLabelAlignmentRequest, MeasurementImageAlignmentSource, - MeasurementImageLabelAlignmentStrategy, - MeasurementLabelSourceAlignmentStrategy, PreparedMeasurementObjectLabels, ) from openhcs.core.measurement_image_alignment import ( @@ -105,9 +104,7 @@ def test_source_reference_requires_exact_shape_after_domain_selection() -> None: source = _MeasurementImageSource(np.ones((4, 5), dtype=np.float32)) with pytest.raises(ValueError, match="incompatible declared domains"): - MeasurementImageLabelAlignmentStrategy.align( - source.alignment_request(labels=np.ones((3, 5), dtype=np.int32)) - ) + source.alignment_request(labels=np.ones((3, 5), dtype=np.int32)).aligned().image @pytest.mark.parametrize( @@ -123,9 +120,7 @@ def test_source_reference_does_not_infer_singleton_or_color_projection( source = _MeasurementImageSource(image) with pytest.raises(ValueError, match="incompatible declared domains"): - MeasurementImageLabelAlignmentStrategy.align( - source.alignment_request(labels=np.ones((4, 5), dtype=np.int32)) - ) + source.alignment_request(labels=np.ones((4, 5), dtype=np.int32)).aligned().image def test_payload_scoped_labels_consume_declared_singleton_runtime_image_plane() -> None: @@ -142,9 +137,7 @@ def test_payload_scoped_labels_consume_declared_singleton_runtime_image_plane() ) source = _MeasurementImageSource(image) - aligned = MeasurementImageLabelAlignmentStrategy.align( - source.object_label_alignment_request(labels) - ) + aligned = source.object_label_alignment_request(labels).aligned().image assert isinstance(aligned, np.ndarray) assert aligned.shape == (4, 5) @@ -164,9 +157,7 @@ def test_payload_scoped_labels_preserve_declared_runtime_volume() -> None: ) source = _MeasurementImageSource(image) - aligned = MeasurementImageLabelAlignmentStrategy.align( - source.object_label_alignment_request(labels) - ) + aligned = source.object_label_alignment_request(labels).aligned().image assert aligned is image @@ -186,9 +177,7 @@ def test_payload_scoped_labels_reject_mismatched_declared_runtime_volume() -> No source = _MeasurementImageSource(image) with pytest.raises(ValueError, match="incompatible declared domains"): - MeasurementImageLabelAlignmentStrategy.align( - source.object_label_alignment_request(labels) - ) + source.object_label_alignment_request(labels).aligned().image def test_payload_scoped_labels_reject_non_runtime_image_plane_axis() -> None: @@ -206,9 +195,7 @@ def test_payload_scoped_labels_reject_non_runtime_image_plane_axis() -> None: source = _MeasurementImageSource(image) with pytest.raises(ValueError, match="incompatible declared domains"): - MeasurementImageLabelAlignmentStrategy.align( - source.object_label_alignment_request(labels) - ) + source.object_label_alignment_request(labels).aligned().image def test_object_reference_requires_nominal_label_payload() -> None: @@ -218,9 +205,7 @@ def test_object_reference_requires_nominal_label_payload() -> None: ) with pytest.raises(ValueError, match="requires an ObjectLabelValue"): - MeasurementImageLabelAlignmentStrategy.align( - source.alignment_request(labels=np.ones((4, 5), dtype=np.int32)) - ) + source.alignment_request(labels=np.ones((4, 5), dtype=np.int32)).aligned().image def test_object_reference_uses_declared_source_spatial_adapter() -> None: @@ -237,9 +222,7 @@ def test_object_reference_uses_declared_source_spatial_adapter() -> None: reference_domain=MeasurementImageReferenceDomain.OBJECT_LABELS, ) - aligned = MeasurementImageLabelAlignmentStrategy.align( - source.object_label_alignment_request(labels) - ) + aligned = source.object_label_alignment_request(labels).aligned().image np.testing.assert_array_equal(aligned, image[1:3, 1:3]) @@ -273,9 +256,7 @@ def test_object_reference_preserves_nominal_image_payload_context() -> None: reference_domain=MeasurementImageReferenceDomain.OBJECT_LABELS, ) - aligned = MeasurementImageLabelAlignmentStrategy.align( - source.object_label_alignment_request(labels) - ) + aligned = source.object_label_alignment_request(labels).aligned().image assert isinstance(aligned, ImageMetadataPayload) assert aligned.metadata.plane_axis is RuntimePlaneAxis.RUNTIME_SLICE @@ -472,9 +453,7 @@ def test_aligned_image_stack_requires_declared_runtime_slice_labels() -> None: source = _MeasurementImageSource(image) with pytest.raises(ValueError, match="declared plane-scoped runtime-slice axis"): - MeasurementImageLabelAlignmentStrategy.align( - source.alignment_request(labels=np.ones((4, 5), dtype=np.int32)) - ) + source.alignment_request(labels=np.ones((4, 5), dtype=np.int32)).aligned().image def test_singleton_aligned_image_stack_accepts_payload_scoped_labels() -> None: @@ -483,7 +462,7 @@ def test_singleton_aligned_image_stack_accepts_payload_scoped_labels() -> None: variant_data=ObjectLabelVariantData(labels=np.ones((4, 5), dtype=np.int32)) ) - aligned = MeasurementLabelSourceAlignmentStrategy.align( + aligned = MeasurementImageLabelAlignmentRequest.labels_for_image( image, labels, label_payload=labels, @@ -513,9 +492,7 @@ def test_aligned_image_stack_accepts_nominal_runtime_slice_payload() -> None: ) source = _MeasurementImageSource(image) - aligned = MeasurementImageLabelAlignmentStrategy.align( - source.object_label_alignment_request(labels) - ) + aligned = source.object_label_alignment_request(labels).aligned().image assert aligned is image @@ -553,9 +530,7 @@ def test_aligned_image_stack_preserves_sparse_runtime_slice_payload() -> None: ) source = _MeasurementImageSource(image) - aligned = MeasurementImageLabelAlignmentStrategy.align( - source.object_label_alignment_request(labels) - ) + aligned = source.object_label_alignment_request(labels).aligned().image assert aligned is image @@ -604,9 +579,7 @@ def test_named_image_output_bundle_broadcasts_one_label_domain() -> None: labels = np.ones((4, 5), dtype=np.int32) source = _MeasurementImageSource(image) - aligned = MeasurementImageLabelAlignmentStrategy.align( - source.alignment_request(labels=labels) - ) + aligned = source.alignment_request(labels=labels).aligned().image assert aligned is image @@ -625,11 +598,13 @@ def test_runtime_slice_aligned_values_require_matching_aligned_image() -> None: ) ) - assert MeasurementLabelSourceAlignmentStrategy.align(image, labels) is labels + assert ( + MeasurementImageLabelAlignmentRequest.labels_for_image(image, labels) is labels + ) mismatched = RuntimeSliceAlignedValues((labels.slices[0],)) with pytest.raises(ValueError, match="must match the aligned image count"): - MeasurementLabelSourceAlignmentStrategy.align(image, mismatched) + MeasurementImageLabelAlignmentRequest.labels_for_image(image, mismatched) def test_object_reference_replaces_unrelated_aligned_image_stack() -> None: @@ -661,6 +636,6 @@ def test_equal_label_planes_are_not_collapsed() -> None: plane = np.arange(20, dtype=np.int32).reshape(4, 5) labels = np.stack((plane, plane)) - aligned = MeasurementLabelSourceAlignmentStrategy.align(image, labels) + aligned = MeasurementImageLabelAlignmentRequest.labels_for_image(image, labels) assert aligned is labels From c751b6b26e2c8f04fd6573e6dafc1b54927fdab7 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 22:27:50 -0400 Subject: [PATCH 199/235] docs: publish fresh three-pipeline native comparison and figures --- .../runtime-paired-v11-20261004/README.rst | 87 + .../matched_runtime.png | Bin 0 -> 181715 bytes .../matched_runtime.svg | 1815 ++++++++++++++ .../matched_runtime_samples.png | Bin 0 -> 189848 bytes .../matched_runtime_samples.svg | 2090 +++++++++++++++++ .../matched_speedup.png | Bin 0 -> 158216 bytes .../matched_speedup.svg | 1756 ++++++++++++++ .../runtime-paired-v11-20261004/metrics.csv | 28 + 8 files changed, 5776 insertions(+) create mode 100644 docs/refactor/receipts/runtime-paired-v11-20261004/README.rst create mode 100644 docs/refactor/receipts/runtime-paired-v11-20261004/matched_runtime.png create mode 100644 docs/refactor/receipts/runtime-paired-v11-20261004/matched_runtime.svg create mode 100644 docs/refactor/receipts/runtime-paired-v11-20261004/matched_runtime_samples.png create mode 100644 docs/refactor/receipts/runtime-paired-v11-20261004/matched_runtime_samples.svg create mode 100644 docs/refactor/receipts/runtime-paired-v11-20261004/matched_speedup.png create mode 100644 docs/refactor/receipts/runtime-paired-v11-20261004/matched_speedup.svg create mode 100644 docs/refactor/receipts/runtime-paired-v11-20261004/metrics.csv diff --git a/docs/refactor/receipts/runtime-paired-v11-20261004/README.rst b/docs/refactor/receipts/runtime-paired-v11-20261004/README.rst new file mode 100644 index 0000000000..98c3bb5068 --- /dev/null +++ b/docs/refactor/receipts/runtime-paired-v11-20261004/README.rst @@ -0,0 +1,87 @@ +Fresh ordinary and native runtime comparison +============================================ + +Measured source: ``771950f652e5237b71bf0b21b0b8050bda48e011``, integrated +through main ``32ededf24`` with eight current editable dependency sources and +matching repository pins. Subsequent alignment-owner consolidation and main +#577 integration are not included in these observations. + +All twelve strict comparisons pass: two ordinary observations against two +fresh native observations for each of three original pipelines. Complete +measurements, relationships and image inventories use the existing CellProfiler +tolerances. The 3D image comparison additionally requires exact pixels for both +original 60 x 256 x 256 uint16 volumes. Original source reference counts remain +180 for 3D, two for Speckles and ten for Beginner. + +Means of two observations +------------------------- + +.. list-table:: Single well, single worker; seconds and ratios of means + :header-rows: 1 + + * - Pipeline + - Compile + - Execution + - Total + - Native invocation + - Execution speedup + - Total speedup + * - 3D monolayer + - 1.032 + - 5.674 + - 7.423 + - 14.505 + - 2.556x + - 1.954x + * - Speckles + - 0.478 + - 1.042 + - 1.852 + - 1.939 + - 1.861x + - 1.047x + * - Beginner + - 0.607 + - 4.053 + - 5.123 + - 15.028 + - 3.708x + - 2.934x + +Clock and interpretation +------------------------ + +Server startup, function-library/kernel readiness and shutdown are outside +OpenHCS pipeline clocks. Execution includes the generic runtime, rather than +only callable time. Total includes compilation and ordinary OUTCOMES/RSS +completion. Native uses the same full invocation clock as the previous v7 +comparison, excluding imports, JVM startup, input loading and the warmup +observation. Its separate first-module-through-post-run clock is retained in +the JSON but is not used for the ratios. + +Both observations are retained and visible in ``matched_runtime_samples``. +These are descriptive observations, not confidence intervals or attribution +to any individual refactor. The 3D execution samples range from 5.016 to +6.333 seconds. Speckles execution ranges from 0.907 to 1.177 seconds. +The total-time target remains unmet for 3D and Speckles; Speckles is only +1.047x faster than native on total. Full-catalog and multi-well scaling +qualification remain outstanding. + +The saved-output/consolidation cleanup is included in the measured source, +but this benchmark disables consolidation. No benchmark gain is attributed +to that fix. Later diagnostic profiling is separate and does not replace +these uninstrumented ordinary observations. + +Retained artifacts +------------------ + +The adjacent PNG/SVG figures, ``metrics.csv``, ``timing_summary.json`` and +``qualification.json`` contain all measured samples, clock definitions, +dependency identities and twelve scientific comparison results. The complete +unchanged campaign is retained at:: + + /home/ts/.local/state/openhcs-maintenance/20261004/canonical-runtime-chains-paired-v11-qualified + +Scientific result SHA-256:: + + 608a83ff5903f51652ff3d0d61793cad4efaf27b9085dda958174d1139e60a52 diff --git a/docs/refactor/receipts/runtime-paired-v11-20261004/matched_runtime.png b/docs/refactor/receipts/runtime-paired-v11-20261004/matched_runtime.png new file mode 100644 index 0000000000000000000000000000000000000000..a51d516b94aa2f3dbe6a6281bf8bac9b085e8605 GIT binary patch literal 181715 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 z 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Ģ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 Lf0?X%{JZ}LCS;8| literal 0 HcmV?d00001 diff --git a/docs/refactor/receipts/runtime-paired-v11-20261004/matched_runtime.svg b/docs/refactor/receipts/runtime-paired-v11-20261004/matched_runtime.svg new file mode 100644 index 0000000000..7030178d5a --- /dev/null +++ b/docs/refactor/receipts/runtime-paired-v11-20261004/matched_runtime.svg @@ -0,0 +1,1815 @@ + + + + + + + + 2026-10-03T22:24:51.485939 + image/svg+xml + + + Matplotlib v3.11.2, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/refactor/receipts/runtime-paired-v11-20261004/matched_runtime_samples.png b/docs/refactor/receipts/runtime-paired-v11-20261004/matched_runtime_samples.png new file mode 100644 index 0000000000000000000000000000000000000000..89a09ce75a6d203861390e17b28582f43b99b8d6 GIT binary patch literal 189848 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 literal 0 HcmV?d00001 diff --git a/docs/refactor/receipts/runtime-paired-v11-20261004/matched_runtime_samples.svg b/docs/refactor/receipts/runtime-paired-v11-20261004/matched_runtime_samples.svg new file mode 100644 index 0000000000..50e8bea0c7 --- /dev/null +++ b/docs/refactor/receipts/runtime-paired-v11-20261004/matched_runtime_samples.svg @@ -0,0 +1,2090 @@ + + + + + + + + 2026-10-03T22:24:52.297611 + image/svg+xml + + + Matplotlib v3.11.2, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/refactor/receipts/runtime-paired-v11-20261004/matched_speedup.png b/docs/refactor/receipts/runtime-paired-v11-20261004/matched_speedup.png new file mode 100644 index 0000000000000000000000000000000000000000..9f02adae2bfb8df3a281a3a54b1d77b850782b8d GIT binary patch literal 158216 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�QYL9&EQKMJGN(4 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�!q3Q@6LDG$a=wylX!8;`;#+edxIffNU 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 + + + + + + + 2026-10-03T22:24:51.855028 + image/svg+xml + + + Matplotlib v3.11.2, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/refactor/receipts/runtime-paired-v11-20261004/metrics.csv b/docs/refactor/receipts/runtime-paired-v11-20261004/metrics.csv new file mode 100644 index 0000000000..882f4da3a4 --- /dev/null +++ b/docs/refactor/receipts/runtime-paired-v11-20261004/metrics.csv @@ -0,0 +1,28 @@ +pipeline_name,method,assay_category,module_category,accuracy_fraction,raw_seconds,speedup,peak_memory_mb +cp_tutorial_3d_monolayer,CellProfiler,,,1.0,14.504645670065656,1.0, +cp_tutorial_3d_monolayer,OH execution,,,1.0,5.674471020698547,2.5561229614456793, +cp_tutorial_3d_monolayer,OH total,,,1.0,7.423485984560102,1.9538860449435018, +ExampleSpeckles,CellProfiler,,,1.0,1.9391879816539586,1.0, +ExampleSpeckles,OH execution,,,1.0,1.04183030128479,1.8613280677885282, +ExampleSpeckles,OH total,,,1.0,1.851556861307472,1.0473283441506658, +cp_tutorial_beginner_segmentation_final,CellProfiler,,,1.0,15.028485956136137,1.0, +cp_tutorial_beginner_segmentation_final,OH execution,,,1.0,4.052742838859558,3.7082258987755448, +cp_tutorial_beginner_segmentation_final,OH total,,,1.0,5.122567740967497,2.933779837784578, +cp_tutorial_3d_monolayer,CellProfiler run 1,,,1.0,14.757951722946018,, +cp_tutorial_3d_monolayer,CellProfiler run 2,,,1.0,14.251339617185295,, +cp_tutorial_3d_monolayer,OH execution run 1,,,1.0,5.0158140659332275,, +cp_tutorial_3d_monolayer,OH execution run 2,,,1.0,6.333127975463867,, +cp_tutorial_3d_monolayer,OH total run 1,,,1.0,6.781453618314117,, +cp_tutorial_3d_monolayer,OH total run 2,,,1.0,8.065518350806087,, +ExampleSpeckles,CellProfiler run 1,,,1.0,2.0039482093416154,, +ExampleSpeckles,CellProfiler run 2,,,1.0,1.8744277539663017,, +ExampleSpeckles,OH execution run 1,,,1.0,0.9069628715515137,, +ExampleSpeckles,OH execution run 2,,,1.0,1.1766977310180664,, +ExampleSpeckles,OH total run 1,,,1.0,1.6977782007306814,, +ExampleSpeckles,OH total run 2,,,1.0,2.0053355218842626,, +cp_tutorial_beginner_segmentation_final,CellProfiler run 1,,,1.0,15.272316726390272,, +cp_tutorial_beginner_segmentation_final,CellProfiler run 2,,,1.0,14.784655185882002,, +cp_tutorial_beginner_segmentation_final,OH execution run 1,,,1.0,3.902729034423828,, +cp_tutorial_beginner_segmentation_final,OH execution run 2,,,1.0,4.202756643295288,, +cp_tutorial_beginner_segmentation_final,OH total run 1,,,1.0,4.9476758115924895,, +cp_tutorial_beginner_segmentation_final,OH total run 2,,,1.0,5.297459670342505,, From e6ef8f1d1c1190f8c6dd650f7ade67e2886595b7 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 22:28:04 -0400 Subject: [PATCH 200/235] docs: retain paired runtime samples and scientific qualification --- .../qualification.json | 326 +++++++++++ .../timing_summary.json | 507 ++++++++++++++++++ 2 files changed, 833 insertions(+) create mode 100644 docs/refactor/receipts/runtime-paired-v11-20261004/qualification.json create mode 100644 docs/refactor/receipts/runtime-paired-v11-20261004/timing_summary.json diff --git a/docs/refactor/receipts/runtime-paired-v11-20261004/qualification.json b/docs/refactor/receipts/runtime-paired-v11-20261004/qualification.json new file mode 100644 index 0000000000..f35a854e8a --- /dev/null +++ b/docs/refactor/receipts/runtime-paired-v11-20261004/qualification.json @@ -0,0 +1,326 @@ +{ + "status": "PASS", + "source_head": "771950f652e5237b71bf0b21b0b8050bda48e011", + "origin_main": "32ededf2421ac79df147628c1732785c139d0970", + "science_path": "/home/ts/.local/state/openhcs-maintenance/20261004/canonical-runtime-chains-paired-v11-qualified/strict-science.json", + "science_sha256": "608a83ff5903f51652ff3d0d61793cad4efaf27b9085dda958174d1139e60a52", + "source_freeze_path": "/home/ts/.local/state/openhcs-maintenance/20261004/canonical-runtime-chains-paired-v11-qualified/source-freeze.json", + "source_freeze_sha256": "feefec8c0fadd3da87b29c1c61495e939802f5729b274192f99a1e673b044be9", + "source_environment_sha256": "b579dda9991d0de75047d9dd84591f556085ef401bafa1f1f8bc9c86ca588ecc", + "dependency_heads": { + "external/ObjectState": "9fb5b7eeea96f3b7bb0579de30718bbcc0018823", + "external/python-introspect": "ed1e4eb15f7a0bf8315274d3e47858fa2b4372bb", + "external/arraybridge": "1e53d03d9f468322a1c085c8be29485fb139caf2", + "external/metaclass-registry": "393a7e03003cdc56df9013f932ed4f26e632d77a", + "external/zmqruntime": "f7b80c871eb4773757368bb21f108920bf461881", + "external/pycodify": "108a8edbf25de258168756bac22c7572f018fa90", + "external/PolyStore": "89deeef3662eabb11bc520fad9acd976698636bd", + "external/pyqt-reactive": "b64ed2eca96fcdad29ba90beb545caa669a4d3d8" + }, + "actual_import_dependency_heads": { + "external/ObjectState": "9fb5b7eeea96f3b7bb0579de30718bbcc0018823", + "external/PolyStore": "89deeef3662eabb11bc520fad9acd976698636bd", + "external/arraybridge": "1e53d03d9f468322a1c085c8be29485fb139caf2", + "external/metaclass-registry": "393a7e03003cdc56df9013f932ed4f26e632d77a", + "external/pycodify": "108a8edbf25de258168756bac22c7572f018fa90", + "external/pyqt-reactive": "b64ed2eca96fcdad29ba90beb545caa669a4d3d8", + "external/python-introspect": "ed1e4eb15f7a0bf8315274d3e47858fa2b4372bb", + "external/zmqruntime": "f7b80c871eb4773757368bb21f108920bf461881" + }, + "ordered_source_ref_counts": { + "cp_tutorial_3d_monolayer": 180, + "ExampleSpeckles": 2, + "cp_tutorial_beginner_segmentation_final": 10 + }, + "comparisons": [ + { + "case": "cp_tutorial_3d_monolayer", + "ordinary_repeat": 0, + "native_repeat": 0, + "measurement_differences": 0, + "image_differences": 0, + "tables": [ + 7, + 6 + ], + "physical_images": [ + 2, + 2 + ], + "logical_images": [ + 2, + 2 + ], + "known_relationship_keys": [ + 3, + 3 + ], + "ordinary_receipt_sha256": "465cf0eac54e4ff82845ec3f98f3402d5b7ab802badfb2bc2434753a59b999ad" + }, + { + "case": "cp_tutorial_3d_monolayer", + "ordinary_repeat": 0, + "native_repeat": 1, + "measurement_differences": 0, + "image_differences": 0, + "tables": [ + 7, + 6 + ], + "physical_images": [ + 2, + 2 + ], + "logical_images": [ + 2, + 2 + ], + "known_relationship_keys": [ + 3, + 3 + ], + "ordinary_receipt_sha256": "465cf0eac54e4ff82845ec3f98f3402d5b7ab802badfb2bc2434753a59b999ad" + }, + { + "case": "ExampleSpeckles", + "ordinary_repeat": 0, + "native_repeat": 0, + "measurement_differences": 0, + "image_differences": 0, + "tables": [ + 3, + 3 + ], + "physical_images": [ + 0, + 0 + ], + "logical_images": [ + 0, + 0 + ], + "known_relationship_keys": [ + 1, + 1 + ], + "ordinary_receipt_sha256": "3831b86c470132b166b1034305d771dba222c1dd8fb42f54093e3b5eaf6d0cf5" + }, + { + "case": "ExampleSpeckles", + "ordinary_repeat": 0, + "native_repeat": 1, + "measurement_differences": 0, + "image_differences": 0, + "tables": [ + 3, + 3 + ], + "physical_images": [ + 0, + 0 + ], + "logical_images": [ + 0, + 0 + ], + "known_relationship_keys": [ + 1, + 1 + ], + "ordinary_receipt_sha256": "3831b86c470132b166b1034305d771dba222c1dd8fb42f54093e3b5eaf6d0cf5" + }, + { + "case": "cp_tutorial_beginner_segmentation_final", + "ordinary_repeat": 0, + "native_repeat": 0, + "measurement_differences": 0, + "image_differences": 0, + "tables": [ + 7, + 7 + ], + "physical_images": [ + 2, + 2 + ], + "logical_images": [ + 2, + 2 + ], + "known_relationship_keys": [ + 5, + 5 + ], + "ordinary_receipt_sha256": "559ecdbb4e6a856c0a911d67e60171458a77e01f8d63f3b49326cc04fd6b2d38" + }, + { + "case": "cp_tutorial_beginner_segmentation_final", + "ordinary_repeat": 0, + "native_repeat": 1, + "measurement_differences": 0, + "image_differences": 0, + "tables": [ + 7, + 7 + ], + "physical_images": [ + 2, + 2 + ], + "logical_images": [ + 2, + 2 + ], + "known_relationship_keys": [ + 5, + 5 + ], + "ordinary_receipt_sha256": "559ecdbb4e6a856c0a911d67e60171458a77e01f8d63f3b49326cc04fd6b2d38" + }, + { + "case": "cp_tutorial_3d_monolayer", + "ordinary_repeat": 1, + "native_repeat": 0, + "measurement_differences": 0, + "image_differences": 0, + "tables": [ + 7, + 6 + ], + "physical_images": [ + 2, + 2 + ], + "logical_images": [ + 2, + 2 + ], + "known_relationship_keys": [ + 3, + 3 + ], + "ordinary_receipt_sha256": "9cecd2752cde33d1860a928dc04ccbcf06275e5319a7c586646d3b1e13222742" + }, + { + "case": "cp_tutorial_3d_monolayer", + "ordinary_repeat": 1, + "native_repeat": 1, + "measurement_differences": 0, + "image_differences": 0, + "tables": [ + 7, + 6 + ], + "physical_images": [ + 2, + 2 + ], + "logical_images": [ + 2, + 2 + ], + "known_relationship_keys": [ + 3, + 3 + ], + "ordinary_receipt_sha256": "9cecd2752cde33d1860a928dc04ccbcf06275e5319a7c586646d3b1e13222742" + }, + { + "case": "ExampleSpeckles", + "ordinary_repeat": 1, + "native_repeat": 0, + "measurement_differences": 0, + "image_differences": 0, + "tables": [ + 3, + 3 + ], + "physical_images": [ + 0, + 0 + ], + "logical_images": [ + 0, + 0 + ], + "known_relationship_keys": [ + 1, + 1 + ], + "ordinary_receipt_sha256": "5d6d7f02523bba49204b9f6f4626b4b33425690c1ba796423ab47aade5d119c5" + }, + { + "case": "ExampleSpeckles", + "ordinary_repeat": 1, + "native_repeat": 1, + "measurement_differences": 0, + "image_differences": 0, + "tables": [ + 3, + 3 + ], + "physical_images": [ + 0, + 0 + ], + "logical_images": [ + 0, + 0 + ], + "known_relationship_keys": [ + 1, + 1 + ], + "ordinary_receipt_sha256": "5d6d7f02523bba49204b9f6f4626b4b33425690c1ba796423ab47aade5d119c5" + }, + { + "case": "cp_tutorial_beginner_segmentation_final", + "ordinary_repeat": 1, + "native_repeat": 0, + "measurement_differences": 0, + "image_differences": 0, + "tables": [ + 7, + 7 + ], + "physical_images": [ + 2, + 2 + ], + "logical_images": [ + 2, + 2 + ], + "known_relationship_keys": [ + 5, + 5 + ], + "ordinary_receipt_sha256": "1595af0e768d7c7b287a6ac21b689f8c6f866b2d5540c751a91e226bad97eb95" + }, + { + "case": "cp_tutorial_beginner_segmentation_final", + "ordinary_repeat": 1, + "native_repeat": 1, + "measurement_differences": 0, + "image_differences": 0, + "tables": [ + 7, + 7 + ], + "physical_images": [ + 2, + 2 + ], + "logical_images": [ + 2, + 2 + ], + "known_relationship_keys": [ + 5, + 5 + ], + "ordinary_receipt_sha256": "1595af0e768d7c7b287a6ac21b689f8c6f866b2d5540c751a91e226bad97eb95" + } + ], + "scope": "Both ordinary observations versus both native measured observations; full strict files/values/correlation policy; descriptive two-repeat clocks, not full30 acceptance." +} diff --git a/docs/refactor/receipts/runtime-paired-v11-20261004/timing_summary.json b/docs/refactor/receipts/runtime-paired-v11-20261004/timing_summary.json new file mode 100644 index 0000000000..d7218959f9 --- /dev/null +++ b/docs/refactor/receipts/runtime-paired-v11-20261004/timing_summary.json @@ -0,0 +1,507 @@ +{ + "source_head": "771950f652e5237b71bf0b21b0b8050bda48e011", + "cases": { + "cp_tutorial_3d_monolayer": { + "native_invocation_seconds": [ + 14.757951722946018, + 14.251339617185295 + ], + "native_mean_seconds": 14.504645670065656, + "native_first_module_through_post_run_seconds": [ + 14.628245717845857, + 14.132655873894691 + ], + "native_first_module_through_post_run_mean_seconds": 14.380450795870274, + "ordinary_samples": { + "compile_seconds": [ + 1.027512788772583, + 1.0361676216125488 + ], + "execute_seconds": [ + 5.0158140659332275, + 6.333127975463867 + ], + "total_seconds": [ + 6.781453618314117, + 8.065518350806087 + ] + }, + "ordinary_means": { + "compile_seconds": 1.031840205192566, + "execute_seconds": 5.674471020698547, + "total_seconds": 7.423485984560102 + }, + "observed_ranges_seconds": { + "native_invocation_seconds": { + "minimum": 14.251339617185295, + "maximum": 14.757951722946018 + }, + "compile_seconds": { + "minimum": 1.027512788772583, + "maximum": 1.0361676216125488 + }, + "execute_seconds": { + "minimum": 5.0158140659332275, + "maximum": 6.333127975463867 + }, + "total_seconds": { + "minimum": 6.781453618314117, + "maximum": 8.065518350806087 + } + }, + "other_total_seconds_mean": 0.7171747586689889, + "execution_speedup_ratio_of_means": 2.5561229614456793, + "total_speedup_ratio_of_means": 1.9538860449435018 + }, + "ExampleSpeckles": { + "native_invocation_seconds": [ + 2.0039482093416154, + 1.8744277539663017 + ], + "native_mean_seconds": 1.9391879816539586, + "native_first_module_through_post_run_seconds": [ + 1.9125203830190003, + 1.793057702947408 + ], + "native_first_module_through_post_run_mean_seconds": 1.8527890429832041, + "ordinary_samples": { + "compile_seconds": [ + 0.4752085208892822, + 0.48175573348999023 + ], + "execute_seconds": [ + 0.9069628715515137, + 1.1766977310180664 + ], + "total_seconds": [ + 1.6977782007306814, + 2.0053355218842626 + ] + }, + "ordinary_means": { + "compile_seconds": 0.47848212718963623, + "execute_seconds": 1.04183030128479, + "total_seconds": 1.851556861307472 + }, + "observed_ranges_seconds": { + "native_invocation_seconds": { + "minimum": 1.8744277539663017, + "maximum": 2.0039482093416154 + }, + "compile_seconds": { + "minimum": 0.4752085208892822, + "maximum": 0.48175573348999023 + }, + "execute_seconds": { + "minimum": 0.9069628715515137, + "maximum": 1.1766977310180664 + }, + "total_seconds": { + "minimum": 1.6977782007306814, + "maximum": 2.0053355218842626 + } + }, + "other_total_seconds_mean": 0.3312444328330457, + "execution_speedup_ratio_of_means": 1.8613280677885282, + "total_speedup_ratio_of_means": 1.0473283441506658 + }, + "cp_tutorial_beginner_segmentation_final": { + "native_invocation_seconds": [ + 15.272316726390272, + 14.784655185882002 + ], + "native_mean_seconds": 15.028485956136137, + "native_first_module_through_post_run_seconds": [ + 15.028584334999323, + 14.578100933227688 + ], + "native_first_module_through_post_run_mean_seconds": 14.803342634113505, + "ordinary_samples": { + "compile_seconds": [ + 0.6007773876190186, + 0.6125655174255371 + ], + "execute_seconds": [ + 3.902729034423828, + 4.202756643295288 + ], + "total_seconds": [ + 4.9476758115924895, + 5.297459670342505 + ] + }, + "ordinary_means": { + "compile_seconds": 0.6066714525222778, + "execute_seconds": 4.052742838859558, + "total_seconds": 5.122567740967497 + }, + "observed_ranges_seconds": { + "native_invocation_seconds": { + "minimum": 14.784655185882002, + "maximum": 15.272316726390272 + }, + "compile_seconds": { + "minimum": 0.6007773876190186, + "maximum": 0.6125655174255371 + }, + "execute_seconds": { + "minimum": 3.902729034423828, + "maximum": 4.202756643295288 + }, + "total_seconds": { + "minimum": 4.9476758115924895, + "maximum": 5.297459670342505 + } + }, + "other_total_seconds_mean": 0.4631534495856613, + "execution_speedup_ratio_of_means": 3.7082258987755448, + "total_speedup_ratio_of_means": 2.933779837784578 + } + }, + "clock_definitions": { + "native_invocation_seconds": "Full existing native invocation clock: pre-first-module work plus first-module-through-post-run. Process imports, JVM startup, input loading and full warmup excluded. This is the same clock used by V7 ratios.", + "native_first_module_through_post_run_seconds": "Separate diagnostic clock, excluding native pre-first-module work. Not used for the plotted speedup ratios.", + "openhcs_execute_seconds": "Ordinary pipeline execution including generic runtime plumbing; not pure callable or kernel time.", + "openhcs_total_seconds": "Ordinary compilation plus execution and default OUTCOMES/RSS completion. ZMQ server READY startup/shutdown excluded." + }, + "scope": "Three current pipelines, one selected well and one worker; two observations per method. Means and observed min/max are descriptive, not confidence intervals or per-cut causal gains. No full30 or scaling claim. All measured samples shown separately; no synthetic pairing of native and OpenHCS repetitions.", + "qualification": { + "status": "PASS", + "source_head": "771950f652e5237b71bf0b21b0b8050bda48e011", + "origin_main": "32ededf2421ac79df147628c1732785c139d0970", + "science_path": "/home/ts/.local/state/openhcs-maintenance/20261004/canonical-runtime-chains-paired-v11-qualified/strict-science.json", + "science_sha256": "608a83ff5903f51652ff3d0d61793cad4efaf27b9085dda958174d1139e60a52", + "source_freeze_path": "/home/ts/.local/state/openhcs-maintenance/20261004/canonical-runtime-chains-paired-v11-qualified/source-freeze.json", + "source_freeze_sha256": "feefec8c0fadd3da87b29c1c61495e939802f5729b274192f99a1e673b044be9", + "source_environment_sha256": "b579dda9991d0de75047d9dd84591f556085ef401bafa1f1f8bc9c86ca588ecc", + "dependency_heads": { + "external/ObjectState": "9fb5b7eeea96f3b7bb0579de30718bbcc0018823", + "external/python-introspect": "ed1e4eb15f7a0bf8315274d3e47858fa2b4372bb", + "external/arraybridge": "1e53d03d9f468322a1c085c8be29485fb139caf2", + "external/metaclass-registry": "393a7e03003cdc56df9013f932ed4f26e632d77a", + "external/zmqruntime": "f7b80c871eb4773757368bb21f108920bf461881", + "external/pycodify": "108a8edbf25de258168756bac22c7572f018fa90", + "external/PolyStore": "89deeef3662eabb11bc520fad9acd976698636bd", + "external/pyqt-reactive": "b64ed2eca96fcdad29ba90beb545caa669a4d3d8" + }, + "actual_import_dependency_heads": { + "external/ObjectState": "9fb5b7eeea96f3b7bb0579de30718bbcc0018823", + "external/PolyStore": "89deeef3662eabb11bc520fad9acd976698636bd", + "external/arraybridge": "1e53d03d9f468322a1c085c8be29485fb139caf2", + "external/metaclass-registry": "393a7e03003cdc56df9013f932ed4f26e632d77a", + "external/pycodify": "108a8edbf25de258168756bac22c7572f018fa90", + "external/pyqt-reactive": "b64ed2eca96fcdad29ba90beb545caa669a4d3d8", + "external/python-introspect": "ed1e4eb15f7a0bf8315274d3e47858fa2b4372bb", + "external/zmqruntime": "f7b80c871eb4773757368bb21f108920bf461881" + }, + "ordered_source_ref_counts": { + "cp_tutorial_3d_monolayer": 180, + "ExampleSpeckles": 2, + "cp_tutorial_beginner_segmentation_final": 10 + }, + "comparisons": [ + { + "case": "cp_tutorial_3d_monolayer", + "ordinary_repeat": 0, + "native_repeat": 0, + "measurement_differences": 0, + "image_differences": 0, + "tables": [ + 7, + 6 + ], + "physical_images": [ + 2, + 2 + ], + "logical_images": [ + 2, + 2 + ], + "known_relationship_keys": [ + 3, + 3 + ], + "ordinary_receipt_sha256": "465cf0eac54e4ff82845ec3f98f3402d5b7ab802badfb2bc2434753a59b999ad" + }, + { + "case": "cp_tutorial_3d_monolayer", + "ordinary_repeat": 0, + "native_repeat": 1, + "measurement_differences": 0, + "image_differences": 0, + "tables": [ + 7, + 6 + ], + "physical_images": [ + 2, + 2 + ], + "logical_images": [ + 2, + 2 + ], + "known_relationship_keys": [ + 3, + 3 + ], + "ordinary_receipt_sha256": "465cf0eac54e4ff82845ec3f98f3402d5b7ab802badfb2bc2434753a59b999ad" + }, + { + "case": "ExampleSpeckles", + "ordinary_repeat": 0, + "native_repeat": 0, + "measurement_differences": 0, + "image_differences": 0, + "tables": [ + 3, + 3 + ], + "physical_images": [ + 0, + 0 + ], + "logical_images": [ + 0, + 0 + ], + "known_relationship_keys": [ + 1, + 1 + ], + "ordinary_receipt_sha256": "3831b86c470132b166b1034305d771dba222c1dd8fb42f54093e3b5eaf6d0cf5" + }, + { + "case": "ExampleSpeckles", + "ordinary_repeat": 0, + "native_repeat": 1, + "measurement_differences": 0, + "image_differences": 0, + "tables": [ + 3, + 3 + ], + "physical_images": [ + 0, + 0 + ], + "logical_images": [ + 0, + 0 + ], + "known_relationship_keys": [ + 1, + 1 + ], + "ordinary_receipt_sha256": "3831b86c470132b166b1034305d771dba222c1dd8fb42f54093e3b5eaf6d0cf5" + }, + { + "case": "cp_tutorial_beginner_segmentation_final", + "ordinary_repeat": 0, + "native_repeat": 0, + "measurement_differences": 0, + "image_differences": 0, + "tables": [ + 7, + 7 + ], + "physical_images": [ + 2, + 2 + ], + "logical_images": [ + 2, + 2 + ], + "known_relationship_keys": [ + 5, + 5 + ], + "ordinary_receipt_sha256": "559ecdbb4e6a856c0a911d67e60171458a77e01f8d63f3b49326cc04fd6b2d38" + }, + { + "case": "cp_tutorial_beginner_segmentation_final", + "ordinary_repeat": 0, + "native_repeat": 1, + "measurement_differences": 0, + "image_differences": 0, + "tables": [ + 7, + 7 + ], + "physical_images": [ + 2, + 2 + ], + "logical_images": [ + 2, + 2 + ], + "known_relationship_keys": [ + 5, + 5 + ], + "ordinary_receipt_sha256": "559ecdbb4e6a856c0a911d67e60171458a77e01f8d63f3b49326cc04fd6b2d38" + }, + { + "case": "cp_tutorial_3d_monolayer", + "ordinary_repeat": 1, + "native_repeat": 0, + "measurement_differences": 0, + "image_differences": 0, + "tables": [ + 7, + 6 + ], + "physical_images": [ + 2, + 2 + ], + "logical_images": [ + 2, + 2 + ], + "known_relationship_keys": [ + 3, + 3 + ], + "ordinary_receipt_sha256": "9cecd2752cde33d1860a928dc04ccbcf06275e5319a7c586646d3b1e13222742" + }, + { + "case": "cp_tutorial_3d_monolayer", + "ordinary_repeat": 1, + "native_repeat": 1, + "measurement_differences": 0, + "image_differences": 0, + "tables": [ + 7, + 6 + ], + "physical_images": [ + 2, + 2 + ], + "logical_images": [ + 2, + 2 + ], + "known_relationship_keys": [ + 3, + 3 + ], + "ordinary_receipt_sha256": "9cecd2752cde33d1860a928dc04ccbcf06275e5319a7c586646d3b1e13222742" + }, + { + "case": "ExampleSpeckles", + "ordinary_repeat": 1, + "native_repeat": 0, + "measurement_differences": 0, + "image_differences": 0, + "tables": [ + 3, + 3 + ], + "physical_images": [ + 0, + 0 + ], + "logical_images": [ + 0, + 0 + ], + "known_relationship_keys": [ + 1, + 1 + ], + "ordinary_receipt_sha256": "5d6d7f02523bba49204b9f6f4626b4b33425690c1ba796423ab47aade5d119c5" + }, + { + "case": "ExampleSpeckles", + "ordinary_repeat": 1, + "native_repeat": 1, + "measurement_differences": 0, + "image_differences": 0, + "tables": [ + 3, + 3 + ], + "physical_images": [ + 0, + 0 + ], + "logical_images": [ + 0, + 0 + ], + "known_relationship_keys": [ + 1, + 1 + ], + "ordinary_receipt_sha256": "5d6d7f02523bba49204b9f6f4626b4b33425690c1ba796423ab47aade5d119c5" + }, + { + "case": "cp_tutorial_beginner_segmentation_final", + "ordinary_repeat": 1, + "native_repeat": 0, + "measurement_differences": 0, + "image_differences": 0, + "tables": [ + 7, + 7 + ], + "physical_images": [ + 2, + 2 + ], + "logical_images": [ + 2, + 2 + ], + "known_relationship_keys": [ + 5, + 5 + ], + "ordinary_receipt_sha256": "1595af0e768d7c7b287a6ac21b689f8c6f866b2d5540c751a91e226bad97eb95" + }, + { + "case": "cp_tutorial_beginner_segmentation_final", + "ordinary_repeat": 1, + "native_repeat": 1, + "measurement_differences": 0, + "image_differences": 0, + "tables": [ + 7, + 7 + ], + "physical_images": [ + 2, + 2 + ], + "logical_images": [ + 2, + 2 + ], + "known_relationship_keys": [ + 5, + 5 + ], + "ordinary_receipt_sha256": "1595af0e768d7c7b287a6ac21b689f8c6f866b2d5540c751a91e226bad97eb95" + } + ], + "scope": "Both ordinary observations versus both native measured observations; full strict files/values/correlation policy; descriptive two-repeat clocks, not full30 acceptance." + }, + "plot_script_sha256": "d6b40f6cb13c5e65b8974371a15b1a22f2a1a3b8eca7e73111332ce82e4980a4", + "figure_owner_sha256": "bb1bf777c5c5d95507b0f751310652e29ab8b3919387334871a890bfe667f3c1", + "v7_plot_script_sha256": "28f92861b2f69908e44e30ee6dd009ac839d8eb76f32326d18a6a12b87a9dd29", + "artifacts": { + "/home/ts/.local/state/openhcs-maintenance/20261004/canonical-runtime-chains-paired-v11-qualified/figures/metrics.csv": "c8959ed19a6812631a8ca672b608c9ea59803904f64cd9a22cb900c0bbfc030b", + "/home/ts/.local/state/openhcs-maintenance/20261004/canonical-runtime-chains-paired-v11-qualified/figures/qualification.json": "bf1ad5acbb60be7582d4ef8eaacfff2327f2eaa356866512ca2fa781f034f127", + "/home/ts/.local/state/openhcs-maintenance/20261004/canonical-runtime-chains-paired-v11-qualified/figures/matched_runtime.png": "0336e6bf17b761fca37440ad6b31e5813fd322ad10987d073511898fa7812224", + "/home/ts/.local/state/openhcs-maintenance/20261004/canonical-runtime-chains-paired-v11-qualified/figures/matched_runtime.svg": "5421554a3b9ddc6a4d661b2e8c98e79a29673865d0079e7de439e6dfd6781368", + "/home/ts/.local/state/openhcs-maintenance/20261004/canonical-runtime-chains-paired-v11-qualified/figures/matched_speedup.png": "6058479d8334a1ea073448fd93082b00ecc2cb2269fa711ef6ae7ea2dcf987b1", + "/home/ts/.local/state/openhcs-maintenance/20261004/canonical-runtime-chains-paired-v11-qualified/figures/matched_speedup.svg": "8fd3e4d1a08db96b277e83d6969805402ea2b73cbea7f95bb627514b5988c8dd", + "/home/ts/.local/state/openhcs-maintenance/20261004/canonical-runtime-chains-paired-v11-qualified/figures/matched_runtime_samples.png": "7b1dff465d7a416a64dff6d2d6106a3444b2edca441d4f77bb64489132ee412b", + "/home/ts/.local/state/openhcs-maintenance/20261004/canonical-runtime-chains-paired-v11-qualified/figures/matched_runtime_samples.svg": "972c877946f42ac06b875f5e9932c5a10784a411d8ba41e157c1fb3f74881c2e" + } +} From c6827619f4da4ed7b537de857df410f40f08cd4c Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 23:06:10 -0400 Subject: [PATCH 201/235] Share measurement feature admission across object and axis queries Columnar schemas now own row facts; table semantics derive subject declarations. RelateObjects consumes feature indexes directly rather than transposing child and parent feature maps. Preserve qualification, source alignment, constructor dispatch, alias order and current column values. Refs #384 --- openhcs/core/measurement_feature_queries.py | 446 +++++++++--------- openhcs/core/runtime_artifact_queries.py | 3 +- .../backends/cellprofiler/relationships.py | 186 ++++---- .../test_cellprofiler_module_execution.py | 22 +- ..._measurement_lookup_dialect_performance.py | 206 ++++++++ tests/unit/test_runtime_artifact_queries.py | 41 +- 6 files changed, 571 insertions(+), 333 deletions(-) diff --git a/openhcs/core/measurement_feature_queries.py b/openhcs/core/measurement_feature_queries.py index 15acdfb0d7..078c566bdc 100644 --- a/openhcs/core/measurement_feature_queries.py +++ b/openhcs/core/measurement_feature_queries.py @@ -3,7 +3,7 @@ from __future__ import annotations from abc import ABC, abstractmethod -from collections.abc import Callable, Mapping, Sequence +from collections.abc import Callable, Iterator, Mapping, Sequence from dataclasses import dataclass, field from types import MappingProxyType from typing import Any, ClassVar @@ -238,8 +238,6 @@ class ColumnarMeasurementTableSchema: columns: tuple[str, ...] normalized_columns: dict[str, str] - object_names: tuple[str, ...] - feature_names: frozenset[str] feature_name_values: Sequence[object] | None object_name_values: Sequence[object] | None source_image_name_values: Sequence[object] | None @@ -258,11 +256,9 @@ def from_table(cls, table: MeasurementTable) -> "ColumnarMeasurementTableSchema" normalized_columns = { column: normalize_measurement_token(column) for column in columns } - table_object_name = table.subject.object_name object_name_values = ( columnar_row_values(rows, MeasurementRowAxisField.OBJECT_NAME.value) - if table_object_name is None - and MeasurementRowAxisField.OBJECT_NAME.value in columns + if MeasurementRowAxisField.OBJECT_NAME.value in columns else None ) source_image_name_values = ( @@ -271,38 +267,11 @@ def from_table(cls, table: MeasurementTable) -> "ColumnarMeasurementTableSchema" else None ) feature_name_values = cls._feature_name_values(rows, columns) - if table_object_name is not None: - object_names = (table_object_name,) - elif object_name_values is not None: - object_names = tuple( - dict.fromkeys( - object_name - for value in object_name_values - for object_name in (str(value).strip(),) - if object_name - ) - ) - else: - object_names = () - return ColumnarMeasurementTableSchemaCache.process_cache().put_bound( rows, cls( columns=columns, normalized_columns=normalized_columns, - object_names=object_names, - feature_names=( - MeasurementTableObjectFeatureSemantics.feature_names_from_names( - columns, - table, - ) - if feature_name_values is None - else frozenset( - str(value) - for value in feature_name_values - if value not in (None, "") - ) - ), feature_name_values=feature_name_values, object_name_values=object_name_values, source_image_name_values=source_image_name_values, @@ -326,19 +295,30 @@ def feature_mask(self, candidates: tuple[str, ...]) -> Any | None: """Return a boolean row mask for row-declared feature ownership.""" if self.feature_name_values is None: return None - cached = self.feature_masks_by_candidates.get(candidates) - if cached is not None: - return cached - normalized_features = np.asarray( - [ - normalize_measurement_token(str(value)) - for value in self.feature_name_values - ], - dtype=object, + return self.feature_masks((candidates,))[candidates] + + def feature_masks( + self, candidates: Sequence[tuple[str, ...]] + ) -> Mapping[tuple[str, ...], Any]: + """Resolve multiple feature selections from one row-feature normalization.""" + missing = tuple( + candidate + for candidate in dict.fromkeys(candidates) + if candidate not in self.feature_masks_by_candidates ) - mask = np.isin(normalized_features, np.asarray(candidates, dtype=object)) - self.feature_masks_by_candidates[candidates] = mask - return mask + if missing and self.feature_name_values is not None: + normalized_features = np.asarray( + [ + normalize_measurement_token(str(value)) + for value in self.feature_name_values + ], + dtype=object, + ) + for candidate in missing: + self.feature_masks_by_candidates[candidate] = np.isin( + normalized_features, np.asarray(candidate, dtype=object) + ) + return self.feature_masks_by_candidates def object_mask(self, object_name: str) -> Any | None: """Return a boolean row mask for a row-owned object table.""" @@ -378,6 +358,148 @@ def source_mask(self, source_candidates: tuple[str, ...]) -> Any | None: self.source_masks_by_candidates[source_candidates] = mask return mask + def feature_value_indexes( + self, + table: MeasurementTable, + queries: Mapping[str, "MeasurementFeatureQuery"], + query_object_names_by_feature: Mapping[str, Mapping[str | None, str | None]], + *, + index_type: type["MeasurementFeatureValueIndex"], + row_masks: Mapping[int | None, Any | None] | None = None, + measurement_value_qualifier: Callable[[object], bool] | None = None, + ) -> Iterator[ + tuple[str, dict[int | None, dict[str | None, "MeasurementFeatureValueIndex"]]] + ]: + """Project one admitted table epoch into feature/object/axis indexes. + + Column values and their qualification are admitted once per physical + column. Object labels are resolved once per selected row, shared across + features and overlapping axis projections; no data survives this call. + """ + rows = table.rows + projections = {None: None} if row_masks is None else row_masks + object_id_field = self.object_id_field(table.subject.object_id_field) + object_ids = ( + None + if object_id_field is None + else columnar_row_values(rows, object_id_field) + ) + object_labels_by_row: dict[int, int | None] = {} + admitted_columns: dict[str, tuple[Any, Any]] = {} + columns_by_token: dict[str, list[str]] = {} + for column, token in self.normalized_columns.items(): + columns_by_token.setdefault(token, []).append(column) + self.feature_masks(tuple(query.field_candidates for query in queries.values())) + for feature_name, query in queries.items(): + query_object_names_by_result = query_object_names_by_feature[feature_name] + if not query.table_source_matches_feature(table): + yield feature_name, { + axis: { + result_name: index_type() + for result_name in query_object_names_by_result + } + for axis in projections + } + continue + if self.feature_name_values is None: + feature_columns = tuple( + dict.fromkeys( + column + for candidate in query.field_candidates + for column in columns_by_token.get(candidate, ()) + ) + ) + else: + feature_column = self.matching_feature_column(query) + feature_columns = () if feature_column is None else (feature_column,) + if not feature_columns: + yield feature_name, {axis: {} for axis in projections} + continue + raw_values = None + value_mask = None + for feature_column in feature_columns: + if feature_column not in admitted_columns: + values = np.asarray( + columnar_row_values(rows, feature_column), dtype=object + ) + mask = np.asarray( + [ + not _is_structural_missing_measurement_cell(value) + and ( + bool(measurement_value_qualifier(value)) + if measurement_value_qualifier is not None + else MeasurementScalarLiteral( + value + ).is_present_measurement_value + ) + for value in values + ], + dtype=bool, + ) + admitted_columns[feature_column] = values, mask + candidate_values, candidate_mask = admitted_columns[feature_column] + if raw_values is None: + raw_values, value_mask = candidate_values, candidate_mask + continue + selected_mask = np.logical_and( + np.logical_not(value_mask), candidate_mask + ) + # Alias precedence selects the first present value per row. + raw_values = np.where(selected_mask, candidate_values, raw_values) + value_mask = np.logical_or(value_mask, candidate_mask) + source_mask = self.source_mask(query.source_candidates) + feature_mask = self.feature_mask(query.field_candidates) + base_mask = value_mask + if source_mask is not None: + base_mask = np.logical_and(base_mask, source_mask) + if feature_mask is not None: + base_mask = np.logical_and(base_mask, feature_mask) + by_axis = {} + for axis, row_mask in projections.items(): + projected_mask = ( + base_mask + if row_mask is None + else np.logical_and(base_mask, row_mask) + ) + indexes = {} + for result_name, object_name in query_object_names_by_result.items(): + query_object_name = object_name + object_mask = None + if query_object_name is not None: + if table.subject.object_name not in (None, query_object_name): + continue + if table.subject.object_name is None: + if self.object_name_values is None: + continue + object_mask = self.object_mask(query_object_name) + effective_mask = ( + projected_mask + if object_mask is None + else np.logical_and(projected_mask, object_mask) + ) + object_values = raw_values[effective_mask].astype(float, copy=False) + if object_ids is None: + indexes[result_name] = index_type( + {}, [float(value) for value in object_values] + ) + continue + values_by_label = {} + for row_index, value in zip( + np.flatnonzero(effective_mask), object_values, strict=True + ): + if row_index not in object_labels_by_row: + object_labels_by_row[row_index] = ( + MeasurementObjectLabelResolution( + object_ids[row_index] + ).object_label + ) + object_label = object_labels_by_row[row_index] + if object_label is not None: + values_by_label[object_label] = float(value) + indexes[result_name] = index_type(values_by_label, []) + by_axis[axis] = indexes + yield feature_name, by_axis + def matching_feature_column(self, query: "MeasurementFeatureQuery") -> str | None: """Return the column matching a measurement feature query.""" if self.feature_name_values is not None: @@ -847,24 +969,31 @@ def value_indexes_by_axis( axis_values = self.table_axis_values(table, row_axis) if not axis_values: continue - for axis_value in axis_values: - target = by_axis.setdefault( - axis_value, - self.empty_indexes(missing_cache_object_names), + axis_column = columnar_row_values(table.rows, row_axis.value) + _feature_name, table_indexes = next( + ColumnarMeasurementTableSchema.from_table(table).feature_value_indexes( + table, + {table_query.feature_name: table_query}, + { + table_query.feature_name: { + object_name: query_objects_by_requested_object[object_name] + for object_name in table_object_names + } + }, + index_type=MeasurementFeatureValueIndex, + row_masks={ + axis_value: MeasurementAxisValueProjection( + row_axis, axis_value + ).mask(axis_column) + for axis_value in axis_values + }, ) - self.merge_indexes( - target, - self.table_value_indexes( - table, - table_query, - table_object_names, - query_objects_by_requested_object, - projection=MeasurementAxisValueProjection( - row_axis, - axis_value, - ), - ), + ) + for axis_value, indexes in table_indexes.items(): + target = by_axis.setdefault( + axis_value, self.empty_indexes(missing_cache_object_names) ) + self.merge_indexes(target, indexes) if not by_axis: return self.requested_axis_value_indexes(cached_axis_indexes) @@ -916,53 +1045,6 @@ def merge_indexes( target[object_name] = MeasurementFeatureValueIndex() target[object_name] = target[object_name].merged(object_index) - def table_value_indexes( - self, - table: MeasurementTable, - table_query: MeasurementFeatureQuery, - table_object_names: tuple[str, ...], - query_objects_by_requested_object: Mapping[str, str | None], - *, - projection: MeasurementAxisValueProjection | None = None, - ) -> dict[str, "MeasurementFeatureValueIndex"]: - """Return object indexes for one table, optionally narrowed by row axis.""" - return self.columnar_table_value_indexes( - table, - table_query, - table_object_names, - query_objects_by_requested_object, - projection=projection, - ) - - def columnar_table_value_indexes( - self, - table: MeasurementTable, - table_query: MeasurementFeatureQuery, - table_object_names: tuple[str, ...], - query_objects_by_requested_object: Mapping[str, str | None], - *, - projection: MeasurementAxisValueProjection | None = None, - ) -> dict[str, "MeasurementFeatureValueIndex"]: - """Return object indexes for one columnar table and optional axis projection.""" - rows = table.rows - row_mask = None - if projection is not None: - column_names = tuple(str(column) for column in rows.columns) - if projection.field_name in column_names: - row_mask = projection.mask( - columnar_row_values(rows, projection.field_name) - ) - columnar_indexes = MeasurementFeatureValueIndex.from_columnar_table_by_object( - table, - table_query, - { - object_name: query_objects_by_requested_object[object_name] - for object_name in table_object_names - }, - row_mask=row_mask, - ) - return columnar_indexes - @staticmethod def table_axis_values( table: MeasurementTable, @@ -1333,114 +1415,20 @@ def from_columnar_table_by_object( row_mask: Any | None = None, measurement_value_qualifier: Callable[[object], bool] | None = None, ) -> dict[str | None, "MeasurementFeatureValueIndex"] | None: - """Return feature indexes from one columnar table scan.""" - rows = table.rows - + """Return one feature through the shared table batch projection.""" if not query.table_source_matches_feature(table): - return { - result_object_name: cls() - for result_object_name in query_object_names_by_result - } - - schema = ColumnarMeasurementTableSchema.from_table(table) - if schema.feature_name_values is None: - feature_columns = matching_measurement_fields( - schema.normalized_columns, - query.field_candidates, - ) - else: - feature_column = schema.matching_feature_column(query) - feature_columns = () if feature_column is None else (feature_column,) - if not feature_columns: - return {} - - raw_values = np.full(rows.row_count(), None, dtype=object) - value_mask = np.zeros(rows.row_count(), dtype=bool) - for feature_column in feature_columns: - candidate_values = np.asarray( - columnar_row_values(rows, feature_column), - dtype=object, + return {result_name: cls() for result_name in query_object_names_by_result} + _feature_name, by_axis = next( + ColumnarMeasurementTableSchema.from_table(table).feature_value_indexes( + table, + {query.feature_name: query}, + {query.feature_name: query_object_names_by_result}, + index_type=cls, + row_masks={None: row_mask}, + measurement_value_qualifier=measurement_value_qualifier, ) - candidate_mask = np.asarray( - [ - not _is_structural_missing_measurement_cell(value) - and ( - bool(measurement_value_qualifier(value)) - if measurement_value_qualifier is not None - else MeasurementScalarLiteral( - value - ).is_present_measurement_value - ) - for value in candidate_values - ], - dtype=bool, - ) - selected_mask = np.logical_and(np.logical_not(value_mask), candidate_mask) - raw_values[selected_mask] = candidate_values[selected_mask] - value_mask = np.logical_or(value_mask, candidate_mask) - source_mask = schema.source_mask(query.source_candidates) - feature_mask = schema.feature_mask(query.field_candidates) - source_feature_mask = ( - value_mask - if source_mask is None - else np.logical_and(value_mask, source_mask) - ) - base_mask = ( - source_feature_mask - if feature_mask is None - else np.logical_and(source_feature_mask, feature_mask) ) - if row_mask is not None: - base_mask = np.logical_and(base_mask, row_mask) - object_id_field = schema.object_id_field(table.subject.object_id_field) - object_ids = ( - None - if object_id_field is None or object_id_field not in schema.columns - else np.asarray(columnar_row_values(rows, object_id_field), dtype=object) - ) - table_object_name = table.subject.object_name - indexes: dict[str | None, MeasurementFeatureValueIndex] = {} - for ( - result_object_name, - query_object_name, - ) in query_object_names_by_result.items(): - object_mask: Any | None = None - if query_object_name is not None: - if table_object_name not in (None, query_object_name): - continue - if table_object_name is None: - if schema.object_name_values is None: - continue - object_mask = schema.object_mask(query_object_name) - - effective_mask = ( - base_mask - if object_mask is None - else np.logical_and(base_mask, object_mask) - ) - object_values = raw_values[effective_mask].astype(float, copy=False) - if object_id_field is None or object_ids is None: - indexes[result_object_name] = cls( - {}, - [float(value) for value in object_values], - ) - continue - indexes[result_object_name] = cls( - { - object_label: float(value) - for raw_object_id, value in zip( - object_ids[effective_mask], - object_values, - strict=True, - ) - for object_label in ( - MeasurementObjectLabelResolution(raw_object_id).object_label, - ) - if object_label is not None - }, - [], - ) - return indexes + return by_axis[None] @classmethod def from_rows( @@ -1539,15 +1527,35 @@ def from_table_declarations( cls, table: MeasurementTable, ) -> "MeasurementTableObjectFeatureSemantics": - """Return semantics from table-level schema when rows need not be scanned.""" + """Derive table declarations from its subject and shared physical rows.""" object_name = table.subject.object_name schema = ColumnarMeasurementTableSchema.from_table(table) - return cls( - object_names=( - (object_name,) if object_name is not None else schema.object_names - ), - feature_names=schema.feature_names, + object_names = ( + (object_name,) + if object_name is not None + else tuple( + dict.fromkeys( + name + for value in ( + () + if schema.object_name_values is None + else schema.object_name_values + ) + for name in (str(value).strip(),) + if name + ) + ) + ) + feature_names = ( + cls.feature_names_from_names(schema.columns, table) + if schema.feature_name_values is None + else frozenset( + str(value) + for value in schema.feature_name_values + if value not in (None, "") + ) ) + return cls(object_names=object_names, feature_names=feature_names) @staticmethod def feature_names_from_names( diff --git a/openhcs/core/runtime_artifact_queries.py b/openhcs/core/runtime_artifact_queries.py index c4c75fa673..fc66a4fe2a 100644 --- a/openhcs/core/runtime_artifact_queries.py +++ b/openhcs/core/runtime_artifact_queries.py @@ -29,6 +29,7 @@ MeasurementFeatureValueIndex, MeasurementObjectFeatureVectorBatchQuery, MeasurementTableFeatureQuery, + MeasurementTableObjectFeatureSemantics, MeasurementValueIndexResult, ) from openhcs.core.measurement_lookup_dialect import ( @@ -189,7 +190,7 @@ def matches(self, table: MeasurementTable) -> bool: if isinstance(table.rows, ColumnarRows): return ( self.object_name - in ColumnarMeasurementTableSchema.from_table(table).object_names + in MeasurementTableObjectFeatureSemantics.from_table(table).object_names ) if not _measurement_table_may_declare_object_name(table): return False diff --git a/openhcs/processing/backends/cellprofiler/relationships.py b/openhcs/processing/backends/cellprofiler/relationships.py index 5d990208f1..498d93295c 100644 --- a/openhcs/processing/backends/cellprofiler/relationships.py +++ b/openhcs/processing/backends/cellprofiler/relationships.py @@ -749,6 +749,7 @@ def feature_name(cls, identity: object) -> str: from openhcs.constants.constants import MemoryType from openhcs.core.memory.decorators import numpy as numpy_decorator from openhcs.core.measurement_feature_queries import ( + ColumnarMeasurementTableSchema, MeasurementAxisValueProjection, MeasurementFeatureQuery, MeasurementFeatureValueIndex, @@ -1477,8 +1478,8 @@ def parent_mean_upstream_measurement_rows( payload: ObjectRelationship, ) -> MeasurementSparseColumnarRows: """Return CellProfiler per-parent means over prior child measurements.""" - values_by_child = self.upstream_child_measurement_values(child_spec) - if not values_by_child: + indexes_by_slice = self.upstream_child_feature_indexes(child_spec) + if not indexes_by_slice: return MeasurementSparseColumnarRows.from_rows((), fields=()) sliced_pairs = payload.payload.runtime_slice_pairs() relationship_slices = ( @@ -1506,55 +1507,55 @@ def parent_mean_upstream_measurement_rows( fields.append(FieldSpec(MeasurementRowAxisField.SLICE_INDEX.value, int)) for slice_index, pairs in relationship_slices: value_slice_index = 0 if slice_index is None else slice_index - feature_values_by_parent: dict[int, dict[str, list[float]]] = {} + children_by_parent: dict[int, list[int]] = {} for parent_id, child_id in pairs: - if int(parent_id) <= 0: - continue - child_values = values_by_child.get( - (value_slice_index, int(child_id)), - {}, - ) - if not child_values: - continue - parent_feature_values = feature_values_by_parent.setdefault( - int(parent_id), - {}, - ) - for feature_name, value in child_values.items(): - parent_feature_values.setdefault(feature_name, []).append(value) - for parent_id, feature_values in sorted(feature_values_by_parent.items()): - means = { - RelateObjectsModule.AggregateMeasurementFeature.MEAN_CHILD.feature_name( - child_object_name=child_spec.name, - child_feature_name=feature_name, - ): float( - np.mean(values) + if int(parent_id) > 0: + children_by_parent.setdefault(int(parent_id), []).append( + int(child_id) ) - for feature_name, values in sorted(feature_values.items()) - if values - } - if means: - rows.append( - { - MeasurementRowAxisField.OBJECT_NAME.value: parent_spec.name, - MeasurementRowAxisField.OBJECT_LABEL.value: parent_id, - **( - {} - if slice_index is None - else { - MeasurementRowAxisField.SLICE_INDEX.value: slice_index - } - ), - **means, - } + means_by_parent: dict[int, dict[str, float]] = {} + for feature_name, feature_index in sorted( + indexes_by_slice.get(value_slice_index, {}).items() + ): + values_by_label = feature_index.values_by_label + mean_feature = RelateObjectsModule.AggregateMeasurementFeature.MEAN_CHILD.feature_name( + child_object_name=child_spec.name, + child_feature_name=feature_name, + ) + for parent_id, child_ids in children_by_parent.items(): + values = np.fromiter( + ( + values_by_label[child_id] + for child_id in child_ids + if child_id in values_by_label + ), + dtype=float, ) - fields.extend( - RelateObjectsModule.AggregateMeasurementFeature.MEAN_CHILD.field_spec( - feature_name, - required=False, + if values.size: + means_by_parent.setdefault(parent_id, {})[mean_feature] = float( + np.mean(values) ) - for feature_name in means + for parent_id, means in sorted(means_by_parent.items()): + rows.append( + { + MeasurementRowAxisField.OBJECT_NAME.value: parent_spec.name, + MeasurementRowAxisField.OBJECT_LABEL.value: parent_id, + **( + {} + if slice_index is None + else { + MeasurementRowAxisField.SLICE_INDEX.value: slice_index + } + ), + **means, + } + ) + fields.extend( + RelateObjectsModule.AggregateMeasurementFeature.MEAN_CHILD.field_spec( + feature_name, required=False ) + for feature_name in means + ) return MeasurementSparseColumnarRows.from_rows( rows, fields=FieldSpec.merge_exact( @@ -1563,11 +1564,11 @@ def parent_mean_upstream_measurement_rows( ), ) - def upstream_child_measurement_values( + def upstream_child_feature_indexes( self, child_spec: ArtifactSpec, - ) -> dict[tuple[int, int], dict[str, float]]: - """Index upstream child values on the relationship's declared plane axis.""" + ) -> dict[int, dict[str, MeasurementFeatureValueIndex]]: + """Index upstream features once on the relationship's declared plane axis.""" declared_tables: list[MeasurementTable] = [] for spec in self.request.callable_contract.artifact_inputs.specs: if spec.artifact_type is not MeasurementsArtifactType: @@ -1596,7 +1597,7 @@ def upstream_child_measurement_values( child_labels, child_spec.name, ) - values_by_child: dict[tuple[int, int], dict[str, float]] = {} + indexes_by_slice: dict[int, dict[str, MeasurementFeatureValueIndex]] = {} for raw_row in core_rows.rows().iter_row_mappings(): row = measurement_row_mapping(raw_row) slice_index = measurement_axis_integer_value( @@ -1609,9 +1610,13 @@ def upstream_child_measurement_values( "CellProfiler core object rows require slice and object-label " "identity." ) - values_by_child.setdefault((slice_index, object_label), {})[ - str(row[MeasurementRowAxisField.FEATURE_NAME.value]) - ] = float(row[MeasurementRowValueField.RESULT_VALUE.value]) + feature_name = str(row[MeasurementRowAxisField.FEATURE_NAME.value]) + indexes = indexes_by_slice.setdefault(slice_index, {}) + if feature_name not in indexes: + indexes[feature_name] = MeasurementFeatureValueIndex() + indexes[feature_name].add( + object_label, row[MeasurementRowValueField.RESULT_VALUE.value] + ) plane_domains = core_rows.label_plane_domains() payload_scoped = ( @@ -1623,13 +1628,15 @@ def upstream_child_measurement_values( slice_index=None if payload_scoped else slice_index, slice_count=None if payload_scoped else len(plane_domains), ) + indexes = indexes_by_slice.setdefault(slice_index, {}) + feature_name = CellProfilerObjectCoreMeasurementFeature.OBJECT_NUMBER.value + if feature_name not in indexes: + indexes[feature_name] = MeasurementFeatureValueIndex() for object_label, object_number in object_numbers.items(): - values_by_child.setdefault((slice_index, object_label), {})[ - CellProfilerObjectCoreMeasurementFeature.OBJECT_NUMBER.value - ] = float(object_number) + indexes[feature_name].add(object_label, object_number) if not tables: - return values_by_child + return indexes_by_slice identity_policy = ( self.request.adapter.request.context.source_image_set_identity_policy ) @@ -1722,40 +1729,53 @@ def upstream_child_measurement_values( table_dialect = cellprofiler_lookup_dialect_for_measurement_owner( table.measurement_feature_owner ) - queries = tuple( - ( + queries = { + feature_name: MeasurementFeatureQuery( feature_name, - MeasurementFeatureQuery( - feature_name, - object_name=child_spec.name, - dialect=table_dialect, - ), + object_name=child_spec.name, + dialect=table_dialect, ) for feature_name in aggregate_features - ) - for _local_slice_index, target_slice_index, row_mask in slice_projections: - for feature_name, query in queries: - feature_indexes = MeasurementFeatureValueIndex.from_columnar_table_by_object( - table, - query, - {child_spec.name: query.query_object_name}, - row_mask=row_mask, - measurement_value_qualifier=( - RelateObjectsModule.aggregate_child_measurement_value_is_qualified - ), - ) + } + target_slices = { + local_slice_index: target_slice_index + for local_slice_index, target_slice_index, _row_mask in slice_projections + } + for ( + feature_name, + indexes_by_local_slice, + ) in ColumnarMeasurementTableSchema.from_table(table).feature_value_indexes( + table, + queries, + { + feature_name: {child_spec.name: query.query_object_name} + for feature_name, query in queries.items() + }, + index_type=MeasurementFeatureValueIndex, + row_masks={ + local_slice_index: row_mask + for local_slice_index, _target_slice_index, row_mask in slice_projections + }, + measurement_value_qualifier=( + RelateObjectsModule.aggregate_child_measurement_value_is_qualified + ), + ): + for ( + local_slice_index, + feature_indexes, + ) in indexes_by_local_slice.items(): feature_index = feature_indexes.get(child_spec.name) if feature_index is None: continue + target_slice_index = target_slices[local_slice_index] + target_index = indexes_by_slice.setdefault( + target_slice_index, {} + ).setdefault(feature_name, MeasurementFeatureValueIndex()) for ( object_label, numeric_value, ) in feature_index.values_by_label.items(): - child_values = values_by_child.setdefault( - (target_slice_index, object_label), - {}, - ) - previous = child_values.get(feature_name) + previous = target_index.values_by_label.get(object_label) if previous is not None and not ( previous == numeric_value or (np.isnan(previous) and np.isnan(numeric_value)) @@ -1766,8 +1786,8 @@ def upstream_child_measurement_values( f"{target_slice_index}, object {object_label}: " f"{previous!r} != {numeric_value!r}." ) - child_values[feature_name] = numeric_value - return values_by_child + target_index.values_by_label[object_label] = numeric_value + return indexes_by_slice class NumbaNumpyObjectRelationshipBackendStrategy(ObjectRelationshipBackendStrategy): diff --git a/tests/unit/test_cellprofiler_module_execution.py b/tests/unit/test_cellprofiler_module_execution.py index c3c2e2e5c6..405fbb5a70 100644 --- a/tests/unit/test_cellprofiler_module_execution.py +++ b/tests/unit/test_cellprofiler_module_execution.py @@ -13783,7 +13783,10 @@ def test_relateobjects_relationship_rows_project_parent_mean_distances() -> None assert "Mean_Children_Distance_Minimum_Parents" in mean_rows[0] -def test_relateobjects_parent_means_align_scoped_child_tables_by_source_plane() -> None: +@pytest.mark.parametrize("conflicting_upstream", (False, True)) +def test_relateobjects_parent_means_align_scoped_child_tables_by_source_plane( + conflicting_upstream: bool, +) -> None: source_paths = ("/source/site1.tif", "/source/site2.tif") source_metadata = ( {"well": "A14", "site": "1", "channel": "3"}, @@ -13848,6 +13851,16 @@ def test_relateobjects_parent_means_align_scoped_child_tables_by_source_plane() ) for site_index, values in enumerate(((0.2, 0.6, 99.0), (0.8, 1.0))) ) + if conflicting_upstream: + first = scoped_tables[0] + changed_rows = MeasurementSparseColumnarRows( + columns={ + **first.rows.columns, + colocalization_feature: (123.0, *first.rows.column_values(colocalization_feature)[1:]), + }, + fields=first.rows.fields, + ) + scoped_tables = (*scoped_tables, replace(first, rows=changed_rows)) unrelated_table = MeasurementTable( name="Unrelated_measurements", rows=MeasurementSparseColumnarRows.from_rows( @@ -13955,6 +13968,13 @@ def test_relateobjects_parent_means_align_scoped_child_tables_by_source_plane() child_spec = executor.callable_contract.artifact_inputs.by_ref(declaration.target) assert parent_spec is not None assert child_spec is not None + if conflicting_upstream: + with pytest.raises(ValueError, match="conflicting values"): + projector.parent_mean_upstream_measurement_rows( + parent_spec=parent_spec, child_spec=child_spec, + payload=runtime.relationships[0], + ) + return rows = projector.parent_mean_upstream_measurement_rows( parent_spec=parent_spec, child_spec=child_spec, diff --git a/tests/unit/test_measurement_lookup_dialect_performance.py b/tests/unit/test_measurement_lookup_dialect_performance.py index 1de36425ee..153a85897c 100644 --- a/tests/unit/test_measurement_lookup_dialect_performance.py +++ b/tests/unit/test_measurement_lookup_dialect_performance.py @@ -4,6 +4,8 @@ import math from openhcs.core.measurement_feature_queries import ( + ColumnarMeasurementTableSchema, + MeasurementAxisValueProjection, MeasurementFeatureQuery, MeasurementFeatureValueIndex, ) @@ -161,3 +163,207 @@ def test_columnar_lookup_distinguishes_source_object_absence_padding_and_nan() - explicit_nan_values = explicit_nan_indexes["Nuclei"].values_by_label assert set(explicit_nan_values) == {4} assert math.isnan(explicit_nan_values[4]) + + +def test_feature_batch_preserves_overlapping_axes_last_write_and_live_columns() -> None: + import numpy as np + from openhcs.core.runtime_measurements import MeasurementRowAxisField + from openhcs.core.measurement_row_materialization import MEASUREMENT_SPARSE_CELL + + ids = np.asarray([1, 1, 2, 3, 4], dtype=object) + first = np.asarray([5.0, 7.0, np.nan, MEASUREMENT_SPARSE_CELL, None], dtype=object) + table = MeasurementTable( + name="MutableCells", + subject=MeasurementSubject( + MeasurementScope.OBJECT, "Cells", id_field="cell_key" + ), + rows=MeasurementSparseColumnarRows( + columns={ + "cell_key": ids, + "slice_index": (None, 0, 1, 0, 1), + "first": first, + "second": (1.0, None, 8.0, 9.0, 10.0), + }, + fields=( + FieldSpec("cell_key", int), + FieldSpec("slice_index", int), + FieldSpec("first", float), + FieldSpec("second", float), + ), + ), + ) + schema = ColumnarMeasurementTableSchema.from_table(table) + queries = { + name: MeasurementFeatureQuery(name, object_name="Cells") + for name in ("first", "second") + } + objects = { + name: {"Cells": query.query_object_name} for name, query in queries.items() + } + masks = { + axis: MeasurementAxisValueProjection( + MeasurementRowAxisField.SLICE_INDEX, axis + ).mask(table.rows.column_values("slice_index")) + for axis in (0, 1) + } + + def project(): + return { + feature: { + axis: indexes["Cells"].values_by_label for axis, indexes in axes.items() + } + for feature, axes in schema.feature_value_indexes( + table, + queries, + objects, + index_type=MeasurementFeatureValueIndex, + row_masks=masks, + ) + } + + initial = project() + assert initial == { + "first": {0: {1: 7.0}, 1: {1: 5.0}}, + "second": {0: {1: 1.0, 3: 9.0}, 1: {1: 1.0, 2: 8.0, 4: 10.0}}, + } + ids[0] = 9 + first[0] = 15.0 + changed = project() + assert changed["first"] == {0: {9: 15.0, 1: 7.0}, 1: {9: 15.0}} + assert changed["second"][1] == {9: 1.0, 2: 8.0, 4: 10.0} + assert initial["first"][1] == {1: 5.0} + + +def test_feature_batch_keeps_alias_column_priority_and_feature_qualification() -> None: + import numpy as np + from openhcs.core.measurement_row_materialization import MEASUREMENT_SPARSE_CELL + from openhcs.processing.backends.cellprofiler.relationships import ( + RelateObjectsModule, + ) + + table = MeasurementTable( + name="SourceQualified", + subject=MeasurementSubject(MeasurementScope.OBJECT, "Nuclei"), + rows=MeasurementSparseColumnarRows( + columns={ + "object_label": (1, 2, 3, 3, 5), + "source_image_name": ("DNA", "DNA", "DNA", "DNA", "Memb"), + "mean-intensity": (None, np.nan, 3.0, MEASUREMENT_SPARSE_CELL, 5.0), + "mean intensity": (1.0, 2.0, 9.0, 4.0, 99.0), + }, + fields=( + FieldSpec("object_label", int), + FieldSpec("source_image_name", str), + FieldSpec("mean-intensity", float), + FieldSpec("mean intensity", float), + ), + ), + ) + queries = { + source: MeasurementFeatureQuery( + f"Intensity_MeanIntensity_{source}", + object_name="Nuclei", + dialect=_lookup_dialect(), + ) + for source in ("DNA", "Memb") + } + objects = { + source: {"Nuclei": query.query_object_name} for source, query in queries.items() + } + schema = ColumnarMeasurementTableSchema.from_table(table) + default = { + feature: axes[None]["Nuclei"].values_by_label + for feature, axes in schema.feature_value_indexes( + table, queries, objects, index_type=MeasurementFeatureValueIndex + ) + } + assert default == {"DNA": {1: 1.0, 2: 2.0, 3: 4.0}, "Memb": {5: 5.0}} + qualified = { + feature: axes[None]["Nuclei"].values_by_label + for feature, axes in schema.feature_value_indexes( + table, + queries, + objects, + index_type=MeasurementFeatureValueIndex, + measurement_value_qualifier=RelateObjectsModule.aggregate_child_measurement_value_is_qualified, + ) + } + assert qualified["DNA"][1] == 1.0 + assert np.isnan(qualified["DNA"][2]) + assert qualified["DNA"][3] == 4.0 + assert qualified["Memb"] == {5: 5.0} + + +def test_shared_rows_keep_table_subject_and_result_constructor_authorities() -> None: + import pytest + from openhcs.core.measurement_feature_queries import ( + MeasurementTableObjectFeatureSemantics, + ) + + rows = MeasurementSparseColumnarRows( + columns={ + "object_name": ("Cells", "Nuclei"), + "object_label": [1, 2], + "cell_key": [1, 2], + "nucleus_key": (10, 20), + "area": (2.0, 3.0), + }, + fields=( + FieldSpec("object_name", str), + FieldSpec("object_label", int), + FieldSpec("cell_key", int), + FieldSpec("nucleus_key", int), + FieldSpec("area", float), + ), + ) + fixed = MeasurementTable( + name="Fixed", + rows=rows, + subject=MeasurementSubject( + MeasurementScope.OBJECT, "Cells", id_field="cell_key" + ), + ) + nuclei = MeasurementTable( + name="Nuclei", + rows=rows, + subject=MeasurementSubject( + MeasurementScope.OBJECT, "Nuclei", id_field="nucleus_key" + ), + ) + mixed = MeasurementTable( + name="Mixed", + rows=rows, + subject=MeasurementSubject( + MeasurementScope.ARTIFACT, "Mixed", id_field="cell_key" + ), + ) + first = MeasurementTableObjectFeatureSemantics.from_table(fixed) + second = MeasurementTableObjectFeatureSemantics.from_table(nuclei) + combined = MeasurementTableObjectFeatureSemantics.from_table(mixed) + assert first.object_names == ("Cells",) + assert second.object_names == ("Nuclei",) + assert combined.object_names == ("Cells", "Nuclei") + assert ( + "cell_key" not in first.feature_names and "nucleus_key" in first.feature_names + ) + assert ( + "nucleus_key" not in second.feature_names and "cell_key" in second.feature_names + ) + + class PositiveLabelIndex(MeasurementFeatureValueIndex): + def __init__(self, values_by_label=None, positional_values=None): + super().__init__(values_by_label or {}, positional_values or []) + if any(label <= 0 for label in self.values_by_label): + raise ValueError("Positive label index requires positive labels") + + query = MeasurementFeatureQuery("area", object_name="Nuclei") + indexed = PositiveLabelIndex.from_columnar_table_by_object( + mixed, query, {"Nuclei": "Nuclei"} + ) + assert isinstance(indexed["Nuclei"], PositiveLabelIndex) + assert indexed["Nuclei"].values_by_label == {2: 3.0} + rows.columns["object_label"][1] = -2 + with pytest.raises(ValueError, match="requires positive labels"): + PositiveLabelIndex.from_columnar_table_by_object( + mixed, query, {"Nuclei": "Nuclei"} + ) diff --git a/tests/unit/test_runtime_artifact_queries.py b/tests/unit/test_runtime_artifact_queries.py index 78d21dc473..bd38e7164e 100644 --- a/tests/unit/test_runtime_artifact_queries.py +++ b/tests/unit/test_runtime_artifact_queries.py @@ -35,6 +35,7 @@ runtime_relationship, ) from openhcs.core.measurement_feature_queries import ( + ColumnarMeasurementTableSchema, MeasurementAxisValueProjection, MeasurementFeatureQuery, MeasurementFeatureValueIndex, @@ -961,40 +962,23 @@ def test_batch_label_slice_measurement_lookup_scans_each_axis_once( np = pytest.importorskip("numpy") MeasurementLabelSliceFeatureBatchQueryCache.process_cache().entries.clear() MeasurementObjectFeatureAxisBatchQueryCache.process_cache().entries.clear() - table_scans: list[tuple[int, int | None, tuple[str, ...]]] = [] - original_table_value_indexes = ( - MeasurementObjectFeatureVectorBatchQuery.table_value_indexes - ) - - def counted_table_value_indexes( - query: MeasurementObjectFeatureVectorBatchQuery, - table: MeasurementTable, - table_query: MeasurementFeatureQuery, - table_object_names: tuple[str, ...], - query_objects_by_requested_object: Mapping[str, str | None], - *, - projection: MeasurementAxisValueProjection | None = None, - ) -> dict[str, MeasurementFeatureValueIndex]: + table_scans: list[tuple[int, tuple[int, ...], tuple[str, ...]]] = [] + original_feature_indexes = ColumnarMeasurementTableSchema.feature_value_indexes + + def counted_feature_indexes(schema, table, queries, objects_by_feature, **kwargs): table_scans.append( ( id(table), - None if projection is None else projection.value, - table_object_names, + tuple(kwargs["row_masks"]), + tuple(next(iter(objects_by_feature.values()))), ) ) - return original_table_value_indexes( - query, - table, - table_query, - table_object_names, - query_objects_by_requested_object, - projection=projection, + yield from original_feature_indexes( + schema, table, queries, objects_by_feature, **kwargs ) monkeypatch.setattr( - MeasurementObjectFeatureVectorBatchQuery, - "table_value_indexes", - counted_table_value_indexes, + ColumnarMeasurementTableSchema, "feature_value_indexes", counted_feature_indexes ) cell_labels = object_labels( np.array( @@ -1081,9 +1065,8 @@ def counted_table_value_indexes( [1.5], [1.9], ) - assert [(table_id, axis) for table_id, axis, _ in table_scans] == [ - (id(table), 0), - (id(table), 1), + assert [(table_id, axes) for table_id, axes, _ in table_scans] == [ + (id(table), (0, 1)), ] assert all( set(object_names) == {"Cells", "Nuclei"} for _, _, object_names in table_scans From 7e737910a6c2697ea041779eeef33a7bb0b26fc4 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 22:57:20 -0400 Subject: [PATCH 202/235] perf: retain admitted wire declarations through compiled runs Refs #384 and #450. Compile, execute, measured completion and agent finalization consume the same prepared request bytes. Retain observed terminal records instead of polling both completed jobs again; requires ZMQRuntime PR17. --- benchmark/control_service.py | 6 +- benchmark/openhcs_measured_run.py | 68 ++++------ .../services/execution_session_service.py | 106 +++++----------- openhcs/runtime/zmq_execution_client.py | 118 ++++++++++++------ tests/unit/agent/test_agent_services.py | 29 +++-- tests/unit/test_benchmark_control.py | 4 +- tests/unit/test_openhcs_adapter.py | 25 ++-- tests/unit/test_openhcs_measured_run.py | 38 ++++-- tests/unit/test_zmq_compiled_pipeline_run.py | 68 +++++++++- tests/unit/test_zmq_submission_timeout.py | 6 +- 10 files changed, 265 insertions(+), 203 deletions(-) diff --git a/benchmark/control_service.py b/benchmark/control_service.py index b81bc78fbe..525f802294 100644 --- a/benchmark/control_service.py +++ b/benchmark/control_service.py @@ -64,7 +64,7 @@ def finalize_measured_run( request.job_id ) observation_path = ZMQAuxiliaryExecutionParams.from_transport( - completed.submission.config_params + completed.request.config_params ).runtime_observation_export_path if observation_path is None: raise ValueError("Completed job has no runtime observation export.") @@ -91,12 +91,12 @@ def finalize_measured_run( seconds=execution_seconds, ) return retain_measured_openhcs_completion( - submission=completed.submission, + request=completed.request, execution_id=record.execution_id, results_summary=record.results_summary, endpoint_provenance=measured_endpoint_provenance(completed.endpoint), phase_timing=phase_timing, - compile_artifact_id=completed.submission.compile_artifact_id, + compile_artifact_id=completed.request.compile_control.compile_artifact_id, ).receipt def discover_cases( diff --git a/benchmark/openhcs_measured_run.py b/benchmark/openhcs_measured_run.py index 46db6511a7..8585e9f564 100644 --- a/benchmark/openhcs_measured_run.py +++ b/benchmark/openhcs_measured_run.py @@ -16,7 +16,7 @@ from typing import Any from zmqruntime import DataControlPortPairAuthority -from zmqruntime.messages import ExecutionStatusSnapshot, PongResponse +from zmqruntime.messages import PongResponse from benchmark.contracts.measured_run_receipt import ( MEASURED_PIPELINE_RUN_RECEIPT_SCHEMA_VERSION, @@ -29,8 +29,6 @@ ) from benchmark.contracts.tool_adapter import ToolExecutionError from benchmark.file_digest import sha256_file -from openhcs.core.config import GlobalPipelineConfig -from openhcs.core.config_document import ConfigDocumentAuthority from openhcs.core.execution_state import ExecutionOutputPlateSummary from openhcs.core.runtime_execution_validation import ( RuntimeArtifactExecutionObservation, @@ -38,6 +36,7 @@ from openhcs.runtime.zmq_application import OPENHCS_ENDPOINT_APPLICATION from openhcs.runtime.zmq_config import OPENHCS_ZMQ_CONFIG from openhcs.runtime.zmq_execution_client import ( + ZMQExecutionRequestBuilder, OpenHCSExecutionSubmission, ZMQExecutionClient, ZMQPipelineRunPhase, @@ -230,25 +229,6 @@ def progress_description(self) -> str: return f"{self.last_progress_phase}/{self.last_progress_status}" -def _completed_server_job_seconds( - client: ZMQExecutionClient, execution_id: str -) -> float: - """Read an ordinary completed job's authoritative server time bounds.""" - - snapshot = ExecutionStatusSnapshot.from_dict(client.poll_status(execution_id)) - record = snapshot.execution - if record is None: - raise ToolExecutionError( - f"Completed OpenHCS job {execution_id!r} has no valid server time bounds." - ) - try: - return completed_server_execution_seconds( - record, expected_execution_id=execution_id - ) - except ValueError as exc: - raise ToolExecutionError(str(exc)) from exc - - def _require_measured_submission( submission: OpenHCSExecutionSubmission, expected_axis_count: int | None, @@ -342,18 +322,25 @@ def execute_measured_openhcs_pipeline_on_client( ) except RuntimeError as exc: raise ToolExecutionError(str(exc)) from exc - phase_timing.record( - BenchmarkPhase.SERVER_COMPILATION_JOB, - seconds=_completed_server_job_seconds(client, run.compile_artifact_id), - ) - phase_timing.record( - BenchmarkPhase.SERVER_PIPELINE_JOB, - seconds=_completed_server_job_seconds(client, run.execution_id), - ) + try: + phase_timing.record( + BenchmarkPhase.SERVER_COMPILATION_JOB, + seconds=completed_server_execution_seconds( + run.compile_record, expected_execution_id=run.compile_artifact_id + ), + ) + phase_timing.record( + BenchmarkPhase.SERVER_PIPELINE_JOB, + seconds=completed_server_execution_seconds( + run.execution_record, expected_execution_id=run.execution_id + ), + ) + except ValueError as exc: + raise ToolExecutionError(str(exc)) from exc timing_observer.record_phase_timings(phase_timing) return ( retain_measured_openhcs_completion( - submission=submission, + request=run.request, execution_id=run.execution_id, results_summary=run.results_summary, endpoint_provenance=endpoint_provenance, @@ -361,13 +348,13 @@ def execute_measured_openhcs_pipeline_on_client( compile_artifact_id=run.compile_artifact_id, expected_axis_count=expected_axis_count, ), - submission.pipeline_code(), + run.request.pipeline_code, ) def retain_measured_openhcs_completion( *, - submission: OpenHCSExecutionSubmission, + request: ZMQExecutionRequestBuilder, execution_id: str, results_summary: Mapping[str, Any], endpoint_provenance: MeasuredEndpointProvenance, @@ -380,7 +367,7 @@ def retain_measured_openhcs_completion( if expected_axis_count is not None and expected_axis_count < 1: raise ValueError("Expected axis count must be positive when declared.") auxiliary_params = ZMQAuxiliaryExecutionParams.from_transport( - submission.config_params + request.config_params ) observation_export_path = auxiliary_params.runtime_observation_export_path if observation_export_path is None: @@ -443,11 +430,8 @@ def retain_measured_openhcs_completion( ) artifact_root = observation_export_path.parent output_roots = tuple(Path(root) for root in observation_export.output_roots) - pipeline_source = submission.pipeline_code() - global_config_source = ConfigDocumentAuthority.render( - submission.global_pipeline_config, - expected_config_type=GlobalPipelineConfig, - ) + pipeline_source = request.pipeline_code + global_config_source = request.global_config_code results_summary_path = MeasuredPipelineRunArtifact.RESULTS_SUMMARY.path_in( artifact_root ) @@ -465,9 +449,9 @@ def retain_measured_openhcs_completion( schema_version=MEASURED_PIPELINE_RUN_RECEIPT_SCHEMA_VERSION, run_id=phase_timing.run_id, pipeline_name=phase_timing.pipeline_name, - plate_id=submission.plate_id, - execution_plate_id=submission.execution_plate_id, - selected_pipeline_path=submission.selected_pipeline_path, + plate_id=request.identity.plate_id, + execution_plate_id=request.identity.execution_plate_id, + selected_pipeline_path=request.identity.selected_pipeline_path, execution_id=execution_id, pipeline_source_sha256=hashlib.sha256( pipeline_source.encode("utf-8") diff --git a/openhcs/agent/services/execution_session_service.py b/openhcs/agent/services/execution_session_service.py index df91176dd9..d4043a1557 100644 --- a/openhcs/agent/services/execution_session_service.py +++ b/openhcs/agent/services/execution_session_service.py @@ -5,12 +5,10 @@ import logging import time from abc import ABC, abstractmethod -from collections.abc import Callable from dataclasses import dataclass, replace from enum import Enum from itertools import count from pathlib import Path -from typing import Self from zmqruntime.execution import ExecutionProgressObservation from zmqruntime.startup import EndpointStartupPhase, EndpointStartupStatus @@ -76,6 +74,7 @@ from openhcs.microscopes.exceptions import MicroscopePixelSizeUnavailableError from openhcs.runtime.zmq_config import OPENHCS_ZMQ_CONFIG, OpenHCSZMQConfig from openhcs.runtime.zmq_execution_client import ( + ZMQExecutionRequestBuilder, ExecutionSubmissionPreparationTimeoutError, OpenHCSExecutionSubmission, ZMQExecutionClient, @@ -252,18 +251,9 @@ def endpoint_handshake(self) -> PongResponse | None: return None @abstractmethod - def submit_compile( + def submit_prepared_pipeline( self, - submission: OpenHCSExecutionSubmission, - *, - timeout_ms: int = OPENHCS_ZMQ_CONFIG.execution_submission_timeout_ms, - ) -> JsonObject: - raise NotImplementedError - - @abstractmethod - def submit_pipeline( - self, - submission: OpenHCSExecutionSubmission, + request: ZMQExecutionRequestBuilder, *, timeout_ms: int = OPENHCS_ZMQ_CONFIG.execution_submission_timeout_ms, ) -> JsonObject: @@ -303,48 +293,17 @@ def disconnect(self) -> None: raise NotImplementedError -ExecutionSubmitter = Callable[ - [ExecutionClientABC, OpenHCSExecutionSubmission, int], - JsonObject, -] - - class ExecutionJobKind(Enum): - COMPILE = ( - "compile", - lambda client, submission, timeout_ms: client.submit_compile( - submission, - timeout_ms=timeout_ms, - ), - ) - EXECUTE = ( - "execute", - lambda client, submission, timeout_ms: client.submit_pipeline( - submission, - timeout_ms=timeout_ms, - ), - ) - - def __new__( - cls, - value: str, - submitter: ExecutionSubmitter, - ) -> Self: - member = object.__new__(cls) - member._value_ = value - member._submitter = submitter - return member - - def submit( - self, - client: ExecutionClientABC, - submission: OpenHCSExecutionSubmission, - *, - timeout_ms: int, - ) -> JsonObject: - """Submit through the operation owned by this job kind.""" - - return self._submitter(client, submission, timeout_ms) + COMPILE = "compile" + EXECUTE = "execute" + + def prepare_request( + self, submission: OpenHCSExecutionSubmission + ) -> ZMQExecutionRequestBuilder: + """Admit the exact wire declaration selected by this job kind.""" + return ZMQExecutionRequestBuilder.from_task( + submission.compile_request() if self is self.COMPILE else submission + ) class ExecutionClientFactoryABC(ABC): @@ -363,21 +322,13 @@ class ZMQExecutionClientAdapter(ExecutionClientABC): def endpoint_handshake(self) -> PongResponse | None: return self.client.connected_endpoint - def submit_compile( - self, - submission: OpenHCSExecutionSubmission, - *, - timeout_ms: int = OPENHCS_ZMQ_CONFIG.execution_submission_timeout_ms, - ) -> JsonObject: - return dict(self.client.submit_compile(submission, timeout_ms=timeout_ms)) - - def submit_pipeline( + def submit_prepared_pipeline( self, - submission: OpenHCSExecutionSubmission, + request: ZMQExecutionRequestBuilder, *, timeout_ms: int = OPENHCS_ZMQ_CONFIG.execution_submission_timeout_ms, ) -> JsonObject: - return dict(self.client.submit_pipeline(submission, timeout_ms=timeout_ms)) + return dict(self.client.submit_prepared_pipeline(request, timeout_ms=timeout_ms)) def get_status( self, @@ -569,7 +520,7 @@ class ExecutionJobRecord: ref: ExecutionJobRef response: JsonObject client: ExecutionClientABC | None - submission: OpenHCSExecutionSubmission | None = None + request: ZMQExecutionRequestBuilder | None = None endpoint: PongResponse | None = None def status(self, response: JsonObject | None = None) -> ExecutionJobStatus: @@ -664,7 +615,7 @@ def register( response: JsonObject, client: ExecutionClientABC | None, *, - submission: OpenHCSExecutionSubmission | None = None, + request: ZMQExecutionRequestBuilder | None = None, endpoint: PongResponse | None = None, ) -> ExecutionJobRef: job_id = f"job-{next(self._counter)}" @@ -681,7 +632,7 @@ def register( ref=ref, response=response, client=client, - submission=submission, + request=request, endpoint=endpoint, ) return ref @@ -712,7 +663,7 @@ class ExecutionJobSubmission: client: ExecutionClientABC response: JsonObject - submission: OpenHCSExecutionSubmission + request: ZMQExecutionRequestBuilder endpoint: PongResponse | None @@ -720,7 +671,7 @@ class ExecutionJobSubmission: class CompletedPipelineExecution: """The exact ordinary submission and server-owned successful result.""" - submission: OpenHCSExecutionSubmission + request: ZMQExecutionRequestBuilder record: ExecutionRecord endpoint: PongResponse | None @@ -739,10 +690,11 @@ def submit( timeout_ms: int = OPENHCS_ZMQ_CONFIG.execution_submission_timeout_ms, ) -> ExecutionJobSubmission: client = self.factory.create_client(record.session.connection) - execution_request = record.submission(compile_artifact_id, auxiliary_params) + execution_request = kind.prepare_request( + record.submission(compile_artifact_id, auxiliary_params) + ) try: - response = kind.submit( - client, + response = client.submit_prepared_pipeline( execution_request, timeout_ms=timeout_ms, ) @@ -763,7 +715,7 @@ def submit( return ExecutionJobSubmission( client=client, response=dict(response), - submission=execution_request, + request=execution_request, endpoint=endpoint, ) @@ -1184,12 +1136,12 @@ def require_completed_pipeline_execution( raise RuntimeError( f"Pipeline execution {job_id} has no successful server result." ) - if job.submission is None: + if job.request is None: raise RuntimeError( f"Pipeline execution {job_id} has no retained submission." ) return CompletedPipelineExecution( - submission=job.submission, + request=job.request, record=snapshot.execution, endpoint=job.endpoint, ) @@ -1290,7 +1242,7 @@ def _submit_job( kind, submission.response, client=submission.client, - submission=submission.submission, + request=submission.request, endpoint=submission.endpoint, ) if wait and ref.server_execution_id is not None: diff --git a/openhcs/runtime/zmq_execution_client.py b/openhcs/runtime/zmq_execution_client.py index 9965ec8672..590d301fc8 100644 --- a/openhcs/runtime/zmq_execution_client.py +++ b/openhcs/runtime/zmq_execution_client.py @@ -9,6 +9,7 @@ import time from collections.abc import Callable, Mapping, Sequence from concurrent.futures import CancelledError +from copy import deepcopy from contextlib import AbstractContextManager, nullcontext from dataclasses import dataclass, replace from enum import Enum @@ -39,6 +40,7 @@ ControlMessageType, ControlRequestHeader, EndpointApplicationCompatibilityError, + ExecutionRecord, MessageFields, PongResponse, ServerRole, @@ -341,9 +343,6 @@ def compile_artifact_id(self) -> str | None: def compile_only(self) -> bool: return self.compile_control.compile_only - def to_task(self) -> "OpenHCSExecutionSubmission": - return self - def with_config_params( self, config_params: ZMQParams ) -> "OpenHCSExecutionSubmission": @@ -396,9 +395,6 @@ def with_compile_artifact_id( def pipeline_code(self) -> str: return PipelineDocumentAuthority.execution_source(self.pipeline_document) - def step_count_label(self) -> str: - return str(len(self.pipeline_steps)) - class ZMQPipelineRunPhase(Enum): """Source-owned boundaries for one compiled pipeline execution.""" @@ -415,17 +411,16 @@ class ZMQCompiledPipelineRun: compile_artifact_id: str execution_id: str - completion_response: Mapping[str, Any] + request: ZMQExecutionRequestBuilder + compile_record: ExecutionRecord + execution_record: ExecutionRecord completion_observed_at: float @property def results_summary(self) -> Mapping[str, Any]: """Project the completed wait response's ordinary result summary.""" - value = self.completion_response.get("results") - if not value: - value = self.completion_response.get(MessageFields.RESULTS_SUMMARY) - return dict(value) if isinstance(value, Mapping) else {} + return dict(self.execution_record.results_summary or {}) @property def output_plate(self) -> ExecutionOutputPlateSummary: @@ -448,8 +443,9 @@ def phase_scope(phase: ZMQPipelineRunPhase) -> AbstractContextManager[None]: return nullcontext() if phase_context is None else phase_context(phase) with phase_scope(ZMQPipelineRunPhase.SUBMIT_COMPILE): + request = ZMQExecutionRequestBuilder.from_task(submission) compile_response = ExecutionSubmissionResponse.from_wire( - client.submit_compile(submission) + client.submit_prepared_pipeline(request.compile_request()) ) if not compile_response.accepted: raise RuntimeError( @@ -463,14 +459,16 @@ def phase_scope(phase: ZMQPipelineRunPhase) -> AbstractContextManager[None]: compile_artifact_id, poll_interval=_COMPILED_PIPELINE_POLL_INTERVAL_SECONDS, ) - ExecutionWaitResult.from_wire(compile_wait_response).require_complete( - "OpenHCS ZMQ compilation failed" + compile_record = ExecutionWaitResult.from_wire( + compile_wait_response + ).require_completed_execution( + "OpenHCS ZMQ compilation failed", expected_execution_id=compile_artifact_id ) - execution_submission = submission.with_compile_artifact_id(compile_artifact_id) + execution_request = request.with_compile_artifact_id(compile_artifact_id) with phase_scope(ZMQPipelineRunPhase.SUBMIT_EXECUTION): execution_response = ExecutionSubmissionResponse.from_wire( - client.submit_pipeline(execution_submission) + client.submit_prepared_pipeline(execution_request) ) if not execution_response.accepted: raise RuntimeError( @@ -485,13 +483,17 @@ def phase_scope(phase: ZMQPipelineRunPhase) -> AbstractContextManager[None]: poll_interval=_COMPILED_PIPELINE_POLL_INTERVAL_SECONDS, ) completion_observed_at = time.time() - ExecutionWaitResult.from_wire(completion_response).require_complete( - "OpenHCS ZMQ execution failed" + execution_record = ExecutionWaitResult.from_wire( + completion_response + ).require_completed_execution( + "OpenHCS ZMQ execution failed", expected_execution_id=execution_id ) return ZMQCompiledPipelineRun( compile_artifact_id=compile_artifact_id, execution_id=execution_id, - completion_response=completion_response, + request=execution_request, + compile_record=compile_record, + execution_record=execution_record, completion_observed_at=completion_observed_at, ) @@ -507,9 +509,10 @@ def _pycodify_config_source( @dataclass(frozen=True, slots=True) class ZMQExecutionRequestBuilder: - task: OpenHCSExecutionSubmission + identity: ZMQExecutionIdentity + compile_control: ZMQExecutionCompileControl pipeline_code: str - config_projection: "ZMQConfigProjection" + config_projection: ZMQConfigProjection @classmethod def from_task( @@ -517,18 +520,40 @@ def from_task( task: OpenHCSExecutionSubmission, ) -> "ZMQExecutionRequestBuilder": return cls( - task=task, + identity=task.identity, + compile_control=task.compile_control, pipeline_code=task.pipeline_code(), config_projection=ZMQConfigProjection.from_task(task), ) + def compile_request(self) -> ZMQExecutionRequestBuilder: + return replace(self, compile_control=self.compile_control.as_compile_request()) + + def with_compile_artifact_id(self, artifact_id: str) -> ZMQExecutionRequestBuilder: + return replace( + self, + compile_control=self.compile_control.as_execution_request(artifact_id), + ) + + @property + def global_config_code(self) -> str: + fields = self.config_projection.source_fields + if fields is None or fields.config_code is None: + raise RuntimeError("Prepared execution request has no global config source") + return fields.config_code + + @property + def config_params(self) -> ZMQParams | None: + boundary = self.config_projection.params_boundary + return None if boundary is None else boundary.params + @property def request_payload(self) -> ZMQExecutionRequestPayload: return ZMQExecutionRequestPayload( - identity=self.task.identity, + identity=self.identity, pipeline_code=self.pipeline_code, config_transport=self.config_projection.signature_transport(), - compile_control=self.task.compile_control, + compile_control=self.compile_control, ) def request(self) -> "ZMQRequest": @@ -536,8 +561,8 @@ def request(self) -> "ZMQRequest": ( (MessageFields.TYPE, ControlMessageType.EXECUTE.value), (MessageFields.PIPELINE_CODE, self.pipeline_code), - *self.task.identity.request_items(), - *self.task.compile_control.request_items(), + *self.identity.request_items(), + *self.compile_control.request_items(), *self.config_projection.request_items(), ) ) @@ -623,7 +648,7 @@ def from_task( config_source = _pycodify_config_source(task.global_pipeline_config) return cls( params_boundary=( - task.config_boundary + ZMQConfigParamsBoundary(deepcopy(task.config_boundary.params)) if task.config_boundary.params is not None else None ), @@ -1026,15 +1051,18 @@ def serialize_task( task: OpenHCSExecutionSubmission, config=None, ) -> dict[str, ZMQValue]: - request_builder = ZMQExecutionRequestBuilder.from_task(task) + return self.serialize_prepared_request(ZMQExecutionRequestBuilder.from_task(task)) + + def serialize_prepared_request( + self, request_builder: ZMQExecutionRequestBuilder + ) -> dict[str, ZMQValue]: request = request_builder.request() request_payload = request_builder.request_payload logger.info( - "Serialize task: plate=%s compile_only=%s artifact_id=%s step_count=%s pipeline_sha=%s config_sha=%s", - task.plate_id, - task.compile_only, - task.compile_artifact_id, - task.step_count_label(), + "Serialize task: plate=%s compile_only=%s artifact_id=%s pipeline_sha=%s config_sha=%s", + request_builder.identity.plate_id, + request_builder.compile_control.compile_only, + request_builder.compile_control.compile_artifact_id, request_payload.pipeline_sha, request_builder.config_projection.config_sha, ) @@ -1047,7 +1075,19 @@ def submit_pipeline( timeout_ms: int | None = None, ): return self._submit_submission( - submission.to_task(), + lambda: self.serialize_task(submission), + timeout_ms=self._submission_timeout_ms(timeout_ms), + ) + + def submit_prepared_pipeline( + self, + request: ZMQExecutionRequestBuilder, + *, + timeout_ms: int | None = None, + ): + """Submit the exact declaration already admitted for a compiled run.""" + return self._submit_submission( + lambda: self.serialize_prepared_request(request), timeout_ms=self._submission_timeout_ms(timeout_ms), ) @@ -1062,7 +1102,9 @@ def submit_debug_pipeline( submission.config_params ).with_updates(debug_config.to_config_params()) return self._submit_submission( - submission.with_config_params(config_params_boundary.params).to_task(), + lambda: self.serialize_task( + submission.with_config_params(config_params_boundary.params) + ), timeout_ms=self._submission_timeout_ms(timeout_ms), ) @@ -1073,7 +1115,7 @@ def submit_compile( timeout_ms: int | None = None, ): return self._submit_submission( - submission.compile_request().to_task(), + lambda: self.serialize_task(submission.compile_request()), timeout_ms=self._submission_timeout_ms(timeout_ms), ) @@ -1129,7 +1171,7 @@ def _submission_timeout_ms(self, timeout_ms: int | None) -> int: def _submit_submission( self, - submission: OpenHCSExecutionSubmission, + build_request: Callable[[], dict[str, ZMQValue]], *, timeout_ms: int, ): @@ -1150,7 +1192,7 @@ def _submit_submission( self._ensure_progress_subscription( timeout_ms=deadline.remaining_milliseconds() ) - request = self.serialize_task(submission, None) + request = build_request() if MessageFields.TYPE not in request: request[MessageFields.TYPE] = ControlMessageType.EXECUTE.value request_timeout_ms = deadline.remaining_milliseconds() diff --git a/tests/unit/agent/test_agent_services.py b/tests/unit/agent/test_agent_services.py index 9dbec7888e..1662ca9b12 100644 --- a/tests/unit/agent/test_agent_services.py +++ b/tests/unit/agent/test_agent_services.py @@ -324,6 +324,11 @@ def __init__(self) -> None: def endpoint_handshake(self): return None + def submit_prepared_pipeline(self, request, *, timeout_ms=OPENHCS_ZMQ_CONFIG.execution_submission_timeout_ms): + if request.compile_control.compile_only: + return self.submit_compile(request, timeout_ms=timeout_ms) + return self.submit_pipeline(request, timeout_ms=timeout_ms) + def submit_compile( self, submission, @@ -3135,13 +3140,13 @@ def test_execution_session_service_submits_compile_and_execution_jobs( ("compile", OPENHCS_ZMQ_CONFIG.execution_submission_timeout_ms), ("execute", OPENHCS_ZMQ_CONFIG.execution_submission_timeout_ms), ] - assert fake_client.compile_submissions[0].plate_id == str(tmp_path.resolve()) + assert fake_client.compile_submissions[0].identity.plate_id == str(tmp_path.resolve()) assert ( session.pipeline_config_id == pipeline_service.get_pipeline(pipeline_ref).pipeline_config_id ) assert ( - fake_client.compile_submissions[0].pipeline_document.pipeline_config + PipelineDocumentAuthority.from_source(fake_client.compile_submissions[0].pipeline_code).pipeline_config == pipeline_service.to_pipeline_document(pipeline_ref).pipeline_config ) assert fake_client.status_requests[0] == ( @@ -3155,11 +3160,11 @@ def test_execution_session_service_submits_compile_and_execution_jobs( assert len(fake_client.status_requests) == 1 assert fake_client.disconnect_count == 1 assert ( - fake_client.execution_submissions[0].compile_artifact_id + fake_client.execution_submissions[0].compile_control.compile_artifact_id == _ExecutionTestId.COMPILE ) - assert type(fake_client.compile_submissions[0].pipeline_steps) is list - assert len(fake_client.compile_submissions[0].pipeline_steps) == 1 + assert type(PipelineDocumentAuthority.from_source(fake_client.compile_submissions[0].pipeline_code).pipeline_steps) is list + assert len(PipelineDocumentAuthority.from_source(fake_client.compile_submissions[0].pipeline_code).pipeline_steps) == 1 assert not hasattr(fake_client.compile_submissions[0], "submission_pipeline") @@ -3378,11 +3383,9 @@ def test_execution_session_service_preserves_pipeline_source_document( execution_service.submit_compile(session_ref.session_id) submission = fake_client.compile_submissions[0] - assert submission.pipeline_document.original_source == pipeline_source - assert submission.pipeline_code() == pipeline_source - assert submission.pipeline_steps == [] - assert submission.pipeline_config == pipeline_config - assert submission.pipeline_code() == pipeline_source + assert submission.pipeline_code == pipeline_source + assert PipelineDocumentAuthority.from_source(submission.pipeline_code).pipeline_steps == [] + assert PipelineDocumentAuthority.from_source(submission.pipeline_code).pipeline_config == pipeline_config assert not hasattr(submission, "pipeline_steps_boundary") @@ -3895,8 +3898,8 @@ def test_pipeline_source_session_uses_prepared_execution_plate(tmp_path: Path): assert session.selected_pipeline_path is None job = service.submit_execution(session_ref.session_id) assert job.server_execution_id == _ExecutionTestId.EXECUTE - assert fake_client.execution_submissions[0].execution_plate_id == str(prepared) - assert fake_client.execution_submissions[0].selected_pipeline_path is None + assert fake_client.execution_submissions[0].identity.execution_plate_id == str(prepared) + assert fake_client.execution_submissions[0].identity.selected_pipeline_path is None with pytest.raises(AgentPathPolicyError, match="outside allowed roots"): service.create_session_from_pipeline_source_request( @@ -3951,7 +3954,7 @@ def get_status( completed = service.require_completed_pipeline_execution(job.job_id) - assert completed.submission is fake_client.execution_submissions[0] + assert completed.request is fake_client.execution_submissions[0] assert completed.record.start_time == 10.0 assert completed.record.end_time == 12.0 assert completed.record.results_summary == {"output_plate_root": str(tmp_path)} diff --git a/tests/unit/test_benchmark_control.py b/tests/unit/test_benchmark_control.py index 833e0e1434..1f392de25f 100644 --- a/tests/unit/test_benchmark_control.py +++ b/tests/unit/test_benchmark_control.py @@ -64,7 +64,7 @@ InstalledDistributionVersion, RuntimeEnvironmentSnapshot, ) -from openhcs.runtime.zmq_execution_client import OpenHCSExecutionSubmission +from openhcs.runtime.zmq_execution_client import OpenHCSExecutionSubmission, ZMQExecutionRequestBuilder from openhcs.runtime.zmq_execution_signature import ZMQAuxiliaryExecutionParams @@ -635,7 +635,7 @@ class CompletedJobService: def require_completed_pipeline_execution(self, job_id: str): assert job_id == "job-1" return CompletedPipelineExecution( - submission=submission, + request=ZMQExecutionRequestBuilder.from_task(submission), record=ExecutionRecord( execution_id="execution-1", plate_id=str(tmp_path), diff --git a/tests/unit/test_openhcs_adapter.py b/tests/unit/test_openhcs_adapter.py index 383c8d8fbe..146640cc5c 100644 --- a/tests/unit/test_openhcs_adapter.py +++ b/tests/unit/test_openhcs_adapter.py @@ -42,6 +42,7 @@ VFSConfig, WellFilterConfig, ) +from openhcs.core.config_document import ConfigDocumentAuthority from openhcs.core.pipeline_document import PipelineDocumentAuthority from openhcs.core.pipeline_document_fields import PipelineDocumentField from openhcs.core.runtime_execution_validation import ( @@ -270,6 +271,11 @@ def __exit__(self, exc_type, exc, traceback): def disconnect(self): return None + def submit_prepared_pipeline(self, request): + if request.compile_control.compile_only: + return self.submit_compile(request) + return self.submit_pipeline(request) + def submit_compile(self, submission): compile_submission = submission.compile_request() self.submitted.append(compile_submission) @@ -283,13 +289,14 @@ def submit_compile(self, submission): def submit_pipeline(self, submission): self.submitted.append(submission) - assert submission.compile_artifact_id == "compile-1" + assert submission.compile_control.compile_artifact_id == "compile-1" return {"status": "accepted", "execution_id": "exec-1"} def wait_for_completion(self, execution_id, poll_interval=0.5): self.waits.append(execution_id) if execution_id == "compile-1": return { + "execution": self.poll_status(execution_id)["execution"], "status": "complete", "execution_id": execution_id, "results": {"well_count": 1, "wells": ["A01"]}, @@ -320,6 +327,7 @@ def wait_for_completion(self, execution_id, poll_interval=0.5): execution_id=execution_id, ).write(observation_path) return { + "execution": self.poll_status(execution_id)["execution"], "status": "complete", "execution_id": execution_id, "results": {"output_plate_root": str(tmp_path)}, @@ -334,6 +342,7 @@ def poll_status(self, execution_id): "plate_id": "/tmp/execution_plate", "client_address": None, "status": "complete", + "results_summary": {"output_plate_root": str(tmp_path)}, "start_time": 10.0 if execution_id == "compile-1" else 13.0, "end_time": 12.0 if execution_id == "compile-1" else 17.0, }, @@ -375,23 +384,25 @@ def poll_status(self, execution_id): ) == execution.results_summary ) - assert source == FakeZMQExecutionClient.submitted[0].pipeline_code() + assert source == FakeZMQExecutionClient.submitted[0].pipeline_code assert ( - FakeZMQExecutionClient.submitted[0].pipeline_document - is FakeZMQExecutionClient.submitted[1].pipeline_document + FakeZMQExecutionClient.submitted[0].pipeline_code + == FakeZMQExecutionClient.submitted[1].pipeline_code ) assert all( - submission.global_pipeline_config is global_config + submission.global_config_code == ConfigDocumentAuthority.render( + global_config, expected_config_type=GlobalPipelineConfig + ) for submission in FakeZMQExecutionClient.submitted ) assert [ - submission.compile_only for submission in FakeZMQExecutionClient.submitted + submission.compile_control.compile_only for submission in FakeZMQExecutionClient.submitted ] == [ True, False, ] assert [ - submission.compile_artifact_id + submission.compile_control.compile_artifact_id for submission in FakeZMQExecutionClient.submitted ] == [None, "compile-1"] assert FakeZMQExecutionClient.waits == ["compile-1", "exec-1"] diff --git a/tests/unit/test_openhcs_measured_run.py b/tests/unit/test_openhcs_measured_run.py index d01a215d92..aa4c2a140a 100644 --- a/tests/unit/test_openhcs_measured_run.py +++ b/tests/unit/test_openhcs_measured_run.py @@ -24,7 +24,7 @@ from openhcs.core.orchestrator.execution_result import ExecutionResult from openhcs.core.pipeline_document import PipelineDocumentAuthority from openhcs.runtime.zmq_application import OPENHCS_ENDPOINT_APPLICATION -from openhcs.runtime.zmq_execution_client import OpenHCSExecutionSubmission +from openhcs.runtime.zmq_execution_client import OpenHCSExecutionSubmission, ZMQExecutionRequestBuilder from openhcs.runtime.zmq_execution_observation import ZMQRuntimeExecutionOutcomeExport from openhcs.runtime.zmq_execution_signature import ( ZMQAuxiliaryExecutionParams, @@ -71,13 +71,18 @@ def __exit__(self, exc_type, exc, traceback): def disconnect(self): self.disconnect_count += 1 + def submit_prepared_pipeline(self, request): + if request.compile_control.compile_only: + return self.submit_compile(request) + return self.submit_pipeline(request) + def submit_compile(self, submitted): - assert submitted.pipeline_document is submission.pipeline_document + assert submitted.pipeline_code == submission.pipeline_code() return {"status": "accepted", "execution_id": "compile-1"} def submit_pipeline(self, submitted): - assert submitted.pipeline_document is submission.pipeline_document - assert submitted.compile_artifact_id == "compile-1" + assert submitted.pipeline_code == submission.pipeline_code() + assert submitted.compile_control.compile_artifact_id == "compile-1" assert ( ZMQAuxiliaryExecutionParams.from_transport( submitted.config_params @@ -90,6 +95,7 @@ def wait_for_completion(self, execution_id, poll_interval=0.5): if execution_id == "execute-1": observation_path.touch() return { + "execution": self.poll_status(execution_id)["execution"], "status": "complete", "execution_id": execution_id, "results": {"output_plate_root": str(tmp_path)}, @@ -219,6 +225,11 @@ def __init__(self) -> None: def owned_server_process_is_alive(self) -> bool: return True + def submit_prepared_pipeline(self, request): + if request.compile_control.compile_only: + return self.submit_compile(request) + return self.submit_pipeline(request) + def submit_compile(self, submission): self.run_number += 1 observer({"phase": "compile", "status": "started", "timestamp": 10.0}) @@ -228,7 +239,7 @@ def submit_compile(self, submission): } def submit_pipeline(self, submission): - assert submission.compile_artifact_id == f"compile-{self.run_number}" + assert submission.compile_control.compile_artifact_id == f"compile-{self.run_number}" observer({"phase": "axis_started", "timestamp": 11.0}) return { "status": "accepted", @@ -242,6 +253,7 @@ def wait_for_completion(self, execution_id, poll_interval=0.5): observer({"phase": "axis_completed", "timestamp": 12.0}) (tmp_path / f"run-{self.run_number - 1}" / "observation.pkl").touch() return { + "execution": self.poll_status(execution_id)["execution"], "status": "complete", "execution_id": execution_id, "results": {"output_plate_root": str(tmp_path)}, @@ -384,7 +396,7 @@ def test_shared_evidence_writer_never_overwrites_existing_artifact( with pytest.raises(FileExistsError, match="Measured run evidence already exists"): measured_run.retain_measured_openhcs_completion( - submission=submission, + request=ZMQExecutionRequestBuilder.from_task(submission), execution_id="execution-1", results_summary={}, endpoint_provenance=endpoint, @@ -486,7 +498,7 @@ def test_outcome_only_run_uses_the_shared_receipt_finalizer(tmp_path: Path) -> N with pytest.raises(ToolExecutionError, match="execution identity does not match"): measured_run.retain_measured_openhcs_completion( - submission=submission, + request=ZMQExecutionRequestBuilder.from_task(submission), execution_id="stale-job", results_summary={"well_count": 1}, endpoint_provenance=endpoint, @@ -499,7 +511,7 @@ def test_outcome_only_run_uses_the_shared_receipt_finalizer(tmp_path: Path) -> N with pytest.raises(ToolExecutionError, match="Expected 2 execution axes"): measured_run.retain_measured_openhcs_completion( - submission=submission, + request=ZMQExecutionRequestBuilder.from_task(submission), execution_id="execution-1", results_summary={"well_count": 1}, endpoint_provenance=endpoint, @@ -520,7 +532,7 @@ def test_outcome_only_run_uses_the_shared_receipt_finalizer(tmp_path: Path) -> N ).write(observation_path) with pytest.raises(ToolExecutionError, match="no completed axes"): measured_run.retain_measured_openhcs_completion( - submission=submission, + request=ZMQExecutionRequestBuilder.from_task(submission), execution_id="execution-1", results_summary={"well_count": 0}, endpoint_provenance=endpoint, @@ -543,7 +555,7 @@ def test_outcome_only_run_uses_the_shared_receipt_finalizer(tmp_path: Path) -> N ).write(observation_path) with pytest.raises(ToolExecutionError, match="lacks compiled axis membership"): measured_run.retain_measured_openhcs_completion( - submission=submission, + request=ZMQExecutionRequestBuilder.from_task(submission), execution_id="execution-1", results_summary={"well_count": 1}, endpoint_provenance=endpoint, @@ -562,7 +574,7 @@ def test_outcome_only_run_uses_the_shared_receipt_finalizer(tmp_path: Path) -> N ).write(observation_path) completion = measured_run.retain_measured_openhcs_completion( - submission=submission, + request=ZMQExecutionRequestBuilder.from_task(submission), execution_id="execution-1", results_summary={"well_count": 1}, endpoint_provenance=endpoint, @@ -595,7 +607,7 @@ def test_outcome_only_run_uses_the_shared_receipt_finalizer(tmp_path: Path) -> N receipt_path.unlink() config_path.unlink() recovered = measured_run.retain_measured_openhcs_completion( - submission=submission, + request=ZMQExecutionRequestBuilder.from_task(submission), execution_id="execution-1", results_summary={"well_count": 1}, endpoint_provenance=endpoint, @@ -610,7 +622,7 @@ def test_outcome_only_run_uses_the_shared_receipt_finalizer(tmp_path: Path) -> N assert MeasuredPipelineRunReceipt.read(receipt_path) == recovered.receipt with pytest.raises(FileExistsError, match="Measured run evidence already exists"): measured_run.retain_measured_openhcs_completion( - submission=submission, + request=ZMQExecutionRequestBuilder.from_task(submission), execution_id="execution-1", results_summary={"well_count": 1}, endpoint_provenance=endpoint, diff --git a/tests/unit/test_zmq_compiled_pipeline_run.py b/tests/unit/test_zmq_compiled_pipeline_run.py index 476ccabb38..1572b6592d 100644 --- a/tests/unit/test_zmq_compiled_pipeline_run.py +++ b/tests/unit/test_zmq_compiled_pipeline_run.py @@ -3,6 +3,7 @@ from __future__ import annotations from contextlib import contextmanager +from dataclasses import replace from pathlib import Path import pytest @@ -13,6 +14,7 @@ from openhcs.runtime.zmq_execution_client import ( OpenHCSExecutionSubmission, ZMQExecutionClient, + ZMQExecutionRequestBuilder, ZMQPipelineRunPhase, run_compiled_pipeline, ) @@ -27,6 +29,11 @@ def __init__(self, *, compile_status="complete", execute_status="complete"): self.compile_submission = None self.execution_submission = None + def submit_prepared_pipeline(self, request): + if request.compile_control.compile_only: + return self.submit_compile(request) + return self.submit_pipeline(request) + def submit_compile(self, submission): self.events.append("submit_compile") self.compile_submission = submission @@ -47,6 +54,13 @@ def wait_for_completion(self, execution_id, poll_interval=0.5): else self.execute_status ), "execution_id": execution_id, + "execution": { + "execution_id": execution_id, + "plate_id": "/tmp/plate", + "status": self.compile_status if execution_id == "compile-1" else self.execute_status, + "start_time": 10.0, + "end_time": 12.0, + }, } @@ -83,7 +97,7 @@ def observe(phase): assert result.compile_artifact_id == "compile-1" assert result.execution_id == "execute-1" - assert result.completion_response["status"] == "complete" + assert result.execution_record.status == "complete" assert result.completion_observed_at > 0 assert client.events == [ "submit_compile", @@ -91,8 +105,8 @@ def observe(phase): "submit_execution", "wait:execute-1", ] - assert client.execution_submission.pipeline_document is submission.pipeline_document - assert client.execution_submission.compile_artifact_id == "compile-1" + assert client.execution_submission.pipeline_code == result.request.pipeline_code + assert client.execution_submission.compile_control.compile_artifact_id == "compile-1" assert observed_phases == [ (event, phase) for phase in ZMQPipelineRunPhase for event in ("start", "end") ] @@ -103,7 +117,7 @@ class ClientWithResults(FakeExecutionClient): def wait_for_completion(self, execution_id, poll_interval=0.5): response = super().wait_for_completion(execution_id, poll_interval) if execution_id == "execute-1": - response["results"] = {"output_plate_root": "/output/plate"} + response["execution"]["results_summary"] = {"output_plate_root": "/output/plate"} return response run = run_compiled_pipeline(ClientWithResults(), _submission()) @@ -117,7 +131,7 @@ class ClientWithLegacyResults(FakeExecutionClient): def wait_for_completion(self, execution_id, poll_interval=0.5): response = super().wait_for_completion(execution_id, poll_interval) if execution_id == "execute-1": - response["results_summary"] = {"output_plate_root": "/legacy/plate"} + response["execution"]["results_summary"] = {"output_plate_root": "/legacy/plate"} return response run = run_compiled_pipeline(ClientWithLegacyResults(), _submission()) @@ -213,3 +227,47 @@ def test_compiled_pipeline_run_does_not_report_failed_execution_as_complete(): run_compiled_pipeline(client, _submission()) assert client.events[-1] == "wait:execute-1" + + +def test_compiled_run_retains_its_admitted_wire_and_standalone_request_stays_fresh(monkeypatch): + submission = _submission() + rendered = [] + original_render = PipelineDocumentAuthority.render + + def render(document): + rendered.append(document) + return original_render(document) + + monkeypatch.setattr(PipelineDocumentAuthority, "render", render) + + class EditingClient(FakeExecutionClient): + def submit_compile(self, request): + response = super().submit_compile(request) + submission.pipeline_document = PipelineDocumentAuthority.from_values( + pipeline_config=PipelineConfig(num_workers=3), pipeline_steps=[] + ) + submission.global_pipeline_config = replace( + submission.global_pipeline_config, num_workers=3 + ) + return response + + client = EditingClient() + run = run_compiled_pipeline(client, submission) + + assert len(rendered) == 1 + assert run.request.pipeline_code == client.compile_submission.pipeline_code + assert run.request.global_config_code == client.compile_submission.global_config_code + fresh = ZMQExecutionRequestBuilder.from_task(submission) + assert fresh.pipeline_code != run.request.pipeline_code + assert fresh.global_config_code != run.request.global_config_code + + +def test_compiled_run_rejects_a_terminal_record_for_another_execution(): + class WrongRecordClient(FakeExecutionClient): + def wait_for_completion(self, execution_id, poll_interval=0.5): + response = super().wait_for_completion(execution_id, poll_interval) + response["execution"]["execution_id"] = "another-job" + return response + + with pytest.raises(RuntimeError, match="matching completed execution record"): + run_compiled_pipeline(WrongRecordClient(), _submission()) diff --git a/tests/unit/test_zmq_submission_timeout.py b/tests/unit/test_zmq_submission_timeout.py index 0b4c857c78..53dc24eaa3 100644 --- a/tests/unit/test_zmq_submission_timeout.py +++ b/tests/unit/test_zmq_submission_timeout.py @@ -66,7 +66,7 @@ def send_control_request(self, request, *, timeout_ms: int): client, ) - client._submit_submission(_Submission(), timeout_ms=15000) + client._submit_submission(lambda: client.serialize_task(_Submission().to_task()), timeout_ms=15000) assert [phase for phase, _timeout_ms in observed] == [ "progress", @@ -116,7 +116,7 @@ def connect( client.connect = MethodType(connect, client) try: - client._submit_submission(_Submission(), timeout_ms=15000) + client._submit_submission(lambda: client.serialize_task(_Submission().to_task()), timeout_ms=15000) except RuntimeError as error: assert str(error) == "Failed to connect to execution server" else: @@ -163,7 +163,7 @@ def send_control_request(self, request, *, timeout_ms: int): ExecutionSubmissionPreparationTimeoutError, match="no execute request was sent", ): - client._submit_submission(_Submission(), timeout_ms=10) + client._submit_submission(lambda: client.serialize_task(_Submission().to_task()), timeout_ms=10) assert observed == ["connect"] From 1b7983597fe2a4b95a9a500dc6ff9580f85a8025 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 23:17:45 -0400 Subject: [PATCH 203/235] Pin merged terminal execution record owner from ZMQRuntime PR17 Refs #384 and #450. The compiled-run consumer reuses the authoritative waiter record rather than polling completed jobs again. --- external/zmqruntime | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/zmqruntime b/external/zmqruntime index f7b80c871e..be67b58f2e 160000 --- a/external/zmqruntime +++ b/external/zmqruntime @@ -1 +1 @@ -Subproject commit f7b80c871eb4773757368bb21f108920bf461881 +Subproject commit be67b58f2e2fc31cba1b829842934df101ef8320 From ee015f1132b9a36d6541dd7ccee28661e254ae73 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 23:22:25 -0400 Subject: [PATCH 204/235] fix: keep measurement query reuse within its runtime epoch Remove five process-wide result and schema caches that retain label/table owners beyond RuntimeValueStore lifetime and return stale values across store replacement. Preserve store-owned CellProfiler query reuse and call-local correlated feature/axis batching. Fixes #582. Refs #394. --- openhcs/core/measurement_feature_queries.py | 524 ++------------------ openhcs/core/runtime_artifact_queries.py | 125 +---- tests/unit/test_runtime_artifact_queries.py | 119 ++++- 3 files changed, 147 insertions(+), 621 deletions(-) diff --git a/openhcs/core/measurement_feature_queries.py b/openhcs/core/measurement_feature_queries.py index 078c566bdc..f40cc4969b 100644 --- a/openhcs/core/measurement_feature_queries.py +++ b/openhcs/core/measurement_feature_queries.py @@ -29,12 +29,7 @@ measurement_rows, measurement_table_axis_values, ) -from openhcs.core.process_local_cache import ( - IdentityBoundProcessCache, - BoundedCache, - RegisteredProcessLocalBoundedCache, - identity_owner_tuples_match, -) +from openhcs.core.process_local_cache import BoundedCache from openhcs.core.registry_strategies import NominalTypeKeyedStrategyMixin from openhcs.core.runtime_identifier import normalize_runtime_identifier from openhcs.core.runtime_measurements import ( @@ -64,14 +59,6 @@ OptionalMeasurementValueIndexResult = MeasurementValueIndexResult | None MeasurementTablesByObject = Mapping[str, tuple[MeasurementTable, ...]] MeasurementValueIndexesByObject = Mapping[str, MeasurementValueIndexResult] -MeasurementObjectFeatureVectorBatchCacheValue = tuple[ - tuple[MeasurementTable, ...], - MeasurementValueIndexesByObject, -] -MeasurementObjectFeatureAxisBatchCacheValue = tuple[ - tuple[MeasurementTable, ...], - Mapping[int, MeasurementValueIndexesByObject], -] _DIAGNOSTIC_NONE = "" @@ -213,28 +200,9 @@ def value_is_present(self, value: object) -> bool: return measurement_axis_integer_value(value, self.axis) is not None -@dataclass(frozen=True, slots=True) -class MeasurementObjectFeatureVectorBatchCacheKey: - """Identity key for a batch object-feature value-index lookup.""" - - feature_name: str - dialect_identity: int - table_identities: tuple[int, ...] - - -@dataclass(frozen=True, slots=True) -class MeasurementObjectFeatureAxisBatchCacheKey: - """Identity key for a batch object-feature row-axis value-index lookup.""" - - feature_name: str - dialect_identity: int - row_axis: MeasurementRowAxisField - table_identities: tuple[int, ...] - - @dataclass(frozen=True, slots=True) class ColumnarMeasurementTableSchema: - """Cached semantic projection for nominal columnar measurement rows.""" + """Call-local semantic projection for nominal columnar measurement rows.""" columns: tuple[str, ...] normalized_columns: dict[str, str] @@ -248,10 +216,6 @@ class ColumnarMeasurementTableSchema: @classmethod def from_table(cls, table: MeasurementTable) -> "ColumnarMeasurementTableSchema": rows = table.rows - cached = ColumnarMeasurementTableSchemaCache.process_cache().get_bound(rows) - if cached is not None: - return cached - columns = tuple(str(column) for column in rows.columns) normalized_columns = { column: normalize_measurement_token(column) for column in columns @@ -267,18 +231,15 @@ def from_table(cls, table: MeasurementTable) -> "ColumnarMeasurementTableSchema" else None ) feature_name_values = cls._feature_name_values(rows, columns) - return ColumnarMeasurementTableSchemaCache.process_cache().put_bound( - rows, - cls( - columns=columns, - normalized_columns=normalized_columns, - feature_name_values=feature_name_values, - object_name_values=object_name_values, - source_image_name_values=source_image_name_values, - feature_masks_by_candidates={}, - object_masks_by_name={}, - source_masks_by_candidates={}, - ), + return cls( + columns=columns, + normalized_columns=normalized_columns, + feature_name_values=feature_name_values, + object_name_values=object_name_values, + source_image_name_values=source_image_name_values, + feature_masks_by_candidates={}, + object_masks_by_name={}, + source_masks_by_candidates={}, ) @staticmethod @@ -542,12 +503,6 @@ def object_id_field(self, declared_object_id_field: str | None) -> str | None: ) -class ColumnarMeasurementTableSchemaCache(IdentityBoundProcessCache): - """Process-local semantic cache keyed by a columnar row object identity.""" - - registry_key = "columnar_measurement_table_schema" - - @dataclass(frozen=True, slots=True) class MeasurementFeatureQuery: """Query for measurement rows carrying one semantic feature value.""" @@ -791,61 +746,26 @@ def value_indexes( self, measurement_tables_by_object: MeasurementTablesByObject, ) -> MeasurementValueIndexesByObject: - """Return value indexes keyed by object name for this feature.""" + """Return current object-feature indexes from one correlated table pass.""" object_names = self.normalized_object_names - cache_object_names = self.cache_object_names(measurement_tables_by_object) - cached_indexes = self.cached_matching_value_indexes( - measurement_tables_by_object, - ) - if cached_indexes is not None: - if all(object_name in cached_indexes for object_name in object_names): - return { - object_name: cached_indexes[object_name] - for object_name in object_names - } - indexes_by_object = dict(cached_indexes) - else: - indexes_by_object = {} - missing_cache_object_names = tuple( - object_name - for object_name in cache_object_names - if object_name not in indexes_by_object - ) - lookup = resolve_runtime_measurement_lookup_dialect( self.dialect ).feature_lookup(self.feature_name) query_objects_by_requested_object = { object_name: lookup.query_object_name(object_name) - for object_name in cache_object_names - } - pending_indexes_by_object = { - object_name: MeasurementFeatureValueIndex() - for object_name in missing_cache_object_names + for object_name in object_names } - objects_by_table_id: dict[int, list[str]] = {} - tables_by_id: dict[int, MeasurementTable] = {} - for table in self.feature_measurement_tables(measurement_tables_by_object): - table_id = id(table) - tables_by_id[table_id] = table - objects_by_table_id[table_id] = list( - object_name - for object_name in self.table_cache_object_names( - table, - object_names, - ) - if object_name in missing_cache_object_names - ) - + indexes_by_object = self.empty_indexes(object_names) table_query = MeasurementFeatureQuery( self.feature_name, dialect=self.dialect, ) - for table_id, table in tables_by_id.items(): - table_object_names = tuple(dict.fromkeys(objects_by_table_id[table_id])) + for table in self.feature_measurement_tables(measurement_tables_by_object): + table_object_names = self.table_object_names(table, object_names) if not table_object_names: continue - columnar_indexes = ( + self.merge_indexes( + indexes_by_object, MeasurementFeatureValueIndex.from_columnar_table_by_object( table, table_query, @@ -853,36 +773,21 @@ def value_indexes( object_name: query_objects_by_requested_object[object_name] for object_name in table_object_names }, - ) + ), ) - for object_name, object_index in columnar_indexes.items(): - pending_indexes_by_object[object_name] = pending_indexes_by_object[ - object_name - ].merged(object_index) - - indexes_by_object.update( - { - object_name: object_index.as_query_result() - for object_name, object_index in pending_indexes_by_object.items() - if object_index.present - } - ) + resolved = { + object_name: object_index.as_query_result() + for object_name, object_index in indexes_by_object.items() + if object_index.present + } missing_object_names = tuple( - object_name - for object_name in object_names - if object_name not in indexes_by_object + object_name for object_name in object_names if object_name not in resolved ) if missing_object_names: - diagnostic_query = MeasurementFeatureQuery( - self.feature_name, - dialect=self.dialect, - ) table_summaries = tuple( f"{object_name}:" + ";".join( - diagnostic_query.table_summaries( - measurement_tables_by_object[object_name] - ) + table_query.table_summaries(measurement_tables_by_object[object_name]) ) for object_name in missing_object_names ) @@ -890,85 +795,36 @@ def value_indexes( f"Could not resolve measurement feature {self.feature_name!r} " f"for object(s) {missing_object_names!r}; tables={table_summaries!r}." ) - resolved = { - object_name: indexes_by_object[object_name] - for object_name in cache_object_names - if object_name in indexes_by_object - } - self.cache_value_indexes(measurement_tables_by_object, resolved) - return {object_name: resolved[object_name] for object_name in object_names} + return resolved def value_indexes_by_axis( self, measurement_tables_by_object: MeasurementTablesByObject, row_axis: MeasurementRowAxisField, ) -> dict[int, MeasurementValueIndexesByObject] | None: - """Return object-keyed feature indexes grouped by one declared row axis.""" - required_axis_values = self.required_axis_values( - measurement_tables_by_object, - row_axis, - ) - if not required_axis_values: - return None - cached_axis_indexes = self.cached_matching_axis_value_indexes( - measurement_tables_by_object, - row_axis, - ) + """Return current feature indexes preserving complete object/axis rows.""" object_names = self.normalized_object_names - cache_object_names = self.cache_object_names(measurement_tables_by_object) - if cached_axis_indexes is not None: - requested = self.requested_axis_value_indexes(cached_axis_indexes) - if self.requested_axis_indexes_present( - requested, - required_axis_values=required_axis_values, - ): - return requested - cached_object_names = frozenset( - object_name - for object_name in cache_object_names - if self.axis_indexes_contain_object( - cached_axis_indexes, - object_name, - required_axis_values=required_axis_values, - ) - ) - else: - cached_axis_indexes = {} - cached_object_names = frozenset() - missing_cache_object_names = tuple( - object_name - for object_name in cache_object_names - if object_name not in cached_object_names - ) - if not missing_cache_object_names: - return self.requested_axis_value_indexes(cached_axis_indexes) - lookup = resolve_runtime_measurement_lookup_dialect( self.dialect ).feature_lookup(self.feature_name) query_objects_by_requested_object = { object_name: lookup.query_object_name(object_name) - for object_name in cache_object_names + for object_name in object_names } table_query = MeasurementFeatureQuery( self.feature_name, dialect=self.dialect, ) by_axis: dict[int, dict[str, MeasurementFeatureValueIndex]] = {} - + has_row_axis = False for table in self.feature_measurement_tables(measurement_tables_by_object): - table_object_names = tuple( - object_name - for object_name in dict.fromkeys( - self.table_cache_object_names(table, object_names) - ) - if object_name in missing_cache_object_names - ) - if not table_object_names: - continue axis_values = self.table_axis_values(table, row_axis) if not axis_values: continue + has_row_axis = True + table_object_names = self.table_object_names(table, object_names) + if not table_object_names: + continue axis_column = columnar_row_values(table.rows, row_axis.value) _feature_name, table_indexes = next( ColumnarMeasurementTableSchema.from_table(table).feature_value_indexes( @@ -990,14 +846,11 @@ def value_indexes_by_axis( ) ) for axis_value, indexes in table_indexes.items(): - target = by_axis.setdefault( - axis_value, self.empty_indexes(missing_cache_object_names) - ) + target = by_axis.setdefault(axis_value, self.empty_indexes(object_names)) self.merge_indexes(target, indexes) - - if not by_axis: - return self.requested_axis_value_indexes(cached_axis_indexes) - resolved = { + if not has_row_axis: + return None + return { axis_value: { object_name: object_index.as_query_result() for object_name, object_index in indexes.items() @@ -1005,18 +858,6 @@ def value_indexes_by_axis( } for axis_value, indexes in by_axis.items() } - merged_resolved = { - axis_value: dict(object_indexes) - for axis_value, object_indexes in cached_axis_indexes.items() - } - for axis_value, object_indexes in resolved.items(): - merged_resolved.setdefault(axis_value, {}).update(object_indexes) - self.cache_axis_value_indexes( - measurement_tables_by_object, - row_axis, - merged_resolved, - ) - return self.requested_axis_value_indexes(merged_resolved) @staticmethod def empty_indexes( @@ -1027,13 +868,6 @@ def empty_indexes( object_name: MeasurementFeatureValueIndex() for object_name in object_names } - @staticmethod - def indexes_present( - indexes: Mapping[str, "MeasurementFeatureValueIndex"], - ) -> bool: - """Return whether any object index carries values.""" - return any(index.present for index in indexes.values()) - @staticmethod def merge_indexes( target: dict[str, "MeasurementFeatureValueIndex"], @@ -1060,48 +894,23 @@ def table_axis_values( row_axis, ) - def cache_object_names( - self, - measurement_tables_by_object: MeasurementTablesByObject, - ) -> tuple[str, ...]: - """Return object domains worth indexing for this feature/table set.""" - return tuple( - dict.fromkeys( - ( - *self.normalized_object_names, - *( - object_name - for table in self.feature_measurement_tables( - measurement_tables_by_object - ) - for object_name in self.table_cache_object_names( - table, - self.normalized_object_names, - ) - ), - ) - ) - ) - - def table_cache_object_names( + def table_object_names( self, table: MeasurementTable, - fallback_object_names: tuple[str, ...], + requested_object_names: tuple[str, ...], ) -> tuple[str, ...]: - """Return result object domains represented by one table scan.""" + """Return requested object domains represented by this table epoch.""" semantics = MeasurementTableObjectFeatureSemantics.from_table(table) if not semantics.object_names: - return fallback_object_names + return requested_object_names feature_lookup = resolve_runtime_measurement_lookup_dialect( self.dialect ).feature_lookup(self.feature_name) - unconstrained_result_names = tuple( - object_name - for object_name in fallback_object_names - if feature_lookup.query_object_name(object_name) is None - ) return tuple( - dict.fromkeys((*semantics.object_names, *unconstrained_result_names)) + object_name + for object_name in requested_object_names + if object_name in semantics.object_names + or feature_lookup.query_object_name(object_name) is None ) def unique_measurement_tables( @@ -1135,231 +944,6 @@ def feature_measurement_tables( if query.table_may_carry_feature(table) ) - def cache_key( - self, - measurement_tables_by_object: MeasurementTablesByObject, - ) -> MeasurementObjectFeatureVectorBatchCacheKey: - """Return the process-local identity key for this feature/table batch.""" - return MeasurementObjectFeatureVectorBatchCacheKey( - feature_name=self.feature_name, - dialect_identity=id( - resolve_runtime_measurement_lookup_dialect(self.dialect) - ), - table_identities=tuple( - id(table) - for table in self.feature_measurement_tables( - measurement_tables_by_object - ) - ), - ) - - def axis_cache_key( - self, - measurement_tables_by_object: MeasurementTablesByObject, - row_axis: MeasurementRowAxisField, - ) -> MeasurementObjectFeatureAxisBatchCacheKey: - """Return the process-local identity key for this feature/table/axis batch.""" - return MeasurementObjectFeatureAxisBatchCacheKey( - feature_name=self.feature_name, - dialect_identity=id( - resolve_runtime_measurement_lookup_dialect(self.dialect) - ), - row_axis=row_axis, - table_identities=tuple( - id(table) - for table in self.feature_measurement_tables( - measurement_tables_by_object - ) - ), - ) - - def table_owners( - self, - measurement_tables_by_object: MeasurementTablesByObject, - ) -> tuple[MeasurementTable, ...]: - """Return table owners used to protect identity-keyed cache entries.""" - return self.feature_measurement_tables(measurement_tables_by_object) - - def cached_value_indexes( - self, - measurement_tables_by_object: MeasurementTablesByObject, - ) -> MeasurementValueIndexesByObject | None: - """Return cached value indexes when table identities still match.""" - cached_indexes = self.cached_matching_value_indexes( - measurement_tables_by_object, - ) - if cached_indexes is None: - return None - if not all( - object_name in cached_indexes - for object_name in self.normalized_object_names - ): - return None - return { - object_name: cached_indexes[object_name] - for object_name in self.normalized_object_names - } - - def cached_matching_value_indexes( - self, - measurement_tables_by_object: MeasurementTablesByObject, - ) -> MeasurementValueIndexesByObject | None: - """Return every cached object index for this feature/table identity.""" - cache = MeasurementObjectFeatureVectorBatchQueryCache.process_cache() - cached = cache.cached_value(self.cache_key(measurement_tables_by_object)) - if cached is None: - return None - cached_owners, cached_indexes = cached - if not identity_owner_tuples_match( - cached_owners, - self.table_owners(measurement_tables_by_object), - ): - return None - return cached_indexes - - def cached_axis_value_indexes( - self, - measurement_tables_by_object: MeasurementTablesByObject, - row_axis: MeasurementRowAxisField, - ) -> dict[int, MeasurementValueIndexesByObject] | None: - """Return cached row-axis indexes when table identities still match.""" - cached_indexes = self.cached_matching_axis_value_indexes( - measurement_tables_by_object, - row_axis, - ) - if cached_indexes is None: - return None - requested = self.requested_axis_value_indexes(cached_indexes) - if not self.requested_axis_indexes_present( - requested, - required_axis_values=self.required_axis_values( - measurement_tables_by_object, - row_axis, - ), - ): - return None - return requested - - def cached_matching_axis_value_indexes( - self, - measurement_tables_by_object: MeasurementTablesByObject, - row_axis: MeasurementRowAxisField, - ) -> Mapping[int, MeasurementValueIndexesByObject] | None: - """Return every cached row-axis index for this feature/table identity.""" - cache = MeasurementObjectFeatureAxisBatchQueryCache.process_cache() - cached = cache.cached_value( - self.axis_cache_key(measurement_tables_by_object, row_axis) - ) - if cached is None: - return None - cached_owners, cached_indexes = cached - if not identity_owner_tuples_match( - cached_owners, - self.table_owners(measurement_tables_by_object), - ): - return None - return cached_indexes - - def requested_axis_value_indexes( - self, - indexes_by_axis: Mapping[int, MeasurementValueIndexesByObject], - ) -> dict[int, MeasurementValueIndexesByObject]: - """Project cached row-axis indexes to requested object names.""" - object_names = self.normalized_object_names - return { - axis_value: { - object_name: object_indexes[object_name] - for object_name in object_names - if object_name in object_indexes - } - for axis_value, object_indexes in indexes_by_axis.items() - } - - def requested_axis_indexes_present( - self, - indexes_by_axis: Mapping[int, MeasurementValueIndexesByObject], - *, - required_axis_values: tuple[int, ...], - ) -> bool: - """Return whether cached axis indexes satisfy every requested object.""" - return all( - self.axis_indexes_contain_object( - indexes_by_axis, - object_name, - required_axis_values=required_axis_values, - ) - for object_name in self.normalized_object_names - ) - - @staticmethod - def axis_indexes_contain_object( - indexes_by_axis: Mapping[int, MeasurementValueIndexesByObject], - object_name: str, - *, - required_axis_values: tuple[int, ...], - ) -> bool: - """Return whether one object has complete cached indexes for an axis scope.""" - return bool(required_axis_values) and all( - object_name in indexes_by_axis.get(axis_value, {}) - for axis_value in required_axis_values - ) - - def required_axis_values( - self, - measurement_tables_by_object: MeasurementTablesByObject, - row_axis: MeasurementRowAxisField, - ) -> tuple[int, ...]: - """Return declared row-axis values that must be represented in the cache.""" - values: set[int] = set() - for table in self.feature_measurement_tables(measurement_tables_by_object): - values.update(self.table_axis_values(table, row_axis)) - return tuple(sorted(values)) - - def cache_value_indexes( - self, - measurement_tables_by_object: MeasurementTablesByObject, - value_indexes: MeasurementValueIndexesByObject, - ) -> None: - """Store value indexes with table-owner references for id-reuse safety.""" - MeasurementObjectFeatureVectorBatchQueryCache.process_cache().store_value( - self.cache_key(measurement_tables_by_object), - (self.table_owners(measurement_tables_by_object), value_indexes), - ) - - def cache_axis_value_indexes( - self, - measurement_tables_by_object: MeasurementTablesByObject, - row_axis: MeasurementRowAxisField, - value_indexes: Mapping[int, MeasurementValueIndexesByObject], - ) -> None: - """Store row-axis value indexes with table-owner id-reuse protection.""" - MeasurementObjectFeatureAxisBatchQueryCache.process_cache().store_value( - self.axis_cache_key(measurement_tables_by_object, row_axis), - (self.table_owners(measurement_tables_by_object), value_indexes), - ) - - -class MeasurementObjectFeatureVectorBatchQueryCache( - RegisteredProcessLocalBoundedCache[ - MeasurementObjectFeatureVectorBatchCacheKey, - MeasurementObjectFeatureVectorBatchCacheValue, - ] -): - """Process-local cache for repeated object-feature table batch indexes.""" - - max_entries = 1024 - - -class MeasurementObjectFeatureAxisBatchQueryCache( - RegisteredProcessLocalBoundedCache[ - MeasurementObjectFeatureAxisBatchCacheKey, - MeasurementObjectFeatureAxisBatchCacheValue, - ] -): - """Process-local cache for repeated object-feature row-axis table indexes.""" - - max_entries = 1024 - @dataclass(frozen=True, slots=True) class MeasurementFeatureValueIndex: @@ -1499,12 +1083,6 @@ def as_query_result(self) -> MeasurementValueIndexResult: return self.values_by_label, self.positional_values -class MeasurementTableObjectFeatureSemanticsCache(IdentityBoundProcessCache): - """Bounded process-local cache for immutable measurement-table semantics.""" - - registry_key = "measurement_table_object_feature_semantics" - - @dataclass(frozen=True, slots=True) class MeasurementTableObjectFeatureSemantics: """Object and feature declarations carried by one measurement table.""" @@ -1514,16 +1092,6 @@ class MeasurementTableObjectFeatureSemantics: @classmethod def from_table( - cls, table: MeasurementTable - ) -> "MeasurementTableObjectFeatureSemantics": - cache = MeasurementTableObjectFeatureSemanticsCache.process_cache() - cached = cache.get_bound(table) - if cached is not None: - return cached - return cache.put_bound(table, cls.from_table_declarations(table)) - - @classmethod - def from_table_declarations( cls, table: MeasurementTable, ) -> "MeasurementTableObjectFeatureSemantics": diff --git a/openhcs/core/runtime_artifact_queries.py b/openhcs/core/runtime_artifact_queries.py index fc66a4fe2a..ac0aa330e2 100644 --- a/openhcs/core/runtime_artifact_queries.py +++ b/openhcs/core/runtime_artifact_queries.py @@ -23,7 +23,6 @@ measurement_table_axis_values, ) from openhcs.core.measurement_feature_queries import ( - ColumnarMeasurementTableSchema, IndexedObjectMeasurementLabelPlaneBinding, MeasurementAxisValueProjection, MeasurementFeatureValueIndex, @@ -32,15 +31,7 @@ MeasurementTableObjectFeatureSemantics, MeasurementValueIndexResult, ) -from openhcs.core.measurement_lookup_dialect import ( - resolve_runtime_measurement_lookup_dialect, -) -from openhcs.core.process_local_cache import ( - BoundedCache, - RegisteredProcessLocalBoundedCache, - identity_owner_tuples_match, - named_identity_owner_tuples_match, -) +from openhcs.core.process_local_cache import BoundedCache from openhcs.core.runtime_measurements import ( MeasurementRowAxisField, MeasurementSubject, @@ -85,37 +76,6 @@ class RuntimeMeasurementTablesQueryCache( """Store-owned measurement tables for one axis and component group.""" -MeasurementLabelSliceFeatureBatchCacheValue = tuple[ - tuple[MeasurementTable, ...], - tuple[tuple[str, object], ...], - Mapping[str, tuple[Any, ...]], -] - - -@dataclass(frozen=True, slots=True) -class MeasurementLabelSliceFeatureBatchCacheKey: - """Identity key for label-plane feature projections.""" - - feature_name: str - object_names: tuple[str, ...] - dialect_identity: int - row_axis: MeasurementRowAxisField - table_identities: tuple[int, ...] - label_identities: tuple[tuple[str, int], ...] - row_axis_values: tuple[tuple[str, tuple[int, ...]], ...] - - -class MeasurementLabelSliceFeatureBatchQueryCache( - RegisteredProcessLocalBoundedCache[ - MeasurementLabelSliceFeatureBatchCacheKey, - MeasurementLabelSliceFeatureBatchCacheValue, - ] -): - """Process-local cache for repeated label-plane feature projections.""" - - max_entries = 1024 - - @dataclass(frozen=True, slots=True) class RuntimeArtifactQueryContext: """Execution-scope view over a RuntimeValueStore.""" @@ -660,10 +620,6 @@ def object_names(self) -> tuple[str, ...]: def values_by_object(self) -> Mapping[str, tuple[Any, ...]]: """Return label-plane-aligned vectors keyed by object name.""" - cached = self.cached_values_by_object() - if cached is not None: - return cached - label_planes_by_object = { object_name: self.object_feature_query(object_name).label_planes(labels) for object_name, labels in self.labels_by_object.items() @@ -738,13 +694,11 @@ def values_by_object(self) -> Mapping[str, tuple[Any, ...]]: strict=True, ) ) - return self.cache_values_by_object( - MappingProxyType( - { - object_name: values_by_object[object_name] - for object_name in object_names - } - ) + return MappingProxyType( + { + object_name: values_by_object[object_name] + for object_name in object_names + } ) def object_feature_query( @@ -760,73 +714,6 @@ def object_feature_query( plane_projector=self.plane_projector, ) - def cache_key(self) -> MeasurementLabelSliceFeatureBatchCacheKey: - """Return the identity key for this label-plane feature projection.""" - object_names = self.object_names - return MeasurementLabelSliceFeatureBatchCacheKey( - feature_name=self.feature_name, - object_names=object_names, - dialect_identity=id( - resolve_runtime_measurement_lookup_dialect(self.dialect) - ), - row_axis=self.row_axis, - table_identities=tuple(id(table) for table in self.measurement_tables), - label_identities=tuple( - (object_name, id(self.labels_by_object[object_name])) - for object_name in object_names - ), - row_axis_values=tuple( - ( - object_name, - self.object_feature_query(object_name) - .select_axis(self.labels_by_object[object_name]) - .row_axis_values, - ) - for object_name in object_names - ), - ) - - def table_owners(self) -> tuple[MeasurementTable, ...]: - """Return table owners used to protect identity-keyed cache entries.""" - return self.measurement_tables - - def label_owners(self) -> tuple[tuple[str, object], ...]: - """Return label owners used to protect identity-keyed cache entries.""" - return tuple( - (object_name, self.labels_by_object[object_name]) - for object_name in self.object_names - ) - - def cached_values_by_object(self) -> Mapping[str, tuple[Any, ...]] | None: - """Return cached label-plane feature projections when owners still match.""" - cached = ( - MeasurementLabelSliceFeatureBatchQueryCache.process_cache().cached_value( - self.cache_key() - ) - ) - if cached is None: - return None - cached_tables, cached_labels, cached_values = cached - if not identity_owner_tuples_match(cached_tables, self.table_owners()): - return None - if not named_identity_owner_tuples_match(cached_labels, self.label_owners()): - return None - return cached_values - - def cache_values_by_object( - self, - values_by_object: Mapping[str, tuple[Any, ...]], - ) -> Mapping[str, tuple[Any, ...]]: - """Store label-plane feature projections with identity-owner protection.""" - return MeasurementLabelSliceFeatureBatchQueryCache.process_cache().store_value( - self.cache_key(), - ( - self.table_owners(), - self.label_owners(), - values_by_object, - ), - )[2] - def _merge_measurement_value_index( target: MeasurementValueIndexResult, diff --git a/tests/unit/test_runtime_artifact_queries.py b/tests/unit/test_runtime_artifact_queries.py index bd38e7164e..0bd48d2aea 100644 --- a/tests/unit/test_runtime_artifact_queries.py +++ b/tests/unit/test_runtime_artifact_queries.py @@ -2,6 +2,7 @@ from collections.abc import Mapping from dataclasses import dataclass +import weakref import pytest @@ -25,7 +26,6 @@ ) from openhcs.core.runtime_artifact_queries import ( RuntimeArtifactQueryContext, - MeasurementLabelSliceFeatureBatchQueryCache, MeasurementLabelSliceFeatureBatchQuery, MeasurementLabelSliceFeatureQuery, MeasurementTableAxisProjection, @@ -38,11 +38,11 @@ ColumnarMeasurementTableSchema, MeasurementAxisValueProjection, MeasurementFeatureQuery, + RuntimeObjectLabelMeasurementQuery, + RuntimeObjectLabelMeasurementQueryCache, MeasurementFeatureValueIndex, - MeasurementObjectFeatureAxisBatchQueryCache, MeasurementObjectFeatureVectorBatchQuery, MeasurementTableObjectFeatureSemantics, - MeasurementTableObjectFeatureSemanticsCache, matching_measurement_field, measurement_feature_candidates, ordered_measurement_feature_candidates, @@ -636,9 +636,7 @@ def test_measurement_feature_query_uses_table_object_id_field() -> None: assert values.tolist() == [10.0, 20.0] -def test_measurement_table_semantics_cache_reuses_table_identity() -> None: - cache = MeasurementTableObjectFeatureSemanticsCache.process_cache() - cache.entries.clear() +def test_measurement_table_semantics_observes_current_row_and_subject_declarations() -> None: table = MeasurementTable( name="ObjectMeasurements", rows=MeasurementSparseColumnarRows.from_rows( @@ -656,11 +654,18 @@ def test_measurement_table_semantics_cache_reuses_table_identity() -> None: ) first = MeasurementTableObjectFeatureSemantics.from_table(table) + table.rows = MeasurementSparseColumnarRows( + {**table.rows.columns, "object_name": ("Nuclei", "Nuclei")}, + fields=table.rows.fields, + ) second = MeasurementTableObjectFeatureSemantics.from_table(table) + table.subject = MeasurementSubject(MeasurementScope.OBJECT, "SelectedObjects") + third = MeasurementTableObjectFeatureSemantics.from_table(table) - assert first is second assert first.object_names == ("Cells",) - assert "area" in first.feature_names + assert second.object_names == ("Nuclei",) + assert third.object_names == ("SelectedObjects",) + assert "area" in first.feature_names == second.feature_names == third.feature_names def test_measurement_feature_candidates_match_cellprofiler_compact_metric_names() -> ( @@ -960,8 +965,6 @@ def test_batch_label_slice_measurement_lookup_scans_each_axis_once( monkeypatch: pytest.MonkeyPatch, ) -> None: np = pytest.importorskip("numpy") - MeasurementLabelSliceFeatureBatchQueryCache.process_cache().entries.clear() - MeasurementObjectFeatureAxisBatchQueryCache.process_cache().entries.clear() table_scans: list[tuple[int, tuple[int, ...], tuple[str, ...]]] = [] original_feature_indexes = ColumnarMeasurementTableSchema.feature_value_indexes @@ -1140,10 +1143,8 @@ def test_label_slice_measurement_lookup_preserves_producer_runtime_slice_axis() assert tuple(value.tolist() for value in values) == ([0.25], [0.0]) -def test_axis_batch_cache_rebuilds_partial_object_axes() -> None: +def test_axis_batch_observes_current_values_across_complete_object_axes() -> None: np = pytest.importorskip("numpy") - cache = MeasurementObjectFeatureAxisBatchQueryCache.process_cache() - cache.entries.clear() table = MeasurementTable( name="IntensityMeasurements", rows=MeasurementProjectedColumnarRows( @@ -1176,17 +1177,11 @@ def test_axis_batch_cache_rebuilds_partial_object_axes() -> None: dialect=CELLPROFILER_MEASUREMENT_LOOKUP_DIALECT, ) table_map = {"Nuclei": (table,)} - cache.store_value( - batch_query.axis_cache_key(table_map, MeasurementRowAxisField.SLICE_INDEX), - ( - batch_query.table_owners(table_map), - { - 0: {"Nuclei": ({1: 1.0, 2: 2.0}, [])}, - 1: {}, - 2: {}, - }, - ), + initial = batch_query.value_indexes_by_axis( + table_map, MeasurementRowAxisField.SLICE_INDEX ) + assert initial[2]["Nuclei"][0] == {1: 5.0, 2: 6.0} + table.rows.columns["mean_intensity"] = (1.0, 2.0, 3.0, 4.0, 7.0, 8.0) labels = object_labels( np.array( ( @@ -1211,7 +1206,7 @@ def test_axis_batch_cache_rebuilds_partial_object_axes() -> None: assert tuple(value.tolist() for value in values) == ( [1.0, 2.0], [3.0, 4.0], - [5.0, 6.0], + [7.0, 8.0], ) @@ -1788,3 +1783,79 @@ def _record_native( path=f"/memory/{native_value.name}.pkl", backend="memory", ) + + +def test_batch_measurement_queries_release_owners_and_observe_store_replacement() -> None: + np = pytest.importorskip("numpy") + + def run_query_epoch(): + store = RuntimeValueStore() + pixels = { + name: np.ones((2, 32, 32), dtype=np.int32) + for name in ("Cells", "Nuclei") + } + labels = { + name: object_labels(value, declared_object_id_domains=((1,), (1,))) + for name, value in pixels.items() + } + feature_values = np.asarray([0.5, 0.9, 1.5, 1.9]) + table = MeasurementTable( + name="MixedShapeMeasurements", + rows=MeasurementSparseColumnarRows( + { + "slice_index": np.asarray([0, 1, 0, 1]), + "object_name": np.asarray(["Cells", "Cells", "Nuclei", "Nuclei"]), + "object_label": np.ones(4, dtype=np.int32), + "FormFactor": feature_values, + }, + fields=( + FieldSpec("slice_index", int), + FieldSpec("object_name", str), + FieldSpec("object_label", int), + FieldSpec("FormFactor", float), + ), + ), + subject=MeasurementSubject(MeasurementScope.ARTIFACT, "MixedShapeMeasurements"), + ) + plan = ArtifactOutputPlan( + name=table.name, path="/memory/measurements", artifact_type=MeasurementsArtifactType + ) + store.record( + RuntimeValue.normalize(plan, table, axis_id=AXIS_ID), + path=plan.path, backend="memory", + ) + query = MeasurementLabelSliceFeatureBatchQuery( + measurement_tables=(table,), + feature_name="AreaShape_FormFactor", + dialect=CELLPROFILER_MEASUREMENT_LOOKUP_DIALECT, + row_axis=MeasurementRowAxisField.SLICE_INDEX, + plane_projector=RuntimePlaneProjection.stack(2), + labels_by_object=labels, + ) + vectors = query.values_by_object() + assert vectors["Cells"][0].tolist() == [0.5] + owned_cache = store.query_cache(RuntimeObjectLabelMeasurementQueryCache) + owned_cache.store_value( + RuntimeObjectLabelMeasurementQuery( + axis_id=AXIS_ID, group_key=None, object_name="Cells", + feature_name="AreaShape_FormFactor", label_domain=(1,), + label_plane_domains=((1,), (1,)), + ), + vectors["Cells"], + ) + feature_values[0] = 7.5 + store.replace( + RuntimeValue.normalize(plan, table, axis_id=AXIS_ID), + path=plan.path, backend="memory", + ) + assert not owned_cache.entries + assert query.values_by_object()["Cells"][0].tolist() == [7.5] + references = tuple(weakref.ref(value) for value in pixels.values()) + ( + weakref.ref(feature_values), + ) + store.clear() + assert not owned_cache.entries + assert len(store) == 0 + return references + + assert all(reference() is None for reference in run_query_epoch()) From a778b07c1ebf93eb6a204016f189979d176734ba Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 23:24:30 -0400 Subject: [PATCH 205/235] Let measurement row owners release dataclass admission snapshots Remove the detached global asdict cache. Dataclass measurement columns retain their existing deep admission snapshot; subsequent raw row reads observe current data and source rows can be released with their owner. Fixes #582 --- openhcs/core/runtime_tabular_values.py | 25 +----------------- .../test_measurement_row_materialization.py | 26 +++++++++++++++++++ 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/openhcs/core/runtime_tabular_values.py b/openhcs/core/runtime_tabular_values.py index 354207da60..2dec6dc24a 100644 --- a/openhcs/core/runtime_tabular_values.py +++ b/openhcs/core/runtime_tabular_values.py @@ -13,8 +13,6 @@ from types import UnionType from typing import Annotated, Any, Union, get_args, get_origin, get_type_hints -from openhcs.core.process_local_cache import RegisteredProcessLocalBoundedCache - class ColumnarRows(ABC): """Nominal ABC for schema-bearing table payloads exposing named columns.""" @@ -262,31 +260,10 @@ def measurement_row_mapping(row: object) -> Mapping[str, object]: if isinstance(row, Mapping): return row if is_dataclass(row): - return MeasurementRowMappingCache.process_cache().mapping(row) + return asdict(row) raise TypeError(f"Unsupported measurement row type {type(row).__name__}.") -@dataclass(slots=True) -class MeasurementRowMappingCache( - RegisteredProcessLocalBoundedCache[int, tuple[object, Mapping[str, object]]] -): - """Bounded process-local cache for immutable dataclass measurement rows.""" - - max_entries: int = 262144 - - def mapping(self, row: object) -> Mapping[str, object]: - row_id = id(row) - cached = self.cached_value(row_id) - if cached is not None: - cached_row, row_mapping = cached - if cached_row is row: - return row_mapping - del self.entries[row_id] - row_mapping = asdict(row) - self.store_value(row_id, (row, row_mapping)) - return row_mapping - - class MeasurementObjectRowIdentity(str, Enum): """How object-scoped measurement rows identify their measured object.""" diff --git a/tests/unit/test_measurement_row_materialization.py b/tests/unit/test_measurement_row_materialization.py index 152db68198..58cb25ea96 100644 --- a/tests/unit/test_measurement_row_materialization.py +++ b/tests/unit/test_measurement_row_materialization.py @@ -455,3 +455,29 @@ def test_sparse_rows_reject_columns_absent_from_declared_fields() -> None: ({"value": 1.0},), fields=(FieldSpec("other", float),), ) + + +def test_dataclass_column_admission_owns_snapshot_and_releases_source_rows() -> None: + import gc + import weakref + + from openhcs.core.runtime_tabular_values import measurement_row_mapping + + @dataclass + class MutableMeasurementRow: + object_label: int + samples: list[float] + + row = MutableMeasurementRow(1, [2.5]) + row_ref = weakref.ref(row) + rows = DataclassMeasurementColumnarRows((row,)) + admitted_samples = rows.column_values("samples")[0] + row.samples.append(7.5) + assert measurement_row_mapping(row)["samples"] == [2.5, 7.5] + assert admitted_samples == [2.5] + assert rows.row_mappings() == ({"object_label": 1, "samples": [2.5]},) + + del rows, row + gc.collect() + assert row_ref() is None + assert admitted_samples == [2.5] From c70879c1a0ec998ebb1f6ae5a5b8dffda52e76a2 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 23:26:14 -0400 Subject: [PATCH 206/235] test: admit live dataclass row mappings independently Migrate the removed row-mapping cache fixtures to current dataclass values and independent deep snapshots. Paired with the nominal row-owner cache deletion. Refs #582 and #394. --- tests/unit/test_runtime_artifact_queries.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/tests/unit/test_runtime_artifact_queries.py b/tests/unit/test_runtime_artifact_queries.py index 0bd48d2aea..7ff190793e 100644 --- a/tests/unit/test_runtime_artifact_queries.py +++ b/tests/unit/test_runtime_artifact_queries.py @@ -51,7 +51,6 @@ from openhcs.core.runtime_tabular_values import ( FieldSpec, MeasurementObjectRowIdentity, - MeasurementRowMappingCache, ) from openhcs.core.runtime_measurements import ( MeasurementRowAxisField, @@ -214,7 +213,6 @@ def test_runtime_measurement_query_matches_schema_and_row_object_subjects() -> N def test_measurement_row_mapping_accepts_slotted_dataclasses() -> None: - MeasurementRowMappingCache.process_cache().entries.clear() row = MeasurementRow(object_name="Nuclei", object_label=1) mapping = measurement_row_mapping(row) @@ -233,15 +231,22 @@ def test_measurement_row_mapping_accepts_slotted_dataclasses() -> None: } -def test_measurement_row_mapping_cache_reuses_dataclass_rows() -> None: - cache = MeasurementRowMappingCache.process_cache() - cache.entries.clear() - row = MeasurementRow(object_name="Nuclei", object_label=1) +def test_measurement_row_mapping_observes_current_dataclass_with_independent_values() -> None: + @dataclass(slots=True) + class MutableMeasurementRow: + object_name: str + values: list[float] + row = MutableMeasurementRow(object_name="Nuclei", values=[0.5]) first = measurement_row_mapping(row) + row.object_name = "Cells" + row.values[0] = 7.5 second = measurement_row_mapping(row) - assert first is second + assert first == {"object_name": "Nuclei", "values": [0.5]} + assert second == {"object_name": "Cells", "values": [7.5]} + second["values"][0] = 9.5 + assert row.values == [7.5] def test_projected_columnar_rows_preserve_none_values() -> None: From cc4461bc8a58c90c2ae13a5dd3870a67e1d8fc0a Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 23:26:27 -0400 Subject: [PATCH 207/235] Resolve source-artifact fallback identity through its exact source mapping Refs #419, #450. Preserve typed workspace positions for anchor admission; use the mapped physical source only when image-set metadata axes are exhausted. Distinct same-basename physical sources remain separate. Validated with 18 focused source binding and identity controls. --- openhcs/core/source_binding_selection.py | 2 +- tests/unit/test_source_bindings.py | 67 ++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/openhcs/core/source_binding_selection.py b/openhcs/core/source_binding_selection.py index 89d30b0142..4a7d055385 100644 --- a/openhcs/core/source_binding_selection.py +++ b/openhcs/core/source_binding_selection.py @@ -1351,7 +1351,7 @@ def _source_image_set_identity( ) return SourceImageSetIdentity.from_metadata( metadata, - fallback_source_path=candidate, + fallback_source_path=self.source_path_for(candidate), policy=self.identity_policy, ) diff --git a/tests/unit/test_source_bindings.py b/tests/unit/test_source_bindings.py index 12ad215366..9252c4e7e4 100644 --- a/tests/unit/test_source_bindings.py +++ b/tests/unit/test_source_bindings.py @@ -1103,6 +1103,73 @@ def test_narrow_step_binding_uses_exact_workspace_provenance_identity(): ) +@pytest.mark.parametrize("shared_source", [True, False]) +def test_source_binding_members_use_exact_source_identity_without_metadata_axes( + shared_source, +): + primary_path = "A01_s001_w2_z001_t001.tif" + object_path = "_source/SavedIPO/A01_s001_w2_z001_t001.tif" + source_path = "/a/saved.labels.tif" + object_source = source_path if shared_source else "/b/saved.labels.tif" + metadata = { + "well": "A01", + "site": "1", + "channel": "2", + "z_index": "1", + "timepoint": "1", + } + coordinates = tuple( + ComponentSelector(component, metadata[component.value]) + for component in AllComponents + ) + primary = NamedSourceBinding(alias="SavedImage", component_identity=coordinates) + objects = NamedSourceBinding( + alias="SavedIPO", + component_identity=coordinates, + artifact_kind=ObjectLabelsArtifactType, + projection_role=SourceProjectionRole.SOURCE_ARTIFACT, + ) + refs = { + primary_path: SourcePixelRef("disk", source_path), + object_path: SourcePixelRef("disk", object_source), + } + address = OpenHCSPlaneAddress.from_complete_source_metadata(metadata) + projection = VirtualWorkspaceSourceProjection( + source_refs_by_virtual_path=refs, + source_metadata_by_path={primary_path: metadata, object_path: metadata}, + source_projections_by_virtual_path={ + primary_path: SourcePlaneProjection( + address=address, ref=refs[primary_path], source_alias=primary.alias + ), + object_path: SourceArtifactProjection( + address=address, + ref=refs[object_path], + source_alias=objects.alias, + artifact_kind=ObjectLabelsArtifactType, + ), + }, + ) + policy = SourceImageSetIdentityPolicy.from_source_bindings( + SourceBindingsConfig(bindings=(primary, objects)) + ) + assert not policy.identity_components() + matched = SourceBindingMatchedImageSet.from_plan( + bindings=(primary, objects), + match_plan=SourceBindingMatchPlan(SourceBindingMatchMethod.ORDER), + source_context=SourcePatternResolutionContext.from_projection( + parser=SourceSchemaFilenameParser(), projection=projection + ), + identity_policy=policy, + ) + assert matched.members_for_binding( + objects, + anchor_provenance=SourceImageProvenance( + source_path=primary_path, source_component_metadata=metadata + ), + source_universe=(object_path,), + ) == ((object_path,) if shared_source else ()) + + def test_source_binding_members_load_one_store_for_multiple_matching_identities(): binding = NamedSourceBinding( alias="Membrane", From a4f6e9e323d295a220caa7e86b9de17bbe28751e Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 23:49:23 -0400 Subject: [PATCH 208/235] Preserve raw image transport for undeclared generic adapter domains Fixes #583. Artifact binding management does not declare ownership of an implicit image ABI. The generic adapter admits context carriers only without raw image input; explicit compiled primary projections and CellProfiler module-owned domains remain authoritative. --- openhcs/core/runtime_adapters.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openhcs/core/runtime_adapters.py b/openhcs/core/runtime_adapters.py index 0872921da7..8cc4b817c5 100644 --- a/openhcs/core/runtime_adapters.py +++ b/openhcs/core/runtime_adapters.py @@ -388,7 +388,9 @@ class RuntimeAdapterSpec: def invocation_domain_inputs( self, contract: "CallableContract", ) -> ArtifactSpecCollection: - """Return declared inputs supplying this adapter's invocation carrier.""" + """Use declared context inputs when the callable has no raw image ABI.""" + if contract.accepts_implicit_main_flow_input: + return ArtifactSpecCollection(()) return contract.group_scope_inputs def __post_init__(self) -> None: From a2bda920f6fa50d59ec97ad9b30d87d49267f66b Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 23:43:58 -0400 Subject: [PATCH 209/235] Document installed saved-pipeline consumer acceptance Publish exact public processing/readback evidence and retained failed checkers, with bounded registered saved-label scope and source identity laws. Refs #419, #435, #450. --- .../README.rst | 96 + .../original-evidence.tar.gz | Bin 0 -> 941714 bytes .../original-five-step-435-saved-pipeline.py | 240 + .../produce-saved-ipo-saved-pipeline.py | 173 + .../readback.py | 108 + .../receipt.json | 6036 +++++++++++++++++ ...geometry-distance-resize-saved-pipeline.py | 329 + 7 files changed, 6982 insertions(+) create mode 100644 docs/refactor/receipts/installed-consumers-419-435-450-20261004/README.rst create mode 100644 docs/refactor/receipts/installed-consumers-419-435-450-20261004/original-evidence.tar.gz create mode 100644 docs/refactor/receipts/installed-consumers-419-435-450-20261004/original-five-step-435-saved-pipeline.py create mode 100644 docs/refactor/receipts/installed-consumers-419-435-450-20261004/produce-saved-ipo-saved-pipeline.py create mode 100644 docs/refactor/receipts/installed-consumers-419-435-450-20261004/readback.py create mode 100644 docs/refactor/receipts/installed-consumers-419-435-450-20261004/receipt.json create mode 100644 docs/refactor/receipts/installed-consumers-419-435-450-20261004/saved-label-geometry-distance-resize-saved-pipeline.py diff --git a/docs/refactor/receipts/installed-consumers-419-435-450-20261004/README.rst b/docs/refactor/receipts/installed-consumers-419-435-450-20261004/README.rst new file mode 100644 index 0000000000..a6ca168749 --- /dev/null +++ b/docs/refactor/receipts/installed-consumers-419-435-450-20261004/README.rst @@ -0,0 +1,96 @@ +Installed saved-pipeline consumer acceptance +========================================== + +``receipt.json`` is the unmodified successful fresh-reader receipt, SHA256 +``67ded2d57e6a7dd10a742ed6e7410851e9d88a0ab329682586cb60e73c09feb5``. +The saved-label execution and readback use OpenHCS source +``769ecb806e59db36ef3f83b20a40c701cf578dc9`` (integrated as +``cc4461bc8a58c90c2ae13a5dd3870a67e1d8fc0a``), the canonical virtualenv and +current editable dependencies, including ZMQRuntime ``be67b58``. Source freezes +retain actual dependency commits, tracked-file hashes, installed distributions, +Python/native identities, input hashes and environment overrides. No module +shims, canonical installation changes or foreign server adoption were used. + +Actual production consumers +--------------------------- + +* #435: the original five processing-step ASTs are unchanged. Only output roots + and the unused DAPI binding were adapted for one physical C2 synthetic source. + The public saved/reopened pipeline compiled and executed at ``ba522b7da`` + (tracked source equal to receiving root ``e6ef8f1d1``). Both 1024 x 1024 float32 + raw/capped roles pass complete-pixel comparisons with the existing + CellProfiler comparison owner and tolerances, public inventory/sample and + a fresh MCP SDK process reopen. Physical C2 and XY1.3556 micrometers survive; + exactly one projection per role and distinct virtual paths remain. Inspection + correctly reports PARTIAL solely because no plate grid was declared. +* #419: the actual registered IPO produced saved 32 x 32 object labels with + object areas 25 and 12 at ``ba522b7da``. A new canonical saved/reopened public + four-step pipeline then used those exact persisted labels for registered + MeasureObjectSizeShape and Morph DISTANCE. It compiled/executed COMPLETE; + the two 2-D area rows are 25 and 12, with no Volume column, and the complete + distance pixels match the independent EDT oracle. The original biological + input and original #419 authored pipeline files are unavailable locally. + This is installed registered-contract acceptance, not original biological + replay or proof of the author's scientific inference. +* #450: the same public pipeline exercises prepared Resize followed by an + ImageMath consumer. Both complete 64 x 64 float32 outputs equal the expected + nearest-neighbor resize and retain C2, source identity and source calibration. + This supplements, rather than replaces, the separate qualified original 3-D + scientific comparison and prepared-contract controls. + +Source and publication laws +--------------------------- + +The real saved-label source-admission failure occurred after valid typed role +selection: primary image and object aliases of the same physical file received +unequal fallback identities from different virtual positions. The existing +``SourceBindingMatchedImageSet`` now uses ``source_path_for(candidate)`` only +for the fallback identity. Metadata-key precedence and independent typed anchor +positions remain. Eighteen focused identity/selection controls pass; same-basename +files at distinct physical paths remain different sources. + +Resize calls ``ImagePayloadMetadata.with_spatial_resize``: it updates the local +source-domain shape/origin and border/mask facts, retaining original source pitch +1.3556 micrometers. It does not rescale source calibration to 0.6778. The terminal +consumer legitimately has a PRIMARY_PLANE publication in ``images/`` and an +independent SOURCE_ARTIFACT checkpoint publication in ``checkpoints_step3/``. +The reader admits the primary role through durable projection metadata and +independently checks both occurrences' complete pixels and public samples; +unique virtual-path and projection cardinality checks remain. + +Evidence preservation and reproduction +-------------------------------------- + +The accepted readback used only health, inspect, query and sample tools: no +runtime startup, compile, processing replay or mutating job retry. All runtime +and MCP children are terminal. ``output_files_before`` and ``output_files_after`` +contain identical SHA256 inventories for all 29 retained outputs. Original +journeys and failed checkers also retain identical hashes. The archived v7 +journey remains RED for its wrong resized-source-pitch expectation; the first +readback remains RED for demanding a single occurrence across distinct roles. +The separate successful receipt corrects those inspection assumptions without +changing outputs, science gates or original failures. + +``original-evidence.tar.gz`` contains byte-exact v2/v3/v7 source freezes, +controllers and journeys, plus the failed readback controller/receipt. +The three small saved pipeline files and final ``readback.py`` are included +separately. Pixel datasets and native binaries are retained at the receipt's +local paths and are not copied into this packet. Controllers are historical, +self-validating source/host-bound commands; their absolute paths were not rewritten +for publication. Their exact original commands were:: + + taskset -c 3 /home/ts/code/projects/openhcs/.venv/bin/python -B /var/tmp/run_public_installed_consumers_419_435_450_v2_20261004.py --run + taskset -c 3 /home/ts/code/projects/openhcs/.venv/bin/python -B /var/tmp/run_public_installed_consumers_419_435_450_v3_20261004.py --run + taskset -c 3 /home/ts/code/projects/openhcs/.venv/bin/python -B /var/tmp/run_public_installed_consumers_419_435_450_v7_20261004.py --run + /home/ts/code/projects/openhcs/.venv/bin/python -B /var/tmp/read_public_installed_consumers_419_435_450_v7_v2_20261004.py + +Controller SHA256 values, respectively: + +* v2: ``36d95e77443e4918d19b269e72fcf1d578cf5f0eed3d0c502fc7d67f2c544167`` +* v3: ``a341a0f74a61d6f21e588ed1b158f5110dc146581e91f60c6254060bd1a66b27`` +* v7: ``733ba0c91ad79a01702708fc899ff5c1a3ff24a3bcb2c5a5ab31609010551bbe`` +* accepted reader: see ``helper_sha256`` in ``receipt.json``. + +This packet makes no performance, full-catalog, scaling, #433 OOM-cause or +repeated-ready-server stability claim. It does not close unrelated graph/viewer +or analysis-unit receiving obligations. diff --git a/docs/refactor/receipts/installed-consumers-419-435-450-20261004/original-evidence.tar.gz b/docs/refactor/receipts/installed-consumers-419-435-450-20261004/original-evidence.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..b004e89a21cbd93ed9bb0ae763cb0aa3d536ecef GIT binary patch literal 941714 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

<@Koyx#S z<-AmL-ZpJi;$_DynD!$S?a(zAQAh>Rxr{KFkCg80t^}Gz>jp)#x8vM$PJ94VM+$ie zKb|5(#Vvsup^Gm3=d7}=2%1;;6Gl{m=o_{)MmvAyy)?{CZ4hk{p|}r5ddg80ZGYPx zP7g=V+SR*4jopMnYZ5#*3eej^;!NpEAZO!JsiOQ)XiM2NC2@O{@AdWGj`ysx@ISs+ zoHS0gg;h~{AsCE>RUBAwR=O=Hjj(2$*%_~8uRxCj9LD2L@DBeDf##LXpN(}I!;0c9 ztfXp62*pyWxZzWtmGlrK+Jt19b!9h95G52ArR|G~LYLY*UutilzB*p@ukSZk|9;oF zZhWrJkY@}+OTj6EI6#sBuj|bVw-*~@^;DKb`kiTU?OlLrFL&`H8u~8YjQaidWgs~M zQ-bQ_>Fg!t))Yal7N;PZa(iqgDV``w3lECB4iTJ)`;N9c-8=fSf>?@P8}z5VH|UGf z%+uZbX}qn)P7&U8%D0Mv)aYszuGw0g%JDJN&)o2!Ffni_?<$%WwD9}4rJ0O(T9;$* z4LaY42-6EZr=du1dlNKK;uS63!rDZRw9SQ!%W57Lp#_t?AhE6D8?|fvH=AJ3@6W~> z^Wm?Lcc)9+l_<3mj-;Wm+lDjuG*j_Vp;B>+1B(a-Zz)j~<-nje3Unx1*0e5-_Urf4 zNhQbX+%`=juu^N&l1rP|@XDfSWc{+YA1e3epk*Zfpg`SbG=e%KmrEm2`*S+<&@0*! z?%}aTONR$l)ktP;2KiVa7<4YQ6R3Ywaw$a^FevVK!T1lJ>3zI=0#4S7hg7Fct{2*O zL1--l^q3T$rmziN9x-$zBTB>VE^diRjl^--2Oo|8CT%}qsN3&1`|7{?{mYy;=Zo;> zn@diB*;5d%Wu5iQAsD3)ulHDLtqt<9DPjc*3`D2~e(rcV^!EqduYax-T>4-2sL#?i z$0G(iqrOY=nT`me#f>m%sbh?y^%^n$#>2J4hPBbPT7<{D9q*5(A%1;Yl*it@)1Fhx z*_@$5De6sfJk$ihP$tr+LwIJtL(v}DTe0<*^jA`(eZRHhK6t*vcYEhc-apu*fqHMG zWDE~onBHYi!G>NZ*QTCMmK7~dUzt18i1?|fjrG`4yuEtcpYR3qyZUW@d)Otk4|#mX z&52ejR&VI4<9_W#c4LZeU#^b&C;*~JyojPGT+znY?%1sM?HfXWptRS2zW~R$FFpl< z!dXZ++I-4RZtr&&r40+44X;0|`V4p}B~+_&*3)y1FA%P<8sAn5UYx;`c-(O%b5|wG z7rA3r;-fB`Luz#55cuiXe=ejpZ583}BkA8bc#wDfoBiVnoG-<{@)hpu_1PBjDY6@A zwJTe-SlQ#RX>%e3n^XUiI2-Me!ZWrd{M#b_OQM+B#>-jXM!u#qi9X>;_sB zpAf895)rH$PC(LgT3w4?vY%5=tk@*zo!%>#+51>`%e`N3?%JW-XYS;EHXMH?z7$-ow(7~hQO9&{6+Pxm_>=r-no(gZO$&}P^%RKPn zL1ZtDzM9yYrEnVG)~>wuxfX5L>p`hM<3}Z-_+oR-9GD^~m(b569dCI_ESucYgpqPN1sJtruYq zZSs&Xy`CM zmhC|ar&QuevCUT15eOZdpsLS)fhWHigP(KH!HRdW`0J&W?R41v<#m zZus&A@lo;^eno@R#zYN#%!tVq6BFgw#W;QJ)6d*d@n8PefBoP8_3FR>U;p*r{`IMx z1KVM3K!LlaCBZS=jHvZ?Ju)hsh;mgSl~phS?6k8i99NY^r+ZRMI}GsRXh?W|B0Che z5=74|9cyLb4_cExWj0y-PmFPfs(h@UiY1$;dx(cx{^gH;by17pl<2oBJ=1jH6oZ{l zdtc7%sN?G88oaG28WD5Y{7!OEWD4zBiNxWD6LvAkzfDtZ}Sl96{za2?vLtiN zsZ-+_6bprE#G|#zGXg96PMKwkn}B~7o(_YJ>-y{M-2=6ie>v5%J~xp+Hmsetm-h1# z5^PyxD0?6TD4y4rP<3C56WV)yt!OB2q!wEnu_T9E`$qls$76TJ-*0yhcgoqnVcar*6@A zX0g}3{`p)^XZms5BXYu{xul}tk($C+&I*>ory_~eAi=*ki+;r1su$>ZKZLlFGp#K}($7nWs*5n(9nM=DJDP#`7J9<}U>z9Uy zij1pvclXHX_s@sM6Qi;%J1(<&@?2&Mg656}K_INwyPpWjPADv=Q+iuHq#%lCEz2waaAsi`bYsWFQ>st2z=f-g;_-#zxTN*5-5uayZG9LWrQ@8(Ws2KO?X?JO+c$4 zklK~6-B?CJa}2TeC9V|}!%`E2RP6eb z!1Vkofjh5%`7i(WpN}n8Nt;XDLAXUcJkT~kuPP@&T^txKLmz~higwFhmd~Mxn2xBk zdFpa{XT(=q5AY^SFNEgsU=ty4yb#}0bug+ z$iLm)d_Selh`7;55CB@Y5~0 z(K$6(mZ%`g%qkR>)IxhYBY!mu5y-#n$34I7U;dXPcm2g1^CYf5#S7P}MRZxDyyfY{ zZp0GBdqULU&2!APaUEKqwc-SuX=66$YOJq@KA*q*f)uoWviR2~*fl}n=nK%}cB&xrw7Ra_T0nwCo- zdMK0fhd1GqqoRoN^r)CtD-oLR9X)a=ZcPf?m5|v$LyS|o6R&5FXc0Zx%9m*ru^chB z_NRj$4rs^MUzMTP?0@t#v8`LLRC)P2JStaS+hX9==k?%GQDYSMRgrLzoqnV%+9bi4^&Q4{dk~5tK%CU zrB5X!TNU<&{{l+prE4SN4<_gvsjKH8O%nYU8|`>`)I-hA^D()U){g@o6p#=~ACGz9cDqFXbE5r4;0t+`DQlm*Q7}l zTxhW*t;(j2st{mf)+%*=_Y@PC#OiGng)hD9=9T2M&+B$+)N^6PS)co;Vs*G{Jk3H$ zZi!{=QPKtB3(?)GR65PJivycBIk^z;<91*dCshKB88&aSzch>MJ2b26Rko2P!!zmv|IJk4=PA8$jP?s||@%I!(1j#rLPekWGa3OB47~uR+{M zq}Qfs&a-o6@$Uk^xAWZ1sD!Bpb5`lBQaINX76gN-l3fZnL4dH)5-o(Le4^Bi)I-c0 zPP7V|mu7MI_5S+ic?+YG%KeMO%}M)^;F!Q42iwBbmLkozt4C=P$&IOR>51xC5p2(Yk%uwB8_cmCJ4be$8k=g8rTJd z!k0DF9!6~sJIlH>(zBD5=iSyX*S%Mw@um=Y#lg^pOGB^IR~*t2q*Y`vtb;}ke-vrx zf_0;4uR6@BFHGX(mvi_1@z-y6zdwF?3W7(%eIy7Jvu^ZW+O<(p0PXF1KYfbd8oKFr zcBpq#1`g*|*v}|KVO!nnrF4A%?Rqapeqkc?jH14JDM5n?$%Zvjaa%6}&mC(W+Km>4 zT5h(z4bCcuCgl=?U#ajuivN9GdMPIz)q{-D-Pp}h=d+OLD(sHif}l;muy>xcR|qZS zf{?c-Fk{ZSAplnwq^b!Za!Q=mtB}Y^=ilqM*M};O@}gak-&QC3)s*sZ6AKt*AgoaZ zOsp208zs74@jd0!V5UF=?aM)Sh1_4iU%!U1RNByr=<#y%i}SQxup$D^U|x+NgQLn0 zljz)xug)ROC|cz720j?&%{!g9WRCH5%(EEq)h+4&Bwc$x*q-8hidxwH45W+1 ztE;2Xh`vT~qC#W3Ko(ThqhS@q&zoSjHem99Hu@gQ=eS1C;aD$z)KNWO9T`t$#juSt zY_Y|dLlub;;}r7p5%ILWqe@6L;Z3m7B4fPgZY=!i;D-``rv3e}AKl$No>=kSGuEU) z66dCTA%@l!-K(ic6&o!9D^$hdj<}sxW2?;;6d4ECm}pSatH^qXKk0W~4tJ_?cz%%ffByG>{XhQGfB%1e z`Op9QpZ>T1`rrP?zy0g~@;`prjX(rt&#GK$H?WClfX$AlO{R)l5J4exdFOEfa7t?d z%|Sb`qAf@yD8kN_`{h~8zs?VBO=k3Gyb5{nwW*ELU-;rJH`Vt&3 zPJ!#)x38L^(W*A(HaMzL=%y!K$XXa{)gK!Fc#fXVR&=W=g%2fDE$4|WG5&5I14B`D zAxcWS#5u(?voYd6WppZE1|&1aae43;r@8gG1DZ!Md6fNj{`5 z5nY8?4K#)BV(gTZV0b*O+Z<&|+;U9swn}TKz}HIdXUym%=yfiujo$a7)!`>nQWRs+ z(3MM_N*LTSYUhgVL=J8M#6uuu<2pzb$fp{9a8leaooB-o=hyp3WB($qHm`rZ{pA?4 zo=iwN?ymYcLf_g+bhP*xH>&P1@Yo$?0_{->*tw&oWDu)rF6X)Tllrxy69CU-(kr?c$oQQ@fy zvlj3geaF9gD3teIzQ>t71(I?Uu5&NBXy42CC4jCymNct80B@GsQV75yu@qma(igv;x!2u; z9OFijPo)a_c5_8Y*6c=>rBIHMls|VY&ZP17S`-U1bI*z!3i!vA&mBrGAYhDpA zGj``deroQiAUfnRru|t{`+keR;BNT&_VqRTsA58KsEL%9H8NAh4gs?r6kH|^fffe> zWGvjw0`-pT5K5!}C(QBXOGJ4`kYmIk*fDCRpg%+y&K8?6J!*}S5)N?qhC(KN1KD`) z_EY#d6Wl%yb*k`jCyXIK`DR*RI|jIp=Kx#KCNiVlrr}c_#~ji)h$&6)$G!TKWTEWL!E#f?zbDF ztF>no_-UxdPYNibmDqO4286V|;$pX)TpXMs$!I@mT5m(0$edu8-%4IeOV*1gD`6_J zYKb)t&r_Wop+(&4obPnB6omP5*n|bbPny^Vp-x~dM-fbPV-^j-2y!ZERszqqtIioGKa9Ih{&L4NNn0>k6#Cc zGVy=H9ACafGiV-18FLiwa|Z3nifME&g{*H#6LAh8^QQl%u|u3!|GgzUbIdhG&yY$N+|9dwX%Pzyv-if)rx;?5^XE+I zV3Tx4zr289*U})GPUk}j+fHpY>T z;I%?1Q{%)byj$rUKxJ@haUs{PXn~K0+qPtIj6>Rw4!?i&UG0DUBlZ#;r}8ZpFPtq1 z=awLepTiBY);jb~6mN05o{?AS#kd@b0&C6AiPO)S)0s^5pq<1T&Bbl zk`mS?nATAN!I~y=&?97r|2M9;T7u~Zr*&kR?=JM4yS?f-iA^u9Y6Wz0Zc%(Kv0^kT zZQT|JKuWd5ya{3z0d}hI2bZ^$PPv~lqr*Bsr%8POl$b+63`hu^J2(7KTtdP-L~gQu ztBxyuZWVHIg-#CYibCq)r_AZ?x7#p;we)MQS|=`(qbSn1fx@s~R2q)1NUt=c08jT5 zo?0DSWqs2hMYVUg?XG^?wGls9;}pXn0$6u}NUKjmGmFi&DryVQ+Fc_NVz>ACZOe3p z2B&YATh)*IX%pH@0yYn(Q`RjTHC!dIXIidey{FvM6yl$8-3HYY=?O#CG) zt}Vs)*?z*r-h4d-8K}fZc*0a%<8XgaB+#}Cqa(poeIN8}d(ozeG|@zxZ(ug+>d&0h z>-UB7mu)yb3rZNo%DLF;Ll7*}b)kSHTA66Pv#1km(OI{DPQgZ3?N6B7L%+K}nP-i$ zp|3etG<51@ZTp^)L9Dq@z8DgMpuJBF!B?n;2_8k$sQF$XK2{JiG$K&YbkfAAb2Zzdy|Jm(w5oo3^vH9PN5j{@<&k zYKbtWzxr?Y^Y*{|FaP7}zaDOF>z{H3iX;X3hU=Y_$-PnzGktl*bpL5uyD$LnCfpY3wtSosL&`e{qtY=VfMS*@n+@G^CMEFjLUVn^K=#Nvb4f{ zm1b>$uMG{pKEw1&@WHPs9<6Wg(+Km^^LnorXm1uew8oq-7SDvD80r);Y)(!DFCs;j zQIYDJM&YWvgyJY(*rv@&TlkDGsv~@Gp8=$QGBHjl5k|dzA{VDP7jQ-i+_Z_|&`u2d zqOTzkJtjgbBG7zVYo=apnF}1vb|^*}>z|qzX0?33H?#WxE&6`c()IQORt$0(Dn>zJ z?&O6AbE-;q(?(5>7F0{=X*AgLiLZhNX?Id89POWuqP8Pv?$)1k@XU|2-Z5B%^^->DJR63M1&F( zC{uQNE@*DVk7o~x;;tpS8f^(bF!Z~AT$ILpq{9#^>rRVO%Mh<5yE?LHMd+UhV|TSd zJ%3&Sy*sXDgt2~L_*Z^hYl=hSZmd)d4~RICP%m!K%0cPBYEeRo(q=jp^*Dz)tw!bd zQZEgBtkFBC%-3Ih)zX5WW6=@ou^drEL7%qJw2k9iSSW)D6w#fSJqu&-L2!L9;l1Gx zoddhc-Pr~EV10Ko!&=`!}N1|p?=t04A65`B47}_KRUsJ9h z4tn%hJXIv#K2kG%wyH=9%Txi1fnd*y!BL%H-_1>yxj~y&d@zWrM5S6iA6ihjE^6nQ z4@W)jr<05Eyt@D1ZcYZqN2|SR7ggz=iP?!E`;?kD=IJC$AtJ`EI)XA}Bgdy}ZyaA4d> zrRg=ycsBO)W&O^mdbH&1GQkdR@dTc=ZEI5yv7)(H@D5vRbSTZx#U{E)^Vg_7&(~dg z$45K0s69M;_^bOb^Y;30SAY9@bM@=I6R~<1R;82`d^uh`cm`R~u+sSfyg3-7q6S%6A}%<;Gp~Z9OV1$_2Xam&Yb5>IwFJDuS+$vmORD|Uf6cESp`J| z=3*(3KrTo0IxGMnw{2)g=$iy;Z5rkB_H|RlhvOojeyUfC3dwqhzJrTd_~$z`o}Lbx#WQSa}W6Ww0;zs>=sYzmph7rb(v7s4648@oEtOxm}d_Cl<#8O2a^gls_ zTeum^^jX^8eq`fDxkinHDkiSFDA6AHX{I*Ir8ykCK6bvuE2Aw-pIp$=3q@Ic z6qM5QVlx?m1yO6y{jUDo#)a@h7NxLO^DDN!ZhLq$^qx<4!zxJ2~+O=3C*W3wO!vRuszA28}a3i z%5upoL*u7Vn>Jsm#La-Ah1D?7r4Ze3cJseTn9pWkCa z!H~2{Pm6_eDciN0V+x3linSp5qU;u2bs^fO7{9sJUiW)`G?9mSGk1T(^A0#0W@ueC z`K}$;U2PwQ-?JOAe3Y!!7fN`Y}e2 zdDy<8X}MR4X5<~R_SNRx#K&ZmZ5DxO69Nt5F6B!L;2lYuCvbBF77K$Cq+J@a2}vO( zx4w%FPGb!P1)^aI)3+8Am$I#gZ z!o^$5kQ;EJ-bS&wKxdvfCc@}l&q)DJ87}&l_S@n zrx!7;xRW^T|0rr>#cUOV$9}hzILZ6-864VC&amS@PI`@yQ}KP#m0bPaT?J;tt7}IT^9r~vJeOFjH%CMer#%b%#UPMXZDTPl-`#yzd5vUPf<+Cu3=BxZG+Q0r_~ z8%=nRe(a>=+m$4W(y*VG$KK!fJ2B5A=MaT0qIQ(1tP$MS$Bkj~YMbz#GMhCNQKTs( z90jp`Q(S<@+x&@<&sgV=r@^m~q5u6_3*+&vl;a9E^;t_zL5X(nS6IWeFp>Nc9@u5v zccb~13KGD|eLlpQIU=qze_~4ST4S78=+*jm{d_*kL1FE88$E2Im{TwqSr(de={Z5L zFyR8O)L9gYr@&c3@rq8l7}qD}v!B|%$d4yHl&FOg7&IaP;*%T_Do<1LoavAdB#sl; zoY}Wx22E@Z5r>*N*KeQrJh+1%$3Nqf7aRnIv9-6kD9wuhSY#4Cr-|Agb%@?0+HCxC z#td{R(yA$He@0iWEAfY>a88o?hr4l}14V@bA7i>o^F4jFt#A6+p0-;QNajWz3p#&7 z>Ph)qR51+X_^~khp*cJ?gYT}sXYZ*H6e71)Jz|wOO72JL^fZW4#?V{ral*)7j+ng)vL6jYd*T+-jelJ_yWf6sxe95^_MpwroXNfO8UU9Km-x zaGh)A6O(wlf;kI9coV8w#d98lp0s$VZybOHx=aEQx-Q8EBPmaR^yCz5FGpUSS30{q z_?O4W`?K;Mexq$B{X}uVj_E@W$~{f5(~5}&p_3MgG*mne*EdX(5Iv62#m^14ld^NL zF`QooZEiu{sx+`Uicj;tzC9HPTaBnnDJFBs7#4m1F%w=iuUNS5lhD zf>WW~^<j zt!>f@F6^61D~3z^qEI5HU@z9wU@N7W|4hfwyadH2*#N{lBV=!W5!T@u>yWEQN!9Uk1Dpi5Z>E`w4cMAa}-Sq;aSM4)8Zp? z=sDF71i02oUUIOKrnD;1An7vt3?_{H}8R|=9LyTC_aCpQ?ytQD`uV1E5U+g zGYVu5UH7hl+K}O6N1M_eQWc*V8a2o7_iv8ZC|TIJe%Jd{KHg|$-aJC?X4*`rzlIgz z(W5JEXw)gRABZ#59DL>#KaT6WH>x?J6KIAlnx_#T;M`EOHDYDsI;M8C9tIB9t;a^3 z_%hq>Rt1*+^KnliwOZ3>;S^C)AGR~Ao-<_u;$`;hep(6^ej?a@;RDrae)m<&P4U{J z9oz+An%@2rl8!$elykNV`}?`)+SR(AH)B7{c}4Lza?lD>XgrBLH{~U*Ms52kl%P$g?_D#Rv1Pxd zZRom)Kv-e;wNk(E8J-DuJ+b}yHxS#i3oI0)J6JCy>kG2E+eDqL)*+m@ZC>=iD7__vA3@C?COb&x22=AH^iQqSt%wE> zG!?hc41R*j=SiI1f+o`2$rgc6Ex?ZSgwhx$qB!7E|ByAeltp-o7YZX+I~S`nHyB6Z+>C9 zp_h2QAK~47f5fZypbCBVJjvQl)R>EwD}0y8CGgx_i(5r4N`ZZV(na1B1Ta0wTj>IV zcv_hYRQ3ML9}m|!g&ec@vvVBp!)dpVf`|ud*GQ3xnJkAv{i2;;6oi6Zr**d<9yqOi zYq1T9%A{6g`ocNd0lz#O_)r@00+tSCM0&^dB^AgFhRzB&)A$WRa8GZyAiBr+8#6nf z<1i_2m0J`97k_XDFE{$?D^{B;#Qslvbm;PJfzds-R!Jg9X zvHI>%r}AWN8iT%}Qp`Z>VPK?~ipMWm5LUoZzrvOza))Mf6{;VjxjIX5m_S|}9MjM7 zG&Uy@PdPRP4n;+}V&ZkRe!qrwZPQ<2g;yT1&FCEOBZ;QB} zpU04Q+y-Y&xLm#BjyB%T#RP9Hy}6hKt=yV&DD7uk(xkBRwA9+sxS>Fi73nWbV286f zPM!5>XDZ#G1MXbeY}q2tom@}~+jT`0tGG5cuG0Dv=q-h~jF7G%ZMqQUUWxF%U@oV< zo~?!cWj%<) z&#q6qg_gHn9!|k-kW-3NIHFJ&r6(Ii-1S@NlxDs2q}A8sg_(T6zG~k%q<$6a5UCbR z6L=D=Q?jG_K>2k94jzZHDr!Vrn*_A$@rRCxho%h+2=H_JP5YAQ^pzTZMn<^7BEOEb~< z_h&7$6eHf|6B8QI3A51%$yb}{OdEq|m(q2Lu6ka%y5ndLJtwVn*RHa0qMLDfttfE}C-MxFr_<aoUo*d zwvZxOq}IOpLe-}L9#s+oGkZ?$dx4lu^ciV=<{*y$Uch_lk$6n4hwyWdt~d?t5}$L^ znqFx_o)_KCT1{mt+{cord!*4y8$*ATP0}A3oIW{Zc<;30$>r&I-y)j-<$SC;v3(PG z>AUQdt3t|%>^AyK1)Hiesner&oA9JC~2`4E}ZQmKIkE3@?q9 z5;lumVhku?#%D_)Ib=ICjqh4asrzjTEz*A;Id&YS?GA67cK>_W{gg9K#SfsCH~TnG zNI}K>vnrHnaq2}U$_Z+(;&2=Ho)f14k5^Zx=C@;pIYJcB@fXM4yC$Bs2%OKZ2hN)! zK3EBF;$IkTW&vTZm6SCAR<#km_hM?>dfV4Lp=1g}y?Zk9a^SZHb((wdYl#oqh-y;I z8G|f8VnW0ii+AOR9HXI?SxG(j4Qba}eaJ0d9PxPI^~XuJ`RcFpk0+1>%umsjr1&@V zCY^1Y(o)`UlxTm|LZ%TwFl~mUJjJQLsF4rYVVd{$?n|A`+C&P^wysCh`OW*d3w3tw zpNJz`78oDMyHKSV;HLpFq{tasp`OcbT4*b5cKgJb_xGow@T-0^>+RVNuPfL^WIqIm zm7+&m6ihMz{C3D(5*i9FHd^1r^>Q<`|Fc_dd(cE zG{Jq`hO~RPY!rz_Pm11ff(!^f(0!ITfT9y-M*SHA3ElU0c@i&8O_u(hnGvg1D9p&` zX_sa}M66(v!j>zP6gX|_rr%B*Zt-nDors(9?&5eaqUL@5rhtN;>$3u)P>|>rrK8^% zrR*NW@x%h?fa1o~?z&QnS-#V~Iz@lAydC#=$ZKDo!V4{3efxTIhqLBEKId3Wf=wn? zHgxqZ?h%ZBV>pmYQXm@~b1B%ZD%jAe;ZkVZ6!*MxhH?ILlQ>H1lD>`>%IBixiu-j# ztz=oq3dXqEQ_gVQ_gbq_POYi7GtgAL>vAi6!dirANr*Hs>#BKln?HixrsB&>E zlz&YZ_TB?*K#`43ZV486B=<&*E)fM=nZ_^Nq^%e~O9LjPj__pl09FATbP27EZnG6Kh ztTMfAWiwE3OD`~O!M(Jk*kF6d$s~&^#k#vFBEqbdY2YyL0_FEcf4sZ9Y4x-dr`UjY zyMnM-f1xtMVR?j7zxCBwK6l1V#ADBhN0A;|m{!rzBW?AGK@W~rU#@A8`a^+$X4|t> zB&R$^6DwjOuDXmn<%af>b$fGku2$Rxqn?b+HGbqHZ^jDzgwH+mVR-tp>KU|R9$w>2 zbYJQNiUE7SxlKWNf#Adx>3XJm$>VfR4?{I;VnY>{pump~`w~mmvh;=%y7*W?DD@hI z-WJMzdQN&xTWkcM(mY-gNOA5k#5FT*|MciDeCw4+wWb9yDD|)$_Ult-YLb-oi@vK( zJ>#t|ptrTm4Zdykcmnrq_|Z{cJ~G!i9eFK%;kbKGL(p@GZP5OnRg5C{yk%j`Ny&20 znDi;M46Z=p>}$ImZ09ZG?{hep?tQ;LFM~z}fnwRD?Q5$d#e$SmByI@BE_B7T!Zqp; zn@M{?p=r&~t);DZIqRJj&X2d+d$j-laJq&(F9|CGTq;+)oi0mT1C3|BiA?o`^|E5C ziIp~Aap!EcI4UjD#6kMT{(*^n|N8Z*pcZ*-D?TSQjU-7ZvCU`(?Vw#;Bh@xHpT#fs zHp(2UR|Xi=G*W8s>c#P&$|jE(YzIPLfkAJ2=ER{}q?4&x9}YI$iw-$qFa2*q7JG;o z=+9c=qB0GtzzZ&mP?GfHlX+WID%fR}-?+ygmVD|hzS3n10SO?9UN{E|*d8j!bs%9YLvuG$2SGH|qy#^f^%Cx_M=2 zTHboxS55qZ$vn>cmzenk)D7|`iAHZRE@y|Zfqt^~#@tmwOLEa@cZhj%VeU}qR<`!o z&5X7R!cLd?Vc;6$G8 z&JIxRlsy(lHx@`bw>Ugj+kzy$D^%$Da}{AyoX|v+S%c{%)Y{O#=VHmm%h>~8if_L` z6OMTz0vEpP~~PRi!%n-L>E3c{xM4G6U^tA zDWd$^@|0wFGnJ$I*jQ?51c4(#ePBIAP zZw*!)IRn_*!79@QOdq(T#xAo}pEo?ugjom4Gg-VlE{=*!=@T!yyRW}KU-w(FqkFCQ zO}l+W5l=*hYPO~zS<*wm&|Vo@F)5om6d}01#D&3))Whe7KW4@!G_Lh4niIK^%*wn) zz+cQlZ?ygZdAQEd=e6HBj4;ZRIeOh((Ch2MuO(SB|ofB*U)r@4I7H+VX!!h9P` z5y9Xr(k;v$xhNADC<4-6K#d3|=7$QGzKznwKqF++g49>K{2E?VMx8J1k0%7DEWF{a z5_}1xC)7XOXoAb4h`#fb;tmdFA(w>O+Z3Wxz7otAx>sB(mkIgP3E2@F;Y6f-^zH89 zbqLg|6I7j?%{jh&4q4nbLlS9kJMr8Z=Nik2P_{0#U_%$B!l!IDk$xNTjde;MNUF`1N383UWA7@xuO&|s#e`XS zWGF<671(a#;8wwf;5+>tR?DHIZ)3GRh*7!rO-s0o^rz20_Xmu2?=(&5GCEx65>A*3 ziykYlDeNi^we*}ISA}VsN|6InOluP%qV&np_aoe2zg>^r`S4^us_)I6lt7w7;LhhK zQHLw$DsEj`Q=(o<|vL>j%Fj5p~n43P5=46ZN!#>;$05p=gUhjUn1E_If-GIw2W zqpWBm6;@N$svPLZDo7%*W^AE<_wwkc)_})5LG!y`|2Sl>)6Tb|0(51TjhmY;Hg9|8 zNIzt4^w#&SxXpou+;jr+yBv!n3qp4Cm>N)Eq;pAhRG`pvfGEB| zL9yin2*Rfl6bt+=jrfVv`ePw&yWX!5&$lNp>WP(VLa-v8A}J#5IGKtG*OsVo82S*g z&aLY0V>_+Yp_L(m(zH8Eb3TP1oW#R19iJO-#YPh9#WiWIQ>2cryf@m=;$g*dZ>Bxz zmR2H7NhJ_;V>>TinwB% z24m!aXa`=o+K4{H%V6Z`kmlGDeU{@n#UBOl6iski1_C2C#RMfZ0!brqo`?X{YN)r< zYoQpdy@%13P}yet<^A`%=I#toUY#1HFzJ#I46zCcwQMbG5!yzXCyrTe*XJN1Deq8a z7Y(q?>ML@f^NBC=hhB!D^4;xQSf<-woxCDy^-?fJ6ESaexLpi^Rn4oOsG8AjV^L{ zQV@l~08v1$zp^^#loBnnsef+#18u6K-am!IJg9`KTJVB2$Rvu9Z(50>kyPS{ASYVH zQ0WQ1X*)!{BrEzNc*^5n9GTYEz0~bcb9wgaoT5V<-a170xgoT@jfD__M|&w`iXG4i z!$r^+g+*IUrVjzf+RqGoEb=^O{g-Do75^!H>L8e{g{PJ5(4@fsEFe$O_df8v)vizP z3vy(T1L{$Q5SII)8N6SuwDl;=;WsF1iJ6ALG7Ct{G&(wxSUb&S%_f1;O>}L<8T9T` zuLyl|(+-xPiEdkA{z_(RUnoN|(utDar7gd6_R_siv#mOfAL;|J#O zRL}LSK2Y1ZZYl8N#w^MUdRE9_K>@>V7M|DHOx)9a5KBSp@I zMduN{5(lZuxq^UZVhbZ8>g@7Xdr$9PA49+`#OCNGj+c`U=Y=Q9hZhz1sOdHVaxHkR zr2@$xjVKdlUKpw;LTpS$L`6lKFrr|&5}!UuRR-{%z+DV`Qbc-L!=dC#o31Mkz@wZq$t@62 zD_&h<(f3wb?btsc_nC|Mh+dK~27L+dEww|*WdAGm_20kU?nkooRYQf>x20dlBZefD zsT?bU)r&D+L{saIo4PE>v8ZS(91d6RWw=6_#>S6_eG+o*_2#-Awa3mt^W`*leJ_W) zYt{-^oNIQ_EWL`hlisYN??Sy|(Pd!DwEI1kcfve#n0Jx?v6lWoZPBCpL*M$)1*7;dRcSbcGvA<#K|D^ql|iizl5#8oLNGI7n!J^Ijli1b*QGWv%zxViq7j?u%- z;C=mrQ%@8=p0(2X%(*8(Cpyl(g<@^o)npXv;w0w_@tSS3O(A!Q|KVUGBpRt4&ObKv zDb1WioD!=(E8ZeTdyTfqgZ!r!SzT0vJFlfkkDaJUQtowI>!vCYqwIcZ@cmw3FPVK4 z-L0ZT^TE-rEtDJP=JwYb(N@tq6zk*sF2~j3;fS+UaR4*2DX4~B8v59@`ZlZR-6@O= z?XaoX5gmu^V&4NBR4slw*h-*$*x@A1MsU-LiKCpq<&PKJ?d{#;ULo_;ym9WJq9&D- zjD-_%Yhth4))%KK3YaL(ia*j^p@BrR@OLbDJEDtbQ~hwrQ&G?hy~6{&d`@FfrW#&9 zIM?B_RyVq;qU@t6k*p$Uv(3E%&{(`7nquq5p}!9SMW1|e{9QTeYVQT_y93$t>jg`+ zM$2JKa0D;-s30a%WisB9`lYeO(I?t>s~Wu!N_Lgkrq$LOpB(+^`sJ32&{s7ngVXXY zD2xOXI6A6Bli7{fs&|UB`p98L&8if1wqqVjV{28!IeU(uoy3!>Fb#y~!h=LPnv$~C zUg%W=;Z%nyUeiXi#C+w_FoO_;wzU}dNWI~QihXOiIQVg`zm9KA;r5xTR$RFDX$pGi zVuJ`AGMq*k4X52vg=?zSVOOiQdI}Ab73Gi))P8Fv z#fE-Zw_HZ+KSEJ5)AZ{#7Q^5*u{{nn|>koK})hRNXvH5sF+>L{(d` z4CG`*UsJ@dO=u}CYyrx;I5fq!l(^iyh-uO6w{jFaO@+st^dIzjo4D;-1Zn>*EqU9= zp+_Qe|GkN2i)_9tUwjz$;fvymlE#00f0CI+?3C?q2-ZC{KtDeGm&EXuOL=T@xI zcTPrzi3s*-kNb_1hS>Z1IA=aQ%&+a{j~6IH-?-0bM^j3|1=W&4RVOz@RPw{tma!>s zc#0~zh%VJA_ReM-2POES0O0NR@!;p?ySs0%v!y6jr-!(5hm4uj(8diNg50p#GF!C1 zhH8uzY;l3Yku18b1nadZKHfp!AMdnxulq4sf4jSxZ#J`j-dW)z?s~986kOb^2bTCK z1w#l*Y?Zvlg*hFvs9`9aXn_GHtYW`=x6Jw25@)E+$a+5XFmYj2(5)kFm<3z2@P2`+6->Qm-FhcGk>ojbKEp!cwRb z9HV^672#E35Cq3;-i=8S%3D3Oq_~aV0Q9Kl@$s;y*2}AtxArk(30fmoMAb+Sgc4jF zb~6hOq?8}L(Kz99 zD%4h{Q^-)fN5A{nN)aziG`$W-|EI{vxIE%BiNP0Z+87Z$(I!2d<4)grRMrKzHf@(p zNn$DiI(h?N=i6#XlEV@XLZ?yM*>48`U_ZMxQM&uA)E0dL$L zDgvIEnrLV1O?>=8pE_tU;d=u$#n2+2I-6EwS}8_k>^VLYTXD;nwk-jj%JrfXycDTL z3OF^r^9;lI#JK-nf2*%fixq(MYF7rSkGZ1HRbXCA8`w>FW zUi{2Ur&MMyr>*_3^LCt1ER<;Wq&k`+^Oi))H+)LtgqN8utaQtQjYN^L1!Q=4aMP#Sq@<(Gm8GW?t z=ezc-S>@0$5b<~T8c+wEX-?Eu$t5r0I^h}tCcRUE3$bt-l{PcUJX-GM;;<*N#S?DP z+7AfriT;V~HY%R6^-Oq*YWe67smE-%CwseF7eo0MikM>l@zdj-H4WahA$Fx4k!a^8 z+Dkk2&?>g0PluHhQZa)rDWOS`l#PP-hGK=5_U24q7pHLI2FF|Vb1!t>Lfnd8U*cX) zIbmp5#z-x!-e+}`*N9h6T1BEVJkK{_U?8U20NNeoYXz z`f!3TT|a%r=soK7jYft*sf<9}G=|zaM^7R;cRl0XG-*iem&R4(J$SEP-}d_L_Qk0d zz#<9*j!3I7m-X7iVfbeC>8TIl-fke1n;M)m|2$QJ1C|PJv#5-+&HsHCl|0R@**gXiKh}hw%xamg=ICmvn z*94uz(7vq^`Q<8v9x8BiiRqXtMA-Q@!2>v{Np{4QeW}Gj;=O$pTkGqybPaU^V zBqJ8)U#OdM1u=Z5SwbN+Xsq79VH2FO*d(F;lsEdtIw9yuq zV4RObO$zoeML|vo$YVR$^|uKoW555^!b|gWBP*Wl<;eHfbDZmKMX;yQI?`S%Vq;bm z#cbkc6unHEki-IGp_MSh2TK=5!_9RGWq-^FUk!ZRV%$CcR^PMyYx>SXn`~1Ot-%Dr z>(MJus5dRr+lu%&^%x>pqW_9Hb}XaSK%t08<&Vwd*%A6VwsO1L#b8oVmbBg}c5hT; zF0Q+26lA9`Kb8^CvbZD{gmxi4E_6yI&5y-DKN2d3B6A}=@1YW zlGkSt$H2^VQH#X*t0|)+&#y(Yv^1x4pPI`NYG2)GJ6-+$t=`}3H-`w%bH0NjQ)i7~ zQ>G0^C03`q-sC1&Ar`d4z*AOWi}K8XMaBO|+PgK!k!)9*zliGayZfU?t(iGC-O@2h zZEf>J^D7Az2~Yr8r%Jzm))$FPBmgFm)U>IM#mj+5guCy(miyjoEhrIZkolJ$7NsoJTG|IT0Zaaq#wZPEAw#T*WaznrSDm*(6 zEx=9<&wfLvM)OfOuEsI9S1P4m41NI}>il(GCs`eO{UfckN?n z?r$ERR-hM1?qi2P^n(aw^)9r|eSK7%9(2$uL3BjQf>)B%c$vCU1!&Qm>qq^5^JwBD zE`ERW$8r1^c{xMPrR|D7qP^mQTJM_txr?zzbcnENP!7>H%p3HFqeKoTt{ngNl$lT7 zUHifKNnn~Yyp^U%TGN6Tvh2?G-RdP zxaB%sf+R#B+lIn-6dphl&_$|5Y@tefF5kLOe(8f?slee(qM6mq=C@XU`B9K{8_z#S zn=|5EkhBdH@uzr2%40bCysMHjCo`2IL0qpl!E*6Z1?#F688OWLD3Y!ml z$c!Se^`1QeL>nt9E{ob_<<3bxDJF-aoFYPaEeY9*j&A+xhxvWpf8T?|F*Q&lU1q~PGlzw|`MCZ=zv)>pe+ zNqs+TXaX+!CgKSmWr&2qm&U8u)s&iOcEwS(*WK$+DJ#TZ8vJ$CNRaz?x9GiaM!^#}*d{Bi!r55dSU=Q)ZSU?qO* zx>112xgb$HlJc|?r?vonUAECaH}nJ^<8=M|sT^7%HNP_Y<@lg+lhWkC_Cgeat|GzH zj0|4#Lz9W-F+G4H(%TxFWR zeDsBuhAJkDd64iy=8{x9#^j9m4A$~4hAT(^K6kUZ`}yUt{y*P*JH?1oCv@F%9e(qf zYuLIe3U_^11(P%09w%xNlGT^L<&wwl?me2SM2b0_hZQfMu{h-wHho_jS_~CG zqIFuC2)f7KJT_m7)}5w1At)$#=x)IO%axO|KZig(l?$4X$3mIs$At<4Ndy*ize2^G zP#f?r4$OY3mGI(r+BXfG-syYNBh&O2;=(*=Uil!8qS9~BeFQ8w4id^PP1d&AsZ}wS z*2AC_Z?QLPiE@LQxEnAg7`h=)#*Wv~{R7ime~Ms`RqH*R?^NKR$GbUG_6gorJk;m~ zoSkx^f@fC4zAbVJ>{ zb_l*vdjn!*Dq;)V5!WVw*Ab5q2h)DxhS88$!nWmf9rHSrKb$7&buF3A!ZN5^r=mnA zgr(iOD~Y5?qk`rL*|FiQ(p^Ga$Lx2I*`vp-efA4a@fEv`=iCGLAOVp;l{xDQ^K;mkes%P(^+A#BQPB6)n4?fc4~W5(40k&*tG+Dz zCpiW8iZuhQFb=16TwG7M9id9CPxi~tAj)&N8vEmfH@9A>3NDr-xkl&yhC>(!k4hc| zSzYQ3gR*L$&De&baWSsr*tf-Bo`lC!fLCpDCC!CDG}oz0X9^g~`TOBI#)I2vZK2dI zpsg)avOqd_NJ8xN&3N+*k8!N8dp;H07Wwc3ab1xDSG?Vdgpk)u~W)6=0d(R?DG z?EGxQx=*-g{K5nL$B*+}AX+;T_%S5Haw}Q^+f=GFkxO>{r+Y$NT+d7G(gYkNPn7Ry3Za!v&&M}uxr$OO{`9LzdGGrnsxTo;Ni z9>3s-N?S_zNSwQ2Cr!ZSxK z-G_HI&aoC!GI6*1X6%}nGR!;VpFjD|uyO|h`uKwDJI#-JIH64tew`=y~pBckHe&*)6-Nc(ie?T8-1f1Bu3)~iEXHjyKQD1JG2v-t_K^E^<9%TmuU!SrM zx+FuZthk^YygHqUZqNo;P-=;-qUVQRJ7%@HYR*y%dma`}n*Nu^J{5huP}^3rZ?lRs zJ}SnCHOwY>to*0{PH7vv0YIO^2}(cO5HlnNBs`JOUi;+-p<(`X9}`eI(8}|H<&bc4 z(j3s5X9TB3r9Fz;qB$cAnDnvy;pA;qo4ozyu^*Ir_KWt1A77u83gWJ?Vd&D<62x<6 z%LRo;{M8i}{DzCWFQ(Z&K~re0m|u6f?BL_CKgQt*@{^L_w`T{DO>d0JRi`IDO^#ir z#xmK{1t*xiS>loc-`KJV+y{tZOH$%VB6@K>b&+D2K1r+ar5wUS8CN6!CyCrUrMxASRB*Yq}+$5lvc~y zPe4*Tbco`F7zP)1m`7cCEc*3s{P^;kLsdmtTEiQL!v_0y4wGUJKDert8AlTC(1T$h zf%c*TwK5}8I(EHt3x08IF^8}9_S~Eolc1`>aL1*#p(Vepl4DYqHkxgG1`SHcz0S-D zd7^)C>vT5NwSMI(PW<*&u}Rh{kJuAY&|+;ztpP6!rH=)Xrk8x1p$Gjlsz04?Lez12 z3gcGkzw#94OH$Y=ZL z2FR7S6F+&3eOsO`4{1sb;%HbCb6dPNU#CNxzV|AgAH-UQU;$;n@TeKEI`8J5V*+5zTD>8P>Bfi}W(rq6!~M6< z=SMxeU2pFNk2qX*cep~Qw#UC&Im_vjP$Ng{)fV)7QK~ewI-qZ^|Kg}8ueKLPw>>2V zKpHkj1RT>cE(i zycW0(#BxRzK#J5QZQ2;g%~J8Z_0&URBp+;OL4QnDs@~rUG#Qbk>n7r{g&(Yb`@n_K z3(BmYz2U-+`gHMuQm`aD;xv}Kg8R-*e!AO^xd)2}?k0W=>Eb@p?n z<6>|Lu}P%6s-UwrnqOgQ(488YWKjPrm{sIWn@^!~6RG3aTu|QU{!5SXJS2Qpq&}{R z##MO7_jghVM?G%aEKb-$&TgUgV>SvR3vmE3>8g~KL}&=*`M>-SKlVR6X_{nHwE{HX zVnrHcFSAU7^?gRR1@zanSOO znVfO#kmhv6fbw0mUgA430Nfv|f`hs@u8| zdDy1nk`r_@lsE`v>QAg9$wn{CuRp^Rz0-Rk0lb{ir$*^Gl-xb~)7+p_bW?B+Q#fnJ zihk}5@ip!w_OqLavk@DP5uSAVePll7cm>q{*eNK-OOHk}x+AhFt=& z9?zMjOnc?{`@!}xIDDJ;d#yh*n1|Dr)0SLq+Tif-_D$rG(>>xs#Jp9%1pone{ys8bd7bYIijZ0i9aQZIm#v=(JgJPXgo&L;_zGuxT+x7 zgx>>7`#nadm^rU}niGV3-tbVof}fp&tF;M}HSK>xhSuzd{mu}dv_dIFt=x6e0f|nM zHkD?unQKQMKl-CG-XUiljd!2Q@$jzIP6R+e_k8t2^%!6R8L+rQEC?U7u!>bQnN<7+ z%8nFAOS|?#ju-0BKSP?8u8u0Uuy*v=EPmk|73|&6cG|b9a1l6y-HILyi#O9K{6L%L zy3r3vvT@MNLVXD-4QC!J?WdkiEGoHOq&y-I)wDCKD7WGSCdXo-Eh%qRO6BaIjjOawmzxWW3HK{jyQRs6ka`AjWYsFKviRO#kDGy+E zIB-W6Vk-)NV=vA1%jsj_IVG;0ht;nu{nC>>D;{{sxf4e7+JH6EaW9CVU-m|(xkULE zhf+9PfG~?9sHaf3iGC$R5C3)@exn)ldHJ32DOTvBj+jL(1>9u3LL$=9NYmA5D9zLO zr6dF{ieOwEY#5~rXMc6r7rRmWx)j0BVUblpMrjr!yy{!s8$T&HbBa=US`1OH-5OnM zK}ka7t6mqCpDGo`&BON{vOUkw^z}4nu{%ToTx^;`uBiBI1cBx@IpNWF^7?b1!VEMY z4c`S|?Hc~dr}$jIy$nX$`-%JKy@gr?7VpK9T-yTWp3Nx(=vh)0yQNxuG?b)j60J4L zjpI6C@IpEEIPq&m`~}CDimFt2tzKplV%Dltw&^}`81DjV3AJOghb5zaN3$L6HgA<6x`M)*oW2@YX_LtMMk6n3Lvy0nv0eA|PaRq>gHKx+?&;fF z0j$`@h*JRv95A)xf~H+?L`i72S_|SkB5n@JsW7a$vLfbb@Shy|Kd11cqQY(~B7Mjn z(3++AE&w3()56mFE7%j8h33xM6b%)!5zOpCA+-G3^E@4*I1y&S@zl{n73@-aR$@*` z`>r>1m6}1E(AA~j+XM$DT7n~u=@TWf*Nv-O_uFGn!pUl_??S!eKu+OZ?^DV8o@yv% z1Y=|LQ_K+=Kd$^@sx$`R>(vCMufsz>ZaWgOCoQ@2S$N42a;6&r%Tq@z7|w0`(RT7x zSysOf`ul{#SrJ^;qlirjw5Pn3uScoB_&|?G3CEMZ7Y~#6s}4gIPTEhHHu00?8)vUL zoM!`aOg*=4>Y>s8D^8hnLq(xpc{lerDoqua#z!|!-U;~4VYMrIuGAb9m(h1q*1_V% z2^2Vi1wD*l`hNC`z$M?MatQ8zaop!yQVe`(WGxEObQ~GR*t?C<+EU6DDb&Yat>gEm zRCTZ%OSy|sTQLaxi-R9E{Es*wI*dz(uqg^|h|t173Hm*)knz>YOWz!kD6M>4rVtg! zAB%kai=)F&cDKwI$IBuiYmNQr*qG(o#0!_h0hcSG--#0^^h~L|FSrdTHJD*%fjeLM z4UT7c&+}2%mXA3h$`RTYvt~izSqR8$3dF-in#I-xhMS1f$Ub7pxmzN7YDV0(V}IQX zukX44;UX54@T6R^6^D;ka6zPlYtB(bm;}0fN_2{JTz`Xjg4lZ~;%fti2OJ4uk=H-M zu~hT;?j2RVy;)aCM{h0;(7Yalo>DwQ6dO4`5cbdqPg;ZeSeYVY;nB_6>j(acY<8VF zA~=2-@HgYr{l7h9fJC4 zO7vA*SRpth!EEiivDH|FLX37{)!#bssdBMX(55qDQig!8N4h-sH%E=$Z#TEMPibc^ z+_Xk_pj$;8v5Wwy*N-)E2^4y~)J8`B0V4xHp~eX*KJC9!-WX2{`?pTuHrMGQ75i8=U6*+Uca{TXqyZiY2q5A2OeV@KM z{r_Ch!QVhTn&PY^U&MQ~juh*r*e-P~=(Zs0I0QADc!~?+M)7jeJ@qo-yrE^YxOGt^ zb9lfgJ4C+)v?Fa3#w$$#TCsdi=Y=-c7B}t@N7rt@I0CK{80~x{Se`Xu-)oV7*V~>+ zpNF2KqInHkFUVmQM5wc1mAEwS>?uz06u{L@U|Y{NunAeznhEySie^iEuL~oy#{}8(ssua4XGkz#@h-GMcEY30d zD*?Uu#ue4`r#M%bdMWXN8Rtq0W=(-i@gmkJR_7=;v?Ae==*@kYr|3qXAMqUSxSK7T z*JHf#2tU5w);ACRP0{+bFnX`l09Hj+L_2)cWJK!lI@cmqC}6=RthoZZXV~UNO~>W2 zkDaY2DZF#p!Q;o@w8kkaL~4W%E5{H+SnWAueJnqXFR46^{vrw zzC3<;ZG_X89-L7)?}ddL=cieazV>89tU}&9c>7R{9|g}y1v`w;a^L#47k|ZT#(Q|? z^0v*H#Eg`k#nrWJQ5xd(x>KfCC>e{i-VCKH7D3z{5ljXx#I6T?dqi!mm*r}dXvgY> zy+9!;R4nzHp)Q6+6|scWmY*nxku-XBLP%qyWX;s#-Wl-G+wYYS?YYYk0zkz_t~9d~ zUeM+h>Q$(0N$Xc@7`7%@kb*$^oTe^eND*b!#WWpneC6Fu;Dv{czOb>%@bu(1hfmQG zEEFtOYS_A1If~L&3rsOh!BblYYkFm~f~vc`R*-mo~-8bfoPRvkTt?Mo@GC{5;NFp*o8jsbC$blvD*Z|?8!UMV$LA$&W=5=YOJ zp9z8lpXo~oJ!-QQF&-b##f!5zj8}}ieT-%2D*1hj=AL7}JI$g;=TU zLlHsmS`pB8abU!*w1~YxMdP<+Ek&z2uC=iHUO6!|J_z;mMq+`Pd9>Dk07}4R&NAd?xM1A&eKaP=V zX^FP1SYfD@P#koMl{Qm_EwG71f+(1XPSw_Kc|T?l-1XW+XeGq)&R9={rmtpo3e*T? zDdP632qAi(W33p8BSSIP)JLY0WCZMuTlX6`;|f)jIJGx4)D9zSRv%7evU`EMI2RK1 z%IsRpLz`$&-s|%&p+;ArvfL8f7Rjq3v7&g2Ra)fPU*0_j?XXiJ>-_zUM&wTIZe>7+ z2P)L4?zODK*kb24>V1VWfOKI)A{CeU4GW5FtnJqh`qFS^)y+?_hhhd5qgcwUlA7BL z7wwJWP<(!@sp9uVw9{d?BUX{yn%f&kY5$4G3_ASv4{&a|4e~g<`8JZ?_8HFap zkEcCYutp?MOLzG`kV@@vqUmt;%!1og2fE!{3kQZvW741XFrQmc+}y7FdTQTE!c(On zw!cE(^!TnQH~64gSAa?*Q2~~M{#C6ar%nty_*E#w=$9$eaoiX3y4IE`N``5sB22W( zE_BDOMhsMN@ZGgAHOJ!X=v82}*hOrxo=|$c2;RnQKRz9=#f}8HQ5LqsFd_nOv(R2G zCjG2bG-;;sen`h;Ec-YJFez(?p`^BHeZM^x0_!KLZM4tyBT<^!&;g@acVU~3svoc@ z#4Kd!t>t2dBVZ%OGE8z^@pW7tiOU7S77AgG4LD!F-965uKF=|?;6G9~5d%tv#POF1 zW}~@G@W};5!iY^uY+GY^eQZFYdE&EJ4c_8r+fUT2)^l$F(r3^mAL6chDnF)GH6Y@9o12 zS8O`;S4MHH7@8%tj0H*@)-ZH6K?#+4hVtCho73rQieU8!W;yyHFO93O*}lyEI{lOj zUQ`%}*{r3^GAOf5bPcYekdSAl>`8%lwiz4R)kQchr-CQwtQf3&@vw`ld;Zbqx5Mei zJnJ_b)gNhdZfGZ&EI36ulbw5lD5WMLsw649N47Lhm_dZhI)oP9iOO~yM&Z(jmIi3lxzspUO zhhzAcLL{%1VZ6}(nus08jnZnX%z#K)+z{Q}+J>7Nnn~H0MC?uzFIFKCp}ctcSYh4# z`_0G4J^uU6{oU<(6x(Wxu5t-04k7{;LUQ7IVFd*$A{^cF)U?sEG40gy6LQ!b`OICm zxZ1~j^szo{s*|l0Qr|mdjmaT#ESldbf*V-z%$2}WYb%w@IP8GvlJg2BrnzysNBs4G znqkGU&(B+rdPt$KfRZo<_DHhLvGfQbFZ6xrd#FM^oXO6K?7rBp$Irr2Zr&~r{}XGB zyKm>^2&ttkaH%KQ{j-&RwV`_r8(N}y7y$+$cP;Cs?W^B+A)}>mN>ARfmk$ot&36{p z&6h7fXuEl|$CzJEP4P$$AZcBUi#_4D6rC5ATQqm!RO0Y9T5IE`OY^Q}uyLF!WKdeW zY$CSq?tgnaVEKt4U=;t@wXJhUWtsC6tDP!z;z0sEJF$9UK{PAHS-M+vUhS(RjCnDF z2sD3s%#inYHy0#zu!!B#+QiE?v+d%r)O)aTjp^Cbfkny=GoivRNZ2r?(HuKuOzj(k zf29ljw@0_ML$~46`fk-^a`Z;iZv-P0|^&li{& zZ;wvcjzq=%P5^Q#ic%2mH|`EMym4KN9)AbV-*|;EJtG~-|K;(ic6w3mGP$X*l`bR_HcqWc@e>`WpyMN^wrad}(A&_B9GVlt} z`=Ih6P8=26SE@=DX>ZKg6!&(==4E45UKJ*Mclf8x{p15Jmh<`U)Z*_4{S7rLz=?c$ zK8JFZkbw~rUwsS#5=UK+@L%R8ZbQ%uVFIz?VcuW&H+Ls(4}HiM3%9cg8xy~yEoU|* z6Q8xMMenkufOtQA5e^sG(WimWulW87KiR`Q-n1O5XwRdX#-W5YCeeGZ@SUPQsjI7q8z*h2&1$y* z?KQ)Eb0M-;hNB1X-SM89HeQNYqxwlXiba3e*enW@aoaPZDw|e|K&TCMYu2Kp6%1Wx zX6*{5dery#(UGn{9TZMB90xWFxAYaL-tW!ULLt)V*n5jrH_obhSu_gc za_!!K^*wvMxOB-iMm*bTrW4k8F;b4KE$cL#i*)}Q(Tq9q&T^ZeFJ5jvM~n2Ki`NbM zw1>~U78UdcjjJ{3lMBHsTMKoIDdBb_GGm}TVc4eqTHVAWZOszk^HhCWPRj0`LrXy~tyMO5(*)Mwb%XbM ze}D6+#|-Ov8Xs*n#f#Xj&8RmbFeZplIfq)&E=_Dm8yDC^`6Qaz6wGH|gOwNC#nZ>n z;uT+R`fsP&T1sm_f1TfN`bl$5>{YQ!32xDWmbMu4HqyYY6k4n3;;>A`647hLC)C)) zK=h2EURGK;b3+1%)|)5W(!muFz4&@PzldJ=&!G;G6bIvLwI7;i};o>Ni&EI z`FJ^39L_g)KKbwy*eNEANGsNRELpmdw=lJowZaRMi+!2CHEr%3Iqww^^SvPTJ(Lga zdVAQjg4OxQbKBVaySsI2XfX*#31bH(=D5KM9Q4lcirMpZ$u2sH7HPFb3s)5^5nGIVM>L{y6m@GOQSybDTWNwSK_uQSqc}6) znQd{aS-+kXlQG)U9YX~zCNBg}%N0(Z6Xom@z3*D%TjOL*|HfUG zF5#N^rr&%&^TR%?2|39LqR_u`$5~UbYeT07105_srl|v0O)a{K#h+F^I@bb8ekg^A z41_lZN5D_%>dR9D} zeXZ(ByqG>w(QSjf5hf_hd0-}lSJ>2b)% zh;??8DssxgDOLNxHYds)1nC?apfy8W#2N9l9A&S+Pa=sU=&#A1TX602fWP1M`dn4` zMdh_JjA)Sy!nqDr#4IS1qcY?+(c!5$7ljGZ;Rud)$+&Uli;`d5E?;b(4gJU)4q5pa zyIug^+=EYCgW)D>N7kaEbzh8St=4>9sf}G4&eN9J6jH<~O)#`jiM3u_#RqYh-LK|w zxQzPynS{WpI#iR3S)I6;qeWeNNkOS<7dw2iDn%a#9q?VQGGe}NiXgIr*Nn}*J^V?T z_U?Y{{RgisLP}MFz-zloh)IJ34W&ORG363sI3~-NglWlt(-r z5RIFMz0~N>libvZ82z>%T!a)tePgI zSWt{kZJP`I_gJ0aqQ|L03qT?7*T+YN_K$Bk>b`iCj00x&ML}T0^}0b|>42}=bH3{7 zcH<7Sn#-JKD6=L(6%la?qAnZ5e60^`wy!_Fe7||rFF6Z&7`*1)=mfWonw$4N2gMbK z8SDUZwo#_4TG+x&x6rzy>`Td9k$lHK*Cd-p+Y)(D3po{q9|=*@7hEmy;oi~N@xPz zriTfUeFB+tXz<;9h`%?Q%Vtmg&8YkY||rg4eC=WjiQkFgH5GtgH36+%k0Rl z`Gj3}95c0Rht%AD-}Qb!>yN*mMRV-MH=Ik1^vm(V8{vH}jYR;9pW8A|8~u6hEI zOmPjflzc|DS3kg)`e_HGPqZUU(>-RCf}Th%aW5=p6`j`iE)+PHM|{ErePdtgA)3PF zU7!$`)_LvFkJ)rLQ8+6Mb|&w2b}bdb1qF~Y8d3^L;DT&nE4EicqgffB0FN9Fbq^X` zo9!YJ1+UbH2k}hB(7*L3s|Z})Hl#Zhy6P$jI|%rTa221u@hDk3RBuJ)gK4Jii%?Nj zn@$sS`4r&*kbbARVQ-yzEc!>5>{%sr5PZQ%7jZbD0qrnSa#<5Ez#ztfRjO!w!Hkvy zFf9PVwH^g%mRsY>@%8Z!nt3~xIW98<`brpvBWBI5zG|dDQxRh-Q&Oxf{HT9{$6fA8 ziGvbZDy`60c=O%ADpKzGx_-J9$vrXYRu&6a%tFao(7=V9LqgXXaV@cI(7iswX8mRb zRmHg7rU);r81$XoTVvh}m1t)74mHTeiCYPR3XwXLyFyk-u5sdXPm!azZ1Q?NJ7|!>Hm<4Zh*hqBXTZJvYd;OzZ+@Q<_g7dH{~<24&8D@DJDm?gqQYQlYZl`j+q9Be zL@}IRS%`DNy=t|fHQ29tmIE|WIQev5Z6FX5hd>^8!H%o24c!mTWYF3voXjAUMaR5T z@=S=@B`@UYgVBnN_WF?zEdCXdcH0mq74sSp$v8NiQaypR(+k}>0&o`d=t5$Qn#+M?QhgH(4>6OlobgG-1}aaF4# zxY)f7GjR9eu6c;lL5?2cp+EJAC851Cw2ucKDpQCn2JK=6v4ucG(QlC_(E`yT-9ROj z$9Ki@PTNknW^DR)etu_~xHw638E;ZlG}niZ-cY$C43>n(Y>pg`iJPKPjX=DTI%h1~ zFyi{}jD9F!ItdevU67!ix0%plp~=jt2zgrWK!s7F*D{NO4=dS8lXZ(`iItDhXbx`1 zby~-|AD2VTA)0>w^V@vYtF#Sj;a61DjDoeI*W$lgabCC^wPB$pA%-Go7=1=bP7xw$ zNK^H3qI=_|53UbSJ_H|yKaOcroLh58xKw@?iEqeIYi}vaY$@eHUy0rx&#^=uA!2%V zfJ~ip_Kh`0TiV0p3m>_TYZ?tn`y4A`rX$w9c~wC=_N3j8-1sKA<`Zon!Va3{Qczw^ zf-e^bTST%+`7%l$rzQisJeN=pc5ZRFymv*Ftjs0q7JCh0zn{hDRwL_Ix?hFHfY&UI5J|hjf)0s?|ZRy@`jGuOGfR-@}+6vE=~jo zM=5(*S>VUr#r?wF!@(7&6F=3Lj(h3ley&oqW;$OS6W*@HW(;hjdOF{Q zR0WIZ!n}C;`FNFbcl+hfQ`g~hHzY+uWem$H|3;CYvNW=1-s1+nCTtDi`9Yr+<%zRS z{7r&*k4xin`RAD$D}~lSzJ6oum+giXhHhQLQoNcf6i71wo&vat&_jd8bw=w&AEi># zR%#F)9&{C#%d~2zkKgClqf?zY&eJ=WT97VhEem~20TFGhN<;bzJu5V?)%W*9-Nmh} zIJN0Ha-GyCUe@Zc_TLUZ=4VCK$Iq=#xM~wErEW^uuL~Y=YKB4kL|?LVX(%+9 zt@jS4s5^b9rc596#1h(XAe0FDhcjzs$2#UA2O<+fXA`p2D4Nzj!rka&v^O zE@i%<4*CQd%AZ4%FJ@MTLtgEj(eLgRBtJbmNB(~E>6t#X!l8(j5ik-%T+s-3c`a@y zxVM)e`=tb`967|<7rIipg&{^6yx2!;?+ksCJgj;H*kELVLO8g$jrwiq~sx0*Ns) zVA3-0D^V#Z+7lJmh1kUK#dY4ozQK(N1>@~{$p z)6#LZ3vhSQ$JGso88K_Rm5(v(hy#(LU1#u}!5=>FJrg@DeKpBKSO>AMt!ot_G>}Zr zc~?zMb;KneW%aAq#~q5g6T9G%?C@fE>vDoRj{SHT{2A{2<&Z^gA3Fk-2~Gqn83j}B zL4~`-*hBAWQb406I7?BaiTLGWkRY&9BBqHM6-!a1ep$Qn_z?TG&h8a_$R|%ivYx6} zao4JCBR(+@rxX)03gLW-fC9|cX}8q$tV(N8(2R)B8xjBa#{FC0!{_PHplF#D%^}ME znBXG3<)(nY_RQ=#X!^H4io>g#LjGxi|6E-srLD_{k&ok@ZvJ1Lz}i;y%vPwrRFQQz z_5)BsABzwl-sN7&3bC23;aZ~Kv=(P+HIB6E)iQtfadeQE@3IB?SzW)v%s zq@BV6LboC>Y^qHws>HaJF0hN5a5ir4ccpEV%SxP&Lq7a@D^7{%*54+sjx@_ZSrR3^ zQMKqvI=j`SxkcID8$?AEL}@m{4p3==6B;J zi~sA^qi{}hX7_K*_&L)lrz~KEaMBhNGD6Yp=7VqSR*iRe+yDH2_wnZDH2T?DeMM;< zUFJ>1L91wF6fLayl9EkGntlC>z=t+##A_3G920$|CHoESmfJ61pNx{uB{|nB7{Dd` zo&Cy+=aJ5UCJ+!xFSvMlt#Z9$K^D#k+z0vvo&LRj!>j)2d~v!A{fUB!_?Stqwq~Y5 zCRZtTU{GW_HzH;A7>t(inogmCwik>KYzvCVN>rB*%x~{*&rUIqtq|V;+$MrBi$y8t1HzWz~*1ACm(rsLbiZRWd zzMQQvMUJstZ#eUf$k~Ljr<*L*u*5c`nBP-!nZ;amga*_1ixB4GH=2@}G z;jC@k@h2x!Jn_I4+rFGpb7~5Tt6HbG)S!CIvfuNC+rIL@p^tK*!H-Vx}Yn#CE)xmLGQ0yenH@Uc-tMbC=V+yFaVr zR6Z0zJ>rYG+oVUg$Eh0kxM86K;{3{bB4@l~4U{p6A7rk7e2E?1$#yv*JRAFPa>}LC z*|A~DbW;kNihnx9cBZGtgwkc+tc_3}b`tP1pR2rx|o5Y8CnbxKk!>vi`ix=(Ne{y}8JccBQGuyR`#HdE^Y| zm9rUpPO-5-xiF<>dp}|qo)p;z+^mS;gnm`__0Ufj#2IYfeT%ky)!;urE5&z;6^$Ih zvS6yxIdF23hE{Gqw9wvApcj${+5g3In&^5w(fCq=EJQ0KF7wGtg}S~)2d%2YXEsrw=l zE-Ve53pR(+tuTRNz;{@*K#YlPbv{632xjomd@*+Z(vcM#y*vzo#3koSOT2*X9u>?!BDT2JE zO>{|_F{YqJfJZ?dSB&`e?tpz>9J?z-YkhFsp1CU4FiFg>MAXWVvJ&yPU5UyN*hCuj zk)zYnUVaLGZ0`^Jyv~0XE>8kI3!;CHx;?>&-Q*HYDXrF21*h<#E9mfX0X9pg&!Nq!YAnm(8t@ zhp&gDs$CE8#hhA40jTz>xH9z=d`6K3{Jm4=n%r;>*kUvWd<}G;eRTRBMN%(cOdpP4 zi4gaC&Knwsp8*C#oU&|*@g zq}8FUx5u0x6?t#Rk^MdSX0(b6u!hAzOejLg*HIs-@zPRcs`xZUoWAfb=W@)W>}k`M zFY84eEu9XF_+`h!r>9C>gd4a_*c5U%#V17ogqW##t9()$T+>1%jgV$F?+)t<~d9;XD z@hwyh2#$`PwV3ca5i;zVJQ}U??L_reAxbQsCVvsliZcVbNVw~b(OJSL@OFUgPl;Ao zh?CSZu#(aH(uR#9CAwoAo9zBq2A`EgTdo##!?$o0@v+Mv`%k5SoAu|%y>tS=`s8XL zX0f>6)EaX1i_wj;YFcAb(OPp*irthFBjQ#DOHq=OTLLxqGQT`jFz?0F&qWN6&0a6A zBWIprR3TJcev||#Y;5@NwHSiv_Xtz;UzjdpNtUNYfrl3KW+-}gS3Jja5&2hw!yuR7 zXzMYknOj8kmbglS3n^sJ5rr&0M|7eNW`|v#lIXc2E@W>%0kI1v_EEw5{y8tvTrN#E zJ60>HuY^{h$GSmbdcU;N%i<9#!s~pldb(UNm#xT8Umo^Q9QN`xPJmbM;_9F}};C!}of({X+k6-}o{(FUp<_#$Vhh=gnsn@m!wz!xeG9+FIPJ z0&&k=Je;B_o_zsEwB8%`QH<`j4g7}RR2Ej7U6E;M?fRLK9~Z~dp5khGTUj$-tHUO! zV|87$m{QhXTy}81|FIukJS&3#nPe9x*LtUYQ+dq3Q#M2CIN^6g^DAP;!Z$;dRBa$5p3M)JUAYwz6=oHN$x;M5lzEwB-bK z1OqJyznIApVt#k{qcFyy1?l(t<;SbLhDQ5C3=4r9O)Ap#Q{mQH{aZPcjU`jyIZDsP;@g}8mN}A zN7N7YKGCMQm!6y9sV@``(4%Dvsu!|x#Zz?r{?O+Jqg&nudN+}W&2ga{9NzS{WkG31 z(!kc$By6C-A}H2MMy1-LajFFV-e`|4xF==Lmqp9)qneSZ@*PunYTT5dtU!qH7LFFd zAPAV~4Gk0_IXgCm6k)*TFCWA{jlY*pyeu1~SBk?M0gOR`ylrv1IGi%lQrs3(PYay8 z5cyYp8xzBlB7`O06E5Fa9fy5pPJcM@_(^?|GC^m7`=K|~e`1_8a$6$5xy0b}-ddkl zqZ33CM0hN@v6ibe6c+pLQye8=o>kq6HtGY0HWTW}n{f5g69lD*LXnE22}TP{Y`Jfp z|L;PwTKEV7`eNRl^2d~Q(!w<+iV0_ItJlcNnTc+F!wI+T+NQ!NqKgS>OO)9*6o{31 zG8Yh+l@3o5D31rgr&~~ErKZ5Fe9Iw~V29SJ^#r)zHlwOE88e_py_yNFlbTwRkg-Jn>KgwzqsGSds_TpWF83x@-q+Y>|l`L!SwI*N*3cv+i< z8vn+1d#iS2Cq!U9;v1EnOksS9bazC1$VSRk~@onD!euE3n!}lBdTu)v6 zw2M-2KBp^)21LhwdZG4f%ti;lepkrpvI^V(MiPhy3Z%RZ`)mF5q7srgfrBYi3G zBJIHJe3-j6Ud6?!aKNTDjpT}8g+OIj9>8k4BjGTpFS&TM@s#B6KefksKGP8pRyh^f zYg*$_bSP~#)94YQb0d_hkT-{$`2RGHP=v9I`( zBZVM~R_aEh^ak+~@o1>-maS{g?zL^cM(iQ`628LDA=GG{@8~;U9P;bkZ}Uu(;?s{4 z@6ULDuP!Jo?lG}u_eF$WtW?~-_uiUj(F*z4QU#NV z&8NoZ_`c7zO^55FcyQ?`-69guo$U&ff;BifY!147@tong?e3#p){Bdr*)Wn*POVzR zy>&6psC8ot(nv}JZkLAaqWo<-Zdl9~YuJQynk(aS+vtn2PaP_!OI~hql73WohWc%$W;RCP8T^E~M#NjBG>uI7jwNS~vM1hL{ zrGj^~!cZJb=lqMKzR)*yVtgYaHf0p`HvL?2%H9DU-fI%J=-Q^OgK2F^v1u{r-{{}z zh20yXpU+0;Qy<&Co*P4n2neQaN~jiRjI0^$ZlE2k#Z>X8oK?Tec32b+1mn9lI(BLKS=+2?Mo^&@Yu;;=>BidPQN(i4omE)^NQy+Ocbp zZs?C+e>^L-Rx;D#st|yYaVb=`$pMA2ATl7PH@Hz3{a;~q?PpsQGj=acuVmQbwPWu$ z3rBh{Oq`n~+&XGK0{2D1h$D2&$CPi}ejV#~oqL59u>=c27~IsB_a=9bkR;l3r$l{3 zB{YToSY4_{dwOs;(P(NLz3gW>*+y#UCwh!i{~e9%8Cxxid(&IyV#UzyPi&bgL8jF?B&4>raNawzL3 zq|UAO3K#mqb_gRj<$7l|`Pfv!djn+f^1wR-+OIWk51Jg$k5H;$j8|5}!#yhy_q?lW zCZ*wB*HVB(Xjo}bb0I|AvX)!d@(=V8j?0IiPaRv2MskP9@#ic1q@ES^lwB3!%V=Kd z*Y4KBLI*|p-)T1s0*FwDpl1x#pp_TqOmo8-<^A#TFt|D8-!W5-ixiIMm2&|BZSd3B zZ43m7hlU*5Tgn)s)=JR&yp-0_sF5%8ksqC+hV|pi$A7;*zfFM^rbfVx&S9 zZQ>Nrd*KKnIn=5YCWcGO{f>3U#j;=!)r+|3IseAEkG)%;=IuOGJKtk~^7ZV#S)d=I zsDw*TW{|ghK)@CHg-hSp_$qQ&eM6l6-`YY2*AHT{*HGZmAcx^-J!(d;iuPk9NlMS zp|TFree}qNO)IZBp%K$47(-)zjHp$B>=AxP7?09Fd09p5)NbAJQa)WtzrMXx_M-sEZSmRFu!E-3gYQPbC_4Vgvo`gn81lPJg6{o|h%<`Sb*oEZEm zFV9h?b(jh}tP+(v=}Q)SQR3KyMarxc+lY7|a8)kbh8@lc??v^0(q@f&O`uIcIZ7KT z2+ms65~B8(Qf0&=K!fU)x3V;;N+HW(16VwjY8PMi#|?V@CnwIAY8V0=C^=;pHJvow zVu2$5GKEvte#^gYSZYNlc!2S}a*Ug;u=t^@Zd8}SJo%J8&WKSkPvqSRr ztxGk%MBJbFUg+DRv(Kj&#VON(hHO9L`A1d&UL3Mq(d!mglwlXm= zsx%afC-kKFYVpt5f|$6Lf)-UE@`Q3)aWk( zgYNW31#@yviMv?TMkk5GEzM1otqQGSE>qRpqvKTmA`*LSZ#&jojGG6=lRsZILWxFg z1vR+MDoN8)t`J@Lz!=RKfpUCDD`zrddCGN2`p1D@OL)VM^l|i~9QB#_%?Wu=B($=U zKdm;i;goh03mu~pT)mp#~Q9#0QxGJEXws%Gq-v55rPu;2dp-hw)pez{LdTgn; ztwu%A6>Bkp;~xb@kxS!r>+ z>Cp=fibx3^VZ63g9DYT3xYJ68h)0i|{eVhj+j8|1WLXX+dIgpT!3<4yG zrHSuP?We%$^Oz$32qz%ngFPOKFe|4JxiEcq^zS#15=t))g*T5n1#WXVK!+y1EvZcp-*kT-)jM8rRmK zqDt8d(GlnHPV$uIb&Sci)!y5}M}qTkE*a`u$CDt7OBqVPCaz(N`u5V4+ItoD)t>bV zTcYG)DRzvWMEexlYRQ-P58m?63G>Y#XT7<_>#4B`a4kn+O;ilGy`UIdmAdImB)Zyz zpDUV8ugU3)Qf8r<_Ohwa-|Efom-X@6pWkkd5&Fm+s~}A1_y*N7441u{mu=^EeS${`%Tw|4F)Ryd#N-l}u(kSacKolhAX`G=1}Ajw zt?UIe_sSJzYdi*P+CtS6p<`a~CM6 zp@4{4Y|$W_wG?(OJDdipe11~6la?V~m{AMdX4stesG+h^q%CWwD4Q?4JSR-T zm#J_|HR5f5K>5qM{nq+jK3_uog%9%D*CK*Zz=?)RycxiXU+WO&SR zHLf;%t6Wym|2T)un3LFzD+TM6Qjx`{s4XeO*vl6hjQi44^_a;zDj}Bo^eXJlb+XuO z4`#2y5k|^XjG^?kYS-IUWXqBZWbG=mHmSwt}-kKo@L`g!yA>(-mDgq$aqaYmG~E~|K>J)vJyK*pFeNkv6{ z3JM1~?Z(t}Yr90}d~@i%cc)9Js-8F-x+#=`iDt1DPtWOwa<@YMoIz_@ zd$mx(p9>d}T-(37tn_I=-Jczr2!-XsS=%(rC90>jXodF?PORcG%?bVKLpG(bqEE~f zLe$<`Li}5CZw+-;Du(X#V|CzhJ-JNrW8V}$(Qox-RfprZofyv;j3{E%Ur}YgDN!4r zs37-6)S%k!t%30uI$W&oMEa>kpaoiIdN+bUqbk}a5p%oI%caj2L@SJTE&hZf52u8X zov%uOe@`wI$juL6ZbU&wi^=F9FT7|@xk!wsY| z1-%z7+@`@LR#hflkrH>|@+hitice1!o&W8B{=;8C{Po}dU!UEFfBonG^)LVMk01Ww z|Nh5+_<#QT&wu%+fBT?!!qUV3-TlM&Lp30l!1U!QS?B!_Su0fW2*2FcTu6gvT73CX zNvqKym4lVLcKn&3{@M6{{rkWC%Rm125C8M;|ME{C{?GsM&;R?wfB(zh|6l*~&;RV_~}SAeT4Dg|FX^tT()=<(rx`>@cj`MZCAxVzo4 zY1%_U2cL3;j!K+pMn+|U!Xr9ERz}A|DS^N}5s--r>OQg+t{$Jh{D;qagwL~%505fP z52xp-8H11vkyd=dssiISB*4?e=s;-$KVh*A(^tWUCn~ostXRE^^Z)mAe7XB{cnt4Q zNlp65MvGJHs%mTb2$NS5-K%sgjU|>N>4xNrK|xZnR*|Xy>@n`X3_T)4J&2mzf9PNC zI7~PWDP)?&L#EOuO08Wus%N3+uqY69BRd4>Q!k1-3#SDfEJRbnW^j(4Zwz_r?S46C zrvIiOG#_XHr7<(ugebU;${51T*{+T-?+R0>B`I_1!xse_9womq=F>v!clY;u)f=Ne zE_aF)`r7OWtt(IJ-L{A$Uyj2n@`>Il5sQIpITXNk&j`C_?5`5#T2~FcZnUq@22I*w z@JY_$wBJTuh+GM6<Mh}^K{uq@_l`)?VnSV0q{bYF^5ZGxAP1GJK z#S4wIi5h?!Fgq(vqtM0hOVx6A+=N?~UOOJpQz5pWbx;wj2ya8YIwL zY-w#B|BZ^fO>MZ+h>8tAU|)`d$bm++2n)52`KhGst#S7m-K)osFRM+`%0NXiiCXs} z#wBPI9hT*enlf({-k?R;5L2X>mAsWxkwd^h)isqBvvci!ZY9q@aJu$7N5<272+c=R|ym(Shy=GXUf~SEo zO*tI1y1FKsJetym!Qj^8+-uJw9dJeo%;9JxnpopP(!oXlB<%Hi;GKGYs0+=fvlnHD z(2VKuw5`Zda1pf21D4HMl1;Fwd-O%qRzzvVMXK#M zTyR7zR?-I3Ra@UID8xj}hDo@)Cr4Epcw;oZ+uiu{@4k*hEE2y62(2lm;zJM-21jT> z-xb@=vJJucu!Z6vC{6*W>&c8oCc(e)84n>r*zN446l$nWD{1eUN?L%h$?68G{Pb$a zhG{k)+GZ5}HL^GvBJO*d?cKr77bIOOoz5?_pGPOWJ2awTRGCUKS|oArD~MhEOE6B5 zRjXBKhHBgOWQlz?*uVe&RcQ9ZvC98>bTuplP0OOqI#$8WI+8c0;@WGF%|xY1SW3w% zw^+0zBj&cD?Bbv6@_6S9@_EucnAb%L{Cwwh$z&3UpZ4 zAY@JzR+#UN`vMuz)3u@qo7Yy4!eohdSp@U-wUwN3oyH7V85C7pF_CT9J`Jr|v!-!* zxXbx@^IF;tIatvi%t zCGRPyp$r@-uJC9OCrX9s^3&%F9((U3=qKt^@kx%oP=Jt-QmmK9dl=P=ucxsf zY(iz-)&;Hw_ZnSZL8DcK>pJ#9D`hcxL2;~Zg#5MpTlQR@_ISmZ^AGxB_nmSaRS2NB zhAELqxR_&GfuSZPXV#z`K~E5>eHrJnn6Vno`ZOi#J$<=i)MsCo3+*CIXG;-_q=qkG z+ba4nbF11ELoh(-qrx)uOs-b!wJ~jS0wg~RqF?*B58uB3`7BM~!tl`VZ@zthTtRK3 z@WJlh5F_Y_E;#&l))Neb$C-^~?UoPkj{BG{UJX1@bt%XIg94Bb zP>k^OJ_~;p?6G3fo{V&nPC*r6JZ+-5t%E%1_Qt?ZY2($Xg0jdD+co;;6d#kvlQndz zf(Ny+yMwn`#sN4zVpw(t%wg%n_w$WWA0w?l8+3;|=%=o|BSJiI>o_-3ft~GC9MTk4 zFP4UZQN!&`8^-DdoeP-@vbeoJVt6s4p+Tf}Uwz(-oYt_Y$9ZuE4$#IHLtRTTU#U%z zX*S!rfL7EiLMiczcZ@Ga9J>O15-aKGzi-H8$1T}zCm!V(Sc`_^ERr}~8z{PKQ8n@Q zf;i#j5jlH#o|iN2(_2S=VLJ#@@1o>JfixZ-~$w7I|7DIKbeK&YCKj z+FOwn0r;ixdZy0y({c85yYu-IE-c&h3JWHO43Q0c59@wYA!y!d^6Rs0=stC?xTFYV zh3uF~ZOS)CdzF|Uw>=l&yONRsf5%#!iZSsR#X1ER5e|F}B1J@;gHp4Y#mGdr7PW)n z?N9kLlp5)R75fg88dKLPleWY28M%ND!U&rcnHsL^*|Zhm)!1wW;O)K3Te34q-1 z{5k3y@R(r4Xoo;c*-k|1TUl;%8*cav>ohl55R*UNfFM=NPn+w+*Y5Kxwc+jL`_x2M4s7 z-6=8m$+5wVAt}rX-e4q*f%?op+k%fQ_50uMKK?%U>TH*f<_GW@Wr`|XtnY}*9c&_M z(bm{vu7bse!`K2v{LJtO7Gez!VwF$ zdDolT1T`prs@-DLC=?Dg5aUyvRnQ}MDGAVK_tPVu`yaO-9>lOH&bYb#^tfy_pO0ns z6fgKusZ;RyQ3^)Ddo@_*lCK%x>5Zz(Yk{H56d{49JEhZhr)cY}WylT+`G=%>(Sm{ZG@aMyQJcPE7_uJW*oc!z=tTyw)m(VJ& zS7o3`KoAR_+B}g1o?f{;L<4w?|Btyh>$2;}jx_&EJvWj*506w5N^w0^Hua(Yl@uBT4g4oYyG&6x;l-RHGWtYj;N9u~ zhEXY4v!lm1TykiOBEF;$FAyVs!=bJ=5EYn8R-YK~s#xrl?Ul|5TVfIT?{Zl_i}xs1 zMvN=9?{Hb%davb1!pXo6DpI*S3lE33`u{1 zpp{T;9?1#j<5}#mw?<=FdphjK8##~Y4smaoG6kZ!0@6IVM~C{?5BjSViJx1)rXy%W zW#7Q)N;K9f{21;cwiaq~iI&q@oA@P#bJ{>oZ#t+rTG6yeM}N08+}WXp89I^;LbJ*{ zSHHD*;tt{A2gm?!n<{s2o}M{u zVHFe*=d)hSK8m^^43=UPmdGXvGl~joT7@T zr$b@sTO3%f|E~T%H_OLq*hZ3!H?G>t9bNsH@c>a^DZL*Z7o}V$BmJA0xqM)x|MBs! zQ7ZE8#?R|7{pN?4?|=NzzYtOSVZHleTjC=zBjUQtp& zV^9Euonw#F!9xT|tViM{bsAVxydL|;P$x%cjrQ_4KmYXVK>A$fVSiq5NkVN^Al8GZw^^KS{_-&uim`bK_jS)w4JHk9VgrwVL^n>gf+o04cy=2v ziBhm0k+~+Kzi^Igw|*Hx)%QQHc`@{v+SPZW^;J+kx6*7^JnQ&XAY$Tn?;KHxRCnRM zl<8?~3Pn`oc_p}{(0uDV{lk}E{qFgTZ=Zkn{hv?AS9%0s8w*!I3r)Hmmk0W`>%id{Ey#0Kk`s)#e_q3Tx%-EiXkP{UHW&C zwaiw?RZOeSVnxYYaY#MsgJ|8kkKJI$&wlg#%dh_M{OWlvzJko<7DZIHqnAr)xC>ra z1%i@M%-B&2t59(gM2!`rLNhd31h;PfFrWF0+q21$;0T)gL*=pudF^Vv<|c8D9MZ&Gu3Cf4MM2Sd zJv_)?*B4*?>9^ni;rSoGJL<`p&KVOr}Ss?6NmiAum7Lt-=4OiHp13c5Lq0GNlkm6Mz%2pkedQ5s4j|> zIDIz(=T?K_lrXbajs9?bS$BWPTboDAmBXp2uXEtOMbX_HN|=t*sf5&=H3jK9K_$bn zHB|~0F&t$Db8i%C&QFDwVh5lbr&;!PPr{t&ycaRg2s~0zI*3ZyPfcBDv|cNi;gLe% zzxUhgZ%3DjUgiD8Pr?ZoZ?43xuh$bm$d$y%Cubffr)X;?T!0^iWV$jK91BU)3U4GM>X^?Ouc#?yt35Z=l3C%u-BK zI8i`BWUS)<#~nd89jQk#Tul{YyVftS`yc~Zy+FVXUK;)GyD+NYDTV?4BWmG{$ z`Z^8(LLsQWE7n~Jd22p8hQ8wdG2Z_~m;Qv8zAs0?{AqtQgpN8LvU(qFFGQL#;tn|P zI^Ze#bGu-BjwapFo}2=O95r`;W*~hu;l<0AkJGFxb9o`G5)2xr0}Co~G43nHh=TBF z!jVFmxF+0^yxw1l7AGAsCv$(K53lFNi?_mCrx6lz4akUVTD3ir6=T+}*xL@Q#09%b zq}IivL=qPO)@i*(KTus-xAyv*rik^M{<2>dB_V>UUq8gY^r_?KM1aZ&3s?1K8HB!S z+aq(hC}t}HBPI{UxV@$>fA}(_Z$T8r>Xm2S!mi3l3av-QzKFQc3E{5m;qkjD1$WzB zgB}_uK1GjPiGv%DcVE1IF<)wWsikj7?)|vlzC2E^uaih5L4<^mq!6<9L#;|34X^|| zY9nuLeGMj{9jHW(DKAmOHt_GdHR8`#?R~#djDL062tg|gil)dIq^NC0=U^Sf&Wh@{ z0^p{-vE*qvr}hTL{aq0Ox3>Y<<%O`a6xQu8-hSBo1*hus1-&&N%^?bfXK~q$Gr6!I z%>v4NMkq4$cK9MtKRlQ-4*)1$qzISiQEq?0(}1^|1|#QX3-g$@P$Z&gg{yCO95N~e z(s+x@S3K;YQYMAQzzt(ewmm;pI0Tlqj6 zAd@0*4fl3@aVe>MM4B&Nyue)H#fxjxSFFqN_0;?ZO4F4Gyi%mw1Jw#aeyfMTtXT;l zAi~HspH>lqHQ7Hl)VO%{>eRP;@%nvx`TC<>N~^w3lR;#fEQg;o^fdM4zmRjL@z=KUC^v433{{u6j;m5 z$B}oS#P?If9y;^4O%f>)rnyNQAW`(C@3eQN){QgZJ$wP?at9w`b3?InwAAn;(uBum z9vkr*|9-Te*>BrVizA3rHlc6dWA2)_L7Hq+F4?2uUKLg9*_HpNUx)pVLcZt(J)dq; z(>!{5%J|1?zx(;!`}NZ`xLAkzaotHYvQ_F(Xv!U8^=p8x^qKX3Q7c#ffYSmJ95ae# zhmOv5o9X#suk^?-^E5@4W^O|0L2Bi^4 zaWMj56S?r*-NYMx3t&LIm92Rfo zYKi=hPW75E5g53s0;9Ym+OB7woJMvk@;Fh~{_zBNua%_CXhq!=`W8isX;7G3XNtlY zj}EtAu2=I#8#nqP@eI(fDr|}aC^|U7jiwzr{90PnTGB2SUlR@?$U4|w%d9(eS}22m z^ppPZ>Y`uTV1JAdLU4r+#I64{Ddw7j0zt{_{y6>=Xor=c<0%=9FVDpqa4NQLZ&bcNO>OjiS?hU<@S`u!f@mgX`!ZV=*Ld2ScNQvUUsaHCqnATBxVRQ3Ifb@2PuHF=$zo5y$`v2?f}aBE+fY?uAN{m9 z_vLA!Y8sOl2*<5-2`g@K{%HiyGIF{im;%Nkb@r%FnKNNDPVzi%>u9zRXGvfc9ZqLl%UydCpCUoxs=)Z5+bPI5Egexi zf0%@)X|3Rr^T;*~SH)DQaGg&W_3g`{Q2Wus)S=2KQi;cc##Au!#-S!FM%EH)Zo>S1 zMBx?oX?0x1Z9M|_ndb5LeAUMK@#57-S#f0|WF{5z#DsmJR5Dhr6h(RK4cdgoRWToZ z;1Ib6Oqf5m>}hh`DL*#a+jXfwb%?ut6e<+=nnKTc4f-qu@DF#VR5|$0+6?p#YoZ1A zgt!8VGB~y_=M))M>yJXrYwhWuF6Mf5J%^T_erh)b?o3v?)*`Chxf>A$3@((voPav^ zW=#H6f@I$}MBX{K(Q7B0Gy`@+$oh0c+1uSQ!DkU9c8%kc&7DXVi zQgJTx5Z<7c^Vpy_HOy}Nb}0GL)`ja%iFzr$WD3jDVU2RTNSX5DKohU@9ug*tK^RRW zQxk5?qtp5R;xFst+DUpx#PTquy^ea;5YI9d>LMQke1 zdu1PSXZZv@vr=NCpG1@Dx&oPo(gQ?%;K*X)>&s`ou)$THstOFs4n?oqY4fs?|1SID zGI<$CvLC{$CMd=z!MB;~W?2rZ^Sh8n7+SMY3|4PVm)B}&cHJKUWW#?u?K<3n^k_7> z)I|&xn#nNpj_|t}$8ojg9S5FNc{NT`xH}3W63N{k;JD4c_Sm{3%&EAfP_i^V+Ie}D zS`nknhw4*b+|)EdaBc3@5tpfmD4D+Yn9cy z9)D^|rV;Wkv+tsmAA=ID_Cwbs<$QjOk6i8www;a{N=Q}o7{z>JxiS?*)mT|8jrwVK z{Lf5+iXaTl{bBu638eO;vdjY`K0xCpGWLDF65`9N%{i*17Cv<)C zwEw6J9~Iz5DjS+S4wJ!x)<~uhTbSOrhGx85dnSeIpwD}B6gW=>W0+?LJawDiP#WHN z;|L;{wZcrePEunfh;k_U3YXDNtJlHDF%;7^sRF!a%|j~a+(&!>!-K4nakE5(F>Rw{ zSahMHxZt9n$k1amYB>j+`8a~JdMlJY;F@PfdJxxM>%3j7Q*ug9125{7lm618fG;S| z6_6`Va0?3%^ex1DX`vG2tXfPUE zUWdCvotX}OC73ezUS^InBR>qN5tz@hMP#Bhw_EGUA|9fUIU=nJBNd8*oDq)Unmn*e z8?t*73zo+RyMt=A^ff&u2Tf+B!(6~xh)nnymzrP&r758rD^qaQ?LteIigYw5$5`hE zy9a3Lt#@V>FoDF+9Nd%?7f9*{Xn9`A=R-3P*U*chkOt|Yb!$`o8z3+ zap5+4becI4tfO0q-q}@=j~6@`=*36G<~{m2f)i&)yE4N?KlF$7;`POQ4L{mV=eub* z*GEwWJ++99h2Vi`L8r_@n-c;3+>xS*9Pa>NK%c*BK9wdB)C+igw=+YZ#nAf@uT1(k zt8kUZ9F=~dowNy27B9G1cW;dgC5bg?Raw_iCyoM7>)dt#J|D7Q_e=?dhoZK(SacH2 zO4JA2Ed(KO>qp=M*N_q0QPx3`5%*Hw39HR4Fnuo8Jc?z-nH92j>ReS&!>9zI4B|{Y zEUJ|7Cw=ELrF|**xRp5%q zan@?-X3qQ-=k-uCW)dYTJLw#FdKf-$Athl>pXtSjtrpBVm+y%+S?3(oi-|?+KiRli zuv_fjC$y^+dCLE|8tXrfKmN!6b*GH+&Cl@;O~J<1S_A1HcE zblnsek$l#(Ded6MpeSh+NffCnmO-2i`qUq)1N_tzTuUKe{`EU|Xcdu-t_Zl6@eM>K zAX%%3T*Ao)rzWFMC*FhT=nUMf)s(dfv~G7FPp!I~EqB6?&n5uQ=%ea+DiBQs4&8%ccb9xa6x2}Px}_0x(zD~($@?6-gV<9FYD{SV)N_w^rtcj`6u z1j&}VH3_v48n4BPmF1DSV>yTu_$|g6MV^q*Y<}s}akCvhYFz)96pn3Wm@C-N)4gU?nW2x;dI@27>V6X=gJ0AhIKf!&dRy1yI&@H zS$hA)0m5I7<2(Ijtv8qPQFt&1P(E%7)=cfw2?0QkadEhBL>rZfH-#=7FHAd^^1sCw zb)OyUWbE+n_Hh01_q0v>k#$^F12F3MpMY)J^nI&JA&+8d0QRBnPAR z)Cu5;tW?fpi$TY+4;|jdsr{ge0mk37Mhx;L%Ft&gXs*AmR~K(?mOm+Ej}pqHR|#iR zElrwr5u0ny?CiC^!b;UCK&82Hq2Xnc0+;yO`42`?KdNv();ms||FpTYuqDMb3Y?MZ zj>IJ@cW_*%rg0qU;zT+02O)4ATwzG(M!Hp5H!6*7Lo3-)|CtA*x*K}q?IBCg4P2tO z%pj<;=;;v=>UjLj)av8qJ!4wUrt+8XSs0pjry}-T1oiZ+tD!+d;UVH178jt|L(#Xi zRslk3>(kXX;vg-Cx#AQ=qZ2!i92YGS+z<%2R@8y1v&1^Vnl40UMGcfO__+DMXSEIf zF-EYcS@9}?V(oCDp@!8>)+(lZIE{T6-c(UoM>}7s4lO0ledA|V+eESUwz}flI;_hh z8u~_Qw<#a!4LbK7o%XNET|cJp3a>&$E2H>$?*RP@W_`q^cTat3%BzYX1uL}@Iiu8A zC}}9LiJ^O3nVVL{+?4p_JiL!4W@^`|hW_+`?X{TWWu3+$7IGRGaY#Cb_3OnBbmL>8 z0v}O0g$G2Ysh$^&^pQ?4lw+$XbkEi3Pd`2I^}+C^8s06%Y>~zu@^KP~I6W%akuJq! z&VuRK2`c{e-7J}eIan`r6Q7 zSBCZ#_faqOaa==$vk0j#hCY4`Mvn~@vrX@upu-@5vbT)1AUqT0<@89SX2#I zUfQK%cI=`^F$a}~!d@Tc*B=>uz2>X)|EGh0d{gv(L0>n4sEWdm@cN|RUyD3cAEOvi z#hD=<$Mg#8*`Vi*%F|PZyjGKb;Ct&m#JmiBABt~PORbATYerMkep|iLPwdb<;2K_u zYm4J3;C60M+7rI_`rBA8s?w=m=qjS&IN!}@d)V&nRgT~36hb9pm@p!E^L2L0}&at)d?etUl5(lzqrl_~( z#wtYTPmlagIP%icrHT3e{O=#_ub0D_DMmCfN}AZ6I?BLMZy_Iu(kt3GuTRYZqrekV z?$uF9tAFRyL!bPMJ|17*c?86xTCWO>T}Uu+(`J$lLQty=J%DyQdMP>4dp%I<)x)&K z`LURUjh}vmkG+(qsZPhWFg?6QBMhavYEcVRK7ZhiFIm ztpO0{=r%kY3tB7NtJTM%ZVLNzR1jwtNwA zF1V?Mf^)13)QdC-6kNzqmXws7EH(m~^X!t14ylgNX8qJ9-ThchzZfUF%?x236B`zZ zQRwiH499&T3s7kj4_Yxo1Y2mi8VkZ3{@k_TBLSG^NypJ8Ru9T@Q|prYNCjo_$~eUQ zl>CNYc$+p%h^QN^4YXNnHJ=>u+AC(gx&5v~_`7tv%@o&^!{H8s^>EPcj0t;Nnrc9( zMfs--hhs4yPLU2XmopJT!+X*T8m7&a#rx5(P7djh1O{NQ&7V@$exu6G}E1iJJ zsA90#=n1MFrkeUoktSjBYP?~*w#Yhr{Q8NYDatA~BU-cY?LB(M%1G8|>HFn};LZY! zMD#@`N$WVc9hmF6ZO1%+4*Sqi--~u@E5-2hhZO?254uEDJG<{a_9g(1> z`Nsq(*4}5LOVL_Ug+P?TMaTK_H=H zj9w9wjn!K^%*8m+gy@&bwsd$!J=No5{_y6*mDbVKaL6F!sK?eO)cz1j_mBolJE)%G za01n5h@6mc?S8b>Q$vGC`x>8jy2|N<@bAQOM&D6TqD?f zowskzzWnN|umAYF7k_;IzhC_0H(&nY`TzdYH~;h0OFO80QN~zUeRLji?5SXpu&5HG z5a`cKVP=T(Z~~>rnzdWBGT{I5R5#o|g}Mg)6Pc8VQJrrjdMmOUP9&c?3ADT1X9hpA+6TF6Ue zCG~Q_L?0q>|`4_a&=cG=?XSbHo zn$;#O;>050qTf?HA5`?%P+$My&)@v%e?6zq{Inw`&|S%f7(ju8mRX1kIcnt~fFD;` z=rd2pU1z7JSENNu5MSln5aRZ5fBNG;Y7Kw);(vYp?e|~){_Fqu(v0P*jUKyB+qJ=| zamY5BG&s=jg6(trG>TE2Plm##XgKH>T&Y^73d=3;N4}?C@0%}w_x!SH|HYqw|K({& z&%M4OcW0I*1%pKOoP}f-tA1rMP3^6LgDy3n(@rF^Ip?wuoRD63?mz$Y@Bj4AeA5@- ze*cF*eEH2kpB9Z#AV9u9S3P(XFmiNK+l}x+-&ECS4tbK$86`sc$L7KDMYg#aI6LC+ z{`~!mtL=76umdGjMbNH2cbtDIkxpA3J?@tNiAYJIwhg5~4$l?gv)lqNPck~M6!3@V z-{_NG&*(SLzx~tizrQh^&CstZBpYLa<#ihO#R`n}K^mrK%f)eU{xInP(jTJC@t|hA zx$^^m{kuyOpF5w{aeh6t`4u~8$!ohMgCKH6^O=%_W!6!Dq?HquH-U)tf|{*_+r_t&}-8}0sOm$EK3GWt2ueJumMOLSN{>&Jx&U0Fq`Pw&BrtAJB# z6vE#w8N2K_d--h4s~^W)S`jJMqWD1t+R_T6Z!jbw&BMetRs87JM(-Ah#;t#RatGE| z&gbqUZo?8IXeu^^^px;-N}aWZwU*Q5lc_G3g;wG|ieB}llzpa0rWncUbEDi$Hl-sZ zqtH+j&1qV47GsW~;3a9|*r6FY-j9_IkT+=owVJLKMumrJ{8m z9f%^>0zs$?3iS)$g^WW4WEAV6@@IcDVuwILOf(YN$BZ zRur)24pxYF1+7as4Luy2^W?+<{zI}A_wNm@Lf9yzTK&AzgEED%i&jBwb>u9K4tr8F!2fyC>kzFT6F<0s8z`M=s1_+ZJK=7z z2NWBGx`BF)YJ-Xgk$Ge+k)roB7N17a>YlbFC8>l4@6h9fvVg*6CRd5Nr&`tTn zVKCA=_B4jN_4Bu(4}%>6=+shj zD+rw1-Opc!-c)9JaruhxZc0NO=S%26QA>rXs1bixI&c=D`*if|cQl;@=JZY!Jg=3^ z>@99n<|hZf|E?p^_c;eu*s=Bq&&UkFNg%!cKQb`{~20_b>1F zFk33#!VtMkLdDpy9M<&8%#qVkbWbw6Y(lAmHaH>+2NkhWp#*GQ&W$S;eKKXg{-UL! zy@v3xUR1Gah!9eon=ya)Gf{R=yRi7gakuZ~iK zT-y4c19h6RDcvsG{Yi5-l}WG-%0_uwywAi(ZqeVp=Xm+1UH?Bhnx4_oqiq4~#miRsdxpCg|adHk+8UmWJi3opQ<~0-%&SIW4t~#u`F{ z0{akMwP)W|A9VcXg<|xp>lMkA3*?#{W79(`!)4_)jH}8}Bhb(_)_r0HRNcw5N^FEsg@T zp3RZs+F9ip4m1lV#rj^FrcG6wHdy5FMVK-p_3O2hN*F^a1~)YQ4Gdr7(4sZxlxI9% z1$v~BOu^q0Q!z!Mj6Na#PE@aY)N_J&ZBcz;1}x>0+~o9zh6%C^KF}370%9BrO*4p*>aRRD)bZb6{ID+Q z*|?zsqjYVxg3w(Xv0UtP3$t(?Ehp!GOl z-VpRANKJ~Y(I}ghmh`Sc$ac1A(!yC(gi9cbkgIt9==9w{hF23P9&d0}ECRTL6{0P% zPClzcB?mdp797 zQiaomTzsY^)@x$5=jNij|FKbTz_hDw0_<-$ez$!p!ijaEJRT90cW7G}1yqbBgkTaz zyrp4y7%)zy-u%(;Ik~ca@ouzNA60IgV9#Oh)^U~8Pi3c^t{tX#^Bnvuh!4e(CiI9! z-$e2WX*@>-SA6u7-n;C~i`uA#P?chr!51VZM2HH)+el(lX_&p>nm5p5o17yZ3+azL ztrB+!yGDYy`xb>20+xW&^TX!2Fc+sslom1j!+{l9dpuZI&pDuPXKTfJ8RGJ^Aurxs z>vK`*o0Yzhiq{a<2=whbly*EyOnGKyVr4TgEYGRC^GL;Kfu3-$V(`v*V9=om`ufA0 zkGd`>`Um{N$^1!TKk*+GT8iOH8LrXIrmJ%JyquK3aqg`D5q@8fwSY5Z# zhSU|KOGVR3u`MC-okTQ608OMYs1Kz+iuJWpO%jt4eJ2EJf6Vo0JbU-*<#G9cL4WJV z^(j_6b3hnlrsIsXyCBklC})TqtB*~M^-5a(0Y4{CyBHM@2!n=4zvoQ=b;YM+q+dL1 zhy*ke$VNh~ZV0>v4q+mPw^nczkp@op4dotXqGJE(?z>qpqIn6KcAZS2wfLNVbSp1R zpRvgkV{Z=Sa>^)5zyMVwj0>`&*G=xxx4QX0+z~9KCE*Vv;PZ()?wk>kqlBjAj@B!x z=F5B`_K=nBdT=zu_D`1CD1Qu5f34x{0JWhP90Ta!<6<8XiolI!5D2AAv1pNdE_M{> zjEnh`?e8e&EbT?QClwTy=Zl5j%HGx2EhZ`O6I1L8RWa6mc-$GGxhd@C!nxkH2h%k& zfMNubej4RpqxdIyqcw1p%~Q!>MlM#5;V4;fqDY77?S@5|=091*bHrb;pt$R`+MWWu z7lA~2DF~phkQQrUihe9R_pBl7|;-qyCZ?*1x+eR!| z7zY_$JYU@*XE>;?7H~0GqG=DGE5N#!FijU?>eQ4d?r{6YN$OEdC$iv$ySR?bjc|a) zdloSXAOts1I*XMP8S$cG4BNZVp)DIlT(}3%91j$Fe9&G=(Q^r?IdN7f_#q=*}(|g~_VJ z^I&Imz@J=-6zsMJaUiUD1o1;RwgG2AZ85V_xh&SJWt7p}K+afOt>Nb5&Mn0=i8;_c zQD7-Tg-U#S!(9x0gGD)IC?pyD0i2MQ_MNtwEm|^|bYSFN&TZ`bIZ`l9z*FfFSqzae z)VLSrF+*T&W{jli3hJDWa=Es;hz#7E{$tbklZ6C$tgOi759LRVA-<{~7N$bWS3$Aw z1UNM~))Xz-*@~?VAqIhkd2XH_$irILo%2X-AQ2%E+XM{@lbegqA*?VPM_k;sE3PB` zJvIV~BL4z<;Zx1o_umaXQM$IX78XLtc!p?zA@*WHZDt*(I9oY`6DkpFL$8%b954oZ zPSfH{(#OKIqV6@k;3)>`v~`rhCFtbxGAafq{X>F}4*3R(k%WR4cSQYc?6{xrtJONQ zaVdk-qoUAUJphM_u~3Zyb2U@Qu;695wF4_>eU*wo5b?R-al16s3g`0l)Q4@8HA7k< zz6eh5qshH${)c#3;m#|fljZdl9T(ffp6fHejapC>FZfdg(Jn*zYYZq+&-A?`ORK^rBq~@>3E}Xt>_(j!P7NG;} za;&el6*3BmhpC&`L~kgdDggA{xC6|i>U$S0*&arqH#>b+`BXX9L&}P{!!&!GqJ>Qp z8}d-obm0N(tiE*aBE?0xHPQ7HQ8Ck7|DP+L%D9Cl(c-S3kK*U16#Y;bSQu92nJ8is zsVr2C+79JF0eMj{(Uc|zcB`!EpT7L#@1FnuPp3*AxUce(MW;^qFLGUx+C*Yx@l~ci z0l8QRl;FqVQqs;9SEnoUw#mYOQccsJ|M|~fzxd|)@1KA9?QI8>DAct%WezT($F5|W zxqH?&>FgG~$7h-a<0)ZUgB?{6h*lg;QMlu5;Z#ZmC%iH8I9iSb5scAVtuYS(0@e(X zKFi8|%b=FKbgNZVjS)M@w}1F}@MquZkH7uy`B%@r`uf|WjOCx78RxDqIO+3YX>u;1 zO+N^0N(OlC2zrRKDV{2{iX(J{T=QsH$}c>y?6_Sk`M^j%r;k^u+bFRtEv6=7T#Hu zBrd0jFBp*`mj%9KUBccD8?~g+tG!Psg9pxo9u7wS-qS|I0>2 z5rnTqa4gD0D?aI-dD?VX4k;nIg{aV$==5YEE=kme_GFRuhs;%-i)rp`}Z zS6(-#ih0(6vVt-OU1jK79hVv{Y;o=4(O_0O70H|I$laB-9fI=0s4hI|MomjI>wjwC z9I;`>gBw)=?;aew&slIiXl#QiLjgr$_I20}0$JoQAeb z&?*r^)T;$g9fzTiIag#s#{KEJ;eB>EP`hX^6$@`PO1!|E5=+=qrI4PvX=``RYhu#G zRna=(Qzbuk;X3GMh42m0qz(<2FJts>on_8h_4B}C1WkHirV{#A;g zG*3)zPVL^?I&JrFgdEbMP@|w^nma34_+)6~)QIkI?OPdZXR+d?EsFq<*Sk~$GqAU- zKOVjjvekzmS}g|J{7hP>K2A{!kcVQUkfqiyPFZ!v&E?ieijq|Ei;mXt-?Vl3a8wP= zd#;*o!G19+U^%N;&~tFeDW|fPM)E?cPY=ZDn`qZyGk>m7KgV5qk(IP&2JF|N-GJG| z5SN-;;d`2lvQ|nVL_B5`eK!7yjk}|}Cvn$c`I=4LQ;f{mAd1o|7iit%1an*vX1&Mz0JF3|~wu7UuqfN^mm z4gLwE&6n?PbML6QrflkjvXeh1O5ZkHr1|s4T~IrDpdE(DlhO91*U{qqqiJtXdBJ-p zGajW-K}GZpIt@hDH%lk7LPs3vmHKd;frnNom{g)N3rB!zmLN_3uHw>WNTqs6hAFJ5dM?LxXrbB6xiTWh@Ih}T?+fcER^nB zq3ts?g+>8NAG`t$I6&DwBGh^InDfTU8;W)a&u40@gyxh^u_%}VB|}uO2X5;YDm+N# zP=g^}Soyz=k6?dnh!@K3KR(bSJ@K`yLbeD24^ofedb1ij%oZNvo1!C4hB)w`nMc7g za(L{`So7?m>h%J53OY$?avuf(ZYji)_=FKLa`T}z1&Lm`jkFd4Oa0ZM7w*E#2sfTP za=f#sx1x-|NOWlgq?D$fUyH8jPY-pI929buolHqIy$H_G^0=z@n%DI2ryen~BOSJD zBOrYyVjO51ugbvUoLTtW=;ygc6|22e-JBnCs{c zjVp11g8HOJx0D#7jh^)-6t>LzJd@I;ytM^ooVcl=Q#gd_R`LPESJAMA9eFM^k}DSJ zTL*EtU;y!D_VNimuv;x_yCU4on#3JCYdIp`rx=2#UZ{uR1Irf2Jk4D}G*v;E#)Fo3 z3+InPifPJ^0oSg_2NV2@2lAf{ILYdId;f!#Hs4y%k!&{O$jI_Vu^le*fH>XJ38u z^5v<=PdZnk?pu&}eZ|5}IQ zIt~F)E30*d&_s;nc^5MZkEe^y24*gheAT2SVD@zXJ{G+e;*O%+x)>{(q`-^e$>e}{hMl_yjicdp?TCBFB zZQydGkhAV-rb~j8v#m%0kfM6z{%=NszM9SsdBf0EC~S=nm%dQ#{rC}i)t>~J{;j?I zsHIgGWiY`UchK4e0=NzzHnhbJm6tAJDoF#p$jUw{B(>KXvZ^lF&wP$+>!KIy^$$q4 zUmM%*Zj@w1Rq=Sh&n_ZF(Wz(6lb+HV0gM8y+!qRAp==wppd+r1pZOS?jUV1F1RD-R z_Ul84OjqT^=ZE42E#BlCNB#&N4XOn}q#g+^PQW6rirQ(e#YD5~{>-Pi%#|cL3QFo`JQ%6TBQhU+)Gy zaFc0XES{S%+Nn7Q(#$yMv&h_3LZNb2Q8LFy>tqzL6Dkn;c-maN{HgtbB*0%@U;Nvv zHGeoHEN+`>rKr6nD(N~V;@ay*KQ69^Z;b024PCUh_M|irUzR9{8o=ITRR6@Kd0UB1 zIZGQ*@l9X6&{HR-TolQu!J{D*{kL%3?h9~GIJ)CjETTh3i7P*8#Ot+peOGw{l%HzL zi!i&Ql4u`yP~a^}YqF(SkLz9)*5x;~#Mk`j(HpZ5%G!$Y^~0YhpphJYdTM%@~d z7-Nqs%gi_@PayK)UIP6{4t5IG$d@A>mLUhD?enS9~zVVYdPiw?jNSjX_Rz>Kk<<(pDg+xkgJvGlcSitO9iw;Sgfw(vw z*tkbG|2d5qO%&KJ<>b^3rM#eQE_mMcSH!%0nAEE)rE#n`vM~7C+^SW->0b}#(F5Jr z&#o@qpS}C>!~1#hZ`VS@2h_5nNMD?A-l;K!+cuBSyg+F!P79~*f+N`F339?w7VTOt z<5Oa@(bo)IST zMVYXR_-_RR{8 z8N)|x9uXKp{L>+I;*=t`^`G{9_r9c_LEyn*K%$JgwQa>}lsvH;SeW=~!Vh;>+%m(8 zoFEBccA&M1?$=y{?|!^^`~DecLZ{EzJc7=MMQ?Olh1!>=s$_CEdI;!sEZmX*Dgk=Y z8y3VIU@QKm{Gao1_r9TkxYeRVyUTXB(%P^8 zVE4Wq_p7;K>}FAT6`fHAUxzm1Ql#l#v%VHe38EyCO8FF(FETx{ZMMIEijN5M?5+9D zzr4G6ec=8k@RH&XwR6ZrM(s#a)b!f6o2wHV<`5t^mU`n5MSr+MFRDPRud*3Tj>n&=4RGuDC z^J4~lj+Wnbn6qhqy;y?6FPGI?a#b)%7*oQS$ee)7lpoZ(shvyy81?Z81=b zMQo6Zhof$LJLEF7>?0k;g=_3ZjNuOo-zK&(po z=oV#1UmQ-WD6@rW8me=dGyc9CY66O8Q!i1p9yf=+=0D@n?&mI~?cf&9vbYfUN&#Oz zZHnnfv)S67Xu_O}UX&>m!%2ysqH59YU+*Drgb$NvPeP4eNs6kdhKi%@C|+qfDp=q_ zn6+go{c715MdxCzo-7=)BERZG-TP#L!vO?<91X$lC@rGExdvfxEG95ND;6s)>UZ;D z9#cd<5l@e%|NT#g$I~;h&Gq`U)^eX&G%aU=pKJ|h?baS$u%*PE!x2PA(PKA?-liB` zOIgWE8&{Ee|EkY)?~8Te-;CnE+r(HApFTrYC==Gy`Z8tER;l@g`^IR5M+PEx6T7cm z|Nc$-_5;3_S9$bG&hvD^=$2qwF}-cP9=X65ZxI*IVk>=U!S|r=R^o9TqogLbTB56k z*6A_+{xA3L$ELvh?4v8%)s&W?(0a(}aW>?ILDoSQ6mIpI8tw%Ij_RqRflZ3aqjQ3d zq>E$z`VV$LHwZ#Bg?oCQ6)f=H>9{^mytKLdRtgX{DUr+lvocTl?bGtBZg8@aAp}l7LQIbqH!JWOcLIsl9ZCUTs2AvDKqO zuoX{0DvrjuSd=~ro;}{#{*;IL7k!P_>*tTKaC=Y!Fv||K1y)0vkCuC6$rR*8*mI8| zp$V^q#7>|Sc~q^a^f^QR?Mj)@0z3sw6c)wK2COzKzikXOgCY+dg&f8!lJAAu;wnaY z8Lb%l8lE!bYyIh8@w|f7oQiFE&r&k>jd=<_8n{$(+bKZQ4CDjG$tbR zE_4EH7e0I3tM@fYPJUsVc-jPA%HoH5o)VS?Od&PME(oK!%rRmO6dAJcAeh8%uJsW)i>H+9LE|UzNd8}6@36V z#4;7)tuw^h*mtc~iy}2N`13Cv|JFz7%PH$&ge4q3P?n|FgpgE{`d2)otyboxltCVyQ}^a9y}msTzII z-ha{Hw_ZQCm}3-1>dtj-w^$uD$up1KY^7N!QfE`0fT&jt^eJOiW;U$bEc28nxOlT( z|2W>=4z7z9W)pA#T0rLWN_ z!PojqVv$YodAia7HdFYkmaw!7wf{vrLkYiNS+2f|uieno)iMP?ct1$9FWk4wjrkhLf6W@#`^9Q zoPBhk^8i8 z>^pCdBJ}Rw2;&JrQK_$puQ#n!vZR2=gr|BZYc^`2*_j$Hfh@|2vJIc}1lOzaW4<4h zFGq!d(a7qlVgY9K-VTqQDAE7 ziK-P|Q>++8yCAUdY^8a_#5MK5j?Q|jns;7Ld4{7`MPzNhY(Kodc=!J0xF$~p*BwmT z{D?pfDvt`MW!Oy$Vo&g{s2r8E9c3*A=PewF=!Q1MryEy-|5&eb!@T7Z`?z`%2itnv zJj3PXG%}a8RQ9miJW48YdR5Bp*hw~YSG7G`-bcj`&ZB?#b9~YE`}G#zS8AqDkf5TS z>9r#xOXx6*LtdZClc6YfB%E^wY8R?^^1%5@7}*{Z|;1#GE+$$ z_8#@1hKN!qcH#n9@qC&QV%eMa16sx_O7LUbM zm3r~iZDx zgxa5Y5Quo`>7(Y_k`J5^v*JdNrRHDlvF@LPO(hJp*tPpz&>BbnS2=5UF5Ap@R?n%Z z5%kq?3vO0ap?IYiB-TH1h<@sFyPmMSM@veHrqrOnN(nv!e3~el`p5~c*A(m_gv1%Y z60~@PPxMe9Uxc_((`mYi zim3+vj_lJOGa+EBC=q_qGn_KWJ-WI*-iKHl3#>UYgIIkPY!04lwuW?OKWcJ2?NvCN z8j)vMa|^>ifjB()G^bVZ@p(>r-psOr^T;f$gykabzZTRhs_IRg!WPNU)>}$SMt^&u zHcO>?c;fPS^HwWl{(N_0Hg^yvL`z?{23Yi1CwlT?#NN{?G~324-d1`MThm%iwus?3 zhY;bD#@4H^w|`yp#rW~%t9gE2T;;GY65c|Z=v1KS)UiBj<~3x1ML zm|KwBKU59%84q)QdhqtM*zUT+h)U$TY4L^%k!Q7{y6K14a!SYY1^oi^y3f|yT7St? zy>EY6w1d33TSDGMErjQaxbwzQFa})8HF2ry!4B8PO`65rsaMp8vw5N(jDvSIA|muiZRK45i=X8D zOeMt_p-gRPWl&9w)i^?X+FsgJaT+aW1-P`hd!slc77EeuJV7{fe0u$e>pxq6eK}EN zr-|j+Tcc?IFGmZ{vqG6}v&RSe%@6;6sR35Ppj>tjBSeldWI=vrRo_C(m|$7`tsKQ` z3u0CaH#UVlVA?eG*Lk1^Y8(W`3z9k!=PyJEW*d$sek85CsG}Gi*H;4cnR7y=?zxLr zeaD&XmpssNig_h{;=b?P z_z&DViWIP$@hBGbYl(J9u{mLzQblf#kCeLLj6)4^3gat1Zv_H67sKa0#p}22*{h3j zMPRNz1Wwh#67Wz`S43&XF5&dmk?`UEhl37x)C0B_dP52^b(9PU&iSd zc5g49LWfy2Tozh~3UAGgZayk?s2MU-__TSxR&NyYw9k8xD_ZqT+j;!u=mNRE)bwc} zYar;YZL6({JA*KBii>+_tJM)WQ1&h=BPsF;s$|ifv^dmDPk4@U<2k{Tq#{~gb931CkTvc=Sd z7z^?@HH^SPc@@B}ISR>RX(jL`@?@LG?Y3Aa>f{l1))wi0{2dQ+qu67YiY9$+D7@Bv z9JGhZwKwgnUi?<+#d-ngp|69=;*3#$nzvQ>dcv{(qfc>fr`Qxr$dOXCOoedJa!GnB z!6y_m^w+l)$7w=mS|4H;^pBXRg(KoW_M?w;S&cuRT|I;z?7Av{XThzdl`hDm7v&t* zxgf4Dvgnm%DiSgxfjMfiXiWMARz!FdNxerS_SFFsJ9J#Un$K+Lv+ioJ_W8P_aVeK;^YHt;D7Y^9&x#$TTKSZ=7aqj9K&Jvq6< zuRaQC6@w}kO~_s#OMi-Nm`I#HzEW1A1gXrs=;D?p$Jn?thK>=UemM)f=mRA@Vj3PQ z*d8d5AZwcRNjw!sRLvZo^`q=_njB+e3ZU)M#G5#lLp)yre2BKk&c(xWLsK>yrfkE} z=Vu3^Er)dRCp0;xX?_Uo*EI2Y(qO`J!j^6nO0mSQhLF6QBMa(8GL! zCWnBOwo)(X7eWmM#M4SGD&=&IiLi3>*%5QGi^9ck)M=GP@d8@^JvBLIHdFi|cO@1+wrSthJ)lKlDbPfGckM(s-3uf5CiEmciM14d zgIl07az_(_;jl*N+h$RS{m4kvth{>p?)`fG(K5EhrjKWhCo9_Fl4zjfK9Z!!TS3^N z?XGx%d^5YKkzy0g$OcWYM@Ad3UebMZCHbMHr09_ma?m2zX9*~&6k6}J9*okF5E;7B zK{VKw%!o{7rCO>l`nOYHgHnYsR_NM!3iW~( z=ojqzq%rR|d|pXRgh1=!=rrxY)ClKPerwSQ>~}7=&8d;gn4Ms&W*fM53#!xJ_ksG!$rHYFRn0FGB%R*kSl|$2;Wgh}V6kC(#KX zYZ4U~f6^!l#smBn$YK-LqJ-Spgn`MReIW>7eBna_V-qT5)by= zEH^q94^Nxgr{2LSKlWl2n+4HuyKF`ao63Jh=A`Igp00;>Pa0aozPWh)?phKwpfTH!#gl;|m9UO~Ag znXH(XQb1Ly&D28Eo0HtLf3-jKWHsoLbKR0Do}{b1x+s>uybLprHz@H;F87b>M29s zle$44kz?;To6s2fm`c=&N094{ki3l7n^PD>=N_k-tEnknVj!5WuY!d(V!}_pZP;mQ~kH+;*97f3k zF*qTrMd5>Of}qB;N)5Fj=)-i52SIr_x)@UqX)tRZdqe&4BMPY617HaU;YJd$2b|)S zEjhMIxS=XH3S*5aT)IYzbm(%)rx-aO&0x3pytviWCf@zv-C|VQ6N=)fixk1MB#T^b*q6H^Hz&C-MAd2AJ=zkJ7W}42#YC>}wg2#i-}rhdX&A+pKIu^z0=&rV zrAI&AsfqgTZmZUg!>v~QC2IpOTBk*+OilQd(gvKxyPC^_;sQd;ZY`sdHDy`fp+9NN zs~YU9M)K1P7UwD*kBXnQCfw(jP={@P1)G-Uw*?%%L}n4z4G8F?*gnz2Q4)<$8v53n z5rD(i*`=Jc-5d_t6S>-ryr;H=z$y}WLi&D$B%&%BsFb9jpyXBjW9d+6>Js|j5EXA) z5PgcE3UViFHMggl&>WEOeS&J&jzhC6~(A4?AW$Hj;DrzqWfv^4v8 zY~75Oi1u0*OA*@eo%HkB zA?dY|6cRh1VFfO4IOgqJs=_J3dTOscrPnz^lXKtHpDIosFDvLW z#M!%;h~`t|G%E|?mS~;Arg^|8;A0ueyDyHj)1$xORHSmM4ZC{RA~?SrwAQRZ6Lg7L z5Ks$7WV=v#ap=ieF4an=kWZ^jX<=`D_jXR#TC!OAUA_67%x)>yLu|4do*mvoIgmeo7)r3s>wWOiPWG))3at z9W5>+gslj_+Th$zk2B8;<-MAh6L$>z*O%{4*ujekDV0~0-!>&wb|PvSbE$?)BCaV+ zQ?}K%F_l`TvbOSgfL1)fzv<|bfYsa2bsdbe|Y++mTLgnB{ z9EPu^oLq08_TLoEJ4renr>F0Je0j=#OMzn|Y(lT}abPhNPlB-X5-rPBoK)dt;FPG1 zm{8VTN1?Fx1tqm-Mf$FA^a(o)k3?t1cl=P`pCT%eLQSDZRD8L@5#v-^;+ARloHt$? zG#dCjMWd2xgJdBHV&FclfPK}vDe39mG%BVE~gtb2I{mWbT9Ll|LmShgz8s@0tJ z<~Vsuyto5Z8aGpI38*{N;c+q19?<6s$Ia%Rt0vS?4r8)RAB56G%wadtlNHbDP~IU0 z1&u+|q5z&x{7o@nwH*wV&om`D`F&`1(3$2lUU0<)W=Q zg(`^3jeuw)U6u=3qY&xW^V(s8;bPLb94bBd;Y{eC81OdK(k`8Ltx5S`iHbs5p-112 z4Weo{o-|jy4-A@Y>14QARmZ6(&r* zC_!`HerA}DsQU<1cVPg&soNlNSr5SEvz@bR*J`{>(Na{NX%)R&VGUstvq3@z3lPr5 zKRN16tglt22ro#XWgm->689sb_5w4D2BFEBQ^eB3R)@8<5gHygXbEkBW&8MGH*vn( z3b`CEEiFeeY9G;X&IN0n+2sDr?)eU6^T|fq5HBu{lL@~Xsh{kyHen{fBNHh-+cWK z-+%Y@A5T_7c%vNV$4l$)(z`bcThw@|pT4YuF9_S?2OhlW7WPUU7N;QWU(bli2HPBk z12cK$1iBg&js^;D-WXvveQ&c;=Cpf`>d6gC6a&G!1}!-&Cac++y;ig1(o=P%w&igT zMf6;}V`A4`s-6v3-I^m8dQ(GYv8wS(*_rDZ9j&&)$O4H;*i=RI5E>jp!Gb2i(Fwop&`PCSpD`xNoH~oI2R3=;8 zAgg|<>!WMkc=^sv?N5jC)Vq&Id$_Eixh8}pnw*SgKG$NyNX257*VD2}Zwj$mDVT$oj0Ur- zh%}!a>9TqD`q()m&?M?AXf43hlt!&p_OJ@>Cd_u+aZW*1kqDuIGEzo`cL5i={*_O9 zl8AzE>`6(1hyI8@cFK1{5Pv}a8!Zl$=bI6c<(Q9{FF>%dkSPAs<{HCw{6odh1Z@T4 zBMn5__yHkgT#yk^cTi%nH&Lx2dg}O>sP8&|NO=1@4KXsVtY+C_0b&G8keTxZ+LAhT zkDJ3b4lhW%#d3~jxbRU>(DZx)*Z%q9)z9C)zXW6VfNiO$18q)@myoq)h7UJE%1ZW~ujQ>3e}~Z}Zo6XO#IjU;lLKy$zIXxw8pe8Yx?Cg&{@U z1iMd^4fHT6WV(lRtczk^PmH(`_UDtfod2uME}l03)ZQMV{!3pDo8aaPYR&o&Eu3hJ z6C=@~RSXg!s741dAu+fpNOb+)m)E@SpJ`C!7VsBYWw)gmt4ku@9yR4ift50~$Y$7} zHk^jH*aH3m3cSm`&t?=``%;@=2js^`rkf)zk>XiP$2nr{LCw2uJCquAs>L&D2bzj2 z?d8S3e!ThP`sElhyj@RkN5EBZKGBJpvd)Le2Bnd>n2Ba)wB^YBQdZEYBIi0MdzqLO ztX6)9bqEDSMr!&B${&4i;U5ZbK6GJyN+)p~bMC^6QyX*u1hmu=c*K?GY^juY@iF8+sf*>?r6`h@~^-9mw&zb=YRaW-=7^| z`r#s7qQXUq+$DFqG`R`IZZZb_G=8HnE{@S5V2q1mE$*qxtgkP0^92oojp?&)L zwrJu z@T|>4)XfclR5NRybi5e^1O#Z-bHeb)_1N{$cD?>|^>Kar{x_dL{d!`#ThN;@@8fa$ z%fd0Z_@;eS6%~Y0y5PTlcqe2Sw4p9V$&sx=8deJgto1Jb=m#>({1heWTbjtcch81!Ay8o zpajhyhqtd_anf7xc)^OBdC;Z{_VUn`etI9 z%1v`4#nkc?;3o)S+ZyUJikLGW8eGC6I!w{ku6QC2X_tHT7oU7Q`WGcF3JkYj*CKk2 zKTgQd#6!UuGj5+0F%|X&qiIzuLQzGKlGnq8!JcH8yW;29 z57SLcfm*xtut*Dt7q;WeqpU6-Ka_a7a!(rZFn4@)7k|F~ygpoiT32gc3$LD+rF7n^ zzABY?Jfn`WeHxpCUaitST$ScG5y*kyJO4BYaw%&l=Eql;J-+F~pUSk00&upT-jU2; zPNrvmV8DcY_CChqv=Z>7LpxIuFYguQ&6ZzV^Xs?u`ReQX_U-x;mG1Li8nc^4AN-=P zmhQ&2v|SX5(7`!ofMPbv$IA3fW9#f&XHFjT4lWGZFT1!c=j^Uvt#Kea8?zFs?c^7=oT|C3U{(HKv{J=ty|SBdZ=qdz(7Upa zfK#|mQAEm!rCIF5M~*LAXdPMY>bCLeyy7A2iU0K0xmMD^)>NB8fPY7&O)o+(tWshV z*k`D*p>%Dt<89_L>YGiw9M(*mK?a?BsIK_SARl#so7pFl&l_t3oq2sbOMB(c)XkK= z1jAlh=u!zj8^AC~k6m69hzxT@tN4TJ)7lZ>Oi|dU(JYLq2&j}P#-`+H(hg54+dbt# z!{@s?-Gw~zpQ(AJ!veAuaGapBPJgpOM^EV=pV%BjD74#l!BxOtT~_-gh>kxc+We{$ zQcr+Y;>F^6Sb)-kgwe@4swJWcg4|6JU5HR;0c|5~X;EZMDO-QjinkJm+y?&X`pdWP z?ZXue= zANsP!y8hFti1^5pNe>eAAhwyPDA+npiK*ZjWB4=!>Dq#MI2WPK;?EvU>5G{b?e@2L z)MqlI%NEC1H0paYi>>MuZa4ODM;$rSy2LHg&;fN+Hx4uA-k*)?zka`-+;M#WswjK) zH~?lL?kMh->5qaEE7*C8xj8R- z=d{*P^-#=-oW?ZlVfcXcmiU`qS*9vOA)qY&V6^&IKex zen#g+ABt;p{D-t>FegZMi*hI^Cx+Vd5Ij&qjq~|MK7Xf=V|{L4PQpL821sjO-6~Go zOO(=HUDNCT=^f+W+A9KZMcl&Lq1~$JE7-7vkKkpiDQMD)+>z0E)CTeLGw8ppTV#L# z0XA0weXfcUmx6rcV!ip=wSx^8_q7V_Dn%%7ig#wQV^K5Yb_wU!^j}K;`J!OC}^u~hs zI(tfP%;)sYHWqYnE}`1gm1d4EM^U(pm!ICI(Z_#LDB|`y3k`tz@%k$zi(f>>?j+3r z{vUt$cfbE1|9tVPQ^wT8cTdl`p+$}!KU{~m(t>~!^-f_K8iY1C@jhc=YIy0{zbta~ z`m_AupZ@9Ze*a(p@Gg^MC)tAO4Sv#>vv|(5K1>{W2*o zK2P&Zs0t23Dt_d`-MBwGuBgn2g9+K{6prG&`=9V^fA_Ed@Q2^ux!JAKxUx}Jjait} z6;Chym^dpQZ7+2r-747ZFfBvOmfmym?vU2_tDfhPDH`JhQBP8B!>4EuEc7bo*?k&e zUSX-EL(CN>1!?Uqv@TE`z$In$zv@{ofBPci>yglG$kC^xsvbr7&1$2!g?O7DqIv-< zru(QV7b^^=4Hl*lUQK`9)7*-e|8jQp_xqes5iFrqvu8ajLG*4lbS zn~95Aa0oU1%D@iQ=gZM?Qp9IYW6Q#)@9dw-w*^7;DFh7(2Bhp}M7sw^8FWl^Bx z0aTH1Lh&@1z73k^X?7(Y3Z83+TE<`Zy8d1K;Cf%{h(46y9t`a3r)RuYsvoo$6+|!; z)Q2ElfS2Un^low%%|Co}8&=3HazQ1NW_Y*g?2D zjGhr`r7zi9yv{w{zvi!cnsfc=7h#rb1P8x9xvh3LTv!QPGrJk_7maxPVXbVe_O-W4 zgd%l1riFe>vEM->L+2!b_9A6|J;cpejVTJ|u6N_2tm$T(A} zue8$i5^P_-pZrhtpb>G-fM-X=kzMWsC~mZe(;`o|(ugn9?#g8q zBbCk7g&v$<#Yl?QO*F+-F-FP_t!5vx{yJ?R7aa}L{_ojJqg~Ml%@Rz#cfE<&<3!BF z)s|>w67v{Hzb|T(Rr_6t*@7gNMaAfytuz*=Jz91}?hjKOqpwgE27x4rZhyR0B66J+ z)#@B&rzx}*P1Ck+KQx7$<)nWYPG>+|yn7^C3|tox7k@FL=Cqz5@EwBQupR`R>C7FI z6=%%|z}FsMSg3vZ(*7c%_LmPgZC+Z~dtPpAlt!(%gr!N|WPPNvMZ{akD+?#?DYAjX zaGTp(qjb(7Q;6$@)xQ4a(>Q+Gc^gt%DeZunPA8MuW$TU!@kr?h`K*w|kK-0{XmlVl zr{W6(Df7qi@Ow+0Zf|c#oH6!gjJZ=)ST1Yt*K8GZBk-g|kth*Sx56-d&lzHBv?2(7 zGBox4GPjoqj|VlxnMFC!5M*XY8C{1}q|@c@F<%joK9OVPh5G8vrC#1=Oml3~2G z;N7v>4yX6IurxtZQ>UdvVrj^<(+<(Fk@CRPEdmtu5_QZwvN1l)mlnCcIou%dz~?fs_p_!~66+ztaM zULJ$0$)YQV#Nlky6cjcTMfD$Bl-Be`-v(lcB?pIfVvPhx`}~p*h7K`?#v0f<6LAvv zHtozPo5E>U+*MYp_*)qUDv^8?HjFgIPR5klesLM`^}G2iy`|Yq#!#tw#f~(s&Jp^3 zenXpe1gbE67@Z+Rf#qHdzGyiZ#y8{hE6wY#e>`vZlD+Q?fSOiKa&+4tLliLuoXZ`S zKNBPSBpgYxZ8M55zBOnwP0ufKuX=F%fhbnu;^sqJUI*7_c33^)DN0tUaMMjW%Hs_w zYTa`2iX{5mLKx34wXgr}`r$xQa^md`1)kY3!P?36K@jv{&xMMAK`_aAq_Xt=lyyZ{ zr$S7$v)7l_x+_!M9$=-}t;f`ca8`+Rh&G9AY40Jw5d=}Pep{v~+IPnd2gO5LFQye= zcp(PfZuIkS+Ifv557$mww_N~D5l7HJyH#sK1-?R9gd81mE7%7_b66$X@xp$%t%Xd& z(sjQ-i;9%7>3ItXo5ZcBMYclWV>1IA$?&JD+a|yYj4Qzyp zevl4lPn*^gaZ$>Q6tM=q{8gLZXkK@%q;<_s0!&__^6v zD_4=3uYxt%K?_hV482i%eFfVM)n~&@2KZQK5&Oh^6~P4b|_%ajrLzDIO+~Fs(Wps#v|E|b~HjZLY!Bk`#{!3~(Bu!+q{2vEJ!EtpLwGjFRi z!A)yoqlTcf`RZcp&)@EJeiZ!=2LSDDi_RoMC)yaTp>%t+$}Q1cqmAWpgx6^_V}z~0 z@AeCT;LFYRW&T?XYgPTv1=t)DntnCI*cu;D>CzjtF=yrqrdkfZM;1%Fmo8cJ;u@$O zh*I2ZE!Q6NhcmH0d8a%b6m(-tT-DJHv~c^@Kko{|dYx5|Q)(%^PHw?$jBqYP3UpvW^qi+?zV*l$|$u5NlR@15(I4ZAmn z($JyP`_&)clNm@fSIj3n{hKivOsr9{h)WUH3rlCkJM)&s??H3@DxLJHL)(!0af8lf z%Y;-}n_7S7W+({{DL{9dhHzA_T@V+y=$ziN+^zlA4=w(=2$ACR#PJZ!S1!bzKoMLk ztmZufNesDWZ99=iMQ-*SL;eb*#=mRr1En2l|Dp;Mw^4A$u@=D$QC6!gk^07%)g0hZ z*!3PUcl1b%zOK?pq& z+tBt^j2>aVWyy07zz^Trh%Jb3anmZeia39Bf(63-!t)^>c4wk2MJaPkVXQuJIt*QW z6uIX7zQqp(&nHgy5c!`%;{uUx=#h$7;2L-sp=yZ~!xZI+5aPw8j<$XfHxPl!$QzIM zJ;iw9f~4y(*QlrsTJbT`Va7<%aXqJemLvVyh)xYHtHePrjzSeWiwNNT#1ow4OxLGh zuRmQ*z1ZBOp%G=#wyd&lqUn2x=_CCtK&DW$o7hn2If>yG_%14|sDJBzOxGw0VnV|HLDl`Yk`>67uPR z8VBREnA{8Q(9Ve32CynA7I-Fhls(b8p}TjA`ih2BfgK|rj7UuOKk+Od+Fvjv{Pgwn zLz#}MTyE&<(D_s>G<35Gwqs^(i(&yyrpl^W+odUHi++Wu$F4d~Vsq{%p5%hypLj8G z8G45~CB|2zZEaG*L9$T%8%^d)3|R=J319ol@PwrhtTY%``H9D&SMv}#d*X4bPd=A~ zP0e8qDqNwMJ+sAxO`mKJkho>&cln@vtq*908_ab2hu!?dxA6*=_a=Ac@3R+F@ z6-+_IJ-Ov#EJg3E;bV#8g@`2u>CgoxlxpAiG$$7L!yD1>0{vwqK(%LA%KGdH`AZRY zuHH(^T5;D!QhlikHO74&Tp;k zpL&Sf`+}!8?eGFr`w)OuHbU6-XzZV~AI^^SXH5$6G=&DG!MO!aLKnA4PC4Mchxqo1 zUJ7u+p&2G@g$&UxhzV~stM{;x@?z*|B0}zct`;u@9LxrqMr|%1--O0*#aAAy*+88X zmxRUzG>(C-QOs#hSWuV=;p(eiRmQPvAa^IAz|Cf;lxg1r(5@ceF(-7c>1U?nSV+tZx9s$z#f1TXRnA?(}r-dD`QUaq9FJw_!C7wf9jv3)+~ z3iPQ3c1j+uL}gZFxgvUcE{3+uTql17Y6Z%Q02`9F>jskv_KY>e@AWD6g7>{=}Rrzjppy?HQ%UWpsEaq!u&r3S*d)VF251#w!yfuz8E;>)7&QL% zT7!7K;TG7=`oyTde&4#i!9EYw-@mWF`hbb{?c#{v0!kH1XiVv`i?d<3Wt`E`RqAly z(+yhH2lhihS%jXh7lENCl=PnHnB~x2B1vQhV-1}%aTD$)i`9m4VhZA~lglr~GhB<7 zvDHVgAbWL0ZGpV@bJ~4R6}W3WG{oq377l0nxfZTw6iBq?#XR(L@}b&oJTsOPJ0ClO z)%Fq_cuu0=RC4AT_K%n9*de;9Y*PhHNXEEci;2%E*vOqJLsQCm%7$|*L!cqisW=z? zi#|QG&@)A6g&hHnNOe-=cP^AqXu=PNgv`b$#0jQ&;bl5*(P?&vk8iXJ3ei5h+Sf}V zy%>)YzBkMTb_C?K6ABYM$1M+~uF`LW$iGHnCp06+Vuo|#x#IwzKW{2xP|JKx(CW7rd6Chs06mO1b6+>Ufv@G ztT(KP{KzK-feUnW`ZJBdQ_G=lPsh*BB~M5Ft1Hkd+mNawI=9+F3X2=ZMgmw1&kL#C z%RK9o0MN~+k9XcU20x=14%J&U@|1FXF*;pExrod&QChazyoNy%OJ@^OEGw-|8Rq3x zpQuJ}$fFt7D2q*P7|>&A`ZJNjAxye`RD~NC<~ZYU#~2cB3uutqmlu3kir)GKE2B{9 z8S?JR&{2#nOhtPA&e)G@JH^FgK2(P!iD*ambn08^`I)sowErZ7Y*5E;-s#ATpj9*xLN?}6^W_6+WGdf}j=45vY5ENr z541poF09AtHQBQ`=;ygDeR%PfH9YFMMQGd(e{$K6pl1miK=A9TN+n-aOSn;9;SAI zRE#>s3@}(|rj0FHb2Q2!P9C?=`-Z?346(-6Nir)n#k|V3aescbAFBLT^w2_l@F?D# z{)ixBKZej3twV*5TD>`1391OC@?hqseo+)MFD`X^E%u>(S`X-*_?k`xhBsLVQb`Bo zDd?mO0wAomH2ugBQEeJ_LxxKBhyyGok6vC}^g*p(e@54Kj|mOiEs)R=3>E*?$BHE* zMLO}P$;_B#T3(Q<>PX$w6;oec>8{W(ge+VvQk@CVXxt2i>8`JW-ih$0ehJn|^&~k! zno@L89u_GQAA9Z>eg5?8?LhN6B^<9_z*wJ2bQcvUrC7v21pIUL$#7}06Dne8pke-m znthw_u=HFUyEMjM-OUtTNIyo~1$&Jxgeep;Ll>>5;mW(n97oo#v$*@#tUPsS8fc4- zF7WqUzPpSGKXX^vEru7X=tmXW=~wl>ITUI-b{oxBB{*m+R*E7E21Pi}RJj7EZ_mBY zyRM1mw!_yoqJI~ZXBFgA&@oFQsm>jd)6k8l0~~Ykh}vPSrI>|Gd4GO^A9Z8yE&>z< zozkYj(Nq>HNZRJ6UqMwzN#0wdZDG+iZmSIRd$OyXK%+-^R%`omv-V-NPZw!#hbbbY zkiLF)hB6Ho3bXFbV2L2~q_22D!7Zf=ah0ZkwaR9|?$zZURhIKK80f9&j&c<(-nJk8 z1UvYIX356%;M204Ih+E+LGVR!3JPXi@cE*&&~Qw0v;YPD$d1%Ky8B87 z!5lrpudey^xAtYts|WpwO5QlDVIrynK=XGlejI*?rJKAEPNRy0WOdMS*Ll*H$kR4N zxVC#)^!tZ<=ZJtX`9ye77(uHeRVW6@n?_QqOtCS>@bChulpgb-^L`|2`g3&U_FnNp zXANdf5h=d45Pa@5bpXm1t?p6#N9+^pL)vnw0MC-}LI_I6%kB2^O80Is(XdA)l;b7b zqd!|%Q;=`P6?~_bt1W?6Zc(bx0gOfILkSO&|85@;lkb1}8*XWiJj2jrr#U1y3Y{KP z8|xV9*(i!B>?(a?j%nyoYFs`j-EG?4MHl+33hDp;?H6qejC7t)Ib08jNkiu|hhv&tq!NKjw`na3%&l~U9Q4a@t9Ur-^-z)UR!b47 zU6fqu#O``sdj2JjDz>w6!`eDhUf)HD)S_g9mnl`HL~0!mO(6gNzy00s|Igq3;~&mK z>lT&cYd?y>an_4_pM+&Kd&RiH>-SY&-I% zO0v9W!z`Sg3v*>pf?G!j<-j8bDd3k46H+g)-M@ePb_j}mS|2X*>oIIwFi>~6Q7VY( z6gC7BBal@Q#&Bc@;*a{&wpX8PVu*WUnUh~$aeTS?`t|CzPvv&l@aFTk>yOv}`9NUA zGvk@L7!dUlX3}@wmdozIXk*W*=#YeX=oO)*7id8mOF=KtT=dFQtWUh_<9)+g?p+jI zWXY)YOfhG#-CXyfQjNrH#C^q4VHWxpPb;PweVxrjC0M7^ky&5)Z?JH4I!0NXW_KOKA$nM>du^|u z*1j{_dAM{G8^j7#t1E&ogxSP(aid$PLdT*2X0`%S)LQ788AU?urmdpw6MhH}m*zWv z)%N@Ffx7Cqc73{@J!?J<9B$Fn#cfJ|o-xWA`qEZ_l44TG#~`3q7D%BIu5DQKYVzWO z-Xy`ajb(Oe;Vs78hHtCrs-G@%zTwtw&H$7)d43JBmlIe=PEN-3m4KWiMEI- zH-iqb*m8M!^@rXKt%j}?h#=cx`PWkIb`7gvKCMxH&STbOwmyM~b5aoJp z^_yQWZaoi{_272#xg-dHOwP*YkWyAGuY;}Z8Up7VTW!l7=wZ+4ckO{L^ZoL&2s4TE zY1Lzs-8s~sWs6E_!LG@iyZ{YEOF+J&V$4@eX13AACqlubg?rmc6;1w{AK3--9P2+e3y_a1#e3OFm1oD^u25vCVmWYxNOcUs36E_L&8OFS!ZMHLs3SYq+%nibaxb5;o;SWk;7pV zGEI5Q6Wr!jL?LeObPaP42En%}wCB+Jn@8JrHDnA#4ix7c&h`bnmEb(Rsfb`Es`>tv zPk-`dR(NekCHBxN_^e1vRrZ|J^f`l}c|{nsY*XWABm*l(5!{?b2?gvKR_~Zwa0ALT z(Bd9^DAT|G0NVAx9Jh48Jhk_eelxQRsB_n)Hy|+QvFt%}sFXucMBGL|Qix3(q%C|5 z9epQ?%GSPV{j0}vlD?u%pjWLNrSwf(=TW^8wQ0Cuc5&{NXe$>B7Gtrkp*9LTLEP-M zRloh^!|}SG_`Pa~JSIe|-8e^4gva-&~?^*M7*>%?5+r|}~ z#RtM5Yhn+t@TC{K1kI;Tmq~|n_2M?BaQ-a{X3ptgm-b40|8^L?ox#th<7PNgs z1p3#@J{yyt6%1{a_-9e$=FUy2MbS%H)!V#j9a#Fl(Yago4a*Ccz*ln0Z6Z4x4L(+D^~}?C zv2zh-MKnh7mzas`LX1Xns{|y@y;p7>`uN-GNGQ;Fg;u73ZAfBoIR+}@nzy%8){9K`@34%~E@d!Gn$DUpkqlDa*&Go#YR7uy=RKzgpO z=Cf=5Uw`LXE@Zg%0me(l zb8d+odLe4P3WSp19Y-5$8}{nGf?6#SG+a74uL_#`8@t6H;TWiCF}WYpVKVa$A3HA3uLsk4*h=-9|o@{-07% z#43!RTjuKBz)P;~cwo~$^$~Yn%5PY5z^3*Sl5dw~zF&M5DaN3^BKW;3?CVp&xTz}Ks=@M{8VMP$4NVw|-=LDKa8gW;sKH=C~#i$$R zr3GJ_EN~@CiT!7O&Nt+ABXa+4;Qnpc(UNe!bIu%h5ctUUP^Z=p^pMDi#Io%44u|BL@F^$GYQ zh{lSzXUE5BrC{aK9O}oLu~ehK+I_sd?8omPzFnUw%Y*cO>((NOx?HA-MzPj&Az|=X zEgk0>8t!SA2zoHDmqN3bz`ZX^Q6Im({mkl*IP>|~(Wre~4$&`hdH__o9kzGkBmyxa zk3{$&%2>o&s>^I77iSqBolE5-pmul2@rcf^XCC0m^M0FgOcE?hO<^tQIMaO8?WW^$ zY`_)voP()`#!SB}otrluT(9gNX8UoY-BUL{tx!>a9z6#yZi44Y4u>g?c=_ZmC^VDN z@ful2R74%Ow-Kr3(dAReODmj_LR=Ugz$a|Q@P#YfNdKr6zhz2wYrE)=3x&S5iJV$4 z=}@=Cn&C&EoAvec&8M$?AaT5#@852*!6iJ`L_NSIzj7D+95g&`3f{DGPDSXY4OxQ@ zyLRgIhPuaUs9bti?Ni1p2i`q>q5C(56H&qGFkd=Ui`aZ z58%b|#7+`&As9FP7$?82_T}nkZwdhSyeh<_jL2%j3%Ac0L>PPUICwSvu%v+FMBKesNEPN*l zb7HJIZaO5vX<@F~Y2-G~iee^6U&xkg!|gF}3sk*u)McJSJ5oVYX+crBE2AI1a9)HE zrAxP~eO44xTtd4}B-Wxvw=CLicoZBhExYx@@%b}G#Cxgqv9|;gzS}e$McKuLXoYF%{t}jcW;LCAbzWy?<&vQOv zKb9MOvr&7ahLc3aXlIXyO%oEOh_OY{)`Pqv&d*)A*#i3JNh{W_?eyEn_T`VK5Pljp z`hLGPtD!Q7-d3$6qHY)KN=fifJe5J;L-r8==`LLACxSsU()UDjh<`Mjb-U_etabI} zyLOgd+L~9m1YHIMF|ERZ7>LN7Ik}P!#Tc+5)Gv&Xvxo#f&F%pKtD;n0ob~PDnSEMb z)F`MH==h=!oJztC~&$5e`T9`~yPE~4N;fOP-wDKCnoCiVn6J^c`* z4978_qV_a@Y1fG8MbU<2t)vdSZtQ-SRHP=Q3G&u#>Buz zLwX6^a-ANQ65iuM+Vg@z9B-?`fqxv^b#-SAjcrj zF!YI9usKC|p?LGS*Y;-bqpY7PE8%fxD{fs0R^YK~TGy;j8y@fR$*tfezW zm`7M3?)j7v@=Hs9y#Dj`Wgq3k7Kbu|)k#svQlf5tb3(kpSd2$iLCKT}TI|9S9?&hC zLu(+tZ)N)S1x2n-NcQ~RQy)}@S3G!Bv$-4L$Dp*(V=PRbRFq1LHrW(r?aDn?^gAib z*!hG?&x?;h-@wJhTX{bVIthXbJbJ6e`$UZw0wiuwP&&@EsVK2pAxM$#TJ^?F!V$o| zyz*1xFhi$ta|5Q=OLVc@bgv}pEpel9S>&7zdWKf{54-GQp9?z+T)MZcNC^ciP{pQi z?V`gO6c~&Gsx>)?-x^BNc{)r-l=3L7jM*zjs`}-C}w%$%l=eqWLD9S=SWImE-N<+?=RxZ7*)m4*63+0k7T5HRB?+Kl&MVnB{A7~!h0xSI|6 z4NFrzzCRo1|^i8|@pG{M^3% zcA6MFcjs@ByCO(0D6&r8hGMtIqiefqu!y5a;SRI_s&!=Kz#LM;vm?G1!A@)5yJ|R> zZ_ePtF;hVMeF`Xrg8FI<+RRQssum$6e;Cn3s)QoChpMQKyD2`bx9sRI>*mW`UyjC` zqgCMcg7>UbiD9AWXluv)uF$2xgdgZ+XNx{{sQTUmYfIF96tx|G$@&W=@4DXd0N3NU zV;thDeLgtJ`NwlPJ{3C!#lxsYXLof=k;ZvU&S|SJohK@anp=s5{JfL6OwFmhDSUoK zM&tetYlBXcu2)&D^@0X~E5^>5^IYQ1X(&*p7I6(IrFI-mJN!W>N>t+IHRpBHmi|H7 zOxYh1n?u<-Y7?W|CMG%Z0X+=8q)JE_)N8ldux;1>pv>LbcA?d`EqeGjU-iFSeH8$s z5Pfy}CwD_p5TK|dsw}j%;n9&(ToadO*bdpGoUwt^!NXv|8d1ZP%VfGXy>gs?Opc0_ zRuoo&QN+$L>O8Cg<&zk9@Ggj+2!IH{qqQki6p=A3I~LgDE0K%y^uv3O`na|s8Fow^ z)@^BaFoSxDxmJdI(nWr z-M&;VbiOL%McQx`IxcirO32tu=&wEaR%nI&^Gbx&i5{X;H z)R?OFh}ku5WJ;s8tjJ{PiVRBG!VO5b<}HtKdlmLrsRp^9zI1?^NPFa_NCb|2trQ_X zuWUSV$4m%q99>XKp!hV4BP5;2@|LAfa%xUV^IjJ+m!JU=FotdLxBe zn(&a&nZ*iAs7W75K-Kt4O%W0xmbIc_qup5niAQdCya#qoqDvMVFAtRReEq~0J--kLBj1=+} z4nza-dPaXn$=-SnyTW6vlS{ME(4*Q_Bo^0fA%u9efWL3~Q(Ahg;b{e3i;GJ5sa3P+ z6xZi>5hE?@^$(+>fKGs$JstO^Zd6=QR~^E7`Ax6j`}KpLjTEtEe7%DzjHR#EO{(iY zX9`J~rYn64KM~9=+W&efNDv}7v&LJNeAH|C@G=H7BS&|JHW8CMh}Sq#8etyg2VQvQ z`aJ|eJ4D+BLp^tX6c9tH?Png~!M0PpQ=Fx7*n9LndKM9F+Rqyfi!%)xqNre$BrwS6 zyD&BOhYacOcdUP}kN$l1X8ml6a&0j+gP?}qvtIPTa!ChGoIwD)m{gViMCNKmicy$! z37RGMj-&KNNoieWzCMImU_(53jRgqG`ugWV}3k$iC z5sa%waWpHoFFYb$s`@7ftI+G-6P*nqs~)Cs#9N=>4BhTxx6<*dQ8We>i0U@&V3^h@ zbTMmh1R9p5Ml8szzxHA>RcThKTpPo$H-B+1zRs`xlCO1|+)y^sk<}Z>A`*0}>!b-o z^tREhRaVTdnJKjFFhZqotovcH{d()#=P8kscEp_}i-|ZQ)asqWV?Ze(SK;L@3`e7t zLO5e1UZ+}H&0#B@6~;II7g$b@x2}I1$rMjM-?NUeqGpM1PK6a6q@|2i89c>(5GS_2 zn-9k%MWscm?7)9*HYBe|zUwAZ0R0$;n5MyxSI>n`J? zO>caPannAmuVc+)K;nubIRp6@IV38xRqRLmEilZ7Se+=*?kja#u9EX(K+Q~^cv%FU zd|Z+niZk|&4{;iKoL8sW!}H|8Bc9Z3+K$35UEv8ae*w|zv#5#jIJh4LUp^8XrRzXA zlknqX%D0U-Kg#!y7r6H55u8Cb!Z1!g5d+1fdf`F6yE6z$j3kt@aJY{(U>t1?g`Uul zW{dFFM>saZaSz=dNKaRt-%X8H2o+(K5t&5OL1;OQ(i7&~N(cB~DhN~>E_i~a2tY~v zjmtkb0DAiwkc%S2K-%f0c4*0>OiH0v`8Y?RfUM;QaR7`lqD66en*Ee{*)tFEq5XQk zvwFnXi+(8)PO+^rC$8gHkGlhlEg}YSMhs2lh{=L#ir_PvqRg%+O@+4xg)K~VoXevY zu;ww>arzz?NGbAw!|y7%9N{GuXiCL!+r=KVRnWMO%lo6-kSd@~lGtRL)|lzv`y`** zpRRv>U`B>e(!ffJ@tnBbp$^FWOb{$l>Mt0bD)|fBr=mAhvRjEMVT#-(zV`vnDEC3z z|G5A+kAWRzRg9@cBsOX3H1=R*+tWm{I|4^BC9bfkNo9oVOH!)LZw(dHIUg@Qj53_E z47VpjSe{;Cu?eOmn*g}-g>uDWa;VhU1c?k{gH{_d(5Ly z4s0f|T|wHx7@hx)q7@FwTqx#4q)?!}vq{@#_9T`%-u(WR$t z)&ZdW+maP*5@J<+Gy%hPLBh0gD(R^Hv1RkSAK^06difIQ1`C!SEw3U)9bqv`V-@8N zq$M*>?p&XFk^ZG^xM%~Lk+=`5aH3LiwsImfPw14>W{FJhD^sdUGD=mT4e z7G2;%lYJ5K6P^vLI~2l4d26D0ex~)DE`x1Ibu@AyOw^-MpOB@~me>2v<+$D4LJ{eR z8_v<*LL6XpuIMY{dYb@-(N&SQ7_Ab@=F3X9h0XitRFE3{z>_ zA|m##2l#gLNAc5#4?q{1&Oqvs0T3*TrcNi_noIjGFsae8NSinnqyHOGrrSnv-n&QQ zNv}S@qru*D-CB`OC!P_z67j^)o@;5EqD+JWm814l>Dir1r3`;<#LrEGRr&HWPw+#I z^z|^!t^#HP3i#RSfEL%aEgRL=!<v2NGI)97f|BQSuNG7SW8eTc<`z@tyFly3EB z?b#Fyr%0f-meKoPBWq#&G@X(%Dj_sl;nA7|+!%G8OWP*AdF@97zGq!#?(r24vHbuO z^yYAZ6!kAd>kQ*TCv_~MHTzCLW{5iXiRAa2U;l9x^cby&RE&-^KCnF=^ALy3oEIFl zs8Zl_z}`QmZd&$OM~BUPnb1Oi^IN}-JI7?da>3C`eM!h0Enx=6BWtGYf5nXHgkbU9iw2zsj>aqgB8Y0NC3%O{= z^)&Gh6LoZ^Z07e&*S~~E-OoM4gR2_1vpcNWgwVtSwqguTGqwq?xViAf6+dVE0d-2z z8!y;ou+{MsvGSjNgp1st%~l(Lehwi*{oyG02%f}&apy7UD>Oh8S~ZiQ2(-XbQbKKJ zPG8>&O~jZUt0{(p8+r7G7U$~l6p!Ne%6nFuzS4H;ws5*oGj5G8jXCM}7h}n|!@hOp zqn^gEzq9CB6$c_K^f!chogI|6QxeXztI;6Wni90FT1ed9ZeamK&yuKwlEr%K(%-MI zzWqi6@r9|KW3QW?30%e}27{E=CS$}~r#?F$pX-0FEYi^itHX!&nX>{yOWSY0as6{O z>%;BEJ#sfn~4Ngar0gGbXVV#_C4SM5KobUV} ztgX!Mlt!YD;+`E{S{E_kQ;@->M!*+7(uOdX@PP|aFAN_#(RPP;>HF3`_P`I`W+qPh zOlJxU-x5n+qf@YoMBA(&sK5_v*2fo6tBT9cnB9s#Mbtmef7LTQ3uAbD(iizSV1L^XNm-gJ5 zIwbRZdB1b{%V_-B*M)%s#a;m$bX*oKb{+E^k;FP z<+7c^nlnTi7JJ5S8|mkc=+cU9p!jKmAX--L#llx$y!JP*`>0fLXE-v)QB)VU7%gbS z>xg?4W+N^3Vw}|`OyaDLd}$Rgt?H*=$Gohze}3~5oD@o*a$*zjpEc_Zi&+qetvFXg zEG`)g$Q5B|v=K?^h`Y_KeT69NZ4It9J0mLOG@)?i!u@|_Whu{P`lYN)h8IQSk{v{Mz9p$g~1pBrN!S@E=Wl} zhN)mBJ>`L-oV#CZ(>||!%Xo+W@^bqVH{KxFCyb*L+%qw+z16$eR|icpRM@Fc@ZXC5 zJ;!`I6E5ks+wrFs)<6Av^Wna+i0}?K&{T`KdzxnV&iunp)pw7B$ucDsMx2Hph>8PoZUZBHo9E3WFZN?R73`Js(0fgiC7v-pMg0PTTQ~GO{WU?^{vFN zrCS8-me($V&ujC4nY>`mu;2XoVkH}#{%nuyp5nNjjZU#7JUJ4+b;{tvnUU^rgh*Em z!graRW0;$J<$NCYY2|KV2`mcRZ^bdJ-K`&V)aUZr)zxY7TfxtL{o(EdkFI4i)w;>G zkOHIfk#Q$kR~+yH$^b`iWYkx&`o=;rN!)!g2mZEoZ|ke~n*o*fgm}!63tHJC9|KpZ z(2zgO2)C|)W%cNcndnhacCv`Ax@PBVJ&>n$&%(O*O+NLfJFY}5=6nj+BENJ%#@v;V z8#hwgyjwV|5TaAi?`C6%u7A4wE4cVSR{P`C`qR4CZX8mNiaA2w+{Ma` zX022BAeZ+o{`Hy*xD)$3{k<~>DVTLwtU<)Sq7Km9BZd+%@n(`m=|Pzgfh1*RBjAkD zvJ-?cN1^?VkMZd@X2<5$F}(I*ek+cwFIbz-)ZoHPXct4ltW|0e?CQcQ2R9!=NV}q< zfdl3~RHWT&1n~R-PZf>NS0OFiCV+==l7b6LpLB}ttSzpuZ1Cn9N@K|np)_sX3T#7(_S|KC`PF>jwn@o_W9VWi17-=nrj2pTdS+SMMoIX*?HLlX$_5^nd zyXS1FbT}E}8Awy<{U*bBgWh{-p;%XGmJlczGdnIv8*^6r1XB=&naQ^;drW!%aPBsD zh>0v|#V~!XVv_5)f$|B@V56^%!h{zO^HJX<;djMU1!F<44e~!)`+;!zIj7y9K39NK z@KiK6N~Qv64f7F-4NXyZA27yQ&Va9It#m(xW1HKi2|wG{YC(^l=F+8jVuN4)bn0)q z|2$PW6G0zAPsH5%;9_(szlfXYk+6-b3kbV5m*ol>UVqD=U8&5F-gp>3-JC_Wzka^{ zbca|`_^?*Ts%E&ScKcSwOKa;un98Q9LQsm(fNGCsiHX_exwJVEu6Wz(CqtOW`(kCJ zx@^KU3q>xYU4axx-dxb=@8L8T4!EVm$gL?l>WZ5UGq}*-wf1w~0kuQ<4-=yLnncAA4?IW?IQny62u5L3tqX1 z%sY+D!wA3vZ*3J?z$&64WI`IL`gHI6Ah*V>x5BBnho;|LBsd(}$3+3d;sS?;z!nJ9 z_YzAEiDGqGhv!w^LVy-8oQ{RP3CMAb{jP^$BtJS-s#HYJ+87})2g*z1G_@p>`ZY@?7eqiP~O`?y?< zS)9yfw21T(n+@Y?Y=<4m2)X=F#qIg&*)$4JtT)TporO;zNRF^n8XOhUJ)kB3_>mR zON^*#c;8Mrd6+)+7{1jmMqr{-I(HV+O_71H3?8rzUDq6YKwwyipl^>0S8h@4y;g76 z`(DVM1IQFc{k2`(0UgGTXml3EvQBAyDx>r&)~H=tW^f8HtIk>lRgk6VMyV)a?OgSt z@=rd+(cOHSOTR1GC_L1Rv`EITPXJV$eKxVBi2R*$>%v{O3A7`lpMy2pEzzZvmG1zrBf|Mhwq75Qn1Pe!1Ogr@Y8y0P-pjxJ!cA}YY0wocK>@KIDe+=3x zP}1t(uRq*VWT9;nX@*d>3!0(B38N|gN`D{AJRND*nV!HOdzqPioAhRjeZ~Fr&-2^O z%^xoVtg|zLpaWy+0$0AH<}l{2jMruQ>l9TJLjY)7IGZfaUx5p|;hg)ar#N=+Nh{sz_F*~wd6J~J&P1d2DBWSI}mm~hnJ(~XW&vEN$a>${cZ<<h@G+d=;)n0eSsQUULb$`SFV!d#p~bXbAeg`awwTP33(Pjd>u0nr$%D3X|+5_nXK z5dj`XNOF(hacy(8DV7*SH*xe!ODQ|YXn*Qy?yduVcphbBC7Th3HpoKM2DkM=O;|xr z8;RvB@!Phco`Uv0+RPihozMM(+~3f>yK}xi$Lv;Pl=OIP2bWbUn2ZAym%bB0TR~%pA2(`z!Y@hW!=U!IinVws zBpc>zL#Y50Mmr0|$*RL%@;jgCB6NLFRJiK5>u(qJVpNMcAWf%~GGyvr??>@ifQ4&q z?HnDgqI@0e+{VtN47S8mz2aau>t`S4k?QtYiE-{EfQ?I)c4VTqkWp@l4pj$~!|_lO z_7o{C4OM`x6iIpSkPLj+!<>51`zh`|QW|QVp`(ztA5HJR^l`F9NP0`6@SaH_;Te<= z=|Dq`q9n|L6pjVJ%TGMWsY~MQy?vR|R|dkI7kzG;ZJBHx3H!P{QCsS!S!DYYjTwNI zA)H^epJNhnk3an!7ek=i_i-p$+eK{^(PPKxiN_kJF`sv6P!1|Jky-ZGQGeB;*DGW? z;GMTh_n+f_3qFNQkFbaxh|r|cT~}xeHn4aaLw`?Dp)aK$v!8No~5Wo({gv1!Rf6C_|BtwsU4DpJy9io|6|PaVYy!x z5avPekFj|j-jaMoAe8VbbZfK=wNDW{C|ovL<=@;obN)Ozm!-V!YtE&%XrnnqAj9t2 z;jde|-oc7u=Yz_XzKXE5=vqJr6_xMzo>-2_JaOYwV82Yn=ODhf2naIoP9&jYjdUB+4UDH%UFJCJPzOAU+B@$)FIEAMMtgb)-A)?lOPQ2Jn6&8`s~rqpa!eIKt<9>J;8wGG zC)r@7Q7hsyKlcz!SUh+asfR7PwffA~qa@5NIAacOAD9eHRs_?;13d<#UJtb>RvdMSEbf4bKKhwv;_Bryd<@-`Dc+7crMRL->{mbVP%cbfifsje@j zlPN$_MR{6#|FfKk#H~EXg?%EBF?|rl1vsPZ6fyP0w)ZAn&gx1WA z%=h?-r$9yG;X^2DppiC8K*~`O8XK@fGB<=I1gJQb1=5x5h9dcDif{_L4!~B+Ht+i^ z&pV^tUQ$GwwYa=VAqm9m)MU)infMWvQ<&G=N4}-6Zga)hTDVkL6(I@5-#0$YiA&wx zKj$WTyX;By8W_3``JEzSO9P6apu)P#d(Db`hOl5qb&PAAFzTDHH&4sLm&U!lDC9tK zc@?MMJB$;78Yt+FOgF54dErHf9jthGe_5z_P299KG0(ShmBUlw`xhniBZ*ybFLf}N zhki?R%(Dh&3Gk>VNjl!JcB%4-f(Ruv&a3dZ_w;kUnN&YD@r#Q_4ER3?I4nlVvZ8Yk zQ@3Mxv>JS3xULpXNXK?Wv2230QiQ_ORn)u?L7~kwZ?T7l zJ+iR7!fYQjm>Fg(eGA?_R7E~501V^5op+A4L%=(OKAOJLA+teVMS7&*+o(U$DuOD* z5^>kbyd~lbtN2=fT!8<)da6;Jwn1&araUgZDg4KDnh0~tLrFGRhr(o5^3d-^6CYt6 zhJ?9WMWXE4rJgCTE1Cc2MF@_Dg0!~w9|FG)(+7l-SXCJlOACs~VQnU-@%i(0HeoLQ zQ#|sywO^LhaYNjzSaFEVH!md%(s#uiCthzv#hSzZho13Ft1?u?tcaWXK>zTWRmb)D zM}NLPEQT-YT%P&KskiA+n@A%bmKUz=QM7qVJ=y|&SD5B@yM*Mm*G0pIQ2(6IEqAJi zJ~3%Q-EsoeqpURt-B2-^E!?BeNnv;>tl}M-Vz90P25&vsQm*h3D)9uzbM*Rxh)*iu+T{ zhc>DYj@wz3F$1EUA}K*XATi@;tao7*)ZU(1?rEW;oB#Nxb~^lomZru1W#I!mu$OX?X(J8iFk`Qzxl`A|y0ujaY4bCP@44?O!n zU46LTi_6wK{b4+jYA&`ot)=sWOhKLlS8wSD9&dMhjH<6klu??6Y6-DGjPa>*FHOwh+{y0c&S! zdRxb2hduDR(%vbKaYHnL28M%}JL!Khvll{%IoST%!cUd#@w3(@LS#~qK8Sh?`{3{A z#Dk5;{3@1iEkg0TjdaLQ2!dk^7h|x`uKBdR%GT}>LI^{dLRjHQZKx$|MPxIK;w|pV z3RNR9KQ~17xZ3r|94PykXO??PNJ^)CdO%5cQA}FDXW~_$P3sgJBF(4{bF)M`omy3# ze<=F<&C{gvmZcuO%x=OG57bYf1zzV+n1{Y-oKa0jp8`#QQFon1#6b+sPHW3xW?BD> z=U(Qa4s{mJ{Gm%o+d-#8yHvcx38xDr;OT-cR0*PkiV6Y0CZ+?$o(^Y%QH|Yt>q05d z0il;|50)EAm@;qe|HjS#@)Io@L<(B7y=W_U{gq4f$h+JuGBA}C>{3k6ZPh<6j6DAR z`@%HlmIog}cKl^rI8pETQfnJUBvIWqq9L?=8xe(t0`QEK@dbay8L|ZWMZAVfqphxDInc{WXaSGOxeQHKtfhSpDJ9(7gpe-F&?Mbh@EF zwG2P--aR*UDK+Vj6nqp?>h!-Nw-hl0U(R4D?}Z~!)X--aelb?3%826#0za~yyuY-L z=$Gqpb=Z1+`*O)C`cnXvO4PIz46egPBaEjjGdq3Nft6gYMR-@3paxTpYPK6<*D*c! zOJ6hwP2<#l1D_;VH7fLePopK{F*;_0ZdmI zMp9bFI>>du$1(*$qJbjPKD1-zOih$|ja`}JXkv_5Sc_A{SC@T4IBo`<(iM5v7AcA7 z_Mu59%;zV@aJWk|hQ)O|`m(1{N2U&)7gorxu8B~bpvhRj-F!F#V*A|s^@ls97z}8{ zKr43j+lrXt--U^!_KP+y0|31o?&gPX5BlNgxb!*2NN3v5KEb!&u0AzJm2W;=P%auo zT;b!7n)N3rxisvf5(6UP|6}i6o8m^2M&bGFU!mxIBd|}}AZ1kM}ovKbrNBvxbC z7?6MMCj*vo=Ang<}gX9RwMCq#K|+ z0LB~66K=1E-h>tmE8SI&3vWE1Zofz75oMkt=}=s2S)O zt7SOIsB{d|%Dd404C=z>UUWWkBwzghD0{UsD9BVG_FJIRKI@1MP5H z7xTj(RxO`)zJy=4+AwFbWjEc9)rFO z&qWy}=v2?3T82v~G>LBY-URvv!WPB`sthsc5g%Vg>;ii{0oeo{=3u747hv$Vik_WT z;HncqO_AAk_qxrgo(F=w3L>)x3j;O}xIPV|2r!I5vo&CNfJX@dfI=5Nv!J_gBUY+-D} zq*9*-FTr%wD7+7U|yA`-A6@VX(3P>@qt?23)L`D?jJF-w}VFnN#gj=|g7*_-L z+yNP8<$#qf6mbk<(Z(xUK%4N59<-#yAVT7kjxOw>iEr0=j*S`e$Eb4C0n~KZ0mH&H zgN|u=<(_+ki1q#TBB-PX(p`ZEHHv3C#BFIO=4APcV7` z>b*|0 zAljyZl`>tRMIdHaA+X}^1kkskfh-QJ49Gtk2(=o(g|1)glA>~Hu6 zeS}XUm=@Y#;Z{cYp0B4(K+FJ|Wa|^S15i`WTVl2y~f9FviOxs~u8ZAa@8&a1}0DrroiXsK zzzsS8ctG7P2Ls%I6*BNmFce(JKs*4cL$8kO2KXj;5W}w^UwWN`bS7o9fE@((NV70L zJaES$J|KZ_85nwCsFb_}k%Jq`kVWmU(V|5^+%Yyh^^^wlG_5}^UT8v~}s$EG5T zpu`A&AR2Jt02YCpFV`E4yUSKNCIWo$7}$AW+WPGfW&f`4tGhXXe7mnZzcUvMsiS%CIe7~yYHFu+&E&%%N| zybXN75_l`0)*69aWsHiftPS;13c~qqon_N_Ye65>*NYh7}IZTt;@?v=5+P-ZLPiM<5xS*^qR$;p&>`@G)0XQfFqh~Qr2Vew*xCw^C zdlQ0vZIDobfTLLKVCY0(k@Pz%I?)=zpxm7VU$;!r$QIAelRTRtrWIDiS2$pERh~|R zWVWds$8$wD6Tv{%3HJ78joYOyiucSRYlmZ7|QW@QlvJ6DLDLkBO3GRGT|G4-9EqDnds_ zl}>E{s4-v8(5Oa>3~>dp=YT$mI0U;W6eROY%F2-GkJOPVEIT2`27SS*gfTH%F2N?~4Qqws=1!04N z#UiJ3wqZ+!$%SZ54Nl1>I5@6&R7rNWSj-vdnImDqGmxXnF!h<@OLjzyKjf+$wjs4{ ziv=}D@s$W=ACpf>k)`MKz%)3{8eILp^X4@LDpj0Km}bjp4lt0$Y_ejX^Gn2@#6y&x zS{=X{A|QU=bobTT1|h9v;G@;Ht_Tx2j$s-H`HTalP&^Gzlkjwzu%@+TGQ>R`2SpMx z=Ya+U0v?ABW~c1Xgw{1U=lwd&Pj zpB53Ff+-&6EXY5d1t~)bdhJ3SUH;E+CneMgoh`B{zs#znDDgkBBd&vL2$Lu{ZWK3t zmQaD1j#VrBGaIMLn08m2T#kM$@hoIe28U6@O|#{695B3DD*%f6F zYBN|OLe10C6}Y_mf)yK9hl^vAF<=dbh{?xsTFw(`lyF01Q1&ork z?|ecU!qG2e{UR%Yg?vFGD8RkQ=m}}{&Hj|-5kW^BBM9LADGW}B)Ie1%hD5PQ#}!E& znzQ5cAmz7FSj(T&xZwOvV~6qX&&s=ByTkcBDu(ll{?u>@Y})WMT*~1TP$UImym>_@;Gv;A)rH` zHNU*(bWszjb&!7qp%m`Hf&qwG$2Wh%m5epot5d_A^g^=?rq=JGL_oz~LPZ%a#os_#&R?AQQt4b!s-!M3#*u?%hS_K8Hvl1l1*X4MuQCt&xZ_5 zFy~4NVl)Rpn1pZ<_Yna0583|KPzqka3kDl*TZ|Y|>}b|ylWDM+r`ZB_X-HjQB%Jd6 z5~LKWq-ih$!7K_&A}XMt@XzQYKV?xsnoJT7XH7w^6w+!KDGER~3dLJdJ71Bjf|D>t zjz~BQe%qY-dKSrK(mW0kZv$y8?~ysOZOLAOyuw74+I)*kw0D*`K;#ec+)w}ri*i`E zhO#Gl93ze4M6xb)FxV@Dx=RWx*1?$(KXQuK8bBD8oEcIujbI;9!oUkAquhXfsm6yU z@Zk7gWT)qIO1b%&*BN7h*}`!}%oq#jU?SZX+3=wFDC|{@nw(B6=~u5ad2z&K9la?J za0P~kN(51Yil^tCnXUEp2DJ;rlXyB$0l^I4pp~MeB{kG93bwT_LJWUrP*25fU2ju{ z3`qGo=d_Im8Armw8DO!HTpPbgC^08mnU+Womzpq^j7>lRO2Ajs;*@iiuwAw0MG+5S z^qgdir(09I%E590Xlw3r`k;%l{A`gebIvK$(D^tGsES0l>tIrZmwyK50YmV*s5OYP z5JY&?PgGKeisF}un&SmtjSD(cr=dz@aH8nIxm%m|x4K{JDfruw*d?C4oN2Vo)3S}AQfADO z(Exs+VJKBbNzT=~HLj>ICWXKeiOK{Jju)IKX`eX0WU|b|xRA_o4u`|^8fLxIY`*Yg zpiaxFWUkBe1W+qGJj;~vgeC6PN2WHa=}Yn=li+NI9WEj!kQ;R<{P*IB@m)2n3oroG zU&edp2{Qs%b(td@6YCf%4LGC-9M7cS6t}(42#5@d^}5U-0t>Y`C8+fqK%HFXFtKpK*5fp+DyMLRigA$ZOyFpctkOu)P=BZC`7})yoL4|l zY8X5PLtYiGR+;pUem{A2cyP3H@`{)$&x3qxF`aXBF1gp~c^nOaBS5WZJS5>tLj+t+ zRXL&^{O83l{_f81t3ChK;hR0F)e|p6*)N(6;UMs}gDu|IR%v}C;x)Rm4^7P*nuawr zEp>QqY~kS17G*NTkq-}E?*DQpLsR)l82jzJgOmL?d;W|4w`(4o+X#ihkN0+nO23GM zQi;S0j*s^b5B%NNJIBXZV~NC3QepoEfk#=<$?r#GI~R*fvG~`M1F}*Mg$#sV93Gtf z_ICed@8{o7_I3|n?A7WV9_<~x+CBDnkKXx*2d{r8{pQQKS^jEo=f&$iI%Qe;@w=ao z-X88k4gcWK|K;dieI|#me?LAse7ko=z1?>&cKl!WkN1Cm4F|XP>poP(3cUQ^C;q>W zcKoB)J0~v>-@d`3Y%(cAkj`h#8SVVCcW~mrg?amL4-Zd{*98R6*U+i{{Kgkvm%iqDVl%IKPh_^_*rs`F0XOy0iVf)rC|rxc>$g^FRL1>(__B`7aKCJ9vG# zLvGt7NQ>Gvez9}1a~If)C=tY)ezJ3NCm5S@LTuzWN3SWuUO&nd_!2hw!MisDA8htK+>ra^=)P!yAwW zK@+F=IVP&aXX+}hhzc`b#SxXBaAN8eHZKsmNi-E9C`_@$iiDSDkURT>F?fGhzLYpu zC_w}zcv!Zb%vNGE(K$1|04a!=Ob9{}O3+>z�D@lyS$(&CY2#H<^}m%hMwNekf0v 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