diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 000000000..70b9b2459 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,62 @@ +name: Bug report +description: Report a reproducible OpenHCS failure +title: "[Bug]: " +labels: + - bug +body: + - type: markdown + attributes: + value: | + Thanks for helping improve OpenHCS. Do not include patient data, credentials, private plate paths, or proprietary images. + - type: input + id: version + attributes: + label: OpenHCS version + description: Copy the version from About, `python -m openhcs --version`, or package metadata. + validations: + required: true + - type: dropdown + id: install + attributes: + label: Installation route + options: + - Windows installer + - macOS installer + - PyPI + - Source checkout + - Other + validations: + required: true + - type: textarea + id: environment + attributes: + label: Environment + description: Operating system, Python version if applicable, CPU/GPU, viewer, and MCP client/surface if involved. + validations: + required: true + - type: textarea + id: reproduce + attributes: + label: Minimal reproduction + description: Numbered steps, with a synthetic or redacted example when data is involved. + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected and actual behavior + validations: + required: true + - type: textarea + id: logs + attributes: + label: Logs or screenshots + description: Paste the smallest relevant excerpt and redact sensitive paths or data. + render: shell + - type: checkboxes + id: sensitive + attributes: + label: Data safety + options: + - label: I removed credentials, patient data, private paths, and proprietary image content. + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..e5395baf0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: true +contact_links: + - name: Security report + url: mailto:tristan.simas@mail.mcgill.ca + about: Report vulnerabilities privately rather than opening an issue. + - name: Documentation + url: https://openhcs.readthedocs.io/ + about: Check installation, architecture, and MCP client guidance. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..f6f8594ec --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,16 @@ +## What changed + +Describe the user-visible outcome and the declaration or typed owner changed. + +## Evidence + +- [ ] Added or updated a focused regression +- [ ] Ran the relevant local tests +- [ ] Preserved declaration ownership; no mirrored registry or metadata source +- [ ] Documented platform, GPU, viewer, or installer journeys not run locally + +Commands and results: + +```text + +``` diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index e5d117188..d1b6c2165 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -533,6 +533,8 @@ jobs: run: git config --global core.longpaths true - uses: actions/checkout@v4 + with: + submodules: ${{ env.OPENHCS_CI_DEP_SOURCE == 'submodules' && 'recursive' || 'false' }} - name: Set up Python 3.12 uses: actions/setup-python@v5 @@ -557,6 +559,9 @@ jobs: python -m pip install build packaging pytest python -m pytest -q -c tests/installer/pytest.ini tests/installer + - name: Build installer source wheelhouse + run: python -m scripts.build_installer_source_wheelhouse --output dist --dependency-source "${{ env.OPENHCS_CI_DEP_SOURCE }}" + - name: Parse Windows PowerShell installer sources if: matrix.platform == 'windows' shell: pwsh @@ -574,8 +579,7 @@ jobs: shell: pwsh timeout-minutes: 30 run: | - python -m build --wheel - $releaseVersion = python -c "from pathlib import Path; from packaging.utils import parse_wheel_filename; print(parse_wheel_filename(next(Path('dist').glob('*.whl')).name)[1])" + $releaseVersion = python -c "from pathlib import Path; from packaging.utils import parse_wheel_filename; versions=[parse_wheel_filename(path.name)[1] for path in Path('dist').glob('*.whl') if str(parse_wheel_filename(path.name)[0])=='openhcs']; assert len(versions)==1, versions; print(versions[0])" $releaseVersion = $releaseVersion.Trim() $stage = Join-Path $env:RUNNER_TEMP "OpenHCS-Windows-Smoke" $installRoot = Join-Path $env:RUNNER_TEMP "OpenHCS Installed" @@ -828,8 +832,7 @@ jobs: - name: Execute and verify macOS installer if: matrix.platform == 'macos' run: | - python -m build --wheel - release_version=$(python -c "from pathlib import Path; from packaging.utils import parse_wheel_filename; print(parse_wheel_filename(next(Path('dist').glob('*.whl')).name)[1])") + release_version=$(python -c "from pathlib import Path; from packaging.utils import parse_wheel_filename; versions=[parse_wheel_filename(path.name)[1] for path in Path('dist').glob('*.whl') if str(parse_wheel_filename(path.name)[0])=='openhcs']; assert len(versions)==1, versions; print(versions[0])") staged_contract="$RUNNER_TEMP/installer_contract.json" installer_app="$RUNNER_TEMP/OpenHCS Release Installer.app" smoke_home="$RUNNER_TEMP/openhcs-installer-home" diff --git a/.gitignore b/.gitignore index a7daeb9df..0739b57d6 100644 --- a/.gitignore +++ b/.gitignore @@ -180,6 +180,10 @@ benchmark/results/napari_streaming_validator/ /*.md !/README.md !/CHANGELOG.md +!/CITATION.cff +!/CODE_OF_CONDUCT.md +!/CONTRIBUTING.md +!/SECURITY.md # Chunkhound .chunkhound/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a0549935..3dad8bfe9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- Removed the dataset-specific BBBC021 and BBBC038 microscope choices. BBBC + datasets now use their declared source bindings, while benchmark dataset IDs + remain available as provenance. +- Renamed Napari component placement from ``slice`` to ``layer`` so display + configuration now distinguishes separate viewer layers from genuine image + slices; Fiji slice placement is unchanged. + ### Added #### Auto-Add Output Plate Feature @@ -41,6 +50,91 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added `requests>=2.31.0` dependency for HTTP communication - Comprehensive test suite for LLM service and chat panel +## [0.7.11] - 2026-08-01 + +### Fixed + +- Windows multiprocessing now preserves the installed interpreter used to + launch OpenHCS instead of resolving to an unrelated Python executable. +- Desktop monitoring presents the canonical brand and exposes its typed + settings without clipped information text. + +## [0.7.10] - 2026-08-01 + +### Fixed + +- Execution progress registration now routes through the control authority so + UI and agent projections observe the same lifecycle. + +## [0.7.9] - 2026-08-01 + +### Changed + +- Configuration layouts now reflow responsively while preserving readable + field and help surfaces. + +## [0.7.8] - 2026-07-31 + +### Fixed + +- Release builds resolve versions for independently published dependencies + from their authoritative package declarations. + +## [0.7.7] - 2026-07-31 + +### Changed + +- Native desktop updates and their recovery/presentation lifecycle were + hardened across supported platforms. + +## [0.7.6] - 2026-07-31 + +### Fixed + +- Dock widgets correctly return to the managed workspace and branding refresh + no longer disturbs the native layout. + +## [0.7.5] - 2026-07-31 + +### Changed + +- Desktop startup, docked workspace presentation, and CI runner path handling + were polished after the initial gallery release. + +## [0.7.4] - 2026-07-31 + +### Added + +- A verified workflow media gallery and capture tooling that rejects fabricated + UI state. +- Unit/core CI gates, typed viewer controls, agent-visible viewer authority, + UI recovery receipts, and hardened bridge endpoint identity. + +### Changed + +- The main workspace uses native dock and control theming, preserves geometry, + and suppresses consoles for background GUI processes. +- ObjectState 1.1.1 provides transactional typed configuration and edit-history + recovery. + +## [0.7.3] - 2026-07-30 + +### Fixed + +- The official logo family is used consistently across release and desktop + surfaces. + +## [0.7.2] - 2026-07-30 + +### Added + +- Canonical brand assets for the GUI, launcher, installer, website, and package. + +### Changed + +- Windows GUI launchers suppress the console and the public site describes the + current beta without stale patch-level launch claims. + ## [0.7.1] - 2026-07-30 ### Added @@ -221,4 +315,14 @@ See git history for changes in versions 0.3.14 and earlier. [0.4.0]: https://github.com/trissim/openhcs/compare/v0.3.15...v0.4.0 [0.3.15]: https://github.com/trissim/openhcs/releases/tag/v0.3.15 +[0.7.11]: https://github.com/OpenHCSDev/OpenHCS/compare/v0.7.10...v0.7.11 +[0.7.10]: https://github.com/OpenHCSDev/OpenHCS/compare/v0.7.9...v0.7.10 +[0.7.9]: https://github.com/OpenHCSDev/OpenHCS/compare/v0.7.8...v0.7.9 +[0.7.8]: https://github.com/OpenHCSDev/OpenHCS/compare/v0.7.7...v0.7.8 +[0.7.7]: https://github.com/OpenHCSDev/OpenHCS/compare/v0.7.6...v0.7.7 +[0.7.6]: https://github.com/OpenHCSDev/OpenHCS/compare/v0.7.5...v0.7.6 +[0.7.5]: https://github.com/OpenHCSDev/OpenHCS/compare/v0.7.4...v0.7.5 +[0.7.4]: https://github.com/OpenHCSDev/OpenHCS/compare/v0.7.3...v0.7.4 +[0.7.3]: https://github.com/OpenHCSDev/OpenHCS/compare/v0.7.2...v0.7.3 +[0.7.2]: https://github.com/OpenHCSDev/OpenHCS/compare/v0.7.1...v0.7.2 [0.7.0]: https://github.com/OpenHCSDev/OpenHCS/compare/v0.6.17...v0.7.0 diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 000000000..268253638 --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,17 @@ +cff-version: 1.2.0 +message: "If you use OpenHCS, cite the exact release and archive identifier used in your analysis." +title: "OpenHCS: typed high-content microscopy workflows" +type: software +authors: + - family-names: Simas + given-names: Tristan + email: tristan.simas@mail.mcgill.ca +repository-code: "https://github.com/OpenHCSDev/OpenHCS" +url: "https://openhcs.readthedocs.io/" +license: MIT +keywords: + - high-content screening + - bioimage analysis + - microscopy + - image processing + - Model Context Protocol diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..a75bae7af --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,23 @@ +# Code of Conduct + +OpenHCS is committed to a welcoming, harassment-free community for scientists, +software contributors, and users of every background and level of experience. + +Be respectful and specific. Critique ideas and evidence rather than people; +assume good faith while accepting correction; protect private research data; +and make space for people who are new to bioimage analysis or open-source +development. Harassment, intimidation, discriminatory language, sexualized +attention, doxxing, and sustained disruption are not acceptable. + +Maintainers may edit or remove contributions and temporarily or permanently +restrict participation when conduct is unsafe or persistently disruptive. +Enforcement decisions should be proportionate, documented privately, and avoid +unnecessary disclosure of the reporter's identity. + +Report conduct concerns privately to +[tristan.simas@mail.mcgill.ca](mailto:tristan.simas@mail.mcgill.ca). Reports +will be reviewed promptly and handled as confidentially as practical. This +policy applies in project repositories, documentation, community forums, and +project-related private communications. + +This policy is informed by the Contributor Covenant, version 2.1. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..36d94a523 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,69 @@ +# Contributing to OpenHCS + +OpenHCS welcomes reproducible bug reports, microscope fixtures, processing +functions, documentation, and focused code changes. + +## Report an issue + +Search the issue tracker first, then include the OpenHCS version, installation +route, operating system, Python version, minimal steps, expected behavior, and +relevant logs. For microscopy failures, describe the instrument, axes, naming +pattern, and expected output without uploading sensitive image data. + +Security vulnerabilities should be reported privately as described in +[SECURITY.md](SECURITY.md). + +## Development checkout + +OpenHCS uses source submodules for its independently published libraries: + +```bash +git clone --depth 1 --recurse-submodules --shallow-submodules \ + https://github.com/OpenHCSDev/OpenHCS.git +cd OpenHCS +python -m venv .venv +source .venv/bin/activate +python -m pip install --upgrade pip +python -m pip install -e "external/python-introspect" +python -m pip install -e "external/metaclass-registry" +python -m pip install -e "external/ObjectState" +python -m pip install -e "external/arraybridge" +python -m pip install -e "external/pycodify" +python -m pip install -e "external/PolyStore" +python -m pip install -e "external/pyqt-reactive" +python -m pip install -e "external/zmqruntime" +python -m pip install -e ".[dev,dev-gui,gui,mcp]" +``` + +See `docs/development_setup.md` for optional GPU, viewer, Bio-Formats, OMERO, +and CellProfiler compatibility dependencies. + +## Architecture expectations + +Read `docs/source/architecture/quick_start.rst`, then +`system_overview.rst`, `nominal_ownership.rst`, and +`abstraction_lattices.rst` before a structural change. + +- Change the declaration or typed owner of a behavior. +- Derive schemas, UI projections, registries, and runtime plans from that owner. +- Do not add mirrored metadata tables, parallel kind registries, sidecars, or + compatibility shims when the owning contract can express the rule. +- Keep compilation deterministic. Workers consume frozen compiled plans rather + than reinterpreting mutable authoring objects. +- Put reusable behavior in the library that owns it; update the OpenHCS + submodule pointer only after that library change is independently tested. + +## Tests and pull requests + +Run the narrowest relevant tests while developing, then the CPU unit gate: + +```bash +OPENHCS_CPU_ONLY=1 python -m pytest tests/unit +python -m ruff check openhcs tests +``` + +GUI tests can use `QT_QPA_PLATFORM=offscreen`. Add a regression that fails for +the reported behavior and exercises its public or nominal boundary. Keep pull +requests focused, explain the evidence boundary, and note any platform or GPU +journey you could not run locally. All contributions must follow +[CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md). diff --git a/README.md b/README.md index c0765a00e..a68da067a 100644 --- a/README.md +++ b/README.md @@ -4,14 +4,15 @@

