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.

Files changed (109) hide show
  1. antioch/__init__.py +101 -0
  2. antioch/{module/execution.py → execution.py} +1 -1
  3. antioch/{module/input.py → input.py} +2 -4
  4. antioch/{module/module.py → module.py} +17 -34
  5. antioch/{module/node.py → node.py} +17 -16
  6. {antioch_py-2.0.6.dist-info → antioch_py-3.0.12.dist-info}/METADATA +8 -11
  7. antioch_py-3.0.12.dist-info/RECORD +61 -0
  8. {antioch_py-2.0.6.dist-info → antioch_py-3.0.12.dist-info}/WHEEL +1 -1
  9. antioch_py-3.0.12.dist-info/licenses/LICENSE +21 -0
  10. common/ark/__init__.py +6 -16
  11. common/ark/ark.py +23 -60
  12. common/ark/hardware.py +13 -37
  13. common/ark/kinematics.py +1 -1
  14. common/ark/module.py +22 -0
  15. common/ark/node.py +46 -3
  16. common/ark/scheduler.py +2 -29
  17. common/ark/sim.py +1 -1
  18. {antioch/module → common/ark}/token.py +17 -0
  19. common/assets/rigging.usd +0 -0
  20. common/constants.py +83 -4
  21. common/core/__init__.py +37 -24
  22. common/core/auth.py +87 -114
  23. common/core/container.py +261 -0
  24. common/core/registry.py +131 -152
  25. common/core/rome.py +251 -0
  26. common/core/telemetry.py +176 -0
  27. common/core/types.py +219 -0
  28. common/message/__init__.py +19 -3
  29. common/message/annotation.py +174 -23
  30. common/message/array.py +25 -1
  31. common/message/camera.py +23 -1
  32. common/message/color.py +32 -6
  33. common/message/detection.py +40 -0
  34. common/message/foxglove.py +20 -0
  35. common/message/frame.py +71 -7
  36. common/message/image.py +58 -9
  37. common/message/imu.py +24 -4
  38. common/message/joint.py +69 -10
  39. common/message/log.py +52 -7
  40. common/message/pir.py +22 -5
  41. common/message/plot.py +57 -0
  42. common/message/point.py +55 -6
  43. common/message/point_cloud.py +55 -19
  44. common/message/pose.py +59 -19
  45. common/message/quaternion.py +105 -92
  46. common/message/radar.py +195 -29
  47. common/message/twist.py +34 -0
  48. common/message/types.py +40 -5
  49. common/message/vector.py +180 -245
  50. common/sim/__init__.py +49 -0
  51. common/sim/objects.py +460 -0
  52. common/sim/state.py +11 -0
  53. common/utils/comms.py +30 -12
  54. common/utils/logger.py +26 -7
  55. antioch/message.py +0 -87
  56. antioch/module/__init__.py +0 -53
  57. antioch/session/__init__.py +0 -150
  58. antioch/session/ark.py +0 -504
  59. antioch/session/asset.py +0 -65
  60. antioch/session/error.py +0 -80
  61. antioch/session/record.py +0 -158
  62. antioch/session/scene.py +0 -1521
  63. antioch/session/session.py +0 -220
  64. antioch/session/task.py +0 -323
  65. antioch/session/views/__init__.py +0 -40
  66. antioch/session/views/animation.py +0 -189
  67. antioch/session/views/articulation.py +0 -245
  68. antioch/session/views/basis_curve.py +0 -186
  69. antioch/session/views/camera.py +0 -92
  70. antioch/session/views/collision.py +0 -75
  71. antioch/session/views/geometry.py +0 -74
  72. antioch/session/views/ground_plane.py +0 -63
  73. antioch/session/views/imu.py +0 -73
  74. antioch/session/views/joint.py +0 -64
  75. antioch/session/views/light.py +0 -175
  76. antioch/session/views/pir_sensor.py +0 -140
  77. antioch/session/views/radar.py +0 -73
  78. antioch/session/views/rigid_body.py +0 -282
  79. antioch/session/views/xform.py +0 -119
  80. antioch_py-2.0.6.dist-info/RECORD +0 -99
  81. antioch_py-2.0.6.dist-info/entry_points.txt +0 -2
  82. common/core/agent.py +0 -296
  83. common/core/task.py +0 -36
  84. common/rome/__init__.py +0 -9
  85. common/rome/client.py +0 -430
  86. common/rome/error.py +0 -16
  87. common/session/__init__.py +0 -54
  88. common/session/environment.py +0 -31
  89. common/session/sim.py +0 -240
  90. common/session/views/__init__.py +0 -263
  91. common/session/views/animation.py +0 -73
  92. common/session/views/articulation.py +0 -184
  93. common/session/views/basis_curve.py +0 -102
  94. common/session/views/camera.py +0 -147
  95. common/session/views/collision.py +0 -59
  96. common/session/views/geometry.py +0 -102
  97. common/session/views/ground_plane.py +0 -41
  98. common/session/views/imu.py +0 -66
  99. common/session/views/joint.py +0 -81
  100. common/session/views/light.py +0 -96
  101. common/session/views/pir_sensor.py +0 -115
  102. common/session/views/radar.py +0 -82
  103. common/session/views/rigid_body.py +0 -236
  104. common/session/views/viewport.py +0 -21
  105. common/session/views/xform.py +0 -39
  106. common/utils/usd.py +0 -12
  107. /antioch/{module/clock.py → clock.py} +0 -0
  108. {antioch_py-2.0.6.dist-info → antioch_py-3.0.12.dist-info}/top_level.txt +0 -0
  109. /common/message/{base.py → message.py} +0 -0
