Gurobi fails to properly report an ERROR, when NonConvex is 0

I discover this behavior of Gurobi, which I think is not exactly expected.
According to Gurobi’s doc, if NonConvex is set to 0, then Gurobi should ERROR when a nonconvex quadratic constraint is present.

julia> import JuMP, Gurobi; GRB_ENV = Gurobi.Env();

julia> using LinearAlgebra

julia> A = [0.6500000000000001 0.4330127018922193; 0.4330127018922193 0.24999999999999994];

julia> eigen(A)
Eigen{Float64, Float64, Matrix{Float64}, Vector{Float64}}
values:
2-element Vector{Float64}:
 -0.02696960070847279
  0.9269696007084729

julia> model = JuMP.Model(() -> Gurobi.Optimizer(GRB_ENV));

julia> JuMP.@variable(model, x[1:2]);

julia> JuMP.@variable(model, z);

julia> JuMP.set_attribute(model, "Nonconvex", 0) # I explicitly set this
Set parameter NonConvex to value 0

julia> JuMP.@constraint(model, z ≥ x' * A * x) # nonconvex
-0.6500000000000001 x[1]² - 0.8660254037844386 x[2]*x[1] - 0.24999999999999994 x[2]² + z >= 0

julia> JuMP.optimize!(model) # 🔴 I think Gurobi should ERROR here, but it didn't
Set parameter NonConvex to value 0
Gurobi Optimizer version 12.0.2 build v12.0.2rc0 (win64 - Windows 11.0 (26100.2))

CPU model: 11th Gen Intel(R) Core(TM) i5-1135G7 @ 2.40GHz, instruction set [SSE2|AVX|AVX2|AVX512]
Thread count: 4 physical cores, 8 logical processors, using up to 8 threads

Non-default parameters:
NonConvex  0

Optimize a model with 0 rows, 3 columns and 0 nonzeros
Model fingerprint: 0x36f7eae6
Model has 1 quadratic constraint
Coefficient statistics:
  Matrix range     [0e+00, 0e+00]
  QMatrix range    [2e-01, 9e-01]
  QLMatrix range   [1e+00, 1e+00]
  Objective range  [0e+00, 0e+00]
  Bounds range     [0e+00, 0e+00]
  RHS range        [0e+00, 0e+00]
Presolve removed 0 rows and 3 columns
Presolve time: 0.00s
Presolve: All rows and columns removed

Barrier solved model in 0 iterations and 0.00 seconds (0.00 work units)
Optimal objective 0.00000000e+00

User-callback calls 28, time in user-callback 0.00 sec

julia> JuMP.@objective(model, Min, z);

julia> JuMP.optimize!(model) # By comparison, this is expected
Gurobi Optimizer version 12.0.2 build v12.0.2rc0 (win64 - Windows 11.0 (26100.2))

CPU model: 11th Gen Intel(R) Core(TM) i5-1135G7 @ 2.40GHz, instruction set [SSE2|AVX|AVX2|AVX512]
Thread count: 4 physical cores, 8 logical processors, using up to 8 threads

Non-default parameters:
NonConvex  0

Optimize a model with 0 rows, 3 columns and 0 nonzeros
Model fingerprint: 0x4893b5bf
Model has 1 quadratic constraint
Coefficient statistics:
  Matrix range     [0e+00, 0e+00]
  QMatrix range    [2e-01, 9e-01]
  QLMatrix range   [1e+00, 1e+00]
  Objective range  [1e+00, 1e+00]
  Bounds range     [0e+00, 0e+00]
  RHS range        [0e+00, 0e+00]

User-callback calls 19, time in user-callback 0.00 sec
ERROR: Gurobi Error 10020: Objective Q not PSD (diagonal adjustment of 7.7e-02 would be required). Set NonConvex parameter to -1 or 2 to solve model.

Here is a sharper example

julia> model = JuMP.Model(() -> Gurobi.Optimizer(GRB_ENV))

julia> JuMP.@variable(model, x)

julia> JuMP.@variable(model, y)

julia> JuMP.set_attribute(model, "NonConvex", 0)

julia> JuMP.@constraint(model, y ≤ x^2)

julia> JuMP.optimize!(model) # 🔴 No ERROR can be seen
Set parameter NonConvex to value 0
Gurobi Optimizer version 12.0.2 build v12.0.2rc0 (win64 - Windows 11.0 (26100.2))

CPU model: 11th Gen Intel(R) Core(TM) i5-1135G7 @ 2.40GHz, instruction set [SSE2|AVX|AVX2|AVX512]
Thread count: 4 physical cores, 8 logical processors, using up to 8 threads

Non-default parameters:
NonConvex  0

Optimize a model with 0 rows, 2 columns and 0 nonzeros
Model fingerprint: 0x27adffff
Model has 1 quadratic constraint
Coefficient statistics:
  Matrix range     [0e+00, 0e+00]
  QMatrix range    [1e+00, 1e+00]
  QLMatrix range   [1e+00, 1e+00]
  Objective range  [0e+00, 0e+00]
  Bounds range     [0e+00, 0e+00]
  RHS range        [0e+00, 0e+00]
Presolve removed 0 rows and 2 columns
Presolve time: 0.00s
Presolve: All rows and columns removed

Barrier solved model in 0 iterations and 0.00 seconds (0.00 work units)
Optimal objective 0.00000000e+00

User-callback calls 28, time in user-callback 0.00 sec

Note

Presolve: All rows and columns removed

I assume Gurobi does not error because it can solve the problem.

You have free variables and no objective, so I’m assuming Gurobi just drops the nonconvex constraint altogether during presolve.

Have a look at this example, in which Gurobi properly throws ERROR

julia> model = JuMP.Model(() -> Gurobi.Optimizer(GRB_ENV));

julia> JuMP.@variable(model, x[1:2]); # free variable

julia> JuMP.@constraint(model, sum(x -> x^2, x) ≥ 2); # nonconvex

julia> JuMP.set_attribute(model, "NonConvex", 0);

julia> JuMP.optimize!(model)
Set parameter NonConvex to value 0
Gurobi Optimizer version 12.0.2 build v12.0.2rc0 (win64 - Windows 11.0 (26100.2))

CPU model: 11th Gen Intel(R) Core(TM) i5-1135G7 @ 2.40GHz, instruction set [SSE2|AVX|AVX2|AVX512]
Thread count: 4 physical cores, 8 logical processors, using up to 8 threads

Non-default parameters:
NonConvex  0

Optimize a model with 0 rows, 2 columns and 0 nonzeros
Model fingerprint: 0x89cce529
Model has 1 quadratic constraint
Coefficient statistics:
  Matrix range     [0e+00, 0e+00]
  QMatrix range    [1e+00, 1e+00]
  Objective range  [0e+00, 0e+00]
  Bounds range     [0e+00, 0e+00]
  RHS range        [0e+00, 0e+00]
  QRHS range       [2e+00, 2e+00]

User-callback calls 19, time in user-callback 0.00 sec
ERROR: Gurobi Error 10020: Constraint Q not PSD (diagonal adjustment of 1.0e+00 would be required). Set NonConvex parameter to -1 or 2 to solve model.

He can drop it in order to infer that the model is feasible (thus OPTIMAL due to the absence of an objective).
But he still need to cope with that nonconvex constraint—he needs to calculate a feasible point—he cannot randomly give me a solution like (x = 0, y = 1) (which violates my nonconvex constraint). At this phase I think Gurobi should ERROR.

You’re thinking too much about the edge cases of Gurobi’s behavior. If you have a nonconvex model AND you set the nonconvex parameter to 0 AND it can be solved in presolve then gurobi will report optimal. If it cannot be presolved to optimality Gurobi will error. This behavior seems like it is intended and it is not a bug.