Repository: Project-MONAI/MONAI

Issue:
Enhance BundleWorkflow properties and the operations
**Is your feature request related to a problem? Please describe.**
During testing with our bundle tutorials, found some improvement areas for the new feature `BundleWorkflow`.
Will comment in this ticket and submit PR to fix then.




Discussion:

Resolving patch:
diff --git a/monai/bundle/properties.py b/monai/bundle/properties.py
--- a/monai/bundle/properties.py
+++ b/monai/bundle/properties.py
@@ -159,6 +159,21 @@
         BundleProperty.REQUIRED: True,
         BundlePropertyConfig.ID: "device",
     },
+    "dataset_dir": {
+        BundleProperty.DESC: "directory path of the dataset.",
+        BundleProperty.REQUIRED: True,
+        BundlePropertyConfig.ID: "dataset_dir",
+    },
+    "dataset": {
+        BundleProperty.DESC: "PyTorch dataset object for the inference / evaluation logic.",
+        BundleProperty.REQUIRED: True,
+        BundlePropertyConfig.ID: "dataset",
+    },
+    "dataset_data": {
+        BundleProperty.DESC: "data source for the inference / evaluation dataset.",
+        BundleProperty.REQUIRED: True,
+        BundlePropertyConfig.ID: f"dataset{ID_SEP_KEY}data",
+    },
     "evaluator": {
         BundleProperty.DESC: "inference / evaluation workflow engine.",
         BundleProperty.REQUIRED: True,
@@ -174,6 +189,12 @@
         BundleProperty.REQUIRED: True,
         BundlePropertyConfig.ID: "inferer",
     },
