From a607e700327f679f59062a124f48ba4b84e35b32 Mon Sep 17 00:00:00 2001 From: "sewon.jeon" Date: Mon, 1 Dec 2025 14:55:36 +0900 Subject: [PATCH 01/16] Enables automatic transform group tracking for inversion Addresses an issue where `Invertd` fails when postprocessing contains invertible transforms before `Invertd` is called. The solution uses automatic group tracking: `Compose` assigns its ID to child transforms, allowing `Invertd` to filter and select only the relevant transforms for inversion. This ensures correct inversion when multiple transform pipelines are used or when post-processing steps include invertible transforms. `TraceableTransform` now stores group information. `Invertd` now filters transforms by group, falling back to the original behavior if no group information is present (for backward compatibility). Adds tests to verify the fix and group isolation. --- monai/transforms/compose.py | 42 ++++ monai/transforms/inverse.py | 8 +- monai/transforms/post/dictionary.py | 28 ++- monai/utils/enums.py | 1 + tests/transforms/inverse/test_invertd.py | 240 +++++++++++++++++++++++ 5 files changed, 317 insertions(+), 2 deletions(-) diff --git a/monai/transforms/compose.py b/monai/transforms/compose.py index 95653ffbd4..5a9b86b9c0 100644 --- a/monai/transforms/compose.py +++ b/monai/transforms/compose.py @@ -255,6 +255,48 @@ def __init__( self.set_random_state(seed=get_seed()) self.overrides = overrides + # Automatically assign group ID to child transforms for inversion tracking + self._set_transform_groups() + + def _set_transform_groups(self): + """ + Automatically set group IDs on child transforms for inversion tracking. + This allows Invertd to identify which transforms belong to this Compose instance. + Recursively sets groups on wrapped transforms (e.g., array transforms inside dictionary transforms). + """ + from monai.transforms.inverse import TraceableTransform + + group_id = str(id(self)) + visited = set() # Track visited objects to avoid infinite recursion + + def set_group_recursive(obj, gid): + """Recursively set group on transform and its wrapped transforms.""" + # Avoid infinite recursion + obj_id = id(obj) + if obj_id in visited: + return + visited.add(obj_id) + + if isinstance(obj, TraceableTransform): + obj._group = gid + + # Handle wrapped transforms in dictionary transforms + # Check common attribute patterns for wrapped transforms + for attr_name in dir(obj): + # Skip magic methods and common non-transform attributes + if attr_name.startswith('__') or attr_name in ('transforms', 'transform'): + continue + try: + attr = getattr(obj, attr_name, None) + if attr is not None and isinstance(attr, TraceableTransform) and not isinstance(attr, Compose): + # Recursively set group on nested transforms + set_group_recursive(attr, gid) + except Exception: + pass + + for transform in self.transforms: + set_group_recursive(transform, group_id) + @LazyTransform.lazy.setter # type: ignore def lazy(self, val: bool): self._lazy = val diff --git a/monai/transforms/inverse.py b/monai/transforms/inverse.py index ecf918f47a..71ef170185 100644 --- a/monai/transforms/inverse.py +++ b/monai/transforms/inverse.py @@ -125,7 +125,13 @@ def get_transform_info(self) -> dict: self.tracing, self._do_transform if hasattr(self, "_do_transform") else True, ) - return dict(zip(self.transform_info_keys(), vals)) + info = dict(zip(self.transform_info_keys(), vals)) + + # Add group if set (automatically set by Compose) + if hasattr(self, "_group") and self._group is not None: + info[TraceKeys.GROUP] = self._group + + return info def push_transform(self, data, *args, **kwargs): """ diff --git a/monai/transforms/post/dictionary.py b/monai/transforms/post/dictionary.py index 65fdd22b22..67496e62fc 100644 --- a/monai/transforms/post/dictionary.py +++ b/monai/transforms/post/dictionary.py @@ -859,6 +859,29 @@ def __init__( self.post_func = ensure_tuple_rep(post_func, len(self.keys)) self._totensor = ToTensor() + def _filter_transforms_by_group(self, all_transforms: list[dict]) -> list[dict]: + """ + Filter applied_operations to only include transforms from the target Compose instance. + Uses automatic group tracking where Compose assigns its ID to child transforms. + """ + from monai.utils import TraceKeys + + # Get the group ID of the transform (Compose instance) + target_group = str(id(self.transform)) + + # Filter transforms that match the target group + filtered = [] + for xform in all_transforms: + xform_group = xform.get(TraceKeys.GROUP) + if xform_group == target_group: + filtered.append(xform) + + # If no transforms match (backward compatibility), return all transforms + if not filtered: + return all_transforms + + return filtered + def __call__(self, data: Mapping[Hashable, Any]) -> dict[Hashable, Any]: d = dict(data) for ( @@ -894,8 +917,11 @@ def __call__(self, data: Mapping[Hashable, Any]) -> dict[Hashable, Any]: orig_meta_key = orig_meta_key or f"{orig_key}_{meta_key_postfix}" if orig_key in d and isinstance(d[orig_key], MetaTensor): - transform_info = d[orig_key].applied_operations + all_transforms = d[orig_key].applied_operations meta_info = d[orig_key].meta + + # Automatically filter by Compose instance group ID + transform_info = self._filter_transforms_by_group(all_transforms) else: transform_info = d[InvertibleTransform.trace_key(orig_key)] meta_info = d.get(orig_meta_key, {}) diff --git a/monai/utils/enums.py b/monai/utils/enums.py index be00b27d73..d61e31383e 100644 --- a/monai/utils/enums.py +++ b/monai/utils/enums.py @@ -334,6 +334,7 @@ class TraceKeys(StrEnum): TRACING: str = "tracing" STATUSES: str = "statuses" LAZY: str = "lazy" + GROUP: str = "group" class TraceStatusKeys(StrEnum): diff --git a/tests/transforms/inverse/test_invertd.py b/tests/transforms/inverse/test_invertd.py index 2b5e9da85d..af7fe12f3d 100644 --- a/tests/transforms/inverse/test_invertd.py +++ b/tests/transforms/inverse/test_invertd.py @@ -137,6 +137,246 @@ def test_invert(self): set_determinism(seed=None) + def test_invertd_with_postprocessing_transforms(self): + """Test that Invertd ignores postprocessing transforms using automatic group tracking. + + This is a regression test for the issue where Invertd would fail when + postprocessing contains invertible transforms before Invertd is called. + The fix uses automatic group tracking where Compose assigns its ID to child transforms. + """ + from monai.data import MetaTensor, create_test_image_2d + from monai.transforms.utility.dictionary import Lambdad + + img, _ = create_test_image_2d(60, 60, 2, 10, num_seg_classes=2) + img = MetaTensor(img, meta={"original_channel_dim": float("nan"), "pixdim": [1.0, 1.0, 1.0]}) + key = "image" + + # Preprocessing pipeline + preprocessing = Compose([ + EnsureChannelFirstd(key), + Spacingd(key, pixdim=[2.0, 2.0]), + ]) + + # Postprocessing with Lambdad before Invertd + # Previously this would raise RuntimeError about transform ID mismatch + postprocessing = Compose([ + Lambdad(key, func=lambda x: x), # Should be ignored during inversion + Invertd(key, transform=preprocessing, orig_keys=key) + ]) + + # Apply transforms + item = {key: img} + pre = preprocessing(item) + + # This should NOT raise an error (was failing before the fix) + try: + post = postprocessing(pre) + # If we get here, the bug is fixed + self.assertIsNotNone(post) + self.assertIn(key, post) + print(f"SUCCESS! Automatic group tracking fixed the bug.") + print(f" Preprocessing group ID: {id(preprocessing)}") + print(f" Postprocessing group ID: {id(postprocessing)}") + except RuntimeError as e: + if "getting the most recently applied invertible transform" in str(e): + self.fail(f"Invertd still has the postprocessing transform bug: {e}") + + def test_invertd_multiple_pipelines(self): + """Test that Invertd correctly handles multiple independent preprocessing pipelines.""" + from monai.data import MetaTensor, create_test_image_2d + from monai.transforms.utility.dictionary import Lambdad + + img1, _ = create_test_image_2d(60, 60, 2, 10, num_seg_classes=2) + img1 = MetaTensor(img1, meta={"original_channel_dim": float("nan"), "pixdim": [1.0, 1.0, 1.0]}) + img2, _ = create_test_image_2d(60, 60, 2, 10, num_seg_classes=2) + img2 = MetaTensor(img2, meta={"original_channel_dim": float("nan"), "pixdim": [1.0, 1.0, 1.0]}) + + # Two different preprocessing pipelines + preprocessing1 = Compose([ + EnsureChannelFirstd("image1"), + Spacingd("image1", pixdim=[2.0, 2.0]), + ]) + + preprocessing2 = Compose([ + EnsureChannelFirstd("image2"), + Spacingd("image2", pixdim=[1.5, 1.5]), + ]) + + # Postprocessing that inverts both + postprocessing = Compose([ + Lambdad(["image1", "image2"], func=lambda x: x), + Invertd("image1", transform=preprocessing1, orig_keys="image1"), + Invertd("image2", transform=preprocessing2, orig_keys="image2"), + ]) + + # Apply transforms + item = {"image1": img1, "image2": img2} + pre1 = preprocessing1(item) + pre2 = preprocessing2(pre1) + + # Should not raise error - each Invertd should only invert its own pipeline + post = postprocessing(pre2) + self.assertIn("image1", post) + self.assertIn("image2", post) + + def test_invertd_multiple_postprocessing_transforms(self): + """Test Invertd with multiple invertible transforms in postprocessing before Invertd.""" + from monai.data import MetaTensor, create_test_image_2d + from monai.transforms.utility.dictionary import Lambdad + + img, _ = create_test_image_2d(60, 60, 2, 10, num_seg_classes=2) + img = MetaTensor(img, meta={"original_channel_dim": float("nan"), "pixdim": [1.0, 1.0, 1.0]}) + key = "image" + + preprocessing = Compose([ + EnsureChannelFirstd(key), + Spacingd(key, pixdim=[2.0, 2.0]), + ]) + + # Multiple transforms in postprocessing before Invertd + postprocessing = Compose([ + Lambdad(key, func=lambda x: x * 2), + Lambdad(key, func=lambda x: x + 1), + Lambdad(key, func=lambda x: x - 1), + Invertd(key, transform=preprocessing, orig_keys=key) + ]) + + item = {key: img} + pre = preprocessing(item) + post = postprocessing(pre) + + self.assertIsNotNone(post) + self.assertIn(key, post) + + def test_invertd_group_isolation(self): + """Test that groups correctly isolate transforms from different Compose instances.""" + from monai.data import MetaTensor, create_test_image_2d + + img, _ = create_test_image_2d(60, 60, 2, 10, num_seg_classes=2) + img = MetaTensor(img, meta={"original_channel_dim": float("nan"), "pixdim": [1.0, 1.0, 1.0]}) + key = "image" + + # First preprocessing + preprocessing1 = Compose([ + EnsureChannelFirstd(key), + Spacingd(key, pixdim=[2.0, 2.0]), + ]) + + # Second preprocessing (different pipeline) + preprocessing2 = Compose([ + Spacingd(key, pixdim=[1.5, 1.5]), + ]) + + item = {key: img} + pre1 = preprocessing1(item) + + # Verify group IDs are in applied_operations + self.assertTrue(len(pre1[key].applied_operations) > 0) + group1 = pre1[key].applied_operations[0].get("group") + self.assertIsNotNone(group1) + self.assertEqual(group1, str(id(preprocessing1))) + + # Apply second preprocessing + pre2 = preprocessing2(pre1) + + # Should have operations from both pipelines with different groups + groups = [op.get("group") for op in pre2[key].applied_operations] + self.assertIn(str(id(preprocessing1)), groups) + self.assertIn(str(id(preprocessing2)), groups) + + # Inverting preprocessing1 should only invert its transforms + inverter = Invertd(key, transform=preprocessing1, orig_keys=key) + inverted = inverter(pre2) + self.assertIsNotNone(inverted) + + def test_compose_inverse_with_groups(self): + """Test that Compose.inverse() works correctly with automatic group tracking.""" + from monai.data import MetaTensor, create_test_image_2d + + img, _ = create_test_image_2d(60, 60, 2, 10, num_seg_classes=2) + img = MetaTensor(img, meta={"original_channel_dim": float("nan"), "pixdim": [1.0, 1.0, 1.0]}) + key = "image" + + # Create a preprocessing pipeline + preprocessing = Compose([ + EnsureChannelFirstd(key), + Spacingd(key, pixdim=[2.0, 2.0]), + ]) + + # Apply preprocessing + item = {key: img} + pre = preprocessing(item) + + # Call inverse() directly on the Compose object + inverted = preprocessing.inverse(pre) + + # Should successfully invert + self.assertIsNotNone(inverted) + self.assertIn(key, inverted) + # Shape should be restored after inversion + self.assertEqual(inverted[key].shape[1:], img.shape) + + def test_compose_inverse_with_postprocessing_groups(self): + """Test Compose.inverse() when data has been through multiple pipelines with different groups.""" + from monai.data import MetaTensor, create_test_image_2d + from monai.transforms.utility.dictionary import Lambdad + + img, _ = create_test_image_2d(60, 60, 2, 10, num_seg_classes=2) + img = MetaTensor(img, meta={"original_channel_dim": float("nan"), "pixdim": [1.0, 1.0, 1.0]}) + key = "image" + + # Preprocessing pipeline + preprocessing = Compose([ + EnsureChannelFirstd(key), + Spacingd(key, pixdim=[2.0, 2.0]), + ]) + + # Postprocessing pipeline (different group) + postprocessing = Compose([ + Lambdad(key, func=lambda x: x * 2), + ]) + + # Apply both pipelines + item = {key: img} + pre = preprocessing(item) + post = postprocessing(pre) + + # Now call inverse() directly on preprocessing + # This tests that inverse() can handle data that has transforms from multiple groups + # This WILL fail because applied_operations contains postprocessing transforms + # and inverse() doesn't do group filtering (only Invertd does) + with self.assertRaises(RuntimeError): + inverted = preprocessing.inverse(post) + + def test_mixed_invertd_and_compose_inverse(self): + """Test mixing Invertd (with group filtering) and Compose.inverse() (without filtering).""" + from monai.data import MetaTensor, create_test_image_2d + + img, _ = create_test_image_2d(60, 60, 2, 10, num_seg_classes=2) + img = MetaTensor(img, meta={"original_channel_dim": float("nan"), "pixdim": [1.0, 1.0, 1.0]}) + key = "image" + + # First pipeline + pipeline1 = Compose([ + EnsureChannelFirstd(key), + Spacingd(key, pixdim=[2.0, 2.0]), + ]) + + # Apply first pipeline + item = {key: img} + result1 = pipeline1(item) + + # Use Compose.inverse() directly - should work fine + inverted1 = pipeline1.inverse(result1) + self.assertIsNotNone(inverted1) + self.assertEqual(inverted1[key].shape[1:], img.shape) + + # Now apply pipeline again and use Invertd + result2 = pipeline1(item) + inverter = Invertd(key, transform=pipeline1, orig_keys=key) + inverted2 = inverter(result2) + self.assertIsNotNone(inverted2) + if __name__ == "__main__": unittest.main() From 08d007619abdd16b79bbae18cf34ebb180e131d0 Mon Sep 17 00:00:00 2001 From: "sewon.jeon" Date: Mon, 1 Dec 2025 15:01:37 +0900 Subject: [PATCH 02/16] autofix formatting DCO Remediation Commit for sewon.jeon I, sewon.jeon , hereby add my Signed-off-by to this commit: d97bdd574db39960dd42046ff2b868f9ac091316 I, sewon.jeon , hereby add my Signed-off-by to this commit: c523e945155f1662eef7e73dc562df7bd812e61d Signed-off-by: sewon.jeon --- monai/transforms/compose.py | 2 +- tests/transforms/inverse/test_invertd.py | 88 +++++++++--------------- 2 files changed, 34 insertions(+), 56 deletions(-) diff --git a/monai/transforms/compose.py b/monai/transforms/compose.py index 5a9b86b9c0..7f7bf94084 100644 --- a/monai/transforms/compose.py +++ b/monai/transforms/compose.py @@ -284,7 +284,7 @@ def set_group_recursive(obj, gid): # Check common attribute patterns for wrapped transforms for attr_name in dir(obj): # Skip magic methods and common non-transform attributes - if attr_name.startswith('__') or attr_name in ('transforms', 'transform'): + if attr_name.startswith("__") or attr_name in ("transforms", "transform"): continue try: attr = getattr(obj, attr_name, None) diff --git a/tests/transforms/inverse/test_invertd.py b/tests/transforms/inverse/test_invertd.py index af7fe12f3d..dac9433b58 100644 --- a/tests/transforms/inverse/test_invertd.py +++ b/tests/transforms/inverse/test_invertd.py @@ -152,17 +152,16 @@ def test_invertd_with_postprocessing_transforms(self): key = "image" # Preprocessing pipeline - preprocessing = Compose([ - EnsureChannelFirstd(key), - Spacingd(key, pixdim=[2.0, 2.0]), - ]) + preprocessing = Compose([EnsureChannelFirstd(key), Spacingd(key, pixdim=[2.0, 2.0])]) # Postprocessing with Lambdad before Invertd # Previously this would raise RuntimeError about transform ID mismatch - postprocessing = Compose([ - Lambdad(key, func=lambda x: x), # Should be ignored during inversion - Invertd(key, transform=preprocessing, orig_keys=key) - ]) + postprocessing = Compose( + [ + Lambdad(key, func=lambda x: x), # Should be ignored during inversion + Invertd(key, transform=preprocessing, orig_keys=key), + ] + ) # Apply transforms item = {key: img} @@ -174,7 +173,7 @@ def test_invertd_with_postprocessing_transforms(self): # If we get here, the bug is fixed self.assertIsNotNone(post) self.assertIn(key, post) - print(f"SUCCESS! Automatic group tracking fixed the bug.") + print("SUCCESS! Automatic group tracking fixed the bug.") print(f" Preprocessing group ID: {id(preprocessing)}") print(f" Postprocessing group ID: {id(postprocessing)}") except RuntimeError as e: @@ -192,22 +191,18 @@ def test_invertd_multiple_pipelines(self): img2 = MetaTensor(img2, meta={"original_channel_dim": float("nan"), "pixdim": [1.0, 1.0, 1.0]}) # Two different preprocessing pipelines - preprocessing1 = Compose([ - EnsureChannelFirstd("image1"), - Spacingd("image1", pixdim=[2.0, 2.0]), - ]) + preprocessing1 = Compose([EnsureChannelFirstd("image1"), Spacingd("image1", pixdim=[2.0, 2.0])]) - preprocessing2 = Compose([ - EnsureChannelFirstd("image2"), - Spacingd("image2", pixdim=[1.5, 1.5]), - ]) + preprocessing2 = Compose([EnsureChannelFirstd("image2"), Spacingd("image2", pixdim=[1.5, 1.5])]) # Postprocessing that inverts both - postprocessing = Compose([ - Lambdad(["image1", "image2"], func=lambda x: x), - Invertd("image1", transform=preprocessing1, orig_keys="image1"), - Invertd("image2", transform=preprocessing2, orig_keys="image2"), - ]) + postprocessing = Compose( + [ + Lambdad(["image1", "image2"], func=lambda x: x), + Invertd("image1", transform=preprocessing1, orig_keys="image1"), + Invertd("image2", transform=preprocessing2, orig_keys="image2"), + ] + ) # Apply transforms item = {"image1": img1, "image2": img2} @@ -228,18 +223,17 @@ def test_invertd_multiple_postprocessing_transforms(self): img = MetaTensor(img, meta={"original_channel_dim": float("nan"), "pixdim": [1.0, 1.0, 1.0]}) key = "image" - preprocessing = Compose([ - EnsureChannelFirstd(key), - Spacingd(key, pixdim=[2.0, 2.0]), - ]) + preprocessing = Compose([EnsureChannelFirstd(key), Spacingd(key, pixdim=[2.0, 2.0])]) # Multiple transforms in postprocessing before Invertd - postprocessing = Compose([ - Lambdad(key, func=lambda x: x * 2), - Lambdad(key, func=lambda x: x + 1), - Lambdad(key, func=lambda x: x - 1), - Invertd(key, transform=preprocessing, orig_keys=key) - ]) + postprocessing = Compose( + [ + Lambdad(key, func=lambda x: x * 2), + Lambdad(key, func=lambda x: x + 1), + Lambdad(key, func=lambda x: x - 1), + Invertd(key, transform=preprocessing, orig_keys=key), + ] + ) item = {key: img} pre = preprocessing(item) @@ -257,15 +251,10 @@ def test_invertd_group_isolation(self): key = "image" # First preprocessing - preprocessing1 = Compose([ - EnsureChannelFirstd(key), - Spacingd(key, pixdim=[2.0, 2.0]), - ]) + preprocessing1 = Compose([EnsureChannelFirstd(key), Spacingd(key, pixdim=[2.0, 2.0])]) # Second preprocessing (different pipeline) - preprocessing2 = Compose([ - Spacingd(key, pixdim=[1.5, 1.5]), - ]) + preprocessing2 = Compose([Spacingd(key, pixdim=[1.5, 1.5])]) item = {key: img} pre1 = preprocessing1(item) @@ -298,10 +287,7 @@ def test_compose_inverse_with_groups(self): key = "image" # Create a preprocessing pipeline - preprocessing = Compose([ - EnsureChannelFirstd(key), - Spacingd(key, pixdim=[2.0, 2.0]), - ]) + preprocessing = Compose([EnsureChannelFirstd(key), Spacingd(key, pixdim=[2.0, 2.0])]) # Apply preprocessing item = {key: img} @@ -326,15 +312,10 @@ def test_compose_inverse_with_postprocessing_groups(self): key = "image" # Preprocessing pipeline - preprocessing = Compose([ - EnsureChannelFirstd(key), - Spacingd(key, pixdim=[2.0, 2.0]), - ]) + preprocessing = Compose([EnsureChannelFirstd(key), Spacingd(key, pixdim=[2.0, 2.0])]) # Postprocessing pipeline (different group) - postprocessing = Compose([ - Lambdad(key, func=lambda x: x * 2), - ]) + postprocessing = Compose([Lambdad(key, func=lambda x: x * 2)]) # Apply both pipelines item = {key: img} @@ -346,7 +327,7 @@ def test_compose_inverse_with_postprocessing_groups(self): # This WILL fail because applied_operations contains postprocessing transforms # and inverse() doesn't do group filtering (only Invertd does) with self.assertRaises(RuntimeError): - inverted = preprocessing.inverse(post) + preprocessing.inverse(post) def test_mixed_invertd_and_compose_inverse(self): """Test mixing Invertd (with group filtering) and Compose.inverse() (without filtering).""" @@ -357,10 +338,7 @@ def test_mixed_invertd_and_compose_inverse(self): key = "image" # First pipeline - pipeline1 = Compose([ - EnsureChannelFirstd(key), - Spacingd(key, pixdim=[2.0, 2.0]), - ]) + pipeline1 = Compose([EnsureChannelFirstd(key), Spacingd(key, pixdim=[2.0, 2.0])]) # Apply first pipeline item = {key: img} From 24bc645a4dbff52c4b616a7b71d415862a4e16bb Mon Sep 17 00:00:00 2001 From: "sewon.jeon" Date: Mon, 1 Dec 2025 15:27:32 +0900 Subject: [PATCH 03/16] fix errors Signed-off-by: sewon.jeon --- monai/transforms/compose.py | 11 ++++------- monai/transforms/inverse.py | 9 ++++++++- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/monai/transforms/compose.py b/monai/transforms/compose.py index 7f7bf94084..34cea479b7 100644 --- a/monai/transforms/compose.py +++ b/monai/transforms/compose.py @@ -286,13 +286,10 @@ def set_group_recursive(obj, gid): # Skip magic methods and common non-transform attributes if attr_name.startswith("__") or attr_name in ("transforms", "transform"): continue - try: - attr = getattr(obj, attr_name, None) - if attr is not None and isinstance(attr, TraceableTransform) and not isinstance(attr, Compose): - # Recursively set group on nested transforms - set_group_recursive(attr, gid) - except Exception: - pass + attr = getattr(obj, attr_name, None) + if attr is not None and isinstance(attr, TraceableTransform) and not isinstance(attr, Compose): + # Recursively set group on nested transforms + set_group_recursive(attr, gid) for transform in self.transforms: set_group_recursive(transform, group_id) diff --git a/monai/transforms/inverse.py b/monai/transforms/inverse.py index 71ef170185..9e0bc6e2b4 100644 --- a/monai/transforms/inverse.py +++ b/monai/transforms/inverse.py @@ -82,6 +82,10 @@ def _init_trace_threadlocal(self): if not hasattr(self._tracing, "value"): self._tracing.value = MONAIEnvVars.trace_transform() != "0" + # Initialize group identifier (set by Compose for automatic group tracking) + if not hasattr(self, "_group"): + self._group: str | None = None + def __getstate__(self): """When pickling, remove the `_tracing` member from the output, if present, since it's not picklable.""" _dict = dict(getattr(self, "__dict__", {})) # this makes __dict__ always present in the unpickled object @@ -119,6 +123,9 @@ def get_transform_info(self) -> dict: """ Return a dictionary with the relevant information pertaining to an applied transform. """ + # Ensure _group is initialized + self._init_trace_threadlocal() + vals = ( self.__class__.__name__, id(self), @@ -128,7 +135,7 @@ def get_transform_info(self) -> dict: info = dict(zip(self.transform_info_keys(), vals)) # Add group if set (automatically set by Compose) - if hasattr(self, "_group") and self._group is not None: + if self._group is not None: info[TraceKeys.GROUP] = self._group return info From 4a918610715c9c15bb12315f6bdbcd1c148ad098 Mon Sep 17 00:00:00 2001 From: sewon jeon Date: Tue, 24 Feb 2026 10:03:01 +0900 Subject: [PATCH 04/16] Add Google-style docstrings for transform group helpers --- monai/transforms/compose.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/monai/transforms/compose.py b/monai/transforms/compose.py index 34cea479b7..6051cc9408 100644 --- a/monai/transforms/compose.py +++ b/monai/transforms/compose.py @@ -261,8 +261,16 @@ def __init__( def _set_transform_groups(self): """ Automatically set group IDs on child transforms for inversion tracking. - This allows Invertd to identify which transforms belong to this Compose instance. - Recursively sets groups on wrapped transforms (e.g., array transforms inside dictionary transforms). + + This allows Invertd to identify which transforms belong to this + ``Compose`` instance, including wrapped transforms (for example, + array transforms inside dictionary transforms). + + Args: + None. + + Returns: + None. """ from monai.transforms.inverse import TraceableTransform @@ -270,7 +278,16 @@ def _set_transform_groups(self): visited = set() # Track visited objects to avoid infinite recursion def set_group_recursive(obj, gid): - """Recursively set group on transform and its wrapped transforms.""" + """ + Recursively set a group ID on a transform and its wrapped transforms. + + Args: + obj: Transform instance to process. + gid: Group identifier to assign. + + Returns: + None. + """ # Avoid infinite recursion obj_id = id(obj) if obj_id in visited: From f1f92e02403bbd9ae27300dfd5ca57aa9794e0a6 Mon Sep 17 00:00:00 2001 From: sewon jeon Date: Tue, 24 Feb 2026 13:33:14 +0900 Subject: [PATCH 05/16] Assign group IDs to wrapped transforms in Compose Extend Compose group tagging to discover traceable transforms wrapped in object attributes and container fields, including common patterns like transform/transforms wrappers. Preserve nested Compose boundaries by setting group IDs only on the nested Compose instance itself, without traversing its internal pipeline. Add a regression test covering wrapped transform attributes and nested container traversal. python -m pip install -U -r requirements-dev.txt --- monai/transforms/compose.py | 50 ++++++++++++++++++------ tests/transforms/compose/test_compose.py | 37 ++++++++++++++++++ 2 files changed, 76 insertions(+), 11 deletions(-) diff --git a/monai/transforms/compose.py b/monai/transforms/compose.py index 6051cc9408..1767ed4a20 100644 --- a/monai/transforms/compose.py +++ b/monai/transforms/compose.py @@ -277,39 +277,67 @@ def _set_transform_groups(self): group_id = str(id(self)) visited = set() # Track visited objects to avoid infinite recursion - def set_group_recursive(obj, gid): + def set_group_recursive(obj, gid, allow_compose: bool = False): """ Recursively set a group ID on a transform and its wrapped transforms. Args: obj: Transform instance to process. gid: Group identifier to assign. + allow_compose: Whether to set group on ``Compose`` instances. + ``Compose`` internals are not traversed to preserve nested + pipeline boundaries. Returns: None. """ + if obj is None or isinstance(obj, (bool, int, float, str, bytes)): + return + # Avoid infinite recursion obj_id = id(obj) if obj_id in visited: return visited.add(obj_id) + if isinstance(obj, Compose): + if allow_compose: + obj._group = gid + return + if isinstance(obj, TraceableTransform): obj._group = gid - # Handle wrapped transforms in dictionary transforms - # Check common attribute patterns for wrapped transforms - for attr_name in dir(obj): - # Skip magic methods and common non-transform attributes - if attr_name.startswith("__") or attr_name in ("transforms", "transform"): - continue - attr = getattr(obj, attr_name, None) - if attr is not None and isinstance(attr, TraceableTransform) and not isinstance(attr, Compose): - # Recursively set group on nested transforms + if isinstance(obj, Mapping): + for attr in obj.values(): + set_group_recursive(attr, gid) + return + + if isinstance(obj, (list, tuple, set)): + for attr in obj: set_group_recursive(attr, gid) + return + + attrs: list[Any] = [] + if hasattr(obj, "__dict__"): + attrs.extend(vars(obj).values()) + + slots = getattr(type(obj), "__slots__", ()) + if isinstance(slots, str): + slots = (slots,) + for slot in slots: + if slot.startswith("__"): + continue + try: + attrs.append(getattr(obj, slot)) + except AttributeError: + continue + + for attr in attrs: + set_group_recursive(attr, gid) for transform in self.transforms: - set_group_recursive(transform, group_id) + set_group_recursive(transform, group_id, allow_compose=True) @LazyTransform.lazy.setter # type: ignore def lazy(self, val: bool): diff --git a/tests/transforms/compose/test_compose.py b/tests/transforms/compose/test_compose.py index 96c6d4606f..132aaeabb5 100644 --- a/tests/transforms/compose/test_compose.py +++ b/tests/transforms/compose/test_compose.py @@ -268,6 +268,43 @@ def test_data_loader_2(self): self.assertAlmostEqual(out_1.cpu().item(), 0.28602141572) set_determinism(None) + def test_set_transform_groups_on_wrapped_transform_attributes(self): + class _IdentityInvertible(mt.InvertibleTransform): + def __call__(self, data): + return data + + def inverse(self, data): + return data + + class _WrapperWithTransform: + def __init__(self): + self.transform = _IdentityInvertible() + + def __call__(self, data): + return self.transform(data) + + class _WrapperWithTransforms: + def __init__(self): + self.transforms = [_IdentityInvertible(), {"inner": _IdentityInvertible()}] + + def __call__(self, data): + for transform in self.transforms: + if isinstance(transform, dict): + for nested_transform in transform.values(): + data = nested_transform(data) + else: + data = transform(data) + return data + + wrapped_transform = _WrapperWithTransform() + wrapped_transforms = _WrapperWithTransforms() + composed = mt.Compose([wrapped_transform, wrapped_transforms]) + expected_group = str(id(composed)) + + self.assertEqual(getattr(wrapped_transform.transform, "_group", None), expected_group) + self.assertEqual(getattr(wrapped_transforms.transforms[0], "_group", None), expected_group) + self.assertEqual(getattr(wrapped_transforms.transforms[1]["inner"], "_group", None), expected_group) + def test_flatten_and_len(self): x = mt.EnsureChannelFirst(channel_dim="no_channel") t1 = mt.Compose([x, x, x, x, mt.Compose([mt.Compose([x, x]), x, x])]) From d110fdc85ed1c857548e181e400f6f4e6dc92c86 Mon Sep 17 00:00:00 2001 From: sewon jeon Date: Tue, 24 Feb 2026 13:37:42 +0900 Subject: [PATCH 06/16] DCO Remediation Commit for sewon jeon I, sewon jeon , hereby add my Signed-off-by to this commit: 821bc01667e00bd0ed2658eda5aa9b621b722923 I, sewon jeon , hereby add my Signed-off-by to this commit: 6e854a8789222d1f1b50112b1ab9a99095aea46f Signed-off-by: sewon jeon From 6d8b0c564d2fd685e4a1ef0665b2d648af13968f Mon Sep 17 00:00:00 2001 From: "sewon.jeon" Date: Wed, 25 Feb 2026 00:06:37 +0900 Subject: [PATCH 07/16] address the comments Signed-off-by: sewon jeon --- monai/transforms/post/dictionary.py | 15 +++-------- tests/transforms/inverse/test_invertd.py | 34 +++++++----------------- 2 files changed, 13 insertions(+), 36 deletions(-) diff --git a/monai/transforms/post/dictionary.py b/monai/transforms/post/dictionary.py index 67496e62fc..5eb5aa7616 100644 --- a/monai/transforms/post/dictionary.py +++ b/monai/transforms/post/dictionary.py @@ -48,7 +48,7 @@ from monai.transforms.transform import MapTransform from monai.transforms.utility.array import ToTensor from monai.transforms.utils import allow_missing_keys_mode, convert_applied_interp_mode -from monai.utils import PostFix, convert_to_tensor, ensure_tuple, ensure_tuple_rep +from monai.utils import PostFix, TraceKeys, convert_to_tensor, ensure_tuple, ensure_tuple_rep from monai.utils.type_conversion import convert_to_dst_type __all__ = [ @@ -864,23 +864,14 @@ def _filter_transforms_by_group(self, all_transforms: list[dict]) -> list[dict]: Filter applied_operations to only include transforms from the target Compose instance. Uses automatic group tracking where Compose assigns its ID to child transforms. """ - from monai.utils import TraceKeys - # Get the group ID of the transform (Compose instance) target_group = str(id(self.transform)) # Filter transforms that match the target group - filtered = [] - for xform in all_transforms: - xform_group = xform.get(TraceKeys.GROUP) - if xform_group == target_group: - filtered.append(xform) + filtered = [xform for xform in all_transforms if xform.get(TraceKeys.GROUP) == target_group] # If no transforms match (backward compatibility), return all transforms - if not filtered: - return all_transforms - - return filtered + return filtered if filtered else all_transforms def __call__(self, data: Mapping[Hashable, Any]) -> dict[Hashable, Any]: d = dict(data) diff --git a/tests/transforms/inverse/test_invertd.py b/tests/transforms/inverse/test_invertd.py index dac9433b58..ecb36df591 100644 --- a/tests/transforms/inverse/test_invertd.py +++ b/tests/transforms/inverse/test_invertd.py @@ -17,7 +17,7 @@ import numpy as np import torch -from monai.data import DataLoader, Dataset, create_test_image_3d, decollate_batch +from monai.data import DataLoader, Dataset, MetaTensor, create_test_image_2d, create_test_image_3d, decollate_batch from monai.transforms import ( CastToTyped, Compose, @@ -36,6 +36,7 @@ ScaleIntensityd, Spacingd, ) +from monai.transforms.utility.dictionary import Lambdad from monai.utils import set_determinism from tests.test_utils import assert_allclose, make_nifti_image @@ -144,9 +145,6 @@ def test_invertd_with_postprocessing_transforms(self): postprocessing contains invertible transforms before Invertd is called. The fix uses automatic group tracking where Compose assigns its ID to child transforms. """ - from monai.data import MetaTensor, create_test_image_2d - from monai.transforms.utility.dictionary import Lambdad - img, _ = create_test_image_2d(60, 60, 2, 10, num_seg_classes=2) img = MetaTensor(img, meta={"original_channel_dim": float("nan"), "pixdim": [1.0, 1.0, 1.0]}) key = "image" @@ -173,18 +171,12 @@ def test_invertd_with_postprocessing_transforms(self): # If we get here, the bug is fixed self.assertIsNotNone(post) self.assertIn(key, post) - print("SUCCESS! Automatic group tracking fixed the bug.") - print(f" Preprocessing group ID: {id(preprocessing)}") - print(f" Postprocessing group ID: {id(postprocessing)}") except RuntimeError as e: if "getting the most recently applied invertible transform" in str(e): self.fail(f"Invertd still has the postprocessing transform bug: {e}") def test_invertd_multiple_pipelines(self): """Test that Invertd correctly handles multiple independent preprocessing pipelines.""" - from monai.data import MetaTensor, create_test_image_2d - from monai.transforms.utility.dictionary import Lambdad - img1, _ = create_test_image_2d(60, 60, 2, 10, num_seg_classes=2) img1 = MetaTensor(img1, meta={"original_channel_dim": float("nan"), "pixdim": [1.0, 1.0, 1.0]}) img2, _ = create_test_image_2d(60, 60, 2, 10, num_seg_classes=2) @@ -216,9 +208,6 @@ def test_invertd_multiple_pipelines(self): def test_invertd_multiple_postprocessing_transforms(self): """Test Invertd with multiple invertible transforms in postprocessing before Invertd.""" - from monai.data import MetaTensor, create_test_image_2d - from monai.transforms.utility.dictionary import Lambdad - img, _ = create_test_image_2d(60, 60, 2, 10, num_seg_classes=2) img = MetaTensor(img, meta={"original_channel_dim": float("nan"), "pixdim": [1.0, 1.0, 1.0]}) key = "image" @@ -244,8 +233,6 @@ def test_invertd_multiple_postprocessing_transforms(self): def test_invertd_group_isolation(self): """Test that groups correctly isolate transforms from different Compose instances.""" - from monai.data import MetaTensor, create_test_image_2d - img, _ = create_test_image_2d(60, 60, 2, 10, num_seg_classes=2) img = MetaTensor(img, meta={"original_channel_dim": float("nan"), "pixdim": [1.0, 1.0, 1.0]}) key = "image" @@ -267,21 +254,25 @@ def test_invertd_group_isolation(self): # Apply second preprocessing pre2 = preprocessing2(pre1) + self.assertTupleEqual(pre2[key].shape, (1, 40, 40)) # Should have operations from both pipelines with different groups groups = [op.get("group") for op in pre2[key].applied_operations] - self.assertIn(str(id(preprocessing1)), groups) - self.assertIn(str(id(preprocessing2)), groups) + preprocessing1_group = str(id(preprocessing1)) + preprocessing2_group = str(id(preprocessing2)) + self.assertIn(preprocessing1_group, groups) + self.assertIn(preprocessing2_group, groups) + self.assertEqual(groups.count(preprocessing1_group), 1) + self.assertEqual(groups.count(preprocessing2_group), 1) # Inverting preprocessing1 should only invert its transforms inverter = Invertd(key, transform=preprocessing1, orig_keys=key) inverted = inverter(pre2) self.assertIsNotNone(inverted) + self.assertTupleEqual(inverted[key].shape, (1, 60, 60)) def test_compose_inverse_with_groups(self): """Test that Compose.inverse() works correctly with automatic group tracking.""" - from monai.data import MetaTensor, create_test_image_2d - img, _ = create_test_image_2d(60, 60, 2, 10, num_seg_classes=2) img = MetaTensor(img, meta={"original_channel_dim": float("nan"), "pixdim": [1.0, 1.0, 1.0]}) key = "image" @@ -304,9 +295,6 @@ def test_compose_inverse_with_groups(self): def test_compose_inverse_with_postprocessing_groups(self): """Test Compose.inverse() when data has been through multiple pipelines with different groups.""" - from monai.data import MetaTensor, create_test_image_2d - from monai.transforms.utility.dictionary import Lambdad - img, _ = create_test_image_2d(60, 60, 2, 10, num_seg_classes=2) img = MetaTensor(img, meta={"original_channel_dim": float("nan"), "pixdim": [1.0, 1.0, 1.0]}) key = "image" @@ -331,8 +319,6 @@ def test_compose_inverse_with_postprocessing_groups(self): def test_mixed_invertd_and_compose_inverse(self): """Test mixing Invertd (with group filtering) and Compose.inverse() (without filtering).""" - from monai.data import MetaTensor, create_test_image_2d - img, _ = create_test_image_2d(60, 60, 2, 10, num_seg_classes=2) img = MetaTensor(img, meta={"original_channel_dim": float("nan"), "pixdim": [1.0, 1.0, 1.0]}) key = "image" From 399fb2b90b15c571cf9e9aa0bcc751ed966c930f Mon Sep 17 00:00:00 2001 From: "sewon.jeon" Date: Wed, 25 Feb 2026 00:10:49 +0900 Subject: [PATCH 08/16] DCO Remediation Commit for sewon.jeon I, sewon.jeon , hereby add my Signed-off-by to this commit: 5734cffe5dd310c549bdaa71a1ccae6cfc691c91 Signed-off-by: sewon.jeon From 95222f3e89a0dbeb5d5d752f8702c2308a5dc96b Mon Sep 17 00:00:00 2001 From: "sewon.jeon" Date: Wed, 25 Feb 2026 00:18:00 +0900 Subject: [PATCH 09/16] Address CodeRabbit follow-up review comments --- monai/transforms/post/dictionary.py | 15 ++++++-- tests/transforms/inverse/test_invertd.py | 49 +++++++++++++++++++----- 2 files changed, 50 insertions(+), 14 deletions(-) diff --git a/monai/transforms/post/dictionary.py b/monai/transforms/post/dictionary.py index 5eb5aa7616..e51fc7af37 100644 --- a/monai/transforms/post/dictionary.py +++ b/monai/transforms/post/dictionary.py @@ -860,9 +860,16 @@ def __init__( self._totensor = ToTensor() def _filter_transforms_by_group(self, all_transforms: list[dict]) -> list[dict]: - """ - Filter applied_operations to only include transforms from the target Compose instance. - Uses automatic group tracking where Compose assigns its ID to child transforms. + """Filter applied operations to only include transforms from the target pipeline. + + Uses automatic group tracking where ``Compose`` assigns its ID to child transforms. + + Args: + all_transforms: Full list of applied transform metadata dictionaries. + + Returns: + Subset whose ``TraceKeys.GROUP`` matches ``str(id(self.transform))``, or the original + list when no match is found for backward compatibility. """ # Get the group ID of the transform (Compose instance) target_group = str(id(self.transform)) @@ -914,7 +921,7 @@ def __call__(self, data: Mapping[Hashable, Any]) -> dict[Hashable, Any]: # Automatically filter by Compose instance group ID transform_info = self._filter_transforms_by_group(all_transforms) else: - transform_info = d[InvertibleTransform.trace_key(orig_key)] + transform_info = self._filter_transforms_by_group(d[InvertibleTransform.trace_key(orig_key)]) meta_info = d.get(orig_meta_key, {}) if nearest_interp: transform_info = convert_applied_interp_mode( diff --git a/tests/transforms/inverse/test_invertd.py b/tests/transforms/inverse/test_invertd.py index ecb36df591..abe826ebd8 100644 --- a/tests/transforms/inverse/test_invertd.py +++ b/tests/transforms/inverse/test_invertd.py @@ -36,8 +36,10 @@ ScaleIntensityd, Spacingd, ) +from monai.transforms.inverse import InvertibleTransform +from monai.transforms.transform import MapTransform from monai.transforms.utility.dictionary import Lambdad -from monai.utils import set_determinism +from monai.utils import TraceKeys, set_determinism from tests.test_utils import assert_allclose, make_nifti_image KEYS = ["image", "label"] @@ -165,15 +167,11 @@ def test_invertd_with_postprocessing_transforms(self): item = {key: img} pre = preprocessing(item) - # This should NOT raise an error (was failing before the fix) - try: - post = postprocessing(pre) - # If we get here, the bug is fixed - self.assertIsNotNone(post) - self.assertIn(key, post) - except RuntimeError as e: - if "getting the most recently applied invertible transform" in str(e): - self.fail(f"Invertd still has the postprocessing transform bug: {e}") + # This should NOT raise an error (was failing before the fix). + # Any exception here means the bug is not fixed. + post = postprocessing(pre) + self.assertIsNotNone(post) + self.assertIn(key, post) def test_invertd_multiple_pipelines(self): """Test that Invertd correctly handles multiple independent preprocessing pipelines.""" @@ -271,6 +269,37 @@ def test_invertd_group_isolation(self): self.assertIsNotNone(inverted) self.assertTupleEqual(inverted[key].shape, (1, 60, 60)) + def test_invertd_filters_trace_key_transforms_by_group(self): + """Test group filtering when Invertd reads transforms from ``trace_key``.""" + + class _IdentityMapInvertible(MapTransform, InvertibleTransform): + def __init__(self, keys): + super().__init__(keys) + + def __call__(self, data): + return dict(data) + + def inverse(self, data): + return dict(data) + + key = "image" + target_transform = _IdentityMapInvertible(key) + target_group = str(id(target_transform)) + item = { + key: torch.zeros((1, 8, 8), dtype=torch.float32), + InvertibleTransform.trace_key(key): [ + {TraceKeys.GROUP: target_group}, + {TraceKeys.GROUP: "other-group"}, + ], + } + + inverter = Invertd(key, transform=target_transform, orig_keys=key, nearest_interp=False) + inverted = inverter(item) + + trace_key = InvertibleTransform.trace_key(key) + self.assertEqual(len(inverted[trace_key]), 1) + self.assertEqual(inverted[trace_key][0].get(TraceKeys.GROUP), target_group) + def test_compose_inverse_with_groups(self): """Test that Compose.inverse() works correctly with automatic group tracking.""" img, _ = create_test_image_2d(60, 60, 2, 10, num_seg_classes=2) From 5abaa0825a1101d9845d643982f09d483d6ad710 Mon Sep 17 00:00:00 2001 From: "sewon.jeon" Date: Wed, 25 Feb 2026 00:20:08 +0900 Subject: [PATCH 10/16] DCO Remediation Commit for sewon.jeon I, sewon.jeon , hereby add my Signed-off-by to this commit: f6777abdc4c002bfdbb314f56bcf06f0e327857d Signed-off-by: sewon.jeon From 51d711ba28717b419c6eb4ef9f9278b8a945a299 Mon Sep 17 00:00:00 2001 From: sewon jeon Date: Wed, 25 Feb 2026 12:47:34 +0900 Subject: [PATCH 11/16] Refactor error messages for clarity and consistency in dictionary transforms DCO Remediation Commit for sewon jeon I, sewon jeon , hereby add my Signed-off-by to this commit: 4a918610715c9c15bb12315f6bdbcd1c148ad098 I, sewon jeon , hereby add my Signed-off-by to this commit: f1f92e02403bbd9ae27300dfd5ca57aa9794e0a6 Signed-off-by: sewon jeon --- monai/apps/detection/transforms/dictionary.py | 36 +++++++------------ tests/transforms/inverse/test_invertd.py | 5 +-- 2 files changed, 13 insertions(+), 28 deletions(-) diff --git a/monai/apps/detection/transforms/dictionary.py b/monai/apps/detection/transforms/dictionary.py index 52b1a7d15d..9817a3b510 100644 --- a/monai/apps/detection/transforms/dictionary.py +++ b/monai/apps/detection/transforms/dictionary.py @@ -125,10 +125,8 @@ def __init__(self, box_keys: KeysCollection, box_ref_image_keys: str, allow_miss super().__init__(box_keys, allow_missing_keys) box_ref_image_keys_tuple = ensure_tuple(box_ref_image_keys) if len(box_ref_image_keys_tuple) > 1: - raise ValueError( - "Please provide a single key for box_ref_image_keys.\ - All boxes of box_keys are attached to box_ref_image_keys." - ) + raise ValueError("Please provide a single key for box_ref_image_keys.\ + All boxes of box_keys are attached to box_ref_image_keys.") self.box_ref_image_keys = box_ref_image_keys def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> dict[Hashable, NdarrayOrTensor]: @@ -289,10 +287,8 @@ def __init__( super().__init__(box_keys, allow_missing_keys) box_ref_image_keys_tuple = ensure_tuple(box_ref_image_keys) if len(box_ref_image_keys_tuple) > 1: - raise ValueError( - "Please provide a single key for box_ref_image_keys.\ - All boxes of box_keys are attached to box_ref_image_keys." - ) + raise ValueError("Please provide a single key for box_ref_image_keys.\ + All boxes of box_keys are attached to box_ref_image_keys.") self.box_ref_image_keys = box_ref_image_keys self.image_meta_key = image_meta_key or f"{box_ref_image_keys}_{image_meta_key_postfix}" self.converter_to_image_coordinate = AffineBox() @@ -310,10 +306,8 @@ def extract_affine(self, data: Mapping[Hashable, torch.Tensor]) -> tuple[Ndarray else: raise ValueError(f"{meta_key} is not found. Please check whether it is the correct the image meta key.") if "affine" not in meta_dict: - raise ValueError( - f"'affine' is not found in {meta_key}. \ - Please check whether it is the correct the image meta key." - ) + raise ValueError(f"'affine' is not found in {meta_key}. \ + Please check whether it is the correct the image meta key.") affine: NdarrayOrTensor = meta_dict["affine"] if self.affine_lps_to_ras: # RAS affine @@ -815,16 +809,12 @@ def __init__( ) -> None: box_keys_tuple = ensure_tuple(box_keys) if len(box_keys_tuple) != 1: - raise ValueError( - "Please provide a single key for box_keys.\ - All label_keys are attached to this box_keys." - ) + raise ValueError("Please provide a single key for box_keys.\ + All label_keys are attached to this box_keys.") box_ref_image_keys_tuple = ensure_tuple(box_ref_image_keys) if len(box_ref_image_keys_tuple) != 1: - raise ValueError( - "Please provide a single key for box_ref_image_keys.\ - All box_keys and label_keys are attached to this box_ref_image_keys." - ) + raise ValueError("Please provide a single key for box_ref_image_keys.\ + All box_keys and label_keys are attached to this box_ref_image_keys.") self.label_keys = ensure_tuple(label_keys) super().__init__(box_keys_tuple, allow_missing_keys) @@ -1091,10 +1081,8 @@ def __init__( box_keys_tuple = ensure_tuple(box_keys) if len(box_keys_tuple) != 1: - raise ValueError( - "Please provide a single key for box_keys.\ - All label_keys are attached to this box_keys." - ) + raise ValueError("Please provide a single key for box_keys.\ + All label_keys are attached to this box_keys.") self.box_keys = box_keys_tuple[0] self.label_keys = ensure_tuple(label_keys) diff --git a/tests/transforms/inverse/test_invertd.py b/tests/transforms/inverse/test_invertd.py index abe826ebd8..d8b3029fb6 100644 --- a/tests/transforms/inverse/test_invertd.py +++ b/tests/transforms/inverse/test_invertd.py @@ -287,10 +287,7 @@ def inverse(self, data): target_group = str(id(target_transform)) item = { key: torch.zeros((1, 8, 8), dtype=torch.float32), - InvertibleTransform.trace_key(key): [ - {TraceKeys.GROUP: target_group}, - {TraceKeys.GROUP: "other-group"}, - ], + InvertibleTransform.trace_key(key): [{TraceKeys.GROUP: target_group}, {TraceKeys.GROUP: "other-group"}], } inverter = Invertd(key, transform=target_transform, orig_keys=key, nearest_interp=False) From 8b31f24d3b509ae86fc2184a314004af24a79444 Mon Sep 17 00:00:00 2001 From: sewon jeon Date: Wed, 25 Feb 2026 12:55:25 +0900 Subject: [PATCH 12/16] DCO Remediation Commit for sewon.jeon I, sewon.jeon , hereby add my Signed-off-by to this commit: 95222f3e89a0dbeb5d5d752f8702c2308a5dc96b Signed-off-by: sewon.jeon --- monai/apps/detection/transforms/box_ops.py | 2 +- monai/apps/detection/utils/anchor_utils.py | 18 ++++++------------ monai/apps/reconstruction/transforms/array.py | 6 ++---- monai/auto3dseg/analyzer.py | 2 +- monai/bundle/scripts.py | 2 +- monai/bundle/utils.py | 6 ++---- monai/data/dataset.py | 4 ++-- monai/handlers/utils.py | 2 +- monai/losses/dice.py | 6 ++---- monai/losses/focal_loss.py | 6 ++---- monai/metrics/utils.py | 2 +- monai/transforms/io/array.py | 1 + monai/transforms/utility/array.py | 2 +- tests/integration/test_loader_semaphore.py | 1 + tests/profile_subclass/profiling.py | 1 + tests/profile_subclass/pyspy_profiling.py | 1 + tests/transforms/croppad/test_pad_nd_dtypes.py | 1 + versioneer.py | 5 ++--- 18 files changed, 29 insertions(+), 39 deletions(-) diff --git a/monai/apps/detection/transforms/box_ops.py b/monai/apps/detection/transforms/box_ops.py index 6e08a88e59..fa714daad1 100644 --- a/monai/apps/detection/transforms/box_ops.py +++ b/monai/apps/detection/transforms/box_ops.py @@ -267,7 +267,7 @@ def convert_box_to_mask( boxes_only_mask = np.ones(box_size, dtype=np.int16) * np.int16(labels_np[b]) # apply to global mask slicing = [b] - slicing.extend(slice(boxes_np[b, d], boxes_np[b, d + spatial_dims]) for d in range(spatial_dims)) # type:ignore + slicing.extend(slice(boxes_np[b, d], boxes_np[b, d + spatial_dims]) for d in range(spatial_dims)) # type: ignore boxes_mask_np[tuple(slicing)] = boxes_only_mask return convert_to_dst_type(src=boxes_mask_np, dst=boxes, dtype=torch.int16)[0] diff --git a/monai/apps/detection/utils/anchor_utils.py b/monai/apps/detection/utils/anchor_utils.py index 20f6fc6025..0306a95c7e 100644 --- a/monai/apps/detection/utils/anchor_utils.py +++ b/monai/apps/detection/utils/anchor_utils.py @@ -124,10 +124,8 @@ def __init__( aspect_ratios = (aspect_ratios,) * len(self.sizes) if len(self.sizes) != len(aspect_ratios): - raise ValueError( - "len(sizes) and len(aspect_ratios) should be equal. \ - It represents the number of feature maps." - ) + raise ValueError("len(sizes) and len(aspect_ratios) should be equal. \ + It represents the number of feature maps.") spatial_dims = len(ensure_tuple(aspect_ratios[0][0])) + 1 spatial_dims = look_up_option(spatial_dims, [2, 3]) @@ -172,16 +170,12 @@ def generate_anchors( scales_t = torch.as_tensor(scales, dtype=dtype, device=device) # sized (N,) aspect_ratios_t = torch.as_tensor(aspect_ratios, dtype=dtype, device=device) # sized (M,) or (M,2) if (self.spatial_dims >= 3) and (len(aspect_ratios_t.shape) != 2): - raise ValueError( - f"In {self.spatial_dims}-D image, aspect_ratios for each level should be \ - {len(aspect_ratios_t.shape) - 1}-D. But got aspect_ratios with shape {aspect_ratios_t.shape}." - ) + raise ValueError(f"In {self.spatial_dims}-D image, aspect_ratios for each level should be \ + {len(aspect_ratios_t.shape) - 1}-D. But got aspect_ratios with shape {aspect_ratios_t.shape}.") if (self.spatial_dims >= 3) and (aspect_ratios_t.shape[1] != self.spatial_dims - 1): - raise ValueError( - f"In {self.spatial_dims}-D image, aspect_ratios for each level should has \ - shape (_,{self.spatial_dims - 1}). But got aspect_ratios with shape {aspect_ratios_t.shape}." - ) + raise ValueError(f"In {self.spatial_dims}-D image, aspect_ratios for each level should has \ + shape (_,{self.spatial_dims - 1}). But got aspect_ratios with shape {aspect_ratios_t.shape}.") # if 2d, w:h = 1:aspect_ratios if self.spatial_dims == 2: diff --git a/monai/apps/reconstruction/transforms/array.py b/monai/apps/reconstruction/transforms/array.py index 911d7a06bb..c1a43043e4 100644 --- a/monai/apps/reconstruction/transforms/array.py +++ b/monai/apps/reconstruction/transforms/array.py @@ -61,10 +61,8 @@ def __init__( real/imaginary parts. """ if len(center_fractions) != len(accelerations): - raise ValueError( - "Number of center fractions \ - should match number of accelerations" - ) + raise ValueError("Number of center fractions \ + should match number of accelerations") self.center_fractions = center_fractions self.accelerations = accelerations diff --git a/monai/auto3dseg/analyzer.py b/monai/auto3dseg/analyzer.py index 8d662df83d..4408d602bd 100644 --- a/monai/auto3dseg/analyzer.py +++ b/monai/auto3dseg/analyzer.py @@ -105,7 +105,7 @@ def update_ops_nested_label(self, nested_key: str, op: Operations) -> None: raise ValueError("Nested_key input format is wrong. Please ensure it is like key1#0#key2") root: str child_key: str - (root, _, child_key) = keys + root, _, child_key = keys if root not in self.ops: self.ops[root] = [{}] self.ops[root][0].update({child_key: None}) diff --git a/monai/bundle/scripts.py b/monai/bundle/scripts.py index 9fdee6acd0..fa9ba27096 100644 --- a/monai/bundle/scripts.py +++ b/monai/bundle/scripts.py @@ -1948,7 +1948,7 @@ def create_workflow( """ _args = update_kwargs(args=args_file, workflow_name=workflow_name, config_file=config_file, **kwargs) - (workflow_name, config_file) = _pop_args( + workflow_name, config_file = _pop_args( _args, workflow_name=ConfigWorkflow, config_file=None ) # the default workflow name is "ConfigWorkflow" if isinstance(workflow_name, str): diff --git a/monai/bundle/utils.py b/monai/bundle/utils.py index 53d619f234..d37d7f1c05 100644 --- a/monai/bundle/utils.py +++ b/monai/bundle/utils.py @@ -124,10 +124,8 @@ "run_name": None, # may fill it at runtime "save_execute_config": True, - "is_not_rank0": ( - "$torch.distributed.is_available() \ - and torch.distributed.is_initialized() and torch.distributed.get_rank() > 0" - ), + "is_not_rank0": ("$torch.distributed.is_available() \ + and torch.distributed.is_initialized() and torch.distributed.get_rank() > 0"), # MLFlowHandler config for the trainer "trainer": { "_target_": "MLFlowHandler", diff --git a/monai/data/dataset.py b/monai/data/dataset.py index 066cec41b7..21b24840b5 100644 --- a/monai/data/dataset.py +++ b/monai/data/dataset.py @@ -139,7 +139,7 @@ class DatasetFunc(Dataset): """ def __init__(self, data: Any, func: Callable, **kwargs) -> None: - super().__init__(data=None, transform=None) # type:ignore + super().__init__(data=None, transform=None) # type: ignore self.src = data self.func = func self.kwargs = kwargs @@ -1635,7 +1635,7 @@ def _cachecheck(self, item_transformed): return (_data, _meta) return _data else: - item: list[dict[Any, Any]] = [{} for _ in range(len(item_transformed))] # type:ignore + item: list[dict[Any, Any]] = [{} for _ in range(len(item_transformed))] # type: ignore for i, _item in enumerate(item_transformed): for k in _item: meta_i_k = self._load_meta_cache(meta_hash_file_name=f"{hashfile.name}-{k}-meta-{i}") diff --git a/monai/handlers/utils.py b/monai/handlers/utils.py index b6771f2dcc..02975039b3 100644 --- a/monai/handlers/utils.py +++ b/monai/handlers/utils.py @@ -48,7 +48,7 @@ def stopping_fn_from_loss() -> Callable[[Engine], Any]: """ def stopping_fn(engine: Engine) -> Any: - return -engine.state.output # type:ignore + return -engine.state.output # type: ignore return stopping_fn diff --git a/monai/losses/dice.py b/monai/losses/dice.py index 948749606b..cec9969c12 100644 --- a/monai/losses/dice.py +++ b/monai/losses/dice.py @@ -204,11 +204,9 @@ def forward(self, input: torch.Tensor, target: torch.Tensor) -> torch.Tensor: self.class_weight = torch.as_tensor([self.class_weight] * num_of_classes) else: if self.class_weight.shape[0] != num_of_classes: - raise ValueError( - """the length of the `weight` sequence should be the same as the number of classes. + raise ValueError("""the length of the `weight` sequence should be the same as the number of classes. If `include_background=False`, the weight should not include - the background category class 0.""" - ) + the background category class 0.""") if self.class_weight.min() < 0: raise ValueError("the value/values of the `weight` should be no less than 0.") # apply class_weight to loss diff --git a/monai/losses/focal_loss.py b/monai/losses/focal_loss.py index caa237fca8..7ab54c319d 100644 --- a/monai/losses/focal_loss.py +++ b/monai/losses/focal_loss.py @@ -183,11 +183,9 @@ def forward(self, input: torch.Tensor, target: torch.Tensor) -> torch.Tensor: self.class_weight = torch.as_tensor([self.class_weight] * num_of_classes) else: if self.class_weight.shape[0] != num_of_classes: - raise ValueError( - """the length of the `weight` sequence should be the same as the number of classes. + raise ValueError("""the length of the `weight` sequence should be the same as the number of classes. If `include_background=False`, the weight should not include - the background category class 0.""" - ) + the background category class 0.""") if self.class_weight.min() < 0: raise ValueError("the value/values of the `weight` should be no less than 0.") # apply class_weight to loss diff --git a/monai/metrics/utils.py b/monai/metrics/utils.py index a451b1a770..4a60e438cf 100644 --- a/monai/metrics/utils.py +++ b/monai/metrics/utils.py @@ -320,7 +320,7 @@ def get_edge_surface_distance( edges_spacing = None if use_subvoxels: edges_spacing = spacing if spacing is not None else ([1] * len(y_pred.shape)) - (edges_pred, edges_gt, *areas) = get_mask_edges( + edges_pred, edges_gt, *areas = get_mask_edges( y_pred, y, crop=True, spacing=edges_spacing, always_return_as_numpy=False ) if not edges_gt.any(): diff --git a/monai/transforms/io/array.py b/monai/transforms/io/array.py index 0628a7fbc4..f0c1d1949d 100644 --- a/monai/transforms/io/array.py +++ b/monai/transforms/io/array.py @@ -11,6 +11,7 @@ """ A collection of "vanilla" transforms for IO functions. """ + from __future__ import annotations import inspect diff --git a/monai/transforms/utility/array.py b/monai/transforms/utility/array.py index 3dc7897feb..7df6e2c5ef 100644 --- a/monai/transforms/utility/array.py +++ b/monai/transforms/utility/array.py @@ -702,7 +702,7 @@ def __init__( # if the root log level is higher than INFO, set a separate stream handler to record console = logging.StreamHandler(sys.stdout) console.setLevel(logging.INFO) - console.is_data_stats_handler = True # type:ignore[attr-defined] + console.is_data_stats_handler = True # type: ignore[attr-defined] _logger.addHandler(console) def __call__( diff --git a/tests/integration/test_loader_semaphore.py b/tests/integration/test_loader_semaphore.py index 78baedc264..c32bcb0b8b 100644 --- a/tests/integration/test_loader_semaphore.py +++ b/tests/integration/test_loader_semaphore.py @@ -10,6 +10,7 @@ # limitations under the License. """this test should not generate errors or UserWarning: semaphore_tracker: There appear to be 1 leaked semaphores""" + from __future__ import annotations import multiprocessing as mp diff --git a/tests/profile_subclass/profiling.py b/tests/profile_subclass/profiling.py index 18aecea2fb..6106259526 100644 --- a/tests/profile_subclass/profiling.py +++ b/tests/profile_subclass/profiling.py @@ -12,6 +12,7 @@ Comparing torch.Tensor, SubTensor, SubWithTorchFunc, MetaTensor Adapted from https://github.com/pytorch/pytorch/tree/v1.11.0/benchmarks/overrides_benchmark """ + from __future__ import annotations import argparse diff --git a/tests/profile_subclass/pyspy_profiling.py b/tests/profile_subclass/pyspy_profiling.py index fac425f577..671dc74c01 100644 --- a/tests/profile_subclass/pyspy_profiling.py +++ b/tests/profile_subclass/pyspy_profiling.py @@ -12,6 +12,7 @@ To be used with py-spy, comparing torch.Tensor, SubTensor, SubWithTorchFunc, MetaTensor Adapted from https://github.com/pytorch/pytorch/tree/v1.11.0/benchmarks/overrides_benchmark """ + from __future__ import annotations import argparse diff --git a/tests/transforms/croppad/test_pad_nd_dtypes.py b/tests/transforms/croppad/test_pad_nd_dtypes.py index 7fa633b8aa..a3f5f93a2d 100644 --- a/tests/transforms/croppad/test_pad_nd_dtypes.py +++ b/tests/transforms/croppad/test_pad_nd_dtypes.py @@ -12,6 +12,7 @@ Tests for pad_nd dtype support and backend selection. Validates PyTorch padding preference and NumPy fallback behavior. """ + from __future__ import annotations import unittest diff --git a/versioneer.py b/versioneer.py index a06587fc3f..6839363323 100644 --- a/versioneer.py +++ b/versioneer.py @@ -273,6 +273,7 @@ [travis-url]: https://travis-ci.com/github/python-versioneer/python-versioneer """ + # pylint:disable=invalid-name,import-outside-toplevel,missing-function-docstring # pylint:disable=missing-class-docstring,too-many-branches,too-many-statements # pylint:disable=raise-missing-from,too-many-lines,too-many-locals,import-error @@ -428,9 +429,7 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, env= return stdout, process.returncode -LONG_VERSION_PY[ - "git" -] = r''' +LONG_VERSION_PY["git"] = r''' # This file helps to compute a version number in source trees obtained from # git-archive tarball (such as those provided by githubs download-from-tag # feature). Distribution tarballs (built by setup.py sdist) and build From ac85826dd210930bbc6d0f159ed8dc6356e7523a Mon Sep 17 00:00:00 2001 From: sewon jeon Date: Wed, 25 Feb 2026 13:03:57 +0900 Subject: [PATCH 13/16] DCO Remediation Commit for sewon jeon I, sewon jeon , hereby add my Signed-off-by to this commit: 8b31f24d3b509ae86fc2184a314004af24a79444 Signed-off-by: sewon jeon From a1a7d58a4ac4b9b58289e7035b2c7b3b140cf4b1 Mon Sep 17 00:00:00 2001 From: "sewon.jeon" Date: Wed, 25 Feb 2026 13:57:04 +0900 Subject: [PATCH 14/16] DCO Remediation Commit for sewon.jeon I, sewon.jeon , hereby add my Signed-off-by to this commit: 95222f3e89a0dbeb5d5d752f8702c2308a5dc96b Signed-off-by: sewon.jeon From b2efefcabf76afcffa9ee229c13972bcc93142e5 Mon Sep 17 00:00:00 2001 From: "sewon.jeon" Date: Wed, 25 Feb 2026 13:59:14 +0900 Subject: [PATCH 15/16] DCO Remediation Commit for sewon.jeon I, sewon.jeon , hereby add my Signed-off-by to this commit: a607e700327f679f59062a124f48ba4b84e35b32 Signed-off-by: sewon.jeon From 226deb6fe42aeeb7b5153112c03aca6046c85a72 Mon Sep 17 00:00:00 2001 From: "sewon.jeon" Date: Thu, 26 Feb 2026 01:16:54 +0900 Subject: [PATCH 16/16] Apply black 25.11.0 formatting fixes Signed-off-by: sewon.jeon --- monai/apps/detection/transforms/dictionary.py | 36 ++++++++++++------- monai/apps/detection/utils/anchor_utils.py | 18 ++++++---- monai/apps/reconstruction/transforms/array.py | 6 ++-- monai/bundle/utils.py | 6 ++-- monai/losses/dice.py | 6 ++-- monai/losses/focal_loss.py | 6 ++-- versioneer.py | 4 ++- 7 files changed, 55 insertions(+), 27 deletions(-) diff --git a/monai/apps/detection/transforms/dictionary.py b/monai/apps/detection/transforms/dictionary.py index 9817a3b510..52b1a7d15d 100644 --- a/monai/apps/detection/transforms/dictionary.py +++ b/monai/apps/detection/transforms/dictionary.py @@ -125,8 +125,10 @@ def __init__(self, box_keys: KeysCollection, box_ref_image_keys: str, allow_miss super().__init__(box_keys, allow_missing_keys) box_ref_image_keys_tuple = ensure_tuple(box_ref_image_keys) if len(box_ref_image_keys_tuple) > 1: - raise ValueError("Please provide a single key for box_ref_image_keys.\ - All boxes of box_keys are attached to box_ref_image_keys.") + raise ValueError( + "Please provide a single key for box_ref_image_keys.\ + All boxes of box_keys are attached to box_ref_image_keys." + ) self.box_ref_image_keys = box_ref_image_keys def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> dict[Hashable, NdarrayOrTensor]: @@ -287,8 +289,10 @@ def __init__( super().__init__(box_keys, allow_missing_keys) box_ref_image_keys_tuple = ensure_tuple(box_ref_image_keys) if len(box_ref_image_keys_tuple) > 1: - raise ValueError("Please provide a single key for box_ref_image_keys.\ - All boxes of box_keys are attached to box_ref_image_keys.") + raise ValueError( + "Please provide a single key for box_ref_image_keys.\ + All boxes of box_keys are attached to box_ref_image_keys." + ) self.box_ref_image_keys = box_ref_image_keys self.image_meta_key = image_meta_key or f"{box_ref_image_keys}_{image_meta_key_postfix}" self.converter_to_image_coordinate = AffineBox() @@ -306,8 +310,10 @@ def extract_affine(self, data: Mapping[Hashable, torch.Tensor]) -> tuple[Ndarray else: raise ValueError(f"{meta_key} is not found. Please check whether it is the correct the image meta key.") if "affine" not in meta_dict: - raise ValueError(f"'affine' is not found in {meta_key}. \ - Please check whether it is the correct the image meta key.") + raise ValueError( + f"'affine' is not found in {meta_key}. \ + Please check whether it is the correct the image meta key." + ) affine: NdarrayOrTensor = meta_dict["affine"] if self.affine_lps_to_ras: # RAS affine @@ -809,12 +815,16 @@ def __init__( ) -> None: box_keys_tuple = ensure_tuple(box_keys) if len(box_keys_tuple) != 1: - raise ValueError("Please provide a single key for box_keys.\ - All label_keys are attached to this box_keys.") + raise ValueError( + "Please provide a single key for box_keys.\ + All label_keys are attached to this box_keys." + ) box_ref_image_keys_tuple = ensure_tuple(box_ref_image_keys) if len(box_ref_image_keys_tuple) != 1: - raise ValueError("Please provide a single key for box_ref_image_keys.\ - All box_keys and label_keys are attached to this box_ref_image_keys.") + raise ValueError( + "Please provide a single key for box_ref_image_keys.\ + All box_keys and label_keys are attached to this box_ref_image_keys." + ) self.label_keys = ensure_tuple(label_keys) super().__init__(box_keys_tuple, allow_missing_keys) @@ -1081,8 +1091,10 @@ def __init__( box_keys_tuple = ensure_tuple(box_keys) if len(box_keys_tuple) != 1: - raise ValueError("Please provide a single key for box_keys.\ - All label_keys are attached to this box_keys.") + raise ValueError( + "Please provide a single key for box_keys.\ + All label_keys are attached to this box_keys." + ) self.box_keys = box_keys_tuple[0] self.label_keys = ensure_tuple(label_keys) diff --git a/monai/apps/detection/utils/anchor_utils.py b/monai/apps/detection/utils/anchor_utils.py index 0306a95c7e..20f6fc6025 100644 --- a/monai/apps/detection/utils/anchor_utils.py +++ b/monai/apps/detection/utils/anchor_utils.py @@ -124,8 +124,10 @@ def __init__( aspect_ratios = (aspect_ratios,) * len(self.sizes) if len(self.sizes) != len(aspect_ratios): - raise ValueError("len(sizes) and len(aspect_ratios) should be equal. \ - It represents the number of feature maps.") + raise ValueError( + "len(sizes) and len(aspect_ratios) should be equal. \ + It represents the number of feature maps." + ) spatial_dims = len(ensure_tuple(aspect_ratios[0][0])) + 1 spatial_dims = look_up_option(spatial_dims, [2, 3]) @@ -170,12 +172,16 @@ def generate_anchors( scales_t = torch.as_tensor(scales, dtype=dtype, device=device) # sized (N,) aspect_ratios_t = torch.as_tensor(aspect_ratios, dtype=dtype, device=device) # sized (M,) or (M,2) if (self.spatial_dims >= 3) and (len(aspect_ratios_t.shape) != 2): - raise ValueError(f"In {self.spatial_dims}-D image, aspect_ratios for each level should be \ - {len(aspect_ratios_t.shape) - 1}-D. But got aspect_ratios with shape {aspect_ratios_t.shape}.") + raise ValueError( + f"In {self.spatial_dims}-D image, aspect_ratios for each level should be \ + {len(aspect_ratios_t.shape) - 1}-D. But got aspect_ratios with shape {aspect_ratios_t.shape}." + ) if (self.spatial_dims >= 3) and (aspect_ratios_t.shape[1] != self.spatial_dims - 1): - raise ValueError(f"In {self.spatial_dims}-D image, aspect_ratios for each level should has \ - shape (_,{self.spatial_dims - 1}). But got aspect_ratios with shape {aspect_ratios_t.shape}.") + raise ValueError( + f"In {self.spatial_dims}-D image, aspect_ratios for each level should has \ + shape (_,{self.spatial_dims - 1}). But got aspect_ratios with shape {aspect_ratios_t.shape}." + ) # if 2d, w:h = 1:aspect_ratios if self.spatial_dims == 2: diff --git a/monai/apps/reconstruction/transforms/array.py b/monai/apps/reconstruction/transforms/array.py index c1a43043e4..911d7a06bb 100644 --- a/monai/apps/reconstruction/transforms/array.py +++ b/monai/apps/reconstruction/transforms/array.py @@ -61,8 +61,10 @@ def __init__( real/imaginary parts. """ if len(center_fractions) != len(accelerations): - raise ValueError("Number of center fractions \ - should match number of accelerations") + raise ValueError( + "Number of center fractions \ + should match number of accelerations" + ) self.center_fractions = center_fractions self.accelerations = accelerations diff --git a/monai/bundle/utils.py b/monai/bundle/utils.py index d37d7f1c05..53d619f234 100644 --- a/monai/bundle/utils.py +++ b/monai/bundle/utils.py @@ -124,8 +124,10 @@ "run_name": None, # may fill it at runtime "save_execute_config": True, - "is_not_rank0": ("$torch.distributed.is_available() \ - and torch.distributed.is_initialized() and torch.distributed.get_rank() > 0"), + "is_not_rank0": ( + "$torch.distributed.is_available() \ + and torch.distributed.is_initialized() and torch.distributed.get_rank() > 0" + ), # MLFlowHandler config for the trainer "trainer": { "_target_": "MLFlowHandler", diff --git a/monai/losses/dice.py b/monai/losses/dice.py index cec9969c12..948749606b 100644 --- a/monai/losses/dice.py +++ b/monai/losses/dice.py @@ -204,9 +204,11 @@ def forward(self, input: torch.Tensor, target: torch.Tensor) -> torch.Tensor: self.class_weight = torch.as_tensor([self.class_weight] * num_of_classes) else: if self.class_weight.shape[0] != num_of_classes: - raise ValueError("""the length of the `weight` sequence should be the same as the number of classes. + raise ValueError( + """the length of the `weight` sequence should be the same as the number of classes. If `include_background=False`, the weight should not include - the background category class 0.""") + the background category class 0.""" + ) if self.class_weight.min() < 0: raise ValueError("the value/values of the `weight` should be no less than 0.") # apply class_weight to loss diff --git a/monai/losses/focal_loss.py b/monai/losses/focal_loss.py index 7ab54c319d..caa237fca8 100644 --- a/monai/losses/focal_loss.py +++ b/monai/losses/focal_loss.py @@ -183,9 +183,11 @@ def forward(self, input: torch.Tensor, target: torch.Tensor) -> torch.Tensor: self.class_weight = torch.as_tensor([self.class_weight] * num_of_classes) else: if self.class_weight.shape[0] != num_of_classes: - raise ValueError("""the length of the `weight` sequence should be the same as the number of classes. + raise ValueError( + """the length of the `weight` sequence should be the same as the number of classes. If `include_background=False`, the weight should not include - the background category class 0.""") + the background category class 0.""" + ) if self.class_weight.min() < 0: raise ValueError("the value/values of the `weight` should be no less than 0.") # apply class_weight to loss diff --git a/versioneer.py b/versioneer.py index 6839363323..5d0a606c91 100644 --- a/versioneer.py +++ b/versioneer.py @@ -429,7 +429,9 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, env= return stdout, process.returncode -LONG_VERSION_PY["git"] = r''' +LONG_VERSION_PY[ + "git" +] = r''' # This file helps to compute a version number in source trees obtained from # git-archive tarball (such as those provided by githubs download-from-tag # feature). Distribution tarballs (built by setup.py sdist) and build