OpenHCS

-**Bioimage analysis platform for high-content screening**\ -**Compile-time validation · Bidirectional GUI↔Code · Multi-GPU · LLM pipeline generation · Extensible function registry** +**Typed bioimage workflows for high-content screening**\ +**GUI ↔ Python · Compile-first execution · Multi-GPU · Local MCP agents · CellProfiler import** [![PyPI version](https://img.shields.io/pypi/v/openhcs.svg)](https://pypi.org/project/openhcs/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/) [![GPU Accelerated](https://img.shields.io/badge/GPU-Accelerated-green.svg)](https://github.com/OpenHCSDev/OpenHCS) [![Documentation](https://readthedocs.org/projects/openhcs/badge/?version=latest)](https://openhcs.readthedocs.io) +[![Integration Tests](https://github.com/OpenHCSDev/OpenHCS/actions/workflows/integration-tests.yml/badge.svg?branch=main)](https://github.com/OpenHCSDev/OpenHCS/actions/workflows/integration-tests.yml) @@ -276,6 +277,21 @@ The GUI and execution services consume the same `list[FunctionStep]`, [API orientation](https://openhcs.readthedocs.io/en/latest/api/) for the explicit low-level execution call and progress lifecycle. +### Local agent quick start + +The optional MCP server projects those same typed declarations and compiler +contracts to local agent clients. Start with a bounded, compile-first request: + +> Inspect this Opera Phenix plate, infer its axes, and draft a nuclei +> segmentation plus per-cell intensity pipeline. Compile it and explain every +> validation result, but do not execute it. + +After reviewing the compiled plan, explicitly authorize a small run such as one +well and one site. Configure read and write roots before connecting a client; +OpenHCS does not provide a public hosted endpoint. See the +[MCP client guide](https://openhcs.readthedocs.io/en/latest/user_guide/mcp_clients.html) +for setup and trust boundaries. +
📦 All installation options @@ -399,17 +415,16 @@ A class-level registry tracks all active form managers. When a value changes in ## 🤝 Contributing -```bash -git clone --recurse-submodules https://github.com/OpenHCSDev/OpenHCS.git -cd OpenHCS -# Install the eight local packages as described in docs/development_setup.md, -# then install OpenHCS itself: -python -m pip install -e ".[dev,gui]" -OPENHCS_CPU_ONLY=1 python -m pytest tests/unit -``` +Start with [CONTRIBUTING.md](CONTRIBUTING.md). A shallow recursive clone is +recommended when you do not need the repository's historical media revisions. **Contribution areas**: microscope formats · processing functions · GPU backends · documentation +Please use the [issue tracker](https://github.com/OpenHCSDev/OpenHCS/issues) for +reproducible bugs and workflow requests. See [SECURITY.md](SECURITY.md) for +private vulnerability reporting and [CITATION.cff](CITATION.cff) for citation +metadata. + --- ## 📄 License diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..9e2ef6004 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,28 @@ +# Security Policy + +## Supported versions + +Security fixes are applied to the current `0.7.x` beta line. Older beta lines +may be asked to upgrade before a fix can be evaluated. + +## Reporting a vulnerability + +Do not open a public issue for a suspected vulnerability. Email +[tristan.simas@mail.mcgill.ca](mailto:tristan.simas@mail.mcgill.ca) with the +affected version, impact, minimal reproduction, and any suggested mitigation. +Please avoid sending real patient data, credentials, access tokens, or +proprietary microscopy datasets. You should receive an acknowledgment within +five business days. + +## MCP and desktop trust boundary + +The supported MCP server is local and uses explicit read and write roots. Its +tools can inspect data, author pipelines, start local execution, and write +outputs within the configured capabilities. Review a compiled plan before +execution, grant the smallest useful roots, and apply the privacy and retention +policy of the agent client and model provider you choose. OpenHCS does not run a +public hosted MCP endpoint. + +The current Windows and macOS beta installers are not code-signed or notarized. +Download them only from the official GitHub Release links and verify that the +resolved repository owner is `OpenHCSDev`. diff --git a/benchmark/contracts/dataset.py b/benchmark/contracts/dataset.py index b1b4d71ee..8baf9a907 100644 --- a/benchmark/contracts/dataset.py +++ b/benchmark/contracts/dataset.py @@ -93,7 +93,7 @@ class DatasetSpec: """Archive format.""" microscope_type: str - """Microscope handler type (e.g., 'bbbc021', 'bbbc038')""" + """Dataset source-family provenance; not necessarily a runtime handler key.""" validation_rule: DatasetValidationRule """How to validate extracted data.""" @@ -137,5 +137,6 @@ class AcquiredDataset: id: str path: Path microscope_type: str + """Dataset source-family provenance inherited from the dataset declaration.""" image_count: int metadata: dict diff --git a/docs/announcements/image_sc_openhcs_0_6_beta.md b/docs/announcements/image_sc_openhcs_0_6_beta.md deleted file mode 100644 index eaf12cfa1..000000000 --- a/docs/announcements/image_sc_openhcs_0_6_beta.md +++ /dev/null @@ -1,182 +0,0 @@ -# image.sc announcement: OpenHCS 0.6 beta - -Post only after every required item in the readiness checklist is checked -against the public release being announced. - -## Proposed title - -OpenHCS 0.6 beta: open high-content screening workflows in a GUI, Python, Napari/Fiji, and MCP - -## Ready-to-paste post - -Hello image.sc community, - -I am releasing the OpenHCS 0.6 beta and would value feedback from people who -build or maintain high-content microscopy workflows. - -OpenHCS is an open-source, Python-native platform for high-content screening. -Its desktop GUI, generated Python, headless runtime, CellProfiler importer, and -optional MCP server all use the same public `PipelineConfig` plus ordered -`FunctionStep` declarations. Pipelines are compiled before execution so source -matching, configuration, artifact routing, memory contracts, and worker -requirements can fail early rather than after a large screen has started. - -The beta currently includes: - -- visual pipeline authoring with bidirectional GUI ↔ Python editing; -- source bindings for plate fields such as well, site, channel, Z, and - timepoint; -- ImageXpress and Opera Phenix plate parsing, plus Bio-Formats and OMERO-backed - source/storage integration; the separate OMERO web panel remains - experimental; -- supported CellProfiler `.cppipe` import into ordinary OpenHCS declarations; -- live image and result streaming to Napari and Fiji/ImageJ; -- CPU multiprocessing, optional CUDA backends, and persisted custom Python - functions; -- typed images, object labels, measurements, relationships, tables, grids, - graphs, and metadata, with ROI and file materialization; -- an optional local MCP server so compatible agents can inspect the same - schemas, documentation, pipeline state, runtime progress, and viewer state. - -For CellProfiler interoperability, OpenHCS uses a source-backed corpus of 30 -example pipelines as end-to-end regression evidence. The current release -passes that Official30 suite under the documented equivalence policies. This -is not a claim that every historical CellProfiler module, plugin, version, and -setting is supported: unfamiliar pipelines should still be imported and -compiled to expose the exact compatibility boundary. - -### Try it - -For Windows or macOS, the landing page links directly to small graphical -installers; no ZIP extraction or existing Python installation is required: - -https://openhcsdev.github.io/openhcs/ - -For an existing Python 3.11–3.13 environment: - -```bash -python -m pip install "openhcs[gui,viz,bioformats,mcp,cellprofiler-compat]" -openhcs -``` - -The desktop installers are CPU-first and install CellProfiler compatibility, -Napari, Fiji/PyImageJ, Bio-Formats, and local MCP support. Fiji's Java -components are resolved on first use, and GPU packages are opt-in. Current -installer assets are unsigned, so Windows SmartScreen or macOS Gatekeeper may -ask you to confirm that you trust them. - -A five-minute interface demo is available here: - -https://openhcs.readthedocs.io/en/latest/_static/openhcs.mp4 - -Documentation: - -https://openhcs.readthedocs.io/ - -Source and issue tracker: - -https://github.com/OpenHCSDev/openhcs - -### Feedback I am looking for - -I would especially like to hear from people who have: - -- an ImageXpress or Opera Phenix plate with a nontrivial naming/layout pattern; -- an existing CellProfiler pipeline they would like to import; -- multi-site, multi-channel, Z-stack, or time-series assays; -- workflows that produce measurements, ROIs, or CellProfiler Analyst outputs; -- a need to move between visual editing, Python, interactive viewers, and - automated execution. - -If you are willing to try it, please reply with the microscope format, image -axes, biological goal, and expected outputs. I am also happy to help the first -few labs migrate one representative pipeline and use that experience to -improve onboarding. - -OpenHCS is MIT licensed. Thank you for any testing, criticism, or workflow -examples you can share. - -## Release readiness checklist - -The release is a **go** only when every required item is verified against the -same commit and public version. - -### 1. Source and release identity - -- [ ] `origin/main`, the annotated version tag, and the GitHub Release resolve - to the same commit. -- [ ] Package, Codex plugin, MCP bundle, and `server.json` metadata all project - the announced version from `openhcs.__version__`. -- [ ] The worktree contains no uncommitted production changes. -- [ ] The PyPI project classifier is `Development Status :: 4 - Beta`. - -### 2. Automated acceptance - -- [ ] The complete GitHub Integration Tests workflow passes for the tagged - commit. -- [ ] Official30 headless CellProfiler parity passes in that workflow. -- [ ] Python 3.11 and 3.13 boundary jobs pass on Linux, Windows, and macOS. -- [ ] Wheel installation, desktop installer, backend/microscope, and OMERO jobs - pass. -- [ ] Documentation and Website workflows pass. -- [ ] The persisted-custom-function spawn regression passes with two genuine - worker processes. -- [ ] The typed `run_plate` action is enabled during execution and dispatches - Stop; cancellation returns the manager to an actionable state. - -### 3. Public artifacts - -- [ ] PyPI exposes the exact announced wheel and sdist, neither yanked. -- [ ] The GitHub Release is public, non-draft, and marked latest. -- [ ] `OpenHCS-Windows-Installer.exe` and - `OpenHCS-macOS-Installer.dmg` are attached without ZIP wrapping. -- [ ] Both landing-page “latest/download” installer URLs return HTTP 200 and - resolve to the announced release. -- [ ] The landing page displays the announced version and does not describe - patch-level fixes as newly introduced platform features. -- [ ] The MCP Registry lists `io.github.OpenHCSDev/openhcs` at the announced - version, marks it latest, and pins - `openhcs[gui,mcp,viz]==`. - -### 4. Installation and first-use journeys - -- [ ] A clean Windows installer run completes, creates a desktop launcher, - launches OpenHCS, and registers detected supported MCP clients without - overwriting unrelated entries. -- [ ] A clean macOS DMG run completes the equivalent user-scoped installation - and launch journey. -- [ ] Installer progress, failure logs, startup splash output, updater, and - unsigned-installer warnings are accurate. -- [ ] A fresh PyPI environment can import the base package and launch the GUI - with the documented desktop extras. -- [ ] `openhcs-mcp-demo --json` completes its portable workflow, observes - nonzero data in Napari, and cleans up its allocated endpoints. - -### 5. Scientific workflow journeys - -- [ ] Add a representative plate, inspect source inventory/bindings, author or - import a pipeline, compile it, run it, and inspect materialized results. -- [ ] A supported `.cppipe` imports through the GUI and exposes ordinary - OpenHCS steps without a hidden CellProfiler runtime. -- [ ] Measurement rows retain biological coordinates and aggregate output - names do not imply site/channel coordinates that were aggregated away. -- [ ] Reusing an output directory cannot contaminate the current summary with - stale measurements from an older execution. -- [ ] Napari and Fiji receive correctly indexed payloads through their tested - viewer routes. -- [ ] A persisted custom function runs with at least two spawned workers, - materializes its declared outputs, cancels cleanly, and can be replaced - and rerun without restarting the GUI. - -### 6. Announcement and support - -- [ ] Every feature claim above has a documentation, test, or live-release - evidence boundary. -- [ ] The screenshot preview and five-minute demo URLs are public. -- [ ] Documentation, GitHub issues, PyPI, release, installer, and MCP Registry - links are public and current. -- [ ] The post states CPU-first/GPU opt-in and unsigned-installer boundaries. -- [ ] The requested feedback is specific enough for a biologist to respond - with microscope format, axes, assay goal, and expected outputs. -- [ ] The maintainer is prepared to answer installation and first-workflow - replies during the first several days after posting. diff --git a/docs/announcements/image_sc_openhcs_beta.md b/docs/announcements/image_sc_openhcs_beta.md new file mode 100644 index 000000000..87d065286 --- /dev/null +++ b/docs/announcements/image_sc_openhcs_beta.md @@ -0,0 +1,169 @@ +# image.sc announcement: OpenHCS beta + +Post only after every required item in the readiness checklist is verified +against the same public release. This draft intentionally does not freeze a +commit or name a not-yet-published patch release. + +## Proposed title + +OpenHCS beta: build and run typed HCS pipelines from the GUI, Python, or a local agent + +## Ready-to-paste post + +Hello image.sc community, + +I am preparing an OpenHCS beta announcement and would value feedback from +people who build or maintain high-content microscopy workflows. + +OpenHCS is an open-source, Python-native platform for turning a microscopy +plate into a compiled, reproducible analysis. You can author the same typed +pipeline in the desktop GUI, generated Python, or a local MCP-capable agent; +inspect it in Napari or Fiji; and import supported CellProfiler `.cppipe` +workflows into ordinary OpenHCS declarations. + +The key design choice is compile before execute. Source matching, +configuration, artifact routing, memory contracts, and worker requirements are +validated across the selected wells, sites, channels, Z planes, and timepoints +before expensive work starts. The GUI, Python API, executor, and MCP tools all +consume the same `PipelineConfig` and ordered `FunctionStep` declarations. + +The beta includes: + +- ImageXpress and Opera Phenix plate parsing, plus Bio-Formats and OMERO-backed + source/storage integration; +- visual pipeline authoring with bidirectional GUI to Python editing; +- CPU multiprocessing and opt-in CUDA/OpenCL processing backends; +- typed images, labels, measurements, relationships, tables, ROIs, and files; +- live, process-isolated streaming to Napari and Fiji/ImageJ; +- supported CellProfiler import with explicit compatibility reports; and +- a local stdio MCP server that projects the typed registry, authoring schema, + compiler, runtime progress, results, and viewer state to compatible clients. + +For CellProfiler interoperability, the automated suite runs a source-backed +corpus of 30 example pipelines under documented equivalence policies. Passing +that Official30 corpus is a concrete regression boundary, not a claim that +every historical CellProfiler plugin, version, or setting is supported. An +unfamiliar pipeline should be imported and compiled to expose its exact +compatibility boundary. + +### A bounded agent workflow + +After configuring explicit read and write roots, a useful first request is: + +> Inspect this Opera Phenix plate, infer its axes, and draft a nuclei +> segmentation plus per-cell intensity pipeline. Compile it and explain every +> validation result, but do not execute it. + +Review the compiled plan, then authorize a deliberately small run: + +> Run A01, site 1; stream the labels to Napari, save the measurements, and +> summarize the cell counts. + +The supported MCP server runs locally; OpenHCS does not provide a public hosted +endpoint. Tool inputs and outputs are also subject to the privacy and retention +policy of the agent client and model provider you choose. Treat data roots, +execution, and output writes as capabilities and grant only what the workflow +needs. + +### Try it + +Windows and macOS installers, verified workflow media, and the PyPI command are +on the landing page: + +https://openhcsdev.github.io/openhcs/ + +For an existing Python 3.11 to 3.13 environment: + +```bash +python -m pip install "openhcs[gui,viz,bioformats,mcp,cellprofiler-compat]" +openhcs +``` + +The desktop installers are CPU-first. Fiji's Java components are resolved on +first use and GPU packages are opt-in. Current beta installers are unsigned, so +Windows SmartScreen or macOS Gatekeeper may ask you to confirm that you trust +them. + +MCP client setup and trust boundaries: + +https://openhcs.readthedocs.io/en/latest/user_guide/mcp_clients.html + +Documentation and five-minute interface demo: + +https://openhcs.readthedocs.io/ + +https://openhcs.readthedocs.io/en/latest/_static/openhcs.mp4 + +Source and issue tracker: + +https://github.com/OpenHCSDev/OpenHCS + +### Feedback I am looking for + +I would especially like to hear from people with: + +- an ImageXpress or Opera Phenix plate with a nontrivial naming pattern; +- a CellProfiler pipeline they would like to import; +- multi-site, multi-channel, Z-stack, or time-series assays; +- workflows producing measurements, ROIs, relationships, or Analyst outputs; +- a need to move between visual editing, Python, viewers, and bounded agent + automation. + +If you try it, please share the microscope format, image axes, biological goal, +and expected outputs. A synthetic or redacted representative is enough. I am +also happy to help the first few labs migrate one representative pipeline and +use that experience to improve onboarding. + +OpenHCS is MIT licensed. Thank you for testing, criticism, and workflow +examples. + +## Release readiness checklist + +The announcement is a **go** only when every required item is verified against +the same tagged commit and public version. Checks below are deliberately left +open until the announcement release exists. + +### 1. Identity and automated acceptance + +- [ ] `main`, the annotated tag, PyPI, GitHub Release, plugin bundle, and MCP + Registry resolve to the same version and commit. +- [ ] The worktree is clean and the package classifier remains Beta. +- [ ] Integration Tests, Official30, documentation, and website workflows pass + for the tagged commit. +- [ ] Python 3.11 and 3.13 boundaries, wheel installation, native installers, + and backend/microscope jobs pass. + +### 2. Public artifacts and clean journeys + +- [ ] Wheel, sdist, direct installer assets, landing-page links, and registry + metadata are public and resolve to the announced release. +- [ ] A clean Windows installer launch, MCP demo, Napari nonzero-layer check, + and desktop shortcut check pass. +- [ ] A clean macOS installer launch, MCP demo, Napari smoke, and desktop app + check pass. +- [ ] A fresh PyPI environment imports and launches with the documented extras. +- [ ] Unsigned/notarized installer warnings remain explicit on the landing page + and release notes. + +### 3. Scientific and agent journeys + +- [ ] Add a representative plate, inspect bindings, author or import a + pipeline, compile, run, and inspect materialized results. +- [ ] Reusing an output directory replaces current-run outputs and cannot + contaminate measurement consolidation with stale execution data. +- [ ] Editing function code updates the live step declaration and saved + pipeline; moved cached paths fall back to a valid directory. +- [ ] Napari and Fiji receive correctly indexed payloads over platform-safe + transport. +- [ ] The two prompts above work through a fresh stdio client: the first cannot + execute, and the second is bounded to A01/site 1. + +### 4. Support boundary + +- [ ] Every claim above links to documentation, a focused test, or a public CI + result. +- [ ] Landing-page gallery media is captured from real UI state; no fabricated + or unverified session is presented as a demo. +- [ ] Issue, security, contribution, conduct, and citation paths are public. +- [ ] The maintainer can answer installation and first-workflow replies during + the first several days after posting. diff --git a/external/PolyStore b/external/PolyStore index a7db3f2c9..8b58fd1fa 160000 --- a/external/PolyStore +++ b/external/PolyStore @@ -1 +1 @@ -Subproject commit a7db3f2c9b8ee7b1f4b9c753a643cbaa7a591d3e +Subproject commit 8b58fd1fa90aef6472883b6e98f7b7c5f547defe diff --git a/external/pyqt-reactive b/external/pyqt-reactive index d44672685..c3dab7316 160000 --- a/external/pyqt-reactive +++ b/external/pyqt-reactive @@ -1 +1 @@ -Subproject commit d44672685543e043b3b3246404820568c7cdd2e5 +Subproject commit c3dab731695d21570df685e7f481f5ce5ef0c1cd diff --git a/external/zmqruntime b/external/zmqruntime index bf5fa8a73..058f0723f 160000 --- a/external/zmqruntime +++ b/external/zmqruntime @@ -1 +1 @@ -Subproject commit bf5fa8a73cbaa8685d8f8ce1785101e1c3206fc9 +Subproject commit 058f0723f53b2566c1d66399b09e4075f789fe37 diff --git a/openhcs/constants/constants.py b/openhcs/constants/constants.py index 30b401a74..76f4da447 100644 --- a/openhcs/constants/constants.py +++ b/openhcs/constants/constants.py @@ -22,8 +22,6 @@ class Microscope(Enum): OPENHCS = "openhcsdata" IMAGEXPRESS = "imagexpress" OPERAPHENIX = "opera_phenix" - BBBC021 = "bbbc021" - BBBC038 = "bbbc038" OMERO = "omero" # Added for OMERO virtual filesystem backend BIOFORMATS = "bioformats" SOURCE_BINDINGS = "source_bindings" diff --git a/openhcs/core/config.py b/openhcs/core/config.py index 56fd53162..1ad49df9f 100644 --- a/openhcs/core/config.py +++ b/openhcs/core/config.py @@ -180,10 +180,10 @@ class GlobalPipelineConfig(AnnotatedDataclassValidationMixin): # (GlobalPipelineConfig → PipelineConfig by removing "Global" prefix) class NapariDimensionMode(Enum): - """How to handle different dimensions in napari visualization.""" + """How component values are placed in Napari image layers.""" - SLICE = "slice" # Show as 2D slice (take middle slice) - STACK = "stack" # Show as 3D stack/volume + LAYER = "layer" # Create a separate Napari layer for each value + STACK = "stack" # Stack values along an axis in one Napari layer class NapariVariableSizeHandling(Enum): @@ -226,37 +226,33 @@ class NapariDisplayConfig( site_mode: NapariDimensionMode = field( default=NapariDimensionMode.STACK, metadata={ - "description": "Whether site values are stacked or reduced to a 2D slice." + "description": "Whether site values are stacked or use separate layers." }, ) channel_mode: NapariDimensionMode = field( default=NapariDimensionMode.STACK, metadata={ - "description": ( - "Whether channel values are stacked or reduced to a 2D slice." - ) + "description": "Whether channel values are stacked or use separate layers." }, ) z_index_mode: NapariDimensionMode = field( default=NapariDimensionMode.STACK, metadata={ - "description": ( - "Whether z-index values are stacked or reduced to a 2D slice." - ) + "description": "Whether z-index values are stacked or use separate layers." }, ) timepoint_mode: NapariDimensionMode = field( default=NapariDimensionMode.STACK, metadata={ "description": ( - "Whether timepoint values are stacked or reduced to a 2D slice." + "Whether timepoint values are stacked or use separate layers." ) }, ) well_mode: NapariDimensionMode = field( default=NapariDimensionMode.STACK, metadata={ - "description": "Whether well values are stacked or reduced to a 2D slice." + "description": "Whether well values are stacked or use separate layers." }, ) @@ -342,13 +338,13 @@ class FijiDimensionMode(Enum): ImageJ hyperstacks have 3 dimensions: Channels (C), Slices (Z), Frames (T). Each OpenHCS component (site, channel, z_index, timepoint) can be mapped to one of these. - - WINDOW: Create separate windows for each value (like Napari SLICE mode) + - WINDOW: Create separate windows for each value (like Napari LAYER mode) - CHANNEL: Map to ImageJ Channel dimension (C) - SLICE: Map to ImageJ Slice dimension (Z) - FRAME: Map to ImageJ Frame dimension (T) """ - WINDOW = "window" # Separate windows (like Napari SLICE mode) + WINDOW = "window" # Separate windows (like Napari LAYER mode) CHANNEL = "channel" # ImageJ Channel dimension (C) SLICE = "slice" # ImageJ Slice dimension (Z) FRAME = "frame" # ImageJ Frame dimension (T) diff --git a/openhcs/core/runtime_equivalence.py b/openhcs/core/runtime_equivalence.py index d80f2f3aa..a5d9a94ce 100644 --- a/openhcs/core/runtime_equivalence.py +++ b/openhcs/core/runtime_equivalence.py @@ -19,16 +19,16 @@ import openhcs.core.runtime_artifact_queries as runtime_artifact_queries import openhcs.core.measurement_feature_queries as measurement_feature_queries import openhcs.core.measurement_row_materialization as measurement_row_materialization -import openhcs.core.equivalence.cells as equivalence_cells -import openhcs.core.equivalence.keys as equivalence_keys -import openhcs.core.equivalence.measurement_facts as measurement_facts -import openhcs.core.equivalence.measurement_features as measurement_features -import openhcs.core.equivalence.measurement_requirements as measurement_requirements -import openhcs.core.equivalence.measurement_rows as equivalence_measurement_rows -import openhcs.core.equivalence.object_label_measurements as object_label_measurements -import openhcs.core.equivalence.policy as equivalence_policy -import openhcs.core.equivalence.relationships as equivalence_relationships -import openhcs.core.equivalence.tables as equivalence_tables +from openhcs.core.equivalence import cells as equivalence_cells +from openhcs.core.equivalence import keys as equivalence_keys +from openhcs.core.equivalence import measurement_facts +from openhcs.core.equivalence import measurement_features +from openhcs.core.equivalence import measurement_requirements +from openhcs.core.equivalence import measurement_rows as equivalence_measurement_rows +from openhcs.core.equivalence import object_label_measurements +from openhcs.core.equivalence import policy as equivalence_policy +from openhcs.core.equivalence import relationships as equivalence_relationships +from openhcs.core.equivalence import tables as equivalence_tables import openhcs.core.runtime_measurements as runtime_measurements from openhcs.core.artifacts import ( ArtifactType, diff --git a/openhcs/microscopes/bbbc.py b/openhcs/microscopes/bbbc.py deleted file mode 100644 index 204c300a7..000000000 --- a/openhcs/microscopes/bbbc.py +++ /dev/null @@ -1,618 +0,0 @@ -""" -BBBC (Broad Bioimage Benchmark Collection) microscope implementations. - -This module provides handlers for BBBC datasets in different formats: -- BBBC021: ImageXpress-like format with UUID, files in Week*/Week*_##### subdirectories -- BBBC038: Simple hex ID filenames in stage1_train/{ImageId}/images/ subdirectories - -Each dataset gets its own handler following the established MicroscopeHandler pattern. -""" - -import logging -import os -import re -from pathlib import Path -from typing import Any, ClassVar, Dict, List, Optional, Tuple, Union, Type - -from openhcs.constants.constants import Backend, Microscope -from openhcs.core.components.parser_metaprogramming import ( - format_filename_component, - require_filename_component, -) -from openhcs.microscopes.microscope_base import MicroscopeHandler -from openhcs.microscopes.microscope_interfaces import ( - DiskImageFileListingMetadataHandler, - FilenameParseResult, - FilenameParser, - MetadataHandler, -) -from openhcs.microscopes.tiff_metadata_mixin import TiffPixelSizeMixin -from polystore.exceptions import MetadataNotFoundError -from polystore.filemanager import FileManager -from polystore.virtual_workspace import SourcePixelRef - -logger = logging.getLogger(__name__) - - -class BBBCFilenameParser(FilenameParser): - """Shared BBBC parser shell for regex-based filename families.""" - - _pattern: ClassVar[re.Pattern[str]] - - def __init__(self, filemanager=None, pattern_format=None): - super().__init__() - self.filemanager = filemanager - self.pattern_format = pattern_format - - @classmethod - def can_parse(cls, filename: str) -> bool: - """Return whether the filename matches this BBBC parser family.""" - basename = Path(str(filename)).name - return cls._pattern.match(basename) is not None - - -class BBBCHandlerBase(MicroscopeHandler): - """Shared BBBC handler shell for parser/metadata wiring.""" - - _parser_class: ClassVar[Type[BBBCFilenameParser]] - _metadata_handler_class: ClassVar[Type[MetadataHandler]] - _microscope_type: ClassVar[str] - - def __init__(self, filemanager: FileManager, pattern_format: Optional[str] = None): - self.parser = self._parser_class(filemanager, pattern_format) - self.metadata_handler = self._metadata_handler_class(filemanager) - super().__init__(parser=self.parser, metadata_handler=self.metadata_handler) - - @property - def microscope_type(self) -> str: - return self._microscope_type - - @property - def metadata_handler_class(self) -> Type[MetadataHandler]: - return self._metadata_handler_class - - @property - def compatible_backends(self) -> List[Backend]: - return [Backend.DISK] - - -# ============================================================================ -# BBBC021 Handler (ImageXpress-like with UUID, in Week subfolders) -# ============================================================================ - -class BBBC021FilenameParser(BBBCFilenameParser): - """ - Parser for BBBC021 dataset filenames. - - Format: {Well}_s{Site}_w{Channel}{UUID}.tif - Example: G10_s1_w1BEDC2073-A983-4B98-95E9-84466707A25D.tif - - Components: - - Well: Alphanumeric plate coordinate (e.g., A01, G10, P24) - - Site: Numeric site/field ID (e.g., 1, 2, 3) - - Channel: Single digit channel ID (1=DAPI, 2=Tubulin, 4=Actin) - - UUID: Hex identifier with dashes (ignored for parsing, but part of filename) - - z_index: Not in filename, defaults to 1 - - timepoint: Not in filename, defaults to 1 - - Note: Channel 3 is not used in BBBC021 (only 1, 2, 4). - """ - - # Pattern matches both original and virtual workspace filenames: - # Original: G10_s1_w1{UUID}.tif - # Virtual: G10_s1_w1_z001_t001.tif - _pattern = re.compile( - r'^.*?' # Optional prefix (non-greedy) - r'([A-P][0-9]{2})' # Well: letter A-P + two digits - r'_s(\d+|\{[^\}]*\})' # Site: _s + digits or placeholder - r'_w(\d|\{[^\}]*\})' # Channel: _w + single digit or placeholder - r'(?:_z(\d+|\{[^\}]*\}))?' # Optional z - r'(?:_t(\d+|\{[^\}]*\}))?' # Optional timepoint - r'([A-F0-9-]*)' # Optional UUID - r'(\.\w+)$', # Extension - re.IGNORECASE - ) - - def parse_filename(self, filename: str) -> Optional[FilenameParseResult]: - """ - Parse BBBC021 filename into components. - - Args: - filename: Filename to parse - - Returns: - Dict with keys: well, site, channel, z_index, timepoint, extension - Or None if parsing fails - """ - basename = Path(str(filename)).name - match = self._pattern.match(basename) - - if not match: - logger.debug("Could not parse BBBC021 filename: %s", filename) - return None - - well, site_str, channel_str, z_str, t_str, uuid, ext = match.groups() - - def parse_component(value: str | None) -> int | None: - if not value or "{" in value: - return None - return int(value) - - return FilenameParseResult({ - 'well': well, - 'site': parse_component(site_str), - 'channel': parse_component(channel_str), - 'z_index': parse_component(z_str), - 'timepoint': parse_component(t_str), - 'extension': ext, - }) - - def extract_component_coordinates(self, component_value: str) -> Tuple[str, str]: - """ - Extract row/column from well identifier. - - Args: - component_value: Well like 'A01', 'G10', etc. - - Returns: - (row, column) tuple like ('A', '01'), ('G', '10') - """ - if not component_value or len(component_value) < 2: - raise ValueError(f"Invalid well format: {component_value}") - - row = component_value[0] # First character (letter) - col = component_value[1:] # Remaining digits - - if not row.isalpha() or not col.isdigit(): - raise ValueError(f"Invalid BBBC021 well format: {component_value}. Expected format like 'A01', 'G10'") - - return (row, col) - - def construct_filename( - self, - extension: str = '.tif', - site_padding: int = 1, # BBBC021 uses single digits for sites - z_padding: int = 3, - timepoint_padding: int = 3, - **component_values - ) -> str: - """ - Construct BBBC021 filename from components for virtual workspace. - - Note: UUID is NOT reconstructed. Virtual workspace filenames include - ALL components (z_index, timepoint) even if not in original filenames. - This ensures consistent pattern discovery. - - Args: - well: Well ID (e.g., 'A01', 'G10') - site: Site number - channel: Channel number - z_index: Z-index (defaults to 1) - timepoint: Timepoint (defaults to 1) - extension: File extension - **component_values: Other component values - - Returns: - Filename: {Well}_s{Site}_w{Channel}_z{Z}_t{T}.tif - """ - well = require_filename_component(component_values, 'well') - site = require_filename_component(component_values, 'site') - channel = require_filename_component(component_values, 'channel') - z_index = require_filename_component(component_values, 'z_index') - timepoint = require_filename_component(component_values, 'timepoint') - - # Build filename parts - parts = [well] - - # Site - parts.append(f"_s{format_filename_component(site, site_padding)}") - - # Channel (no padding) - parts.append(f"_w{format_filename_component(channel)}") - - # Z-index (ALWAYS include for virtual workspace) - parts.append(f"_z{format_filename_component(z_index, z_padding)}") - - # Timepoint (ALWAYS include for virtual workspace) - parts.append(f"_t{format_filename_component(timepoint, timepoint_padding)}") - - return "".join(parts) + extension - - -class BBBCSinglePlaneMetadataHandler(DiskImageFileListingMetadataHandler): - """Shared metadata defaults for BBBC single-plane image collections.""" - - def get_grid_dimensions(self, plate_path: Union[str, Path]) -> Tuple[int, int]: - return (1, 1) - - def get_well_values(self, plate_path: Union[str, Path]) -> Optional[Dict[str, Optional[str]]]: - return None - - def get_site_values(self, plate_path: Union[str, Path]) -> Optional[Dict[str, Optional[str]]]: - return None - - def get_z_index_values(self, plate_path: Union[str, Path]) -> Optional[Dict[str, Optional[str]]]: - return None - - def get_timepoint_values(self, plate_path: Union[str, Path]) -> Optional[Dict[str, Optional[str]]]: - return None - - -class BBBC021MetadataHandler(TiffPixelSizeMixin, BBBCSinglePlaneMetadataHandler): - """ - Metadata handler for BBBC021 dataset. - - BBBC021 public mirror ships only TIFFs; we extract metadata from TIFF tags. - """ - - def __init__(self, filemanager: FileManager): - super().__init__() - self.filemanager = filemanager - - def find_metadata_file(self, plate_path: Union[str, Path]) -> Path: - """ - BBBC021 ship we have contains no separate metadata files; rely solely on TIFFs. - Ensure caller pointed at the expected plate directory. - """ - plate_path = Path(plate_path) - if plate_path.name != "Week1_22123": - raise MetadataNotFoundError( - f"BBBC021 plate must be the Week1_22123 directory, got '{plate_path.name}'" - ) - return plate_path - - def get_pixel_size(self, plate_path: Union[str, Path]) -> float: - return self._pixel_size_from_tiff(plate_path, self.filemanager) - - def get_channel_values(self, plate_path: Union[str, Path]) -> Optional[Dict[str, Optional[str]]]: - # Derive channel names from TIFF tag (if present). May return {'1': 'DAPI'} etc. - return self._channel_from_tiff(plate_path, self.filemanager) - - -class BBBC021Handler(BBBCHandlerBase): - """ - Microscope handler for BBBC021 dataset. - - BBBC021: Human MCF7 cells from compound profiling experiment. - Format: ImageXpress-like with {Well}_s{Site}_w{Channel}{UUID}.tif pattern. - Files are in Week#/Week#_#####/ subdirectories. - """ - - _microscope_type = Microscope.BBBC021.value - _parser_class = BBBC021FilenameParser - _metadata_handler_class = BBBC021MetadataHandler - - @classmethod - def detect(cls, plate_folder: Path, filemanager: FileManager) -> bool: - """ - Detect via metadata CSV first, else via filename parser match. - """ - plate_folder = Path(plate_folder) - # Filename signal only (no external metadata shipped) - try: - files = filemanager.list_files(plate_folder, Backend.DISK.value, recursive=True) - parser = BBBC021FilenameParser() - for f in files: - name = Path(f).name - if name.lower().endswith((".tif", ".tiff")) and parser.can_parse(name): - return True - except Exception: - return False - return False - - @property - def root_dir(self) -> str: - """ - BBBC021 virtual workspace is at plate root. - - Files are physically in Week#/Week#_##### subdirectories, - but virtually flattened to plate root. - """ - return "." - - def _build_virtual_mapping(self, plate_path: Path, filemanager: FileManager) -> Path: - """ - Build virtual workspace mapping for BBBC021. - - Flattens Week#/Week#_##### subdirectory structure to plate root, - and adds missing z_index and timepoint components to filenames. - - Args: - plate_path: Path to plate directory - filemanager: FileManager instance - - Returns: - Path to plate root - """ - plate_path = Path(plate_path) - - logger.info(f"🔄 BUILDING VIRTUAL MAPPING: BBBC021 folder flattening for {plate_path}") - - # Initialize mapping dict (PLATE-RELATIVE paths) - workspace_mapping = {} - - # Recursively find all .tif files - image_files = filemanager.list_image_files(plate_path, Backend.DISK.value, recursive=True) - - for file_path in image_files: - # Get filename - if isinstance(file_path, str): - filename = os.path.basename(file_path) - elif isinstance(file_path, Path): - filename = file_path.name - else: - continue - - # Parse original filename - metadata = self.parser.parse_filename(filename) - if not metadata: - logger.warning(f"Could not parse BBBC021 filename: {filename}") - continue - - # Add default z_index and timepoint (missing from original filenames) - if metadata['z_index'] is None: - metadata['z_index'] = 1 - if metadata['timepoint'] is None: - metadata['timepoint'] = 1 - - # Reconstruct filename with all components (standardized) - new_filename = self.parser.construct_filename(**metadata) - - # Build PLATE-RELATIVE virtual path (at plate root) - virtual_relative = new_filename - - # Build PLATE-RELATIVE real path (in subfolder) - real_relative = Path(file_path).relative_to(plate_path).as_posix() - - # Add to mapping - workspace_mapping[virtual_relative] = SourcePixelRef( - backend=Backend.DISK.value, - backend_address=real_relative, - ) - logger.debug(f" Mapped: {virtual_relative} → {real_relative}") - - logger.info(f"Built {len(workspace_mapping)} virtual path mappings for BBBC021") - - # Save virtual workspace mapping - self.save_virtual_workspace_metadata(plate_path, workspace_mapping) - - return plate_path - - -# ============================================================================ -# BBBC038 Handler (Kaggle Nuclei - Hex ID Format) -# ============================================================================ - -class BBBC038FilenameParser(BBBCFilenameParser): - """ - Parser for BBBC038 dataset (Kaggle 2018 Data Science Bowl). - - Format: {HexID}.png - Example: 0a7e06cd488667b8fe53a1521d88ab3f4e8d8a05b5663e89dc5df7b02ca93f38.png - - BBBC038 uses simple hex string identifiers as filenames. - Each ImageId represents a unique image (treated as a unique "well"). - - Organization: stage1_train/{ImageId}/images/{ImageId}.png - Parser only sees the filename, not the full path structure. - """ - - # Pattern: hex string + .png extension - _pattern = re.compile(r'^([a-f0-9]+)\.png$', re.IGNORECASE) - - def parse_filename(self, filename: str) -> Optional[FilenameParseResult]: - """ - Parse BBBC038 filename into components. - - Args: - filename: Filename to parse - - Returns: - Dict with well=ImageId, site/channel/z all fixed at 1 - Or None if parsing fails - """ - basename = Path(str(filename)).name - match = self._pattern.match(basename) - - if not match: - logger.debug("Could not parse BBBC038 filename: %s", filename) - return None - - image_id = match.group(1) - - return FilenameParseResult({ - 'well': image_id, # ImageId is the well identifier - 'site': 1, # Single image per ID - 'channel': 1, # Single channel (nuclei stain) - 'z_index': None, # No Z-stacks, will default to 1 - 'timepoint': None, # No timepoints, will default to 1 - 'extension': '.png', - }) - - def extract_component_coordinates(self, component_value: str) -> Tuple[str, str]: - """ - Extract coordinates from ImageId. - - BBBC038 has no spatial grid layout - ImageIds are arbitrary identifiers. - Split the hex string for display purposes only. - - Args: - component_value: ImageId (hex string) - - Returns: - (first_half, second_half) of the hex ID - """ - if not component_value: - raise ValueError("Invalid ImageId: empty") - - mid = len(component_value) // 2 - return (component_value[:mid], component_value[mid:]) - - def construct_filename( - self, - extension: str = '.png', - **component_values - ) -> str: - """ - Construct BBBC038 filename from components. - - Args: - well: ImageId (hex string) - extension: File extension - **component_values: Other components (ignored) - - Returns: - Filename string: {ImageId}.png - """ - image_id = require_filename_component(component_values, 'well') - return f"{image_id}{extension}" - - -class BBBC038MetadataHandler(BBBCSinglePlaneMetadataHandler): - """ - Metadata handler for BBBC038 (Kaggle nuclei dataset). - - Metadata comes from: - - metadata.xlsx - - stage1_train_labels.csv (run-length encoded masks) - - stage1_solution.csv (evaluation metrics) - """ - - def __init__(self, filemanager: FileManager): - super().__init__() - self.filemanager = filemanager - - def find_metadata_file(self, plate_path: Union[str, Path]) -> Path: - """Find metadata.xlsx or stage1_train_labels.csv.""" - plate_path = Path(plate_path) - - candidates = [ - plate_path / "metadata.xlsx", - plate_path / "stage1_train_labels.csv", - plate_path.parent / "metadata.xlsx", - plate_path.parent / "stage1_train_labels.csv", - ] - - for candidate in candidates: - if candidate.exists(): - return candidate - - raise MetadataNotFoundError( - f"BBBC038 metadata not found in {plate_path}. " - "Download from https://data.broadinstitute.org/bbbc/BBBC038/" - ) - - def get_pixel_size(self, plate_path: Union[str, Path]) -> float: - """BBBC038 pixel size varies across different imaging conditions.""" - return 1.0 # No standard pixel size (diverse sources) - - def get_channel_values(self, plate_path: Union[str, Path]) -> Optional[Dict[str, Optional[str]]]: - """BBBC038 is single-channel (nuclei stain).""" - return {"1": "Nuclei"} - - -class BBBC038Handler(BBBCHandlerBase): - """ - Microscope handler for BBBC038 dataset (Kaggle nuclei, PNG format). - - BBBC038: Nuclei from diverse organisms and imaging conditions. - Format: {HexID}.png in stage1_train/{ImageId}/images/ subdirectories. - """ - - _microscope_type = Microscope.BBBC038.value - _parser_class = BBBC038FilenameParser - _metadata_handler_class = BBBC038MetadataHandler - - @classmethod - def detect(cls, plate_folder: Path, filemanager: FileManager) -> bool: - """ - Detect BBBC038 by presence of stage1_train folder with PNGs. - """ - stage1 = Path(plate_folder) / "stage1_train" - if not stage1.exists(): - return False - try: - files = filemanager.list_files(stage1, Backend.DISK.value, pattern="*.png", recursive=True) - return len(files) > 0 - except Exception: - return False - - @property - def root_dir(self) -> str: - """ - BBBC038 virtual workspace is at stage1_train directory. - - Images are in stage1_train/{ImageId}/images/ subdirectories. - """ - return "stage1_train" - - def _build_virtual_mapping(self, plate_path: Path, filemanager: FileManager) -> Path: - """ - Build virtual workspace mapping for BBBC038. - - Flattens stage1_train/{ImageId}/images/ structure. - Since filenames are already unique (ImageId), just flatten to stage1_train/. - - Args: - plate_path: Path to plate directory (contains stage1_train/) - filemanager: FileManager instance - - Returns: - Path to stage1_train directory - """ - plate_path = Path(plate_path) - stage1_path = plate_path / "stage1_train" - - if not stage1_path.exists(): - logger.warning(f"stage1_train directory not found in {plate_path}") - return plate_path - - logger.info(f"🔄 BUILDING VIRTUAL MAPPING: BBBC038 folder flattening for {plate_path}") - - # Initialize mapping dict (PLATE-RELATIVE paths) - workspace_mapping = {} - - # Find all .png files in images/ subdirectories - image_files = filemanager.list_image_files(stage1_path, Backend.DISK.value, recursive=True) - - for file_path in image_files: - # Only process files in images/ subdirectories (skip masks/) - if '/images/' not in str(file_path): - continue - - # Get filename - if isinstance(file_path, str): - filename = os.path.basename(file_path) - elif isinstance(file_path, Path): - filename = file_path.name - else: - continue - - # Parse filename - metadata = self.parser.parse_filename(filename) - if not metadata: - logger.warning(f"Could not parse BBBC038 filename: {filename}") - continue - - # Filename is already correct (ImageId.png) - # Just flatten to stage1_train/ directory - - # Build PLATE-RELATIVE virtual path (in stage1_train/) - virtual_relative = (Path("stage1_train") / filename).as_posix() - - # Build PLATE-RELATIVE real path (in stage1_train/{ImageId}/images/) - real_relative = Path(file_path).relative_to(plate_path).as_posix() - - # Add to mapping - workspace_mapping[virtual_relative] = SourcePixelRef( - backend=Backend.DISK.value, - backend_address=real_relative, - ) - logger.debug(f" Mapped: {virtual_relative} → {real_relative}") - - logger.info(f"Built {len(workspace_mapping)} virtual path mappings for BBBC038") - - # Save virtual workspace mapping - self.save_virtual_workspace_metadata(plate_path, workspace_mapping) - - return stage1_path diff --git a/openhcs/runtime/napari_viewer_server.py b/openhcs/runtime/napari_viewer_server.py index 3dc4c3078..66b00dbab 100644 --- a/openhcs/runtime/napari_viewer_server.py +++ b/openhcs/runtime/napari_viewer_server.py @@ -892,12 +892,12 @@ def routing_context( context="Napari separate-layer routing", ) if well_component in context.layout.components_for_mode( - ViewerComponentMode.SLICE + ViewerComponentMode.LAYER ): return context component_modes = dict(context.layout.component_modes) - component_modes[well_component] = ViewerComponentMode.SLICE.value + component_modes[well_component] = ViewerComponentMode.LAYER.value return replace( context, layout=ViewerComponentLayout.from_parts( @@ -1254,7 +1254,7 @@ def _build_nd_image_array( class NapariLayerTitleAuthority: - """Build visible layer titles from producer identity and real slice axes.""" + """Build visible titles from producer identity and layer-routed components.""" @classmethod def title( @@ -1268,7 +1268,7 @@ def title( ) -> str: parts = [StreamProducerDisplayNameAuthority.output_label(producer)] for component in component_layout.components_for_mode( - ViewerComponentMode.SLICE + ViewerComponentMode.LAYER ): value = ViewerComponentCoordinateAuthority.required_value( component_info, diff --git a/scripts/build_installer_source_wheelhouse.py b/scripts/build_installer_source_wheelhouse.py new file mode 100644 index 000000000..899177924 --- /dev/null +++ b/scripts/build_installer_source_wheelhouse.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +"""Build the candidate wheels consumed by installer source tests. + +Project metadata remains the package authority. Submodule mode discovers +first-party dependency candidates from ``external/*/pyproject.toml`` instead +of maintaining a second package list in the workflow. +""" + +from __future__ import annotations + +import argparse +from collections.abc import Callable, Sequence +from pathlib import Path +import subprocess +import sys + +from scripts.validate_local_release_floors import discover_local_projects + + +REPO_ROOT = Path(__file__).resolve().parents[1] +BuildRunner = Callable[[Sequence[str]], None] + + +def source_projects( + repo_root: Path, + dependency_source: str, +) -> tuple[Path, ...]: + """Return source trees whose wheels belong in the installer wheelhouse.""" + + projects = [repo_root] + if dependency_source == "submodules": + projects.extend( + project.path.parent for project in discover_local_projects(repo_root) + ) + return tuple(projects) + + +def _run_build(command: Sequence[str]) -> None: + subprocess.run(command, check=True) + + +def build_wheelhouse( + output_directory: Path, + dependency_source: str, + *, + repo_root: Path = REPO_ROOT, + runner: BuildRunner = _run_build, +) -> tuple[Path, ...]: + """Build the root candidate and any metadata-discovered local dependencies.""" + + output_directory.mkdir(parents=True, exist_ok=True) + projects = source_projects(repo_root, dependency_source) + for project in projects: + runner( + ( + sys.executable, + "-m", + "build", + "--wheel", + "--outdir", + str(output_directory), + str(project), + ) + ) + return projects + + +def main(argv: Sequence[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument( + "--dependency-source", + choices=("submodules", "pypi"), + required=True, + ) + args = parser.parse_args(argv) + build_wheelhouse(args.output, args.dependency_source) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/installer/test_windows_simple_installer.py b/tests/installer/test_windows_simple_installer.py index be304072c..fb4c82344 100644 --- a/tests/installer/test_windows_simple_installer.py +++ b/tests/installer/test_windows_simple_installer.py @@ -553,6 +553,21 @@ def test_windows_installer_ci_has_an_absolute_safety_ceiling() -> None: assert "$installerProcess.ExitCode" in smoke_step +def test_desktop_installer_source_ci_builds_declared_dependency_candidates() -> None: + workflow = INTEGRATION_WORKFLOW_PATH.read_text(encoding="utf-8") + desktop_job = workflow[ + workflow.index(" desktop-installer-source-test:") : workflow.index( + " pypi-dependency-readiness:" + ) + ] + + assert "submodules: ${{ env.OPENHCS_CI_DEP_SOURCE" in desktop_job + assert "python -m scripts.build_installer_source_wheelhouse" in desktop_job + assert '--dependency-source "${{ env.OPENHCS_CI_DEP_SOURCE }}"' in desktop_job + assert '$env:UV_FIND_LINKS = (Resolve-Path "dist").Path' in desktop_job + assert 'export UV_FIND_LINKS="$GITHUB_WORKSPACE/dist"' in desktop_job + + def test_windows_installer_ci_exercises_long_path_update_cleanup() -> None: workflow = INTEGRATION_WORKFLOW_PATH.read_text(encoding="utf-8") smoke_step = workflow[ diff --git a/tests/integration/test_cellprofiler_generated_pipeline.py b/tests/integration/test_cellprofiler_generated_pipeline.py index 1be32f3d8..f8ccba4a7 100644 --- a/tests/integration/test_cellprofiler_generated_pipeline.py +++ b/tests/integration/test_cellprofiler_generated_pipeline.py @@ -477,7 +477,6 @@ def test_bbbc021_cppipe_executes_named_channel_bindings_through_zmq( tmp_path, cppipe_path=cppipe_path, source_root=plate_path, - microscope=Microscope.BBBC021, ) nuclei_records = _runtime_records( @@ -517,7 +516,6 @@ def test_bbbc021_canonical_illum_cppipe_materializes_declared_images_through_zmq tmp_path, cppipe_path=cppipe_path, source_root=plate_path, - microscope=Microscope.BBBC021, ) image_names = {path.name for path in export.exports.image_outputs} diff --git a/tests/unit/pyqt_gui/test_dual_editor_session.py b/tests/unit/pyqt_gui/test_dual_editor_session.py new file mode 100644 index 000000000..4ec713284 --- /dev/null +++ b/tests/unit/pyqt_gui/test_dual_editor_session.py @@ -0,0 +1,49 @@ +"""Regression tests for dual-editor declaration synchronization.""" + +from __future__ import annotations + +from types import SimpleNamespace + +from objectstate import ObjectState, ObjectStateRegistry + +from openhcs.core.steps.function_step import FunctionStep +from openhcs.pyqt_gui.windows.dual_editor_session import ( + DualEditorFunctionPatternController, + DualEditorSession, +) + + +def _original(image): + return image + + +def _replacement(image): + return image + + +def test_saved_function_pattern_updates_step_object_state() -> None: + """Saving edited code must update the step declaration used by the pipeline.""" + + ObjectStateRegistry.clear() + editing_step = FunctionStep(func=_original, name="edited step") + state = ObjectState(editing_step, scope_id="plate::functionstep_0") + ObjectStateRegistry.register(state, _skip_snapshot=True) + session = DualEditorSession( + editing_step=editing_step, + step_editor=SimpleNamespace(state=state), + func_editor=SimpleNamespace(current_pattern=_replacement), + ) + changes: list[str] = [] + controller = DualEditorFunctionPatternController( + session=session, + detect_changes=lambda: changes.append("changed"), + invalidate_artifact_plan=lambda: changes.append("invalidated"), + ) + + try: + controller.handle_change() + + assert session.object_session().to_object(update_delegate=False).func is _replacement + assert changes == ["changed", "invalidated"] + finally: + ObjectStateRegistry.clear() diff --git a/tests/unit/test_build_installer_source_wheelhouse.py b/tests/unit/test_build_installer_source_wheelhouse.py new file mode 100644 index 000000000..1da3831ba --- /dev/null +++ b/tests/unit/test_build_installer_source_wheelhouse.py @@ -0,0 +1,61 @@ +"""Tests for the installer source wheelhouse projection.""" + +from pathlib import Path +from types import SimpleNamespace + +from scripts import build_installer_source_wheelhouse as wheelhouse + + +def test_pypi_mode_builds_only_the_openhcs_candidate(tmp_path: Path) -> None: + output_directory = tmp_path / "wheelhouse" + commands: list[tuple[str, ...]] = [] + + projects = wheelhouse.build_wheelhouse( + output_directory, + "pypi", + repo_root=tmp_path, + runner=lambda command: commands.append(tuple(command)), + ) + + assert projects == (tmp_path,) + assert output_directory.is_dir() + assert len(commands) == 1 + assert commands[0][-1] == str(tmp_path) + + +def test_submodule_mode_derives_dependency_candidates_from_project_metadata( + monkeypatch, + tmp_path: Path, +) -> None: + output_directory = tmp_path / "wheelhouse" + dependency_paths = ( + tmp_path / "external" / "PolyStore" / "pyproject.toml", + tmp_path / "external" / "zmqruntime" / "pyproject.toml", + ) + monkeypatch.setattr( + wheelhouse, + "discover_local_projects", + lambda repo_root: tuple( + SimpleNamespace(path=path) for path in dependency_paths + ), + ) + commands: list[tuple[str, ...]] = [] + + projects = wheelhouse.build_wheelhouse( + output_directory, + "submodules", + repo_root=tmp_path, + runner=lambda command: commands.append(tuple(command)), + ) + + assert projects == ( + tmp_path, + dependency_paths[0].parent, + dependency_paths[1].parent, + ) + assert [command[-1] for command in commands] == [ + str(project) for project in projects + ] + assert all( + command[-3:-1] == ("--outdir", str(output_directory)) for command in commands + ) diff --git a/tests/unit/test_build_website.py b/tests/unit/test_build_website.py index 8e2da5dc0..031ad2624 100644 --- a/tests/unit/test_build_website.py +++ b/tests/unit/test_build_website.py @@ -196,7 +196,10 @@ def test_shipping_copy_projects_current_release_and_keeps_boundaries_explicit( assert "https://openhcs.readthedocs.io/en/latest/" in html assert "https://openhcs.readthedocs.io/en/latest/api/" in html assert ">Install local MCP" in html - assert "https://github.com/OpenHCSDev/OpenHCS/releases" in html + assert "https://openhcs.readthedocs.io/en/latest/user_guide/mcp_clients.html" in html + assert "Compile and explain it" in html + assert "but do not execute" in html + assert "Run A01, site 1" in html assert 'id="gallery"' in html assert 'src="assets/gallery/' in html assert "CellProfiler" in html and package_version in html diff --git a/tests/unit/test_file_materialization_options.py b/tests/unit/test_file_materialization_options.py index 754a8f047..35fb1dfd9 100644 --- a/tests/unit/test_file_materialization_options.py +++ b/tests/unit/test_file_materialization_options.py @@ -168,6 +168,25 @@ def test_materialization_spec_error_write_mode_refuses_existing_path(tmp_path) - assert output_path.read_bytes() == b"existing" +def test_default_write_mode_replaces_existing_memory_artifact() -> None: + """Re-running a pipeline replaces its prior in-memory output by default.""" + + _image_options, bundle_options = _option_types() + filemanager = FileManager({"memory": MemoryStorageBackend()}) + spec = MaterializationSpec(bundle_options()) + + for contents in (b"first run", b"second run"): + materialize( + spec, + data={"report.txt": contents}, + path="/analysis/ExportBundle.pkl", + filemanager=filemanager, + backends=("memory",), + ) + + assert filemanager.load("/analysis/report.txt", "memory") == b"second run" + + @pytest.mark.parametrize( "bundle", ( diff --git a/tests/unit/test_microscope_handler_identity.py b/tests/unit/test_microscope_handler_identity.py index aba4031df..e93807bd5 100644 --- a/tests/unit/test_microscope_handler_identity.py +++ b/tests/unit/test_microscope_handler_identity.py @@ -58,7 +58,7 @@ def test_typed_microscope_values_are_exact_registered_handler_keys() -> None: if microscope is not Microscope.AUTO } - assert configured_types <= handler_types + assert configured_types == handler_types assert { handler_type._microscope_type for handler_type in MicroscopeHandler.__registry__.values() diff --git a/tests/unit/test_napari_streaming_handlers.py b/tests/unit/test_napari_streaming_handlers.py index a380840ec..82e6704ef 100644 --- a/tests/unit/test_napari_streaming_handlers.py +++ b/tests/unit/test_napari_streaming_handlers.py @@ -3268,7 +3268,7 @@ def test_napari_layer_title_authority_uses_stream_display_name_policy(): artifact_kind="object_labels", ) component_layout = ViewerComponentLayout( - component_modes={"well": "slice", "channel": "stack"}, + component_modes={"well": "layer", "channel": "stack"}, component_order=["well", "channel"], ) @@ -3530,7 +3530,7 @@ def _stream(config: NapariDisplayConfig): ) assert len(separate_server.component_groups) == 2 assert { - route.layout.components_for_mode("slice") + route.layout.components_for_mode("layer") for _, _, route in separate_server.display_pipeline.scheduled } == {("well",)} assert sorted( diff --git a/tests/unit/test_path_cache.py b/tests/unit/test_path_cache.py new file mode 100644 index 000000000..669254d69 --- /dev/null +++ b/tests/unit/test_path_cache.py @@ -0,0 +1,25 @@ +"""Regression tests for persisted UI path selection.""" + +from __future__ import annotations + +import json + +from openhcs.core.path_cache import PathCacheKey, UnifiedPathCache + + +def test_stale_pipeline_path_is_removed_and_fallback_is_used(tmp_path) -> None: + """A moved pipeline cannot strand the next file dialog on a dead path.""" + + cache_file = tmp_path / "path_cache.json" + moved_path = tmp_path / "moved-away" + fallback = tmp_path / "pipelines" + fallback.mkdir() + cache_file.write_text( + json.dumps({PathCacheKey.PIPELINE_FILES.value: str(moved_path)}), + encoding="utf-8", + ) + + cache = UnifiedPathCache(cache_file) + + assert cache.get_initial_path(PathCacheKey.PIPELINE_FILES, fallback) == fallback + assert json.loads(cache_file.read_text(encoding="utf-8")) == {} diff --git a/tests/unit/test_runtime_equivalence_package.py b/tests/unit/test_runtime_equivalence_package.py index a2bb45abe..154d7d6fe 100644 --- a/tests/unit/test_runtime_equivalence_package.py +++ b/tests/unit/test_runtime_equivalence_package.py @@ -3,6 +3,8 @@ import ast from collections import Counter from pathlib import Path +import subprocess +import sys from unittest.mock import patch import numpy as np @@ -43,6 +45,41 @@ PROJECT_ROOT = Path(__file__).parents[2] +def test_runtime_equivalence_imports_owned_package_in_a_cold_process() -> None: + completed = subprocess.run( + [ + sys.executable, + "-c", + ( + "import sys; import openhcs.core; " + "assert 'openhcs.core.equivalence' not in sys.modules; " + "import openhcs.core.runtime_equivalence" + ), + ], + cwd=PROJECT_ROOT, + capture_output=True, + text=True, + check=False, + ) + + assert completed.returncode == 0, completed.stderr + + +def test_runtime_equivalence_does_not_depend_on_deep_alias_import_side_effects() -> ( + None +): + source_path = PROJECT_ROOT / "openhcs/core/runtime_equivalence.py" + tree = ast.parse(source_path.read_text(encoding="utf-8"), filename=str(source_path)) + + assert not [ + alias.name + for node in ast.walk(tree) + if isinstance(node, ast.Import) + for alias in node.names + if alias.name.startswith("openhcs.core.equivalence.") + ] + + def test_runtime_equivalence_report_types_have_package_owner() -> None: assert runtime_equivalence.RuntimeEquivalenceDifference is ( RuntimeEquivalenceDifference diff --git a/tests/unit/test_source_binding_workspace.py b/tests/unit/test_source_binding_workspace.py index 800dce234..5113f1ac1 100644 --- a/tests/unit/test_source_binding_workspace.py +++ b/tests/unit/test_source_binding_workspace.py @@ -391,7 +391,7 @@ def test_nonempty_source_bindings_override_physical_source_microscope( tmp_path, ): handler = create_microscope_handler( - microscope_type=Microscope.BBBC021.value, + microscope_type=Microscope.IMAGEXPRESS.value, plate_folder=tmp_path, filemanager=_filemanager(), source_bindings_config=SourceBindingsConfig( diff --git a/tests/unit/test_streaming_service.py b/tests/unit/test_streaming_service.py index ca10cea63..0623cb4d2 100644 --- a/tests/unit/test_streaming_service.py +++ b/tests/unit/test_streaming_service.py @@ -128,6 +128,14 @@ def test_streaming_config_component_modes_apply_display_defaults() -> None: } +def test_napari_dimension_modes_distinguish_layers_from_slices() -> None: + assert tuple(NapariDimensionMode) == ( + NapariDimensionMode.LAYER, + NapariDimensionMode.STACK, + ) + assert NapariDimensionMode.LAYER.value == "layer" + + def test_stream_images_uses_resolved_config_backend_not_viewer_name( monkeypatch, ) -> None: diff --git a/website/index.html b/website/index.html index 3728e6a8f..48d8927a3 100644 --- a/website/index.html +++ b/website/index.html @@ -475,7 +475,7 @@

Agent access to pipeline and runtime state.

@@ -487,7 +487,9 @@

Agent access to pipeline and runtime state.

- Load this plate, group by well, segment nuclei, and report intensity. + Inspect this Opera Phenix plate, infer its axes, and draft a nuclei + segmentation plus per-cell intensity pipeline. Compile and explain it, + but do not execute.
OPENHCS @@ -498,8 +500,8 @@

Agent access to pipeline and runtime state.

openhcs_create_config draft config
- Installer users: connect once, restart the agent, and ask - Use OpenHCS to inspect this microscopy plate. + Then review the plan and explicitly authorize a bounded run + Run A01, site 1; stream labels and save measurements.