Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ngio projection #866

Merged
merged 25 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
e55821e
add ngio dependency
lorenzocerrone Nov 15, 2024
605d469
ngio based re-implementation of projection task
lorenzocerrone Nov 15, 2024
5b46f01
update poetry.lock
lorenzocerrone Nov 15, 2024
dd6dba9
rm python=3.0 from ci
lorenzocerrone Nov 15, 2024
802bdb8
update changelog
lorenzocerrone Nov 15, 2024
3da52c8
pre-commit fix
lorenzocerrone Nov 15, 2024
c8b050a
Merge branch 'main' into ngio-projection
tcompa Nov 26, 2024
2d63e84
Pass new_plate_name from projection init task to projection image lis…
jluethi Dec 12, 2024
0155294
Add correct zarr file ending to plate name
jluethi Dec 12, 2024
a17d4bb
Fix roi.z settings
jluethi Dec 12, 2024
1adf2bd
Update manifest
jluethi Dec 12, 2024
da10279
Rename variable roi_table to roi_table_name
jluethi Dec 12, 2024
9e744e4
Remove remaining Python 3.9 CI actions
jluethi Dec 12, 2024
65f69be
Fix workflow test updated init args
jluethi Dec 12, 2024
b784936
Update expected projection metadata output
jluethi Dec 12, 2024
b2abce6
Ensure all table values are floats
jluethi Dec 12, 2024
f76134d
rm type_check blocks
lorenzocerrone Dec 16, 2024
77a9941
revert to tasks-core logger
lorenzocerrone Dec 16, 2024
2f03cfe
new table copy behaviour
lorenzocerrone Dec 16, 2024
cb67710
Merge branch 'main' into ngio-projection
lorenzocerrone Dec 17, 2024
387aaae
minor logging changes
lorenzocerrone Dec 17, 2024
aacb3e2
update pyproject and lock files
lorenzocerrone Dec 17, 2024
6bbc5bf
Merge branch 'ngio-projection' of github.com:fractal-analytics-platfo…
lorenzocerrone Dec 17, 2024
bc25920
Merge branch 'main' into ngio-projection
tcompa Dec 18, 2024
4c12f2b
Add extra
tcompa Dec 19, 2024
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
8 changes: 2 additions & 6 deletions .github/workflows/ci_pip.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,8 @@ jobs:
strategy:
matrix:
os: [ubuntu-22.04, macos-latest]
python-version: ["3.9", "3.10", "3.11", "3.12"]
python-version: ["3.10", "3.11", "3.12"]
exclude:
- os: macos-latest
python-version: '3.9'
- os: macos-latest
python-version: '3.10'
name: "Core, Python ${{ matrix.python-version }}, ${{ matrix.os }}"
Expand Down Expand Up @@ -50,10 +48,8 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
python-version: ["3.9", "3.10", "3.11", "3.12"]
python-version: ["3.10", "3.11", "3.12"]
exclude:
- os: macos-latest
python-version: '3.9'
- os: macos-latest
python-version: '3.10'
name: "Tasks, Python ${{ matrix.python-version }}, ${{ matrix.os }}"
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/ci_poetry.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:

strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12"]
python-version: ["3.10", "3.11", "3.12"]
jluethi marked this conversation as resolved.
Show resolved Hide resolved

steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -60,7 +60,7 @@ jobs:

strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12"]
python-version: ["3.10", "3.11", "3.12"]

steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -131,7 +131,7 @@ jobs:
python-version: "3.10"

- name: Install dependencies
run: poetry install --only dev
run: poetry install --with dev -E fractal-tasks

