Repository: Project-MONAI/MONAI

Issue:
mypy error in efficientnet BlockArgs
**Describe the bug**
Running  ./runtests.sh --mypy can sometimes lead to the following error on local machines:

Traceback (most recent call last):
  File "/usr/lib/python3.8/runpy.py", line 194, in _run_module_as_main
    return _run_code(code, main_globals, None,
  File "/usr/lib/python3.8/runpy.py", line 87, in _run_code
    exec(code, run_globals)
  File "/opt/virtualenvs/dvenv/lib/python3.8/site-packages/mypy/__main__.py", line 23, in <module>
    console_entry()
  File "/opt/virtualenvs/dvenv/lib/python3.8/site-packages/mypy/__main__.py", line 11, in console_entry
    main(None, sys.stdout, sys.stderr)
  File "mypy/main.py", line 90, in main
  File "mypy/build.py", line 179, in build
  File "mypy/build.py", line 253, in _build
  File "mypy/build.py", line 2638, in dispatch
  File "mypy/build.py", line 2955, in process_graph
  File "mypy/build.py", line 3033, in process_fresh_modules
  File "mypy/build.py", line 1977, in fix_cross_refs
  File "mypy/fixup.py", line 26, in fixup_module
  File "mypy/fixup.py", line 77, in visit_symbol_table
  File "mypy/fixup.py", line 301, in lookup_qualified_stnode
  File "mypy/lookup.py", line 47, in lookup_fully_qualified
AssertionError: Cannot find component 'BlockArgs@791' for 'monai.networks.nets.efficientnet.BlockArgs@791'

This was reported in this pull request: https://github.com/Project-MONAI/MONAI/pull/2066#issuecomment-828855868 by @lyndonboone 

Opening this to record the issue and fix in a pull request.

**To Reproduce**
Steps to reproduce the behavior:
1. Clone MONAI repo: git clone https://github.com/Project-MONAI/MONAI
2. Install 'python setup.py develop'
3. Run commands ./runtests.sh --mypy'

I was able to see the error message on Ubuntu 20.04.

**Expected behavior**
No error message when running mypy


Discussion:

Resolving patch:
diff --git a/monai/networks/nets/__init__.py b/monai/networks/nets/__init__.py
--- a/monai/networks/nets/__init__.py
+++ b/monai/networks/nets/__init__.py
@@ -31,7 +31,7 @@
     densenet264,
 )
 from .dynunet import DynUNet, DynUnet, Dynunet, dynunet
-from .efficientnet import EfficientNet, EfficientNetBN, drop_connect, get_efficientnet_image_size
+from .efficientnet import BlockArgs, EfficientNet, EfficientNetBN, drop_connect, get_efficientnet_image_size
 from .fullyconnectednet import FullyConnectedNet, VarFullyConnectedNet
 from .generator import Generator
 from .highresnet import HighResBlock, HighResNet
diff --git a/monai/networks/nets/efficientnet.py b/monai/networks/nets/efficientnet.py
--- a/monai/networks/nets/efficientnet.py
+++ b/monai/networks/nets/efficientnet.py
@@ -264,7 +264,7 @@ def __init__(
         ]
 
         # decode blocks args into arguments for MBConvBlock
-        blocks_args = _decode_block_list(blocks_args_str)
+        blocks_args = [BlockArgs.from_string(s) for s in blocks_args_str]
 
         # checks for successful decoding of blocks_args_str
         if not isinstance(blocks_args, list):
