Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ba8aa34
Pin integrated neurite pixel-unit gap and original source owners
trissim Oct 3, 2026
f542d41
Merge remote-tracking branch 'openhcsdev/main' into fix/neurite-pixel…
trissim Oct 4, 2026
21e1ca3
Clarify physical neurite schema and publish current unit-owner closure
trissim Oct 4, 2026
5dad36f
Own analysis dimension projections on original settings hooks
trissim Oct 4, 2026
4f47ad8
Publish qualified detector projection source checkpoint
trissim Oct 4, 2026
7916dda
Merge remote-tracking branch 'openhcsdev/main' into fix/neurite-pixel…
trissim Oct 4, 2026
6830b8f
docs: qualify current-main neurite projections and actual unit seam
trissim Oct 4, 2026
221357a
Merge commit 'b461b48852bdd226ca5ffdb1abd2b21f261fc70d' into fix/neur…
trissim Oct 5, 2026
e3af210
Revalidate current unit owners after native retirement source repair
trissim Oct 5, 2026
98ab957
Declare graph analysis units separately from native source calibration
trissim Oct 5, 2026
64d6559
Trust nominal graph spacing and delete duplicate type admission
trissim Oct 5, 2026
229e7bb
Publish complete graph-unit qualification and explicit pixel-route re…
trissim Oct 5, 2026
44a5ffa
Implement declared pixel neurite recipe on shared analysis owner
trissim Oct 5, 2026
155ced5
Exercise pixel declaration through original source-plan compiler
trissim Oct 5, 2026
1f5d9d7
Merge remote-tracking branch 'openhcsdev/main' into fix/neurite-pixel…
trissim Oct 5, 2026
8c0d9dd
Trust metric declarations and use native ROI reader path contract
trissim Oct 5, 2026
7628406
Qualify nuclear pixel settings and original skeleton test owner
trissim Oct 5, 2026
39df6d4
Name shared wavelength metric without falsely asserting physical units
trissim Oct 5, 2026
1d6673d
Publish executable pixel neurite qualification and installed receivin…
trissim Oct 5, 2026
e5a7fc7
Prepare ordinary installed pixel neurite receiving and original ACK q…
trissim Oct 5, 2026
28637bd
Record delivered ObjectState artifact and actual installed receiving …
trissim Oct 5, 2026
59cbf34
Qualify original ACK streaming fixture against whole installed pixel …
trissim Oct 5, 2026
714a379
Qualify installed public pixel-space execution and saved graph reopen
trissim Oct 5, 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
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
785 changes: 785 additions & 0 deletions docs/validation/neurite-pixel-coordinate-contract-20261003.rst

Large diffs are not rendered by default.

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
121 changes: 121 additions & 0 deletions docs/validation/neurite-public-pixel-acceptance-20261005.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
#541 installed public pixel-space acceptance
===========================================

Singer exercised the exact Dewey-released ADMIN541_PUBLIC17_88 once through
the original recorded-mcp owner. Receiving17 attempt02 is immutable source
cc1805bfd6526f9093ec80c4cf1f532245988ab9; #541 production is
39df6d49c34dd9efebf4b22e877e195c32052310. No scientific bundle was changed.

Actual user path
----------------

The sole CLI handle was 2961. Native 1358914/create1791230257.99 on 6020 TCP
became READY through its original owned observation, then explicit catalog
preparation selected this endpoint. The complete path-only projected source
document pipeline-public01.py used SOURCE_BINDINGS with empty acquisition
spacing, no physical pixel-size artifact, and the registered
neurite_outgrowth_metaxpress_pixels declaration.

Artifact plan passed; session-1 compiled as job-1, execution
2dcd29cd-75b1-4f93-a589-95f426884727, COMPLETE. Execution job-2,
240582cd-78b7-4033-a8c8-9ebc7d378690, COMPLETE with no execution errors.
The original values export is at the admitted HDD destination, values01.
ZMQRuntimeExecutionObservationExport.read().require_valid_observation()
accepts the original installed export: 12 artifacts on A01, three graph edges,
two cell rows and one summary row. Analysis coordinate_spacing.unit is PIXELS;
inherited acquisition source_voxel_spacing has no values. Both details CSVs
declare coordinate_unit=pixels and have no misleading _um columns. All three
persisted graph ROI feature records declare pixels; their original bound
acquisition metadata retains empty spacing. No SWC was written.

