Basis matrices
This tutorial was generated using Literate.jl. Download the source as a .jl file.
This tutorial explains how to query the basis of a linear program.
Setup
This tutorial uses the following packages:
using JuMP
import HiGHSStandard form example
Consider the following example, which is from the Wikipedia article on Basic feasible solutions:
\[\begin{aligned} \max \; & 0 \\ \text{s.t.}\; & 1x_1 + 5x_2 + 3x_3 + 4x_4 + 6x_5 = 14 \\ & 0x_1 + 1x_2 + 3x_3 + 5x_4 + 6x_5 = 7 \\ & x_i \ge 0, \forall i = 1,\ldots,5. \end{aligned}\]
The A matrix is:
A = [1 5 3 4 6; 0 1 3 5 6]2×5 Matrix{Int64}:
1 5 3 4 6
0 1 3 5 6and the right-hand side b vector is:
b = [14, 7]2-element Vector{Int64}:
14
7We can create and optimize the problem in the standard form:
n = size(A, 2)
model = Model(HiGHS.Optimizer)
set_silent(model)
@variable(model, x[1:n] >= 0)
@constraint(model, A * x == b)
optimize!(model)
assert_is_solved_and_feasible(model)This has a solution:
value.(x)5-element Vector{Float64}:
0.0
2.0
0.0
1.0
0.0Query the basis status of a variable using MOI.VariableBasisStatus:
get_attribute(x[1], MOI.VariableBasisStatus())NONBASIC_AT_LOWER::BasisStatusCode = 2the result is a MOI.BasisStatusCode. Query all of the basis statuses with the broadcast get_attribute.(:
get_attribute.(x, MOI.VariableBasisStatus())5-element Vector{MathOptInterface.BasisStatusCode}:
NONBASIC_AT_LOWER::BasisStatusCode = 2
BASIC::BasisStatusCode = 0
NONBASIC_AT_LOWER::BasisStatusCode = 2
BASIC::BasisStatusCode = 0
NONBASIC_AT_LOWER::BasisStatusCode = 2For this problem, the values are either MOI.NONBASIC_AT_LOWER or MOI.BASIC. All of the MOI.NONBASIC_AT_LOWER variables have a value at their lower bound. The MOI.BASIC variables correspond to the columns of the optimal basis.
Get the columns using:
indices = get_attribute.(x, MOI.VariableBasisStatus()) .== MOI.BASIC5-element BitVector:
0
1
0
1
0Filter the basis matrix from A:
B = A[:, indices]2×2 Matrix{Int64}:
5 4
1 5Since the basis matrix is non-singular, solving the system Bx = b must yield the optimal primal solution of the basic variables:
B \ b2-element Vector{Float64}:
2.0
0.9999999999999998value.(x[indices])2-element Vector{Float64}:
2.0
1.0A more complicated example
Often, you may want to work with the basis of a model that is not in a nice standard form. For example:
model = Model(HiGHS.Optimizer)
set_silent(model)
@variable(model, x >= 0)
@variable(model, 0 <= y <= 3)
@variable(model, z <= 1)
@objective(model, Min, 12x + 20y - z)
@constraint(model, c1, 6x + 8y >= 100)
@constraint(model, c2, 7x + 12y >= 120)
@constraint(model, c3, x + y <= 20)
optimize!(model)
assert_is_solved_and_feasible(model)A common way to query the basis status of every variable is:
v_basis = Dict(
xi => get_attribute(xi, MOI.VariableBasisStatus()) for
xi in all_variables(model)
)Dict{VariableRef, MathOptInterface.BasisStatusCode} with 3 entries:
y => BASIC
x => BASIC
z => NONBASIC_AT_UPPERDespite the model having three constraints, there are only two basic variables. Since the basis matrix must be square, where is the other basic variable?
The answer is that solvers will reformulate inequality constraints:
\[A x \le b\]
into the system:
\[A x + Is = b\]
Thus, for every inequality constraint there is a slack variable s.
Query the basis status of the slack variables associated with a constraint using MOI.ConstraintBasisStatus:
c_basis = Dict(
ci => get_attribute(ci, MOI.ConstraintBasisStatus()) for ci in
all_constraints(model; include_variable_in_set_constraints = false)
)Dict{ConstraintRef{Model, C, ScalarShape} where C, MathOptInterface.BasisStatusCode} with 3 entries:
c1 : 6 x + 8 y ≥ 100 => NONBASIC
c3 : x + y ≤ 20 => BASIC
c2 : 7 x + 12 y ≥ 120 => NONBASICThus, the basis is formed by x, y, and the slack associated with c3.
A simple way to get the A matrix of an unstructured linear program is with lp_matrix_data:
matrix = lp_matrix_data(model)
matrix.A3×3 SparseArrays.SparseMatrixCSC{Float64, Int64} with 6 stored entries:
6.0 8.0 ⋅
7.0 12.0 ⋅
1.0 1.0 ⋅ You can check the permutation of the rows and columns using
matrix.variables3-element Vector{VariableRef}:
x
y
zand
matrix.affine_constraints3-element Vector{ConstraintRef}:
c1 : 6 x + 8 y ≥ 100
c2 : 7 x + 12 y ≥ 120
c3 : x + y ≤ 20We can construct the slack column associated with c3 as:
s_column = zeros(size(matrix.A, 1))
s_column[3] = 1.01.0The full basis matrix is therefore:
B = hcat(matrix.A[:, [1, 2]], s_column)3×3 SparseArrays.SparseMatrixCSC{Float64, Int64} with 7 stored entries:
6.0 8.0 ⋅
7.0 12.0 ⋅
1.0 1.0 1.0lp_matrix_data returns separate vectors for the lower and upper row bounds. Convert to a single right-hand side vector by taking the finite elements:
b = ifelse.(isfinite.(matrix.b_lower), matrix.b_lower, matrix.b_upper)3-element Vector{Float64}:
100.0
120.0
20.0Solving the Basis system as before yields:
B \ b3-element Vector{Float64}:
14.999999999999995
1.250000000000004
3.75which is the value of x, y, and the slack associated with c3.
Identifying degenerate variables
Another common task is identifying degenerate variables. A degenerate variable is a basic variable that has an optimal value at its lower or upper bound.
Here is a function that computes whether a variable is degenerate:
function is_degenerate(x)
if get_attribute(x, MOI.VariableBasisStatus()) == MOI.BASIC
return (has_lower_bound(x) && ≈(value(x), lower_bound(x))) ||
(has_upper_bound(x) && ≈(value(x), upper_bound(x)))
end
return false
endis_degenerate (generic function with 1 method)A simple example of a linear program with a degenerate solution is:
A, b, c = [1 1; 0 1], [1, 1], [1, 1]
model = Model(HiGHS.Optimizer);
set_silent(model)
@variable(model, x[1:2] >= 0)
@objective(model, Min, c' * x)
@constraint(model, A * x == b)
optimize!(model)
degenerate_variables = filter(is_degenerate, all_variables(model))1-element Vector{VariableRef}:
x[1]The solution is degenerate because:
value(x[1])-0.0and
get_attribute(x[1], MOI.VariableBasisStatus())BASIC::BasisStatusCode = 0