@@ -775,32 +775,25 @@ def _calculate_output_image_size(input_image_size: List[int], stride: Union[int,
     return [int(math.ceil(im_sz / stride)) for im_sz in input_image_size]
 
 
-def _decode_block_list(string_list: List[str]):
+class BlockArgs(NamedTuple):
     """
-    Decode a list of string notations to specify blocks inside the network.
-
-    Args:
-        string_list: a list of strings, each string is a notation of block.
-
-    Returns:
-        blocks_args: a list of BlockArgs namedtuples of block args.
+    BlockArgs object to assist in decoding string notation
+        of arguments for MBConvBlock definition.
     """
-    # Parameters for an individual model block
-    # namedtuple with defaults for mypy help from:
-    # https://stackoverflow.com/a/53255358
-    class BlockArgs(NamedTuple):
-        num_repeat: int
-        kernel_size: int
-        stride: int
-        expand_ratio: int
-        input_filters: int
-        output_filters: int
-        id_skip: bool
-        se_ratio: Optional[float] = None
-
-    def _decode_block_string(block_string: str):
+
+    num_repeat: int
+    kernel_size: int
+    stride: int
+    expand_ratio: int
+    input_filters: int
+    output_filters: int
+    id_skip: bool
+    se_ratio: Optional[float] = None
+
+    @staticmethod
+    def from_string(block_string: str):
         """
-        Get a block through a string notation of arguments.
+        Get a BlockArgs object from a string notation of arguments.
 
         Args:
             block_string (str): A string notation of arguments.
@@ -837,10 +830,25 @@ def _decode_block_string(block_string: str):
             se_ratio=float(options["se"]) if "se" in options else None,
         )
 
-    # convert block strings into BlockArgs for each entry in string_list list
-    blocks_args: List[BlockArgs] = []
-    for current_string in string_list:
-        blocks_args.append(_decode_block_string(current_string))
+    def to_string(self):
+        """
+        Return a block string notation for current BlockArgs object
+
+        Returns:
+            A string notation of BlockArgs object arguments.
+                Example: "r1_k3_s11_e1_i32_o16_se0.25_noskip".
+        """
+        string = "r{}_k{}_s{}{}_e{}_i{}_o{}_se{}".format(
+            self.num_repeat,
+            self.kernel_size,
+            self.stride,
+            self.stride,
+            self.expand_ratio,
+            self.input_filters,
+            self.output_filters,
+            self.se_ratio,
+        )
 
-    # return blocks_args list, to be used for arguments of MBConv layers in EfficientNet
-    return blocks_args
+        if not self.id_skip:
+            string += "_noskip"
+        return string


Test patch:
diff --git a/tests/test_efficientnet.py b/tests/test_efficientnet.py
--- a/tests/test_efficientnet.py
+++ b/tests/test_efficientnet.py
@@ -18,7 +18,7 @@
 from parameterized import parameterized
 
 from monai.networks import eval_mode
-from monai.networks.nets import EfficientNetBN, drop_connect, get_efficientnet_image_size
+from monai.networks.nets import BlockArgs, EfficientNetBN, drop_connect, get_efficientnet_image_size
 from monai.utils import optional_import
 from tests.utils import skip_if_quick, test_pretrained_networks, test_script_save
 
@@ -55,6 +55,26 @@ def get_expected_model_shape(model_name):
     return model_input_shapes[model_name]
 
 
+def get_block_args():
+    # test string list
+    return [
+        "r1_k3_s11_e1_i32_o16_se0.25",
+        "r2_k3_s22_e6_i16_o24_se0.25",
+        "r2_k5_s22_e6_i24_o40_se0.25",
+        "r3_k3_s22_e6_i40_o80_se0.25",
+        "r3_k5_s11_e6_i80_o112_se0.25",
+        "r4_k5_s22_e6_i112_o192_se0.25",
+        "r1_k3_s11_e6_i192_o320_se0.25",
+        "r1_k3_s11_e1_i32_o16_se0.25_noskip",
+        "r2_k3_s22_e6_i16_o24_se0.25_noskip",
+        "r2_k5_s22_e6_i24_o40_se0.25_noskip",
+        "r3_k3_s22_e6_i40_o80_se0.25_noskip",
+        "r3_k5_s11_e6_i80_o112_se0.25_noskip",
+        "r4_k5_s22_e6_i112_o192_se0.25_noskip",
+        "r1_k3_s11_e6_i192_o320_se0.25_noskip",
+    ]
+
+
 def make_shape_cases(models, spatial_dims, batches, pretrained, in_channels=3, num_classes=1000):
     ret_tests = []
     for spatial_dim in spatial_dims:  # selected spatial_dims
@@ -233,7 +253,7 @@ def test_kitty_pretrained(self, input_param, image_path, expected_label):
         image_size = get_efficientnet_image_size(input_param["model_name"])
         img = PIL.Image.open(image_path)
 
-        # defin ImageNet transform
+        # define ImageNet transforms
         tfms = torchvision.transforms.Compose(
             [
                 torchvision.transforms.Resize(image_size),
@@ -254,11 +274,12 @@ def test_kitty_pretrained(self, input_param, image_path, expected_label):
             result = net(img)
         pred_label = torch.argmax(result, dim=-1)
 
-        # check output
+        # check output label
         self.assertEqual(pred_label, expected_label)
 
     def test_drop_connect_layer(self):
         p_list = [float(d + 1) / 10.0 for d in range(9)]
+
         # testing 1D, 2D and 3D shape
         for rand_tensor_shape in [(512, 16, 4), (384, 16, 4, 4), (256, 16, 4, 4, 4)]:
 
@@ -284,6 +305,17 @@ def test_drop_connect_layer(self):
 
                 self.assertTrue(abs(p_calculated - p) < tol)
 
+    def test_block_args_decode(self):
+        blocks_args_str = get_block_args()
+
+        # convert strings to BlockArgs
+        blocks_args = [BlockArgs.from_string(s) for s in blocks_args_str]
+        # convert BlockArgs back to string
+        blocks_args_str_convert = [s.to_string() for s in blocks_args]
+
+        # check if converted strings match original
+        [self.assertEqual(original, converted) for original, converted in zip(blocks_args_str, blocks_args_str_convert)]
+
     def test_ill_arg(self):
         with self.assertRaises(ValueError):
             # wrong spatial_dims