robo-goggles 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.
- goggles/__init__.py +786 -0
- goggles/_core/integrations/__init__.py +26 -0
- goggles/_core/integrations/console.py +111 -0
- goggles/_core/integrations/storage.py +382 -0
- goggles/_core/integrations/wandb.py +253 -0
- goggles/_core/logger.py +602 -0
- goggles/_core/routing.py +127 -0
- goggles/config.py +68 -0
- goggles/decorators.py +81 -0
- goggles/history/__init__.py +39 -0
- goggles/history/buffer.py +185 -0
- goggles/history/spec.py +143 -0
- goggles/history/types.py +9 -0
- goggles/history/utils.py +191 -0
- goggles/media.py +284 -0
- goggles/shutdown.py +70 -0
- goggles/types.py +79 -0
- robo_goggles-0.1.0.dist-info/METADATA +600 -0
- robo_goggles-0.1.0.dist-info/RECORD +22 -0
- robo_goggles-0.1.0.dist-info/WHEEL +5 -0
- robo_goggles-0.1.0.dist-info/licenses/LICENSE +21 -0
- robo_goggles-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Integration module for Goggles core.
|
|
2
|
+
|
|
3
|
+
This module defines the handlers to be attached to the EventBus to dispatch
|
|
4
|
+
events to the appropriate integration modules.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
class PrintHandler(TextHandler):
|
|
8
|
+
def emit(self, record: LogRecord) -> None:
|
|
9
|
+
print(self.format(record))
|
|
10
|
+
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
# TODO: actually write the docs here
|
|
14
|
+
|
|
15
|
+
from .console import ConsoleHandler
|
|
16
|
+
from .storage import LocalStorageHandler
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"ConsoleHandler",
|
|
20
|
+
"LocalStorageHandler",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
from .wandb import WandBHandler
|
|
25
|
+
except ImportError:
|
|
26
|
+
__all__.append("WandBHandler")
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Console-based log handler for EventBus integration."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import ClassVar, FrozenSet, Literal
|
|
6
|
+
from typing_extensions import Self
|
|
7
|
+
|
|
8
|
+
from goggles.types import Event, Kind
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ConsoleHandler:
|
|
12
|
+
"""Handle 'log' events and output them to console using Python's logging API.
|
|
13
|
+
|
|
14
|
+
Attributes:
|
|
15
|
+
name (str): Stable handler identifier.
|
|
16
|
+
capabilities (set[str]): Supported event kinds (only {"log"}).
|
|
17
|
+
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
name: str = "goggles.console"
|
|
21
|
+
capabilities: ClassVar[FrozenSet[Kind]] = frozenset({"log"})
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
*,
|
|
26
|
+
name: str = "goggles.console",
|
|
27
|
+
level: int = logging.NOTSET,
|
|
28
|
+
path_style: Literal["absolute", "relative"] = "relative",
|
|
29
|
+
project_root: Path | None = None,
|
|
30
|
+
) -> None:
|
|
31
|
+
"""Initialize the ConsoleHandler.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
name (str): Stable handler identifier.
|
|
35
|
+
level (int): Minimum log level to handle.
|
|
36
|
+
path_style (Literal["absolute", "relative"]): Whether to print absolute
|
|
37
|
+
or relative file paths. Defaults to "relative".
|
|
38
|
+
project_root (Path | None): Root path used for relative paths.
|
|
39
|
+
|
|
40
|
+
"""
|
|
41
|
+
self.name = name
|
|
42
|
+
self.level = int(level)
|
|
43
|
+
self.path_style = path_style
|
|
44
|
+
self.project_root = Path(project_root or Path.cwd())
|
|
45
|
+
self._logger: logging.Logger
|
|
46
|
+
|
|
47
|
+
def can_handle(self, kind: Kind) -> bool:
|
|
48
|
+
"""Return whether this handler can process the given kind."""
|
|
49
|
+
return kind in self.capabilities
|
|
50
|
+
|
|
51
|
+
def handle(self, event: Event) -> None:
|
|
52
|
+
"""Forward a log event to Python's logging system."""
|
|
53
|
+
if event.kind != "log":
|
|
54
|
+
raise ValueError(f"Unsupported event kind '{event.kind}'")
|
|
55
|
+
|
|
56
|
+
level = int(event.level) if event.level else logging.NOTSET
|
|
57
|
+
message = str(event.payload)
|
|
58
|
+
|
|
59
|
+
# Derive display path
|
|
60
|
+
path = Path(event.filepath)
|
|
61
|
+
if self.path_style == "relative":
|
|
62
|
+
try:
|
|
63
|
+
path = path.relative_to(self.project_root)
|
|
64
|
+
except ValueError:
|
|
65
|
+
pass # fallback to absolute if outside root
|
|
66
|
+
path_str = f"{path}:{event.lineno}"
|
|
67
|
+
|
|
68
|
+
# We manually construct prefix since stacklevel=3 may mislead
|
|
69
|
+
self._logger.log(level, f"{path_str} - {message}", stacklevel=2)
|
|
70
|
+
|
|
71
|
+
def open(self) -> None:
|
|
72
|
+
"""Initialize the handler (create logger and formatter)."""
|
|
73
|
+
self._logger = logging.getLogger(self.name)
|
|
74
|
+
if not self._logger.handlers:
|
|
75
|
+
handler = logging.StreamHandler()
|
|
76
|
+
handler.setFormatter(
|
|
77
|
+
logging.Formatter(
|
|
78
|
+
"%(asctime)s - %(levelname)s - %(message)s",
|
|
79
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
self._logger.addHandler(handler)
|
|
83
|
+
self._logger.setLevel(self.level or logging.INFO)
|
|
84
|
+
|
|
85
|
+
def close(self) -> None:
|
|
86
|
+
"""Flush and release console handler resources."""
|
|
87
|
+
for handler in self._logger.handlers:
|
|
88
|
+
handler.flush()
|
|
89
|
+
|
|
90
|
+
def to_dict(self) -> dict:
|
|
91
|
+
"""Serialize the handler for later reconstruction."""
|
|
92
|
+
return {
|
|
93
|
+
"cls": self.__class__.__name__,
|
|
94
|
+
"data": {
|
|
95
|
+
"name": self.name,
|
|
96
|
+
"level": self.level,
|
|
97
|
+
"path_style": self.path_style,
|
|
98
|
+
"project_root": str(self.project_root),
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
@classmethod
|
|
103
|
+
def from_dict(cls, serialized: dict) -> Self:
|
|
104
|
+
"""Reconstruct a handler from its serialized representation."""
|
|
105
|
+
data = serialized.get("data", serialized)
|
|
106
|
+
return cls(
|
|
107
|
+
name=data["name"],
|
|
108
|
+
level=data["level"],
|
|
109
|
+
path_style=data.get("path_style", "relative"),
|
|
110
|
+
project_root=Path(data.get("project_root", Path.cwd())),
|
|
111
|
+
)
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
"""JSONL integration for Goggles logging framework."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import threading
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, FrozenSet, Optional
|
|
7
|
+
from uuid import uuid4
|
|
8
|
+
from typing_extensions import Self
|
|
9
|
+
import logging
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
from goggles.types import Event, Kind
|
|
13
|
+
from goggles.media import (
|
|
14
|
+
save_numpy_gif,
|
|
15
|
+
save_numpy_image,
|
|
16
|
+
save_numpy_mp4,
|
|
17
|
+
save_numpy_vector_field_visualization,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class LocalStorageHandler:
|
|
22
|
+
"""Write events to a structured directory locally.
|
|
23
|
+
|
|
24
|
+
This handler creates a directory structure:
|
|
25
|
+
- {base_path}/log.jsonl: Main JSONL log file with all events
|
|
26
|
+
- {base_path}/images/: Directory for image files
|
|
27
|
+
- {base_path}/videos/: Directory for video files
|
|
28
|
+
- {base_path}/artifacts/: Directory for other artifact files
|
|
29
|
+
|
|
30
|
+
For media events (image, video, artifact), the binary data is saved to
|
|
31
|
+
the appropriate subdirectory and the relative path is logged in the
|
|
32
|
+
JSONL file instead of the raw data.
|
|
33
|
+
|
|
34
|
+
Thread-safe and line-buffered, ensuring atomic writes per event.
|
|
35
|
+
|
|
36
|
+
Attributes:
|
|
37
|
+
name (str): Stable handler identifier.
|
|
38
|
+
capabilities (set[str]): Supported event kinds.
|
|
39
|
+
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
name: str = "jsonl"
|
|
43
|
+
capabilities: FrozenSet[str] = frozenset(
|
|
44
|
+
{"log", "metric", "image", "video", "artifact", "vector_field", "histogram"}
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def __init__(self, path: Path, name: str = "jsonl") -> None:
|
|
48
|
+
"""Initialize the handler with a base directory.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
path (Path): Base directory for logs and media files. Will be created if it doesn't exist.
|
|
52
|
+
name (str): Handler identifier (for logging diagnostics).
|
|
53
|
+
|
|
54
|
+
"""
|
|
55
|
+
self.name = name
|
|
56
|
+
self._base_path = Path(path)
|
|
57
|
+
|
|
58
|
+
def open(self) -> None:
|
|
59
|
+
"""Create directory structure and open the JSONL file for appending."""
|
|
60
|
+
self._lock = threading.Lock()
|
|
61
|
+
|
|
62
|
+
# Create directory structure
|
|
63
|
+
self._log_file = self._base_path / "log.jsonl"
|
|
64
|
+
self._images_dir = self._base_path / "images"
|
|
65
|
+
self._videos_dir = self._base_path / "videos"
|
|
66
|
+
self._artifacts_dir = self._base_path / "artifacts"
|
|
67
|
+
self._vector_fields_dir = self._base_path / "vector_fields"
|
|
68
|
+
self._histograms_dir = self._base_path / "histograms"
|
|
69
|
+
self._base_path.mkdir(parents=True, exist_ok=True)
|
|
70
|
+
self._images_dir.mkdir(exist_ok=True)
|
|
71
|
+
self._videos_dir.mkdir(exist_ok=True)
|
|
72
|
+
self._artifacts_dir.mkdir(exist_ok=True)
|
|
73
|
+
self._vector_fields_dir.mkdir(exist_ok=True)
|
|
74
|
+
self._histograms_dir.mkdir(exist_ok=True)
|
|
75
|
+
|
|
76
|
+
# Open log file
|
|
77
|
+
self._fp = open(self._log_file, "a", encoding="utf-8", buffering=1)
|
|
78
|
+
|
|
79
|
+
# Open logger for diagnostics
|
|
80
|
+
self._logger = logging.getLogger(self.name)
|
|
81
|
+
|
|
82
|
+
def close(self) -> None:
|
|
83
|
+
"""Flush and close the JSONL file."""
|
|
84
|
+
if self._fp and not self._fp.closed:
|
|
85
|
+
with self._lock:
|
|
86
|
+
self._fp.flush()
|
|
87
|
+
self._fp.close()
|
|
88
|
+
|
|
89
|
+
def can_handle(self, kind: Kind) -> bool:
|
|
90
|
+
"""Return True if this handler supports the given event kind.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
kind (Kind): Kind of event ("log", "metric", "image", "artifact").
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
bool: True if the kind is supported, False otherwise.
|
|
97
|
+
|
|
98
|
+
"""
|
|
99
|
+
return kind in self.capabilities
|
|
100
|
+
|
|
101
|
+
def handle(self, event: Event) -> None:
|
|
102
|
+
"""Write a single event to the JSONL file.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
event (Event): The event to serialize.
|
|
106
|
+
|
|
107
|
+
"""
|
|
108
|
+
event = event.to_dict()
|
|
109
|
+
|
|
110
|
+
# Handle media events by saving files and updating payload
|
|
111
|
+
kind = event["kind"]
|
|
112
|
+
if kind == "image":
|
|
113
|
+
event = self._save_image_to_file(event)
|
|
114
|
+
elif kind == "video":
|
|
115
|
+
event = self._save_video_to_file(event)
|
|
116
|
+
elif kind == "artifact":
|
|
117
|
+
event = self._save_artifact_to_file(event)
|
|
118
|
+
elif kind == "vector_field":
|
|
119
|
+
event = self._save_vector_field_to_file(event)
|
|
120
|
+
elif kind == "histogram":
|
|
121
|
+
event = self._save_histogram_to_file(event)
|
|
122
|
+
|
|
123
|
+
if event is None:
|
|
124
|
+
self._logger.warning(
|
|
125
|
+
"Skipping event logging due to unsupported media format."
|
|
126
|
+
)
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
with self._lock:
|
|
131
|
+
json.dump(
|
|
132
|
+
event, self._fp, ensure_ascii=False, default=self._json_serializer
|
|
133
|
+
)
|
|
134
|
+
self._fp.write("\n")
|
|
135
|
+
self._fp.flush()
|
|
136
|
+
except Exception:
|
|
137
|
+
logging.getLogger(self.name).exception("Failed to write JSONL event")
|
|
138
|
+
|
|
139
|
+
def to_dict(self) -> dict:
|
|
140
|
+
"""Serialize handler configuration to dictionary."""
|
|
141
|
+
return {
|
|
142
|
+
"cls": self.__class__.__name__,
|
|
143
|
+
"data": {
|
|
144
|
+
"path": str(self._base_path),
|
|
145
|
+
"name": self.name,
|
|
146
|
+
},
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
@classmethod
|
|
150
|
+
def from_dict(cls, serialized: dict) -> Self:
|
|
151
|
+
"""Reconstruct a handler from its serialized representation."""
|
|
152
|
+
data = serialized.get("data", serialized)
|
|
153
|
+
return cls(
|
|
154
|
+
path=Path(data["path"]),
|
|
155
|
+
name=data["name"],
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
def _json_serializer(self, obj: Any) -> str:
|
|
159
|
+
"""Serialize object to JSON-compatible format.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
obj: Object to serialize.
|
|
163
|
+
|
|
164
|
+
"""
|
|
165
|
+
if isinstance(obj, np.ndarray):
|
|
166
|
+
return obj.tolist()
|
|
167
|
+
elif isinstance(obj, (np.integer, np.floating)):
|
|
168
|
+
return obj.item()
|
|
169
|
+
|
|
170
|
+
# For other non-serializable objects, convert to string
|
|
171
|
+
return str(obj)
|
|
172
|
+
|
|
173
|
+
def _save_image_to_file(self, event: dict) -> dict:
|
|
174
|
+
"""Save image data to file and update event with file path.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
event (dict): Event dictionary.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
dict: Updated event with file path instead of raw data.
|
|
181
|
+
|
|
182
|
+
"""
|
|
183
|
+
image_format = "png"
|
|
184
|
+
if event["extra"] and "format" in event["extra"]:
|
|
185
|
+
image_format = event["extra"]["format"]
|
|
186
|
+
|
|
187
|
+
image_name = str(uuid4())
|
|
188
|
+
if event["extra"] and "name" in event["extra"]:
|
|
189
|
+
image_name = event["extra"]["name"]
|
|
190
|
+
|
|
191
|
+
image_path = self._images_dir / Path(f"{image_name}.{image_format}")
|
|
192
|
+
save_numpy_image(
|
|
193
|
+
event["payload"],
|
|
194
|
+
image_path,
|
|
195
|
+
format=image_format,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
event["payload"] = str(image_path.relative_to(self._base_path))
|
|
199
|
+
return event
|
|
200
|
+
|
|
201
|
+
def _save_video_to_file(self, event: dict) -> dict:
|
|
202
|
+
"""Save video data to file and update event with file path.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
event (dict): Event dictionary.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
dict: Updated event with file path instead of raw data.
|
|
209
|
+
|
|
210
|
+
"""
|
|
211
|
+
video_format = "mp4"
|
|
212
|
+
if "format" in event["extra"]:
|
|
213
|
+
video_format = event["extra"]["format"]
|
|
214
|
+
if video_format not in {"mp4", "gif"}:
|
|
215
|
+
self._logger.warning(
|
|
216
|
+
f"Unknown video format '{video_format}'."
|
|
217
|
+
" Supported formats are: 'mp4', 'gif'."
|
|
218
|
+
" The video will not be logged."
|
|
219
|
+
)
|
|
220
|
+
return None
|
|
221
|
+
|
|
222
|
+
video_name = str(uuid4())
|
|
223
|
+
if event["extra"] and "name" in event["extra"]:
|
|
224
|
+
video_name = event["extra"]["name"]
|
|
225
|
+
|
|
226
|
+
fps = 1.0
|
|
227
|
+
if event["extra"] and "fps" in event["extra"]:
|
|
228
|
+
fps = float(event["extra"]["fps"])
|
|
229
|
+
|
|
230
|
+
if video_format == "gif":
|
|
231
|
+
video_data: np.ndarray = event["payload"]
|
|
232
|
+
loop = 0
|
|
233
|
+
if event["extra"] and "loop" in event["extra"]:
|
|
234
|
+
loop = event["extra"]["loop"]
|
|
235
|
+
gif_path = self._videos_dir / Path(f"{video_name}.gif")
|
|
236
|
+
save_numpy_gif(video_data, gif_path, fps=fps, loop=loop)
|
|
237
|
+
event["payload"] = str(gif_path.relative_to(self._base_path))
|
|
238
|
+
elif video_format == "mp4":
|
|
239
|
+
video_data: np.ndarray = event["payload"]
|
|
240
|
+
video_codec = "libx264"
|
|
241
|
+
pix_fmt = "yuv420p"
|
|
242
|
+
bitrate = None
|
|
243
|
+
crf = 18
|
|
244
|
+
convert_gray_to_rgb = True
|
|
245
|
+
preset = "medium"
|
|
246
|
+
if event["extra"]:
|
|
247
|
+
if "codec" in event["extra"]:
|
|
248
|
+
video_codec = event["extra"]["codec"]
|
|
249
|
+
if "pix_fmt" in event["extra"]:
|
|
250
|
+
pix_fmt = event["extra"]["pix_fmt"]
|
|
251
|
+
if "bitrate" in event["extra"]:
|
|
252
|
+
bitrate = event["extra"]["bitrate"]
|
|
253
|
+
if "crf" in event["extra"]:
|
|
254
|
+
crf = event["extra"]["crf"]
|
|
255
|
+
if "convert_gray_to_rgb" in event["extra"]:
|
|
256
|
+
convert_gray_to_rgb = event["extra"]["convert_gray_to_rgb"]
|
|
257
|
+
if "preset" in event["extra"]:
|
|
258
|
+
preset = event["extra"]["preset"]
|
|
259
|
+
|
|
260
|
+
mp4_path = self._videos_dir / Path(f"{video_name}.mp4")
|
|
261
|
+
save_numpy_mp4(
|
|
262
|
+
video_data,
|
|
263
|
+
mp4_path,
|
|
264
|
+
fps=fps,
|
|
265
|
+
codec=video_codec,
|
|
266
|
+
pix_fmt=pix_fmt,
|
|
267
|
+
bitrate=bitrate,
|
|
268
|
+
crf=crf,
|
|
269
|
+
convert_gray_to_rgb=convert_gray_to_rgb,
|
|
270
|
+
preset=preset,
|
|
271
|
+
)
|
|
272
|
+
event["payload"] = str(mp4_path.relative_to(self._base_path))
|
|
273
|
+
return event
|
|
274
|
+
|
|
275
|
+
def _save_artifact_to_file(self, event: dict) -> Optional[dict]:
|
|
276
|
+
"""Save artifact data to file and update event with file path.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
event (dict): Event dictionary.
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
Optional[dict]: Updated event with file path instead of raw data.
|
|
283
|
+
If the artifact format is unknown, returns None.
|
|
284
|
+
|
|
285
|
+
"""
|
|
286
|
+
artifact_format = "txt"
|
|
287
|
+
if event["extra"] and "format" in event["extra"]:
|
|
288
|
+
artifact_format = event["extra"]["format"]
|
|
289
|
+
|
|
290
|
+
if artifact_format not in {"txt", "csv", "json", "yaml"}:
|
|
291
|
+
self._logger.warning(
|
|
292
|
+
f"Unknown artifact format '{artifact_format}'."
|
|
293
|
+
" Supported formats are: 'txt', 'csv', 'json', 'yaml'."
|
|
294
|
+
" The artifact will not be logged."
|
|
295
|
+
)
|
|
296
|
+
return None
|
|
297
|
+
|
|
298
|
+
if artifact_format == "json":
|
|
299
|
+
import json
|
|
300
|
+
|
|
301
|
+
event["payload"] = json.dumps(event["payload"], indent=2)
|
|
302
|
+
|
|
303
|
+
if artifact_format == "yaml":
|
|
304
|
+
import yaml
|
|
305
|
+
|
|
306
|
+
event["payload"] = yaml.dump(event["payload"])
|
|
307
|
+
|
|
308
|
+
artifact_name = str(uuid4())
|
|
309
|
+
if event["extra"] and "name" in event["extra"]:
|
|
310
|
+
artifact_name = event["extra"]["name"]
|
|
311
|
+
|
|
312
|
+
artifact_path = self._artifacts_dir / Path(f"{artifact_name}.{artifact_format}")
|
|
313
|
+
|
|
314
|
+
with open(artifact_path, "w") as f:
|
|
315
|
+
f.write(event["payload"])
|
|
316
|
+
|
|
317
|
+
event["payload"] = str(artifact_path.relative_to(self._base_path))
|
|
318
|
+
return event
|
|
319
|
+
|
|
320
|
+
def _save_vector_field_to_file(self, event: dict) -> Optional[dict]:
|
|
321
|
+
"""Save vector field data to file and update event with file path.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
event (dict): Event dictionary.
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
dict: Updated event with file path instead of raw data.
|
|
328
|
+
|
|
329
|
+
"""
|
|
330
|
+
vector_field_name = str(uuid4())
|
|
331
|
+
if event["extra"] and "name" in event["extra"]:
|
|
332
|
+
vector_field_name = event["extra"]["name"]
|
|
333
|
+
|
|
334
|
+
if event["extra"] and "store_visualization" in event["extra"]:
|
|
335
|
+
add_colorbar = False
|
|
336
|
+
if event["extra"] and "add_colorbar" in event["extra"]:
|
|
337
|
+
add_colorbar = event["extra"]["add_colorbar"]
|
|
338
|
+
|
|
339
|
+
mode = "magnitude"
|
|
340
|
+
if event["extra"] and "mode" in event["extra"]:
|
|
341
|
+
mode = event["extra"]["mode"]
|
|
342
|
+
|
|
343
|
+
if mode not in {"vorticity", "magnitude"}:
|
|
344
|
+
self._logger.warning(
|
|
345
|
+
f"Unknown vector field visualization mode '{mode}'."
|
|
346
|
+
" Supported modes are: 'vorticity', 'magnitude'."
|
|
347
|
+
" The vector field visualization will not be saved."
|
|
348
|
+
)
|
|
349
|
+
else:
|
|
350
|
+
save_numpy_vector_field_visualization(
|
|
351
|
+
event["payload"],
|
|
352
|
+
dir=self._vector_fields_dir,
|
|
353
|
+
name=f"{vector_field_name}_visualization",
|
|
354
|
+
mode=mode,
|
|
355
|
+
add_colorbar=add_colorbar,
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
vector_field_path = self._vector_fields_dir / Path(f"{vector_field_name}.npy")
|
|
359
|
+
np.save(vector_field_path, event["payload"])
|
|
360
|
+
|
|
361
|
+
event["payload"] = str(vector_field_path.relative_to(self._base_path))
|
|
362
|
+
return event
|
|
363
|
+
|
|
364
|
+
def _save_histogram_to_file(self, event: dict) -> Optional[dict]:
|
|
365
|
+
"""Save histogram data to file and update event with file path.
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
event (dict): Event dictionary.
|
|
369
|
+
|
|
370
|
+
Returns:
|
|
371
|
+
dict: Updated event with file path instead of raw data.
|
|
372
|
+
|
|
373
|
+
"""
|
|
374
|
+
histogram_name = str(uuid4())
|
|
375
|
+
if event["extra"] and "name" in event["extra"]:
|
|
376
|
+
histogram_name = event["extra"]["name"]
|
|
377
|
+
|
|
378
|
+
histogram_path = self._histograms_dir / Path(f"{histogram_name}.npy")
|
|
379
|
+
np.save(histogram_path, event["payload"])
|
|
380
|
+
|
|
381
|
+
event["payload"] = str(histogram_path.relative_to(self._base_path))
|
|
382
|
+
return event
|