PathGeometry.cpp
Go to the documentation of this file.
1/**
2 * This file is part of ArmarX.
3 *
4 * ArmarX is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License version 2 as
6 * published by the Free Software Foundation.
7 *
8 * ArmarX is distributed in the hope that it will be useful, but
9 * WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 */
16
17#include "PathGeometry.h"
18
19#include <algorithm>
20#include <cmath>
21#include <limits>
22
23#include <Eigen/Core>
24
26{
27 namespace
28 {
29 Eigen::Vector2d
30 step(const core::GlobalTrajectory& trajectory, const std::size_t from)
31 {
32 const auto& a = trajectory.points().at(from).waypoint.pose.translation();
33 const auto& b = trajectory.points().at(from + 1).waypoint.pose.translation();
34
35 return Eigen::Vector2d{static_cast<double>(b.x() - a.x()),
36 static_cast<double>(b.y() - a.y())};
37 }
38 } // namespace
39
40 std::vector<std::size_t>
42 {
43 std::vector<std::size_t> violations;
44
45 const std::size_t count = trajectory.points().size();
46
47 if (count < 3)
48 {
49 return violations;
50 }
51
52 for (std::size_t i = 1; i + 1 < count; i++)
53 {
54 const Eigen::Vector2d incoming = step(trajectory, i - 1);
55 const Eigen::Vector2d outgoing = step(trajectory, i);
56
57 const double incomingLength = incoming.norm();
58 const double outgoingLength = outgoing.norm();
59
60 // A vertex with no length on either side has no direction to compare, and the
61 // heading read off it downstream is noise either way.
62 if (incomingLength < limits.minSegmentLength or
63 outgoingLength < limits.minSegmentLength)
64 {
65 violations.push_back(i);
66 continue;
67 }
68
69 const double alignment = incoming.dot(outgoing) / (incomingLength * outgoingLength);
70
71 if (alignment <= limits.minDirectionDot)
72 {
73 violations.push_back(i);
74 }
75 }
76
77 return violations;
78 }
79
82 std::vector<std::size_t>& removed,
83 const GeometryLimits& limits)
84 {
85 removed.clear();
86
87 // Indices are reported against the trajectory as it stands, so they are mapped back to
88 // the original numbering after each pass -- the caller logs them, and an index that
89 // shifted with every removal would not name the waypoint the planner produced.
90 std::vector<std::size_t> originalIndices(trajectory.points().size());
91 for (std::size_t i = 0; i < originalIndices.size(); i++)
92 {
93 originalIndices.at(i) = i;
94 }
95
97
98 // One fold can span several waypoints, and removing one can expose the next. Bounded so a
99 // pathological path cannot spin here: this runs inside a planning request.
100 constexpr int maxPasses = 8;
101
102 for (int pass = 0; pass < maxPasses; pass++)
103 {
104 const std::vector<std::size_t> violations = findGeometryViolations(repaired, limits);
105
106 if (violations.empty())
107 {
108 break;
109 }
110
111 // A single displaced waypoint reverses *two* consecutive direction pairs -- the step
112 // into it and the step out of it -- so it shows up as a run of adjacent violations
113 // while only one waypoint is actually at fault. Removing the whole run would discard
114 // an innocent neighbour and pull the path off the route the optimizer chose.
115 //
116 // The culprit is identified by length: a fold makes the path travel backwards and
117 // then forwards again, so removing the displaced point shortens the local path more
118 // than removing any of its neighbours does.
119 std::vector<std::size_t> toRemove;
120
121 for (std::size_t runStart = 0; runStart < violations.size();)
122 {
123 std::size_t runEnd = runStart;
124 while (runEnd + 1 < violations.size() and
125 violations.at(runEnd + 1) == violations.at(runEnd) + 1)
126 {
127 runEnd++;
128 }
129
130 const std::size_t first = violations.at(runStart);
131 const std::size_t last = violations.at(runEnd);
132
133 // Span one waypoint either side of the run, which is what any single removal
134 // inside it can affect.
135 const std::size_t from = (first > 0) ? first - 1 : 0;
136 const std::size_t to = std::min(last + 1, repaired.points().size() - 1);
137
138 std::size_t bestCandidate = last;
139 double bestLength = std::numeric_limits<double>::max();
140 double bestSpacingSpread = std::numeric_limits<double>::max();
141
142 for (std::size_t candidate = first; candidate <= last; candidate++)
143 {
144 std::vector<double> segments;
145 std::size_t previous = from;
146
147 for (std::size_t i = from + 1; i <= to; i++)
148 {
149 if (i == candidate)
150 {
151 continue;
152 }
153
154 const auto& a = repaired.points().at(previous).waypoint.pose.translation();
155 const auto& b = repaired.points().at(i).waypoint.pose.translation();
156 segments.push_back(std::hypot(static_cast<double>(b.x() - a.x()),
157 static_cast<double>(b.y() - a.y())));
158 previous = i;
159 }
160
161 double length = 0.0;
162 for (const double segment : segments)
163 {
164 length += segment;
165 }
166
167 // On a straight run every candidate removal yields the same length, so length
168 // alone cannot say which waypoint is the displaced one. Spacing does: the
169 // optimizer drives the path towards uniform spacing (`SpacingResidual`), so
170 // the removal that leaves the segments most even is the one that took out the
171 // outlier rather than one of its innocent neighbours.
172 double spacingSpread = 0.0;
173 if (not segments.empty())
174 {
175 const double mean = length / static_cast<double>(segments.size());
176 for (const double segment : segments)
177 {
178 spacingSpread += (segment - mean) * (segment - mean);
179 }
180 }
181
182 const double lengthTolerance = 1e-6 * std::max(1.0, bestLength);
183 const bool shorter = length < bestLength - lengthTolerance;
184 const bool sameLengthButMoreEven =
185 std::abs(length - bestLength) <= lengthTolerance and
186 spacingSpread < bestSpacingSpread;
187
188 if (shorter or sameLengthButMoreEven)
189 {
190 bestLength = length;
191 bestSpacingSpread = spacingSpread;
192 bestCandidate = candidate;
193 }
194 }
195
196 toRemove.push_back(bestCandidate);
197 runStart = runEnd + 1;
198 }
199
200 std::vector<core::GlobalTrajectoryPoint> kept;
201 std::vector<std::size_t> keptIndices;
202 kept.reserve(repaired.points().size());
203 keptIndices.reserve(originalIndices.size());
204
205 for (std::size_t i = 0; i < repaired.points().size(); i++)
206 {
207 const bool drop = std::find(toRemove.begin(), toRemove.end(), i) != toRemove.end();
208
209 if (drop)
210 {
211 removed.push_back(originalIndices.at(i));
212 continue;
213 }
214
215 kept.push_back(repaired.points().at(i));
216 keptIndices.push_back(originalIndices.at(i));
217 }
218
219 // Never strip the path down to something that is no longer a path.
220 if (kept.size() < 3)
221 {
222 break;
223 }
224
225 repaired = core::GlobalTrajectory{kept};
226 originalIndices = keptIndices;
227 }
228
229 std::sort(removed.begin(), removed.end());
230
231 return repaired;
232 }
233
234 namespace
235 {
236 /// Yaw of a planar pose. `atan2` on the rotation's first column rather than an rpy
237 /// decomposition, which is unstable when the matrix carries numerical pitch.
238 double
239 yawOf(const core::Pose& pose)
240 {
241 return std::atan2(static_cast<double>(pose.linear()(1, 0)),
242 static_cast<double>(pose.linear()(0, 0)));
243 }
244
245 /// Signed difference wrapped to (-pi, pi], i.e. the way the base would actually turn.
246 /// Without the wrap a profile crossing the +-pi branch cut reads as a full revolution.
247 double
248 shortestAngularDiff(const double after, const double before)
249 {
250 double diff = after - before;
251
252 while (diff > M_PI)
253 {
254 diff -= 2.0 * M_PI;
255 }
256 while (diff <= -M_PI)
257 {
258 diff += 2.0 * M_PI;
259 }
260
261 return diff;
262 }
263
264 /// Turn rate of the segment starting at `index`, or 0 for a segment too short to carry a
265 /// meaningful one -- those are the fold check's business, and dividing by them here would
266 /// report every one of them as a turn-rate violation too.
267 double
268 segmentTurnRate(const core::GlobalTrajectory& trajectory,
269 const std::size_t index,
270 const GeometryLimits& limits)
271 {
272 const auto& points = trajectory.points();
273
274 const double length = (points.at(index + 1).waypoint.pose.translation() -
275 points.at(index).waypoint.pose.translation())
276 .norm();
277
278 if (not(length > limits.minSegmentLength))
279 {
280 return 0.0;
281 }
282
283 const double delta = shortestAngularDiff(yawOf(points.at(index + 1).waypoint.pose),
284 yawOf(points.at(index).waypoint.pose));
285
286 return std::abs(delta) / length;
287 }
288 } // namespace
289
290 std::vector<std::size_t>
292 {
293 std::vector<std::size_t> violations;
294
295 if (trajectory.points().size() < 2 or not(limits.maxTurnRate > 0.0))
296 {
297 return violations;
298 }
299
300 for (std::size_t i = 0; i + 1 < trajectory.points().size(); i++)
301 {
302 if (segmentTurnRate(trajectory, i, limits) > limits.maxTurnRate)
303 {
304 violations.push_back(i);
305 }
306 }
307
308 return violations;
309 }
310
311 double
313 {
314 double worst = 0.0;
315
316 for (std::size_t i = 0; i + 1 < trajectory.points().size(); i++)
317 {
318 worst = std::max(worst, segmentTurnRate(trajectory, i, limits));
319 }
320
321 return worst;
322 }
323
324} // namespace armarx::navigation::algorithms::spfa::smoothing
uint8_t index
#define M_PI
Definition MathTools.h:17
const std::vector< GlobalTrajectoryPoint > & points() const
double a(double t, double a0, double j)
Definition CtrlUtil.h:45
std::vector< std::size_t > findTurnRateViolations(const core::GlobalTrajectory &trajectory, const GeometryLimits &limits)
Segments whose commanded heading turns faster than the base could follow.
double maxTurnRate(const core::GlobalTrajectory &trajectory, const GeometryLimits &limits)
The largest |dyaw| / segment length in the trajectory [rad/mm].
std::vector< std::size_t > findGeometryViolations(const core::GlobalTrajectory &trajectory, const GeometryLimits &limits)
Indices of waypoints that make the path double back on itself.
core::GlobalTrajectory repairGeometry(const core::GlobalTrajectory &trajectory, std::vector< std::size_t > &removed, const GeometryLimits &limits)
Drop the offending waypoints, repeating until the path is monotone.
Eigen::Isometry3f Pose
Definition basic_types.h:31
std::optional< float > mean(const boost::circular_buffer< NameValueMap > &buffer, const std::string &key)
Thresholds for what counts as a geometrically usable path.
double minSegmentLength
Segments shorter than this [mm] carry no reliable direction, and the heading a downstream spline read...
double minDirectionDot
Consecutive travel directions whose dot product is below this have reversed.
double maxTurnRate
Most the commanded heading may turn per millimetre travelled [rad/mm].