humalab 0.0.5__py3-none-any.whl → 0.0.7__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.
Potentially problematic release.
This version of humalab might be problematic. Click here for more details.
- humalab/__init__.py +25 -0
- humalab/assets/__init__.py +8 -2
- humalab/assets/files/resource_file.py +96 -6
- humalab/assets/files/urdf_file.py +49 -11
- humalab/assets/resource_operator.py +139 -0
- humalab/constants.py +48 -5
- humalab/dists/__init__.py +7 -0
- humalab/dists/bernoulli.py +26 -1
- humalab/dists/categorical.py +25 -0
- humalab/dists/discrete.py +27 -2
- humalab/dists/distribution.py +11 -0
- humalab/dists/gaussian.py +27 -2
- humalab/dists/log_uniform.py +29 -3
- humalab/dists/truncated_gaussian.py +33 -4
- humalab/dists/uniform.py +24 -0
- humalab/episode.py +291 -11
- humalab/humalab.py +93 -38
- humalab/humalab_api_client.py +297 -95
- humalab/humalab_config.py +49 -0
- humalab/humalab_test.py +46 -17
- humalab/metrics/__init__.py +11 -5
- humalab/metrics/code.py +59 -0
- humalab/metrics/metric.py +69 -102
- humalab/metrics/scenario_stats.py +163 -0
- humalab/metrics/summary.py +45 -24
- humalab/run.py +224 -101
- humalab/scenarios/__init__.py +11 -0
- humalab/{scenario.py → scenarios/scenario.py} +130 -136
- humalab/scenarios/scenario_operator.py +114 -0
- humalab/{scenario_test.py → scenarios/scenario_test.py} +150 -269
- humalab/utils.py +37 -0
- {humalab-0.0.5.dist-info → humalab-0.0.7.dist-info}/METADATA +1 -1
- humalab-0.0.7.dist-info/RECORD +39 -0
- humalab/assets/resource_manager.py +0 -58
- humalab/evaluators/__init__.py +0 -16
- humalab/humalab_main.py +0 -119
- humalab/metrics/dist_metric.py +0 -22
- humalab-0.0.5.dist-info/RECORD +0 -37
- {humalab-0.0.5.dist-info → humalab-0.0.7.dist-info}/WHEEL +0 -0
- {humalab-0.0.5.dist-info → humalab-0.0.7.dist-info}/entry_points.txt +0 -0
- {humalab-0.0.5.dist-info → humalab-0.0.7.dist-info}/licenses/LICENSE +0 -0
- {humalab-0.0.5.dist-info → humalab-0.0.7.dist-info}/top_level.txt +0 -0
humalab/dists/gaussian.py
CHANGED
|
@@ -4,6 +4,12 @@ import numpy as np
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class Gaussian(Distribution):
|
|
7
|
+
"""Gaussian (normal) distribution.
|
|
8
|
+
|
|
9
|
+
Samples values from a normal distribution with specified mean (loc) and
|
|
10
|
+
standard deviation (scale). Supports scalar outputs as well as multi-dimensional
|
|
11
|
+
arrays with 1D, 2D, or 3D variants.
|
|
12
|
+
"""
|
|
7
13
|
def __init__(self,
|
|
8
14
|
generator: np.random.Generator,
|
|
9
15
|
loc: float | Any,
|
|
@@ -25,6 +31,15 @@ class Gaussian(Distribution):
|
|
|
25
31
|
|
|
26
32
|
@staticmethod
|
|
27
33
|
def validate(dimensions: int, *args) -> bool:
|
|
34
|
+
"""Validate distribution parameters for the given dimensions.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
dimensions (int): The number of dimensions (0 for scalar, -1 for any).
|
|
38
|
+
*args: The distribution parameters (loc, scale).
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
bool: True if parameters are valid, False otherwise.
|
|
42
|
+
"""
|
|
28
43
|
arg1 = args[0]
|
|
29
44
|
arg2 = args[1]
|
|
30
45
|
if dimensions == 0:
|
|
@@ -37,18 +52,28 @@ class Gaussian(Distribution):
|
|
|
37
52
|
return True
|
|
38
53
|
if not isinstance(arg1, (int, float)):
|
|
39
54
|
if isinstance(arg1, (list, np.ndarray)):
|
|
40
|
-
if len(arg1)
|
|
55
|
+
if len(arg1) != dimensions:
|
|
41
56
|
return False
|
|
42
57
|
if not isinstance(arg2, (int, float)):
|
|
43
58
|
if isinstance(arg2, (list, np.ndarray)):
|
|
44
|
-
if len(arg2)
|
|
59
|
+
if len(arg2) != dimensions:
|
|
45
60
|
return False
|
|
46
61
|
return True
|
|
47
62
|
|
|
48
63
|
def _sample(self) -> int | float | np.ndarray:
|
|
64
|
+
"""Generate a sample from the Gaussian distribution.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
int | float | np.ndarray: Sampled value(s) from N(loc, scale).
|
|
68
|
+
"""
|
|
49
69
|
return self._generator.normal(loc=self._loc, scale=self._scale, size=self._size)
|
|
50
70
|
|
|
51
71
|
def __repr__(self) -> str:
|
|
72
|
+
"""String representation of the Gaussian distribution.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
str: String representation showing loc, scale, and size.
|
|
76
|
+
"""
|
|
52
77
|
return f"Gaussian(loc={self._loc}, scale={self._scale}, size={self._size})"
|
|
53
78
|
|
|
54
79
|
@staticmethod
|
humalab/dists/log_uniform.py
CHANGED
|
@@ -4,6 +4,13 @@ from typing import Any
|
|
|
4
4
|
import numpy as np
|
|
5
5
|
|
|
6
6
|
class LogUniform(Distribution):
|
|
7
|
+
"""Log-uniform distribution.
|
|
8
|
+
|
|
9
|
+
Samples values uniformly in log-space, useful for hyperparameters that
|
|
10
|
+
span multiple orders of magnitude (e.g., learning rates). The result is
|
|
11
|
+
exp(uniform(log(low), log(high))). Supports scalar outputs as well as
|
|
12
|
+
multi-dimensional arrays with 1D variants.
|
|
13
|
+
"""
|
|
7
14
|
def __init__(self,
|
|
8
15
|
generator: np.random.Generator,
|
|
9
16
|
low: float | Any,
|
|
@@ -25,6 +32,15 @@ class LogUniform(Distribution):
|
|
|
25
32
|
|
|
26
33
|
@staticmethod
|
|
27
34
|
def validate(dimensions: int, *args) -> bool:
|
|
35
|
+
"""Validate distribution parameters for the given dimensions.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
dimensions (int): The number of dimensions (0 for scalar, -1 for any).
|
|
39
|
+
*args: The distribution parameters (low, high).
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
bool: True if parameters are valid, False otherwise.
|
|
43
|
+
"""
|
|
28
44
|
arg1 = args[0]
|
|
29
45
|
arg2 = args[1]
|
|
30
46
|
if dimensions == 0:
|
|
@@ -37,18 +53,28 @@ class LogUniform(Distribution):
|
|
|
37
53
|
return True
|
|
38
54
|
if not isinstance(arg1, (int, float)):
|
|
39
55
|
if isinstance(arg1, (list, np.ndarray)):
|
|
40
|
-
if len(arg1)
|
|
56
|
+
if len(arg1) != dimensions:
|
|
41
57
|
return False
|
|
42
58
|
if not isinstance(arg2, (int, float)):
|
|
43
59
|
if isinstance(arg2, (list, np.ndarray)):
|
|
44
|
-
if len(arg2)
|
|
60
|
+
if len(arg2) != dimensions:
|
|
45
61
|
return False
|
|
46
62
|
return True
|
|
47
63
|
|
|
48
64
|
def _sample(self) -> int | float | np.ndarray:
|
|
65
|
+
"""Generate a sample from the log-uniform distribution.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
int | float | np.ndarray: Sampled value(s) in log-space.
|
|
69
|
+
"""
|
|
49
70
|
return np.exp(self._generator.uniform(self._log_low, self._log_high, size=self._size))
|
|
50
|
-
|
|
71
|
+
|
|
51
72
|
def __repr__(self) -> str:
|
|
73
|
+
"""String representation of the log-uniform distribution.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
str: String representation showing low, high, and size.
|
|
77
|
+
"""
|
|
52
78
|
return f"LogUniform(low={np.exp(self._log_low)}, high={np.exp(self._log_high)}, size={self._size})"
|
|
53
79
|
|
|
54
80
|
@staticmethod
|
|
@@ -4,6 +4,13 @@ import numpy as np
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class TruncatedGaussian(Distribution):
|
|
7
|
+
"""Truncated Gaussian (normal) distribution.
|
|
8
|
+
|
|
9
|
+
Samples values from a normal distribution with specified mean (loc) and
|
|
10
|
+
standard deviation (scale), but constrained to lie within [low, high].
|
|
11
|
+
Values outside the bounds are resampled until they fall within range.
|
|
12
|
+
Supports scalar outputs as well as multi-dimensional arrays with 1D, 2D, or 3D variants.
|
|
13
|
+
"""
|
|
7
14
|
def __init__(self,
|
|
8
15
|
generator: np.random.Generator,
|
|
9
16
|
loc: float | Any,
|
|
@@ -31,6 +38,15 @@ class TruncatedGaussian(Distribution):
|
|
|
31
38
|
|
|
32
39
|
@staticmethod
|
|
33
40
|
def validate(dimensions: int, *args) -> bool:
|
|
41
|
+
"""Validate distribution parameters for the given dimensions.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
dimensions (int): The number of dimensions (0 for scalar, -1 for any).
|
|
45
|
+
*args: The distribution parameters (loc, scale, low, high).
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
bool: True if parameters are valid, False otherwise.
|
|
49
|
+
"""
|
|
34
50
|
arg1 = args[0]
|
|
35
51
|
arg2 = args[1]
|
|
36
52
|
arg3 = args[2]
|
|
@@ -49,23 +65,31 @@ class TruncatedGaussian(Distribution):
|
|
|
49
65
|
return True
|
|
50
66
|
if not isinstance(arg1, (int, float)):
|
|
51
67
|
if isinstance(arg1, (list, np.ndarray)):
|
|
52
|
-
if len(arg1)
|
|
68
|
+
if len(arg1) != dimensions:
|
|
53
69
|
return False
|
|
54
70
|
if not isinstance(arg2, (int, float)):
|
|
55
71
|
if isinstance(arg2, (list, np.ndarray)):
|
|
56
|
-
if len(arg2)
|
|
72
|
+
if len(arg2) != dimensions:
|
|
57
73
|
return False
|
|
58
74
|
if not isinstance(arg3, (int, float)):
|
|
59
75
|
if isinstance(arg3, (list, np.ndarray)):
|
|
60
|
-
if len(arg3)
|
|
76
|
+
if len(arg3) != dimensions:
|
|
61
77
|
return False
|
|
62
78
|
if not isinstance(arg4, (int, float)):
|
|
63
79
|
if isinstance(arg4, (list, np.ndarray)):
|
|
64
|
-
if len(arg4)
|
|
80
|
+
if len(arg4) != dimensions:
|
|
65
81
|
return False
|
|
66
82
|
return True
|
|
67
83
|
|
|
68
84
|
def _sample(self) -> int | float | np.ndarray:
|
|
85
|
+
"""Generate a sample from the truncated Gaussian distribution.
|
|
86
|
+
|
|
87
|
+
Samples are generated from N(loc, scale) and resampled if they fall
|
|
88
|
+
outside [low, high].
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
int | float | np.ndarray: Sampled value(s) within [low, high].
|
|
92
|
+
"""
|
|
69
93
|
samples = self._generator.normal(loc=self._loc, scale=self._scale, size=self._size)
|
|
70
94
|
mask = (samples < self._low) | (samples > self._high)
|
|
71
95
|
while np.any(mask):
|
|
@@ -74,6 +98,11 @@ class TruncatedGaussian(Distribution):
|
|
|
74
98
|
return samples
|
|
75
99
|
|
|
76
100
|
def __repr__(self) -> str:
|
|
101
|
+
"""String representation of the truncated Gaussian distribution.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
str: String representation showing loc, scale, low, high, and size.
|
|
105
|
+
"""
|
|
77
106
|
return f"TruncatedGaussian(loc={self._loc}, scale={self._scale}, low={self._low}, high={self._high}, size={self._size})"
|
|
78
107
|
|
|
79
108
|
@staticmethod
|
humalab/dists/uniform.py
CHANGED
|
@@ -4,6 +4,11 @@ from typing import Any
|
|
|
4
4
|
import numpy as np
|
|
5
5
|
|
|
6
6
|
class Uniform(Distribution):
|
|
7
|
+
"""Uniform distribution over a continuous or discrete range.
|
|
8
|
+
|
|
9
|
+
Samples values uniformly from the half-open interval [low, high). Supports
|
|
10
|
+
scalar outputs as well as multi-dimensional arrays with 1D, 2D, or 3D variants.
|
|
11
|
+
"""
|
|
7
12
|
def __init__(self,
|
|
8
13
|
generator: np.random.Generator,
|
|
9
14
|
low: float | Any,
|
|
@@ -25,6 +30,15 @@ class Uniform(Distribution):
|
|
|
25
30
|
|
|
26
31
|
@staticmethod
|
|
27
32
|
def validate(dimensions: int, *args) -> bool:
|
|
33
|
+
"""Validate distribution parameters for the given dimensions.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
dimensions (int): The number of dimensions (0 for scalar, -1 for any).
|
|
37
|
+
*args: The distribution parameters (low, high).
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
bool: True if parameters are valid, False otherwise.
|
|
41
|
+
"""
|
|
28
42
|
arg1 = args[0]
|
|
29
43
|
arg2 = args[1]
|
|
30
44
|
if dimensions == 0:
|
|
@@ -46,9 +60,19 @@ class Uniform(Distribution):
|
|
|
46
60
|
return True
|
|
47
61
|
|
|
48
62
|
def _sample(self) -> int | float | np.ndarray:
|
|
63
|
+
"""Generate a sample from the uniform distribution.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
int | float | np.ndarray: Sampled value(s) from [low, high).
|
|
67
|
+
"""
|
|
49
68
|
return self._generator.uniform(self._low, self._high, size=self._size)
|
|
50
69
|
|
|
51
70
|
def __repr__(self) -> str:
|
|
71
|
+
"""String representation of the uniform distribution.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
str: String representation showing low, high, and size.
|
|
75
|
+
"""
|
|
52
76
|
return f"Uniform(low={self._low}, high={self._high}, size={self._size})"
|
|
53
77
|
|
|
54
78
|
@staticmethod
|
humalab/episode.py
CHANGED
|
@@ -1,20 +1,223 @@
|
|
|
1
|
+
from humalab.constants import RESERVED_NAMES, ArtifactType
|
|
2
|
+
from humalab.humalab_api_client import HumaLabApiClient, EpisodeStatus
|
|
3
|
+
from humalab.metrics.code import Code
|
|
4
|
+
from humalab.metrics.summary import Summary
|
|
5
|
+
from humalab.metrics.metric import Metrics
|
|
6
|
+
from omegaconf import DictConfig, ListConfig, OmegaConf
|
|
7
|
+
from typing import Any
|
|
8
|
+
import pickle
|
|
9
|
+
import traceback
|
|
1
10
|
|
|
2
|
-
from humalab.
|
|
3
|
-
from omegaconf import DictConfig, OmegaConf
|
|
11
|
+
from humalab.utils import is_standard_type
|
|
4
12
|
|
|
5
13
|
|
|
6
14
|
class Episode:
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
15
|
+
"""Represents a single episode within a run.
|
|
16
|
+
|
|
17
|
+
An Episode is a context manager that tracks a single execution instance of a
|
|
18
|
+
scenario. It provides access to scenario configuration values, supports metric
|
|
19
|
+
logging, and manages episode lifecycle with various completion statuses.
|
|
20
|
+
|
|
21
|
+
Episodes can be finished with different statuses:
|
|
22
|
+
- SUCCESS: Episode completed successfully
|
|
23
|
+
- FAILED: Episode failed
|
|
24
|
+
- CANCELED: Episode was discarded/canceled
|
|
25
|
+
- ERRORED: Episode encountered an error
|
|
26
|
+
|
|
27
|
+
Use as a context manager to automatically handle episode lifecycle:
|
|
28
|
+
with episode:
|
|
29
|
+
# Your code here
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
Attributes:
|
|
33
|
+
run_id (str): The unique identifier of the parent run.
|
|
34
|
+
episode_id (str): The unique identifier for this episode.
|
|
35
|
+
scenario (DictConfig | ListConfig): The resolved scenario configuration.
|
|
36
|
+
status (EpisodeStatus): The current status of the episode.
|
|
37
|
+
episode_vals (dict): The sampled values from scenario distributions.
|
|
38
|
+
is_finished (bool): Whether the episode has been finalized.
|
|
39
|
+
"""
|
|
40
|
+
def __init__(self,
|
|
41
|
+
run_id: str,
|
|
42
|
+
episode_id: str,
|
|
43
|
+
scenario_conf: DictConfig | ListConfig,
|
|
44
|
+
episode_vals: dict | None = None,
|
|
45
|
+
|
|
46
|
+
base_url: str | None = None,
|
|
47
|
+
api_key: str | None = None,
|
|
48
|
+
timeout: float | None = None,):
|
|
49
|
+
self._run_id = run_id
|
|
50
|
+
self._episode_id = episode_id
|
|
51
|
+
self._episode_status = EpisodeStatus.RUNNING
|
|
52
|
+
self._scenario_conf = scenario_conf
|
|
53
|
+
self._logs = {}
|
|
54
|
+
self._episode_vals = episode_vals or {}
|
|
55
|
+
self._is_finished = False
|
|
56
|
+
|
|
57
|
+
self._api_client = HumaLabApiClient(base_url=base_url,
|
|
58
|
+
api_key=api_key,
|
|
59
|
+
timeout=timeout)
|
|
11
60
|
|
|
12
61
|
@property
|
|
13
|
-
def
|
|
14
|
-
|
|
62
|
+
def run_id(self) -> str:
|
|
63
|
+
"""The unique identifier of the parent run.
|
|
15
64
|
|
|
16
|
-
|
|
17
|
-
|
|
65
|
+
Returns:
|
|
66
|
+
str: The run ID.
|
|
67
|
+
"""
|
|
68
|
+
return self._run_id
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def episode_id(self) -> str:
|
|
72
|
+
"""The unique identifier for this episode.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
str: The episode ID.
|
|
76
|
+
"""
|
|
77
|
+
return self._episode_id
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def scenario(self) -> DictConfig | ListConfig:
|
|
81
|
+
"""The resolved scenario configuration for this episode.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
DictConfig | ListConfig: The scenario configuration.
|
|
85
|
+
"""
|
|
86
|
+
return self._scenario_conf
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def status(self) -> EpisodeStatus:
|
|
90
|
+
"""The current status of the episode.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
EpisodeStatus: The episode status.
|
|
94
|
+
"""
|
|
95
|
+
return self._episode_status
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def episode_vals(self) -> dict:
|
|
99
|
+
"""The sampled values from scenario distributions.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
dict: Dictionary mapping scenario variable names to their sampled values.
|
|
103
|
+
"""
|
|
104
|
+
return self._episode_vals
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def is_finished(self) -> bool:
|
|
108
|
+
"""Whether the episode has been finalized.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
bool: True if the episode is finished, False otherwise.
|
|
112
|
+
"""
|
|
113
|
+
return self._is_finished
|
|
114
|
+
|
|
115
|
+
def __enter__(self):
|
|
116
|
+
"""Enter the episode context."""
|
|
117
|
+
return self
|
|
118
|
+
|
|
119
|
+
def __exit__(self, exception_type, exception_value, exception_traceback):
|
|
120
|
+
"""Exit the episode context and finalize the episode."""
|
|
121
|
+
if self._is_finished:
|
|
122
|
+
return
|
|
123
|
+
if exception_type is not None:
|
|
124
|
+
err_msg = "".join(traceback.format_exception(exception_type, exception_value, exception_traceback))
|
|
125
|
+
self.finish(status=EpisodeStatus.ERRORED, err_msg=err_msg)
|
|
126
|
+
else:
|
|
127
|
+
self.finish(status=EpisodeStatus.SUCCESS)
|
|
128
|
+
|
|
129
|
+
def __getattr__(self, name: Any) -> Any:
|
|
130
|
+
"""Access scenario configuration values as attributes.
|
|
131
|
+
|
|
132
|
+
Allows accessing scenario configuration using dot notation (e.g., episode.my_param).
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
name (Any): The attribute/key name from scenario configuration.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Any: The value from scenario configuration.
|
|
139
|
+
|
|
140
|
+
Raises:
|
|
141
|
+
AttributeError: If the attribute is not in scenario configuration.
|
|
142
|
+
"""
|
|
143
|
+
if name in self._scenario_conf:
|
|
144
|
+
return self._scenario_conf[name]
|
|
145
|
+
raise AttributeError(f"'Scenario' object has no attribute '{name}'")
|
|
146
|
+
|
|
147
|
+
def __getitem__(self, key: Any) -> Any:
|
|
148
|
+
"""Access scenario configuration values using subscript notation.
|
|
149
|
+
|
|
150
|
+
Allows accessing scenario configuration using bracket notation (e.g., episode['my_param']).
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
key (Any): The key name from scenario configuration.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
Any: The value from scenario configuration.
|
|
157
|
+
|
|
158
|
+
Raises:
|
|
159
|
+
KeyError: If the key is not in scenario configuration.
|
|
160
|
+
"""
|
|
161
|
+
if key in self._scenario_conf:
|
|
162
|
+
return self._scenario_conf[key]
|
|
163
|
+
raise KeyError(f"'Scenario' object has no key '{key}'")
|
|
164
|
+
|
|
165
|
+
def add_metric(self, name: str, metric: Metrics) -> None:
|
|
166
|
+
"""Add a metric to track for this episode.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
name (str): The name of the metric.
|
|
170
|
+
metric (Metrics): The metric instance to add.
|
|
171
|
+
|
|
172
|
+
Raises:
|
|
173
|
+
ValueError: If the name is already used.
|
|
174
|
+
"""
|
|
175
|
+
if name in self._logs:
|
|
176
|
+
raise ValueError(f"{name} is a reserved name and is not allowed.")
|
|
177
|
+
self._logs[name] = metric
|
|
178
|
+
|
|
179
|
+
def log_code(self, key: str, code_content: str) -> None:
|
|
180
|
+
"""Log code content as an artifact.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
key (str): The key for the code artifact.
|
|
184
|
+
code_content (str): The code content to log.
|
|
185
|
+
"""
|
|
186
|
+
if key in RESERVED_NAMES:
|
|
187
|
+
raise ValueError(f"{key} is a reserved name and is not allowed.")
|
|
188
|
+
self._logs[key] = Code(
|
|
189
|
+
run_id=self._run_id,
|
|
190
|
+
key=key,
|
|
191
|
+
code_content=code_content,
|
|
192
|
+
episode_id=self._episode_id
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
def log(self, data: dict, x: dict | None = None, replace: bool = False) -> None:
|
|
196
|
+
"""Log data points or values for the episode.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
data (dict): Dictionary of key-value pairs to log.
|
|
200
|
+
x (dict | None): Optional dictionary of x-axis values for each key.
|
|
201
|
+
replace (bool): Whether to replace existing values. Defaults to False.
|
|
202
|
+
|
|
203
|
+
Raises:
|
|
204
|
+
ValueError: If a key is reserved or logging fails.
|
|
205
|
+
"""
|
|
206
|
+
for key, value in data.items():
|
|
207
|
+
if key in RESERVED_NAMES:
|
|
208
|
+
raise ValueError(f"{key} is a reserved name and is not allowed.")
|
|
209
|
+
if key not in self._logs:
|
|
210
|
+
self._logs[key] = value
|
|
211
|
+
else:
|
|
212
|
+
cur_val = self._logs[key]
|
|
213
|
+
if isinstance(cur_val, Metrics):
|
|
214
|
+
cur_x = x.get(key) if x is not None else None
|
|
215
|
+
cur_val.log(value, x=cur_x, replace=replace)
|
|
216
|
+
else:
|
|
217
|
+
if replace:
|
|
218
|
+
self._logs[key] = value
|
|
219
|
+
else:
|
|
220
|
+
raise ValueError(f"Cannot log value for key '{key}' as there is already a value logged.")
|
|
18
221
|
|
|
19
222
|
@property
|
|
20
223
|
def yaml(self) -> str:
|
|
@@ -23,4 +226,81 @@ class Episode:
|
|
|
23
226
|
Returns:
|
|
24
227
|
str: The current scenario as a YAML string.
|
|
25
228
|
"""
|
|
26
|
-
return OmegaConf.to_yaml(self.
|
|
229
|
+
return OmegaConf.to_yaml(self._scenario_conf)
|
|
230
|
+
|
|
231
|
+
def discard(self) -> None:
|
|
232
|
+
"""Mark the episode as discarded/canceled."""
|
|
233
|
+
self._finish(EpisodeStatus.CANCELED)
|
|
234
|
+
|
|
235
|
+
def success(self) -> None:
|
|
236
|
+
"""Mark the episode as successfully completed."""
|
|
237
|
+
self._finish(EpisodeStatus.SUCCESS)
|
|
238
|
+
|
|
239
|
+
def fail(self) -> None:
|
|
240
|
+
"""Mark the episode as failed."""
|
|
241
|
+
self._finish(EpisodeStatus.FAILED)
|
|
242
|
+
|
|
243
|
+
def finish(self, status: EpisodeStatus, err_msg: str | None = None) -> None:
|
|
244
|
+
"""Finish the episode with a specific status.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
status (EpisodeStatus): The final status of the episode.
|
|
248
|
+
err_msg (str | None): Optional error message if the episode errored.
|
|
249
|
+
"""
|
|
250
|
+
if self._is_finished:
|
|
251
|
+
return
|
|
252
|
+
self._is_finished = True
|
|
253
|
+
self._episode_status = status
|
|
254
|
+
|
|
255
|
+
self._api_client.upload_code(
|
|
256
|
+
artifact_key="scenario",
|
|
257
|
+
run_id=self._run_id,
|
|
258
|
+
episode_id=self._episode_id,
|
|
259
|
+
code_content=self.yaml
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
# TODO: submit final metrics
|
|
263
|
+
for key, value in self._logs.items():
|
|
264
|
+
if isinstance(value, Summary):
|
|
265
|
+
metric_val = value.finalize()
|
|
266
|
+
pickled = pickle.dumps(metric_val["value"])
|
|
267
|
+
self._api_client.upload_python(
|
|
268
|
+
artifact_key=key,
|
|
269
|
+
run_id=self._run_id,
|
|
270
|
+
episode_id=self._episode_id,
|
|
271
|
+
pickled_bytes=pickled
|
|
272
|
+
)
|
|
273
|
+
elif isinstance(value, Metrics):
|
|
274
|
+
metric_val = value.finalize()
|
|
275
|
+
pickled = pickle.dumps(metric_val)
|
|
276
|
+
self._api_client.upload_metrics(
|
|
277
|
+
artifact_key=key,
|
|
278
|
+
run_id=self._run_id,
|
|
279
|
+
episode_id=self._episode_id,
|
|
280
|
+
pickled_bytes=pickled,
|
|
281
|
+
graph_type=value.graph_type.value,
|
|
282
|
+
)
|
|
283
|
+
elif isinstance(value, Code):
|
|
284
|
+
self._api_client.upload_code(
|
|
285
|
+
artifact_key=value.key,
|
|
286
|
+
run_id=value.run_id,
|
|
287
|
+
episode_id=value.episode_id,
|
|
288
|
+
code_content=value.code_content
|
|
289
|
+
)
|
|
290
|
+
else:
|
|
291
|
+
if not is_standard_type(value):
|
|
292
|
+
raise ValueError(f"Value for key '{key}' is not a standard type.")
|
|
293
|
+
pickled = pickle.dumps(value)
|
|
294
|
+
self._api_client.upload_python(
|
|
295
|
+
artifact_key=key,
|
|
296
|
+
run_id=self._run_id,
|
|
297
|
+
episode_id=self._episode_id,
|
|
298
|
+
pickled_bytes=pickled
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
self._api_client.update_episode(
|
|
302
|
+
run_id=self._run_id,
|
|
303
|
+
episode_id=self._episode_id,
|
|
304
|
+
status=status,
|
|
305
|
+
err_msg=err_msg
|
|
306
|
+
)
|