Diagnostics.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 * @author Fabian Reister ( fabian dot reister at kit dot edu )
17 * @date 2026
18 * @copyright http://www.gnu.org/licenses/gpl-2.0.txt
19 * GNU General Public License
20 */
21
22#include "Diagnostics.h"
23
24#include <cmath>
25#include <filesystem>
26#include <fstream>
27#include <iomanip>
28#include <ostream>
29#include <sstream>
30
31#include <SimoxUtility/json/json.hpp>
32
35
37
39{
40
41 namespace
42 {
43
44 float
45 yawOf(const core::Pose& pose)
46 {
47 return std::atan2(pose.linear()(1, 0), pose.linear()(0, 0));
48 }
49
50 /// The point schema `server/ParametrizationDump.cpp` writes, so the same reader takes both.
51 nlohmann::json
53 {
54 nlohmann::json points = nlohmann::json::array();
55
56 for (const core::GlobalTrajectoryPoint& point : trajectory.points())
57 {
58 const core::Position& position = point.waypoint.pose.translation();
59
60 points.push_back(nlohmann::json{{"x", position.x()},
61 {"y", position.y()},
62 {"yaw", yawOf(point.waypoint.pose)},
63 {"velocity", point.velocity}});
64 }
65
66 if (trajectory.points().empty())
67 {
68 return nlohmann::json{{"points", points}, {"length", 0.F}, {"duration", 0.F}};
69 }
70
71 return nlohmann::json{
72 {"points", points},
73 {"length", trajectory.length()},
74 {"duration",
76 }
77
78 nlohmann::json
80 {
81 // Same key names as `controller_config/GlobalTrajectory/*.json`, so a dump can be
82 // compared against the file it came from without a translation step.
83 return nlohmann::json{
84 {"pidPos",
85 {{"Kp", params.pidPos.Kp}, {"Ki", params.pidPos.Ki}, {"Kd", params.pidPos.Kd}}},
86 {"pidOri",
87 {{"Kp", params.pidOri.Kp}, {"Ki", params.pidOri.Ki}, {"Kd", params.pidOri.Kd}}},
88 {"limits", {{"linear", params.limits.linear}, {"angular", params.limits.angular}}},
89 {"velocityFactor", params.velocityFactor},
90 {"alpha", params.alpha},
91 {"orientationWeight", params.orientationWeight},
92 {"maxSegmentsAhead", params.maxSegmentsAhead},
93 {"lookaheadDistance", params.lookaheadDistance},
94 {"angularFeedforwardFraction", params.angularFeedforwardFraction},
95 {"minFeedforwardVelocityFraction", params.minFeedforwardVelocityFraction},
96 {"coupleLinearAndAngularLimits", params.coupleLinearAndAngularLimits},
97 {"enableAngularFeedforward", params.enableAngularFeedforward},
98 {"maxLinearAcceleration", params.maxLinearAcceleration},
99 {"maxAngularAcceleration", params.maxAngularAcceleration}};
100 }
101
102 /// Seconds spanned by a sample range, 0 for fewer than two samples.
103 template <class Sample>
104 double
105 span(const std::vector<Sample>& samples)
106 {
107 if (samples.size() < 2)
108 {
109 return 0.0;
110 }
111
112 return static_cast<double>(samples.back().timestampUs - samples.front().timestampUs) *
113 1e-6;
114 }
115
116 void
117 writeRtCsv(const std::filesystem::path& file, const std::vector<RtSample>& samples)
118 {
119 std::ofstream stream{file};
120 if (not stream.good())
121 {
122 ARMARX_WARNING << "Cannot write " << file;
123 return;
124 }
125
126 stream << "t_us,episode,seq,x,y,yaw,v_meas_x,v_meas_y,w_meas,v_tgt_x,v_tgt_y,w_tgt,"
127 "v_cmd_x,v_cmd_y,w_cmd,dt,stale,slew_limited\n";
128
129 stream << std::setprecision(9);
130
131 for (const RtSample& s : samples)
132 {
133 stream << s.timestampUs << ',' << s.episode << ',' << s.sequence << ',' << s.x
134 << ',' << s.y << ',' << s.yaw << ',' << s.vMeasX << ',' << s.vMeasY << ','
135 << s.wMeas << ',' << s.vTgtX << ',' << s.vTgtY << ',' << s.wTgt << ','
136 << s.vCmdX << ',' << s.vCmdY << ',' << s.wCmd << ',' << s.dt << ','
137 << (s.stale ? 1 : 0) << ',' << (s.slewLimited ? 1 : 0) << '\n';
138 }
139 }
140
141 void
142 writeControlCsv(const std::filesystem::path& file,
143 const std::vector<ControlSample>& samples)
144 {
145 std::ofstream stream{file};
146 if (not stream.good())
147 {
148 ARMARX_WARNING << "Cannot write " << file;
149 return;
150 }
151
152 stream << "t_us,episode,seq,traj_rev,x,y,yaw,ref_x,ref_y,ref_yaw,ref_v,proj_idx,"
153 "final_seg,pos_err,ori_err,cur_ori,des_ori,ff_ang,capped_ff_v,ff_sat,guard,"
154 "v_raw_x,v_raw_y,w_raw,v_filt_x,v_filt_y,w_filt,"
155 "limit_linear,limit_angular,velocity_factor\n";
156
157 stream << std::setprecision(9);
158
159 for (const ControlSample& s : samples)
160 {
161 stream << s.timestampUs << ',' << s.episode << ',' << s.sequence << ','
162 << s.trajectoryRevision << ',' << s.x << ',' << s.y << ',' << s.yaw << ','
163 << s.refX << ',' << s.refY << ',' << s.refYaw << ',' << s.refVelocity << ','
164 << s.projectionIndex << ',' << (s.finalSegment ? 1 : 0) << ','
165 << s.positionError << ',' << s.orientationError << ','
166 << s.currentOrientation << ',' << s.desiredOrientation << ',' << s.ffAngular
167 << ',' << s.cappedFfVelocity << ',' << (s.ffSaturated ? 1 : 0) << ','
168 << s.guard << ',' << s.vRawX << ',' << s.vRawY << ',' << s.wRaw << ','
169 << s.vFiltX << ',' << s.vFiltY << ',' << s.wFilt << ',' << s.limitLinear
170 << ',' << s.limitAngular << ',' << s.velocityFactor << '\n';
171 }
172 }
173
174 } // namespace
175
176 std::string
177 writeDump(const Episode& episode, const Params& params)
178 try
179 {
180 std::ostringstream name;
181 name << armarx::core::time::DateTime::Now().toDateTimeString() << "-ep" << std::setw(3)
182 << std::setfill('0') << episode.episode;
183
184 const std::filesystem::path directory =
185 std::filesystem::path{params.outputDirectory} / name.str();
186
187 std::filesystem::create_directories(directory);
188
189 nlohmann::json meta;
191 meta["episode"] = episode.episode;
192 meta["controller"] = episode.controllerInstance;
193 meta["platform"] = episode.platform;
194 meta["params"] = toJson(episode.params);
195
196 meta["rt"] = nlohmann::json{{"recorded", episode.rtSamples.size()},
197 {"truncated", episode.rtTruncated},
198 {"overwritten_lifetime", episode.rtOverwritten},
199 {"duration", span(episode.rtSamples)}};
200 meta["control"] = nlohmann::json{{"recorded", episode.controlSamples.size()},
201 {"truncated", episode.controlTruncated},
202 {"overwritten_lifetime", episode.controlOverwritten},
203 {"duration", span(episode.controlSamples)}};
204
205 meta["simulation"] = episode.simulation;
206 meta["slew_dt_bound"] = episode.slewDtBound;
207 meta["control_task_exceptions"] = episode.controlTaskExceptions;
208 meta["control_target_stale_cycles"] = episode.controlTargetStaleCycles;
209
210 {
211 std::ofstream stream{directory / "meta.json"};
212 if (not stream.good())
213 {
214 ARMARX_WARNING << "Cannot write " << (directory / "meta.json");
215 return {};
216 }
217 stream << meta.dump(2) << std::endl;
218 }
219
220 {
221 nlohmann::json revisions = nlohmann::json::array();
222
223 for (const TrajectoryRevision& revision : episode.trajectories)
224 {
225 nlohmann::json entry = toJson(revision.trajectory);
226 entry["revision"] = revision.revision;
227 revisions.push_back(entry);
228 }
229
230 const nlohmann::json trajectories{
231 {"revisions", revisions},
232 {"dropped_revisions", episode.trajectoryRevisionsDropped}};
233
234 std::ofstream stream{directory / "trajectory.json"};
235 if (not stream.good())
236 {
237 ARMARX_WARNING << "Cannot write " << (directory / "trajectory.json");
238 return {};
239 }
240 stream << trajectories.dump(2) << std::endl;
241 }
242
243 writeRtCsv(directory / "rt.csv", episode.rtSamples);
244 writeControlCsv(directory / "control.csv", episode.controlSamples);
245
246 ARMARX_INFO << "Wrote the execution diagnostics of episode " << episode.episode << " to "
247 << directory << " (" << episode.rtSamples.size() << " rt samples, "
248 << episode.controlSamples.size() << " control samples, "
249 << episode.trajectories.size() << " trajectory revisions).";
250
251 return directory.string();
252 }
253 catch (const std::exception& e)
254 {
255 // A debugging aid must never be able to disturb the controller that produced it.
256 ARMARX_WARNING << "Could not write the execution diagnostics: " << e.what();
257 return {};
258 }
259
260} // namespace armarx::navigation::platform_controller::diagnostics
static DateTime Now()
Definition DateTime.cpp:51
std::string toDateTimeString() const
Definition DateTime.cpp:75
#define ARMARX_INFO
The normal logging level.
Definition Logging.h:179
#define ARMARX_WARNING
The logging level for unexpected behaviour, but not a serious problem.
Definition Logging.h:191
Eigen::Isometry3f Pose
Definition basic_types.h:31
Eigen::Vector3f Position
Definition basic_types.h:36
std::string writeDump(const Episode &episode, const Params &params)
Write episode as <outputDirectory>/<DateTime>-ep<NNN>/.
One cycle of the 100 Hz control task, i.e. one TrajectoryFollowingController::control.
Everything one dump is written from. Assembled off the real-time thread.
std::uint64_t rtOverwritten
Lifetime overwrite counts, for context only. See Ring::overwritten.
traj_ctrl::global::TrajectoryFollowingControllerParams params
float slewDtBound
The dt bound the real-time slew was actually using [s].
bool rtTruncated
Whether the ring overwrote samples of this episode before the dump read them.
bool simulation
Whether this ran against a simulated RobotUnit.
What the diagnostics mode records and where it writes it.
Definition Diagnostics.h:46
One trajectory the episode executed, with the revision the samples refer to.
float maxLinearAcceleration
Rate at which the RT thread walks the commanded twist towards this controller's output.
float minFeedforwardVelocityFraction
Floor on the capped linear feed-forward, as a fraction of the requested velocity.
float angularFeedforwardFraction
Fraction of limits.angular the angular feed-forward may use. 1 lets it saturate.
float lookaheadDistance
Lookahead as a distance [mm]. 0 keeps the pure maxSegmentsAhead behaviour.