idmtools 0.0.0.dev0__py3-none-any.whl → 0.0.2__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.
- idmtools/__init__.py +27 -8
- idmtools/analysis/__init__.py +5 -0
- idmtools/analysis/add_analyzer.py +89 -0
- idmtools/analysis/analyze_manager.py +490 -0
- idmtools/analysis/csv_analyzer.py +103 -0
- idmtools/analysis/download_analyzer.py +96 -0
- idmtools/analysis/map_worker_entry.py +100 -0
- idmtools/analysis/platform_analysis_bootstrap.py +94 -0
- idmtools/analysis/platform_anaylsis.py +291 -0
- idmtools/analysis/tags_analyzer.py +93 -0
- idmtools/assets/__init__.py +9 -0
- idmtools/assets/asset.py +453 -0
- idmtools/assets/asset_collection.py +514 -0
- idmtools/assets/content_handlers.py +19 -0
- idmtools/assets/errors.py +23 -0
- idmtools/assets/file_list.py +191 -0
- idmtools/builders/__init__.py +11 -0
- idmtools/builders/arm_simulation_builder.py +152 -0
- idmtools/builders/csv_simulation_builder.py +76 -0
- idmtools/builders/simulation_builder.py +348 -0
- idmtools/builders/sweep_arm.py +109 -0
- idmtools/builders/yaml_simulation_builder.py +82 -0
- idmtools/config/__init__.py +7 -0
- idmtools/config/idm_config_parser.py +486 -0
- idmtools/core/__init__.py +10 -0
- idmtools/core/cache_enabled.py +114 -0
- idmtools/core/context.py +68 -0
- idmtools/core/docker_task.py +207 -0
- idmtools/core/enums.py +51 -0
- idmtools/core/exceptions.py +91 -0
- idmtools/core/experiment_factory.py +71 -0
- idmtools/core/id_file.py +70 -0
- idmtools/core/interfaces/__init__.py +5 -0
- idmtools/core/interfaces/entity_container.py +64 -0
- idmtools/core/interfaces/iassets_enabled.py +58 -0
- idmtools/core/interfaces/ientity.py +331 -0
- idmtools/core/interfaces/iitem.py +206 -0
- idmtools/core/interfaces/imetadata_operations.py +89 -0
- idmtools/core/interfaces/inamed_entity.py +17 -0
- idmtools/core/interfaces/irunnable_entity.py +159 -0
- idmtools/core/logging.py +387 -0
- idmtools/core/platform_factory.py +316 -0
- idmtools/core/system_information.py +104 -0
- idmtools/core/task_factory.py +145 -0
- idmtools/entities/__init__.py +10 -0
- idmtools/entities/command_line.py +229 -0
- idmtools/entities/command_task.py +155 -0
- idmtools/entities/experiment.py +787 -0
- idmtools/entities/generic_workitem.py +43 -0
- idmtools/entities/ianalyzer.py +163 -0
- idmtools/entities/iplatform.py +1106 -0
- idmtools/entities/iplatform_default.py +39 -0
- idmtools/entities/iplatform_ops/__init__.py +5 -0
- idmtools/entities/iplatform_ops/iplatform_asset_collection_operations.py +148 -0
- idmtools/entities/iplatform_ops/iplatform_experiment_operations.py +415 -0
- idmtools/entities/iplatform_ops/iplatform_simulation_operations.py +315 -0
- idmtools/entities/iplatform_ops/iplatform_suite_operations.py +322 -0
- idmtools/entities/iplatform_ops/iplatform_workflowitem_operations.py +301 -0
- idmtools/entities/iplatform_ops/utils.py +185 -0
- idmtools/entities/itask.py +316 -0
- idmtools/entities/iworkflow_item.py +167 -0
- idmtools/entities/platform_requirements.py +20 -0
- idmtools/entities/relation_type.py +14 -0
- idmtools/entities/simulation.py +255 -0
- idmtools/entities/suite.py +188 -0
- idmtools/entities/task_proxy.py +37 -0
- idmtools/entities/templated_simulation.py +325 -0
- idmtools/frozen/frozen_dict.py +71 -0
- idmtools/frozen/frozen_list.py +66 -0
- idmtools/frozen/frozen_set.py +86 -0
- idmtools/frozen/frozen_tuple.py +18 -0
- idmtools/frozen/frozen_utils.py +179 -0
- idmtools/frozen/ifrozen.py +66 -0
- idmtools/plugins/__init__.py +5 -0
- idmtools/plugins/git_commit.py +117 -0
- idmtools/registry/__init__.py +4 -0
- idmtools/registry/experiment_specification.py +105 -0
- idmtools/registry/functions.py +28 -0
- idmtools/registry/hook_specs.py +132 -0
- idmtools/registry/master_plugin_registry.py +51 -0
- idmtools/registry/platform_specification.py +138 -0
- idmtools/registry/plugin_specification.py +129 -0
- idmtools/registry/task_specification.py +104 -0
- idmtools/registry/utils.py +119 -0
- idmtools/services/__init__.py +5 -0
- idmtools/services/ipersistance_service.py +135 -0
- idmtools/services/platforms.py +13 -0
- idmtools/utils/__init__.py +5 -0
- idmtools/utils/caller.py +24 -0
- idmtools/utils/collections.py +246 -0
- idmtools/utils/command_line.py +45 -0
- idmtools/utils/decorators.py +300 -0
- idmtools/utils/display/__init__.py +22 -0
- idmtools/utils/display/displays.py +181 -0
- idmtools/utils/display/settings.py +25 -0
- idmtools/utils/dropbox_location.py +30 -0
- idmtools/utils/entities.py +127 -0
- idmtools/utils/file.py +72 -0
- idmtools/utils/file_parser.py +151 -0
- idmtools/utils/filter_simulations.py +182 -0
- idmtools/utils/filters/__init__.py +5 -0
- idmtools/utils/filters/asset_filters.py +88 -0
- idmtools/utils/general.py +286 -0
- idmtools/utils/gitrepo.py +336 -0
- idmtools/utils/hashing.py +239 -0
- idmtools/utils/info.py +124 -0
- idmtools/utils/json.py +82 -0
- idmtools/utils/language.py +107 -0
- idmtools/utils/local_os.py +40 -0
- idmtools/utils/time.py +22 -0
- idmtools-0.0.2.dist-info/METADATA +120 -0
- idmtools-0.0.2.dist-info/RECORD +116 -0
- idmtools-0.0.2.dist-info/entry_points.txt +9 -0
- idmtools-0.0.2.dist-info/licenses/LICENSE.TXT +3 -0
- idmtools-0.0.0.dev0.dist-info/METADATA +0 -41
- idmtools-0.0.0.dev0.dist-info/RECORD +0 -5
- {idmtools-0.0.0.dev0.dist-info → idmtools-0.0.2.dist-info}/WHEEL +0 -0
- {idmtools-0.0.0.dev0.dist-info → idmtools-0.0.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,787 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Our Experiment class definition.
|
|
3
|
+
|
|
4
|
+
Experiments can be thought of as a metadata object analogous to a folder on a filesystem. An experiment is a container that
|
|
5
|
+
contains one or more simulations. Before creations, *experiment.simulations* can be either a list of a TemplatedSimulations.
|
|
6
|
+
TemplatedSimulations are useful for building large numbers of similar simulations such as sweeps.
|
|
7
|
+
|
|
8
|
+
Copyright 2021, Bill & Melinda Gates Foundation. All rights reserved.
|
|
9
|
+
"""
|
|
10
|
+
import copy
|
|
11
|
+
from dataclasses import dataclass, field, InitVar, fields
|
|
12
|
+
from logging import getLogger, DEBUG
|
|
13
|
+
from types import GeneratorType
|
|
14
|
+
from typing import NoReturn, Set, Union, Iterator, Type, Dict, Any, List, TYPE_CHECKING, Generator
|
|
15
|
+
from idmtools import IdmConfigParser
|
|
16
|
+
from idmtools.assets import AssetCollection, Asset
|
|
17
|
+
from idmtools.builders import SimulationBuilder
|
|
18
|
+
from idmtools.core import ItemType, EntityStatus
|
|
19
|
+
from idmtools.core.interfaces.entity_container import EntityContainer
|
|
20
|
+
from idmtools.core.interfaces.iassets_enabled import IAssetsEnabled
|
|
21
|
+
from idmtools.core.interfaces.iitem import IItem
|
|
22
|
+
from idmtools.core.interfaces.inamed_entity import INamedEntity
|
|
23
|
+
from idmtools.core.interfaces.irunnable_entity import IRunnableEntity
|
|
24
|
+
from idmtools.core.logging import SUCCESS, NOTICE
|
|
25
|
+
from idmtools.entities.itask import ITask
|
|
26
|
+
from idmtools.entities.platform_requirements import PlatformRequirements
|
|
27
|
+
from idmtools.entities.templated_simulation import TemplatedSimulations
|
|
28
|
+
from idmtools.registry.experiment_specification import ExperimentPluginSpecification, get_model_impl, \
|
|
29
|
+
get_model_type_impl
|
|
30
|
+
from idmtools.registry.plugin_specification import get_description_impl
|
|
31
|
+
from idmtools.utils.caller import get_caller
|
|
32
|
+
from idmtools.utils.collections import ExperimentParentIterator
|
|
33
|
+
from idmtools.utils.entities import get_default_tags
|
|
34
|
+
|
|
35
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
36
|
+
from idmtools.entities.iplatform import IPlatform
|
|
37
|
+
from idmtools.entities.simulation import Simulation # noqa: F401
|
|
38
|
+
from idmtools.entities.suite import Suite # noqa: F401
|
|
39
|
+
|
|
40
|
+
logger = getLogger(__name__)
|
|
41
|
+
user_logger = getLogger('user')
|
|
42
|
+
SUPPORTED_SIM_TYPE = Union[
|
|
43
|
+
EntityContainer,
|
|
44
|
+
Generator['Simulation', None, None],
|
|
45
|
+
TemplatedSimulations,
|
|
46
|
+
Iterator['Simulation']
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(repr=False)
|
|
51
|
+
class Experiment(IAssetsEnabled, INamedEntity, IRunnableEntity):
|
|
52
|
+
"""
|
|
53
|
+
Class that represents a generic experiment.
|
|
54
|
+
|
|
55
|
+
This class needs to be implemented for each model type with specifics.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
name: The experiment name.
|
|
59
|
+
assets: The asset collection for assets global to this experiment.
|
|
60
|
+
"""
|
|
61
|
+
#: Suite ID
|
|
62
|
+
suite_id: str = field(default=None)
|
|
63
|
+
#: Item Item(always an experiment)
|
|
64
|
+
item_type: ItemType = field(default=ItemType.EXPERIMENT, compare=False, init=False)
|
|
65
|
+
#: Task Type(defaults to command)
|
|
66
|
+
task_type: str = field(default='idmtools.entities.command_task.CommandTask')
|
|
67
|
+
#: List of Requirements for the task that a platform must meet to be able to run
|
|
68
|
+
platform_requirements: Set[PlatformRequirements] = field(default_factory=set)
|
|
69
|
+
#: Is the Experiment Frozen
|
|
70
|
+
frozen: bool = field(default=False, init=False)
|
|
71
|
+
#: Simulation in this experiment
|
|
72
|
+
simulations: InitVar[SUPPORTED_SIM_TYPE] = None
|
|
73
|
+
#: Internal storage of simulation
|
|
74
|
+
__simulations: SUPPORTED_SIM_TYPE = field(default_factory=lambda: EntityContainer(), compare=False)
|
|
75
|
+
|
|
76
|
+
#: Determines if we should gather assets from the first task. Only use when not using TemplatedSimulations
|
|
77
|
+
gather_common_assets_from_task: bool = field(default=None, compare=False)
|
|
78
|
+
|
|
79
|
+
#: Determines if we should gather assets from the first task. Only use when not using TemplatedSimulations
|
|
80
|
+
disable_default_pre_create: bool = field(default=False, compare=False)
|
|
81
|
+
|
|
82
|
+
#: Enable replacing the task with a proxy to reduce the memory footprint. Useful in provisioning large sets of
|
|
83
|
+
# simulations
|
|
84
|
+
__replace_task_with_proxy: bool = field(default=True, init=False, compare=False)
|
|
85
|
+
|
|
86
|
+
def __post_init__(self, simulations):
|
|
87
|
+
"""
|
|
88
|
+
Initialize Experiment.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
simulations: Simulations to initialize with
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
None
|
|
95
|
+
"""
|
|
96
|
+
super().__post_init__()
|
|
97
|
+
if simulations is not None and not isinstance(simulations, property):
|
|
98
|
+
self.simulations = simulations
|
|
99
|
+
|
|
100
|
+
if self.gather_common_assets_from_task is None:
|
|
101
|
+
self.gather_common_assets_from_task = isinstance(self.simulations.items, EntityContainer)
|
|
102
|
+
self.__simulations.parent = self
|
|
103
|
+
|
|
104
|
+
def post_creation(self, platform: 'IPlatform') -> None:
|
|
105
|
+
"""
|
|
106
|
+
Post creation of experiments.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
platform: Platform the experiment was created on
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
None
|
|
113
|
+
"""
|
|
114
|
+
IItem.post_creation(self, platform)
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def status(self):
|
|
118
|
+
"""
|
|
119
|
+
Get status of experiment. Experiment status is based in simulations.
|
|
120
|
+
|
|
121
|
+
The first rule to be true is used. The rules are:
|
|
122
|
+
* If simulations is a TemplatedSimulations we assume status is None if _platform_object is not set.
|
|
123
|
+
* If simulations is a TemplatedSimulations we assume status is CREATED if _platform_object is set.
|
|
124
|
+
* If simulations length is 0 or all simulations have a status of None, experiment status is none
|
|
125
|
+
* If any simulation has a running status, experiment is considered running.
|
|
126
|
+
* If any simulation has a created status and any other simulation has a FAILED or SUCCEEDED status, experiment is considered running.
|
|
127
|
+
* If any simulation has a None status and any other simulation has a FAILED or SUCCEEDED status, experiment is considered Created.
|
|
128
|
+
* If any simulation has a status of failed, experiment is considered failed.
|
|
129
|
+
* If any simulation has a status of SUCCEEDED, experiment is considered SUCCEEDED.
|
|
130
|
+
* If any simulation has a status of CREATED, experiment is considered CREATED.
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Status
|
|
135
|
+
"""
|
|
136
|
+
# still creating sims since we have a template. When adding new simulations, we will pre-create sim objects unless
|
|
137
|
+
# the item is new
|
|
138
|
+
if isinstance(self.simulations.items, TemplatedSimulations):
|
|
139
|
+
status = EntityStatus.CREATED if self._platform_object else None
|
|
140
|
+
return status
|
|
141
|
+
|
|
142
|
+
sim_statuses = set([s.status for s in self.simulations.items])
|
|
143
|
+
any_succeeded_failed = any([s in [EntityStatus.FAILED, EntityStatus.SUCCEEDED] for s in sim_statuses])
|
|
144
|
+
if len(self.simulations.items) == 0 or all([s is None for s in sim_statuses]):
|
|
145
|
+
status = None # this will trigger experiment creation on a platform
|
|
146
|
+
elif any([s == EntityStatus.RUNNING for s in sim_statuses]):
|
|
147
|
+
status = EntityStatus.RUNNING
|
|
148
|
+
elif any([s == EntityStatus.CREATED for s in sim_statuses]) and any_succeeded_failed:
|
|
149
|
+
status = EntityStatus.RUNNING
|
|
150
|
+
elif any([s is None for s in sim_statuses]) and any_succeeded_failed:
|
|
151
|
+
status = EntityStatus.CREATED
|
|
152
|
+
elif any([s == EntityStatus.FAILED for s in sim_statuses]):
|
|
153
|
+
status = EntityStatus.FAILED
|
|
154
|
+
elif all([s == EntityStatus.SUCCEEDED for s in sim_statuses]):
|
|
155
|
+
status = EntityStatus.SUCCEEDED
|
|
156
|
+
else:
|
|
157
|
+
status = EntityStatus.CREATED
|
|
158
|
+
return status
|
|
159
|
+
|
|
160
|
+
@status.setter
|
|
161
|
+
def status(self, value):
|
|
162
|
+
"""
|
|
163
|
+
Set status of experiment. Experiments status is an aggregate of its children so you cannot set status.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
value: Value to set
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
None
|
|
170
|
+
|
|
171
|
+
Notes:
|
|
172
|
+
TODO: Deprecate this
|
|
173
|
+
"""
|
|
174
|
+
# this method is needed because dataclasses will always try to set each field, even if not allowed to in
|
|
175
|
+
# the case of Experiment.
|
|
176
|
+
|
|
177
|
+
caller = get_caller()
|
|
178
|
+
if caller not in ['__init__']:
|
|
179
|
+
logger.warning('Experiment status cannot be directly altered. Status unchanged.')
|
|
180
|
+
|
|
181
|
+
def __repr__(self):
|
|
182
|
+
"""Experiment as string."""
|
|
183
|
+
return f"<Experiment: {self.uid} - {self.name} / Sim count {len(self.simulations) if self.simulations else 0}>"
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def suite(self):
|
|
187
|
+
"""
|
|
188
|
+
Suite the experiment belongs to.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
Suite
|
|
192
|
+
"""
|
|
193
|
+
return self.parent
|
|
194
|
+
|
|
195
|
+
@suite.setter
|
|
196
|
+
def suite(self, suite):
|
|
197
|
+
"""
|
|
198
|
+
Set suite of the experiment.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
suite: Suite to set
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
None
|
|
205
|
+
"""
|
|
206
|
+
self.parent = suite
|
|
207
|
+
|
|
208
|
+
@property
|
|
209
|
+
def parent(self):
|
|
210
|
+
"""
|
|
211
|
+
Return parent object for item.
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
Parent Suite if set
|
|
215
|
+
"""
|
|
216
|
+
if not self._parent:
|
|
217
|
+
self.parent_id = self.parent_id or self.suite_id
|
|
218
|
+
if not self.parent_id:
|
|
219
|
+
return None
|
|
220
|
+
if not self.platform:
|
|
221
|
+
from idmtools.core import NoPlatformException
|
|
222
|
+
raise NoPlatformException("The object has no platform set...")
|
|
223
|
+
suite = self.platform.get_item(self.parent_id, ItemType.SUITE, force=True)
|
|
224
|
+
suite.add_experiment(self)
|
|
225
|
+
|
|
226
|
+
return self._parent
|
|
227
|
+
|
|
228
|
+
@parent.setter
|
|
229
|
+
def parent(self, parent: 'Suite'):
|
|
230
|
+
"""
|
|
231
|
+
Sets the parent object for Entity.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
parent: Parent object
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
None
|
|
238
|
+
"""
|
|
239
|
+
if parent is not None:
|
|
240
|
+
parent.add_experiment(self)
|
|
241
|
+
else:
|
|
242
|
+
self._parent = self.parent_id = self.suite_id = None
|
|
243
|
+
|
|
244
|
+
def display(self):
|
|
245
|
+
"""
|
|
246
|
+
Display the experiment.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
None
|
|
250
|
+
"""
|
|
251
|
+
from idmtools.utils.display import display, experiment_table_display
|
|
252
|
+
display(self, experiment_table_display)
|
|
253
|
+
|
|
254
|
+
def pre_creation(self, platform: 'IPlatform', gather_assets=True) -> None:
|
|
255
|
+
"""
|
|
256
|
+
Experiment pre_creation callback.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
platform: Platform experiment is being created on
|
|
260
|
+
gather_assets: Determines if an experiment will try to gather the common assets or defer. It most cases, you want this enabled but when modifying existing experiments you may want to disable if there are new assets and the platform has performance hits to determine those assets
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
None
|
|
264
|
+
|
|
265
|
+
Raises:
|
|
266
|
+
ValueError - If simulations length is 0
|
|
267
|
+
"""
|
|
268
|
+
# Gather the assets
|
|
269
|
+
IItem.pre_creation(self, platform)
|
|
270
|
+
if not self.disable_default_pre_create:
|
|
271
|
+
self.gather_assets()
|
|
272
|
+
|
|
273
|
+
# to keep experiments clean, let's only do this is we have a special experiment class
|
|
274
|
+
if self.__class__ is not Experiment:
|
|
275
|
+
# Add a tag to keep the Experiment class name
|
|
276
|
+
self.tags["experiment_type"] = f'{self.__class__.__module__}.{self.__class__.__name__}'
|
|
277
|
+
|
|
278
|
+
# if it is a template, set task type on experiment
|
|
279
|
+
if gather_assets:
|
|
280
|
+
if isinstance(self.simulations.items, TemplatedSimulations):
|
|
281
|
+
if len(self.simulations.items) == 0:
|
|
282
|
+
raise ValueError("You cannot run an empty experiment")
|
|
283
|
+
if logger.isEnabledFor(DEBUG):
|
|
284
|
+
logger.debug("Using Base task from template for experiment level assets")
|
|
285
|
+
self.simulations.items.base_task.gather_common_assets()
|
|
286
|
+
self.assets.add_assets(self.simulations.items.base_task.common_assets, fail_on_duplicate=False)
|
|
287
|
+
for sim in self.simulations.items.extra_simulations():
|
|
288
|
+
self.assets.add_assets(sim.task.gather_common_assets(), fail_on_duplicate=False)
|
|
289
|
+
if "task_type" not in self.tags:
|
|
290
|
+
task_class = self.simulations.items.base_task.__class__
|
|
291
|
+
self.tags["task_type"] = f'{task_class.__module__}.{task_class.__name__}'
|
|
292
|
+
elif self.gather_common_assets_from_task and isinstance(self.simulations.items, List):
|
|
293
|
+
if len(self.simulations.items) == 0:
|
|
294
|
+
raise ValueError("You cannot run an empty experiment")
|
|
295
|
+
if logger.isEnabledFor(DEBUG):
|
|
296
|
+
logger.debug("Using all tasks to gather assets")
|
|
297
|
+
task_class = self.__simulations[0].task.__class__
|
|
298
|
+
self.tags["task_type"] = f'{task_class.__module__}.{task_class.__name__}'
|
|
299
|
+
pbar = self.__simulations
|
|
300
|
+
if not IdmConfigParser.is_progress_bar_disabled():
|
|
301
|
+
from tqdm import tqdm
|
|
302
|
+
pbar = tqdm(self.__simulations, desc="Discovering experiment assets from tasks",
|
|
303
|
+
unit="simulation")
|
|
304
|
+
for sim in pbar:
|
|
305
|
+
# don't gather assets from simulations that have been provisioned
|
|
306
|
+
if sim.status is None:
|
|
307
|
+
assets = sim.task.gather_common_assets()
|
|
308
|
+
if assets is not None:
|
|
309
|
+
self.assets.add_assets(assets, fail_on_duplicate=True, fail_on_deep_comparison=True)
|
|
310
|
+
elif isinstance(self.simulations.items, List) and len(self.simulations.items) == 0:
|
|
311
|
+
raise ValueError("You cannot run an empty experiment")
|
|
312
|
+
|
|
313
|
+
self.tags.update(get_default_tags())
|
|
314
|
+
|
|
315
|
+
@property
|
|
316
|
+
def done(self):
|
|
317
|
+
"""
|
|
318
|
+
Return if an experiment has finished executing.
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
True if all simulations have ran, False otherwise
|
|
322
|
+
"""
|
|
323
|
+
return all([s.done for s in self.simulations])
|
|
324
|
+
|
|
325
|
+
@property
|
|
326
|
+
def succeeded(self) -> bool:
|
|
327
|
+
"""
|
|
328
|
+
Return if an experiment has succeeded. An experiment is succeeded when all simulations have succeeded.
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
True if all simulations have succeeded, False otherwise
|
|
332
|
+
"""
|
|
333
|
+
return all([s.succeeded for s in self.simulations])
|
|
334
|
+
|
|
335
|
+
@property
|
|
336
|
+
def any_failed(self) -> bool:
|
|
337
|
+
"""
|
|
338
|
+
Return if an experiment has any simulation in failed state.
|
|
339
|
+
|
|
340
|
+
Returns:
|
|
341
|
+
True if all simulations have succeeded, False otherwise
|
|
342
|
+
"""
|
|
343
|
+
return any([s.failed for s in self.simulations])
|
|
344
|
+
|
|
345
|
+
@property
|
|
346
|
+
def simulations(self) -> ExperimentParentIterator: # noqa: F811
|
|
347
|
+
"""
|
|
348
|
+
Get the experiment's simulations.
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
ExperimentParentIterator: Iterator wrapping internal simulation container.
|
|
352
|
+
"""
|
|
353
|
+
if self.__simulations is None:
|
|
354
|
+
return ExperimentParentIterator([], parent=self)
|
|
355
|
+
return ExperimentParentIterator(self.__simulations, parent=self)
|
|
356
|
+
|
|
357
|
+
def get_simulations(self) -> ExperimentParentIterator: # noqa: F811:
|
|
358
|
+
"""
|
|
359
|
+
Resolve and return simulations from internal container.
|
|
360
|
+
|
|
361
|
+
Returns:
|
|
362
|
+
ExperimentParentIterator
|
|
363
|
+
"""
|
|
364
|
+
return self.simulations
|
|
365
|
+
|
|
366
|
+
@simulations.setter
|
|
367
|
+
def simulations(self, simulations: SUPPORTED_SIM_TYPE):
|
|
368
|
+
"""
|
|
369
|
+
Set and normalize the simulations input.
|
|
370
|
+
|
|
371
|
+
Args:
|
|
372
|
+
simulations (SUPPORTED_SIM_TYPE): Simulations, task list, or generator.
|
|
373
|
+
|
|
374
|
+
Raises:
|
|
375
|
+
ValueError: If unsupported input type or invalid simulation list item.
|
|
376
|
+
"""
|
|
377
|
+
from idmtools.entities.simulation import Simulation
|
|
378
|
+
if isinstance(simulations, GeneratorType):
|
|
379
|
+
simulations = list(simulations)
|
|
380
|
+
|
|
381
|
+
if isinstance(simulations, (EntityContainer, TemplatedSimulations)):
|
|
382
|
+
self.__simulations = simulations
|
|
383
|
+
self.gather_common_assets_from_task = isinstance(simulations, EntityContainer)
|
|
384
|
+
elif isinstance(simulations, (list, set)):
|
|
385
|
+
self.gather_common_assets_from_task = True
|
|
386
|
+
container = EntityContainer()
|
|
387
|
+
for sim in simulations:
|
|
388
|
+
if isinstance(sim, ITask):
|
|
389
|
+
container.append(sim.to_simulation())
|
|
390
|
+
elif isinstance(sim, Simulation):
|
|
391
|
+
container.append(sim)
|
|
392
|
+
else:
|
|
393
|
+
raise ValueError("Only Simulation or Task objects are allowed in simulation list.")
|
|
394
|
+
self.__simulations = container
|
|
395
|
+
else:
|
|
396
|
+
raise ValueError(
|
|
397
|
+
"Simulations must be an EntityContainer, Generator, TemplatedSimulations, or a List/Set of Simulations."
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
@property
|
|
401
|
+
def simulation_count(self) -> int:
|
|
402
|
+
"""
|
|
403
|
+
Return the total simulations.
|
|
404
|
+
|
|
405
|
+
Returns:
|
|
406
|
+
Length of simulations
|
|
407
|
+
"""
|
|
408
|
+
return len(self.simulations)
|
|
409
|
+
|
|
410
|
+
def refresh_simulations(self) -> NoReturn:
|
|
411
|
+
"""
|
|
412
|
+
Refresh the simulations from the platform.
|
|
413
|
+
|
|
414
|
+
Returns:
|
|
415
|
+
None
|
|
416
|
+
"""
|
|
417
|
+
from idmtools.core import ItemType
|
|
418
|
+
self.simulations = self.platform.get_children(self.uid, ItemType.EXPERIMENT, force=True)
|
|
419
|
+
|
|
420
|
+
def refresh_simulations_status(self):
|
|
421
|
+
"""
|
|
422
|
+
Refresh the simulation status.
|
|
423
|
+
|
|
424
|
+
Returns:
|
|
425
|
+
None
|
|
426
|
+
"""
|
|
427
|
+
self.platform.refresh_status(item=self)
|
|
428
|
+
|
|
429
|
+
def pre_getstate(self):
|
|
430
|
+
"""
|
|
431
|
+
Return default values for :meth:`~idmtools.interfaces.ientity.pickle_ignore_fields`.
|
|
432
|
+
|
|
433
|
+
Call before pickling.
|
|
434
|
+
"""
|
|
435
|
+
from idmtools.assets import AssetCollection
|
|
436
|
+
return {"assets": AssetCollection(), "simulations": EntityContainer()}
|
|
437
|
+
|
|
438
|
+
def gather_assets(self) -> AssetCollection():
|
|
439
|
+
"""
|
|
440
|
+
Gather all our assets for our experiment.
|
|
441
|
+
|
|
442
|
+
Returns:
|
|
443
|
+
Assets
|
|
444
|
+
"""
|
|
445
|
+
return self.assets
|
|
446
|
+
|
|
447
|
+
@classmethod
|
|
448
|
+
def from_task(cls, task, name: str = None, tags: Dict[str, Any] = None, assets: AssetCollection = None,
|
|
449
|
+
gather_common_assets_from_task: bool = True) -> 'Experiment':
|
|
450
|
+
"""
|
|
451
|
+
Creates an Experiment with one Simulation from a task.
|
|
452
|
+
|
|
453
|
+
Args:
|
|
454
|
+
task: Task to use
|
|
455
|
+
assets: Asset collection to use for common tasks. Defaults to gather assets from task
|
|
456
|
+
name: Name of experiment
|
|
457
|
+
tags: Tags for the items
|
|
458
|
+
gather_common_assets_from_task: Whether we should attempt to gather assets from the Task object for the
|
|
459
|
+
experiment. With large amounts of tasks, this can be expensive as we loop through all
|
|
460
|
+
Returns:
|
|
461
|
+
|
|
462
|
+
"""
|
|
463
|
+
if tags is None:
|
|
464
|
+
tags = dict()
|
|
465
|
+
if name is None:
|
|
466
|
+
name = task.__class__.__name__
|
|
467
|
+
e = Experiment(name=name, tags=tags, assets=AssetCollection() if assets is None else assets,
|
|
468
|
+
gather_common_assets_from_task=gather_common_assets_from_task)
|
|
469
|
+
e.simulations = [task]
|
|
470
|
+
return e
|
|
471
|
+
|
|
472
|
+
@classmethod
|
|
473
|
+
def from_builder(cls, builders: Union[SimulationBuilder, List[SimulationBuilder]], base_task: ITask,
|
|
474
|
+
name: str = None,
|
|
475
|
+
assets: AssetCollection = None, tags: Dict[str, Any] = None) -> 'Experiment':
|
|
476
|
+
"""
|
|
477
|
+
Creates an experiment from a SimulationBuilder object(or list of builders.
|
|
478
|
+
|
|
479
|
+
Args:
|
|
480
|
+
builders: List of builder to create experiment from
|
|
481
|
+
base_task: Base task to use as template
|
|
482
|
+
name: Experiment name
|
|
483
|
+
assets: Experiment level assets
|
|
484
|
+
tags: Experiment tags
|
|
485
|
+
|
|
486
|
+
Returns:
|
|
487
|
+
Experiment object from the builders
|
|
488
|
+
"""
|
|
489
|
+
ts = TemplatedSimulations(base_task=base_task)
|
|
490
|
+
if not isinstance(builders, list):
|
|
491
|
+
builders = [builders]
|
|
492
|
+
for builder in builders:
|
|
493
|
+
ts.add_builder(builder)
|
|
494
|
+
if name is None:
|
|
495
|
+
name = base_task.__class__.__name__
|
|
496
|
+
if len(builders) == 1:
|
|
497
|
+
name += " " + builders[0].__class__.__name__
|
|
498
|
+
return cls.from_template(ts, name=name, tags=tags, assets=assets)
|
|
499
|
+
|
|
500
|
+
@classmethod
|
|
501
|
+
def from_template(cls, template: TemplatedSimulations, name: str = None, assets: AssetCollection = None,
|
|
502
|
+
tags: Dict[str, Any] = None) -> 'Experiment':
|
|
503
|
+
"""
|
|
504
|
+
Creates an Experiment from a TemplatedSimulation object.
|
|
505
|
+
|
|
506
|
+
Args:
|
|
507
|
+
template: TemplatedSimulation object
|
|
508
|
+
name: Experiment name
|
|
509
|
+
assets: Experiment level assets
|
|
510
|
+
tags: Tags
|
|
511
|
+
|
|
512
|
+
Returns:
|
|
513
|
+
Experiment object from the TemplatedSimulation object
|
|
514
|
+
"""
|
|
515
|
+
if tags is None:
|
|
516
|
+
tags = dict()
|
|
517
|
+
if name is None:
|
|
518
|
+
name = template.base_task.__class__.__name__
|
|
519
|
+
e = Experiment(name=name, tags=tags, assets=AssetCollection() if assets is None else assets)
|
|
520
|
+
e.simulations = template
|
|
521
|
+
return e
|
|
522
|
+
|
|
523
|
+
def __deepcopy__(self, memo):
|
|
524
|
+
"""
|
|
525
|
+
Deep copy for experiments. It converts generators and templates to realized lists to allow copying.
|
|
526
|
+
|
|
527
|
+
Args:
|
|
528
|
+
memo: The memo object used for copying
|
|
529
|
+
|
|
530
|
+
Returns:
|
|
531
|
+
Copied experiment
|
|
532
|
+
"""
|
|
533
|
+
cls = self.__class__
|
|
534
|
+
result = cls.__new__(cls)
|
|
535
|
+
memo[id(self)] = result
|
|
536
|
+
for k, v in self.__dict__.items():
|
|
537
|
+
if k in ['_Experiment__simulations'] and isinstance(v, (GeneratorType, TemplatedSimulations)):
|
|
538
|
+
v = list(v)
|
|
539
|
+
setattr(result, k, copy.deepcopy(v, memo))
|
|
540
|
+
result._task_log = getLogger(__name__)
|
|
541
|
+
return result
|
|
542
|
+
|
|
543
|
+
def list_static_assets(self, children: bool = False, platform: 'IPlatform' = None, **kwargs) -> List[Asset]:
|
|
544
|
+
"""
|
|
545
|
+
List assets that have been uploaded to a server already.
|
|
546
|
+
|
|
547
|
+
Args:
|
|
548
|
+
children: When set to true, simulation assets will be loaded as well
|
|
549
|
+
platform: Optional platform to load assets list from
|
|
550
|
+
**kwargs:
|
|
551
|
+
|
|
552
|
+
Returns:
|
|
553
|
+
List of assets
|
|
554
|
+
"""
|
|
555
|
+
if self.id is None:
|
|
556
|
+
raise ValueError("You can only list static assets on an existing experiment")
|
|
557
|
+
p = super()._check_for_platform_from_context(platform)
|
|
558
|
+
return p._experiments.list_assets(self, children, **kwargs)
|
|
559
|
+
|
|
560
|
+
def run(self, wait_until_done: bool = False, platform: 'IPlatform' = None, regather_common_assets: bool = None,
|
|
561
|
+
wait_on_done_progress: bool = True, **run_opts) -> NoReturn:
|
|
562
|
+
"""
|
|
563
|
+
Runs an experiment on a platform.
|
|
564
|
+
|
|
565
|
+
Args:
|
|
566
|
+
wait_until_done: Whether we should wait on experiment to finish running as well. Defaults to False
|
|
567
|
+
platform: Platform object to use. If not specified, we first check object for platform object then the current context
|
|
568
|
+
regather_common_assets: Triggers gathering of assets for *existing* experiments. If not provided, we use the platforms default behaviour. See platform details for performance implications of this. For most platforms, it should be ok but for others, it could decrease performance when assets are not changing.
|
|
569
|
+
It is important to note that when using this feature, ensure the previous simulations have finished provisioning. Failure to do so can lead to unexpected behaviour
|
|
570
|
+
wait_on_done_progress: Should experiment status be shown when waiting
|
|
571
|
+
**run_opts: Options to pass to the platform
|
|
572
|
+
|
|
573
|
+
Returns:
|
|
574
|
+
None
|
|
575
|
+
"""
|
|
576
|
+
p = super()._check_for_platform_from_context(platform)
|
|
577
|
+
if 'wait_on_done' in run_opts:
|
|
578
|
+
raise TypeError(
|
|
579
|
+
"The 'wait_on_done' parameter has been removed in idmtools 1.8.0. Please update your code with 'wait_until_done'.")
|
|
580
|
+
if regather_common_assets is None:
|
|
581
|
+
regather_common_assets = p.is_regather_assets_on_modify()
|
|
582
|
+
if regather_common_assets and not self.assets.is_editable():
|
|
583
|
+
message = "To modify an experiment's asset collection, you must make a copy of it first. For example\nexperiment.assets = experiment.assets.copy()"
|
|
584
|
+
user_logger.error(message) # Show it bold red to user
|
|
585
|
+
raise ValueError(message)
|
|
586
|
+
if not self.assets.is_editable() and isinstance(self.simulations.items,
|
|
587
|
+
TemplatedSimulations) and not regather_common_assets:
|
|
588
|
+
user_logger.warning(
|
|
589
|
+
"You are modifying and existing experiment by using a template without gathering common assets. Ensure your Template configuration is the same as existing experiments or enable gathering of new common assets through regather_common_assets.")
|
|
590
|
+
run_opts['regather_common_assets'] = regather_common_assets
|
|
591
|
+
p.run_items(self, **run_opts)
|
|
592
|
+
if wait_until_done:
|
|
593
|
+
_refresh_interval = run_opts.get('refresh_interval', None)
|
|
594
|
+
if _refresh_interval is None:
|
|
595
|
+
_refresh_interval = p.refresh_interval
|
|
596
|
+
self.wait(wait_on_done_progress=wait_on_done_progress, refresh_interval=_refresh_interval)
|
|
597
|
+
|
|
598
|
+
def to_dict(self):
|
|
599
|
+
"""
|
|
600
|
+
Convert experiment to dictionary.
|
|
601
|
+
|
|
602
|
+
Returns:
|
|
603
|
+
Dictionary of experiment.
|
|
604
|
+
"""
|
|
605
|
+
result = dict()
|
|
606
|
+
for f in fields(self):
|
|
607
|
+
# Include:
|
|
608
|
+
# - public fields (not starting with '_')
|
|
609
|
+
# Exclude:
|
|
610
|
+
# - fields named 'parent'
|
|
611
|
+
if not f.name.startswith("_") and f.name not in ['parent']:
|
|
612
|
+
result[f.name] = getattr(self, f.name)
|
|
613
|
+
|
|
614
|
+
result['_uid'] = self.uid
|
|
615
|
+
return result
|
|
616
|
+
|
|
617
|
+
# Define this here for better completion in IDEs for end users
|
|
618
|
+
@classmethod
|
|
619
|
+
def from_id(cls, item_id: str, platform: 'IPlatform' = None, copy_assets: bool = False,
|
|
620
|
+
**kwargs) -> 'Experiment':
|
|
621
|
+
"""
|
|
622
|
+
Helper function to provide better intellisense to end users.
|
|
623
|
+
|
|
624
|
+
Args:
|
|
625
|
+
item_id: Item id to load
|
|
626
|
+
platform: Optional platform. Fallbacks to context
|
|
627
|
+
copy_assets: Allow copying assets on load. Makes modifying experiments easier when new assets are involved.
|
|
628
|
+
**kwargs: Optional arguments to be passed on to the platform
|
|
629
|
+
|
|
630
|
+
Returns:
|
|
631
|
+
Experiment loaded with ID
|
|
632
|
+
"""
|
|
633
|
+
result = super().from_id(item_id, platform, **kwargs)
|
|
634
|
+
if copy_assets:
|
|
635
|
+
result.assets = result.assets.copy()
|
|
636
|
+
return result
|
|
637
|
+
|
|
638
|
+
def print(self, verbose: bool = False):
|
|
639
|
+
"""
|
|
640
|
+
Print summary of experiment.
|
|
641
|
+
|
|
642
|
+
Args:
|
|
643
|
+
verbose: Verbose printing
|
|
644
|
+
|
|
645
|
+
Returns:
|
|
646
|
+
None
|
|
647
|
+
"""
|
|
648
|
+
user_logger.info(f"Experiment <{self.id}>")
|
|
649
|
+
user_logger.info(f"Total Simulations: {self.simulation_count}")
|
|
650
|
+
user_logger.info(f"Tags: {self.tags}")
|
|
651
|
+
user_logger.info(f"Platform: {self.platform.__class__.__name__}")
|
|
652
|
+
# determine status
|
|
653
|
+
if self.status:
|
|
654
|
+
# if succeeded print that
|
|
655
|
+
if self.succeeded:
|
|
656
|
+
user_logger.log(SUCCESS, "Succeeded")
|
|
657
|
+
elif not self.done:
|
|
658
|
+
user_logger.log(NOTICE, "RUNNING")
|
|
659
|
+
else:
|
|
660
|
+
user_logger.critical("Experiment failed. Please check output")
|
|
661
|
+
|
|
662
|
+
if verbose:
|
|
663
|
+
user_logger.info(f"Simulation Type: {type(self.__simulations)}")
|
|
664
|
+
user_logger.info(f"Assets: {self.assets}")
|
|
665
|
+
|
|
666
|
+
def add_simulation(self, item: 'Simulation'): # noqa F821
|
|
667
|
+
"""
|
|
668
|
+
Adds a simulation to an experiment.
|
|
669
|
+
Args:
|
|
670
|
+
item: Item to add
|
|
671
|
+
Returns:
|
|
672
|
+
None
|
|
673
|
+
"""
|
|
674
|
+
# Append into underlying collection
|
|
675
|
+
self.simulations.append(item)
|
|
676
|
+
|
|
677
|
+
def add_simulations(self, item: Union[List['Simulation'], 'TemplatedSimulations']): # noqa F821
|
|
678
|
+
"""
|
|
679
|
+
Extends experiment's simulations.
|
|
680
|
+
Args:
|
|
681
|
+
item: Item to extend
|
|
682
|
+
Returns:
|
|
683
|
+
None
|
|
684
|
+
"""
|
|
685
|
+
self.simulations.extend(item)
|
|
686
|
+
|
|
687
|
+
def get_simulations_by_tags(self, tags=None, status=None, skip_sims=None, entity_type=False, max_simulations=None,
|
|
688
|
+
**kwargs) -> List[str]:
|
|
689
|
+
"""
|
|
690
|
+
Retrieve a list of simulation IDs or simulation objects with matching tags.
|
|
691
|
+
This method filters simulations based on the provided tags, skipping specified simulations,
|
|
692
|
+
and limiting the number of results if `max_simulations` is set. The return type can be
|
|
693
|
+
either a list of simulation IDs or simulation objects, depending on the `entity_type` flag.
|
|
694
|
+
Args:
|
|
695
|
+
tags (dict, optional): A simulation's tags to filter by.
|
|
696
|
+
status (EntityStatus, Optional): Simulation status.
|
|
697
|
+
entity_type (bool, optional): If True, return simulation objects; otherwise, return simulation IDs. Defaults to False.
|
|
698
|
+
skip_sims (list, optional): A list of simulation IDs to exclude from the results.
|
|
699
|
+
max_simulations (int, optional): The maximum number of simulations to return.
|
|
700
|
+
**kwargs: Additional filter parameters.
|
|
701
|
+
Returns:
|
|
702
|
+
list: A list of simulation IDs or simulation objects, depending on the `entity_type` flag.
|
|
703
|
+
"""
|
|
704
|
+
from idmtools.utils.filter_simulations import FilterItem
|
|
705
|
+
return FilterItem.filter_item(
|
|
706
|
+
platform=self.platform,
|
|
707
|
+
item=self,
|
|
708
|
+
tags=tags,
|
|
709
|
+
status=status,
|
|
710
|
+
entity_type=entity_type,
|
|
711
|
+
skip_sims=skip_sims,
|
|
712
|
+
max_simulations=max_simulations,
|
|
713
|
+
**kwargs
|
|
714
|
+
)
|
|
715
|
+
|
|
716
|
+
def check_duplicate(self, simulation_id: str) -> bool:
|
|
717
|
+
"""
|
|
718
|
+
Check if a simulation ID already exists.
|
|
719
|
+
Args:
|
|
720
|
+
simulation_id: given Simulation ID
|
|
721
|
+
Returns:
|
|
722
|
+
True/False
|
|
723
|
+
"""
|
|
724
|
+
if isinstance(self.simulations.items, (list, set)):
|
|
725
|
+
ids = [sim.id for sim in self.simulations.items]
|
|
726
|
+
return simulation_id in ids
|
|
727
|
+
elif isinstance(self.simulations.items, TemplatedSimulations):
|
|
728
|
+
return self.simulations.items.check_duplicate(simulation_id)
|
|
729
|
+
else:
|
|
730
|
+
return False
|
|
731
|
+
|
|
732
|
+
def clear_directory_cache(self):
|
|
733
|
+
"""
|
|
734
|
+
Clear directory cache for Experiment and Simulations.
|
|
735
|
+
"""
|
|
736
|
+
from idmtools.core.context import get_current_platform
|
|
737
|
+
platform = get_current_platform()
|
|
738
|
+
|
|
739
|
+
# Skip COMPS Platform
|
|
740
|
+
if platform and hasattr(platform, 'job_directory'):
|
|
741
|
+
# Clear experiment directory cache
|
|
742
|
+
r1 = getattr(self, "_platform_directory", None)
|
|
743
|
+
if r1:
|
|
744
|
+
self._platform_directory = None
|
|
745
|
+
logger.warning(f"The Experiment {self.uid} referenced a temporary directory {r1}.")
|
|
746
|
+
|
|
747
|
+
# Clear Children's directory cache
|
|
748
|
+
if isinstance(self.simulations.items, (list, set)):
|
|
749
|
+
for sim in self.simulations.items:
|
|
750
|
+
sim._platform_directory = None
|
|
751
|
+
elif isinstance(self.simulations.items, TemplatedSimulations):
|
|
752
|
+
self.simulations.items.clear_extra_simulation_directory_cache()
|
|
753
|
+
else:
|
|
754
|
+
pass
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
class ExperimentSpecification(ExperimentPluginSpecification):
|
|
758
|
+
"""
|
|
759
|
+
ExperimentSpecification is the spec for Experiment plugins.
|
|
760
|
+
"""
|
|
761
|
+
|
|
762
|
+
@get_description_impl
|
|
763
|
+
def get_description(self) -> str:
|
|
764
|
+
"""
|
|
765
|
+
Description of our plugin.
|
|
766
|
+
|
|
767
|
+
Returns:
|
|
768
|
+
Description
|
|
769
|
+
"""
|
|
770
|
+
return "Provides access to the Local Platform to IDM Tools"
|
|
771
|
+
|
|
772
|
+
@get_model_impl
|
|
773
|
+
def get(self, configuration: dict) -> Experiment: # noqa: F821
|
|
774
|
+
"""
|
|
775
|
+
Get experiment with configuration.
|
|
776
|
+
"""
|
|
777
|
+
return Experiment(**configuration)
|
|
778
|
+
|
|
779
|
+
@get_model_type_impl
|
|
780
|
+
def get_type(self) -> Type[Experiment]:
|
|
781
|
+
"""
|
|
782
|
+
Return the experiment type.
|
|
783
|
+
|
|
784
|
+
Returns:
|
|
785
|
+
Experiment type.
|
|
786
|
+
"""
|
|
787
|
+
return Experiment
|