NLoptNet icon indicating copy to clipboard operation
NLoptNet copied to clipboard

Adding Equality constraint breaks variables / gradients computation

Open JeremyBois opened this issue 3 years ago • 5 comments

Hi,

Description

Adding an equality constraint silently break the optimization. Independent variables becomes NaN for all iteration steps (excepting the first one) breaking gradient computation too.

Notes

  • I can add inequality constraints
  • I test writing equality constraints with delegate, lambda, class method --> Same result
  • I'm using latest version of NLoptNet
  • Tried with NLopt 2.6.2 and 2.6.1 --> Same result

Minimal breaking example

Below the minimal code that can be used to reproduce the behaviour.

public void TestSLSQP()
{
    var gradientsHistory = new List<double[]>();
    var variablesHistory = new List<double[]>();
    double? finalScore;
    NloptResult result;

    using (var solver = new NLoptSolver(NLoptAlgorithm.LD_SLSQP, 2, maximumIterations: 20))
    {
        var initialValue = new[] { 1.234, 5.678 };
        solver.SetLowerBounds(new[] { double.NegativeInfinity, 0.0000000000001 });

        int count = 0;
        solver.SetMinObjective((variables, gradient) =>
        {
            if (gradient != null)
            {
                gradient[0] = 0.0;
                gradient[1] = variables[1];
            }
            count++;

            var iterationGradients = new double[2];
            Array.Copy(gradient, iterationGradients, 2);
            gradientsHistory.Add(iterationGradients);

            var iterationValues = new double[2];
            Array.Copy(variables, iterationValues, 2);
            variablesHistory.Add(iterationValues);

            return variables[1];
        });

        // @BUG This is the line that broke everything
        solver.AddEqualZeroConstraint(TestEq);
        // @BUG This is the line that broke everything

        result = solver.Optimize(initialValue, out finalScore);
    }
}

private double TestEq(double[] values, double[] grads)
{
    return values.Length + grads.Length;
}

JeremyBois avatar Jun 16 '21 13:06 JeremyBois

You're certain that variables[1] is never zero?

Also, without looking into it, I'm unsure why @Martin1994 made the gradient handling on the equality constraint different from the inequality mechanism. See https://github.com/BrannonKing/NLoptNet/blob/72be44053fd4ed933d4a860238c2eccc1c0742e5/NLoptNet/NLoptSolver.cs#L249

BrannonKing avatar Jun 16 '21 15:06 BrannonKing

Actually this is only a dummy example to isolated the issue I have with the problem I want to optimize. In the complete optimization I use a two-point estimation for gradient calculation so this is not a division by zero error. I test again the dummy example provided without division and the problem still persist.

