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
48 changes: 48 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,54 @@ OME-XML description is parsed (which images are marked is unchanged; a malformed
debug line without a traceback), and while the capture is attached the package's DEBUG records
go to the "Technical details" panel only, with WARNING and above still reaching the root handlers.

### Notebook redesign 5: seven steps, what the files hold first

Owner's test of the first redesign (10 November 2026), items (o), (p) and (q). The shipped
notebook has 7 steps instead of 12, in the owner's order: **Images → Axes → Channels → Masks →
Design → Check and save → Run**. For the same answers the published `morphobricks.toml`,
`experiment_design.csv`, `plane_histograms.csv`, `calibration.csv`, the mask files and the
manifest (but for its time and paths) are byte-identical to before; the Spec behaviour is
unchanged.

- **Axes holds the image type** (what the channel dimension holds: separate stains or markers,
or colour), in the pattern of the pixel size (`notebook_image_type_line.ImageTypeLine`): one
line for all folders, pre-filled from what the image headers say and **recorded only by the
click Use for all folders**; **Set per folder…** holds each folder's row as before; when the
headers differ nothing is pre-filled, the line says how many folders say what, and the
per-folder part opens. Folders answered differently are said at once with the refusal that
follows (`R_HETEROGENEOUS_LAYOUT_NOT_ENABLED`); colour pictures beside separate-channel
images are still refused at the axes. Replacing answers already given asks first. The step is
done once the roles are confirmed and every folder's image type is answered.
- **Channels** is *Stains* and *Tag channels* on one page: name the stains, then tag.
- **Masks** holds the masks the check paired (with **Check the masks**, moved from step 1),
what each mask type is, and **Describe the masks**. The mask step reads the masks a run
recorded, so it stays greyed out, with its reason, until a run exists; after such a run the
run step says to come back for it. `A_MASK_UNCHECKED` is unchanged.
- **Design** is *Conditions*, *Replicates* and *Compared against* in one grid
(`notebook_design_step`): conditions down, biological repeats across, each cell the folder
of that repeat, and what each condition is compared against in the last column. The names
are the folder names, folders of the same name are one repeat, and "no comparison" is shown
for every condition: all proposals. **Looks right: confirm the design** adopts every ID as
shown (all or none, each refusal listed, as *Yes, use these IDs*) and records "no
comparison" where nothing else was chosen. A name that cannot be proposed blocks the click
with its reason and opens **Edit names or pairing…**, which holds the earlier rows.
*Check and save* waits for the click and says so. The grid is updated in place.
- **Check and save** lists at most eight missing answers at once; the others are one click
away (`notebook_check_view.TODO_SHOWN`). Every one is still listed.
- Smaller: the tiles of counts are one line of text in the stepper; mask pictures are
smaller; once the axes are confirmed the tick box and the "roles are valid" line make way
for the confirmation. Messages name the new steps (*Channels*, *Design*, *Axes*).
- The notebook's Markdown cell and the user guide §4 describe the seven steps (the stale
card-by-card text of §4 is removed). `RELEASE_REF` and the code cells are unchanged.
- **For bricks that embed the pages:** `welcome`, `folder_step`, `axis_step`, `channels_step`,
`masks_step`, `check_save_step`, `run_step`, `results_step` and the public methods are
unchanged (`tests/test_embedding_api.py` is untouched). Texts that changed on the stacked
pages too: the masks' "Before you run" items end in "(Masks)" instead of "(step 1)" /
"(step 1, Masks)"; the mask step's hint reads "Available after a run that recorded
masks: …"; its help paragraph starts with what it is needed for; a mask question without
labels says "declare them on Channels"; the missing-answer lines end in "(Channels)",
"(Design)" and "(Axes)".

### Notebook redesign 4: one column, less on screen; Compare groups in the run step

Owner test of v0.5.0 (10 November 2026), items (a) and (b), and the owner's test of the first
Expand Down
437 changes: 61 additions & 376 deletions docs/user_guide.md

Large diffs are not rendered by default.

