Skip to content

Commit 4009298

Browse files
authored
Add tests to improve code coverage (#204)
1 parent 321d596 commit 4009298

File tree

5 files changed

+129
-72
lines changed

5 files changed

+129
-72
lines changed

src/MosekTools.jl

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -435,28 +435,29 @@ function MOI.optimize!(m::Optimizer)
435435
),
436436
)
437437
end
438-
# We need to sort the solutions, so that an optimal one is first (if it
439-
# exists). The priority is:
440-
# 1. MSK_SOL_STA_INTEGER_OPTIMAL or MSK_SOL_STA_OPTIMAL
441-
# 2. MSK_SOL_ITG, MSK_SOL_BAS, MSK_SOL_ITR
442-
function solution_priority(sol)
443-
solsta_priority =
444-
sol.solsta == Mosek.MSK_SOL_STA_INTEGER_OPTIMAL ||
445-
sol.solsta == Mosek.MSK_SOL_STA_OPTIMAL
446-
if sol.whichsol == Mosek.MSK_SOL_ITG
447-
return (solsta_priority, 3)
448-
elseif sol.whichsol == Mosek.MSK_SOL_BAS
449-
return (solsta_priority, 2)
450-
else
451-
@assert sol.whichsol == Mosek.MSK_SOL_ITR
452-
return (solsta_priority, 1)
453-
end
454-
end
455438
# Sort solutions largest priority to smallest
456-
sort!(m.solutions; by = solution_priority, rev = true)
439+
sort!(m.solutions; by = _solution_priority, rev = true)
457440
return
458441
end
459442

443+
# We need to sort the solutions, so that an optimal one is first (if it
444+
# exists). The priority is:
445+
# 1. MSK_SOL_STA_INTEGER_OPTIMAL or MSK_SOL_STA_OPTIMAL
446+
# 2. MSK_SOL_ITG, MSK_SOL_BAS, MSK_SOL_ITR
447+
function _solution_priority(sol)
448+
solsta_priority =
449+
sol.solsta == Mosek.MSK_SOL_STA_INTEGER_OPTIMAL ||
450+
sol.solsta == Mosek.MSK_SOL_STA_OPTIMAL
451+
if sol.whichsol == Mosek.MSK_SOL_ITG
452+
return (solsta_priority, 3)
453+
elseif sol.whichsol == Mosek.MSK_SOL_BAS
454+
return (solsta_priority, 2)
455+
else
456+
@assert sol.whichsol == Mosek.MSK_SOL_ITR
457+
return (solsta_priority, 1)
458+
end
459+
end
460+
460461
# MOI.Name
461462

462463
MOI.supports(::Optimizer, ::MOI.Name) = true

src/attributes.jl

Lines changed: 29 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -637,36 +637,40 @@ const _TERMINATION_STATUS_MAP = Dict(
637637
Mosek.MSK_RES_TRM_INTERNAL_STOP.value => MOI.OTHER_ERROR,
638638
)
639639

