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.

Files changed (39) hide show
  1. humalab/__init__.py +11 -0
  2. humalab/assets/__init__.py +2 -2
  3. humalab/assets/files/resource_file.py +29 -3
  4. humalab/assets/files/urdf_file.py +14 -10
  5. humalab/assets/resource_operator.py +91 -0
  6. humalab/constants.py +39 -5
  7. humalab/dists/bernoulli.py +16 -0
  8. humalab/dists/categorical.py +4 -0
  9. humalab/dists/discrete.py +22 -0
  10. humalab/dists/gaussian.py +22 -0
  11. humalab/dists/log_uniform.py +22 -0
  12. humalab/dists/truncated_gaussian.py +36 -0
  13. humalab/dists/uniform.py +22 -0
  14. humalab/episode.py +196 -0
  15. humalab/humalab.py +116 -153
  16. humalab/humalab_api_client.py +760 -62
  17. humalab/humalab_config.py +0 -13
  18. humalab/humalab_test.py +46 -29
  19. humalab/metrics/__init__.py +5 -5
  20. humalab/metrics/code.py +28 -0
  21. humalab/metrics/metric.py +41 -108
  22. humalab/metrics/scenario_stats.py +95 -0
  23. humalab/metrics/summary.py +24 -18
  24. humalab/run.py +180 -115
  25. humalab/scenarios/__init__.py +4 -0
  26. humalab/scenarios/scenario.py +372 -0
  27. humalab/scenarios/scenario_operator.py +82 -0
  28. humalab/{scenario_test.py → scenarios/scenario_test.py} +150 -269
  29. humalab/utils.py +37 -0
  30. {humalab-0.0.4.dist-info → humalab-0.0.6.dist-info}/METADATA +1 -1
  31. humalab-0.0.6.dist-info/RECORD +39 -0
  32. humalab/assets/resource_manager.py +0 -57
  33. humalab/metrics/dist_metric.py +0 -22
  34. humalab/scenario.py +0 -225
  35. humalab-0.0.4.dist-info/RECORD +0 -34
  36. {humalab-0.0.4.dist-info → humalab-0.0.6.dist-info}/WHEEL +0 -0
  37. {humalab-0.0.4.dist-info → humalab-0.0.6.dist-info}/entry_points.txt +0 -0
  38. {humalab-0.0.4.dist-info → humalab-0.0.6.dist-info}/licenses/LICENSE +0 -0
  39. {humalab-0.0.4.dist-info → humalab-0.0.6.dist-info}/top_level.txt +0 -0
@@ -1,15 +1,11 @@
1
1
 
2
- from humalab.metrics.metric import MetricGranularity, Metrics, MetricType
3
- from humalab.constants import EpisodeStatus
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__(name, MetricType.SUMMARY, episode_id=episode_id, run_id=run_id, granularity=granularity)
30
- self.summary = summary
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 _submit(self) -> None:
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
- self._values = [self._values[-1]]
40
+ final_val = self._values[-1]
38
41
  elif self.summary == "first":
39
- self._values = [self._values[0]]
42
+ final_val = self._values[0]
40
43
  elif self.summary == "none":
41
- self._values = []
44
+ final_val = None
42
45
  elif self.summary in {"min", "max", "mean"}:
43
46
  if not self._values:
44
- self._values = []
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
- self._values = [agg_value]
55
+ final_val = agg_value
53
56
 
54
- super()._submit()
57
+ return {
58
+ "value": final_val,
59
+ "summary": self.summary
60
+ }
humalab/run.py CHANGED
@@ -1,26 +1,37 @@
1
1
  import uuid
2
- from humalab.metrics.dist_metric import DistributionMetric
3
- from humalab.metrics.metric import MetricGranularity, MetricType, Metrics
4
- from humalab.constants import EpisodeStatus
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
- from humalab.scenario import Scenario
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 finish(self,
118
- status: EpisodeStatus = EpisodeStatus.PASS,
119
- quiet: bool | None = None) -> None:
120
- """Finish the run and submit final metrics.
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 final status of the episode.
124
- quiet (bool | None): Whether to suppress output.
128
+ status (EpisodeStatus): The status of the current episode before reset.
125
129
  """
126
- self._finished = True
127
- self._scenario.finish()
128
- for metric in self._metrics.values():
129
- metric.finish(status=status)
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 log(self,
132
- data: dict,
133
- step: int | None = None,
134
- commit: bool = True,
135
- ) -> None:
136
- """Log metrics for the run.
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
- data (dict): A dictionary of metric names and their values.
140
- step (int | None): The step number for the metrics.
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 self._metrics:
145
- metric = self._metrics[key]
146
- metric.log(value, step=step, commit=commit)
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._metrics[key] = Metrics(key,
149
- metric_type=MetricType.DEFAULT,
150
- run_id=self._id,
151
- granularity=MetricGranularity.EPISODE,
152
- episode_id=self._episode)
153
- self._metrics[key].log(value, step=step, commit=commit)
154
-
155
- def reset(self, status: EpisodeStatus = EpisodeStatus.PASS) -> None:
156
- """Reset the run for a new episode.
157
-
158
- Args:
159
- status (EpisodeStatus): The status of the current episode before reset.
160
- """
161
- self._submit_episode_status(status=status, episode=self._episode)
162
- self._episode = str(uuid.uuid4())
163
- self._finished = False
164
- self._scenario.reset(episode_id=self._episode)
165
- for metric in self._metrics.values():
166
- metric.reset(episode=self._episode)
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
- name (str): The name of the metric.
183
- metric_type (MetricType): The type of the metric.
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 name not in self._metrics or replace:
193
- if metric_type == MetricType.DISTRIBUTION:
194
- if distribution_type is None:
195
- raise ValueError("distribution_type must be specified for distribution metrics.")
196
- self._metrics[name] = DistributionMetric(name=name,
197
- distribution_type=distribution_type,
198
- run_id=self._id,
199
- episode_id=self._episode,
200
- granularity=granularity)
201
- elif summary is not None:
202
- self._metrics[name] = Summary(name=name,
203
- summary=summary,
204
- run_id=self._id,
205
- episode_id=self._episode,
206
- granularity=granularity)
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
- self._metrics[name] = Metrics(name=name,
209
- metric_type=metric_type,
210
- run_id=self._id,
211
- episode_id=self._episode,
212
- granularity=granularity)
213
- else:
214
- raise ValueError(f"Metric {name} already exists.")
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
+ )
@@ -0,0 +1,4 @@
1
+ from .scenario import Scenario
2
+ from .scenario_operator import list_scenarios, get_scenario
3
+
4
+ __all__ = ["Scenario", "list_scenarios", "get_scenario"]