15 changes: 9 additions & 6 deletions notebooks/MorphoBricks_Preprocessing.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -370,14 +370,17 @@
"\n",
"Opens the preprocessing app. Run cells 1 and 2 first: without them this cell stops and says which cell to run.\n",
"\n",
"In the app:\n",
"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. Give the **experiment folder** (masks and output folders are optional) and click **Scan the folders**. Look at the folders and the example image, then click **Looks right: read all image headers**.\n",
"2. Go through the steps one at a time with **Next** (or click a step in the strip at the top): axes and pixel size, conditions, replicates, comparisons, image type, stains, channel tags and masks.\n",
"3. On **Check and save**, read your experiment in words and save it; then **Run** computes the histograms.\n",
"4. After the run, the intensity checks and **Compare groups** are collapsed panels of the run step.\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",
"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",
"7. **Run**: computes the histograms; the intensity checks and **Compare groups** are collapsed panels after the run.\n",
"\n",
"Every wait shows its progress, and a greyed-out button says beside it what it waits for."
"What the app proposes is recorded only when you click. Every wait shows its progress, and a greyed-out button says beside it what it waits for."
]
},
{
Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ baseline = "tools/code_rules_baseline.json"
"morphobricks_preprocessing.notebook_axes" = "ui"
"morphobricks_preprocessing.notebook_progress" = "ui"
"morphobricks_preprocessing.notebook_compact" = "ui"
"morphobricks_preprocessing.notebook_design_step" = "ui"
"morphobricks_preprocessing.notebook_image_type_line" = "ui"
"morphobricks_preprocessing.notebook_quick_scan" = "ui"
"morphobricks_preprocessing.notebook_axes_quick" = "ui"
"morphobricks_preprocessing.notebook_channel_thumbs" = "ui"
Expand Down
8 changes: 6 additions & 2 deletions src/morphobricks_preprocessing/notebook_axes_quick.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,12 @@ def _refresh_quick_axes(self, drawn: bool = False) -> None:
for widget in (self.preview_checked.widget, self.preview_blocker, self.confirm_button,
self.confirm_blocker): # fmt: skip
ui.set_visible(widget, not offered)
# The offer says the roles are the headers' and nothing is open: the line is not repeated.
ui.set_visible(self.axis_status, not offered)
# The offer says the roles are the headers' and nothing is open: the line is not repeated;
# once confirmed, the confirmation says it, and Change axis roles takes it back.
confirmed = self.confirmed_mapping is not None
ui.set_visible(self.axis_status, not offered and not confirmed)
if confirmed:
ui.set_visible(self.preview_checked.widget, False)
if offer is None:
return
if offer.roles != self.mapping.roles:
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 @@ -408,11 +408,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 " (Image type)"
where = "" if self.place_types else " (Axes)"
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 Image type."
return "Colour is pre-ticked from the header: confirm it on Axes (Image type)."

def _pending(self, layout: UnitLayout) -> bool:
"""Return True when the unit shows a proposal not saved yet.
Expand Down
2 changes: 1 addition & 1 deletion src/morphobricks_preprocessing/notebook_check_save.py
Original file line number Diff line number Diff line change
Expand Up @@ -502,7 +502,7 @@ def save_experiment(self) -> bool:
self.save_status.value = ui.status(
"error",
"Not saved: a short name was refused.",
["See Conditions and Replicates (steps 4 and 5)."],
["See Design: the names are under Edit names or pairing."],
)
return False
if not self._save_description():
Expand Down
27 changes: 20 additions & 7 deletions src/morphobricks_preprocessing/notebook_check_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,15 @@
"settings). They are then one biological replicate, compared with each other."
)
QUESTION_WORDS = {
"id_pending": "a short name is missing for {subject} (Conditions or Replicates)",
"id_pending": "a short name is missing for {subject} (Design)",
"repeat_not_confirmed": "{subject}: answer “same way?” above",
"confirmed_on_missing": "{subject}: answer “same way?” again above",
"unit_channels_missing": "the channels of {subject} are not tagged (Tag channels)",
"unit_channel_untagged": "{subject} is not tagged (Tag channels)",
"channel_undeclared": "the label {subject} is used but not declared (Stains)",
"unit_image_type_unanswered": "the image type of {subject} is not answered (Image type)",
"unit_channels_missing": "the channels of {subject} are not tagged (Channels)",
"unit_channel_untagged": "{subject} is not tagged (Channels)",
"channel_undeclared": "the label {subject} is used but not declared (Channels)",
"unit_image_type_unanswered": "the image type of {subject} is not answered (Axes)",
"unit_channels_stale": (
"the tags of {subject} were saved for another image: tag its channels again (Tag channels)"
"the tags of {subject} were saved for another image: tag its channels again (Channels)"
),
"unit_channel_count_unknown": (
"the channels of {subject} are not known from its images: confirm the axis roles "
Expand Down Expand Up @@ -115,6 +115,10 @@ def names_html(names: Sequence[tuple[str, Sequence[str]]]) -> str:
)


TODO_SHOWN = 8
"""The most missing answers listed at once on *Check and save*; the others are folded."""


def todo_html(questions: Sequence[OpenQuestion], extra: Sequence[str]) -> str:
"""Return what is still to answer before saving, or nothing."""
lines = [
Expand All @@ -126,7 +130,16 @@ def todo_html(questions: Sequence[OpenQuestion], extra: Sequence[str]) -> str:
lines += list(extra)
if not lines:
return ""
return ui.status("warn", f"Still to answer before saving: {len(lines)}.", lines)
title = f"Still to answer before saving: {len(lines)}."
if len(lines) <= TODO_SHOWN:
return ui.status("warn", title, lines)
# Every line is still there: the first ones shown, the others one click away.
first = [html.escape(line) for line in lines[: TODO_SHOWN - 2]]
rest = "".join(f"<li>{html.escape(line)}</li>" for line in lines[TODO_SHOWN - 2 :])
more = (
f"<details><summary>and {len(lines) - len(first)} more</summary><ul>{rest}</ul></details>"
)
return ui.status("warn", title, [*first, more], raw=True)


def _issue_lines(issues: Sequence[ValidationIssue]) -> list[str]:
Expand Down
20 changes: 10 additions & 10 deletions src/morphobricks_preprocessing/notebook_compact.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,11 @@
"Optional; nothing is written. Statistics are <b>pooled</b> from the histograms of all "
"planes of a group and channel: a group's median is the median of all its pixels."
)
CHIPS = {
"Images found": "Images",
"Compared against": "Compared to",
"Tag channels": "Tags",
"Check and save": "Save",
"Describe the masks": "Describe masks",
}
MASKS_HINT = (
"This run recorded masks. Bricks that use masks accept only masks that were read once: "
"<b>Describe the masks</b> on Masks (step 4) reads them and reports how the types relate."
)
CHIPS = {"Check and save": "Save"}
"""The shorter names on the chips, so that the strip is one row on a laptop screen."""


Expand Down Expand Up @@ -78,20 +76,20 @@ def _images_page(self) -> list[widgets.Widget]:
self.folder_status, # the check's progress, right under the button clicked
self.folder_lock.card,
self.folder_tiles,
self.masks_card,
self.issues_card,
self.found_fold,
self.report_fold,
self.exclude_box,
]

def _run_page(self) -> list[widgets.Widget]:
"""Return the run's widgets (the mask step has its own step after it)."""
"""Return the run's widgets (the optional mask description is on the Masks step)."""
self.settings_fold = ui.accordion("Settings and mode", [self.settings_card])
self.plan_fold = ui.accordion("What will happen", [self.plan_card])
self.checks_fold = ui.accordion("Intensity checks and plots", [self.run_checks.card])
self.compare_fold = ui.accordion("Compare groups", [self._compare_box()])
self.run_report_fold = ui.accordion("Run report", [self.run_report.widget])
self.masks_hint = ui.help_text(MASKS_HINT) # after a run that recorded masks
self.log_text = widgets.HTML()
self.details.observe(lambda _: self._write_log_text(), names="value")
self.details_box.children = (self.log_text,)
Expand All @@ -107,6 +105,7 @@ def _run_page(self) -> list[widgets.Widget]:
self.run_lock.card,
self.run_tiles,
self.run_issues,
self.masks_hint,
self.checks_fold,
self.compare_fold,
self.run_report_fold,
Expand Down Expand Up @@ -163,6 +162,7 @@ def _refresh_folds(self) -> None:
ui.set_visible(self.checks_fold, written and ui.is_visible(self.run_checks.card))
ui.set_visible(self.compare_fold, written)
ui.set_visible(self.run_report_fold, bool(self.run_report.where.value))
ui.set_visible(self.masks_hint, written and not self.mask_step_button.disabled)


__all__ = ["CHIPS", "COMPARE_HELP", "FOLDERS_HELP", "FOLDER_INTRO", "_CompactPages"]
__all__ = ["CHIPS", "COMPARE_HELP", "FOLDERS_HELP", "FOLDER_INTRO", "MASKS_HINT", "_CompactPages"]
4 changes: 2 additions & 2 deletions src/morphobricks_preprocessing/notebook_described.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,8 +202,8 @@ def _described_image_type(self) -> str | None:

def _refresh_axis_next(self) -> None:
self.axis_next.value = ui.next_steps(
"Go to Stains.",
"Name what you stained, then Tag channels, Masks and Check and save.",
"Go to Channels.",
"Name what you stained and tag the channels, then Masks, Design and Check and save.",
)

# The run -----------------------------------------------------------------------------
Expand Down
8 changes: 6 additions & 2 deletions src/morphobricks_preprocessing/notebook_description.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,7 @@ def _build_references(self) -> None:
references = self.draft.references
for condition in conditions:
box = self.reference_boxes[condition]
options, chosen = self._reference_options(condition), self._chosen_reference(condition)
options, chosen = self._reference_options(condition), self._shown_reference(condition)
if list(box.options) != options:
box.options = options
if box.value != chosen:
Expand All @@ -361,7 +361,7 @@ def _new_reference_rows(self, conditions: list[str]) -> None:
for condition in conditions:
box = widgets.Dropdown(
options=self._reference_options(condition),
value=self._chosen_reference(condition),
value=self._shown_reference(condition),
layout=widgets.Layout(width="99%"),
)
box.observe(
Expand All @@ -386,6 +386,10 @@ def _chosen_reference(self, condition: str) -> str | None:
answer = self.draft_answers.conditions.get(condition, ConditionAnswer()).reference
return self.draft.references.get(condition) if answer is None else answer

def _shown_reference(self, condition: str) -> str | None:
"""Return what the dropdown shows: the saved choice (the stepper shows a proposal)."""
return self._chosen_reference(condition)

def _references_answered(self) -> bool:
"""Return True once any condition was answered (including "no comparison")."""
answers = self.draft_answers
Expand Down
Loading
Loading