Public inventory resolved the actual saved graph archive. The public stream
reopened it as three feature-bearing native Shapes paths with 69 coordinates,
channel 2/site 1/Z 1/time 1/A01, pending_update=false. The independently saved
128x128 uint16 source image reopened through the same public inventory/stream
owner. Its summary reports 396 nonzero pixels, maximum 1000. Matched raw,
graph-only and combined render-complete screenshots were personally opened:
the three graph paths overlay their synthetic raw supports. This verifies
engineering geometry/presentation, not biological accuracy or physical units.

Unit/presentation qualification
-------------------------------

Native transforms use scale 1. Viewer payload summaries report these display
unit values as micrometers, although the saved graph acquisition spacing is
empty. This readback does NOT establish physical calibration and is not used
for the pixel metrics. The numerical graph metric, CSV and ROI features remain
explicitly pixels. The physical route/physical SWC guards are unchanged. This
qualification must remain visible; it does not negate the demonstrated pixel
execute/persist/reopen workflow.

Exact existing owner trace: SpatialGraph.coordinate_spacing owns the analysis
metric; inherited SourceVoxelSpacingFields owns acquisition spacing.
SpatialGraphROI materialization binds graph.source_voxel_spacing, not the
analysis metric, through original ImagePayloadMetadata/ROIArchiveSourceMetadata.
SourceVoxelSpacing.has_values is false for the empty tuple regardless of its
default enum tag. Thus the archive contains no calibrated coordinates.
On reopen ViewerStreamingSource.calibrated_metadata fills absent spacing from
plate_image_metadata; the metadata handler's source_voxel_spacing projects
get_pixel_size into a MICROMETERS tuple. This separate legacy numeric-default
projection explains the native summary and needs distinct source-owner review,
not changes to pixel graph metrics or a fabricated acquisition calibration.
Singer retains that follow-up boundary; it is not a pixel-route merge gate.

Parent independently opened these exact three original captures and checked
the two cell CSV lengths 23 and 59.982756, consolidated PrimaryCoordinateUnit
pixels and all three ROI pixel-unit features. The supported visual claim is
local alignment of synthetic horizontal/diagonal paths, not complete tracing
of every fixture support or biological success.

Known negatives and exact closure
--------------------------------

Original help/session --help shell parser errors, an undeclared short tool
name, and a snapshot request with output_directory instead of the declared
output_dir_path are retained. Those requests did not submit pipeline execution
or replay an UNKNOWN. Correctly declared subsequent requests succeeded.
verify-public01 failed because the receiving checker queried nonexistent
SpatialGraph.coordinate_unit; verify-public02 uses the actual owner
coordinate_spacing.unit and passes. Both originals are retained unchanged.

Viewer 1371582/create1791230595.53: public close acknowledged, endpoint
terminated, process_exited and succeeded all true. Native 1358914: original
owned close likewise returned all five true. One exit was sent to original
CLI2961, which terminated with numeric exit 2; this is retained separately
from successful native/tool outcomes and includes the known parser failures.
Independent ps shows client1355894/native1358914/viewer1371582 absent; TCP
6020/6021/7020/7021 listeners absent. No viewer was abandoned, no job replayed.
The original display helpers remain with Dewey; lane88 is returned for his
next separately recorded workflow. Science89/94/96 was untouched.

Original evidence
-----------------

Control root:
/home/ts/wt/openhcs-issue-batch-20260929/engineering541/public88-receiving17

Full journals are ADMIN541_PUBLIC17_88/author-workspace/output/runtime/
mcp.stdin, mcp.stdout and mcp.timing. verify-public01/02 scripts and original
stdout/stderr are siblings of this receipt. pipeline-public01.py is in that
author-workspace/output. Native/viewer logs remain in output/runtime/data/
openhcs/logs. The original operation and helper custody handoff is READY.rst
and #541 comment6001870666; READY-FREEZE.sha256 passed before launch.

