Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,32 @@ v0.5.1: no run before v0.5.1 is read.)

## Unreleased

**Owner feedback on the v0.5.2 Colab test (12 November 2026)**

- **Axes: the confirmation is seen at once.** The instant **Confirm these axes** or
**Confirm axis roles** (or **Change axis roles**) is clicked, the button is greyed out and
says *Working…*, and the progress line of the folder check appears under it before any of
the work: *Working…*, then the 5 parts of the confirmation with a bar (n of 5, what it does
now). Both clear when it is done.
If something fails part-way, the button is given back and the error is said in red where
it is seen (under **Confirm axis roles** once the one-click card has gone), with the full
report in *Technical details*; nothing stays on *Working…*.
- **Axes: the confirmation is faster.** It read every channel of every folder's first image for
the per-folder image-type rows, which are folded under *Set per folder…* (28 folders of
3 × 2048² images: 93 plane reads, 84 of them for those rows), and rebuilt the pairing editor
of Design for pictures the stepper never shows. The rows now read their pictures only once
opened, and the editor is not rebuilt there: 9 plane reads instead of 93 (about 72 MiB read
instead of 744 MiB) and 1,037 widgets built instead of 1,439 on the same data. Nothing
validated changed.
- **The image type moved from Axes to Channels.** It is proposed at the quick scan, display
only, from the headers (the shown image's before the folder check, every header after it;
nothing is recorded there), and confirmed on Channels, first on the page, since it decides
whether the channels are R, G and B or the stains. The rules are unchanged: recorded only by
a click, pre-filled only when every header agrees, per folder when they differ, **Use for all
folders**, and folders of different types refused (`R_HETEROGENEOUS_LAYOUT_NOT_ENABLED`). The
Axes step is done once the roles are confirmed; the Channels step needs the image type. The
published files are unchanged for the same answers.

**Parent-label (grouped) masks** (MorphoBricks-Spec PR #28, merge `69a58b3`: ADR 0001 PL1,
experiment-v1 §4.5, §4.5a-§4.5c, §7, §8; ADR 0002 resolver contract version 2)

