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
9 changes: 6 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,8 +305,10 @@ src/morphobricks_preprocessing/
comparable questions and CSV writes, the IDs toggle and the Image type card,
and reopen from the published file
notebook_check_save.py the Check and save screen (sentences, one "same way?" per replicate,
Yes, remove, the save click, the folded reopen); notebook_check_view.py its
HTML; description_review.py (pure) the sentences and the CSV row diff;
Yes, remove, the save click, the folded reopen); notebook_replicates.py its
replicate boxes (rebuilt only when what they show changed);
notebook_check_view.py its HTML; description_review.py (pure) the
sentences and the CSV row diff;
description_candidate.py the checks on the exact bytes; description_reopen.py
the published file against the folders; published_answers.py (pure) the
published file as draft answers, retired IDs, reopened replicates
Expand Down Expand Up @@ -1099,7 +1101,8 @@ jupyter lab notebooks/MorphoBricks_Preprocessing.ipynb
[docs/archive/plan-milestones-v0.1-v0.4.md](docs/archive/plan-milestones-v0.1-v0.4.md); the older
status log of this file is in
[docs/archive/agents-status-history-v0.1-v0.3.md](docs/archive/agents-status-history-v0.1-v0.3.md).
- **Unreleased:** none yet; later changes go in a new "Unreleased" section of the changelog.
- **Unreleased:** the faster *Looks right: confirm the design* click (see the changelog's
"Unreleased" section).
- **Scope:** 2-D per plane (a Z-stack is refused as `z_stack_not_supported`; since v0.5.2 the
notebook saves Z-stacks as recorded projections); acquisitions may have different channel
lists (v0.5.2, Stage 3 S3.3: manifest version 5, histogram table schema version 5; units of
Expand Down
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,21 @@ The design is `docs/restart/preprocessing-design-v0.4.md` in MorphoFeature, on b
are in [docs/plan.md](docs/plan.md) §12. (The v0.3 to v0.4 migration guide was removed in
v0.5.1: no run before v0.5.1 is read.)

## Unreleased

### Fixed

- **Looks right: confirm the design** seemed to do nothing, or was very slow (owner, v0.5.2
Colab test). The click saved the answers twice and refreshed every page three times, each
time rebuilding one image-type row per folder and every replicate box of *Check and save*,
so it grew with the number of folders (headless: 1.0 s for 36 images, 5.3 s for 384, 7.7 s
for 900; about 2,400 to 14,500 widgets sent to the page). It now saves one answer, refreshes
once, and rebuilds the image-type rows and the replicate boxes only when what they show
changed (0.14 s, 0.29 s and 0.46 s; about 250 to 450 widgets). The button is disabled and
says *Confirming the design…* with a working line before any work, and the click ends with
the result, or with the reason it was not confirmed; an unexpected failure is said there,
the full report in Technical details. Nothing recorded changed.

## v0.5.2 (12 November 2026)

Acquisitions may now have different channels (Stage 3 S3.3, owner decision of 11 November
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ baseline = "tools/code_rules_baseline.json"
"morphobricks_preprocessing.notebook_masks_screen" = "ui"
"morphobricks_preprocessing.notebook_check_save" = "ui"
"morphobricks_preprocessing.notebook_check_view" = "ui"
"morphobricks_preprocessing.notebook_replicates" = "ui"
"morphobricks_preprocessing.notebook_described" = "ui"
"morphobricks_preprocessing.notebook_mask_question" = "ui"
"morphobricks_preprocessing.notebook_mask_step" = "ui"
Expand Down
48 changes: 44 additions & 4 deletions src/morphobricks_preprocessing/notebook_channels_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,10 +189,21 @@ def answered(self) -> int:
return sum(1 for u in self.types.layouts if u.usable and self.types.saved(u.key))

def refresh(self) -> None:
"""Rebuild the rows from the saved answers (without pictures while they are folded)."""
self.types.open(self.types.layouts) # a fresh set of cells, built from the answers
self.pictured = not self.folded()
self._build_rows()
"""Rebuild the rows from the saved answers (without pictures while they are folded).

The rows are rebuilt only when something they show changed: the folders, a saved
type, the pictures or the saved experiment's statuses. Every answer elsewhere (a
name, a pairing, the design's confirmation) refreshes this page too, and rebuilding
one row per folder each time made those clicks slow on large experiments (owner,
12 November 2026: *Looks right: confirm the design* seemed to do nothing).
"""
pictured = not self.folded()
shown = self._shown(pictured)
if not self._same_rows(shown):
self.types.open(self.types.layouts) # a fresh set of cells, built from the answers
self.pictured = pictured
self._build_rows()
self._built = (self.types.radios, shown)
self.problem.value = self.types.message
usable = sum(1 for u in self.types.layouts if u.usable)
done = self.answered
Expand All @@ -202,6 +213,35 @@ def refresh(self) -> None:
else ui.status("warn", f"Not finished: {usable - done} image type(s) to choose.")
)

def _shown(self, pictured: bool) -> tuple:
"""Return what the rows show, to tell whether they must be rebuilt."""
layouts = self.types.layouts
units = self.reopen()
return (
layouts,
pictured,
self._thumbs() if pictured else None,
tuple((self.types.saved(u.key), self.types.note(u)) for u in layouts),
None if units is None else tuple(sorted(units.items())),
)

def _same_rows(self, shown: tuple) -> bool:
"""Return True when the rows on the page were built for exactly ``shown``.

Objects (the layouts, the pictures) are compared by identity, the answers by value;
cells replaced since (:meth:`ImageTypeColumn.open`) are never kept.
"""
built = getattr(self, "_built", None)
if built is None or built[0] is not self.types.radios:
return False
before = built[1]
return (
before[0] is shown[0]
and before[1] == shown[1]
and before[2] is shown[2]
and before[3:] == shown[3:]
)

def unfolded(self) -> None:
"""Rebuild the rows with their pictures now that they are shown, unless they have them."""
if not self.pictured and not self.folded() and ui.is_visible(self.widget):
Expand Down
110 changes: 4 additions & 106 deletions src/morphobricks_preprocessing/notebook_check_save.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@

from __future__ import annotations

import html
from dataclasses import replace
from datetime import date
from pathlib import Path
Expand All @@ -31,7 +30,7 @@
from .description import DescriptionError
from .description_candidate import CandidateCheck, check_candidate
from .description_removal import Removal, reconfirmations
from .description_review import comparisons, replicates, short_names
from .description_review import comparisons, short_names
from .draft import DraftError
from .draft_answers import (
RepeatAnswer,
Expand All @@ -43,28 +42,20 @@
from .fingerprint import changed_since_check
from .mask_answers import masks_unanswered
from .name_issues import NAME_CODES
from .notebook_channel_thumbs import Thumb
from .notebook_check_view import (
SAME_WAY,
SAME_WAY_HELP,
blocked_html,
changes_html,
checks_html,
comparisons_html,
member_html,
missing_html,
names_html,
removal_text,
replicate_title,
run_stop_html,
saved_html,
todo_html,
)
from .notebook_description import _with_ids
from .notebook_render import blocked_line
from .notebook_row_fold import RowFold, row_status
from .notebook_thumb_tile import ThumbTile, show_detail
from .preview_crop import TilePictures
from .notebook_replicates import _ReplicateBoxes
from .published_answers import kept_date
from .run import OutputLockedError
from .run.description_file import PublicationRefused
Expand All @@ -79,15 +70,7 @@
FOLDED = ("design_card", "names_card", "references_card", "channels_box", "masks_box")


def _enlarge(detail: widgets.VBox, title: str, found: TilePictures | Thumb) -> None:
"""Show a folder's enlarged picture in ``detail``, or why there is none."""
if isinstance(found, Thumb):
show_detail(detail, title, None, found.note)
else:
show_detail(detail, title, found, "")


class _CheckSaveScreen:
class _CheckSaveScreen(_ReplicateBoxes):
"""The *Check and save* screen, its save click and the folded reopening."""

def _build_check_save(self) -> None:
Expand Down Expand Up @@ -319,92 +302,6 @@ def _candidate_check(self, answers: object) -> CandidateCheck | None:
_, confirmations = self._publication_terms(state, candidate)
return check_candidate(candidate, self.preflight, roles, published, confirmations)

def _show_replicates(self, applied: object, answers: object, labels: list[str]) -> None:
"""One box per replicate: folders, pictures, channels, and its own question."""
boxes, toggles = [], {}
units, counts = self._reopen_units(), self._channel_counts()
state = self._published()
saved_dates = (
{}
if state is None
else {key: r.confirmed_on for key, r in state.description.repeats.items()}
)
views = replicates(applied, answers)
for view in views:
toggle = ui.ConfirmToggle(
SAME_WAY,
SAME_WAY_HELP,
"Yes, the same way",
f"Confirmed on {view.confirmed_on}: one replicate." if view.confirmed_on else "",
"Undo",
)
toggle.set(view.confirmed_on is not None, notify=False)
toggle.observe(lambda value, label=view.label: self._same_way(label, value))
toggles[view.label] = toggle
members = [
widgets.HBox(
[
self._member_picture(
(m.condition, m.source_repeat), self.replicates_detail
),
widgets.HTML(member_html(m, labels, counts)),
]
)
for m in view.members
]
body = [widgets.HTML(replicate_title(view)), *members, widgets.HTML(missing_html(view))]
box = widgets.VBox(
[*body, toggle.widget],
layout=widgets.Layout(
border="1px solid #E2E8F0", padding="8px 12px", margin="0 0 10px 0"
),
)
keys = [u.key for m in view.members for u in m.units]
# A box with a warning (an acquisition lacking a label) never folds, nor one whose
# date is not the published one (publication's R_CONFIRMED_ON_CHANGED names it).
settled = (
view.confirmed_on is not None
and not view.missing
and view.confirmed_on == saved_dates.get(view.label, view.confirmed_on)
)
boxes.append((box, row_status(units, keys, settled)))
self.same_way = toggles
shown = (
self._channel_thumbs,
[[(m.condition, m.source_repeat) for m in v.members] for v in views],
)
if shown != getattr(self, "_replicates_shown", None): # other folders or pictures (#252)
self.replicates_detail.children = [] # an Enlarge of the boxes before is closed
self._replicates_shown = shown
if not hasattr(self, "_replicate_fold"):
self._replicate_fold = RowFold("replicate")
self.replicates_box.children = self._replicate_fold.arrange(boxes)

def _member_picture(self, key: tuple[str, str], detail: widgets.VBox) -> widgets.Widget:
"""Return the picture of a folder's first image (channel 1, or in colour when RGB).

A large image gets its crop at native pixels beside it; **Enlarge** shows both in
``detail``.
"""
thumbs, layouts = self._channel_thumbs, self._channel_layouts
layout = next((u for u in layouts if u.key[:2] == key), None)
if thumbs is None or layout is None:
return widgets.HTML(
"<div class='mb-help' style='width:120px'>picture once the axis roles are "
"confirmed</div>"
)
colour = self.channel_types.shown(layout) == "rgb"
found = thumbs.tile(layout, colour)
if isinstance(found, Thumb):
return ThumbTile(None, problem=found.note, width="120px").widget
title = f"<b>{html.escape('/'.join(key))}</b> · {html.escape(layout.image or '')}"
return ThumbTile(
found,
colour=colour,
enlarge=lambda: _enlarge(detail, title, thumbs.enlarged(layout, colour)),
width="120px",
).widget

def _folder_cell(self, key: tuple[str, str], text: str) -> widgets.Widget:
"""Return a folder of the pairing card, with its picture."""
return widgets.VBox([widgets.HTML(text), self._member_picture(key, self.pairing_detail)])
Expand Down Expand Up @@ -464,6 +361,7 @@ def _same_way(self, label: str, value: bool) -> None:
repeats = {**answers.repeats, label: replace(entry, confirmed_on=day)}
if not self._answer(replace(answers, repeats=repeats)):
self.save_status.value = ui.status("error", "Not taken.", [str(self._refusal)])
self._replicates_built = None # the clicked box shows an answer that was not taken
self._refresh_check_save()

def _remove(self, condition_id: str) -> None:
Expand Down
10 changes: 9 additions & 1 deletion src/morphobricks_preprocessing/notebook_description.py
Original file line number Diff line number Diff line change
Expand Up @@ -313,14 +313,22 @@ def _wanted_ids(self) -> dict[tuple[str, str], str]:
def _confirm_ids(self) -> bool:
"""Adopt every ID as shown, or none: each refusal is listed with the reader's reason."""
wanted = self._wanted_ids()
return self._adopt_ids(wanted, _with_ids(self.draft_answers, wanted))

def _adopt_ids(self, wanted: dict[tuple[str, str], str], answers: DraftAnswers) -> bool:
"""Save ``answers`` (which adopt ``wanted``) in one answer, unless an ID is refused.

Each ID is first tried on its own, so every refusal is listed with the reader's
reason; then ``answers`` are saved once. Nothing is adopted when anything is refused.
"""
problems = []
for (kind, key), text in wanted.items():
try:
apply_answers(self._draft_base, _with_ids(self.draft_answers, {(kind, key): text}))
except DraftError as exc:
problems.append((f"{KIND_NAMES[kind].capitalize()} {key}", exc))
if not problems:
if self._answer(_with_ids(self.draft_answers, wanted)):
if self._answer(answers):
return True
problems.append(("These IDs together", self._refusal))
self._names_refusal = refusal_card(
Expand Down
Loading
Loading