Repository: DARMA-tasking/LB-analysis-framework

Issue:
Accept range in phase indices in lbaf config
e.g. if we want to simulate 100 phases we would want to be able to have something like:

```yml
from_data:
  phase_ids:
    - [0,100]
```

@ppebay mentioned I should file an issue for this


Discussion:

Patch:
diff --git a/src/lbaf/Applications/LBAF_app.py b/src/lbaf/Applications/LBAF_app.py
index e971488..78fcca2 100644
--- a/src/lbaf/Applications/LBAF_app.py
+++ b/src/lbaf/Applications/LBAF_app.py
@@ -112,7 +112,11 @@ class internalParameters:
         # Parse data parameters if present
         if self.configuration.get("from_data") is not None:
             self.data_stem = self.configuration.get("from_data").get("data_stem")
-            self.phase_ids = self.configuration.get("from_data").get("phase_ids")
+            if isinstance(self.configuration.get("from_data").get("phase_ids"), str):
+                range_list = list(map(int, self.configuration.get("from_data").get("phase_ids").split('-')))
+                self.phase_ids = list(range(range_list[0], range_list[1] + 1))
+            else:
+                self.phase_ids = self.configuration.get("from_data").get("phase_ids")
 
         # Parse sampling parameters if present
         if self.configuration.get("from_samplers") is not None:
diff --git a/src/lbaf/Applications/conf.yaml b/src/lbaf/Applications/conf.yaml
index 21fbeca..e0053aa 100644
--- a/src/lbaf/Applications/conf.yaml
+++ b/src/lbaf/Applications/conf.yaml
@@ -25,7 +25,7 @@
 # y_procs [int]                number of procs in y direction for rank visualization
 # z_procs [int]                number of procs in z direction for rank visualization
 # data_stem [str]              base file name of VT load logs
-# phase_ids [list]             list of ids of phase to be read in VT load logs
+# phase_ids [list or str]      list of ids of phase to be read in VT load logs e.g. [1, 2, 3] or "1-3"
 # map_file [str]               base file name for VT object/proc mapping
 # file_suffix [str]            file suffix of VT data files (default: "json")
 # output_dir [str]             output directory (default: '.')
diff --git a/src/lbaf/IO/configurationValidator.py b/src/lbaf/IO/configurationValidator.py
index e350036..443f02b 100644
--- a/src/lbaf/IO/configurationValidator.py
+++ b/src/lbaf/IO/configurationValidator.py
@@ -2,7 +2,7 @@ from collections import Iterable
 from logging import Logger
 import sys
 
-from schema import And, Optional, Or, Schema, Use
+from schema import And, Optional, Or, Regex, Schema, Use
 from ..Utils.exception_handler import exc_handler
 
 
