blob: 1df96404fa6f49dc85974cdd6e1c9ea13257a54e [file] [log] [blame]
James Kuszmaul886675c2024-10-09 20:32:00 -07001#include <memory>
2
3#include "absl/log/check.h"
4#include "absl/log/log.h"
5#include <Eigen/Dense>
6
7#include "frc971/control_loops/c2d.h"
8#include "frc971/control_loops/dlqr.h"
9#include "frc971/control_loops/jacobian.h"
10#include "frc971/control_loops/swerve/dynamics.h"
11#include "frc971/control_loops/swerve/linearization_utils.h"
12
13namespace frc971::control_loops::swerve {
14
15// Provides a simple LQR controller that takes a non-linear system, linearizes
16// the dynamics at each timepoint, recalculates the LQR gains for those
17// dynamics, and calculates the relevant feedback inputs to provide.
18template <int NStates, typename Scalar = double>
19class LinearizedController {
20 public:
21 typedef Eigen::Matrix<Scalar, NStates, 1> State;
22 typedef Eigen::Matrix<Scalar, NStates, NStates> StateSquare;
23 typedef Eigen::Matrix<Scalar, kNumInputs, 1> Input;
24 typedef Eigen::Matrix<Scalar, kNumInputs, kNumInputs> InputSquare;
25 typedef Eigen::Matrix<Scalar, NStates, kNumInputs> BMatrix;
26 typedef DynamicsInterface<State, Input> Dynamics;
27
28 struct Parameters {
29 // State cost matrix.
30 StateSquare Q;
31 // Input cost matrix.
32 InputSquare R;
33 // period at which the controller is called.
34 std::chrono::nanoseconds dt;
35 // The dynamics to use.
36 // TODO(james): I wrote this before creating the auto-differentiation
37 // functions; we should swap to the auto-differentiation, since the
38 // numerical linearization is one of the bigger timesinks in this controller
39 // right now.
40 std::unique_ptr<Dynamics> dynamics;
41 };
42
43 // Represents the linearized dynamics of the system.
44 struct LinearDynamics {
45 StateSquare A;
46 BMatrix B;
47 };
48
49 // Debug information for a given cycle of the controller.
50 struct ControllerDebug {
51 // Feedforward input which we provided.
52 Input U_ff;
53 // Calculated feedback input to provide.
54 Input U_feedback;
55 Eigen::Matrix<Scalar, kNumInputs, NStates> feedback_contributions;
56 };
57
58 struct ControllerResult {
59 // Control input to provide to the robot.
60 Input U;
61 ControllerDebug debug;
62 };
63
64 LinearizedController(Parameters params) : params_(std::move(params)) {}
65
66 // Runs the controller for a given iteration, relinearizing the dynamics about
67 // the provided current state X, attempting to control the robot to the
68 // desired goal state.
69 // The U_ff input will be added into the returned control input.
70 ControllerResult RunController(const State &X, const State &goal,
71 Input U_ff) {
72 auto start_time = aos::monotonic_clock::now();
73 // TODO(james): Swap this to the auto-diff methods; this is currently about
74 // a third of the total time spent in this method when run on the roborio.
75 const struct LinearDynamics continuous_dynamics =
76 LinearizeDynamics(X, U_ff);
77 auto linearization_time = aos::monotonic_clock::now();
78 struct LinearDynamics discrete_dynamics;
79 frc971::controls::C2D(continuous_dynamics.A, continuous_dynamics.B,
80 params_.dt, &discrete_dynamics.A,
81 &discrete_dynamics.B);
82 auto c2d_time = aos::monotonic_clock::now();
83 VLOG(2) << "Controllability of dynamics (ideally should be " << NStates
84 << "): "
85 << frc971::controls::Controllability(discrete_dynamics.A,
86 discrete_dynamics.B);
87 Eigen::Matrix<Scalar, kNumInputs, NStates> K;
88 Eigen::Matrix<Scalar, NStates, NStates> S;
89 // TODO(james): Swap this to a cheaper DARE solver; we should probably just
90 // do something like we do in Trajectory::CalculatePathGains for the tank
91 // spline controller where we approximate the infinite-horizon DARE solution
92 // by doing a finite-horizon LQR.
93 // Currently the dlqr call represents ~60% of the time spent in the
94 // RunController() method.
95 frc971::controls::dlqr(discrete_dynamics.A, discrete_dynamics.B, params_.Q,
96 params_.R, &K, &S);
97 auto dlqr_time = aos::monotonic_clock::now();
98 const Input U_feedback = K * (goal - X);
99 const Input U = U_ff + U_feedback;
100 Eigen::Matrix<Scalar, kNumInputs, NStates> feedback_contributions;
101 for (int state_idx = 0; state_idx < NStates; ++state_idx) {
102 feedback_contributions.col(state_idx) =
103 K.col(state_idx) * (goal - X)(state_idx);
104 }
105 VLOG(2) << "linearization time "
106 << aos::time::DurationInSeconds(linearization_time - start_time)
107 << " c2d time "
108 << aos::time::DurationInSeconds(c2d_time - linearization_time)
109 << " dlqr time "
110 << aos::time::DurationInSeconds(dlqr_time - c2d_time);
111 return {.U = U,
112 .debug = {.U_ff = U_ff,
113 .U_feedback = U_feedback,
114 .feedback_contributions = feedback_contributions}};
115 }
116
117 LinearDynamics LinearizeDynamics(const State &X, const Input &U) {
118 return {.A = NumericalJacobianX(*params_.dynamics, X, U),
119 .B = NumericalJacobianU(*params_.dynamics, X, U)};
120 }
121
122 private:
123 const Parameters params_;
124};
125
126} // namespace frc971::control_loops::swerve