HDD artifact root:
/run/media/ts/hdd/openhcs-engineering/engineering541-public88-20261005

CSV/ROI artifacts: results/acquisition_openhcs/results.
VALUES SHA256: 0849d1ad8b65d4be8c600073aa3e80f6b56720667d62766dec58a7f153fd799f.
Graph archive SHA256: b931643373add3b81fd9598765985f3d95a4745cf8c7690b6b3084f2993dd312.
Original raw SHA256: 3d33ce1805136d105973a36d961a6a844d904fa10e414293d5a8446f345975ab.

Personally opened original captures:
captures/raw/20261005T200517067165Z_napari_6021_OpenHCS_Napari_Visualization.png
captures/result/20261005T200538465883Z_napari_6021_OpenHCS_Napari_Visualization.png
captures/combined/20261005T200421506476Z_napari_6021_OpenHCS_Napari_Visualization.png

Source owner remains the existing engine profile/coordinate capability and
typed SourceVoxelSpacing/SpatialGraph/materializer family; receiving added no
algorithm, unit mirror, calibration alias or codec. Original source and
installed source-fixture controls are not rerun for this receiving receipt.

10 changes: 1 addition & 9 deletions openhcs/core/artifacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -1767,15 +1767,7 @@ def contextualize_output(
)
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
)
contextualized_provenance = output_value.source_provenance.with_missing_from(
source_provenance
)
if contextualized_provenance == output_value.source_provenance:
return output_value
return replace(output_value, source_provenance=contextualized_provenance)
return output_value.contextualized_source_metadata(image_payload_metadata(source_payload))

