1516X Push Back 1.0
1516X's robot code for the 2025-2026 VEX Robotics Competition
Loading...
Searching...
No Matches
ltv.cpp
Go to the documentation of this file.
1#include "ltv.h"
2#include "lemlib/util.hpp"
3#include "pros/motors.h"
4#include <cmath>
5#include <sys/types.h>
6#include <vector>
7#include <string>
8#include <sstream>
9#include <iostream>
10#include <algorithm>
11#include <iomanip>
12
13LTVPathFollower::Vector2::Vector2(float x, float y) : x(x), y(y) {}
14
15std::string LTVPathFollower::Vector2::latex() const {
16 std::ostringstream oss;
17 oss << "\\left(" << std::fixed << std::setprecision(3) << this->x << "," << this->y << "\\right)";
18 return oss.str();
19}
20
21double LTVPathFollower::angleError(double robotAngle, double targetAngle) {
22 return std::remainder(targetAngle - robotAngle, 2.0 * M_PI);
23}
24
25double LTVPathFollower::clamp(double value, double min, double max) {
26 if (value < min) return min;
27 if (value > max) return max;
28 return value;
29}
30
31LTVPathFollower::LTVPathFollower(const VelocityControllerConfig& config)
32 : controller(
33 config.kV,
34 config.KA_straight,
35 config.KA_turn,
36 config.KS_straight,
37 config.KS_turn,
38 11.4953431776,
39 54.5797495382,
40 99999.0,
41 11.55f * INCH_TO_METER
42 ) {}
43
44void LTVPathFollower::followPath(const std::string& path_name, const ltvConfig& l_config) {
45 if (is_running) {
46 cancel();
47 waitUntilDone();
48 }
49
50 is_running = true;
51 cancel_request = false;
52 distance_traveled_inches = 0.0f;
53
54 TaskParams* params = new TaskParams{this, path_name, l_config, {}};
55 task = new pros::Task(task_trampoline, params, "LTVTask");
56
57 if (task == nullptr) {
58 delete params;
59 is_running = false;
60 std::cout << "[LTV] Failed to start task!" << std::endl;
61 return;
62 }
63 pros::delay(10);
64}
65
66double LTVPathFollower::getPathLength(const std::string& path_name) {
67 std::vector<State> trajectory = prepare_trajectory(path_name);
68 if (trajectory.empty()) return 0.0;
69 double length_meters = 0.0;
70 for (size_t i = 1; i < trajectory.size(); ++i) {
71 double dx = trajectory[i].x - trajectory[i-1].x;
72 double dy = trajectory[i].y - trajectory[i-1].y;
73 length_meters += std::sqrt(dx*dx + dy*dy);
74 }
75 return length_meters * METER_TO_INCH;
76}
77
78void LTVPathFollower::task_trampoline(void* params) {
79 TaskParams* p = static_cast<TaskParams*>(params);
80 if (p && p->instance) {
81 p->instance->followPathImpl(p->path_name, p->config, p->dynamic_path);
82 }
83 delete p;
84}
85
86void LTVPathFollower::waitUntilDone() {
87 while (is_running) {
88 pros::delay(10);
89 }
90}
91
92void LTVPathFollower::waitUntil(float dist_inches) {
93 while (is_running && distance_traveled_inches < dist_inches) {
94 pros::delay(10);
95 }
96}
97
98void LTVPathFollower::waitUntil(float x_inch, float y_inch, float radius_inch) {
99 while (is_running) {
100 lemlib::Pose p = chassis.getPose();
101 float dist = std::sqrt(std::pow(p.x - x_inch, 2) + std::pow(p.y - y_inch, 2));
102 if (dist < radius_inch) {
103 break;
104 }
105 pros::delay(10);
106 }
107}
108
109void LTVPathFollower::cancel() {
110 cancel_request = true;
111}
112
113bool LTVPathFollower::isRunning() {
114 return is_running;
115}
116
117void LTVPathFollower::followPathImpl(const std::string& path_name, const ltvConfig& l_config, const std::vector<State>& dynamic_path) {
118 std::vector<State> trajectory;
119
120 if (!dynamic_path.empty()) {
121 trajectory = dynamic_path;
122 } else if(l_config.path_index >= 0 && (size_t)(l_config.path_index) < precomputed_paths.size()) {
123 if (!precomputed_paths.at(l_config.path_index).empty()) {
124 trajectory = precomputed_paths.at(l_config.path_index);
125 }
126 }
127 if (trajectory.empty() && !path_name.empty()) {
128 trajectory = prepare_trajectory(path_name);
129 }
130
131 if (trajectory.empty()) {
132 std::cout << "[LTV] Error: Empty trajectory." << std::endl;
133 is_running = false;
134 return;
135 }
136
137 if(l_config.test) {
138 double start_theta = l_config.backwards ? trajectory[0].heading + M_PI : trajectory[0].heading;
139 double gps_start_theta = M_PI_2 - start_theta;
140 chassis.setPose(trajectory[0].x / INCH_TO_METER, trajectory[0].y / INCH_TO_METER, lemlib::radToDeg(gps_start_theta));
141 } else if(l_config.turnFirst) {
142 double start_theta = l_config.backwards ? trajectory[0].heading + M_PI : trajectory[0].heading;
143 double gps_target = lemlib::radToDeg(M_PI_2 - start_theta);
144 chassis.turnToHeading(gps_target, 1000);
145 }
146
147 std::vector<std::string> logs;
148 std::vector<std::string> logs_2;
149 int trajectory_size = trajectory.size();
150 u_int32_t start_time = pros::millis();
151 uint32_t prev_time = pros::millis();
152
153 double sum_lat_error = 0;
154 double sum_head_error = 0;
155 double sum_oscillation = 0;
156 float prev_w_cmd = 0;
157 int steps = 0;
158
159 lemlib::Pose start_pose = chassis.getPose();
160
161 Eigen::MatrixXf cached_K(2, 3);
162 cached_K.setZero();
163 float last_solve_v = -9999.0f;
164 float last_solve_w = -9999.0f;
165 bool dare_solved_once = false;
166
167 constexpr double FIXED_DT = 0.01;
168
169 float active_q_x = l_config.backwards ? l_config.q_x_backward : l_config.q_x_forward;
170 float active_q_y = l_config.backwards ? l_config.q_y_backward : l_config.q_y_forward;
171 float active_q_theta = l_config.backwards ? l_config.q_theta_backward : l_config.q_theta_forward;
172 float active_r_vel = l_config.backwards ? l_config.r_vel_backward : l_config.r_vel_forward;
173 float active_r_ang = l_config.backwards ? l_config.r_ang_backward : l_config.r_ang_forward;
174
175 for (int i = 0; i < trajectory_size; ++i) {
176 if (cancel_request) break;
177
178 u_int32_t current_time = pros::millis();
179
180 double measured_dt = (current_time - prev_time) / 1000.0;
181 if (measured_dt <= 0.002) measured_dt = 0.01;
182 prev_time = current_time;
183
184 const auto &target_state = trajectory[i];
185 lemlib::Pose current_pose = chassis.getPose(true);
186
187 distance_traveled_inches = start_pose.distance(current_pose);
188
189 current_pose.x *= INCH_TO_METER;
190 current_pose.y *= INCH_TO_METER;
191
192 double progress = (double)i / trajectory_size;
193 double velocity_scale = 1.0;
194 double q_gain_mult = 1.0;
195 double r_vel_mult = 1.0;
196 double q_x_boost = 1.0;
197 if (progress > 0.85) {
198 double end_phase = (progress - 0.85) / 0.15;
199
200 if (end_phase > 1.0) end_phase = 1.0;
201
202 double ramp = 1.0 - end_phase;
203 velocity_scale *= ramp;
204
205 r_vel_mult = 1.0 + (1.0 * end_phase);
206 q_gain_mult = 1.0 + (2.0 * end_phase);
207 }
208
209 if (std::abs(target_state.angular_vel) > 0.5) {
210 q_x_boost = 2;
211 }
212
213 Eigen::Matrix3f Q_mat;
214 Q_mat << active_q_x * q_gain_mult * q_x_boost, 0, 0,
215 0, active_q_y * q_gain_mult, 0,
216 0, 0, active_q_theta * q_gain_mult;
217
218 Eigen::Matrix2f R_mat;
219 R_mat << active_r_vel * r_vel_mult, 0,
220 0, active_r_ang;
221
222 double math_theta = M_PI_2 - current_pose.theta;
223
224 double effective_theta = l_config.backwards ? math_theta + M_PI : math_theta;
225
226 double target_heading = target_state.heading;
227
228 double errorTheta = angleError(effective_theta, target_heading);
229
230 Eigen::Vector3d global_error;
231 global_error << target_state.x - current_pose.x, target_state.y - current_pose.y, errorTheta;
232
233 Eigen::Matrix3d rotation_matrix;
234 rotation_matrix << std::cos(effective_theta), std::sin(effective_theta), 0,
235 -std::sin(effective_theta), std::cos(effective_theta), 0,
236 0, 0, 1;
237 Eigen::Vector3d error = rotation_matrix * global_error;
238
239 float v_ref = std::abs(target_state.linear_vel) * velocity_scale;
240
241 float w_ref = target_state.angular_vel * velocity_scale;
242
243 float lateral_coupling = std::clamp(v_ref, -1.0f, 1.0f);
244
245 constexpr float eps = -1e-3f;
246 Eigen::Matrix3f A;
247 A << eps, w_ref, 0,
248 -w_ref, eps, lateral_coupling,
249 0, 0, eps;
250
251 Eigen::Matrix<float, 3, 2> B;
252 B << 1, 0,
253 0, 0,
254 0, 1;
255
256 if (!dare_solved_once ||
257 std::abs(v_ref - last_solve_v) > 0.15f ||
258 std::abs(w_ref - last_solve_w) > 0.25f) {
259
260 auto discAB = discretizeAB(A, B, measured_dt);
261 Eigen::MatrixXf X = dareSolver(discAB.first, discAB.second, Q_mat, R_mat);
262
263 cached_K = (R_mat + discAB.second.transpose() * X * discAB.second).inverse() * discAB.second.transpose() * X * discAB.first;
264
265 last_solve_v = v_ref;
266 last_solve_w = w_ref;
267 dare_solved_once = true;
268 }
269
270 Eigen::Vector2f u = cached_K * error.cast<float>();
271
272 float u_v = clamp(u(0), -l_config.max_lin_correction, l_config.max_lin_correction);
273 float u_w = clamp(u(1), -l_config.max_ang_correction, l_config.max_ang_correction);
274
275 float v_cmd = v_ref + u_v;
276 float w_cmd = w_ref + u_w;
277
278
279 if(l_config.backwards) {
280 v_cmd = -v_cmd;
281 }
282
283 double lat_err = std::abs(error(1));
284 double head_err = std::abs(error(2));
285 double instant_oscillation = std::abs(w_cmd - prev_w_cmd);
286 prev_w_cmd = w_cmd;
287
288 sum_lat_error += lat_err;
289 sum_head_error += head_err;
290 sum_oscillation += instant_oscillation;
291 steps++;
292
293 float left_actual_mps = leftMotors.get_actual_velocity() * rpm_to_mps_factor;
294 float right_actual_mps = rightMotors.get_actual_velocity() * rpm_to_mps_factor;
295
296
297 DrivetrainVoltages output_voltages = controller.update(
298 v_cmd, w_cmd, left_actual_mps, right_actual_mps
299 );
300
301 output_voltages.rightVoltage = clamp(output_voltages.rightVoltage, -12.0, 12.0);
302 output_voltages.leftVoltage = clamp(output_voltages.leftVoltage, -12.0, 12.0);
303
304 rightMotors.move_voltage(output_voltages.rightVoltage * 1000.0);
305 leftMotors.move_voltage(output_voltages.leftVoltage * 1000.0);
306
307 if(l_config.log) {
308 std::ostringstream ss;
309 ss << Vector2(current_pose.x, current_pose.y).latex() << ",";
310 logs.push_back(ss.str());
311 }
312
313 pros::Task::delay_until(&current_time, 10);
314 }
315
316 rightMotors.brake();
317 leftMotors.brake();
318
319 if (steps == 0) steps = 1;
320 double avg_lat_error = sum_lat_error / steps;
321 double avg_head_error = sum_head_error / steps;
322 double avg_jerk = sum_oscillation / steps;
323
324 if(l_config.log) {
325 std::cout << "\n--- LTV PERFORMANCE SUMMARY ---" << std::endl;
326 std::cout << "Steps Completed: " << steps << " / " << trajectory_size << std::endl;
327 std::cout << "Avg Lateral Error: " << (avg_lat_error / INCH_TO_METER) << " in" << std::endl;
328 std::cout << "Avg Heading Error: " << lemlib::radToDeg(avg_head_error) << " deg" << std::endl;
329 std::cout << "Avg Control Jerk: " << avg_jerk << std::endl;
330
331 std::cout << "\n--- COORDINATE LOG START ---" << std::endl;
332 for (const auto& line : logs) {
333 std::cout << line;
334 pros::delay(10);
335 }
336 std::cout << "\n--- COORDINATE LOG END ---" << std::endl;
337 }
338
339 is_running = false;
340}
341
342
343Eigen::MatrixXf LTVPathFollower::dareSolver(const Eigen::MatrixXf &A, const Eigen::MatrixXf &B, const Eigen::MatrixXf &Q, const Eigen::MatrixXf &R) {
344 Eigen::MatrixXf X = Q;
345 Eigen::MatrixXf X_prev;
346 Eigen::MatrixXf K;
347
348 for (int i = 0; i < 80; ++i) {
349 X_prev = X;
350 Eigen::MatrixXf R_BXB = R + B.transpose() * X * B;
351 K = R_BXB.inverse() * B.transpose() * X * A;
352 X = A.transpose() * X * (A - B * K) + Q;
353 if ((X - X_prev).norm() < 1e-4) {
354 break;
355 }
356 }
357 return X;
358}
359
360std::pair<Eigen::MatrixXf, Eigen::MatrixXf> LTVPathFollower::discretizeAB(
361 const Eigen::MatrixXf& contA, const Eigen::MatrixXf& contB, double dtSeconds) {
362 if(dtSeconds <= 0.001) dtSeconds = 0.01;
363 int states = contA.rows();
364 int inputs = contB.cols();
365 Eigen::MatrixXf M(states + inputs, states + inputs);
366 M.setZero();
367 M.topLeftCorner(states, states) = contA;
368 M.topRightCorner(states, inputs) = contB;
369 Eigen::MatrixXf Mdt = M * dtSeconds;
370 Eigen::MatrixXf I = Eigen::MatrixXf::Identity(M.rows(), M.cols());
371 Eigen::MatrixXf M2 = Mdt * Mdt;
372 Eigen::MatrixXf phi = I + Mdt + (M2 * 0.5f);
373 Eigen::MatrixXf discA = phi.topLeftCorner(states, states);
374 Eigen::MatrixXf discB = phi.topRightCorner(states, inputs);
375 return {discA, discB};
376}
377
378void LTVPathFollower::precompute_paths(const std::vector<std::string>& path_names) {
379 auto* stored = new std::vector<std::string>(path_names);
380 pros::Task t(precompute_paths_task, stored, "PathCompute");
381}
382
383void LTVPathFollower::precompute_paths_task(void* param) {
384 auto* path_names = static_cast<std::vector<std::string>*>(param);
385 precomputed_paths.clear();
386 precomputed_paths.reserve(path_names->size());
387 for (const auto& name : *path_names) {
388 precomputed_paths.push_back(prepare_trajectory(name));
389 pros::delay(10);
390 }
391 delete path_names;
392}
393
394std::vector<std::pair<double,double>> LTVPathFollower::parse_pairs(const std::string& line) {
395 std::vector<std::pair<double,double>> result;
396 std::string temp;
397 bool inside_parens = false;
398 for (char c : line) {
399 if (c == '(') {
400 temp.clear();
401 inside_parens = true;
402 } else if (c == ')') {
403 std::replace(temp.begin(), temp.end(), ',', ' ');
404 std::istringstream ss(temp);
405 double first, second;
406 ss >> first >> second;
407 result.emplace_back(first, second);
408 inside_parens = false;
409 } else if (inside_parens) {
410 temp += c;
411 }
412 }
413 return result;
414}
415
416std::vector<State> LTVPathFollower::prepare_trajectory(const std::string& data) {
417 std::istringstream ss(data);
418 std::vector<std::pair<double,double>> X, L, A;
419 std::string line;
420 while (std::getline(ss, line)) {
421 if (line.find("X =") != std::string::npos) X = parse_pairs(line.substr(line.find('[')));
422 else if (line.find("L =") != std::string::npos) L = parse_pairs(line.substr(line.find('[')));
423 else if (line.find("A =") != std::string::npos) A = parse_pairs(line.substr(line.find('[')));
424 }
425
426 // FIX 1: Prevent Segfaults by grabbing the smallest array size
427 size_t n = std::min({X.size(), L.size(), A.size()});
428 if (n == 0) return {};
429
430 std::vector<State> states(n);
431 for (size_t i = 0; i < n; i++) {
432 states[i].x = X[i].first;
433 states[i].y = X[i].second;
434 states[i].linear_vel = L[i].second;
435 states[i].angular_vel = A[i].second;
436 }
437
438 for (size_t i = 0; i < n; i++) {
439 size_t next_idx = i + 1;
440 while (next_idx < n && states[next_idx].x == states[i].x && states[next_idx].y == states[i].y) {
441 next_idx++;
442 }
443
444 if (next_idx < n) {
445 states[i].heading = std::atan2(states[next_idx].y - states[i].y, states[next_idx].x - states[i].x);
446 } else if (i > 0) {
447 states[i].heading = states[i - 1].heading;
448 } else {
449 states[i].heading = 0.0;
450 }
451 }
452
453 return states;
454}
const VelocityControllerConfig config
pros::MotorGroup rightMotors
lemlib::Chassis chassis
pros::MotorGroup leftMotors
Definition globals.cpp:22
pros::Controller controller
static constexpr float INCH_TO_METER
static constexpr float rpm_to_mps_factor