2#include "lemlib/util.hpp"
3#include "pros/motors.h"
13LTVPathFollower::Vector2::Vector2(
float x,
float y) : x(x), y(y) {}
15std::string LTVPathFollower::Vector2::latex()
const {
16 std::ostringstream oss;
17 oss <<
"\\left(" << std::fixed << std::setprecision(3) << this->x <<
"," << this->y <<
"\\right)";
21double LTVPathFollower::angleError(
double robotAngle,
double targetAngle) {
22 return std::remainder(targetAngle - robotAngle, 2.0 * M_PI);
25double LTVPathFollower::clamp(
double value,
double min,
double max) {
26 if (value < min)
return min;
27 if (value > max)
return max;
31LTVPathFollower::LTVPathFollower(
const VelocityControllerConfig&
config)
44void LTVPathFollower::followPath(
const std::string& path_name,
const ltvConfig& l_config) {
51 cancel_request =
false;
52 distance_traveled_inches = 0.0f;
54 TaskParams* params =
new TaskParams{
this, path_name, l_config, {}};
55 task =
new pros::Task(task_trampoline, params,
"LTVTask");
57 if (task ==
nullptr) {
60 std::cout <<
"[LTV] Failed to start task!" << std::endl;
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);
75 return length_meters * METER_TO_INCH;
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);
86void LTVPathFollower::waitUntilDone() {
92void LTVPathFollower::waitUntil(
float dist_inches) {
93 while (is_running && distance_traveled_inches < dist_inches) {
98void LTVPathFollower::waitUntil(
float x_inch,
float y_inch,
float radius_inch) {
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) {
109void LTVPathFollower::cancel() {
110 cancel_request =
true;
113bool LTVPathFollower::isRunning() {
117void LTVPathFollower::followPathImpl(
const std::string& path_name,
const ltvConfig& l_config,
const std::vector<State>& dynamic_path) {
118 std::vector<State> trajectory;
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);
127 if (trajectory.empty() && !path_name.empty()) {
128 trajectory = prepare_trajectory(path_name);
131 if (trajectory.empty()) {
132 std::cout <<
"[LTV] Error: Empty trajectory." << std::endl;
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;
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);
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();
153 double sum_lat_error = 0;
154 double sum_head_error = 0;
155 double sum_oscillation = 0;
156 float prev_w_cmd = 0;
159 lemlib::Pose start_pose =
chassis.getPose();
161 Eigen::MatrixXf cached_K(2, 3);
163 float last_solve_v = -9999.0f;
164 float last_solve_w = -9999.0f;
165 bool dare_solved_once =
false;
167 constexpr double FIXED_DT = 0.01;
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;
175 for (
int i = 0; i < trajectory_size; ++i) {
176 if (cancel_request)
break;
178 u_int32_t current_time = pros::millis();
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;
184 const auto &target_state = trajectory[i];
185 lemlib::Pose current_pose =
chassis.getPose(
true);
187 distance_traveled_inches = start_pose.distance(current_pose);
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;
200 if (end_phase > 1.0) end_phase = 1.0;
202 double ramp = 1.0 - end_phase;
203 velocity_scale *= ramp;
205 r_vel_mult = 1.0 + (1.0 * end_phase);
206 q_gain_mult = 1.0 + (2.0 * end_phase);
209 if (std::abs(target_state.angular_vel) > 0.5) {
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;
218 Eigen::Matrix2f R_mat;
219 R_mat << active_r_vel * r_vel_mult, 0,
222 double math_theta = M_PI_2 - current_pose.theta;
224 double effective_theta = l_config.backwards ? math_theta + M_PI : math_theta;
226 double target_heading = target_state.heading;
228 double errorTheta = angleError(effective_theta, target_heading);
230 Eigen::Vector3d global_error;
231 global_error << target_state.x - current_pose.x, target_state.y - current_pose.y, errorTheta;
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,
237 Eigen::Vector3d error = rotation_matrix * global_error;
239 float v_ref = std::abs(target_state.linear_vel) * velocity_scale;
241 float w_ref = target_state.angular_vel * velocity_scale;
243 float lateral_coupling = std::clamp(v_ref, -1.0f, 1.0f);
245 constexpr float eps = -1e-3f;
248 -w_ref, eps, lateral_coupling,
251 Eigen::Matrix<float, 3, 2> B;
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) {
260 auto discAB = discretizeAB(A, B, measured_dt);
261 Eigen::MatrixXf X = dareSolver(discAB.first, discAB.second, Q_mat, R_mat);
263 cached_K = (R_mat + discAB.second.transpose() * X * discAB.second).inverse() * discAB.second.transpose() * X * discAB.first;
265 last_solve_v = v_ref;
266 last_solve_w = w_ref;
267 dare_solved_once =
true;
270 Eigen::Vector2f u = cached_K * error.cast<
float>();
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);
275 float v_cmd = v_ref + u_v;
276 float w_cmd = w_ref + u_w;
279 if(l_config.backwards) {
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);
288 sum_lat_error += lat_err;
289 sum_head_error += head_err;
290 sum_oscillation += instant_oscillation;
297 DrivetrainVoltages output_voltages =
controller.update(
298 v_cmd, w_cmd, left_actual_mps, right_actual_mps
301 output_voltages.rightVoltage = clamp(output_voltages.rightVoltage, -12.0, 12.0);
302 output_voltages.leftVoltage = clamp(output_voltages.leftVoltage, -12.0, 12.0);
304 rightMotors.move_voltage(output_voltages.rightVoltage * 1000.0);
305 leftMotors.move_voltage(output_voltages.leftVoltage * 1000.0);
308 std::ostringstream ss;
309 ss << Vector2(current_pose.x, current_pose.y).latex() <<
",";
310 logs.push_back(ss.str());
313 pros::Task::delay_until(¤t_time, 10);
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;
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;
331 std::cout <<
"\n--- COORDINATE LOG START ---" << std::endl;
332 for (
const auto& line : logs) {
336 std::cout <<
"\n--- COORDINATE LOG END ---" << std::endl;
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;
348 for (
int i = 0; i < 80; ++i) {
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) {
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);
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};
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");
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));
394std::vector<std::pair<double,double>> LTVPathFollower::parse_pairs(
const std::string& line) {
395 std::vector<std::pair<double,double>> result;
397 bool inside_parens =
false;
398 for (
char c : line) {
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) {
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;
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(
'[')));
427 size_t n = std::min({X.size(), L.size(), A.size()});
428 if (n == 0)
return {};
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;
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) {
445 states[i].heading = std::atan2(states[next_idx].y - states[i].y, states[next_idx].x - states[i].x);
447 states[i].heading = states[i - 1].heading;
449 states[i].heading = 0.0;
const VelocityControllerConfig config
pros::MotorGroup rightMotors
pros::MotorGroup leftMotors
pros::Controller controller
static constexpr float INCH_TO_METER
static constexpr float rpm_to_mps_factor