Skip to content

Commit 878ef38

Browse files
committed
Added command line option support for ease of running python app directly
Signed-off-by: M Q <mingmelvinq@nvidia.com>
1 parent 029f8bc commit 878ef38

File tree

20 files changed

+3505
-919
lines changed

20 files changed

+3505
-919
lines changed

examples/apps/ai_livertumor_seg_app/app.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ def compose(self):
4646
"""Creates the app specific operators and chain them up in the processing DAG."""
4747

4848
self._logger.info(f"Begin {self.compose.__name__}")
49-
app_context = AppContext({}) # Let it figure out all the attributes without overriding
49+
# Use command line options over environment variables to init context.
50+
app_context = Application.init_app_context(self.argv)
5051
app_input_path = Path(app_context.input_path)
5152
app_output_path = Path(app_context.output_path)
5253
model_path = Path(app_context.model_path)

examples/apps/ai_multi_ai_app/app.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,8 @@ def compose(self):
8686

8787
logging.info(f"Begin {self.compose.__name__}")
8888

89-
app_context = AppContext({}) # Let it figure out all the attributes without overriding
89+
# Use command line options over environment variables to init context.
90+
app_context = Application.init_app_context(self.argv)
9091
app_input_path = Path(app_context.input_path)
9192
app_output_path = Path(app_context.output_path)
9293

examples/apps/ai_pancreas_seg_app/app.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,8 @@ def compose(self):
6868

6969
logging.info(f"Begin {self.compose.__name__}")
7070

71-
app_context = AppContext({}) # Let it figure out all the attributes without overriding
71+
# Use command line options over environment variables to init context.
72+
app_context = Application.init_app_context(self.argv)
7273
app_input_path = Path(app_context.input_path)
7374
app_output_path = Path(app_context.output_path)
7475

examples/apps/ai_spleen_seg_app/app.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ def compose(self):
6666

6767
logging.info(f"Begin {self.compose.__name__}")
6868

69-
app_context = AppContext({}) # Let it figure out all the attributes without overriding
69+
# Use Commandline options over environment variables to init context.
70+
app_context = Application.init_app_context(self.argv)
7071
app_input_path = Path(app_context.input_path)
7172
app_output_path = Path(app_context.output_path)
7273

examples/apps/ai_unetr_seg_app/app.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ def compose(self):
4646
"""Creates the app specific operators and chain them up in the processing DAG."""
4747

4848
self._logger.info(f"Begin {self.compose.__name__}")
49-
app_context = AppContext({}) # Let it figure out all the attributes without overriding
49+
# Use command line options over environment variables to init context.
50+
app_context = Application.init_app_context(self.argv)
5051
app_input_path = Path(app_context.input_path)
5152
app_output_path = Path(app_context.output_path)
5253
model_path = Path(app_context.model_path)

examples/apps/breast_density_classifer_app/app.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ def compose(self):
2828
"""Creates the app specific operators and chain them up in the processing DAG."""
2929
logging.info(f"Begin {self.compose.__name__}")
3030

31-
app_context = AppContext({}) # Let it figure out all the attributes without overriding
31+
# Use command line options over environment variables to init context.
32+
app_context = Application.init_app_context(self.argv)
3233
app_input_path = Path(app_context.input_path)
3334
app_output_path = Path(app_context.output_path)
3435
model_path = Path(app_context.model_path)

