Skip to content

Commit

Permalink
6424 Add support to register customized app required properties (Proj…
Browse files Browse the repository at this point in the history
…ect-MONAI#6432)

Fixes Project-MONAI#6424 .

### Description

This PR added support to register customized app required properties to
the `BundleWorkflow`, then check and access the properties.

### Types of changes
<!--- Put an `x` in all the boxes that apply, and remove the not
applicable items -->
- [x] Non-breaking change (fix or new feature that would not break
existing functionality).
- [ ] Breaking change (fix or new feature that would cause existing
functionality to change).
- [ ] New tests added to cover the changes.
- [ ] Integration tests passed locally by running `./runtests.sh -f -u
--net --coverage`.
- [ ] Quick tests passed locally by running `./runtests.sh --quick
--unittests --disttests`.
- [ ] In-line docstrings updated.
- [ ] Documentation updated, tested `make html` command in the `docs/`
folder.

---------

Signed-off-by: Nic Ma <nma@nvidia.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
  • Loading branch information
Nic-Ma and pre-commit-ci[bot] authored Apr 28, 2023
1 parent d23221f commit 4219e0f
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 8 deletions.
21 changes: 21 additions & 0 deletions monai/bundle/properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
14 changes: 10 additions & 4 deletions monai/bundle/scripts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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 \
Expand Down
42 changes: 40 additions & 2 deletions monai/bundle/workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}'.")
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
19 changes: 17 additions & 2 deletions tests/nonconfig_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -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":
Expand All @@ -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":
Expand Down
4 changes: 4 additions & 0 deletions tests/test_fl_monai_algo_dist.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit 4219e0f

Please sign in to comment.