antioch-py 2.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 antioch-py might be problematic. Click here for more details.

Files changed (99) hide show
  1. antioch/__init__.py +0 -0
  2. antioch/message.py +87 -0
  3. antioch/module/__init__.py +53 -0
  4. antioch/module/clock.py +62 -0
  5. antioch/module/execution.py +278 -0
  6. antioch/module/input.py +127 -0
  7. antioch/module/module.py +218 -0
  8. antioch/module/node.py +357 -0
  9. antioch/module/token.py +42 -0
  10. antioch/session/__init__.py +150 -0
  11. antioch/session/ark.py +504 -0
  12. antioch/session/asset.py +65 -0
  13. antioch/session/error.py +80 -0
  14. antioch/session/record.py +158 -0
  15. antioch/session/scene.py +1521 -0
  16. antioch/session/session.py +220 -0
  17. antioch/session/task.py +323 -0
  18. antioch/session/views/__init__.py +40 -0
  19. antioch/session/views/animation.py +189 -0
  20. antioch/session/views/articulation.py +245 -0
  21. antioch/session/views/basis_curve.py +186 -0
  22. antioch/session/views/camera.py +92 -0
  23. antioch/session/views/collision.py +75 -0
  24. antioch/session/views/geometry.py +74 -0
  25. antioch/session/views/ground_plane.py +63 -0
  26. antioch/session/views/imu.py +73 -0
  27. antioch/session/views/joint.py +64 -0
  28. antioch/session/views/light.py +175 -0
  29. antioch/session/views/pir_sensor.py +140 -0
  30. antioch/session/views/radar.py +73 -0
  31. antioch/session/views/rigid_body.py +282 -0
  32. antioch/session/views/xform.py +119 -0
  33. antioch_py-2.0.6.dist-info/METADATA +115 -0
  34. antioch_py-2.0.6.dist-info/RECORD +99 -0
  35. antioch_py-2.0.6.dist-info/WHEEL +5 -0
  36. antioch_py-2.0.6.dist-info/entry_points.txt +2 -0
  37. antioch_py-2.0.6.dist-info/top_level.txt +2 -0
  38. common/__init__.py +0 -0
  39. common/ark/__init__.py +60 -0
  40. common/ark/ark.py +128 -0
  41. common/ark/hardware.py +121 -0
  42. common/ark/kinematics.py +31 -0
  43. common/ark/module.py +85 -0
  44. common/ark/node.py +94 -0
  45. common/ark/scheduler.py +439 -0
  46. common/ark/sim.py +33 -0
  47. common/assets/__init__.py +3 -0
  48. common/constants.py +47 -0
  49. common/core/__init__.py +52 -0
  50. common/core/agent.py +296 -0
  51. common/core/auth.py +305 -0
  52. common/core/registry.py +331 -0
  53. common/core/task.py +36 -0
  54. common/message/__init__.py +59 -0
  55. common/message/annotation.py +89 -0
  56. common/message/array.py +500 -0
  57. common/message/base.py +517 -0
  58. common/message/camera.py +91 -0
  59. common/message/color.py +139 -0
  60. common/message/frame.py +50 -0
  61. common/message/image.py +171 -0
  62. common/message/imu.py +14 -0
  63. common/message/joint.py +47 -0
  64. common/message/log.py +31 -0
  65. common/message/pir.py +16 -0
  66. common/message/point.py +109 -0
  67. common/message/point_cloud.py +63 -0
  68. common/message/pose.py +148 -0
  69. common/message/quaternion.py +273 -0
  70. common/message/radar.py +58 -0
  71. common/message/types.py +37 -0
  72. common/message/vector.py +786 -0
  73. common/rome/__init__.py +9 -0
  74. common/rome/client.py +430 -0
  75. common/rome/error.py +16 -0
  76. common/session/__init__.py +54 -0
  77. common/session/environment.py +31 -0
  78. common/session/sim.py +240 -0
  79. common/session/views/__init__.py +263 -0
  80. common/session/views/animation.py +73 -0
  81. common/session/views/articulation.py +184 -0
  82. common/session/views/basis_curve.py +102 -0
  83. common/session/views/camera.py +147 -0
  84. common/session/views/collision.py +59 -0
  85. common/session/views/geometry.py +102 -0
  86. common/session/views/ground_plane.py +41 -0
  87. common/session/views/imu.py +66 -0
  88. common/session/views/joint.py +81 -0
  89. common/session/views/light.py +96 -0
  90. common/session/views/pir_sensor.py +115 -0
  91. common/session/views/radar.py +82 -0
  92. common/session/views/rigid_body.py +236 -0
  93. common/session/views/viewport.py +21 -0
  94. common/session/views/xform.py +39 -0
  95. common/utils/__init__.py +4 -0
  96. common/utils/comms.py +571 -0
  97. common/utils/logger.py +123 -0
  98. common/utils/time.py +42 -0
  99. common/utils/usd.py +12 -0
