robot-msgs 0.1.0__tar.gz
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.
- robot_msgs-0.1.0/.github/workflows/publish.yml +31 -0
- robot_msgs-0.1.0/.github/workflows/test.yml +16 -0
- robot_msgs-0.1.0/.gitignore +7 -0
- robot_msgs-0.1.0/.python-version +1 -0
- robot_msgs-0.1.0/LICENSE +26 -0
- robot_msgs-0.1.0/PKG-INFO +122 -0
- robot_msgs-0.1.0/README.md +95 -0
- robot_msgs-0.1.0/docs/DESIGN.md +305 -0
- robot_msgs-0.1.0/docs/build_report.py +219 -0
- robot_msgs-0.1.0/docs/report_template.html +220 -0
- robot_msgs-0.1.0/examples/gallery.py +208 -0
- robot_msgs-0.1.0/examples/ros_export.py +46 -0
- robot_msgs-0.1.0/examples/tour.py +26 -0
- robot_msgs-0.1.0/examples/ur5e_pick.py +135 -0
- robot_msgs-0.1.0/pyproject.toml +46 -0
- robot_msgs-0.1.0/src/robot_msgs/__init__.py +29 -0
- robot_msgs-0.1.0/src/robot_msgs/_core.py +352 -0
- robot_msgs-0.1.0/src/robot_msgs/compare/__init__.py +18 -0
- robot_msgs-0.1.0/src/robot_msgs/compare/_dispatch.py +92 -0
- robot_msgs-0.1.0/src/robot_msgs/compare/align.py +53 -0
- robot_msgs-0.1.0/src/robot_msgs/compare/comparison.py +52 -0
- robot_msgs-0.1.0/src/robot_msgs/compare/difference.py +116 -0
- robot_msgs-0.1.0/src/robot_msgs/compare/error_stats.py +62 -0
- robot_msgs-0.1.0/src/robot_msgs/compare/metrics.py +66 -0
- robot_msgs-0.1.0/src/robot_msgs/geometry/__init__.py +31 -0
- robot_msgs-0.1.0/src/robot_msgs/geometry/accel.py +52 -0
- robot_msgs-0.1.0/src/robot_msgs/geometry/inertia.py +99 -0
- robot_msgs-0.1.0/src/robot_msgs/geometry/point.py +79 -0
- robot_msgs-0.1.0/src/robot_msgs/geometry/pose.py +90 -0
- robot_msgs-0.1.0/src/robot_msgs/geometry/pose2d.py +33 -0
- robot_msgs-0.1.0/src/robot_msgs/geometry/pose_stamped.py +46 -0
- robot_msgs-0.1.0/src/robot_msgs/geometry/quaternion.py +177 -0
- robot_msgs-0.1.0/src/robot_msgs/geometry/rotations.py +251 -0
- robot_msgs-0.1.0/src/robot_msgs/geometry/transform.py +125 -0
- robot_msgs-0.1.0/src/robot_msgs/geometry/transform_stamped.py +32 -0
- robot_msgs-0.1.0/src/robot_msgs/geometry/twist.py +52 -0
- robot_msgs-0.1.0/src/robot_msgs/geometry/vector3.py +115 -0
- robot_msgs-0.1.0/src/robot_msgs/geometry/wrench.py +52 -0
- robot_msgs-0.1.0/src/robot_msgs/ros/__init__.py +19 -0
- robot_msgs-0.1.0/src/robot_msgs/ros/bag.py +57 -0
- robot_msgs-0.1.0/src/robot_msgs/ros/convert.py +939 -0
- robot_msgs-0.1.0/src/robot_msgs/ros/vision_msgs.py +46 -0
- robot_msgs-0.1.0/src/robot_msgs/sensor/__init__.py +9 -0
- robot_msgs-0.1.0/src/robot_msgs/sensor/camera_info.py +135 -0
- robot_msgs-0.1.0/src/robot_msgs/sensor/image.py +151 -0
- robot_msgs-0.1.0/src/robot_msgs/sensor/imu.py +36 -0
- robot_msgs-0.1.0/src/robot_msgs/sensor/joint_state.py +69 -0
- robot_msgs-0.1.0/src/robot_msgs/sensor/point_cloud.py +116 -0
- robot_msgs-0.1.0/src/robot_msgs/std/__init__.py +5 -0
- robot_msgs-0.1.0/src/robot_msgs/std/header.py +81 -0
- robot_msgs-0.1.0/src/robot_msgs/trajectory/__init__.py +29 -0
- robot_msgs-0.1.0/src/robot_msgs/trajectory/joint_limits.py +65 -0
- robot_msgs-0.1.0/src/robot_msgs/trajectory/joint_trajectory.py +208 -0
- robot_msgs-0.1.0/src/robot_msgs/trajectory/pose_trajectory.py +175 -0
- robot_msgs-0.1.0/src/robot_msgs/trajectory/time_repair.py +145 -0
- robot_msgs-0.1.0/src/robot_msgs/trajectory/trajectory.py +567 -0
- robot_msgs-0.1.0/src/robot_msgs/vision/__init__.py +21 -0
- robot_msgs-0.1.0/src/robot_msgs/vision/bounding_box2d.py +68 -0
- robot_msgs-0.1.0/src/robot_msgs/vision/bounding_box3d.py +50 -0
- robot_msgs-0.1.0/src/robot_msgs/vision/detection2d.py +43 -0
- robot_msgs-0.1.0/src/robot_msgs/vision/detection2d_array.py +80 -0
- robot_msgs-0.1.0/src/robot_msgs/vision/detection3d.py +43 -0
- robot_msgs-0.1.0/src/robot_msgs/vision/detection3d_array.py +104 -0
- robot_msgs-0.1.0/src/robot_msgs/vision/object_hypothesis.py +23 -0
- robot_msgs-0.1.0/src/robot_msgs/vision/object_hypothesis_with_pose.py +29 -0
- robot_msgs-0.1.0/src/robot_msgs/viz/__init__.py +21 -0
- robot_msgs-0.1.0/src/robot_msgs/viz/_common.py +167 -0
- robot_msgs-0.1.0/src/robot_msgs/viz/_dispatch.py +20 -0
- robot_msgs-0.1.0/src/robot_msgs/viz/images.py +215 -0
- robot_msgs-0.1.0/src/robot_msgs/viz/movie.py +280 -0
- robot_msgs-0.1.0/src/robot_msgs/viz/spatial.py +277 -0
- robot_msgs-0.1.0/src/robot_msgs/viz/trajectory.py +525 -0
- robot_msgs-0.1.0/tests/catalog.py +120 -0
- robot_msgs-0.1.0/tests/test_compare.py +271 -0
- robot_msgs-0.1.0/tests/test_contract.py +76 -0
- robot_msgs-0.1.0/tests/test_core.py +121 -0
- robot_msgs-0.1.0/tests/test_geometry.py +679 -0
- robot_msgs-0.1.0/tests/test_header_meta_and_from_samples.py +258 -0
- robot_msgs-0.1.0/tests/test_inertia_and_euler_matrix.py +145 -0
- robot_msgs-0.1.0/tests/test_joint_trajectory.py +142 -0
- robot_msgs-0.1.0/tests/test_pose_trajectory.py +86 -0
- robot_msgs-0.1.0/tests/test_ros.py +643 -0
- robot_msgs-0.1.0/tests/test_rotations.py +91 -0
- robot_msgs-0.1.0/tests/test_sensor.py +610 -0
- robot_msgs-0.1.0/tests/test_trajectory.py +197 -0
- robot_msgs-0.1.0/tests/test_vision.py +467 -0
- robot_msgs-0.1.0/tests/test_viz.py +672 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Publishes to PyPI when a version tag (v*) is pushed, through PyPI trusted publishing:
|
|
2
|
+
# no API token is stored anywhere. One-time setup on pypi.org: a "pending publisher" for
|
|
3
|
+
# project robot-msgs, owner zacharyyamaoka, repository robot_msgs, workflow publish.yml,
|
|
4
|
+
# environment pypi.
|
|
5
|
+
name: publish
|
|
6
|
+
on:
|
|
7
|
+
push:
|
|
8
|
+
tags: ["v*"]
|
|
9
|
+
workflow_dispatch:
|
|
10
|
+
jobs:
|
|
11
|
+
test:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- uses: astral-sh/setup-uv@v6
|
|
16
|
+
# WHY: test_viz writes a real .mp4 through matplotlib's FFMpegWriter; the runner image has no ffmpeg.
|
|
17
|
+
- run: sudo apt-get update -q && sudo apt-get install -y -q ffmpeg
|
|
18
|
+
- run: uv run --isolated --extra test python -m pytest -q
|
|
19
|
+
env:
|
|
20
|
+
MPLBACKEND: Agg
|
|
21
|
+
publish:
|
|
22
|
+
needs: test
|
|
23
|
+
runs-on: ubuntu-latest
|
|
24
|
+
environment: pypi
|
|
25
|
+
permissions:
|
|
26
|
+
id-token: write
|
|
27
|
+
steps:
|
|
28
|
+
- uses: actions/checkout@v4
|
|
29
|
+
- uses: astral-sh/setup-uv@v6
|
|
30
|
+
- run: uv build
|
|
31
|
+
- run: uv publish --trusted-publishing always
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
name: test
|
|
2
|
+
on: [push, pull_request]
|
|
3
|
+
jobs:
|
|
4
|
+
test:
|
|
5
|
+
runs-on: ubuntu-latest
|
|
6
|
+
strategy:
|
|
7
|
+
matrix:
|
|
8
|
+
python: ["3.11", "3.12", "3.13"]
|
|
9
|
+
steps:
|
|
10
|
+
- uses: actions/checkout@v4
|
|
11
|
+
- uses: astral-sh/setup-uv@v6
|
|
12
|
+
# WHY: test_viz writes a real .mp4 through matplotlib's FFMpegWriter; the runner image has no ffmpeg.
|
|
13
|
+
- run: sudo apt-get update -q && sudo apt-get install -y -q ffmpeg
|
|
14
|
+
- run: uv run --isolated --python ${{ matrix.python }} --extra test python -m pytest -q
|
|
15
|
+
env:
|
|
16
|
+
MPLBACKEND: Agg
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
robot_msgs-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Zach Yamaoka
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
src/robot_msgs/ros/vision_msgs.py embeds message definitions from
|
|
26
|
+
https://github.com/ros-perception/vision_msgs (tag 4.1.1), licensed Apache-2.0.
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: robot-msgs
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Standard robot message types as frozen Python dataclasses, with the helper functions ROS messages never had.
|
|
5
|
+
Project-URL: Homepage, https://robotmsgs.com
|
|
6
|
+
Project-URL: Source, https://github.com/zacharyyamaoka/robot_msgs
|
|
7
|
+
Author: Zach Yamaoka
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: dataclasses,geometry,messages,robotics,ros,trajectory
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering
|
|
15
|
+
Requires-Python: >=3.11
|
|
16
|
+
Requires-Dist: numpy>=1.26
|
|
17
|
+
Provides-Extra: ros
|
|
18
|
+
Requires-Dist: rosbags>=0.10; extra == 'ros'
|
|
19
|
+
Provides-Extra: test
|
|
20
|
+
Requires-Dist: matplotlib>=3.8; extra == 'test'
|
|
21
|
+
Requires-Dist: pytest>=8; extra == 'test'
|
|
22
|
+
Requires-Dist: rosbags>=0.10; extra == 'test'
|
|
23
|
+
Requires-Dist: scipy>=1.11; extra == 'test'
|
|
24
|
+
Provides-Extra: viz
|
|
25
|
+
Requires-Dist: matplotlib>=3.8; extra == 'viz'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# robot_msgs
|
|
29
|
+
|
|
30
|
+
A cohesive domain model for robot data: the standard message types — ROS's `geometry_msgs`,
|
|
31
|
+
`sensor_msgs`, `trajectory_msgs`, `vision_msgs` — as frozen Python dataclasses, **with the
|
|
32
|
+
functions that move between them**. Offsetting a pose, interpolating a trajectory, projecting a
|
|
33
|
+
point, turning a depth image into a cloud: first-class methods, not separate libraries.
|
|
34
|
+
|
|
35
|
+
- **ROS's shape, not ROS.** Same packages and field names (the ROS docs are our docs), no ROS
|
|
36
|
+
install, no codegen. ROS 2 messages and bags are an export (`robot_msgs.ros`).
|
|
37
|
+
- **Immutable values over compact numpy storage.** Every message is frozen; its arrays are
|
|
38
|
+
read-only, so slices, rows and columns are free views.
|
|
39
|
+
- **One runtime dependency: numpy.** Plots (`[viz]`) and ROS export (`[ros]`) are extras.
|
|
40
|
+
|
|
41
|
+
## A trajectory is one entity with many views
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
import numpy as np
|
|
45
|
+
|
|
46
|
+
from robot_msgs.geometry import Pose
|
|
47
|
+
from robot_msgs.trajectory import JointTrajectory
|
|
48
|
+
|
|
49
|
+
t = np.linspace(0.0, 2.0, 201)
|
|
50
|
+
traj = JointTrajectory(
|
|
51
|
+
t=t,
|
|
52
|
+
joint_names=("shoulder", "elbow"),
|
|
53
|
+
positions=np.stack([np.sin(t), np.cos(t)], axis=1),
|
|
54
|
+
).with_derivatives()
|
|
55
|
+
|
|
56
|
+
print(traj) # a summary, never 400 numbers
|
|
57
|
+
print(traj.positions.shape) # batch view, no copy
|
|
58
|
+
print(traj[100]) # one point
|
|
59
|
+
print(traj.at(0.123).positions) # interpolated at any time
|
|
60
|
+
for point in traj[:2]: # slices are views too
|
|
61
|
+
print(point.t, point.positions)
|
|
62
|
+
print(traj.joint("elbow").joint_names) # one joint, still a JointTrajectory
|
|
63
|
+
print(traj.resample(dt=0.5).t) # onto a fixed grid
|
|
64
|
+
|
|
65
|
+
grasp = Pose.from_xyz_rpy(0.5, 0.0, 0.2, np.pi, 0.0, 0.0) # tool pointing down
|
|
66
|
+
pregrasp = grasp.shifted(dz=-0.10, local=True) # back off along the tool
|
|
67
|
+
print(pregrasp.position)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
```text
|
|
71
|
+
JointTrajectory(N=201, t=[0 … 2] s, joint_names=('shoulder', 'elbow'), positions=float64[201×2], velocities=float64[201×2], accelerations=float64[201×2])
|
|
72
|
+
(201, 2)
|
|
73
|
+
JointTrajectoryPoint(t=1.0, positions=[0.8415, 0.5403], velocities=[ 0.5403, -0.8415], accelerations=[-0.8414, -0.5403])
|
|
74
|
+
[0.12268879 0.99243461]
|
|
75
|
+
0.0 [0. 1.]
|
|
76
|
+
0.01 [0.00999983 0.99995 ]
|
|
77
|
+
('elbow',)
|
|
78
|
+
[0. 0.5 1. 1.5 2. ]
|
|
79
|
+
Point(x=0.5, y=1.2246467991473533e-17, z=0.30000000000000004)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`JointTrajectory` and `PoseTrajectory` subclass the domain-free `Trajectory` (samples at strictly
|
|
83
|
+
increasing times). Anything else you want to send along rides as `channels`
|
|
84
|
+
(`traj.with_channel("kp", kp)`); a new kind of trajectory subclasses `Trajectory` and declares
|
|
85
|
+
its `sample()` fields — it gets indexing, interpolation, plotting and serialisation for free.
|
|
86
|
+
|
|
87
|
+
Context ROS has no field for (an origin, units, provenance) rides `Header.meta`, a frozen
|
|
88
|
+
`str → str` mapping that survives every view and file and is dropped on ROS export. A recording
|
|
89
|
+
whose stamps repeat or step back is built with
|
|
90
|
+
`JointTrajectory.from_samples(..., duplicate_times="drop_later" | "average" | "error")`, which
|
|
91
|
+
returns `(trajectory, repair)` and writes the same receipt into `header.meta`; never silently.
|
|
92
|
+
|
|
93
|
+
## Packages
|
|
94
|
+
|
|
95
|
+
| Import | Types |
|
|
96
|
+
|---|---|
|
|
97
|
+
| `robot_msgs.std` | `Header` |
|
|
98
|
+
| `robot_msgs.geometry` | `Vector3` `Point` `Quaternion` `Pose` `Pose2D` `Transform` `Twist` `Wrench` `Accel` `Inertia` `PoseStamped` `TransformStamped` · batch maths in `geometry.rotations` (incl. direct `euler_to_matrix` / `matrix_to_euler`) |
|
|
99
|
+
| `robot_msgs.sensor` | `JointState` `Image` `CameraInfo` `PointCloud` `Imu` |
|
|
100
|
+
| `robot_msgs.vision` | `BoundingBox2D` `BoundingBox3D` `ObjectHypothesis` `ObjectHypothesisWithPose` `Detection2D` `Detection2DArray` `Detection3D` `Detection3DArray` |
|
|
101
|
+
| `robot_msgs.trajectory` | `Trajectory` `JointTrajectory` `PoseTrajectory` (+ points) `JointLimits` `concatenate` `TimeRepair` |
|
|
102
|
+
| `robot_msgs.viz` `[viz]` | `plot(msg)` for every type, `animate(*trajectories, path=...)` |
|
|
103
|
+
| `robot_msgs.ros` `[ros]` | `to_ros` / `from_ros` (ROS 2 Jazzy via rosbags), `write_bag` / `read_bag` (mcap) |
|
|
104
|
+
|
|
105
|
+
Every message: `==` and `hash` by value, a short `repr`, `replace(...)`, `to_dict()` /
|
|
106
|
+
`from_dict()`, `save_npz` / `load_npz`, pickling.
|
|
107
|
+
|
|
108
|
+
## Conventions
|
|
109
|
+
|
|
110
|
+
SI units · quaternions `(x, y, z, w)` (MuJoCo/viser `wxyz` only through `xyzw_to_wxyz`) ·
|
|
111
|
+
Euler = ROS roll-pitch-yaw (fixed X, Y, Z) · `a @ b` applies `b` in `a`'s frame ·
|
|
112
|
+
`Header.stamp` float seconds, trajectory `t` relative to it · `Header.meta` str → str · absent optional arrays are `None`.
|
|
113
|
+
The full contract, what we keep from ROS and what we drop, and how to extend: [docs/DESIGN.md](docs/DESIGN.md).
|
|
114
|
+
|
|
115
|
+
## Develop
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
env -u VIRTUAL_ENV uv run --directory /home/bam/robot_msgs --isolated --extra test python -m pytest -q
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Examples: `examples/tour.py` (above), `examples/ur5e_pick.py` (waypoints → minimum-jerk joint
|
|
122
|
+
trajectory → tool pose trajectory through UR5e FK), `examples/gallery.py OUTDIR` (every plot).
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# robot_msgs
|
|
2
|
+
|
|
3
|
+
A cohesive domain model for robot data: the standard message types — ROS's `geometry_msgs`,
|
|
4
|
+
`sensor_msgs`, `trajectory_msgs`, `vision_msgs` — as frozen Python dataclasses, **with the
|
|
5
|
+
functions that move between them**. Offsetting a pose, interpolating a trajectory, projecting a
|
|
6
|
+
point, turning a depth image into a cloud: first-class methods, not separate libraries.
|
|
7
|
+
|
|
8
|
+
- **ROS's shape, not ROS.** Same packages and field names (the ROS docs are our docs), no ROS
|
|
9
|
+
install, no codegen. ROS 2 messages and bags are an export (`robot_msgs.ros`).
|
|
10
|
+
- **Immutable values over compact numpy storage.** Every message is frozen; its arrays are
|
|
11
|
+
read-only, so slices, rows and columns are free views.
|
|
12
|
+
- **One runtime dependency: numpy.** Plots (`[viz]`) and ROS export (`[ros]`) are extras.
|
|
13
|
+
|
|
14
|
+
## A trajectory is one entity with many views
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
import numpy as np
|
|
18
|
+
|
|
19
|
+
from robot_msgs.geometry import Pose
|
|
20
|
+
from robot_msgs.trajectory import JointTrajectory
|
|
21
|
+
|
|
22
|
+
t = np.linspace(0.0, 2.0, 201)
|
|
23
|
+
traj = JointTrajectory(
|
|
24
|
+
t=t,
|
|
25
|
+
joint_names=("shoulder", "elbow"),
|
|
26
|
+
positions=np.stack([np.sin(t), np.cos(t)], axis=1),
|
|
27
|
+
).with_derivatives()
|
|
28
|
+
|
|
29
|
+
print(traj) # a summary, never 400 numbers
|
|
30
|
+
print(traj.positions.shape) # batch view, no copy
|
|
31
|
+
print(traj[100]) # one point
|
|
32
|
+
print(traj.at(0.123).positions) # interpolated at any time
|
|
33
|
+
for point in traj[:2]: # slices are views too
|
|
34
|
+
print(point.t, point.positions)
|
|
35
|
+
print(traj.joint("elbow").joint_names) # one joint, still a JointTrajectory
|
|
36
|
+
print(traj.resample(dt=0.5).t) # onto a fixed grid
|
|
37
|
+
|
|
38
|
+
grasp = Pose.from_xyz_rpy(0.5, 0.0, 0.2, np.pi, 0.0, 0.0) # tool pointing down
|
|
39
|
+
pregrasp = grasp.shifted(dz=-0.10, local=True) # back off along the tool
|
|
40
|
+
print(pregrasp.position)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
```text
|
|
44
|
+
JointTrajectory(N=201, t=[0 … 2] s, joint_names=('shoulder', 'elbow'), positions=float64[201×2], velocities=float64[201×2], accelerations=float64[201×2])
|
|
45
|
+
(201, 2)
|
|
46
|
+
JointTrajectoryPoint(t=1.0, positions=[0.8415, 0.5403], velocities=[ 0.5403, -0.8415], accelerations=[-0.8414, -0.5403])
|
|
47
|
+
[0.12268879 0.99243461]
|
|
48
|
+
0.0 [0. 1.]
|
|
49
|
+
0.01 [0.00999983 0.99995 ]
|
|
50
|
+
('elbow',)
|
|
51
|
+
[0. 0.5 1. 1.5 2. ]
|
|
52
|
+
Point(x=0.5, y=1.2246467991473533e-17, z=0.30000000000000004)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`JointTrajectory` and `PoseTrajectory` subclass the domain-free `Trajectory` (samples at strictly
|
|
56
|
+
increasing times). Anything else you want to send along rides as `channels`
|
|
57
|
+
(`traj.with_channel("kp", kp)`); a new kind of trajectory subclasses `Trajectory` and declares
|
|
58
|
+
its `sample()` fields — it gets indexing, interpolation, plotting and serialisation for free.
|
|
59
|
+
|
|
60
|
+
Context ROS has no field for (an origin, units, provenance) rides `Header.meta`, a frozen
|
|
61
|
+
`str → str` mapping that survives every view and file and is dropped on ROS export. A recording
|
|
62
|
+
whose stamps repeat or step back is built with
|
|
63
|
+
`JointTrajectory.from_samples(..., duplicate_times="drop_later" | "average" | "error")`, which
|
|
64
|
+
returns `(trajectory, repair)` and writes the same receipt into `header.meta`; never silently.
|
|
65
|
+
|
|
66
|
+
## Packages
|
|
67
|
+
|
|
68
|
+
| Import | Types |
|
|
69
|
+
|---|---|
|
|
70
|
+
| `robot_msgs.std` | `Header` |
|
|
71
|
+
| `robot_msgs.geometry` | `Vector3` `Point` `Quaternion` `Pose` `Pose2D` `Transform` `Twist` `Wrench` `Accel` `Inertia` `PoseStamped` `TransformStamped` · batch maths in `geometry.rotations` (incl. direct `euler_to_matrix` / `matrix_to_euler`) |
|
|
72
|
+
| `robot_msgs.sensor` | `JointState` `Image` `CameraInfo` `PointCloud` `Imu` |
|
|
73
|
+
| `robot_msgs.vision` | `BoundingBox2D` `BoundingBox3D` `ObjectHypothesis` `ObjectHypothesisWithPose` `Detection2D` `Detection2DArray` `Detection3D` `Detection3DArray` |
|
|
74
|
+
| `robot_msgs.trajectory` | `Trajectory` `JointTrajectory` `PoseTrajectory` (+ points) `JointLimits` `concatenate` `TimeRepair` |
|
|
75
|
+
| `robot_msgs.viz` `[viz]` | `plot(msg)` for every type, `animate(*trajectories, path=...)` |
|
|
76
|
+
| `robot_msgs.ros` `[ros]` | `to_ros` / `from_ros` (ROS 2 Jazzy via rosbags), `write_bag` / `read_bag` (mcap) |
|
|
77
|
+
|
|
78
|
+
Every message: `==` and `hash` by value, a short `repr`, `replace(...)`, `to_dict()` /
|
|
79
|
+
`from_dict()`, `save_npz` / `load_npz`, pickling.
|
|
80
|
+
|
|
81
|
+
## Conventions
|
|
82
|
+
|
|
83
|
+
SI units · quaternions `(x, y, z, w)` (MuJoCo/viser `wxyz` only through `xyzw_to_wxyz`) ·
|
|
84
|
+
Euler = ROS roll-pitch-yaw (fixed X, Y, Z) · `a @ b` applies `b` in `a`'s frame ·
|
|
85
|
+
`Header.stamp` float seconds, trajectory `t` relative to it · `Header.meta` str → str · absent optional arrays are `None`.
|
|
86
|
+
The full contract, what we keep from ROS and what we drop, and how to extend: [docs/DESIGN.md](docs/DESIGN.md).
|
|
87
|
+
|
|
88
|
+
## Develop
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
env -u VIRTUAL_ENV uv run --directory /home/bam/robot_msgs --isolated --extra test python -m pytest -q
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Examples: `examples/tour.py` (above), `examples/ur5e_pick.py` (waypoints → minimum-jerk joint
|
|
95
|
+
trajectory → tool pose trajectory through UR5e FK), `examples/gallery.py OUTDIR` (every plot).
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
# robot_msgs — design contract (v1)
|
|
2
|
+
|
|
3
|
+
A cohesive domain model for robot data: the vocabulary that flows between robot components,
|
|
4
|
+
plus the functions that move between its parts. The parts are defined by each other, like a
|
|
5
|
+
dictionary — a trajectory is poses over time, a pose is a point and an orientation in a frame,
|
|
6
|
+
a point cloud comes from a depth image and its camera info — so the relationships ship with the
|
|
7
|
+
types. It is the stable bottom tier: everything above it (like_keras/Clank, bam_traj_gen,
|
|
8
|
+
traj_compare, sim, drivers) can depend on it, and it depends on nothing but numpy.
|
|
9
|
+
|
|
10
|
+
## What we keep from ROS, and what we drop
|
|
11
|
+
|
|
12
|
+
| Keep | Drop |
|
|
13
|
+
|---|---|
|
|
14
|
+
| The domain model: `geometry_msgs`, `sensor_msgs`, `trajectory_msgs`, `vision_msgs` — decades of agreement on what the nouns are | Behaviourless structs. Pose offsetting, interpolation, projection and FK-shaped batch maths were separate libraries (tf, image_geometry, …); here they are methods on the type |
|
|
15
|
+
| Field names verbatim, so ROS docs are our docs and conversion is mechanical | Array-of-structs as the only storage: a `JointTrajectory` is not 500 point messages |
|
|
16
|
+
| Every stamped message carries its time and frame (`Header`) | Code generation, a build system and a ROS install just to have a `Pose` |
|
|
17
|
+
| Explicit frames (`frame_id`, `child_frame_id`) | Verbose construction (`msg.pose.pose.position.x = …`) and mutable messages |
|
|
18
|
+
| Wire formats as an *export* (`robot_msgs.ros`) | The wire format as the programming model |
|
|
19
|
+
|
|
20
|
+
## Scope
|
|
21
|
+
|
|
22
|
+
- **In:** the standard vocabulary and the standard functions between its parts — offsets
|
|
23
|
+
(`pose.shifted(dz=-0.1, local=True)`), composition, interpolation, resampling, projection,
|
|
24
|
+
depth → cloud, joint trajectory → joint state, limit checks.
|
|
25
|
+
- **Out:** business logic and non-standard types. The moment a type needs your application's
|
|
26
|
+
concepts (a bin, an order, a grasp score policy) it belongs in your code. See *Extending*.
|
|
27
|
+
|
|
28
|
+
## Principles (each one is a rule, with its reason)
|
|
29
|
+
|
|
30
|
+
1. **Copy ROS's shape, not ROS.** Types and field names follow the ROS 2 message of the same
|
|
31
|
+
name verbatim (`JointState.name/position/velocity/effort`, `CameraInfo.k/d/r/p`,
|
|
32
|
+
`Header.stamp/frame_id`) so the ROS docs are our docs and conversion is mechanical.
|
|
33
|
+
No ROS install, no rclpy, no codegen.
|
|
34
|
+
2. **Behaviour lives with its type.** ROS messages are dead structs; here `pose.py` holds `Pose`
|
|
35
|
+
*and* `compose()`, `inverse()`, `to_matrix()`. Helper methods are the point of the package.
|
|
36
|
+
3. **Immutable values.** Every message is `@dataclass(frozen=True, eq=False, repr=False)` and
|
|
37
|
+
subclasses `robot_msgs._core.Msg`. Methods never mutate; they return new values
|
|
38
|
+
(`replace()` re-validates). WHY: a message handed to three consumers can't be changed under
|
|
39
|
+
any of them, so logs, replays and tests are reproducible.
|
|
40
|
+
4. **Numbers live in read-only numpy arrays.** `frozen_array()` copies once at construction and
|
|
41
|
+
marks the array read-only; views of it (slices, rows, columns) are then free and still
|
|
42
|
+
read-only. WHY: Zach wants "the most compact, efficient storage, with many views of it".
|
|
43
|
+
Tuple-backed storage (bam_traj_gen) makes every numpy "view" a copy.
|
|
44
|
+
5. **Validate at construction.** Shapes, lengths, monotonic time and name counts fail in
|
|
45
|
+
`__post_init__` with a `ValueError` naming the field, never later in a consumer.
|
|
46
|
+
6. **One type per file, named for the type** (`geometry/pose_stamped.py` → `PoseStamped`), plus
|
|
47
|
+
any functions that operate on it. Each domain package re-exports its types in `__init__`.
|
|
48
|
+
7. **Conventions are fixed and written down** (below), never inferred.
|
|
49
|
+
8. **The function decides how specific the type is.** A function that works on anything with
|
|
50
|
+
samples over time takes a `Trajectory` (`resample`, `window`, `plot`); one that needs joint
|
|
51
|
+
angles takes a `JointTrajectory`; one that needs poses takes a `PoseTrajectory`. The type is
|
|
52
|
+
validation: passing a `JointTrajectory` where a `PoseTrajectory` is required is an error, and
|
|
53
|
+
that is the point.
|
|
54
|
+
9. **Methods that return a modified copy are named for the result**: `shifted`, `rotated`,
|
|
55
|
+
`normalized`, `rebased`, `time_shifted`, `time_scaled`, `transformed`, `with_*`. Spatial
|
|
56
|
+
words (`shifted`, `rotated`) move things in space; `time_*` moves them in time.
|
|
57
|
+
10. **Runtime dependency: numpy only.** matplotlib (`[viz]`), rosbags (`[ros]`), scipy (tests,
|
|
58
|
+
as an independent oracle) are extras. A boundary test enforces this.
|
|
59
|
+
|
|
60
|
+
## Conventions
|
|
61
|
+
|
|
62
|
+
| Thing | Convention |
|
|
63
|
+
|---|---|
|
|
64
|
+
| Units | SI: metres, radians, seconds, newtons, N·m |
|
|
65
|
+
| Quaternion | `(x, y, z, w)` as fields and as arrays; Hamilton product; MuJoCo/viser `wxyz` only via `xyzw_to_wxyz` / `wxyz_to_xyzw` |
|
|
66
|
+
| Euler | ROS RPY: fixed axes X then Y then Z, `R = Rz(yaw)·Ry(pitch)·Rx(roll)` |
|
|
67
|
+
| `a.compose(b)` / `a @ b` | apply `b` in `a`'s frame: `T_world_tool = T_world_base @ T_base_tool` |
|
|
68
|
+
| Time | `Header.stamp`: float seconds. Trajectory `t`: float seconds relative to `header.stamp` |
|
|
69
|
+
| Context beyond a frame | `Header.meta`, str → str (origin, units, provenance); dropped on ROS export |
|
|
70
|
+
| Absent optional array | `None` (ROS uses an empty array; converters map between them) |
|
|
71
|
+
| Matrices | `(3, 3)` rotation, `(4, 4)` homogeneous, row-major numpy |
|
|
72
|
+
|
|
73
|
+
## Mechanics every message follows
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from __future__ import annotations
|
|
77
|
+
from dataclasses import dataclass, field
|
|
78
|
+
import numpy as np
|
|
79
|
+
from robot_msgs._core import Array, Msg, frozen_array, optional_array, set_field
|
|
80
|
+
from robot_msgs.std import Header
|
|
81
|
+
|
|
82
|
+
@dataclass(frozen=True, eq=False, repr=False) # always these three flags
|
|
83
|
+
class JointState(Msg):
|
|
84
|
+
name: tuple[str, ...]
|
|
85
|
+
position: Array
|
|
86
|
+
velocity: Array | None = None
|
|
87
|
+
effort: Array | None = None
|
|
88
|
+
header: Header = field(default_factory=Header)
|
|
89
|
+
|
|
90
|
+
def __post_init__(self) -> None:
|
|
91
|
+
set_field(self, "name", tuple(str(n) for n in self.name))
|
|
92
|
+
set_field(self, "position", frozen_array(self.position)) # copies once, read-only
|
|
93
|
+
...validate shapes, raise ValueError("JointState.velocity has 5 values, name has 6")
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
- Small value types with only scalar fields (`Point`, `Quaternion`, `Vector3`) take positional
|
|
97
|
+
arguments like ROS (`Point(1, 2, 3)`) and store Python floats. Array-holding and trajectory
|
|
98
|
+
types are `kw_only=True`.
|
|
99
|
+
- `Msg` gives every message: array-aware `==` and `hash`, a compact `repr` (big arrays print as
|
|
100
|
+
`float64[500×6]`), `replace(**changes)`, `to_dict()` / `from_dict()` (JSON-safe, lossless,
|
|
101
|
+
`"_type": "geometry/Pose"`), and `save_npz` / `load_npz` for compact files.
|
|
102
|
+
- `robot_msgs.allclose(a, b)` compares messages approximately.
|
|
103
|
+
|
|
104
|
+
## Packages and their v1 types
|
|
105
|
+
|
|
106
|
+
| Package | Types (one file each) | ROS source |
|
|
107
|
+
|---|---|---|
|
|
108
|
+
| `robot_msgs.std` | `Header` | std_msgs |
|
|
109
|
+
| `robot_msgs.geometry` | `Vector3`, `Point`, `Quaternion`, `Pose`, `Pose2D`, `Transform`, `Twist`, `Wrench`, `Accel`, `Inertia`, `PoseStamped`, `TransformStamped`; batch math in `rotations.py` | geometry_msgs |
|
|
110
|
+
| `robot_msgs.sensor` | `JointState`, `Image`, `CameraInfo`, `PointCloud`, `Imu` | sensor_msgs |
|
|
111
|
+
| `robot_msgs.vision` | `BoundingBox2D`, `BoundingBox3D`, `ObjectHypothesis`, `ObjectHypothesisWithPose`, `Detection2D`, `Detection2DArray`, `Detection3D`, `Detection3DArray` | vision_msgs |
|
|
112
|
+
| `robot_msgs.trajectory` | `Trajectory`, `TrajectoryPoint`, `JointTrajectory`, `JointTrajectoryPoint`, `PoseTrajectory`, `PoseTrajectoryPoint`, `JointLimits`, `concatenate` | trajectory_msgs, nav_msgs/Path |
|
|
113
|
+
| `robot_msgs.compare` | `compare(reference, measured)` → `Comparison` (error trajectory + `ErrorStats` per metric), `align`, `error_stats`; also `traj.compare(other)` — numpy only | control_msgs/JointTrajectoryControllerState (error sign) |
|
|
114
|
+
| `robot_msgs.viz` | `plot(msg)` for every type above (matplotlib, `[viz]` extra) | — |
|
|
115
|
+
| `robot_msgs.ros` | `to_ros(msg)` / `from_ros(ros_msg)` / `ros_type()` against ROS 2 Jazzy via rosbags' `typestore()` (vision_msgs 4.1.1 registered); `write_bag` / `read_bag` (mcap or sqlite3) — `[ros]` extra | — |
|
|
116
|
+
|
|
117
|
+
## Geometry API (frozen contract — other modules import these names)
|
|
118
|
+
|
|
119
|
+
- `Vector3(x=0.0, y=0.0, z=0.0)` and `Point(x=0.0, y=0.0, z=0.0)`: `as_array() -> (3,)`,
|
|
120
|
+
`from_array(a)`, `+ - ` same type, `* / ` scalar, unary `-`, `norm()`, `dot()`, `cross()`
|
|
121
|
+
(Vector3), `distance_to()` (Point), `normalized()` (Vector3).
|
|
122
|
+
- `Quaternion(x=0.0, y=0.0, z=0.0, w=1.0)`: `identity()`, `as_array()` (xyzw), `from_array(a)`,
|
|
123
|
+
`from_wxyz(a)`, `to_wxyz()`, `from_euler(roll, pitch, yaw)`, `to_euler() -> (r, p, y)`,
|
|
124
|
+
`from_matrix(R)`, `to_matrix()`, `from_axis_angle(axis, angle)`, `from_rotvec(v)`,
|
|
125
|
+
`to_rotvec()`, `multiply(other)` and `__mul__`, `conjugate()`, `inverse()`, `normalized()`,
|
|
126
|
+
`rotate(v)` (Vector3, Point or array → same kind), `slerp(other, alpha)`, `angle_to(other)`.
|
|
127
|
+
- `Pose(position=Point(), orientation=Quaternion())` and
|
|
128
|
+
`Transform(translation=Vector3(), rotation=Quaternion())`: `identity()`,
|
|
129
|
+
`from_xyz_rpy(x, y, z, roll, pitch, yaw)`, `from_matrix(M)`, `to_matrix()`, `compose(other)` and
|
|
130
|
+
`__matmul__`, `inverse()`, `transform_point(p)`, `as_array() -> (7,)` = x y z qx qy qz qw,
|
|
131
|
+
`from_array(a)`, `Pose.as_transform()`, `Transform.as_pose()`.
|
|
132
|
+
- `PoseStamped(header=Header(), pose=Pose())`, `TransformStamped(header=Header(),
|
|
133
|
+
child_frame_id="", transform=Transform())`, `Twist(linear=Vector3(), angular=Vector3())`,
|
|
134
|
+
`Wrench(force=Vector3(), torque=Vector3())`, `Accel(linear, angular)`, `Pose2D(x, y, theta)`.
|
|
135
|
+
- `Inertia(m=0.0, com=Vector3(), ixx, ixy, ixz, iyy, iyz, izz)` (geometry_msgs/Inertia, tensor
|
|
136
|
+
about the centre of mass): `to_matrix()` / `from_matrix(m, com, I)` (symmetric only),
|
|
137
|
+
`rotated(q)` (`R·I·Rᵀ`), `is_physical()`, `as_array() -> (10,)`.
|
|
138
|
+
- `rotations.euler_to_matrix(r, p, y)` / `matrix_to_euler(R)`: ROS RPY straight to and from
|
|
139
|
+
rotation matrices, no quaternion detour (a URDF `rpy` is printed verbatim); yaw = 0 at gimbal
|
|
140
|
+
lock.
|
|
141
|
+
|
|
142
|
+
## Trajectory — one entity, many views
|
|
143
|
+
|
|
144
|
+
A trajectory is samples at strictly increasing times, in any domain. The base class holds the
|
|
145
|
+
time axis and the machinery; subclasses only declare their per-sample fields.
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
traj = JointTrajectory(t=t, joint_names=names, positions=q, velocities=qd)
|
|
149
|
+
|
|
150
|
+
traj.positions # (N, J) read-only array — the batch view, no copy
|
|
151
|
+
traj["positions"] # same, by name (also works for channels)
|
|
152
|
+
traj[10] # JointTrajectoryPoint at sample 10 (row views, no copy)
|
|
153
|
+
for point in traj:
|
|
154
|
+
... # per-step view
|
|
155
|
+
traj[100:200] # JointTrajectory, sliced as views — O(1)
|
|
156
|
+
traj.at(1.25) # interpolated point (linear; slerp for orientations)
|
|
157
|
+
traj.resample(dt=0.004) # on a fixed controller grid
|
|
158
|
+
traj.window(1.0, 2.0) # the part between two times
|
|
159
|
+
traj.joint("elbow") # one joint, still a JointTrajectory
|
|
160
|
+
traj.channels["kp"] # any extra per-sample data you layer on top
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
- **Per-sample fields** are declared with `sample()`; their first axis is time and they are
|
|
164
|
+
sliced, iterated and interpolated together. `sample(interp="slerp")` for quaternions,
|
|
165
|
+
`"hold"` for discrete values. Per-trajectory constants are ordinary fields
|
|
166
|
+
(`joint_names`, `limits`, `child_frame_id`).
|
|
167
|
+
- **`channels`** carries extra per-sample arrays any trajectory can grow without a new type
|
|
168
|
+
(`kp`, `tau_ff`, `mode`, `temp_c`). Float channels interpolate linearly, others hold.
|
|
169
|
+
- **New domains subclass `Trajectory`** and declare `sample()` fields; they get indexing,
|
|
170
|
+
iteration, interpolation, resampling, plotting and serialisation for free, plus a generated
|
|
171
|
+
point type.
|
|
172
|
+
- `t` is relative to `header.stamp`; `rebased()` moves `t[0]` to 0 and folds the offset into the
|
|
173
|
+
stamp. Out-of-range interpolation raises unless `extrapolate="clamp"`.
|
|
174
|
+
- Recordings whose stamps repeat or step back go through `from_samples(..., duplicate_times=...)`,
|
|
175
|
+
which says what it changed (below).
|
|
176
|
+
|
|
177
|
+
## Header.meta — context ROS has no field for
|
|
178
|
+
|
|
179
|
+
ROS's only per-message context is `std_msgs/Header`: a stamp and a `frame_id`. There is no free
|
|
180
|
+
metadata, so a consumer that needs to say *what the numbers are relative to* beyond a frame name
|
|
181
|
+
(the origin a joint angle is measured from, its units, which recorder wrote it, what a cleaner
|
|
182
|
+
changed) ends up encoding it into `frame_id` (bam_rack_twin did, 2026-11-04).
|
|
183
|
+
|
|
184
|
+
**Decision:** `Header.meta: Mapping[str, str]`, default empty, frozen (`FrozenDict`), copied at
|
|
185
|
+
construction.
|
|
186
|
+
|
|
187
|
+
- **WHY on the header, not on each message:** the header is already the slot that says what a
|
|
188
|
+
message is relative to; extending it gives every stamped message and every trajectory the same
|
|
189
|
+
place once, and everything that keeps the header keeps meta for free: slicing, masks,
|
|
190
|
+
`window`, `resample`, `rebased`, `joint()`, `with_*`, `to_dict` / JSON, `save_npz`, pickling.
|
|
191
|
+
- **Points carry their trajectory's header** (`traj[i].header`, `traj.at(t).header`), so a point
|
|
192
|
+
taken out keeps its frame, origin and units. ROS's points have no header; ours are views, and
|
|
193
|
+
`t` is meaningless without the stamp it counts from. `from_points` takes the points' shared
|
|
194
|
+
header back (and refuses points whose headers differ, unless you pass `header=`).
|
|
195
|
+
- **WHY strings only:** meta is read by people and by other tools. A float would be `str()`-ed
|
|
196
|
+
silently and its spelling chosen for you, so non-strings are refused and the caller writes
|
|
197
|
+
`repr(x)` (render stored values truthfully). Structured values go in as JSON strings.
|
|
198
|
+
- **`concatenate` refuses pieces with different meta**, as it refuses different frames: two
|
|
199
|
+
trajectories measured from different origins are not one trajectory.
|
|
200
|
+
- **Dropped on ROS export** (`to_ros`), always, without `drop_extras`: std_msgs/Header has nowhere
|
|
201
|
+
to put it and every stamped message exports through one header converter. `from_ros` gives an
|
|
202
|
+
empty meta. robot_msgs' own files are the lossless form.
|
|
203
|
+
- **Not for per-sample data** (that is `channels`) or per-trajectory numbers a function computes
|
|
204
|
+
with (those are typed fields, like `limits`). Meta is description, never input.
|
|
205
|
+
|
|
206
|
+
## Recorded stamps — `from_samples`
|
|
207
|
+
|
|
208
|
+
`t` must strictly increase, and the constructor keeps raising otherwise: interpolation, `dt`,
|
|
209
|
+
`window` and `searchsorted` all depend on it. Recordings break it (a loop logs one stamp twice, a
|
|
210
|
+
clock steps back), so there is one explicit door:
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
traj, repair = JointTrajectory.from_samples(
|
|
214
|
+
t=t, joint_names=names, positions=q, duplicate_times="drop_later" # | "average" | "error"
|
|
215
|
+
)
|
|
216
|
+
repair.dropped # (17, 230): indices into the samples as given; repair.dropped_t their stamps
|
|
217
|
+
str(repair) # "drop_later: dropped 2 of 1000 samples (repeated or backwards stamps) at ..."
|
|
218
|
+
traj.header.meta["time_repair.dropped"] # the same receipt, inside the trajectory
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
- `duplicate_times` has **no default**: the caller decides what a repeated stamp means.
|
|
222
|
+
`"drop_later"` keeps a sample only if its stamp is after every earlier kept one (first of a
|
|
223
|
+
repeat stays, backwards stamps go, values never altered). `"average"` merges samples at exactly
|
|
224
|
+
equal stamps (floats averaged, quaternions on the short arc, held/discrete values must agree or
|
|
225
|
+
it raises) and drops backwards stamps, which have no partner. `"error"` is the constructor.
|
|
226
|
+
- **WHY a receipt in two places** (the returned `TimeRepair` and `header.meta["time_repair.*"]`):
|
|
227
|
+
silent repair is not allowed. A cleaned recording that does not say so is indistinguishable
|
|
228
|
+
from a clean one; the meta copy means the saved file says so too. Clean input changes nothing
|
|
229
|
+
and stamps nothing.
|
|
230
|
+
|
|
231
|
+
## Comparing trajectories — `robot_msgs.compare`
|
|
232
|
+
|
|
233
|
+
Zach, 2026-11-10: *"literally this package is saying we are comparing two of these data types of
|
|
234
|
+
robot msgs. The whole issue with ROS msgs is that they don't have enough of the helpers at arm's
|
|
235
|
+
reach."* So comparison is a robot_msgs helper, built like `viz`: its own subpackage, one generic
|
|
236
|
+
function dispatched on the type, and a one-line method (`traj.compare(other)`) so the type files
|
|
237
|
+
stay the types. Viewers (traj_compare's HTML report, Rerun) live outside and read the values.
|
|
238
|
+
|
|
239
|
+
```python
|
|
240
|
+
result = target.compare(real) # == compare(target, real)
|
|
241
|
+
result.error # JointTrajectory: target - real, on real's samples
|
|
242
|
+
result.metrics["positions/j2"].rms # ErrorStats: n rms mean median std min max max_abs p95_abs final
|
|
243
|
+
tip = lambda traj: PoseTrajectory.from_matrices(traj.t, fk(traj.positions), child_frame_id="L2 tip")
|
|
244
|
+
tip(target).compare(tip(real)).metrics["lag"].median # cartesian: one PoseTrajectory per point
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Rules, each with its reason:
|
|
248
|
+
|
|
249
|
+
1. **Pairwise.** `compare(reference, measured)`; N runs, or sim vs real with no target, is a loop.
|
|
250
|
+
2. **`error = reference - measured`**, the ROS 2 `JointTrajectoryControllerState` convention, so a
|
|
251
|
+
positive error is the correction still to apply and a positive `lag` means the run is behind.
|
|
252
|
+
3. **The error is the same type as its inputs**, so every view, plot and serialiser already works
|
|
253
|
+
on it. Derived forms ride along as channels: a pose error carries `rotation_vector` (world
|
|
254
|
+
axes; never a difference of Euler angles), `rotation_angle`, `lag` and `cross_track`.
|
|
255
|
+
4. **Joint ↔ cartesian is not compare's job.** Build a `PoseTrajectory` per point you care about
|
|
256
|
+
(TCP, L2 tip, coupler) with your FK, name it in `child_frame_id`, and compare those.
|
|
257
|
+
5. **One clock, no extrapolation.** Both runs are placed on `header.stamp + t`; measured samples
|
|
258
|
+
outside the reference are dropped (`dropped`), and `coverage` says how much of the reference
|
|
259
|
+
was measured. A known latency is an explicit `time_offset_s`, never guessed.
|
|
260
|
+
6. **Errors on the measured run's own samples**, never on a shared resampled grid.
|
|
261
|
+
7. **Every decision travels with the numbers.** `Comparison` holds `time_offset_s`, `dropped` and
|
|
262
|
+
`coverage` beside `error` and `metrics`, and round-trips through JSON/npz like any message.
|
|
263
|
+
8. **Metric names are stable strings** (`positions/<joint>`, `position`, `rotation`, `x`…`rz`,
|
|
264
|
+
`lag`, `cross_track`, `channels/<name>`), so adding a metric never changes a type. `rows()`
|
|
265
|
+
gives them as tidy rows for tables.
|
|
266
|
+
|
|
267
|
+
Not yet in (prior art: evo, trajectopy, ROS 2 `joint_trajectory_controller`, ISO 9283): exact
|
|
268
|
+
contour error (nearest point on the reference path), latency estimation, relative/drift error
|
|
269
|
+
over windows (evo RPE), tolerances with a pass/fail outcome (`path_tolerance`, `goal_tolerance`),
|
|
270
|
+
accuracy vs repeatability across N runs (ISO 9283). Each is a new metric or a new function over
|
|
271
|
+
`Comparison`s; none changes the shape above.
|
|
272
|
+
|
|
273
|
+
## Extending without forking
|
|
274
|
+
|
|
275
|
+
1. **Extra per-sample data → `channels`.** Gains, feed-forward torque, controller mode,
|
|
276
|
+
temperature, a waypoint's name: `traj.with_channel("kp", kp)`. No new type needed.
|
|
277
|
+
2. **A new kind of trajectory → subclass `Trajectory` in your own file** and declare its
|
|
278
|
+
`sample()` fields. Indexing, iteration, interpolation, resampling, plotting and serialisation
|
|
279
|
+
come for free:
|
|
280
|
+
|
|
281
|
+
```python
|
|
282
|
+
@dataclass(frozen=True, eq=False, repr=False, kw_only=True)
|
|
283
|
+
class GripperTrajectory(Trajectory):
|
|
284
|
+
width: Array = sample() # metres
|
|
285
|
+
grasping: Array | None = sample(default=None, interp="hold", dtype=bool)
|
|
286
|
+
```
|
|
287
|
+
3. **Business types → your own dataclasses that hold robot_msgs values** (composition). A
|
|
288
|
+
`GraspCandidate(target: Detection3D, approach: Pose, quality: float)` lives in your
|
|
289
|
+
application, not here.
|
|
290
|
+
4. **A new standard type** (it exists in a ROS package and crosses a wire between domains) →
|
|
291
|
+
add it here, one file, with a catalog example; the contract tests then cover it.
|
|
292
|
+
|
|
293
|
+
## Storage is the cross-language contract; the API is the views
|
|
294
|
+
|
|
295
|
+
Zach, 2026-11-10: *"I don't really care about the internal implementation, do it for the best
|
|
296
|
+
performance on any language. With the views it can act however we want it to be."* So:
|
|
297
|
+
|
|
298
|
+
- **What callers see is the views** — `traj.positions`, `traj[i]`, iteration, `at(t)`, slices.
|
|
299
|
+
They are the stable API; how a type stores its data behind them may change for speed.
|
|
300
|
+
- **What crosses a language boundary is the storage**, and it is chosen to be zero-copy in every
|
|
301
|
+
language: one contiguous, little-endian, C-ordered array per field, time on axis 0
|
|
302
|
+
(struct-of-arrays). That is exactly an Arrow record batch, an `Eigen::Map`, an `ndarray` view in
|
|
303
|
+
Rust, or a flatbuffer vector — a C++ or Rust `robot_msgs` maps the same bytes without parsing.
|
|
304
|
+
- **Field names, units, frames and conventions are the semantics**, bound early and identical in
|
|
305
|
+
every language; the technology (numpy today, C++/Rust bindings later) binds late.
|