Expand Down
15 changes: 11 additions & 4 deletions docs/colab-release-check.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,18 @@ release PR moves both together and records the pair in `RELEASE_CALLS` of
7. - [ ] **Images:** paste the Drive paths (experiment, masks, output); **Browse…** says it
cannot open here and offers **Copy path**. **Scan the folders** shows the folders and an
example picture with a progress line; **Looks right: read all image headers** shows its
progress (done of total, time left) and the result.
progress (done of total, time left) and the result. The image type (separate channels or
colour RGB) is shown from the headers, display only: "nothing is recorded here".
8. - [ ] **Axes:** one row per dimension with its role, the example redrawn when a role changes,
the pixel size in one line, the image type in one line (**Use for all folders**). The roles
are confirmed only by the click.
9. - [ ] **Channels:** name the stains, then tag each folder's channels.
the pixel size in one line; no image type. The roles are confirmed only by the click. The
instant **Confirm these axes** (or **Confirm axis roles**) is clicked, the button is greyed
out and says **Working…**, and a progress bar under it counts the parts done (n of 5, what
it does now); both clear when the roles are confirmed.
9. - [ ] **Channels:** first the image type in one line, pre-filled only when every header
agrees and recorded only by **Use for all folders** (or each folder under **Set per
folder…**; folders of different types are refused, `R_HETEROGENEOUS_LAYOUT_NOT_ENABLED`);
then name the stains (none for colour RGB: its channels are R, G and B) and tag each
folder's channels.
10. - [ ] **Masks:** **Check the masks** runs with progress; each mask type can be described.
**Describe the masks** is greyed out, with its reason, until a run exists.
11. - [ ] **Design:** the grid of conditions by biological repeats; **Looks right: confirm the
Expand Down
2 changes: 1 addition & 1 deletion docs/contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -590,7 +590,7 @@ plan N34); this brick suggests it, asks for it after the axis roles, and records
| Types | `fluorescence` (any number of channels, every pixel type) and `rgb` (exactly three channels of an integer pixel type, the rule of the normalization brick's `check_rgb`). |
| RGB samples | A TIFF image is stored as RGB colour when its first series has the photometric interpretation RGB with three samples per pixel (`tifffile` axis `S`; `TiffMetadata.rgb_samples`). RGB with an alpha sample, several grey samples and OME-Zarr images are not. |
| Suggestion | `rgb` when every analysed image is stored as RGB colour, the confirmed C role is on its samples axis, and `rgb` is possible; `fluorescence` otherwise. `review_image_type` gives the suggestion, why, and why `rgb` is not possible (from the headers and the confirmed axis roles; no pixels are read). |
| Confirmation | Never applied without the user's confirmation. The notebook asks in step 2 once the axis roles are confirmed, with the suggestion preselected and a **Yes, this is the right image type** button; `rgb` cannot be chosen, with the reason shown, when it is not possible. Changing the axis roles or the folder asks again. Step 3 runs only once the type is confirmed. The command line takes `--image-type`, or uses the suggestion and prints it. |
| Confirmation | Never applied without the user's confirmation. The notebook proposes it at the quick scan (display only, from the headers; nothing is recorded there) and asks on the Channels step once the axis roles are confirmed (owner, 12 November 2026), pre-filled only when every header agrees and recorded only by a click (**Use for all folders**, or each folder); folders of different types are refused (`R_HETEROGENEOUS_LAYOUT_NOT_ENABLED`). Changing the axis roles or the folder asks again. The run takes the type the saved description states. The command line takes `--image-type`, or uses the suggestion and prints it. |
| Run | `run_experiment(..., image_type=...)` records the type and `channels_from_rgb_samples` in the manifest (§9). `rgb` for images that cannot be RGB is the error `rgb_not_possible`; any other value raises `ValueError`. Without a type the manifest records null. |
| Outputs | `plane_histograms.csv` and `experiment_design.csv` do not depend on the type: a plane of an RGB image is one row with channels C0, C1 and C2, as for any image. Changing the type on a later run reads no image again. |

Expand Down
4 changes: 2 additions & 2 deletions notebooks/MorphoBricks_Preprocessing.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -395,8 +395,8 @@
"The app has seven steps, one on screen at a time; move with **Next** or click a step in the strip at the top:\n",
"\n",
"1. **Images**: give the experiment folder (masks and output are optional), click **Scan the folders**, look at the example image, then **Looks right: read all image headers**.\n",
"2. **Axes**: what each image dimension is, the pixel size, and the image type (separate stains or colour).\n",
"3. **Channels**: name what you stained, then tag each folder's channels.\n",
"2. **Axes**: what each image dimension is, and the pixel size.\n",
"3. **Channels**: confirm the image type (separate stains or colour), name what you stained, then tag each folder's channels.\n",
"4. **Masks**: say what each mask type is (nothing to do without masks). After the run, come back here for **Describe the masks** if later bricks use the masks.\n",
"5. **Design**: conditions, biological repeats and comparisons in one grid; **Looks right: confirm the design**.\n",
"6. **Check and save**: read your experiment in words and save it.\n",
Expand Down
22 changes: 20 additions & 2 deletions src/morphobricks_preprocessing/notebook_axes_quick.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,11 @@ def _build_quick_axes(self) -> None:
tooltip="The example looks right: use these roles for every image",
main=True,
)
self.quick_confirm.on_click(lambda _: self._confirm_quick_axes())
self.quick_card = widgets.VBox([self.quick_text, self.quick_confirm, self.quick_caption])
self.quick_confirm.on_click(lambda _: self._quick_clicked())
self.quick_progress = widgets.HTML() # Working…, then the parts done (owner, 12 Nov)
self.quick_card = widgets.VBox(
[self.quick_text, self.quick_confirm, self.quick_progress, self.quick_caption]
)
ui.set_visible(self.quick_card, False)

# What can be offered -------------------------------------------------------------------
Expand Down Expand Up @@ -178,6 +181,21 @@ def _headers_changed_line(self) -> list[str]:

# The click ---------------------------------------------------------------------------

def _quick_clicked(self) -> None:
"""*Confirm these axes*: working at once, then the parts of the confirmation."""
self._working(self.quick_confirm, self.quick_progress, self._confirm_quick_axes)

def _after_working(self, button: widgets.Button) -> None:
super()._after_working(button)
if button is getattr(self, "quick_confirm", None):
self._refresh_quick_axes(drawn=True) # its state, from the roles and the example