@classmethod
def contextualize_output_from_projector(
Expand Down
46 changes: 25 additions & 21 deletions openhcs/core/runtime_spatial_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,19 @@
from collections.abc import Mapping, Sequence
from dataclasses import dataclass, field
from pathlib import Path
from typing import ClassVar, Self
from typing import ClassVar, Self, TYPE_CHECKING

import numpy as np

from openhcs.core.artifacts import NamedArtifactPayload
from openhcs.core.source_metadata import SourceVoxelSpacing, SourceVoxelSpacingFields
from openhcs.core.source_image_provenance import (
SourceImageProvenance,
SourceImageProvenanceFields,
)

if TYPE_CHECKING:
from openhcs.core.runtime_image_values import ImagePayloadMetadata

SpatialGraphFeatureValue = str | int | float | bool | None
SpatialCoordinate = tuple[float, ...]

Expand Down Expand Up @@ -56,7 +59,7 @@ def _validated_features(

@dataclass(frozen=True, slots=True)
class SpatialGraphNode:
"""One spatial topology node with scalar features and a physical radius."""
"""One topology node with features and radius in its graph's analysis unit."""

node_id: int
coordinates: SpatialCoordinate
Expand Down Expand Up @@ -188,7 +191,7 @@ def feature_mapping(self) -> dict[str, SpatialGraphFeatureValue]:


@dataclass(slots=True)
class SpatialGraph(SourceImageProvenanceFields, NamedArtifactPayload):
class SpatialGraph(SourceImageProvenanceFields, SourceVoxelSpacingFields, NamedArtifactPayload):
"""Named spatial graph with direct node references and path geometry."""

SWC_TYPE_FEATURE: ClassVar[str] = "swc_type"
Expand All @@ -197,12 +200,18 @@ class SpatialGraph(SourceImageProvenanceFields, NamedArtifactPayload):
name: str
nodes: tuple[SpatialGraphNode, ...]
edges: tuple[SpatialGraphEdge, ...]
coordinate_spacing: SpatialCoordinate = (1.0, 1.0)
coordinate_spacing: SourceVoxelSpacing
"""Analysis metric per source-pixel coordinate; distinct from acquisition calibration.

Node radii and dimensional feature values use this metric's declared unit.
The inherited source_voxel_spacing retains acquisition/native coordinates.
"""
source_plane_index: int | None = None

def __post_init__(self, *source_provenance_values: object) -> None:
self.absorb_explicit_source_provenance(source_provenance_values)
self.normalize_source_provenance_fields()
self.normalize_source_voxel_spacing_fields()
self.validate_artifact_name()
nodes = tuple(self.nodes)
edges = tuple(self.edges)
Expand All @@ -216,15 +225,11 @@ def __post_init__(self, *source_provenance_values: object) -> None:
if len(set(edge_ids)) != len(edge_ids):
raise ValueError("SpatialGraph edge IDs must be unique.")

spacing = tuple(float(value) for value in self.coordinate_spacing)
spacing = self.coordinate_spacing.values_zyx
if len(spacing) not in (2, 3):
raise ValueError(
"SpatialGraph.coordinate_spacing must contain two or three values."
)
if not np.all(np.isfinite(spacing)) or any(value <= 0 for value in spacing):
raise ValueError(
"SpatialGraph.coordinate_spacing must contain finite positive values."
)
if any(len(node.coordinates) != len(spacing) for node in nodes):
raise ValueError(
"Every SpatialGraphNode dimensionality must match coordinate_spacing."
Expand All @@ -243,7 +248,6 @@ def __post_init__(self, *source_provenance_values: object) -> None:
raise ValueError(
"Every SpatialGraphEdge dimensionality must match coordinate_spacing."
)
object.__setattr__(self, "coordinate_spacing", spacing)
if self.source_plane_index is not None:
if isinstance(self.source_plane_index, bool) or not isinstance(
self.source_plane_index,
Expand All @@ -254,15 +258,15 @@ def __post_init__(self, *source_provenance_values: object) -> None:
raise ValueError("SpatialGraph.source_plane_index cannot be negative")
object.__setattr__(self, "source_plane_index", int(self.source_plane_index))

def contextualized_source_provenance(
self,
source_provenance: SourceImageProvenance,
) -> SourceImageProvenance:
"""Project invocation provenance through the graph's declared source plane."""

if self.source_plane_index is None:
return source_provenance
return source_provenance.for_source_plane(self.source_plane_index)
def contextualized_source_metadata(self, metadata: ImagePayloadMetadata) -> Self:
"""Bind acquisition provenance/calibration without altering the analysis metric."""
if self.source_plane_index is not None:
metadata = metadata.for_source_plane(self.source_plane_index)
provenance = self.source_provenance.with_missing_from(metadata.source_provenance)
spacing = self.source_voxel_spacing.with_missing_from(metadata.source_voxel_spacing)
if provenance == self.source_provenance and spacing == self.source_voxel_spacing:
return self
return self.replace_fields(source_provenance=provenance, source_voxel_spacing=spacing)

@classmethod
def from_swc(cls, path: str | Path) -> Self:
Expand Down Expand Up @@ -356,7 +360,7 @@ def from_swc(cls, path: str | Path) -> Self:
name=graph_name,
nodes=nodes,
edges=tuple(edges),
coordinate_spacing=(1.0, 1.0, 1.0),
coordinate_spacing=SourceVoxelSpacing((1.0, 1.0, 1.0)),
source_path=str(source_path),
)
graph.require_directed_forest()
Expand Down
28 changes: 24 additions & 4 deletions openhcs/core/source_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from abc import ABC, abstractmethod
from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence
from dataclasses import dataclass, field
from enum import Enum
from enum import Enum, StrEnum
from functools import lru_cache
from math import isfinite
from pathlib import Path
Expand Down Expand Up @@ -841,7 +841,7 @@ def merge_into(
)


class SourceVoxelSpacingUnit(Enum):
class SourceVoxelSpacingUnit(StrEnum):
"""Coordinate units own their projection into physical scalar calibration."""

native_unit: str
Expand All @@ -850,24 +850,32 @@ class SourceVoxelSpacingUnit(Enum):
"micrometers",
lambda spacing: spacing.isotropic_xy_spacing,
"micrometer",
lambda spacing: spacing.values_zyx,
)
RELATIVE = "relative", lambda spacing: None, "dimensionless"
RELATIVE = "relative", lambda spacing: None, "dimensionless", lambda spacing: None
PIXELS = "pixels", lambda spacing: None, "pixel", lambda spacing: None

def __new__(
cls,
name: str,
physical_projection: Callable[["SourceVoxelSpacing"], float | None],
native_unit: str,
physical_coordinates: Callable[["SourceVoxelSpacing"], tuple[float, ...] | None],
):
member = object.__new__(cls)
member = str.__new__(cls, name)
member._value_ = name
member._physical_projection = physical_projection
member.native_unit = native_unit
member._physical_coordinates = physical_coordinates
return member

def physical_pixel_size(self, spacing: "SourceVoxelSpacing") -> float | None:
return self._physical_projection(spacing)

def physical_coordinates(self, spacing: "SourceVoxelSpacing") -> tuple[float, ...] | None:
"""Project coordinates only when this declaration establishes physical units."""
return self._physical_coordinates(spacing)


@dataclass(frozen=True, slots=True)
class SourceVoxelSpacing:
Expand All @@ -889,6 +897,8 @@ class SourceVoxelSpacing:
coordinate ratios, including CellProfiler spacing normalized by Y. Relative
spacing and legacy spacing metadata without recorded units do not provide
physical scalar calibration. An empty values tuple leaves spacing unspecified.
PIXELS explicitly measures source-pixel coordinates; it does not establish
physical calibration or authorize physical coordinate exports.
"""

def __post_init__(self) -> None:
Expand Down Expand Up @@ -1004,6 +1014,16 @@ def require_physical_pixel_size(
)
return value

def require_physical_coordinates(self) -> tuple[float, ...]:
"""Admit physical exports without treating pixel or relative units as micrometers."""
coordinates = self.unit.physical_coordinates(self)
if not coordinates:
raise ValueError(
"Physical coordinate export requires explicit micrometer spacing; "
"pixel and relative analysis coordinates cannot provide it."
)
return coordinates

@classmethod
def resolve_physical_pixel_size(
cls,
Expand Down
18 changes: 13 additions & 5 deletions openhcs/processing/backends/analysis/count_cells_simple.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,14 @@ class MetaXpressWavelengthSettings:
intensity_above_local_background: float = 100.0
"""Minimum raw-intensity difference above adaptive local background."""

def minimum_width_px(self, coordinate_scale: float) -> float:
"""Project this wavelength's lower width into image pixels."""
return self.approx_min_width / coordinate_scale

def maximum_width_px(self, coordinate_scale: float) -> float:
"""Project this wavelength's upper width into image pixels."""
return self.approx_max_width / coordinate_scale

def validate(self, name: str) -> None:
"""Validate one wavelength's complete public settings block."""

Expand Down Expand Up @@ -579,23 +587,23 @@ def accepted_labels(self) -> np.ndarray:
def segment_metaxpress_round_objects(
slice_data: np.ndarray,
settings: MetaXpressWavelengthSettings,
pixel_size_um: float,
coordinate_scale: float,
) -> np.ndarray:
"""Segment bright round objects using shared MetaXpress-style controls."""
return round_object_segmentation_stages(
slice_data, settings, pixel_size_um
slice_data, settings, coordinate_scale
).accepted_labels


def round_object_segmentation_stages(
slice_data: np.ndarray,
settings: MetaXpressWavelengthSettings,
pixel_size_um: float,
coordinate_scale: float,
) -> RoundObjectSegmentationStages:
"""Run the shared detector once and retain its exact acceptance evidence."""

min_width_px = settings.approx_min_width / pixel_size_um
max_width_px = settings.approx_max_width / pixel_size_um
min_width_px = settings.minimum_width_px(coordinate_scale)
max_width_px = settings.maximum_width_px(coordinate_scale)
intensity_above_background = local_background_response(
slice_data,
object_width_px=max_width_px,
Expand Down
Loading
Loading