+    "handlers": {
+        BundleProperty.DESC: "event-handlers for the inference / evaluation logic.",
+        BundleProperty.REQUIRED: False,
+        BundlePropertyConfig.ID: "handlers",
+        BundlePropertyConfig.REF_ID: f"evaluator{ID_SEP_KEY}val_handlers",
+    },
     "preprocessing": {
         BundleProperty.DESC: "preprocessing for the input data.",
         BundleProperty.REQUIRED: False,
diff --git a/monai/bundle/scripts.py b/monai/bundle/scripts.py
--- a/monai/bundle/scripts.py
+++ b/monai/bundle/scripts.py
@@ -608,16 +608,22 @@ def run(
     .. code-block:: bash
 
         # Execute this module as a CLI entry:
+        python -m monai.bundle run --meta_file <meta path> --config_file <config path>
+
+        # Execute with specified `run_id=training`:
         python -m monai.bundle run training --meta_file <meta path> --config_file <config path>
 
+        # Execute with all specified `run_id=runtest`, `init_id=inittest`, `final_id=finaltest`:
+        python -m monai.bundle run --run_id runtest --init_id inittest --final_id finaltest ...
+
         # Override config values at runtime by specifying the component id and its new value:
-        python -m monai.bundle run training --net#input_chns 1 ...
+        python -m monai.bundle run --net#input_chns 1 ...
 
         # Override config values with another config file `/path/to/another.json`:
-        python -m monai.bundle run evaluating --net %/path/to/another.json ...
+        python -m monai.bundle run --net %/path/to/another.json ...
 
         # Override config values with part content of another config file:
-        python -m monai.bundle run training --net %/data/other.json#net_arg ...
+        python -m monai.bundle run --net %/data/other.json#net_arg ...
 
         # Set default args of `run` in a JSON / YAML file, help to record and simplify the command line.
         # Other args still can override the default args at runtime:
@@ -1462,7 +1468,7 @@ def init_bundle(
         Describe your model here and how to run it, for example using `inference.json`:
 
         ```
-        python -m monai.bundle run evaluating \
+        python -m monai.bundle run \
             --meta_file /path/to/bundle/configs/metadata.json \
             --config_file /path/to/bundle/configs/inference.json \
             --dataset_dir ./input \
diff --git a/monai/bundle/workflows.py b/monai/bundle/workflows.py
--- a/monai/bundle/workflows.py
+++ b/monai/bundle/workflows.py
@@ -15,6 +15,7 @@
 import time
 import warnings
 from abc import ABC, abstractmethod
+from copy import copy
 from logging.config import fileConfig
 from pathlib import Path
 from typing import Any, Sequence
@@ -53,10 +54,10 @@ def __init__(self, workflow: str | None = None):
             self.workflow = None
             return
         if workflow.lower() in self.supported_train_type:
-            self.properties = TrainProperties
+            self.properties = copy(TrainProperties)
             self.workflow = "train"
         elif workflow.lower() in self.supported_infer_type:
-            self.properties = InferProperties
+            self.properties = copy(InferProperties)
             self.workflow = "infer"
         else:
             raise ValueError(f"Unsupported workflow type: '{workflow}'.")
@@ -129,6 +130,24 @@ def get_workflow_type(self):
         """
         return self.workflow
 
+    def add_property(self, name: str, required: str, desc: str | None = None) -> None:
+        """
+        Besides the default predefined properties, some 3rd party aplications may need the bundle
+        definition to provide additonal properties for the specific use cases, if the bundlle can't
+        provide the property, means it can't work with the application.
+        This utility adds the property for the application requirements check and access.
+
+        Args:
+            name: the name of target property.
+            required: whether the property is "must-have".
+            desc: descriptions for the property.
+        """
+        if self.properties is None:
+            self.properties = {}
+        if name in self.properties:
+            warnings.warn(f"property '{name}' already exists in the properties list, overriding it.")
+        self.properties[name] = {BundleProperty.DESC: desc, BundleProperty.REQUIRED: required}
+
     def check_properties(self) -> list[str] | None:
         """
         Check whether the required properties are existing in the bundle workflow.
@@ -316,6 +335,25 @@ def _set_property(self, name: str, property: dict, value: Any) -> None:
             self._is_initialized = False
             self.parser.ref_resolver.reset()
 
+    def add_property(  # type: ignore[override]
+        self, name: str, required: str, config_id: str, desc: str | None = None
+    ) -> None:
+        """
+        Besides the default predefined properties, some 3rd party aplications may need the bundle
+        definition to provide additonal properties for the specific use cases, if the bundlle can't
+        provide the property, means it can't work with the application.
+        This utility adds the property for the application requirements check and access.
+
+        Args:
+            name: the name of target property.
+            required: whether the property is "must-have".
+            config_id: the config ID of target property in the bundle definition.
+            desc: descriptions for the property.
+
+        """
+        super().add_property(name=name, required=required, desc=desc)
+        self.properties[name][BundlePropertyConfig.ID] = config_id  # type: ignore[index]
+
     def _check_optional_id(self, name: str, property: dict) -> bool:
         """
         If an optional property has reference in the config, check whether the property is existing.


Test patch:
diff --git a/tests/nonconfig_workflow.py b/tests/nonconfig_workflow.py
--- a/tests/nonconfig_workflow.py
+++ b/tests/nonconfig_workflow.py
@@ -41,7 +41,10 @@ def __init__(self, filename, output_dir):
         self.filename = filename
         self.output_dir = output_dir
         self._bundle_root = "will override"
+        self._dataset_dir = "."
         self._device = torch.device("cpu")
+        self._data = [{"image": self.filename}]
+        self._dataset = None
         self._network_def = None
         self._inferer = None
         self._preprocessing = None
@@ -54,8 +57,8 @@ def initialize(self):
             self._preprocessing = Compose(
                 [LoadImaged(keys="image"), EnsureChannelFirstd(keys="image"), ScaleIntensityd(keys="image")]
             )
-        dataset = Dataset(data=[{"image": self.filename}], transform=self._preprocessing)
-        dataloader = DataLoader(dataset, batch_size=1, num_workers=4)
+        self._dataset = Dataset(data=self._data, transform=self._preprocessing)
+        dataloader = DataLoader(self._dataset, batch_size=1, num_workers=4)
 
         if self._network_def is None:
             self._network_def = UNet(
@@ -97,6 +100,12 @@ def finalize(self):
     def _get_property(self, name, property):
         if name == "bundle_root":
             return self._bundle_root
+        if name == "dataset_dir":
+            return self._dataset_dir
+        if name == "dataset_data":
+            return self._data
+        if name == "dataset":
+            return self._dataset
         if name == "device":
             return self._device
         if name == "evaluator":
@@ -117,6 +126,12 @@ def _set_property(self, name, property, value):
             self._bundle_root = value
         elif name == "device":
             self._device = value
+        elif name == "dataset_dir":
+            self._dataset_dir = value
+        elif name == "dataset_data":
+            self._data = value
+        elif name == "dataset":
+            self._dataset = value
         elif name == "evaluator":
             self._evaluator = value
         elif name == "network_def":
diff --git a/tests/test_fl_monai_algo_dist.py b/tests/test_fl_monai_algo_dist.py
--- a/tests/test_fl_monai_algo_dist.py
+++ b/tests/test_fl_monai_algo_dist.py
@@ -41,6 +41,10 @@ def test_train(self):
             pathjoin(_data_dir, "config_fl_evaluate.json"),
             pathjoin(_data_dir, "multi_gpu_evaluate.json"),
         ]
+        train_workflow = ConfigWorkflow(config_file=train_configs, workflow="train", logging_file=_logging_file)
+        # simulate the case that this application has specific requirements for a bundle workflow
+        train_workflow.add_property(name="loader", required=True, config_id="train#training_transforms#0", desc="NA")
+
         # initialize algo
         algo = MonaiAlgo(
             bundle_root=_data_dir,