640+
# Mosek.jl defines `MosekEnum <: Integer` but it does not define
641+
# `hash(::MosekEnum)`. This means creating a dictionary fails. Instead of fixing
642+
# in Mosek.jl, or pirating a Base.hash(::Mosek.MosekEnum, ::UInt64) method here,
643+
# we just use the `.value::Int32` field as the key.
644+
const _PROSTA_STATUS_MAP = Dict(
645+
Mosek.MSK_PRO_STA_UNKNOWN.value => MOI.OTHER_ERROR,
646+
Mosek.MSK_PRO_STA_PRIM_AND_DUAL_FEAS.value => MOI.LOCALLY_SOLVED,
647+
Mosek.MSK_PRO_STA_PRIM_FEAS.value => MOI.LOCALLY_SOLVED,
648+
# We proved only dual feasibility? What this one returns is up for debate.
649+
Mosek.MSK_PRO_STA_DUAL_FEAS.value => MOI.OTHER_ERROR,
650+
Mosek.MSK_PRO_STA_PRIM_INFEAS.value => MOI.INFEASIBLE,
651+
Mosek.MSK_PRO_STA_DUAL_INFEAS.value => MOI.DUAL_INFEASIBLE,
652+
Mosek.MSK_PRO_STA_PRIM_AND_DUAL_INFEAS.value => MOI.INFEASIBLE,
653+
Mosek.MSK_PRO_STA_ILL_POSED.value => MOI.OTHER_ERROR,
654+
Mosek.MSK_PRO_STA_PRIM_INFEAS_OR_UNBOUNDED.value =>
655+
MOI.INFEASIBLE_OR_UNBOUNDED,
656+
)
657+
640658
function MOI.get(m::Optimizer, attr::MOI.TerminationStatus)
641659
if m.trm === nothing
642660
return MOI.OPTIMIZE_NOT_CALLED
643-
elseif m.trm == Mosek.MSK_RES_OK
644-
# checking `any(sol -> sol.solsta == Mosek.MSK_SOL_STA_PRIM_INFEAS_CER, m.solutions)`
645-
# doesn't work for MIP as there is not certificate, i.e. the solutions status is
646-
# `UNKNOWN`, only the problem status is `INFEAS`.
647-
if any(
648-
sol -> sol.prosta in
649-
[Mosek.MSK_PRO_STA_PRIM_INFEAS, Mosek.MSK_PRO_STA_ILL_POSED],
650-
m.solutions,
651-
)
652-
return MOI.INFEASIBLE
653-
elseif any(
654-
sol -> sol.prosta == Mosek.MSK_PRO_STA_DUAL_INFEAS,
655-
m.solutions,
656-
)
657-
return MOI.DUAL_INFEASIBLE
658-
elseif any(
659-
sol -> sol.prosta == Mosek.MSK_PRO_STA_PRIM_INFEAS_OR_UNBOUNDED,
660-
m.solutions,
661-
)
662-
return MOI.INFEASIBLE_OR_UNBOUNDED
663-
elseif any(
664-
sol -> sol.solsta in
665-
[Mosek.MSK_SOL_STA_OPTIMAL, Mosek.MSK_SOL_STA_INTEGER_OPTIMAL],
666-
m.solutions,
667-
)
661+
elseif m.trm == Mosek.MSK_RES_OK && length(m.solutions) > 0
662+
# The different solutions _could_ have different `.prosta`, but we just
663+
# return the information of the first one. This makes the most sense
664+
# because `result_index` defaults to 1, and we sort the solutions in
665+
# `MOI.optimize!` to ensure that the first solution is OPTIMAL, if one
666+
# exists.
667+
sol = first(m.solutions)
668+
if sol.solsta == Mosek.MSK_SOL_STA_OPTIMAL
669+
return MOI.OPTIMAL
670+
elseif sol.solsta == Mosek.MSK_SOL_STA_INTEGER_OPTIMAL
668671
return MOI.OPTIMAL
669672
end
673+
return _PROSTA_STATUS_MAP[sol.prosta.value]
670674
end
671675
return get(_TERMINATION_STATUS_MAP, m.trm.value, MOI.OTHER_ERROR)
672676
end

src/constraint.jl

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -569,17 +569,18 @@ _check_bound_compat(::Optimizer, ::MOI.VariableIndex, ::MOI.Integer) = nothing
569569

570570
function MOI.add_constraint(
571571
m::Optimizer,
572-
xs::MOI.VariableIndex,
573-
dom::D,
574-
) where {D<:Union{ScalarLinearDomain,MOI.Integer}}
575-
msk_idx = mosek_index(m, xs)
576-
if !(msk_idx isa ColumnIndex)
577-
error("Cannot add $D constraint on a matrix variable")
572+
x::MOI.VariableIndex,
573+
set::S,
574+
) where {S<:Union{ScalarLinearDomain,MOI.Integer}}
575+
index = mosek_index(m, x)
576+
if index isa MatrixIndex
577+
msg = "Cannot add $S constraint on a matrix variable."
578+
throw(MOI.AddConstraintNotAllowed{MOI.VariableIndex,S}(msg))
578579
end
579-
_check_bound_compat(m, xs, dom)
580-
set_flag(m, xs, D)
581-
_add_variable_constraint(m, msk_idx, dom)
582-
return MOI.ConstraintIndex{MOI.VariableIndex,D}(xs.value)
580+
_check_bound_compat(m, x, set)
581+
set_flag(m, x, S)
582+
_add_variable_constraint(m, index, set)
583+
return MOI.ConstraintIndex{MOI.VariableIndex,S}(x.value)
583584
end
584585

