antioch-py 2.0.6__py3-none-any.whl → 3.0.12__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.
- antioch/__init__.py +101 -0
- antioch/{module/execution.py → execution.py} +1 -1
- antioch/{module/input.py → input.py} +2 -4
- antioch/{module/module.py → module.py} +17 -34
- antioch/{module/node.py → node.py} +17 -16
- {antioch_py-2.0.6.dist-info → antioch_py-3.0.12.dist-info}/METADATA +8 -11
- antioch_py-3.0.12.dist-info/RECORD +61 -0
- {antioch_py-2.0.6.dist-info → antioch_py-3.0.12.dist-info}/WHEEL +1 -1
- antioch_py-3.0.12.dist-info/licenses/LICENSE +21 -0
- common/ark/__init__.py +6 -16
- common/ark/ark.py +23 -60
- common/ark/hardware.py +13 -37
- common/ark/kinematics.py +1 -1
- common/ark/module.py +22 -0
- common/ark/node.py +46 -3
- common/ark/scheduler.py +2 -29
- common/ark/sim.py +1 -1
- {antioch/module → common/ark}/token.py +17 -0
- common/assets/rigging.usd +0 -0
- common/constants.py +83 -4
- common/core/__init__.py +37 -24
- common/core/auth.py +87 -114
- common/core/container.py +261 -0
- common/core/registry.py +131 -152
- common/core/rome.py +251 -0
- common/core/telemetry.py +176 -0
- common/core/types.py +219 -0
- common/message/__init__.py +19 -3
- common/message/annotation.py +174 -23
- common/message/array.py +25 -1
- common/message/camera.py +23 -1
- common/message/color.py +32 -6
- common/message/detection.py +40 -0
- common/message/foxglove.py +20 -0
- common/message/frame.py +71 -7
- common/message/image.py +58 -9
- common/message/imu.py +24 -4
- common/message/joint.py +69 -10
- common/message/log.py +52 -7
- common/message/pir.py +22 -5
- common/message/plot.py +57 -0
- common/message/point.py +55 -6
- common/message/point_cloud.py +55 -19
- common/message/pose.py +59 -19
- common/message/quaternion.py +105 -92
- common/message/radar.py +195 -29
- common/message/twist.py +34 -0
- common/message/types.py +40 -5
- common/message/vector.py +180 -245
- common/sim/__init__.py +49 -0
- common/sim/objects.py +460 -0
- common/sim/state.py +11 -0
- common/utils/comms.py +30 -12
- common/utils/logger.py +26 -7
- antioch/message.py +0 -87
- antioch/module/__init__.py +0 -53
- antioch/session/__init__.py +0 -150
- antioch/session/ark.py +0 -504
- antioch/session/asset.py +0 -65
- antioch/session/error.py +0 -80
- antioch/session/record.py +0 -158
- antioch/session/scene.py +0 -1521
- antioch/session/session.py +0 -220
- antioch/session/task.py +0 -323
- antioch/session/views/__init__.py +0 -40
- antioch/session/views/animation.py +0 -189
- antioch/session/views/articulation.py +0 -245
- antioch/session/views/basis_curve.py +0 -186
- antioch/session/views/camera.py +0 -92
- antioch/session/views/collision.py +0 -75
- antioch/session/views/geometry.py +0 -74
- antioch/session/views/ground_plane.py +0 -63
- antioch/session/views/imu.py +0 -73
- antioch/session/views/joint.py +0 -64
- antioch/session/views/light.py +0 -175
- antioch/session/views/pir_sensor.py +0 -140
- antioch/session/views/radar.py +0 -73
- antioch/session/views/rigid_body.py +0 -282
- antioch/session/views/xform.py +0 -119
- antioch_py-2.0.6.dist-info/RECORD +0 -99
- antioch_py-2.0.6.dist-info/entry_points.txt +0 -2
- common/core/agent.py +0 -296
- common/core/task.py +0 -36
- common/rome/__init__.py +0 -9
- common/rome/client.py +0 -430
- common/rome/error.py +0 -16
- common/session/__init__.py +0 -54
- common/session/environment.py +0 -31
- common/session/sim.py +0 -240
- common/session/views/__init__.py +0 -263
- common/session/views/animation.py +0 -73
- common/session/views/articulation.py +0 -184
- common/session/views/basis_curve.py +0 -102
- common/session/views/camera.py +0 -147
- common/session/views/collision.py +0 -59
- common/session/views/geometry.py +0 -102
- common/session/views/ground_plane.py +0 -41
- common/session/views/imu.py +0 -66
- common/session/views/joint.py +0 -81
- common/session/views/light.py +0 -96
- common/session/views/pir_sensor.py +0 -115
- common/session/views/radar.py +0 -82
- common/session/views/rigid_body.py +0 -236
- common/session/views/viewport.py +0 -21
- common/session/views/xform.py +0 -39
- common/utils/usd.py +0 -12
- /antioch/{module/clock.py → clock.py} +0 -0
- {antioch_py-2.0.6.dist-info → antioch_py-3.0.12.dist-info}/top_level.txt +0 -0
- /common/message/{base.py → message.py} +0 -0
common/message/quaternion.py
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
3
5
|
import numpy as np
|
|
6
|
+
from foxglove.schemas import Quaternion as FoxgloveQuaternion
|
|
7
|
+
from pydantic import Field, model_validator
|
|
4
8
|
from scipy.spatial.transform import Rotation
|
|
5
9
|
|
|
6
|
-
from common.message.
|
|
10
|
+
from common.message.message import Message
|
|
7
11
|
|
|
8
12
|
try:
|
|
9
13
|
from pxr import Gf # type: ignore
|
|
@@ -15,57 +19,41 @@ except ImportError:
|
|
|
15
19
|
|
|
16
20
|
class Quaternion(Message):
|
|
17
21
|
"""
|
|
18
|
-
A quaternion for 3D rotations.
|
|
22
|
+
A quaternion for 3D rotations (w, x, y, z) where w is the scalar component.
|
|
23
|
+
|
|
24
|
+
Supports multiple construction styles:
|
|
25
|
+
- Quaternion(w=1.0, x=0.0, y=0.0, z=0.0) - keyword args
|
|
26
|
+
- Quaternion.from_rpy(roll, pitch, yaw) - from euler angles
|
|
27
|
+
- Quaternion.from_any([1.0, 0.0, 0.0, 0.0]) - from list/tuple
|
|
28
|
+
|
|
29
|
+
Example:
|
|
30
|
+
```python
|
|
31
|
+
from common.message import Quaternion
|
|
32
|
+
import math
|
|
33
|
+
|
|
34
|
+
# Create identity quaternion (no rotation)
|
|
35
|
+
identity = Quaternion.identity()
|
|
36
|
+
|
|
37
|
+
# Create from roll-pitch-yaw angles
|
|
38
|
+
quat = Quaternion.from_rpy(
|
|
39
|
+
roll=0.0,
|
|
40
|
+
pitch=0.0,
|
|
41
|
+
yaw=math.pi / 2, # 90 degrees
|
|
42
|
+
)
|
|
19
43
|
|
|
20
|
-
|
|
44
|
+
# Convert back to RPY
|
|
45
|
+
roll, pitch, yaw = quat.to_rpy()
|
|
21
46
|
|
|
22
|
-
|
|
23
|
-
Quaternion.
|
|
24
|
-
|
|
47
|
+
# Create from list (w, x, y, z order)
|
|
48
|
+
q = Quaternion.from_any([1.0, 0.0, 0.0, 0.0])
|
|
49
|
+
```
|
|
25
50
|
"""
|
|
26
51
|
|
|
27
52
|
_type = "antioch/quaternion"
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"""
|
|
33
|
-
Get the w (scalar) component.
|
|
34
|
-
|
|
35
|
-
:return: The w (scalar) component.
|
|
36
|
-
"""
|
|
37
|
-
|
|
38
|
-
return self.data[0]
|
|
39
|
-
|
|
40
|
-
@property
|
|
41
|
-
def x(self) -> float:
|
|
42
|
-
"""
|
|
43
|
-
Get the x component.
|
|
44
|
-
|
|
45
|
-
:return: The x component.
|
|
46
|
-
"""
|
|
47
|
-
|
|
48
|
-
return self.data[1]
|
|
49
|
-
|
|
50
|
-
@property
|
|
51
|
-
def y(self) -> float:
|
|
52
|
-
"""
|
|
53
|
-
Get the y component.
|
|
54
|
-
|
|
55
|
-
:return: The y component.
|
|
56
|
-
"""
|
|
57
|
-
|
|
58
|
-
return self.data[2]
|
|
59
|
-
|
|
60
|
-
@property
|
|
61
|
-
def z(self) -> float:
|
|
62
|
-
"""
|
|
63
|
-
Get the z component.
|
|
64
|
-
|
|
65
|
-
:return: The z component.
|
|
66
|
-
"""
|
|
67
|
-
|
|
68
|
-
return self.data[3]
|
|
53
|
+
w: float = Field(default=1.0, description="Scalar (real) component of the quaternion")
|
|
54
|
+
x: float = Field(default=0.0, description="X component of the quaternion vector part")
|
|
55
|
+
y: float = Field(default=0.0, description="Y component of the quaternion vector part")
|
|
56
|
+
z: float = Field(default=0.0, description="Z component of the quaternion vector part")
|
|
69
57
|
|
|
70
58
|
def __len__(self) -> int:
|
|
71
59
|
"""
|
|
@@ -83,7 +71,7 @@ class Quaternion(Message):
|
|
|
83
71
|
:return: Iterator over [w, x, y, z].
|
|
84
72
|
"""
|
|
85
73
|
|
|
86
|
-
return iter(self.
|
|
74
|
+
return iter((self.w, self.x, self.y, self.z))
|
|
87
75
|
|
|
88
76
|
def __getitem__(self, index: int) -> float:
|
|
89
77
|
"""
|
|
@@ -93,7 +81,7 @@ class Quaternion(Message):
|
|
|
93
81
|
:return: The component value.
|
|
94
82
|
"""
|
|
95
83
|
|
|
96
|
-
return self.
|
|
84
|
+
return (self.w, self.x, self.y, self.z)[index]
|
|
97
85
|
|
|
98
86
|
def __eq__(self, other: object) -> bool:
|
|
99
87
|
"""
|
|
@@ -105,7 +93,7 @@ class Quaternion(Message):
|
|
|
105
93
|
|
|
106
94
|
if not isinstance(other, Quaternion):
|
|
107
95
|
return False
|
|
108
|
-
return self.
|
|
96
|
+
return self.w == other.w and self.x == other.x and self.y == other.y and self.z == other.z
|
|
109
97
|
|
|
110
98
|
def __repr__(self) -> str:
|
|
111
99
|
"""
|
|
@@ -114,7 +102,7 @@ class Quaternion(Message):
|
|
|
114
102
|
:return: String representation.
|
|
115
103
|
"""
|
|
116
104
|
|
|
117
|
-
return f"Quaternion(
|
|
105
|
+
return f"Quaternion({self.w}, {self.x}, {self.y}, {self.z})"
|
|
118
106
|
|
|
119
107
|
def __str__(self) -> str:
|
|
120
108
|
"""
|
|
@@ -123,45 +111,53 @@ class Quaternion(Message):
|
|
|
123
111
|
:return: String representation.
|
|
124
112
|
"""
|
|
125
113
|
|
|
126
|
-
return f"Quaternion(
|
|
114
|
+
return f"Quaternion({self.w}, {self.x}, {self.y}, {self.z})"
|
|
127
115
|
|
|
128
116
|
@classmethod
|
|
129
|
-
def
|
|
117
|
+
def identity(cls) -> Quaternion:
|
|
130
118
|
"""
|
|
131
|
-
Create
|
|
119
|
+
Create an identity quaternion (no rotation).
|
|
132
120
|
|
|
133
|
-
:
|
|
134
|
-
:param x: The x component.
|
|
135
|
-
:param y: The y component.
|
|
136
|
-
:param z: The z component.
|
|
137
|
-
:return: A quaternion.
|
|
121
|
+
:return: An identity quaternion [1, 0, 0, 0].
|
|
138
122
|
"""
|
|
139
123
|
|
|
140
|
-
return cls(
|
|
124
|
+
return cls(w=1.0, x=0.0, y=0.0, z=0.0)
|
|
141
125
|
|
|
142
126
|
@classmethod
|
|
143
|
-
def
|
|
127
|
+
def from_any(cls, value: Quaternion | tuple | list | dict | np.ndarray | None) -> Quaternion:
|
|
144
128
|
"""
|
|
145
|
-
Create
|
|
129
|
+
Create Quaternion from any compatible type.
|
|
146
130
|
|
|
147
|
-
:
|
|
131
|
+
Accepts:
|
|
132
|
+
- Quaternion instance (returned as-is)
|
|
133
|
+
- Tuple/list of 4 floats: (w, x, y, z)
|
|
134
|
+
- Dict with 'w', 'x', 'y', and 'z' keys
|
|
135
|
+
- Numpy array of shape (4,)
|
|
136
|
+
- None (returns identity quaternion)
|
|
137
|
+
|
|
138
|
+
:param value: Any compatible input type.
|
|
139
|
+
:return: A Quaternion instance.
|
|
148
140
|
"""
|
|
149
141
|
|
|
150
|
-
|
|
142
|
+
if value is None:
|
|
143
|
+
return cls.identity()
|
|
144
|
+
if isinstance(value, cls):
|
|
145
|
+
return value
|
|
146
|
+
return cls.model_validate(value)
|
|
151
147
|
|
|
152
148
|
@classmethod
|
|
153
149
|
def from_numpy(cls, array: np.ndarray) -> Quaternion:
|
|
154
150
|
"""
|
|
155
151
|
Create a quaternion from a numpy array [w, x, y, z].
|
|
156
152
|
|
|
157
|
-
:param array: The numpy array
|
|
153
|
+
:param array: The numpy array (must have 4 elements).
|
|
158
154
|
:return: A quaternion.
|
|
159
155
|
:raises ValueError: If the array does not have exactly 4 elements.
|
|
160
156
|
"""
|
|
161
157
|
|
|
162
158
|
if array.shape != (4,):
|
|
163
159
|
raise ValueError(f"Quaternion array must have shape (4,), got {array.shape}")
|
|
164
|
-
return cls(
|
|
160
|
+
return cls(w=float(array[0]), x=float(array[1]), y=float(array[2]), z=float(array[3]))
|
|
165
161
|
|
|
166
162
|
@classmethod
|
|
167
163
|
def from_rpy(cls, roll: float, pitch: float, yaw: float) -> Quaternion:
|
|
@@ -174,16 +170,9 @@ class Quaternion(Message):
|
|
|
174
170
|
:return: A quaternion.
|
|
175
171
|
"""
|
|
176
172
|
|
|
177
|
-
rotation = Rotation.from_euler("
|
|
173
|
+
rotation = Rotation.from_euler("XYZ", [roll, pitch, yaw])
|
|
178
174
|
quat_xyzw = rotation.as_quat()
|
|
179
|
-
return cls(
|
|
180
|
-
data=(
|
|
181
|
-
float(quat_xyzw[3]),
|
|
182
|
-
float(quat_xyzw[0]),
|
|
183
|
-
float(quat_xyzw[1]),
|
|
184
|
-
float(quat_xyzw[2]),
|
|
185
|
-
)
|
|
186
|
-
)
|
|
175
|
+
return cls(w=float(quat_xyzw[3]), x=float(quat_xyzw[0]), y=float(quat_xyzw[1]), z=float(quat_xyzw[2]))
|
|
187
176
|
|
|
188
177
|
def to_numpy(self) -> np.ndarray:
|
|
189
178
|
"""
|
|
@@ -192,7 +181,7 @@ class Quaternion(Message):
|
|
|
192
181
|
:return: The numpy array [w, x, y, z].
|
|
193
182
|
"""
|
|
194
183
|
|
|
195
|
-
return np.array(self.
|
|
184
|
+
return np.array([self.w, self.x, self.y, self.z], dtype=np.float32)
|
|
196
185
|
|
|
197
186
|
def to_list(self) -> list[float]:
|
|
198
187
|
"""
|
|
@@ -201,7 +190,16 @@ class Quaternion(Message):
|
|
|
201
190
|
:return: The list [w, x, y, z].
|
|
202
191
|
"""
|
|
203
192
|
|
|
204
|
-
return
|
|
193
|
+
return [self.w, self.x, self.y, self.z]
|
|
194
|
+
|
|
195
|
+
def to_tuple(self) -> tuple[float, float, float, float]:
|
|
196
|
+
"""
|
|
197
|
+
Convert the quaternion to a tuple.
|
|
198
|
+
|
|
199
|
+
:return: The tuple (w, x, y, z).
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
return (self.w, self.x, self.y, self.z)
|
|
205
203
|
|
|
206
204
|
def to_gf_quatf(self) -> Gf.Quatf:
|
|
207
205
|
"""
|
|
@@ -213,8 +211,8 @@ class Quaternion(Message):
|
|
|
213
211
|
:raises ImportError: If pxr is not installed.
|
|
214
212
|
"""
|
|
215
213
|
|
|
216
|
-
|
|
217
|
-
|
|
214
|
+
from pxr import Gf
|
|
215
|
+
|
|
218
216
|
return Gf.Quatf(self.w, self.x, self.y, self.z)
|
|
219
217
|
|
|
220
218
|
def to_gf_quatd(self) -> Gf.Quatd:
|
|
@@ -227,8 +225,8 @@ class Quaternion(Message):
|
|
|
227
225
|
:raises ImportError: If pxr is not installed.
|
|
228
226
|
"""
|
|
229
227
|
|
|
230
|
-
|
|
231
|
-
|
|
228
|
+
from pxr import Gf
|
|
229
|
+
|
|
232
230
|
return Gf.Quatd(self.w, self.x, self.y, self.z)
|
|
233
231
|
|
|
234
232
|
def normalize(self) -> Quaternion:
|
|
@@ -242,13 +240,11 @@ class Quaternion(Message):
|
|
|
242
240
|
norm = np.linalg.norm(arr)
|
|
243
241
|
if norm > 0:
|
|
244
242
|
normalized = arr / norm
|
|
245
|
-
return
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
float(normalized[3]),
|
|
251
|
-
)
|
|
243
|
+
return Quaternion(
|
|
244
|
+
w=float(normalized[0]),
|
|
245
|
+
x=float(normalized[1]),
|
|
246
|
+
y=float(normalized[2]),
|
|
247
|
+
z=float(normalized[3]),
|
|
252
248
|
)
|
|
253
249
|
return self.identity()
|
|
254
250
|
|
|
@@ -263,11 +259,28 @@ class Quaternion(Message):
|
|
|
263
259
|
rpy = rotation.as_euler("xyz")
|
|
264
260
|
return (float(rpy[0]), float(rpy[1]), float(rpy[2]))
|
|
265
261
|
|
|
266
|
-
def
|
|
262
|
+
def to_foxglove(self) -> FoxgloveQuaternion:
|
|
267
263
|
"""
|
|
268
|
-
Convert to
|
|
264
|
+
Convert to Foxglove Quaternion for telemetry.
|
|
269
265
|
|
|
270
|
-
:return:
|
|
266
|
+
:return: Foxglove Quaternion schema.
|
|
267
|
+
"""
|
|
268
|
+
|
|
269
|
+
return FoxgloveQuaternion(x=self.x, y=self.y, z=self.z, w=self.w)
|
|
270
|
+
|
|
271
|
+
@model_validator(mode="before")
|
|
272
|
+
@classmethod
|
|
273
|
+
def _convert_input(cls, data: Any) -> Any:
|
|
274
|
+
"""
|
|
275
|
+
Convert various input types to Quaternion format.
|
|
271
276
|
"""
|
|
272
277
|
|
|
273
|
-
|
|
278
|
+
if isinstance(data, cls):
|
|
279
|
+
return {"w": data.w, "x": data.x, "y": data.y, "z": data.z}
|
|
280
|
+
if isinstance(data, (tuple, list)) and len(data) == 4:
|
|
281
|
+
return {"w": float(data[0]), "x": float(data[1]), "y": float(data[2]), "z": float(data[3])}
|
|
282
|
+
if isinstance(data, np.ndarray) and data.shape == (4,):
|
|
283
|
+
return {"w": float(data[0]), "x": float(data[1]), "y": float(data[2]), "z": float(data[3])}
|
|
284
|
+
if isinstance(data, dict) and all(k in data for k in ("w", "x", "y", "z")):
|
|
285
|
+
return {"w": float(data["w"]), "x": float(data["x"]), "y": float(data["y"]), "z": float(data["z"])}
|
|
286
|
+
return data
|
common/message/radar.py
CHANGED
|
@@ -1,31 +1,106 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
1
6
|
from pydantic import Field
|
|
2
7
|
|
|
3
|
-
from common.message.
|
|
8
|
+
from common.message.array import Array
|
|
9
|
+
from common.message.image import Image, ImageEncoding
|
|
10
|
+
from common.message.message import Message
|
|
4
11
|
from common.message.point_cloud import PointCloud
|
|
5
|
-
from common.message.vector import Vector3
|
|
6
12
|
|
|
7
13
|
|
|
8
|
-
class
|
|
9
|
-
"""
|
|
10
|
-
Single radar detection point with position and properties.
|
|
14
|
+
class RadarScan(Message):
|
|
11
15
|
"""
|
|
16
|
+
Radar scan data containing all detections from a single scan.
|
|
12
17
|
|
|
13
|
-
|
|
14
|
-
position: Vector3 = Field(description="3D position of detection in sensor frame")
|
|
15
|
-
range: float = Field(description="Range to target in meters")
|
|
16
|
-
azimuth: float = Field(description="Azimuth angle in radians")
|
|
17
|
-
elevation: float = Field(description="Elevation angle in radians")
|
|
18
|
-
velocity: float = Field(default=0.0, description="Radial velocity in m/s (positive = moving away)")
|
|
19
|
-
rcs: float = Field(default=0.0, description="Radar cross section in dBsm")
|
|
18
|
+
Data is stored as numpy arrays (via Array) for efficient processing.
|
|
20
19
|
|
|
20
|
+
Example:
|
|
21
|
+
```python
|
|
22
|
+
from common.message import RadarScan
|
|
21
23
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
# Access detection data efficiently via arrays
|
|
25
|
+
scan = radar.get_scan()
|
|
26
|
+
ranges = scan.ranges.to_numpy()
|
|
27
|
+
rcs = scan.rcs.to_numpy()
|
|
28
|
+
|
|
29
|
+
# Filter detections by range
|
|
30
|
+
close_mask = ranges < 10.0
|
|
31
|
+
close_rcs = rcs[close_mask]
|
|
32
|
+
|
|
33
|
+
# Convert to point cloud for visualization
|
|
34
|
+
point_cloud = scan.to_point_cloud(frame_id="radar_link")
|
|
35
|
+
```
|
|
25
36
|
"""
|
|
26
37
|
|
|
27
38
|
_type = "antioch/radar_scan"
|
|
28
|
-
|
|
39
|
+
|
|
40
|
+
# Raw spherical coordinates (stored as Arrays for serialization)
|
|
41
|
+
ranges: Array = Field(default_factory=lambda: Array.zeros(0), description="Range to targets in meters")
|
|
42
|
+
azimuths: Array = Field(default_factory=lambda: Array.zeros(0), description="Azimuth angles in radians")
|
|
43
|
+
elevations: Array = Field(default_factory=lambda: Array.zeros(0), description="Elevation angles in radians")
|
|
44
|
+
rcs: Array = Field(default_factory=lambda: Array.zeros(0), description="Radar cross section in dBsm")
|
|
45
|
+
velocities: Array = Field(default_factory=lambda: Array.zeros(0), description="Radial velocities in m/s")
|
|
46
|
+
|
|
47
|
+
# Cartesian positions (computed from spherical, stored for efficiency)
|
|
48
|
+
x: Array = Field(default_factory=lambda: Array.zeros(0), description="X positions in sensor frame")
|
|
49
|
+
y: Array = Field(default_factory=lambda: Array.zeros(0), description="Y positions in sensor frame")
|
|
50
|
+
z: Array = Field(default_factory=lambda: Array.zeros(0), description="Z positions in sensor frame")
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def num_detections(self) -> int:
|
|
54
|
+
"""
|
|
55
|
+
Number of detections in this scan.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
return len(self.ranges)
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def from_arrays(
|
|
62
|
+
cls,
|
|
63
|
+
ranges: np.ndarray,
|
|
64
|
+
azimuths: np.ndarray,
|
|
65
|
+
elevations: np.ndarray,
|
|
66
|
+
rcs: np.ndarray,
|
|
67
|
+
velocities: np.ndarray,
|
|
68
|
+
x: np.ndarray,
|
|
69
|
+
y: np.ndarray,
|
|
70
|
+
z: np.ndarray,
|
|
71
|
+
) -> RadarScan:
|
|
72
|
+
"""
|
|
73
|
+
Create RadarScan directly from numpy arrays.
|
|
74
|
+
|
|
75
|
+
This is the most efficient way to create a RadarScan as it avoids
|
|
76
|
+
creating any intermediate Python objects.
|
|
77
|
+
|
|
78
|
+
:param ranges: Range values in meters.
|
|
79
|
+
:param azimuths: Azimuth angles in radians.
|
|
80
|
+
:param elevations: Elevation angles in radians.
|
|
81
|
+
:param rcs: RCS values in dBsm.
|
|
82
|
+
:param velocities: Radial velocities in m/s.
|
|
83
|
+
:param x: X positions in sensor frame.
|
|
84
|
+
:param y: Y positions in sensor frame.
|
|
85
|
+
:param z: Z positions in sensor frame.
|
|
86
|
+
:return: RadarScan instance.
|
|
87
|
+
:raises ValueError: If arrays have mismatched lengths.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
n = len(ranges)
|
|
91
|
+
if not all(len(arr) == n for arr in [azimuths, elevations, rcs, velocities, x, y, z]):
|
|
92
|
+
raise ValueError("All arrays must have the same length")
|
|
93
|
+
|
|
94
|
+
return cls(
|
|
95
|
+
ranges=Array.from_numpy(ranges.astype(np.float32)),
|
|
96
|
+
azimuths=Array.from_numpy(azimuths.astype(np.float32)),
|
|
97
|
+
elevations=Array.from_numpy(elevations.astype(np.float32)),
|
|
98
|
+
rcs=Array.from_numpy(rcs.astype(np.float32)),
|
|
99
|
+
velocities=Array.from_numpy(velocities.astype(np.float32)),
|
|
100
|
+
x=Array.from_numpy(x.astype(np.float32)),
|
|
101
|
+
y=Array.from_numpy(y.astype(np.float32)),
|
|
102
|
+
z=Array.from_numpy(z.astype(np.float32)),
|
|
103
|
+
)
|
|
29
104
|
|
|
30
105
|
def to_point_cloud(self, frame_id: str = "radar") -> PointCloud:
|
|
31
106
|
"""
|
|
@@ -35,24 +110,115 @@ class RadarScan(Message):
|
|
|
35
110
|
:return: PointCloud with detection positions.
|
|
36
111
|
"""
|
|
37
112
|
|
|
38
|
-
if not self.detections:
|
|
39
|
-
return PointCloud(frame_id=frame_id, x=[], y=[], z=[])
|
|
40
|
-
|
|
41
113
|
return PointCloud(
|
|
42
114
|
frame_id=frame_id,
|
|
43
|
-
x=
|
|
44
|
-
y=
|
|
45
|
-
z=
|
|
115
|
+
x=self.x.to_list(),
|
|
116
|
+
y=self.y.to_list(),
|
|
117
|
+
z=self.z.to_list(),
|
|
46
118
|
)
|
|
47
119
|
|
|
48
|
-
|
|
49
|
-
|
|
120
|
+
def to_foxglove(self) -> dict[str, Any]:
|
|
121
|
+
"""
|
|
122
|
+
Convert to a JSON-serializable dict for Foxglove visualization.
|
|
123
|
+
|
|
124
|
+
:return: Dictionary with all scan data as lists.
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
"num_detections": self.num_detections,
|
|
129
|
+
"ranges": self.ranges.to_list(),
|
|
130
|
+
"azimuths": self.azimuths.to_list(),
|
|
131
|
+
"elevations": self.elevations.to_list(),
|
|
132
|
+
"rcs": self.rcs.to_list(),
|
|
133
|
+
"velocities": self.velocities.to_list(),
|
|
134
|
+
"x": self.x.to_list(),
|
|
135
|
+
"y": self.y.to_list(),
|
|
136
|
+
"z": self.z.to_list(),
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class RangeMap(Message):
|
|
141
|
+
"""
|
|
142
|
+
1D range bin map for radar processing.
|
|
143
|
+
|
|
144
|
+
Represents radar signal strength across range bins, commonly used for
|
|
145
|
+
MTI (Moving Target Indication) processing and detection.
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
_type = "antioch/range_map"
|
|
149
|
+
|
|
150
|
+
r_bins: int = Field(description="Number of range bins")
|
|
151
|
+
r_min_m: float = Field(description="Minimum range in meters (inclusive)")
|
|
152
|
+
r_max_m: float = Field(description="Maximum range in meters (inclusive)")
|
|
153
|
+
dr_m: float = Field(description="Range bin size in meters")
|
|
154
|
+
data: Array = Field(description="Float32 array of shape (r_bins,)")
|
|
155
|
+
|
|
156
|
+
def to_numpy(self) -> np.ndarray:
|
|
157
|
+
"""
|
|
158
|
+
Convert the range map to a 1D numpy array.
|
|
159
|
+
|
|
160
|
+
:return: float32 array with shape (r_bins,).
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
return self.data.to_numpy().reshape(self.r_bins)
|
|
164
|
+
|
|
165
|
+
def to_image(
|
|
166
|
+
self,
|
|
167
|
+
*,
|
|
168
|
+
transform: str = "log1p",
|
|
169
|
+
encoding: ImageEncoding = ImageEncoding.MONO8,
|
|
170
|
+
clip_percentile: float = 99.5,
|
|
171
|
+
height: int = 32,
|
|
172
|
+
) -> Image:
|
|
173
|
+
"""
|
|
174
|
+
Convert this range map into an Image for visualization.
|
|
175
|
+
|
|
176
|
+
The image is a horizontal bar where x-axis is range bins.
|
|
177
|
+
|
|
178
|
+
:param transform: "linear" or "log1p".
|
|
179
|
+
:param encoding: Image encoding to use (default MONO8).
|
|
180
|
+
:param clip_percentile: When encoding is MONO8, clip values to this percentile.
|
|
181
|
+
:param height: Height of the output image in pixels.
|
|
182
|
+
:return: Image representing the range map.
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
data = self.to_numpy()
|
|
186
|
+
if transform == "log1p":
|
|
187
|
+
values = np.log1p(data).astype(np.float32)
|
|
188
|
+
elif transform == "linear":
|
|
189
|
+
values = data.astype(np.float32, copy=False)
|
|
190
|
+
else:
|
|
191
|
+
raise ValueError(f"Unsupported transform '{transform}'")
|
|
192
|
+
|
|
193
|
+
if encoding == ImageEncoding.DEPTH_F32:
|
|
194
|
+
tiled = np.tile(values.reshape(1, -1), (height, 1))
|
|
195
|
+
return Image.from_numpy(tiled.astype(np.float32, copy=False), encoding=encoding)
|
|
196
|
+
|
|
197
|
+
if encoding != ImageEncoding.MONO8:
|
|
198
|
+
raise ValueError(f"Unsupported encoding '{encoding.value}' for range map image")
|
|
199
|
+
|
|
200
|
+
# Normalize to 8-bit
|
|
201
|
+
vmax = float(np.percentile(values, clip_percentile)) if values.size else 0.0
|
|
202
|
+
if vmax <= 0.0 or not np.isfinite(vmax):
|
|
203
|
+
mono = np.zeros((height, self.r_bins), dtype=np.uint8)
|
|
204
|
+
else:
|
|
205
|
+
scaled = np.clip(values / np.float32(vmax), 0.0, 1.0)
|
|
206
|
+
mono_row = (scaled * np.float32(255.0)).astype(np.uint8, copy=False)
|
|
207
|
+
mono = np.tile(mono_row.reshape(1, -1), (height, 1))
|
|
208
|
+
|
|
209
|
+
return Image.from_numpy(mono, encoding=encoding)
|
|
210
|
+
|
|
211
|
+
def to_foxglove(self) -> dict[str, Any]:
|
|
50
212
|
"""
|
|
51
|
-
|
|
213
|
+
Convert to a JSON-serializable dict for Foxglove visualization.
|
|
52
214
|
|
|
53
|
-
:
|
|
54
|
-
:return: Combined radar scan with all detections.
|
|
215
|
+
:return: Dictionary with range map metadata and data as list.
|
|
55
216
|
"""
|
|
56
217
|
|
|
57
|
-
|
|
58
|
-
|
|
218
|
+
return {
|
|
219
|
+
"r_bins": self.r_bins,
|
|
220
|
+
"r_min_m": self.r_min_m,
|
|
221
|
+
"r_max_m": self.r_max_m,
|
|
222
|
+
"dr_m": self.dr_m,
|
|
223
|
+
"data": self.data.to_list(),
|
|
224
|
+
}
|
common/message/twist.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from pydantic import Field
|
|
2
|
+
|
|
3
|
+
from common.message.message import Message
|
|
4
|
+
from common.message.vector import Vector3
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Twist(Message):
|
|
8
|
+
"""
|
|
9
|
+
Linear and angular velocity (twist).
|
|
10
|
+
|
|
11
|
+
Represents the velocity of a rigid body in 3D space, combining both
|
|
12
|
+
linear velocity (translation) and angular velocity (rotation).
|
|
13
|
+
|
|
14
|
+
Example:
|
|
15
|
+
```python
|
|
16
|
+
from common.message import Twist, Vector3
|
|
17
|
+
|
|
18
|
+
# Create a twist for forward motion with rotation
|
|
19
|
+
twist = Twist(
|
|
20
|
+
linear=Vector3(x=1.0, y=0.0, z=0.0), # 1 m/s forward
|
|
21
|
+
angular=Vector3(x=0.0, y=0.0, z=0.1), # 0.1 rad/s yaw
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# Zero velocity
|
|
25
|
+
stationary = Twist(
|
|
26
|
+
linear=Vector3.zeros(),
|
|
27
|
+
angular=Vector3.zeros(),
|
|
28
|
+
)
|
|
29
|
+
```
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
_type = "antioch/twist"
|
|
33
|
+
linear: Vector3 = Field(description="Linear velocity in m/s (x, y, z)")
|
|
34
|
+
angular: Vector3 = Field(description="Angular velocity in rad/s (x, y, z)")
|
common/message/types.py
CHANGED
|
@@ -1,37 +1,72 @@
|
|
|
1
|
-
from
|
|
1
|
+
from pydantic import Field
|
|
2
|
+
|
|
3
|
+
from common.message.message import Message
|
|
2
4
|
|
|
3
5
|
|
|
4
6
|
class Bool(Message):
|
|
5
7
|
"""
|
|
6
8
|
Boolean value message.
|
|
9
|
+
|
|
10
|
+
Example:
|
|
11
|
+
```python
|
|
12
|
+
from common.message import Bool
|
|
13
|
+
|
|
14
|
+
msg = Bool(value=True)
|
|
15
|
+
if msg.value:
|
|
16
|
+
print("Value is true")
|
|
17
|
+
```
|
|
7
18
|
"""
|
|
8
19
|
|
|
9
20
|
_type = "antioch/bool"
|
|
10
|
-
value: bool
|
|
21
|
+
value: bool = Field(description="Boolean value")
|
|
11
22
|
|
|
12
23
|
|
|
13
24
|
class Int(Message):
|
|
14
25
|
"""
|
|
15
26
|
Integer value message.
|
|
27
|
+
|
|
28
|
+
Example:
|
|
29
|
+
```python
|
|
30
|
+
from common.message import Int
|
|
31
|
+
|
|
32
|
+
msg = Int(value=42)
|
|
33
|
+
print(f"The answer is {msg.value}")
|
|
34
|
+
```
|
|
16
35
|
"""
|
|
17
36
|
|
|
18
37
|
_type = "antioch/int"
|
|
19
|
-
value: int
|
|
38
|
+
value: int = Field(description="Integer value")
|
|
20
39
|
|
|
21
40
|
|
|
22
41
|
class Float(Message):
|
|
23
42
|
"""
|
|
24
43
|
Float value message.
|
|
44
|
+
|
|
45
|
+
Example:
|
|
46
|
+
```python
|
|
47
|
+
from common.message import Float
|
|
48
|
+
|
|
49
|
+
msg = Float(value=3.14159)
|
|
50
|
+
print(f"Pi is approximately {msg.value:.2f}")
|
|
51
|
+
```
|
|
25
52
|
"""
|
|
26
53
|
|
|
27
54
|
_type = "antioch/float"
|
|
28
|
-
value: float
|
|
55
|
+
value: float = Field(description="Floating-point value")
|
|
29
56
|
|
|
30
57
|
|
|
31
58
|
class String(Message):
|
|
32
59
|
"""
|
|
33
60
|
String value message.
|
|
61
|
+
|
|
62
|
+
Example:
|
|
63
|
+
```python
|
|
64
|
+
from common.message import String
|
|
65
|
+
|
|
66
|
+
msg = String(value="Hello, World!")
|
|
67
|
+
print(msg.value)
|
|
68
|
+
```
|
|
34
69
|
"""
|
|
35
70
|
|
|
36
71
|
_type = "antioch/string"
|
|
37
|
-
value: str
|
|
72
|
+
value: str = Field(description="String value")
|