- name: Download data
uses: actions/download-artifact@v4
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
**Note**: Numbers like (\#123) point to closed Pull Requests on the fractal-tasks-core repository.


# Unreleased

* Tasks:
* Refactor projection task to use ngio
* Dependencies:
* Add `ngio==0.1.4` to the dependencies
* Require `python >=3.10,<3.13`
* CI:
* Remove Python 3.9 from the CI matrix
* Tests:
* Use locked version of `coverage` in GitHub action (\#882).
* Bump `coverage` version from 6.5 to 7.6 (\#882).
Expand All @@ -13,6 +21,7 @@
* Support providing `docs_info=file:task_info/description.md` (\#876).
* Deprecate `check_manifest.py` module, in favor of additional GitHub action steps (\#876).


# 1.3.3

* Add new metadata (authors, category, modality, tags) to manifest models and to tasks (\#855).
Expand Down
2 changes: 1 addition & 1 deletion docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ poetry run pytest --ignore tests/tasks

The tests files are in the `tests` folder of the repository. Its structure reflects the `fractal_tasks_core` structure, with tests for the core library in the main folder and tests for `tasks` and `dev` subpckages in their own subfolders.

Tests are also run through GitHub Actions, with Python 3.9, 3.10 and 3.11. Note that within GitHub actions we run tests for both the `poetry`-installed and `pip`-installed versions of the code, which may e.g. have different versions of some dependencies (since `pip install` does not rely on the `poetry.lock` lockfile).
Tests are also run through GitHub Actions, with Python 3.10, 3.11 and 3.12. Note that within GitHub actions we run tests for both the `poetry`-installed and `pip`-installed versions of the code, which may e.g. have different versions of some dependencies (since `pip install` does not rely on the `poetry.lock` lockfile).

## Documentation

Expand Down
7 changes: 6 additions & 1 deletion fractal_tasks_core/__FRACTAL_MANIFEST__.json
Original file line number Diff line number Diff line change
Expand Up @@ -653,12 +653,17 @@
"overwrite": {
"title": "Overwrite",
"type": "boolean"
},
"new_plate_name": {
"title": "New Plate Name",
"type": "string"
}
},
"required": [
"origin_url",
"method",
"overwrite"
"overwrite",
"new_plate_name"
],
"title": "InitArgsMIP",
"type": "object"
Expand Down
5 changes: 4 additions & 1 deletion fractal_tasks_core/tasks/copy_ome_zarr_hcs_plate.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,10 @@ def copy_ome_zarr_hcs_plate(
parallelization_item = dict(
zarr_url=new_zarr_url,
init_args=dict(
origin_url=zarr_url, method=method.value, overwrite=overwrite
origin_url=zarr_url,
method=method.value,
overwrite=overwrite,
new_plate_name=f"{new_plate_name}.zarr",
),
)
InitArgsMIP(**parallelization_item["init_args"])
Expand Down
2 changes: 2 additions & 0 deletions fractal_tasks_core/tasks/io_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,13 @@ class InitArgsMIP(BaseModel):
origin_url: Path to the zarr_url with the 3D data
method: Projection method to be used. See `DaskProjectionMethod`
overwrite: If `True`, overwrite the task output.
new_plate_name: Name of the new OME-Zarr HCS plate
"""

origin_url: str
method: str
overwrite: bool
new_plate_name: str


class MultiplexingAcquisition(BaseModel):
Expand Down
188 changes: 71 additions & 117 deletions fractal_tasks_core/tasks/projection.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,30 +12,39 @@
"""
Task for 3D->2D maximum-intensity projection.
"""
from __future__ import annotations

import logging
from typing import Any

import anndata as ad
import dask.array as da
import zarr
from ngio import NgffImage
from ngio.core import Image
from pydantic import validate_call
from zarr.errors import ContainsArrayError

from fractal_tasks_core.ngff import load_NgffImageMeta
from fractal_tasks_core.pyramids import build_pyramid
from fractal_tasks_core.roi import (
convert_ROIs_from_3D_to_2D,
)
from fractal_tasks_core.tables import write_table
from fractal_tasks_core.tables.v1 import get_tables_list_v1

from fractal_tasks_core.tasks.io_models import InitArgsMIP
from fractal_tasks_core.tasks.projection_utils import DaskProjectionMethod
from fractal_tasks_core.zarr_utils import OverwriteNotAllowedError


logger = logging.getLogger(__name__)


def _compute_new_shape(source_image: Image) -> tuple[int]:
"""Compute the new shape of the image after the projection.

The new shape is the same as the original one,
except for the z-axis, which is set to 1.
"""
on_disk_shape = source_image.on_disk_shape
logger.info(f"Source {on_disk_shape=}")

on_disk_z_index = source_image.dataset.on_disk_axes_names.index("z")

dest_on_disk_shape = list(on_disk_shape)
dest_on_disk_shape[on_disk_z_index] = 1
logger.info(f"Destination {dest_on_disk_shape=}")
return tuple(dest_on_disk_shape)


@validate_call
def projection(
*,
Expand All @@ -60,123 +69,68 @@
logger.info(f"{method=}")

# Read image metadata
ngff_image = load_NgffImageMeta(init_args.origin_url)
# Currently not using the validation models due to wavelength_id issue
# See #681 for discussion
# new_attrs = ngff_image.model_dump(exclude_none=True)
# Current way to get the necessary metadata for MIP
group = zarr.open_group(init_args.origin_url, mode="r")
new_attrs = group.attrs.asdict()

# Create the zarr image with correct
new_image_group = zarr.group(zarr_url)
new_image_group.attrs.put(new_attrs)

# Load 0-th level
data_czyx = da.from_zarr(init_args.origin_url + "/0")
num_channels = data_czyx.shape[0]
chunksize_y = data_czyx.chunksize[-2]
chunksize_x = data_czyx.chunksize[-1]
logger.info(f"{num_channels=}")
logger.info(f"{chunksize_y=}")
logger.info(f"{chunksize_x=}")

# Loop over channels
accumulate_chl = []
for ind_ch in range(num_channels):
# Perform MIP for each channel of level 0
project_yx = da.stack(
[method.apply(data_czyx[ind_ch], axis=0)], axis=0
)
accumulate_chl.append(project_yx)
accumulated_array = da.stack(accumulate_chl, axis=0)

# Write to disk (triggering execution)
try:
accumulated_array.to_zarr(
f"{zarr_url}/0",
overwrite=init_args.overwrite,
dimension_separator="/",
write_empty_chunks=False,
)
except ContainsArrayError as e:
error_msg = (
f"Cannot write array to zarr group at '{zarr_url}/0', "
f"with {init_args.overwrite=} (original error: {str(e)}).\n"
"Hint: try setting overwrite=True."
original_ngff_image = NgffImage(init_args.origin_url)
orginal_image = original_ngff_image.get_image()

if orginal_image.is_2d or orginal_image.is_2d_time_series:
raise ValueError(

Check notice on line 76 in fractal_tasks_core/tasks/projection.py

View workflow job for this annotation

GitHub Actions / Coverage

Missing coverage

Missing coverage on line 76
"The input image is 2D, "
"projection is only supported for 3D images."
)
logger.error(error_msg)
raise OverwriteNotAllowedError(error_msg)

# Starting from on-disk highest-resolution data, build and write to disk a
# pyramid of coarser levels
build_pyramid(
zarrurl=zarr_url,
# Compute the new shape and pixel size
dest_on_disk_shape = _compute_new_shape(orginal_image)

dest_pixel_size = orginal_image.pixel_size
dest_pixel_size.z = 1.0
logger.info(f"New shape: {dest_on_disk_shape=}")

# Create the new empty image
new_ngff_image = original_ngff_image.derive_new_image(
store=zarr_url,
name="MIP",
on_disk_shape=dest_on_disk_shape,
pixel_sizes=dest_pixel_size,
overwrite=init_args.overwrite,
num_levels=ngff_image.num_levels,
coarsening_xy=ngff_image.coarsening_xy,
chunksize=(1, 1, chunksize_y, chunksize_x),
copy_labels=False,
copy_tables=True,
)
logger.info(f"New Projection image created - {new_ngff_image=}")
new_image = new_ngff_image.get_image()

# Copy over any tables from the original zarr
# Generate the list of tables:
tables = get_tables_list_v1(init_args.origin_url)
roi_tables = get_tables_list_v1(init_args.origin_url, table_type="ROIs")
non_roi_tables = [table for table in tables if table not in roi_tables]

for table in roi_tables:
logger.info(
f"Reading {table} from "
f"{init_args.origin_url=}, convert it to 2D, and "
"write it back to the new zarr file."
)
new_ROI_table = ad.read_zarr(f"{init_args.origin_url}/tables/{table}")
old_ROI_table_attrs = zarr.open_group(
f"{init_args.origin_url}/tables/{table}"
).attrs.asdict()

# Convert 3D ROIs to 2D
pxl_sizes_zyx = ngff_image.get_pixel_sizes_zyx(level=0)
new_ROI_table = convert_ROIs_from_3D_to_2D(
new_ROI_table, pixel_size_z=pxl_sizes_zyx[0]
)
# Write new table
write_table(
new_image_group,
table,
new_ROI_table,
table_attrs=old_ROI_table_attrs,
overwrite=init_args.overwrite,
)
# Process the image
z_axis_index = orginal_image.find_axis("z")
source_dask = orginal_image.get_array(
mode="dask", preserve_dimensions=True
)

for table in non_roi_tables:
logger.info(
f"Reading {table} from "
f"{init_args.origin_url=}, and "
"write it back to the new zarr file."
)
new_non_ROI_table = ad.read_zarr(
f"{init_args.origin_url}/tables/{table}"
)
old_non_ROI_table_attrs = zarr.open_group(
f"{init_args.origin_url}/tables/{table}"
).attrs.asdict()

# Write new table
write_table(
new_image_group,
table,
new_non_ROI_table,
table_attrs=old_non_ROI_table_attrs,
overwrite=init_args.overwrite,
)
dest_dask = method.apply(dask_array=source_dask, axis=z_axis_index)
dest_dask = da.expand_dims(dest_dask, axis=z_axis_index)
new_image.set_array(dest_dask)
new_image.consolidate()
# Ends

# Copy over the tables
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naive question by someone who is not involved in ngio development/integration: where does this feature fit best?

Would it make sense to have something like

ngio.copy_tables(original_ngff_image, new_ngff_image, project_z=True)

or at least

ngio.copy_tables(original_ngff_image, new_ngff_image)
# and then set `z_length` manually

or would it be just additional complexity?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The responsability of copying the tables is now moved to the NgffImage.derive_new_image method.

for roi_table_name in new_ngff_image.tables.list(table_type="roi_table"):
table = new_ngff_image.tables.get_table(roi_table_name)

roi_list = []
for roi in table.rois:
roi.z = 0.0
roi.z_length = 1.0
roi_list.append(roi)

table.set_rois(roi_list, overwrite=True)
table.consolidate()
logger.info(f"Table {roi_table_name} Projection done")

# Generate image_list_updates
image_list_update_dict = dict(
image_list_updates=[
dict(
zarr_url=zarr_url,
origin=init_args.origin_url,
attributes=dict(plate=init_args.new_plate_name),
types=dict(is_3D=False),
)
]
Expand Down
Loading
Loading