@@ -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.base import 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
- Serializes as a tuple [w, x, y, z] for space efficiency where w is the scalar component.
44
+ # Convert back to RPY
45
+ roll, pitch, yaw = quat.to_rpy()
21
46
 
22
- Examples:
23
- Quaternion.new(1.0, 0.0, 0.0, 0.0) # Create identity quaternion
24
- Quaternion.from_rpy(0.0, 0.0, 1.57) # Create from RPY angles
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
- data: tuple[float, float, float, float]
29
-
30
- @property
31
- def w(self) -> float:
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.data)
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.data[index]
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.data == other.data
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(w={self.w}, x={self.x}, y={self.y}, z={self.z})"
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(w={self.w}, x={self.x}, y={self.y}, z={self.z})"
114
+ return f"Quaternion({self.w}, {self.x}, {self.y}, {self.z})"
127
115
 
128
116
  @classmethod
129
- def new(cls, w: float, x: float, y: float, z: float) -> Quaternion:
117
+ def identity(cls) -> Quaternion:
130
118
  """
131
- Create a new quaternion.
119
+ Create an identity quaternion (no rotation).
132
120
 
133
- :param w: The w component.
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(data=(w, x, y, z))
124
+ return cls(w=1.0, x=0.0, y=0.0, z=0.0)
141
125
 
142
126
  @classmethod
143
- def identity(cls) -> Quaternion:
127
+ def from_any(cls, value: Quaternion | tuple | list | dict | np.ndarray | None) -> Quaternion:
144
128
  """
145
- Create an identity quaternion (no rotation).
129
+ Create Quaternion from any compatible type.
146
130
 
147
- :return: An identity quaternion [1, 0, 0, 0].
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
- return cls(data=(1.0, 0.0, 0.0, 0.0))
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 to create the quaternion from (must have 4 elements).
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(data=(float(array[0]), float(array[1]), float(array[2]), float(array[3])))
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("xyz", [roll, pitch, yaw])
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.data, dtype=np.float32)
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 list(self.data)
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
- if not HAS_PXR:
217
- raise ImportError("pxr is not installed")
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
- if not HAS_PXR:
231
- raise ImportError("pxr is not installed")
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 self.__class__(
246
- data=(
247
- float(normalized[0]),
248
- float(normalized[1]),
249
- float(normalized[2]),
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 as_array(self) -> list[float]:
262
+ def to_foxglove(self) -> FoxgloveQuaternion:
267
263
  """
268
- Convert to a list (alias for to_list for Rust compatibility).
264
+ Convert to Foxglove Quaternion for telemetry.
269
265
 
270
- :return: The list [w, x, y, z].
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
- return self.to_list()
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.base import 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 RadarDetection(Message):
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
- _type = "antioch/radar_detection"
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
- class RadarScan(Message):
23
- """
24
- Radar scan data containing all detections from a single scan.
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
- detections: list[RadarDetection] = Field(default_factory=list, description="List of radar detections")
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=[d.position.x for d in self.detections],
44
- y=[d.position.y for d in self.detections],
45
- z=[d.position.z for d in self.detections],
115
+ x=self.x.to_list(),
116
+ y=self.y.to_list(),
117
+ z=self.z.to_list(),
46
118
  )
47
119
 
48
- @staticmethod
49
- def combine(scans: list["RadarScan"]) -> "RadarScan":
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
- Combine multiple radar scans into a single scan.
213
+ Convert to a JSON-serializable dict for Foxglove visualization.
52
214
 
53
- :param scans: List of radar scans to combine.
54
- :return: Combined radar scan with all detections.
215
+ :return: Dictionary with range map metadata and data as list.
55
216
  """
56
217
 
57
- detections = sum((scan.detections for scan in scans), [])
58
- return RadarScan(detections=detections)
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
+ }
@@ -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 common.message.base import Message
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")