diff --git a/looptrace_regionals_vis/examples/P0001_rois.merge_contributors.csv b/looptrace_regionals_vis/examples/P0001_rois.merge_contributors.csv index 087421b..639399d 100644 --- a/looptrace_regionals_vis/examples/P0001_rois.merge_contributors.csv +++ b/looptrace_regionals_vis/examples/P0001_rois.merge_contributors.csv @@ -1,9 +1,9 @@ -index,fieldOfView,timepoint,spotChannel,zc,yc,xc,intensityMean,zMin,zMax,yMin,yMax,xMin,xMax,mergeIndex +index,fieldOfView,timepoint,spotChannel,zc,yc,xc,intensityMean,zMin,zMax,yMin,yMax,xMin,xMax,mergeIndices 0,P0001.zarr,79,0,3.907628987532479,231.9874778925304,871.9833511648726,118.26726920593931,-2.092371012467521,9.90762898753248,219.9874778925304,243.9874778925304,859.9833511648726,883.9833511648726,101 -1,P0001.zarr,79,0,17.994259347453493,24.042015416774795,1360.0069098862991,117.1394688491732,11.994259347453491,23.994259347453493,12.042015416774795,36.0420154167748,1348.0069098862991,1372.0069098862991,102 +1,P0001.zarr,79,0,17.994259347453493,24.042015416774795,1360.0069098862991,117.1394688491732,11.994259347453491,23.994259347453493,12.042015416774795,36.0420154167748,1348.0069098862991,1372.0069098862991,102;103 2,P0001.zarr,79,0,23.00910242218976,231.98008711401275,871.9596645390719,116.14075915047448,17.009102422189756,29.00910242218976,219.98008711401275,243.98008711401275,859.9596645390719,883.9596645390719,101 -3,P0001.zarr,79,0,16.527137137619988,422.5165732477932,667.2129969610728,157.0530303030303,10.527137137619988,22.527137137619988,410.5165732477932,434.5165732477932,655.2129969610728,679.2129969610728,102 -4,P0001.zarr,79,0,16.950424004488077,118.88259896330818,349.6540530977019,161.81065410400436,10.950424004488076,22.95042400448808,106.88259896330818,130.88259896330817,337.6540530977019,361.6540530977019,102 +3,P0001.zarr,79,0,16.527137137619988,422.5165732477932,667.2129969610728,157.0530303030303,10.527137137619988,22.527137137619988,410.5165732477932,434.5165732477932,655.2129969610728,679.2129969610728,102;103 +4,P0001.zarr,79,0,16.950424004488077,118.88259896330818,349.6540530977019,161.81065410400436,10.950424004488076,22.95042400448808,106.88259896330818,130.88259896330817,337.6540530977019,361.6540530977019,102;103 5,P0001.zarr,79,0,18.177805530982404,445.4564669785048,607.9657421380375,160.33961681087763,12.177805530982404,24.177805530982404,433.4564669785048,457.4564669785048,595.9657421380375,619.9657421380375,100 6,P0001.zarr,79,0,17.83959674876146,1006.0753359579252,306.5263466292306,160.1254275940707,11.839596748761458,23.83959674876146,994.0753359579252,1018.0753359579252,294.5263466292306,318.5263466292306,100 7,P0001.zarr,79,0,17.70877472362621,1040.482813665982,290.6567022086824,163.12094117647058,11.70877472362621,23.70877472362621,1028.482813665982,1052.482813665982,278.6567022086824,302.6567022086824,100 diff --git a/looptrace_regionals_vis/point.py b/looptrace_regionals_vis/point.py index fe41913..5872226 100644 --- a/looptrace_regionals_vis/point.py +++ b/looptrace_regionals_vis/point.py @@ -6,7 +6,7 @@ import numpy as np from numpydoc_decorator import doc # type: ignore[import-untyped] -FloatLike = float | np.float64 +from .types import FloatLike @doc( diff --git a/looptrace_regionals_vis/reader.py b/looptrace_regionals_vis/reader.py index 2229e43..1cd2664 100644 --- a/looptrace_regionals_vis/reader.py +++ b/looptrace_regionals_vis/reader.py @@ -8,27 +8,29 @@ from pathlib import Path from typing import Literal, Optional -import numpy as np import pandas as pd from numpydoc_decorator import doc # type: ignore[import-untyped] from .bounding_box import BoundingBox3D -from .colors import IBM_BLUE, IBM_ORANGE, IBM_PINK, IBM_PURPLE, IBM_YELLOW from .point import FloatLike, Point3D -from .types import LayerParams, PathOrPaths +from .roi import MergeContributorRoi, MergedRoi, NonNuclearRoi, ProximityRejectedRoi, SingletonRoi +from .types import Channel, LayerParams, NucleusNumber, PathOrPaths, RoiIndex, Timepoint +# Aliases FullDataLayer = tuple[list[list[list[int | FloatLike]]], LayerParams, Literal["shapes"]] +IdAndContributors = tuple[RoiIndex, list[RoiIndex]] Reader = Callable[[PathOrPaths], list[FullDataLayer]] - -SHAPE_PARAMS_KEY = "shape_type" -COLOR_PARAMS_KEY = "edge_color" +# Constants Z_COLUMN = "zc" Y_COLUMN = "yc" X_COLUMN = "xc" BOX_CENTER_COLUMN_NAMES = [Z_COLUMN, Y_COLUMN, X_COLUMN] -TIME_COLUMN = "timepoint" CHANNEL_COLUMN = "spotChannel" +COLOR_PARAMS_KEY = "edge_color" +SHAPE_PARAMS_KEY = "shape_type" +TEXT_SIZE = 5 +TIME_COLUMN = "timepoint" class InputFileContentType(Enum): @@ -59,21 +61,6 @@ def from_filepath(cls, fp: Path) -> Optional["InputFileContentType"]: return cls.from_filename(fp.name) -class RoiType(Enum): - """The type of ROI to display, and in which color""" - - MergeContributor = IBM_BLUE - DiscardForProximity = IBM_PURPLE - DiscardForNonNuclearity = IBM_ORANGE - AcceptedSingleton = IBM_PINK - AcceptedMerger = IBM_YELLOW - - @property - def color(self) -> str: - """More reader-friendly alias for accessing the color associated with the ROI type""" - return self.value - - @doc( summary="The main interface for the napari plugin reader contribution", parameters=dict(path="Path to file with data to visualise"), @@ -124,44 +111,75 @@ def build_layers(folder) -> list[FullDataLayer]: # type: ignore[no-untyped-def] for file_type, file_path in file_by_kind.items(): logging.debug("Processing data for file type %s: %s", file_type.name, file_path) - time_channel_location_trios: list[ - tuple[int | FloatLike, int | FloatLike, BoundingBox3D] - ] - if file_type == InputFileContentType.NucleiLabeled: - time_channel_location_trios_by_roi_type = _parse_non_contributor_non_proximal_rois( - file_path - ) + if file_type == InputFileContentType.MergeContributors: + rois_by_type = { + MergeContributorRoi: [ + _parse_merge_contributor_record(row) + for _, row in pd.read_csv(file_path).iterrows() + ] + } + elif file_type == InputFileContentType.NucleiLabeled: + rois_by_type = {} + for roi in _parse_non_contributor_non_proximal_rois(file_path): + rois_by_type.setdefault(type(roi), []).append(roi) + elif file_type == InputFileContentType.ProximityRejects: + rois_by_type = { + ProximityRejectedRoi: [ + ProximityRejectedRoi(timepoint=t, channel=c, bounding_box=b) + for t, c, b in parse_boxes(file_path) + ] + } else: - time_channel_location_trios = parse_boxes(file_path) - if file_type == InputFileContentType.MergeContributors: - rt = RoiType.MergeContributor - elif file_type == InputFileContentType.ProximityRejects: - rt = RoiType.DiscardForProximity - else: - raise RuntimeError( - f"Unexpected file type (can't determine ROI type)! {file_type}" - ) - time_channel_location_trios_by_roi_type = {rt: time_channel_location_trios} - - for ( - roi_type, - time_channel_location_trios, - ) in time_channel_location_trios_by_roi_type.items(): + raise RuntimeError(f"Unexpected file type (can't determine ROI type)! {file_type}") + + for roi_type, rois in rois_by_type.items(): + get_text_color = lambda: rois[0].color # noqa: B023 corners: list[list[list[int | FloatLike]]] = [] shapes: list[str] = [] - for timepoint, channel, box in time_channel_location_trios: - for q1, q2, q3, q4, is_center_slice in box.iter_z_slices_nonnegative(): + for roi in rois: + for ( + q1, + q2, + q3, + q4, + is_center_slice, + ) in roi.bounding_box.iter_z_slices_nonnegative(): corners.append( - [[timepoint, channel, *_point_to_list(pt)] for pt in [q1, q2, q3, q4]] + [ + [roi.timepoint, roi.channel, *_point_to_list(pt)] + for pt in [q1, q2, q3, q4] + ] ) shapes.append("rectangle" if is_center_slice else "ellipse") - logging.debug("Point count for ROI type %s: %d", roi_type.name, len(corners)) + logging.debug("Point count for ROI type %s: %d", roi.typename, len(shapes)) params: dict[str, object] = { - "name": roi_type.name, + "name": roi.typename, "shape_type": shapes, "face_color": "transparent", - COLOR_PARAMS_KEY: roi_type.color, + COLOR_PARAMS_KEY: roi.color, } + if roi_type in [MergeContributorRoi, MergedRoi]: + if roi_type == MergeContributorRoi: + features = { + "index": [roi.index for roi in rois], + "merge_indices": [roi.merge_indices for roi in rois], + } + text = { + "string": "{index} --> {';'.join(sorted(merge_indices))}", + "size": TEXT_SIZE, + "color": get_text_color(), + } + elif roi_type == MergedRoi: + features = { + "index": [roi.index for roi in rois], + "contributors": [roi.contributors for roi in rois], + } + text = { + "string": "{index} <-- {contributors}", + "size": TEXT_SIZE, + "color": get_text_color(), + } + params.update({"features": features, "text": text}) layers.append((corners, params, "shapes")) return layers @@ -180,16 +198,34 @@ def _is_plausible_input_file(path: Path) -> bool: def _parse_non_contributor_non_proximal_rois( path: Path, -) -> dict[RoiType, list[tuple[int | FloatLike, int | FloatLike, BoundingBox3D]]]: - rois_by_type: dict[RoiType, list[tuple[int | FloatLike, int | FloatLike, BoundingBox3D]]] = {} +) -> list[NonNuclearRoi | SingletonRoi | MergedRoi]: + rois: list[NonNuclearRoi | SingletonRoi | MergedRoi] = [] for _, row in pd.read_csv(path).iterrows(): - time, channel, loc, is_singleton, is_in_nuc = _parse_nucleus_labeled_record(row) - if is_in_nuc: - key = RoiType.AcceptedSingleton if is_singleton else RoiType.AcceptedMerger - else: - key = RoiType.DiscardForNonNuclearity - rois_by_type.setdefault(key, []).append((time, channel, loc)) - return rois_by_type + time, channel, box, maybe_nuc_num, maybe_id_and_contribs = _parse_nucleus_labeled_record( + row + ) + match (maybe_nuc_num, maybe_id_and_contribs): + case (None, _): + roi = NonNuclearRoi(timepoint=time, channel=channel, bounding_box=box) + case (nuc_num, None): + roi = SingletonRoi( + timepoint=time, channel=channel, bounding_box=box, nucleus_number=nuc_num + ) + case (nuc_num, (main_id, contrib_ids)): + roi = MergedRoi( + index=main_id, + timepoint=time, + channel=channel, + bounding_box=box, + nucleus_number=nuc_num, + contributors=contrib_ids, + ) + case _: + raise Exception( # noqa: TRY002 + f"Could not determine how to build ROI! maybe_nuc_num={maybe_nuc_num}, maybe_id_and_contribs={maybe_id_and_contribs}" + ) + rois.append(roi) + return rois @doc( @@ -199,21 +235,53 @@ def _parse_non_contributor_non_proximal_rois( ) def parse_boxes( # noqa: D103 path: Path, -) -> list[tuple[int | FloatLike, int | FloatLike, BoundingBox3D]]: +) -> list[tuple[Timepoint, Channel, BoundingBox3D]]: box_cols = [f.name for f in dataclasses.fields(BoundingBox3D) if f.name != "center"] spot_data = pd.read_csv( path, usecols=BOX_CENTER_COLUMN_NAMES + box_cols + [TIME_COLUMN, CHANNEL_COLUMN] ) - time_channel_location_trios: list[tuple[int | FloatLike, int | FloatLike, BoundingBox3D]] = [ + time_channel_location_trios: list[tuple[Timepoint, Channel, BoundingBox3D]] = [ _parse_time_channel_box_trio(record) for _, record in spot_data.iterrows() ] return time_channel_location_trios +def _parse_merge_contributor_record( + record: pd.Series, # type: ignore[type-arg] +) -> MergeContributorRoi: + record: dict[str, int | FloatLike] = record.to_dict() # type: ignore[no-redef] + index: RoiIndex = record["index"] + merge_indices: set[RoiIndex] = {int(i) for i in record["mergeIndices"].split(";")} + time, channel, box = _parse_time_channel_box_trio(record) + return MergeContributorRoi( + index=index, timepoint=time, channel=channel, bounding_box=box, merge_indices=merge_indices + ) + + +def _parse_nucleus_labeled_record( + record: pd.Series, # type: ignore[type-arg] +) -> tuple[Timepoint, Channel, BoundingBox3D, Optional[NucleusNumber], Optional[IdAndContributors]]: + record: dict[str, int | FloatLike] = record.to_dict() # type: ignore[no-redef] + raw_nuc_num: int = record["nucleusNumber"] + maybe_nuc_num: Optional[NucleusNumber] = ( + None if raw_nuc_num == 0 else NucleusNumber(raw_nuc_num) + ) + raw_merge_rois: object = record["mergeRois"] + id_and_contribs: Optional[IdAndContributors] + if raw_merge_rois is None or pd.isna(raw_merge_rois): + id_and_contribs = None + else: + roi_id: RoiIndex = record["index"] + contribs = [int(i) for i in raw_merge_rois.split(";")] + id_and_contribs = (roi_id, contribs) + time, channel, box = _parse_time_channel_box_trio(record) + return time, channel, box, maybe_nuc_num, id_and_contribs + + def _parse_time_channel_box_trio( record: dict[str, int | FloatLike] | pd.Series, # type: ignore[type-arg] -) -> tuple[int | FloatLike, int | FloatLike, BoundingBox3D]: - record: dict[str, int | float | np.float64] = ( # type: ignore[no-redef] +) -> tuple[Timepoint, Channel, BoundingBox3D]: + record: dict[str, int | FloatLike] = ( # type: ignore[no-redef] record if isinstance(record, dict) else record.to_dict() ) time = record.pop(TIME_COLUMN) @@ -230,17 +298,6 @@ def _parse_time_channel_box_trio( return time, channel, box -def _parse_nucleus_labeled_record( - record: pd.Series, # type: ignore[type-arg] -) -> tuple[int | FloatLike, int | FloatLike, BoundingBox3D, bool, bool]: - record: dict[str, int | float | np.float64] = record.to_dict() # type: ignore[no-redef] - nuc_num: int = record["nucleusNumber"] - in_nuc: bool = nuc_num != 0 - is_singleton: bool = record["mergeRois"] is None or pd.isna(record["mergeRois"]) - time, channel, box = _parse_time_channel_box_trio(record) - return time, channel, box, is_singleton, in_nuc - - @doc( summary="Flatten point coordinates to list", parameters=dict(pt="Point to flatten"), diff --git a/looptrace_regionals_vis/roi.py b/looptrace_regionals_vis/roi.py new file mode 100644 index 0000000..20dff83 --- /dev/null +++ b/looptrace_regionals_vis/roi.py @@ -0,0 +1,118 @@ +"""Region of interest (ROI) abstractions""" + +from dataclasses import dataclass +from typing import Protocol + +from .bounding_box import BoundingBox3D +from .colors import IBM_BLUE, IBM_ORANGE, IBM_PINK, IBM_PURPLE, IBM_YELLOW +from .types import Channel, NucleusNumber, RoiIndex, Timepoint + + +class RegionOfInterest(Protocol): + """Attribute each ROI must have to contribute to a Napari layer""" + + @property + def color(self) -> str: + """Which color to use for ROIs of this type""" + ... + + @property + def typename(self) -> str: + """The name of the specific ROI subtype""" + return type(self).__name__ + + +@dataclass(kw_only=True, frozen=True) +class MergeContributorRoi(RegionOfInterest): + """A ROI which contributed to a merge""" + + index: RoiIndex + timepoint: Timepoint + channel: Channel + bounding_box: BoundingBox3D + merge_indices: set[RoiIndex] + + @property + def color(self) -> str: + """A merge contributor ROI is blue.""" + return IBM_BLUE + + def __post_init__(self) -> None: + if self.index in self.merge_indices: + raise ValueError( + f"A {self.__class__.__name__}'s index can't equal be among its merge_indices" + ) + + +@dataclass(kw_only=True, frozen=True) +class ProximityRejectedRoi(RegionOfInterest): + """A ROI which was rejected on account of proximity to another ROI""" + + timepoint: Timepoint + channel: Channel + bounding_box: BoundingBox3D + + @property + def color(self) -> str: + """A ROI rejected on account of proximity is purple.""" + return IBM_PURPLE + + +@dataclass(kw_only=True, frozen=True) +class NonNuclearRoi(RegionOfInterest): + """A ROI which was rejected on account of not being in a nucleus""" + + timepoint: Timepoint + channel: Channel + bounding_box: BoundingBox3D + + @property + def color(self) -> str: + """A ROI rejected on account of non-nuclearity is orange.""" + return IBM_ORANGE + + +@dataclass(kw_only=True, frozen=True) +class SingletonRoi(RegionOfInterest): + """A ROI which passed filters and is not the result of a merge""" + + timepoint: Timepoint + channel: Channel + bounding_box: BoundingBox3D + nucleus_number: NucleusNumber + + @property + def color(self) -> str: + """A singleton ROI is pink.""" + return IBM_PINK + + +@dataclass(kw_only=True, frozen=True) +class MergedRoi(RegionOfInterest): + """A ROI which passed filters and resulted from a merge""" + + index: int + timepoint: Timepoint + channel: Channel + bounding_box: BoundingBox3D + contributors: set[RoiIndex] + nucleus_number: NucleusNumber + + def __post_init__(self) -> None: + if not isinstance(self.contributors, list): + raise TypeError( + f"For a {self.__class__.__name__}, contributors must be list, not {type(self.contributors).__name__}" + ) + if len(self.contributors) < 2: # noqa: PLR2004 + raise ValueError( + f"A {self.__class__.__name__} must have at least 2 contributors, got {len(self.contributors)}" + ) + if self.index in self.contributors: + raise ValueError( + f"A {self.__class__.__name__}'s index can't be in its collection of contributors" + ) + + @property + def color(self) -> str: + """A merged ROI is yellow.""" + return IBM_YELLOW diff --git a/looptrace_regionals_vis/types.py b/looptrace_regionals_vis/types.py index e7cd1b6..1abf692 100644 --- a/looptrace_regionals_vis/types.py +++ b/looptrace_regionals_vis/types.py @@ -1,7 +1,27 @@ """General custom data types""" +from dataclasses import dataclass from pathlib import Path +import numpy as np + +Channel = int +FloatLike = float | np.float64 LayerParams = dict[str, object] PathLike = str | Path PathOrPaths = PathLike | list[PathLike] +RoiIndex = int +Timepoint = int + + +@dataclass(frozen=True) +class NucleusNumber: + """Wrap a positive integer as indicating a particular nucleus.""" + + get: int + + def __post_init__(self) -> None: + if self.get < 1: + raise ValueError( + f"A nucleus number must be a strictly positive integer, not ({type(self.get).__name__}) {self.get}" + ) diff --git a/tests/test_color_determination.py b/tests/test_color_determination.py deleted file mode 100644 index 5f11ce6..0000000 --- a/tests/test_color_determination.py +++ /dev/null @@ -1,26 +0,0 @@ -import pytest - -from looptrace_regionals_vis.colors import IBM_BLUE, IBM_ORANGE, IBM_PINK, IBM_PURPLE, IBM_YELLOW -from looptrace_regionals_vis.reader import RoiType - - -@pytest.mark.parametrize( - ("status", "expected_color"), - [ - (RoiType.MergeContributor, IBM_BLUE), - (RoiType.DiscardForProximity, IBM_PURPLE), - (RoiType.AcceptedSingleton, IBM_PINK), - (RoiType.DiscardForNonNuclearity, IBM_ORANGE), - (RoiType.AcceptedMerger, IBM_YELLOW), - ], -) -def test_colors_are_as_expected(status, expected_color): - observed_color = status.color - assert ( - observed_color == expected_color - ), f"Color for data processing status {status} wasn't as expected ({expected_color}): {observed_color}" - - -@pytest.mark.parametrize("roi_type", list(RoiType)) -def test_each_member_of_data_status_enum_resolves_to_a_color(roi_type): - assert roi_type.color is not None, f"Got null color for data ROI type {roi_type}" diff --git a/tests/test_layers.py b/tests/test_layers.py index bb8c404..ee07ce9 100644 --- a/tests/test_layers.py +++ b/tests/test_layers.py @@ -3,7 +3,15 @@ import pytest from looptrace_regionals_vis import get_package_examples_folder -from looptrace_regionals_vis.reader import COLOR_PARAMS_KEY, LayerParams, RoiType, get_reader +from looptrace_regionals_vis.colors import IBM_BLUE, IBM_ORANGE, IBM_PINK, IBM_PURPLE, IBM_YELLOW +from looptrace_regionals_vis.reader import COLOR_PARAMS_KEY, LayerParams, get_reader +from looptrace_regionals_vis.roi import ( + MergeContributorRoi, + MergedRoi, + NonNuclearRoi, + ProximityRejectedRoi, + SingletonRoi, +) @pytest.mark.skip("not implemented") @@ -12,8 +20,14 @@ def test_shapes_are_as_expected(): def test_colors_are_as_expected(): - # Input data means every RoiType should be found. - exp_color_by_name = {roi.name: roi.value for roi in RoiType} + # Input data means every ROI type should be found. + exp_color_by_name = { + MergeContributorRoi.__name__: IBM_BLUE, + ProximityRejectedRoi.__name__: IBM_PURPLE, + NonNuclearRoi.__name__: IBM_ORANGE, + SingletonRoi.__name__: IBM_PINK, + MergedRoi.__name__: IBM_YELLOW, + } params: list[LayerParams] = determine_parameters(get_package_examples_folder()) obs_color_by_name = {p["name"]: p[COLOR_PARAMS_KEY] for p in params} diff --git a/tests/test_what_can_and_cannot_be_parsed.py b/tests/test_what_can_and_cannot_be_parsed.py index d9acacb..357b0f9 100644 --- a/tests/test_what_can_and_cannot_be_parsed.py +++ b/tests/test_what_can_and_cannot_be_parsed.py @@ -7,19 +7,31 @@ import pytest from looptrace_regionals_vis import get_package_examples_folder, list_package_example_files -from looptrace_regionals_vis.reader import InputFileContentType, RoiType, get_reader +from looptrace_regionals_vis.reader import InputFileContentType, get_reader +from looptrace_regionals_vis.roi import ( + MergeContributorRoi, + MergedRoi, + NonNuclearRoi, + ProximityRejectedRoi, + RegionOfInterest, + SingletonRoi, +) EXAMPLE_FILES: list[Path] = list_package_example_files() -ROI_TYPES_BY_FILE_TYPE: dict[InputFileContentType, set[RoiType]] = { - InputFileContentType.MergeContributors: {RoiType.MergeContributor}, - InputFileContentType.ProximityRejects: {RoiType.DiscardForProximity}, +ROI_TYPES_BY_FILE_TYPE: dict[InputFileContentType, set[RegionOfInterest]] = { + InputFileContentType.MergeContributors: {MergeContributorRoi}, + InputFileContentType.ProximityRejects: {ProximityRejectedRoi}, InputFileContentType.NucleiLabeled: { - RoiType.AcceptedSingleton, - RoiType.DiscardForNonNuclearity, - RoiType.AcceptedMerger, + NonNuclearRoi, + SingletonRoi, + MergedRoi, }, } +NUM_ROI_TYPES = len( + [MergeContributorRoi, ProximityRejectedRoi, NonNuclearRoi, SingletonRoi, MergedRoi] +) + def test_cannot_read_list_of_files(): assert ( @@ -64,8 +76,8 @@ def test_non_csv_files_are_skipped(tmp_path, wrap, paths_to_mutate): read_data = get_reader(wrap(tmp_path)) layers = read_data(tmp_path) - # Input data means every RoiType should be found. - assert len(layers) == len(RoiType) - expected_roi_type_loss + # Input data means every ROI type should be found. + assert len(layers) == NUM_ROI_TYPES - expected_roi_type_loss @pytest.mark.parametrize("wrap", [str, Path]) @@ -77,8 +89,8 @@ def test_csv_with_unparsable_data_processing_status_is_skipped(tmp_path, wrap): read_data = get_reader(wrap(tmp_path)) layers = read_data(tmp_path) - # Input data means every RoiType should be found. - assert len(layers) == len(RoiType) + # Input data means every ROI type should be found. + assert len(layers) == NUM_ROI_TYPES @pytest.mark.parametrize("wrap", [str, Path])