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