roboapi 0.4.2__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.
Files changed (94) hide show
  1. roboapi/__init__.py +2 -0
  2. roboapi/calibration/__init__.py +6 -0
  3. roboapi/calibration/profile.py +139 -0
  4. roboapi/calibration/session.py +350 -0
  5. roboapi/cli.py +215 -0
  6. roboapi/config/__init__.py +22 -0
  7. roboapi/config/loader.py +119 -0
  8. roboapi/config/models.py +191 -0
  9. roboapi/embodiment.py +251 -0
  10. roboapi/errors.py +37 -0
  11. roboapi/geometry.py +337 -0
  12. roboapi/hardware/__init__.py +5 -0
  13. roboapi/hardware/base.py +113 -0
  14. roboapi/hardware/camera.py +167 -0
  15. roboapi/hardware/franka/__init__.py +5 -0
  16. roboapi/hardware/franka/arm.py +476 -0
  17. roboapi/hardware/franka/desk.py +555 -0
  18. roboapi/hardware/franka/feedback.py +196 -0
  19. roboapi/hardware/franka/guiding.py +104 -0
  20. roboapi/hardware/franka/ik.py +543 -0
  21. roboapi/hardware/franka/limits.py +110 -0
  22. roboapi/hardware/franka/settings.py +116 -0
  23. roboapi/hardware/robotiq.py +350 -0
  24. roboapi/hardware/ur/__init__.py +16 -0
  25. roboapi/hardware/ur/build.py +92 -0
  26. roboapi/hardware/ur/compliant.py +128 -0
  27. roboapi/hardware/ur/dashboard.py +103 -0
  28. roboapi/hardware/ur/feedback.py +159 -0
  29. roboapi/hardware/ur/ipc.py +206 -0
  30. roboapi/hardware/ur/native/CMakeLists.txt +37 -0
  31. roboapi/hardware/ur/native/dependencies.lock.json +13 -0
  32. roboapi/hardware/ur/native/include/ur_cpp/cartesian_speedl.hpp +170 -0
  33. roboapi/hardware/ur/native/include/ur_cpp/control_loop.hpp +126 -0
  34. roboapi/hardware/ur/native/include/ur_cpp/geom.hpp +72 -0
  35. roboapi/hardware/ur/native/include/ur_cpp/motion_primitives.hpp +350 -0
  36. roboapi/hardware/ur/native/include/ur_cpp/robot_io.hpp +26 -0
  37. roboapi/hardware/ur/native/include/ur_cpp/streaming_pose.hpp +369 -0
  38. roboapi/hardware/ur/native/src/control_loop.cpp +512 -0
  39. roboapi/hardware/ur/native/src/robot_io_urcl.cpp +143 -0
  40. roboapi/hardware/ur/native/tests/cartesian_speedl_unit.cpp +113 -0
  41. roboapi/hardware/ur/native/tests/control_modes_unit.cpp +137 -0
  42. roboapi/hardware/ur/native/tests/streaming_pose_unit.cpp +348 -0
  43. roboapi/hardware/ur/native/tests/velocity_governor_unit.cpp +65 -0
  44. roboapi/hardware/ur/native/worker.cpp +147 -0
  45. roboapi/hardware/ur/position.py +253 -0
  46. roboapi/hardware/ur/settings.py +95 -0
  47. roboapi/images.py +16 -0
  48. roboapi/initialize.py +137 -0
  49. roboapi/mcp.py +497 -0
  50. roboapi/motion.py +125 -0
  51. roboapi/naming.py +7 -0
  52. roboapi/operations.py +45 -0
  53. roboapi/policy/__init__.py +7 -0
  54. roboapi/policy/client.py +277 -0
  55. roboapi/policy/protocol.py +166 -0
  56. roboapi/policy/settings.py +79 -0
  57. roboapi/product-names.json +12 -0
  58. roboapi/records.py +149 -0
  59. roboapi/runtime.py +911 -0
  60. roboapi/semantics.py +6 -0
  61. roboapi/tasks.py +337 -0
  62. roboapi/web/__init__.py +5 -0
  63. roboapi/web/presets.py +44 -0
  64. roboapi/web/server.py +419 -0
  65. roboapi/web/static/assets/Bundler-LICENSE.txt +48 -0
  66. roboapi/web/static/assets/CalibrationScene-BO_spRoL.js +4220 -0
  67. roboapi/web/static/assets/Inter-OFL.txt +93 -0
  68. roboapi/web/static/assets/Inter-Variable-c8O0ljhh.ttf +0 -0
  69. roboapi/web/static/assets/Lucide-LICENSE.txt +43 -0
  70. roboapi/web/static/assets/Meshoptimizer-LICENSE.txt +23 -0
  71. roboapi/web/static/assets/MiSans-Regular-ES4DrbaF.woff2 +0 -0
  72. roboapi/web/static/assets/MiSans-Semibold-B34HIk9C.woff2 +0 -0
  73. roboapi/web/static/assets/PokeBrand-NOTICE.txt +20 -0
  74. roboapi/web/static/assets/React-LICENSE.txt +23 -0
  75. roboapi/web/static/assets/Three-LICENSE.txt +23 -0
  76. roboapi/web/static/assets/home-reference-CTpf8-oV.png +0 -0
  77. roboapi/web/static/assets/index-CGLhpUm0.js +28 -0
  78. roboapi/web/static/assets/index-QXqV-fDC.css +1 -0
  79. roboapi/web/static/assets/robotiq-ace-pro-2-EJuoOXTy.glb +0 -0
  80. roboapi/web/static/index.html +15 -0
  81. roboapi-0.4.2.dist-info/METADATA +205 -0
  82. roboapi-0.4.2.dist-info/RECORD +94 -0
  83. roboapi-0.4.2.dist-info/WHEEL +5 -0
  84. roboapi-0.4.2.dist-info/entry_points.txt +2 -0
  85. roboapi-0.4.2.dist-info/licenses/LICENSE +202 -0
  86. roboapi-0.4.2.dist-info/licenses/NOTICE +16 -0
  87. roboapi-0.4.2.dist-info/licenses/frontend/public/assets/Bundler-LICENSE.txt +48 -0
  88. roboapi-0.4.2.dist-info/licenses/frontend/public/assets/Inter-OFL.txt +93 -0
  89. roboapi-0.4.2.dist-info/licenses/frontend/public/assets/Lucide-LICENSE.txt +43 -0
  90. roboapi-0.4.2.dist-info/licenses/frontend/public/assets/Meshoptimizer-LICENSE.txt +23 -0
  91. roboapi-0.4.2.dist-info/licenses/frontend/public/assets/PokeBrand-NOTICE.txt +20 -0
  92. roboapi-0.4.2.dist-info/licenses/frontend/public/assets/React-LICENSE.txt +23 -0
  93. roboapi-0.4.2.dist-info/licenses/frontend/public/assets/Three-LICENSE.txt +23 -0
  94. roboapi-0.4.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,113 @@
