diff --git a/docs/validation/viewer-retirement-519-selection95/CHECKPOINT.rst b/docs/validation/viewer-retirement-519-selection95/CHECKPOINT.rst new file mode 100644 index 0000000000..c648b582c1 --- /dev/null +++ b/docs/validation/viewer-retirement-519-selection95/CHECKPOINT.rst @@ -0,0 +1,174 @@ +522 selected-member receiving packet: published source checkpoint +================================================================ + +Current packet location +----------------------- + +Owned checkout /home/ts/wt/openhcs-ui-workflow-rendering-20261001, directory +docs/validation/viewer-retirement-519-selection95. RECEIVING.rst contains the +custody, source relation,64MiB total budget and precise remaining prerequisite; +PUBLIC.commands contains public argv templates, not an executable batch. +fixture_522.py and pipeline.py are a NEW tiny engineering declaration/document, +not scientific inputs or product changes. None has been executed or staged. + +Source integration completed +---------------------------- + +Only Planck fixture commits7cfcf1713/945b959 were cherry-picked, as499800a2d/ +170370dce. The first context conflict introduced the already-merged524 feature +test missing from the old522 base; unrelated calibration context was not copied +by that pick. Normal merge130ef9d36e1856669f2e1920fdeadf7fd127ebc7 then integrates +parent-reviewed mainb1e9b5ca84b52c4831965421b2e33fd42a4e15ed, including its +original calibration consumers. No Planck combined-package ancestry imported. + +The native point builder retains original ROIArchiveSourceMetadata.feature_metadata +and derives member keys from NapariStreamLayerItem.ELEMENT_IDENTITY_FEATURE in +its existing assembly loop. The Shapes owner also retains that original metadata +projection. No duplicate transport-field exclusion, codec or selection authority. +The controller/cooperative presentation retention owner remains the reviewed522 +implementation. Root producer/persistence/materialization files were not edited. + +Exact byte comparisons against qualified package07 source8168e3aa: +napari_viewer_server.py and napari_streaming_handlers.py MATCH. The whole +openhcs production-tree comparison has ONLY the three independently-owned511 +catalog-preparation paths; no other production difference. Those511 paths are +not borrowed into522. This is byte equivalence, not a new runtime result. + +All three migrated test files byte-match Planck945b959 exactly: + +* test_napari_streaming_handlers.py: + b6466a57ad45ec6e28d5edeb6a645953715daec1a7f71bfea7d80571d457920e +* test_streaming_service.py: + b7ff0978f3ecbeab717621845f79a090284143cddeec377d7aa1b6284cf8e091 +* test_materialization_core.py: + 6ba9f0636d113954ce993c900265f093b57a356e5e9c7450d5956872937320d5 + +One stdlib-only syntax/byte check parsed the two integrated production files, +three fixtures and two new packet Python files: seven parsed, zero errors, +0.15s. No OpenHCS/scientific import, test, detector rerun, build, install or +runtime launch. Original installed88+24+3 controls and104-tool/fresh-health +receipts are READY-PACKAGE07-42.json/PACKAGE07-FAMILY42.rst under engineering494. +Original40 failures remain. Historical a78 all-nine-path R0/complete AST and +uncompleted global NRA OOM retain their original pins/limitations; neither is +renamed as a fresh integrated-head scan. Target07 is immutable; no rebuild. + +Precise remaining public prerequisite +------------------------------------ + +Parent owns the sole future95 client/native receiving. Saved-archive-only public +reopening does not yet demonstrate TWO endpoint items in each surviving route: +record-key producer grouping and declared-domain containment are determining +contracts, not permissions to fabricate a producer/source receipt. A singleton +well-coordinate translation is not rematerialization. The conditional grouped +pipeline is essential to the current proposed3-to2 relation and therefore needs +parent's decision on the known Root435 main-flow publication risk. No publication +fix, failed scientific replay, pipeline execution or native selection proof is +claimed. Source/installed selection controls and old06 public raw-survivor PASS +remain separate from the new Points-and-Shapes live remount acceptance. + +Packet bytes before this checkpoint +----------------------------------- + +Four files28099 apparent bytes/36KiB allocated, before this text receipt: + +* RECEIVING.rst SHA256 + 5db0fd3b697695cd81cc086d72fd928909582baf06d6bf0e45dafe4a2aa75ab8 +* PUBLIC.commands SHA256 + 41ecfc50c0ac6f728d232a61d4ec0ca78ec84a43e59c81c4a23a15b24a9c64d0 +* fixture_522.py SHA256 + f8f05ea40b53d7a4a0cfb1d6cf4744fe6b2d092d270af4788138b9233fcaeba7 +* pipeline.py SHA256 + 08f61ffb266250aad4a93ecf1977d2a2989c290cec028927759bd3d3878552a9 + +Foreign dirty gitlinks/untracked validation traces and frozen source/archives +are unchanged. No cleanup, package/skill hot update or scientific author contact. + +Root435 current source repair verified; receiving deferred +-------------------------------------------------------- + +After parent deferred95 to Dewey's full-volume SCI launch, Singer read current +PR394 head5af63decedc018a905c33057b47758bf84dc6520 and actual repair +e8587bd7eb0c4aa283bc300c5eb9f2294f24890f. The production writer compares the +persisted occurrence against record.filename_address, not the optional scalar +artifact address, while retaining complete image-metadata equality. This is +already implemented by Root, not an unowned patch for Singer to recreate. + +Current GitHub source blobs byte-identify the original repair: +function_outputs.py7297a44416dbead154e7950fddfe2fe2737001ad and +test_collapsed_persisted_occurrence_435.py564af6fce342a9cec9b650285e2fd7afa7cc464e. +The full383-line fixture covers collapsed SITE, scalar, genuine metadata/address +conflicts and original real TIFF publication/reopening. Root reports243 local +controls, not installed original494 acceptance. It does not declare this packet's +four-plane aggregateZ/two-endpoint Points-and-Shapes journey. No new test run. + +Existing Root integration request updated with that actual source progress: +https://github.com/OpenHCSDev/openhcs/pull/394#issuecomment-5968788140 +Root retains the writer/shared fixture; request is a link to an existing exact +aggregate control or a narrow extension through that owner. No competing edit, +new writer, installed patch or claim that whole394 must meet its optimization +target before an independently qualified usage repair can ship. + +Package07 still excludes Root435; all packet/current target bytes unchanged. +No95 reservation/process exists here. Parent must provide a fresh custody and +funding handoff after the SCI lane releases before any native receiving. No +source or runtime operation is waiting on an UNKNOWN retry, and no old failure +or untracked/foreign source is overwritten. + +Existing152 automatic Point-ROI producer: new installed witness +-------------------------------------------------------------- + +Parent requested read-only diagnosis of the original third-H002 v2 execution; +no scientist contact, live call, replay, image/table/ROI decode or source edit. +The retained original reply is under +/home/ts/wt/openhcs-issue-batch-20260929/next-h002-distinct95-20261003/third-h00207/ +H002_THIRD95/author-workspace/output/candidates/v2/execution-failed-viewer.json, +SHA256a6cc3d758e8101bfd8935d9a3fbb0917a1ee5c238ddcfdebed762f5cf1e32600. +Job5/fa9c98c9-81d3-4338-96c3-88ee8ac2a3f7 axis completed, but execution FAILED. +Computational output persistence is not successful viewer settlement. + +Original output/runtime/scratch/data/openhcs/logs/napari_detached_port_6015.log +lines73..124 records the first route error: NapariPointsLayerDisplayHandler. +geometric_component_values rejects "Fractional-Z point ROI requires a projected +z_index axis" at installed target07 line2191. The native execution log later +records the settlement progress request's Resource temporarily unavailable and +the generic execution failure. Original ManagedViewerLifecycleMixin settlement +and compiled_plate_execution failure propagation were read; no timeout change. +The v1 trailing-return ABI error is an independent original author failure. + +Saved viewer-state-complete.json has the automatic centre route unmounted with +empty axes. Saved viewer-state-reopened.json retains that unmounted route while +the separate manual archive route is mounted with STACK Z1..60. This is a saved +public-state comparison, not a new reopen, point-coordinate read or biology QA. +The actual failing automatic request's complete incoming wire operands were not +retained; do not claim those operands were directly observed. + +This is SAME tracked152 witness5962961567, not a new issue or viewer fallback: +https://github.com/OpenHCSDev/openhcs/issues/152#issuecomment-5962961567 +Planck already released the coherent494 producer family to Root394: +https://github.com/OpenHCSDev/openhcs/pull/394#issuecomment-5969244906 +Planck has appended this fresh witness to that existing request; no duplicate +request or competing patch here. Current source checks pin Root5570b7a458ab035c +and mainba7b26b82717a7bc4de5c9701b413aa64ba13e35. AST queries of both original +core/options modules show their point writer matches target07 exactly; neither +contains PointROIOutput or PointROIOptions.stream_source_identities. Both retain +merged523 ViewerStreamBackendCallKwargs.projected_outputs. Merged531's CP output +policy changes do not add that point producer family. These targeted AST facts +are not a new whole-family census or a structural refactor qualification. + +The ownership distinction is emitted scalar planes versus the source planes +represented by one archive (IDEN-1/BOUND-2). Existing MaterializationSpec. +emitted_source_identities only returns variable-plane writer identities; +ArtifactStreamSourceMetadataAuthority consumes that result, while the scoped +component layout removes missing declared axes. Original494 already provides +PointROIOutput's exact source-plane anchor, PointROIOptions.measurement_payload/ +stream_source_identities, shared FileOutputOptions behavior, and the existing +MaterializationSpec/ArtifactStreamSourceMetadataAuthority consumer migration. + +Shortest coherent closure is Root integrating that released family, preserving +full archive provenance/calibration, all original source guards and merged523 +projected-output accounting. Do not bulk-copy old494 core, guess Z in the viewer, +force a display mode, suppress settlement failure, add a pending store or widen +timeouts. Root owns its tiny new automatic producer/wire/strict-receiver control +and genuine malformed-domain negatives; parent owns fresh installed/public +acceptance after an explicit safe handoff. Original job remains FAILED and all +scientific outputs/journals/negative inputs remain untouched. No native test now. diff --git a/docs/validation/viewer-retirement-519-selection95/PUBLIC.commands b/docs/validation/viewer-retirement-519-selection95/PUBLIC.commands new file mode 100644 index 0000000000..d7df0c772d --- /dev/null +++ b/docs/validation/viewer-retirement-519-selection95/PUBLIC.commands @@ -0,0 +1,60 @@ +# SOURCE-ONLY, parent retained shell only; NOT an automatic batch. +# NOT DISPATCH-READY: grouped publication prerequisite/Root435 risk in RECEIVING.rst. +# Runtime markers are replaced from current public replies, NEVER old handles. +call openhcs_health_check --arguments '{}' --json +call openhcs_get_authoring_context --arguments '{"kind":"first_use"}' --json +call openhcs_get_authoring_context --arguments '{"kind":"headless_execution"}' --json +call openhcs_get_authoring_context --arguments '{"kind":"viewer_review"}' --json +tools --contains openhcs_register_custom_function --json +tools --contains openhcs_create_orchestrator_session_from_pipeline_source --json +tools --contains openhcs_submit_compile --json +tools --contains openhcs_submit_pipeline_execution --json +tools --contains openhcs_get_execution_status --json +tools --contains openhcs_query_plate_files --json +tools --contains openhcs_stream_plate_files_to_viewer --json +tools --contains openhcs_retire_viewer_window_layers --json +tools --contains openhcs_navigate_viewer_window --json +tools --contains openhcs_get_viewer_window_payloads --json +call openhcs_start_owned_runtime --arguments '{"host":"127.0.0.1","port":6014,"transport_mode":"tcp","persistent":true}' --json +# Observe SAME original returned native handle to READY, no start retry. +call openhcs_observe_owned_runtime --arguments '{"handle":}' --json +call openhcs_start_function_catalog_preparation --arguments '{"host":"127.0.0.1","port":6014,"transport_mode":"tcp","persistent":true}' --json +# Observe original returned catalog preparation, then register actual file bytes. +call openhcs_register_custom_function --arguments '{"source_code":,"function_name":"point_shape_volume_fixture_522","storage_dir":,"persist":true,"host":"127.0.0.1","port":6014,"transport_mode":"tcp","persistent":true}' --json +# Describe actual returned canonical ID; stable import must match pipeline.py. +call openhcs_create_config --arguments '{"config_type":"global","values":{"num_workers":1,"use_threading":true,"microscope":"source_bindings","materialize_runtime_artifacts":true}}' --json +call openhcs_inspect_pipeline_source_artifact_plan --arguments '{"plate_path":"/home/ts/wt/openhcs-issue-batch-20260929/engineering494/standalone95-selection40/endpoints","pipeline_source":,"global_config_id":}' --json +# Reflect current query schema; query source workspace returned by original projector. +call openhcs_query_plate_files --arguments '{"plate_path":,"kind":"image","limit":4}' --json +call openhcs_stream_plate_files_to_viewer --arguments '{"plate_path":,"file_paths":,"kind":"image","host":"127.0.0.1","port":6015,"transport_mode":"tcp","persistent":true,"fresh_viewer":false}' --json +viewer-state --port 6015 --host 127.0.0.1 --transport-mode tcp --json +call openhcs_create_orchestrator_session_from_pipeline_source --arguments '{"plate_path":"/home/ts/wt/openhcs-issue-batch-20260929/engineering494/standalone95-selection40/endpoints","pipeline_source":,"global_config_id":,"host":"127.0.0.1","port":6014,"transport_mode":"tcp","persistent":true}' --json +# Compile/execute/observe use original reflected schemas and returned IDs. +call openhcs_submit_compile --arguments '{"session_id":,"wait":false}' --json +call openhcs_get_execution_status --arguments '{"job_id":}' --json +# Only when that compile is terminal COMPLETE with the original artifact: +call openhcs_submit_pipeline_execution --arguments '{"session_id":,"compile_artifact_id":,"wait":false,"runtime_observation_export_path":"/home/ts/wt/openhcs-issue-batch-20260929/engineering494/standalone95-selection40/runtime/VALUES","runtime_observation_export_scope":"values"}' --json +call openhcs_get_execution_status --arguments '{"job_id":}' --json +# Observe ONE execution through terminal including automatic viewer settlement. +# Never dispatch another pipeline on an error/UNKNOWN or fabricate a stream receipt. +viewer-state --port 6015 --host 127.0.0.1 --transport-mode tcp --json +call openhcs_get_viewer_window_payloads --arguments '{"host":"127.0.0.1","port":6015,"transport_mode":"tcp","route_key":,"include_array_values":false,"include_shape_payloads":true,"max_shape_payloads":8}' --json +call openhcs_get_viewer_window_payloads --arguments '{"host":"127.0.0.1","port":6015,"transport_mode":"tcp","route_key":,"include_array_values":false,"include_shape_payloads":true,"max_shape_payloads":8}' --json +call openhcs_navigate_viewer_window --arguments '{"host":"127.0.0.1","port":6015,"transport_mode":"tcp","route_key":,"data_index":,"visible":true,"selected":true}' --json +call openhcs_navigate_viewer_window --arguments '{"host":"127.0.0.1","port":6015,"transport_mode":"tcp","route_key":,"data_index":,"visible":true,"selected":true}' --json +# Record BOTH selected sets/source association after original earlier work settles. +# Matched triples before/after use original public visibility/viewport/intensity +# commands with selected:null/no data_index; exact axes/camera from current replies. +call openhcs_set_viewer_native_window --arguments '{"host":"127.0.0.1","port":6015,"transport_mode":"tcp","presentation":{}}' --json +call openhcs_viewer_snapshot_window --arguments '{"host":"127.0.0.1","port":6015,"transport_mode":"tcp","output_dir_path":,"capture_scope":"window"}' --json +# Require BEFORE well extent3 in BOTH geometry survivors and complete member proof. +call openhcs_retire_viewer_window_layers --arguments '{"host":"127.0.0.1","port":6015,"transport_mode":"tcp","expected_producers":}' --json +# No reselection: read native indices/domain and original source/payloads FIRST. +viewer-state --port 6015 --host 127.0.0.1 --transport-mode tcp --json +# Require BOTH extent2 + same source member/group; 12 matched captures total. +# State/payload/source identity + actual native selected geometry, not indices alone. +# Stop at first definitive contradiction/UNKNOWN; preserve complete raw journals. +call openhcs_close_viewer_window --arguments '{"host":"127.0.0.1","port":6015,"transport_mode":"tcp","confirmed":true}' --json +call openhcs_close_owned_runtime --arguments '{"handle":}' --json +# Verify ACK/process identity+exit/ports gone; normal retained shell terminal. +exit diff --git a/docs/validation/viewer-retirement-519-selection95/RECEIVING.rst b/docs/validation/viewer-retirement-519-selection95/RECEIVING.rst new file mode 100644 index 0000000000..8df84c34c0 --- /dev/null +++ b/docs/validation/viewer-retirement-519-selection95/RECEIVING.rst @@ -0,0 +1,233 @@ +NEW95 selected-source Points and Shapes survivor receiving +========================================================= + +Source-only packet, Singer522; combined ordinary package Planck; ONE retained +public client/native/viewer and admission parent. No operation has been started. +Use NEW qualified target07 only. Original06, POINTS494_95, receiving29, live30, +all old journals, saved source/results and original failures stay immutable. +Production release is bf3223713d26068dfdcf7e8a660d9bb8fc53690d (a78 production). +Singer read original engineering494/READY-PACKAGE07-42.json in full: source +8168e3aa83322955aa25e246f634a88c9a7aaf8e, target07, wheel in wheels06 SHA256 +08f2de5edd9a64137b57cac57aaf89eb307aa50669cd502bfff412a43a1121a0. +It records actual104-tool construction, fresh healthy stdio MCP/child exited, +1816 accounted files/832 source-verified caches,88+24+3 installed controls PASS. +Original40 two fixture failures remain;41/42 correct only fixtures without a +rebuild. This installed tier is not detached survivor receiving. Read current +origins, pointer and fresh funding again at admission, without reinstalling. + +NOT READY FOR DISPATCH: the remaining prerequisite is an ordinary producer +mounting both endpoint wells in EACH geometry route. The saved-archive-only +alternative does not currently supply that relation through the public owners. +The grouped pipeline below is conditional on parent accepting its known Root435 +publication risk; it is not a proven substitute or a new Root repair proposal. + +Read the public contracts first (use-openhcs/viewer QA), then the exact original +source/projection/capability owners (NRA and authoritative audit ZIP). IDEN-6: +source membership is not a native row number. BOUND-2: existing typed source, +producer, result, lifecycle and selection authorities supply the facts. No new +consumer registry, selection mirror, serializer, launcher or camera algorithm. +This is an engineering receiving how-to, not an agent-facing science recipe. + +Determining public source relation +--------------------------------- + +The old raw and Points both retain Z0..3. Retiring raw does not prune that domain. +Manual ROI reopening uses a fixed producer per original result record.key: +PlateStreamingService.stream_files -> StreamingService.stream_rois -> original +native source binder/projector. Separate per-well archives therefore do NOT +produce the source-test's single route spanning two endpoint wells. A singleton +route's translation can shift without changing aligned extent/rematerializing. +No fabricated producer or altered archive/source receipt makes this a proof. + +Determining current source sites: PlateStreamingService.stream_files creates +fixed manual identities from each record.key (plate_streaming_service.py279..282). +Distinct relative archive keys therefore mount distinct singleton routes. The +same basename across separate result directories does not solve this: retaining +the first route item while declaring only the second archive's source domain +conflicts with the original projection step's require_contains contract. That +is source inference, not an attempted public call. The existing +reconcile_mounted_axis_projections rematerializes only when aligned_component_shape +changes (napari_viewer_server.py2363); its equal-shape branch only updates native +translation. Thus a singleton archive shifted from well ordinal2 to1 cannot +demonstrate the required remount. Do not relabel that path as full acceptance. + +Use the original494 engineering declaration, now separately named522 and with +ROIOptions(min_area=0) declared on its LABEL output. Its exact tiny numerical +computation is unchanged; the original measurement/PointROI/ROI writers supply +all source, subject, plane and calibration metadata. fixture_522.py is a new +engineering input declaration, not production or a viewer-manipulation helper. +pipeline.py is a complete ordinary typed PipelineDocument. One pipeline output +route per geometry family receives BOTH A01 and A03 under the ordinary compiled +producer, while independently streamed raw A02 is the sole contributor of A02. + +Before retirement, each survivor's well projection spans A01,A02,A03 (extent3). +After retiring ONLY the separate manual A02 raw route, it spans A01,A03 (extent2). +The original coordinator's shape-changing branch must rematerialize Points and +Shapes. Source A03 moves from viewer well coordinate2 to1, not to a different +source. This tests actual geometry remount, not a visibility/translation-only +case. Verify those actual projected extents on BOTH routes before dispatch; +if this prerequisite is absent, stop and report the actual determining state. + +Prepare the tiny new sources only after parent admission +------------------------------------------------------- + +CASE = /home/ts/wt/openhcs-issue-batch-20260929/engineering494/standalone95-selection40 +Prepared packet files live in the same522 checkout under this directory in docs. +Do not reuse an existing journal prefix or overwrite any staged file. +The source TIFFs are the four original326B engineering494 INPUT files named +A01_s001_w1_z000_t001.tif through z003, not results or scientific data. Verify +their original hashes/read-only participation. Stage byte-identical new copies +under CASE/endpoints with well filenames A01 and A03, and CASE/middle/source +with well filenames A02:12 files,3912B total. Record each original-to-new path +and exact byte equality/hash before any public call. Declare these NEW own +engineering wells; never claim they are extra acquisitions from the old source. +No image decode, processing, file staging or project import was done in this +source-only preparation. + +Use the original SourceBindingWorkspaceProjector.materialize to prepare ONLY +CASE/middle/workspace from the four CASE/middle/source files, via the existing +paired interpreter with asserted target07 origins. Take source_bindings from +pipeline_config in pipeline.py; do not execute pipeline_steps or the callable. +The original parser is SourceSchemaFilenameParser(), FileManager uses original +registered storage backends, source/workspace backend DISK, source_files exact4. +This is the existing tests/unit/test_source_binding_workspace.py materialization +contract, not a new metadata writer. Retain its returned metadata_path and source +projections. Do not write OpenHCS metadata by hand or infer virtual aliases. +Inventory that returned workspace publicly before streaming it; derive the +four file_paths from that actual inventory. Source well must be A02, fullZ0..3, +physical spacing2/.65/.65um. Pipeline endpoint initialization supplies its own +A01/A03 workspace. No result receipt, synthetic historical snapshot or guard +relaxation is used. Unexpected source projection is a retained known failure. + +Custody and bounded funding +--------------------------- + +Use Planck's existing engineering494 programme, recorded-mcp lifecycle, actual +qualified target07 and paired interpreter. No new guard/programme/launcher/env. +Parent must admit the extra processing leg explicitly: TCP6014/ACK7014 native, +TCP6015/ACK7015 viewer, DISPLAY:95 ONLY; never6012/6013/6016/6020/7777. Read current +Dalton95 X/WM/VNC identities (PID plus create/start), port absence and parent +full remaining SCI growth/floor funding before launch. Old helper IDs and old +closed native3457301/client63937 are NOT new handles or reusable releases. +Record original shell/CLI/MCP/native/viewer identities, paths, scope/peak/Swap0 +and all participating target roots. No launched handle exists in this packet. + +Total receiving disk allowance67108864B (64MiB), including this packet, staged +sources/workspaces/custom source, BOTH native and viewer logs/journals/captures, +all scratch/cache/temp/result files, registration and copied persisted receipts. +Allocate retained32MiB + scratch32MiB in the existing resource owner; no second +cap/store. Original byte copies3912B; reserve1MiB for ALL tiny source/workspace/ +result/custom declaration files,12MiB for complete journals/metadata,19MiB for +captures/receipts; scratch32MiB includes all new registry/Qt/Numba temporaries. +Reuse only original readonly native caches/deps with explicit borrowing proof. +The original processing leg previously allocated about26MiB scratch; the old +viewer-only receipt is not a processing budget. If scratch exceeds32MiB, stop +new work; do not raise bound, truncate logs, replay or invent a lighter writer. + +Native geometry1440x950 is the previously accepted readback, not a guaranteed +minimum. Planning twelve full-window RGBA rasters at that size plus65536B each +is66450432B, TOO LARGE even before journals/scratch. Therefore no claim that +arbitrary uncompressed captures fit. Check actual geometry/PNG allocated bytes +after the FIRST render_complete bitmap under original64MiB owner; existing +live30 nine native PNGs total730733B is measured history, not a guarantee. Budget +each NEXT capture from actual bytes and remaining funding. If compression/current +geometry cannot fund the next matched set, retain the originals and report that +determining budget to parent. No forced undersized Qt window or silent extra +capture allowance. Measure apparent AND allocated disk in the original owner. + +Public operations (PUBLIC.commands contains argv templates) +--------------------------------------------------------- + +1. Parent starts ONE original retained full public shell on admitted target07. + Actual MCP health/first_use/viewer_review/headless contexts and original tool + reflection first;104-tool construction must be healthy, not diagnostic-only. + Start/observe the ONE native6014 handle and its original catalog preparation. + Do not guess storage_dir, reuse an old handle or restart on uncertain startup. +2. Register fixture_522.py ONCE through original CustomFunctionManager, explicit + function_name=point_shape_volume_fixture_522 and returned native storage_dir. + Verify original persisted source hash, describe returned canonical ID and + stable export. Then create original global config (one worker/threading, + source_bindings/materialize_runtime_artifacts=true). Inspect complete source + artifact plan for CASE/endpoints and pipeline.py. Require exact2 wells, + 4x5x7 uint16/source Z0..3 and label/centre/source/ROI output declarations. + No source execution if publication or planning fails. +3. FIRST public-stream the middle workspace's four A02 raw planes on6015, with + default all-STACK Napari display configuration (same as pipeline.py). Record + actual raw route and complete nine-field producer. Require settled known + terminal stream and correct original source/calibration. This producer is + not either compiled endpoint pipeline geometry producer. +4. Create the ONE source-backed endpoint session; compile wait=false and observe + that original job. Execute its exact compiled artifact ONCE, observe SAME + job through terminal and automatic viewer settlement. Root435 owns that + existing publication/automatic-settlement path. A failure there remains an + independent receiving blocker, NOT evidence against selection restoration + or permission to substitute separate manual per-well singleton routes. + No replay of old494 job9292 or any UNKNOWN attempt. +5. Require BOTH native Points and Shapes routes mounted/settled, EACH source + item_count2 with A01/A03 and well projection extent3. Keep original image/ + Labels/source routes, outputs and every saved ROI archive. Record complete + state/payload and source/table/ZIP hashes, native axes/scale/translate/camera. + Point geometry comes from the exact fractional centre computed by the original + fixture; each label7 has two populated source planes and native shape members. + Confirm actual shape member cardinality/linked-subject binding, not a guessed + row count. Native features and saved payload order are different authorities. +6. Public data_index selection: use current payload/source participation and + original builder order to identify the A03 Point and A03 Shapes member(s), + never a hardcoded old data_index. Navigate/select the visible native Points, + then Shapes through the original controller; read back BOTH current selected + sets after earlier accepted navigation settles. Record any linked selection + across sibling outputs as the existing declared group, not only the clicked + row. If selecting one clears the other, stop at that determining state and + report the limitation before retirement; no unprepared second case, injected + selected_data or fabricated simultaneous selection. +7. Capture before: for EACH selected family at A03/XY with its actual source Z + and the same raw window, raw-only/result-only/combined. Keep the other geometry + and ALL underlying non-raw images/Labels hidden as appropriate. Use visibility + only, selected:null/no data_index; read back selected membership after toggles. + Do not use isolation if it changes selection. Read matched camera/native + world/source coordinates before EVERY capture and personally open all PNGs. +8. Obtain CURRENT complete producer identities of the sole manual A02 raw route. + Retire that route ONCE through the canonical nine-field guarded MCP request. + Require applied/observed/errors[] and exactly that retired route. Existing + coordinator validates whole selected incarnation before mutation. Retain raw + endpoint pipeline route, Points, Shapes, Labels, files and latest evidence. +9. Read state/payload BEFORE any new data_index/navigation: well domain A01/A03, + BOTH survivor extent2, same exact source rows/producer/path/calibration. Join + selected_data_indices to verified native member association/source receipts + and before/after native selected-outline/Features Table evidence. Compare + producer+source component address+path+original member/coordinate, including + linked subject members, NOT index equality or equal selection counts alone. + Public state currently exposes selected indices, not the native derived + openhcs_source_element feature column. Public payloads expose original source + members, not the native Features Table. Do NOT pretend that column was read + over MCP, recreate its repr codec or inspect native objects via a console. + If source-member association cannot be established from actual public native + geometry/readback and captures, report that precise proof limit to parent. + Source/installed reordered-member controls remain a separate evidence tier. +10. Capture the same two matched triples after pruning (12captures total), + navigating the viewer source A03 at its NEW coordinate1 only with no data_index + and no selection change. Native highlights/geometry must still identify the + same source member/group and body-local geometry. Global well ordinal2->1 + is expected; arbitrary camera change, cleared/changed member or stale callback + is a failure. Re-read state once after capture queue settles; no late movement. +11. Freeze complete original journals/state/payload/source/member/capture hashes + and actual allocated disk; no biological, RSS-drop or Root-wide readiness + claim. Public close the exact observed viewer, require ACK/process_exited/ + endpoint_terminated and PID/ports absence; public close original native handle + with the same proofs; normal shell exit and actual terminal. Parent/Dalton + retain95helper custody. No broad kill/restart/clear/archive deletion. + +Current checkpoint +------------------ + +Packet source and declaration semantics reviewed; no project import/test/build/ +install/staging/public/native operation here. Source folder allocated36KiB before +these receipt additions; final measured size is recorded in CHECKPOINT.rst. +Parent release of07 receiving, +its6014 processing leg and fresh64MiB/full-growth budget remain required. +This future public grouped-source route also exercises original Root435 +publication; the old manual receiving PASS does not establish that prerequisite. +The exact source-prerequisite decision remains with parent/Root: accept the new +ordinary grouped attempt with its original failure retained, or supply an +already-supported public multi-item saved-archive route. No public call occurred +and no receipt/path/producer was invented to remove that dependency. diff --git a/docs/validation/viewer-retirement-519-selection95/fixture_522.py b/docs/validation/viewer-retirement-519-selection95/fixture_522.py new file mode 100644 index 0000000000..3653124a30 --- /dev/null +++ b/docs/validation/viewer-retirement-519-selection95/fixture_522.py @@ -0,0 +1,77 @@ +"""Tiny engineering declaration derived from the original494 volume fixture.""" + +from dataclasses import dataclass + +import numpy as np + +from openhcs.core.memory import numpy as numpy_func +from openhcs.core.artifacts import ( + ImageArtifactType, MainFlowStackOutputSpec, MeasurementsArtifactType, + ObjectLabelsArtifactType, ObjectMeasurementSubjectRelation, +) +from openhcs.core.measurement_row_materialization import DataclassMeasurementColumnarRows +from openhcs.core.pipeline.function_contracts import artifact_outputs +from openhcs.core.runtime_measurements import ( + ObjectCoreMeasurementFeature, RuntimeMeasurementFeatureOwner, +) +from openhcs.processing.backends.lib_registry.unified_registry import ProcessingContract +from openhcs.processing.materialization import ( + CsvOptions, MaterializationSpec, PointROIOptions, ROIOptions, +) + + +class FixtureCentreFeatureOwner(RuntimeMeasurementFeatureOwner): + @classmethod + def owns_measurement_feature_name(cls, feature_name: str) -> bool: + return any(feature.feature_name == feature_name for feature in ObjectCoreMeasurementFeature) + + @classmethod + def owns_primary_measurement_feature_name(cls, feature_name: str) -> bool: + return cls.owns_measurement_feature_name(feature_name) + + +@dataclass(frozen=True) +class FixtureCentreRow: + object_label: int + center_z: float + center_y: float + center_x: float + + +FIXTURE_IMAGE = MainFlowStackOutputSpec.output('fixture_image_522', ImageArtifactType) +FIXTURE_LABELS = MainFlowStackOutputSpec.output( + 'fixture_labels_522', ObjectLabelsArtifactType, + materialization=MaterializationSpec(ROIOptions(min_area=0)), +) +FIXTURE_CENTRES = MainFlowStackOutputSpec.output( + 'fixture_centres_522', MeasurementsArtifactType, + measurement_feature_owner=FixtureCentreFeatureOwner, + relations=(ObjectMeasurementSubjectRelation(source=FIXTURE_LABELS.ref(), id_field='object_label'),), + materialization=MaterializationSpec( + CsvOptions(), + PointROIOptions( + z_feature=ObjectCoreMeasurementFeature.CENTER_Z, + y_feature=ObjectCoreMeasurementFeature.CENTER_Y, + x_feature=ObjectCoreMeasurementFeature.CENTER_X, + ), + ), +) + + +@numpy_func(contract=ProcessingContract.PURE_3D) +@artifact_outputs(FIXTURE_IMAGE, FIXTURE_LABELS, FIXTURE_CENTRES) +def point_shape_volume_fixture_522( + image: np.ndarray, +) -> tuple[np.ndarray, np.ndarray, DataclassMeasurementColumnarRows]: + """Original494 computation; add only the declared native ROI publication.""" + if image.shape != (4, 5, 7) or image.dtype != np.dtype('uint16'): + raise ValueError('Expected four-plane 5x7 uint16 engineering fixture') + foreground = image > 0 + if not np.any(foreground): + raise ValueError('Own positive engineering fixture unexpectedly empty') + z, y, x = np.argwhere(foreground).mean(axis=0) + labels = np.where(foreground, 7, 0).astype(np.int32) + rows = DataclassMeasurementColumnarRows( + (FixtureCentreRow(7, float(z), float(y), float(x)),), row_type=FixtureCentreRow, + ) + return image.copy(), labels, rows diff --git a/docs/validation/viewer-retirement-519-selection95/pipeline.py b/docs/validation/viewer-retirement-519-selection95/pipeline.py new file mode 100644 index 0000000000..ea9bf2bc79 --- /dev/null +++ b/docs/validation/viewer-retirement-519-selection95/pipeline.py @@ -0,0 +1,41 @@ +"""New endpoint-well engineering document; never execute during preparation.""" + +from pathlib import Path + +from openhcs.constants.constants import AllComponents, GroupBy, VariableComponents +from openhcs.core.config import ( + PipelineConfig, LazyProcessingConfig, LazyPathPlanningConfig, + LazySourceBindingsConfig, LazyNapariStreamingConfig, +) +from openhcs.core.source_bindings import ( + MetadataExtractionRule, MetadataSource, SourceFilterClause, + SourceFilterMatchType, SourceFilterSubject, +) +from openhcs.core.source_metadata import SourceVoxelSpacing +from openhcs.core.steps.function_step import FunctionStep +from openhcs.processing.custom_functions import point_shape_volume_fixture_522 +from zmqruntime.config import TransportMode + +ROOT = Path('/home/ts/wt/openhcs-issue-batch-20260929/engineering494/standalone95-selection40') + +pipeline_config = PipelineConfig( + processing_config=LazyProcessingConfig( + variable_components=[VariableComponents.Z_INDEX], group_by=GroupBy.CHANNEL, + ), + path_planning_config=LazyPathPlanningConfig(global_output_folder=ROOT / 'results'), + source_bindings_config=LazySourceBindingsConfig( + source_filters=(SourceFilterClause(SourceFilterSubject.EXTENSION, SourceFilterMatchType.IS_TIF),), + metadata_rules=(MetadataExtractionRule( + source=MetadataSource.FILE_NAME, + pattern=r'(?P[A-H]\d{2})_s(?P\d+)_w(?P\d+)_z(?P\d+)_t(?P\d+)\.tif', + ),), + source_stack_components=(AllComponents.Z_INDEX,), + source_voxel_spacing=SourceVoxelSpacing((2.0, 0.65, 0.65)), + ), + napari_streaming_config=LazyNapariStreamingConfig( + enabled=True, persistent=True, host='127.0.0.1', listen_host='127.0.0.1', + port=6015, transport_mode=TransportMode.TCP, + ), +) + +pipeline_steps = [FunctionStep(func=point_shape_volume_fixture_522, name='Own selected-member contract522')] diff --git a/docs/validation/viewer-retirement-522-native-frame15.rst b/docs/validation/viewer-retirement-522-native-frame15.rst new file mode 100644 index 0000000000..b67b2c507a --- /dev/null +++ b/docs/validation/viewer-retirement-522-native-frame15.rst @@ -0,0 +1,137 @@ +Installed15 source-frame retirement correction +============================================= + +Singer owns the original native-frame/remount/selection family. Planck confirmed +no overlapping geometry edits; canonical merged748 ROI metadata is preserved. +Normal main b461b48852bdd226ca5ffdb1abd2b21f261fc70d is integrated at77a878d8f. +The two fixture conflicts combine main's complete component domains and current +metadata with522's fractional-Z/navigation controls; foreign gitlinks are not +changed by this work. + +Original installed negative and exact closure +-------------------------------------------- + +The original engineering15 receipt root is +``/home/ts/wt/openhcs-issue-batch-20260929/engineering494/public748-after-writer95-attempt02``. +``OWNER-522-PARTIAL-RETIREMENT-FAILURE15.rst`` records one public retirement after +job4 COMPLETE. Before: A01/A02/A03 extent3, selected A03 Points row1 and A03/Z2 +Shapes row3. The producer-exact A02 retirement partially removed its route and +remounted raw/Points, then refused with: + + Rematerialized selected source members have no displayed geometry on the current slice. + +Points selection was emptied; Shapes still had the old domain; the native well +ordinal stayed2. This is a real partially mutated scene, not no-side-effects or +a successful retirement. Original BEFORE/AFTER states and payloads are retained +as ``OWNER-522-SELECTED-BEFORE15.json``, +``OWNER-522-RETIREMENT-FAILURE-STATE15.json`` and +``OWNER-522-{POINTS,SHAPES}-AFTER-FAILURE15.json``. No replay, reselection, live +patch, or science intervention occurred. + +Dewey's original client17936 is terminal1. Exact viewer690321 and native648055 +were positively closed, with independent PID absence and three sealed journals +verified. ``OWNER-TERMINAL-CUSTODY15.rst`` and +``OWNER-CLOSED-JOURNALS15.sha256`` preserve that disposition. Separate748 ordinary +saved reopening passed; it does not qualify selected-source remount. + +Owner correction and qualification boundary +------------------------------------------ + +``NapariAxisPresentation.preserve_native_axes`` already owns semantic/native +positions and ordering. It now derives the old source member from its original +projection and transform, then locates that same member through the replacement +projection and existing ``ViewerComponentCoordinateAuthority``. It preserves +within-plane world position and native spatial calibration, not just former +ordinals. An out-of-route or removed member does not invent a surviving identity; +mounted native bounds still own clipping. + +The original pinned R0 on1cf90c9c is retained as frame-r0-15: it detects four +additional raw transform-key reads. Those reads are replaced through Napari's +existing Affine owner, constructed from the original admitted native kwargs. +Its inverse, sliced forward transform and typed scale/translate own coordinate +conversion; no raw transform codec or copied inverse arithmetic remains. + +``NapariLayerDisplayPipeline.reconcile_mounted_axis_projections`` now prepares +that one detached batch frame for domain changes too, not only slot insertion. +Both rematerialized handlers and shape-neutral translation updates restore the +same frame. The existing handler ancestor now opens all declared cooperative +presentation contexts through ExitStack before any peer changes its slice. +It closes those contexts only after every survivor has its new geometry and +shared source frame. Per-handler late capture is deleted: an earlier raw remount +must not clear a later Shapes selection before it can be retained. +The original selectable capability's cooperative MRO still checks +displayed membership after frame restoration; the strict off-slice guard is not +weakened. No new selection store, camera owner, registry, or layer-kind dispatch. + +Relevant catalog risks: IDEN-6 (a source position keyed by an old display ordinal), +BOUND-2 (bypassing the existing projection/frame owners), IMPL-12 (forking frame +or selection restoration). The existing AST caller before15 parsed702 production, +706 test and401 dependency modules, zero parse failures, with73/58/43 selected +modules. It includes declarations, imports, reads/writes, dispatch and MRO sites. +``validation/viewer-retirement-519/frame-family-before15.jsonl`` is the complete +retained source evidence. This is not a global NRA/R1 clean claim: its historical +OOM remains a limitation. + +The first coherent source batch is retained as frame-controls15-01:45PASS/8FAIL. +It exposed the peer-before-selection-capture defect above, plus four old fixture +assumptions that A01 stayed current after expansion. Those fixtures now select +their declared A01 source frame through original navigation before assigning its +native members; no guard or assertion is weakened. + +Frame-controls15-02 has52PASS/1FAIL in13.31s. All four new joined/singleton and +row-reorder variants pass simultaneous raw/Points/Shapes retention at A03/Z2 +through extent3 to2, with non-unit Z2um spacing, XY.65, camera/order and exact +feature-owned source selection. Singleton routes exercise translation without +native replacement. The one remaining failure counted cooperative-hook calls +from fixture construction as if they belonged to retirement; its counter is +now reset immediately before the original retirement, retaining its exact +four-call assertion. Its terminal focused result is recorded below. + +Terminal source checkpoint +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Production4b44e19ff8338ec96e7846dc84113e327939aa92 carries the complete correction. +Frame-controls15-03 checked the corrected cooperative fixture:1PASS/52deselected. +Frame-controls15-04 checked the Affine conversion and affected calibrated/native +families:16PASS/200deselected in4.26s. Frame-controls15-05 has all53 real-Qt +controls PASS in12.97s (whole process19.80s,545412KiB maxRSS, process swaps0). + +The exact original15 BEFORE state also includes an unrelated hidden A01 saved +Points route with retained row0 outside the focused A03 slice. Unchanged geometry +is not newly rematerialized selection: the original request now owns that +retention decision in its context, rather than a foreign negative predicate in +the generic handler. Changed/remounted and translation-only survivors enter +their existing declared capability; untouched routes are not re-admitted or +erased. The competing computed predicate/consumer branch is deleted. + +The new four last-source controls include that untouched hidden route alongside +simultaneous Points/Shapes. They cover joined extent3-to2 replacement and +singleton translation, both source-row orders, exact source-member features, +non-unit Z/XY calibration, native frame/order/camera, no later navigation and +unchanged unrelated selection. Final request-owned lifecycle controls15-06: +6PASS/47deselected in2.40s. The independent declared Points/image capabilities +execute cooperative enter/exit hooks without generic consumer edits. + +Final existing audit caller parses702 production,706 tests and408 dependencies, +zero failures; selected73/58/53. It includes the original native Affine and +updated request/batch contexts, with declarations, writes, imports and MRO. The +earlier global NRA/R1 OOM is not converted to a clean claim. Original pinned +R0 is terminal0 against mainb461b488 over ALL THREE current changed production +paths: streaming handlers, viewer server and viewer controls. Every positive +delta is empty; raw StringSubscript decreases2. The intermediate full-PR R0's +foreign absence-probe growth1 is preserved and fixed at the existing request +owner, not offset or waived. + +Byte-exact source/tool/logs, original public partial-failure state and closure +receipts are published in +``docs/validation/viewer-retirement-native-frame15-20261005.tar.gz``; +SHA2566385ef7604fe5fa50f06114511cf5a29bd3362111b59d277ff1ef05a39a82481. +Original loose validation inputs remain preserved. No cleanup, package build, +install, public client or native launch occurred during this repair. + +These are source/real-Qt model controls, not installed public proof. A fresh +ordinary whole installed public case must pass once on Dewey's released lane; +original15 cannot be replayed. Original time/RSS logs are retained separately; +bootstrap cgroup peaks belong to the shared common slice, not this test process. +541's typed pixel-unit route follows this repair checkpoint. Closed394 is no +longer an exclusive source ownership dependency. diff --git a/docs/validation/viewer-retirement-native-frame15-20261005.tar.gz b/docs/validation/viewer-retirement-native-frame15-20261005.tar.gz new file mode 100644 index 0000000000..a4db460696 Binary files /dev/null and b/docs/validation/viewer-retirement-native-frame15-20261005.tar.gz differ diff --git a/docs/validation/viewer-selective-retirement-519-20261003.rst b/docs/validation/viewer-selective-retirement-519-20261003.rst index d90f83957f..1dea91df0a 100644 --- a/docs/validation/viewer-selective-retirement-519-20261003.rst +++ b/docs/validation/viewer-selective-retirement-519-20261003.rst @@ -4,6 +4,500 @@ Selective managed-viewer retirement519: original-owner source checkpoint Scope and custody ----------------- +Exact selection-retention integration release to Planck551 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Actual main78f2f0b31 contains merged549's current-slice selection authority, +but not522's selectable presentation/source-member retention family. Singer +releases that complete family at4245a9f73 to Planck's existing551 shared-axis/ +footer integration: NapariStreamLayerItem.element_identity/feature declaration, +original Shapes feature-column writer/caller, Points member/coordinate stamps, +NapariSelectablePresentationRetention and both registered geometry handler MROs, +NapariResultSelectionController.preserve_selection/_element_identities, and +their direct builder/remount/cooperative fixture consumers. No branch ancestry, +foreign gitlink or Root shared producer/unit file is released or reapplied. + +Planck must reuse main549's displayed_indices/select_indices, not restore the +old selection writes. Rank-changing remount must establish the existing semantic +viewer frame before native displayed-membership admission, preserving exact +source-member selection or reporting the original owner error. It must not drop +selection or bypass eligibility. The existing original handler rematerialize +and cooperative presentation capabilities carry the work. Singer will not +compete on551's layout/footer or copied selection mechanism. + +This is an affirmative integration release, not a claim that551 is implemented, +installed or live-qualified. Singer's separate fractionalXYZ declaration hooks +and remaining public receiving stay on522;541 remains Root-unit/frame dependent. +Source, current archives and frozen targets are unchanged. This checkpoint +requires no test/build/install or new lane; independent authors remain unblocked. + +Shared native selection projection received (2026-10-03) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Singer receives only Planck549f1b9607f0's two released source/fixture hunks, +not its branch ancestry, after reading current claims. Normal maineda0121a8 +integration changes only the disjoint operational funding family. Root394 +c50a947f4 still owns publication/source-frame/unit integration; its relevant +viewer and unit-source diff from74693f589 is empty. The named receiving +dependency is394comment5971678466;541's unit/frame request5970482598 remains +unreleased. No native/installed change or new worktree. + +NapariResultElementSelectionAuthority now owns both current-slice projection +and the exact native selection assignment/readback. Logical subject membership +remains on the existing group index. Controller synchronization and remount +restore use that one owner. Premature expansion and failure-finally replay are +deleted, as are both controller-native selected_data writes and the restore's +copied readback check. A remapped retained member outside the native slice is +explicitly rejected before native assignment, not silently dropped. Legitimate +empty native selection remains supported. No new registry, selection store, +layer-kind switch or alternate frame/camera authority is introduced. + +Original Napari Points/Shapes _indices_view owns visible row membership; Shapes +multi-member interaction_box calls create_box on displayed vertices and fails +if they are empty. Both original implementations were read. IMPL-12 and BOUND-2 +apply to the deleted writer/readback copies; IDEN-6 applies to preserving source +member identity instead of its former native row. Existing independent +selectable/presentation capabilities and cooperative MRO remain load bearing. + +The existing audit caller before01 parses704 production/675tests/401dependencies, +zero omissions, selected71/53/43, pinned872d42940/Rootc50a947f4. Original sources, +imports, declarations, checks, native writes and consumers are recorded in +projection-family-before01.stdout. Terminal0,19.513s,94.5MiB,Swap0. This is +complete family source evidence, not complete NRA/R1 proof; the historical +global OOM remains a limitation. + +Dalton's20:32:53Z read-only capacity check confirms one serial512MiB/CPU1/Swap0/ +60s source scope fits inside the same common slice and conservative remaining +science growth, with10MiB evidence allowance. Source controls follow the +coherent implementation; results will be recorded without rewriting negatives. +Public selected Points/Shapes remount remains the sole native receiving gate, +with Root's producer prerequisite and parent funding/custody. New three-author +launches do not wait on522/541. No scientific author was contacted. + +Terminal shared-selection qualification +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Productione31eda7f0 is unchanged at fixture-corrected3ba4da613. Original +projection-controls01 is retained43PASS/2FAIL/26deselected. One received fixture +attached a group binding without its declared owner feature; another old +cooperative-hook fixture selected an off-slice native Points row before its +source frame. The corrected fixtures declare the feature and use the original +navigation prepare/viewer_step, respectively. Assertions, native admission and +stored geometry are unchanged; production was not revised in response. + +projection-controls02 is terminal45PASS/26deselected in5.89s; whole scope12.363s, +cgroup347475968B/Swap0/OOM0, CPU1/512MiB/60s in the existing common slice. +It exercises genuine Shapes empty-interaction-box failure and its projected +linked selection, mixed Points/Shapes logical versus displayed membership, +queued navigation before expansion, exact assignments, both geometry-family +remount rejection before an off-slice selection write, valid empty selection, +source row reordering, real typed retirement/domain pruning, fractionalXYZ, +strict Shapes/acquisition laws and independent cooperative presentation hooks. +No consumer edit is needed for the new registered presentation capability. + +The existing source-control caller receives Planck's explicit source04 peer +bootstrap mode. Read-only target08 supplies unchanged peer dependencies; all +THREE changed production modules are loaded by their exact source paths and +hashes printed in the raw reply. Original paired Qt snapshot binding remains +read-only. This is source + real Qt + ViewerModel, not an installed MCP/server +or detached native rendering acceptance. Both fixture files are collected by +the existing pytest owner; no second runner or product bootstrap is added. + +projection-family-after01 pins3ba4da613/Rootc50a947f4:704production/675tests/ +401dependencies, selected71/53/43, zero parse omissions; terminal0/20.692s/ +91.8MiB/Swap0. The shared audit AST facts include declaration/import/base, +write/read/call/check closure, with original native Points/Shapes consumers. +One selected_data assignment remains in NapariResultElementSelectionAuthority; +both controller copies are gone. Projection, source identities and logical +subject membership remain distinct authorities, not duplicated stores. + +Original pinned R0 projection-r0-01 is terminalPASS26.113s/78.8MiB/Swap0, +detector3b03785f unchanged, base maineda0121a8/head3ba4da613. All THREE actual +production deltas are included: napari_viewer_server.py, +napari_streaming_handlers.py and viewer_controls.py. Positive deltas are empty; +no omission/waiver/cap increase or full NRA/R1 claim. Full global NRA's earlier +OOM remains retained. Production/fixture/receipt diff-check is clean; raw logs +are not rewritten to remove authentic whitespace. + +Byte-exact18-member archive,1126237bytes, tar compare terminal0: +viewer-selective-retirement-522-shared-selection-20261003.tar.gz, SHA256 +04cbde5af7c74ad751b090562905d056fa1aee62ae17474547cf04b8c4e1b5b0. +It includes current three production files, both fixtures, all original callers +and the complete before/after/R0/failed-and-passing raw logs. Older archives, +untracked ledgers and all seven foreign gitlinks remain preserved. All five +source scopes are terminal; no owned runtime/client or UNKNOWN source handle. + +Remaining acceptance is unchanged: ONE parent-reviewed ordinary installed +Points AND Shapes selected-member remount/fractional navigation journey after +Root's published grouped-producer checkpoint and fresh measured native release. +No slot is reserved and no client is launched. Separate541 still needs Root's +named unit/frame release; it is not an excuse to hold the independent new fleet. + +Resumed spatial-point selection owner (2026-10-03) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Singer resumed after the recorded CLI pause. PR5222ce107dad, PR541ba8aa341e, +main0332a696f and Root39461ab83842 were read before editing. Planck confirmed +successor08 is source/package preparation only and claims no navigation hunk. +Root's source-frame, point-producer, graph, persistence and materialization +owners remain protected. PR541 still requires the narrow shared-unit release +requested in394 comment5970482598; no unit/calibration bypass is implemented. +Normal merge5f8d153ed receives0332 without changing any foreign worktree or +the seven recorded external gitlinks. Original failures and retained untracked +ledgers remain in place. No build, install, provider, native or viewer operation. + +The independent determining witness is the frozen target07 public point-row +selection failure in original H002_THIRD95 output/runtime/mcp.stdout at70625, +70672 and72365: XY accepts the same row; XZ/YZ reject its fractional hidden +X/Y coordinate. Planck's394 comment5969244906 records this consumer defect +separately from producer152/494. No scientific input or reference answer was +read, changed, replayed or sent to an author. + +The existing ViewerResultElementCoordinateAuthority still owns coordinate-row +admission and slice derivation. Its numeric admission is shared by the strict +parent and the renamed ViewerPointCoordinateAuthority leaf. That leaf rounds +only axes supplied by the original NapariAxisPresentation.spatial_axis_labels; +acquisition indices delegate through cooperative super and remain integral. +It does not round stored geometry, infer axes from rank, or relax Shapes. +NapariLayerDisplayHandler declares the strict owner; the Points member declares +its spatial coordinate owner. The existing handler registry resolves the routed +source item's type. Navigation no longer switches on a concrete Points class. +The old Z-only class, direct ancestor call and duplicated finite/numeric checks +are deleted, with both original direct fixture consumers migrated. + +Applicable catalog patterns: IMPL-3/4 concrete dispatch outside an existing +family; IMPL-12 copied numeric admission; BOUND-2 bypassing original route/axis +contracts; IDEN-1 distinguishing a navigation slice from stored geometry. +Existing selectable/common presentation capabilities and their cooperative MRO +remain unchanged. A new source case needs only its registered handler declaration +and coordinate hook; the fixture adds an independent admission-recording +capability and exercises cooperative super through the unchanged consumer. + +Existing complete AST caller extended its selected symbols, not its scanner: +before01 parses704 production/675tests/401dependencies, selected71/53/42, +zero parse omissions; source5f8d153ed/Root61ab83842,23.952s/maxRSS84084KiB, +cgroup86.8MiB/Swap0. Original audit ParsedModule/FunctionFacts/Repository +provide declarations, imports, reads/writes, checks and bases; this is not +global NRA proof. The earlier full NRA/R1 OOM remains preserved, not repeated. + +Dalton's persistent SINGER522-ONE-SERIAL-SOURCE-CAPACITY-READONLY-20261003-1714.rst +confirms one serial CPU1/512MiB/Swap0/60s source check fits the original common +slice, physical headroom and conservative remaining disk reservations. It is +not admission for a build/native fleet. New raw before01 and controls01 logs +are retained under validation/viewer-retirement-519; final controls and changed +source R0/after-AST outcomes will be appended at their actual terminal strength. + +Actual public selected Points/Shapes rematerialization and fractional X/Y +navigation remain parent receiving work after a fresh qualified target and +resource release. Target07 is immutable; no claimed installed fix or new lane. + +Terminal spatial-point source qualification +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Productiona2762b832/e4db2163b is unchanged between the two heads; e4db only +corrects the new fixture's mistaken trailing-axis assumption. Original +fractional-controls01 retains27PASS/4FAIL/62deselected,12.217s/maxRSS467852KiB, +cgroup339.9MiB/Swap0/OOM0. The failing comparison included WELL, whose index +correctly changes during pruning; the registered axis order is not trailingZYX. +No scientific coordinate, source guard or expected geometry was relaxed. +The fixture now obtains spatial columns from the original presentation owner. + +fractional-controls02 is terminal31PASS/62deselected,10.261s/ +maxRSS465352KiB/cgroup280825856B/Swap0/OOM0. It uses the original source +bootstrap/run_source_controls.py, real Qt scheduling and ViewerModel, plugin/ +conftest/provider-free, CPU1/512MiB/60s in openhcs-blindsol3phase03.slice. +The original mixed-Qt snapshot owner remains borrowed read-only. No MCP, +native server, GL canvas or scientific input was launched. + +The three orthogonal cases select the same source member before/after actual +typed retirement, interior well-domain pruning3to2 and native rematerialization. +Source items are reordered; the same source member moves row1to0. Stored Z/Y/X, +source payload, native selection and0.65 calibration remain exact. Both explicit +display pair and implicit actual native displayed dimensions agree. The new +registered point declaration composes independent coordinate-admission recording +with the point owner through cooperative super, and all three spatial hooks +execute through the unchanged generic navigation consumer. Existing Points/ +Shapes survivor, linked-selection, empty-selection and no-late-navigation +controls are included, alongside strict acquisition and hidden-Shape guards. + +fractional-family-after01 names e4db2163b and pinned Root61ab83842; +704production/675tests/401dependencies,71/53/42 selected, zero parse omissions, +23.734s/maxRSS84032KiB/cgroup76.6MiB/Swap0. Declaration/read/write/import/base +closure confirms one shared coordinate algorithm, handler-owned leaf selection, +and no remaining production import/use of the old Z-only authority or concrete +Points switch in result_element_axis_indices. Dynamic registration is exercised +by the new-case control; AST alone is not executed proof. + +Original Git-pinned R0 fractional-r0-01 names main0332a696f/e4db2163b, ALL THREE +actual production delta paths: napari_streaming_handlers.py, +napari_viewer_server.py and viewer_controls.py. TerminalPASS24.412s/ +maxRSS87008KiB/cgroup76MiB/Swap0; positive deltas are empty. Detector3b03785f +and run_pinned_r0_419.py are unchanged, with no omitted path, raised threshold +or full NRA/R1 claim. Production/tests/docs diff-check is clean; original raw +failure-log whitespace is archived byte-exact rather than rewritten. + +Root advanced to74693f589 during qualification. Exact61abto74693 source diff +is EMPTY for all three viewer owners and the two protected source-metadata/ +graph-unit files. Other Root changes remain its qualification responsibility; +this receipt does not relabel the AST's pinned Root revision or receive394. +PR541's shared release is still absent in current comments. All source units +are terminal; no runtime/client/UNKNOWN source operation remains active. + +Byte-exact new source archive: +viewer-selective-retirement-522-fractional-spatial-20261003.tar.gz, +1124614bytes/19members, SHA256 +22d65ada8008f1237f7ec8a333ebcdb1ad172d5218980c9aeddf6cd9b4f6030e. +It contains all three actual production delta files, three current fixture +files, the three original callers, both original control outcomes, before/after +AST and original R0 raw stdout/stderr. GNU tar diff against each original is +quiet/terminal0. Prior source archives and loose untracked originals are +unchanged. New raw evidence plus this archive remains within the10MiB allowance. + +New95 source packet and migrated consumer family +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +See viewer-retirement-519-selection95/CHECKPOINT.rst and RECEIVING.rst for the +direct published packet, not a secondhand ready claim. Planck7cfcf1713/945b959 +fixture-only changes are received on522; normal merge130ef9d36 integrates +parent-reviewed mainb1e9/524 without combined-package ancestry. Both original +native builders byte-match qualified07/8168; all three migrated tests match +Planck945b959. No tests/build/install/runtime or original R0 repeated. +Original current07 READY42 records104 tools/fresh health/1816files/832caches/ +88+24+3 installed PASS, original40 failures retained, target immutable. +The new public geometry-survivor remount remains NOT RUN. The precise packet +dependency is grouped endpoint publication: manual archive singleton routes +only translate, and Root435 publication is not part of main07. No fabricated +receipt/group or competing Root implementation removes that limitation. + +Current-head selection qualification and received live proof +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Published production/fixtures a78b0d68da141a2923d05deb8e92d45293f9ded2 are frozen +for this checkpoint. Later receipt-only commits do not invalidate these bytes. +Selection14 and changed-builder9 are terminal PASS, not repeated old33 controls: +selection04 10.03s/maxRSS507916KiB/cgroup315.5MiB; builders01 8.01s/ +maxRSS444524KiB/cgroup270MiB. Both CPU1/512MiB/Swap0/60s, original paired +bootstrap and environment, plugin/conftest/provider-free. Explicit deselections +21 and139 remain in original raw stdout, not a full-suite claim. + +Existing complete-family audit caller before/after parses704 production/669tests/ +401 actual dependency modules, zero parse omissions, selected69/51/42. After +selection-family02 names actual current a78b0d68d/Root4c0d62e45;17.03s/ +maxRSS85432KiB/cgroup81.9MiB/Swap0. Source declaration search names exactly ONE +production literal openhcs_source_element: NapariStreamLayerItem's ClassVar. +Its existing native builders and controller read that declaration; no second +identity roster, body/object-ID counter, coordinate/camera authority or codec. +There was no old row-retention algorithm to relocate: the missing bridge is +implemented on the original controller; obsolete replaced-layer callbacks and +their original weak derived indices are removed in the same recipe. + +Original pinned R0 selection-r0-08 is terminal PASS on actual a78b0d68d versus +mainf7de, ALL9 changed production paths, positive delta map EMPTY.28.81s/ +maxRSS87148KiB/cgroup75.8MiB/Swap0; unchanged original Git-pinned detector +3b03785f45df2ef5dc62ba6aed99294192ecbb01 and caller run_pinned_r0_419.py. +No detector copy, omitted changed file, waived positive delta or widened bound. +Production/test/docs diff-check passes; authentic raw log whitespace remains. + +One actual latest NRA full/raw-record context attempt is UNQUALIFIED: +selection-r1-01.json is original EMPTY output; stderr records hard cgroup +OOM-kill at5.889s/peak512MiB/Swap0 before any findings. It used both changed +files, full OpenHCS production plus original ZMQ/metaclass/paired Qt/PolyStore +and actual complete Napari source context, parse/analysis workers1, +--no-auto-context-root --no-cache --json --raw-findings --json-payload full, +original CPU1/512MiB/Swap0/60s. No product module/provider/native execution, +runtime mutation or uncertain external side effect. No retry, limit increase, +copied scanner or falsely clean R1. Complete AST and R0 above remain qualified +at their own strength; complete NRA raw findings are the explicit remaining +structural limit, not represented by a local partial result. + +Singer read the complete ORIGINAL parent standalone95-live30/ACTUAL-RECEIVING.rst +under engineering494 and verified its byte SHA256 +c8dace4508bd7ebee524245a4dba94b77af5a6b7d737e54d084aa807d00cc25c. +The frozen complete public stdout SHA256 is +1acda7973aee96bcd1bdc3fb6f86c5930a7ca16ff05b270f35ec4474a29340ae. +Target06/d92 actual canonical9-field public point retirement succeeded; +raw-only survivor source/payload/calibration/domain/camera unchanged; +same-native saved ZIP reopened;9 matched XY/XZ/YZ raw/point/combined captures +personally reviewed by parent. No process-RSS drop or biological claim. +Original geometry rejection remains. Typed close returned ACK/process_exited/ +endpoint_terminated/succeededtrue; independent native/MCP/port absence proven, +original parent client63937 terminal1 at10:08:33 (retained aggregate error, +not a failed close). No replay or runtime operation by Singer. + +Parent requests ONE NEW normal package07 source-only future projection, +coordinated directly with Planck. Immutable borrowed06 does NOT contain a78. +Parent owns review/funding/normal MCP health and fresh installed selected-row +receiving; no new source install/build/runtime here, no science contact or hot +skill/package sync. Native row-survivor preservation remains pending actual +receiving, not a permanent guide caveat or an old06 success claim. + +Byte-exact final selection archive +viewer-selective-retirement-519-selection-qualified-20261003.tar.gz contains33 +members/1197812bytes, SHA256 +4c1dfc06901cbead1578539fd330fab36a3121a6e638285aa35691419ff2ca48. +ALL9 current changed production files/two actual fixtures, existing original +bootstrap/AST/R0 callers, original before/after family logs, ALL01..04 selection +logs including failures/RSS-limit misses, changed-builder/R0 logs, original EMPTY +NRA output and OOM stderr, and received parent ACTUAL-RECEIVING.rst are included. +Each of33 tar members was byte-compared against its actual original, including +the parent's closed receipt. Historical archives and untracked raw originals +remain unchanged; no evidence-only warning/whitespace rewriting or deletion. + +Survivor row-selection source checkpoint (2026-10-03) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Parent assigns Singer the remaining Points/Shapes selected_data defect on522; +Planck explicitly confirms no competing selection hunk. His point-calibration +and sole parent TCP95 receiving packet remain disjoint. Root394 checked at +4c0d62e45c8afb85e34b69d7279cd00710f519b5; no producer/persistence edits. +Guide526 has separately merged532b13f5; no guide, target06, scientific borrower, +package, runtime, foreign gitlink or original journal changes in this worker. + +Original NapariResultSelectionController now owns one transient +preserve_selection(route_key) context. It resolves both native layers through +its original route-state store. NapariStreamLayerItem derives source-member +identity from its complete producer, component address, path and unchanged +payload member/coordinate positions. Existing Points and Shapes builders project +that opaque identity into native features in their existing assembly loops. +This is not a new object-ID authority, persistent selection roster, transport +codec, coordinate heuristic or restored old table index. Native feature order +may change without selecting a different source member. Edited/replaced source +payloads are not a promise to retain arbitrary native manual edits. + +The controller maps only selected source identities onto new native rows, +preserves original declared linked-subject binding, disconnects replaced native +callbacks/removes their derived weak lookups, and suppresses navigation during +restoration through its original synchronization/generation owners. Empty +selection remains empty; missing/duplicate source identities refuse before +remount. NapariSelectablePresentationRetention composes that independent +capability with the existing common-presentation ancestor through cooperative +super(). Points/Shapes leaves declare it only; generic reconciliation and +registry consumers are unchanged. No second algorithm or row cache is added. + +Latest NRA/current authoritative audit ZIP and applicable IDEN-1/6, BOUND-2, +IMPL-12/13, MEMB-1/2 and TIME-9 catalog entries informed this original-owner +choice. Existing audit caller selection-family01 parses704 production/669tests/ +401 actual dependencies (including native Points/Shapes/base/events), zero +parse omissions, selected69/51/42 modules; before-source1df1c98c. Static source +facts and semantically read consumers are not a global NRA R1 clean claim. + +Actual real Qt/ViewerModel selection-controls04:14PASS/21explicitdeselections, +10.03s/maxRSS507916KiB/cgroup315.5MiB/Swap0, CPU1/512MiB/60s. Tests exercise +both native families after registered retirement prunes an interior shared +domain; source-item order reversal changes native row indices while exact +selected source identities survive, empty/multirow selection, original linked +groups, active layer/calibration/domain/style/camera, canceled queued navigation +even without unmount, stale removed callback detachment, and an independent +declaration's executed cooperative enter/exit hooks without consumer edits. +Original source bootstrap, dependency env and paired Qt owner reused. + +Original selection-controls01 retains33PASS/two Shapes visibility failures: +an earlier accepted row-selection timer ran before retirement dispatch and +made the layer visible before capture. The fixture now settles that original +action before selecting the presentation to retain; a separate original queued +callback control tests cancellation without unmount. Original01/02/03 logs +remain byte-exact. All completed within original cgroup512MiB/Swap0, but01 +maxRSS583980KiB and02/03 537564/539012KiB exceeded requested RSS; they are NOT +qualified bounded receipts. Small04 removes needless eager MCP construction +imports from model-only tests: those original imports now live only inside +the unchanged actual-SDK test, which still constructs/dispatches when selected. +No test assertions or limits were relaxed; discarded fixture-GC experiment is +not retained in source. + +Final changed-builder controls/after-AST/original R0 remain pending at this +working checkpoint. Parent reports standalone95-live30 actual target06 public +typed point retirement,9 opened matched XY/XZ/YZ raw/point/combined captures, +unchanged raw source/payload/calibration/domain/camera and same-native ZIP +reopen. This is received detached public proof, not independently replayed here, +not biological acceptance, and NOT selected_data-survivor proof (raw-only +survivor). New source row fix still needs parent review and separately qualified +installed/live receiving; current06 stays immutable. + +Final target06 semantic checkpoint received +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Planck completed and published original engineering494/MCP-SEMANTICS28.rst, +READY-MCP28.json and PACKAGE28-QUALIFICATION.json; Singer read those complete +receipts plus actual construction27/health27/installed26 logs. Source d92fd0b8c +and this branch's4414159b6 have byte-identical retirement production/fixture +files. No additional source change, build, install or runtime in this worker. + +NEW ordinary immutable target06/wheels05 (not04/05 overlay) SHA256 +46070edad4363d406b47ea771d07e795bff5eb268c4fa58d564881b5ec48ca04 qualifies +815 tracked source inputs/90 assets/13 skill members/two native extensions, +original RECORD/scripts/dependency APIs and347 original-source-compiled caches: +1331 actual files accounted, none ignored. Original shared dependency/source +absence and earlier qualification failures remain preserved. This is received +owner whole-package proof, not an independently repeated audit. + +Actual104-tool build_server constructs the original nine-field identity schema; +fresh non-resident stdio health27 returns status=ok/errors=[]/resources-ready at +exact target06 source. Owner child exited, no native/viewer/catalog/science run. +Installed26 is30PASS/11.67s/377876480B peak, CPU1/1GiB/Swap0/60s: actual SDK +admission matches original from_payload semantics for bool/int/string coercion, +unknown identity keys, optional-empty normalization and required/null/invalid +integer rejection; top-level unknown tool keys remain forbidden. Original +SDK-to-serialized-Qt-queue action and surviving raw object/backing array controls +also pass. This is not external detached TCP or process-RSS release evidence. +SCHEMA-FAMILY27-FINAL.json expands the received original AST context to704 +production+162 actual introspect/PolyStore/Pydantic/FastMCP modules, zero parse +omissions; dynamic relationships are read semantically, not global equivalence. + +Parent's latest release permits healthy corrected06 scientific startup and +ordinary baseline analysis after its original resource guard and source/packet +freeze. Detached public retirement/shared-domain survivor/reopen qualification +remains a separate FEATURE-MERGE gap, not a universal fresh-SCI gate. Singer +does not release or contact scientists; frozen04/05 and original failures stay +intact. Untargeted geometry row-selection limitation remains recorded below. + +One final ORIGINAL pinned R0 on frozen af293bb9ed32ab4fc311b141d1f2e1b429029a73 +production (including final4414 annotations) passes: ALL9 changed production +files, positive delta mapping empty, terminal0/28.76s/maxRSS87008KiB/ +cgroup75.7M/Swap0. Unchanged run_pinned_r0_419.py and original detector Git +3b03785f45df2ef5dc62ba6aed99294192ecbb01, existing3.14 -I -B, base mainf7de, +CPU1/512MiB/60s; no copied detector, omitted changed path, waived delta or widened +bound. Only newly changed source was guarded; installed/Qt/package suites were +not repeated. Original critical disk/swap advisory remains (home4.5GiB, +root7.5GiB,swap14.6GiB,availableRAM19.2GiB); no native admission inferred. + +Final additional byte-exact archive +viewer-selective-retirement-519-semantic-qualified-20261003.tar.gz contains13 +members: two final production/two fixture files, two original R0 logs and seven +original owner target06 receipts/AST/construction/health/control files.186986bytes, +SHA2563277a792721e1819e721fbaae8a45843a7794a6f377a03418b370bc6a6bfeca8. +Tar comparison passed all original members, including the owning issue-batch +paths. Earlier archives/raw failures/SCI UNKNOWN dispositions remain intact. + +Decoder-parity correction received: qualification pending +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Parent requires original admission semantics, not merely a constructible SDK +schema. Plain91d/dataclass and target05 permit some required-empty identities +and differ from original numeric/bool/optional-empty normalization. Their actual +construction/health/queue controls remain valid at that strength, but do not +establish complete decoder parity. They are not merge-ready receipts. + +Planck published d92fd0b8c786d52c4290bf764b3cc3a84da9b600 and opened issue528. +Singer receives only its two-file change as normal cherry-pick4414159b62ae92a901e37861cbe85856ae48f750. +The canonical DTO now declares Annotated[StreamProducerIdentity, +BeforeValidator(StreamProducerIdentity.from_payload)] inside the route mapping. +This calls the existing behavior owner before Pydantic constructs the dataclass; +there is no new validator body, field list, TypedDict, alias/codec, PolyStore edit +or generic consumer branch. Original get_type_hints(include_extras=True) already +carries that declared annotation. The new fixture compares original decoder +versus actual SDK admission for numeric/bool/string positions and identity +fields, optional empty values, ignored unknown identity keys and original +required-empty/null/invalid-integer failures. + +All two production/two fixture files are byte-identical to Planck's d92 source; +no combined ancestry is imported. Owner's new ordinary target06 whole-package, +actual MCP construction/health/decoder-parity controls are still qualifying; +actual detached public receiving remains required before merge or fresh SCI. +No replay, hot target04/05 edit or further install/runtime in this worker. +The one R0 below remains pinned to5354, before this subsequent annotation change, +not claimed as a completed current4414 guard. Existing failure and qualification +archives remain byte-exact; no unchanged test or package work is repeated here. + Actual MCP construction correction received (2026-10-03) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -34,7 +528,7 @@ typed-or-original StreamProducerPayloadMapping acceptance is now annotated honestly. The generic generated tool binding/codec, retirement whole-set and invocation guards, receipt MRO and deadlines are unchanged. No central leaf switch, field/schema roster, anonymous/named JSON replacement, compatibility -reader, Any escape or larger recursion/timeout limit. BOUND-2/8 and MEMB-5 apply: +reader, Any escape or larger recursion/timeout limit. BOUND-2 and MEMB-5 apply: carry the original identity, derive schema from its declaration, delete the overly broad producer record annotation. @@ -386,7 +880,7 @@ Source evidence and proof limits Read latest NRA skill and authoritative nominal-refactor-advisor/skills/ refactor-audit.skill SKILL/catalog README plus full identity, membership, implementation, boundaries and over-time pattern files. Applicable patterns: -IDEN-1/6 (route versus generation/visibility), BOUND-2/8 (typed source/domain/ +IDEN-1/6 (route versus generation/visibility), BOUND-2 (typed source/domain/ producer facts across request/reply), IMPL-12/13 (copied purge/settlement loops), MEMB-1/2 (parallel tool/route rosters), TIME-9 (alternate codec/compatibility). Existing behavior-owning classes and shared registered ABCs remain load bearing; diff --git a/docs/validation/viewer-selective-retirement-519-selection-qualified-20261003.tar.gz b/docs/validation/viewer-selective-retirement-519-selection-qualified-20261003.tar.gz new file mode 100644 index 0000000000..a26cf13114 Binary files /dev/null and b/docs/validation/viewer-selective-retirement-519-selection-qualified-20261003.tar.gz differ diff --git a/docs/validation/viewer-selective-retirement-519-semantic-qualified-20261003.tar.gz b/docs/validation/viewer-selective-retirement-519-semantic-qualified-20261003.tar.gz new file mode 100644 index 0000000000..dc8d8efbd3 Binary files /dev/null and b/docs/validation/viewer-selective-retirement-519-semantic-qualified-20261003.tar.gz differ diff --git a/docs/validation/viewer-selective-retirement-522-fractional-spatial-20261003.tar.gz b/docs/validation/viewer-selective-retirement-522-fractional-spatial-20261003.tar.gz new file mode 100644 index 0000000000..5a10b04b06 Binary files /dev/null and b/docs/validation/viewer-selective-retirement-522-fractional-spatial-20261003.tar.gz differ diff --git a/docs/validation/viewer-selective-retirement-522-shared-selection-20261003.tar.gz b/docs/validation/viewer-selective-retirement-522-shared-selection-20261003.tar.gz new file mode 100644 index 0000000000..e07102eae7 Binary files /dev/null and b/docs/validation/viewer-selective-retirement-522-shared-selection-20261003.tar.gz differ diff --git a/openhcs/runtime/napari_streaming_handlers.py b/openhcs/runtime/napari_streaming_handlers.py index 876f19cd06..0be297e649 100644 --- a/openhcs/runtime/napari_streaming_handlers.py +++ b/openhcs/runtime/napari_streaming_handlers.py @@ -15,6 +15,7 @@ import numpy as np from napari.layers.shapes._shapes_constants import ShapeType +from napari.utils.transforms import Affine from polystore.streaming.identity import StreamProducerIdentity from polystore.streaming_constants import StreamingDataType from zmqruntime.viewer_protocol import ViewerComponentMode, ViewerWireField @@ -33,6 +34,7 @@ ComponentValues, ViewerComponentAxisSemantics, ViewerComponentLayout, + ViewerComponentCoordinateAuthority, ViewerComponentValueDomainPayload, ViewerRouteComponentValueTracker, ViewerLayerAxisProjection, @@ -1369,7 +1371,7 @@ def preserve_native_axes( self, dims, replacement: "NapariAxisPresentation", items: Sequence[NapariStreamLayerItem], ): - """Carry actual native world positions/order through semantic slot insertion. + """Carry source positions/order through semantic slot or domain changes. This is a transient presentation snapshot, not a second component domain. Values come from native Dims and names from the original presentations. @@ -1380,6 +1382,8 @@ def preserve_native_axes( points = {name: dims.point[axis] for axis, name in names.items()} ranges = {name: dims.range[axis] for axis, name in names.items()} order = tuple(names[axis] for axis in dims.order if axis in names) + original_transform = Affine(**self.spatial_layer_kwargs(items, self.payload_axis_labels)) + local_points = original_transform.inverse(tuple(points[name] for name in self.axis_labels)) yield target_dimensions = replacement.viewer_dimension_indices(dims.ndim) target_axes = dict(zip(replacement.axis_labels, target_dimensions, strict=True)) @@ -1388,7 +1392,39 @@ def preserve_native_axes( for name, value in points.items(): point[target_axes[name]] = value native_ranges[target_axes[name]] = ranges[name] - transform = replacement.spatial_layer_kwargs(items, replacement.payload_axis_labels) + transform = Affine(**replacement.spatial_layer_kwargs(items, replacement.payload_axis_labels)) + original_values = self.projection.component_values | self.projection.scalar_component_values + replacement_values = ( + replacement.projection.component_values | replacement.projection.scalar_component_values + ) + for name, values in original_values.items(): + if name not in points or name not in replacement_values: + continue + original_axis = self.axis_labels.index(name) + local_point = local_points[original_axis] + source_index = round(local_point) + # An out-of-route frame or a retired source has no surviving identity + # to retain. Native mounted-layer bounds still own its clipping. + if not 0 <= source_index < len(values): + continue + source_value = values[source_index] + if source_value not in replacement_values[name]: + continue + index = ViewerComponentCoordinateAuthority.value_index( + value=source_value, component_values=replacement_values, + component=name, context="rematerialized native source frame", + ) + axis = replacement.axis_labels.index(name) + scale = transform.scale[axis] + translate = transform.translate[axis] + # Preserve within-plane world position too; source geometry is not + # rounded when a component changes its shared-domain ordinal. + point[target_axes[name]] = transform.set_slice((axis,))( + (index + local_point - source_index,) + )[0] + native_ranges[target_axes[name]] = ( + translate, translate + (len(replacement_values[name]) - 1) * scale, scale, + ) for name in replacement.display_axis_components: if name in points: continue @@ -1400,9 +1436,9 @@ def preserve_native_axes( axis = replacement.axis_labels.index(name) # This newly inserted singleton's source-local index is zero. # The original transform already contains its shared-domain offset. - point[target_axes[name]] = transform["translate"][axis] + point[target_axes[name]] = transform.translate[axis] native_ranges[target_axes[name]] = ( - point[target_axes[name]], point[target_axes[name]], transform["scale"][axis], + point[target_axes[name]], point[target_axes[name]], transform.scale[axis], ) # Dims clips points to its ranges. Remap the detached snapshot's bounds # first; the live viewer's bounds remain owned by its mounted layers. diff --git a/openhcs/runtime/napari_viewer_server.py b/openhcs/runtime/napari_viewer_server.py index b34339eeb2..7d354eb010 100644 --- a/openhcs/runtime/napari_viewer_server.py +++ b/openhcs/runtime/napari_viewer_server.py @@ -16,7 +16,7 @@ import weakref from abc import ABC, abstractmethod from collections.abc import Callable, Hashable, Iterable, Mapping -from contextlib import contextmanager +from contextlib import ExitStack, contextmanager from dataclasses import dataclass, field, replace from enum import Enum from functools import partial @@ -148,7 +148,7 @@ ViewerPolylineControlOptions, ViewerRegionControlOptions, ViewerRoutedImageControlOptions, - ViewerFractionalZPointCoordinateAuthority, + ViewerPointCoordinateAuthority, ViewerIntensityWindowControlOptions, ViewerNativeDimensions, ViewerResultElementCoordinateAuthority, @@ -1830,6 +1830,25 @@ def requires_rematerialization(self) -> bool: != self.original_presentation.aligned_component_shape() ) + @contextmanager + def preserve_native_presentation(self): + """Only changed geometry/coordinates need remount selection admission. + + Untouched routes may legitimately hold hidden, off-slice native rows. + Their unchanged membership is not a new rematerialized selection. + """ + if ( + self.requires_rematerialization + or self.presentation.projection != self.original_presentation.projection + ): + handler = NapariLayerDisplayHandler.for_data_type( + self.items[0].address.stream_layer_data_type, + ) + with handler.preserve_native_presentation(self): + yield + else: + yield + def restore_native_frame(self) -> None: """Apply the one batch snapshot before native selection eligibility.""" dims = self.pipeline.server.viewer.dims @@ -1872,6 +1891,9 @@ class NapariLayerDisplayHandler( """Executable display handler for one Napari stream data type.""" title_suffix: ClassVar[str] = "" + result_coordinate_authority: ClassVar[type[ViewerResultElementCoordinateAuthority]] = ( + ViewerResultElementCoordinateAuthority + ) @contextmanager def preserve_native_presentation(self, request: NapariLayerDisplayRequest): @@ -1885,9 +1907,21 @@ def preserve_native_presentation(self, request: NapariLayerDisplayRequest): layer.visible, layer.opacity, layer.blending = visible, opacity, blending def rematerialize(self, request: NapariRematerializationRequest) -> None: - with self.preserve_native_presentation(request): - self.handle(request) - request.restore_native_frame() + self.handle(request) + request.restore_native_frame() + + @classmethod + @contextmanager + def preserve_native_presentations(cls, requests: Sequence[NapariRematerializationRequest]): + """Capture every survivor before a peer can change its native slice. + + Retention remains on the declared handler's cooperative capabilities; + all restores run after the shared frame and every layer are reconciled. + """ + with ExitStack() as retention: + for request in requests: + retention.enter_context(request.preserve_native_presentation()) + yield def geometric_component_values( self, @@ -2206,6 +2240,9 @@ class NapariPointsLayerDisplayHandler( streaming_data_type: ClassVar[StreamingDataType] = StreamingDataType.POINTS title_suffix: ClassVar[str] = "points" + result_coordinate_authority: ClassVar[type[ViewerResultElementCoordinateAuthority]] = ( + ViewerPointCoordinateAuthority + ) def geometric_component_values( self, @@ -2430,37 +2467,37 @@ def reconcile_mounted_axis_projections( if not apply or not requests: return - if any(request.requires_rematerialization for request in requests): - # Capture once, before any peer changes native rank. The original - # label/selection resolver owns which source frame is current. - current_route = self.dimension_label_overlay.route_resolver.resolve().route_key - basis = next( - (request for request in requests if request.presentation.route_key == current_route), - requests[0], - ) - native_frame = self.server.viewer.dims.copy() - if basis.slots_changed: - with basis.original_presentation.preserve_native_axes( - native_frame, basis.presentation, basis.items, - ): - native_frame.ndim = max(native_frame.ndim, len(basis.presentation.axis_labels)) - offset = native_frame.ndim - len(basis.presentation.axis_labels) - native_frame.axis_labels = ( - *native_frame.axis_labels[:offset], *basis.presentation.axis_labels, - ) - requests = [replace(request, native_frame=native_frame) for request in requests] - - for request in requests: - if request.requires_rematerialization: - NapariLayerDisplayHandler.for_data_type( - request.items[0].address.stream_layer_data_type - ).rematerialize(request) - else: - layer = self.server.layer_route_state.layer(request.presentation.route_key) - layer.translate = request.presentation.spatial_layer_kwargs( - request.items, request.presentation.payload_axis_labels, - )["translate"] - request.publish() + # Capture once, before any peer changes native rank or component domain. + # The original label/selection resolver owns the current source frame. + current_route = self.dimension_label_overlay.route_resolver.resolve().route_key + basis = next( + (request for request in requests if request.presentation.route_key == current_route), + requests[0], + ) + native_frame = self.server.viewer.dims.copy() + with basis.original_presentation.preserve_native_axes( + native_frame, basis.presentation, basis.items, + ): + native_frame.ndim = max(native_frame.ndim, len(basis.presentation.axis_labels)) + offset = native_frame.ndim - len(basis.presentation.axis_labels) + native_frame.axis_labels = ( + *native_frame.axis_labels[:offset], *basis.presentation.axis_labels, + ) + requests = [replace(request, native_frame=native_frame) for request in requests] + + with NapariLayerDisplayHandler.preserve_native_presentations(requests): + for request in requests: + if request.requires_rematerialization: + NapariLayerDisplayHandler.for_data_type( + request.items[0].address.stream_layer_data_type + ).rematerialize(request) + else: + layer = self.server.layer_route_state.layer(request.presentation.route_key) + layer.translate = request.presentation.spatial_layer_kwargs( + request.items, request.presentation.payload_axis_labels, + )["translate"] + request.publish() + request.restore_native_frame() def schedule_layer_update( self, @@ -5804,14 +5841,16 @@ def result_element_axis_indices( if displayed_axis_indices is None else displayed_axis_indices ) - coordinate_authority = ( - ViewerFractionalZPointCoordinateAuthority - if isinstance(layer, napari.layers.Points) - else ViewerResultElementCoordinateAuthority - ) + items = server.component_groups.existing_items_for(route_key) + if not items: + raise ValueError("Result selection requires its original routed source items.") + coordinate_authority = NapariLayerDisplayHandler.for_data_type( + items[0].address.stream_layer_data_type, + ).result_coordinate_authority return coordinate_authority.axis_indices( coordinates=cast(Sequence[object], coordinates), axis_labels=dimension_state.axis_labels, + spatial_axis_labels=presentation.spatial_axis_labels, displayed_axis_indices=tuple( local_axis for local_axis, viewer_axis in enumerate(dimensions) diff --git a/openhcs/runtime/viewer_controls.py b/openhcs/runtime/viewer_controls.py index 4d802d354e..43c5693a2f 100644 --- a/openhcs/runtime/viewer_controls.py +++ b/openhcs/runtime/viewer_controls.py @@ -25,8 +25,6 @@ from zmqruntime.viewer_protocol import ViewerWireField -from openhcs.constants import AllComponents - ViewerScalar: TypeAlias = str | int | float | bool | None VerticesYX: TypeAlias = tuple[tuple[float, float], ...] ViewerPayloadAxisIndices: TypeAlias = tuple[int, ...] | dict[str, int] @@ -77,6 +75,7 @@ def axis_indices( coordinates: Iterable[object], axis_labels: Sequence[str], displayed_axis_indices: Sequence[int], + spatial_axis_labels: Sequence[str], ) -> dict[str, int]: """Return exact route-local indices for every non-displayed axis.""" @@ -95,6 +94,9 @@ def axis_indices( raise ValueError("Viewer axis_labels must contain non-empty strings.") if len(set(labels)) != len(labels): raise ValueError("Viewer axis_labels must be unique.") + spatial = tuple(spatial_axis_labels) + if len(set(spatial)) != len(spatial) or any(axis not in labels for axis in spatial): + raise ValueError("Viewer spatial axes must be unique declared route axes.") rows = cls._coordinate_rows(coordinates) coordinate_width = len(rows[0]) @@ -117,6 +119,7 @@ def axis_indices( rows, axis_position=axis_position, axis_label=labels[axis_position], + spatial_axis_labels=spatial, ) for axis_position in range(coordinate_width) if axis_position not in displayed @@ -163,11 +166,13 @@ def _slice_index( *, axis_position: int, axis_label: str, + spatial_axis_labels: Sequence[str], ) -> int: coordinates = tuple( cls._slice_coordinate( row[axis_position], axis_label=axis_label, + spatial_axis_labels=spatial_axis_labels, ) for row in rows ) @@ -178,37 +183,46 @@ def _slice_index( ) return coordinates[0] + @classmethod + def _slice_coordinate( + cls, value: object, *, axis_label: str, spatial_axis_labels: Sequence[str], + ) -> int: + numeric_value = cls._coordinate_value(value, axis_label=axis_label) + if not numeric_value.is_integer(): + raise ValueError( + f"Viewer result element coordinate for axis {axis_label!r} " + f"must identify one integral slice, got {value!r}." + ) + return int(numeric_value) + @staticmethod - def _slice_coordinate(value: object, *, axis_label: str) -> int: + def _coordinate_value(value: object, *, axis_label: str) -> float: if isinstance(value, bool) or not isinstance(value, Real): raise TypeError( f"Viewer result element coordinate for axis {axis_label!r} " "must be numeric." ) numeric_value = float(value) - if not isfinite(numeric_value) or not numeric_value.is_integer(): + if not isfinite(numeric_value): raise ValueError( f"Viewer result element coordinate for axis {axis_label!r} " - f"must identify one integral slice, got {value!r}." + f"must be finite, got {value!r}." ) - return int(numeric_value) + return numeric_value -class ViewerFractionalZPointCoordinateAuthority(ViewerResultElementCoordinateAuthority): - """Navigate to the nearest Z slice without rounding stored point geometry.""" +class ViewerPointCoordinateAuthority(ViewerResultElementCoordinateAuthority): + """Navigate to the nearest spatial slice without rounding stored geometry.""" - @staticmethod - def _slice_coordinate(value: object, *, axis_label: str) -> int: - if axis_label != AllComponents.Z_INDEX.value: - return ViewerResultElementCoordinateAuthority._slice_coordinate( - value, axis_label=axis_label + @classmethod + def _slice_coordinate( + cls, value: object, *, axis_label: str, spatial_axis_labels: Sequence[str], + ) -> int: + if axis_label not in spatial_axis_labels: + return super()._slice_coordinate( + value, axis_label=axis_label, spatial_axis_labels=spatial_axis_labels, ) - if isinstance(value, bool) or not isinstance(value, Real): - raise TypeError("Viewer point Z coordinate must be numeric.") - numeric_value = float(value) - if not isfinite(numeric_value): - raise ValueError("Viewer point Z coordinate must be finite.") - return floor(numeric_value + 0.5) + return floor(cls._coordinate_value(value, axis_label=axis_label) + 0.5) @dataclass(frozen=True, slots=True) diff --git a/tests/unit/test_materialization_core.py b/tests/unit/test_materialization_core.py index 462fb6c592..85d93e9b97 100644 --- a/tests/unit/test_materialization_core.py +++ b/tests/unit/test_materialization_core.py @@ -22,6 +22,7 @@ FixedStreamProducerIdentityKind, StreamProducerIdentity, ) +from polystore.streaming_constants import StreamingDataType from polystore.streaming.viewer_transport import ( BatchViewerStreamSourceMetadata, ViewerDisplayConfigABC, @@ -56,7 +57,14 @@ ) from openhcs.core.roi_point_metadata import ROIFractionalZ from openhcs.core.roi_source_metadata import ROIArchiveSourceMetadata, ROIPlaneMetadata -from openhcs.runtime.viewer_component_system import ViewerLayerAxisProjection +from openhcs.runtime.viewer_component_system import ( + ViewerComponentValueDomainPayload, + ViewerLayerAxisProjection, +) +from openhcs.runtime.napari_streaming_handlers import ( + NapariStreamLayerAddress, + NapariStreamLayerItem, +) from openhcs.core.runtime_object_label_building import ( SourceImageObjectLabelBuildRequest, ) @@ -833,32 +841,30 @@ def test_point_roi_materialization_native_reopen_preserves_fractional_z( routed_component_values={"z_index": z_values}, axis_offsets=(0,), ) - from openhcs.runtime.napari_streaming_handlers import ( - NapariStreamLayerAddress, NapariStreamLayerItem, - ) - from openhcs.runtime.viewer_component_system import ViewerComponentValueDomainPayload - from polystore.streaming_constants import StreamingDataType - - points, properties = viewer_server._build_nd_points( - [ - NapariStreamLayerItem( - data=NapariROIConverter.rois_to_shapes(rois), - producer=StreamProducerIdentity.fixed_output( - FixedStreamProducerIdentityKind.MANUAL, "point_reopen", - ), - address=NapariStreamLayerAddress( - source_domain[0], archive, StreamingDataType.POINTS, - ), - image_metadata=metadata, - plane_component_domain=ViewerComponentValueDomainPayload(()), - ) - ], - projection, + item = NapariStreamLayerItem( + data=NapariROIConverter.rois_to_shapes(rois), + producer=StreamProducerIdentity.pipeline_output( + output_kind="artifact", output_key="centres", + projection_key="centres", step_name="Synthetic Centres", + pipeline_position=0, step_scope_id="synthetic-centres", + ), + address=NapariStreamLayerAddress( + components=source_domain[0], path=archive, + stream_layer_data_type=StreamingDataType.POINTS, + ), + image_metadata=metadata, + plane_component_domain=ViewerComponentValueDomainPayload.from_wire_mapping( + {"z_index": z_values}, context="materialized point source domain", + ), ) + points, properties = viewer_server._build_nd_points([item], projection) assert points.tolist() == [[2.375, 1.25, 3.5]] assert properties["label"] == [7] assert properties["object_label"] == [7] assert properties["response"] == [4.75] + assert properties[NapariStreamLayerItem.ELEMENT_IDENTITY_FEATURE] == [ + item.element_identity(0) + ] from napari.layers import Points native_layer = Points(points, properties=properties) diff --git a/tests/unit/test_napari_orthogonal_navigation.py b/tests/unit/test_napari_orthogonal_navigation.py index a055d3e330..57365cceea 100644 --- a/tests/unit/test_napari_orthogonal_navigation.py +++ b/tests/unit/test_napari_orthogonal_navigation.py @@ -212,6 +212,7 @@ def test_hidden_indices_use_actual_displayed_dimensions(displayed, expected): coordinates=(1, 2, 4, 7), axis_labels=("channel", "z_index", "y", "x"), displayed_axis_indices=displayed, + spatial_axis_labels=("z_index", "y", "x"), ) == expected ) @@ -222,12 +223,14 @@ def test_displayed_z_may_be_fractional_and_span_slices_but_hidden_y_must_not(): coordinates=((1, 2.25, 4, 7), (1, 3.5, 4, 8)), axis_labels=("channel", "z_index", "y", "x"), displayed_axis_indices=(1, 3), + spatial_axis_labels=("z_index", "y", "x"), ) == {"channel": 1, "y": 4} with pytest.raises(ValueError, match="spans multiple 'y'"): ViewerResultElementCoordinateAuthority.axis_indices( coordinates=((1, 2.25, 4, 7), (1, 3.5, 5, 8)), axis_labels=("channel", "z_index", "y", "x"), displayed_axis_indices=(1, 3), + spatial_axis_labels=("z_index", "y", "x"), ) diff --git a/tests/unit/test_napari_reset_transitions.py b/tests/unit/test_napari_reset_transitions.py index 522102b38e..40881d50a0 100644 --- a/tests/unit/test_napari_reset_transitions.py +++ b/tests/unit/test_napari_reset_transitions.py @@ -41,19 +41,24 @@ NapariShapesLayerDisplayHandler, NapariPointsLayerDisplayHandler, NapariSelectablePresentationRetention, + NapariNavigationControlMessageAction, NapariResultSelectionController, + NapariResultSelectionGroupBinding, NapariStreamLayerContext, NapariViewerServer, ) from openhcs.runtime.napari_streaming_handlers import NapariStreamLayerItem from openhcs.agent.dto.execution import ExecutionConnectionSpec +from openhcs.core.artifacts import ObjectArtifactSubjectBinding from openhcs.agent.dto.viewer import ViewerWindowLayerRetirementRequest from openhcs.agent.services.viewer_window_service import ( ViewerWindowService, ZMQViewerWindowGateway, ) -from openhcs.mcp.context import OpenHCSAgentContext -from openhcs.mcp.server import build_server -from openhcs.runtime.viewer_controls import ViewerLayerRetirementControlOptions +from openhcs.runtime.viewer_controls import ( + ViewerLayerRetirementControlOptions, ViewerNavigationControlOptions, + ViewerPointCoordinateAuthority, +) +from openhcs.core.roi_point_metadata import ROIFractionalZ from openhcs.runtime.viewer_protocol import ( OpenHCSViewerControlMessageType, ViewerSettlePhase, ) @@ -103,15 +108,18 @@ def enqueue( data_type=StreamingDataType.IMAGE, spacing=0.65, domain=None, + z_domain=None, + z_index=1, display_config=None, channel=1, channels=None, + voxel_spacing=None, ): config = display_config or NapariDisplayConfig( well_mode=NapariDimensionMode.STACK, site_mode=NapariDimensionMode.LAYER, channel_mode=NapariDimensionMode.LAYER, - z_index_mode=NapariDimensionMode.LAYER, + z_index_mode=(NapariDimensionMode.STACK if z_domain else NapariDimensionMode.LAYER), timepoint_mode=NapariDimensionMode.LAYER, variable_size_handling=NapariVariableSizeHandling.PAD_TO_MAX, ) @@ -124,7 +132,7 @@ def enqueue( ), ViewerComponentValueDomainPayload.from_ordered_wire_mapping( {"well": domain or [well], "channel": channels or [channel], - "site": [1], "z_index": [1], "timepoint": [1]}, + "site": [1], "z_index": z_domain or [z_index], "timepoint": [1]}, context="synthetic transition" ), ) @@ -139,12 +147,12 @@ def enqueue( pipeline_position=0, ), address=NapariStreamLayerAddress( - {"well": well, "site": 1, "channel": channel, "z_index": 1, "timepoint": 1}, + {"well": well, "site": 1, "channel": channel, "z_index": z_index, "timepoint": 1}, f"{well}.tif", data_type, ), image_metadata=ImagePayloadMetadata( - source_voxel_spacing=SourceVoxelSpacing((spacing, spacing)) + source_voxel_spacing=SourceVoxelSpacing(voxel_spacing or (spacing, spacing)) ), plane_component_domain=ViewerComponentValueDomainPayload(()), display_config=config, @@ -562,6 +570,8 @@ def test_selected_retirement_uses_registered_queue_and_releases_payloads(receive def test_real_fastmcp_retirement_decodes_identity_and_reaches_native_queue(receiver): from mcp.server.fastmcp.exceptions import ToolError + from openhcs.mcp.context import OpenHCSAgentContext + from openhcs.mcp.server import build_server raw = np.ones((2, 2), dtype=np.uint16) keep, update = enqueue(receiver, raw, producer='raw') @@ -705,6 +715,9 @@ class NewDeclaredImageHandler(IndependentPresentationCapability, NapariImageLaye originals[route] = receiver.component_groups.existing_items_for(route) receiver.viewer.camera.center = (0, 1, 1) receiver.viewer.camera.zoom = 31 + # Earlier domain reconciliation also exercises the declared capability. + # Measure this retirement batch independently, not its fixture construction. + calls.clear() result = ViewerWindowService(QueuedRetirementGateway(receiver)).presentation(retirement_request(receiver, middle)) assert result.applied and not result.errors assert len(calls) == 4 @@ -722,3 +735,441 @@ class NewDeclaredImageHandler(IndependentPresentationCapability, NapariImageLaye assert tuple(receiver.viewer.camera.center) == (0, 1, 1) assert receiver.component_values.shared_values_for(["well"]) == {"well": ["A01", "A03"]} assert issubclass(NewDeclaredImageHandler, NapariImagePresentationRetention) + + +@pytest.mark.parametrize("data_type", [StreamingDataType.POINTS, StreamingDataType.SHAPES]) +@pytest.mark.parametrize("selected", [False, True]) +@pytest.mark.parametrize("reorder", [False, True]) +def test_retirement_retains_survivor_source_members_without_late_navigation( + receiver, data_type, selected, reorder, +): + middle, update = enqueue(receiver, np.ones((2, 2)), well="A02", producer="middle") + advance_in_qt(receiver, middle, update) + for well, subject_id in [("A01", 7), ("A03", 8)]: + members = [ + {"type": "points" if data_type is StreamingDataType.POINTS else "path", + "coordinates": [[i, i], [i + 1, i + 1]], + "metadata": {"label": subject_id, + ObjectArtifactSubjectBinding.SUBJECT_FEATURE: "test-object", + ObjectArtifactSubjectBinding.SUBJECT_ID_FEATURE: subject_id}} + for i in range(2) + ] + # Points metadata is not a declared cross-layer subject binding; its + # source members are retained independently of the Shapes group owner. + if data_type is StreamingDataType.POINTS: + for member in members: + member["metadata"] = {"label": subject_id} + route, update = enqueue(receiver, members, well=well, producer="survivor", + data_type=data_type, domain=["A01", "A03"]) + advance_in_qt(receiver, route, update) + old = receiver.layer_route_state.layer(route) + receiver.viewer.layers.selection.active = old + if selected: + # Domain expansion now retains the actual source frame. This fixture + # explicitly selects A01 members, so choose A01 before native assignment. + prepared = NapariNavigationControlMessageAction().prepare( + receiver, ViewerNavigationControlOptions(route_key=route, data_index=0), + ) + receiver.viewer.dims.current_step = prepared.viewer_step + old.selected_data = {0, 1} + # The public retirement boundary follows earlier accepted selection work. + # Its original zero-delay navigation must settle before choosing visibility. + QApplication.instance().processEvents() + feature = NapariStreamLayerItem.ELEMENT_IDENTITY_FEATURE + retained = set(old.features.iloc[list(old.selected_data)][feature]) + source_items = receiver.component_groups.existing_items_for(route) + if reorder: + source_items.reverse() + old.opacity, old.visible = 0.35, False + receiver.viewer.camera.center = (0, 2, 3) + receiver.viewer.camera.zoom = 19 + result = ViewerWindowService(QueuedRetirementGateway(receiver)).presentation( + retirement_request(receiver, middle) + ) + assert result.applied and not result.errors + native = receiver.layer_route_state.layer(route) + assert native is not old and old not in receiver.viewer.layers + assert set(native.features.iloc[list(native.selected_data)][feature]) == retained + if reorder and selected: + assert native.selected_data != {0, 1} + assert receiver.component_groups.existing_items_for(route) is source_items + assert tuple(native.scale[-2:]) == (0.65, 0.65) + assert native.opacity == 0.35 and not native.visible + assert receiver.viewer.layers.selection.active is native + assert receiver.viewer.camera.zoom == 19 + assert tuple(receiver.viewer.camera.center) == (0, 2, 3) + step = receiver.viewer.dims.current_step + QApplication.instance().processEvents() + assert receiver.viewer.dims.current_step == step + assert set(native.features.iloc[list(native.selected_data)][feature]) == retained + assert receiver.viewer.layers.selection.active is native + + +def test_new_selectable_capability_executes_cooperative_retention_hooks(receiver, monkeypatch): + calls = [] + monkeypatch.setitem(NapariPointsLayerDisplayHandler.__registry__, StreamingDataType.POINTS, + NapariPointsLayerDisplayHandler) + + class IndependentPresentationCapability: + @contextmanager + def preserve_native_presentation(self, request): + calls.append("enter") + with super().preserve_native_presentation(request): + yield + calls.append("exit") + + class NewDeclaredPointsHandler(IndependentPresentationCapability, NapariPointsLayerDisplayHandler): + streaming_data_type = StreamingDataType.POINTS + + middle, update = enqueue(receiver, np.ones((2, 2)), well="A02", producer="middle") + advance_in_qt(receiver, middle, update) + for well in ("A01", "A03"): + route, update = enqueue(receiver, [{"type": "points", "coordinates": [[1, 2]], + "metadata": {}}], well=well, producer="new-case", + data_type=StreamingDataType.POINTS, domain=["A01", "A03"]) + advance_in_qt(receiver, route, update) + old = receiver.layer_route_state.layer(route) + # Select the declared A03 source frame before retaining its member. Native + # selected_data alone can contain an off-slice Points row. + prepared = NapariNavigationControlMessageAction().prepare( + receiver, ViewerNavigationControlOptions(route_key=route, data_index=1, + visible=True, selected=True), + ) + assert prepared.viewer_step is not None + receiver.viewer.dims.current_step = prepared.viewer_step + old.selected_data = {1} + old.opacity = 0.2 + result = ViewerWindowService(QueuedRetirementGateway(receiver)).presentation( + retirement_request(receiver, middle) + ) + assert result.applied and not result.errors + native = receiver.layer_route_state.layer(route) + assert calls == ["enter", "exit"] + assert native.selected_data == {1} and native.opacity == 0.2 + assert issubclass(NewDeclaredPointsHandler, NapariSelectablePresentationRetention) + + +@pytest.mark.parametrize("joined", [False, True]) +@pytest.mark.parametrize("reorder", [False, True]) +def test_last_source_frame_retains_points_and_shapes_during_domain_pruning( + receiver, joined, reorder, +): + """Original installed15: both selected families at A03/Z2, not first-well QA.""" + spacing = (2.0, 0.65, 0.65) + middle, update = enqueue( + receiver, np.ones((5, 7)), well="A02", producer="middle", + z_index=0, voxel_spacing=spacing, + ) + advance_in_qt(receiver, middle, update) + for well in ("A01", "A03"): + for z in range(4): + raw, update = enqueue( + receiver, np.ones((5, 7)), well=well, producer="source-volume", + domain=["A01", "A03"], z_domain=list(range(4)), z_index=z, + voxel_spacing=spacing, + ) + advance_in_qt(receiver, raw, update) + # The actual installed scene also had an unrelated saved A01 Points route + # selected, then hidden before A03 became current. It is not rematerialized. + untouched, update = enqueue( + receiver, [{"type": "points", "coordinates": [[1.5, 2.5]], + "metadata": {"label": 7, ROIFractionalZ.FIELD: 1.5}}], + producer="saved-A01", data_type=StreamingDataType.POINTS, + z_domain=list(range(4)), z_index=0, voxel_spacing=spacing, + ) + advance_in_qt(receiver, untouched, update) + untouched_layer = receiver.layer_route_state.layer(untouched) + untouched_layer.selected_data = {0} + untouched_layer.visible = False + routes = [] + for data_type, name in ((StreamingDataType.POINTS, "centres"), + (StreamingDataType.SHAPES, "outlines")): + for well in (("A01", "A03") if joined else ("A03",)): + payload = ( + [{"type": "points", "coordinates": [[1.5, 2.5]], + "metadata": {"label": 7, ROIFractionalZ.FIELD: 1.5}}] + if data_type is StreamingDataType.POINTS else + [{"type": "polygon", "coordinates": [[1, 2], [1, 3], [2, 3], [2, 2]], + "metadata": {"label": 7}}] + ) + route, update = enqueue( + receiver, payload, well=well, producer=name, data_type=data_type, + domain=["A01", "A03"] if joined else ["A03"], + z_domain=list(range(4)), z_index=0 if data_type is StreamingDataType.POINTS else 2, + voxel_spacing=spacing, + ) + advance_in_qt(receiver, route, update) + routes.append(route) + viewer = receiver.viewer + shapes = receiver.layer_route_state.layer(routes[1]) + presentation = receiver.layer_route_state.dimension_state_for(routes[1]).presentation + step = list(viewer.dims.current_step) + step[presentation.axis_labels.index("well")] = 2 + step[presentation.axis_labels.index("z_index")] = 2 + viewer.dims.current_step = tuple(step) + viewer.layers.selection.active = shapes + retained = {} + feature = NapariStreamLayerItem.ELEMENT_IDENTITY_FEATURE + for route in routes: + layer = receiver.layer_route_state.layer(route) + selected = len(layer.data) - 1 + assert selected in layer._indices_view + layer.selected_data = {selected} + retained[route] = (layer, layer.features.iloc[selected][feature]) + QApplication.instance().processEvents() + assert viewer.dims.current_step[presentation.axis_labels.index("well")] == 2 + for route in routes: + if reorder: + receiver.component_groups.existing_items_for(route).reverse() + viewer.camera.center, viewer.camera.zoom = (0, 1.3, 1.95), 31 + frame = viewer.dims.point + order = viewer.dims.order + result = ViewerWindowService(QueuedRetirementGateway(receiver)).presentation( + retirement_request(receiver, middle) + ) + assert result.applied and not result.errors + assert receiver.layer_route_state.layer(untouched) is untouched_layer + assert untouched_layer.selected_data == {0} and not untouched_layer.visible + assert middle not in receiver.layer_route_state.layers + assert receiver.layer_route_state.layer(raw).data.shape[-3:] == (2, 5, 7) + for route, (old, identity) in retained.items(): + native = receiver.layer_route_state.layer(route) + assert (native is not old) is joined + assert set(native.features.iloc[list(native.selected_data)][feature]) == {identity} + assert set(native.selected_data) <= set(native._indices_view) + state = receiver.layer_route_state.dimension_state_for(route) + assert state.presentation.projection.component_values["well"] == ( + ["A01", "A03"] if joined else ["A03"] + ) + assert tuple(native.scale[-2:]) == (0.65, 0.65) + assert native.scale[presentation.axis_labels.index("z_index")] == 2 + assert viewer.layers.selection.active is receiver.layer_route_state.layer(routes[1]) + expected = list(frame) + expected[presentation.axis_labels.index("well")] = 1 + np.testing.assert_allclose(viewer.dims.point, expected) + assert viewer.dims.order == order + assert viewer.camera.zoom == 31 + np.testing.assert_allclose(viewer.camera.center, (0, 1.3, 1.95)) + QApplication.instance().processEvents() + np.testing.assert_allclose(viewer.dims.point, expected) + for route, (_, identity) in retained.items(): + layer = receiver.layer_route_state.layer(route) + assert set(layer.features.iloc[list(layer.selected_data)][feature]) == {identity} + + +def test_controller_remount_retains_linked_subject_members_and_binding(receiver): + viewer = receiver.viewer + paths = [np.asarray([[i, i], [i + 1, i + 1]], dtype=float) for i in range(3)] + feature = NapariStreamLayerItem.ELEMENT_IDENTITY_FEATURE + old = viewer.add_shapes(paths, shape_type="path", + features={feature: ["a", "b", "c"], "owner": [8, 8, 9]}) + linked = viewer.add_shapes(paths[:2], shape_type="path", + features={feature: ["x", "y"], "owner": [8, 9]}) + controller = receiver.result_selection_controller + binding = NapariResultSelectionGroupBinding("independent-subject", "owner") + controller.bind(old, binding) + controller.bind(linked, binding) + controller.select(old, 0) + assert old.selected_data == {0, 1} and linked.selected_data == {0} + receiver.layer_route_state.set_layer("retained-selection", old) + with controller.preserve_selection("retained-selection"): + viewer.layers.remove(old) + replacement = viewer.add_shapes(paths[::-1], shape_type="path", + features={feature: ["c", "b", "a"], "owner": [9, 8, 8]}) + receiver.layer_route_state.set_layer("retained-selection", replacement) + QApplication.instance().processEvents() + assert replacement.selected_data == {1, 2} and linked.selected_data == {0} + assert not controller.is_bound_result_layer(old) + old.selected_data = {2} + assert linked.selected_data == {0} + controller.select(replacement, 0) + assert replacement.selected_data == {0} and linked.selected_data == {1} + + +@pytest.mark.parametrize("identities", [None, ["duplicate", "duplicate"]]) +def test_controller_refuses_unidentified_members_before_remount(receiver, identities): + features = {} if identities is None else { + NapariStreamLayerItem.ELEMENT_IDENTITY_FEATURE: identities, + } + old = receiver.viewer.add_points([[1, 2], [3, 4]], features=features) + receiver.layer_route_state.set_layer("retained-selection", old) + with pytest.raises(ValueError, match="source element identities"): + with receiver.result_selection_controller.preserve_selection("retained-selection"): + pytest.fail("Invalid member identity must be rejected before mutation.") + assert old in receiver.viewer.layers + + +def test_controller_retains_empty_native_geometry_without_invented_identity(receiver): + old = receiver.viewer.add_shapes(ndim=2) + receiver.layer_route_state.set_layer("retained-selection", old) + with receiver.result_selection_controller.preserve_selection("retained-selection"): + receiver.viewer.layers.remove(old) + replacement = receiver.viewer.add_shapes(ndim=2) + receiver.layer_route_state.set_layer("retained-selection", replacement) + assert not replacement.selected_data + + +@pytest.mark.parametrize("data_type", [StreamingDataType.POINTS, StreamingDataType.SHAPES]) +def test_controller_refuses_off_slice_remapped_selection_before_native_assignment( + receiver, data_type, +): + viewer = receiver.viewer + viewer.add_image(np.zeros((2, 8, 8), dtype=np.uint8)) + feature = NapariStreamLayerItem.ELEMENT_IDENTITY_FEATURE + coordinates = [np.asarray([[0, i, i], [0, i + 1, i + 1]], dtype=float) for i in range(2)] + + def mount(z_index): + data = [path + [z_index, 0, 0] for path in coordinates] + features = {feature: ["a", "b"], "owner": [8, 8]} + if data_type is StreamingDataType.POINTS: + return viewer.add_points([path[0] for path in data], features=features) + return viewer.add_shapes(data, shape_type="path", features=features) + + old = mount(0) + viewer.dims.current_step = (0, 0, 0) + controller = receiver.result_selection_controller + controller.bind(old, NapariResultSelectionGroupBinding("retained-subject", "owner")) + controller.select(old, 0) + assert old.selected_data == {0, 1} + receiver.layer_route_state.set_layer("retained-selection", old) + with pytest.raises(ValueError, match="selected source members have no displayed geometry"): + with controller.preserve_selection("retained-selection"): + viewer.layers.remove(old) + replacement = mount(1) + receiver.layer_route_state.set_layer("retained-selection", replacement) + assert not replacement.selected_data + assert tuple(replacement.features[feature]) == ("a", "b") + assert viewer.dims.current_step == (0, 0, 0) + step = viewer.dims.current_step + QApplication.instance().processEvents() + assert viewer.dims.current_step == step and not replacement.selected_data + + +def test_controller_preservation_cancels_queued_navigation_without_unmount(receiver): + feature = NapariStreamLayerItem.ELEMENT_IDENTITY_FEATURE + layer = receiver.viewer.add_shapes([[[0, 0], [1, 1]]], shape_type="path", + features={feature: ["member"]}) + controller = receiver.result_selection_controller + controller.bind(layer) + layer.selected_data = {0} + layer.visible = False + generation = controller._pending_generation + step = receiver.viewer.dims.current_step + receiver.layer_route_state.set_layer("retained-selection", layer) + with controller.preserve_selection("retained-selection"): + pass + assert controller._pending_generation > generation + QApplication.instance().processEvents() + assert not layer.visible and layer.selected_data == {0} + assert receiver.viewer.dims.current_step == step + + +@pytest.mark.parametrize("pair", [("y", "x"), ("z_index", "x"), ("z_index", "y")]) +def test_fractional_spatial_selection_survives_real_qt_rematerialization(receiver, pair): + middle, update = enqueue(receiver, np.ones((5, 6)), well="A02", producer="middle") + advance_in_qt(receiver, middle, update) + receiver.layer_route_state.layer(middle).visible = False + for well in ("A01", "A03"): + for z_index in (1, 2, 3, 4): + raw, update = enqueue( + receiver, np.ones((5, 6)), well=well, producer="raw-frames", + domain=["A01", "A03"], z_domain=[1, 2, 3, 4], z_index=z_index, + ) + advance_in_qt(receiver, raw, update) + payloads = [] + for well in ("A01", "A03"): + payload = [{"type": "points", "coordinates": [[1.5, 2.5]], + "metadata": {ROIFractionalZ.FIELD: 1.5, "label": 7}}] + payloads.append(payload) + route, update = enqueue( + receiver, payload, well=well, producer="fractional-survivor", + data_type=StreamingDataType.POINTS, domain=["A01", "A03"], + z_domain=[1, 2, 3, 4], + ) + advance_in_qt(receiver, route, update) + receiver.raise_result_selection_surface = lambda: None # No dock/socket shell in this fixture. + old = receiver.layer_route_state.layer(route) + feature = NapariStreamLayerItem.ELEMENT_IDENTITY_FEATURE + identity = old.features.iloc[1][feature] + presentation = receiver.layer_route_state.dimension_state_for(route).presentation + spatial_dimensions = tuple( + presentation.axis_labels.index(axis) for axis in presentation.spatial_axis_labels + ) + original_coordinates = old.data[1, spatial_dimensions].copy() + sources = receiver.component_groups.existing_items_for(route) + navigation = NapariNavigationControlMessageAction() + + def select_and_check(layer): + member = tuple(layer.features[feature]).index(identity) + prepared = navigation.prepare(receiver, ViewerNavigationControlOptions( + route_key=route, data_index=member, visible=True, selected=True, + display_axes=pair, + )) + presentation = receiver.layer_route_state.dimension_state_for(route).presentation + hidden_spatial = next(axis for axis in presentation.spatial_axis_labels if axis not in pair) + assert prepared.request.axis_indices[hidden_spatial] == ( + 3 if hidden_spatial == "x" else 2 + ) + navigation.apply_prepared(receiver, prepared) + QApplication.instance().processEvents() # Exercise the original selection callback too. + assert tuple(receiver.viewer.dims.displayed) == tuple( + presentation.axis_labels.index(axis) for axis in pair + ) + assert set(layer.features.iloc[list(layer.selected_data)][feature]) == {identity} + np.testing.assert_array_equal(layer.data[member, spatial_dimensions], original_coordinates) + # Implicit displayed dimensions and explicit pair overrides have the same contract. + assert navigation.result_element_axis_indices(receiver, layer, route, member) == ( + prepared.request.axis_indices + ) + assert tuple(layer.scale[-2:]) == (0.65, 0.65) + + select_and_check(old) + sources.reverse() # Same source identity, deliberately different native row position. + result = ViewerWindowService(QueuedRetirementGateway(receiver)).presentation( + retirement_request(receiver, middle) + ) + assert result.applied and not result.errors + native = receiver.layer_route_state.layer(route) + assert native is not old and native.selected_data == {0} + assert receiver.component_groups.existing_items_for(route) is sources + select_and_check(native) + assert payloads == [[{"type": "points", "coordinates": [[1.5, 2.5]], + "metadata": {ROIFractionalZ.FIELD: 1.5, "label": 7}}]] * 2 + + +def test_declared_point_coordinate_capability_executes_cooperative_hooks(receiver, monkeypatch): + admitted = [] + monkeypatch.setitem(NapariPointsLayerDisplayHandler.__registry__, StreamingDataType.POINTS, + NapariPointsLayerDisplayHandler) + + class CoordinateAdmissionRecording: + @classmethod + def _coordinate_value(cls, value, *, axis_label): + admitted.append(axis_label) + return super()._coordinate_value(value, axis_label=axis_label) + + class RecordedPointCoordinates(CoordinateAdmissionRecording, ViewerPointCoordinateAuthority): + pass + + class RecordedPointsHandler(NapariPointsLayerDisplayHandler): + streaming_data_type = StreamingDataType.POINTS + result_coordinate_authority = RecordedPointCoordinates + + route, update = enqueue(receiver, [{"type": "points", "coordinates": [[1.5, 2.5]], + "metadata": {ROIFractionalZ.FIELD: 1.5}}], + producer="new-coordinate-case", data_type=StreamingDataType.POINTS, + z_domain=[1, 2, 3, 4]) + advance_in_qt(receiver, route, update) + layer = receiver.layer_route_state.layer(route) + presentation = receiver.layer_route_state.dimension_state_for(route).presentation + action = NapariNavigationControlMessageAction() + for pair in (("y", "x"), ("z_index", "x"), ("z_index", "y")): + action.result_element_axis_indices( + receiver, layer, route, 0, + displayed_axis_indices=tuple(presentation.axis_labels.index(axis) for axis in pair), + ) + assert admitted.count("z_index") == admitted.count("y") == admitted.count("x") == 1 + np.testing.assert_array_equal(layer.data[0, tuple( + presentation.axis_labels.index(axis) for axis in presentation.spatial_axis_labels + )], (1.5, 1.5, 2.5)) + assert RecordedPointsHandler.result_coordinate_authority is RecordedPointCoordinates diff --git a/tests/unit/test_streaming_service.py b/tests/unit/test_streaming_service.py index 5f801971c6..ddf4673d1f 100644 --- a/tests/unit/test_streaming_service.py +++ b/tests/unit/test_streaming_service.py @@ -15,6 +15,7 @@ FixedStreamProducerIdentityKind, StreamProducerIdentity, ) +from polystore.streaming_constants import StreamingDataType from polystore.streaming.viewer_transport import ViewerStreamKwarg, ViewerStreamProducer from polystore.virtual_workspace import SourcePixelRef from polystore.zmq_config import POLYSTORE_ZMQ_CONFIG @@ -65,6 +66,10 @@ ) from openhcs.runtime.fiji_stream_visualizer import FijiStreamVisualizer from openhcs.runtime.napari_stream_visualizer import NapariStreamVisualizer +from openhcs.runtime.napari_streaming_handlers import ( + NapariStreamLayerAddress, + NapariStreamLayerItem, +) from openhcs.runtime.viewer_protocol import ( DetachedViewerLaunchFailure, DetachedViewerServerEntrypointSpec, @@ -1068,11 +1073,16 @@ def test_3d_point_archive_reopens_with_native_z_domain_and_features(tmp_path): entries=domain.entries, layout=ViewerObjectDisplayConfigInput(config).layout(), ) - item = SimpleNamespace( - address=SimpleNamespace( - components=stream.source.metadata.metadata_by_path[archive] + item = NapariStreamLayerItem( + address=NapariStreamLayerAddress( + components=stream.source.metadata.metadata_by_path[archive], + path=archive, + stream_layer_data_type=StreamingDataType.POINTS, ), + producer=stream.producer.identities[0], data=NapariROIConverter.rois_to_shapes(data[0]), + image_metadata=reopened_source, + plane_component_domain=domain, ) request = ViewerLayerAxisProjectionRequestAuthority.from_component_axis_semantics( route_key="centres", @@ -1089,6 +1099,9 @@ def test_3d_point_archive_reopens_with_native_z_domain_and_features(tmp_path): points, properties = _build_nd_points([item], projection) assert points[0, projection.projected_axis_components.index("z_index")] == 2.375 assert properties["response"] == [4.75] + assert properties[NapariStreamLayerItem.ELEMENT_IDENTITY_FEATURE] == [ + item.element_identity(0) + ] def test_explicit_native_reopening_rejects_an_external_roi_without_source(tmp_path): diff --git a/tests/unit/test_viewer_result_element_selection.py b/tests/unit/test_viewer_result_element_selection.py index 949c604885..51563b1f9b 100644 --- a/tests/unit/test_viewer_result_element_selection.py +++ b/tests/unit/test_viewer_result_element_selection.py @@ -9,6 +9,8 @@ import numpy as np import pytest +from polystore.streaming.identity import StreamProducerIdentity +from polystore.streaming_constants import StreamingDataType from zmqruntime.viewer_protocol import ViewerComponentMode from openhcs.agent.dto.execution import ExecutionConnectionSpec @@ -25,7 +27,10 @@ NapariComponentGroupStore, NapariDimensionLayerState, NapariLayerRouteStateStore, + NapariStreamLayerAddress, + NapariStreamLayerItem, ) +from openhcs.core.runtime_image_values import ImagePayloadMetadata from openhcs.runtime.napari_viewer_server import ( NapariLayerIsolationControlMessageAction, NapariNavigationControlMessageAction, @@ -36,7 +41,7 @@ NapariViewerServer, ) from openhcs.runtime.viewer_controls import ( - ViewerFractionalZPointCoordinateAuthority, + ViewerPointCoordinateAuthority, ViewerLayerIsolationControlOptions, ViewerNavigationControlOptions, ViewerResultElementCoordinateAuthority, @@ -45,6 +50,7 @@ ViewerComponentAxisSemanticsAuthority, ViewerComponentLayout, ViewerLayerAxisProjection, + ViewerComponentValueDomainPayload, ) from openhcs.runtime.viewer_protocol import ( ViewerControlResponseField, @@ -155,6 +161,26 @@ def _viewer_server(viewer, layer, route_key: str = "result-rois", group_binding= ) server.result_selection_controller = NapariResultSelectionController(server) if NapariResultElementSelectionAuthority.state(layer).supported: + from napari.layers import Points + + server.component_groups.items_for(route_key).append( + NapariStreamLayerItem( + data=layer.data, + producer=StreamProducerIdentity( + origin="manual", output_kind="fixture", output_key=route_key, + projection_key=route_key, + ), + address=NapariStreamLayerAddress( + components={}, path=f"fixture/{route_key}", + stream_layer_data_type=( + StreamingDataType.POINTS if isinstance(layer, Points) + else StreamingDataType.SHAPES + ), + ), + image_metadata=ImagePayloadMetadata(), + plane_component_domain=ViewerComponentValueDomainPayload(()), + ) + ) server.result_selection_controller.bind(layer, group_binding=group_binding) return server, overlay, result_selection_dock, qt_window @@ -596,12 +622,14 @@ def test_result_element_coordinate_authority_derives_native_slice_indices() -> N coordinates=coordinates, axis_labels=("channel", "z", "y", "x"), displayed_axis_indices=(2, 3), + spatial_axis_labels=("y", "x"), ) == {"channel": 2, "z": 3} assert ( ViewerResultElementCoordinateAuthority.axis_indices( coordinates=(4, 5), axis_labels=("y", "x"), displayed_axis_indices=(0, 1), + spatial_axis_labels=("y", "x"), ) == {} ) @@ -613,22 +641,45 @@ def test_result_element_coordinate_authority_rejects_cross_slice_geometry() -> N coordinates=((2, 3, 4, 4), (2, 4, 4, 6), (2, 3, 6, 6)), axis_labels=("channel", "z", "y", "x"), displayed_axis_indices=(2, 3), + spatial_axis_labels=("y", "x"), ) def test_fractional_z_point_navigation_chooses_slice_without_rounding_geometry() -> ( None ): - assert ViewerFractionalZPointCoordinateAuthority.axis_indices( + assert ViewerPointCoordinateAuthority.axis_indices( coordinates=(2.375, 1.25, 3.5), axis_labels=("z_index", "y", "x"), displayed_axis_indices=(1, 2), + spatial_axis_labels=("z_index", "y", "x"), ) == {"z_index": 2} with pytest.raises(ValueError, match="integral slice"): ViewerResultElementCoordinateAuthority.axis_indices( coordinates=(2.375, 1.25, 3.5), axis_labels=("z_index", "y", "x"), displayed_axis_indices=(1, 2), + spatial_axis_labels=("z_index", "y", "x"), + ) + + +@pytest.mark.parametrize("coordinate", [1.5, float("nan"), float("inf")]) +def test_point_coordinate_owner_preserves_strict_acquisition_admission(coordinate): + with pytest.raises(ValueError): + ViewerPointCoordinateAuthority.axis_indices( + coordinates=(coordinate, 1.5, 2.5, 3.5), + axis_labels=("channel", "z_index", "y", "x"), + displayed_axis_indices=(2, 3), + spatial_axis_labels=("z_index", "y", "x"), + ) + + +@pytest.mark.parametrize("displayed", [(1, 2), (0, 2), (0, 1)]) +def test_shapes_coordinate_owner_stays_strict_for_hidden_spatial_axes(displayed): + with pytest.raises(ValueError, match="integral slice"): + ViewerResultElementCoordinateAuthority.axis_indices( + coordinates=((1.5, 2.5, 3.5),), axis_labels=("z_index", "y", "x"), + displayed_axis_indices=displayed, spatial_axis_labels=("z_index", "y", "x"), )