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
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) > dimensions:
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) > dimensions:
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
@@ -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) > dimensions:
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) > dimensions:
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) > dimensions:
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) > dimensions:
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) > dimensions:
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) > dimensions:
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.scenario import Scenario
3
- from omegaconf import DictConfig, OmegaConf
11
+ from humalab.utils import is_standard_type
4
12
 
5
13
 
6
14
  class Episode:
7
- def __init__(self, run_id: str, episode_id: str, scenario_conf: DictConfig):
8
- self.run_id = run_id
9
- self.episode_id = episode_id
10
- self.scenario_conf = scenario_conf
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 scenario(self) -> DictConfig:
14
- return self.scenario_conf
62
+ def run_id(self) -> str:
63
+ """The unique identifier of the parent run.
15
64
 
16
- def finish(self):
17
- print(f"Finishing episode {self.episode_id} for scenario {self.scenario.name}")
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.scenario_conf)
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
+ )