TransformHelper.cpp
Go to the documentation of this file.
1#include "TransformHelper.h"
2
3#include <optional>
4#include <set>
5#include <string>
6
7#include <SimoxUtility/algorithm/get_map_keys_values.h>
8#include <SimoxUtility/algorithm/string/string_tools.h>
9#include <SimoxUtility/math/pose/interpolate.h>
10
16
23#include <RobotAPI/libraries/armem_robot_state/aron/Transform.aron.generated.h>
27
29{
30
31 template <class... Args>
33 TransformHelper::_lookupTransform(
34 const armem::base::CoreSegmentBase<Args...>& localizationCoreSegment,
35 const TransformQuery& query)
36 {
37 const std::vector<std::string> tfChain =
38 _buildTransformChain(localizationCoreSegment, query);
39 if (tfChain.empty())
40 {
41 return {.transform = {.header = query.header},
43 .errorMessage = "Cannot create tf lookup chain '" + query.header.parentFrame +
44 " -> " + query.header.frame + "' for robot `" +
45 query.header.agent + "`."};
46 }
47
48 const std::vector<Eigen::Isometry3f> transforms = _obtainTransforms(
49 localizationCoreSegment, tfChain, query.header.agent, query.header.timestamp);
50
51 const std::optional<armem::Time> sanitizedTimestamp =
52 _obtainTimestamp(localizationCoreSegment, query.header.timestamp);
53
54 if (not sanitizedTimestamp.has_value())
55 {
56 return {.transform = {.header = query.header},
58 .errorMessage = "Error: Issue with timestamp"};
59 }
60
61
62 auto header = query.header;
63
64 ARMARX_CHECK(sanitizedTimestamp.has_value());
65
66 // ARMARX_INFO << header.timestamp << "vs" << sanitizedTimestamp;
67
68 header.timestamp = sanitizedTimestamp.value();
69
70 if (transforms.empty())
71 {
72 ARMARX_INFO << deactivateSpam(1) << "No transform available.";
73 return {.transform = {.header = query.header},
75 .errorMessage = "Error in TF lookup: '" + query.header.parentFrame + " -> " +
76 query.header.frame +
77 "'. No memory data in time range. Reference time " +
78 sanitizedTimestamp.value().toTimeString()};
79 }
80
81 const Eigen::Isometry3f transform = std::accumulate(transforms.begin(),
82 transforms.end(),
83 Eigen::Isometry3f::Identity(),
84 std::multiplies<>());
85
86 ARMARX_DEBUG << "Found valid transform";
87
88 return {.transform = {.header = header, .transform = transform},
90 }
91
92 template <class... Args>
94 TransformHelper::_lookupTransformChain(
95 const armem::base::CoreSegmentBase<Args...>& localizationCoreSegment,
96 const TransformQuery& query)
97 {
98 const std::vector<std::string> tfChain =
99 _buildTransformChain(localizationCoreSegment, query);
100 if (tfChain.empty())
101 {
102 ARMARX_DEBUG << "TF chain is empty";
103 return {.header = query.header,
104 .transforms = std::vector<Eigen::Isometry3f>{},
106 .errorMessage = "Cannot create tf lookup chain '" + query.header.parentFrame +
107 " -> " + query.header.frame + "' for robot `" +
108 query.header.agent + "`."};
109 }
110
111 const std::vector<Eigen::Isometry3f> transforms = _obtainTransforms(
112 localizationCoreSegment, tfChain, query.header.agent, query.header.timestamp);
113 if (transforms.empty())
114 {
115 ARMARX_INFO << deactivateSpam(1) << "No transform available.";
116 return {.header = query.header,
117 .transforms = {},
119 .errorMessage = "Error in TF lookup: '" + query.header.parentFrame + " -> " +
120 query.header.frame +
121 "'. No memory data in time range. Reference time " +
122 query.header.timestamp.toTimeString()};
123 }
124
125
126 ARMARX_DEBUG << "Found valid transform";
127
128 return {.header = query.header,
129 .transforms = transforms,
131 }
132
133 template <class... Args>
134 std::vector<std::string>
135 TransformHelper::_buildTransformChain(
136 const armem::base::CoreSegmentBase<Args...>& localizationCoreSegment,
137 const TransformQuery& query)
138 {
139 ARMARX_DEBUG << "Building transform chain for robot `" << query.header.agent << "`.";
140
141 std::vector<std::string> chain;
142
143 const auto& agentProviderSegment =
144 localizationCoreSegment.getProviderSegment(query.header.agent);
145
146 const std::vector<std::string> tfs = agentProviderSegment.getEntityNames();
147
148 // lookup from robot root to global
149 std::map<std::string, std::string> tfLookup;
150
151 for (const std::string& tf : tfs)
152 {
153 const auto frames = simox::alg::split(tf, ",");
154 ARMARX_CHECK_EQUAL(frames.size(), 2);
155
156 tfLookup[frames.front()] = frames.back();
157 }
158
159 std::string currentFrame = query.header.parentFrame;
160 chain.push_back(currentFrame);
161
162 // Walking the parent->child map without remembering where we have been loops forever on a
163 // cyclic graph -- two components publishing inverse transforms ("A,B" and "B,A") is enough
164 // -- growing the chain until the process runs out of memory. The lookup thread simply
165 // never returns. ROS TF detects loops explicitly; so do we.
166 std::set<std::string> visited{currentFrame};
167 while (tfLookup.count(currentFrame) > 0 and currentFrame != query.header.frame)
168 {
169 currentFrame = tfLookup.at(currentFrame);
170
171 if (not visited.insert(currentFrame).second)
172 {
173 ARMARX_WARNING << deactivateSpam(60) << "Cycle in the transform graph of robot `"
174 << query.header.agent << "` at frame '" << currentFrame
175 << "'. Cannot create tf lookup chain '" << query.header.parentFrame
176 << " -> " << query.header.frame << "'.";
177 return {};
178 }
179
180 chain.push_back(currentFrame);
181 }
182
183 ARMARX_DEBUG << VAROUT(chain);
184
185 if (chain.empty() or chain.back() != query.header.frame)
186 {
187 ARMARX_INFO << deactivateSpam(60) << "Cannot create tf lookup chain '"
188 << query.header.parentFrame << " -> " << query.header.frame
189 << "' for robot `" + query.header.agent + "`.";
190 return {};
191 }
192
193 std::vector<std::string> frameChain;
194 for (size_t i = 0; i < (chain.size() - 1); i++)
195 {
196 frameChain.push_back(chain.at(i) + "," + chain.at(i + 1));
197 }
198
199 return frameChain;
200 }
201
202 template <class... Args>
203 std::optional<armarx::core::time::DateTime>
204 TransformHelper::_obtainTimestamp(
205 const armem::base::CoreSegmentBase<Args...>& localizationCoreSegment,
206 const armem::Time& timestamp)
207 {
208
209 // first we check which the newest timestamp is
210 std::optional<int64_t> timeSinceEpochUs = std::nullopt;
211
212 localizationCoreSegment.forEachEntity(
213 [&timeSinceEpochUs, &timestamp](const auto& entity)
214 {
215 auto snapshot = entity.findLatestSnapshotBeforeOrAt(timestamp);
216
217 if (snapshot == nullptr)
218 {
219 return;
220 }
221
222 if (not snapshot->hasInstance(0))
223 {
224 return;
225 }
226
227 const armem::wm::EntityInstance& item = snapshot->getInstance(0);
228 const auto tf = _convertEntityToTransform(item);
229
230 const auto& dataTs = tf.header.timestamp;
231
232 timeSinceEpochUs =
233 std::max(timeSinceEpochUs.value_or(0), dataTs.toMicroSecondsSinceEpoch());
234 });
235
236 if (not timeSinceEpochUs.has_value())
237 {
238 return std::nullopt;
239 }
240
241 // then we ensure that the timestamp is not more recent than the query timestamp
242 timeSinceEpochUs = std::min(timeSinceEpochUs.value(), timestamp.toMicroSecondsSinceEpoch());
243
244 return armarx::core::time::DateTime(
245 armarx::core::time::Duration::MicroSeconds(timeSinceEpochUs.value()));
246 }
247
248 template <class... Args>
249 std::vector<Eigen::Isometry3f>
250 TransformHelper::_obtainTransforms(
251 const armem::base::CoreSegmentBase<Args...>& localizationCoreSegment,
252 const std::vector<std::string>& tfChain,
253 const std::string& agent,
254 const armem::Time& timestamp)
255 {
256 const auto& agentProviderSegment = localizationCoreSegment.getProviderSegment(agent);
257
258 ARMARX_DEBUG << "Provider segments" << localizationCoreSegment.getProviderSegmentNames();
259 ARMARX_DEBUG << "Entities: " << agentProviderSegment.getEntityNames();
260
261 try
262 {
263 std::vector<Eigen::Isometry3f> transforms;
264 transforms.reserve(tfChain.size());
265 std::transform(tfChain.begin(),
266 tfChain.end(),
267 std::back_inserter(transforms),
268 [&](const std::string& entityName) {
269 return _obtainTransform(entityName, agentProviderSegment, timestamp);
270 });
271 return transforms;
272 }
273 catch (const armem::error::MissingEntry& missingEntryError)
274 {
275 ARMARX_VERBOSE << missingEntryError.what();
276 }
277 catch (const ::armarx::exceptions::local::ExpressionException& ex)
278 {
279 ARMARX_VERBOSE << "Local expression exception: " << ex.what();
280 }
281 catch (const ::armarx::LocalException& ex)
282 {
283 ARMARX_VERBOSE << "Local exception: " << ex.what();
284 }
285 catch (...)
286 {
287 ARMARX_VERBOSE << "Unexpected error: " << GetHandledExceptionString();
288 }
289
290 return {};
291 }
292
293 template <class... Args>
294 Eigen::Isometry3f
295 TransformHelper::_obtainTransform(
296 const std::string& entityName,
297 const armem::base::ProviderSegmentBase<Args...>& agentProviderSegment,
298 const armem::Time& timestamp)
299 {
300 // ARMARX_DEBUG << "getEntity:" + entityName;
301 const auto& entity = agentProviderSegment.getEntity(entityName);
302
303 // ARMARX_DEBUG << "History (size: " << entity.size() << "): " << entity.getTimestamps();
304
305 // if (entity.history.empty())
306 // {
307 // // TODO(fabian.reister): fixme boom
308 // ARMARX_ERROR << "No snapshots received.";
309 // return Eigen::Isometry3f::Identity();
310 // }
311
312 std::vector<::armarx::armem::robot_state::localization::Transform> transforms;
313
314 auto snapshot = entity.findLatestSnapshotBeforeOrAt(timestamp);
315 ARMARX_CHECK(snapshot) << "No snapshot found before or at time " << timestamp;
316 // The snapshot may exist but be empty (e.g. created before any
317 // instance was committed, or all instances stripped by a
318 // truncation pass). getInstance(0) would throw NoSuchInstance
319 // and propagate uncaught into the localization hot path. Mirror
320 // the hasInstance(0) guard already used in _obtainTimestamp at
321 // line 205.
322 ARMARX_CHECK(snapshot->hasInstance(0))
323 << "Snapshot at time " << snapshot->time() << " for entity '" << entityName
324 << "' has no instances";
325 transforms.push_back(_convertEntityToTransform(snapshot->getInstance(0)));
326
327 // ARMARX_DEBUG << "obtaining transform";
328 if (transforms.size() > 1)
329 {
330 // TODO(fabian.reister): remove
331 return transforms.front().transform;
332
333 // ARMARX_DEBUG << "More than one snapshots received: " << transforms.size();
334 const auto p = _interpolateTransform(transforms, timestamp);
335 // ARMARX_DEBUG << "Done interpolating transform";
336 return p;
337 }
338
339 // accept this to fail (will raise armem::error::MissingEntry)
340 if (transforms.empty())
341 {
342 // ARMARX_DEBUG << "empty transform";
343
344 throw armem::error::MissingEntry("foo", "bar", "foo2", "bar2", 0);
345 }
346
347 // ARMARX_DEBUG << "single transform";
348
349 return transforms.front().transform;
350 }
351
352 ::armarx::armem::robot_state::localization::Transform
353 TransformHelper::_convertEntityToTransform(const armem::wm::EntityInstance& item)
354 {
355 arondto::Transform aronTransform;
356 aronTransform.fromAron(item.data());
357
358 ::armarx::armem::robot_state::localization::Transform transform;
359 fromAron(aronTransform, transform);
360
361 return transform;
362 }
363
364 auto
366 const std::vector<::armarx::armem::robot_state::localization::Transform>& transforms,
367 const armem::Time& timestamp)
368 {
369 const auto comp = [](const armem::Time& timestamp, const auto& transform)
370 { return transform.header.timestamp < timestamp; };
371
372 const auto it = std::upper_bound(transforms.begin(), transforms.end(), timestamp, comp);
373
374 auto timestampBeyond = [timestamp](const localization::Transform& transform)
375 { return transform.header.timestamp > timestamp; };
376
377 const auto poseNextIt = std::find_if(transforms.begin(), transforms.end(), timestampBeyond);
378
379 ARMARX_CHECK(it == poseNextIt);
380
381 return poseNextIt;
382 }
383
384 Eigen::Isometry3f
385 TransformHelper::_interpolateTransform(
386 const std::vector<::armarx::armem::robot_state::localization::Transform>& queue,
388 {
390
391 ARMARX_DEBUG << "Entering";
392
393 ARMARX_CHECK(not queue.empty())
394 << "The queue has to contain at least two items to perform a lookup";
395
396 ARMARX_DEBUG << "Entering ... "
397 << "Q front " << queue.front().header.timestamp << " "
398 << "Q back " << queue.back().header.timestamp << " "
399 << "query timestamp " << timestamp;
400
401 // TODO(fabian.reister): sort queue.
402
403 ARMARX_CHECK(queue.back().header.timestamp > timestamp)
404 << "Cannot perform lookup into the future!";
405
406 // ARMARX_DEBUG << "Entering 1.5 " << queue.front().timestamp << " " << timestamp;
407 ARMARX_CHECK(queue.front().header.timestamp < timestamp)
408 << "Cannot perform lookup. Timestamp too old";
409 // => now we know that there is an element right after and before the timestamp within our queue
410
411 ARMARX_DEBUG << "Entering 2";
412
413 const auto poseNextIt = findFirstElementAfter(queue, timestamp);
414
415 ARMARX_DEBUG << "it ari";
416
417 const auto posePreIt = poseNextIt - 1;
418
419 ARMARX_DEBUG << "deref";
420
421 // the time fraction [0..1] of the lookup wrt to posePre and poseNext
422 const double t =
423 (timestamp - posePreIt->header.timestamp).toMicroSecondsDouble() /
424 (poseNextIt->header.timestamp - posePreIt->header.timestamp).toMicroSecondsDouble();
425
426 ARMARX_DEBUG << "interpolate";
427
428 return simox::math::interpolatePose(
429 posePreIt->transform, poseNextIt->transform, static_cast<float>(t));
430 }
431
432 TransformResult
434 const TransformQuery& query)
435 {
436 return _lookupTransform(localizationCoreSegment, query);
437 }
438
441 const TransformQuery& query)
442 {
443 return _lookupTransform(localizationCoreSegment, query);
444 }
445
448 const TransformQuery& query)
449 {
450 return _lookupTransformChain(localizationCoreSegment, query);
451 }
452
455 const armem::server::wm::CoreSegment& localizationCoreSegment,
456 const TransformQuery& query)
457 {
458 return _lookupTransformChain(localizationCoreSegment, query);
459 }
460
461
462} // namespace armarx::armem::robot_state::localization
std::string timestamp()
SpamFilterDataPtr deactivateSpam(SpamFilterDataPtr const &spamFilter, float deactivationDurationSec, const std::string &identifier, bool deactivate)
Definition Logging.cpp:75
#define VAROUT(x)
EntityT & getEntity(const std::string &name)
static TransformResult lookupTransform(const armem::wm::CoreSegment &localizationCoreSegment, const TransformQuery &query)
static TransformChainResult lookupTransformChain(const armem::wm::CoreSegment &localizationCoreSegment, const TransformQuery &query)
Client-side working memory core segment.
static Duration MicroSeconds(std::int64_t microSeconds)
Constructs a duration in microseconds.
Definition Duration.cpp:24
#define ARMARX_CHECK(expression)
Shortcut for ARMARX_CHECK_EXPRESSION.
#define ARMARX_CHECK_EQUAL(lhs, rhs)
This macro evaluates whether lhs is equal (==) rhs and if it turns out to be false it will throw an E...
#define ARMARX_INFO
The normal logging level.
Definition Logging.h:179
#define ARMARX_DEBUG
The logging level for output that is only interesting while debugging.
Definition Logging.h:182
#define ARMARX_WARNING
The logging level for unexpected behaviour, but not a serious problem.
Definition Logging.h:191
#define ARMARX_VERBOSE
The logging level for verbose information.
Definition Logging.h:185
const std::string localizationCoreSegment
Definition constants.h:29
auto findFirstElementAfter(const std::vector<::armarx::armem::robot_state::localization::Transform > &transforms, const armem::Time &timestamp)
void fromAron(const arondto::Transform &dto, Transform &bo)
armarx::core::time::DateTime Time
std::string GetHandledExceptionString()
auto transform(const Container< InputT, Alloc > &in, OutputT(*func)(InputT const &)) -> Container< OutputT, typename std::allocator_traits< Alloc >::template rebind_alloc< OutputT > >
Convenience function (with less typing) to transform a container of type InputT into the same contain...
Definition algorithm.h:351
#define ARMARX_TRACE
Definition trace.h:75