1
+ // Pure Cartesian control-law regression.
2
+ #include "ur_cpp/cartesian_speedl.hpp"
3
+
4
+ #include <cassert>
5
+ #include <cmath>
6
+ #include <limits>
7
+ #include <string>
8
+
9
+ int main() {
10
+ Args args;
11
+ std::string error;
12
+ CartesianSpeedlLaw law(args);
13
+ assert(law.validate(error));
14
+
15
+ const vec6 actual{0.3, -0.2, 0.4, 0.0, 3.14159265, 0.0};
16
+ law.reset(actual, vec6{});
17
+ CartesianTick tick;
18
+ tick.reference_ = actual;
19
+ tick.reference_[0] += 0.01;
20
+ tick.actual_ = actual;
21
+ tick.dt_ = 1.0 / args.freq_;
22
+ CartesianCommand command;
23
+ assert(law.step(tick, command));
24
+ assert(finite6(command.twist_));
25
+ assert(command.fault_ == LoopFault::None);
26
+
27
+ Args invalid = args;
28
+ invalid.speedl_acc_trans_ = 0;
29
+ error.clear();
30
+ CartesianSpeedlLaw invalid_law(invalid);
31
+ assert(!invalid_law.validate(error));
32
+ assert(error.find("acceleration and jerk") != std::string::npos);
33
+
34
+ // Large, sustained tracking error and force keep flowing through all stages.
35
+ CartesianSpeedlLaw continuous(args);
36
+ continuous.reset(actual, {});
37
+ tick.reference_ = actual;
38
+ tick.reference_[0] += 0.5;
39
+ tick.actual_ = actual; // Deliberately stalled plant: no tracking-triggered pause.
40
+ const auto rotation =
41
+ quatMul(rotvecToQuat(0.8, 0, 0), rotvecToQuat(actual[3], actual[4], actual[5]));
42
+ quatToRotvec(rotation, tick.reference_[3], tick.reference_[4], tick.reference_[5]);
43
+ tick.wrench_[0] = 200.0;
44
+ vec6 acceleration{};
45
+ for (int n = 0; n < 1000; ++n) {
46
+ assert(continuous.step(tick, command));
47
+ assert(command.fault_ == LoopFault::None);
48
+ assert(norm3(command.twist_) <= args.cmd_vel_trans_ + 1e-8);
49
+ assert(norm3(command.twist_, 3) <= args.cmd_vel_rot_ + 1e-8);
50
+ assert(norm3(continuous.law_.vel_) <= args.tun_.max_vel_ + 1e-8);
51
+ for (int i = 0; i < 6; ++i) {
52
+ const double a = (i < 3 ? args.speedl_acc_trans_ : args.speedl_acc_rot_) / std::sqrt(3.0);
53
+ const double j = (i < 3 ? args.speedl_jerk_trans_ : args.speedl_jerk_rot_) / std::sqrt(3.0);
54
+ assert(std::abs(continuous.governor_.acceleration_[i]) <= a + 1e-8);
55
+ assert(std::abs(continuous.governor_.acceleration_[i] - acceleration[i]) <=
56
+ j * tick.dt_ + 1e-8);
57
+ }
58
+ acceleration = continuous.governor_.acceleration_;
59
+ if (n == 0)
60
+ assert(continuous.shaper_.last_cmd_[0] - actual[0] < 0.001);
61
+ }
62
+ assert(continuous.shaper_.last_cmd_[0] > actual[0] + 0.45);
63
+ assert(command.desired_[0] > actual[0] + 0.1);
64
+ assert(norm3(command.twist_) > 0.01);
65
+
66
+ // Explicit position returns must ignore force offsets, including after a policy run.
67
+ CartesianSpeedlLaw position(args), no_force(args);
68
+ position.reset(actual, {});
69
+ no_force.reset(actual, {});
70
+ tick.reference_ = actual;
71
+ tick.reference_[0] += 0.03;
72
+ tick.actual_ = actual;
73
+ tick.wrench_ = {100, -50, 80, 5, -3, 2};
74
+ CartesianCommand baseline;
75
+ for (int n = 0; n < 500; ++n) {
76
+ assert(position.step(tick, command, false));
77
+ auto unloaded = tick;
78
+ unloaded.wrench_ = {};
79
+ assert(no_force.step(unloaded, baseline, false));
80
+ assert(command.twist_ == baseline.twist_);
81
+ assert(command.desired_ == baseline.desired_);
82
+ }
83
+ assert(norm3(position.law_.off_) == 0);
84
+ continuous.reset(actual, {}); // The mode handoff resets from stationary measured feedback.
85
+ tick.reference_ = actual;
86
+ assert(continuous.step(tick, command, false));
87
+ assert(command.desired_ == actual && norm3(command.twist_) < 1e-12);
88
+
89
+ CartesianTick invalid_tick;
90
+ invalid_tick.reference_ = invalid_tick.actual_ = actual;
91
+ for (double dt : {0.0, -.002, .004, .015, std::numeric_limits<double>::quiet_NaN(),
92
+ std::numeric_limits<double>::infinity()}) {
93
+ law.reset(actual, vec6{});
94
+ invalid_tick.dt_ = dt;
95
+ assert(!law.step(invalid_tick, command));
96
+ assert(command.fault_ == LoopFault::VirtualStateInvalid);
97
+ assert(command.twist_ == vec6{});
98
+ }
99
+ invalid_tick.dt_ = 1.0 / args.freq_;
100
+ for (int field = 0; field < 3; ++field)
101
+ for (double value : {std::numeric_limits<double>::quiet_NaN(),
102
+ std::numeric_limits<double>::infinity()}) {
103
+ law.reset(actual, {});
104
+ invalid_tick.reference_ = invalid_tick.actual_ = actual;
105
+ invalid_tick.wrench_ = {};
106
+ auto &invalid = field == 0 ? invalid_tick.reference_
107
+ : field == 1 ? invalid_tick.actual_ : invalid_tick.wrench_;
108
+ invalid[0] = value;
109
+ assert(!law.step(invalid_tick, command));
110
+ assert(command.fault_ == LoopFault::VirtualStateInvalid);
111
+ assert(command.twist_ == vec6{});
112
+ }
113
+ }
@@ -0,0 +1,137 @@
1
+ // Exercise the real control loop with in-memory robot IO; no UR controller is contacted.
2
+ #include "ur_cpp/robot_io.hpp"
3
+
4
+ #include <cassert>
5
+ #include <chrono>
6
+ #include <cmath>
7
+ #include <memory>
8
+ #include <mutex>
9
+ #include <thread>
10
+
11
+ namespace {
12
+ struct Plant {
13
+ std::mutex mutex;
14
+ vec6 pose{0.3, -0.2, 0.4, 0, 3.14159265, 0};
15
+ vec6 wrench{100, -50, 80, 5, -3, 2};
16
+ vec6 command{};
17
+ unsigned writes = 0, keepalives = 0, zero_requests = 0;
18
+ bool stopped = false, accept_zero = true;
19
+ } plant;
20
+
21
+ struct TestIO : RobotIO {
22
+ TestIO() { program_running_ = true; }
23
+ bool tick(vec6 &pose, vec6 &q, vec6 &wrench) override {
24
+ std::this_thread::sleep_for(std::chrono::milliseconds(2));
25
+ std::lock_guard<std::mutex> lock(plant.mutex);
26
+ pose = plant.pose;
27
+ wrench = plant.wrench;
28
+ q = {};
29
+ robot_time_ += 0.002;
30
+ return true;
31
+ }
32
+ bool writeTwist(const vec6 &command) override {
33
+ std::lock_guard<std::mutex> lock(plant.mutex);
34
+ plant.command = command;
35
+ ++plant.writes;
36
+ return true;
37
+ }
38
+ bool writeKeepalive() override {
39
+ std::lock_guard<std::mutex> lock(plant.mutex);
40
+ ++plant.keepalives;
41
+ return true;
42
+ }
43
+ bool zeroFT() override {
44
+ std::lock_guard<std::mutex> lock(plant.mutex);
45
+ ++plant.zero_requests;
46
+ return plant.accept_zero;
47
+ }
48
+ void stopControl() override {
49
+ std::lock_guard<std::mutex> lock(plant.mutex);
50
+ plant.stopped = true;
51
+ }
52
+ };
53
+
54
+ template <typename Predicate> void waitFor(Predicate ready) {
55
+ const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(3);
56
+ while (!ready()) {
57
+ assert(std::chrono::steady_clock::now() < deadline);
58
+ std::this_thread::sleep_for(std::chrono::milliseconds(1));
59
+ }
60
+ }
61
+ } // namespace
62
+
63
+ std::unique_ptr<RobotIO> openRobotIo(const Args &, std::atomic<bool> &, std::string *) {
64
+ return std::make_unique<TestIO>();
65
+ }
66
+
67
+ int main() {
68
+ Args args;
69
+ ControlLoop loop(args);
70
+ loop.start();
71
+ assert(loop.waitReady(3.0));
72
+ const auto home = plant.pose;
73
+ loop.setPoseRef(home, false);
74
+ waitFor([&] { return loop.snapshot().mode_ == 5; });
75
+ unsigned first_writes;
76
+ {
77
+ std::lock_guard<std::mutex> lock(plant.mutex);
78
+ first_writes = plant.writes;
79
+ }
80
+ waitFor([&] {
81
+ std::lock_guard<std::mutex> lock(plant.mutex);
82
+ for (double value : plant.command) assert(std::abs(value) < 1e-12);
83
+ return plant.writes >= first_writes + 30;
84
+ });
85
+
86
+ // A refused zero is a warning, not a latched fault or a stop in either mode.
87
+ auto checkZero = [&](bool accepted) {
88
+ unsigned requests, writes;
89
+ const auto mode = loop.snapshot().mode_;
90
+ {
91
+ std::lock_guard<std::mutex> lock(plant.mutex);
92
+ plant.accept_zero = accepted;
93
+ requests = plant.zero_requests;
94
+ writes = plant.writes;
95
+ }
96
+ loop.zeroFt();
97
+ waitFor([&] {
98
+ std::lock_guard<std::mutex> lock(plant.mutex);
99
+ return plant.zero_requests == requests + 1 && plant.writes >= writes + 10;
100
+ });
101
+ const auto state = loop.snapshot();
102
+ assert(state.loop_alive_ && state.loop_ok_ && state.mode_ == mode);
103
+ assert(state.fault_ == LoopFault::None && state.last_error_.empty());
104
+ };
105
+ checkZero(false);
106
+ checkZero(true);
107
+
108
+ // Policy execution can enter admittance using the same connection.
109
+ loop.setPoseRef(home);
110
+ waitFor([&] { return loop.snapshot().mode_ == 1; });
111
+ waitFor([&] {
112
+ std::lock_guard<std::mutex> lock(plant.mutex);
113
+ return std::abs(plant.command[0]) > 1e-4;
114
+ });
115
+ checkZero(false);
116
+ checkZero(true);
117
+
118
+ // A subsequent Home stops the old stream and clears its accumulated force offset.
119
+ unsigned previous_keepalives;
120
+ {
121
+ std::lock_guard<std::mutex> lock(plant.mutex);
122
+ previous_keepalives = plant.keepalives;
123
+ }
124
+ loop.setPoseRef(home, false);
125
+ waitFor([&] { return loop.snapshot().mode_ == 5; });
126
+ {
127
+ std::lock_guard<std::mutex> lock(plant.mutex);
128
+ assert(plant.keepalives > previous_keepalives);
129
+ for (double value : plant.command) assert(std::abs(value) < 1e-12);
130
+ }
131
+ loop.stopMotion();
132
+ waitFor([&] { return loop.snapshot().mode_ == 0; });
133
+ loop.requestStop();
134
+ loop.join();
135
+ assert(plant.stopped);
136
+ assert(loop.snapshot().last_error_.empty());
137
+ }
@@ -0,0 +1,348 @@
1
+ // Pure StreamingPoseShaper/cartesian_speedl regression tests.
2
+ // Runs without robot I/O or wall-clock timing.
3
+ #include "ur_cpp/cartesian_speedl.hpp"
4
+ #include "ur_cpp/geom.hpp"
5
+ #include "ur_cpp/streaming_pose.hpp"
6
+
7
+ #include <algorithm>
8
+ #include <cmath>
9
+ #include <cstdlib>
10
+ #include <iostream>
11
+ #include <limits>
12
+ #include <stdexcept>
13
+ #include <string>
14
+
15
+ namespace {
16
+
17
+ constexpr double kDt = 0.002;
18
+ constexpr double kGain = 15.0;
19
+ const vec6 kOrigin{0.3, -0.2, 0.4, 0, 0, 0};
20
+
21
+ void check(bool condition, const std::string &message) {
22
+ if (!condition)
23
+ throw std::runtime_error(message);
24
+ }
25
+
26
+ double poseAngle(const vec6 &a, const vec6 &b) {
27
+ const auto delta =
28
+ quatMul(rotvecToQuat(a[3], a[4], a[5]), quatConj(rotvecToQuat(b[3], b[4], b[5])));
29
+ vec6 error{};
30
+ quatToRotvec(delta, error[3], error[4], error[5]);
31
+ return norm3(error, 3);
32
+ }
33
+
34
+ struct BoundAudit {
35
+ vec6 previous_acceleration{};
36
+
37
+ void sample(const StreamingPoseShaper &shaper) {
38
+ check(shaper.errors_ == 0, "StreamingPoseShaper numeric failure");
39
+ check(finite6(shaper.last_cmd_) && finite6(shaper.last_vel_), "non-finite shaper state");
40
+ for (int axis = 0; axis < 6; ++axis) {
41
+ const double max_acceleration =
42
+ (axis < 3 ? shaper.args_.cmd_acc_trans_ : shaper.args_.cmd_acc_rot_) / std::sqrt(3.0);
43
+ const double max_jerk = 20.0 * max_acceleration;
44
+ check(std::abs(shaper.tracker_.acceleration_[axis]) <= max_acceleration + 1e-8,
45
+ "streaming acceleration bound exceeded");
46
+ check(std::abs(shaper.tracker_.acceleration_[axis] - previous_acceleration[axis]) /
47
+ shaper.dt_ <=
48
+ max_jerk + 1e-6,
49
+ "streaming jerk bound exceeded");
50
+ }
51
+ check(norm3(shaper.last_vel_) <= shaper.args_.cmd_vel_trans_ + 1e-8 &&
52
+ norm3(shaper.last_vel_, 3) <= shaper.args_.cmd_vel_rot_ + 1e-8,
53
+ "streaming vector velocity bound exceeded");
54
+ previous_acceleration = shaper.tracker_.acceleration_;
55
+ }
56
+ };
57
+
58
+ void stationary() {
59
+ StreamingPoseShaper shaper(Args{}, kGain);
60
+ shaper.sync(kOrigin);
61
+ BoundAudit bounds;
62
+ for (int i = 0; i < 1500; ++i) {
63
+ shaper.step(kOrigin);
64
+ bounds.sample(shaper);
65
+ }
66
+ for (int axis = 0; axis < 6; ++axis) {
67
+ check(std::abs(shaper.last_cmd_[axis] - kOrigin[axis]) < 1e-12, "stationary target moved");
68
+ check(std::abs(shaper.last_vel_[axis]) < 1e-12, "stationary target acquired speed");
69
+ }
70
+ }
71
+
72
+ void movingTarget() {
73
+ StreamingPoseShaper shaper(Args{}, kGain);
74
+ shaper.sync(kOrigin);
75
+ BoundAudit bounds;
76
+ vec6 target = kOrigin;
77
+ for (int i = 0; i < 3500; ++i) {
78
+ target[0] = kOrigin[0] + 0.2 * i * kDt;
79
+ shaper.step(target);
80
+ bounds.sample(shaper);
81
+ }
82
+ const double lag = target[0] - shaper.last_cmd_[0];
83
+ check(std::isfinite(lag) && lag >= 0 && lag < 0.06,
84
+ "continuous moving target retained excessive lag");
85
+ check(shaper.last_vel_[0] > 0.1, "continuous target did not produce forward speed");
86
+ }
87
+
88
+ void smallStep() {
89
+ StreamingPoseShaper shaper(Args{}, kGain);
90
+ shaper.sync(kOrigin);
91
+ BoundAudit bounds;
92
+ vec6 target = kOrigin;
93
+ target[0] += .01;
94
+ double overshoot = 0, tail_speed = 0;
95
+ for (int i = 0; i < 2500; ++i) {
96
+ shaper.step(target);
97
+ bounds.sample(shaper);
98
+ overshoot = std::max(overshoot, shaper.last_cmd_[0] - target[0]);
99
+ if (i >= 2000)
100
+ tail_speed = std::max(tail_speed, std::abs(shaper.last_vel_[0]));
101
+ }
102
+ check(overshoot <= .002, "10mm step exceeds 2mm overshoot gate");
103
+ check(std::abs(shaper.last_cmd_[0] - target[0]) < 1e-5 && tail_speed < 1e-4,
104
+ "10mm step failed to settle");
105
+ }
106
+
107
+ void movingStop() {
108
+ StreamingPoseShaper shaper(Args{}, kGain);
109
+ shaper.sync(kOrigin);
110
+ BoundAudit bounds;
111
+ vec6 target = kOrigin;
112
+ double overshoot = 0, tail_speed = 0;
113
+ for (int i = 0; i < 3500; ++i) {
114
+ if (i < 750)
115
+ target[0] += .2 * kDt;
116
+ shaper.step(target);
117
+ bounds.sample(shaper);
118
+ if (i >= 750)
119
+ overshoot = std::max(overshoot, shaper.last_cmd_[0] - target[0]);
120
+ if (i >= 3000)
121
+ tail_speed = std::max(tail_speed, std::abs(shaper.last_vel_[0]));
122
+ }
123
+ check(overshoot <= .003, "moving 0.2m/s stop exceeds 3mm overshoot gate");
124
+ check(std::abs(shaper.last_cmd_[0] - target[0]) < 1e-5 && tail_speed < 1e-4,
125
+ "moving target stop failed to settle");
126
+ }
127
+
128
+ void largeStepAndReversal() {
129
+ StreamingPoseShaper shaper(Args{}, kGain);
130
+ shaper.sync(kOrigin);
131
+ BoundAudit bounds;
132
+ vec6 target = kOrigin;
133
+ double excursion = 0, tail_speed = 0;
134
+ for (int i = 0; i < 7000; ++i) {
135
+ target[0] = kOrigin[0] + (i < 1500 ? .1 : i < 3000 ? -.1 : 0);
136
+ shaper.step(target);
137
+ bounds.sample(shaper);
138
+ excursion = std::max(excursion, std::abs(shaper.last_cmd_[0] - kOrigin[0]));
139
+ if (i >= 6500)
140
+ tail_speed = std::max(tail_speed, std::abs(shaper.last_vel_[0]));
141
+ }
142
+ check(excursion < .103, "100mm reversal exceeds 3mm overshoot gate");
143
+ check(std::abs(shaper.last_cmd_[0] - kOrigin[0]) < 1e-5 && tail_speed < 1e-4,
144
+ "100mm reversal failed to settle");
145
+ }
146
+
147
+ void crossingPi() {
148
+ StreamingPoseShaper shaper(Args{}, kGain);
149
+ vec6 start = kOrigin, target = kOrigin;
150
+ start[5] = M_PI - .03;
151
+ target[5] = -M_PI + .03;
152
+ shaper.sync(start);
153
+ BoundAudit bounds;
154
+ vec6 previous = start;
155
+ double angular_path = 0, tail_speed = 0;
156
+ for (int i = 0; i < 2500; ++i) {
157
+ shaper.step(target);
158
+ bounds.sample(shaper);
159
+ const double delta = poseAngle(shaper.last_cmd_, previous);
160
+ check(delta <= Args{}.cmd_vel_rot_ * kDt + 1e-8, "cross-pi physical rotation jumped");
161
+ angular_path += delta;
162
+ previous = shaper.last_cmd_;
163
+ if (i >= 2000)
164
+ tail_speed = std::max(tail_speed, norm3(shaper.last_vel_, 3));
165
+ }
166
+ check(angular_path < .09, "cross-pi followed a long rotation branch");
167
+ check(poseAngle(shaper.last_cmd_, target) < 1e-5 && tail_speed < 1e-4,
168
+ "cross-pi target failed to settle");
169
+ }
170
+
171
+ void multiAxisExactPi() {
172
+ StreamingPoseShaper shaper(Args{}, kGain);
173
+ shaper.sync(kOrigin);
174
+ BoundAudit bounds;
175
+ vec6 target = kOrigin;
176
+ const double u = 1 / std::sqrt(3.0);
177
+ for (int i = 0; i < 6500; ++i) {
178
+ const double t = std::min(i * kDt, 4.0);
179
+ target[0] = kOrigin[0] + .01 * std::sin(t * M_PI);
180
+ target[1] = kOrigin[1] + .01 * (std::cos(t * M_PI) - 1);
181
+ target[2] = kOrigin[2] + .005 * std::sin(t * M_PI / 2);
182
+ const double angle = M_PI * std::min(t / 4.0, 1.0);
183
+ for (int axis = 3; axis < 6; ++axis)
184
+ target[axis] = u * (i % 2 == 0 && angle > 1e-6 ? angle - 2 * M_PI : angle);
185
+ const vec6 old = shaper.last_cmd_;
186
+ shaper.step(target);
187
+ bounds.sample(shaper);
188
+ check(poseAngle(old, shaper.last_cmd_) <= Args{}.cmd_vel_rot_ * kDt + 1e-8,
189
+ "multi-axis exact-pi representation physically jumped");
190
+ }
191
+ check(poseAngle(shaper.last_cmd_, target) < 1e-5 && norm3(shaper.last_vel_, 3) < 1e-4,
192
+ "multi-axis exact-pi target failed to settle");
193
+ }
194
+
195
+ void nonCommutingRotation() {
196
+ StreamingPoseShaper shaper(Args{}, kGain);
197
+ shaper.sync(kOrigin);
198
+ BoundAudit bounds;
199
+ vec6 target = kOrigin;
200
+ vec6 previous = kOrigin;
201
+ for (int i = 0; i < 2500; ++i) {
202
+ const double t = i * kDt;
203
+ target[3] = .45 * std::sin(.73 * t);
204
+ target[4] = .35 * std::sin(1.11 * t + .4);
205
+ target[5] = .30 * std::sin(.91 * t - .7);
206
+ shaper.step(target);
207
+ bounds.sample(shaper);
208
+ check(poseAngle(previous, shaper.last_cmd_) <= Args{}.cmd_vel_rot_ * kDt + 1e-8,
209
+ "noncommuting rotation jumped between ticks");
210
+ previous = shaper.last_cmd_;
211
+ }
212
+ const vec6 final_target = target;
213
+ for (int i = 0; i < 2500; ++i) {
214
+ shaper.step(final_target);
215
+ bounds.sample(shaper);
216
+ }
217
+ check(poseAngle(shaper.last_cmd_, final_target) < 1e-5, "noncommuting rotation failed to settle");
218
+ check(norm3(shaper.last_vel_, 3) < 1e-4, "noncommuting rotation retained angular motion");
219
+ }
220
+
221
+ void intermediateBrakingPeak() {
222
+ StreamingPoseShaper shaper(Args{}, kGain);
223
+ shaper.sync(kOrigin, vec6{.02, 0, 0, 0, 0, 0});
224
+ // This is an intentionally difficult state: the target is close, but the
225
+ // current velocity and acceleration already carry an intermediate stopping
226
+ // peak. The envelope must inspect the whole trajectory, not just endpoint.
227
+ shaper.tracker_.in_.current_velocity[0] = .02;
228
+ shaper.tracker_.in_.current_acceleration[0] = -1.7;
229
+ shaper.last_vel_[0] = .02;
230
+ const double error = .0001;
231
+ const double proposed = kGain * error;
232
+
233
+ const double limited = shaper.stoppingLimitedGoal(0, proposed, error);
234
+ check(limited == 0, "intermediate brake crossing did not select bounded stop");
235
+ }
236
+
237
+ void invalidAndReset() {
238
+ StreamingPoseShaper shaper(Args{}, kGain);
239
+ shaper.sync(kOrigin);
240
+ vec6 invalid = kOrigin;
241
+ invalid[1] = std::numeric_limits<double>::quiet_NaN();
242
+ shaper.step(invalid);
243
+ check(shaper.errors_ > 0 && shaper.last_cmd_ == kOrigin,
244
+ "NaN target was accepted or changed command");
245
+ shaper.sync(kOrigin);
246
+ check(shaper.errors_ == 0, "explicit reset did not clear shaper failure");
247
+ for (int axis : {0, 3}) {
248
+ for (double value : {-1e308, 1e308}) {
249
+ shaper.sync(kOrigin);
250
+ vec6 huge = kOrigin;
251
+ huge[axis] = value;
252
+ shaper.step(huge);
253
+ check(shaper.errors_ > 0 && shaper.last_cmd_ == kOrigin, "huge finite pose was accepted");
254
+ }
255
+ }
256
+ }
257
+
258
+ void rotationIntegrationAgainstSubsteps() {
259
+ StreamingPoseShaper shaper(Args{}, kGain);
260
+ vec6 start = kOrigin;
261
+ start[3] = .3;
262
+ start[4] = -.4;
263
+ start[5] = .2;
264
+ shaper.sync(start);
265
+ BoundAudit bounds;
266
+ Quat integrated = rotvecToQuat(start[3], start[4], start[5]);
267
+ double max_error = 0;
268
+ constexpr int substeps = 64;
269
+ for (int i = 0; i < 1600; ++i) {
270
+ const double t = i * kDt;
271
+ vec6 target = start;
272
+ target[3] += .25 * std::sin(1.3 * t);
273
+ target[4] += .22 * std::sin(.9 * t);
274
+ target[5] += .3 * std::sin(1.7 * t);
275
+ // Preserve the pre-step state: the governor mutates current v/a.
276
+ auto input = shaper.tracker_.in_;
277
+ input.current_position = {};
278
+ shaper.step(target);
279
+ bounds.sample(shaper);
280
+ input.target_velocity = shaper.tracker_.in_.target_velocity;
281
+ ruckig::Trajectory<6> accepted;
282
+ check(shaper.tracker_.otg_.calculate(input, accepted) >= 0,
283
+ "independent accepted-trajectory reconstruction failed");
284
+ vec6 p{}, velocity{}, acceleration{};
285
+ for (int sub = 0; sub < substeps; ++sub) {
286
+ accepted.at_time(kDt * (sub + .5) / substeps, p, velocity, acceleration);
287
+ const double h = kDt / substeps;
288
+ integrated =
289
+ quatMul(rotvecToQuat(h * velocity[3], h * velocity[4], h * velocity[5]), integrated);
290
+ }
291
+ vec6 numerical = kOrigin;
292
+ quatToRotvec(integrated, numerical[3], numerical[4], numerical[5]);
293
+ max_error = std::max(max_error, poseAngle(numerical, shaper.last_cmd_));
294
+ }
295
+ check(max_error < 1e-5, "spatial quaternion integration disagrees with 64 midpoint substeps");
296
+ std::cout << "changing_axis_integrator_max_error_rad=" << max_error << '\n';
297
+ }
298
+
299
+ void productionLawUsesFixedGain() {
300
+ CartesianSpeedlLaw law(Args{});
301
+ std::string error;
302
+ check(law.validate(error), "production cartesian_speedl validation failed: " + error);
303
+ check(std::abs(law.shaper_.gain_ - kGain) < 1e-12,
304
+ "production Cartesian controller did not use the configured reference gain");
305
+ law.reset(kOrigin, {});
306
+ CartesianTick tick;
307
+ tick.reference_ = tick.actual_ = kOrigin;
308
+ tick.dt_ = kDt;
309
+ CartesianCommand command;
310
+ for (int i = 0; i < 1000; ++i) {
311
+ check(law.step(tick, command), "production cartesian_speedl stationary step failed");
312
+ check(command.fault_ == LoopFault::None && finite6(command.twist_),
313
+ "production cartesian_speedl produced invalid stationary output");
314
+ }
315
+ }
316
+
317
+ } // namespace
318
+
319
+ int main() {
320
+ const struct {
321
+ const char *name;
322
+ void (*run)();
323
+ } tests[] = {{"stationary", stationary},
324
+ {"moving_target", movingTarget},
325
+ {"step_10mm", smallStep},
326
+ {"moving_stop", movingStop},
327
+ {"step_100mm_reversal", largeStepAndReversal},
328
+ {"cross_pi", crossingPi},
329
+ {"multi_axis_exact_pi", multiAxisExactPi},
330
+ {"noncommuting_rotation", nonCommutingRotation},
331
+ {"rotation_substeps", rotationIntegrationAgainstSubsteps},
332
+ {"intermediate_braking_peak", intermediateBrakingPeak},
333
+ {"invalid_reset", invalidAndReset},
334
+ {"production_fixed_gain", productionLawUsesFixedGain}};
335
+ unsigned failures = 0;
336
+ for (const auto &test : tests) {
337
+ try {
338
+ test.run();
339
+ std::cout << "PASS " << test.name << '\n';
340
+ } catch (const std::exception &error) {
341
+ ++failures;
342
+ std::cerr << "FAIL " << test.name << ": " << error.what() << '\n';
343
+ }
344
+ }
345
+ std::cout << "streaming_pose_unit failures=" << failures << '/'
346
+ << (sizeof(tests) / sizeof(tests[0])) << '\n';
347
+ return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
348
+ }
@@ -0,0 +1,65 @@
1
+ // Ruckig trajectory regression without robot I/O.
2
+ #include "ur_cpp/motion_primitives.hpp"
3
+
4
+ #include <cassert>
5
+ #include <limits>
6
+
7
+ int main() {
8
+ constexpr double dt = .002;
9
+ TwistGovernor governor(dt, 1, 2.5, 25, 30, 2000, 2000);
10
+ governor.reset();
11
+ vec6 previous_acceleration{};
12
+ for (int n = 0; n < 3000; ++n) {
13
+ const double angle = n * .009;
14
+ vec6 target{4 * std::cos(angle), 4 * std::sin(angle), 1.0,
15
+ 5 * std::sin(angle), 5 * std::cos(angle), 2.0};
16
+ assert(governor.step(target));
17
+ assert(norm3(governor.sent_) <= 1 + 1e-8);
18
+ assert(norm3(governor.sent_, 3) <= 2.5 + 1e-8);
19
+ for (int axis = 0; axis < 6; ++axis) {
20
+ assert(std::abs(governor.acceleration_[axis]) <=
21
+ (axis < 3 ? 25 : 30) / std::sqrt(3.0) + 1e-8);
22
+ assert(std::abs(governor.acceleration_[axis] - previous_acceleration[axis]) <=
23
+ 2000 / std::sqrt(3.0) * dt + 1e-8);
24
+ }
25
+ previous_acceleration = governor.acceleration_;
26
+ }
27
+
28
+ // Inject a failed candidate calculation while a valid trajectory is moving.
29
+ // The accepted trajectory must advance, then accept a new target on recovery.
30
+ governor.reset();
31
+ for (int n = 0; n < 10; ++n)
32
+ assert(governor.step({.6, 0, 0, 0, 0, 0}));
33
+ const double before = governor.sent_[0];
34
+ const auto jerk = governor.in_.max_jerk;
35
+ governor.in_.max_jerk[0] = 0;
36
+ for (int n = 0; n < 10; ++n) {
37
+ assert(governor.step({-.2, 0, 0, 0, 0, 0}));
38
+ assert(governor.displacement_[0] > 0);
39
+ }
40
+ assert(governor.sent_[0] > before);
41
+ governor.in_.max_jerk = jerk;
42
+ for (int n = 0; n < 500; ++n)
43
+ assert(governor.step({-.2, 0, 0, 0, 0, 0}));
44
+ assert(std::abs(governor.sent_[0] + .2) < 1e-8);
45
+
46
+ // A long trajectory remains an ordinary shaping calculation.
47
+ TwistGovernor slow(dt, 1, 2.5, .003, .003, .06, .06);
48
+ slow.reset();
49
+ assert(slow.step({1, 0, 0, 0, 0, 0}));
50
+ assert(slow.sent_[0] > 0);
51
+ ruckig::Trajectory<6> trajectory;
52
+ assert(slow.otg_.calculate(slow.in_, trajectory) >= 0);
53
+ assert(trajectory.get_duration() > 30);
54
+
55
+ // Ruckig may correct initial acceleration after entering the mode.
56
+ governor.reset();
57
+ governor.in_.current_acceleration[0] = 20;
58
+ assert(governor.step({.5, 0, 0, 0, 0, 0}));
59
+ assert(governor.sent_[0] > 0);
60
+ assert(governor.acceleration_[0] < 20);
61
+
62
+ vec6 invalid{};
63
+ invalid[0] = std::numeric_limits<double>::quiet_NaN();
64
+ assert(!governor.step(invalid));
65
+ }