From 9e0bc160e3e3c8bce375c5ee95935d75ccf0e709 Mon Sep 17 00:00:00 2001 From: Aaron Alvarez Date: Mon, 1 Jul 2024 17:07:50 -0700 Subject: [PATCH 01/38] add update_scale_metadata.py --- iohub/update_scale_metadata.py | 46 ++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 iohub/update_scale_metadata.py diff --git a/iohub/update_scale_metadata.py b/iohub/update_scale_metadata.py new file mode 100644 index 00000000..d7a660d3 --- /dev/null +++ b/iohub/update_scale_metadata.py @@ -0,0 +1,46 @@ +from typing import List + +import click + +from iohub import open_ome_zarr +from iohub.ngff_meta import TransformationMeta + +from mantis.cli.parsing import input_position_dirpaths + + +@click.command() +@input_position_dirpaths() +def update_scale_metadata( + input_position_dirpaths: List[str], +): + with open_ome_zarr(input_position_dirpaths[0]) as input_dataset: + print( + f"The first dataset in the list you provided has (z, y, x) scale {input_dataset.scale[2:]}" + ) + + print( + "Please enter the new z, y, and x scales that you would like to apply to all of the positions in the list." + ) + print( + "The old scale will be saved in a metadata field named 'old_scale', and the new scale will adhere to the NGFF spec." + ) + new_scale = [] + for character in "zyx": + new_scale.append(float(input(f"Enter a new {character} scale: "))) + + for input_position_dirpath in input_position_dirpaths: + with open_ome_zarr(input_position_dirpath, layout="fov", mode="a") as input_dataset: + input_dataset.zattrs['old_scale'] = input_dataset.scale[2:] + transform = [ + TransformationMeta( + type="scale", + scale=( + 1, + 1, + ) + + tuple(new_scale), + ) + ] + input_dataset.set_transform("0", transform=transform) + + print(f"The dataset now has (z, y, x) scale {tuple(new_scale)}.") \ No newline at end of file From 59ab2ac8b098148bccf35ca3b2bb5678fdbff661 Mon Sep 17 00:00:00 2001 From: Aaron Alvarez Date: Mon, 1 Jul 2024 17:33:31 -0700 Subject: [PATCH 02/38] add update-scale-metadata command to cli --- iohub/cli/cli.py | 16 ++++++++++++++++ iohub/update_scale_metadata.py | 6 ------ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/iohub/cli/cli.py b/iohub/cli/cli.py index befdeb3d..6f2ac1b7 100644 --- a/iohub/cli/cli.py +++ b/iohub/cli/cli.py @@ -3,6 +3,7 @@ from iohub._version import __version__ from iohub.convert import TIFFConverter from iohub.reader import print_info +from iohub.update_scale_metadata import update_scale_metadata as _update_scale_metadata VERSION = __version__ @@ -117,3 +118,18 @@ def convert( chunks=chunks, ) converter.run(check_image=check_image) + + +@cli.command() +@click.help_option("-h", "--help") +@click.argument( + "files", + nargs=-1, + required=True, + type=_DATASET_PATH, +) +def update_scale_metadata( + files +): + """Update scale metadata in OME-Zarr datasets""" + _update_scale_metadata(files) \ No newline at end of file diff --git a/iohub/update_scale_metadata.py b/iohub/update_scale_metadata.py index d7a660d3..3277131b 100644 --- a/iohub/update_scale_metadata.py +++ b/iohub/update_scale_metadata.py @@ -1,15 +1,9 @@ from typing import List -import click - from iohub import open_ome_zarr from iohub.ngff_meta import TransformationMeta -from mantis.cli.parsing import input_position_dirpaths - -@click.command() -@input_position_dirpaths() def update_scale_metadata( input_position_dirpaths: List[str], ): From 6217bad6c4468100104467f06ffacc6a33970a69 Mon Sep 17 00:00:00 2001 From: Aaron Alvarez Date: Tue, 2 Jul 2024 15:10:44 -0700 Subject: [PATCH 03/38] add zyx flags for update-scale-metadata utility --- iohub/cli/cli.py | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/iohub/cli/cli.py b/iohub/cli/cli.py index 6f2ac1b7..20700ccc 100644 --- a/iohub/cli/cli.py +++ b/iohub/cli/cli.py @@ -4,6 +4,7 @@ from iohub.convert import TIFFConverter from iohub.reader import print_info from iohub.update_scale_metadata import update_scale_metadata as _update_scale_metadata +from mantis.cli.parsing import input_position_dirpaths VERSION = __version__ @@ -122,14 +123,28 @@ def convert( @cli.command() @click.help_option("-h", "--help") -@click.argument( - "files", - nargs=-1, - required=True, - type=_DATASET_PATH, +@input_position_dirpaths() +@click.option( + "--x-scale", + "-x", + required=False, + type=float, + help="New x scale", ) -def update_scale_metadata( - files -): +@click.option( + "--y-scale", + "-y", + required=False, + type=float, + help="New y scale", +) +@click.option( + "--z-scale", + "-z", + required=False, + type=float, + help="New z scale", +) +def update_scale_metadata(input_position_dirpaths, z_scale, y_scale, x_scale): """Update scale metadata in OME-Zarr datasets""" - _update_scale_metadata(files) \ No newline at end of file + _update_scale_metadata(input_position_dirpaths, z_scale=z_scale, y_scale=y_scale, x_scale=x_scale) \ No newline at end of file From 7bda4b383d1722e078846b9651fa09563e421f6a Mon Sep 17 00:00:00 2001 From: Aaron Alvarez Date: Tue, 2 Jul 2024 15:11:38 -0700 Subject: [PATCH 04/38] add handling of missing zyx cli flags for update-scale-metadata --- iohub/update_scale_metadata.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/iohub/update_scale_metadata.py b/iohub/update_scale_metadata.py index 3277131b..733b1577 100644 --- a/iohub/update_scale_metadata.py +++ b/iohub/update_scale_metadata.py @@ -6,6 +6,9 @@ def update_scale_metadata( input_position_dirpaths: List[str], + x_scale: float = None, + y_scale: float = None, + z_scale: float = None, ): with open_ome_zarr(input_position_dirpaths[0]) as input_dataset: print( @@ -18,9 +21,10 @@ def update_scale_metadata( print( "The old scale will be saved in a metadata field named 'old_scale', and the new scale will adhere to the NGFF spec." ) - new_scale = [] - for character in "zyx": - new_scale.append(float(input(f"Enter a new {character} scale: "))) + new_scale = [z_scale, y_scale, x_scale] + for i, character in enumerate("zyx"): + if new_scale[i] is None: + new_scale[i] = (float(input(f"Enter a new {character} scale: "))) for input_position_dirpath in input_position_dirpaths: with open_ome_zarr(input_position_dirpath, layout="fov", mode="a") as input_dataset: From 953b4b93f5200b0a6b1a2450942d4ed2b374ae43 Mon Sep 17 00:00:00 2001 From: Aaron Alvarez Date: Tue, 2 Jul 2024 16:41:12 -0700 Subject: [PATCH 05/38] update order of params in update_scale_metadata to be passed as z, y, x --- iohub/update_scale_metadata.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/iohub/update_scale_metadata.py b/iohub/update_scale_metadata.py index 733b1577..fa7d169d 100644 --- a/iohub/update_scale_metadata.py +++ b/iohub/update_scale_metadata.py @@ -6,9 +6,9 @@ def update_scale_metadata( input_position_dirpaths: List[str], - x_scale: float = None, - y_scale: float = None, z_scale: float = None, + y_scale: float = None, + x_scale: float = None, ): with open_ome_zarr(input_position_dirpaths[0]) as input_dataset: print( From ea4dd73695281d42229cecf074e02d072fc9cbad Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Tue, 9 Jul 2024 09:59:48 -0700 Subject: [PATCH 06/38] black --- iohub/cli/cli.py | 24 ++++++++++++++++-------- iohub/update_scale_metadata.py | 10 ++++++---- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/iohub/cli/cli.py b/iohub/cli/cli.py index 0a540f0e..85b8852e 100644 --- a/iohub/cli/cli.py +++ b/iohub/cli/cli.py @@ -5,7 +5,9 @@ from iohub._version import __version__ from iohub.convert import TIFFConverter from iohub.reader import print_info -from iohub.update_scale_metadata import update_scale_metadata as _update_scale_metadata +from iohub.update_scale_metadata import ( + update_scale_metadata as _update_scale_metadata, +) from mantis.cli.parsing import input_position_dirpaths VERSION = __version__ @@ -90,30 +92,36 @@ def convert(input, output, grid_layout, chunks): ) converter() + @cli.command() @click.help_option("-h", "--help") @input_position_dirpaths() @click.option( - "--x-scale", - "-x", + "--x-scale", + "-x", required=False, type=float, help="New x scale", ) @click.option( - "--y-scale", - "-y", + "--y-scale", + "-y", required=False, type=float, help="New y scale", ) @click.option( - "--z-scale", - "-z", + "--z-scale", + "-z", required=False, type=float, help="New z scale", ) def update_scale_metadata(input_position_dirpaths, z_scale, y_scale, x_scale): """Update scale metadata in OME-Zarr datasets""" - _update_scale_metadata(input_position_dirpaths, z_scale=z_scale, y_scale=y_scale, x_scale=x_scale) \ No newline at end of file + _update_scale_metadata( + input_position_dirpaths, + z_scale=z_scale, + y_scale=y_scale, + x_scale=x_scale, + ) diff --git a/iohub/update_scale_metadata.py b/iohub/update_scale_metadata.py index fa7d169d..c7b9d233 100644 --- a/iohub/update_scale_metadata.py +++ b/iohub/update_scale_metadata.py @@ -24,11 +24,13 @@ def update_scale_metadata( new_scale = [z_scale, y_scale, x_scale] for i, character in enumerate("zyx"): if new_scale[i] is None: - new_scale[i] = (float(input(f"Enter a new {character} scale: "))) + new_scale[i] = float(input(f"Enter a new {character} scale: ")) for input_position_dirpath in input_position_dirpaths: - with open_ome_zarr(input_position_dirpath, layout="fov", mode="a") as input_dataset: - input_dataset.zattrs['old_scale'] = input_dataset.scale[2:] + with open_ome_zarr( + input_position_dirpath, layout="fov", mode="a" + ) as input_dataset: + input_dataset.zattrs["old_scale"] = input_dataset.scale[2:] transform = [ TransformationMeta( type="scale", @@ -41,4 +43,4 @@ def update_scale_metadata( ] input_dataset.set_transform("0", transform=transform) - print(f"The dataset now has (z, y, x) scale {tuple(new_scale)}.") \ No newline at end of file + print(f"The dataset now has (z, y, x) scale {tuple(new_scale)}.") From fdfca083da59c71afcda7f9a8a40c2a8eaffc2ee Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Tue, 9 Jul 2024 10:01:44 -0700 Subject: [PATCH 07/38] isort --- iohub/cli/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iohub/cli/cli.py b/iohub/cli/cli.py index 85b8852e..88ef8632 100644 --- a/iohub/cli/cli.py +++ b/iohub/cli/cli.py @@ -1,6 +1,7 @@ import pathlib import click +from mantis.cli.parsing import input_position_dirpaths from iohub._version import __version__ from iohub.convert import TIFFConverter @@ -8,7 +9,6 @@ from iohub.update_scale_metadata import ( update_scale_metadata as _update_scale_metadata, ) -from mantis.cli.parsing import input_position_dirpaths VERSION = __version__ From a5ec62fcca51c0129a9988ff0c6082eb5d7dbe5c Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Tue, 9 Jul 2024 10:04:28 -0700 Subject: [PATCH 08/38] flake8 --- iohub/update_scale_metadata.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/iohub/update_scale_metadata.py b/iohub/update_scale_metadata.py index c7b9d233..6fa9695a 100644 --- a/iohub/update_scale_metadata.py +++ b/iohub/update_scale_metadata.py @@ -12,14 +12,17 @@ def update_scale_metadata( ): with open_ome_zarr(input_position_dirpaths[0]) as input_dataset: print( - f"The first dataset in the list you provided has (z, y, x) scale {input_dataset.scale[2:]}" + f"The first dataset in the list you provided" + f"has (z, y, x) scale {input_dataset.scale[2:]}" ) print( - "Please enter the new z, y, and x scales that you would like to apply to all of the positions in the list." + "Please enter the new z, y, and x scales that you would like" + "to apply to all of the positions in the list." ) print( - "The old scale will be saved in a metadata field named 'old_scale', and the new scale will adhere to the NGFF spec." + "The old scale will be saved in a metadata field named 'old_scale'," + "and the new scale will adhere to the NGFF spec." ) new_scale = [z_scale, y_scale, x_scale] for i, character in enumerate("zyx"): From e660042dcf03570803a8164162f7e1808b0002ad Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Tue, 17 Sep 2024 13:35:56 -0700 Subject: [PATCH 09/38] update to `iohub.ngff.models` --- iohub/update_scale_metadata.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iohub/update_scale_metadata.py b/iohub/update_scale_metadata.py index 6fa9695a..36c6be09 100644 --- a/iohub/update_scale_metadata.py +++ b/iohub/update_scale_metadata.py @@ -1,7 +1,7 @@ from typing import List from iohub import open_ome_zarr -from iohub.ngff_meta import TransformationMeta +from iohub.ngff.models import TransformationMeta def update_scale_metadata( From fcc408db181ceeb7e88af38d604dcec3e5b768ca Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Tue, 17 Sep 2024 13:43:56 -0700 Subject: [PATCH 10/38] move parsing utilities to iohub --- iohub/cli/cli.py | 2 +- iohub/cli/parsing.py | 88 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 iohub/cli/parsing.py diff --git a/iohub/cli/cli.py b/iohub/cli/cli.py index 88ef8632..ab428546 100644 --- a/iohub/cli/cli.py +++ b/iohub/cli/cli.py @@ -1,9 +1,9 @@ import pathlib import click -from mantis.cli.parsing import input_position_dirpaths from iohub._version import __version__ +from iohub.cli.parsing import input_position_dirpaths from iohub.convert import TIFFConverter from iohub.reader import print_info from iohub.update_scale_metadata import ( diff --git a/iohub/cli/parsing.py b/iohub/cli/parsing.py new file mode 100644 index 00000000..7eb2a4a2 --- /dev/null +++ b/iohub/cli/parsing.py @@ -0,0 +1,88 @@ +from pathlib import Path +from typing import Callable + +import click +from natsort import natsorted + +from iohub.ngff import Plate, open_ome_zarr + + +def _validate_and_process_paths( + ctx: click.Context, opt: click.Option, value: str +) -> list[Path]: + # Sort and validate the input paths, + # expanding plates into lists of positions + input_paths = [Path(path) for path in natsorted(value)] + for path in input_paths: + with open_ome_zarr(path, mode="r") as dataset: + if isinstance(dataset, Plate): + plate_path = input_paths.pop() + for position in dataset.positions(): + input_paths.append(plate_path / position[0]) + + return input_paths + + +def input_position_dirpaths() -> Callable: + def decorator(f: Callable) -> Callable: + return click.option( + "--input-position-dirpaths", + "-i", + cls=OptionEatAll, + type=tuple, + required=True, + callback=_validate_and_process_paths, + help=( + "List of paths to input po" + "each with the same TCZYX shape. " + "Supports wildcards e.g. 'input.zarr/*/*/*'." + ), + )(f) + + return decorator + + +# Copied directly from https://stackoverflow.com/a/48394004 +# Enables `-i ./input.zarr/*/*/*` +class OptionEatAll(click.Option): + def __init__(self, *args, **kwargs): + self.save_other_options = kwargs.pop("save_other_options", True) + nargs = kwargs.pop("nargs", -1) + assert nargs == -1, "nargs, if set, must be -1 not {}".format(nargs) + super(OptionEatAll, self).__init__(*args, **kwargs) + self._previous_parser_process = None + self._eat_all_parser = None + + def add_to_parser(self, parser, ctx): + def parser_process(value, state): + # method to hook to the parser.process + done = False + value = [value] + if self.save_other_options: + # grab everything up to the next option + while state.rargs and not done: + for prefix in self._eat_all_parser.prefixes: + if state.rargs[0].startswith(prefix): + done = True + if not done: + value.append(state.rargs.pop(0)) + else: + # grab everything remaining + value += state.rargs + state.rargs[:] = [] + value = tuple(value) + + # call the actual process + self._previous_parser_process(value, state) + + retval = super(OptionEatAll, self).add_to_parser(parser, ctx) + for name in self.opts: + our_parser = parser._long_opt.get(name) or parser._short_opt.get( + name + ) + if our_parser: + self._eat_all_parser = our_parser + self._previous_parser_process = our_parser.process + our_parser.process = parser_process + break + return retval From c8816e546dfb10a829ed2a3d830dbf5ef450d95d Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Tue, 17 Sep 2024 14:01:15 -0700 Subject: [PATCH 11/38] move update_scale_metadata inside cli folder --- iohub/{ => cli}/update_scale_metadata.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename iohub/{ => cli}/update_scale_metadata.py (100%) diff --git a/iohub/update_scale_metadata.py b/iohub/cli/update_scale_metadata.py similarity index 100% rename from iohub/update_scale_metadata.py rename to iohub/cli/update_scale_metadata.py From 2642a519c6f97b7c0e5a7a8614537c9eab3aeced Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Tue, 17 Sep 2024 14:01:30 -0700 Subject: [PATCH 12/38] typo --- iohub/cli/parsing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iohub/cli/parsing.py b/iohub/cli/parsing.py index 7eb2a4a2..cc6ae759 100644 --- a/iohub/cli/parsing.py +++ b/iohub/cli/parsing.py @@ -33,7 +33,7 @@ def decorator(f: Callable) -> Callable: required=True, callback=_validate_and_process_paths, help=( - "List of paths to input po" + "List of paths to input positions, " "each with the same TCZYX shape. " "Supports wildcards e.g. 'input.zarr/*/*/*'." ), From e0ff8e20be7557c5e7ed042c40de6f5e38ea5dd4 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Tue, 17 Sep 2024 14:02:01 -0700 Subject: [PATCH 13/38] update import for refactor --- iohub/cli/cli.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/iohub/cli/cli.py b/iohub/cli/cli.py index ab428546..0b0a5997 100644 --- a/iohub/cli/cli.py +++ b/iohub/cli/cli.py @@ -4,11 +4,11 @@ from iohub._version import __version__ from iohub.cli.parsing import input_position_dirpaths -from iohub.convert import TIFFConverter -from iohub.reader import print_info -from iohub.update_scale_metadata import ( +from iohub.cli.update_scale_metadata import ( update_scale_metadata as _update_scale_metadata, ) +from iohub.convert import TIFFConverter +from iohub.reader import print_info VERSION = __version__ @@ -97,11 +97,11 @@ def convert(input, output, grid_layout, chunks): @click.help_option("-h", "--help") @input_position_dirpaths() @click.option( - "--x-scale", - "-x", + "--z-scale", + "-z", required=False, type=float, - help="New x scale", + help="New z scale", ) @click.option( "--y-scale", @@ -111,14 +111,17 @@ def convert(input, output, grid_layout, chunks): help="New y scale", ) @click.option( - "--z-scale", - "-z", + "--x-scale", + "-x", required=False, type=float, - help="New z scale", + help="New x scale", ) def update_scale_metadata(input_position_dirpaths, z_scale, y_scale, x_scale): - """Update scale metadata in OME-Zarr datasets""" + """Update scale metadata in OME-Zarr datasets. + + >> iohub update-scale-metatdata -i input.zarr/*/*/* -z 1.0 -y 0.5 -x 0.5 + """ _update_scale_metadata( input_position_dirpaths, z_scale=z_scale, From 7fac9f84fa3f1ab56baa9f5c81e36db07494b432 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Tue, 17 Sep 2024 14:21:40 -0700 Subject: [PATCH 14/38] require -z, -y, -x flags --- iohub/cli/cli.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/iohub/cli/cli.py b/iohub/cli/cli.py index 0b0a5997..bde789b9 100644 --- a/iohub/cli/cli.py +++ b/iohub/cli/cli.py @@ -99,21 +99,21 @@ def convert(input, output, grid_layout, chunks): @click.option( "--z-scale", "-z", - required=False, + required=True, type=float, help="New z scale", ) @click.option( "--y-scale", "-y", - required=False, + required=True, type=float, help="New y scale", ) @click.option( "--x-scale", "-x", - required=False, + required=True, type=float, help="New x scale", ) From de5c8834d5cb4bf8c5f11e96c8f033505d52ae00 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Tue, 17 Sep 2024 14:22:50 -0700 Subject: [PATCH 15/38] simplify interface and print statements --- iohub/cli/update_scale_metadata.py | 37 +++++++----------------------- 1 file changed, 8 insertions(+), 29 deletions(-) diff --git a/iohub/cli/update_scale_metadata.py b/iohub/cli/update_scale_metadata.py index 36c6be09..6f33014a 100644 --- a/iohub/cli/update_scale_metadata.py +++ b/iohub/cli/update_scale_metadata.py @@ -6,44 +6,23 @@ def update_scale_metadata( input_position_dirpaths: List[str], - z_scale: float = None, - y_scale: float = None, - x_scale: float = None, + z_scale: float, + y_scale: float, + x_scale: float, ): - with open_ome_zarr(input_position_dirpaths[0]) as input_dataset: - print( - f"The first dataset in the list you provided" - f"has (z, y, x) scale {input_dataset.scale[2:]}" - ) - - print( - "Please enter the new z, y, and x scales that you would like" - "to apply to all of the positions in the list." - ) - print( - "The old scale will be saved in a metadata field named 'old_scale'," - "and the new scale will adhere to the NGFF spec." - ) - new_scale = [z_scale, y_scale, x_scale] - for i, character in enumerate("zyx"): - if new_scale[i] is None: - new_scale[i] = float(input(f"Enter a new {character} scale: ")) - for input_position_dirpath in input_position_dirpaths: with open_ome_zarr( input_position_dirpath, layout="fov", mode="a" ) as input_dataset: + print( + f"Changing {input_position_dirpath.name} scale from " + f"{input_dataset.scale[2:]} to {z_scale, y_scale, x_scale}." + ) input_dataset.zattrs["old_scale"] = input_dataset.scale[2:] transform = [ TransformationMeta( type="scale", - scale=( - 1, - 1, - ) - + tuple(new_scale), + scale=(1, 1, z_scale, y_scale, x_scale), ) ] input_dataset.set_transform("0", transform=transform) - - print(f"The dataset now has (z, y, x) scale {tuple(new_scale)}.") From 9d6492ef51b05ee7147da28c88beea1de358f99d Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Tue, 17 Sep 2024 14:29:34 -0700 Subject: [PATCH 16/38] clean up print statement --- iohub/cli/update_scale_metadata.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/iohub/cli/update_scale_metadata.py b/iohub/cli/update_scale_metadata.py index 6f33014a..785ef3a8 100644 --- a/iohub/cli/update_scale_metadata.py +++ b/iohub/cli/update_scale_metadata.py @@ -15,8 +15,9 @@ def update_scale_metadata( input_position_dirpath, layout="fov", mode="a" ) as input_dataset: print( - f"Changing {input_position_dirpath.name} scale from " - f"{input_dataset.scale[2:]} to {z_scale, y_scale, x_scale}." + f"Updating {input_position_dirpath} scale from " + f"{tuple(input_dataset.scale[2:])} to " + f"{z_scale, y_scale, x_scale}." ) input_dataset.zattrs["old_scale"] = input_dataset.scale[2:] transform = [ From 387ac596accdc07662c93b6e2371484aba97353f Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Tue, 17 Sep 2024 15:30:56 -0700 Subject: [PATCH 17/38] update the last three dimensions for OME compatibility --- iohub/cli/update_scale_metadata.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/iohub/cli/update_scale_metadata.py b/iohub/cli/update_scale_metadata.py index 785ef3a8..48174120 100644 --- a/iohub/cli/update_scale_metadata.py +++ b/iohub/cli/update_scale_metadata.py @@ -16,14 +16,15 @@ def update_scale_metadata( ) as input_dataset: print( f"Updating {input_position_dirpath} scale from " - f"{tuple(input_dataset.scale[2:])} to " + f"{tuple(input_dataset.scale[-3:])} to " f"{z_scale, y_scale, x_scale}." ) input_dataset.zattrs["old_scale"] = input_dataset.scale[2:] transform = [ TransformationMeta( type="scale", - scale=(1, 1, z_scale, y_scale, x_scale), + scale=(len(input_dataset.scale) - 3) * (1,) + + (z_scale, y_scale, x_scale), ) ] input_dataset.set_transform("0", transform=transform) From 461dc6578fbd936af5889c8aeb38a0b4153f1fb4 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Tue, 17 Sep 2024 16:43:54 -0700 Subject: [PATCH 18/38] fix tests --- tests/cli/test_cli.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py index 4f4f4b34..7549beac 100644 --- a/tests/cli/test_cli.py +++ b/tests/cli/test_cli.py @@ -1,9 +1,11 @@ +import random import re from unittest.mock import patch import pytest from click.testing import CliRunner +from iohub import open_ome_zarr from iohub._version import __version__ from iohub.cli.cli import cli from tests.conftest import ( @@ -103,3 +105,35 @@ def test_cli_convert_ome_tiff(grid_layout, tmpdir): result = runner.invoke(cli, cmd) assert result.exit_code == 0, result.output assert "Converting" in result.output + + +def test_cli_update_scale_metadata(): + position_path = hcs_ref / "B" / "03" / "0" + with open_ome_zarr(position_path, layout="fov", mode="a") as input_dataset: + old_scale = input_dataset.scale + + random_z = random.uniform(0, 1) + + runner = CliRunner() + result_pos = runner.invoke( + cli, + [ + "update-scale-metadata", + "-i", + str(position_path), + "-z", + 0.5, + "-z", + random_z, + "-y", + 0.5, + "-x", + 0.5, + ], + ) + assert result_pos.exit_code == 0 + assert "Updating" in result_pos.output + + with open_ome_zarr(position_path, layout="fov") as output_dataset: + assert tuple(output_dataset.scale[-3:]) == (random_z, 0.5, 0.5) + assert output_dataset.scale != old_scale From dcb00a1c9b5a21db029b4e037bbf1e08067f91f5 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Tue, 17 Sep 2024 16:49:20 -0700 Subject: [PATCH 19/38] fix test --- tests/cli/test_cli.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py index 7549beac..a5f800b6 100644 --- a/tests/cli/test_cli.py +++ b/tests/cli/test_cli.py @@ -122,8 +122,6 @@ def test_cli_update_scale_metadata(): "-i", str(position_path), "-z", - 0.5, - "-z", random_z, "-y", 0.5, From 37add164b74721147ca339a582d39148d6af92f8 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Wed, 18 Sep 2024 15:04:20 -0700 Subject: [PATCH 20/38] helper functions for axis names --- iohub/ngff/nodes.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/iohub/ngff/nodes.py b/iohub/ngff/nodes.py index 9624baeb..03b4e739 100644 --- a/iohub/ngff/nodes.py +++ b/iohub/ngff/nodes.py @@ -976,6 +976,31 @@ def scale(self) -> list[float]: scale = [s1 * s2 for s1, s2 in zip(scale, trans.scale)] return scale + @property + def axis_names(self) -> list[str]: + """ + Helper function for axis names of the highest resolution scale. + """ + return [axis.name for axis in self.metadata.multiscales[0].axes] + + def get_axis_index( + self, axis_name: Literal["T", "C", "Z", "Y", "X"] + ) -> int: + """ + Get the index of a given axis. + + Parameters + ---------- + name : str + Name of the axis. + + Returns + ------- + int + Index of the axis. + """ + return self.axis_names.index(axis_name) + def set_transform( self, image: str | Literal["*"], From 6b4461dee6655bc995fc01f78fa8c0a310b6b473 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Wed, 18 Sep 2024 15:05:03 -0700 Subject: [PATCH 21/38] set_scale API --- iohub/ngff/nodes.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/iohub/ngff/nodes.py b/iohub/ngff/nodes.py index 03b4e739..ae25ba32 100644 --- a/iohub/ngff/nodes.py +++ b/iohub/ngff/nodes.py @@ -1032,6 +1032,45 @@ def set_transform( raise ValueError(f"Key {image} not recognized.") self.dump_meta() + def set_scale( + self, + image: str | Literal["*"], + axis_name: Literal["T", "C", "Z", "Y", "X"], + new_scale: float, + ): + """Set the scale for a named axis. + Either one image array or the whole FOV. + + Parameters + ---------- + image : str | Literal[ + Name of one image array (e.g. "0") to transform, + or "*" for the whole FOV + axis_name : Literal["T", "C", "Z", "Y", "X"] + Name of the axis to set. + new_scale : float + Value of the new scale. + """ + if len(self.metadata.multiscales) > 1: + raise NotImplementedError( + "Cannot set scale for multi-resolution images." + ) + + if new_scale <= 0: + raise ValueError("New scale must be positive.") + + axis_index = self.get_axis_index(axis_name) + self.zattrs["iohub:old_{axis_name}"] = self.scale[axis_index] + + # Update scale while preserving existing transforms + current_transforms = ( + self.metadata.multiscales[0].datasets[0].coordinate_transformations + ) + for transform in current_transforms: + if transform.type == "scale": + transform.scale[axis_index] = new_scale + self.set_transform(image, current_transforms) + class TiledPosition(Position): """Variant of the NGFF position node From 353ab65ad08540771241738665fb4d16a5abb7bb Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Wed, 18 Sep 2024 15:07:15 -0700 Subject: [PATCH 22/38] consolidate and clean CLI --- iohub/cli/cli.py | 52 ++++++++++++++++++++++-------- iohub/cli/update_scale_metadata.py | 30 ----------------- 2 files changed, 38 insertions(+), 44 deletions(-) delete mode 100644 iohub/cli/update_scale_metadata.py diff --git a/iohub/cli/cli.py b/iohub/cli/cli.py index bde789b9..fa050e41 100644 --- a/iohub/cli/cli.py +++ b/iohub/cli/cli.py @@ -2,11 +2,9 @@ import click +from iohub import open_ome_zarr from iohub._version import __version__ from iohub.cli.parsing import input_position_dirpaths -from iohub.cli.update_scale_metadata import ( - update_scale_metadata as _update_scale_metadata, -) from iohub.convert import TIFFConverter from iohub.reader import print_info @@ -96,35 +94,61 @@ def convert(input, output, grid_layout, chunks): @cli.command() @click.help_option("-h", "--help") @input_position_dirpaths() +@click.option( + "--t-scale", + "-t", + required=False, + type=float, + help="New t scale", +) @click.option( "--z-scale", "-z", - required=True, + required=False, type=float, help="New z scale", ) @click.option( "--y-scale", "-y", - required=True, + required=False, type=float, help="New y scale", ) @click.option( "--x-scale", "-x", - required=True, + required=False, type=float, help="New x scale", ) -def update_scale_metadata(input_position_dirpaths, z_scale, y_scale, x_scale): +def set_scale( + input_position_dirpaths, + t_scale=None, + z_scale=None, + y_scale=None, + x_scale=None, +): """Update scale metadata in OME-Zarr datasets. - >> iohub update-scale-metatdata -i input.zarr/*/*/* -z 1.0 -y 0.5 -x 0.5 + >> iohub set-scale -i input.zarr/*/*/* -t 1.0 -z 1.0 -y 0.5 -x 0.5 + + Supports setting a single axis at a time: + + >> iohub set-scale -i input.zarr/*/*/* -z 2.0 """ - _update_scale_metadata( - input_position_dirpaths, - z_scale=z_scale, - y_scale=y_scale, - x_scale=x_scale, - ) + for input_position_dirpath in input_position_dirpaths: + with open_ome_zarr( + input_position_dirpath, layout="fov", mode="a" + ) as dataset: + for name, value in zip( + ["T", "Z", "Y", "X"], [t_scale, z_scale, y_scale, x_scale] + ): + if value is None: + continue + old_value = dataset.scale[dataset.get_axis_index(name)] + print( + f"Updating {input_position_dirpath} {name} scale from " + f"{old_value} to {value}." + ) + dataset.set_scale("0", name, value) diff --git a/iohub/cli/update_scale_metadata.py b/iohub/cli/update_scale_metadata.py deleted file mode 100644 index 48174120..00000000 --- a/iohub/cli/update_scale_metadata.py +++ /dev/null @@ -1,30 +0,0 @@ -from typing import List - -from iohub import open_ome_zarr -from iohub.ngff.models import TransformationMeta - - -def update_scale_metadata( - input_position_dirpaths: List[str], - z_scale: float, - y_scale: float, - x_scale: float, -): - for input_position_dirpath in input_position_dirpaths: - with open_ome_zarr( - input_position_dirpath, layout="fov", mode="a" - ) as input_dataset: - print( - f"Updating {input_position_dirpath} scale from " - f"{tuple(input_dataset.scale[-3:])} to " - f"{z_scale, y_scale, x_scale}." - ) - input_dataset.zattrs["old_scale"] = input_dataset.scale[2:] - transform = [ - TransformationMeta( - type="scale", - scale=(len(input_dataset.scale) - 3) * (1,) - + (z_scale, y_scale, x_scale), - ) - ] - input_dataset.set_transform("0", transform=transform) From a9007637c4b29086e4172d4b3539612b40be654d Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Wed, 18 Sep 2024 16:15:30 -0700 Subject: [PATCH 23/38] test get_axis_index --- tests/ngff/test_ngff.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/ngff/test_ngff.py b/tests/ngff/test_ngff.py index d1701529..61beadd6 100644 --- a/tests/ngff/test_ngff.py +++ b/tests/ngff/test_ngff.py @@ -560,6 +560,22 @@ def test_get_channel_index(wrong_channel_name): _ = dataset.get_channel_index(wrong_channel_name) +def test_get_axis_index(): + with open_ome_zarr(hcs_ref, layout="hcs", mode="r+") as dataset: + position = dataset["B/03/0"] + + assert position.axis_names == ["c", "z", "y", "x"] + + assert position.get_axis_index("z") == 1 + assert position.get_axis_index("Z") == 1 + + with pytest.raises(ValueError): + _ = position.get_axis_index("t") + + with pytest.raises(ValueError): + _ = position.get_axis_index("DOG") + + @given( row=short_alpha_numeric, col=short_alpha_numeric, pos=short_alpha_numeric ) From b98381fc210ee72e7a1bdb8a064cf73f4521ef5c Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Wed, 18 Sep 2024 16:15:58 -0700 Subject: [PATCH 24/38] test_set_scale --- tests/ngff/test_ngff.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/ngff/test_ngff.py b/tests/ngff/test_ngff.py index 61beadd6..227db257 100644 --- a/tests/ngff/test_ngff.py +++ b/tests/ngff/test_ngff.py @@ -403,6 +403,36 @@ def test_set_transform_fov(ch_shape_dtype, arr_name): ] +@given( + ch_shape_dtype=_channels_and_random_5d_shape_and_dtype(), +) +def test_set_scale(ch_shape_dtype): + channel_names, shape, dtype = ch_shape_dtype + transform = [ + TransformationMeta(type="translation", translation=(1, 2, 3, 4, 5)), + TransformationMeta(type="scale", scale=(5, 4, 3, 2, 1)), + ] + with TemporaryDirectory() as temp_dir: + store_path = os.path.join(temp_dir, "ome.zarr") + with open_ome_zarr( + store_path, layout="fov", mode="w-", channel_names=channel_names + ) as dataset: + dataset.create_zeros(name="0", shape=shape, dtype=dtype) + dataset.set_transform(image="0", transform=transform) + dataset.set_scale(image="0", axis_name="z", new_scale=10.0) + assert dataset.scale[-3] == 10.0 + assert ( + dataset.metadata.multiscales[0] + .datasets[0] + .coordinate_transformations[0] + .translation[-1] + == 5 + ) + + with pytest.raises(ValueError): + dataset.set_scale(image="0", axis_name="z", new_scale=-1.0) + + @given(channel_names=channel_names_st) @settings(max_examples=16) def test_create_tiled(channel_names): From 74bdb7b8d51b210e8752c04376ae35d56a895339 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Wed, 18 Sep 2024 16:16:49 -0700 Subject: [PATCH 25/38] case insensitive axis name --- iohub/cli/cli.py | 2 +- iohub/ngff/nodes.py | 18 ++++++++++-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/iohub/cli/cli.py b/iohub/cli/cli.py index fa050e41..d9358dcc 100644 --- a/iohub/cli/cli.py +++ b/iohub/cli/cli.py @@ -142,7 +142,7 @@ def set_scale( input_position_dirpath, layout="fov", mode="a" ) as dataset: for name, value in zip( - ["T", "Z", "Y", "X"], [t_scale, z_scale, y_scale, x_scale] + ["t", "z", "y", "x"], [t_scale, z_scale, y_scale, x_scale] ): if value is None: continue diff --git a/iohub/ngff/nodes.py b/iohub/ngff/nodes.py index ae25ba32..7664fdbd 100644 --- a/iohub/ngff/nodes.py +++ b/iohub/ngff/nodes.py @@ -980,26 +980,28 @@ def scale(self) -> list[float]: def axis_names(self) -> list[str]: """ Helper function for axis names of the highest resolution scale. + + Returns lowercase axis names. """ - return [axis.name for axis in self.metadata.multiscales[0].axes] + return [ + axis.name.lower() for axis in self.metadata.multiscales[0].axes + ] - def get_axis_index( - self, axis_name: Literal["T", "C", "Z", "Y", "X"] - ) -> int: + def get_axis_index(self, axis_name: str) -> int: """ Get the index of a given axis. Parameters ---------- name : str - Name of the axis. + Name of the axis. Case insensitive. Returns ------- int Index of the axis. """ - return self.axis_names.index(axis_name) + return self.axis_names.index(axis_name.lower()) def set_transform( self, @@ -1035,7 +1037,7 @@ def set_transform( def set_scale( self, image: str | Literal["*"], - axis_name: Literal["T", "C", "Z", "Y", "X"], + axis_name: str, new_scale: float, ): """Set the scale for a named axis. @@ -1046,7 +1048,7 @@ def set_scale( image : str | Literal[ Name of one image array (e.g. "0") to transform, or "*" for the whole FOV - axis_name : Literal["T", "C", "Z", "Y", "X"] + axis_name : str Name of the axis to set. new_scale : float Value of the new scale. From 6b857a9f286d2b475c45c651bd683d9c372a053d Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Wed, 18 Sep 2024 16:50:50 -0700 Subject: [PATCH 26/38] tests don't overwrite data --- iohub/cli/cli.py | 2 +- tests/cli/test_cli.py | 62 ++++++++++++++++++++++++------------------- 2 files changed, 36 insertions(+), 28 deletions(-) diff --git a/iohub/cli/cli.py b/iohub/cli/cli.py index d9358dcc..bcf6d556 100644 --- a/iohub/cli/cli.py +++ b/iohub/cli/cli.py @@ -139,7 +139,7 @@ def set_scale( """ for input_position_dirpath in input_position_dirpaths: with open_ome_zarr( - input_position_dirpath, layout="fov", mode="a" + input_position_dirpath, layout="fov", mode="r+" ) as dataset: for name, value in zip( ["t", "z", "y", "x"], [t_scale, z_scale, y_scale, x_scale] diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py index a5f800b6..469f892d 100644 --- a/tests/cli/test_cli.py +++ b/tests/cli/test_cli.py @@ -1,5 +1,6 @@ import random import re +from pathlib import Path from unittest.mock import patch import pytest @@ -15,6 +16,8 @@ ndtiff_v3_labeled_positions, ) +from ..ngff.test_ngff import _temp_copy + def pytest_generate_tests(metafunc): if "mm2gamma_ome_tiff" in metafunc.fixturenames: @@ -107,31 +110,36 @@ def test_cli_convert_ome_tiff(grid_layout, tmpdir): assert "Converting" in result.output -def test_cli_update_scale_metadata(): - position_path = hcs_ref / "B" / "03" / "0" - with open_ome_zarr(position_path, layout="fov", mode="a") as input_dataset: - old_scale = input_dataset.scale - - random_z = random.uniform(0, 1) +def test_cli_set_scale(): + with _temp_copy(hcs_ref) as store_path: + store_path = Path(store_path) + position_path = Path(store_path) / "B" / "03" / "0" + + with open_ome_zarr( + position_path, layout="fov", mode="r+" + ) as input_dataset: + old_scale = input_dataset.scale + + random_z = random.uniform(0, 1) + + runner = CliRunner() + result_pos = runner.invoke( + cli, + [ + "set-scale", + "-i", + position_path, + "-z", + random_z, + "-y", + 0.5, + "-x", + 0.5, + ], + ) + assert result_pos.exit_code == 0 + assert "Updating" in result_pos.output - runner = CliRunner() - result_pos = runner.invoke( - cli, - [ - "update-scale-metadata", - "-i", - str(position_path), - "-z", - random_z, - "-y", - 0.5, - "-x", - 0.5, - ], - ) - assert result_pos.exit_code == 0 - assert "Updating" in result_pos.output - - with open_ome_zarr(position_path, layout="fov") as output_dataset: - assert tuple(output_dataset.scale[-3:]) == (random_z, 0.5, 0.5) - assert output_dataset.scale != old_scale + with open_ome_zarr(position_path, layout="fov") as output_dataset: + assert tuple(output_dataset.scale[-3:]) == (random_z, 0.5, 0.5) + assert output_dataset.scale != old_scale From 9e6d676d3d97ab4f0576f1aae8538fc68ec76227 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Wed, 18 Sep 2024 17:13:37 -0700 Subject: [PATCH 27/38] save old metadata in a namespace --- iohub/ngff/nodes.py | 2 +- tests/ngff/test_ngff.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/iohub/ngff/nodes.py b/iohub/ngff/nodes.py index 7664fdbd..67f7600f 100644 --- a/iohub/ngff/nodes.py +++ b/iohub/ngff/nodes.py @@ -1062,7 +1062,7 @@ def set_scale( raise ValueError("New scale must be positive.") axis_index = self.get_axis_index(axis_name) - self.zattrs["iohub:old_{axis_name}"] = self.scale[axis_index] + self.zattrs["iohub"] = {f"old_{axis_name}": self.scale[axis_index]} # Update scale while preserving existing transforms current_transforms = ( diff --git a/tests/ngff/test_ngff.py b/tests/ngff/test_ngff.py index 227db257..1c2480c7 100644 --- a/tests/ngff/test_ngff.py +++ b/tests/ngff/test_ngff.py @@ -432,6 +432,8 @@ def test_set_scale(ch_shape_dtype): with pytest.raises(ValueError): dataset.set_scale(image="0", axis_name="z", new_scale=-1.0) + assert dataset.zattrs["iohub"]["old_z"] == 3.0 + @given(channel_names=channel_names_st) @settings(max_examples=16) From 9df8007bdcca743fa7233b7122a6cdd1b8ce72d1 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Wed, 18 Sep 2024 17:31:48 -0700 Subject: [PATCH 28/38] test multiple inputs to cli --- tests/cli/test_cli.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py index 469f892d..4fa0f59c 100644 --- a/tests/cli/test_cli.py +++ b/tests/cli/test_cli.py @@ -128,7 +128,8 @@ def test_cli_set_scale(): [ "set-scale", "-i", - position_path, + str(position_path), + str(position_path), "-z", random_z, "-y", From e858dc68afcded15fa506c18ef0c952fdca69264 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Wed, 18 Sep 2024 18:26:33 -0700 Subject: [PATCH 29/38] handle empty current_transforms --- iohub/ngff/nodes.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/iohub/ngff/nodes.py b/iohub/ngff/nodes.py index 67f7600f..40cd6937 100644 --- a/iohub/ngff/nodes.py +++ b/iohub/ngff/nodes.py @@ -1068,6 +1068,9 @@ def set_scale( current_transforms = ( self.metadata.multiscales[0].datasets[0].coordinate_transformations ) + if current_transforms is None: + return + for transform in current_transforms: if transform.type == "scale": transform.scale[axis_index] = new_scale From 75d2525044c85f0679dda5ba38f4a76f3ddfb9b6 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Wed, 18 Sep 2024 18:31:05 -0700 Subject: [PATCH 30/38] improved empty handling --- iohub/ngff/nodes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iohub/ngff/nodes.py b/iohub/ngff/nodes.py index 40cd6937..f2af8ae1 100644 --- a/iohub/ngff/nodes.py +++ b/iohub/ngff/nodes.py @@ -1069,7 +1069,7 @@ def set_scale( self.metadata.multiscales[0].datasets[0].coordinate_transformations ) if current_transforms is None: - return + current_transforms = [TransformationMeta(type="scale")] for transform in current_transforms: if transform.type == "scale": From b46c5f3371743050bec3b19ccb5e903f7b7e490d Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Wed, 25 Sep 2024 10:25:05 -0700 Subject: [PATCH 31/38] unit test CLI plate expansion into positions --- iohub/cli/parsing.py | 4 ++-- tests/cli/test_parsing.py | 41 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 tests/cli/test_parsing.py diff --git a/iohub/cli/parsing.py b/iohub/cli/parsing.py index cc6ae759..aed9319d 100644 --- a/iohub/cli/parsing.py +++ b/iohub/cli/parsing.py @@ -1,5 +1,5 @@ from pathlib import Path -from typing import Callable +from typing import Callable, List import click from natsort import natsorted @@ -8,7 +8,7 @@ def _validate_and_process_paths( - ctx: click.Context, opt: click.Option, value: str + ctx: click.Context, opt: click.Option, value: List[str] ) -> list[Path]: # Sort and validate the input paths, # expanding plates into lists of positions diff --git a/tests/cli/test_parsing.py b/tests/cli/test_parsing.py new file mode 100644 index 00000000..456639cd --- /dev/null +++ b/tests/cli/test_parsing.py @@ -0,0 +1,41 @@ +import click +import numpy as np + +from iohub import open_ome_zarr +from iohub.cli.parsing import _validate_and_process_paths + + +def test_validate_and_process_paths(tmpdir): + plate_path = tmpdir / "dataset.zarr" + + position_list = [("A", "1", "0"), ("B", "2", "0"), ("X", "4", "1")] + with open_ome_zarr( + plate_path, mode="w", layout="hcs", channel_names=["1", "2"] + ) as dataset: + for position in position_list: + pos = dataset.create_position(*position) + pos.create_zeros("0", shape=(1, 1, 1, 1, 1), dtype=np.uint8) + + cmd = click.Command("test") + ctx = click.Context(cmd) + opt = click.Option(["--path"], type=click.Path(exists=True)) + + # Check plate expansion + processed = _validate_and_process_paths(ctx, opt, [str(plate_path)]) + assert len(processed) == len(position_list) + for i, position in enumerate(position_list): + assert processed[i].parts[-3:] == position + + # Check single position + processed = _validate_and_process_paths( + ctx, opt, [str(plate_path / "A" / "1" / "0")] + ) + assert len(processed) == 1 + + # Check two positions + processed = _validate_and_process_paths( + ctx, + opt, + [str(plate_path / "A" / "1" / "0"), str(plate_path / "B" / "2" / "0")], + ) + assert len(processed) == 2 From d657c88bf388111ee9410d96a2e4c1fad367d64f Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Wed, 25 Sep 2024 10:41:57 -0700 Subject: [PATCH 32/38] cleanup test --- tests/cli/test_parsing.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/cli/test_parsing.py b/tests/cli/test_parsing.py index 456639cd..878892ce 100644 --- a/tests/cli/test_parsing.py +++ b/tests/cli/test_parsing.py @@ -6,8 +6,8 @@ def test_validate_and_process_paths(tmpdir): + # Setup plate plate_path = tmpdir / "dataset.zarr" - position_list = [("A", "1", "0"), ("B", "2", "0"), ("X", "4", "1")] with open_ome_zarr( plate_path, mode="w", layout="hcs", channel_names=["1", "2"] @@ -16,6 +16,7 @@ def test_validate_and_process_paths(tmpdir): pos = dataset.create_position(*position) pos.create_zeros("0", shape=(1, 1, 1, 1, 1), dtype=np.uint8) + # Setup click cmd = click.Command("test") ctx = click.Context(cmd) opt = click.Option(["--path"], type=click.Path(exists=True)) From 0e3ac36b31806b87aefb7f24d4b00b4d0c31dae5 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Wed, 25 Sep 2024 11:22:12 -0700 Subject: [PATCH 33/38] test OptionEatAll --- tests/cli/test_parsing.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/tests/cli/test_parsing.py b/tests/cli/test_parsing.py index 878892ce..296e537c 100644 --- a/tests/cli/test_parsing.py +++ b/tests/cli/test_parsing.py @@ -1,8 +1,9 @@ import click import numpy as np +from click.testing import CliRunner from iohub import open_ome_zarr -from iohub.cli.parsing import _validate_and_process_paths +from iohub.cli.parsing import OptionEatAll, _validate_and_process_paths def test_validate_and_process_paths(tmpdir): @@ -40,3 +41,22 @@ def test_validate_and_process_paths(tmpdir): [str(plate_path / "A" / "1" / "0"), str(plate_path / "B" / "2" / "0")], ) assert len(processed) == 2 + + +def test_option_eat_all(): + @click.command() + @click.option( + "--test", cls=OptionEatAll + ) # tests will fail w/o OptionEatAll + def foo(test): + print(test) + + runner = CliRunner() + result = runner.invoke(foo, ["--test", "a", "b", "c"]) + assert "('a', 'b', 'c')" in result.output + assert "Error" not in result.output + + result = runner.invoke(foo, ["--test", "a"]) + assert "a" in result.output + assert "b" not in result.output + assert "Error" not in result.output From e1a04cc66c07459c5e341badd41de88e88797ea6 Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Wed, 25 Sep 2024 11:25:40 -0700 Subject: [PATCH 34/38] stronger plate-expansion test --- tests/cli/test_cli.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py index 4fa0f59c..f71d241b 100644 --- a/tests/cli/test_cli.py +++ b/tests/cli/test_cli.py @@ -144,3 +144,18 @@ def test_cli_set_scale(): with open_ome_zarr(position_path, layout="fov") as output_dataset: assert tuple(output_dataset.scale[-3:]) == (random_z, 0.5, 0.5) assert output_dataset.scale != old_scale + + # Test plate-expands-into-positions behavior + runner = CliRunner() + result_pos = runner.invoke( + cli, + [ + "set-scale", + "-i", + str(store_path), + "-x", + 0.1, + ], + ) + with open_ome_zarr(position_path, layout="fov") as output_dataset: + assert output_dataset.scale[-1] == 0.1 From f678f44e9b516c2b4a70680c1dcce358acadb7f0 Mon Sep 17 00:00:00 2001 From: Ivan Ivanov Date: Wed, 25 Sep 2024 18:31:06 -0700 Subject: [PATCH 35/38] fix bug when scale transform does not exist --- iohub/ngff/nodes.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/iohub/ngff/nodes.py b/iohub/ngff/nodes.py index f2af8ae1..4ebbe065 100644 --- a/iohub/ngff/nodes.py +++ b/iohub/ngff/nodes.py @@ -1065,16 +1065,21 @@ def set_scale( self.zattrs["iohub"] = {f"old_{axis_name}": self.scale[axis_index]} # Update scale while preserving existing transforms - current_transforms = ( + transforms = ( self.metadata.multiscales[0].datasets[0].coordinate_transformations ) - if current_transforms is None: - current_transforms = [TransformationMeta(type="scale")] - - for transform in current_transforms: + # Replace default identity transform with scale + if len(transforms) == 1 and transforms[0].type == "identity": + transforms = [TransformationMeta(type="scale", scale=[1] * 5)] + # Add scale transform if not present + if not any([transform.type == "scale" for transform in transforms]): + transforms.append(TransformationMeta(type="scale", scale=[1] * 5)) + + for transform in transforms: if transform.type == "scale": transform.scale[axis_index] = new_scale - self.set_transform(image, current_transforms) + + self.set_transform(image, transforms) class TiledPosition(Position): From a551257d3522d3266468239ee1f1a8c01e1ad0ba Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Wed, 25 Sep 2024 19:49:00 -0700 Subject: [PATCH 36/38] create "iohub" dict if it doesn't exist, and update it --- iohub/ngff/nodes.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/iohub/ngff/nodes.py b/iohub/ngff/nodes.py index 4ebbe065..5ddf9ad3 100644 --- a/iohub/ngff/nodes.py +++ b/iohub/ngff/nodes.py @@ -1062,7 +1062,14 @@ def set_scale( raise ValueError("New scale must be positive.") axis_index = self.get_axis_index(axis_name) - self.zattrs["iohub"] = {f"old_{axis_name}": self.scale[axis_index]} + + # Append old scale to metadata + if "iohub" not in self.zattrs: + iohub_dict = {} + else: + iohub_dict = self.zattrs["iohub"] + iohub_dict.update({f"old_{axis_name}": self.scale[axis_index]}) + self.zattrs["iohub"] = iohub_dict # Update scale while preserving existing transforms transforms = ( From b49980e21770850f0af2af7743930a5e83d5cd3c Mon Sep 17 00:00:00 2001 From: Talon Chandler Date: Wed, 25 Sep 2024 19:58:19 -0700 Subject: [PATCH 37/38] test old_* metadata --- tests/cli/test_cli.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py index f71d241b..56f9f407 100644 --- a/tests/cli/test_cli.py +++ b/tests/cli/test_cli.py @@ -129,7 +129,6 @@ def test_cli_set_scale(): "set-scale", "-i", str(position_path), - str(position_path), "-z", random_z, "-y", @@ -144,6 +143,9 @@ def test_cli_set_scale(): with open_ome_zarr(position_path, layout="fov") as output_dataset: assert tuple(output_dataset.scale[-3:]) == (random_z, 0.5, 0.5) assert output_dataset.scale != old_scale + assert output_dataset.zattrs["iohub"]["old_x"] == old_scale[-1] + assert output_dataset.zattrs["iohub"]["old_y"] == old_scale[-2] + assert output_dataset.zattrs["iohub"]["old_z"] == old_scale[-3] # Test plate-expands-into-positions behavior runner = CliRunner() @@ -159,3 +161,4 @@ def test_cli_set_scale(): ) with open_ome_zarr(position_path, layout="fov") as output_dataset: assert output_dataset.scale[-1] == 0.1 + assert output_dataset.zattrs["iohub"]["old_x"] == 0.5 From bb419b29978606e39511046954777a504a710034 Mon Sep 17 00:00:00 2001 From: Ivan Ivanov Date: Thu, 26 Sep 2024 13:36:09 -0700 Subject: [PATCH 38/38] rename old_x to prior_x_scale --- iohub/ngff/nodes.py | 7 +++---- tests/cli/test_cli.py | 17 +++++++++++++---- tests/ngff/test_ngff.py | 2 +- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/iohub/ngff/nodes.py b/iohub/ngff/nodes.py index 5ddf9ad3..6d63a8e0 100644 --- a/iohub/ngff/nodes.py +++ b/iohub/ngff/nodes.py @@ -1064,11 +1064,10 @@ def set_scale( axis_index = self.get_axis_index(axis_name) # Append old scale to metadata - if "iohub" not in self.zattrs: - iohub_dict = {} - else: + iohub_dict = {} + if "iohub" in self.zattrs: iohub_dict = self.zattrs["iohub"] - iohub_dict.update({f"old_{axis_name}": self.scale[axis_index]}) + iohub_dict.update({f"prior_{axis_name}_scale": self.scale[axis_index]}) self.zattrs["iohub"] = iohub_dict # Update scale while preserving existing transforms diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py index 56f9f407..c0dc81f4 100644 --- a/tests/cli/test_cli.py +++ b/tests/cli/test_cli.py @@ -143,9 +143,18 @@ def test_cli_set_scale(): with open_ome_zarr(position_path, layout="fov") as output_dataset: assert tuple(output_dataset.scale[-3:]) == (random_z, 0.5, 0.5) assert output_dataset.scale != old_scale - assert output_dataset.zattrs["iohub"]["old_x"] == old_scale[-1] - assert output_dataset.zattrs["iohub"]["old_y"] == old_scale[-2] - assert output_dataset.zattrs["iohub"]["old_z"] == old_scale[-3] + assert ( + output_dataset.zattrs["iohub"]["prior_x_scale"] + == old_scale[-1] + ) + assert ( + output_dataset.zattrs["iohub"]["prior_y_scale"] + == old_scale[-2] + ) + assert ( + output_dataset.zattrs["iohub"]["prior_z_scale"] + == old_scale[-3] + ) # Test plate-expands-into-positions behavior runner = CliRunner() @@ -161,4 +170,4 @@ def test_cli_set_scale(): ) with open_ome_zarr(position_path, layout="fov") as output_dataset: assert output_dataset.scale[-1] == 0.1 - assert output_dataset.zattrs["iohub"]["old_x"] == 0.5 + assert output_dataset.zattrs["iohub"]["prior_x_scale"] == 0.5 diff --git a/tests/ngff/test_ngff.py b/tests/ngff/test_ngff.py index 1c2480c7..50376a3a 100644 --- a/tests/ngff/test_ngff.py +++ b/tests/ngff/test_ngff.py @@ -432,7 +432,7 @@ def test_set_scale(ch_shape_dtype): with pytest.raises(ValueError): dataset.set_scale(image="0", axis_name="z", new_scale=-1.0) - assert dataset.zattrs["iohub"]["old_z"] == 3.0 + assert dataset.zattrs["iohub"]["prior_z_scale"] == 3.0 @given(channel_names=channel_names_st)