def _failure_line(self, progress: widgets.HTML) -> widgets.HTML:
"""Say a failure of *Confirm these axes* under *Confirm axis roles* once its card hides."""
if progress is getattr(self, "quick_progress", None) and not ui.is_visible(self.quick_card):
return self.confirm_progress
return super()._failure_line(progress)

def _confirm_quick_axes(self) -> bool:
"""Confirm the offered roles: the preview checked, then the roles, as the role boxes do.

Expand Down
99 changes: 98 additions & 1 deletion src/morphobricks_preprocessing/notebook_axis_preview.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
from __future__ import annotations

import html
import traceback
from collections.abc import Callable

import ipywidgets as widgets

from . import notebook_style as ui
from .axes import AxisMapping, axis_name, file_mapping_for
Expand All @@ -17,10 +21,90 @@
from .preview import PreviewRefused, check_unchanged, checked_fingerprint
from .validation import plural

WORKING = "Working…"
"""What a confirmation button says, and its progress line first, the instant it is clicked."""
CONFIRM_STEPS = (
"Checking the roles against every image",
"Redrawing the example with these roles",
"Reading the pixel sizes",
"Preparing the run",
"Preparing Channels, Masks and Check and save",
)
"""The parts of *Confirm axis roles* (or of taking it back), in order, for its progress bar."""


class _AxisPreview:
"""Step 2's preview, the confirmation of the axis roles and the image type."""

# The click, shown at once ------------------------------------------------------------

def _confirm_clicked(self) -> None:
"""*Confirm axis roles* (or *Change axis roles*): working at once, then the parts."""
self._working(self.confirm_button, self.confirm_progress, self._toggle_confirmation)

def _working(
self, button: widgets.Button, progress: widgets.HTML, action: Callable[[], object]
) -> object:
"""Run ``action`` behind a disabled button and a progress line drawn before it starts.

Owner (12 November 2026): the click must show at once that something happens. The
button is disabled and says *Working…*, and the line under it shows *Working…* before
any of the work; the parts of the work then advance its bar
(:meth:`_confirm_step`). The widgets' state is sent to the page as it is set, so the
line is seen while the work runs. Afterwards the line is cleared and the button given
back the state the answers give it (:meth:`_after_working`). A disabled button does
nothing. An unexpected failure part-way gives the button back too, and says what
failed where it is seen (:meth:`_failure_line`), the full report in Technical details.
"""
if button.disabled:
return None
saved = (button.description, button.icon)
self._busy(button, WORKING)
self._working_on = button # it stays disabled, saying Working…, until the end
wait = self._confirm_wait = Wait(progress)
wait.start(WORKING, len(CONFIRM_STEPS))
failed: str = ""
try:
return action()
except Exception as exc: # unexpected failures still get a readable message
self.log.lines.append(traceback.format_exc())
failed = ui.status(
"error",
"The confirmation stopped because of an unexpected problem.",
[
f"{type(exc).__name__}: {exc}",
"Open Technical details for the full report. Click the button again; "
"once the roles are confirmed, Change axis roles takes them back.",
],
)
return None
finally:
self._confirm_wait = self._working_on = None
progress.value = ""
button.description, button.icon = saved
button.disabled = False
try:
self._after_working(button)
finally:
if failed:
self._failure_line(progress).value = failed
self._show_log()

def _failure_line(self, progress: widgets.HTML) -> widgets.HTML:
"""Return the line that says a failure: the click's own, where it is seen."""
return progress

def _after_working(self, button: widgets.Button) -> None:
"""Give the confirmation buttons back the state the step's answers give them."""
self._confirm_words(self.confirmed_mapping is not None)
self._update_confirm_button()

def _confirm_step(self, index: int) -> None:
"""Advance the progress of a confirmation to part ``index`` (0-based), when one shows."""
wait = getattr(self, "_confirm_wait", None)
if wait is not None:
wait.phase(CONFIRM_STEPS[index], len(CONFIRM_STEPS), index)

