Skip to content
Merged
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -601,7 +601,7 @@ Technical details go to `logging` (shown in the notebook's "Technical details" p
| Significant bits (Q4) | Manifest only, information only; `bit_depth` and binning unchanged |
| Corrupt files (Q6) | Explicit exclusion list, recorded in manifest, rows removed |
| Notebook target | Google Colab and JupyterLab from one file, following LabConstrictor's notebook portability guide (plan Q58; was local Jupyter only): `# @title` cells, Colab-only code (Drive mount, custom widget manager) inside `if "google.colab" in sys.modules:`, no magics, GitHub install pinned to `RELEASE_TAG` |
| CLI | `check <folder>` (summary, label tree, issues, suggested `--axes`) and `run <folder> --axes ROLES` with `--output`, `--exclude`, `--bins`, `--histogram-range`, `--robust-quantiles`, `--memory-limit 2GiB`, `--missing-as-singleton`, `--mode restart|full`, `--no-masks`, `--no-exclude`, `--remove-stale-lock`, `--log`, and the conversion options above. Axis roles are always stated explicitly, never taken silently from metadata. Exit 1 on errors |
| CLI | `check <folder>` (summary, label tree, issues, suggested `--axes`) and `run <folder> --axes ROLES` with `--output`, `--exclude`, `--bins`, `--histogram-range`, `--robust-quantiles`, `--memory-limit 2GiB`, `--missing-as-singleton`, `--mode restart|full`, `--no-masks`, `--no-exclude`, `--remove-stale-lock`, `--log`, and the conversion options above. Axis roles are always stated explicitly, never taken silently from metadata (amended 3 November 2026, plan §12 question 73: when every image names all its axes, `run` uses those roles without `--axes` and prints them as the images name them; plain stacks still need `--axes`). Exit 1 on errors |
| Large planes (Q15) | Planes over the memory limit are read in blocks automatically, announced by the warning `planes_read_in_blocks` and `read_in_blocks` in the manifest. Block size derived from the limit (whole strips/tiles/chunks, uncompressed pieces and chunks also by rows). Compressed single-strip/tile planes keep the `plane_over_memory_limit` error. Two-pass histograms identical to whole planes. (Up to v0.3, OME-Zarr copies were written block by block; conversion was removed in v0.4.0.) Axis preview reduced (every n-th pixel) with a caption saying so |
| Normalization brick (P1) | The modules `dtypes`, `histogram`, `records`, `csv_io`, `pooling` and `normalization` moved unchanged to `morphobricks_normalization` (normalization plan N1, N2, N6) and are imported from there. Up to v0.3 their public names stayed importable from `morphobricks_preprocessing` and the old submodules, with a `DeprecationWarning`; those stand-ins (`_moved.py`) were removed in v0.4.0 (plan Q46). The package's own code never uses the old paths. Outputs unchanged (CSV schema 2); the manifest's `versions` adds `morphobricks-normalization`; Step 0 checks for it; contract §3–§7 text moved to the normalization contract (§1.2, now §6.2 for the CSV) |
| Dataset consistency (Q21, P2) | Step 1 "Dataset consistency" panel: balance table (images and technical-repeat folders per condition x biological repeat), `unbalanced_images` (< 0.5 or > 2 x median), `repeat_missing_conditions`, `different_pixel_sizes`, `missing_pixel_size` (OME-Zarr scale included), `different_plane_counts`; `different_sizes` narrowed to width/height; consistency issues shown only in that panel, not in Messages. Warnings only; outputs unchanged |
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,16 @@ and the bricks' migration).

### Fixed

- Cross-brick test of 3 November 2026 (plan §12 question 73): `masks` accepts per-plane masks
with a stated shared 2-D mask (`--mask-axes Nuclei=YX`) as `run` does, instead of
`mask_planes_differ` (F1); `run` needs no `--axes` when every image names its axes (F9);
a rerun that changes nothing leaves `run_manifest.json` byte-identical, keeping its
`written_at`, so pinned manifest SHA-256s stay valid (F10); `output_locked` says a lock
left by a process no longer running on this computer looks stale and how to remove it
(also `convert`'s, for the folder of the copies; it is never removed on its own), `run`
without `--axes` names the images' differing arrangements instead of `--axes None`, and a resumed run no longer says all rows were computed (F11); `convert --output` names
the runs' output folder in the printed next commands, and `inconsistent_dimensions` says
that named-axis copies keep each image's order (F8).
- Preprocessing audit of 3 November 2026 (plan §12 question 72): `read_plane` and
`RunInputs.read_plane` no longer count histogram memory, so a 5000² float32 plane is read
within the 1 GiB limit (I2); a header size in an unsupported unit (OME `pixel`) or without
Expand Down
14 changes: 8 additions & 6 deletions docs/contract.md

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions docs/migration-0.3-to-0.4.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,11 @@ later bricks on the new run.
is refused).
- `different_sizes` and `different_plane_counts` compare the images by their confirmed roles
once the roles are confirmed.
- `run` needs `--axes` only when some images are plain stacks; images that all name their
axes are run with those roles. A rerun that changes nothing keeps `run_manifest.json`
byte-identical (its `written_at` too). `convert` takes `--output`, the output folder of
your runs on the originals, for the next commands it prints (cross-brick test, plan §12
question 73).
- The notebook runs on Google Colab and in JupyterLab. It installs the pinned release
(`RELEASE_TAG`), never `main`.

Expand Down
25 changes: 25 additions & 0 deletions docs/plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -1834,3 +1834,28 @@ Raised during review; to be decided before the milestones that need them:
an ImageJ resolution tag of another type is `invalid_physical_calibration`, and an
OME-Zarr scale without a unit is named by `unsupported_pixel_size_unit` ("without a
unit"); `same_pixel_size` takes NumPy real scalars as m4 does.
73. How are the findings F1, F8, F9, F10 and F11 of the cross-brick test of 3 November 2026
fixed? *Decided (3 November 2026, owner: fix before the round-2 audit; the choices in
F8 are the conservative ones, within design v0.4 §3 and §5; contract §9, §15, §17):*
(F1) `scan_masks` leaves a stated shared 2-D mask out of the comparison of an image's
masks' planes, as `run` does, and compares it with each plane of the other masks; never
across planes. (F9) `run` needs no `--axes` when every readable image names all its axes
and the suggested mapping gives each a role; a plain stack still needs it; `convert`
always needs it. (F10) a rerun whose manifest differs only in `written_at` keeps the
earlier time, so the manifest's bytes and SHA-256 stay the same; any other change writes
the new time. (F11) the `output_locked` message says that a lock left by a process on
this computer that is no longer running looks stale, and how to remove it (also
`convert`'s, for the folder of the copies); it is never removed automatically. A resumed run's summary says that the staged rows were resumed,
not that all rows were computed. (F8a) `convert` keeps each image's stored order of
dimensions (§17): writing copies in the experiment's common order would be a pixel-exact
transposition under named roles, but it changes §17's copy axes and is left as an open
question for the owner; for now `inconsistent_dimensions` and `inconsistent_mapping` say
that named-axis copies do not solve them. (F8b) `convert --output FOLDER` names the output
folder of the runs on the originals in the printed next commands (and is where the
recorded masks folder is looked up); without it, the default folder is named with what
to do if the runs used another one. (F8c) a plain stack whose pages interleave several
dimensions (`IYX`, no shape metadata) cannot be converted: a reshape is not a role, and
the spec has no way to state one, so the user guide says to save the image again with
its dimensions (for example as a Fiji hyperstack). Follow-up for the owner: whether
`convert` should accept a stated reshape (for example `--pages CZ=2x3`) or reorder copies
into the common order.
37 changes: 28 additions & 9 deletions docs/user_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -546,8 +546,10 @@ morphobricks-preprocessing run /data/exp1 --axes ZCYX
```

`--axes` gives one role per dimension of the images, in their order: `X`, `Y`, `Z`, `C`,
`T`, or `-` for a length-1 dimension without a role. Roles are never taken from metadata
without being stated. A later `run` in the same output folder needs neither the roles nor
`T`, or `-` for a length-1 dimension without a role. It is needed only when some images are
plain stacks, whose metadata do not name their axes: when every image names all its axes,
`run` uses those roles without `--axes` (it prints **Axis roles (as every image names
them)**; design v0.4 §3), and they are still checked as below. A later `run` in the same output folder needs neither the roles nor
the experiment folder again, unless plain stacks were added or changed:

```bash
Expand Down Expand Up @@ -609,7 +611,9 @@ A later `run` in the same output folder reuses everything the earlier run record
axis roles (`--axes` is needed again only for plain stacks that are new or changed), the
masks folder and mask axes, the exclusions, the image type, the bins and the histogram and
intensity settings. An option given that differs from the recorded value is used, and the
report names the change and its effect. Other options:
report names the change and its effect. A rerun that changes nothing leaves
`run_manifest.json` exactly as it was, its `written_at` included, so bricks that recorded its
SHA-256 still accept it; the report records the rerun. Other options:

| Option | Meaning |
|---|---|
Expand Down Expand Up @@ -750,7 +754,7 @@ When the metadata of some images name their axes wrongly (the errors `axes_yx_sw
copies with your roles, then run preprocessing on the copies:

```bash
morphobricks-preprocessing convert /data/exp1 --to /data/exp1_named_axes --axes ZXY
morphobricks-preprocessing convert /data/exp1 --to /data/exp1_named_axes --axes ZXY --output /data/exp1_MorphoBricks_Preprocessing
morphobricks-preprocessing check /data/exp1_named_axes --output /data/exp1_MorphoBricks_Preprocessing
morphobricks-preprocessing run /data/exp1_named_axes --output /data/exp1_MorphoBricks_Preprocessing --axes ZYX
```
Expand All @@ -773,7 +777,20 @@ which is then kept; a depth step is never written as a time step. State the miss
in `calibration.csv` if you know them (`calibrate`). Images whose planes cannot be read
within the memory limit, and a folder without the space for the copies, are refused
before anything is written. The command prints what it
wrote and the next commands, with the suggested `--axes` for the copies.
wrote and the next commands, with the suggested `--axes` for the copies and the output
folder of your runs on the originals: give it with `--output` (without it the default
output folder is named, which only works if your runs used it). `--output` is also where
the masks folder is looked up when `--masks` is not given.

`convert` keeps each image's stored order of dimensions (`ZXY` stays three dimensions in
that order, with your roles): it does not reorder images, so it cannot solve
`inconsistent_dimensions` (images arranged differently, for example `CZYX` among `ZCYX`);
export those images again in the same order, or process them as a separate experiment
(plan §12 question 73). A plain stack that holds several dimensions in one sequence of pages
(for example channels and depth slices interleaved, read as `IYX`) cannot be converted
either: one role per stored dimension cannot say how the pages split, and the brick never
guesses it. Save it again with its dimensions (for example as a hyperstack in Fiji, which
writes ImageJ metadata naming them), then check it again.

Give `run` the output folder of your runs on the originals: the design, image type and
stated pixel sizes are kept (the copies have the same names), the report says the
Expand Down Expand Up @@ -919,7 +936,9 @@ with `chmod a+r <output folder>/*`, or delete them before running again.
Only one run at a time can use an output folder: a run holds a lock file
(`.morphobricks-preprocessing.lock`) and a second run is refused (`output_locked`). If the
process is forcibly stopped, its lock stays; remove it with `--remove-stale-lock` once you
are sure no run is going on (every command takes it). `check` holds the lock
are sure no run is going on (every command takes it). When the lock was left by a process on
this computer that is no longer running, the message says the lock looks stale; it is still
never removed without your request. `check` holds the lock
while it writes its report, `masks` (and **Describe the masks**) from before the first
mask is read until `mask_relationships.json` is written (a locked folder is refused before
any mask is read), `design` and `calibrate` while they change their files,
Expand Down Expand Up @@ -983,7 +1002,7 @@ continues.
| `unsupported_dtype` | Error | Unsupported pixel type (e.g. float16). Convert, or exclude it. |
| `mixed_dtypes` | Error | Images have different pixel types. Process them separately or convert. |
| `different_channel_counts` | Error | Images have different numbers of channels (an image without a channel dimension has one). Every image of an experiment must have the same channels: split the others into a separate experiment, or exclude them. If a dimension got the channel role by mistake, correct the axis roles. |
| `inconsistent_dimensions`, `inconsistent_mapping` | Error | Images have their dimensions in a different order. Export them the same way or process separately. |
| `inconsistent_dimensions`, `inconsistent_mapping` | Error | Images have their dimensions in a different order. Export them the same way or process separately; named-axis copies (`convert`) keep each image's order, so they do not solve this. |
| `missing_axes` | Error | Some images lack a dimension. Confirm they have one plane along it (step 2), or export them again. |
| `ambiguous_missing_axes` | Error | It is unclear which dimension an image lacks. Export it again. |
| `axes_not_named` | Warning | The metadata of some images does not name their axes (a plain stack of pages, `IYX` or `QYX`), so which dimension is depth, time or channel comes only from the roles you confirm. The images are read with them, and the roles are recorded as confirmed by you. Check the roles with the preview. Before segmenting these images in other tools, consider saving named-axis copies (OME-TIFF) with the `convert` command (§5), so every tool reads the same axes. |
Expand All @@ -1000,7 +1019,7 @@ continues.
| `invalid_bin_count` | Error | Bin count outside the allowed range. Use the default or a value in range. |
| `experiment_not_recorded` | Error | `run` was given no experiment folder, and the output folder holds no v0.4.0 run that records one (or no output folder was given). Give the experiment folder. |
| `axes_not_confirmed` | Error | No axis roles were given and the output folder holds no earlier run that records them. Confirm the roles (`--axes`). |
| `output_locked` | Error | Another run is using the output folder; it is named with its computer, process and start time. Wait for it to finish; if it was stopped or the computer restarted, remove its lock (`--remove-stale-lock`). Nothing was written. |
| `output_locked` | Error | Another run is using the output folder; it is named with its computer, process and start time (a process on this computer that is no longer running is named as a lock that looks stale). Wait for it to finish; if it was stopped or the computer restarted, remove its lock (`--remove-stale-lock`). Nothing was written. |
| `copy_folder_overlaps_experiment` | Error | `convert`: the folder for the copies is the experiment folder, inside it, or holds it. Choose a new folder outside the experiment. |
| `copy_folder_overlaps_masks` | Error | `convert`: the folder for the copies is the masks folder, inside it, or holds it; copies there would be read as masks. Choose a folder outside the masks folder. |
| `copy_folder_not_a_folder` | Error | `convert`: the path given with `--to` is a file. Choose a folder. |
Expand Down Expand Up @@ -1061,7 +1080,7 @@ continues.
| `mask_not_2d` | Error | A mask has dimensions other than Y and X that its metadata does not name as depth (Z) or time (T), for example a plain stack. (A mask one pixel high or wide is a 2D mask, not this error.) State the axes of its mask type once (`--mask-axes Cells=TYX`), or save it as a 2D label image, or with its axes named (an ImageJ hyperstack or OME-TIFF with axes TYX or ZYX). |
| `invalid_mask_axes`, `mask_axes_contradict_header` | Error | Axes stated for a mask type do not fit its masks (one role, Y, X, Z or T, per axis; Y and X once each), name a type that is not found, or contradict the axes a mask's metadata names. Correct the statement. |
| `mask_layout` | Error | A mask is stored with its Y and X the other way round from its image as read (its header names X before Y, for example `XY`, named in the message with the mask; or the image's height and width swapped). Masks are never transposed: save it again with `numpy.swapaxes(mask, -1, -2)`. |
| `mask_planes_differ` | Error | The masks of one image have different numbers of depth slices or time points. Make every mask of an image with the image's planes. |
| `mask_planes_differ` | Error | The masks of one image have different numbers of depth slices or time points. Make every mask of an image with the image's planes. A 2-D mask stated as shared (`--mask-axes Nuclei=YX`) is one plane for every plane and is left out of this check, by `run` and by `masks` alike. |
| `mask_planes_mismatch` | Error (`run`) | A mask does not have the depth slices and time points of its image under the confirmed axis roles: a 2D mask fits only an image with one plane (not a Z-stack or time-lapse) unless its type is stated `YX` (`--mask-axes Cells=YX`: one mask shared by every plane), and a mask with Z or T needs as many slices or time points as its image. Save one mask plane per image plane, with the axes named, or check the axis roles. |
| `mask_size_mismatch` | Error | A mask does not have the width and height of its image. Check that it was made from that image at full resolution. |
| `mask_pixel_type` | Error | A mask's pixels are not whole numbers (for example decimals). Save it as an integer label image. |
Expand Down
4 changes: 2 additions & 2 deletions src/morphobricks_preprocessing/axis_review.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
TiffMetadata,
channel_count_issues,
)
from .size_checks import CONVERT_KEEPS_ORDER
from .validation import ValidationIssue, error, plural


Expand Down Expand Up @@ -222,8 +223,7 @@ def review_axes(
f"{plural(len(misfits), 'image')}: their dimensions are arranged "
"differently.",
misfits,
"Export the images again with the same dimension order, or process the "
"differently arranged images as a separate experiment.",
CONVERT_KEEPS_ORDER,
)
)
issues.extend(
Expand Down
Loading
Loading