Repository: mlcommons/GaNDLF

Issue:
Add more details to documentation, specifically related to training customization
**Is your feature request related to a problem? Please describe.**
Currently, anyone interested in customizing their training will need to look into the [`config_all_options.yaml`](https://github.com/mlcommons/GaNDLF/blob/master/samples/config_all_options.yaml) file for details, which can be cumbersome.

**Describe the solution you'd like**
Add details from this file in the usage document.

**Describe alternatives you've considered**
N.A.

**Additional context**
N.A.


Discussion:

Resolving patch:
diff --git a/GANDLF/data/augmentation/wrap_torchio.py b/GANDLF/data/augmentation/wrap_torchio.py
--- a/GANDLF/data/augmentation/wrap_torchio.py
+++ b/GANDLF/data/augmentation/wrap_torchio.py
@@ -50,22 +50,30 @@ def affine(parameters):
 
 def elastic(parameters):
     # define defaults
-    num_controls = 7
-    max_displacement = 7.5
-    if parameters["patch_size"] is not None:
+    parameters["num_control_points"] = parameters.get("num_control_points", None)
+    parameters["max_displacement"] = parameters.get("max_displacement", None)
+    parameters["locked_borders"] = parameters.get("locked_borders", 2)
+    assert (
+        "patch_size" in parameters
+    ), "'patch_size' must be defined for elastic deformation"
+
+    if parameters["num_control_points"] is None:
         # define the control points and swap axes for augmentation
-        num_controls = []
+        parameters["num_control_points"] = []
         for _, n in enumerate(parameters["patch_size"]):
-            num_controls.append(max(n, 5))  # always at least have 5
-        max_displacement = np.divide(num_controls, 10)
-        if num_controls[-1] == 1:
+            parameters["num_control_points"].append(max(n, 5))  # always at least have 5
+
+    if parameters["max_displacement"] is None:
+        parameters["max_displacement"] = np.divide(parameters["num_control_points"], 10)
+        if parameters["num_control_points"][-1] == 1:
             # ensure maximum displacement is never greater than patch size
-            max_displacement[-1] = 0.1
-        max_displacement = max_displacement.tolist()
+            parameters["max_displacement"][-1] = 0.1
+        parameters["max_displacement"] = parameters["max_displacement"].tolist()
 
     return RandomElasticDeformation(
-        num_control_points=num_controls,
-        max_displacement=max_displacement,
+        num_control_points=parameters["num_control_points"],
+        max_displacement=parameters["max_displacement"],
+        locked_borders=parameters["locked_borders"],
         p=parameters["probability"],
     )
 
diff --git a/GANDLF/metrics/__init__.py b/GANDLF/metrics/__init__.py
--- a/GANDLF/metrics/__init__.py
+++ b/GANDLF/metrics/__init__.py
@@ -24,6 +24,7 @@
     "mse": MSE_loss,
     "hd95": hd95,
     "hd95_per_label": hd95_per_label,
+    "hausdorff95_per_label": hd95_per_label,
     "hausdorff95": hd95,
     "hd100": hd100,
     "hd100_per_label": hd100_per_label,
diff --git a/GANDLF/parseConfig.py b/GANDLF/parseConfig.py
--- a/GANDLF/parseConfig.py
+++ b/GANDLF/parseConfig.py
@@ -113,15 +113,13 @@ def parseConfig(config_file_path, version_check_flag=True):
             params = yaml.safe_load(f)
 
     if version_check_flag:  # this is only to be used for testing
-        if not ("version" in params):
-            sys.exit(
-                "The 'version' key needs to be defined in config with 'minimum' and 'maximum' fields to determine the compatibility of configuration with code base"
-            )
-        else:
-            version_check(
-                params["version"],
-                version_to_check=pkg_resources.require("GANDLF")[0].version,
-            )
+        assert (
+            "version" in params
+        ), "The 'version' key needs to be defined in config with 'minimum' and 'maximum' fields to determine the compatibility of configuration with code base"
+        version_check(
+            params["version"],
+            version_to_check=pkg_resources.require("GANDLF")[0].version,
+        )
 
     if "patch_size" in params:
         # duplicate patch size if it is an int or float
@@ -145,10 +143,7 @@ def parseConfig(config_file_path, version_check_flag=True):
         elif len(params["patch_size"]) == 3:  # 2d check
             if "dimension" not in params["model"]:
                 params["model"]["dimension"] = 3
-    else:
-        sys.exit(
-            "The 'patch_size' parameter needs to be present in the configuration file"
-        )
+    assert "patch_size" in params, "Patch size needs to be defined in the config file"
 
     if "resize" in params:
         print(
@@ -156,17 +151,17 @@ def parseConfig(config_file_path, version_check_flag=True):
             file=sys.stderr,
         )
 
-    if "modality" in params:
-        modality = str(params["modality"])
-        if modality.lower() == "rad":
-            pass
-        elif modality.lower() in ["path", "histo"]:
-            pass
-        else:
-            sys.exit(
-                "The 'modality' should be set to either 'rad' or 'path'. Please check for spelling errors and it should be set to either of the two given options."
-            )
-
+    assert "modality" in params, "'modality' needs to be defined in the config file"
+    params["modality"] = params["modality"].lower()
+    assert params["modality"] in [
+        "rad",
+        "histo",
+        "path",
+    ], "Modality should be either 'rad' or 'path'"
+
+    assert (
+        "loss_function" in params
+    ), "'loss_function' needs to be defined in the config file"
     if "loss_function" in params:
         # check if user has passed a dict
         if isinstance(params["loss_function"], dict):  # if this is a dict
@@ -188,6 +183,7 @@ def parseConfig(config_file_path, version_check_flag=True):
                 params["loss_function"]["mse"] = {}
                 params["loss_function"]["mse"]["reduction"] = "mean"
 
+    assert "metrics" in params, "'metrics' needs to be defined in the config file"
     if "metrics" in params:
         if not isinstance(params["metrics"], dict):
             temp_dict = {}
@@ -269,9 +265,6 @@ def parseConfig(config_file_path, version_check_flag=True):
 
         params["metrics"] = temp_dict
 
-    else:
-        sys.exit("The key 'metrics' needs to be defined")
-
     # this is NOT a required parameter - a user should be able to train with NO augmentations
     params = initialize_key(params, "data_augmentation", {})
     if not (params["data_augmentation"] is None):
@@ -497,12 +490,21 @@ def parseConfig(config_file_path, version_check_flag=True):
             params["data_postprocessing"].pop(key)
 
     if "model" in params:
-        if not (isinstance(params["model"], dict)):
-            sys.exit("The 'model' parameter needs to be populated as a dictionary")
-        elif len(params["model"]) == 0:  # only proceed if something is defined
-            sys.exit(
-                "The 'model' parameter needs to be populated as a dictionary and should have all properties present"
-            )
+        assert isinstance(
+            params["model"], dict
+        ), "The 'model' parameter needs to be populated as a dictionary"
+        assert (
+            len(params["model"]) > 0
+        ), "The 'model' parameter needs to be populated as a dictionary and should have all properties present"
+        assert (
+            "architecture" in params["model"]
+        ), "The 'model' parameter needs 'architecture' to be defined"
+        assert (
+            "final_layer" in params["model"]
+        ), "The 'model' parameter needs 'final_layer' to be defined"
+        assert (
+            "dimension" in params["model"]
+        ), "The 'model' parameter needs 'dimension' to be defined"
 
         if "amp" in params["model"]:
             pass
@@ -523,14 +525,6 @@ def parseConfig(config_file_path, version_check_flag=True):
             print("WARNING: Initializing 'norm_type' as 'batch'", flush=True)
             params["model"]["norm_type"] = "batch"
 
-        if not ("architecture" in params["model"]):
-            sys.exit("The 'model' parameter needs 'architecture' key to be defined")
-        if not ("final_layer" in params["model"]):
-            sys.exit("The 'model' parameter needs 'final_layer' key to be defined")
-        if not ("dimension" in params["model"]):
-            sys.exit(
-                "The 'model' parameter needs 'dimension' key to be defined, which should either 2 or 3"
-            )
         if not ("base_filters" in params["model"]):
             base_filters = 32
             params["model"]["base_filters"] = base_filters
@@ -569,9 +563,6 @@ def parseConfig(config_file_path, version_check_flag=True):
                 "WARNING: 'save_at_every_epoch' will result in TREMENDOUS storage usage; use at your own risk."
             )
 
