Skip to content

A C++ implementation of an MPC-focused quadratic-programming solver that uses ramp functions (Valmorbida and Hovd, 2023)

License

Notifications You must be signed in to change notification settings

heirung/mpc-qp-ramp-solver

Repository files navigation

QP-ramp

This repo contains a C++ implementation of the quadratic-programming algorithm by Valmorbida and Hovd (2023). While this algorithm is geared toward solving the optimal-control problems that arise in MPC, it also works well for convex inequality-constrained QP problems.

The paper is really interesting and well worth a read. I've seen lots of approaches to solving QP problems, but none that strike me as similar to the one in this paper. When I first read it, my first reaction was along the lines of "How on earth does this solve a QP problem?" After thinking about it more my main question was more along the lines of "Why haven't anyone thought of this before?"

There's a lot I could do to improve this, including better error handling, using Eigen/Sparse, improving memory use, adding support for output constraints and constraining the state and input to general polytopes, and so forth. I might get around to doing some of that one day.

Dependencies

Both of the examples below can be found in examples.cpp and run with bazel run --config=linux //:examples (or --config=macos).

Example 1: "Classic" QP

int SolveClassicQpExample() {
    // Solve a "classic" QP, of the form min 1/2 xᵀQx + xᵀc, s. t. Ax ≥ b.
    // Example 16.4 in Nocedal & Wright (2006):
    std::cout << "\nSolving a classic QP with two variables and five constraints." << std::endl;
    const Eigen::MatrixXd Q = 2.0 * Eigen::MatrixXd::Identity(2, 2);
    const Eigen::Vector2d c{
          {-2.0, -5.0}
    };
    const Eigen::MatrixXd A{
          { 1.0, -2.0},
          {-1.0, -2.0},
          {-1.0,  2.0},
          { 1.0,  0.0},
          { 0.0,  1.0}
    };
    const Eigen::VectorXd b{
          {-2.0, -6.0, -2.0, 0.0, 0.0}
    };
    if (!RampSolver::parametersAreValid(Q, c, A, b)) {
        std::cerr << "Invalid problem." << std::endl;
        return 1;
    }

    RampSolver solver(Q, c, A, b);
    const Eigen::VectorXd y0 = solver.getColdStartY();
    const ConstraintSet activeSet0;
    const auto [y, activeSet, invQ, iter, success] = solver.solveImplicitEquation(y0, activeSet0);
    if (!success) {
        std::cerr << "Solver failure." << std::endl;
        return 1;
    }

    Eigen::IOFormat vectorFmt(5, 0, ", ", "\n", "[", "]'");
    std::cout << "Solution: " << solver.getSolution(y).transpose().format(vectorFmt) << std::endl;
    std::cout << "The KKT conditions are "
              << (std::ranges::all_of(solver.kktConditionsAreSatisfied(y), std::identity{})
                        ? ""
                        : "NOT ")
              << "satisfied." << std::endl;
    std::cout << "Number of active constraints at the solution: " << activeSet.size() << std::endl;
    std::cout << "Multiplier: " << RampSolver::r(y).transpose().format(vectorFmt) << std::endl;
    return 0;
}

The output should be

Solving a classic QP with two variables and five constraints.
Solution: [1.4, 1.7]'
The KKT conditions are satisfied.
Number of active constraints at the solution: 1
Multiplier: [0.8,   0,   0,   0,   0]'

Example 2: Optimal-control problem

The code below solves the initial optimal-control problem in Example 1 in the paper.

int SolveDoubleIntegratorExample() {
    std::cout << "\nSolving an optimal-control problem, from Example 1 in the paper." << std::endl;
    const Eigen::MatrixXd A{
          {1.0, 1.0},
          {0.0, 1.0}
    };
    const Eigen::MatrixXd B{{1.0}, {0.3}};
    const Eigen::MatrixXd P = Eigen::MatrixXd::Identity(2, 2);
    const Eigen::MatrixXd R = Eigen::MatrixXd::Identity(1, 1);
    const Eigen::MatrixXd P_T = Eigen::MatrixXd::Identity(2, 2);
    const RampSolver::Constraints constraints{
          .xMin = -Eigen::VectorXd::Ones(2) * 5.0,
          .xMax = Eigen::VectorXd::Ones(2) * 5.0,
          .uMin = -Eigen::VectorXd::Ones(1),
          .uMax = Eigen::VectorXd::Ones(1)};
    const int N = 20;  // 10 in the paper -- too short to get reasonably close to the origin.
    const Eigen::VectorXd x_0{
          {5.0, -2.0}
    };
    if (!RampSolver::parametersAreValid(A, B, P, R, P_T, constraints)) {
        std::cerr << "Invalid problem." << std::endl;
        return 1;
    }

    RampSolver solver(A, B, P, R, P_T, constraints, N);
    solver.setInitialState(x_0);
    const Eigen::VectorXd y0 = solver.getColdStartY();  // Requires a valid initial state
    const ConstraintSet activeSet0;
    const auto [y, activeSet, invQ, iter, success] = solver.solveImplicitEquation(y0, activeSet0);
    if (!success) {
        std::cerr << "Solver failure." << std::endl;
        return 1;
    }

    Eigen::IOFormat vectorFmt(5, 0, ", ", "\n", "[", "]'");
    // Extract the input and predicted state sequences U and X (vectors), X = Āx(k) + B̅U
    const Eigen::VectorXd U = solver.getSolution(y);
    const Eigen::VectorXd X = solver.getStateTrajectory(U);
    std::cout << "Final control input, u(k + N - 1) = " << U.tail(B.cols()) << std::endl;
    std::cout << "Final predicted state, x(k + N) = "
              << X.tail(A.cols()).transpose().format(vectorFmt) << std::endl;
    std::cout << "The KKT conditions are "
              << (std::ranges::all_of(solver.kktConditionsAreSatisfied(y), std::identity{})
                        ? ""
                        : "NOT ")
              << "satisfied." << std::endl;
    std::cout << "Number of active constraints at the solution: " << activeSet.size() << std::endl;
    std::cout << "Iterations: " << iter << std::endl;

    const std::string filename = "example_1_solution.csv";
    const std::optional<std::string> maybeFile =
          util::writeSolutionToCsv(filename, x_0, X, U, N, A.cols(), B.cols());
    if (maybeFile.has_value()) {
        std::cout << "Solution written to " << maybeFile.value() << std::endl;
    } else {
        std::cerr << "Unable to write solution to file." << std::endl;
    }
    return 0;
}

The output should be

Solving an optimal-control problem, from Example 1 in the paper.
Final control input, u(k + N - 1) = 0.00331883
Final predicted state, x(k + N) = [-0.0022083, -0.0037017]'
The KKT conditions are satisfied.
Number of active constraints at the solution: 5
Iterations: 5
Solution written to /path/to/example_1_solution.csv

and the state and input profiles look like this:

image

References

Valmorbida, G. and Hovd, M. Quadratic programming with ramp functions and fast online QP-MPC solutions. Automatica, 153:111011, 2023. https://doi.org/10.1016/j.automatica.2023.111011

Nocedal, J. and Wright, S. J. Numerical Optimization. Springer, second edition, 2006. https://doi.org/10.1007/978-0-387-40065-5

Bemporad, A., Morari, M., Dua, V., and Pistikopoulos, E. N. The explicit linear quadratic regulator for constrained systems. Automatica, 38(1):3–20, 2002. https://doi.org/10.1016/S0005-1098(01)00174-1

About

A C++ implementation of an MPC-focused quadratic-programming solver that uses ramp functions (Valmorbida and Hovd, 2023)

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published