examples/apps/dicom_series_to_image_app/app.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ class App(Application):
2626
"""
2727

2828
def compose(self):
29-
app_context = AppContext({}) # Let it figure out the data paths using well-known env vars etc.
29+
# Use command line options over environment variables to init context.
30+
app_context = Application.init_app_context(self.argv)
3031
input_dcm_folder = Path(app_context.input_path)
3132
output_folder = Path(app_context.output_path)
3233
print(f"input_dcm_folder: {input_dcm_folder}")

examples/apps/mednist_classifier_monaideploy/mednist_classifier_monaideploy.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
MEDNIST_CLASSES = ["AbdomenCT", "BreastMRI", "CXR", "ChestCT", "Hand", "HeadCT"]
2525

2626

27+
# Decorator support is not available in this version of the SDK, to be re-introduced later
2728
# @md.env(pip_packages=["pillow"])
2829
class LoadPILOperator(Operator):
2930
"""Load image from the given input (DataPath) and set numpy array to the output (Image)."""
@@ -200,13 +201,15 @@ def compute(self, op_input, op_output, context):
200201
json.dump(result, fp)
201202

202203

204+
# Decorator support is not available in this version of the SDK, to be re-introduced later
203205
# @md.resource(cpu=1, gpu=1, memory="1Gi")
204206
# @md.env(pip_packages=["pydicom >= 2.3.0", "highdicom>=0.18.2"]) # because of the use of DICOM writer operator
205207
class App(Application):
206208
"""Application class for the MedNIST classifier."""
207209

208210
def compose(self):
209-
app_context = AppContext({}) # Let it figure out all the attributes without overriding
211+
# Use Commandline options over environment variables to init context.
212+
app_context = Application.init_app_context(self.argv)
210213
app_input_path = Path(app_context.input_path)
211214
app_output_path = Path(app_context.output_path)
212215
model_path = Path(app_context.model_path)

examples/apps/simple_imaging_app/app.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@
2020
from monai.deploy.core import AppContext, Application
2121

2222

23+
# Decorator support is not available in this version of the SDK, to be re-introduced later
2324
# @resource(cpu=1)
24-
# # pip_packages can be a string that is a path(str) to requirements.txt file or a list of packages.
2525
# @env(pip_packages=["scikit-image >= 0.17.2"])
2626
class App(Application):
2727
"""This is a very basic application.
@@ -42,10 +42,12 @@ def compose(self):
4242
Each operator has a single input and a single output port.
4343
Each operator performs some kind of image processing function.
4444
"""
45-
app_context = AppContext({}) # Let it figure out all the attributes without overriding
45+
46+
# Use Commandline options over environment variables to init context.
47+
app_context = Application.init_app_context(self.argv)
4648
sample_data_path = Path(app_context.input_path)
4749
output_data_path = Path(app_context.output_path)
48-
print(f"sample_data_path: {sample_data_path}")
50+
logging.info(f"sample_data_path: {sample_data_path}")
4951

5052
# Please note that the Application object, self, is passed as the first positonal argument
5153
# and the others as kwargs.
@@ -70,7 +72,7 @@ def compose(self):
7072
"in1",
7173
)
7274
},
73-
) # Using port name is optional for single port cases
75+
)
7476

7577

7678
if __name__ == "__main__":

monai/deploy/core/__init__.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,22 @@
2424
InputContext
2525
OutputContext
2626
RuntimeEnv
27+
init_app_context
28+
parse_args
2729
"""
2830

2931
# Need to import explicit ones to quiet mypy complaints
3032
from holoscan.core import *
3133
from holoscan.core import Application, Condition, ConditionType, Fragment, Operator, OperatorSpec
3234

33-
from .app_context import AppContext
35+
from .app_context import AppContext, init_app_context
36+
from .arg_parser import parse_args
3437
from .domain.datapath import DataPath
3538
from .domain.image import Image
3639
from .io_type import IOType
3740
from .models import Model, ModelFactory, NamedModel, TorchScriptModel, TritonModel
3841
from .runtime_env import RuntimeEnv
42+
43+
# Add the function to the existing Application class, which could've been used as helper func too.
44+
# It is well understood that deriving from the Application base is a better approach, but maybe later.
45+
Application.init_app_context = init_app_context

monai/deploy/core/app_context.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@
99
# See the License for the specific language governing permissions and
1010
# limitations under the License.
1111

12+
import logging
1213
from os.path import abspath
13-
from typing import Dict, Optional
14+
from typing import Dict, List, Optional
1415

16+
from .arg_parser import parse_args, set_up_logging
1517
from .models.factory import ModelFactory
1618
from .models.model import Model
1719
from .runtime_env import RuntimeEnv
@@ -55,3 +57,26 @@ def __repr__(self):
5557
f"AppContext(input_path={self.input_path}, output_path={self.output_path}, "
5658
f"model_path={self.model_path}, workdir={self.workdir})"
5759
)
60+
61+
62+
def init_app_context(argv: Optional[List[str]] = None, runtime_env: Optional[RuntimeEnv] = None) -> AppContext:
63+
"""Initializes the app context with arguments and well-known environment variables.
64+
65+
The arguments, if passed in, override the attributes set with environment variables.
66+
67+
Args:
68+
argv (Optional[List[str]], optional): arguments passed to the program. Defaults to None.
69+
70+
Returns:
71+
AppContext: the AppContext object
72+
"""
73+
74+
args = parse_args(argv)
75+
set_up_logging(args.log_level)
76+
logging.info(f"Parsed args: {args}")
77+
78+
# The parsed args from the command line override that from the environment variables
79+
app_context = AppContext({key: val for key, val in vars(args).items() if val}, runtime_env)
80+
logging.info(f"AppContext object: {app_context}")
81+
82+
return app_context