@@ -0,0 +1,139 @@
1
+ from __future__ import annotations
2
+
3
+ from pydantic import field_validator
4
+
5
+ from common.message.base import Message
6
+
7
+
8
+ class Color(Message):
9
+ """
10
+ An RGBA color with values in the range [0.0, 1.0].
11
+
12
+ Used in image annotations and visualization.
13
+ """
14
+
15
+ _type = "antioch/color"
16
+ r: float
17
+ g: float
18
+ b: float
19
+ a: float
20
+
21
+ @field_validator("r", "g", "b", "a")
22
+ @classmethod
23
+ def validate_range(cls, v: float) -> float:
24
+ """
25
+ Validate that color values are in the range [0.0, 1.0].
26
+
27
+ :param v: The color value to validate.
28
+ :return: The validated color value.
29
+ :raises ValueError: If the value is not in [0.0, 1.0].
30
+ """
31
+
32
+ if not 0.0 <= v <= 1.0:
33
+ raise ValueError(f"Color values must be in range [0.0, 1.0], got {v}")
34
+ return v
35
+
36
+ def __repr__(self) -> str:
37
+ """
38
+ Return a readable string representation.
39
+
40
+ :return: String representation.
41
+ """
42
+
43
+ return f"Color(r={self.r}, g={self.g}, b={self.b}, a={self.a})"
44
+
45
+ def __str__(self) -> str:
46
+ """
47
+ Return a readable string representation.
48
+
49
+ :return: String representation.
50
+ """
51
+
52
+ return f"Color(r={self.r}, g={self.g}, b={self.b}, a={self.a})"
53
+
54
+ @classmethod
55
+ def rgba(cls, r: float, g: float, b: float, a: float = 1.0) -> Color:
56
+ """
57
+ Create a color from RGBA values.
58
+
59
+ :param r: Red component [0.0, 1.0].
60
+ :param g: Green component [0.0, 1.0].
61
+ :param b: Blue component [0.0, 1.0].
62
+ :param a: Alpha component [0.0, 1.0]. Defaults to 1.0 (opaque).
63
+ :return: A Color instance.
64
+ """
65
+
66
+ return cls(r=r, g=g, b=b, a=a)
67
+
68
+ @classmethod
69
+ def rgb(cls, r: float, g: float, b: float) -> Color:
70
+ """
71
+ Create an opaque color from RGB values.
72
+
73
+ :param r: Red component [0.0, 1.0].
74
+ :param g: Green component [0.0, 1.0].
75
+ :param b: Blue component [0.0, 1.0].
76
+ :return: A Color instance with alpha = 1.0.
77
+ """
78
+
79
+ return cls(r=r, g=g, b=b, a=1.0)
80
+
81
+ @classmethod
82
+ def red(cls) -> Color:
83
+ """
84
+ Create a red color.
85
+
86
+ :return: Red color (1.0, 0.0, 0.0, 1.0).
87
+ """
88
+
89
+ return cls(r=1.0, g=0.0, b=0.0, a=1.0)
90
+
91
+ @classmethod
92
+ def green(cls) -> Color:
93
+ """
94
+ Create a green color.
95
+
96
+ :return: Green color (0.0, 1.0, 0.0, 1.0).
97
+ """
98
+
99
+ return cls(r=0.0, g=1.0, b=0.0, a=1.0)
100
+
101
+ @classmethod
102
+ def blue(cls) -> Color:
103
+ """
104
+ Create a blue color.
105
+
106
+ :return: Blue color (0.0, 0.0, 1.0, 1.0).
107
+ """
108
+
109
+ return cls(r=0.0, g=0.0, b=1.0, a=1.0)
110
+
111
+ @classmethod
112
+ def white(cls) -> Color:
113
+ """
114
+ Create a white color.
115
+
116
+ :return: White color (1.0, 1.0, 1.0, 1.0).
117
+ """
118
+
119
+ return cls(r=1.0, g=1.0, b=1.0, a=1.0)
120
+
121
+ @classmethod
122
+ def black(cls) -> Color:
123
+ """
124
+ Create a black color.
125
+
126
+ :return: Black color (0.0, 0.0, 0.0, 1.0).
127
+ """
128
+
129
+ return cls(r=0.0, g=0.0, b=0.0, a=1.0)
130
+
131
+ @classmethod
132
+ def transparent(cls) -> Color:
133
+ """
134
+ Create a transparent color.
135
+
136
+ :return: Transparent color (0.0, 0.0, 0.0, 0.0).
137
+ """
138
+
139
+ return cls(r=0.0, g=0.0, b=0.0, a=0.0)
@@ -0,0 +1,50 @@
1
+ from pydantic import Field
2
+
3
+ from common.message.base import Message
4
+ from common.message.quaternion import Quaternion
5
+ from common.message.vector import Vector3
6
+
7
+
8
+ class FrameTransform(Message):
9
+ """
10
+ A transform between two reference frames in 3D space.
11
+
12
+ :param parent_frame_id: Name of the parent frame.
13
+ :param child_frame_id: Name of the child frame.
14
+ :param translation: Translation component of the transform.
15
+ :param rotation: Rotation component of the transform.
16
+ """
17
+
18
+ _type = "antioch/frame_transform"
19
+ parent_frame_id: str = Field(description="Name of the parent frame")
20
+ child_frame_id: str = Field(description="Name of the child frame")
21
+ translation: Vector3 = Field(description="Translation component of the transform")
22
+ rotation: Quaternion = Field(description="Rotation component of the transform")
23
+
24
+ @classmethod
25
+ def identity(cls, parent_frame_id: str, child_frame_id: str) -> "FrameTransform":
26
+ """
27
+ Create an identity transform between two frames.
28
+
29
+ :param parent_frame_id: Name of the parent frame.
30
+ :param child_frame_id: Name of the child frame.
31
+ :return: Identity frame transform.
32
+ """
33
+
34
+ return cls(
35
+ parent_frame_id=parent_frame_id,
36
+ child_frame_id=child_frame_id,
37
+ translation=Vector3.zeros(),
38
+ rotation=Quaternion.identity(),
39
+ )
40
+
41
+
42
+ class FrameTransforms(Message):
43
+ """
44
+ An array of FrameTransform messages.
45
+
46
+ :param transforms: Array of transforms.
47
+ """
48
+
49
+ _type = "antioch/frame_transforms"
50
+ transforms: list[FrameTransform] = Field(default_factory=list, description="Array of transforms")
@@ -0,0 +1,171 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import Enum
4
+
5
+ import numpy as np
6
+
7
+ from common.message.base import Message
8
+
9
+
10
+ class ImageEncoding(str, Enum):
11
+ """
12
+ Image encodings with associated metadata.
13
+ """
14
+
15
+ # Monochrome images
16
+ MONO8 = "mono8"
17
+ MONO16 = "mono16"
18
+
19
+ # Color images
20
+ RGB8 = "rgb8"
21
+ RGBA8 = "rgba8"
22
+ BGR8 = "bgr8"
23
+ BGRA8 = "bgra8"
24
+
25
+ # Depth images
26
+ DEPTH_U16 = "16UC1"
27
+ DEPTH_F32 = "32FC1"
28
+
29
+ @property
30
+ def bytes_per_pixel(self) -> int:
31
+ """
32
+ Get the number of bytes per pixel for this encoding.
33
+
34
+ :return: The number of bytes per pixel.
35
+ """
36
+
37
+ return {
38
+ ImageEncoding.MONO8: 1,
39
+ ImageEncoding.MONO16: 2,
40
+ ImageEncoding.RGB8: 3,
41
+ ImageEncoding.RGBA8: 4,
42
+ ImageEncoding.BGR8: 3,
43
+ ImageEncoding.BGRA8: 4,
44
+ ImageEncoding.DEPTH_U16: 2,
45
+ ImageEncoding.DEPTH_F32: 4,
46
+ }[self]
47
+
48
+ @property
49
+ def numpy_dtype(self) -> np.dtype:
50
+ """
51
+ Get the numpy dtype for this encoding.
52
+
53
+ :return: The numpy dtype.
54
+ """
55
+
56
+ dtype_map = {
57
+ ImageEncoding.MONO8: np.uint8,
58
+ ImageEncoding.MONO16: np.uint16,
59
+ ImageEncoding.RGB8: np.uint8,
60
+ ImageEncoding.RGBA8: np.uint8,
61
+ ImageEncoding.BGR8: np.uint8,
62
+ ImageEncoding.BGRA8: np.uint8,
63
+ ImageEncoding.DEPTH_U16: np.uint16,
64
+ ImageEncoding.DEPTH_F32: np.float32,
65
+ }
66
+ return np.dtype(dtype_map[self])
67
+
68
+ @property
69
+ def channels(self) -> int:
70
+ """
71
+ Get the number of color channels.
72
+
73
+ :return: The number of color channels.
74
+ """
75
+
76
+ if self in (
77
+ ImageEncoding.MONO8,
78
+ ImageEncoding.MONO16,
79
+ ImageEncoding.DEPTH_U16,
80
+ ImageEncoding.DEPTH_F32,
81
+ ):
82
+ return 1
83
+ elif self in (ImageEncoding.RGB8, ImageEncoding.BGR8):
84
+ return 3
85
+ elif self in (ImageEncoding.RGBA8, ImageEncoding.BGRA8):
86
+ return 4
87
+ else:
88
+ return 1
89
+
90
+
91
+ class Image(Message):
92
+ """
93
+ An image with raw data.
94
+
95
+ :param encoding: The encoding of the image.
96
+ :param width: The width of the image in pixels.
97
+ :param height: The height of the image in pixels.
98
+ :param data: The raw image data bytes.
99
+ """
100
+
101
+ _type = "antioch/image"
102
+ encoding: ImageEncoding
103
+ width: int
104
+ height: int
105
+ data: bytes
106
+
107
+ @classmethod
108
+ def from_numpy(cls, array: np.ndarray, encoding: ImageEncoding | None = None) -> Image:
109
+ """
110
+ Create an Image from a numpy array.
111
+
112
+ For uint8 encodings (RGB8, RGBA8, MONO8), pixel values should be in the range [0, 255].
113
+ For float32 depth encodings (DEPTH_F32), values are typically in meters.
114
+ For uint16 encodings (DEPTH_U16, MONO16), values use the full uint16 range.
115
+
116
+ If the array dtype doesn't match the encoding's dtype, it will be converted
117
+ via astype().
118
+
119
+ :param array: The numpy array containing image data.
120
+ :param encoding: The image encoding (auto-detected if None).
121
+ :return: An Image instance.
122
+ :raises ValueError: If array shape doesn't match a supported format.
123
+ """
124
+
125
+ # Auto-detect encoding based on array shape and dtype
126
+ if encoding is None:
127
+ if array.ndim == 2:
128
+ # Grayscale or depth
129
+ if array.dtype == np.uint8:
130
+ encoding = ImageEncoding.MONO8
131
+ elif array.dtype == np.uint16:
132
+ encoding = ImageEncoding.DEPTH_U16
133
+ elif array.dtype == np.float32:
134
+ encoding = ImageEncoding.DEPTH_F32
135
+ else:
136
+ raise ValueError(f"Unsupported dtype for 2D array: {array.dtype}")
137
+ elif array.ndim == 3:
138
+ # Color image
139
+ channels = array.shape[2]
140
+ if channels == 3:
141
+ encoding = ImageEncoding.RGB8
142
+ elif channels == 4:
143
+ encoding = ImageEncoding.RGBA8
144
+ else:
145
+ raise ValueError(f"Unsupported number of channels: {channels}")
146
+ else:
147
+ raise ValueError(f"Unsupported array dimensions: {array.ndim}")
148
+
149
+ # Convert array dtype to match encoding if necessary
150
+ expected_dtype = encoding.numpy_dtype
151
+ if array.dtype != expected_dtype:
152
+ array = array.astype(expected_dtype)
153
+
154
+ # Standard packing (handles both contiguous and non-contiguous)
155
+ if array.ndim < 2:
156
+ raise ValueError(f"Image array must be at least 2D, got shape {array.shape}")
157
+ height, width = array.shape[:2]
158
+ data = array.tobytes()
159
+ return cls(encoding=encoding, width=width, height=height, data=data)
160
+
161
+ def to_numpy(self) -> np.ndarray:
162
+ """
163
+ Convert the image to a numpy array.
164
+
165
+ :return: A numpy array with the image data.
166
+ """
167
+
168
+ # Standard contiguous buffer
169
+ shape = (self.height, self.width) if self.encoding.channels == 1 else (self.height, self.width, self.encoding.channels)
170
+ array = np.frombuffer(self.data, dtype=self.encoding.numpy_dtype)
171
+ return array.reshape(shape)
common/message/imu.py ADDED
@@ -0,0 +1,14 @@
1
+ from common.message.base import Message
2
+ from common.message.quaternion import Quaternion
3
+ from common.message.vector import Vector3
4
+
5
+
6
+ class ImuSample(Message):
7
+ """
8
+ IMU sensor sample data.
9
+ """
10
+
11
+ _type = "antioch/imu_sample"
12
+ linear_acceleration: Vector3
13
+ angular_velocity: Vector3
14
+ orientation: Quaternion
@@ -0,0 +1,47 @@
1
+ from common.message.base import Message
2
+
3
+
4
+ class JointState(Message):
5
+ """
6
+ State of a single joint.
7
+
8
+ Represents the complete physical state of a joint including its position,
9
+ velocity, and measured effort (force/torque).
10
+ """
11
+
12
+ _type = "antioch/joint_state"
13
+ position: float
14
+ velocity: float
15
+ effort: float
16
+
17
+
18
+ class JointTarget(Message):
19
+ """
20
+ Control target for a single joint.
21
+
22
+ Specifies desired position, velocity, and/or effort targets for a joint's
23
+ PD controller. All fields are optional - omitted values are not controlled.
24
+ """
25
+
26
+ _type = "antioch/joint_target"
27
+ position: float | None = None
28
+ velocity: float | None = None
29
+ effort: float | None = None
30
+
31
+
32
+ class JointStates(Message):
33
+ """
34
+ Collection of joint states for an actuator group.
35
+ """
36
+
37
+ _type = "antioch/joint_states"
38
+ states: list[JointState]
39
+
40
+
41
+ class JointTargets(Message):
42
+ """
43
+ Collection of joint targets for an actuator group.
44
+ """
45
+
46
+ _type = "antioch/joint_targets"
47
+ targets: list[JointTarget]
common/message/log.py ADDED
@@ -0,0 +1,31 @@
1
+ import time
2
+ from enum import Enum
3
+
4
+ from pydantic import Field
5
+
6
+ from common.message.base import Message
7
+
8
+
9
+ class LogLevel(str, Enum):
10
+ """
11
+ Log level.
12
+ """
13
+
14
+ DEBUG = "debug"
15
+ INFO = "info"
16
+ WARNING = "warning"
17
+ ERROR = "error"
18
+
19
+
20
+ class Log(Message):
21
+ """
22
+ Log entry structure.
23
+ """
24
+
25
+ _type = "antioch/log"
26
+ timestamp_us: int = Field(default_factory=lambda: int(time.time_ns() // 1000))
27
+ let_us: int
28
+ level: LogLevel
29
+ message: str | None = None
30
+ channel: str | None = None
31
+ telemetry: bytes | None = None
common/message/pir.py ADDED
@@ -0,0 +1,16 @@
1
+ from pydantic import Field
2
+
3
+ from common.message.base import Message
4
+
5
+
6
+ class PirStatus(Message):
7
+ """
8
+ PIR sensor status containing detection state and signal information.
9
+ """
10
+
11
+ _type = "antioch/pir_status"
12
+ is_detected: bool = Field(description="Whether motion is currently detected")
13
+ signal_strength: float = Field(description="Current analog signal value after filtering")
14
+ threshold: float = Field(description="Current detection threshold")
15
+ element_flux: list[float] = Field(default_factory=list, description="Raw accumulated IR flux per element")
16
+ element_signal: list[float] = Field(default_factory=list, description="Pyroelectric voltage signal per element (proportional to dT/dt)")
@@ -0,0 +1,109 @@
1
+ from __future__ import annotations
2
+
3
+ from common.message.base import Message
4
+
5
+
6
+ class Point2(Message):
7
+ """
8
+ A point representing a position in 2D space.
9
+
10
+ Used in image annotations and 2D coordinate systems.
11
+ """
12
+
13
+ _type = "antioch/point2"
14
+ x: float
15
+ y: float
16
+
17
+ def __repr__(self) -> str:
18
+ """
19
+ Return a readable string representation.
20
+
21
+ :return: String representation.
22
+ """
23
+
24
+ return f"Point2(x={self.x}, y={self.y})"
25
+
26
+ def __str__(self) -> str:
27
+ """
28
+ Return a readable string representation.
29
+
30
+ :return: String representation.
31
+ """
32
+
33
+ return f"Point2(x={self.x}, y={self.y})"
34
+
35
+ @classmethod
36
+ def new(cls, x: float, y: float) -> Point2:
37
+ """
38
+ Create a new 2D point.
39
+
40
+ :param x: The x coordinate.
41
+ :param y: The y coordinate.
42
+ :return: A 2D point.
43
+ """
44
+
45
+ return cls(x=x, y=y)
46
+
47
+ @classmethod
48
+ def zero(cls) -> Point2:
49
+ """
50
+ Create a point at the origin.
51
+
52
+ :return: A point at (0, 0).
53
+ """
54
+
55
+ return cls(x=0.0, y=0.0)
56
+
57
+
58
+ class Point3(Message):
59
+ """
60
+ A point representing a position in 3D space.
61
+
62
+ Used in 3D graphics and spatial coordinate systems.
63
+ """
64
+
65
+ _type = "antioch/point3"
66
+ x: float
67
+ y: float
68
+ z: float
69
+
70
+ def __repr__(self) -> str:
71
+ """
72
+ Return a readable string representation.
73
+
74
+ :return: String representation.
75
+ """
76
+
77
+ return f"Point3(x={self.x}, y={self.y}, z={self.z})"
78
+
79
+ def __str__(self) -> str:
80
+ """
81
+ Return a readable string representation.
82
+
83
+ :return: String representation.
84
+ """
85
+
86
+ return f"Point3(x={self.x}, y={self.y}, z={self.z})"
87
+
88
+ @classmethod
89
+ def new(cls, x: float, y: float, z: float) -> Point3:
90
+ """
91
+ Create a new 3D point.
92
+
93
+ :param x: The x coordinate.
94
+ :param y: The y coordinate.
95
+ :param z: The z coordinate.
96
+ :return: A 3D point.
97
+ """
98
+
99
+ return cls(x=x, y=y, z=z)
100
+
101
+ @classmethod
102
+ def zero(cls) -> Point3:
103
+ """
104
+ Create a point at the origin.
105
+
106
+ :return: A point at (0, 0, 0).
107
+ """
108
+
109
+ return cls(x=0.0, y=0.0, z=0.0)
@@ -0,0 +1,63 @@
1
+ import struct
2
+
3
+ from pydantic import Field, model_validator
4
+
5
+ from common.message.base import Message
6
+
7
+
8
+ class PointCloud(Message):
9
+ """
10
+ A collection of 3D points.
11
+
12
+ :param frame_id: Frame of reference.
13
+ :param x: X coordinates of points.
14
+ :param y: Y coordinates of points.
15
+ :param z: Z coordinates of points.
16
+ """
17
+
18
+ _type = "antioch/point_cloud"
19
+ frame_id: str = Field(default="", description="Frame of reference")
20
+ x: list[float] = Field(description="X coordinates of points")
21
+ y: list[float] = Field(description="Y coordinates of points")
22
+ z: list[float] = Field(description="Z coordinates of points")
23
+
24
+ @model_validator(mode="after")
25
+ def validate_array_lengths(self) -> "PointCloud":
26
+ """
27
+ Validate that all coordinate arrays have the same length.
28
+ """
29
+
30
+ lengths = [len(self.x), len(self.y), len(self.z)]
31
+
32
+ if len(set(lengths)) > 1:
33
+ raise ValueError(f"All coordinate arrays must have the same length: x={len(self.x)}, y={len(self.y)}, z={len(self.z)}")
34
+
35
+ return self
36
+
37
+ def to_bytes(self) -> bytes:
38
+ """
39
+ Pack point cloud data into bytes for Foxglove.
40
+
41
+ :return: Packed data with x, y, z for each point.
42
+ """
43
+
44
+ data = bytearray()
45
+ for i in range(len(self.x)):
46
+ data.extend(struct.pack("<fff", self.x[i], self.y[i], self.z[i]))
47
+
48
+ return bytes(data)
49
+
50
+ @staticmethod
51
+ def combine(point_clouds: list["PointCloud"], frame_id: str = "") -> "PointCloud":
52
+ """
53
+ Combine multiple point clouds into a single point cloud.
54
+
55
+ :param point_clouds: List of point clouds to combine.
56
+ :param frame_id: Frame of reference for the combined point cloud.
57
+ :return: Combined point cloud with all points.
58
+ """
59
+
60
+ all_x = sum((pc.x for pc in point_clouds), [])
61
+ all_y = sum((pc.y for pc in point_clouds), [])
62
+ all_z = sum((pc.z for pc in point_clouds), [])
63
+ return PointCloud(frame_id=frame_id, x=all_x, y=all_y, z=all_z)