humalab 0.1.0__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.
@@ -0,0 +1,59 @@
1
+ class Code:
2
+ """Class for logging code artifacts.
3
+
4
+ Code artifacts capture source code or configuration files associated with
5
+ runs or episodes. They are stored as text content and can be retrieved later
6
+ for reproducibility and debugging purposes.
7
+
8
+ Attributes:
9
+ run_id (str): The unique identifier of the associated run.
10
+ key (str): The artifact key/name for this code.
11
+ code_content (str): The actual code or text content.
12
+ episode_id (str | None): Optional episode identifier if scoped to an episode.
13
+ """
14
+ def __init__(self,
15
+ run_id: str,
16
+ key: str,
17
+ code_content: str,
18
+ episode_id: str | None = None) -> None:
19
+ super().__init__()
20
+ self._run_id = run_id
21
+ self._key = key
22
+ self._code_content = code_content
23
+ self._episode_id = episode_id
24
+
25
+ @property
26
+ def run_id(self) -> str:
27
+ """The unique identifier of the associated run.
28
+
29
+ Returns:
30
+ str: The run ID.
31
+ """
32
+ return self._run_id
33
+
34
+ @property
35
+ def key(self) -> str:
36
+ """The artifact key/name for this code.
37
+
38
+ Returns:
39
+ str: The artifact key.
40
+ """
41
+ return self._key
42
+
43
+ @property
44
+ def code_content(self) -> str:
45
+ """The actual code or text content.
46
+
47
+ Returns:
48
+ str: The code content.
49
+ """
50
+ return self._code_content
51
+
52
+ @property
53
+ def episode_id(self) -> str | None:
54
+ """Optional episode identifier if scoped to an episode.
55
+
56
+ Returns:
57
+ str | None: The episode ID, or None if run-scoped.
58
+ """
59
+ return self._episode_id
@@ -0,0 +1,96 @@
1
+ from typing import Any
2
+ from humalab.constants import MetricDimType, GraphType
3
+
4
+ GRAPH_TO_DIM_TYPE = {
5
+ GraphType.NUMERIC: MetricDimType.ZERO_D,
6
+ GraphType.LINE: MetricDimType.ONE_D,
7
+ GraphType.HISTOGRAM: MetricDimType.ONE_D,
8
+ GraphType.BAR: MetricDimType.ONE_D,
9
+ GraphType.GAUSSIAN: MetricDimType.ONE_D,
10
+ GraphType.SCATTER: MetricDimType.TWO_D,
11
+ GraphType.HEATMAP: MetricDimType.TWO_D,
12
+ GraphType.THREE_D_MAP: MetricDimType.THREE_D,
13
+ }
14
+
15
+
16
+ class Metrics:
17
+ """Base class for tracking and logging metrics during runs and episodes.
18
+
19
+ Metrics provide a flexible way to log time-series data or aggregated values
20
+ during experiments. Data points are collected with optional x-axis values
21
+ and can be visualized using different graph types.
22
+
23
+ Subclasses should override _finalize() to implement custom processing logic.
24
+
25
+ Attributes:
26
+ graph_type (GraphType): The type of graph used for visualization.
27
+ """
28
+ def __init__(self,
29
+ graph_type: GraphType=GraphType.LINE) -> None:
30
+ """Initialize a new Metrics instance.
31
+
32
+ Args:
33
+ graph_type (GraphType): The type of graph to use for visualization
34
+ (e.g., LINE, BAR, HISTOGRAM, SCATTER). Defaults to LINE.
35
+ """
36
+ self._values = []
37
+ self._x_values = []
38
+ self._step = -1
39
+ self._metric_dim_type = GRAPH_TO_DIM_TYPE.get(graph_type, MetricDimType.ONE_D)
40
+ self._graph_type = graph_type
41
+
42
+ @property
43
+ def metric_dim_type(self) -> MetricDimType:
44
+ """The dimensionality of the metric data.
45
+
46
+ Returns:
47
+ MetricDimType: The metric dimension type.
48
+ """
49
+ return self._metric_dim_type
50
+
51
+ @property
52
+ def graph_type(self) -> GraphType:
53
+ """The type of graph used for visualization.
54
+
55
+ Returns:
56
+ GraphType: The graph type.
57
+ """
58
+ return self._graph_type
59
+
60
+ def log(self, data: Any, x: Any = None, replace: bool = False) -> None:
61
+ """Log a new data point for the metric.
62
+
63
+ Args:
64
+ data (Any): The data point to log.
65
+ x (Any | None): The x-axis value associated with the data point.
66
+ If None, uses an auto-incrementing step counter.
67
+ replace (bool): Whether to replace the last logged value. Defaults to False.
68
+ """
69
+ if replace:
70
+ self._values[-1] = data
71
+ if x is not None:
72
+ self._x_values[-1] = x
73
+ else:
74
+ self._values.append(data)
75
+ if x is not None:
76
+ self._x_values.append(x)
77
+ else:
78
+ self._x_values.append(self._step)
79
+ self._step += 1
80
+
81
+ def finalize(self) -> dict:
82
+ """Finalize the logged data for processing."""
83
+ ret_result = self._finalize()
84
+
85
+ return ret_result
86
+
87
+ def _finalize(self) -> dict:
88
+ """Process the logged data before submission. To be implemented by subclasses."""
89
+ ret_val = {
90
+ "values": self._values,
91
+ "x_values": self._x_values
92
+ }
93
+ self._values = []
94
+ self._x_values = []
95
+ self._step = -1
96
+ return ret_val
@@ -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.HISTOGRAM,
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.HISTOGRAM,
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
+
@@ -0,0 +1,75 @@
1
+
2
+ from humalab.metrics.metric import Metrics
3
+ from humalab.constants import MetricDimType, GraphType
4
+
5
+
6
+ class Summary(Metrics):
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,
17
+ summary: str,
18
+ ) -> None:
19
+ """
20
+ A summary metric that captures a single value per episode or run.
21
+
22
+ Args:
23
+ summary (str): Specify the aggregation method for the summary.
24
+ Supported aggregations include "min", "max", "mean", "last",
25
+ "first", and "none". "none" prevents a summary from being generated.
26
+ """
27
+ if summary not in {"min", "max", "mean", "last", "first", "none"}:
28
+ raise ValueError(f"Unsupported summary type: {summary}. Supported types are 'min', 'max', 'mean', 'last', 'first', and 'none'.")
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.
43
+
44
+ Returns:
45
+ dict: Dictionary containing the aggregated value and summary type.
46
+ """
47
+ if not self._values:
48
+ return {
49
+ "value": None,
50
+ "summary": self.summary
51
+ }
52
+ final_val = None
53
+ # For summary metrics, we only keep the latest value
54
+ if self.summary == "last":
55
+ final_val = self._values[-1]
56
+ elif self.summary == "first":
57
+ final_val = self._values[0]
58
+ elif self.summary == "none":
59
+ final_val = None
60
+ elif self.summary in {"min", "max", "mean"}:
61
+ if not self._values:
62
+ final_val = None
63
+ else:
64
+ if self.summary == "min":
65
+ agg_value = min(self._values)
66
+ elif self.summary == "max":
67
+ agg_value = max(self._values)
68
+ elif self.summary == "mean":
69
+ agg_value = sum(self._values) / len(self._values)
70
+ final_val = agg_value
71
+
72
+ return {
73
+ "value": final_val,
74
+ "summary": self.summary
75
+ }