Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions monai/deploy/operators/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
IOMapping
ModelInfo
MonaiBundleInferenceOperator
MONetBundleInferenceOperator
MonaiSegInferenceOperator
PNGConverterOperator
PublisherOperator
Expand All @@ -49,6 +50,7 @@
from .inference_operator import InferenceOperator
from .monai_bundle_inference_operator import BundleConfigNames, IOMapping, MonaiBundleInferenceOperator
from .monai_seg_inference_operator import MonaiSegInferenceOperator
from .monet_bundle_inference_operator import MONetBundleInferenceOperator
from .nii_data_loader_operator import NiftiDataLoader
from .png_converter_operator import PNGConverterOperator
from .publisher_operator import PublisherOperator
Expand All @@ -69,6 +71,7 @@
"IOMapping",
"ModelInfo",
"MonaiBundleInferenceOperator",
"MONetBundleInferenceOperator",
"MonaiSegInferenceOperator",
"NiftiDataLoader",
"PNGConverterOperator",
Expand Down
5 changes: 3 additions & 2 deletions monai/deploy/operators/monai_bundle_inference_operator.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ def get_bundle_config(bundle_path, config_names):
Gets the configuration parser from the specified Torchscript bundle file path.
"""

bundle_suffixes = (".json", ".yaml", "yml") # The only supported file ext(s)
bundle_suffixes = (".json", ".yaml", ".yml") # The only supported file ext(s)
config_folder = "extra"

def _read_from_archive(archive, root_name: str, config_name: str, do_search=True):
Expand Down Expand Up @@ -216,7 +216,7 @@ def _read_from_archive(archive, root_name: str, config_name: str, do_search=True
name_list = archive.namelist()
for suffix in bundle_suffixes:
for n in name_list:
if (f"{config_name}{suffix}").casefold in n.casefold():
if (f"{config_name}{suffix}").casefold() in n.casefold():
logging.debug(f"Trying to read content of config {config_name!r} from {n!r}.")
content_text = archive.read(n)
break
Expand Down Expand Up @@ -745,6 +745,7 @@ def compute(self, op_input, op_output, context):
# value: NdarrayOrTensor # MyPy complaints
value, meta_data = self._receive_input(name, op_input, context)
value = convert_to_dst_type(value, dst=value)[0]
meta_data = meta_data or {}
if not isinstance(meta_data, dict):
raise ValueError("`meta_data` must be a dict.")
value = MetaTensor.ensure_torch_and_prune_meta(value, meta_data)
Expand Down
101 changes: 101 additions & 0 deletions monai/deploy/operators/monet_bundle_inference_operator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Copyright 2025 MONAI Consortium
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from typing import Any, Dict, Tuple, Union

from monai.deploy.core import Image
from monai.deploy.operators.monai_bundle_inference_operator import MonaiBundleInferenceOperator, get_bundle_config
from monai.deploy.utils.importutil import optional_import
from monai.transforms import ConcatItemsd, ResampleToMatch
from monai.deploy.core.models.torch_model import TorchScriptModel
from monai.deploy.core.models.triton_model import TritonModel
torch, _ = optional_import("torch", "1.10.2")
MetaTensor, _ = optional_import("monai.data.meta_tensor", name="MetaTensor")
__all__ = ["MONetBundleInferenceOperator"]


class MONetBundleInferenceOperator(MonaiBundleInferenceOperator):
"""
A specialized operator for performing inference using the MONet bundle (https://github.com/minnelab/MONet-Bundle).
For more details, please refer to the [MONet-Bundle](https://github.com/minnelab/MONet-Bundle) repository.
This operator extends the `MonaiBundleInferenceOperator` to support nnUNet-specific
configurations and prediction logic. It initializes the nnUNet predictor and provides
a method for performing inference on input data.

Attributes
----------
_nnunet_predictor : torch.nn.Module
The nnUNet predictor module used for inference.

Methods
-------
_init_config(config_names)
Initializes the configuration for the nnUNet bundle, including parsing the bundle
configuration and setting up the nnUNet predictor.
predict(data, *args, **kwargs)
Performs inference on the input data using the nnUNet predictor.
"""

def __init__(
self,
*args,
**kwargs,
):

super().__init__(*args, **kwargs)

self._nnunet_predictor: torch.nn.Module = None

def _init_config(self, config_names):

super()._init_config(config_names)
parser = get_bundle_config(str(self._bundle_path), config_names)
self._parser = parser

self._nnunet_predictor = parser.get_parsed_content("network_def")
Comment on lines +57 to +63
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Double parsing: _init_config parses the bundle config twice.

super()._init_config(config_names) already calls get_bundle_config and assigns self._parser. Lines 61–62 immediately re-parse the same bundle and overwrite self._parser, discarding the one the parent just set up. This doubles the I/O and parsing work. More importantly, the parent's _init_config configures self._device, self._inferer, self._preproc, self._postproc, etc., all using the first parser. Overwriting self._parser afterward creates a divergence between those cached objects and the active parser.

If the intent is just to get network_def, you can use the parser that super() already set:

Proposed fix
     def _init_config(self, config_names):
 
         super()._init_config(config_names)
-        parser = get_bundle_config(str(self._bundle_path), config_names)
-        self._parser = parser
-
-        self._nnunet_predictor = parser.get_parsed_content("network_def")
+        self._nnunet_predictor = self._parser.get_parsed_content("network_def")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def _init_config(self, config_names):
super()._init_config(config_names)
parser = get_bundle_config(str(self._bundle_path), config_names)
self._parser = parser
self._nnunet_predictor = parser.get_parsed_content("network_def")
def _init_config(self, config_names):
super()._init_config(config_names)
self._nnunet_predictor = self._parser.get_parsed_content("network_def")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@monai/deploy/operators/monet_bundle_inference_operator.py` around lines 58 -
64, The _init_config implementation is re-parsing the bundle and overwriting
self._parser after calling super()._init_config, which causes double I/O and a
mismatch with objects the parent initialized (e.g., self._device, self._inferer,
self._preproc, self._postproc); remove the extra get_bundle_config call and
instead reuse the parser the parent already created (use self._parser) to obtain
network_def via self._parser.get_parsed_content("network_def") and assign that
to self._nnunet_predictor without reassigning self._parser.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This suggestion from copilot needs review - If the parent class is initializing the model properly, we can accept the suggestion, but I am not sure if get_bundle_config is needed to apply any config patch.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SimoneBendazzoli93 - please review this suggestion above

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!


def _set_model_network(self, model_network):
"""
Sets the model network for the nnUNet predictor.

Parameters
----------
model_network : torch.nn.Module or torch.jit.ScriptModule
The model network to be used for inference.
"""
if (
not isinstance(model_network, torch.nn.Module)
and not isinstance(model_network, torch.jit.ScriptModule)
and not isinstance(model_network, TorchScriptModel)
and not isinstance(model_network, TritonModel)
):
raise TypeError("model_network must be an instance of torch.nn.Module or torch.jit.ScriptModule")
self._nnunet_predictor.predictor.network = model_network

def predict(self, data: Any, *args, **kwargs) -> Union[Image, Any, Tuple[Any, ...], Dict[Any, Any]]:
"""Predicts output using the inferer. If multimodal data is provided as keyword arguments,
it concatenates the data with the main input data."""

self._set_model_network(self._model_network)

if len(kwargs) > 0:
multimodal_data = {"image": data}
for key in kwargs.keys():
if isinstance(kwargs[key], MetaTensor):
multimodal_data[key] = ResampleToMatch(mode="bilinear")(kwargs[key], img_dst=data)
data = ConcatItemsd(keys=list(multimodal_data.keys()), name="image")(multimodal_data)["image"]
Comment on lines +89 to +94
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Mar 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Do not silently drop non-MetaTensor modalities from kwargs.

At Line 92–94, only MetaTensor values are added to multimodal_data; all other provided modalities are ignored without error. That can produce incomplete inference inputs.

Proposed fix
         if len(kwargs) > 0:
             multimodal_data = {"image": data}
             for key in kwargs.keys():
                 if isinstance(kwargs[key], MetaTensor):
                     multimodal_data[key] = ResampleToMatch(mode="bilinear")(kwargs[key], img_dst=data)
+                else:
+                    multimodal_data[key] = kwargs[key]
             data = ConcatItemsd(keys=list(multimodal_data.keys()), name="image")(multimodal_data)["image"]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@monai/deploy/operators/monet_bundle_inference_operator.py` around lines 89 -
94, The current loop in the inference operator only adds kwargs entries when
isinstance(..., MetaTensor), silently dropping others; update the handling in
the method (where multimodal_data, ResampleToMatch, and ConcatItemsd are used)
to validate kwargs: iterate items in kwargs and for each key either resample and
add it to multimodal_data if it's a MetaTensor, or raise a clear
TypeError/ValueError that includes the offending key name and its actual type so
callers know they passed an unsupported modality type (do not silently ignore
non-MetaTensor values).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can be ignored

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@chezhia, understood — I'll defer to your judgment on this one. Acknowledged and moving on!

if len(data.shape) == 4:
data = data[None]
prediction = self._nnunet_predictor(data)
if hasattr(prediction, "meta") and hasattr(data, "meta"):
prediction.meta = data.meta
return prediction
return prediction

Check warning on line 101 in monai/deploy/operators/monet_bundle_inference_operator.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Delete this unreachable code or refactor the code to make it reachable.

See more on https://sonarcloud.io/project/issues?id=Project-MONAI_monai-deploy-app-sdk&issues=AZ0D4tdM2KxiX-UOsOHC&open=AZ0D4tdM2KxiX-UOsOHC&pullRequest=574