9LTVPathFollower::Vector2::Vector2(
float x,
float y) : x(x), y(y) {}
11std::string LTVPathFollower::Vector2::latex()
const {
12 std::ostringstream oss;
13 oss <<
"\\left(" << std::fixed << std::setprecision(3) << this->x <<
"," << this->y <<
"\\right)";
17double LTVPathFollower::angleError(
double robotAngle,
double targetAngle) {
18 return std::remainder(targetAngle - robotAngle, 2.0 * M_PI);
21double LTVPathFollower::clamp(
double value,
double min,
double max) {
22 if (value < min)
return min;
23 if (value > max)
return max;
27LTVPathFollower::LTVPathFollower(
const VelocityControllerConfig&
config)
40void LTVPathFollower::followPath(
const std::string& path_name,
const ltvConfig& l_config) {
47 cancel_request =
false;
48 distance_traveled_inches = 0.0f;
50 TaskParams* params =
new TaskParams{
this, path_name, l_config, {}};
51 task =
new pros::Task(task_trampoline, params,
"LTVTask");
53 if (task ==
nullptr) {
56 std::cout <<
"[LTV] Failed to start task!" << std::endl;
62double LTVPathFollower::getPathLength(
const std::string& path_name) {
63 std::vector<State> trajectory = prepare_trajectory(path_name);
64 if (trajectory.empty())
return 0.0;
65 double length_meters = 0.0;
66 for (
size_t i = 1; i < trajectory.size(); ++i) {
67 double dx = trajectory[i].x - trajectory[i-1].x;
68 double dy = trajectory[i].y - trajectory[i-1].y;
69 length_meters += std::sqrt(dx*dx + dy*dy);
71 return length_meters * METER_TO_INCH;
74void LTVPathFollower::task_trampoline(
void* params) {
75 TaskParams* p =
static_cast<TaskParams*
>(params);
76 if (p && p->instance) {
77 p->instance->followPathImpl(p->path_name, p->config, p->dynamic_path);
82void LTVPathFollower::waitUntilDone() {
88void LTVPathFollower::waitUntil(
float dist_inches) {
89 while (is_running && distance_traveled_inches < dist_inches) {
94void LTVPathFollower::waitUntil(
float x_inch,
float y_inch,
float radius_inch) {
96 lemlib::Pose p =
chassis.getPose();
97 float dist = std::sqrt(std::pow(p.x - x_inch, 2) + std::pow(p.y - y_inch, 2));
98 if (dist < radius_inch) {
105void LTVPathFollower::cancel() {
106 cancel_request =
true;
109bool LTVPathFollower::isRunning() {
113void LTVPathFollower::followPathImpl(
const std::string& path_name,
const ltvConfig& l_config,
const std::vector<State>& dynamic_path) {
114 std::vector<State> trajectory;
116 if (!dynamic_path.empty()) {
117 trajectory = dynamic_path;
120 else if (precomputed_paths.count(path_name) > 0) {
121 if (!precomputed_paths[path_name].empty()) {
122 trajectory = precomputed_paths[path_name];
126 if (trajectory.empty() && !path_name.empty()) {
127 trajectory = prepare_trajectory(path_name);
130 if (trajectory.empty()) {
131 std::cout <<
"[LTV] Error: Empty trajectory." << std::endl;
137 double start_theta = l_config.backwards ? trajectory[0].heading + M_PI : trajectory[0].heading;
138 double gps_start_theta = M_PI_2 - start_theta;
140 }
else if(l_config.turnFirst) {
141 double start_theta = l_config.backwards ? trajectory[0].heading + M_PI : trajectory[0].heading;
142 double gps_target = lemlib::radToDeg(M_PI_2 - start_theta);
143 chassis.turnToHeading(gps_target, 1000);
146 std::vector<std::string> logs;
147 std::vector<std::string> logs_2;
148 int trajectory_size = trajectory.size();
149 u_int32_t start_time = pros::millis();
150 uint32_t prev_time = pros::millis();
152 double sum_lat_error = 0;
153 double sum_head_error = 0;
154 double sum_oscillation = 0;
155 float prev_w_cmd = 0;
158 lemlib::Pose start_pose =
chassis.getPose();
160 Eigen::MatrixXf cached_K(2, 3);
162 float last_solve_v = -9999.0f;
163 float last_solve_w = -9999.0f;
164 bool dare_solved_once =
false;
166 constexpr double FIXED_DT = 0.01;
168 for (
int i = 0; i < trajectory_size; ++i) {
169 if (cancel_request)
break;
171 u_int32_t current_time = pros::millis();
173 double measured_dt = (current_time - prev_time) / 1000.0;
174 if (measured_dt <= 0.002) measured_dt = 0.01;
175 prev_time = current_time;
177 const auto &target_state = trajectory[i];
178 lemlib::Pose current_pose =
chassis.getPose(
true);
180 distance_traveled_inches = start_pose.distance(current_pose);
185 double progress = (double)i / trajectory_size;
186 double velocity_scale = 1.0;
187 double q_gain_mult = 1.0;
188 double r_vel_mult = 1.0;
189 double q_x_boost = 1.0;
191 float q_x_effective = (l_config.backwards) ? l_config.q_x_b : l_config.q_x;
192 float q_y_effective = (l_config.backwards) ? l_config.q_y_b : l_config.q_y;
193 float q_theta_effective = (l_config.backwards) ? l_config.q_theta_b : l_config.q_theta;
194 float r_ang_effective = (l_config.backwards) ? l_config.r_ang_b : l_config.r_ang;
195 float r_vel_effective = (l_config.backwards) ? l_config.r_vel_b : l_config.r_vel;
199 Eigen::Matrix3f Q_mat;
200 Q_mat << q_x_effective * q_gain_mult * q_x_boost * l_config.q_scalar, 0, 0,
201 0, q_y_effective * q_gain_mult * l_config.q_scalar, 0,
202 0, 0, q_theta_effective * q_gain_mult * l_config.q_scalar;
204 Eigen::Matrix2f R_mat;
205 R_mat << r_vel_effective * r_vel_mult, 0,
208 double math_theta = M_PI_2 - current_pose.theta;
210 double effective_theta = l_config.backwards ? math_theta + M_PI : math_theta;
212 double target_heading = target_state.heading;
214 double errorTheta = angleError(effective_theta, target_heading);
216 Eigen::Vector3d global_error;
217 global_error << target_state.x - current_pose.x, target_state.y - current_pose.y, errorTheta;
219 Eigen::Matrix3d rotation_matrix;
220 rotation_matrix << std::cos(effective_theta), std::sin(effective_theta), 0,
221 -std::sin(effective_theta), std::cos(effective_theta), 0,
223 Eigen::Vector3d error = rotation_matrix * global_error;
224 float v_ref = std::abs(target_state.linear_vel) * velocity_scale;
225 float w_ref = target_state.angular_vel * velocity_scale;
226 float a_v_ref = (v_ref < 0.15f) ? 0.15f : v_ref;
227 constexpr float eps = -1e-3f;
230 -w_ref, eps, a_v_ref,
233 Eigen::Matrix<float, 3, 2> B;
238 auto discAB = discretizeAB(A, B, measured_dt);
239 Eigen::MatrixXf X = dareSolver(discAB.first, discAB.second, Q_mat, R_mat);
241 cached_K = (R_mat + discAB.second.transpose() * X * discAB.second).inverse() * discAB.second.transpose() * X * discAB.first;
243 last_solve_v = v_ref;
244 last_solve_w = w_ref;
246 Eigen::Vector2f u = cached_K * error.cast<
float>();
248 float u_v = clamp(u(0), -l_config.max_lin_correction, l_config.max_lin_correction);
249 float u_w = clamp(u(1), -l_config.max_ang_correction, l_config.max_ang_correction);
252 float v_cmd = v_ref + u_v;
253 float w_cmd = w_ref + u_w;
256 if(l_config.backwards) {
260 double lat_err = std::abs(error(1));
261 double head_err = std::abs(error(2));
262 double instant_oscillation = std::abs(w_cmd - prev_w_cmd);
265 sum_lat_error += lat_err;
266 sum_head_error += head_err;
267 sum_oscillation += instant_oscillation;
273 DrivetrainVoltages output_voltages =
controller.update(
274 v_cmd, w_cmd, left_actual_mps, right_actual_mps
277 output_voltages.rightVoltage = clamp(output_voltages.rightVoltage, -12.0, 12.0);
278 output_voltages.leftVoltage = clamp(output_voltages.leftVoltage, -12.0, 12.0);
280 rightMotors.move_voltage(output_voltages.rightVoltage * 1000.0);
281 leftMotors.move_voltage(output_voltages.leftVoltage * 1000.0);
284 std::ostringstream ss;
285 ss << Vector2(current_pose.x, current_pose.y).latex() <<
",";
288 logs.push_back(ss.str());
291 pros::Task::delay_until(¤t_time, 10);
297 if (steps == 0) steps = 1;
298 double avg_lat_error = sum_lat_error / steps;
299 double avg_head_error = sum_head_error / steps;
300 double avg_jerk = sum_oscillation / steps;
303 std::cout <<
"\n--- LTV PERFORMANCE SUMMARY ---" << std::endl;
304 std::cout <<
"Steps Completed: " << steps <<
" / " << trajectory_size << std::endl;
305 std::cout <<
"Avg Lateral Error: " << (avg_lat_error /
INCH_TO_METER) <<
" in" << std::endl;
306 std::cout <<
"Avg Heading Error: " << lemlib::radToDeg(avg_head_error) <<
" deg" << std::endl;
307 std::cout <<
"Avg Control Jerk: " << avg_jerk << std::endl;
309 std::cout <<
"\n--- COORDINATE LOG START ---" << std::endl;
310 for (
const auto& line : logs) {
314 std::cout <<
"\n--- COORDINATE LOG END ---" << std::endl;
320Eigen::MatrixXf LTVPathFollower::dareSolver(
const Eigen::MatrixXf &A,
const Eigen::MatrixXf &B,
const Eigen::MatrixXf &Q,
const Eigen::MatrixXf &R) {
321 int states = A.rows();
323 Eigen::MatrixXf A_k = A;
324 Eigen::MatrixXf G_k = B * R.llt().solve(B.transpose());
326 Eigen::MatrixXf H_k1 = Q;
328 Eigen::MatrixXf I = Eigen::MatrixXf::Identity(states, states);
330 for (
int i = 0; i < 80; ++i) {
332 Eigen::MatrixXf W = I + G_k * H_k;
333 auto W_solver = W.partialPivLu();
334 Eigen::MatrixXf V_1 = W_solver.solve(A_k);
335 Eigen::MatrixXf V_2 = W_solver.solve(G_k);
337 G_k += A_k * V_2 * A_k.transpose();
338 H_k1 = H_k + V_1.transpose() * H_k * A_k;
340 if ((H_k1 - H_k).norm() <= 1e-10f * H_k1.norm()) {
348std::pair<Eigen::MatrixXf, Eigen::MatrixXf> LTVPathFollower::discretizeAB(
349 const Eigen::MatrixXf& contA,
const Eigen::MatrixXf& contB,
double dtSeconds) {
351 int states = contA.rows();
352 int inputs = contB.cols();
353 Eigen::MatrixXf M(states + inputs, states + inputs);
355 M.topLeftCorner(states, states) = contA;
356 M.topRightCorner(states, inputs) = contB;
357 Eigen::MatrixXf Mdt = M * dtSeconds;
358 Eigen::MatrixXf I = Eigen::MatrixXf::Identity(M.rows(), M.cols());
359 Eigen::MatrixXf M2 = Mdt * Mdt;
360 Eigen::MatrixXf phi = I + Mdt + (M2 * 0.5f);
361 Eigen::MatrixXf discA = phi.topLeftCorner(states, states);
362 Eigen::MatrixXf discB = phi.topRightCorner(states, inputs);
363 return {discA, discB};
366void LTVPathFollower::precompute_paths(
const std::vector<std::string>& path_names) {
367 auto* stored =
new std::vector<std::string>(path_names);
368 pros::Task t(precompute_paths_task, stored,
"PathCompute");
371void LTVPathFollower::precompute_paths_task(
void* param) {
372 auto* path_names =
static_cast<std::vector<std::string>*
>(param);
374 precomputed_paths.clear();
375 precomputed_paths.reserve(path_names->size());
377 for (
const auto& name : *path_names) {
378 precomputed_paths[name] = prepare_trajectory(name);
384std::vector<std::vector<double>> LTVPathFollower::parse_tuples(
const std::string& line) {
385 std::vector<std::vector<double>> result;
387 bool inside_parens =
false;
389 for (
char c : line) {
392 inside_parens =
true;
393 }
else if (c ==
')') {
394 std::replace(temp.begin(), temp.end(),
',',
' ');
395 std::istringstream ss(temp);
396 std::vector<double> tuple;
400 tuple.push_back(val);
403 result.push_back(tuple);
404 inside_parens =
false;
405 }
else if (inside_parens) {
412std::vector<State> LTVPathFollower::prepare_trajectory(
const std::string& data) {
413 std::istringstream ss(data);
414 std::vector<std::vector<double>> P, V;
417 while (std::getline(ss, line)) {
418 if (line.find(
"P =") != std::string::npos) {
419 P = parse_tuples(line.substr(line.find(
'{')));
420 }
else if (line.find(
"V =") != std::string::npos) {
421 V = parse_tuples(line.substr(line.find(
'{')));
425 size_t n = std::min(P.size(), V.size());
426 if (n == 0)
return {};
428 std::vector<State> states(n);
429 for (
size_t i = 0; i < n; i++) {
430 if (P[i].size() >= 3) {
431 states[i].x = P[i][0];
432 states[i].y = P[i][1];
433 states[i].heading = P[i][2];
436 if (V[i].size() >= 2) {
437 states[i].linear_vel = V[i][0];
438 states[i].angular_vel = V[i][1];
const VelocityControllerConfig config
Chassis chassis(drivebase, lateral_controller, angular_controller, sensors, &throttle_curve, &steer_curve)
lemlib::Drivetrain drivebase & leftMotors
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