-    else:
-        sys.exit("The 'model' parameter needs to be populated as a dictionary")
-
     if isinstance(params["model"]["class_list"], str):
         if ("||" in params["model"]["class_list"]) or (
             "&&" in params["model"]["class_list"]
@@ -593,28 +584,14 @@ def parseConfig(config_file_path, version_check_flag=True):
             except AssertionError:
                 raise AssertionError("Could not evaluate the 'class_list' in 'model'")
 
-    if "kcross_validation" in params:
-        sys.exit(
-            "'kcross_validation' is no longer used, please use 'nested_training' instead"
-        )
-
-    if not ("nested_training" in params):
-        sys.exit("The parameter 'nested_training' needs to be defined")
-    if not ("testing" in params["nested_training"]):
-        if not ("holdout" in params["nested_training"]):
-            kfolds = -5
-            print("Using default folds for testing split: ", kfolds)
-        else:
-            print(
-                "WARNING: 'holdout' should not be defined under 'nested_training', please use 'testing' instead;",
-                file=sys.stderr,
-            )
-            kfolds = params["nested_training"]["holdout"]
-        params["nested_training"]["testing"] = kfolds
-    if not ("validation" in params["nested_training"]):
-        kfolds = -5
-        print("Using default folds for validation split: ", kfolds)
-        params["nested_training"]["validation"] = kfolds
+    assert (
+        "nested_training" in params
+    ), "The parameter 'nested_training' needs to be defined"
+    # initialize defaults for nested training
+    params["nested_training"]["testing"] = params["nested_training"].get("testing", -5)
+    params["nested_training"]["validation"] = params["nested_training"].get(
+        "validation", -5
+    )
 
     parallel_compute_command = ""
     if "parallel_compute_command" in params:


Test patch:
diff --git a/testing/config_classification.yaml b/testing/config_classification.yaml
--- a/testing/config_classification.yaml
+++ b/testing/config_classification.yaml
@@ -23,6 +23,7 @@ metrics:
       reduction: sum,
     }
     
