From be465abc60d6c0a7c7323c2f437026b67b1cce2a Mon Sep 17 00:00:00 2001 From: Mr-Neutr0n Date: Wed, 24 Jun 2026 21:47:53 +0530 Subject: [PATCH] test(losses): add regression tests for clDice smooth_dr and zero/non-overlap inputs The source division-by-zero fix is already present on upstream/dev via #8703 (it added smooth/smooth_dr guards and validates smooth > 0). This commit adds the missing regression coverage: - test_zero_input_is_finite for SoftclDiceLoss and SoftDiceclDiceLoss - test_non_default_smooth_dr_changes_result to prove the new parameter is wired through and actually affects the output - test_non_overlapping_input_is_finite for both loss classes, covering the scenario the original bug report was about Also clarifies the SoftclDiceLoss smooth_dr docstring. --- monai/losses/cldice.py | 3 ++- tests/losses/test_cldice_loss.py | 46 ++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/monai/losses/cldice.py b/monai/losses/cldice.py index 7d7e447c54..60e8601459 100644 --- a/monai/losses/cldice.py +++ b/monai/losses/cldice.py @@ -134,7 +134,8 @@ def __init__( Args: iter_: Number of iterations for skeletonization. Must be a non-negative integer. Defaults to 3. smooth_nr: a small constant added to the numerator to avoid zero. Defaults to 1.0. - smooth_dr: a small constant added to the denominator to avoid nan. Defaults to 1.0. + smooth_dr: a small constant added to the denominator of the individual precision / + sensitivity ratios and the internal Dice denominator to avoid nan. Defaults to 1.0. smooth: a small constant added to the denominator of the harmonic mean to avoid nan. Defaults to 1e-4. include_background: if False, channel index 0 (background category) is excluded from the calculation. if the non-background segmentations are small compared to the total image size they can get overwhelmed diff --git a/tests/losses/test_cldice_loss.py b/tests/losses/test_cldice_loss.py index cb17cb81ad..23c22dd395 100644 --- a/tests/losses/test_cldice_loss.py +++ b/tests/losses/test_cldice_loss.py @@ -114,6 +114,29 @@ def test_invalid_iter_value(self): with self.assertRaises(ValueError): SoftclDiceLoss(iter_=-1) + def test_zero_input_is_finite(self): + loss = SoftclDiceLoss(smooth=1e-7, smooth_dr=1e-5) + result = loss(torch.zeros((1, 2, 4, 4)), torch.zeros((1, 2, 4, 4))) + self.assertTrue(torch.isfinite(result).all()) + + def test_non_default_smooth_dr_changes_result(self): + input_tensor = torch.zeros((1, 2, 4, 4)) + target = torch.zeros((1, 2, 4, 4)) + loss_a = SoftclDiceLoss(smooth=1e-7, smooth_dr=1e-3) + loss_b = SoftclDiceLoss(smooth=1e-7, smooth_dr=1e-5) + result_a = loss_a(input_tensor, target) + result_b = loss_b(input_tensor, target) + self.assertTrue(torch.isfinite(result_a).all()) + self.assertTrue(torch.isfinite(result_b).all()) + self.assertNotAlmostEqual(result_a.item(), result_b.item(), places=5) + + def test_non_overlapping_input_is_finite(self): + loss = SoftclDiceLoss(smooth=1e-7, smooth_dr=1e-5) + input_tensor = torch.tensor([[[[1.0, 0.0], [0.0, 0.0]], [[0.0, 0.0], [0.0, 0.0]]]]) + target = torch.tensor([[[[0.0, 0.0], [0.0, 1.0]], [[0.0, 0.0], [0.0, 0.0]]]]) + result = loss(input_tensor, target) + self.assertTrue(torch.isfinite(result).all()) + class TestSoftDiceclDiceLoss(unittest.TestCase): @parameterized.expand(COMBINED_CASES) @@ -146,6 +169,29 @@ def test_invalid_alpha_negative(self): with self.assertRaises(ValueError): SoftDiceclDiceLoss(alpha=-0.5) + def test_zero_input_is_finite(self): + loss = SoftDiceclDiceLoss(smooth=1e-7, smooth_dr=1e-5) + result = loss(torch.zeros((1, 2, 4, 4)), torch.zeros((1, 2, 4, 4))) + self.assertTrue(torch.isfinite(result).all()) + + def test_non_default_smooth_dr_changes_result(self): + input_tensor = torch.zeros((1, 2, 4, 4)) + target = torch.zeros((1, 2, 4, 4)) + loss_a = SoftDiceclDiceLoss(smooth=1e-7, smooth_dr=1e-3) + loss_b = SoftDiceclDiceLoss(smooth=1e-7, smooth_dr=1e-5) + result_a = loss_a(input_tensor, target) + result_b = loss_b(input_tensor, target) + self.assertTrue(torch.isfinite(result_a).all()) + self.assertTrue(torch.isfinite(result_b).all()) + self.assertNotAlmostEqual(result_a.item(), result_b.item(), places=5) + + def test_non_overlapping_input_is_finite(self): + loss = SoftDiceclDiceLoss(smooth=1e-7, smooth_dr=1e-5) + input_tensor = torch.tensor([[[[1.0, 0.0], [0.0, 0.0]], [[0.0, 0.0], [0.0, 0.0]]]]) + target = torch.tensor([[[[0.0, 0.0], [0.0, 1.0]], [[0.0, 0.0], [0.0, 0.0]]]]) + result = loss(input_tensor, target) + self.assertTrue(torch.isfinite(result).all()) + if __name__ == "__main__": unittest.main()