libstp
Robotics library for Botball
Loading...
Searching...
No Matches
trapezoidal_profile.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <algorithm>
4#include <cmath>
5#include <stdexcept>
6
7namespace libstp::motion
8{
21 {
22 public:
24 {
25 double max_velocity{1.0}; // Maximum velocity
26 double max_acceleration{2.0}; // Maximum acceleration rate
27 double max_deceleration{0.0}; // Maximum deceleration rate (0 = use max_acceleration)
28
29 double effectiveDeceleration() const
30 {
32 }
33
38 void validate() const
39 {
40 if (!(max_velocity > 0.0))
41 throw std::invalid_argument("TrapezoidalProfile: max_velocity must be positive");
42 if (!(max_acceleration > 0.0))
43 throw std::invalid_argument("TrapezoidalProfile: max_acceleration must be positive");
44 if (max_deceleration < 0.0)
45 throw std::invalid_argument("TrapezoidalProfile: max_deceleration must be >= 0");
46 }
47 };
48
49 struct State
50 {
51 double position{0.0};
52 double velocity{0.0};
53 };
54
61 TrapezoidalProfile(const State& initial, double target, const Constraints& constraints)
62 : initial_(initial)
63 , target_(target)
64 , constraints_(constraints)
65 {
66 constraints_.validate();
67 // Reject a degenerate zero-distance (or NaN) target at config time:
68 // compute_profile() would compute cruise_time_ = 0.0 / 0.0 = NaN, so
69 // getTotalTime() is NaN and isComplete() could never return true.
70 if (!(std::abs(target_ - initial_.position) > 0.0))
71 throw std::invalid_argument(
72 "TrapezoidalProfile: target must differ from the initial "
73 "position (zero-distance profile is undefined)");
74 compute_profile();
75 }
76
82 State getSetpoint(double elapsed_time) const
83 {
84 const double t = std::max(0.0, elapsed_time);
85
86 if (t >= total_time_)
87 {
88 // Profile complete, return target
89 return State{target_, 0.0};
90 }
91
92 const double sign = (target_ >= initial_.position) ? 1.0 : -1.0;
93 const double a_dec = constraints_.effectiveDeceleration();
94
95 State state;
96
97 if (t <= accel_time_)
98 {
99 // Acceleration phase
100 const double accel = sign * constraints_.max_acceleration;
101 state.velocity = initial_.velocity + accel * t;
102 state.position = initial_.position + initial_.velocity * t + 0.5 * accel * t * t;
103 }
104 else if (t <= accel_time_ + cruise_time_)
105 {
106 // Cruise phase
107 const double cruise_t = t - accel_time_;
108 state.velocity = sign * cruise_velocity_;
109 state.position = accel_end_position_ + state.velocity * cruise_t;
110 }
111 else
112 {
113 // Deceleration phase
114 const double decel_t = t - accel_time_ - cruise_time_;
115 const double decel = -sign * a_dec;
116 state.velocity = sign * cruise_velocity_ + decel * decel_t;
117 state.position = decel_start_position_ + sign * cruise_velocity_ * decel_t + 0.5 * decel * decel_t * decel_t;
118 }
119
120 return state;
121 }
122
139 static State calculate(double dt, const State& current, const State& goal,
140 const Constraints& constraints)
141 {
142 constraints.validate();
143 const double max_v = constraints.max_velocity;
144 const double a_acc = constraints.max_acceleration;
145 const double a_dec = constraints.effectiveDeceleration();
146
147 // Direction: flip so goal is always "ahead" of current
148 const int dir = (current.position > goal.position) ? -1 : 1;
149
150 // Work in directed frame
151 double c_pos = current.position * dir;
152 double c_vel = current.velocity * dir;
153 const double g_pos = goal.position * dir;
154 const double g_vel = goal.velocity * dir;
155
156 // Clamp current velocity to constraints
157 if (std::abs(c_vel) > max_v)
158 c_vel = std::copysign(max_v, c_vel);
159
160 // Treat as a full zero-to-zero profile, then trim the portions
161 // before current velocity and after goal velocity.
162 // Acceleration side uses a_acc, deceleration side uses a_dec.
163 const double cutoff_begin = c_vel / a_acc;
164 const double cutoff_dist_begin = cutoff_begin * cutoff_begin * a_acc * 0.5;
165
166 const double cutoff_end = g_vel / a_dec;
167 const double cutoff_dist_end = cutoff_end * cutoff_end * a_dec * 0.5;
168
169 const double full_trapezoid_dist =
170 cutoff_dist_begin + (g_pos - c_pos) + cutoff_dist_end;
171
172 double accel_time = max_v / a_acc;
173 double decel_time = max_v / a_dec;
174 double full_speed_dist =
175 full_trapezoid_dist - 0.5 * a_acc * accel_time * accel_time
176 - 0.5 * a_dec * decel_time * decel_time;
177
178 double cruise_v = max_v;
179
180 // Triangular profile if we can't reach full speed
181 if (full_speed_dist < 0.0)
182 {
183 // v_peak^2 = 2 * d * a_acc * a_dec / (a_acc + a_dec)
184 const double v_peak_sq = 2.0 * std::max(0.0, full_trapezoid_dist)
185 * a_acc * a_dec / (a_acc + a_dec);
186 cruise_v = std::sqrt(std::max(0.0, v_peak_sq));
187 accel_time = cruise_v / a_acc;
188 decel_time = cruise_v / a_dec;
189 full_speed_dist = 0.0;
190 }
191
192 const double end_accel = accel_time - cutoff_begin;
193 const double end_full_speed =
194 end_accel + (cruise_v > 1e-10 ? full_speed_dist / cruise_v : 0.0);
195 const double end_decel = end_full_speed + decel_time - cutoff_end;
196
197 double r_pos, r_vel;
198
199 if (dt < end_accel)
200 {
201 // Acceleration phase
202 r_vel = c_vel + dt * a_acc;
203 r_pos = c_pos + (c_vel + dt * a_acc * 0.5) * dt;
204 }
205 else if (dt < end_full_speed)
206 {
207 // Cruise phase
208 r_vel = cruise_v;
209 r_pos = c_pos
210 + (c_vel + end_accel * a_acc * 0.5) * end_accel
211 + cruise_v * (dt - end_accel);
212 }
213 else if (dt <= end_decel)
214 {
215 // Deceleration phase (computed backwards from goal -- exact)
216 const double time_left = end_decel - dt;
217 r_vel = g_vel + time_left * a_dec;
218 r_pos = g_pos - (g_vel + time_left * a_dec * 0.5) * time_left;
219 }
220 else
221 {
222 // Profile complete within this timestep
223 r_pos = g_pos;
224 r_vel = g_vel;
225 }
226
227 // Convert back from directed frame
228 return State{r_pos * dir, r_vel * dir};
229 }
230
234 bool isComplete(double elapsed_time) const
235 {
236 return elapsed_time >= total_time_;
237 }
238
242 double getTotalTime() const
243 {
244 return total_time_;
245 }
246
247 private:
248 void compute_profile()
249 {
250 const double distance = target_ - initial_.position;
251 const double abs_distance = std::abs(distance);
252 const double sign = (distance >= 0.0) ? 1.0 : -1.0;
253
254 const double a_acc = constraints_.max_acceleration;
255 const double a_dec = constraints_.effectiveDeceleration();
256
257 // Max velocity considering asymmetric accel/decel:
258 // v_peak = sqrt(2 * d * a_acc * a_dec / (a_acc + a_dec))
259 const double max_reachable_velocity = std::sqrt(
260 2.0 * abs_distance * a_acc * a_dec / (a_acc + a_dec));
261 cruise_velocity_ = std::min(constraints_.max_velocity, max_reachable_velocity);
262
263 // Time to accelerate to cruise velocity
264 accel_time_ = cruise_velocity_ / a_acc;
265 const double decel_time = cruise_velocity_ / a_dec;
266
267 // Distance covered during acceleration
268 const double accel_distance = 0.5 * a_acc * accel_time_ * accel_time_;
269
270 // Distance covered during deceleration (may differ from accel)
271 const double decel_distance = 0.5 * a_dec * decel_time * decel_time;
272
273 // Distance covered during cruise
274 const double cruise_distance = abs_distance - accel_distance - decel_distance;
275
276 if (cruise_distance < 0.0)
277 {
278 // Triangular profile (no cruise phase)
279 cruise_time_ = 0.0;
280 cruise_velocity_ = max_reachable_velocity;
281 accel_time_ = cruise_velocity_ / a_acc;
282
283 const double new_accel_distance = 0.5 * a_acc * accel_time_ * accel_time_;
284 accel_end_position_ = initial_.position + sign * new_accel_distance;
285 decel_start_position_ = accel_end_position_;
286 }
287 else
288 {
289 // Trapezoidal profile (has cruise phase)
290 cruise_time_ = cruise_distance / cruise_velocity_;
291
292 accel_end_position_ = initial_.position + sign * accel_distance;
293 decel_start_position_ = initial_.position + sign * (accel_distance + cruise_distance);
294 }
295
296 // Total time with asymmetric decel
297 total_time_ = accel_time_ + cruise_time_ + cruise_velocity_ / a_dec;
298 }
299
300 State initial_;
301 double target_;
302 Constraints constraints_;
303
304 // Computed profile parameters
305 double accel_time_{0.0};
306 double cruise_time_{0.0};
307 double cruise_velocity_{0.0};
308 double accel_end_position_{0.0};
309 double decel_start_position_{0.0};
310 double total_time_{0.0};
311 };
312}
TrapezoidalProfile(const State &initial, double target, const Constraints &constraints)
bool isComplete(double elapsed_time) const
State getSetpoint(double elapsed_time) const
static State calculate(double dt, const State &current, const State &goal, const Constraints &constraints)