+modality: rad
 model:
   amp: false
   architecture: vgg16
diff --git a/testing/config_regression.yaml b/testing/config_regression.yaml
--- a/testing/config_regression.yaml
+++ b/testing/config_regression.yaml
@@ -8,6 +8,7 @@ learning_rate: 0.001
 loss_function: mse
 metrics:
   - mse
+modality: rad
 model:
   amp: false
   architecture: densenet121
diff --git a/testing/config_segmentation.yaml b/testing/config_segmentation.yaml
--- a/testing/config_segmentation.yaml
+++ b/testing/config_segmentation.yaml
@@ -29,6 +29,7 @@ inference_mechanism: {
   grid_aggregator_overlap: average,
   patch_overlap: 0,
 }
+modality: rad
 # Patch size during training - 2D patch for breast images since third dimension is not patched 
 patch_size: [128,128]
 # Number of epochs
diff --git a/testing/test_full.py b/testing/test_full.py
--- a/testing/test_full.py
+++ b/testing/test_full.py
@@ -1651,12 +1651,18 @@ def test_generic_augmentation_functions():
         aug_lower = aug.lower()
         output_tensor = None
         if aug_lower in global_augs_dict:
-            print(aug_lower)
             output_tensor = global_augs_dict[aug](
                 params_all_preprocessing_and_augs["data_augmentation"][aug_lower]
             )(input_tensor)
             assert output_tensor != None, "Augmentation should work"
 
+    # additional test for elastic
+    params_elastic = params_all_preprocessing_and_augs["data_augmentation"]["elastic"]
+    for key_to_pop in ["num_control_points", "max_displacement", "locked_borders"]:
+        params_elastic.pop(key_to_pop, None)
+    output_tensor = global_augs_dict["elastic"](params_elastic)(input_tensor)
+    assert output_tensor != None, "Augmentation for base elastic transform should work"
+
     print("passed")