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.

Files changed (42) hide show
  1. humalab/__init__.py +25 -0
  2. humalab/assets/__init__.py +8 -2
  3. humalab/assets/files/resource_file.py +96 -6
  4. humalab/assets/files/urdf_file.py +49 -11
  5. humalab/assets/resource_operator.py +139 -0
  6. humalab/constants.py +48 -5
  7. humalab/dists/__init__.py +7 -0
  8. humalab/dists/bernoulli.py +26 -1
  9. humalab/dists/categorical.py +25 -0
  10. humalab/dists/discrete.py +27 -2
  11. humalab/dists/distribution.py +11 -0
  12. humalab/dists/gaussian.py +27 -2
  13. humalab/dists/log_uniform.py +29 -3
  14. humalab/dists/truncated_gaussian.py +33 -4
  15. humalab/dists/uniform.py +24 -0
  16. humalab/episode.py +291 -11
  17. humalab/humalab.py +93 -38
  18. humalab/humalab_api_client.py +297 -95
  19. humalab/humalab_config.py +49 -0
  20. humalab/humalab_test.py +46 -17
  21. humalab/metrics/__init__.py +11 -5
  22. humalab/metrics/code.py +59 -0
  23. humalab/metrics/metric.py +69 -102
  24. humalab/metrics/scenario_stats.py +163 -0
  25. humalab/metrics/summary.py +45 -24
  26. humalab/run.py +224 -101
  27. humalab/scenarios/__init__.py +11 -0
  28. humalab/{scenario.py → scenarios/scenario.py} +130 -136
  29. humalab/scenarios/scenario_operator.py +114 -0
  30. humalab/{scenario_test.py → scenarios/scenario_test.py} +150 -269
  31. humalab/utils.py +37 -0
  32. {humalab-0.0.5.dist-info → humalab-0.0.7.dist-info}/METADATA +1 -1
  33. humalab-0.0.7.dist-info/RECORD +39 -0
  34. humalab/assets/resource_manager.py +0 -58
  35. humalab/evaluators/__init__.py +0 -16
  36. humalab/humalab_main.py +0 -119
  37. humalab/metrics/dist_metric.py +0 -22
  38. humalab-0.0.5.dist-info/RECORD +0 -37
  39. {humalab-0.0.5.dist-info → humalab-0.0.7.dist-info}/WHEEL +0 -0
  40. {humalab-0.0.5.dist-info → humalab-0.0.7.dist-info}/entry_points.txt +0 -0
  41. {humalab-0.0.5.dist-info → humalab-0.0.7.dist-info}/licenses/LICENSE +0 -0
  42. {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
+
@@ -1,47 +1,65 @@
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
- def __init__(self,
8
- name: str,
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
- name (str): The name of the metric.
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__(name, MetricType.SUMMARY, episode_id=episode_id, run_id=run_id, granularity=granularity)
30
- self.summary = summary
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
- def _submit(self) -> None:
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
- self._values = [self._values[-1]]
55
+ final_val = self._values[-1]
38
56
  elif self.summary == "first":
39
- self._values = [self._values[0]]
57
+ final_val = self._values[0]
40
58
  elif self.summary == "none":
41
- self._values = []
59
+ final_val = None
42
60
  elif self.summary in {"min", "max", "mean"}:
43
61
  if not self._values:
44
- self._values = []
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
- self._values = [agg_value]
70
+ final_val = agg_value
53
71
 
54
- super()._submit()
72
+ return {
73
+ "value": final_val,
74
+ "summary": self.summary
75
+ }
humalab/run.py CHANGED
@@ -1,19 +1,51 @@
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:
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 finish(self,
106
- status: EpisodeStatus = EpisodeStatus.PASS,
107
- quiet: bool | None = None) -> None:
108
- """Finish the run and submit final metrics.
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
- status (EpisodeStatus): The final status of the episode.
112
- quiet (bool | None): Whether to suppress output.
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
- self._finished = True
115
- self._scenario.finish()
116
- for metric in self._metrics.values():
117
- metric.finish(status=status)
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 log(self,
120
- data: dict,
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
- data (dict): A dictionary of metric names and their values.
128
- step (int | None): The step number for the metrics.
129
- commit (bool): Whether to commit the metrics immediately.
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
- for key, value in data.items():
132
- if key in self._metrics:
133
- metric = self._metrics[key]
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 reset(self, status: EpisodeStatus = EpisodeStatus.PASS) -> None:
144
- """Reset the run for a new episode.
190
+ def log_code(self, key: str, code_content: str) -> None:
191
+ """Log code content as an artifact.
145
192
 
146
193
  Args:
147
- status (EpisodeStatus): The status of the current episode before reset.
194
+ key (str): The key for the code artifact.
195
+ code_content (str): The code content to log.
148
196
  """
149
- self._submit_episode_status(status=status, episode=self._episode)
150
- self._episode = str(uuid.uuid4())
151
- self._finished = False
152
- self._scenario.reset(episode_id=self._episode)
153
- for metric in self._metrics.values():
154
- metric.reset(episode=self._episode)
155
-
156
- def _submit_episode_status(self, status: EpisodeStatus, episode: str) -> None:
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
- name (str): The name of the metric.
171
- metric_type (MetricType): The type of the metric.
172
- granularity (MetricGranularity): The granularity of the metric.
173
- distribution_type (str | None): The type of distribution if metric_type is DISTRIBUTION.
174
- summary (str | None): Specify aggregate metrics added to summary.
175
- Supported aggregations include "min", "max", "mean", "last",
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
- if name not in self._metrics or replace:
181
- if metric_type == MetricType.DISTRIBUTION:
182
- if distribution_type is None:
183
- raise ValueError("distribution_type must be specified for distribution metrics.")
184
- self._metrics[name] = DistributionMetric(name=name,
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._metrics[name] = Metrics(name=name,
197
- metric_type=metric_type,
198
- run_id=self._id,
199
- episode_id=self._episode,
200
- granularity=granularity)
201
- else:
202
- raise ValueError(f"Metric {name} already exists.")
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"]