@@ -89,8 +89,11 @@ class ConfigurationValidator:
         })
         self.__from_data = Schema(
             {"data_stem": str,
-             "phase_ids": And(list, lambda x: all([isinstance(y, int) for y in x]),
-                              error="Should be of type 'list' of 'int' types")})
+             "phase_ids": Or(
+                 And(list, lambda x: all([isinstance(y, int) for y in x]),
+                     error="Should be of type 'list' of 'int' types"),
+                 Regex(r"^[0-9]+-[0-9]+$", error="Should be of type 'str' like '0-100'"))
+             })
         self.__from_samplers = Schema({
             "n_objects": And(int, lambda x: x > 0,
                              error="Should be of type 'int' and > 0"),


Test patch:
diff --git a/tests/data/config/conf_correct_phase_ids_str_001.yml b/tests/data/config/conf_correct_phase_ids_str_001.yml
new file mode 100644
index 0000000..6252110
--- /dev/null
+++ b/tests/data/config/conf_correct_phase_ids_str_001.yml
@@ -0,0 +1,36 @@
+# Specify input
+from_data:
+  data_stem: "../data/synthetic_lb_data/data"
+  phase_ids: "0-3"
+
+# Specify work model
+work_model:
+  name: AffineCombination
+  parameters:
+    alpha: 1.
+    beta: 0.
+    gamma: 0.
+
+# Specify balancing algorithm
+algorithm:
+  name: InformAndTransfer
+  parameters:
+    n_iterations: 8
+    n_rounds: 4
+    fanout: 4
+    order_strategy: element_id
+    criterion: Tempered
+    max_objects_per_transfer: 8
+    deterministic_transfer: True
+
+# Specify output
+n_ranks: 4
+terminal_background: light
+generate_multimedia: False
+output_dir: ../../../output
+output_file_stem: output_file
+generate_meshes:
+  x_ranks: 2
+  y_ranks: 2
+  z_ranks: 1
+  object_jitter: 0.5
diff --git a/tests/data/config/conf_wrong_phase_ids_str_001.yml b/tests/data/config/conf_wrong_phase_ids_str_001.yml
new file mode 100644
index 0000000..54a3edd
--- /dev/null
+++ b/tests/data/config/conf_wrong_phase_ids_str_001.yml
@@ -0,0 +1,36 @@
+# Specify input
+from_data:
+  data_stem: "../data/synthetic_lb_data/data"
+  phase_ids: "0-3r"
+
+# Specify work model
+work_model:
+  name: AffineCombination
+  parameters:
+    alpha: 1.
+    beta: 0.
+    gamma: 0.
+
+# Specify balancing algorithm
+algorithm:
+  name: InformAndTransfer
+  parameters:
+    n_iterations: 8
+    n_rounds: 4
+    fanout: 4
+    order_strategy: element_id
+    criterion: Tempered
+    max_objects_per_transfer: 8
+    deterministic_transfer: True
+
+# Specify output
+n_ranks: 4
+terminal_background: light
+generate_multimedia: False
+output_dir: ../../../output
+output_file_stem: output_file
+generate_meshes:
+  x_ranks: 2
+  y_ranks: 2
+  z_ranks: 1
+  object_jitter: 0.5
diff --git a/tests/test_configuration_validator.py b/tests/test_configuration_validator.py
index 53cd464..6b38644 100644
--- a/tests/test_configuration_validator.py
+++ b/tests/test_configuration_validator.py
@@ -78,7 +78,7 @@ class TestConfig(unittest.TestCase):
 
         with self.assertRaises(SchemaError) as err:
             ConfigurationValidator(config_to_validate=configuration, logger=logger()).main()
-        self.assertEqual(err.exception.args[0], "Should be of type 'list' of 'int' types")
+        self.assertEqual(err.exception.args[0], "Should be of type 'list' of 'int' types\nShould be of type 'str' like '0-100'")
 
     def test_config_validator_wrong_from_data_phase_name(self):
         with open(os.path.join(self.config_dir, 'conf_wrong_from_data_phase_name.yml'), 'rt') as config_file:
@@ -203,6 +203,20 @@ class TestConfig(unittest.TestCase):
             ConfigurationValidator(config_to_validate=configuration, logger=logger()).main()
         self.assertEqual(err.exception.args[0], "Key 'parameters' error:\nMissing key: 'fanout'")
 
+    def test_config_validator_correct_phase_ids_str_001(self):
+        with open(os.path.join(self.config_dir, 'conf_correct_phase_ids_str_001.yml'), 'rt') as config_file:
+            yaml_str = config_file.read()
+            configuration = yaml.safe_load(yaml_str)
+        ConfigurationValidator(config_to_validate=configuration, logger=logger()).main()
+
+    def test_config_validator_wrong_phase_ids_str_001(self):
+        with open(os.path.join(self.config_dir, 'conf_wrong_phase_ids_str_001.yml'), 'rt') as config_file:
+            yaml_str = config_file.read()
+            configuration = yaml.safe_load(yaml_str)
+        with self.assertRaises(SchemaError) as err:
+            ConfigurationValidator(config_to_validate=configuration, logger=logger()).main()
+        self.assertEqual(err.exception.args[0], "Should be of type 'list' of 'int' types\nShould be of type 'str' like '0-100'")
+
 
 if __name__ == '__main__':
     unittest.main()