Skip to content

Commit 798a88d

Browse files
committed
fix(networks): support QuickNAT without optional SE blocks
Signed-off-by: kyinhub <kevinpyin@gmail.com>
1 parent 3ee058b commit 798a88d

2 files changed

Lines changed: 66 additions & 3 deletions

File tree

monai/networks/nets/quicknat.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,28 @@ class SkipConnectionWithIdx(SkipConnection):
4343
"""
4444

4545
def forward(self, input, indices): # type: ignore[override]
46-
return super().forward(input), indices
46+
"""Combine the input with the indexed submodule output.
47+
48+
Args:
49+
input: input tensor for the skip connection and submodule.
50+
indices: pooling indices to preserve for the following decoder.
51+
52+
Returns:
53+
A tuple containing the combined tensor and the original indices.
54+
55+
Raises:
56+
NotImplementedError: if the configured skip mode is unsupported.
57+
"""
58+
submodule_output, _ = self.submodule(input, None)
59+
if self.mode == "cat":
60+
output = torch.cat([input, submodule_output], dim=self.dim)
61+
elif self.mode == "add":
62+
output = torch.add(input, submodule_output)
63+
elif self.mode == "mul":
64+
output = torch.mul(input, submodule_output)
65+
else:
66+
raise NotImplementedError(f"Unsupported mode {self.mode}.")
67+
return output, indices
4768

4869

4970
class SequentialWithIdx(nn.Sequential):
@@ -325,7 +346,7 @@ class Quicknat(nn.Module):
325346
stride_convolution: convolution stride. Defaults to 1.
326347
pool: kernel size of the pooling layer,
327348
stride_pool: stride for the pooling layer.
328-
se_block: Squeeze and Excite block type to be included, defaults to None. Valid options : NONE, CSE, SSE, CSSE,
349+
se_block: Squeeze and Excite block type to include. Use ``None`` or ``"None"`` to disable it; valid enabled values are ``"CSE"``, ``"SSE"``, and ``"CSSE"``.
329350
droup_out: dropout ratio. Defaults to no dropout.
330351
act: activation type and arguments. Defaults to PReLU.
331352
norm: feature normalization type and arguments. Defaults to instance norm.
@@ -358,7 +379,7 @@ def __init__(
358379
pool: int = 2,
359380
stride_pool: int = 2,
360381
# Valid options : NONE, CSE, SSE, CSSE
361-
se_block: str = "None",
382+
se_block: str | None = "None",
362383
drop_out: float = 0,
363384
act: tuple | str = Act.PRELU,
364385
norm: tuple | str = Norm.INSTANCE,

tests/networks/nets/test_quicknat.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,21 @@
1818

1919
from monai.networks import eval_mode
2020
from monai.networks.nets import Quicknat
21+
from monai.networks.nets.quicknat import SkipConnectionWithIdx
2122
from monai.utils import optional_import
2223
from tests.test_utils import test_script_save
2324

2425
_, has_se = optional_import("squeeze_and_excitation")
2526

27+
28+
class _DoubleWithIdx(torch.nn.Module):
29+
"""Double tensor values using QuickNAT's indexed-module signature."""
30+
31+
def forward(self, input, indices):
32+
"""Return the doubled tensor and unchanged indices."""
33+
return input * 2, indices
34+
35+
2636
TEST_CASES = [
2737
# params, input_shape, expected_shape
2838
[{"num_classes": 1, "num_channels": 1, "num_filters": 1, "se_block": None}, (1, 1, 32, 32), (1, 1, 32, 32)],
@@ -36,6 +46,38 @@
3646
]
3747

3848

49+
class TestQuicknatCore(unittest.TestCase):
50+
"""Test QuickNAT paths that do not require optional dependencies."""
51+
52+
@parameterized.expand(["cat", "add", "mul"])
53+
def test_skip_connection_modes_preserve_indices(self, mode):
54+
"""Verify each fusion mode and preservation of pooling indices."""
55+
skip = SkipConnectionWithIdx(_DoubleWithIdx(), mode=mode)
56+
input = torch.tensor([[[[2.0]]]])
57+
indices = torch.tensor([[[[3]]]])
58+
59+
output, returned_indices = skip(input, indices)
60+
61+
expected = {"cat": torch.cat([input, input * 2], dim=1), "add": input * 3, "mul": input * input * 2}[mode]
62+
self.assertTrue(torch.equal(output, expected))
63+
self.assertIs(returned_indices, indices)
64+
65+
def test_skip_connection_rejects_unsupported_mode(self):
66+
"""Verify unsupported fusion modes fail explicitly."""
67+
skip = SkipConnectionWithIdx(_DoubleWithIdx(), mode="cat")
68+
skip.mode = "unsupported"
69+
70+
with self.assertRaisesRegex(NotImplementedError, "Unsupported mode"):
71+
skip(torch.ones((1, 1, 1, 1)), torch.ones((1, 1, 1, 1)))
72+
73+
def test_forward_without_optional_se_dependency(self):
74+
"""Verify QuickNAT runs when squeeze-and-excitation is disabled."""
75+
net = Quicknat(num_classes=2, num_channels=1, num_filters=4, se_block=None)
76+
with eval_mode(net):
77+
result = net(torch.randn(1, 1, 32, 32))
78+
self.assertEqual(result.shape, (1, 2, 32, 32))
79+
80+
3981
@unittest.skipUnless(has_se, "squeeze_and_excitation not installed")
4082
class TestQuicknat(unittest.TestCase):
4183
@parameterized.expand(TEST_CASES)

0 commit comments

Comments
 (0)