メインコンテンツへスキップ

FuncPieceError not working as expected

回答済み

コメント

6件のコメント

  • Jaromił Najman
    • Gurobi Staff

    Hi Stefan,

    You are allowing for an approximation error of at most 20 by setting FuncPieceError = 20. This parameter should be set to something like FuncPieceError=1e-3 or a bit lower. Note that Gurobi's piecewise-linear approximation is performed in presolve.  Thus, the approximation will not be recomputed during the B&B algorithm meaning that if the approximation introduces a big error, it will stay during the whole optimization process. This often leads to warnings about violation of general constraints because they only approximate the original function with a fixed error ratio.

    For your function of interest, you don't have to use general constraints. It is a quadratic function, thus you can directly model it in Gurobi instead of using piecewise-linear approximation. In C++ you would use a GRBQuadExpr object and the addQConstr method. For an example of a quadratic model, please refer to bilinear_c++.cpp. For more details of how Gurobi handles quadratic terms, please refer to our NonConvex Webinar and Tech Talk.

    Best regards, 
    Jaromił

    1
  • Stefan Engels
    • Gurobi-versary
    • First Comment
    • First Question

    Hi Jarmoil,

    thank you for your qick response and pointint to the addQConstr method. I have a few follow up questions though, because I have still trouble of understanding the exact meaning of FuncPieceError / it behaves differently than I would have expected from reading the docs.

    I was trying to use piecewise linear functions, because I have a significant amount of binary variables in the full model. I believe(d) that MILPS are significantly easier to solve compared to mixed integer programs with linear objective and quadratic / nonlinear equality constraints (MIQCP, right?) and global optimality usually not guaranteed for the latter. I now read that Gurobi uses spatial branching, so Gurobi can solve to optimality similarly to classic MILPS, right? Do you have any feeling on if spatial branching or defining PWLs (hence classical MILP) is usually faster?

    While you say that FuncPieceError should be something in the order of 1e-3 (though my modeling detail really doesn't care if the error is 1e-3 or around say 20 in practice so that error is really ok for me), I still have trouble understanding what the value does. I expected an approximation error of AT MOST 20 (and as a user I would expect this independently of if the chosen error is reasonable or not, also given that by hand I can calculate the number of pieces needed for this, detail see below).

    However to me it seems that Gurobi sometimes allowes a larger error (more details in my little "experiment" below). Can you explain why that is? Or did you mean that this is because the absolute error is ensured in some altered presolved model, which might differ from the original model? But that would also not really make sense, because I (as a user) would expect my parameters hold for the constraint that I have specified.

    Using WolframAlpha I calculated the number of expected pieces by hand, more precisely the maximal error is <= ABS_ERROR if the number of pieces N >= MAX_X/(2*std::sqrt(2)*std::sqrt(D)*std::sqrt(ABS_ERROR)). Hence, I would expect Gurobi to use

    N = std::ceil(MAX_X/(2*std::sqrt(2)*std::sqrt(D)*std::sqrt(ABS_ERROR)));

    pieces. In the code attached (I put the code at the very end of this message so that the following output makes sense). This seems to always produce the desired absolute approximation error, however Gurobi seems to only sometimes and not always use this.

    Gurobi seems to use exactly this expected approximation for MAX_X=20, D=1.0, ABS_ERROR=5, i.e.,

    PWL Gurobi and PWL floor are NOT equal
    PWL Gurobi and PWL ceil are equal
    PWL Gurobi and PWL round are NOT equal

    However for MAX_X=20, D=1.0, ABS_ERROR=10 Gurobi seems to floor the number and hence obtain an error larger than the specified 10 for some value (e.g. for x = 5 the error is 12.5 > 10), i.e.,

    x = 0, f(x) = 0
       PWL Gurobi: 0, error = 0
       PWL ceil: 0, error = 0
       PWL floor: 0, error = 0
       PWL round: 0, error = 0
    x = 1, f(x) = 0.5
       PWL Gurobi: 5, error = 4.5
       PWL ceil: 3, error = 2.5
       PWL floor: 5, error = 4.5
       PWL round: 5, error = 4.5
    x = 2, f(x) = 2
       PWL Gurobi: 10, error = 8
       PWL ceil: 6, error = 4
       PWL floor: 10, error = 8
       PWL round: 10, error = 8
    x = 3, f(x) = 4.5
       PWL Gurobi: 15, error = 10.5
       PWL ceil: 9, error = 4.5
       PWL floor: 15, error = 10.5
       PWL round: 15, error = 10.5
    x = 4, f(x) = 8
       PWL Gurobi: 20, error = 12
       PWL ceil: 12, error = 4
       PWL floor: 20, error = 12
       PWL round: 20, error = 12
    x = 5, f(x) = 12.5
       PWL Gurobi: 25, error = 12.5
       PWL ceil: 15, error = 2.5
       PWL floor: 25, error = 12.5
       PWL round: 25, error = 12.5
    x = 6, f(x) = 18
       PWL Gurobi: 30, error = 12
       PWL ceil: 18, error = 0
       PWL floor: 30, error = 12
       PWL round: 30, error = 12
    x = 7, f(x) = 24.5
       PWL Gurobi: 35, error = 10.5
       PWL ceil: 27.5, error = 3
       PWL floor: 35, error = 10.5
       PWL round: 35, error = 10.5
    x = 8, f(x) = 32
       PWL Gurobi: 40, error = 8
       PWL ceil: 37, error = 5
       PWL floor: 40, error = 8
       PWL round: 40, error = 8
    x = 9, f(x) = 40.5
       PWL Gurobi: 45, error = 4.5
       PWL ceil: 46.5, error = 6
       PWL floor: 45, error = 4.5
       PWL round: 45, error = 4.5
    x = 10, f(x) = 50
       PWL Gurobi: 50, error = 0
       PWL ceil: 56, error = 6
       PWL floor: 50, error = 0
       PWL round: 50, error = 0
    x = 11, f(x) = 60.5
       PWL Gurobi: 65, error = 4.5
       PWL ceil: 65.5, error = 5
       PWL floor: 65, error = 4.5
       PWL round: 65, error = 4.5
    x = 12, f(x) = 72
       PWL Gurobi: 80, error = 8
       PWL ceil: 75, error = 3
       PWL floor: 80, error = 8
       PWL round: 80, error = 8
    x = 13, f(x) = 84.5
       PWL Gurobi: 95, error = 10.5
       PWL ceil: 84.5, error = 0
       PWL floor: 95, error = 10.5
       PWL round: 95, error = 10.5
    x = 14, f(x) = 98
       PWL Gurobi: 110, error = 12
       PWL ceil: 101, error = 3
       PWL floor: 110, error = 12
       PWL round: 110, error = 12
    x = 15, f(x) = 112.5
       PWL Gurobi: 125, error = 12.5
       PWL ceil: 117.5, error = 5
       PWL floor: 125, error = 12.5
       PWL round: 125, error = 12.5
    x = 16, f(x) = 128
       PWL Gurobi: 140, error = 12
       PWL ceil: 134, error = 6
       PWL floor: 140, error = 12
       PWL round: 140, error = 12
    x = 17, f(x) = 144.5
       PWL Gurobi: 155, error = 10.5
       PWL ceil: 150.5, error = 6
       PWL floor: 155, error = 10.5
       PWL round: 155, error = 10.5
    x = 18, f(x) = 162
       PWL Gurobi: 170, error = 8
       PWL ceil: 167, error = 5
       PWL floor: 170, error = 8
       PWL round: 170, error = 8
    x = 19, f(x) = 180.5
       PWL Gurobi: 185, error = 4.5
       PWL ceil: 183.5, error = 3
       PWL floor: 185, error = 4.5
       PWL round: 185, error = 4.5
    x = 20, f(x) = 200
       PWL Gurobi: 200, error = 0
       PWL ceil: 200, error = 0
       PWL floor: 200, error = 0
       PWL round: 200, error = 0
    PWL Gurobi and PWL floor are equal
    PWL Gurobi and PWL ceil are NOT equal
    PWL Gurobi and PWL round are equal

     

    Thank you in advance. Best regards,

    Stefan

    ------------------------

    main.cpp used:

    #include <iostream>
    #include "gurobi_c++.h"
    #include <vector>
    #include <memory>

    #define MAX_X 20
    #define D 1.0
    #define ABS_ERROR 10

    void add_standard_vars_and_constraints(GRBModel& model, const int& target_x) {
        const double max_y = MAX_X * MAX_X / (2 * D);
        GRBVar x = model.addVar(0, MAX_X, 0, GRB_CONTINUOUS, "x");
        GRBVar y = model.addVar(0, max_y, 0, GRB_CONTINUOUS, "y");
        GRBVar z = model.addVar(0, GRB_INFINITY, 0, GRB_CONTINUOUS, "z");

        GRBLinExpr obj = z;
        model.setObjective(obj, GRB_MINIMIZE);

        model.addConstr(z, GRB_GREATER_EQUAL, target_x - x, "z_constr_1");
        model.addConstr(z, GRB_GREATER_EQUAL, x - target_x, "z_constr_2");

        model.update();
    }

    void add_pwl_constraint(GRBModel& model, const int& N) {
        auto x = model.getVarByName("x");
        auto y = model.getVarByName("y");
        std::unique_ptr<double[]> xpts(new double[N+1]);
        std::unique_ptr<double[]> ypts(new double[N+1]);
        for (int i = 0; i <= N; ++i) {
            xpts[i] = i * MAX_X / N;
            ypts[i] = xpts[i] * xpts[i] / (2 * D);
        }
        model.addGenConstrPWL(x, y, N+1, xpts.get(), ypts.get(), "gen_const");

        model.update();
    }

    int main() {
        std::vector<int> x_vals;
        std::vector<double> y_vals_pwl_floor;
        std::vector<double> y_vals_pwl_ceil;
        std::vector<double> y_vals_pwl_round;
        std::vector<double> y_vals_pwl_gurobi;
        std::vector<double> real_vals;
        std::vector<double> gen_constr_approximation_errors_pwl_floor;
        std::vector<double> gen_constr_approximation_errors_pwl_ceil;
        std::vector<double> gen_constr_approximation_errors_pwl_round;
        std::vector<double> gen_constr_approximation_errors_pwl_gurobi;

        for (int target_x = 0; target_x <= MAX_X; target_x += 1) {
            x_vals.emplace_back(target_x);
            real_vals.emplace_back(x_vals.back() * x_vals.back() / (2 * D));
        }

        for (int target_x = 0; target_x <= MAX_X; target_x += 1) {
            GRBEnv env = new GRBEnv(true);
            env.start();
            GRBModel model = GRBModel(env);

            add_standard_vars_and_constraints(model, target_x);
            auto x = model.getVarByName("x");
            auto y = model.getVarByName("y");

            const double p[3]{1 / (2 * D), 0, 0};
            model.addGenConstrPoly(x, y, 3, p, "gen_const",
                                   "FuncPieceRatio=1 FuncPieces=-1 FuncPieceError=" + std::to_string(ABS_ERROR));
            model.update();


            model.optimize();
            y_vals_pwl_gurobi.emplace_back(y.get(GRB_DoubleAttr_X));
            gen_constr_approximation_errors_pwl_gurobi.emplace_back(std::abs(y_vals_pwl_gurobi.back() - real_vals[target_x]));
        }
        for (int target_x = 0; target_x <= MAX_X; target_x += 1) {
            GRBEnv env = new GRBEnv(true);
            env.start();
            GRBModel model = GRBModel(env);

            add_standard_vars_and_constraints(model, target_x);
            const int N = std::ceil(MAX_X/(2*std::sqrt(2)*std::sqrt(D)*std::sqrt(ABS_ERROR)));
            add_pwl_constraint(model, N);

            model.optimize();
            auto y = model.getVarByName("y");
            y_vals_pwl_ceil.emplace_back(y.get(GRB_DoubleAttr_X));
            gen_constr_approximation_errors_pwl_ceil.emplace_back(std::abs(y_vals_pwl_ceil.back() - real_vals[target_x]));
        }
        for (int target_x = 0; target_x <= MAX_X; target_x += 1) {
            GRBEnv env = new GRBEnv(true);
            env.start();
            GRBModel model = GRBModel(env);

            add_standard_vars_and_constraints(model, target_x);
            const int N = std::floor(MAX_X/(2*std::sqrt(2)*std::sqrt(D)*std::sqrt(ABS_ERROR)));
            add_pwl_constraint(model, N);

            model.optimize();
            auto y = model.getVarByName("y");
            y_vals_pwl_floor.emplace_back(y.get(GRB_DoubleAttr_X));
            gen_constr_approximation_errors_pwl_floor.emplace_back(std::abs(y_vals_pwl_floor.back() - real_vals[target_x]));
        }
        for (int target_x = 0; target_x <= MAX_X; target_x += 1) {
            GRBEnv env = new GRBEnv(true);
            env.start();
            GRBModel model = GRBModel(env);

            add_standard_vars_and_constraints(model, target_x);
            const int N = std::round(MAX_X/(2*std::sqrt(2)*std::sqrt(D)*std::sqrt(ABS_ERROR)));
            add_pwl_constraint(model, N);

            model.optimize();
            auto y = model.getVarByName("y");
            y_vals_pwl_round.emplace_back(y.get(GRB_DoubleAttr_X));
            gen_constr_approximation_errors_pwl_round.emplace_back(std::abs(y_vals_pwl_round.back() - real_vals[target_x]));
        }

        bool equal_to_floor = true;
        bool equal_to_ceil = true;
        bool equal_to_round = true;
        for (int i = 0; i < x_vals.size(); ++i) {
            std::cout << "x = " << x_vals[i] << ", f(x) = " << real_vals[i] << std::endl;
            std::cout << "   PWL Gurobi: " << y_vals_pwl_gurobi[i] << ", error = " << gen_constr_approximation_errors_pwl_gurobi[i] << std::endl;
            std::cout << "   PWL ceil: " << y_vals_pwl_ceil[i] << ", error = " << gen_constr_approximation_errors_pwl_ceil[i] << std::endl;
            std::cout << "   PWL floor: " << y_vals_pwl_floor[i] << ", error = " << gen_constr_approximation_errors_pwl_floor[i] << std::endl;
            std::cout << "   PWL round: " << y_vals_pwl_round[i] << ", error = " << gen_constr_approximation_errors_pwl_round[i] << std::endl;
            if (y_vals_pwl_gurobi[i] != y_vals_pwl_floor[i]) {
                equal_to_floor = false;
            }
            if (y_vals_pwl_gurobi[i] != y_vals_pwl_ceil[i]) {
                equal_to_ceil = false;
            }
            if (y_vals_pwl_gurobi[i] != y_vals_pwl_round[i]) {
                equal_to_round = false;
            }
        }

        if (equal_to_floor) {
            std::cout << "PWL Gurobi and PWL floor are equal" << std::endl;
        } else {
            std::cout << "PWL Gurobi and PWL floor are NOT equal" << std::endl;
        }
        if (equal_to_ceil) {
            std::cout << "PWL Gurobi and PWL ceil are equal" << std::endl;
        } else {
            std::cout << "PWL Gurobi and PWL ceil are NOT equal" << std::endl;
        }
        if (equal_to_round) {
            std::cout << "PWL Gurobi and PWL round are equal" << std::endl;
        } else {
            std::cout << "PWL Gurobi and PWL round are NOT equal" << std::endl;
        }

    }
    0
  • Jaromił Najman
    • Gurobi Staff

    Hi Stefan,

     I believe(d) that MILPS are significantly easier to solve compared to mixed integer programs with linear objective and quadratic / nonlinear equality constraints (MIQCP, right?) and global optimality usually not guaranteed for the latter.

    The algorithms and heuristics are more advanced for MILPs than for quadratic / nonlinear programs. Thus, it is most often better to solve a MILP instead of a nonlinear problem. However as usual, there are exceptions.

    It is possible to guarantee global optimality for MINLPs through, e.g., the spatial B&B algorithm. If you use a local solution method then no guarantee of global optimality can be given.

    I now read that Gurobi uses spatial branching, so Gurobi can solve to optimality similarly to classic MILPS, right?

    Yes, see our Nonconvex Webinar and Tech Talk for more details.

    Do you have any feeling on if spatial branching or defining PWLs (hence classical MILP) is usually faster?

    Usually, if a suitable algorithm is available, I would always first try to solve the nonlinear model instead of directly using an approximation. A disadvantage of the piecewise-linear approximation is that often a lot of binary variables are needed to properly approximate the original nonlinear function. This bloats up the model size a lot and makes the MILP way harder to solve. Moreover, one always introduces an approximation error, which, depending on the application, may or may not be acceptable.

    However to me it seems that Gurobi sometimes allowes a larger error (more details in my little "experiment" below). Can you explain why that is? Or did you mean that this is because the absolute error is ensured in some altered presolved model, which might differ from the original model? But that would also not really make sense, because I (as a user) would expect my parameters hold for the constraint that I have specified.

    I am not sure I understand the issue here. Could you please share a model in LP or MPS format for which you see a constraint violation larger than 20 despite you setting FuncPieceError=20 and FuncPieces=-1. Note that uploading files in the Community Forum is not possible but we discuss an alternative in Posting to the Community Forum.

    However for MAX_X=20, D=1.0, ABS_ERROR=10 Gurobi seems to floor the number and hence obtain an error larger than the specified 10 for some value (e.g. for x = 5 the error is 12.5 > 10), i.e.,

    I don't understand how you got these values. In particular how you computed the Gurobi error at a given point. Could you please elaborate?

    Edit: I think I now understand how you compute the error values. Could you still please write out the particular LP/MPS file where the error is violated such that I could have a look?

    Note that Gurobi does not necessarily use a uniform grid to approximate a given function.

    Best regards, 
    Jaromił

    0
  • Stefan Engels
    • Gurobi-versary
    • First Comment
    • First Question

    Hi Jaromil,

    thank you for you answer, I am happy to provide an mps file. Further down in my post, I will also try to explain my thoughts a bit more (hope that helps and clarifies what I mean).

    In Short:
    The mps file provided here: https://gigamove.rwth-aachen.de/de/download/e1bf82759cf5607bdf463c5f44b49d85 (passphrase: almighty-jujitsu-deluxe) can be downloaded until June 29 has a pwl approximation of y=0.5x^2. Optimal solution is x=4 and y=40 (and z=0). But the "real" value of the function at x=4 is 8. Hence, the pwl absolute error at x=4 is 32. Moreover gurobi prints a constraint violation of around 27.
    I can also post the content of the mps file here in plain text. It is:

    NAME (null)
    ROWS
     N  OBJ
     G  z_constr_1
     G  z_constr_2
    COLUMNS
        x         z_constr_1  1
        x         z_constr_2  -1
        y         OBJ       0
        z         OBJ       1
        z         z_constr_1  1
        z         z_constr_2  1
    RHS
        RHS1      z_constr_1  4
        RHS1      z_constr_2  -4
    BOUNDS
     UP BND1      x         20
     UP BND1      y         200
    GENCONS
     POLY gen_const
        Options  -1  0.01  20  -1
        x  y
        2  0.5
    ENDATA

     

    Some details on my reasoning:
    Let me share my thoughts on the minimal example above. I want to approximate the function \(yf(x)=0.5x^2\) using a picewise linear approximation. In order to access the approximated value at a given x value, I want to make sure that Gurobi picks a certain target x (but I still want to remain a feasible region in a certain range). So lets say, we want to find f(4) but want gurobi to approximate the function on the interval [0,20]. Then I model

    min abs(x-4)
    0 <= x <= 20
    y = 0.5x^2 (using pwl, FuncPieces=-1 FuncPieceError=20)

    i.e., with an additional variable

    min z
    z >= x-4
    z >= 4-x
    y = 0.5x^2 (pwl)
    0 <= x <= 20

    Gurobi returns as an optimal solution x=4 and y=40. However f(4)=8, hence the difference is 32, which is larger than 20. Gurobi specifies the constraint violaton as 27.514 (I am not sure what this value means in this context though, since I feel like the actual error is 32, but both values are significantly above 20). I have saved the mps file (as written above). I provide the cpp code at the bottom of this post.


    Pleas note that I am getting the same results (x=4, y=40) when running gurobi from the command line as follows:

    gurobi_cl ResultFile=model_cl.sol model.mps

    I know that Gurobi does not necessarily approximate uniformly. In my previous post I was just using this as one possible approximation that I would have expected to be used. Using wolframalpha I found that a uniform grid on

    N = ceil(20/(2*sqrt(2*ABS_ERROR)))

    sections leads a desired maximal absolute error. So I would have expected Gurobi to select this or a similar approximation. At least it seems to be more or less straight forward to find an explicit (in this case even uniform) pwl approximation that fulfills an absolute error of at most 20. This was only for trying to understand what kind of appoximation gurobi might have used under the hood (but not really successfull in finding it in every case).

    Thank you for looking into this. Best,

    Stefan

    ----
    main.cpp used to generate the above output and files:

    #include <iostream>
    #include "gurobi_c++.h"
    #include <vector>

    #define max_x 20
    #define target_x 4

    int main() {
        GRBEnv env = new GRBEnv(true);
        env.start();
        GRBModel model = GRBModel(env);
        const double max_y = max_x * max_x / 2;
        GRBVar x = model.addVar(0, max_x, 0, GRB_CONTINUOUS, "x");
        GRBVar y = model.addVar(0, max_y, 0, GRB_CONTINUOUS, "y");
        GRBVar z = model.addVar(0, GRB_INFINITY, 0, GRB_CONTINUOUS, "z");

        GRBLinExpr obj = z;
        model.setObjective(obj, GRB_MINIMIZE);

        // model y = 1/2 * x^2 using a piecewise linear function
        const double p[3]{0.5, 0, 0};
        model.addGenConstrPoly(x, y, 3, p, "gen_const",
                               "FuncPieces=-1 FuncPieceError=20");

        // Making sure that target_x is the optimal solution
        model.addConstr(z, GRB_GREATER_EQUAL, target_x - x, "z_constr_1");
        model.addConstr(z, GRB_GREATER_EQUAL, x - target_x, "z_constr_2");

        model.optimize();

        model.write("model.lp");
        model.write("model.mps");
        model.write("model.sol");

        auto x_val = x.get(GRB_DoubleAttr_X);
        auto y_val = y.get(GRB_DoubleAttr_X);
        auto constraint_violation = model.get(GRB_DoubleAttr_ConstrVio);
        auto real_val = x_val * x_val / 2;
        auto gen_constr_approximation_error = std::abs(y_val - real_val);

        std::cout << "x = " << x_val << std::endl;
        std::cout << "y = " << y_val << " (exact value: " << real_val << ")" << std::endl;
        std::cout << "approximation error at x is " << gen_constr_approximation_error << std::endl;
        std::cout << "gurobi specifies a constraint violation of " << constraint_violation << std::endl;

    }
    0
  • Jaromił Najman
    • Gurobi Staff

    Hi Stefan,

    Thank you for the model and your insights. I can reproduce the behavior on my end and will investigate.

    I will come back once I have something relevant.

    Best regards, 
    Jaromił

    0
  • Jaromił Najman
    • Gurobi Staff

    Hi Stefan,

    We identified the source of the issue. A wrong violation calculation is performed and reported. The actual violation is \(< 20\). This issue will be fixed an upcoming release. Unfortunately, there is currently no workaround for this.

    Thank you for reporting this issue.

    Best regards, 
    Jaromił

    0

サインインしてコメントを残してください。