diff --git a/ci/bundle_custom_data.py b/ci/bundle_custom_data.py index 711bce3f..2965350c 100644 --- a/ci/bundle_custom_data.py +++ b/ci/bundle_custom_data.py @@ -36,6 +36,7 @@ "breast_density_classification", "mednist_reg", "brats_mri_axial_slices_generative_diffusion", + "vista3d", ] # This dict is used for our CI tests to install required dependencies that cannot be installed by `pip install` directly. diff --git a/ci/unit_tests/test_vista3d.py b/ci/unit_tests/test_vista3d.py new file mode 100644 index 00000000..b12d3e69 --- /dev/null +++ b/ci/unit_tests/test_vista3d.py @@ -0,0 +1,442 @@ +# Copyright (c) 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. + +import os +import shutil +import sys +import tempfile +import unittest + +import nibabel as nib +import numpy as np +from monai.bundle import ConfigWorkflow +from parameterized import parameterized +from utils import check_workflow + +TEST_CASE_INFER = [ + { + "bundle_root": "models/vista3d", + "input_dict": {"label_prompt": [25], "points": [[123, 212, 151]], "point_labels": [1]}, + "patch_size": [32, 32, 32], + "checkpointloader#_disabled_": True, # do not load weights" + "initialize": ["$monai.utils.set_determinism(seed=123)"], + } +] +TEST_CASE_INFER_STR_PROMPT = [ + { + "bundle_root": "models/vista3d", + "input_dict": {"label_prompt": ["spleen"], "points": [[123, 212, 151]], "point_labels": [1]}, + "patch_size": [32, 32, 32], + "checkpointloader#_disabled_": True, # do not load weights" + "initialize": ["$monai.utils.set_determinism(seed=123)"], + } +] +TEST_CASE_INFER_MULTI_PROMPT = [ + { + "bundle_root": "models/vista3d", + "input_dict": {"label_prompt": [25, 24, 1]}, + "patch_size": [32, 32, 32], + "checkpointloader#_disabled_": True, # do not load weights" + "initialize": ["$monai.utils.set_determinism(seed=123)"], + } +] +TEST_CASE_INFER_MULTI_STR_PROMPT = [ + { + "bundle_root": "models/vista3d", + "input_dict": {"label_prompt": ["hepatic vessel", "pancreatic tumor", "liver"]}, + "patch_size": [32, 32, 32], + "checkpointloader#_disabled_": True, # do not load weights" + "initialize": ["$monai.utils.set_determinism(seed=123)"], + } +] +TEST_CASE_INFER_MULTI_NEW_STR_PROMPT = [ + { + "bundle_root": "models/vista3d", + "input_dict": {"label_prompt": ["new class 1", "new class 2", "new class 3"]}, + "patch_size": [32, 32, 32], + "checkpointloader#_disabled_": True, # do not load weights" + "initialize": ["$monai.utils.set_determinism(seed=123)"], + } +] +TEST_CASE_INFER_SUBCLASS = [ + { + "bundle_root": "models/vista3d", + "input_dict": {"label_prompt": [2, 20, 21]}, + "patch_size": [32, 32, 32], + "checkpointloader#_disabled_": True, # do not load weights" + "initialize": ["$monai.utils.set_determinism(seed=123)"], + } +] +TEST_CASE_INFER_NO_PROMPT = [ + { + "bundle_root": "models/vista3d", + "input_dict": {}, # put an empty dict, and will add an image in the test function + "patch_size": [32, 32, 32], + "checkpointloader#_disabled_": True, # do not load weights" + "initialize": ["$monai.utils.set_determinism(seed=123)"], + } +] +TEST_CASE_EVAL = [ + { + "bundle_root": "models/vista3d", + "patch_size": [32, 32, 32], + "initialize": ["$monai.utils.set_determinism(seed=123)"], + } +] +TEST_CASE_TRAIN = [ + { + "bundle_root": "models/vista3d", + "patch_size": [32, 32, 32], + "epochs": 2, + "val_interval": 1, + "initialize": ["$monai.utils.set_determinism(seed=123)"], + } +] +TEST_CASE_TRAIN_CONTINUAL = [ + { + "bundle_root": "models/vista3d", + "patch_size": [32, 32, 32], + "epochs": 2, + "val_interval": 1, + "initialize": ["$monai.utils.set_determinism(seed=123)"], + "finetune": False, + } +] +TEST_CASE_ERROR_PROMPTS = [ + [ + { + "bundle_root": "models/vista3d", + "input_dict": {}, + "patch_size": [32, 32, 32], + "checkpointloader#_disabled_": True, # do not load weights" + "everything_labels": None, + "initialize": ["$monai.utils.set_determinism(seed=123)"], + "error": "Prompt must be given for inference.", + } + ], + [ + { + "bundle_root": "models/vista3d", + "input_dict": {"label_prompt": [[25, 26, 27]]}, + "patch_size": [32, 32, 32], + "checkpointloader#_disabled_": True, # do not load weights" + "initialize": ["$monai.utils.set_determinism(seed=123)"], + "error": "Label prompt must be a list of single scalar, [1,2,3,4,...,].", + } + ], + [ + { + "bundle_root": "models/vista3d", + "input_dict": {"label_prompt": 25}, + "patch_size": [32, 32, 32], + "checkpointloader#_disabled_": True, # do not load weights" + "initialize": ["$monai.utils.set_determinism(seed=123)"], + "error": "Label prompt must be a list, [1,2,3,4,...,].", + } + ], + [ + { + "bundle_root": "models/vista3d", + "input_dict": {"label_prompt": [256]}, + "patch_size": [32, 32, 32], + "checkpointloader#_disabled_": True, # do not load weights" + "initialize": ["$monai.utils.set_determinism(seed=123)"], + "error": "Current bundle only supports label prompt smaller than 255.", + } + ], + [ + { + "bundle_root": "models/vista3d", + "input_dict": {"label_prompt": [25], "points": [[123, 212, 151]]}, + "patch_size": [32, 32, 32], + "checkpointloader#_disabled_": True, # do not load weights" + "initialize": ["$monai.utils.set_determinism(seed=123)"], + "error": "Point labels must be given if points are given.", + } + ], + [ + { + "bundle_root": "models/vista3d", + "input_dict": {"label_prompt": [25], "point_labels": [1]}, + "patch_size": [32, 32, 32], + "checkpointloader#_disabled_": True, # do not load weights" + "initialize": ["$monai.utils.set_determinism(seed=123)"], + "error": "Points must be given if point labels are given.", + } + ], + [ + { + "bundle_root": "models/vista3d", + "input_dict": {"label_prompt": [25], "points": [[1, 123, 212, 151]], "point_labels": [1]}, + "patch_size": [32, 32, 32], + "checkpointloader#_disabled_": True, # do not load weights" + "initialize": ["$monai.utils.set_determinism(seed=123)"], + "error": "Points must be three dimensional (x,y,z) in the shape of [[x,y,z],...,[x,y,z]].", + } + ], + [ + { + "bundle_root": "models/vista3d", + "input_dict": {"label_prompt": [25], "points": [[[123, 212, 151]]], "point_labels": [1]}, + "patch_size": [32, 32, 32], + "checkpointloader#_disabled_": True, # do not load weights" + "initialize": ["$monai.utils.set_determinism(seed=123)"], + "error": "Points must be three dimensional (x,y,z) in the shape of [[x,y,z],...,[x,y,z]].", + } + ], + [ + { + "bundle_root": "models/vista3d", + "input_dict": {"label_prompt": [25], "points": [[123, 212, 151]], "point_labels": [1, 1]}, + "patch_size": [32, 32, 32], + "checkpointloader#_disabled_": True, # do not load weights" + "initialize": ["$monai.utils.set_determinism(seed=123)"], + "error": "Points must match point labels.", + } + ], + [ + { + "bundle_root": "models/vista3d", + "input_dict": {"label_prompt": [1], "points": [[123, 212, 151]], "point_labels": [-2]}, + "patch_size": [32, 32, 32], + "checkpointloader#_disabled_": True, # do not load weights" + "initialize": ["$monai.utils.set_determinism(seed=123)"], + "error": "Point labels can only be -1,0,1 and 2,3 for special flags.", + } + ], + [ + { + "bundle_root": "models/vista3d", + "input_dict": {"label_prompt": [25, 26], "points": [[123, 212, 151]], "point_labels": [1]}, + "patch_size": [32, 32, 32], + "checkpointloader#_disabled_": True, # do not load weights" + "initialize": ["$monai.utils.set_determinism(seed=123)"], + "error": "Label prompt can only be a single object if provided with point prompts.", + } + ], +] + + +def test_order(test_name1, test_name2): + def get_order(name): + if "train_config" in name: + return 1 + if "train_continual" in name: + return 2 + if "eval" in name: + return 3 + return 4 + + return get_order(test_name1) - get_order(test_name2) + + +class TestVista3d(unittest.TestCase): + def setUp(self): + self.dataset_dir = tempfile.mkdtemp() + self.dataset_size = 5 + input_shape = (64, 64, 64) + for s in range(self.dataset_size): + test_image = np.random.randint(low=0, high=2, size=input_shape).astype(np.int8) + test_label = np.random.randint(low=0, high=2, size=input_shape).astype(np.int8) + image_filename = os.path.join(self.dataset_dir, f"image_{s}.nii.gz") + label_filename = os.path.join(self.dataset_dir, f"label_{s}.nii.gz") + nib.save(nib.Nifti1Image(test_image, np.eye(4)), image_filename) + nib.save(nib.Nifti1Image(test_label, np.eye(4)), label_filename) + + def tearDown(self): + shutil.rmtree(self.dataset_dir) + + @parameterized.expand([TEST_CASE_TRAIN]) + def test_train_config(self, override): + train_size = self.dataset_size // 2 + train_datalist = [ + { + "image": os.path.join(self.dataset_dir, f"image_{i}.nii.gz"), + "label": os.path.join(self.dataset_dir, f"label_{i}.nii.gz"), + } + for i in range(train_size) + ] + val_datalist = [ + { + "image": os.path.join(self.dataset_dir, f"image_{i}.nii.gz"), + "label": os.path.join(self.dataset_dir, f"label_{i}.nii.gz"), + } + for i in range(train_size, self.dataset_size) + ] + override["train_datalist"] = train_datalist + override["val_datalist"] = val_datalist + + bundle_root = override["bundle_root"] + sys.path = [bundle_root] + sys.path + trainer = ConfigWorkflow( + workflow_type="train", + config_file=os.path.join(bundle_root, "configs/train.json"), + logging_file=os.path.join(bundle_root, "configs/logging.conf"), + meta_file=os.path.join(bundle_root, "configs/metadata.json"), + **override, + ) + check_workflow(trainer, check_properties=False) + + @parameterized.expand([TEST_CASE_EVAL]) + def test_eval_config(self, override): + train_size = self.dataset_size // 2 + train_datalist = [ + { + "image": os.path.join(self.dataset_dir, f"image_{i}.nii.gz"), + "label": os.path.join(self.dataset_dir, f"label_{i}.nii.gz"), + } + for i in range(train_size) + ] + val_datalist = [ + { + "image": os.path.join(self.dataset_dir, f"image_{i}.nii.gz"), + "label": os.path.join(self.dataset_dir, f"label_{i}.nii.gz"), + } + for i in range(train_size, self.dataset_size) + ] + override["train_datalist"] = train_datalist + override["val_datalist"] = val_datalist + bundle_root = override["bundle_root"] + sys.path = [bundle_root] + sys.path + config_files = [ + os.path.join(bundle_root, "configs/train.json"), + os.path.join(bundle_root, "configs/train_continual.json"), + os.path.join(bundle_root, "configs/evaluate.json"), + os.path.join(bundle_root, "configs/data.yaml"), + ] + trainer = ConfigWorkflow( + workflow_type="train", + config_file=config_files, + logging_file=os.path.join(bundle_root, "configs/logging.conf"), + meta_file=os.path.join(bundle_root, "configs/metadata.json"), + **override, + ) + check_workflow(trainer, check_properties=False) + + @parameterized.expand([TEST_CASE_TRAIN_CONTINUAL]) + def test_train_continual_config(self, override): + train_size = self.dataset_size // 2 + train_datalist = [ + { + "image": os.path.join(self.dataset_dir, f"image_{i}.nii.gz"), + "label": os.path.join(self.dataset_dir, f"label_{i}.nii.gz"), + } + for i in range(train_size) + ] + val_datalist = [ + { + "image": os.path.join(self.dataset_dir, f"image_{i}.nii.gz"), + "label": os.path.join(self.dataset_dir, f"label_{i}.nii.gz"), + } + for i in range(train_size, self.dataset_size) + ] + override["train_datalist"] = train_datalist + override["val_datalist"] = val_datalist + + bundle_root = override["bundle_root"] + sys.path = [bundle_root] + sys.path + trainer = ConfigWorkflow( + workflow_type="train", + config_file=[ + os.path.join(bundle_root, "configs/train.json"), + os.path.join(bundle_root, "configs/train_continual.json"), + ], + logging_file=os.path.join(bundle_root, "configs/logging.conf"), + meta_file=os.path.join(bundle_root, "configs/metadata.json"), + **override, + ) + check_workflow(trainer, check_properties=False) + + @parameterized.expand( + [ + TEST_CASE_INFER, + TEST_CASE_INFER_MULTI_PROMPT, + TEST_CASE_INFER_NO_PROMPT, + TEST_CASE_INFER_SUBCLASS, + TEST_CASE_INFER_STR_PROMPT, + TEST_CASE_INFER_MULTI_STR_PROMPT, + TEST_CASE_INFER_MULTI_NEW_STR_PROMPT, + ] + ) + def test_infer_config(self, override): + # update input_dict with dataset dir + input_dict = override["input_dict"] + input_dict["image"] = os.path.join(self.dataset_dir, "image_0.nii.gz") + override["input_dict"] = input_dict + + bundle_root = override["bundle_root"] + sys.path = [bundle_root] + sys.path + + inferrer = ConfigWorkflow( + workflow_type="infer", + config_file=os.path.join(bundle_root, "configs/inference.json"), + logging_file=os.path.join(bundle_root, "configs/logging.conf"), + meta_file=os.path.join(bundle_root, "configs/metadata.json"), + **override, + ) + # check_properties=False because this bundle does not have some required properties such as dataset_dir + check_workflow(inferrer, check_properties=False) + + @parameterized.expand( + [TEST_CASE_INFER, TEST_CASE_INFER_MULTI_PROMPT, TEST_CASE_INFER_NO_PROMPT, TEST_CASE_INFER_SUBCLASS] + ) + def test_batch_infer_config(self, override): + # update input_dict with dataset dir + params = override.copy() + params.pop("input_dict", None) + params["input_dir"] = self.dataset_dir + params["input_suffix"] = "image_*.nii.gz" + + bundle_root = override["bundle_root"] + sys.path = [bundle_root] + sys.path + config_files = [ + os.path.join(bundle_root, "configs/inference.json"), + os.path.join(bundle_root, "configs/batch_inference.json"), + ] + inferrer = ConfigWorkflow( + workflow_type="infer", + config_file=config_files, + logging_file=os.path.join(bundle_root, "configs/logging.conf"), + meta_file=os.path.join(bundle_root, "configs/metadata.json"), + **params, + ) + # check_properties=False because this bundle does not have some required properties such as dataset_dir + check_workflow(inferrer, check_properties=False) + + @parameterized.expand(TEST_CASE_ERROR_PROMPTS) + def test_error_prompt_infer_config(self, override): + # update input_dict with dataset dir + input_dict = override["input_dict"] + input_dict["image"] = os.path.join(self.dataset_dir, "image_0.nii.gz") + override["input_dict"] = input_dict + + bundle_root = override["bundle_root"] + sys.path = [bundle_root] + sys.path + + inferrer = ConfigWorkflow( + workflow_type="infer", + config_file=os.path.join(bundle_root, "configs/inference.json"), + logging_file=os.path.join(bundle_root, "configs/logging.conf"), + meta_file=os.path.join(bundle_root, "configs/metadata.json"), + **override, + ) + inferrer.initialize() + with self.assertRaises(RuntimeError) as context: + inferrer.run() + runtime_error = context.exception + original_exception = runtime_error.__cause__ + self.assertEqual(str(original_exception), override["error"]) + + +if __name__ == "__main__": + loader = unittest.TestLoader() + loader.sortTestMethodsUsing = test_order + unittest.main(testLoader=loader) diff --git a/ci/unit_tests/test_vista3d_mgpu.py b/ci/unit_tests/test_vista3d_mgpu.py new file mode 100644 index 00000000..7d125e21 --- /dev/null +++ b/ci/unit_tests/test_vista3d_mgpu.py @@ -0,0 +1,179 @@ +# Copyright (c) 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. + +import os +import shutil +import sys +import tempfile +import unittest + +import nibabel as nib +import numpy as np +import torch +from parameterized import parameterized +from utils import export_config_and_run_mgpu_cmd + +TEST_CASE_TRAIN_MGPU = [{"bundle_root": "models/vista3d", "patch_size": [32, 32, 32], "epochs": 2, "val_interval": 1}] + +TEST_CASE_EVAL_MGPU = [{"bundle_root": "models/vista3d", "patch_size": [32, 32, 32]}] + +TEST_CASE_TRAIN_CONTINUAL = [ + {"bundle_root": "models/vista3d", "patch_size": [32, 32, 32], "epochs": 2, "val_interval": 1, "finetune": False} +] + + +def test_order(test_name1, test_name2): + def get_order(name): + if "train_mgpu" in name: + return 1 + if "train_continual" in name: + return 2 + if "eval" in name: + return 3 + return 4 + + return get_order(test_name1) - get_order(test_name2) + + +class TestVista3d(unittest.TestCase): + def setUp(self): + self.dataset_dir = tempfile.mkdtemp() + self.dataset_size = 5 + input_shape = (64, 64, 64) + for s in range(self.dataset_size): + test_image = np.random.randint(low=0, high=2, size=input_shape).astype(np.int8) + test_label = np.random.randint(low=0, high=2, size=input_shape).astype(np.int8) + image_filename = os.path.join(self.dataset_dir, f"image_{s}.nii.gz") + label_filename = os.path.join(self.dataset_dir, f"label_{s}.nii.gz") + nib.save(nib.Nifti1Image(test_image, np.eye(4)), image_filename) + nib.save(nib.Nifti1Image(test_label, np.eye(4)), label_filename) + + def tearDown(self): + shutil.rmtree(self.dataset_dir) + + @parameterized.expand([TEST_CASE_TRAIN_MGPU]) + def test_train_mgpu_config(self, override): + train_size = self.dataset_size // 2 + train_datalist = [ + { + "image": os.path.join(self.dataset_dir, f"image_{i}.nii.gz"), + "label": os.path.join(self.dataset_dir, f"label_{i}.nii.gz"), + } + for i in range(train_size) + ] + val_datalist = [ + { + "image": os.path.join(self.dataset_dir, f"image_{i}.nii.gz"), + "label": os.path.join(self.dataset_dir, f"label_{i}.nii.gz"), + } + for i in range(train_size, self.dataset_size) + ] + override["train_datalist"] = train_datalist + override["val_datalist"] = val_datalist + + bundle_root = override["bundle_root"] + sys.path = [bundle_root] + sys.path + train_file = os.path.join(bundle_root, "configs/train.json") + mgpu_train_file = os.path.join(bundle_root, "configs/multi_gpu_train.json") + output_path = os.path.join(bundle_root, "configs/train_override.json") + n_gpu = torch.cuda.device_count() + export_config_and_run_mgpu_cmd( + config_file=[train_file, mgpu_train_file], + logging_file=os.path.join(bundle_root, "configs/logging.conf"), + meta_file=os.path.join(bundle_root, "configs/metadata.json"), + override_dict=override, + output_path=output_path, + ngpu=n_gpu, + ) + + @parameterized.expand([TEST_CASE_EVAL_MGPU]) + def test_eval_mgpu_config(self, override): + train_size = self.dataset_size // 2 + train_datalist = [ + { + "image": os.path.join(self.dataset_dir, f"image_{i}.nii.gz"), + "label": os.path.join(self.dataset_dir, f"label_{i}.nii.gz"), + } + for i in range(train_size) + ] + val_datalist = [ + { + "image": os.path.join(self.dataset_dir, f"image_{i}.nii.gz"), + "label": os.path.join(self.dataset_dir, f"label_{i}.nii.gz"), + } + for i in range(train_size, self.dataset_size) + ] + override["train_datalist"] = train_datalist + override["val_datalist"] = val_datalist + + bundle_root = override["bundle_root"] + sys.path = [bundle_root] + sys.path + config_files = [ + os.path.join(bundle_root, "configs/train.json"), + os.path.join(bundle_root, "configs/train_continual.json"), + os.path.join(bundle_root, "configs/evaluate.json"), + os.path.join(bundle_root, "configs/mgpu_evaluate.json"), + os.path.join(bundle_root, "configs/data.yaml"), + ] + output_path = os.path.join(bundle_root, "configs/evaluate_override.json") + n_gpu = torch.cuda.device_count() + export_config_and_run_mgpu_cmd( + config_file=config_files, + logging_file=os.path.join(bundle_root, "configs/logging.conf"), + meta_file=os.path.join(bundle_root, "configs/metadata.json"), + override_dict=override, + output_path=output_path, + ngpu=n_gpu, + ) + + @parameterized.expand([TEST_CASE_TRAIN_CONTINUAL]) + def test_train_continual_config(self, override): + train_size = self.dataset_size // 2 + train_datalist = [ + { + "image": os.path.join(self.dataset_dir, f"image_{i}.nii.gz"), + "label": os.path.join(self.dataset_dir, f"label_{i}.nii.gz"), + } + for i in range(train_size) + ] + val_datalist = [ + { + "image": os.path.join(self.dataset_dir, f"image_{i}.nii.gz"), + "label": os.path.join(self.dataset_dir, f"label_{i}.nii.gz"), + } + for i in range(train_size, self.dataset_size) + ] + override["train_datalist"] = train_datalist + override["val_datalist"] = val_datalist + + bundle_root = override["bundle_root"] + sys.path = [bundle_root] + sys.path + config_files = [ + os.path.join(bundle_root, "configs/train.json"), + os.path.join(bundle_root, "configs/train_continual.json"), + os.path.join(bundle_root, "configs/multi_gpu_train.json"), + ] + output_path = os.path.join(bundle_root, "configs/train_continual_override.json") + n_gpu = torch.cuda.device_count() + export_config_and_run_mgpu_cmd( + config_file=config_files, + logging_file=os.path.join(bundle_root, "configs/logging.conf"), + meta_file=os.path.join(bundle_root, "configs/metadata.json"), + override_dict=override, + output_path=output_path, + ngpu=n_gpu, + ) + + +if __name__ == "__main__": + loader = unittest.TestLoader() + loader.sortTestMethodsUsing = test_order + unittest.main(testLoader=loader) diff --git a/ci/unit_tests/utils.py b/ci/unit_tests/utils.py index ebd1137a..7fda766d 100644 --- a/ci/unit_tests/utils.py +++ b/ci/unit_tests/utils.py @@ -20,7 +20,6 @@ def export_overrided_config(config_file, override_dict, output_path): parser = ConfigParser() parser.read_config(config_file) parser.update(pairs=override_dict) - ConfigParser.export_config_file(parser.config, output_path, indent=4) diff --git a/models/model_info.json b/models/model_info.json index 00a4ebd0..9bbbbe53 100644 --- a/models/model_info.json +++ b/models/model_info.json @@ -1522,5 +1522,9 @@ "spleen_ct_segmentation_v0.5.8": { "checksum": "3e43f860ba08cb3d121d521cadde0e5b59f72526", "source": "https://api.ngc.nvidia.com/v2/models/nvidia/monaihosting/spleen_ct_segmentation/versions/0.5.8/files/spleen_ct_segmentation_v0.5.8.zip" + }, + "vista3d_v0.4.1": { + "checksum": "b7d9f7be09dcaa62a06a532feda50fea16cc7958", + "source": "https://api.ngc.nvidia.com/v2/models/nvidia/monaihosting/vista3d/versions/0.4.1/files/vista3d_v0.4.1.zip" } }