humalab 0.0.4__py3-none-any.whl → 0.0.6__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 +11 -0
- humalab/assets/__init__.py +2 -2
- humalab/assets/files/resource_file.py +29 -3
- humalab/assets/files/urdf_file.py +14 -10
- humalab/assets/resource_operator.py +91 -0
- humalab/constants.py +39 -5
- humalab/dists/bernoulli.py +16 -0
- humalab/dists/categorical.py +4 -0
- humalab/dists/discrete.py +22 -0
- humalab/dists/gaussian.py +22 -0
- humalab/dists/log_uniform.py +22 -0
- humalab/dists/truncated_gaussian.py +36 -0
- humalab/dists/uniform.py +22 -0
- humalab/episode.py +196 -0
- humalab/humalab.py +116 -153
- humalab/humalab_api_client.py +760 -62
- humalab/humalab_config.py +0 -13
- humalab/humalab_test.py +46 -29
- humalab/metrics/__init__.py +5 -5
- humalab/metrics/code.py +28 -0
- humalab/metrics/metric.py +41 -108
- humalab/metrics/scenario_stats.py +95 -0
- humalab/metrics/summary.py +24 -18
- humalab/run.py +180 -115
- humalab/scenarios/__init__.py +4 -0
- humalab/scenarios/scenario.py +372 -0
- humalab/scenarios/scenario_operator.py +82 -0
- humalab/{scenario_test.py → scenarios/scenario_test.py} +150 -269
- humalab/utils.py +37 -0
- {humalab-0.0.4.dist-info → humalab-0.0.6.dist-info}/METADATA +1 -1
- humalab-0.0.6.dist-info/RECORD +39 -0
- humalab/assets/resource_manager.py +0 -57
- humalab/metrics/dist_metric.py +0 -22
- humalab/scenario.py +0 -225
- humalab-0.0.4.dist-info/RECORD +0 -34
- {humalab-0.0.4.dist-info → humalab-0.0.6.dist-info}/WHEEL +0 -0
- {humalab-0.0.4.dist-info → humalab-0.0.6.dist-info}/entry_points.txt +0 -0
- {humalab-0.0.4.dist-info → humalab-0.0.6.dist-info}/licenses/LICENSE +0 -0
- {humalab-0.0.4.dist-info → humalab-0.0.6.dist-info}/top_level.txt +0 -0
humalab/metrics/summary.py
CHANGED
|
@@ -1,15 +1,11 @@
|
|
|
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
7
|
def __init__(self,
|
|
8
|
-
name: str,
|
|
9
8
|
summary: str,
|
|
10
|
-
episode_id: str,
|
|
11
|
-
run_id: str,
|
|
12
|
-
granularity: MetricGranularity = MetricGranularity.RUN,
|
|
13
9
|
) -> None:
|
|
14
10
|
"""
|
|
15
11
|
A summary metric that captures a single value per episode or run.
|
|
@@ -22,26 +18,33 @@ class Summary(Metrics):
|
|
|
22
18
|
from being generated.
|
|
23
19
|
granularity (MetricGranularity): The granularity of the metric.
|
|
24
20
|
"""
|
|
25
|
-
if granularity == MetricGranularity.RUN:
|
|
26
|
-
raise ValueError("Summary metrics cannot have RUN granularity.")
|
|
27
21
|
if summary not in {"min", "max", "mean", "last", "first", "none"}:
|
|
28
22
|
raise ValueError(f"Unsupported summary type: {summary}. Supported types are 'min', 'max', 'mean', 'last', 'first', and 'none'.")
|
|
29
|
-
super().__init__(
|
|
30
|
-
|
|
23
|
+
super().__init__(metric_dim_type= MetricDimType.ZERO_D,
|
|
24
|
+
graph_type=GraphType.NUMERIC)
|
|
25
|
+
self._summary = summary
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def summary(self) -> str:
|
|
29
|
+
return self._summary
|
|
31
30
|
|
|
32
|
-
def
|
|
31
|
+
def _finalize(self) -> dict:
|
|
33
32
|
if not self._values:
|
|
34
|
-
return
|
|
33
|
+
return {
|
|
34
|
+
"value": None,
|
|
35
|
+
"summary": self.summary
|
|
36
|
+
}
|
|
37
|
+
final_val = None
|
|
35
38
|
# For summary metrics, we only keep the latest value
|
|
36
39
|
if self.summary == "last":
|
|
37
|
-
|
|
40
|
+
final_val = self._values[-1]
|
|
38
41
|
elif self.summary == "first":
|
|
39
|
-
|
|
42
|
+
final_val = self._values[0]
|
|
40
43
|
elif self.summary == "none":
|
|
41
|
-
|
|
44
|
+
final_val = None
|
|
42
45
|
elif self.summary in {"min", "max", "mean"}:
|
|
43
46
|
if not self._values:
|
|
44
|
-
|
|
47
|
+
final_val = None
|
|
45
48
|
else:
|
|
46
49
|
if self.summary == "min":
|
|
47
50
|
agg_value = min(self._values)
|
|
@@ -49,6 +52,9 @@ class Summary(Metrics):
|
|
|
49
52
|
agg_value = max(self._values)
|
|
50
53
|
elif self.summary == "mean":
|
|
51
54
|
agg_value = sum(self._values) / len(self._values)
|
|
52
|
-
|
|
55
|
+
final_val = agg_value
|
|
53
56
|
|
|
54
|
-
|
|
57
|
+
return {
|
|
58
|
+
"value": final_val,
|
|
59
|
+
"summary": self.summary
|
|
60
|
+
}
|
humalab/run.py
CHANGED
|
@@ -1,26 +1,37 @@
|
|
|
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:
|
|
10
19
|
def __init__(self,
|
|
11
|
-
entity: str,
|
|
12
|
-
project: str,
|
|
13
20
|
scenario: Scenario,
|
|
21
|
+
project: str = DEFAULT_PROJECT,
|
|
14
22
|
name: str | None = None,
|
|
15
23
|
description: str | None = None,
|
|
16
24
|
id: str | None = None,
|
|
17
25
|
tags: list[str] | None = None,
|
|
26
|
+
|
|
27
|
+
base_url: str | None = None,
|
|
28
|
+
api_key: str | None = None,
|
|
29
|
+
timeout: float | None = None,
|
|
18
30
|
) -> None:
|
|
19
31
|
"""
|
|
20
32
|
Initialize a new Run instance.
|
|
21
33
|
|
|
22
34
|
Args:
|
|
23
|
-
entity (str): The entity (user or team) under which the run is created.
|
|
24
35
|
project (str): The project name under which the run is created.
|
|
25
36
|
scenario (Scenario): The scenario instance for the run.
|
|
26
37
|
name (str | None): The name of the run.
|
|
@@ -28,28 +39,21 @@ class Run:
|
|
|
28
39
|
id (str | None): The unique identifier for the run. If None, a UUID is generated.
|
|
29
40
|
tags (list[str] | None): A list of tags associated with the run.
|
|
30
41
|
"""
|
|
31
|
-
self._entity = entity
|
|
32
42
|
self._project = project
|
|
33
43
|
self._id = id or str(uuid.uuid4())
|
|
34
44
|
self._name = name or ""
|
|
35
45
|
self._description = description or ""
|
|
36
46
|
self._tags = tags or []
|
|
37
|
-
self._finished = False
|
|
38
|
-
|
|
39
|
-
self._episode = str(uuid.uuid4())
|
|
40
47
|
|
|
41
48
|
self._scenario = scenario
|
|
49
|
+
self._logs = {}
|
|
50
|
+
self._episodes = {}
|
|
51
|
+
self._is_finished = False
|
|
52
|
+
|
|
53
|
+
self._api_client = HumaLabApiClient(base_url=base_url,
|
|
54
|
+
api_key=api_key,
|
|
55
|
+
timeout=timeout)
|
|
42
56
|
|
|
43
|
-
self._metrics = {}
|
|
44
|
-
|
|
45
|
-
@property
|
|
46
|
-
def entity(self) -> str:
|
|
47
|
-
"""The entity (user or team) under which the run is created.
|
|
48
|
-
|
|
49
|
-
Returns:
|
|
50
|
-
str: The entity name.
|
|
51
|
-
"""
|
|
52
|
-
return self._entity
|
|
53
57
|
|
|
54
58
|
@property
|
|
55
59
|
def project(self) -> str:
|
|
@@ -96,15 +100,6 @@ class Run:
|
|
|
96
100
|
"""
|
|
97
101
|
return self._tags
|
|
98
102
|
|
|
99
|
-
@property
|
|
100
|
-
def episode(self) -> str:
|
|
101
|
-
"""The episode ID for the run.
|
|
102
|
-
|
|
103
|
-
Returns:
|
|
104
|
-
str: The episode ID.
|
|
105
|
-
"""
|
|
106
|
-
return self._episode
|
|
107
|
-
|
|
108
103
|
@property
|
|
109
104
|
def scenario(self) -> Scenario:
|
|
110
105
|
"""The scenario associated with the run.
|
|
@@ -113,102 +108,172 @@ class Run:
|
|
|
113
108
|
Scenario: The scenario instance.
|
|
114
109
|
"""
|
|
115
110
|
return self._scenario
|
|
111
|
+
|
|
112
|
+
def __enter__(self):
|
|
113
|
+
return self
|
|
116
114
|
|
|
117
|
-
def
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
115
|
+
def __exit__(self, exception_type, exception_value, exception_traceback):
|
|
116
|
+
if self._is_finished:
|
|
117
|
+
return
|
|
118
|
+
if exception_type is not None:
|
|
119
|
+
err_msg = "".join(traceback.format_exception(exception_type, exception_value, exception_traceback))
|
|
120
|
+
self.finish(status=RunStatus.ERRORED, err_msg=err_msg)
|
|
121
|
+
else:
|
|
122
|
+
self.finish()
|
|
123
|
+
|
|
124
|
+
def create_episode(self, episode_id: str | None = None) -> Episode:
|
|
125
|
+
"""Reset the run for a new episode.
|
|
121
126
|
|
|
122
127
|
Args:
|
|
123
|
-
status (EpisodeStatus): The
|
|
124
|
-
quiet (bool | None): Whether to suppress output.
|
|
128
|
+
status (EpisodeStatus): The status of the current episode before reset.
|
|
125
129
|
"""
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
+
episode = None
|
|
131
|
+
episode_id = episode_id or str(uuid.uuid4())
|
|
132
|
+
cur_scenario, episode_vals = self._scenario.resolve()
|
|
133
|
+
episode = Episode(run_id=self._id,
|
|
134
|
+
episode_id=episode_id,
|
|
135
|
+
scenario_conf=cur_scenario,
|
|
136
|
+
episode_vals=episode_vals)
|
|
137
|
+
self._handle_scenario_stats(episode, episode_vals)
|
|
138
|
+
|
|
139
|
+
return episode
|
|
140
|
+
|
|
141
|
+
def _handle_scenario_stats(self, episode: Episode, episode_vals: dict) -> None:
|
|
142
|
+
for metric_name, value in episode_vals.items():
|
|
143
|
+
if metric_name not in self._logs:
|
|
144
|
+
stat = ScenarioStats(name=metric_name,
|
|
145
|
+
distribution_type=value["distribution"],
|
|
146
|
+
metric_dim_type=value["metric_dim_type"],
|
|
147
|
+
graph_type=value["graph_type"])
|
|
148
|
+
self._logs[metric_name] = stat
|
|
149
|
+
self._logs[metric_name].log(data=value["value"],
|
|
150
|
+
x=episode.episode_id)
|
|
151
|
+
self._episodes[episode.episode_id] = episode
|
|
130
152
|
|
|
131
|
-
def
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
153
|
+
def add_metric(self, name: str, metric: Metrics) -> None:
|
|
154
|
+
if name in self._logs:
|
|
155
|
+
raise ValueError(f"{name} is a reserved name and is not allowed.")
|
|
156
|
+
self._logs[name] = metric
|
|
157
|
+
|
|
158
|
+
def log_code(self, key: str, code_content: str) -> None:
|
|
159
|
+
"""Log code content as an artifact.
|
|
137
160
|
|
|
138
161
|
Args:
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
commit (bool): Whether to commit the metrics immediately.
|
|
162
|
+
key (str): The key for the code artifact.
|
|
163
|
+
code_content (str): The code content to log.
|
|
142
164
|
"""
|
|
165
|
+
if key in RESERVED_NAMES:
|
|
166
|
+
raise ValueError(f"{key} is a reserved name and is not allowed.")
|
|
167
|
+
self._logs[key] = Code(
|
|
168
|
+
run_id=self._id,
|
|
169
|
+
key=key,
|
|
170
|
+
code_content=code_content,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def log(self, data: dict, x: dict | None = None, replace: bool = False) -> None:
|
|
143
175
|
for key, value in data.items():
|
|
144
|
-
if key in
|
|
145
|
-
|
|
146
|
-
|
|
176
|
+
if key in RESERVED_NAMES:
|
|
177
|
+
raise ValueError(f"{key} is a reserved name and is not allowed.")
|
|
178
|
+
if key not in self._logs:
|
|
179
|
+
self._logs[key] = value
|
|
147
180
|
else:
|
|
148
|
-
self.
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
def _submit_episode_status(self, status: EpisodeStatus, episode: str) -> None:
|
|
169
|
-
# TODO: Implement submission of episode status
|
|
170
|
-
pass
|
|
171
|
-
|
|
172
|
-
def define_metric(self,
|
|
173
|
-
name: str,
|
|
174
|
-
metric_type: MetricType = MetricType.DEFAULT,
|
|
175
|
-
granularity: MetricGranularity = MetricGranularity.RUN,
|
|
176
|
-
distribution_type: str | None = None,
|
|
177
|
-
summary: str | None = None,
|
|
178
|
-
replace: bool = False) -> None:
|
|
179
|
-
"""Define a new metric for the run.
|
|
181
|
+
cur_val = self._logs[key]
|
|
182
|
+
if isinstance(cur_val, Metrics):
|
|
183
|
+
cur_x = x.get(key) if x is not None else None
|
|
184
|
+
cur_val.log(value, x=cur_x, replace=replace)
|
|
185
|
+
else:
|
|
186
|
+
if replace:
|
|
187
|
+
self._logs[key] = value
|
|
188
|
+
else:
|
|
189
|
+
raise ValueError(f"Cannot log value for key '{key}' as there is already a value logged.")
|
|
190
|
+
def _finish_episodes(self,
|
|
191
|
+
status: RunStatus,
|
|
192
|
+
err_msg: str | None = None) -> None:
|
|
193
|
+
for episode in self._episodes.values():
|
|
194
|
+
if not episode.is_finished:
|
|
195
|
+
if status == RunStatus.FINISHED:
|
|
196
|
+
episode.finish(status=EpisodeStatus.CANCELED, err_msg=err_msg)
|
|
197
|
+
elif status == RunStatus.ERRORED:
|
|
198
|
+
episode.finish(status=EpisodeStatus.ERRORED, err_msg=err_msg)
|
|
199
|
+
elif status == RunStatus.CANCELED:
|
|
200
|
+
episode.finish(status=EpisodeStatus.CANCELED, err_msg=err_msg)
|
|
180
201
|
|
|
202
|
+
|
|
203
|
+
def finish(self,
|
|
204
|
+
status: RunStatus = RunStatus.FINISHED,
|
|
205
|
+
err_msg: str | None = None) -> None:
|
|
206
|
+
"""Finish the run and submit final metrics.
|
|
207
|
+
|
|
181
208
|
Args:
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
granularity (MetricGranularity): The granularity of the metric.
|
|
185
|
-
distribution_type (str | None): The type of distribution if metric_type is DISTRIBUTION.
|
|
186
|
-
summary (str | None): Specify aggregate metrics added to summary.
|
|
187
|
-
Supported aggregations include "min", "max", "mean", "last",
|
|
188
|
-
"first", and "none". "none" prevents a summary
|
|
189
|
-
from being generated.
|
|
190
|
-
replace (bool): Whether to replace the metric if it already exists.
|
|
209
|
+
status (RunStatus): The final status of the run.
|
|
210
|
+
err_msg (str | None): An optional error message.
|
|
191
211
|
"""
|
|
192
|
-
if
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
212
|
+
if self._is_finished:
|
|
213
|
+
return
|
|
214
|
+
self._is_finished = True
|
|
215
|
+
self._finish_episodes(status=status, err_msg=err_msg)
|
|
216
|
+
|
|
217
|
+
self._api_client.upload_code(
|
|
218
|
+
artifact_key="scenario",
|
|
219
|
+
run_id=self._id,
|
|
220
|
+
code_content=self.scenario.yaml
|
|
221
|
+
)
|
|
222
|
+
# TODO: submit final metrics
|
|
223
|
+
for key, value in self._logs.items():
|
|
224
|
+
if isinstance(value, ScenarioStats):
|
|
225
|
+
for episode_id, episode in self._episodes.items():
|
|
226
|
+
episode_status = episode.status
|
|
227
|
+
value.log_status(
|
|
228
|
+
episode_id=episode_id,
|
|
229
|
+
episode_status=episode_status
|
|
230
|
+
)
|
|
231
|
+
metric_val = value.finalize()
|
|
232
|
+
pickled = pickle.dumps(metric_val)
|
|
233
|
+
self._api_client.upload_scenario_stats_artifact(
|
|
234
|
+
artifact_key=key,
|
|
235
|
+
run_id=self._id,
|
|
236
|
+
pickled_bytes=pickled,
|
|
237
|
+
graph_type=value.graph_type.value,
|
|
238
|
+
metric_dim_type=value.metric_dim_type.value
|
|
239
|
+
)
|
|
240
|
+
elif isinstance(value, Summary):
|
|
241
|
+
metric_val = value.finalize()
|
|
242
|
+
pickled = pickle.dumps(metric_val["value"])
|
|
243
|
+
self._api_client.upload_python(
|
|
244
|
+
artifact_key=key,
|
|
245
|
+
run_id=self._id,
|
|
246
|
+
pickled_bytes=pickled
|
|
247
|
+
)
|
|
248
|
+
elif isinstance(value, Metrics):
|
|
249
|
+
metric_val = value.finalize()
|
|
250
|
+
pickled = pickle.dumps(metric_val)
|
|
251
|
+
self._api_client.upload_metrics(
|
|
252
|
+
artifact_key=key,
|
|
253
|
+
run_id=self._id,
|
|
254
|
+
pickled_bytes=pickled,
|
|
255
|
+
graph_type=value.graph_type.value,
|
|
256
|
+
metric_dim_type=value.metric_dim_type.value
|
|
257
|
+
)
|
|
258
|
+
elif isinstance(value, Code):
|
|
259
|
+
self._api_client.upload_code(
|
|
260
|
+
artifact_key=value.key,
|
|
261
|
+
run_id=value.run_id,
|
|
262
|
+
episode_id=value.episode_id,
|
|
263
|
+
code_content=value.code_content
|
|
264
|
+
)
|
|
207
265
|
else:
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
266
|
+
if not is_standard_type(value):
|
|
267
|
+
raise ValueError(f"Value for key '{key}' is not a standard type.")
|
|
268
|
+
pickled = pickle.dumps(value)
|
|
269
|
+
self._api_client.upload_python(
|
|
270
|
+
artifact_key=key,
|
|
271
|
+
run_id=self._id,
|
|
272
|
+
pickled_bytes=pickled
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
self._api_client.update_run(
|
|
276
|
+
run_id=self._id,
|
|
277
|
+
status=status,
|
|
278
|
+
err_msg=err_msg
|
|
279
|
+
)
|