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
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
from humalab.metrics.metric import Metrics
|
|
2
|
+
from humalab.constants import ArtifactType, GraphType, MetricDimType
|
|
3
|
+
from humalab.humalab_api_client import EpisodeStatus
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
SCENARIO_STATS_NEED_FLATTEN = {
|
|
8
|
+
"uniform_1d",
|
|
9
|
+
"bernoulli_1d",
|
|
10
|
+
"categorical_1d",
|
|
11
|
+
"discrete_1d",
|
|
12
|
+
"log_uniform_1d",
|
|
13
|
+
"gaussian_1d",
|
|
14
|
+
"truncated_gaussian_1d"
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
DISTRIBUTION_GRAPH_TYPE = {
|
|
19
|
+
# 0D distributions
|
|
20
|
+
"uniform": GraphType.HISTOGRAM,
|
|
21
|
+
"bernoulli": GraphType.BAR,
|
|
22
|
+
"categorical": GraphType.BAR,
|
|
23
|
+
"discrete": GraphType.BAR,
|
|
24
|
+
"log_uniform": GraphType.HISTOGRAM,
|
|
25
|
+
"gaussian": GraphType.GAUSSIAN,
|
|
26
|
+
"truncated_gaussian": GraphType.GAUSSIAN,
|
|
27
|
+
|
|
28
|
+
# 1D distributions
|
|
29
|
+
"uniform_1d": GraphType.HISTOGRAM,
|
|
30
|
+
"bernoulli_1d": GraphType.BAR,
|
|
31
|
+
"categorical_1d": GraphType.BAR,
|
|
32
|
+
"discrete_1d": GraphType.BAR,
|
|
33
|
+
"log_uniform_1d": GraphType.HISTOGRAM,
|
|
34
|
+
"gaussian_1d": GraphType.GAUSSIAN,
|
|
35
|
+
"truncated_gaussian_1d": GraphType.GAUSSIAN,
|
|
36
|
+
|
|
37
|
+
# 2D distributions
|
|
38
|
+
"uniform_2d": GraphType.SCATTER,
|
|
39
|
+
"gaussian_2d": GraphType.HEATMAP,
|
|
40
|
+
"truncated_gaussian_2d": GraphType.HEATMAP,
|
|
41
|
+
|
|
42
|
+
# 3D distributions
|
|
43
|
+
"uniform_3d": GraphType.THREE_D_MAP,
|
|
44
|
+
"gaussian_3d": GraphType.THREE_D_MAP,
|
|
45
|
+
"truncated_gaussian_3d": GraphType.THREE_D_MAP,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
class ScenarioStats(Metrics):
|
|
49
|
+
"""Metric to track scenario statistics across episodes.
|
|
50
|
+
|
|
51
|
+
This class logs sampled values from scenario distributions and tracks episode
|
|
52
|
+
statuses. It supports various distribution types and automatically handles
|
|
53
|
+
flattening for 1D distributions.
|
|
54
|
+
|
|
55
|
+
Attributes:
|
|
56
|
+
name (str): The name of the scenario statistic.
|
|
57
|
+
distribution_type (str): The type of distribution (e.g., 'uniform', 'gaussian').
|
|
58
|
+
artifact_type (ArtifactType): The artifact type, always SCENARIO_STATS.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def __init__(self,
|
|
62
|
+
name: str,
|
|
63
|
+
distribution_type: str,
|
|
64
|
+
) -> None:
|
|
65
|
+
super().__init__(
|
|
66
|
+
graph_type=DISTRIBUTION_GRAPH_TYPE[distribution_type]
|
|
67
|
+
)
|
|
68
|
+
self._name = name
|
|
69
|
+
self._distribution_type = distribution_type
|
|
70
|
+
self._artifact_type = ArtifactType.SCENARIO_STATS
|
|
71
|
+
self._values = {}
|
|
72
|
+
self._results = {}
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def name(self) -> str:
|
|
76
|
+
"""The name of the scenario statistic.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
str: The statistic name.
|
|
80
|
+
"""
|
|
81
|
+
return self._name
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def distribution_type(self) -> str:
|
|
85
|
+
"""The type of distribution used for this statistic.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
str: The distribution type (e.g., 'uniform', 'gaussian').
|
|
89
|
+
"""
|
|
90
|
+
return self._distribution_type
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def artifact_type(self) -> ArtifactType:
|
|
94
|
+
"""The artifact type, always SCENARIO_STATS.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
ArtifactType: The artifact type.
|
|
98
|
+
"""
|
|
99
|
+
return self._artifact_type
|
|
100
|
+
|
|
101
|
+
def log(self, data: Any, x: Any = None, replace: bool = False) -> None:
|
|
102
|
+
"""Log a sampled value from the scenario distribution.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
data (Any): The sampled value to log.
|
|
106
|
+
x (Any | None): The key/identifier for this sample (typically episode_id).
|
|
107
|
+
If None, auto-incrementing step is used.
|
|
108
|
+
replace (bool): Whether to replace an existing value. Defaults to False.
|
|
109
|
+
|
|
110
|
+
Raises:
|
|
111
|
+
ValueError: If data for the given x already exists and replace is False.
|
|
112
|
+
"""
|
|
113
|
+
if x in self._values:
|
|
114
|
+
if replace:
|
|
115
|
+
if self._distribution_type in SCENARIO_STATS_NEED_FLATTEN:
|
|
116
|
+
data = data[0]
|
|
117
|
+
self._values[x] = data
|
|
118
|
+
else:
|
|
119
|
+
raise ValueError(f"Data for episode_id {x} already exists. Use replace=True to overwrite.")
|
|
120
|
+
else:
|
|
121
|
+
if self._distribution_type in SCENARIO_STATS_NEED_FLATTEN:
|
|
122
|
+
data = data[0]
|
|
123
|
+
self._values[x] = data
|
|
124
|
+
|
|
125
|
+
def log_status(self,
|
|
126
|
+
episode_id: str,
|
|
127
|
+
episode_status: EpisodeStatus,
|
|
128
|
+
replace: bool = False) -> None:
|
|
129
|
+
"""Log the status of an episode.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
episode_id (str): The unique identifier of the episode.
|
|
133
|
+
episode_status (EpisodeStatus): The status of the episode.
|
|
134
|
+
replace (bool): Whether to replace an existing status for this episode.
|
|
135
|
+
Defaults to False.
|
|
136
|
+
|
|
137
|
+
Raises:
|
|
138
|
+
ValueError: If status for the episode_id already exists and replace is False.
|
|
139
|
+
"""
|
|
140
|
+
if episode_id in self._results:
|
|
141
|
+
if replace:
|
|
142
|
+
self._results[episode_id] = episode_status.value
|
|
143
|
+
else:
|
|
144
|
+
raise ValueError(f"Data for episode_id {episode_id} already exists. Use replace=True to overwrite.")
|
|
145
|
+
else:
|
|
146
|
+
self._results[episode_id] = episode_status.value
|
|
147
|
+
|
|
148
|
+
def _finalize(self) -> dict:
|
|
149
|
+
"""Finalize and return all collected scenario statistics.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
dict: Dictionary containing values, results, and distribution type.
|
|
153
|
+
"""
|
|
154
|
+
ret_val = {
|
|
155
|
+
"values": self._values,
|
|
156
|
+
"results": self._results,
|
|
157
|
+
"distribution_type": self._distribution_type,
|
|
158
|
+
}
|
|
159
|
+
self._values = {}
|
|
160
|
+
self._results = {}
|
|
161
|
+
return ret_val
|
|
162
|
+
|
|
163
|
+
|
humalab/metrics/summary.py
CHANGED
|
@@ -1,47 +1,65 @@
|
|
|
1
1
|
|
|
2
|
-
from humalab.metrics.metric import
|
|
3
|
-
from humalab.constants import
|
|
2
|
+
from humalab.metrics.metric import Metrics
|
|
3
|
+
from humalab.constants import MetricDimType, GraphType
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class Summary(Metrics):
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
"""A metric that aggregates logged values into a single summary statistic.
|
|
8
|
+
|
|
9
|
+
Summary metrics collect values throughout a run or episode and compute a single
|
|
10
|
+
aggregated result. Supported aggregation methods include min, max, mean, first,
|
|
11
|
+
last, and none (no aggregation).
|
|
12
|
+
|
|
13
|
+
Attributes:
|
|
14
|
+
summary (str): The aggregation method used.
|
|
15
|
+
"""
|
|
16
|
+
def __init__(self,
|
|
9
17
|
summary: str,
|
|
10
|
-
episode_id: str,
|
|
11
|
-
run_id: str,
|
|
12
|
-
granularity: MetricGranularity = MetricGranularity.RUN,
|
|
13
18
|
) -> None:
|
|
14
19
|
"""
|
|
15
20
|
A summary metric that captures a single value per episode or run.
|
|
16
21
|
|
|
17
22
|
Args:
|
|
18
|
-
|
|
19
|
-
summary (str | None): Specify aggregate metrics added to summary.
|
|
23
|
+
summary (str): Specify the aggregation method for the summary.
|
|
20
24
|
Supported aggregations include "min", "max", "mean", "last",
|
|
21
|
-
"first", and "none". "none" prevents a summary
|
|
22
|
-
from being generated.
|
|
23
|
-
granularity (MetricGranularity): The granularity of the metric.
|
|
25
|
+
"first", and "none". "none" prevents a summary from being generated.
|
|
24
26
|
"""
|
|
25
|
-
if granularity == MetricGranularity.RUN:
|
|
26
|
-
raise ValueError("Summary metrics cannot have RUN granularity.")
|
|
27
27
|
if summary not in {"min", "max", "mean", "last", "first", "none"}:
|
|
28
28
|
raise ValueError(f"Unsupported summary type: {summary}. Supported types are 'min', 'max', 'mean', 'last', 'first', and 'none'.")
|
|
29
|
-
super().__init__(
|
|
30
|
-
self.
|
|
29
|
+
super().__init__(graph_type=GraphType.NUMERIC)
|
|
30
|
+
self._summary = summary
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def summary(self) -> str:
|
|
34
|
+
"""The aggregation method for this summary metric.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
str: The summary type (e.g., 'min', 'max', 'mean').
|
|
38
|
+
"""
|
|
39
|
+
return self._summary
|
|
40
|
+
|
|
41
|
+
def _finalize(self) -> dict:
|
|
42
|
+
"""Compute the final aggregated value.
|
|
31
43
|
|
|
32
|
-
|
|
44
|
+
Returns:
|
|
45
|
+
dict: Dictionary containing the aggregated value and summary type.
|
|
46
|
+
"""
|
|
33
47
|
if not self._values:
|
|
34
|
-
return
|
|
48
|
+
return {
|
|
49
|
+
"value": None,
|
|
50
|
+
"summary": self.summary
|
|
51
|
+
}
|
|
52
|
+
final_val = None
|
|
35
53
|
# For summary metrics, we only keep the latest value
|
|
36
54
|
if self.summary == "last":
|
|
37
|
-
|
|
55
|
+
final_val = self._values[-1]
|
|
38
56
|
elif self.summary == "first":
|
|
39
|
-
|
|
57
|
+
final_val = self._values[0]
|
|
40
58
|
elif self.summary == "none":
|
|
41
|
-
|
|
59
|
+
final_val = None
|
|
42
60
|
elif self.summary in {"min", "max", "mean"}:
|
|
43
61
|
if not self._values:
|
|
44
|
-
|
|
62
|
+
final_val = None
|
|
45
63
|
else:
|
|
46
64
|
if self.summary == "min":
|
|
47
65
|
agg_value = min(self._values)
|
|
@@ -49,6 +67,9 @@ class Summary(Metrics):
|
|
|
49
67
|
agg_value = max(self._values)
|
|
50
68
|
elif self.summary == "mean":
|
|
51
69
|
agg_value = sum(self._values) / len(self._values)
|
|
52
|
-
|
|
70
|
+
final_val = agg_value
|
|
53
71
|
|
|
54
|
-
|
|
72
|
+
return {
|
|
73
|
+
"value": final_val,
|
|
74
|
+
"summary": self.summary
|
|
75
|
+
}
|
humalab/run.py
CHANGED
|
@@ -1,19 +1,51 @@
|
|
|
1
1
|
import uuid
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
import traceback
|
|
3
|
+
import pickle
|
|
4
|
+
import base64
|
|
5
5
|
|
|
6
|
+
from humalab.metrics.code import Code
|
|
6
7
|
from humalab.metrics.summary import Summary
|
|
7
|
-
|
|
8
|
+
|
|
9
|
+
from humalab.constants import DEFAULT_PROJECT, RESERVED_NAMES, ArtifactType
|
|
10
|
+
from humalab.metrics.scenario_stats import ScenarioStats
|
|
11
|
+
from humalab.humalab_api_client import EpisodeStatus, HumaLabApiClient, RunStatus
|
|
12
|
+
from humalab.metrics.metric import Metrics
|
|
13
|
+
from humalab.episode import Episode
|
|
14
|
+
from humalab.utils import is_standard_type
|
|
15
|
+
|
|
16
|
+
from humalab.scenarios.scenario import Scenario
|
|
8
17
|
|
|
9
18
|
class Run:
|
|
19
|
+
"""Represents a run containing multiple episodes for a scenario.
|
|
20
|
+
|
|
21
|
+
A Run is a context manager that tracks experiments or evaluations using a specific
|
|
22
|
+
scenario. It manages episode creation, metric logging, and code artifacts. The run
|
|
23
|
+
can contain multiple episodes, each representing a single execution instance.
|
|
24
|
+
|
|
25
|
+
Use as a context manager to automatically handle run lifecycle:
|
|
26
|
+
with Run(scenario=my_scenario) as run:
|
|
27
|
+
# Your code here
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
Attributes:
|
|
31
|
+
project (str): The project name under which the run is created.
|
|
32
|
+
id (str): The unique identifier for the run.
|
|
33
|
+
name (str): The name of the run.
|
|
34
|
+
description (str): A description of the run.
|
|
35
|
+
tags (list[str]): A list of tags associated with the run.
|
|
36
|
+
scenario (Scenario): The scenario associated with the run.
|
|
37
|
+
"""
|
|
10
38
|
def __init__(self,
|
|
11
|
-
project: str,
|
|
12
39
|
scenario: Scenario,
|
|
40
|
+
project: str = DEFAULT_PROJECT,
|
|
13
41
|
name: str | None = None,
|
|
14
42
|
description: str | None = None,
|
|
15
43
|
id: str | None = None,
|
|
16
44
|
tags: list[str] | None = None,
|
|
45
|
+
|
|
46
|
+
base_url: str | None = None,
|
|
47
|
+
api_key: str | None = None,
|
|
48
|
+
timeout: float | None = None,
|
|
17
49
|
) -> None:
|
|
18
50
|
"""
|
|
19
51
|
Initialize a new Run instance.
|
|
@@ -31,13 +63,16 @@ class Run:
|
|
|
31
63
|
self._name = name or ""
|
|
32
64
|
self._description = description or ""
|
|
33
65
|
self._tags = tags or []
|
|
34
|
-
self._finished = False
|
|
35
|
-
|
|
36
|
-
self._episode = str(uuid.uuid4())
|
|
37
66
|
|
|
38
67
|
self._scenario = scenario
|
|
68
|
+
self._logs = {}
|
|
69
|
+
self._episodes = {}
|
|
70
|
+
self._is_finished = False
|
|
71
|
+
|
|
72
|
+
self._api_client = HumaLabApiClient(base_url=base_url,
|
|
73
|
+
api_key=api_key,
|
|
74
|
+
timeout=timeout)
|
|
39
75
|
|
|
40
|
-
self._metrics = {}
|
|
41
76
|
|
|
42
77
|
@property
|
|
43
78
|
def project(self) -> str:
|
|
@@ -84,15 +119,6 @@ class Run:
|
|
|
84
119
|
"""
|
|
85
120
|
return self._tags
|
|
86
121
|
|
|
87
|
-
@property
|
|
88
|
-
def episode(self) -> str:
|
|
89
|
-
"""The episode ID for the run.
|
|
90
|
-
|
|
91
|
-
Returns:
|
|
92
|
-
str: The episode ID.
|
|
93
|
-
"""
|
|
94
|
-
return self._episode
|
|
95
|
-
|
|
96
122
|
@property
|
|
97
123
|
def scenario(self) -> Scenario:
|
|
98
124
|
"""The scenario associated with the run.
|
|
@@ -101,102 +127,199 @@ class Run:
|
|
|
101
127
|
Scenario: The scenario instance.
|
|
102
128
|
"""
|
|
103
129
|
return self._scenario
|
|
130
|
+
|
|
131
|
+
def __enter__(self):
|
|
132
|
+
"""Enter the run context."""
|
|
133
|
+
return self
|
|
104
134
|
|
|
105
|
-
def
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
135
|
+
def __exit__(self, exception_type, exception_value, exception_traceback):
|
|
136
|
+
"""Exit the run context and finalize the run."""
|
|
137
|
+
if self._is_finished:
|
|
138
|
+
return
|
|
139
|
+
if exception_type is not None:
|
|
140
|
+
err_msg = "".join(traceback.format_exception(exception_type, exception_value, exception_traceback))
|
|
141
|
+
self.finish(status=RunStatus.ERRORED, err_msg=err_msg)
|
|
142
|
+
else:
|
|
143
|
+
self.finish()
|
|
144
|
+
|
|
145
|
+
def create_episode(self, episode_id: str | None = None) -> Episode:
|
|
146
|
+
"""Create a new episode for this run.
|
|
109
147
|
|
|
110
148
|
Args:
|
|
111
|
-
|
|
112
|
-
|
|
149
|
+
episode_id (str | None): Optional unique identifier for the episode.
|
|
150
|
+
If None, a UUID is generated automatically.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Episode: The newly created episode instance.
|
|
113
154
|
"""
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
155
|
+
episode = None
|
|
156
|
+
episode_id = episode_id or str(uuid.uuid4())
|
|
157
|
+
cur_scenario, episode_vals = self._scenario.resolve()
|
|
158
|
+
episode = Episode(run_id=self._id,
|
|
159
|
+
episode_id=episode_id,
|
|
160
|
+
scenario_conf=cur_scenario,
|
|
161
|
+
episode_vals=episode_vals)
|
|
162
|
+
self._handle_scenario_stats(episode, episode_vals)
|
|
163
|
+
|
|
164
|
+
return episode
|
|
165
|
+
|
|
166
|
+
def _handle_scenario_stats(self, episode: Episode, episode_vals: dict) -> None:
|
|
167
|
+
for metric_name, value in episode_vals.items():
|
|
168
|
+
if metric_name not in self._logs:
|
|
169
|
+
stat = ScenarioStats(name=metric_name,
|
|
170
|
+
distribution_type=value["distribution"])
|
|
171
|
+
self._logs[metric_name] = stat
|
|
172
|
+
self._logs[metric_name].log(data=value["value"],
|
|
173
|
+
x=episode.episode_id)
|
|
174
|
+
self._episodes[episode.episode_id] = episode
|
|
118
175
|
|
|
119
|
-
def
|
|
120
|
-
|
|
121
|
-
step: int | None = None,
|
|
122
|
-
commit: bool = True,
|
|
123
|
-
) -> None:
|
|
124
|
-
"""Log metrics for the run.
|
|
176
|
+
def add_metric(self, name: str, metric: Metrics) -> None:
|
|
177
|
+
"""Add a metric to track for this run.
|
|
125
178
|
|
|
126
179
|
Args:
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
180
|
+
name (str): The name of the metric.
|
|
181
|
+
metric (Metrics): The metric instance to add.
|
|
182
|
+
|
|
183
|
+
Raises:
|
|
184
|
+
ValueError: If the name is already used.
|
|
130
185
|
"""
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
metric.log(value, step=step, commit=commit)
|
|
135
|
-
else:
|
|
136
|
-
self._metrics[key] = Metrics(key,
|
|
137
|
-
metric_type=MetricType.DEFAULT,
|
|
138
|
-
run_id=self._id,
|
|
139
|
-
granularity=MetricGranularity.EPISODE,
|
|
140
|
-
episode_id=self._episode)
|
|
141
|
-
self._metrics[key].log(value, step=step, commit=commit)
|
|
186
|
+
if name in self._logs:
|
|
187
|
+
raise ValueError(f"{name} is a reserved name and is not allowed.")
|
|
188
|
+
self._logs[name] = metric
|
|
142
189
|
|
|
143
|
-
def
|
|
144
|
-
"""
|
|
190
|
+
def log_code(self, key: str, code_content: str) -> None:
|
|
191
|
+
"""Log code content as an artifact.
|
|
145
192
|
|
|
146
193
|
Args:
|
|
147
|
-
|
|
194
|
+
key (str): The key for the code artifact.
|
|
195
|
+
code_content (str): The code content to log.
|
|
148
196
|
"""
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
self.
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
# TODO: Implement submission of episode status
|
|
158
|
-
pass
|
|
159
|
-
|
|
160
|
-
def define_metric(self,
|
|
161
|
-
name: str,
|
|
162
|
-
metric_type: MetricType = MetricType.DEFAULT,
|
|
163
|
-
granularity: MetricGranularity = MetricGranularity.RUN,
|
|
164
|
-
distribution_type: str | None = None,
|
|
165
|
-
summary: str | None = None,
|
|
166
|
-
replace: bool = False) -> None:
|
|
167
|
-
"""Define a new metric for the run.
|
|
197
|
+
if key in RESERVED_NAMES:
|
|
198
|
+
raise ValueError(f"{key} is a reserved name and is not allowed.")
|
|
199
|
+
self._logs[key] = Code(
|
|
200
|
+
run_id=self._id,
|
|
201
|
+
key=key,
|
|
202
|
+
code_content=code_content,
|
|
203
|
+
)
|
|
204
|
+
|
|
168
205
|
|
|
206
|
+
def log(self, data: dict, x: dict | None = None, replace: bool = False) -> None:
|
|
207
|
+
"""Log data points or values for the run.
|
|
208
|
+
|
|
169
209
|
Args:
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
"first", and "none". "none" prevents a summary
|
|
177
|
-
from being generated.
|
|
178
|
-
replace (bool): Whether to replace the metric if it already exists.
|
|
210
|
+
data (dict): Dictionary of key-value pairs to log.
|
|
211
|
+
x (dict | None): Optional dictionary of x-axis values for each key.
|
|
212
|
+
replace (bool): Whether to replace existing values. Defaults to False.
|
|
213
|
+
|
|
214
|
+
Raises:
|
|
215
|
+
ValueError: If a key is reserved or logging fails.
|
|
179
216
|
"""
|
|
180
|
-
|
|
181
|
-
if
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
self.
|
|
185
|
-
distribution_type=distribution_type,
|
|
186
|
-
run_id=self._id,
|
|
187
|
-
episode_id=self._episode,
|
|
188
|
-
granularity=granularity)
|
|
189
|
-
elif summary is not None:
|
|
190
|
-
self._metrics[name] = Summary(name=name,
|
|
191
|
-
summary=summary,
|
|
192
|
-
run_id=self._id,
|
|
193
|
-
episode_id=self._episode,
|
|
194
|
-
granularity=granularity)
|
|
217
|
+
for key, value in data.items():
|
|
218
|
+
if key in RESERVED_NAMES:
|
|
219
|
+
raise ValueError(f"{key} is a reserved name and is not allowed.")
|
|
220
|
+
if key not in self._logs:
|
|
221
|
+
self._logs[key] = value
|
|
195
222
|
else:
|
|
196
|
-
self.
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
223
|
+
cur_val = self._logs[key]
|
|
224
|
+
if isinstance(cur_val, Metrics):
|
|
225
|
+
cur_x = x.get(key) if x is not None else None
|
|
226
|
+
cur_val.log(value, x=cur_x, replace=replace)
|
|
227
|
+
else:
|
|
228
|
+
if replace:
|
|
229
|
+
self._logs[key] = value
|
|
230
|
+
else:
|
|
231
|
+
raise ValueError(f"Cannot log value for key '{key}' as there is already a value logged.")
|
|
232
|
+
def _finish_episodes(self,
|
|
233
|
+
status: RunStatus,
|
|
234
|
+
err_msg: str | None = None) -> None:
|
|
235
|
+
for episode in self._episodes.values():
|
|
236
|
+
if not episode.is_finished:
|
|
237
|
+
if status == RunStatus.FINISHED:
|
|
238
|
+
episode.finish(status=EpisodeStatus.CANCELED, err_msg=err_msg)
|
|
239
|
+
elif status == RunStatus.ERRORED:
|
|
240
|
+
episode.finish(status=EpisodeStatus.ERRORED, err_msg=err_msg)
|
|
241
|
+
elif status == RunStatus.CANCELED:
|
|
242
|
+
episode.finish(status=EpisodeStatus.CANCELED, err_msg=err_msg)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def finish(self,
|
|
246
|
+
status: RunStatus = RunStatus.FINISHED,
|
|
247
|
+
err_msg: str | None = None) -> None:
|
|
248
|
+
"""Finish the run and submit final metrics.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
status (RunStatus): The final status of the run.
|
|
252
|
+
err_msg (str | None): An optional error message.
|
|
253
|
+
"""
|
|
254
|
+
if self._is_finished:
|
|
255
|
+
return
|
|
256
|
+
self._is_finished = True
|
|
257
|
+
self._finish_episodes(status=status, err_msg=err_msg)
|
|
258
|
+
|
|
259
|
+
self._api_client.upload_code(
|
|
260
|
+
artifact_key="scenario",
|
|
261
|
+
run_id=self._id,
|
|
262
|
+
code_content=self.scenario.yaml
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
self._api_client.upload_python(
|
|
266
|
+
artifact_key="seed",
|
|
267
|
+
run_id=self._id,
|
|
268
|
+
pickled_bytes=pickle.dumps(self.scenario.seed)
|
|
269
|
+
)
|
|
270
|
+
# TODO: submit final metrics
|
|
271
|
+
for key, value in self._logs.items():
|
|
272
|
+
if isinstance(value, ScenarioStats):
|
|
273
|
+
for episode_id, episode in self._episodes.items():
|
|
274
|
+
episode_status = episode.status
|
|
275
|
+
value.log_status(
|
|
276
|
+
episode_id=episode_id,
|
|
277
|
+
episode_status=episode_status
|
|
278
|
+
)
|
|
279
|
+
metric_val = value.finalize()
|
|
280
|
+
pickled = pickle.dumps(metric_val)
|
|
281
|
+
self._api_client.upload_scenario_stats_artifact(
|
|
282
|
+
artifact_key=key,
|
|
283
|
+
run_id=self._id,
|
|
284
|
+
pickled_bytes=pickled,
|
|
285
|
+
graph_type=value.graph_type.value,
|
|
286
|
+
)
|
|
287
|
+
elif isinstance(value, Summary):
|
|
288
|
+
metric_val = value.finalize()
|
|
289
|
+
pickled = pickle.dumps(metric_val["value"])
|
|
290
|
+
self._api_client.upload_python(
|
|
291
|
+
artifact_key=key,
|
|
292
|
+
run_id=self._id,
|
|
293
|
+
pickled_bytes=pickled
|
|
294
|
+
)
|
|
295
|
+
elif isinstance(value, Metrics):
|
|
296
|
+
metric_val = value.finalize()
|
|
297
|
+
pickled = pickle.dumps(metric_val)
|
|
298
|
+
self._api_client.upload_metrics(
|
|
299
|
+
artifact_key=key,
|
|
300
|
+
run_id=self._id,
|
|
301
|
+
pickled_bytes=pickled,
|
|
302
|
+
graph_type=value.graph_type.value,
|
|
303
|
+
)
|
|
304
|
+
elif isinstance(value, Code):
|
|
305
|
+
self._api_client.upload_code(
|
|
306
|
+
artifact_key=value.key,
|
|
307
|
+
run_id=value.run_id,
|
|
308
|
+
episode_id=value.episode_id,
|
|
309
|
+
code_content=value.code_content
|
|
310
|
+
)
|
|
311
|
+
else:
|
|
312
|
+
if not is_standard_type(value):
|
|
313
|
+
raise ValueError(f"Value for key '{key}' is not a standard type.")
|
|
314
|
+
pickled = pickle.dumps(value)
|
|
315
|
+
self._api_client.upload_python(
|
|
316
|
+
artifact_key=key,
|
|
317
|
+
run_id=self._id,
|
|
318
|
+
pickled_bytes=pickled
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
self._api_client.update_run(
|
|
322
|
+
run_id=self._id,
|
|
323
|
+
status=status,
|
|
324
|
+
err_msg=err_msg
|
|
325
|
+
)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Scenario management and configuration.
|
|
2
|
+
|
|
3
|
+
This module provides the Scenario class and related utilities for managing scenario
|
|
4
|
+
configurations with probabilistic distributions, supporting randomized scenario generation
|
|
5
|
+
for robotics experiments.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .scenario import Scenario
|
|
9
|
+
from .scenario_operator import list_scenarios, get_scenario
|
|
10
|
+
|
|
11
|
+
__all__ = ["Scenario", "list_scenarios", "get_scenario"]
|