idmtools 0.0.0.dev0__py3-none-any.whl → 0.0.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. idmtools/__init__.py +27 -8
  2. idmtools/analysis/__init__.py +5 -0
  3. idmtools/analysis/add_analyzer.py +89 -0
  4. idmtools/analysis/analyze_manager.py +490 -0
  5. idmtools/analysis/csv_analyzer.py +103 -0
  6. idmtools/analysis/download_analyzer.py +96 -0
  7. idmtools/analysis/map_worker_entry.py +100 -0
  8. idmtools/analysis/platform_analysis_bootstrap.py +94 -0
  9. idmtools/analysis/platform_anaylsis.py +291 -0
  10. idmtools/analysis/tags_analyzer.py +93 -0
  11. idmtools/assets/__init__.py +9 -0
  12. idmtools/assets/asset.py +453 -0
  13. idmtools/assets/asset_collection.py +514 -0
  14. idmtools/assets/content_handlers.py +19 -0
  15. idmtools/assets/errors.py +23 -0
  16. idmtools/assets/file_list.py +191 -0
  17. idmtools/builders/__init__.py +11 -0
  18. idmtools/builders/arm_simulation_builder.py +152 -0
  19. idmtools/builders/csv_simulation_builder.py +76 -0
  20. idmtools/builders/simulation_builder.py +348 -0
  21. idmtools/builders/sweep_arm.py +109 -0
  22. idmtools/builders/yaml_simulation_builder.py +82 -0
  23. idmtools/config/__init__.py +7 -0
  24. idmtools/config/idm_config_parser.py +486 -0
  25. idmtools/core/__init__.py +10 -0
  26. idmtools/core/cache_enabled.py +114 -0
  27. idmtools/core/context.py +68 -0
  28. idmtools/core/docker_task.py +207 -0
  29. idmtools/core/enums.py +51 -0
  30. idmtools/core/exceptions.py +91 -0
  31. idmtools/core/experiment_factory.py +71 -0
  32. idmtools/core/id_file.py +70 -0
  33. idmtools/core/interfaces/__init__.py +5 -0
  34. idmtools/core/interfaces/entity_container.py +64 -0
  35. idmtools/core/interfaces/iassets_enabled.py +58 -0
  36. idmtools/core/interfaces/ientity.py +331 -0
  37. idmtools/core/interfaces/iitem.py +206 -0
  38. idmtools/core/interfaces/imetadata_operations.py +89 -0
  39. idmtools/core/interfaces/inamed_entity.py +17 -0
  40. idmtools/core/interfaces/irunnable_entity.py +159 -0
  41. idmtools/core/logging.py +387 -0
  42. idmtools/core/platform_factory.py +316 -0
  43. idmtools/core/system_information.py +104 -0
  44. idmtools/core/task_factory.py +145 -0
  45. idmtools/entities/__init__.py +10 -0
  46. idmtools/entities/command_line.py +229 -0
  47. idmtools/entities/command_task.py +155 -0
  48. idmtools/entities/experiment.py +787 -0
  49. idmtools/entities/generic_workitem.py +43 -0
  50. idmtools/entities/ianalyzer.py +163 -0
  51. idmtools/entities/iplatform.py +1106 -0
  52. idmtools/entities/iplatform_default.py +39 -0
  53. idmtools/entities/iplatform_ops/__init__.py +5 -0
  54. idmtools/entities/iplatform_ops/iplatform_asset_collection_operations.py +148 -0
  55. idmtools/entities/iplatform_ops/iplatform_experiment_operations.py +415 -0
  56. idmtools/entities/iplatform_ops/iplatform_simulation_operations.py +315 -0
  57. idmtools/entities/iplatform_ops/iplatform_suite_operations.py +322 -0
  58. idmtools/entities/iplatform_ops/iplatform_workflowitem_operations.py +301 -0
  59. idmtools/entities/iplatform_ops/utils.py +185 -0
  60. idmtools/entities/itask.py +316 -0
  61. idmtools/entities/iworkflow_item.py +167 -0
  62. idmtools/entities/platform_requirements.py +20 -0
  63. idmtools/entities/relation_type.py +14 -0
  64. idmtools/entities/simulation.py +255 -0
  65. idmtools/entities/suite.py +188 -0
  66. idmtools/entities/task_proxy.py +37 -0
  67. idmtools/entities/templated_simulation.py +325 -0
  68. idmtools/frozen/frozen_dict.py +71 -0
  69. idmtools/frozen/frozen_list.py +66 -0
  70. idmtools/frozen/frozen_set.py +86 -0
  71. idmtools/frozen/frozen_tuple.py +18 -0
  72. idmtools/frozen/frozen_utils.py +179 -0
  73. idmtools/frozen/ifrozen.py +66 -0
  74. idmtools/plugins/__init__.py +5 -0
  75. idmtools/plugins/git_commit.py +117 -0
  76. idmtools/registry/__init__.py +4 -0
  77. idmtools/registry/experiment_specification.py +105 -0
  78. idmtools/registry/functions.py +28 -0
  79. idmtools/registry/hook_specs.py +132 -0
  80. idmtools/registry/master_plugin_registry.py +51 -0
  81. idmtools/registry/platform_specification.py +138 -0
  82. idmtools/registry/plugin_specification.py +129 -0
  83. idmtools/registry/task_specification.py +104 -0
  84. idmtools/registry/utils.py +119 -0
  85. idmtools/services/__init__.py +5 -0
  86. idmtools/services/ipersistance_service.py +135 -0
  87. idmtools/services/platforms.py +13 -0
  88. idmtools/utils/__init__.py +5 -0
  89. idmtools/utils/caller.py +24 -0
  90. idmtools/utils/collections.py +246 -0
  91. idmtools/utils/command_line.py +45 -0
  92. idmtools/utils/decorators.py +300 -0
  93. idmtools/utils/display/__init__.py +22 -0
  94. idmtools/utils/display/displays.py +181 -0
  95. idmtools/utils/display/settings.py +25 -0
  96. idmtools/utils/dropbox_location.py +30 -0
  97. idmtools/utils/entities.py +127 -0
  98. idmtools/utils/file.py +72 -0
  99. idmtools/utils/file_parser.py +151 -0
  100. idmtools/utils/filter_simulations.py +182 -0
  101. idmtools/utils/filters/__init__.py +5 -0
  102. idmtools/utils/filters/asset_filters.py +88 -0
  103. idmtools/utils/general.py +286 -0
  104. idmtools/utils/gitrepo.py +336 -0
  105. idmtools/utils/hashing.py +239 -0
  106. idmtools/utils/info.py +124 -0
  107. idmtools/utils/json.py +82 -0
  108. idmtools/utils/language.py +107 -0
  109. idmtools/utils/local_os.py +40 -0
  110. idmtools/utils/time.py +22 -0
  111. idmtools-0.0.3.dist-info/METADATA +120 -0
  112. idmtools-0.0.3.dist-info/RECORD +116 -0
  113. idmtools-0.0.3.dist-info/entry_points.txt +9 -0
  114. idmtools-0.0.3.dist-info/licenses/LICENSE.TXT +3 -0
  115. idmtools-0.0.0.dev0.dist-info/METADATA +0 -41
  116. idmtools-0.0.0.dev0.dist-info/RECORD +0 -5
  117. {idmtools-0.0.0.dev0.dist-info → idmtools-0.0.3.dist-info}/WHEEL +0 -0
  118. {idmtools-0.0.0.dev0.dist-info → idmtools-0.0.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,103 @@
1
+ """idmtools CSVAnalyzer.
2
+
3
+ Example of a csv analyzer to concatenate csv results into one csv from your experiment's simulations.
4
+
5
+ Copyright 2021, Bill & Melinda Gates Foundation. All rights reserved.
6
+ """
7
+ import os
8
+ from typing import Dict
9
+ import pandas as pd
10
+ from idmtools.entities import IAnalyzer
11
+ from idmtools.entities.ianalyzer import ANALYSIS_ITEM_MAP_DATA_TYPE, ANALYZABLE_ITEM
12
+
13
+
14
+ class CSVAnalyzer(IAnalyzer):
15
+ """
16
+ Provides an analyzer for CSV output.
17
+
18
+ Examples:
19
+ .. _simple-csv-example:
20
+
21
+ Simple Example
22
+ This example covers the basic usage of the CSVAnalyzer
23
+
24
+ .. literalinclude:: ../../examples/analyzers/example_analysis_CSVAnalyzer.py
25
+
26
+ .. _multiple-csvs:
27
+
28
+ Multiple CSVs
29
+ This example covers analyzing multiple CSVs
30
+
31
+ .. literalinclude:: ../../examples/analyzers/example_analysis_MultiCSVAnalyzer.py
32
+ """
33
+ # Arg option for analyzer init are uid, working_dir, parse (True to leverage the :class:`OutputParser`;
34
+ # False to get the raw data in the :meth:`select_simulation_data`), and filenames
35
+ # In this case, we want parse=True, and the filename(s) to analyze
36
+ def __init__(self, filenames, output_path="output_csv"):
37
+ """
38
+ Initialize our analyzer.
39
+
40
+ Args:
41
+ filenames: Filenames we want to pull
42
+ output_path: Output path to write the csv
43
+ """
44
+ super().__init__(parse=True, filenames=filenames)
45
+ # Raise exception early if files are not csv files
46
+ if not all(['csv' in os.path.splitext(f)[1].lower() for f in self.filenames]):
47
+ raise Exception('Please ensure all filenames provided to CSVAnalyzer have a csv extension.')
48
+
49
+ self.output_path = output_path
50
+
51
+ def initialize(self):
52
+ """
53
+ Initialize on run. Create an output directory.
54
+
55
+ Returns:
56
+ None
57
+ """
58
+ self.output_path = os.path.join(self.working_dir, self.output_path)
59
+
60
+ # Create the output path
61
+ if not os.path.exists(self.output_path):
62
+ os.makedirs(self.output_path)
63
+
64
+ # Map is called to get for each simulation a data object (all the metadata of the simulations) and simulation object
65
+ def map(self, data: ANALYSIS_ITEM_MAP_DATA_TYPE, simulation: ANALYZABLE_ITEM) -> pd.DataFrame:
66
+ """
67
+ Map each simulation/workitem data here.
68
+
69
+ The data is a mapping of files -> content(in this case, dataframes since it is csvs parsed).
70
+
71
+ Args:
72
+ data: Data mapping of files -> content
73
+ simulation: Simulation/Workitem we are mapping
74
+
75
+ Returns:
76
+ Items joined together into a dataframe.
77
+ """
78
+ # If there are 1 to many csv files, concatenate csv data columns into one dataframe
79
+ concatenated_df = pd.concat(list(data.values()), axis=0, ignore_index=True, sort=True)
80
+ return concatenated_df
81
+
82
+ # In reduce, we are printing the simulation and result data filtered in map
83
+ def reduce(self, all_data: Dict[ANALYZABLE_ITEM, pd.DataFrame]):
84
+ """
85
+ Reduce(combine) all the data from our mapping.
86
+
87
+ Args:
88
+ all_data: Mapping of our data in form Item(Simulation/Workitem) -> Mapped dataframe
89
+
90
+ Returns:
91
+ None
92
+ """
93
+ results = pd.concat(list(all_data.values()), axis=0, # Combine a list of all the sims csv data column values
94
+ keys=[str(k.uid) for k in all_data.keys()], # Add a hierarchical index with the keys option
95
+ names=['SimId']) # Label the index keys you create with the names option
96
+ results.index = results.index.droplevel(1) # Remove default index
97
+
98
+ # Make a directory labeled the exp id to write the csv results to
99
+ first_sim = list(all_data.keys())[0] # get first Simulation
100
+ exp_id = first_sim.experiment.id # Set the exp id from the first sim data
101
+ output_folder = os.path.join(self.output_path, exp_id)
102
+ os.makedirs(output_folder, exist_ok=True)
103
+ results.to_csv(os.path.join(output_folder, self.__class__.__name__ + '.csv'))
@@ -0,0 +1,96 @@
1
+ """idmtools Download analyzer.
2
+
3
+ Download Analyzer.
4
+
5
+ Copyright 2021, Bill & Melinda Gates Foundation. All rights reserved.
6
+ """
7
+ import os
8
+ from logging import getLogger
9
+ from idmtools.entities.ianalyzer import IAnalyzer, ANALYSIS_REDUCE_DATA_TYPE, ANALYZABLE_ITEM, ANALYSIS_ITEM_MAP_DATA_TYPE
10
+
11
+ logger = getLogger(__name__)
12
+
13
+
14
+ class DownloadAnalyzer(IAnalyzer):
15
+ """
16
+ A simple base class that will download the files specified in filenames without further treatment.
17
+
18
+ Can be used by creating a child class:
19
+
20
+ .. code-block:: python
21
+
22
+ class InsetDownloader(DownloadAnalyzer):
23
+ filenames = ['output/InsetChart.json']
24
+
25
+ Or by directly calling it:
26
+
27
+ .. code-block:: python
28
+
29
+ analyzer = DownloadAnalyzer(filenames=['output/InsetChart.json'])
30
+
31
+
32
+ Examples:
33
+ .. literalinclude:: ../../examples/analyzers/example_analysis_DownloadAnalyzer.py
34
+ """
35
+
36
+ def reduce(self, all_data: ANALYSIS_REDUCE_DATA_TYPE):
37
+ """
38
+ Combine the :meth:`map` data for a set of items into an aggregate result. In this case, for downloading, we just ignore it because there is no reduction.
39
+
40
+ Args:
41
+ all_data: Dictionary in form item->map result where item is Simulations or WorkItems
42
+
43
+ Returns:
44
+ None
45
+ """
46
+ pass
47
+
48
+ def __init__(self, filenames=None, output_path="output", **kwargs):
49
+ """Constructor of the analyzer."""
50
+ super().__init__(filenames=filenames, parse=False, **kwargs)
51
+ self.output_path = output_path
52
+
53
+ def initialize(self):
54
+ """
55
+ Initialize our sim. In this case, we create our output directory.
56
+
57
+ Returns:
58
+ None
59
+ """
60
+ self.output_path = os.path.join(self.working_dir, self.output_path)
61
+ os.makedirs(self.output_path, exist_ok=True)
62
+
63
+ def get_item_folder(self, item: ANALYZABLE_ITEM):
64
+ """
65
+ Concatenate the specified top-level output folder with the item ID.
66
+
67
+ Args:
68
+ item: A simulation output parsing thread.
69
+
70
+ Returns:
71
+ The name of the folder to download this simulation's output to.
72
+ """
73
+ return os.path.join(self.output_path, str(item.uid))
74
+
75
+ def map(self, data: ANALYSIS_ITEM_MAP_DATA_TYPE, item: ANALYZABLE_ITEM):
76
+ """
77
+ Provide a map of filenames->data for each item. We then download each of these files to our output folder.
78
+
79
+ Args:
80
+ data: Map filenames->data
81
+ item: Item we are mapping.
82
+
83
+ Returns:
84
+ None
85
+ """
86
+ # Create a folder for the current simulation/item
87
+ sim_folder = self.get_item_folder(item)
88
+ os.makedirs(sim_folder, exist_ok=True)
89
+
90
+ # Create the requested files
91
+ for filename in self.filenames:
92
+ file_path = os.path.join(sim_folder, os.path.basename(filename))
93
+
94
+ logger.debug(f'Writing to path: {file_path}')
95
+ with open(file_path, 'wb') as outfile:
96
+ outfile.write(data[filename])
@@ -0,0 +1,100 @@
1
+ """
2
+ We define our map entry items here for analysis framework.
3
+
4
+ Most of these function are used either to initialize a thread or to handle exceptions while executing.
5
+
6
+ Copyright 2021, Bill & Melinda Gates Foundation. All rights reserved.
7
+ """
8
+ import itertools
9
+ from logging import getLogger, DEBUG
10
+ from idmtools.core.interfaces.ientity import IEntity
11
+ from idmtools.utils.file_parser import FileParser
12
+ from typing import TYPE_CHECKING, Dict
13
+ from idmtools.core.interfaces.iitem import IItem
14
+ from idmtools.entities.ianalyzer import TAnalyzerList
15
+ from idmtools.utils.general import FilterSafeItem
16
+
17
+ if TYPE_CHECKING: # pragma: no cover
18
+ from idmtools.entities.iplatform import IPlatform
19
+
20
+ logger = getLogger(__name__)
21
+
22
+
23
+ def map_item(item: IItem) -> Dict[str, Dict]:
24
+ """
25
+ Initialize some worker-global values; a worker process entry point for analyzer item-mapping.
26
+
27
+ Args:
28
+ item: The item (often simulation) to process.
29
+
30
+ Returns:
31
+ Dict[str, Dict]
32
+ """
33
+ # Retrieve the global variables coming from the pool initialization
34
+
35
+ if logger.isEnabledFor(DEBUG):
36
+ logger.debug(f"Init item {item.uid} in worker")
37
+ analyzers = map_item.analyzers
38
+ platform = map_item.platform
39
+ return _get_mapped_data_for_item(item, analyzers, platform)
40
+
41
+
42
+ def _get_mapped_data_for_item(item: IEntity, analyzers: TAnalyzerList, platform: 'IPlatform') -> Dict[str, Dict]:
43
+ """
44
+ Get mapped data from an item.
45
+
46
+ Args:
47
+ item: The :class:`~idmtools.entities.iitem.IItem` object to call analyzer
48
+ :meth:`~idmtools.analysis.AddAnalyzer.map` methods on.
49
+ analyzers: The :class:`~idmtools.analysis.IAnalyzer` items with
50
+ :meth:`~idmtools.analysis.AddAnalyzer.map` methods to call on the provided items.
51
+ platform: A platform object to query for information.
52
+
53
+ Returns:
54
+ Dict[str, Dict] - Array mapping file data to from str to contents
55
+
56
+ """
57
+ try:
58
+ # determine which analyzers (and by extension, which filenames) are applicable to this item
59
+ # ensure item has a platform
60
+ item.platform = platform
61
+ analyzers_to_use = [a for a in analyzers if a.filter(FilterSafeItem(item))]
62
+ analyzer_uids = [a.uid for a in analyzers]
63
+
64
+ filenames = set(itertools.chain(*(a.filenames for a in analyzers_to_use)))
65
+ filenames = [f.replace("\\", '/') for f in filenames]
66
+
67
+ if logger.isEnabledFor(DEBUG):
68
+ logger.debug(f"Analyzers to use on item: {str(analyzer_uids)}")
69
+ logger.debug(f"Filenames to analyze: {filenames}")
70
+
71
+ # The byte_arrays will associate filename with content
72
+ if len(filenames) > 0:
73
+ file_data = platform.get_files(item, filenames)
74
+ else:
75
+ file_data = dict()
76
+
77
+ # Selected data will be a dict with analyzer.uid: data entries
78
+ selected_data = {}
79
+ for analyzer in analyzers_to_use:
80
+ # If the analyzer needs the parsed data, parse
81
+ if analyzer.parse:
82
+ logger.debug(f'Parsing content for {analyzer.uid}')
83
+ data = {filename: FileParser.parse(filename, content) for filename, content in file_data.items() if filename in analyzer.filenames}
84
+ else:
85
+ # If the analyzer doesnt wish to parse, give the raw data
86
+ data = {filename: content for filename, content in file_data.items() if filename in analyzer.filenames}
87
+
88
+ # run the mapping routine for this analyzer and item
89
+ logger.debug("Running map on selected data")
90
+ selected_data[analyzer.uid] = analyzer.map(data, item)
91
+
92
+ # Store all analyzer results for this item in the result cache
93
+ if logger.isEnabledFor(DEBUG):
94
+ logger.debug(f"Setting result to cache on {item.id}")
95
+ logger.debug(f"Wrote Setting result to cache on {item.id}")
96
+ except Exception as e:
97
+ e.item = item
98
+ logger.error(e)
99
+ raise e
100
+ return selected_data
@@ -0,0 +1,94 @@
1
+ """
2
+ This script is executed as entrypoint in the docker SSMT worker.
3
+
4
+ Its role is to collect the experiment ids and analyzers and run the analysis.
5
+
6
+ Copyright 2021, Bill & Melinda Gates Foundation. All rights reserved.
7
+ """
8
+ import argparse
9
+ import os
10
+ import pickle
11
+ import sys
12
+ from logging import getLogger
13
+ from pydoc import locate
14
+
15
+
16
+ sys.path.append(os.path.dirname(__file__))
17
+
18
+ if __name__ == "__main__":
19
+ parser = argparse.ArgumentParser("PlatformAnalysis bootstrap")
20
+ parser.add_argument("--experiment-ids", default=None, help="A comma separated list of experiments to analyze")
21
+ parser.add_argument("--simulation-ids", default=None, help="A comma separated list of simulations to analyze")
22
+ parser.add_argument("--work-item-ids", default=None, help="A comma separated list of work items to analyze")
23
+ parser.add_argument("--analyzers", help="Commas separated list of analyzers")
24
+ parser.add_argument("--block", help="Configuration block to use")
25
+ parser.add_argument("--verbose", default=False, action="store_true", help="Verbose logging")
26
+ parser.add_argument("--pre-run-func", default=None, help="List of function to run before starting analysis. Useful to load packages up in docker container before run")
27
+ parser.add_argument("--analyzer-manager-args-file", default=None, help="Path to extra arguments for analyzer manager")
28
+ parser.add_argument("--platform-args", default=None, help="Arguments used to create Platform")
29
+
30
+ args = parser.parse_args()
31
+ if args.verbose:
32
+ # enable verbose logging before we load idmtools
33
+ os.environ['IDMTOOLS_LOGGING_LEVEL'] = 'DEBUG'
34
+ os.environ['IDMTOOLS_LOGGING_CONSOLE'] = '1'
35
+
36
+ # delay loading idmtools so we can change log level through environment
37
+ from idmtools.core import ItemType
38
+ from idmtools.core.platform_factory import Platform
39
+ from idmtools.analysis.analyze_manager import AnalyzeManager
40
+
41
+ logger = getLogger('SSMT Analysis')
42
+
43
+ if args.pre_run_func:
44
+ import pre_run
45
+ getattr(pre_run, args.pre_run_func)()
46
+
47
+ item_ids = []
48
+ # Get the experiments, analyzers and platform
49
+ if args.experiment_ids:
50
+ experiments = args.experiment_ids.split(",")
51
+ for experiment in experiments:
52
+ experiment_tuple = (experiment, ItemType.EXPERIMENT)
53
+ item_ids.append(experiment_tuple)
54
+
55
+ # Get the simulations, analyzers and platform
56
+ if args.simulation_ids:
57
+ simulations = args.simulation_ids.split(",")
58
+ for simulation in simulations:
59
+ simulation_tuple = (simulation, ItemType.SIMULATION)
60
+ item_ids.append(simulation_tuple)
61
+
62
+ # Get the experiments, analyzers and platform
63
+ if args.work_item_ids:
64
+ work_items = args.work_item_ids.split(",")
65
+ for wi in work_items:
66
+ wi_tuple = (wi, ItemType.WORKFLOW_ITEM)
67
+ item_ids.append(wi_tuple)
68
+
69
+ # load analyzer args pickle file
70
+ analyzer_config = pickle.load(open(r"analyzer_args.pkl", 'rb'))
71
+
72
+ # Create analyzers
73
+ analyzers = []
74
+ for analyzer in args.analyzers.split(","):
75
+ A = locate(analyzer)
76
+ a = A(**analyzer_config[analyzer])
77
+ analyzers.append(a)
78
+
79
+ if not all(analyzers):
80
+ raise Exception("Not all analyzers could be found...\n{}".format(",".join(analyzers)))
81
+
82
+ if args.analyzer_manager_args_file is not None:
83
+ logger.info(f"Loading extra AnalyzerManager args from {args.analyzer_manager_args_file}")
84
+ with open(args.analyzer_manager_args_file, 'rb') as pin:
85
+ extra_args = pickle.load(pin)
86
+
87
+ # load platform args pickle file
88
+ platform_args = pickle.load(open(r"platform_args.pkl", 'rb'))
89
+
90
+ # get platform
91
+ platform = Platform(args.block, **platform_args)
92
+ logger.info(f"Analyzer Manager called with the following extra arguments: {extra_args}")
93
+ am = AnalyzeManager(platform=platform, ids=item_ids, analyzers=analyzers, **extra_args)
94
+ am.analyze()
@@ -0,0 +1,291 @@
1
+ """
2
+ Platform Analysis is a wrapper to allow execution of analysis through SSMT vs Locally.
3
+
4
+ Running remotely has great advantages over local execution with the biggest being more compute resources and less data transfer.
5
+ Platform Analysis tries to make the process of running remotely similar to local execution.
6
+
7
+ Copyright 2021, Bill & Melinda Gates Foundation. All rights reserved.
8
+ """
9
+ import re
10
+ from typing import List, Callable, Union, Type, Dict, Any
11
+ import inspect
12
+ import os
13
+ import pickle
14
+ from logging import getLogger, DEBUG
15
+ from idmtools.assets import Asset, AssetCollection
16
+ from idmtools.assets.file_list import FileList
17
+ from idmtools.config import IdmConfigParser
18
+ from idmtools.entities import IAnalyzer
19
+ from idmtools.entities.iplatform import IPlatform
20
+ from idmtools.entities.iplatform_default import AnalyzerManagerPlatformDefault
21
+ from idmtools.utils.info import get_help_version_url
22
+
23
+ logger = getLogger(__name__)
24
+ user_logger = getLogger('user')
25
+
26
+
27
+ class PlatformAnalysis:
28
+ """
29
+ PlatformAnalysis allows remote Analysis on the server.
30
+
31
+ See Also:
32
+ :py:class:`idmtools.analysis.analyze_manager.AnalyzeManager`
33
+ """
34
+
35
+ def __init__(self, platform: IPlatform, analyzers: List[Type[IAnalyzer]],
36
+ experiment_ids: List['str'] = [], simulation_ids: List['str'] = [], work_item_ids: List['str'] = [],
37
+ analyzers_args=None, analysis_name: str = 'WorkItem Test', tags=None,
38
+ additional_files: Union[FileList, AssetCollection, List[str]] = None, asset_collection_id=None,
39
+ asset_files: Union[FileList, AssetCollection, List[str]] = None, wait_till_done: bool = True,
40
+ idmtools_config: str = None, pre_run_func: Callable = None, wrapper_shell_script: str = None,
41
+ verbose: bool = False, extra_args: Dict[str, Any] = None):
42
+ """
43
+ Initialize our platform analysis.
44
+
45
+ Args:
46
+ platform: Platform
47
+ experiment_ids: Experiment ids
48
+ simulation_ids: Simulation ids
49
+ work_item_ids: WorkItem ids
50
+ analyzers: Analyzers to run
51
+ analyzers_args: Arguments for our analyzers
52
+ analysis_name: Analysis name
53
+ tags: Tags for the workitem
54
+ additional_files: Additional files for server analysis
55
+ asset_collection_id: Asset Collection to use
56
+ asset_files: Asset files to attach
57
+ wait_till_done: Wait until analysis is done
58
+ idmtools_config: Optional path to idmtools.ini to use on server. Mostly useful for development
59
+ pre_run_func: A function (with no arguments) to be executed before analysis starts on the remote server
60
+ wrapper_shell_script: Optional path to a wrapper shell script. This script should redirect all arguments to command passed to it. Mostly useful for development purposes
61
+ verbose: Enables verbose logging remotely
62
+ extra_args: Optional extra arguments to pass to AnalyzerManager on the server side. See :meth:`~idmtools.analysis.analyze_manager.AnalyzeManager.__init__`
63
+
64
+ See Also:
65
+ :meth:`idmtools.analysis.analyze_manager.AnalyzeManager.__init__`
66
+ """
67
+ self.platform = platform
68
+ self.experiment_ids = experiment_ids or []
69
+ self.simulation_ids = simulation_ids or []
70
+ self.work_item_ids = work_item_ids or []
71
+ self.analyzers = analyzers
72
+ self.analyzers_args = analyzers_args
73
+ self.analysis_name = analysis_name
74
+ self.tags = tags
75
+ if isinstance(additional_files, list):
76
+ additional_files = AssetCollection(additional_files)
77
+ elif isinstance(additional_files, FileList):
78
+ additional_files = additional_files.to_asset_collection()
79
+ self.additional_files: AssetCollection = additional_files or AssetCollection()
80
+ self.asset_collection_id = asset_collection_id
81
+ if isinstance(asset_files, list):
82
+ asset_files = AssetCollection(asset_files)
83
+ elif isinstance(asset_files, FileList):
84
+ asset_files = asset_files.to_asset_collection()
85
+ self.asset_files: AssetCollection = asset_files or AssetCollection()
86
+ self.wi = None
87
+ self.wait_till_done = wait_till_done
88
+ self.idmtools_config = idmtools_config
89
+ self.pre_run_func = pre_run_func
90
+ self.wrapper_shell_script = wrapper_shell_script
91
+ self.shell_script_binary = "/bin/bash"
92
+ self.verbose = verbose
93
+ self.extra_args = extra_args if extra_args else dict()
94
+
95
+ self.validate_args()
96
+
97
+ def analyze(self, check_status=True):
98
+ """
99
+ Analyze remotely.
100
+
101
+ Args:
102
+ check_status: Should we check status
103
+
104
+ Returns:
105
+ None
106
+
107
+ Notes:
108
+ TODO: check_status is not being used
109
+ """
110
+ command = self._prep_analyze()
111
+
112
+ logger.debug(f"Command: {command}")
113
+ from idmtools_platform_comps.ssmt_work_items.comps_workitems import SSMTWorkItem
114
+
115
+ ac = AssetCollection.from_id(self.asset_collection_id,
116
+ platform=self.platform) if self.asset_collection_id else AssetCollection()
117
+ ac.add_assets(self.asset_files)
118
+ self.wi = SSMTWorkItem(name=self.analysis_name, command=command, tags=self.tags,
119
+ transient_assets=self.additional_files, assets=ac,
120
+ related_experiments=self.experiment_ids,
121
+ related_simulations=self.simulation_ids,
122
+ related_work_items=self.work_item_ids
123
+ )
124
+
125
+ # Run the workitem
126
+ self.platform.run_items(self.wi)
127
+ if self.wait_till_done:
128
+ self.platform.wait_till_done(self.wi)
129
+ logger.debug(f"Status: {self.wi.status}")
130
+
131
+ def _prep_analyze(self):
132
+ """
133
+ Prepare for analysis.
134
+
135
+ Returns:
136
+ None
137
+ """
138
+ # Add the platform_analysis_bootstrap.py file to the collection
139
+ dir_path = os.path.dirname(os.path.realpath(__file__))
140
+ self.additional_files.add_or_replace_asset(os.path.join(dir_path, "platform_analysis_bootstrap.py"))
141
+ # check if user gave us an override to idmtools config
142
+ if self.idmtools_config:
143
+ self.additional_files.add_or_replace_asset(self.idmtools_config)
144
+ else:
145
+ # look for one from idmtools.
146
+ config_path = IdmConfigParser.get_config_path()
147
+ if config_path and os.path.exists(config_path):
148
+ if logger.isEnabledFor(DEBUG):
149
+ logger.debug(f"Adding config file: {config_path}")
150
+ self.additional_files.add_or_replace_asset(config_path)
151
+
152
+ # build analyzer args dict
153
+ args_dict = {}
154
+ a_args = zip(self.analyzers, self.analyzers_args)
155
+ for a, g in a_args:
156
+ args_dict[f"{inspect.getmodulename(inspect.getfile(a))}.{a.__name__}"] = g
157
+ if self.pre_run_func:
158
+ self.__pickle_pre_run()
159
+ # save pickle file as a temp file
160
+ self.__pickle_analyzers(args_dict)
161
+
162
+ # Add all the analyzers files
163
+ for a in self.analyzers:
164
+ self.additional_files.add_or_replace_asset(inspect.getfile(a))
165
+
166
+ # add our extra arguments for analyzer manager
167
+ if 'max_workers' not in self.extra_args:
168
+ am_defaults: List[AnalyzerManagerPlatformDefault] = self.platform.get_defaults_by_type(
169
+ AnalyzerManagerPlatformDefault)
170
+ if len(am_defaults):
171
+ if logger.isEnabledFor(DEBUG):
172
+ logger.debug(f"Setting max workers to comps default of: {am_defaults[0].max_workers}")
173
+ self.extra_args['max_workers'] = am_defaults[0].max_workers
174
+
175
+ # Create the command
176
+ command = ''
177
+ if self.wrapper_shell_script:
178
+ self.additional_files.add_or_replace_asset(self.wrapper_shell_script)
179
+ command += f'{self.shell_script_binary} {os.path.basename(self.wrapper_shell_script)} '
180
+ command += "python3 platform_analysis_bootstrap.py"
181
+ # Add the experiments
182
+ if self.experiment_ids:
183
+ command += f' --experiment-ids {",".join(self.experiment_ids)}'
184
+ # Add the simulations
185
+ if self.simulation_ids:
186
+ command += f' --simulation-ids {",".join(self.simulation_ids)}'
187
+ # Add the work items
188
+ if self.work_item_ids:
189
+ command += f' --work-item-ids {",".join(self.work_item_ids)}'
190
+ # Add the analyzers
191
+ command += " --analyzers {}".format(
192
+ ",".join(f"{inspect.getmodulename(inspect.getfile(a))}.{a.__name__}" for a in self.analyzers))
193
+
194
+ if self.pre_run_func:
195
+ command += f" --pre-run-func {self.pre_run_func.__name__}"
196
+
197
+ # Pickle the extra args
198
+ if len(self.extra_args):
199
+ from idmtools.analysis.analyze_manager import AnalyzeManager
200
+ argspec = inspect.signature(AnalyzeManager.__init__)
201
+ for argname, value in self.extra_args.items():
202
+ if argname not in argspec.parameters:
203
+ raise ValueError(
204
+ f"AnalyzerManager does not support the argument {argname}. Valid args are {' '.join([str(s) for s in argspec.parameters.keys()])}. See {get_help_version_url('idmtools.analysis.analyze_manager.html#idmtools.analysis.analyze_manager.AnalyzeManager')} for a valid list of arguments.")
205
+ # TODO do type validations later
206
+ self.additional_files.add_or_replace_asset(
207
+ Asset(filename="extra_args.pkl", content=pickle.dumps(self.extra_args)))
208
+ command += " --analyzer-manager-args-file extra_args.pkl"
209
+
210
+ self.__pickle_platform_args()
211
+ command += " --platform-args platform_args.pkl"
212
+
213
+ # Add platform
214
+ ssmt_config_block = f"{self.platform._config_block}_SSMT"
215
+ command += " --block {}".format(ssmt_config_block)
216
+ if self.verbose:
217
+ command += " --verbose"
218
+
219
+ return command
220
+
221
+ def __pickle_analyzers(self, args_dict):
222
+ """
223
+ Pickle our analyzers and add as assets.
224
+
225
+ Args:
226
+ args_dict: Analyzer and args
227
+
228
+ Returns:
229
+ None
230
+ """
231
+ self.additional_files.add_or_replace_asset(Asset(filename='analyzer_args.pkl', content=pickle.dumps(args_dict)))
232
+
233
+ def __pickle_pre_run(self):
234
+ """
235
+ Pickle objects before we run and add items as assets.
236
+
237
+ Returns:
238
+ None
239
+ """
240
+ source = inspect.getsource(self.pre_run_func).splitlines()
241
+ space_base = 0
242
+ while source[0][space_base] == " ":
243
+ space_base += 1
244
+ replace_expr = re.compile("^[ ]{" + str(space_base) + "}")
245
+ new_source = []
246
+ for line in source:
247
+ new_source.append(replace_expr.sub("", line))
248
+
249
+ self.additional_files.add_or_replace_asset(Asset(filename="pre_run.py", content="\n".join(new_source)))
250
+
251
+ def __pickle_platform_args(self):
252
+ """
253
+ Pickle platform args and add as assets.
254
+
255
+ Returns:
256
+ None
257
+ """
258
+ # Pickle the platform args
259
+ platform_kwargs = self.platform._kwargs
260
+ self.additional_files.add_or_replace_asset(
261
+ Asset(filename="platform_args.pkl", content=pickle.dumps(platform_kwargs)))
262
+
263
+ def validate_args(self):
264
+ """
265
+ Validate arguments for the platform analysis and analyzers.
266
+
267
+ Returns:
268
+ None
269
+ """
270
+ if self.analyzers_args is None:
271
+ self.analyzers_args = [{}] * len(self.analyzers)
272
+ return
273
+
274
+ self.analyzers_args = [g if g is not None else {} for g in self.analyzers_args]
275
+
276
+ if len(self.analyzers_args) < len(self.analyzers):
277
+ self.analyzers_args = self.analyzers_args + [{}] * (len(self.analyzers) - len(self.analyzers_args))
278
+ return
279
+
280
+ if len(self.analyzers) < len(self.analyzers_args):
281
+ user_logger.error("two list 'analyzers' and 'analyzers_args' must have the same length.")
282
+ exit()
283
+
284
+ def get_work_item(self):
285
+ """
286
+ Get work item being using to run analysis job on server.
287
+
288
+ Returns:
289
+ Workflow item
290
+ """
291
+ return self.wi