// Copyright (c) FIRST and other WPILib contributors. // Open Source Software; you can modify and/or share it under the terms of // the WPILib BSD license file in the root directory of this project. #pragma once #include #include #include #include #include "Eigen/Cholesky" #include "Eigen/Core" #include "Eigen/Eigenvalues" #include "drake/math/discrete_algebraic_riccati_equation.h" #include "frc/StateSpaceUtil.h" #include "frc/system/Discretization.h" #include "frc/system/LinearSystem.h" #include "units/time.h" #include "unsupported/Eigen/MatrixFunctions" #include "wpimath/MathShared.h" namespace frc { namespace detail { /** * Contains the controller coefficients and logic for a linear-quadratic * regulator (LQR). * LQRs use the control law u = K(r - x). * * For more on the underlying math, read * https://file.tavsys.net/control/controls-engineering-in-frc.pdf. * * @tparam States Number of states. * @tparam Inputs Number of inputs. */ template class LinearQuadraticRegulatorImpl { public: /** * Constructs a controller with the given coefficients and plant. * * @param plant The plant being controlled. * @param Qelems The maximum desired error tolerance for each state. * @param Relems The maximum desired control effort for each input. * @param dt Discretization timestep. */ template LinearQuadraticRegulatorImpl( const LinearSystem& plant, const wpi::array& Qelems, const wpi::array& Relems, units::second_t dt) : LinearQuadraticRegulatorImpl(plant.A(), plant.B(), Qelems, Relems, dt) { } /** * Constructs a controller with the given coefficients and plant. * * @param A Continuous system matrix of the plant being controlled. * @param B Continuous input matrix of the plant being controlled. * @param Qelems The maximum desired error tolerance for each state. * @param Relems The maximum desired control effort for each input. * @param dt Discretization timestep. */ LinearQuadraticRegulatorImpl(const Eigen::Matrix& A, const Eigen::Matrix& B, const wpi::array& Qelems, const wpi::array& Relems, units::second_t dt) : LinearQuadraticRegulatorImpl(A, B, MakeCostMatrix(Qelems), MakeCostMatrix(Relems), dt) {} /** * Constructs a controller with the given coefficients and plant. * * @param A Continuous system matrix of the plant being controlled. * @param B Continuous input matrix of the plant being controlled. * @param Q The state cost matrix. * @param R The input cost matrix. * @param dt Discretization timestep. */ LinearQuadraticRegulatorImpl(const Eigen::Matrix& A, const Eigen::Matrix& B, const Eigen::Matrix& Q, const Eigen::Matrix& R, units::second_t dt) { Eigen::Matrix discA; Eigen::Matrix discB; DiscretizeAB(A, B, dt, &discA, &discB); if (!IsStabilizable(discA, discB)) { std::string msg = fmt::format( "The system passed to the LQR is uncontrollable!\n\nA =\n{}\nB " "=\n{}\n", discA, discB); wpi::math::MathSharedStore::ReportError(msg); throw std::invalid_argument(msg); } Eigen::Matrix S = drake::math::DiscreteAlgebraicRiccatiEquation(discA, discB, Q, R); // K = (BᵀSB + R)⁻¹BᵀSA m_K = (discB.transpose() * S * discB + R) .llt() .solve(discB.transpose() * S * discA); Reset(); } /** * Constructs a controller with the given coefficients and plant. * * @param A Continuous system matrix of the plant being controlled. * @param B Continuous input matrix of the plant being controlled. * @param Q The state cost matrix. * @param R The input cost matrix. * @param N The state-input cross-term cost matrix. * @param dt Discretization timestep. */ LinearQuadraticRegulatorImpl(const Eigen::Matrix& A, const Eigen::Matrix& B, const Eigen::Matrix& Q, const Eigen::Matrix& R, const Eigen::Matrix& N, units::second_t dt) { Eigen::Matrix discA; Eigen::Matrix discB; DiscretizeAB(A, B, dt, &discA, &discB); Eigen::Matrix S = drake::math::DiscreteAlgebraicRiccatiEquation(discA, discB, Q, R, N); // K = (BᵀSB + R)⁻¹(BᵀSA + Nᵀ) m_K = (discB.transpose() * S * discB + R) .llt() .solve(discB.transpose() * S * discA + N.transpose()); Reset(); } LinearQuadraticRegulatorImpl(LinearQuadraticRegulatorImpl&&) = default; LinearQuadraticRegulatorImpl& operator=(LinearQuadraticRegulatorImpl&&) = default; /** * Returns the controller matrix K. */ const Eigen::Matrix& K() const { return m_K; } /** * Returns an element of the controller matrix K. * * @param i Row of K. * @param j Column of K. */ double K(int i, int j) const { return m_K(i, j); } /** * Returns the reference vector r. * * @return The reference vector. */ const Eigen::Vector& R() const { return m_r; } /** * Returns an element of the reference vector r. * * @param i Row of r. * * @return The row of the reference vector. */ double R(int i) const { return m_r(i, 0); } /** * Returns the control input vector u. * * @return The control input. */ const Eigen::Vector& U() const { return m_u; } /** * Returns an element of the control input vector u. * * @param i Row of u. * * @return The row of the control input vector. */ double U(int i) const { return m_u(i, 0); } /** * Resets the controller. */ void Reset() { m_r.setZero(); m_u.setZero(); } /** * Returns the next output of the controller. * * @param x The current state x. */ Eigen::Vector Calculate( const Eigen::Vector& x) { m_u = m_K * (m_r - x); return m_u; } /** * Returns the next output of the controller. * * @param x The current state x. * @param nextR The next reference vector r. */ Eigen::Vector Calculate( const Eigen::Vector& x, const Eigen::Vector& nextR) { m_r = nextR; return Calculate(x); } /** * Adjusts LQR controller gain to compensate for a pure time delay in the * input. * * Linear-Quadratic regulator controller gains tend to be aggressive. If * sensor measurements are time-delayed too long, the LQR may be unstable. * However, if we know the amount of delay, we can compute the control based * on where the system will be after the time delay. * * See https://file.tavsys.net/control/controls-engineering-in-frc.pdf * appendix C.4 for a derivation. * * @param plant The plant being controlled. * @param dt Discretization timestep. * @param inputDelay Input time delay. */ template void LatencyCompensate(const LinearSystem& plant, units::second_t dt, units::second_t inputDelay) { Eigen::Matrix discA; Eigen::Matrix discB; DiscretizeAB(plant.A(), plant.B(), dt, &discA, &discB); m_K = m_K * (discA - discB * m_K).pow(inputDelay / dt); } private: // Current reference Eigen::Vector m_r; // Computed controller output Eigen::Vector m_u; // Controller gain Eigen::Matrix m_K; }; } // namespace detail template class LinearQuadraticRegulator : public detail::LinearQuadraticRegulatorImpl { public: /** * Constructs a controller with the given coefficients and plant. * * @tparam Outputs The number of outputs. * @param plant The plant being controlled. * @param Qelems The maximum desired error tolerance for each state. * @param Relems The maximum desired control effort for each input. * @param dt Discretization timestep. */ template LinearQuadraticRegulator(const LinearSystem& plant, const wpi::array& Qelems, const wpi::array& Relems, units::second_t dt) : LinearQuadraticRegulator(plant.A(), plant.B(), Qelems, Relems, dt) {} /** * Constructs a controller with the given coefficients and plant. * * @param A Continuous system matrix of the plant being controlled. * @param B Continuous input matrix of the plant being controlled. * @param Qelems The maximum desired error tolerance for each state. * @param Relems The maximum desired control effort for each input. * @param dt Discretization timestep. */ LinearQuadraticRegulator(const Eigen::Matrix& A, const Eigen::Matrix& B, const wpi::array& Qelems, const wpi::array& Relems, units::second_t dt) : LinearQuadraticRegulator(A, B, MakeCostMatrix(Qelems), MakeCostMatrix(Relems), dt) {} /** * Constructs a controller with the given coefficients and plant. * * @param A Continuous system matrix of the plant being controlled. * @param B Continuous input matrix of the plant being controlled. * @param Q The state cost matrix. * @param R The input cost matrix. * @param dt Discretization timestep. */ LinearQuadraticRegulator(const Eigen::Matrix& A, const Eigen::Matrix& B, const Eigen::Matrix& Q, const Eigen::Matrix& R, units::second_t dt) : detail::LinearQuadraticRegulatorImpl{A, B, Q, R, dt} {} /** * Constructs a controller with the given coefficients and plant. * * @param A Continuous system matrix of the plant being controlled. * @param B Continuous input matrix of the plant being controlled. * @param Q The state cost matrix. * @param R The input cost matrix. * @param N The state-input cross-term cost matrix. * @param dt Discretization timestep. */ LinearQuadraticRegulator(const Eigen::Matrix& A, const Eigen::Matrix& B, const Eigen::Matrix& Q, const Eigen::Matrix& R, const Eigen::Matrix& N, units::second_t dt) : detail::LinearQuadraticRegulatorImpl{A, B, Q, R, N, dt} {} LinearQuadraticRegulator(LinearQuadraticRegulator&&) = default; LinearQuadraticRegulator& operator=(LinearQuadraticRegulator&&) = default; }; // Template specializations are used here to make common state-input pairs // compile faster. template <> class WPILIB_DLLEXPORT LinearQuadraticRegulator<1, 1> : public detail::LinearQuadraticRegulatorImpl<1, 1> { public: template LinearQuadraticRegulator(const LinearSystem<1, 1, Outputs>& plant, const wpi::array& Qelems, const wpi::array& Relems, units::second_t dt) : LinearQuadraticRegulator(plant.A(), plant.B(), Qelems, Relems, dt) {} LinearQuadraticRegulator(const Eigen::Matrix& A, const Eigen::Matrix& B, const wpi::array& Qelems, const wpi::array& Relems, units::second_t dt); LinearQuadraticRegulator(const Eigen::Matrix& A, const Eigen::Matrix& B, const Eigen::Matrix& Q, const Eigen::Matrix& R, units::second_t dt); LinearQuadraticRegulator(const Eigen::Matrix& A, const Eigen::Matrix& B, const Eigen::Matrix& Q, const Eigen::Matrix& R, const Eigen::Matrix& N, units::second_t dt); LinearQuadraticRegulator(LinearQuadraticRegulator&&) = default; LinearQuadraticRegulator& operator=(LinearQuadraticRegulator&&) = default; }; // Template specializations are used here to make common state-input pairs // compile faster. template <> class WPILIB_DLLEXPORT LinearQuadraticRegulator<2, 1> : public detail::LinearQuadraticRegulatorImpl<2, 1> { public: template LinearQuadraticRegulator(const LinearSystem<2, 1, Outputs>& plant, const wpi::array& Qelems, const wpi::array& Relems, units::second_t dt) : LinearQuadraticRegulator(plant.A(), plant.B(), Qelems, Relems, dt) {} LinearQuadraticRegulator(const Eigen::Matrix& A, const Eigen::Matrix& B, const wpi::array& Qelems, const wpi::array& Relems, units::second_t dt); LinearQuadraticRegulator(const Eigen::Matrix& A, const Eigen::Matrix& B, const Eigen::Matrix& Q, const Eigen::Matrix& R, units::second_t dt); LinearQuadraticRegulator(const Eigen::Matrix& A, const Eigen::Matrix& B, const Eigen::Matrix& Q, const Eigen::Matrix& R, const Eigen::Matrix& N, units::second_t dt); LinearQuadraticRegulator(LinearQuadraticRegulator&&) = default; LinearQuadraticRegulator& operator=(LinearQuadraticRegulator&&) = default; }; // Template specializations are used here to make common state-input pairs // compile faster. template <> class WPILIB_DLLEXPORT LinearQuadraticRegulator<2, 2> : public detail::LinearQuadraticRegulatorImpl<2, 2> { public: template LinearQuadraticRegulator(const LinearSystem<2, 2, Outputs>& plant, const wpi::array& Qelems, const wpi::array& Relems, units::second_t dt) : LinearQuadraticRegulator(plant.A(), plant.B(), Qelems, Relems, dt) {} LinearQuadraticRegulator(const Eigen::Matrix& A, const Eigen::Matrix& B, const wpi::array& Qelems, const wpi::array& Relems, units::second_t dt); LinearQuadraticRegulator(const Eigen::Matrix& A, const Eigen::Matrix& B, const Eigen::Matrix& Q, const Eigen::Matrix& R, units::second_t dt); LinearQuadraticRegulator(const Eigen::Matrix& A, const Eigen::Matrix& B, const Eigen::Matrix& Q, const Eigen::Matrix& R, const Eigen::Matrix& N, units::second_t dt); LinearQuadraticRegulator(LinearQuadraticRegulator&&) = default; LinearQuadraticRegulator& operator=(LinearQuadraticRegulator&&) = default; }; } // namespace frc