Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
79 commits
Select commit Hold shift + click to select a range
6cec6f0
Pixel calibration: per-detector, pending apply flow
beniroquai May 1, 2026
68ec081
Add objective-aware pixel calibration
beniroquai May 1, 2026
b312860
Use protocol pixel_size for ashlar, tidy imports
beniroquai May 2, 2026
ab8c18f
Add detector-aware pixel calibration UI & API
beniroquai May 6, 2026
30512fe
Add Opentrons labware models and well selector
beniroquai May 9, 2026
9eeeac0
Adopt Opentrons labware; UI confirmations & grid
beniroquai May 9, 2026
85a6f4a
Overview: autonomous scan, freehand, overlay tab
beniroquai May 10, 2026
efef36c
update versions of libraries (cv2)
beniroquai May 11, 2026
8a49d4c
Persist overview registrations and group metadata
beniroquai May 11, 2026
4b0290f
Merge branch 'feature/OpentronsWellplatelayout' of https://github.com…
beniroquai May 11, 2026
4490dc2
remove binaries
beniroquai May 11, 2026
0070b35
fixing overlap converting to 0 frontend
beniroquai May 11, 2026
815d82b
Objective metadata, UI and status updates
beniroquai May 11, 2026
b5712bc
fix colours
beniroquai May 11, 2026
0357d9a
fix pixelsize in liveview
beniroquai May 11, 2026
7648a8f
Add per-detector streaming UI and backend support
beniroquai May 11, 2026
2b811b2
Add Z-speed, camera defaults, and stream UI tweaks
beniroquai May 12, 2026
e6a6c8c
Use LiveViewController for overview streams
beniroquai May 12, 2026
3a72709
Reuse shared LiveView for ObservationCamera
beniroquai May 12, 2026
83a5993
Add configurable camera move speeds (XY/Z)
beniroquai May 12, 2026
f1a52f0
Mount overview wizard in AxonTab; tweak image
beniroquai May 12, 2026
d855e6e
Add stage-offset calibration and recapture slot
beniroquai May 13, 2026
d21e5f2
Stage offset calibration UI + backend scan params
beniroquai May 13, 2026
0a59807
Make overview tasks async and add status API
beniroquai May 18, 2026
fd5a409
Merge Changes
beniroquai May 18, 2026
09da9a6
Update UC2ConfigManager.py
beniroquai May 18, 2026
12bb687
Sync objective controller API and UI improvements
beniroquai May 19, 2026
cd49d39
Persist per-detector stream settings and sync UI
beniroquai May 19, 2026
2b7a58a
Merge branch 'FixObjectiveState' into feature/OpentronsWellplatelayout
beniroquai May 19, 2026
827d6af
have 4 positions for manual pixel calibration
beniroquai May 19, 2026
f459752
Update LiveViewController.py
beniroquai May 20, 2026
d4b852a
Add twoStageDivisor for autofocus fine scan
beniroquai May 20, 2026
95b87e8
Add backlash compensation to pixel calibration
beniroquai May 20, 2026
e30f255
Add calibration preview, accept/discard flow
beniroquai May 20, 2026
a66e9e7
Add DetectorToggle and integrate into AxonTabComponent
beniroquai May 20, 2026
347baa3
Show objective info & add manual calibration flow
beniroquai May 20, 2026
080a1d6
Update AxonTabComponent.js
beniroquai May 21, 2026
c851a30
Use backend subsampling in manual calibration
beniroquai May 21, 2026
64d2dee
Sync active tab with backend active stream
beniroquai May 21, 2026
3fb517b
Defensive array handling in ParameterRangeSlice
beniroquai May 22, 2026
194eb6e
Update SetupInfo.py
beniroquai May 22, 2026
8ec91d0
Update LiveViewController.py
beniroquai May 22, 2026
8a2a9fa
Update ZFocusDimension.js
beniroquai May 22, 2026
240339f
Update FocusMapDimension.js
beniroquai May 22, 2026
18c0eab
add objective to experimentcontroller
beniroquai May 22, 2026
7b921ba
Handle null arrays and add 'Read EXP/GAIN' button
beniroquai May 22, 2026
31bb0f5
Add StreamPresets component and integrate into UI
beniroquai May 22, 2026
9a9ac83
Save/restore detector in presets; add JPEG defaults
beniroquai May 22, 2026
f9783ba
fix pixel format for rgb camera
beniroquai May 22, 2026
5280f7a
Merge branch 'feature/OpentronsWellplatelayout' of https://github.com…
beniroquai May 22, 2026
9e2e017
first implementation of stitching files in Ashlar while doing scanning
beniroquai May 26, 2026
62e32ae
additional changes w.r.t. RGB imaging in Experimentcontroller
beniroquai May 27, 2026
6010478
additional ways to work with RGB Data
beniroquai May 27, 2026
4bfeb7c
Update/stream detectorparameters to frontend during auto (#276)
gokugiant May 19, 2026
8758ab5
Feat/calibration menu category (#277)
gokugiant May 20, 2026
73736e7
Detect hydrophobic drops and refactor measurement
beniroquai May 21, 2026
e7dbeab
UI Updates on the Wellplate Controller (#272)
beniroquai May 22, 2026
c11ae3e
disable auto-reconnect via frontend for now
beniroquai May 26, 2026
81113a9
update firmware names to match canopen
beniroquai May 26, 2026
ff36f42
Only have diagnostics with real cameras
beniroquai May 26, 2026
5aa033a
Populate version strings from CI (#278)
ethanjli May 23, 2026
bb8ff8d
default pixelsize for ashlar stitching now taken from pixel calibration
beniroquai May 28, 2026
28893e8
Merge branch 'franziAshlar' of https://github.com/openuc2/imswitch in…
beniroquai May 28, 2026
c21e557
revert rebase mistake
Franzili May 28, 2026
271f65c
add updated ashlarUC2 fork as dependency for imswitch
Franzili May 28, 2026
8c5ded6
add ashlar parameter fields to TilingDimension and set values in Expe…
Franzili May 28, 2026
ad4fb62
add calibrated pixelsize to experiment parameters
Franzili May 28, 2026
debe288
remove unnecessary skimage patches and fix rgb stitching
Franzili May 28, 2026
651653f
remove duplicate conversion in the reshape block (Bayer frames are fu…
Franzili May 29, 2026
a1d984e
Update/stream detectorparameters to frontend during auto (#276)
gokugiant May 19, 2026
f61a317
Feat/calibration menu category (#277)
gokugiant May 20, 2026
9a3a662
Populate version strings from CI (#278)
ethanjli May 23, 2026
77a103e
update tucam binaries for libra16
beniroquai May 27, 2026
e956bf0
ensure binning works in HIK
beniroquai May 26, 2026
1f3447d
Fix/blacklevel synchronisation (#279)
gokugiant May 26, 2026
1311bbc
Feature/objective UI gating (#280)
gokugiant May 26, 2026
510df55
Refactor objective position validation and improve UI layout for bett…
gokugiant May 27, 2026
ed302d0
Update/UI cosmetics (#283)
gokugiant May 28, 2026
81d0f27
Fix/stage offset calibration (#281)
beniroquai May 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .github/workflows/build-docker-server.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ jobs:
# Don't download unnecessary data:
fetch-depth: 0
filter: 'blob:none'
# We fetch tags so that the staged pallet bundle has a record of ancestral tags, for
# pseudoversion string generation:
fetch-tags: true

- name: Wait for frontend to build
id: wait-build-frontend
Expand Down Expand Up @@ -92,6 +95,40 @@ jobs:
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Determine version string
id: version-string
run: |
# Get the SHA of the actual commit (not the fake merge commit) on PR-triggered runs
# (refer to https://stackoverflow.com/a/68068674/23202949):
if [ -n "${{ github.event.pull_request.head.sha }}" ]; then
ACTUAL_SHA="${{ github.event.pull_request.head.sha }}"
else
# Note: we want to have a SHA to check out even in merge queues, so we always fall back
# to a SHA even if it's a fictional SHA detached from any branches:
ACTUAL_SHA="${{ github.sha }}"
fi

TAG="$(git describe --tags --exact-match --match "v*" "$ACTUAL_SHA" 2>/dev/null || printf "")"
VERSION="$(git describe --tags --match "v*" --abbrev=7 "$ACTUAL_SHA")"
TIMESTAMP="$(TZ=UTC git show -s --format=%ad --date=format:'t%Y%m%d%H%M%S' "$ACTUAL_SHA")"

if [[ "$VERSION" != "$TAG" ]]; then
VERSION="$VERSION-$TIMESTAMP"
if [[ -n "${{ github.event.pull_request.head.sha }}" ]]; then
# We're in a pull request
VERSION="$VERSION-pr${{ github.event.pull_request.number }}"
fi
fi

echo "version: $VERSION"
echo "version=$VERSION" >> $GITHUB_OUTPUT

- name: Record version
run: |
VERSION="${{ steps.version-string.outputs.version }}"
echo "version = \"$VERSION\"" > ./imswitch/version.py
cat ./imswitch/version.py

- name: Build and push by digest
id: build
uses: docker/build-push-action@v6
Expand Down
37 changes: 37 additions & 0 deletions .github/workflows/build-frontend.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ jobs:
# This repo has tons of cruft in its history, so we try to only fetch files we actually need:
fetch-depth: 0
filter: 'blob:none'
# We fetch tags so that the staged pallet bundle has a record of ancestral tags, for
# pseudoversion string generation:
fetch-tags: true

- name: Use cached ~/.npm
uses: actions/cache@v4
Expand All @@ -37,6 +40,40 @@ jobs:
working-directory: ./frontend
run: npm install

- name: Determine version string
id: version-string
run: |
# Get the SHA of the actual commit (not the fake merge commit) on PR-triggered runs
# (refer to https://stackoverflow.com/a/68068674/23202949):
if [ -n "${{ github.event.pull_request.head.sha }}" ]; then
ACTUAL_SHA="${{ github.event.pull_request.head.sha }}"
else
# Note: we want to have a SHA to check out even in merge queues, so we always fall back
# to a SHA even if it's a fictional SHA detached from any branches:
ACTUAL_SHA="${{ github.sha }}"
fi

TAG="$(git describe --tags --exact-match --match "v*" "$ACTUAL_SHA" 2>/dev/null || printf "")"
VERSION="$(git describe --tags --match "v*" --abbrev=7 "$ACTUAL_SHA")"
TIMESTAMP="$(TZ=UTC git show -s --format=%ad --date=format:'t%Y%m%d%H%M%S' "$ACTUAL_SHA")"

if [[ "$VERSION" != "$TAG" ]]; then
VERSION="$VERSION-$TIMESTAMP"
if [[ -n "${{ github.event.pull_request.head.sha }}" ]]; then
# We're in a pull request
VERSION="$VERSION-pr${{ github.event.pull_request.number }}"
fi
fi

echo "version: $VERSION"
echo "version=$VERSION" >> $GITHUB_OUTPUT

- name: Record version
run: |
VERSION="${{ steps.version-string.outputs.version }}"
echo "export const APP_VERSION = \"$VERSION\";" > ./frontend/src/version.js
cat ./frontend/src/version.js

- name: Build
working-directory: ./frontend
run: |
Expand Down
10 changes: 10 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@
"subProcess": true,
"python": "${workspaceFolder}/.venv/bin/python"
},
{
"name": "Python: Aktuelle Datei (WINDOWS)",
"type": "python",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal",
"justMyCode": false,
"subProcess": true,
"python": "${workspaceFolder}/.venv/Scripts/python.exe"
},
{
"name": "Python: Aktuelle Datei x86",
"type": "python",
Expand Down
4 changes: 0 additions & 4 deletions docker/build-imswitch-deps.sh
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,6 @@ export PATH="/root/.local/bin:$PATH"
# slow-changing dependencies without installing ImSwitch itself yet.
# This creates a separately-cached Docker image layer from the rapidly-changing source.
mkdir -p /opt/imswitch/imswitch
cat >/opt/imswitch/imswitch/__init__.py <<EOF
# temporary placeholder to be overwritten by build-imswitch.sh
__version__ = "0.0.0"
EOF
cp /mnt/ImSwitch/pyproject.toml /opt/imswitch/pyproject.toml
cp /mnt/ImSwitch/uv.lock /opt/imswitch/uv.lock
cd /opt/imswitch
Expand Down
206 changes: 206 additions & 0 deletions docs/labware_opentrons.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
# Opentrons-style Labware in ImSwitch

ImSwitch ships an Opentrons-compatible labware layer that replaces the older
hard-coded `wellplate_layouts` module. Plates are described by JSON files
that follow the [Opentrons labware schema v2](https://github.com/Opentrons/opentrons/blob/edge/shared-data/labware/schemas/2.json),
loaded once at controller startup and exposed to the frontend through a small
set of REST endpoints.

This document covers:

1. Where labware definitions live and how they are discovered
2. The on-disk JSON schema (mm) and the in-memory model (µm)
3. Adding a new plate — both built-in (Python generator) and via dropping in
an Opentrons community JSON
4. The HTTP endpoints
5. The frontend `LabwareSelectionPanel`
6. The OME-NGFF `plate_metadata.json` sidecar emitted by experiments

---

## 1. Layout on disk

```
imswitch/imcontrol/model/labware/
├── __init__.py # public API: LabwareManager, load_labware_*
├── models.py # Pydantic models (Opentrons mm + ImSwitch µm)
├── loader.py # JSON → LabwareDefinition (mm → µm)
├── manager.py # LabwareManager: discovery, caching, offsets
├── selector.py # WellSelectionPattern → resolved wells/points
├── generators.py # Built-in SBS plate generator (Python → JSON)
├── schema_v2.json # Vendored Opentrons schema (relaxed `format`)
└── definitions/
├── openuc2/
│ ├── corning_6_wellplate_16.8ml_flat/1.json
│ ├── corning_12_wellplate_6.9ml_flat/1.json
│ ├── corning_24_wellplate_3.4ml_flat/1.json
│ ├── corning_48_wellplate_1.6ml_flat/1.json
│ ├── corning_96_wellplate_360ul_flat/1.json
│ ├── corning_384_wellplate_112ul_flat/1.json
│ ├── greiner_96_wellplate_650ul_uclear/1.json
│ ├── ibidi_8well_chambered_coverslip/1.json
│ ├── slide_4x_histosample_heidstar/1.json
│ ├── ropod_2slides_uc2/1.json
│ └── dep_chip_8x6_uc2/1.json
└── <your-namespace>/<your_load_name>/<version>.json
```

`LabwareManager` recursively scans `definitions/` on first use. The directory
layout follows Opentrons' convention: `<namespace>/<loadName>/<version>.json`.
The `loadName` field inside the JSON is what the rest of ImSwitch (and the
frontend) uses to refer to the plate.

## 2. Units

| Layer | Units | Purpose |
|--------------------------|-------|---------------------------------------------|
| JSON on disk | mm | Opentrons-native, drag-drop community defs |
| `OpentronsLabwareV2` | mm | Loader-internal Pydantic mirror |
| `LabwareDefinition` (RAM)| µm | What every other ImSwitch module sees |

Conversion happens in exactly one place: `loader.py` (`MM_TO_UM = 1000.0`).

## 3. Adding a new plate

### 3a. Drop in an Opentrons community JSON

The fastest way: download a JSON from the
[Opentrons labware library](https://labware.opentrons.com/) (or write your own),
place it at:

```
imswitch/imcontrol/model/labware/definitions/<namespace>/<loadName>/<version>.json
```

then restart ImSwitch. The new plate appears in `getLabwareList` and in the
frontend's labware dropdown automatically.

The schema requirements are minimal:

- `schemaVersion: 2`
- `parameters.loadName` — must match the folder name
- `parameters.format` — any string (`"96Standard"`, `"384Standard"`,
`"24Standard"`, `"irregular"`, …); ImSwitch does not gate behaviour on it
- `ordering` — list of columns of well IDs (must agree with `wells` keys)
- `wells` — dict keyed by well ID (`"A1"`, `"H12"`, …) with `x`, `y`, `z`,
`depth`, `totalLiquidVolume`, `shape` (`"circular"` or `"rectangular"`),
and `diameter` *or* `xDimension`/`yDimension`
- `dimensions` — `xDimension`/`yDimension`/`zDimension` of the outer plate
- `metadata.displayName`, `metadata.displayCategory`,
`metadata.displayVolumeUnits` (`"µL"` | `"mL"` | `"L"`)
- `brand.brand`
- `cornerOffsetFromSlot` — vector `{x, y, z}`

Any additional Opentrons fields are tolerated (`extra="allow"`).

A bad file is logged with a `LabwareValidationError` and skipped — it never
blocks controller startup.

### 3b. Add a built-in SBS plate via the generator

Built-in plates live in `generators.py` (`_BUILTIN_SPECS`). Append a new entry
and run:

```bash
python -m imswitch.imcontrol.model.labware.generators
```

This regenerates the JSONs under `definitions/openuc2/`. Example spec:

```python
dict(
rows=4, cols=6,
well_diameter_mm=16.26,
well_depth_mm=17.4,
total_volume_uL=3400.0,
well_spacing_x_mm=19.3,
well_spacing_y_mm=19.3,
a1_x_offset_mm=17.48,
a1_y_offset_mm=13.67,
load_name="corning_24_wellplate_3.4ml_flat",
display_name="Corning 24 Well Plate 3.4 mL Flat",
brand="Corning",
format_override="24Standard",
tags=["SBS", "wellPlate", "_generated"],
),
```

The generator follows the SBS convention: A1 at the back-left of the plate.

## 4. HTTP endpoints

All endpoints hang off `ExperimentController`.

### New (preferred)

- `GET /ExperimentController/getLabwareList`
→ `[{load_name, display_name, format, rows, cols, well_count, tags, ...}]`
- `GET /ExperimentController/getLabwareDefinition?load_name=...&offset_x_um=...&offset_y_um=...`
→ full `LabwareDefinition` (µm) with optional rigid offset baked in
- `POST /ExperimentController/selectWellsByPattern`
body: `{ loadName, pattern: WellSelectionPattern, offsetXUm, offsetYUm }`
→ resolved wells + their µm coordinates
- `POST /ExperimentController/applyWellSelectionToExperiment`
body: `{ loadName, pattern, offsetXUm, offsetYUm, conditionLabels, pointNameTemplate }`
→ `{ points: [...] }` ready to push into the experiment's `pointList`

### `WellSelectionPattern` shape

```json
{
"wells": ["A1", "B5"], // explicit IDs (optional)
"ranges": ["A1:C3", "H12"], // Excel-style ranges (optional)
"rows": ["A", "C"], // entire rows (optional)
"columns": [1, 6, 12], // entire columns (optional)
"all": false // shorthand for the whole plate
}
```

Pattern fields are unioned. `pointNameTemplate` understands `{well_id}`,
`{row}`, `{column}`, `{label}`.

### Compatibility wrappers (legacy clients)

- `GET /ExperimentController/getAvailableWellplateLayouts`
- `GET /ExperimentController/getWellplateLayout?layout_name=...`

Both delegate to the labware manager and return the legacy dict shape the
old frontend canvas understands (`{name, wells: [...], width, height,
unit: "um", ...}`). The `generateCustomWellplateLayout` endpoint and the
`wellplate_layouts` module have been removed — define plates as JSON instead.

## 5. Frontend: `LabwareSelectionPanel`

`frontend/src/components/LabwareSelectionPanel.jsx` mounts inside
`WellSelectorComponent` and exposes:

- **Labware picker** — populated from `getLabwareList`; switching it clears
the current well selection and (after confirmation) the experiment's
point list
- **Well chip grid** — click chips to toggle, *or* type a pattern
(`A1:C3, B5`) and hit *Resolve* to expand it into chips
- **Condition labels** — tag selected wells with a free-text label that
travels through to the OME-NGFF metadata
- **Sub-position generator** — emit one point per well at the well centre,
*or* an `Nx × Ny` rectangular grid inside each well with user-set µm
spacing; a small SVG preview shows the dot pattern in the well outline
- **Apply** — append or replace the experiment's `pointList`

The panel uses the new endpoints exclusively. Layout switching (the legacy
`Layout` dropdown in `WellSelectorComponent`) shows a confirmation dialog
when the existing `pointList` is non-empty so users do not lose their
selections by accident.

## 6. OME-NGFF plate metadata sidecar

When an experiment runs against a labware definition, ImSwitch writes a
`plate_metadata.json` sidecar next to the OME-Zarr store. The file follows
the [OME-NGFF 0.4 plate spec](https://ngff.openmicroscopy.org/0.4/#plate-md)
plus an `imswitch_well` block per well that carries:

- `labwareLoadName`, `wellRow`, `wellColumn`
- `conditionLabel` (if set)
- the resolved acquisition `points` for that well

See `imswitch/imcontrol/model/io/ome_writers/plate_metadata.py` for the
exact schema.
21 changes: 19 additions & 2 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions frontend/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
{
"name": "microscope-app",
"version": "1.6.4",
"name": "imswitch-frontend",
"private": true,
"homepage": "/imswitch/ui",
"dependencies": {
Expand Down
Loading