I cannot see any differences between implementation for both equality and inequality (at least on the C# side). There is two different interfaces depending on the chosen optimizer (some required gradient to be computed where others not).

JeremyBois avatar Jun 16 '21 16:06 JeremyBois

I tried to reproduce this with libnlopt. Such an equality constraint neither works with C, but behaves differently. Instead of using up 20 iterations, SLSQP actually exits right after the first iteration with NLOPT_ROUNDOFF_LIMITED error.

#include <stdio.h>
#include <nlopt.h>

int count = 0;

double minObj(unsigned int n, const double* variables, double* gradient, void* data) {
    if (gradient)
    {
        gradient[0] = 0.0;
        gradient[1] = variables[1];
    }

    printf("> Iteration #%d: x = [%f, %f], gradient = [%f, %f]\n", count, variables[0], variables[1], gradient[0], gradient[1]);

    count++;

    return variables[1];
}

double testEq(unsigned int n, const double* variables, double* gradient, void* data) {
    return 4;
}

int main() {
    double zero = 0;
    double finalScore;
    nlopt_result result;

    nlopt_opt opt = nlopt_create(NLOPT_LD_SLSQP, 2);
    if (!opt) {
        printf("Unable to initialize the algorithm.\n");
        return 1;
    }

    result = nlopt_set_xtol_rel(opt, 0.0001);
    if (result != NLOPT_SUCCESS) {
        printf("Unable to set primary tolerance. Result: %d\n", result);
        return 1;
    }

    result = nlopt_set_maxeval(opt, 20);
    if (result != NLOPT_SUCCESS) {
        printf("Unable to set max evaluation. Result: %d\n", result);
        return 1;
    }

    double initialValue[] = { 1.234, 5.678 };

    double minimums[] = { -1.0 / 0.0, 0.0000000000001 };
    result = nlopt_set_lower_bounds(opt, minimums);
    if (result != NLOPT_SUCCESS) {
        printf("Unable to set lower bounds. Result: %d\n", result);
        return 1;
    }

    result = nlopt_set_min_objective(opt, minObj, &zero);
    if (result != NLOPT_SUCCESS) {
        printf("Unable to set the objective function. Result: %d\n", result);
    }

    // @BUG This is the line that broke everything
    nlopt_add_equality_constraint(opt, testEq, &zero, 0.001);
    // @BUG This is the line that broke everything

    result = nlopt_optimize(opt, initialValue, &finalScore);

    nlopt_destroy(opt);

    printf("result: %d\n", result);
}

Output:

> Iteration #0: x = [1.234000, 5.678000], gradient = [0.000000, 5.678000]
result: -4

Martin1994 avatar Jun 17 '21 07:06 Martin1994

A more complete code example using numerical two point differentiation for gradient calculation and a equality constraint that is always respected (always 0.0).

  • Works without the equality constraint
  • Breaks with the equality constraint with two different errors

@Martin1994, I also get the ROUNDOFF_LIMITED error code alternating with MAXEVAL_REACHED (see outputs below) Can you confirm you get the same behaviour with this code in C++. If this is the case, maybe we could open an issue directly on the NLopt repository because it will not be related to marshalling issues.

Source code (C#)

public void TestSLSQP()
{
    var gradientsHistory = new List<double[]>();
    var variablesHistory = new List<double[]>();
    double? finalScore;
    NloptResult result;
    int count = 0;

    using (var solver = new NLoptSolver(NLoptAlgorithm.LD_SLSQP, 2, maximumIterations: 20))
    {
        var initialValue = new[] { 1.234, 5.678 };
        solver.SetLowerBounds(new[] { 1.0, 1.0 });
        solver.SetUpperBounds(new[] { 10.0, 10.0 });

        solver.SetMinObjective((variables, gradient) =>
        {
            var h = 0.00001;
            var f0 = Math.Pow(variables[1] * variables[0], 2);
            var fi0 = Math.Pow(variables[1] * (variables[0] + h), 2);
            var fi1 = Math.Pow((variables[1] + h) * variables[0], 2);

            if (gradient != null)
            {
                // two point numerical differentiation
                gradient[0] = (fi0 - f0) / h;
                gradient[1] = (fi1 - f0) / h;
            }
            count++;

            var iterationGradients = new double[2];
            Array.Copy(gradient, iterationGradients, 2);
            gradientsHistory.Add(iterationGradients);

            var iterationValues = new double[2];
            Array.Copy(variables, iterationValues, 2);
            variablesHistory.Add(iterationValues);

            return f0;
        });

        // @BUG This is the line that broke everything
        solver.AddEqualZeroConstraint(TestEq);
        // @BUG This is the line that broke everything

        result = solver.Optimize(initialValue, out finalScore);
        Debug.Log($"Target value = {finalScore.Value} (nloptCode={result})");
    }

    for (int i = 0; i < count; i++)
    {
        Debug.Log($"#{i} --> variables=({variablesHistory[i][0]}, {variablesHistory[i][1]}) - gradient=({gradientsHistory[i][0]}, {gradientsHistory[i][1]})");
    }
}

private double TestEq(double[] values, double[] grads)
{
    return 0.0;
}

Outputs

// Without equality constraint
Target value = 1 (nloptCode=XTOL_REACHED)
#0 --> variables=(1.234, 5.678) - gradient=(79.5678625088669, 17.2924323628365)
#1 --> variables=(1, 1) - gradient=(2.00001000001393, 2.00001000001393)

// With equality constraint (happens from time to time ...)
Target value = 49.093172249104 (nloptCode=MAXEVAL_REACHED)
#0 --> variables=(1.234, 5.678) - gradient=(79.5678625088669, 17.2924323628365)
#1 --> variables=(NaN, NaN) - gradient=(NaN, NaN)
...
#19 --> variables=(NaN, NaN) - gradient=(NaN, NaN)

// With equality constraint (happens from time to time ...)
Target value = 49.093172249104 (nloptCode=ROUNDOFF_LIMITED)
#0 --> variables=(1.234, 5.678) - gradient=(79.5678625088669, 17.2924323628365)

JeremyBois avatar Jun 17 '21 09:06 JeremyBois

Open an issue directly to nlopt repository. See stevengj/nlopt#400.

JeremyBois avatar Jun 22 '21 09:06 JeremyBois