def _fill_preview_files(self) -> None:
"""List the images that fit the roles in the preview dropdown."""
files = [
Expand Down Expand Up @@ -211,13 +295,14 @@ def _preview_header(self): # noqa: ANN202
def _toggle_confirmation(self) -> None:
"""Confirm the roles shown (or take the confirmation back) and update what follows."""
if self.confirmed_mapping is None:
if self.confirm_button.disabled:
if self._confirm_blockers(): # what disables the button (it shows Working… now)
return
self.confirmed_mapping = self._current_mapping()
else:
self.confirmed_mapping = None
locked = self.confirmed_mapping is not None
self._set_locked(locked)
self._confirm_step(0)
if locked:
roles = [
f"{axis_name(a)} → {r}"
Expand Down Expand Up @@ -256,11 +341,17 @@ def _toggle_confirmation(self) -> None:
self.confirmed_status.value = ""
# The image type depends on the channels the roles give.
self._close_image_type()
self._confirm_step(1)
self._mapping_changed()
self._update_confirm_button()
if locked:
self._confirm_step(1)
self._render_preview_if_shown()
self._confirm_step(2)
self._show_calibration()
self._confirm_step(3)
self._refresh_run_step()
self._confirm_step(4)
self._refresh_channels()

def _open_image_type(self) -> None:
Expand Down Expand Up @@ -350,6 +441,12 @@ def _set_locked(self, locked: bool) -> None:
dropdown.disabled = locked
self.missing_as_singleton.disabled = locked
self._gate_preview_tick()
self._confirm_words(locked)

def _confirm_words(self, locked: bool) -> None:
"""Say *Confirm axis roles*, or *Change axis roles* once they are confirmed."""
if getattr(self, "_working_on", None) is self.confirm_button:
return # it says Working… until the work is done (:meth:`_working`)
self.confirm_button.description = " Change axis roles" if locked else " Confirm axis roles"
self.confirm_button.icon = "pencil" if locked else "check"
self.confirm_button.button_style = "warning" if locked else "success"
Expand Down
8 changes: 6 additions & 2 deletions src/morphobricks_preprocessing/notebook_axis_step.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ def _build_axis_step(self) -> None:
tooltip="Use these roles for every image of the experiment",
main=True,
)
self.confirm_progress = widgets.HTML() # Working…, then the parts done (owner, 12 Nov)
self.confirm_blocker = widgets.HTML()
self.confirmed_status = widgets.HTML()
self.axis_next = widgets.HTML()
Expand All @@ -59,7 +60,7 @@ def _build_axis_step(self) -> None:
for slider in self.preview_sliders.values():
slider.observe(lambda _: self._render_preview_if_shown(), names="value")
self.preview_checked.observe(lambda _: self._update_confirm_button())
self.confirm_button.on_click(lambda _: self._toggle_confirmation())
self.confirm_button.on_click(lambda _: self._confirm_clicked())
self._build_image_type_card()

def _build_missing_card(self) -> None:
Expand Down Expand Up @@ -204,9 +205,10 @@ def _refresh_axis_step(self) -> None:
self.convert_card,
self.axis_sizes_slot,
self.confirm_button,
self.confirm_progress,
self.confirm_blocker,
self.confirmed_status,
self.image_type_card,
# No image type here (owner, 12 November 2026): it is confirmed on Channels.
self.axis_next,
]
self._fill_preview_files()
Expand Down Expand Up @@ -393,6 +395,8 @@ def _mapping_changed(self) -> None:

def _update_confirm_button(self) -> None:
"""Enable *Confirm axis roles* when nothing blocks it; say beside it what does."""
if getattr(self, "_working_on", None) is self.confirm_button:
return # disabled, saying Working…, until the work is done
if self.confirmed_mapping is not None:
self.confirm_button.disabled = False
self.confirm_blocker.value = ""
Expand Down
4 changes: 2 additions & 2 deletions src/morphobricks_preprocessing/notebook_channels_grid.py
Original file line number Diff line number Diff line change
Expand Up @@ -416,11 +416,11 @@ def _block_text(self, units: Sequence[UnitLayout], colour: bool, apart: bool) ->
def _type_text(self, layout: UnitLayout) -> str:
"""Say the image-type answer of a colour block, as the image-type page gave it."""
if self.types.saved(layout.key) == "rgb":
where = "" if self.place_types else " (Axes)"
where = "" if self.place_types else " (Image type, above)"
return f"Answered colour (RGB){where}: its channels are R, G and B, nothing to tag."
if self.place_types:
return "Colour is pre-ticked from the header: confirm it with the button below."
return "Colour is pre-ticked from the header: confirm it on Axes (Image type)."
return "Colour is pre-ticked from the header: confirm it under Image type, above."

def _pending(self, layout: UnitLayout) -> bool:
"""Return True when the unit shows a proposal not saved yet.
Expand Down
Loading
Loading