monai/deploy/core/arg_parser.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# Copyright 2021-2023 MONAI Consortium
2+
# Licensed under the Apache License, Version 2.0 (the "License");
3+
# you may not use this file except in compliance with the License.
4+
# You may obtain a copy of the License at
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
# Unless required by applicable law or agreed to in writing, software
7+
# distributed under the License is distributed on an "AS IS" BASIS,
8+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9+
# See the License for the specific language governing permissions and
10+
# limitations under the License.
11+
import argparse
12+
import json
13+
import logging.config
14+
from pathlib import Path
15+
from typing import List, Optional, Union
16+
17+
from monai.deploy.utils import argparse_types
18+
19+
LOG_CONFIG_FILENAME = "logging.json"
20+
21+
22+
def parse_args(argv: Optional[List[str]] = None) -> argparse.Namespace:
23+
"""Parses the arguments passed to the application.
24+
25+
Args:
26+
argv (Optional[List[str]], optional): The command line arguments to parse. The first item should be the path to the python executable. If not specified, ``sys.argv`` is used. Defaults to None.
27+
28+
Returns:
29+
argparse.Namespace: parsed arguments.
30+
"""
31+
if argv is None:
32+
import sys
33+
34+
argv = sys.argv
35+
argv = list(argv) # copy argv for manipulation to avoid side-effects
36+
37+
# We have intentionally not set the default using `default="INFO"` here so that the default
38+
# value from here doesn't override the value in `LOG_CONFIG_FILENAME` unless the user indends to do
39+
# so. If the user doesn't use this flag to set log level, this argument is set to "None"
40+
# and the logging level specified in `LOG_CONFIG_FILENAME` is used.
41+
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
42+
parser.add_argument(
43+
"-l",
44+
"--log-level",
45+
dest="log_level",
46+
type=str.upper,
47+
choices=["DEBUG", "INFO", "WARN", "ERROR", "CRITICAL"],
48+
help="Set the logging level (default: INFO)",
49+
)
50+
parser.add_argument(
51+
"--input", "-i", type=argparse_types.valid_existing_path, help="Path to input folder/file (default: input)"
52+
)
53+
parser.add_argument(
54+
"--output", "-o", type=argparse_types.valid_dir_path, help="Path to output folder (default: output)"
55+
)
56+
parser.add_argument(
57+
"--model", "-m", type=argparse_types.valid_existing_path, help="Path to model(s) folder/file (default: models)"
58+
)
59+
parser.add_argument(
60+
"--workdir",
61+
"-w",
62+
type=argparse_types.valid_dir_path,
63+
help="Path to workspace folder (default: A temporary '.monai_workdir' folder in the current folder)",
64+
)
65+
66+
args = parser.parse_args(argv[1:])
67+
args.argv = argv # save argv for later use in runpy
68+
69+
return args
70+
71+
72+
def set_up_logging(level: Optional[str], config_path: Union[str, Path] = LOG_CONFIG_FILENAME):
73+
"""Initializes the logger and sets up logging level.
74+
75+
Args:
76+
level (str): A logging level (DEBUG, INFO, WARN, ERROR, CRITICAL).
77+
log_config_path (str): A path to logging config file.
78+
"""
79+
# Default log config path
80+
log_config_path = Path(__file__).absolute().parent.parent / LOG_CONFIG_FILENAME
81+
82+
config_path = Path(config_path)
83+
84+
# If a logging config file that is specified by `log_config_path` exists in the current folder,
85+
# it overrides the default one
86+
if config_path.exists():
87+
log_config_path = config_path
88+
89+
config_dict = json.loads(log_config_path.read_bytes())
90+
91+
if level is not None and "root" in config_dict:
92+
config_dict["root"]["level"] = level
93+
logging.config.dictConfig(config_dict)

monai/deploy/core/runtime_env.py

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,6 @@
1313
from abc import ABC
1414
from typing import Dict, Optional, Tuple
1515

16-
# from monai.deploy.core.datastores.factory import DatastoreFactory
17-
# from monai.deploy.core.executors.factory import ExecutorFactory
18-
# from monai.deploy.core.graphs.factory import GraphFactory
19-
2016

2117
class RuntimeEnv(ABC):
2218
"""Class responsible for managing run time settings.
@@ -30,18 +26,12 @@ class RuntimeEnv(ABC):
3026
"output": ("HOLOSCAN_OUTPUT_PATH", "output"),
3127
"model": ("HOLOSCAN_MODEL_PATH", "models"),
3228
"workdir": ("HOLOSCAN_WORKDIR", ""),
33-
# "graph": ("MONAI_GRAPH", GraphFactory.DEFAULT), # The 'MONAI_GRAPH' is not part of MAP spec.
34-
# "datastore": ("MONAI_DATASTORE", DatastoreFactory.DEFAULT), # The 'MONAI_DATASTORE' is not part of MAP spec.
35-
# "executor": ("MONAI_EXECUTOR", ExecutorFactory.DEFAULT), # The 'MONAI_EXECUTOR' is not part of MAP spec.
3629
}
3730

3831
input: str = ""
3932
output: str = ""
4033
model: str = ""
4134
workdir: str = ""
42-
# graph: str = ""
43-
# datastore: str = ""
44-
# executor: str = ""
4535

4636
def __init__(self, defaults: Optional[Dict[str, Tuple[str, ...]]] = None):
4737
if defaults is None:

notebooks/tutorials/01_simple_app.ipynb

Lines changed: 208 additions & 337 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)