585586
_cone_type(::Type{MOI.ExponentialCone}) = Mosek.MSK_CT_PEXP
@@ -746,8 +747,8 @@ function MOI.get(
746747
nnz, cols, vals = Mosek.getarow(m.task, row(m, ci))
747748
@assert nnz == length(cols) == length(vals)
748749
terms = MOI.ScalarAffineTerm{Float64}[
749-
MOI.ScalarAffineTerm(vals[i], index_of_column(m, cols[i])) for
750-
i in 1:nnz
750+
MOI.ScalarAffineTerm(v, _col_to_index(m, c)) for
751+
(v, c) in zip(vals, cols)
751752
]
752753
return MOI.ScalarAffineFunction(terms, 0.0)
753754
end
@@ -775,10 +776,8 @@ function MOI.get(
775776
::MOI.ConstraintFunction,
776777
ci::MOI.ConstraintIndex{MOI.VectorOfVariables,S},
777778
) where {S<:VectorCone}
778-
return MOI.VectorOfVariables([
779-
index_of_column(m, col) for
780-
col in reorder(columns(m, ci).values, S, true)
781-
])
779+
cols = reorder(columns(m, ci).values, S, true)
780+
return MOI.VectorOfVariables(_col_to_index.(m, cols))
782781
end
783782

784783
function MOI.get(
@@ -791,15 +790,17 @@ function MOI.get(
791790
throw(MOI.GetAttributeNotAllowed(attr))
792791
end
793792
r = rows(m, ci)
794-
(frow, fcol, fval) = Mosek.getaccftrip(m.task)
793+
frow, fcol, fval = Mosek.getaccftrip(m.task)
795794
constants = Mosek.getaccb(m.task, ci.value)
796795
set = MOI.Utilities.set_with_dimension(S, length(r))
797-
terms = [
798-
MOI.VectorAffineTerm(
799-
reorder(frow[i] - first(r) + 1, set, false),
800-
MOI.ScalarAffineTerm(fval[i], index_of_column(m, fcol[i])),
801-
) for i in eachindex(frow) if frow[i] in r
802-
]
796+
terms = MOI.VectorAffineTerm{Float64}[]
797+
for (frowi, fcoli, fvali) in zip(frow, fcol, fval)
798+
if frowi in r
799+
row = reorder(frowi - first(r) + 1, set, false)
800+
term = MOI.ScalarAffineTerm(fvali, _col_to_index(m, fcoli))
801+
push!(terms, MOI.VectorAffineTerm(row, term))
802+
end
803+
end
803804
return MOI.VectorAffineFunction(terms, -reorder(constants, S, false))
804805
end
805806

src/variable.jl

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,9 @@ function mosek_index(m::Optimizer, vi::MOI.VariableIndex)
4343
return m.x_sd[vi.value]
4444
end
4545

46-
function index_of_column(m::Optimizer, col::Int32)
47-
id = m.x_block.back[col]
48-
if iszero(id)
49-
return nothing
50-
end
51-
return MOI.VariableIndex(id)
46+
function _col_to_index(m::Optimizer, col::Int32)
47+
@assert !iszero(m.x_block.back[col])
48+
return MOI.VariableIndex(m.x_block.back[col])
5249
end
5350

5451
## Delete #####################################################################

test/runtests.jl

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -873,6 +873,60 @@ function test_ConstraintName()
873873
return
874874
end
875875

876+
function test_MSK_RES_ERR_MISSING_LICENSE_FILE()
877+
model = Mosek.Optimizer()
878+
x = MOI.add_variable(model)
879+
try
880+
MOI.optimize!(model)
881+
catch err
882+
@test err isa Mosek.MosekError
883+
@test Mosek.Rescode(err.rcode) == Mosek.MSK_RES_ERR_MISSING_LICENSE_FILE
884+
end
885+
return
886+
end
887+
888+
function test_throw_add_constraint_not_allowed_scalar()
889+
model = Mosek.Optimizer()
890+
set = MOI.PositiveSemidefiniteConeTriangle(2)
891+
x, _ = MOI.add_constrained_variables(model, set)
892+
@test_throws(
893+
MOI.AddConstraintNotAllowed{MOI.VariableIndex,MOI.Integer},
894+
MOI.add_constraint(model, x[1], MOI.Integer()),
895+
)
896+
return
897+
end
898+
899+
function test_solution_priority()
900+
for (whichsol, priority) in
901+
(Mosek.MSK_SOL_ITG => 3, Mosek.MSK_SOL_BAS => 2, Mosek.MSK_SOL_ITR => 1)
902+
for (solsta, flag) in (
903+
Mosek.MSK_SOL_STA_INTEGER_OPTIMAL => true,
904+
Mosek.MSK_SOL_STA_OPTIMAL => true,
905+
Mosek.MSK_SOL_STA_UNKNOWN => false,
906+
)
907+
solution = MosekTools.MosekSolution(
908+
whichsol,
909+
solsta,
910+
Mosek.MSK_PRO_STA_PRIM_AND_DUAL_FEAS,
911+
Mosek.Stakey[],
912+
Float64[],
913+
Vector{Float64}[],
914+
Float64[],
915+
Float64[],
916+
Float64[],
917+
Float64[],
918+
Mosek.Stakey[],
919+
Float64[],
920+
Float64[],
921+
Float64[],
922+
Float64[],
923+
)
924+
@test MosekTools._solution_priority(solution) == (flag, priority)
925+
end
926+
end
927+
return
928+
end
929+
876930
end # module
877931

878932
TestMosekTools.runtests()

0 commit comments

Comments
 (0)