diff --git a/docs/src/numerical.md b/docs/src/numerical.md index f8a5a67..ed38aff 100644 --- a/docs/src/numerical.md +++ b/docs/src/numerical.md @@ -31,9 +31,10 @@ ModelAnalyzer.Numerical.SmallObjectiveCoefficient ModelAnalyzer.Numerical.LargeObjectiveCoefficient ModelAnalyzer.Numerical.SmallObjectiveQuadraticCoefficient ModelAnalyzer.Numerical.LargeObjectiveQuadraticCoefficient -ModelAnalyzer.Numerical.NonconvexQuadraticConstraint ModelAnalyzer.Numerical.SmallMatrixQuadraticCoefficient ModelAnalyzer.Numerical.LargeMatrixQuadraticCoefficient +ModelAnalyzer.Numerical.NonconvexQuadraticObjective +ModelAnalyzer.Numerical.NonconvexQuadraticConstraint ``` These issues are saved in the data structure that is returned from the `ModelAnalyzer.analyze` function: diff --git a/src/feasibility.jl b/src/feasibility.jl index e96a04b..cb38584 100644 --- a/src/feasibility.jl +++ b/src/feasibility.jl @@ -815,7 +815,8 @@ function _analyze_complementarity!(model, data) obj = JuMP.constraint_object(con) func = obj.func set = obj.set - func_val = JuMP.value(x -> data.primal_point[x], func) - _set_value(set) + func_val = + JuMP.value.(x -> data.primal_point[x], func) - _set_value(set) comp_val = MOI.Utilities.set_dot(func_val, data.dual_point[con], set) if abs(comp_val) > data.atol push!(data.complementarity, ComplemetarityViolation(con, comp_val)) @@ -824,9 +825,14 @@ function _analyze_complementarity!(model, data) return end -function _set_value(set::MOI.AbstractScalarSet) - return 0.0 -end +# not needed because it would have stoped in dualization before +# function _set_value(set::MOI.AbstractScalarSet) +# return 0.0 +# end +# function _set_value(set::MOI.Interval) +# error("Interval sets are not supported.") +# return (set.lower, set.upper) +# end function _set_value(set::MOI.AbstractVectorSet) return zeros(MOI.dimension(set)) @@ -844,11 +850,6 @@ function _set_value(set::MOI.EqualTo) return set.value end -function _set_value(set::MOI.Interval) - error("Interval sets are not supported.") - return (set.lower, set.upper) -end - function _analyze_objectives!( model::JuMP.GenericModel{T}, dual_model, @@ -915,13 +916,9 @@ function _analyze_objectives!( end ### + +# unsafe as is its checked upstream function _last_primal_solution(model::JuMP.GenericModel) - if !JuMP.has_values(model) - error( - "No primal solution is available. You must provide a point at " * - "which to check feasibility.", - ) - end return Dict(v => JuMP.value(v) for v in JuMP.all_variables(model)) end @@ -1138,137 +1135,13 @@ function _fix_ret( return ret end -function _add_with_resize!(vec, val, i) - if i > length(vec) - resize!(vec, i) - end - return vec[i] = val -end - -""" - dual_feasibility_report( - point::Function, - model::GenericModel{T}; - atol::T = zero(T), - skip_missing::Bool = false, - ) where {T} - -A form of `dual_feasibility_report` where a function is passed as the first -argument instead of a dictionary as the second argument. - -## Example - -```jldoctest -julia> model = Model(); - -julia> @variable(model, 0.5 <= x <= 1, start = 1.3); TODO - -julia> dual_feasibility_report(model) do v - return dual_start_value(v) - end -Dict{Any, Float64} with 1 entry: - x ≤ 1 => 0.3 TODO -``` -""" -# probablye remove this method -function dual_feasibility_report( - point::Function, - model::JuMP.GenericModel{T}; - atol::T = zero(T), - skip_missing::Bool = false, -) where {T} - if JuMP.num_nonlinear_constraints(model) > 0 - error( - "Nonlinear constraints are not supported. " * - "Use `dual_feasibility_report` instead.", - ) - end - if !skip_missing - constraint_list = JuMP.all_constraints( - model; - include_variable_in_set_constraints = false, - ) - for c in constraint_list - if !haskey(point, c) - error( - "point does not contain a dual for constraint $c. Provide " * - "a dual, or pass `skip_missing = true`.", - ) - end - end - end - dual_model = _dualize2(model) - map = dual_model.ext[:dualization_primal_dual_map].primal_con_dual_var - - dual_var_primal_con = _reverse_primal_con_dual_var_map(map) - - function dual_point(jump_dual_var::JuMP.GenericVariableRef{T}) - # v is a variable in the dual jump model - # we need the associated cosntraint in the primal jump model - moi_dual_var = JuMP.index(jump_dual_var) - moi_primal_con, i = dual_var_primal_con[moi_dual_var] - jump_primal_con = JuMP.constraint_ref_with_index(model, moi_primal_con) - pre_point = point(jump_primal_con) - if ismissing(pre_point) - if !skip_missing - error( - "point does not contain a dual for constraint $jump_primal_con. Provide " * - "a dual, or pass `skip_missing = true`.", - ) - else - return missing - end - end - return point(jump_primal_con)[i] - end - - dual_con_to_violation = JuMP.primal_feasibility_report( - dual_point, - dual_model; - atol = atol, - skip_missing = skip_missing, - ) - - # some dual model constraints are associated with primal model variables (primal_con_dual_var) - # if variable is free - primal_var_dual_con = - dual_model.ext[:dualization_primal_dual_map].primal_var_dual_con - # if variable is bounded - primal_convar_dual_con = - dual_model.ext[:dualization_primal_dual_map].constrained_var_dual - # other dual model constraints (bounds) are associated with primal model constraints (non-bounds) - primal_con_dual_con = - dual_model.ext[:dualization_primal_dual_map].primal_con_dual_con - - dual_con_primal_all = _build_dual_con_primal_all( - primal_var_dual_con, - primal_convar_dual_con, - primal_con_dual_con, - ) - - ret = _fix_ret(dual_con_to_violation, model, dual_con_primal_all) - - return ret -end - -function _reverse_primal_con_dual_var_map(primal_con_dual_var) - dual_var_primal_con = - Dict{MOI.VariableIndex,Tuple{MOI.ConstraintIndex,Int}}() - for (moi_con, vec_vars) in primal_con_dual_var - for (i, moi_var) in enumerate(vec_vars) - dual_var_primal_con[moi_var] = (moi_con, i) - end - end - return dual_var_primal_con -end - function _dualize2( model::JuMP.GenericModel, optimizer_constructor = nothing; kwargs..., ) mode = JuMP.mode(model) - if mode != JuMP.AUTOMATIC + if mode == JuMP.MANUAL error("Dualization does not support solvers in $(mode) mode") end dual_model = JuMP.Model() diff --git a/src/infeasibility.jl b/src/infeasibility.jl index 419d2ed..079c8e1 100644 --- a/src/infeasibility.jl +++ b/src/infeasibility.jl @@ -261,7 +261,9 @@ function ModelAnalyzer.analyze( end iis = iis_elastic_filter(model, optimizer) # for now, only one iis is computed - push!(out.iis, IrreducibleInfeasibleSubset(iis)) + if iis !== nothing + push!(out.iis, IrreducibleInfeasibleSubset(iis)) + end return out end @@ -278,7 +280,7 @@ function iis_elastic_filter(original_model::JuMP.GenericModel, optimizer) println( "iis resolver cannot continue because model is found to be $(status) by the solver", ) - return + return nothing end model, reference_map = JuMP.copy_model(original_model) @@ -315,19 +317,20 @@ function iis_elastic_filter(original_model::JuMP.GenericModel, optimizer) var = collect(keys(func.terms)) coef1 = func.terms[var[1]] coef2 = func.terms[var[2]] - if JuMP.value(var1) > tolerance && JuMP.value(var2) > tolerance + if JuMP.value(var[1]) > tolerance && + JuMP.value(var[2]) > tolerance error("IIS failed due numerical instability") elseif JuMP.value(var[1]) > tolerance has_lower = JuMP.has_lower_bound(var[1]) JuMP.fix(var[1], 0.0; force = true) - # or delete(model, var1) + # or delete(model, var[1]) delete!(constraint_to_affine, con) constraint_to_affine[con] = coef2 * var[2] push!(de_elastisized, (con, var[1], has_lower)) elseif JuMP.value(var[2]) > tolerance has_lower = JuMP.has_lower_bound(var[2]) JuMP.fix(var[2], 0.0; force = true) - # or delete(model, var2) + # or delete(model, var[2]) delete!(constraint_to_affine, con) constraint_to_affine[con] = coef1 * var[1] push!(de_elastisized, (con, var[2], has_lower)) @@ -570,7 +573,7 @@ function ModelAnalyzer._summarize( end function ModelAnalyzer._summarize(io::IO, issue::IrreducibleInfeasibleSubset) - return print(io, "IIS: ", join(map(issue.constraint, _name), ", ")) + return print(io, "IIS: ", join(map(_name, issue.constraint), ", ")) end function ModelAnalyzer._verbose_summarize( @@ -629,7 +632,7 @@ function ModelAnalyzer._verbose_summarize( return print( io, "Irreducible Infeasible Subset: ", - join(map(issue.constraint, _name), ", "), + join(map(_name, issue.constraint), ", "), ) end diff --git a/src/intervals.jl b/src/intervals.jl index beca9a9..1449316 100644 --- a/src/intervals.jl +++ b/src/intervals.jl @@ -66,28 +66,31 @@ function Base.iszero(a::Interval) return iszero(a.hi) && iszero(a.lo) end -Base.:+(a::Interval) = a -Base.:-(a::Interval) = Interval(-a.hi, -a.lo) +# this code is only used for interval += scalar_coef * interval +# so only bivariate + and * are needes -function Base.:+(a::Interval{T}, b::T) where {T<:Real} - return Interval(a.lo + b, a.hi + b) -end -Base.:+(b::T, a::Interval{T}) where {T<:Real} = a + b +# Base.:+(a::Interval) = a +# Base.:-(a::Interval) = Interval(-a.hi, -a.lo) -function Base.:-(a::Interval{T}, b::T) where {T<:Real} - return Interval(a.lo - b, a.hi - b) -end -function Base.:-(b::T, a::Interval{T}) where {T<:Real} - return Interval(b - a.hi, b - a.lo) -end +# function Base.:+(a::Interval{T}, b::T) where {T<:Real} +# return Interval(a.lo + b, a.hi + b) +# end +# Base.:+(b::T, a::Interval{T}) where {T<:Real} = a + b + +# function Base.:-(a::Interval{T}, b::T) where {T<:Real} +# return Interval(a.lo - b, a.hi - b) +# end +# function Base.:-(b::T, a::Interval{T}) where {T<:Real} +# return Interval(b - a.hi, b - a.lo) +# end function Base.:+(a::Interval{T}, b::Interval{T}) where {T<:Real} return Interval(a.lo + b.lo, a.hi + b.hi) end -function Base.:-(a::Interval{T}, b::Interval{T}) where {T<:Real} - return Interval(a.lo - b.hi, a.hi - b.lo) -end +# function Base.:-(a::Interval{T}, b::Interval{T}) where {T<:Real} +# return Interval(a.lo - b.hi, a.hi - b.lo) +# end ## Multiplication function Base.:*(x::T, a::Interval{T}) where {T<:Real} @@ -99,6 +102,6 @@ function Base.:*(x::T, a::Interval{T}) where {T<:Real} end end -Base.:*(a::Interval{T}, x::T) where {T<:Real} = x * a +# Base.:*(a::Interval{T}, x::T) where {T<:Real} = x * a Base.convert(::Type{Interval{T}}, x::T) where {T<:Real} = Interval(x, x) diff --git a/src/numerical.jl b/src/numerical.jl index 62ba782..35d9677 100644 --- a/src/numerical.jl +++ b/src/numerical.jl @@ -279,23 +279,6 @@ struct LargeObjectiveQuadraticCoefficient <: AbstractNumericalIssue coefficient::Float64 end -""" - NonconvexQuadraticConstraint - -The `NonconvexQuadraticConstraint` issue is identified when a quadratic -constraint is non-convex. - -For more information, run: -```julia -julia> ModelAnalyzer.summarize( - ModelAnalyzer.Numerical.NonconvexQuadraticConstraint -) -``` -""" -struct NonconvexQuadraticConstraint <: AbstractNumericalIssue - ref::JuMP.ConstraintRef -end - """ SmallMatrixQuadraticCoefficient <: AbstractNumericalIssue @@ -336,6 +319,38 @@ struct LargeMatrixQuadraticCoefficient <: AbstractNumericalIssue coefficient::Float64 end +""" + NonconvexQuadraticObjective <: AbstractNumericalIssue + +The `NonconvexQuadraticObjective` issue is identified when a quadratic +objective function is non-convex. + +For more information, run: +```julia +julia> ModelAnalyzer.summarize( + ModelAnalyzer.Numerical.NonconvexQuadraticObjective +) +``` +""" +struct NonconvexQuadraticObjective <: AbstractNumericalIssue end + +""" + NonconvexQuadraticConstraint + +The `NonconvexQuadraticConstraint` issue is identified when a quadratic +constraint is non-convex. + +For more information, run: +```julia +julia> ModelAnalyzer.summarize( + ModelAnalyzer.Numerical.NonconvexQuadraticConstraint +) +``` +""" +struct NonconvexQuadraticConstraint <: AbstractNumericalIssue + ref::JuMP.ConstraintRef +end + """ Data @@ -395,7 +410,8 @@ Base.@kwdef mutable struct Data <: ModelAnalyzer.AbstractData has_quadratic_objective::Bool = false objective_quadratic_range::Vector{Float64} = sizehint!(Float64[1.0, 1.0], 2) matrix_quadratic_range::Vector{Float64} = sizehint!(Float64[1.0, 1.0], 2) - nonconvex_objective::Bool = false + nonconvex_objective::Vector{NonconvexQuadraticObjective} = + NonconvexQuadraticObjective[] objective_quadratic_small::Vector{SmallObjectiveQuadraticCoefficient} = SmallObjectiveQuadraticCoefficient[] objective_quadratic_large::Vector{LargeObjectiveQuadraticCoefficient} = @@ -408,82 +424,6 @@ function _update_range(range::Vector{Float64}, value::Number) return 1 end -function _get_constraint_data( - data, - ref::JuMP.ConstraintRef, - func::JuMP.GenericAffExpr; - ignore_empty = false, -) - if length(func.terms) == 1 - if isapprox(first(values(func.terms)), 1.0) - push!(data.bound_rows, VariableBoundAsConstraint(ref)) - data.matrix_nnz += 1 - return - end - end - nnz = 0 - for (variable, coefficient) in func.terms - if iszero(coefficient) - continue - end - nnz += _update_range(data.matrix_range, coefficient) - if abs(coefficient) < data.threshold_small - push!( - data.matrix_small, - SmallMatrixCoefficient(ref, variable, coefficient), - ) - elseif abs(coefficient) > data.threshold_large - push!( - data.matrix_large, - LargeMatrixCoefficient(ref, variable, coefficient), - ) - end - push!(data.variables_in_constraints, variable) - end - if nnz == 0 - if !ignore_empty - push!(data.empty_rows, EmptyConstraint(ref)) - end - return - end - if nnz / data.number_of_variables > data.threshold_dense_fill_in && - nnz > data.threshold_dense_entries - push!(data.dense_rows, DenseConstraint(ref, nnz)) - end - data.matrix_nnz += nnz - return -end - -function _get_constraint_data( - data, - ref::JuMP.ConstraintRef, - func::JuMP.GenericQuadExpr, -) - nnz = 0 - for (v, coefficient) in func.terms - if iszero(coefficient) - continue - end - nnz += _update_range(data.matrix_quadratic_range, coefficient) - if abs(coefficient) < data.threshold_small - push!( - data.matrix_quadratic_small, - SmallMatrixQuadraticCoefficient(ref, v.a, v.b, coefficient), - ) - elseif abs(coefficient) > data.threshold_large - push!( - data.matrix_quadratic_large, - LargeMatrixQuadraticCoefficient(ref, v.a, v.b, coefficient), - ) - end - push!(data.variables_in_constraints, v.a) - push!(data.variables_in_constraints, v.b) - end - data.has_quadratic_constraints = true - _get_constraint_data(data, ref, func.aff, ignore_empty = nnz > 0) - return -end - function _get_variable_data(data, variable, coefficient::Number) if !(iszero(coefficient)) _update_range(data.bounds_range, coefficient) @@ -546,9 +486,13 @@ function _get_objective_data(data, func::JuMP.GenericQuadExpr) end data.has_quadratic_objective = true if data.sense == JuMP.MAX_SENSE - data.nonconvex_objective = !_quadratic_vexity(func, -1) + if !_quadratic_vexity(func, -1) + push!(data.nonconvex_objective, NonconvexQuadraticObjective()) + end elseif data.sense == JuMP.MIN_SENSE - data.nonconvex_objective = !_quadratic_vexity(func, 1) + if !_quadratic_vexity(func, 1) + push!(data.nonconvex_objective, NonconvexQuadraticObjective()) + end end return end @@ -577,14 +521,100 @@ function _quadratic_vexity(func::JuMP.GenericQuadExpr, sign::Int) return LinearAlgebra.issuccess(ret) end -function _get_constraint_data(data, func::Vector{JuMP.GenericAffExpr}, set) +function _get_constraint_matrix_data( + data, + ref::JuMP.ConstraintRef, + func::JuMP.GenericAffExpr; + ignore_extras = false, +) + if length(func.terms) == 1 + if !ignore_extras && isapprox(first(values(func.terms)), 1.0) + push!(data.bound_rows, VariableBoundAsConstraint(ref)) + data.matrix_nnz += 1 + # in this case we do not count that the variable is in a constraint + return + end + end + nnz = 0 + for (variable, coefficient) in func.terms + if iszero(coefficient) + continue + end + nnz += _update_range(data.matrix_range, coefficient) + if abs(coefficient) < data.threshold_small + push!( + data.matrix_small, + SmallMatrixCoefficient(ref, variable, coefficient), + ) + elseif abs(coefficient) > data.threshold_large + push!( + data.matrix_large, + LargeMatrixCoefficient(ref, variable, coefficient), + ) + end + push!(data.variables_in_constraints, variable) + end + if nnz == 0 + if !ignore_extras + push!(data.empty_rows, EmptyConstraint(ref)) + end + return + end + if nnz / data.number_of_variables > data.threshold_dense_fill_in && + nnz > data.threshold_dense_entries + push!(data.dense_rows, DenseConstraint(ref, nnz)) + end + data.matrix_nnz += nnz + return +end + +function _get_constraint_matrix_data( + data, + ref::JuMP.ConstraintRef, + func::JuMP.GenericQuadExpr, +) + nnz = 0 + for (v, coefficient) in func.terms + if iszero(coefficient) + continue + end + nnz += _update_range(data.matrix_quadratic_range, coefficient) + if abs(coefficient) < data.threshold_small + push!( + data.matrix_quadratic_small, + SmallMatrixQuadraticCoefficient(ref, v.a, v.b, coefficient), + ) + elseif abs(coefficient) > data.threshold_large + push!( + data.matrix_quadratic_large, + LargeMatrixQuadraticCoefficient(ref, v.a, v.b, coefficient), + ) + end + push!(data.variables_in_constraints, v.a) + push!(data.variables_in_constraints, v.b) + end + data.has_quadratic_constraints = true + _get_constraint_matrix_data(data, ref, func.aff, ignore_extras = nnz > 0) + return +end + +function _get_constraint_matrix_data( + data, + ref, + func::Vector{F}, +) where {F<:Union{JuMP.GenericAffExpr,JuMP.GenericQuadExpr}} for f in func - _get_constraint_data(data, ref, f, set) + _get_constraint_matrix_data(data, ref, f) end return true end -function _get_constraint_data(data, func::Vector{JuMP.GenericQuadExpr}, set) +function _get_constraint_data( + data, + ref, + func::Vector{F}, + set, +) where {F<:Union{JuMP.GenericAffExpr,JuMP.GenericQuadExpr}} for f in func _get_constraint_data(data, ref, f, set) end @@ -605,9 +635,10 @@ function _get_constraint_data(data, ref, func::JuMP.GenericAffExpr, set) return end +# this function is basically checking just the RHS just like the above +# skip additional checks for quadratics in non simples sets function _get_constraint_data(data, ref, func::JuMP.GenericQuadExpr, set) _get_constraint_data(data, ref, func.aff, set) - # skip additional checks for quadratics in non-scalar simples sets return end @@ -615,7 +646,7 @@ function _get_constraint_data( data, ref, func::JuMP.GenericQuadExpr, - set::MOI.LessThan, + set::Union{MOI.LessThan,MOI.Nonpositives}, ) _get_constraint_data(data, ref, func.aff, set) if !_quadratic_vexity(func, 1) @@ -647,7 +678,7 @@ function _get_constraint_data( data, ref, func::JuMP.GenericQuadExpr, - set::MOI.GreaterThan, + set::Union{MOI.GreaterThan,MOI.Nonnegatives}, ) _get_constraint_data(data, ref, func.aff, set) if !_quadratic_vexity(func, -1) @@ -679,7 +710,7 @@ function _get_constraint_data( data, ref, func::JuMP.GenericQuadExpr, - set::Union{MOI.EqualTo,MOI.Interval}, + set::Union{MOI.EqualTo,MOI.Interval,MOI.Zeros}, ) _get_constraint_data(data, ref, func.aff, set) push!(data.nonconvex_rows, NonconvexQuadraticConstraint(ref)) @@ -733,16 +764,13 @@ function _get_constraint_data( return end -function _get_constraint_data(data, func::Vector{JuMP.VariableRef}) +function _get_constraint_matrix_data(data, func::Vector{JuMP.VariableRef}) for var in func push!(data.variables_in_constraints, var) end return end -# Default fallback for unsupported constraints. -_update_range(data, func, set) = false - """ analyze(model::Model; threshold_dense_fill_in = 0.10, threshold_dense_entries = 1000, threshold_small = 1e-5, threshold_large = 1e+5) @@ -779,6 +807,9 @@ function ModelAnalyzer.analyze( if JuMP.has_upper_bound(var) _get_variable_data(data, var, JuMP.upper_bound(var)) end + if JuMP.is_fixed(var) + _get_variable_data(data, var, JuMP.fix_value(var)) + end end # constraints pass for (F, S) in JuMP.list_of_constraint_types(model) @@ -790,13 +821,13 @@ function ModelAnalyzer.analyze( if F == Vector{JuMP.VariableRef} for con in JuMP.all_constraints(model, F, S) con_obj = JuMP.constraint_object(con) - _get_constraint_data(data, con_obj.func) + _get_constraint_matrix_data(data, con_obj.func) end continue end for con in JuMP.all_constraints(model, F, S) con_obj = JuMP.constraint_object(con) - _get_constraint_data(data, con, con_obj.func) + _get_constraint_matrix_data(data, con, con_obj.func) _get_constraint_data(data, con, con_obj.func, con_obj.set) end end @@ -894,10 +925,6 @@ function ModelAnalyzer._summarize( return print(io, "# LargeObjectiveQuadraticCoefficient") end -function ModelAnalyzer._summarize(io::IO, ::Type{NonconvexQuadraticConstraint}) - return print(io, "# NonconvexQuadraticConstraint") -end - function ModelAnalyzer._summarize( io::IO, ::Type{SmallMatrixQuadraticCoefficient}, @@ -912,6 +939,14 @@ function ModelAnalyzer._summarize( return print(io, "# LargeMatrixQuadraticCoefficient") end +function ModelAnalyzer._summarize(io::IO, ::Type{NonconvexQuadraticObjective}) + return print(io, "# NonconvexQuadraticObjective") +end + +function ModelAnalyzer._summarize(io::IO, ::Type{NonconvexQuadraticConstraint}) + return print(io, "# NonconvexQuadraticConstraint") +end + function ModelAnalyzer._verbose_summarize( io::IO, ::Type{VariableNotInConstraints}, @@ -924,7 +959,9 @@ function ModelAnalyzer._verbose_summarize( ## What A `VariableNotInConstraints` issue is identified when a variable appears - in no constraints. + in no constraints. If a variable only appears alone in a constraint and + it has a coefficient of 1 it is considered a + `VariableNotInConstraints`, because this emulates a bound. ## Why @@ -948,6 +985,8 @@ function ModelAnalyzer._verbose_summarize(io::IO, ::Type{EmptyConstraint}) return print( io, """ + # `EmptyConstraint` + ## What An `EmptyConstraint` issue is identified when a constraint has no @@ -978,6 +1017,8 @@ function ModelAnalyzer._verbose_summarize( return print( io, """ + # `VariableBoundAsConstraint` + ## What A `VariableBoundAsConstraint` issue is identified when a constraint is @@ -1005,6 +1046,8 @@ function ModelAnalyzer._verbose_summarize(io::IO, ::Type{DenseConstraint}) return print( io, """ + # `DenseConstraint` + ## What A `DenseConstraint` issue is identified when a constraint has a high @@ -1038,6 +1081,8 @@ function ModelAnalyzer._verbose_summarize( return print( io, """ + # `SmallMatrixCoefficient` + ## What A `SmallMatrixCoefficient` issue is identified when a constraint has a @@ -1069,6 +1114,8 @@ function ModelAnalyzer._verbose_summarize( return print( io, """ + # `LargeMatrixCoefficient` + ## What A `LargeMatrixCoefficient` issue is identified when a constraint has a @@ -1097,6 +1144,8 @@ function ModelAnalyzer._verbose_summarize(io::IO, ::Type{SmallBoundCoefficient}) return print( io, """ + # `SmallBoundCoefficient` + ## What A `SmallBoundCoefficient` issue is identified when a variable has a @@ -1124,6 +1173,8 @@ function ModelAnalyzer._verbose_summarize(io::IO, ::Type{LargeBoundCoefficient}) return print( io, """ + # `LargeBoundCoefficient` + ## What A `LargeBoundCoefficient` issue is identified when a variable has a @@ -1151,6 +1202,8 @@ function ModelAnalyzer._verbose_summarize(io::IO, ::Type{SmallRHSCoefficient}) return print( io, """ + # `SmallRHSCoefficient` + ## What A `SmallRHSCoefficient` issue is identified when a constraint has a @@ -1179,6 +1232,8 @@ function ModelAnalyzer._verbose_summarize(io::IO, ::Type{LargeRHSCoefficient}) return print( io, """ + # `LargeRHSCoefficient` + ## What A `LargeRHSCoefficient` issue is identified when a constraint has a @@ -1210,6 +1265,8 @@ function ModelAnalyzer._verbose_summarize( return print( io, """ + # `SmallObjectiveCoefficient` + ## What A `SmallObjectiveCoefficient` issue is identified when the objective @@ -1241,6 +1298,8 @@ function ModelAnalyzer._verbose_summarize( return print( io, """ + # `LargeObjectiveCoefficient` + ## What A `LargeObjectiveCoefficient` issue is identified when the objective @@ -1272,6 +1331,8 @@ function ModelAnalyzer._verbose_summarize( return print( io, """ + # `SmallObjectiveQuadraticCoefficient` + ## What A `SmallObjectiveQuadraticCoefficient` issue is identified when the @@ -1303,6 +1364,8 @@ function ModelAnalyzer._verbose_summarize( return print( io, """ + # `LargeObjectiveQuadraticCoefficient` + ## What A `LargeObjectiveQuadraticCoefficient` issue is identified when the @@ -1329,25 +1392,61 @@ end function ModelAnalyzer._verbose_summarize( io::IO, - ::Type{NonconvexQuadraticConstraint}, + ::Type{SmallMatrixQuadraticCoefficient}, ) return print( io, """ + # `SmallMatrixQuadraticCoefficient` + ## What - A `NonconvexQuadraticConstraint` issue is identified when a quadratic - constraint is nonconvex, that is, the quadratic matrix is not positive - semidefinite. + A `SmallMatrixQuadraticCoefficient` issue is identified when a quadratic + constraint has a coefficient with a small absolute value. ## Why - Nonconvex constraints are not expected by many solver and can lead to + Small coefficients can lead to numerical instability in the solution + process. + + ## How to fix + + Check if the coefficient is correct. Check if the units of variables and + coefficients are correct. Check if the number makes is + reasonable given that solver have tolerances. Sometimes these + coefficients can be replaced by zeros. + + ## More information + + - https://jump.dev/JuMP.jl/stable/tutorials/getting_started/tolerances/ + """, + ) +end + +function ModelAnalyzer._verbose_summarize( + io::IO, + ::Type{NonconvexQuadraticObjective}, +) + return print( + io, + """ + # `NonconvexQuadraticObjective` + + ## What + + A `NonconvexQuadraticObjective` issue is identified when a quadratic + objective is nonconvex, that is, the quadratic matrix is not positive + semidefinite for minimization or the quadratic matrix is not negative + semidefinite for maximization. + + ## Why + + Nonconvex objectives are not expected by many solver and can lead to wrong solutions or even convergence issues. ## How to fix - Check if the constraint is correct. Coefficient signs might have been + Check if the objective is correct. Coefficient signs might have been inverted. This also occurs if user fix a variable to emulate a parameter, in this case some solvers will not be able to solve the model properly, other tools such as ParametricOptInteface.jl might be @@ -1362,31 +1461,35 @@ end function ModelAnalyzer._verbose_summarize( io::IO, - ::Type{SmallMatrixQuadraticCoefficient}, + ::Type{NonconvexQuadraticConstraint}, ) return print( io, """ + # `NonconvexQuadraticConstraint` + ## What - A `SmallMatrixQuadraticCoefficient` issue is identified when a quadratic - constraint has a coefficient with a small absolute value. + A `NonconvexQuadraticConstraint` issue is identified when a quadratic + constraint is nonconvex, that is, the quadratic matrix is not positive + semidefinite. ## Why - Small coefficients can lead to numerical instability in the solution - process. + Nonconvex constraints are not expected by many solver and can lead to + wrong solutions or even convergence issues. ## How to fix - Check if the coefficient is correct. Check if the units of variables and - coefficients are correct. Check if the number makes is - reasonable given that solver have tolerances. Sometimes these - coefficients can be replaced by zeros. + Check if the constraint is correct. Coefficient signs might have been + inverted. This also occurs if user fix a variable to emulate a + parameter, in this case some solvers will not be able to solve the + model properly, other tools such as ParametricOptInteface.jl might be + more suitable than fixing variables. ## More information - - https://jump.dev/JuMP.jl/stable/tutorials/getting_started/tolerances/ + No extra information for this issue. """, ) end @@ -1398,6 +1501,8 @@ function ModelAnalyzer._verbose_summarize( return print( io, """ + # `LargeMatrixQuadraticCoefficient` + ## What A `LargeMatrixQuadraticCoefficient` issue is identified when a quadratic @@ -1512,10 +1617,6 @@ function ModelAnalyzer._summarize( ) end -function ModelAnalyzer._summarize(io::IO, issue::NonconvexQuadraticConstraint) - return print(io, _name(issue.ref)) -end - function ModelAnalyzer._summarize( io::IO, issue::SmallMatrixQuadraticCoefficient, @@ -1548,6 +1649,14 @@ function ModelAnalyzer._summarize( ) end +function ModelAnalyzer._summarize(io::IO, ::NonconvexQuadraticObjective) + return print(io, "Objective is Nonconvex quadratic") +end + +function ModelAnalyzer._summarize(io::IO, issue::NonconvexQuadraticConstraint) + return print(io, _name(issue.ref)) +end + function ModelAnalyzer._verbose_summarize( io::IO, issue::VariableNotInConstraints, @@ -1697,13 +1806,6 @@ function ModelAnalyzer._verbose_summarize( ) end -function ModelAnalyzer._verbose_summarize( - io::IO, - issue::NonconvexQuadraticConstraint, -) - return print(io, "Constraint: ", _name(issue.ref)) -end - function ModelAnalyzer._verbose_summarize( io::IO, issue::SmallMatrixQuadraticCoefficient, @@ -1738,6 +1840,20 @@ function ModelAnalyzer._verbose_summarize( ) end +function ModelAnalyzer._verbose_summarize( + io::IO, + issue::NonconvexQuadraticObjective, +) + return ModelAnalyzer._summarize(io, issue) +end + +function ModelAnalyzer._verbose_summarize( + io::IO, + issue::NonconvexQuadraticConstraint, +) + return print(io, "Constraint: ", _name(issue.ref)) +end + function ModelAnalyzer.list_of_issues( data::Data, ::Type{VariableNotInConstraints}, @@ -1820,23 +1936,30 @@ end function ModelAnalyzer.list_of_issues( data::Data, - ::Type{NonconvexQuadraticConstraint}, + ::Type{SmallMatrixQuadraticCoefficient}, ) - return data.nonconvex_rows + return data.matrix_quadratic_small end function ModelAnalyzer.list_of_issues( data::Data, - ::Type{SmallMatrixQuadraticCoefficient}, + ::Type{LargeMatrixQuadraticCoefficient}, ) - return data.matrix_quadratic_small + return data.matrix_quadratic_large end function ModelAnalyzer.list_of_issues( data::Data, - ::Type{LargeMatrixQuadraticCoefficient}, + ::Type{NonconvexQuadraticObjective}, ) - return data.matrix_quadratic_large + return data.nonconvex_objective +end + +function ModelAnalyzer.list_of_issues( + data::Data, + ::Type{NonconvexQuadraticConstraint}, +) + return data.nonconvex_rows end function ModelAnalyzer.list_of_issue_types(data::Data) @@ -1856,9 +1979,10 @@ function ModelAnalyzer.list_of_issue_types(data::Data) LargeObjectiveCoefficient, SmallObjectiveQuadraticCoefficient, LargeObjectiveQuadraticCoefficient, - NonconvexQuadraticConstraint, SmallMatrixQuadraticCoefficient, LargeMatrixQuadraticCoefficient, + NonconvexQuadraticConstraint, + NonconvexQuadraticObjective, ) if !isempty(ModelAnalyzer.list_of_issues(data, type)) push!(ret, type) diff --git a/test/feasibility.jl b/test/feasibility.jl index 29f77c4..93a46e4 100644 --- a/test/feasibility.jl +++ b/test/feasibility.jl @@ -33,6 +33,12 @@ function test_no_solution() @test_throws ErrorException ModelAnalyzer.Feasibility.dual_feasibility_report( model, ) + # non linear not accepted + @constraint(model, c, x^4 >= 0) # this will make the model non-linear + @test_throws ErrorException ModelAnalyzer.Feasibility.dual_feasibility_report( + model, + Dict(), # to skip dual solutions error + ) end function test_only_bounds() @@ -46,6 +52,8 @@ function test_only_bounds() Dict(LowerBoundRef(x) => 1.0), ) @test isempty(report) + + return end function test_no_lb() @@ -169,6 +177,64 @@ function test_analyse_simple() return end +function test_analyse_simple_direct() + model = direct_model(HiGHS.Optimizer()) + set_silent(model) + @variable(model, x) + @constraint(model, c, x >= 0) + @objective(model, Min, 2 * x) + + optimize!(model) + + data = ModelAnalyzer.analyze(ModelAnalyzer.Feasibility.Analyzer(), model) + + list = ModelAnalyzer.list_of_issue_types(data) + + @test length(list) == 0 + + return +end + +function test_with_interval() + model = Model(HiGHS.Optimizer) + set_silent(model) + @variable(model, x >= 1) + @constraint(model, c, 2 * x in MOI.Interval(0.0, 3.0)) + @objective(model, Min, x) + optimize!(model) + @test !ModelAnalyzer.Feasibility._can_dualize(model) + # TODO this should eb a waning at least + data = ModelAnalyzer.analyze(ModelAnalyzer.Feasibility.Analyzer(), model) + return +end + +function test_analyse_many_constraint_types() + model = Model(HiGHS.Optimizer) + set_silent(model) + @variable(model, x >= 1) + @variable(model, y <= 0) + @variable(model, z == 0) + @variable(model, w) + @variable(model, 0 <= v <= 1) + @constraint(model, c1, x >= 0) + @constraint(model, c2, x <= 10) + @constraint(model, c3, x == 5) + @constraint(model, c4, y >= -1) + @constraint(model, c5, w == 3) + @constraint(model, c6, [2v] in Nonpositives()) # this should be redundant as z is fixed to 0 + @objective(model, Min, x) + + optimize!(model) + + data = ModelAnalyzer.analyze(ModelAnalyzer.Feasibility.Analyzer(), model) + + list = ModelAnalyzer.list_of_issue_types(data) + + @test length(list) == 0 + + return +end + function test_analyse_mip() model = Model(HiGHS.Optimizer) set_silent(model) @@ -184,7 +250,13 @@ function test_analyse_mip() @test length(list) == 0 - ModelAnalyzer.summarize(data) + buf = IOBuffer() + ModelAnalyzer.summarize(buf, data) + + buf = IOBuffer() + Base.show(buf, data) + str = String(take!(buf)) + @test str == "Feasibility analysis found 0 issues" return end @@ -196,11 +268,20 @@ function test_analyse_no_opt() @constraint(model, c, x >= 0) @objective(model, Min, x) + # test no primal point @test_throws ErrorException ModelAnalyzer.analyze( ModelAnalyzer.Feasibility.Analyzer(), model, ) + # test no dual point + @test_throws ErrorException ModelAnalyzer.analyze( + ModelAnalyzer.Feasibility.Analyzer(), + model, + primal_point = Dict(x => 1.0), + dual_check = true, + ) + data = ModelAnalyzer.analyze( ModelAnalyzer.Feasibility.Analyzer(), model, @@ -260,9 +341,44 @@ function test_analyse_no_opt() ret = ModelAnalyzer.list_of_issues(data, list[4]) @test ret[1] == ModelAnalyzer.Feasibility.PrimalDualMismatch(-1.0, 0.0) - ModelAnalyzer.summarize(data) + buf = IOBuffer() - ModelAnalyzer.summarize(data, verbose = false) + ModelAnalyzer.summarize(buf, data) + + ModelAnalyzer.summarize(buf, data, verbose = false) + + return +end + +# these tests are harder to permorm with a real solver as they tipically +# return coherent objectives +function test_lowlevel_mismatch() + buf = IOBuffer() + issues = [] + push!(issues, ModelAnalyzer.Feasibility.PrimalObjectiveMismatch(0.0, 1.0)) + push!(issues, ModelAnalyzer.Feasibility.DualObjectiveMismatch(0.0, 1.0)) + push!(issues, ModelAnalyzer.Feasibility.PrimalDualSolverMismatch(0.0, 1.0)) + for verbose in (true, false) + ModelAnalyzer.summarize( + buf, + ModelAnalyzer.Feasibility.PrimalObjectiveMismatch, + verbose = verbose, + ) + ModelAnalyzer.summarize( + buf, + ModelAnalyzer.Feasibility.DualObjectiveMismatch, + verbose = verbose, + ) + ModelAnalyzer.summarize( + buf, + ModelAnalyzer.Feasibility.PrimalDualSolverMismatch, + verbose = verbose, + ) + for issue in issues + # ensure we can summarize each issue type + ModelAnalyzer.summarize(buf, issue, verbose = verbose) + end + end return end diff --git a/test/infeasibility.jl b/test/infeasibility.jl index fde6f77..1d1ce2c 100644 --- a/test/infeasibility.jl +++ b/test/infeasibility.jl @@ -34,6 +34,31 @@ function test_bounds() @test length(ret) == 1 @test ret[] == ModelAnalyzer.Infeasibility.InfeasibleBounds{Float64}(y, 2.0, 1.0) + # + buf = IOBuffer() + ModelAnalyzer.summarize( + buf, + ModelAnalyzer.Infeasibility.InfeasibleBounds{Float64}, + ) + str = String(take!(buf)) + @test startswith(str, "# `InfeasibleBounds`") + ModelAnalyzer.summarize( + buf, + ModelAnalyzer.Infeasibility.InfeasibleBounds{Float64}, + verbose = false, + ) + str = String(take!(buf)) + @test str == "# InfeasibleBounds" + # + ModelAnalyzer.summarize(buf, ret[1], verbose = true) + str = String(take!(buf)) + @test startswith(str, "Variable: ") + @test contains(str, " with lower bound ") + @test contains(str, " and upper bound ") + ModelAnalyzer.summarize(buf, ret[1], verbose = false) + str = String(take!(buf)) + @test contains(str, " : ") + @test contains(str, " !<= ") return end @@ -54,6 +79,33 @@ function test_integrality() 2.9, MOI.Integer(), ) + # + buf = IOBuffer() + ModelAnalyzer.summarize( + buf, + ModelAnalyzer.Infeasibility.InfeasibleIntegrality{Float64}, + ) + str = String(take!(buf)) + @test startswith(str, "# `InfeasibleIntegrality`") + ModelAnalyzer.summarize( + buf, + ModelAnalyzer.Infeasibility.InfeasibleIntegrality{Float64}, + verbose = false, + ) + str = String(take!(buf)) + @test str == "# InfeasibleIntegrality" + # + ModelAnalyzer.summarize(buf, ret[1], verbose = true) + str = String(take!(buf)) + @test startswith(str, "Variable: ") + @test contains(str, " with lower bound ") + @test contains(str, " and upper bound ") + @test contains(str, " and integrality constraint: ") + ModelAnalyzer.summarize(buf, ret[1], verbose = false) + str = String(take!(buf)) + @test contains(str, " : [") + @test contains(str, "; ") + @test contains(str, "], ") return end @@ -95,9 +147,158 @@ function test_range() 22.0, MOI.LessThan{Float64}(1.0), ) + # + buf = IOBuffer() + ModelAnalyzer.summarize( + buf, + ModelAnalyzer.Infeasibility.InfeasibleConstraintRange{Float64}, + ) + str = String(take!(buf)) + @test startswith(str, "# `InfeasibleConstraintRange`") + ModelAnalyzer.summarize( + buf, + ModelAnalyzer.Infeasibility.InfeasibleConstraintRange{Float64}, + verbose = false, + ) + str = String(take!(buf)) + @test str == "# InfeasibleConstraintRange" + # + ModelAnalyzer.summarize(buf, ret[1], verbose = true) + str = String(take!(buf)) + @test startswith(str, "Constraint: ") + @test contains(str, " with computed lower bound ") + @test contains(str, " and computed upper bound ") + @test contains(str, " and set: ") + ModelAnalyzer.summarize(buf, ret[1], verbose = false) + str = String(take!(buf)) + @test contains(str, " : [") + @test contains(str, "; ") + @test contains(str, "], !in ") + return +end + +function test_range_neg() + model = Model() + @variable(model, 10 <= x <= 11) + @variable(model, -11 <= y <= -1) + @constraint(model, c, x - y <= 1) + @objective(model, Max, x + y) + data = ModelAnalyzer.analyze(ModelAnalyzer.Infeasibility.Analyzer(), model) + list = ModelAnalyzer.list_of_issue_types(data) + @test length(list) == 1 + ret = ModelAnalyzer.list_of_issues(data, list[1]) + @test length(ret) == 1 + @test ret[] == + ModelAnalyzer.Infeasibility.InfeasibleConstraintRange{Float64}( + c, + 11.0, + 22.0, + MOI.LessThan{Float64}(1.0), + ) + return +end + +function test_range_equalto() + model = Model() + @variable(model, x == 1) + @variable(model, y == 2) + @constraint(model, c, x + y == 1) + @objective(model, Max, x + y) + data = ModelAnalyzer.analyze(ModelAnalyzer.Infeasibility.Analyzer(), model) + list = ModelAnalyzer.list_of_issue_types(data) + @test length(list) == 1 + ret = ModelAnalyzer.list_of_issues(data, list[1]) + @test length(ret) == 1 + @test ret[] == + ModelAnalyzer.Infeasibility.InfeasibleConstraintRange{Float64}( + c, + 3.0, + 3.0, + MOI.EqualTo{Float64}(1.0), + ) + return +end + +function test_range_equalto() + model = Model() + @variable(model, x == 1) + @variable(model, y == 2) + @constraint(model, c, 3x + 2y == 1) + @objective(model, Max, x + y) + data = ModelAnalyzer.analyze(ModelAnalyzer.Infeasibility.Analyzer(), model) + list = ModelAnalyzer.list_of_issue_types(data) + @test length(list) == 1 + ret = ModelAnalyzer.list_of_issues(data, list[1]) + @test length(ret) == 1 + @test ret[] == + ModelAnalyzer.Infeasibility.InfeasibleConstraintRange{Float64}( + c, + 7.0, + 7.0, + MOI.EqualTo{Float64}(1.0), + ) + return +end + +function test_range_greaterthan() + model = Model() + @variable(model, 10 <= x <= 11) + @variable(model, 1 <= y <= 11) + @constraint(model, c, x + y >= 100) + @objective(model, Max, x + y) + data = ModelAnalyzer.analyze(ModelAnalyzer.Infeasibility.Analyzer(), model) + list = ModelAnalyzer.list_of_issue_types(data) + @test length(list) == 1 + ret = ModelAnalyzer.list_of_issues(data, list[1]) + @test length(ret) == 1 + @test ret[] == + ModelAnalyzer.Infeasibility.InfeasibleConstraintRange{Float64}( + c, + 11.0, + 22.0, + MOI.GreaterThan{Float64}(100.0), + ) + return +end + +function test_range_equalto() + model = Model() + @variable(model, 10 <= x <= 11) + @variable(model, 1 <= y <= 11) + @constraint(model, c, x + y == 100) + @objective(model, Max, x + y) + data = ModelAnalyzer.analyze(ModelAnalyzer.Infeasibility.Analyzer(), model) + list = ModelAnalyzer.list_of_issue_types(data) + @test length(list) == 1 + ret = ModelAnalyzer.list_of_issues(data, list[1]) + @test length(ret) == 1 + @test ret[] == + ModelAnalyzer.Infeasibility.InfeasibleConstraintRange{Float64}( + c, + 11.0, + 22.0, + MOI.EqualTo{Float64}(100.0), + ) return end +function test_iis_feasible() + model = Model(HiGHS.Optimizer) + set_silent(model) + @variable(model, 0 <= x <= 10) + @variable(model, 0 <= y <= 20) + @constraint(model, c1, x + y <= 1) + @objective(model, Max, x + y) + optimize!(model) + data = ModelAnalyzer.analyze( + ModelAnalyzer.Infeasibility.Analyzer(), + model, + optimizer = HiGHS.Optimizer, + ) + list = ModelAnalyzer.list_of_issue_types(data) + @test length(list) == 0 +end + function test_iis() model = Model(HiGHS.Optimizer) set_silent(model) @@ -107,6 +308,151 @@ function test_iis() @constraint(model, c2, x + y >= 2) @objective(model, Max, x + y) optimize!(model) + data = ModelAnalyzer.analyze(ModelAnalyzer.Infeasibility.Analyzer(), model) + list = ModelAnalyzer.list_of_issue_types(data) + @test length(list) == 0 + data = ModelAnalyzer.analyze( + ModelAnalyzer.Infeasibility.Analyzer(), + model, + optimizer = HiGHS.Optimizer, + ) + list = ModelAnalyzer.list_of_issue_types(data) + @test length(list) == 1 + ret = ModelAnalyzer.list_of_issues(data, list[1]) + @test length(ret) == 1 + @test length(ret[].constraint) == 2 + @test Set([ret[].constraint[1], ret[].constraint[2]]) == Set([c2, c1]) + # + buf = IOBuffer() + ModelAnalyzer.summarize( + buf, + ModelAnalyzer.Infeasibility.IrreducibleInfeasibleSubset, + ) + str = String(take!(buf)) + @test startswith(str, "# `IrreducibleInfeasibleSubset`") + ModelAnalyzer.summarize( + buf, + ModelAnalyzer.Infeasibility.IrreducibleInfeasibleSubset, + verbose = false, + ) + str = String(take!(buf)) + @test str == "# IrreducibleInfeasibleSubset" + # + ModelAnalyzer.summarize(buf, ret[1], verbose = true) + str = String(take!(buf)) + @test startswith(str, "Irreducible Infeasible Subset: ") + @test contains(str, ", ") + ModelAnalyzer.summarize(buf, ret[1], verbose = false) + str = String(take!(buf)) + @test startswith(str, "IIS: ") + @test contains(str, ", ") + + buf = IOBuffer() + Base.show(buf, data) + str = String(take!(buf)) + @test startswith(str, "Infeasibility analysis found 1 issues") + + ModelAnalyzer.summarize(buf, data, verbose = true) + str = String(take!(buf)) + @test startswith(str, "## Infeasibility Analysis\n\n") + return +end + +function test_iis_multiple() + model = Model(HiGHS.Optimizer) + set_silent(model) + @variable(model, 0 <= x <= 10) + @variable(model, 0 <= y <= 20) + @constraint(model, c1, x + y <= 1) + @constraint(model, c3, x + y <= 1.5) + @constraint(model, c2, x + y >= 2) + @objective(model, Max, x + y) + optimize!(model) + data = ModelAnalyzer.analyze(ModelAnalyzer.Infeasibility.Analyzer(), model) + list = ModelAnalyzer.list_of_issue_types(data) + @test length(list) == 0 + data = ModelAnalyzer.analyze( + ModelAnalyzer.Infeasibility.Analyzer(), + model, + optimizer = HiGHS.Optimizer, + ) + list = ModelAnalyzer.list_of_issue_types(data) + @test length(list) == 1 + ret = ModelAnalyzer.list_of_issues(data, list[1]) + @test length(ret) == 1 + @test length(ret[].constraint) == 2 + @test c2 in Set([ret[].constraint[1], ret[].constraint[2]]) + @test Set([ret[].constraint[1], ret[].constraint[2]]) ⊆ Set([c3, c2, c1]) + return +end + +function test_iis_interval_right() + model = Model(HiGHS.Optimizer) + set_silent(model) + @variable(model, 0 <= x <= 10) + @variable(model, 0 <= y <= 20) + @constraint(model, c1, 0 <= x + y <= 1) + @constraint(model, c2, x + y >= 2) + @objective(model, Max, x + y) + optimize!(model) + data = ModelAnalyzer.analyze(ModelAnalyzer.Infeasibility.Analyzer(), model) + list = ModelAnalyzer.list_of_issue_types(data) + @test length(list) == 0 + data = ModelAnalyzer.analyze( + ModelAnalyzer.Infeasibility.Analyzer(), + model, + optimizer = HiGHS.Optimizer, + ) + list = ModelAnalyzer.list_of_issue_types(data) + @test length(list) == 1 + ret = ModelAnalyzer.list_of_issues(data, list[1]) + @test length(ret) == 1 + @test length(ret[].constraint) == 2 + @test Set([ret[].constraint[1], ret[].constraint[2]]) == Set([c2, c1]) + return +end + +function test_iis_interval_left() + model = Model(HiGHS.Optimizer) + set_silent(model) + @variable(model, 0 <= x <= 10) + @variable(model, 0 <= y <= 20) + @constraint(model, c1, x + y <= 1) + @constraint(model, c2, 2 <= x + y <= 5) + @objective(model, Max, x + y) + optimize!(model) + data = ModelAnalyzer.analyze(ModelAnalyzer.Infeasibility.Analyzer(), model) + list = ModelAnalyzer.list_of_issue_types(data) + @test length(list) == 0 + data = ModelAnalyzer.analyze( + ModelAnalyzer.Infeasibility.Analyzer(), + model, + optimizer = HiGHS.Optimizer, + ) + list = ModelAnalyzer.list_of_issue_types(data) + @test length(list) == 1 + ret = ModelAnalyzer.list_of_issues(data, list[1]) + @test length(ret) == 1 + @test length(ret[].constraint) == 2 + @test Set([ret[].constraint[1], ret[].constraint[2]]) == Set([c2, c1]) + return +end + +function test_iis_spare() + model = Model(HiGHS.Optimizer) + set_silent(model) + @variable(model, 0 <= x <= 10) + @variable(model, 0 <= y <= 20) + @variable(model, 0 <= z <= 20) + @constraint(model, c0, 2z <= 1) + @constraint(model, c00, 3z <= 1) + @constraint(model, c1, x + y <= 1) + @constraint(model, c2, x + y >= 2) + @objective(model, Max, x + y) + optimize!(model) + data = ModelAnalyzer.analyze(ModelAnalyzer.Infeasibility.Analyzer(), model) + list = ModelAnalyzer.list_of_issue_types(data) + @test length(list) == 0 data = ModelAnalyzer.analyze( ModelAnalyzer.Infeasibility.Analyzer(), model, @@ -117,8 +463,7 @@ function test_iis() ret = ModelAnalyzer.list_of_issues(data, list[1]) @test length(ret) == 1 @test length(ret[].constraint) == 2 - @test ret[].constraint[1] == c2 - @test ret[].constraint[2] == c1 + @test Set([ret[].constraint[1], ret[].constraint[2]]) == Set([c2, c1]) return end diff --git a/test/numerical.jl b/test/numerical.jl index 1aa9460..1a6e98d 100644 --- a/test/numerical.jl +++ b/test/numerical.jl @@ -20,24 +20,929 @@ function runtests() return end -function test_linear() +function test_variable_bounds() + model = Model() + @variable(model, xg <= 2e9) + @variable(model, xs <= 2e-9) + @variable(model, ys >= 3e-10) + @variable(model, yg >= 3e+10) + @variable(model, zs == 4e-11) + @variable(model, zg == 4e+11) + data = ModelAnalyzer.analyze(ModelAnalyzer.Numerical.Analyzer(), model) + list = ModelAnalyzer.list_of_issue_types(data) + @test length(list) == 3 + ret = ModelAnalyzer.list_of_issues( + data, + ModelAnalyzer.Numerical.VariableNotInConstraints, + ) + @test length(ret) == 6 + ret = ModelAnalyzer.list_of_issues( + data, + ModelAnalyzer.Numerical.SmallBoundCoefficient, + ) + @test length(ret) == 3 + ret = ModelAnalyzer.list_of_issues( + data, + ModelAnalyzer.Numerical.LargeBoundCoefficient, + ) + @test length(ret) == 3 + + buf = IOBuffer() + ModelAnalyzer.summarize(buf, ModelAnalyzer.Numerical.SmallBoundCoefficient) + str = String(take!(buf)) + @test startswith(str, "# `SmallBoundCoefficient`") + ModelAnalyzer.summarize( + buf, + ModelAnalyzer.Numerical.SmallBoundCoefficient, + verbose = false, + ) + str = String(take!(buf)) + @test str == "# SmallBoundCoefficient" + buf = IOBuffer() + ModelAnalyzer.summarize(buf, ModelAnalyzer.Numerical.LargeBoundCoefficient) + str = String(take!(buf)) + @test startswith(str, "# `LargeBoundCoefficient`") + ModelAnalyzer.summarize( + buf, + ModelAnalyzer.Numerical.LargeBoundCoefficient, + verbose = false, + ) + str = String(take!(buf)) + @test str == "# LargeBoundCoefficient" + + return +end + +function test_constraint_bounds() + model = Model() + @variable(model, x) + @constraint(model, x >= 2e+10) + @constraint(model, x >= 2e-11) + @constraint(model, x == 4e-12) + @constraint(model, x == 4e+13) + @constraint(model, x <= 1e-14) + @constraint(model, x <= 1e+15) + @constraint(model, [x - 1e-16] in MOI.Nonnegatives(1)) + @constraint(model, [x - 1e+17] in MOI.Nonnegatives(1)) + data = ModelAnalyzer.analyze(ModelAnalyzer.Numerical.Analyzer(), model) + list = ModelAnalyzer.list_of_issue_types(data) + @test length(list) == 4 + ret = ModelAnalyzer.list_of_issues( + data, + ModelAnalyzer.Numerical.VariableBoundAsConstraint, + ) + @test length(ret) == 8 + ret = ModelAnalyzer.list_of_issues( + data, + ModelAnalyzer.Numerical.VariableNotInConstraints, + ) + @test length(ret) == 1 + ret = ModelAnalyzer.list_of_issues( + data, + ModelAnalyzer.Numerical.SmallRHSCoefficient, + ) + @test length(ret) == 4 + ret = ModelAnalyzer.list_of_issues( + data, + ModelAnalyzer.Numerical.LargeRHSCoefficient, + ) + @test length(ret) == 4 + + buf = IOBuffer() + ModelAnalyzer.summarize(buf, ModelAnalyzer.Numerical.SmallRHSCoefficient) + str = String(take!(buf)) + @test startswith(str, "# `SmallRHSCoefficient`") + ModelAnalyzer.summarize( + buf, + ModelAnalyzer.Numerical.SmallRHSCoefficient, + verbose = false, + ) + str = String(take!(buf)) + @test str == "# SmallRHSCoefficient" + buf = IOBuffer() + ModelAnalyzer.summarize(buf, ModelAnalyzer.Numerical.LargeRHSCoefficient) + str = String(take!(buf)) + @test startswith(str, "# `LargeRHSCoefficient`") + ModelAnalyzer.summarize( + buf, + ModelAnalyzer.Numerical.LargeRHSCoefficient, + verbose = false, + ) + str = String(take!(buf)) + @test str == "# LargeRHSCoefficient" + + return +end + +function test_constraint_bounds_quad() + model = Model() + @variable(model, x) + @constraint(model, x^2 <= 1e-14) + @constraint(model, x^2 <= 1e+15) + @constraint(model, [x^2 - 1e-16] in MOI.Nonpositives(1)) + @constraint(model, [x^2 - 1e+17] in MOI.Nonpositives(1)) + data = ModelAnalyzer.analyze(ModelAnalyzer.Numerical.Analyzer(), model) + list = ModelAnalyzer.list_of_issue_types(data) + @test length(list) == 2 + ret = ModelAnalyzer.list_of_issues( + data, + ModelAnalyzer.Numerical.VariableBoundAsConstraint, + ) + @test length(ret) == 0 + ret = ModelAnalyzer.list_of_issues( + data, + ModelAnalyzer.Numerical.VariableNotInConstraints, + ) + @test length(ret) == 0 + ret = ModelAnalyzer.list_of_issues( + data, + ModelAnalyzer.Numerical.SmallRHSCoefficient, + ) + @test length(ret) == 2 + ret = ModelAnalyzer.list_of_issues( + data, + ModelAnalyzer.Numerical.LargeRHSCoefficient, + ) + @test length(ret) == 2 + + buf = IOBuffer() + ModelAnalyzer.summarize(buf, ModelAnalyzer.Numerical.SmallRHSCoefficient) + str = String(take!(buf)) + @test startswith(str, "# `SmallRHSCoefficient`") + ModelAnalyzer.summarize( + buf, + ModelAnalyzer.Numerical.SmallRHSCoefficient, + verbose = false, + ) + str = String(take!(buf)) + @test str == "# SmallRHSCoefficient" + buf = IOBuffer() + ModelAnalyzer.summarize(buf, ModelAnalyzer.Numerical.LargeRHSCoefficient) + str = String(take!(buf)) + @test startswith(str, "# `LargeRHSCoefficient`") + ModelAnalyzer.summarize( + buf, + ModelAnalyzer.Numerical.LargeRHSCoefficient, + verbose = false, + ) + str = String(take!(buf)) + @test str == "# LargeRHSCoefficient" + + return +end + +function test_variable_not_in_constraints() + model = Model() + @variable(model, x) + @variable(model, y) + @constraint(model, 7y >= 3) + data = ModelAnalyzer.analyze(ModelAnalyzer.Numerical.Analyzer(), model) + list = ModelAnalyzer.list_of_issue_types(data) + @test length(list) == 1 + ret = ModelAnalyzer.list_of_issues( + data, + ModelAnalyzer.Numerical.VariableNotInConstraints, + ) + @test length(ret) == 1 + # + buf = IOBuffer() + ModelAnalyzer.summarize( + buf, + ModelAnalyzer.Numerical.VariableNotInConstraints, + ) + str = String(take!(buf)) + @test startswith(str, "# `VariableNotInConstraints`") + ModelAnalyzer.summarize( + buf, + ModelAnalyzer.Numerical.VariableNotInConstraints, + verbose = false, + ) + str = String(take!(buf)) + @test str == "# VariableNotInConstraints" + # + ModelAnalyzer.summarize(buf, ret[1], verbose = true) + str = String(take!(buf)) + @test startswith(str, "Variable: ") + ModelAnalyzer.summarize(buf, ret[1], verbose = false) + str = String(take!(buf)) + return +end + +function test_empty_constraint_model() + model = Model() + @variable(model, x) + @constraint(model, 2 * x == 5) + @constraint(model, 0.0 * x == 3) + data = ModelAnalyzer.analyze(ModelAnalyzer.Numerical.Analyzer(), model) + list = ModelAnalyzer.list_of_issue_types(data) + @test length(list) == 1 + ret = ModelAnalyzer.list_of_issues( + data, + ModelAnalyzer.Numerical.EmptyConstraint, + ) + @test length(ret) == 1 + # + buf = IOBuffer() + ModelAnalyzer.summarize(buf, ModelAnalyzer.Numerical.EmptyConstraint) + str = String(take!(buf)) + @test startswith(str, "# `EmptyConstraint`") + ModelAnalyzer.summarize( + buf, + ModelAnalyzer.Numerical.EmptyConstraint, + verbose = false, + ) + str = String(take!(buf)) + @test str == "# EmptyConstraint" + # + ModelAnalyzer.summarize(buf, ret[1], verbose = true) + str = String(take!(buf)) + @test startswith(str, "Constraint: ") + ModelAnalyzer.summarize(buf, ret[1], verbose = false) + str = String(take!(buf)) + return +end + +function test_variable_bound_as_constraint() + model = Model() + @variable(model, x) + @constraint(model, x <= 2) + @constraint(model, 3x <= 4) + data = ModelAnalyzer.analyze(ModelAnalyzer.Numerical.Analyzer(), model) + list = ModelAnalyzer.list_of_issue_types(data) + @test length(list) == 1 + ret = ModelAnalyzer.list_of_issues( + data, + ModelAnalyzer.Numerical.VariableBoundAsConstraint, + ) + @test length(ret) == 1 + # + buf = IOBuffer() + ModelAnalyzer.summarize( + buf, + ModelAnalyzer.Numerical.VariableBoundAsConstraint, + ) + str = String(take!(buf)) + @test startswith(str, "# `VariableBoundAsConstraint`") + ModelAnalyzer.summarize( + buf, + ModelAnalyzer.Numerical.VariableBoundAsConstraint, + verbose = false, + ) + str = String(take!(buf)) + @test str == "# VariableBoundAsConstraint" + # + ModelAnalyzer.summarize(buf, ret[1], verbose = true) + str = String(take!(buf)) + @test startswith(str, "Constraint: ") + ModelAnalyzer.summarize(buf, ret[1], verbose = false) + str = String(take!(buf)) + return +end + +function test_dense_constraint() + model = Model() + @variable(model, x[1:10_000] <= 1) + @constraint(model, sum(x) <= 4) + data = ModelAnalyzer.analyze(ModelAnalyzer.Numerical.Analyzer(), model) + list = ModelAnalyzer.list_of_issue_types(data) + @test length(list) == 1 + ret = ModelAnalyzer.list_of_issues( + data, + ModelAnalyzer.Numerical.DenseConstraint, + ) + @test length(ret) == 1 + # + buf = IOBuffer() + ModelAnalyzer.summarize(buf, ModelAnalyzer.Numerical.DenseConstraint) + str = String(take!(buf)) + @test startswith(str, "# `DenseConstraint`") + ModelAnalyzer.summarize( + buf, + ModelAnalyzer.Numerical.DenseConstraint, + verbose = false, + ) + str = String(take!(buf)) + @test str == "# DenseConstraint" + # + ModelAnalyzer.summarize(buf, ret[1], verbose = true) + str = String(take!(buf)) + @test startswith(str, "Constraint: ") + ModelAnalyzer.summarize(buf, ret[1], verbose = false) + str = String(take!(buf)) + @test contains(str, " : ") + return +end + +function test_small_matrix_coef() + model = Model() + @variable(model, x <= 1) + @constraint(model, 1e-9 * x <= 4) + data = ModelAnalyzer.analyze(ModelAnalyzer.Numerical.Analyzer(), model) + list = ModelAnalyzer.list_of_issue_types(data) + @test length(list) == 1 + ret = ModelAnalyzer.list_of_issues( + data, + ModelAnalyzer.Numerical.SmallMatrixCoefficient, + ) + @test length(ret) == 1 + # + buf = IOBuffer() + ModelAnalyzer.summarize(buf, ModelAnalyzer.Numerical.SmallMatrixCoefficient) + str = String(take!(buf)) + @test startswith(str, "# `SmallMatrixCoefficient`") + ModelAnalyzer.summarize( + buf, + ModelAnalyzer.Numerical.SmallMatrixCoefficient, + verbose = false, + ) + str = String(take!(buf)) + @test str == "# SmallMatrixCoefficient" + # + ModelAnalyzer.summarize(buf, ret[1], verbose = true) + str = String(take!(buf)) + @test startswith(str, "(Constraint -- Variable): (") + @test contains(str, ") with coefficient ") + ModelAnalyzer.summarize(buf, ret[1], verbose = false) + str = String(take!(buf)) + @test contains(str, " -- ") + @test contains(str, " : ") + return +end + +function test_large_matrix_coef() + model = Model() + @variable(model, x <= 1) + @constraint(model, 1e+9 * x <= 4) + data = ModelAnalyzer.analyze(ModelAnalyzer.Numerical.Analyzer(), model) + list = ModelAnalyzer.list_of_issue_types(data) + @test length(list) == 1 + ret = ModelAnalyzer.list_of_issues( + data, + ModelAnalyzer.Numerical.LargeMatrixCoefficient, + ) + @test length(ret) == 1 + # + buf = IOBuffer() + ModelAnalyzer.summarize(buf, ModelAnalyzer.Numerical.LargeMatrixCoefficient) + str = String(take!(buf)) + @test startswith(str, "# `LargeMatrixCoefficient`") + ModelAnalyzer.summarize( + buf, + ModelAnalyzer.Numerical.LargeMatrixCoefficient, + verbose = false, + ) + str = String(take!(buf)) + @test str == "# LargeMatrixCoefficient" + # + ModelAnalyzer.summarize(buf, ret[1], verbose = true) + str = String(take!(buf)) + @test startswith(str, "(Constraint -- Variable): (") + @test contains(str, ") with coefficient ") + ModelAnalyzer.summarize(buf, ret[1], verbose = false) + str = String(take!(buf)) + @test contains(str, " -- ") + @test contains(str, " : ") + return +end + +function test_small_bound_coef() + model = Model() + @variable(model, x <= 1e-9) + @constraint(model, 3 * x <= 4) + data = ModelAnalyzer.analyze(ModelAnalyzer.Numerical.Analyzer(), model) + list = ModelAnalyzer.list_of_issue_types(data) + @test length(list) == 1 + ret = ModelAnalyzer.list_of_issues( + data, + ModelAnalyzer.Numerical.SmallBoundCoefficient, + ) + @test length(ret) == 1 + # + buf = IOBuffer() + ModelAnalyzer.summarize(buf, ModelAnalyzer.Numerical.SmallBoundCoefficient) + str = String(take!(buf)) + @test startswith(str, "# `SmallBoundCoefficient`") + ModelAnalyzer.summarize( + buf, + ModelAnalyzer.Numerical.SmallBoundCoefficient, + verbose = false, + ) + str = String(take!(buf)) + @test str == "# SmallBoundCoefficient" + # + ModelAnalyzer.summarize(buf, ret[1], verbose = true) + str = String(take!(buf)) + @test startswith(str, "Variable: ") + @test contains(str, " with bound ") + ModelAnalyzer.summarize(buf, ret[1], verbose = false) + str = String(take!(buf)) + @test contains(str, " : ") + return +end + +function test_large_bound_coef() + model = Model() + @variable(model, x <= 1e+9) + @constraint(model, 3 * x <= 4) + data = ModelAnalyzer.analyze(ModelAnalyzer.Numerical.Analyzer(), model) + list = ModelAnalyzer.list_of_issue_types(data) + @test length(list) == 1 + ret = ModelAnalyzer.list_of_issues( + data, + ModelAnalyzer.Numerical.LargeBoundCoefficient, + ) + @test length(ret) == 1 + # + buf = IOBuffer() + ModelAnalyzer.summarize(buf, ModelAnalyzer.Numerical.LargeBoundCoefficient) + str = String(take!(buf)) + @test startswith(str, "# `LargeBoundCoefficient`") + ModelAnalyzer.summarize( + buf, + ModelAnalyzer.Numerical.LargeBoundCoefficient, + verbose = false, + ) + str = String(take!(buf)) + @test str == "# LargeBoundCoefficient" + # + ModelAnalyzer.summarize(buf, ret[1], verbose = true) + str = String(take!(buf)) + @test startswith(str, "Variable: ") + @test contains(str, " with bound ") + ModelAnalyzer.summarize(buf, ret[1], verbose = false) + str = String(take!(buf)) + @test contains(str, " : ") + return +end + +function test_small_rhs_coef() + model = Model() + @variable(model, x <= 1) + @constraint(model, 3 * x <= 1e-9) + data = ModelAnalyzer.analyze(ModelAnalyzer.Numerical.Analyzer(), model) + list = ModelAnalyzer.list_of_issue_types(data) + @test length(list) == 1 + ret = ModelAnalyzer.list_of_issues( + data, + ModelAnalyzer.Numerical.SmallRHSCoefficient, + ) + @test length(ret) == 1 + # + buf = IOBuffer() + ModelAnalyzer.summarize(buf, ModelAnalyzer.Numerical.SmallRHSCoefficient) + str = String(take!(buf)) + @test startswith(str, "# `SmallRHSCoefficient`") + ModelAnalyzer.summarize( + buf, + ModelAnalyzer.Numerical.SmallRHSCoefficient, + verbose = false, + ) + str = String(take!(buf)) + @test str == "# SmallRHSCoefficient" + # + ModelAnalyzer.summarize(buf, ret[1], verbose = true) + str = String(take!(buf)) + @test startswith(str, "Constraint: ") + @test contains(str, " with right-hand-side ") + ModelAnalyzer.summarize(buf, ret[1], verbose = false) + str = String(take!(buf)) + @test contains(str, " : ") + return +end + +function test_large_rhs_coef() + model = Model() + @variable(model, x <= 1) + @constraint(model, 3 * x <= 1e+9) + data = ModelAnalyzer.analyze(ModelAnalyzer.Numerical.Analyzer(), model) + list = ModelAnalyzer.list_of_issue_types(data) + @test length(list) == 1 + ret = ModelAnalyzer.list_of_issues( + data, + ModelAnalyzer.Numerical.LargeRHSCoefficient, + ) + @test length(ret) == 1 + # + buf = IOBuffer() + ModelAnalyzer.summarize(buf, ModelAnalyzer.Numerical.LargeRHSCoefficient) + str = String(take!(buf)) + @test startswith(str, "# `LargeRHSCoefficient`") + ModelAnalyzer.summarize( + buf, + ModelAnalyzer.Numerical.LargeRHSCoefficient, + verbose = false, + ) + str = String(take!(buf)) + @test str == "# LargeRHSCoefficient" + # + ModelAnalyzer.summarize(buf, ret[1], verbose = true) + str = String(take!(buf)) + @test startswith(str, "Constraint: ") + @test contains(str, " with right-hand-side ") + ModelAnalyzer.summarize(buf, ret[1], verbose = false) + str = String(take!(buf)) + @test contains(str, " : ") + return +end + +function test_small_objective_coef() + model = Model() + @variable(model, x <= 1) + @constraint(model, 3 * x <= 4) + @objective(model, Min, 1e-9 * x) + data = ModelAnalyzer.analyze(ModelAnalyzer.Numerical.Analyzer(), model) + list = ModelAnalyzer.list_of_issue_types(data) + @test length(list) == 1 + ret = ModelAnalyzer.list_of_issues( + data, + ModelAnalyzer.Numerical.SmallObjectiveCoefficient, + ) + @test length(ret) == 1 + # + buf = IOBuffer() + ModelAnalyzer.summarize( + buf, + ModelAnalyzer.Numerical.SmallObjectiveCoefficient, + ) + str = String(take!(buf)) + @test startswith(str, "# `SmallObjectiveCoefficient`") + ModelAnalyzer.summarize( + buf, + ModelAnalyzer.Numerical.SmallObjectiveCoefficient, + verbose = false, + ) + str = String(take!(buf)) + @test str == "# SmallObjectiveCoefficient" + # + ModelAnalyzer.summarize(buf, ret[1], verbose = true) + str = String(take!(buf)) + @test startswith(str, "Variable: ") + @test contains(str, " with coefficient ") + ModelAnalyzer.summarize(buf, ret[1], verbose = false) + str = String(take!(buf)) + @test contains(str, " : ") + return +end + +function test_large_objective_coef() + model = Model() + @variable(model, x <= 1) + @constraint(model, 3 * x <= 4) + @objective(model, Min, 1e+9 * x) + data = ModelAnalyzer.analyze(ModelAnalyzer.Numerical.Analyzer(), model) + list = ModelAnalyzer.list_of_issue_types(data) + @test length(list) == 1 + ret = ModelAnalyzer.list_of_issues( + data, + ModelAnalyzer.Numerical.LargeObjectiveCoefficient, + ) + @test length(ret) == 1 + # + buf = IOBuffer() + ModelAnalyzer.summarize( + buf, + ModelAnalyzer.Numerical.LargeObjectiveCoefficient, + ) + str = String(take!(buf)) + @test startswith(str, "# `LargeObjectiveCoefficient`") + ModelAnalyzer.summarize( + buf, + ModelAnalyzer.Numerical.LargeObjectiveCoefficient, + verbose = false, + ) + str = String(take!(buf)) + @test str == "# LargeObjectiveCoefficient" + # + ModelAnalyzer.summarize(buf, ret[1], verbose = true) + str = String(take!(buf)) + @test startswith(str, "Variable: ") + @test contains(str, " with coefficient ") + ModelAnalyzer.summarize(buf, ret[1], verbose = false) + str = String(take!(buf)) + @test contains(str, " : ") + return +end + +function test_small_objective_coef_quad() + model = Model() + @variable(model, x <= 1) + @constraint(model, 3 * x <= 4) + @objective(model, Min, 1e-9 * x^2) + data = ModelAnalyzer.analyze(ModelAnalyzer.Numerical.Analyzer(), model) + list = ModelAnalyzer.list_of_issue_types(data) + @test length(list) == 1 + ret = ModelAnalyzer.list_of_issues( + data, + ModelAnalyzer.Numerical.SmallObjectiveQuadraticCoefficient, + ) + @test length(ret) == 1 + # + buf = IOBuffer() + ModelAnalyzer.summarize( + buf, + ModelAnalyzer.Numerical.SmallObjectiveQuadraticCoefficient, + ) + str = String(take!(buf)) + @test startswith(str, "# `SmallObjectiveQuadraticCoefficient`") + ModelAnalyzer.summarize( + buf, + ModelAnalyzer.Numerical.SmallObjectiveQuadraticCoefficient, + verbose = false, + ) + str = String(take!(buf)) + @test str == "# SmallObjectiveQuadraticCoefficient" + # + ModelAnalyzer.summarize(buf, ret[1], verbose = true) + str = String(take!(buf)) + @test startswith(str, "(Variable -- Variable): (") + @test contains(str, " with coefficient ") + ModelAnalyzer.summarize(buf, ret[1], verbose = false) + str = String(take!(buf)) + @test contains(str, " -- ") + @test contains(str, " : ") + return +end + +function test_large_objective_coef_quad() + model = Model() + @variable(model, x <= 1) + @constraint(model, 3 * x <= 4) + @objective(model, Min, 1e+9 * x^2) + data = ModelAnalyzer.analyze(ModelAnalyzer.Numerical.Analyzer(), model) + list = ModelAnalyzer.list_of_issue_types(data) + @test length(list) == 1 + ret = ModelAnalyzer.list_of_issues( + data, + ModelAnalyzer.Numerical.LargeObjectiveQuadraticCoefficient, + ) + @test length(ret) == 1 + # + buf = IOBuffer() + ModelAnalyzer.summarize( + buf, + ModelAnalyzer.Numerical.LargeObjectiveQuadraticCoefficient, + ) + str = String(take!(buf)) + @test startswith(str, "# `LargeObjectiveQuadraticCoefficient`") + ModelAnalyzer.summarize( + buf, + ModelAnalyzer.Numerical.LargeObjectiveQuadraticCoefficient, + verbose = false, + ) + str = String(take!(buf)) + @test str == "# LargeObjectiveQuadraticCoefficient" + # + ModelAnalyzer.summarize(buf, ret[1], verbose = true) + str = String(take!(buf)) + @test startswith(str, "(Variable -- Variable): (") + @test contains(str, " with coefficient ") + ModelAnalyzer.summarize(buf, ret[1], verbose = false) + str = String(take!(buf)) + @test contains(str, " -- ") + @test contains(str, " : ") + return +end + +function test_small_matrix_coef_quad() + model = Model() + @variable(model, x <= 1) + @constraint(model, 1e-9 * x^2 + x <= 4) + data = ModelAnalyzer.analyze(ModelAnalyzer.Numerical.Analyzer(), model) + list = ModelAnalyzer.list_of_issue_types(data) + @test length(list) == 1 + ret = ModelAnalyzer.list_of_issues( + data, + ModelAnalyzer.Numerical.SmallMatrixQuadraticCoefficient, + ) + @test length(ret) == 1 + # + buf = IOBuffer() + ModelAnalyzer.summarize( + buf, + ModelAnalyzer.Numerical.SmallMatrixQuadraticCoefficient, + ) + str = String(take!(buf)) + @test startswith(str, "# `SmallMatrixQuadraticCoefficient`") + ModelAnalyzer.summarize( + buf, + ModelAnalyzer.Numerical.SmallMatrixQuadraticCoefficient, + verbose = false, + ) + str = String(take!(buf)) + @test str == "# SmallMatrixQuadraticCoefficient" + # + ModelAnalyzer.summarize(buf, ret[1], verbose = true) + str = String(take!(buf)) + @test startswith(str, "(Constraint -- Variable -- Variable): (") + @test contains(str, ") with coefficient ") + ModelAnalyzer.summarize(buf, ret[1], verbose = false) + str = String(take!(buf)) + @test contains(str, " -- ") + @test contains(str, " : ") + return +end + +function test_large_matrix_coef_quad() + model = Model() + @variable(model, x <= 1) + @constraint(model, 1e+9 * x^2 <= 4) + data = ModelAnalyzer.analyze(ModelAnalyzer.Numerical.Analyzer(), model) + list = ModelAnalyzer.list_of_issue_types(data) + @test length(list) == 1 + ret = ModelAnalyzer.list_of_issues( + data, + ModelAnalyzer.Numerical.LargeMatrixQuadraticCoefficient, + ) + @test length(ret) == 1 + # + buf = IOBuffer() + ModelAnalyzer.summarize( + buf, + ModelAnalyzer.Numerical.LargeMatrixQuadraticCoefficient, + ) + str = String(take!(buf)) + @test startswith(str, "# `LargeMatrixQuadraticCoefficient`") + ModelAnalyzer.summarize( + buf, + ModelAnalyzer.Numerical.LargeMatrixQuadraticCoefficient, + verbose = false, + ) + str = String(take!(buf)) + @test str == "# LargeMatrixQuadraticCoefficient" + # + ModelAnalyzer.summarize(buf, ret[1], verbose = true) + str = String(take!(buf)) + @test startswith(str, "(Constraint -- Variable -- Variable): (") + @test contains(str, ") with coefficient ") + ModelAnalyzer.summarize(buf, ret[1], verbose = false) + str = String(take!(buf)) + @test contains(str, " -- ") + @test contains(str, " : ") + return +end + +function test_objective_nonconvex() + model = Model() + @variable(model, x <= 1) + @constraint(model, 3 * x <= 4) + @objective(model, Max, x^2) + data = ModelAnalyzer.analyze(ModelAnalyzer.Numerical.Analyzer(), model) + list = ModelAnalyzer.list_of_issue_types(data) + @test length(list) == 1 + ret = ModelAnalyzer.list_of_issues( + data, + ModelAnalyzer.Numerical.NonconvexQuadraticObjective, + ) + @test length(ret) == 1 + # + buf = IOBuffer() + ModelAnalyzer.summarize( + buf, + ModelAnalyzer.Numerical.NonconvexQuadraticObjective, + ) + str = String(take!(buf)) + @test startswith(str, "# `NonconvexQuadraticObjective`") + ModelAnalyzer.summarize( + buf, + ModelAnalyzer.Numerical.NonconvexQuadraticObjective, + verbose = false, + ) + str = String(take!(buf)) + @test str == "# NonconvexQuadraticObjective" + # + ModelAnalyzer.summarize(buf, ret[1], verbose = true) + str = String(take!(buf)) + @test startswith(str, "Objective is Nonconvex quadratic") + ModelAnalyzer.summarize(buf, ret[1], verbose = false) + str = String(take!(buf)) + @test startswith(str, "Objective is Nonconvex quadratic") + return +end + +function test_objective_nonconvex_2() + model = Model() + @variable(model, x <= 1) + @constraint(model, 3 * x <= 4) + @objective(model, Min, -x^2) + data = ModelAnalyzer.analyze(ModelAnalyzer.Numerical.Analyzer(), model) + list = ModelAnalyzer.list_of_issue_types(data) + @test length(list) == 1 + ret = ModelAnalyzer.list_of_issues( + data, + ModelAnalyzer.Numerical.NonconvexQuadraticObjective, + ) + @test length(ret) == 1 + # + buf = IOBuffer() + ModelAnalyzer.summarize( + buf, + ModelAnalyzer.Numerical.NonconvexQuadraticObjective, + ) + str = String(take!(buf)) + @test startswith(str, "# `NonconvexQuadraticObjective`") + ModelAnalyzer.summarize( + buf, + ModelAnalyzer.Numerical.NonconvexQuadraticObjective, + verbose = false, + ) + str = String(take!(buf)) + @test str == "# NonconvexQuadraticObjective" + # + ModelAnalyzer.summarize(buf, ret[1], verbose = true) + str = String(take!(buf)) + @test startswith(str, "Objective is Nonconvex quadratic") + ModelAnalyzer.summarize(buf, ret[1], verbose = false) + str = String(take!(buf)) + @test startswith(str, "Objective is Nonconvex quadratic") + return +end + +function test_constraint_nonconvex() + model = Model() + @variable(model, x <= 1) + @constraint(model, x^2 >= 4) + data = ModelAnalyzer.analyze(ModelAnalyzer.Numerical.Analyzer(), model) + list = ModelAnalyzer.list_of_issue_types(data) + @test length(list) == 1 + ret = ModelAnalyzer.list_of_issues( + data, + ModelAnalyzer.Numerical.NonconvexQuadraticConstraint, + ) + @test length(ret) == 1 + # + buf = IOBuffer() + ModelAnalyzer.summarize( + buf, + ModelAnalyzer.Numerical.NonconvexQuadraticConstraint, + ) + str = String(take!(buf)) + @test startswith(str, "# `NonconvexQuadraticConstraint`") + ModelAnalyzer.summarize( + buf, + ModelAnalyzer.Numerical.NonconvexQuadraticConstraint, + verbose = false, + ) + str = String(take!(buf)) + @test str == "# NonconvexQuadraticConstraint" + # + ModelAnalyzer.summarize(buf, ret[1], verbose = true) + str = String(take!(buf)) + @test startswith(str, "Constraint: ") + ModelAnalyzer.summarize(buf, ret[1], verbose = false) + str = String(take!(buf)) + return +end + +function test_empty_model() + model = Model() + data = ModelAnalyzer.analyze(ModelAnalyzer.Numerical.Analyzer(), model) + list = ModelAnalyzer.list_of_issue_types(data) + @test length(list) == 0 + return +end + +# TODO, test SDP and empty model + +function test_many() model = Model() @variable(model, x <= 2e9) @variable(model, y >= 3e-9) @variable(model, z == 4e-9) + @variable(model, w[1:1] in MOI.Nonnegatives(1)) + @variable(model, u[1:1]) + @variable(model, v[1:1]) @constraint(model, x + y <= 4e8) @constraint(model, x + y + 5e7 <= 2) @constraint(model, 7e6 * x + 6e-15 * y + 2e-12 >= 0) @constraint(model, x <= 100) - @constraint(model, 0 * x <= 100) + @constraint(model, x <= 1e-17) + @constraint(model, x >= 1e+18) + @constraint(model, x == 1e+19) + @constraint(model, x == 1e-20) + @constraint(model, 0 * x == 0) + @constraint(model, 1e-21 <= x <= 1e-22) + @constraint(model, 1e+23 <= x <= 1e+24) + @constraint(model, [1.0 * x] in MOI.Nonnegatives(1)) + @constraint(model, [1.0 * x * x] in MOI.Nonnegatives(1)) + @constraint(model, u in MOI.Nonnegatives(1)) + @constraint(model, v in MOI.PositiveSemidefiniteConeTriangle(1)) + @constraint(model, [v[] - 1.0] in MOI.PositiveSemidefiniteConeTriangle(1)) + @objective(model, Max, 1e8 * x + 8e-11 * y) data = ModelAnalyzer.analyze(ModelAnalyzer.Numerical.Analyzer(), model) - ret = sprint(show, data) - print(ret) + buf = IOBuffer() + Base.show(buf, data) + str = String(take!(buf)) + + buf = IOBuffer() + ModelAnalyzer.summarize(buf, data) + ModelAnalyzer.summarize(buf, data, verbose = false) - ModelAnalyzer.summarize(data) + redirect_stdout(devnull) do + return ModelAnalyzer.summarize(data) + end return end @@ -59,27 +964,17 @@ function test_nonconvex_qp() data = ModelAnalyzer.analyze(ModelAnalyzer.Numerical.Analyzer(), model) - ret = sprint(show, data) - print(ret) + buf = IOBuffer() + Base.show(buf, data) + str = String(take!(buf)) - ModelAnalyzer.summarize(data) + buf = IOBuffer() + ModelAnalyzer.summarize(buf, data) + ModelAnalyzer.summarize(buf, data, verbose = false) return end -function test_dense() - model = Model() - @variable(model, x[1:10_000] <= 1) - @constraint(model, sum(x) <= 4) - - data = ModelAnalyzer.analyze(ModelAnalyzer.Numerical.Analyzer(), model) - - ret = sprint(show, data) - print(ret) - - return ModelAnalyzer.summarize(data) -end - function test_qp_range() model = Model() @variable(model, x) @@ -89,10 +984,15 @@ function test_qp_range() data = ModelAnalyzer.analyze(ModelAnalyzer.Numerical.Analyzer(), model) - ret = sprint(show, data) - print(ret) + buf = IOBuffer() + Base.show(buf, data) + str = String(take!(buf)) - return ModelAnalyzer.summarize(data) + buf = IOBuffer() + ModelAnalyzer.summarize(buf, data) + ModelAnalyzer.summarize(buf, data, verbose = false) + + return end end # module