Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
37ad2d2
WIP: K1 receipt: re-measured at d597aa224; remaining work blocked on …
trissim Oct 11, 2026
e9c8ae0
Merge origin/main into K1 (E1 landed)
trissim Oct 11, 2026
3b656ba
WIP: K1: image metadata and array data are runtime-value family methods
trissim Oct 11, 2026
e9d89f7
WIP: K1: RuntimePlaneAxis is a family; composition mode merged into it
trissim Oct 11, 2026
296b9cc
WIP: K1: NoPlaneAxis replaces None plane axes; axis questions are fam…
trissim Oct 11, 2026
6c5a84f
WIP: K1: ObjectLabelDomainScope is a family that owns its plane-domai…
trissim Oct 11, 2026
089456b
WIP: K1: whole-axis and selected-plane projections are classes; plane…
trissim Oct 11, 2026
8bdce93
WIP: K1: per-slice source provenance is a runtime-value method; G3 pr…
trissim Oct 11, 2026
782476d
WIP: K1: absent scalar source metadata stays absent; volume-projectio…
trissim Oct 11, 2026
b71531b
WIP: K1 guards
trissim Oct 11, 2026
d52c7c1
WIP: K1: positive family predicates and require methods; no negated f…
trissim Oct 11, 2026
adc79e4
WIP: K1: scope validates declared ids by value; label set answers its…
trissim Oct 11, 2026
42a3d53
WIP: K1: label set answers its alias question; positive-flag guard
trissim Oct 11, 2026
1f57f18
Merge origin/main into K1 (N1b landed)
trissim Oct 12, 2026
d1fda82
K1: receipt: plane-mode families, typed absence, payload methods
trissim Oct 12, 2026
25d28d4
Merge origin/main into K1 (K8 landed)
trissim Oct 12, 2026
c8cbd1b
Merge origin/main into K1 (K7, B6b landed)
trissim Oct 12, 2026
2736e6d
Merge origin/main into K1 (U4, B6d landed)
trissim Oct 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions benchmark/equivalence/object_label_measurements.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Object-label derived runtime measurement projection."""

from __future__ import annotations
from openhcs.core.runtime_object_label_domains import PayloadLabelScope, PlaneLabelScope

from openhcs.core.equivalence.measurement_features import (
RuntimeMeasurementFactCounterMapping,
Expand Down Expand Up @@ -190,7 +191,7 @@ def has_declared_object_domain(self) -> bool:

def owns_payload_spatial_location(self) -> bool:
"""Return whether this payload is authoritative for dimensional locations."""
if self.domain.scope is not ObjectLabelDomainScope.PAYLOAD:
if self.domain.scope.ids_per_plane:
return False
projections = ObjectLabelMeasurementProjectionStrategy.for_scope(
self.domain.scope
Expand Down Expand Up @@ -322,7 +323,7 @@ class ObjectLabelMeasurementProjectionStrategy(ABC, metaclass=AutoRegisterMeta):
@classmethod
def for_scope(
cls,
scope: ObjectLabelDomainScope,
scope: type[ObjectLabelDomainScope],
) -> "ObjectLabelMeasurementProjectionStrategy":
matches = tuple(
strategy_type()
Expand Down Expand Up @@ -367,7 +368,7 @@ def declared_identifier_domains(
) -> tuple[tuple[int, ...], ...]:
"""Return declared ObjectNumber domains aligned to measurement planes."""
domain = context.object_labels.domain
if domain.scope is ObjectLabelDomainScope.PAYLOAD:
if domain.scope.ids_span_payload:
return context.dense_identity_domains()
return domain.declared_object_id_domains

Expand All @@ -377,8 +378,8 @@ class PayloadObjectLabelMeasurementProjectionStrategy(
):
"""Payload-scoped labels measure the dense object payload as one domain."""

scope = ObjectLabelDomainScope.PAYLOAD
strategy_label = ObjectLabelDomainScope.PAYLOAD.value
scope = PayloadLabelScope
strategy_label = PayloadLabelScope.name

def projections(
self,
Expand All @@ -401,8 +402,8 @@ class PlaneObjectLabelMeasurementProjectionStrategy(
):
"""Plane-scoped labels measure each dense XY plane independently."""

scope = ObjectLabelDomainScope.PLANE
strategy_label = ObjectLabelDomainScope.PLANE.value
scope = PlaneLabelScope
strategy_label = PlaneLabelScope.name

def projections(
self,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis
import os,sys,time,json,resource,ctypes,gc
from pathlib import Path
from multiprocessing import get_context
Expand All @@ -6,7 +7,6 @@
import numpy as np
from PIL import Image
from openhcs.core.runtime_image_values import ImageMetadataPayload,ImagePayloadMetadata,image_payload_data
from openhcs.core.runtime_plane_projection import RuntimePlaneAxis
from openhcs.core.aligned_image_payload import ImagePayloadSliceStack
from openhcs.interop.cellprofiler.image_normalization import normalize_cellprofiler_image_payload
from openhcs.processing.backends.cellprofiler.color import combine_color_to_gray
Expand All @@ -15,12 +15,12 @@
os.sched_setaffinity(0,{0})
root=Path('/home/ts/.cache/openhcs/cellprofiler_examples/ExampleWoundHealing/images')
data=np.stack([np.asarray(Image.open(root/name)) for name in ('DMSO_B5_t0.JPG','DMSO_B5_t24.JPG')])
metadata=ImagePayloadMetadata(source_channel_axis=-1,plane_axis=RuntimePlaneAxis.RUNTIME_SLICE,source_plane_intensity_scales=(255,255),source_plane_dtypes=('uint8','uint8'))
metadata=ImagePayloadMetadata(source_channel_axis=-1,plane_axis=RuntimeSlicePlaneAxis,source_plane_intensity_scales=(255,255),source_plane_dtypes=('uint8','uint8'))
payload=ImageMetadataPayload(data,metadata)
def chain():
normalized=normalize_cellprofiler_image_payload(payload)
gray=[combine_color_to_gray(normalized.metadata.for_leading_source_plane(i).payload_with(image_payload_data(normalized)[i]),(0,1,2),(1.,1.,1.)) for i in range(2)]
produced=ImagePayloadSliceStack.from_output_slices(gray,memory_type='numpy',plane_axis=RuntimePlaneAxis.RUNTIME_SLICE)
produced=ImagePayloadSliceStack.from_output_slices(gray,memory_type='numpy',plane_axis=RuntimeSlicePlaneAxis)
composed=produced.compose()
normalized_gray=normalize_cellprofiler_image_payload(composed)
smoothed=[smooth_image(image_payload_data(normalized_gray)[i],auto_object_size=False,object_size=20.) for i in range(2)]
Expand All @@ -44,7 +44,7 @@ def worker(slot,connection):
barrier.wait(); total_start=time.perf_counter(); before=usage(); phases={}
start=time.perf_counter(); normalized=normalize_cellprofiler_image_payload(payload); phases['normalize']=time.perf_counter()-start
start=time.perf_counter(); gray=[combine_color_to_gray(normalized.metadata.for_leading_source_plane(i).payload_with(image_payload_data(normalized)[i]),(0,1,2),(1.,1.,1.)) for i in range(2)]; phases['color']=time.perf_counter()-start
start=time.perf_counter(); produced=ImagePayloadSliceStack.from_output_slices(gray,memory_type='numpy',plane_axis=RuntimePlaneAxis.RUNTIME_SLICE); composed=produced.compose(); normalized_gray=normalize_cellprofiler_image_payload(composed); phases['publication_normalize']=time.perf_counter()-start
start=time.perf_counter(); produced=ImagePayloadSliceStack.from_output_slices(gray,memory_type='numpy',plane_axis=RuntimeSlicePlaneAxis); composed=produced.compose(); normalized_gray=normalize_cellprofiler_image_payload(composed); phases['publication_normalize']=time.perf_counter()-start
start=time.perf_counter(); smoothed=[smooth_image(image_payload_data(normalized_gray)[i],auto_object_size=False,object_size=20.) for i in range(2)]; phases['smooth']=time.perf_counter()-start
digest=[float(image_payload_data(a).sum(dtype=np.float64)) for a in smoothed]
del normalized,gray,smoothed,produced,composed,normalized_gray
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis
import os,sys,time,json,resource,ctypes,gc
from pathlib import Path
from multiprocessing import get_context
Expand All @@ -6,15 +7,14 @@
import numpy as np
from PIL import Image
from openhcs.core.runtime_image_values import ImageMetadataPayload,ImagePayloadMetadata,image_payload_data
from openhcs.core.runtime_plane_projection import RuntimePlaneAxis
from openhcs.interop.cellprofiler.image_normalization import normalize_cellprofiler_image_payload
from openhcs.processing.backends.cellprofiler.color import combine_color_to_gray
from openhcs.processing.backends.cellprofiler.smoothing import smooth_image
mode=sys.argv[1]; advice=int(sys.argv[2]); np._core.multiarray._set_madvise_hugepage(advice)
os.sched_setaffinity(0,{0})
root=Path('/home/ts/.cache/openhcs/cellprofiler_examples/ExampleWoundHealing/images')
data=np.stack([np.asarray(Image.open(root/name)) for name in ('DMSO_B5_t0.JPG','DMSO_B5_t24.JPG')])
metadata=ImagePayloadMetadata(source_channel_axis=-1,plane_axis=RuntimePlaneAxis.RUNTIME_SLICE,source_plane_intensity_scales=(255,255),source_plane_dtypes=('uint8','uint8'))
metadata=ImagePayloadMetadata(source_channel_axis=-1,plane_axis=RuntimeSlicePlaneAxis,source_plane_intensity_scales=(255,255),source_plane_dtypes=('uint8','uint8'))
payload=ImageMetadataPayload(data,metadata)
def chain():
normalized=normalize_cellprofiler_image_payload(payload)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
slices: Sequence[RuntimeArrayData],
) -> list[RuntimeArrayData]:
"""Project each child once, keeping strict batch-mask cardinality."""
if self.metadata.plane_axis is None:
if self.metadata.plane_axis.unstacked:
if len(slices) != 1:
raise ValueError(
"Image payload produced multiple slices without a declared "
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from pathlib import Path
from nominal_refactor_advisor.codemod import CodemodPlanDocument, CodemodSourceSnapshot, PatchTargetOperation, RefactorRecipe, SourceRewriteTarget, SourceTextReplacement
root=Path('/home/ts/code/projects/openhcs-compile-perf');path=root/'openhcs/core/runtime_image_values.py';old=path.read_text()
before=''' if self.metadata.plane_axis is None:
before=''' if self.metadata.plane_axis.unstacked:
if plane_index != 0:
raise ValueError(
"Image payload without a plane axis cannot select nonzero "
Expand Down
92 changes: 92 additions & 0 deletions docs/refactor/consolidation-20261009/K1-plane-modes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# K1: Image payloads and plane modes

**Status:** most of K1 landed through G3; this file records that, and the remainder K1 built. **Head audited:** `openhcs` `main` at `d597aa224` (#1205), re-measured at `44980dfe2` (#1203, E1) and merged up to `625150118`. **Rules:** [00-RULES.md](00-RULES.md). **Step 4.**
**Shared abstractions** ([02-SHARED-ABSTRACTIONS.md](02-SHARED-ABSTRACTIONS.md)). *Builds:* the plane-mode families. *Uses:* A1 (built by G3), `AutoRegisterMeta`, python-introspect's registered-class codec.

## Already done (G3)

| Audit item | At `d597aa224` |
|---|---|
| Bare-array vs carrier switch at 513 call sites | accessors and `ImagePayloadMetadataCarrier` gone |
| Four-type switch `payload_slices_for_alignment` | deleted; `alignment_slices`/`map_slices` on the slice family |
| Two slice-projection mechanisms | `RuntimeSliceProjectionStrategy` deleted; `RuntimeSliceProjectableValue` only |
| Triple payload dispatch in `steps/function_runtime.py` | payload methods |

## What was wrong (the remainder)

**Plane modes were three behaviourless enums whose meaning every caller re-decided, and their absence was `None`.**

- `RuntimePlaneAxis` (`RUNTIME_SLICE`/`SOURCE_BINDING`): 198 lines in 34 files, 93 member references, with an `EnumKeyedStrategyMixin` strategy family beside it.
- `ImagePayloadMetadataCompositionMode` (`STACK`/`BUNDLE`) restated `RuntimePlaneAxis` one to one (`for_plane_axis`): 64 lines in 10 files.
- `ObjectLabelDomainScope` (`PAYLOAD`/`PLANE`): 64 lines in 13 files, again with an enum-keyed strategy family (`ObjectLabelPlaneDomainStrategy`) beside it.
- `plane_axis is (not) None` at 69 lines in 24 files; `plane_index is (not) None` at 43 lines in 19 files (one projection class whose `None` index meant "whole axis").
- `image_metadata_of` / `array_data_of` (K2 handoff): two helpers that `isinstance`-switched on `ImagePayload`, 34 lines in 12 files.
- G3's gate `test_cellprofiler_projection_enters_only_runtime_slice_projection` failed on two type branches (`invocation.py`, `measurement_execution_support.py`).

## Target (as built)

```python
# core/runtime_plane_projection.py
class RuntimePlaneAxis(ABC, metaclass=AutoRegisterMeta): # members are used as classes; `name` is the wire spelling
leads_data / unstacked / one_plane_per_runtime_slice / one_plane_per_source_binding / keeps_image_whole
plane_index(projector, aliases) / axis_size(projector, aliases) / projected_axis(count)
keeps_planes_of(axis) / axis_when_stacked() / compose_provenance(...)
require_runtime_slices(message) / require_source_bindings(message)
named(name) / leading_axes() / require_member(value)
class RuntimeSlicePlaneAxis; class SourceBindingPlaneAxis
class NoPlaneAxis # typed absence: replaces `plane_axis None`

class RuntimePlaneAxisValueProjection(ABC) # preserve() / from_selected_plane() / for_plane()
class WholeAxisProjection; class SelectedPlaneProjection # only the selecting member has `plane_index`
class RuntimePlaneProjection(ABC) # stack() / selected()
class WholeStackProjection; class StackPlaneSelection

class RuntimeSliceProjectableValue: # every owned runtime value
image_metadata / array_data / scalar_source_provenance() / slice_source_provenances()

# core/runtime_object_label_domains.py
class ObjectLabelDomainScope(ABC, metaclass=AutoRegisterMeta) # owns the plane-domain behaviour
class PayloadLabelScope; class PlaneLabelScope # ids_span_payload / ids_per_plane
```

- **One plane-axis family.** The composition mode is the axis the members are composed along; `stack`/`bundle` provenance is `axis.compose_provenance`. Deleted: `ImagePayloadMetadataCompositionMode`, `RuntimePlaneAxisStrategy` and its two leaves.
- **Typed absence.** `plane_axis` is always a member; `NoPlaneAxis` answers each question (`axis_when_stacked()` is the runtime-slice axis, `projected_axis` of a consumed axis is `NoPlaneAxis`). A projection is a `WholeAxisProjection` or a `SelectedPlaneProjection`; an Optional index from a resolver becomes one of them once, in `for_plane`.
- **Scope family owns its strategy.** The enum-keyed `ObjectLabelPlaneDomainStrategy` family merged into `ObjectLabelDomainScope`; a domain validates its declared IDs through its scope.
- **Payload methods.** `image_metadata`, `array_data` and the per-slice source provenance are family methods; `as_runtime_value` wraps a foreign value once for layers that hold values of every kind. Deleted: `image_metadata_of`, `array_data_of`, `CellProfilerSourceIdentityMixin.scalar_source_provenance`.
- **Positive questions.** Each family question has a positive spelling or a `require_*` method; no caller negates a flag or compares with a member.

## Persisted state

| Store | Class | At cutover |
|---|---|---|
| Viewer image metadata, worker bundles, debug facts, CellProfiler profile fields | runtime | reset: `plane_axis` is always spelled (`"none"` where it was null); a plane-axis-less debug fact reads `declared_plane_axis='none'` |

## Guards

`tests/unit/test_k1_plane_mode_guards.py` (AST over `openhcs/`): the deleted names are gone; no comparison names a member of either family; no `*plane_axis` or `<projection>.plane_index` is compared with `None`; `plane_index` exists only on the selecting members and the two projection bases are abstract; no family flag is negated; every member answers the family questions (registry-driven). E1's table loses the three enums' 36 rows. G3's projection gate passes.

## Tests

- B5's G3 rows. Volume projection (4): three failed because `with_source_context_from` turned an absent scalar source record into an empty one (`{}` for `None`); fixed in production. The fourth asserted that one selected plane stays a `(1, Y, X)` stack, which #395 changed on purpose the next day (one selected plane is that source plane; `test_selected_plane_stream_materialization` pins it for both axes and the neurite, BaSiC and primary-object diagnostics depend on it); it now asserts #395's behaviour. Derived role (2): stale. The test wrote its main-flow file as a bare array, bypassing the runtime's declared TIFF write, so tifffile guessed RGB for a 4-plane stack; and it read the provenance planes in an old wire shape. It now writes through `ImageFileFormat.storage_write_batches` as the runtime does.
- Template crop (16): settled by K8 (#1208), not touched here.
- Deleted: four internal-order tests that mutated `plane_axis` to an invalid string after construction (a state the type no longer has).
- A test-only projection subclass now composes its capability with `WholeAxisProjection`.

## New-case experiments

- A new plane axis (say, one plane per time point): before, an enum member, a strategy leaf, a composition-mode member and a branch at each of ~90 comparisons. After: one `RuntimePlaneAxis` subclass.
- A new label scope: before, an enum member plus a strategy leaf plus each `is PAYLOAD` site. After: one `ObjectLabelDomainScope` subclass.

## Reassigned

| Item | Evidence | Belongs to |
|---|---|---|
| Plane resolvers answer `int \| None` | `RuntimePlaneAxisProjector.runtime_slice_plane_index()` and its CellProfiler adapter callers (`artifact_binding.py`, `adapter.py`): 6 `plane_index is None` reads of a resolver answer | K7 (execution protocol) |
| Optional record fields named `*plane_index` | `SpatialGraph.source_plane_index`, `RuntimeRelationshipPlaneRecord.plane_index`, `project_intensity_proof(plane_index)` | neurite module; K3 |
| `SourceBuildRequest.domain_scope: ... \| None` | "caller did not constrain the scope" in `runtime_object_label_building.py` | K5 (callable contract) |
| Undeclared 3- or 4-plane arrays are written as RGB | `TiffConfig()` leaves `photometric` unset and tifffile guesses | PolyStore |
| Names `composition_metadata_mode` / `metadata_mode` | now hold a plane axis | P2 (rename with the package move) |

## Done when

The three enums are families, absence is typed, the helper switch is gone, the guards, G3's gate and B5's volume-projection and derived-role rows pass, parity is 30/30, and the ratchet and nra-r1 show no regression.
4 changes: 2 additions & 2 deletions openhcs/agent/services/plate_streaming_service.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Agent-facing plate file streaming service."""

from __future__ import annotations
from openhcs.core.runtime_plane_projection import RuntimeSlicePlaneAxis

import json
from dataclasses import replace
Expand Down Expand Up @@ -39,7 +40,6 @@
PlateFileRecord,
)
from openhcs.core.runtime_image_values import ImagePayloadMetadata
from openhcs.core.runtime_plane_projection import RuntimePlaneAxis
from openhcs.core.source_image_provenance import (
SourceImageProvenance,
SourceImageProvenancePlanes,
Expand Down Expand Up @@ -424,7 +424,7 @@ def _receipt_source_projection(
ref=source_ref,
source_metadata=first_components,
image_metadata=ImagePayloadMetadata(
plane_axis=RuntimePlaneAxis.RUNTIME_SLICE,
plane_axis=RuntimeSlicePlaneAxis,
source_provenance=SourceImageProvenance(
source_path=image_path,
source_component_metadata=record.components,
Expand Down
Loading
Loading