JuMP/CPLEX: confusion with number of cuts added in callback

Dear folks,
I use JuMP (v0.21.4) and CPLEX (v0.7.3) to solve a MILP with lazy and user cuts added in a generic callback. However, I am confused with the output. To obtain the number of cuts after solving, I wrote the function:

function cpx_getnumcuts( model::Model, cuttype::Int )
    moi_model = backend(model)

    data_p = Ref{Cint}()
    ret = CPXgetnumcuts(moi_model.env, moi_model.lp, cuttype, data_p)

    if ret != 0
        @warn "error retrieving $cuttype"
    end
    return data_p[]::Int32
end

cuttype with values CPLEX.CPX_CUT_TABLE or CPLEX.CPX_CUT_USER should return the number of lazy and user cuts, respectively, as far as I understand the cplex documentation. Now there are two issues:
(i) If I deactivate adding user cuts, the function with cuttype = CPLEX.CPX_CUT_TABLE always returns zero whereas the returned value coincides with the CPLEX output “User cuts applied:” if cuttype = CPLEX.CPX_CUT_USER . It is the same behavior when the user cuts are activated.

(ii) When I submit a cut, I also count the number of submissions manually, e.g.,

MOI.submit(model, MOI.LazyConstraint(cb_data), con)
nLCUTS += 1

and

MOI.submit(model, MOI.UserCut(cb_data), con)
nUCUTS += 1

However, the number of cuts never coincide, for instance, here is an output of an instance

nCUTS: 76   #return value of the above function with cuttype = CPLEX.CPX_CUT_USER
nLCUTS: 550
nUCUTS: 17

Which values are correct? And/or how can I retrieve the correct numbers of added lazy and user cuts (separately)?

Many thanks in advance, mike_k

The operative word in the documentation is “in use”:

the number of cuts of the specified type in use at the end of the previous optimization

CPLEX can decide not to add cuts that you submit, provided the final solution respects all cuts that were submitted.

I don’t understand why nCUTS is less than nUCUTS though.

Thank you odow. However, I still do not understand why the number of lazy constraints is retrieved with CPLEX.CPX_CUT_USER and not with CPLEX.CPX_CUT_TABLE as stated in the documentation. Here is an MWE (the example from the CPLEX.jl page together with the above function):

using JuMP, CPLEX

function cpx_getnumcuts( model::Model, cuttype::Int )
    moi_model = backend( model )

    data_p = Ref{Cint}()
    ret = CPXgetnumcuts(moi_model.env, moi_model.lp, cuttype, data_p)

    if ret != 0
        @warn "error retrieving $cuttype"
    end
    return data_p[]::Int32
end

model = direct_model(CPLEX.Optimizer())
set_silent(model)

# This is very, very important!!! Only use callbacks in single-threaded mode.
MOI.set(model, MOI.NumberOfThreads(), 1)

@variable(model, 0 <= x <= 2.5, Int)
@variable(model, 0 <= y <= 2.5, Int)
@objective(model, Max, y)
cb_calls = Clong[]
function my_callback_function(cb_data::CPLEX.CallbackContext, context_id::Clong)
    # You can reference variables outside the function as normal
    push!(cb_calls, context_id)
    # You can select where the callback is run
    if context_id != CPX_CALLBACKCONTEXT_CANDIDATE
        return
    end
    ispoint_p = Ref{Cint}()
    ret = CPXcallbackcandidateispoint(cb_data, ispoint_p)
    if ret != 0 || ispoint_p[] == 0
        return  # No candidate point available or error
    end
    # You can query CALLBACKINFO items
    valueP = Ref{Cdouble}()
    ret = CPXcallbackgetinfodbl(cb_data, CPXCALLBACKINFO_BEST_BND, valueP)
    @info "Best bound is currently: $(valueP[])"
    # As well as any other C API
    x_p = Vector{Cdouble}(undef, 2)
    obj_p = Ref{Cdouble}()
    ret = CPXcallbackgetincumbent(cb_data, x_p, 0, 1, obj_p)
    if ret == 0
        @info "Objective incumbent is: $(obj_p[])"
        @info "Incumbent solution is: $(x_p)"
        # Use CPLEX.column to map between variable references and the 1-based
        # column.
        x_col = CPLEX.column(cb_data, index(x))
        @info "x = $(x_p[x_col])"
    else
        # Unable to query incumbent.
    end

    # Before querying `callback_value`, you must call:
    CPLEX.load_callback_variable_primal(cb_data, context_id)
    x_val = callback_value(cb_data, x)
    y_val = callback_value(cb_data, y)
    # You can submit solver-independent MathOptInterface attributes such as
    # lazy constraints, user-cuts, and heuristic solutions.
    if y_val - x_val > 1 + 1e-6
        con = @build_constraint(y - x <= 1)
        MOI.submit(model, MOI.LazyConstraint(cb_data), con)
    elseif y_val + x_val > 3 + 1e-6
        con = @build_constraint(y + x <= 3)
        MOI.submit(model, MOI.LazyConstraint(cb_data), con)
    end
end
MOI.set(model, CPLEX.CallbackFunction(), my_callback_function)
optimize!(model)

nLCUTS  = cpx_getnumcuts( model, CPLEX.CPX_CUT_TABLE )
@info "nLCUTS: $nLCUTS "
nUCUTS  = cpx_getnumcuts( model, CPLEX.CPX_CUT_USER )
@info "nUCUTS: $nUCUTS "

yields:

[ Info: Best bound is currently: 2.0
[ Info: Objective incumbent is: 0.0
[ Info: Incumbent solution is: [0.0, 0.0]
[ Info: x = 0.0
[ Info: nLCUTS: 0 
[ Info: nUCUTS: 1 

MOI.LazyConstraint calls https://www.ibm.com/support/knowledgecenter/SSSA5P_12.10.0/ilog.odms.cplex.help/refcallablelibrary/cpxapi/callbackrejectcandidate.html.

It CPLEX is returning the wrong number, that’s a bug in CPLEX or their documentation. You should contact IBM support. (Note that CPLEX.jl is maintained by the community, and is not a product of IBM.)