From 3c0b3ade35f3316add6dd90daf5cf905e0bba68d Mon Sep 17 00:00:00 2001 From: odow Date: Thu, 16 Nov 2023 09:33:44 +1300 Subject: [PATCH 1/8] Add StandardFormMatrix --- src/JuMP.jl | 1 + src/lp_sensitivity2.jl | 114 ++---------------- src/standard_form_matrix.jl | 192 ++++++++++++++++++++++++++++++ test/test_standard_form_matrix.jl | 99 +++++++++++++++ 4 files changed, 305 insertions(+), 101 deletions(-) create mode 100644 src/standard_form_matrix.jl create mode 100644 test/test_standard_form_matrix.jl diff --git a/src/JuMP.jl b/src/JuMP.jl index ddf75e36450..495d1c68d55 100644 --- a/src/JuMP.jl +++ b/src/JuMP.jl @@ -1091,6 +1091,7 @@ include("complement.jl") include("copy.jl") include("feasibility_checker.jl") include("file_formats.jl") +include("standard_form_matrix.jl") include("lp_sensitivity2.jl") include("indicator.jl") include("reified.jl") diff --git a/src/lp_sensitivity2.jl b/src/lp_sensitivity2.jl index 700c50863cd..eee95d3a8fd 100644 --- a/src/lp_sensitivity2.jl +++ b/src/lp_sensitivity2.jl @@ -325,114 +325,26 @@ end """ _standard_form_matrix(model::GenericModel) -Given a problem: - - r_l <= Ax <= r_u - c_l <= x <= c_u - -Return the standard form: - - [A -I] [x, y] = 0 - [c_l, r_l] <= [x, y] <= [c_u, r_u] - -`columns` maps the variable references to column indices. +See [`StandardFormMatrix`](@ref) instead. """ function _standard_form_matrix(model::GenericModel{T}) where {T} - columns = Dict(var => i for (i, var) in enumerate(all_variables(model))) - n = length(columns) - c_l, c_u = fill(typemin(T), n), fill(typemax(T), n) - r_l, r_u = T[], T[] - I, J, V = Int[], Int[], T[] - bound_constraints = ConstraintRef[] - affine_constraints = ConstraintRef[] - for (F, S) in list_of_constraint_types(model) - _fill_standard_form( - model, - columns, - bound_constraints, - affine_constraints, - F, - S, - c_l, - c_u, - r_l, - r_u, - I, - J, - V, - ) + matrix = StandardFormMatrix(model) + m, n = length(matrix.affine_constraints), length(matrix.variable_to_column) + for row in 1:m + push!(matrix.I, row) + push!(matrix.J, n + row) + push!(matrix.V, -one(T)) end return ( - columns = columns, - lower = vcat(c_l, r_l), - upper = vcat(c_u, r_u), - A = SparseArrays.sparse(I, J, V, length(r_l), n + length(r_l)), - bounds = bound_constraints, - constraints = affine_constraints, + columns = matrix.variable_to_column, + lower = vcat(matrix.x_l, matrix.b_l), + upper = vcat(matrix.x_u, matrix.b_u), + A = SparseArrays.sparse(matrix.I, matrix.J, matrix.V, m, m + n), + bounds = matrix.variable_constraints, + constraints = matrix.affine_constraints, ) end -function _fill_standard_form( - model::GenericModel{T}, - x::Dict{GenericVariableRef{T},Int}, - bound_constraints::Vector{ConstraintRef}, - ::Vector{ConstraintRef}, - F::Type{GenericVariableRef{T}}, - S::Type, - c_l::Vector{T}, - c_u::Vector{T}, - ::Vector{T}, - ::Vector{T}, - ::Vector{Int}, - ::Vector{Int}, - ::Vector{T}, -) where {T} - for c in all_constraints(model, F, S) - push!(bound_constraints, c) - c_obj = constraint_object(c) - i = x[c_obj.func] - set = MOI.Interval(c_obj.set) - c_l[i] = max(c_l[i], set.lower) - c_u[i] = min(c_u[i], set.upper) - end - return -end - -function _fill_standard_form( - model::GenericModel{T}, - x::Dict{GenericVariableRef{T},Int}, - ::Vector{ConstraintRef}, - affine_constraints::Vector{ConstraintRef}, - F::Type{<:GenericAffExpr}, - S::Type, - ::Vector{T}, - ::Vector{T}, - r_l::Vector{T}, - r_u::Vector{T}, - I::Vector{Int}, - J::Vector{Int}, - V::Vector{T}, -) where {T} - for c in all_constraints(model, F, S) - push!(affine_constraints, c) - c_obj = constraint_object(c) - @assert iszero(c_obj.func.constant) - row = length(r_l) + 1 - set = MOI.Interval(c_obj.set) - push!(r_l, set.lower) - push!(r_u, set.upper) - for (var, coef) in c_obj.func.terms - push!(I, row) - push!(J, x[var]) - push!(V, coef) - end - push!(I, row) - push!(J, length(x) + row) - push!(V, -one(T)) - end - return -end - _convert_nonbasic_status(::MOI.LessThan) = MOI.NONBASIC_AT_UPPER _convert_nonbasic_status(::MOI.GreaterThan) = MOI.NONBASIC_AT_LOWER _convert_nonbasic_status(::Any) = MOI.NONBASIC diff --git a/src/standard_form_matrix.jl b/src/standard_form_matrix.jl new file mode 100644 index 00000000000..cf5982b60ba --- /dev/null +++ b/src/standard_form_matrix.jl @@ -0,0 +1,192 @@ +# Copyright 2017, Iain Dunning, Joey Huchette, Miles Lubin, and contributors +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +struct StandardFormMatrix{T} + A::SparseArrays.SparseMatrixCSC{T,Int} + b_lower::Vector{T} + b_upper::Vector{T} + x_lower::Vector{T} + x_upper::Vector{T} + c::Vector{T} + c_offset::T + sense::MOI.OptimizationSense + variable_to_column::Dict{GenericVariableRef{T},Int} + affine_constraints::Vector{ConstraintRef} + variable_constraints::Vector{ConstraintRef} +end + +""" + StandardFormMatrix(model::GenericModel{T}) + +Given a problem in the form: +```math +\\begin{aligned} +\\min & c^\\top x + c_0\\ + & b_l \\le A x \\le b_u \\ + & x_l \\le x \\le x_u +\\end{aligned} +``` +return a struct storing problem data in matrix form. + +## Fields + +The struct returned by [`StandardFormMatrix`](@ref) has the fields: + + * `A::SparseArrays.SparseMatrixCSC{T,Int}`: the constraint matrix in sparse + matrix form. + * `b_lower::Vector{T}`: the dense vector of row lower bounds. If missing, the + value of `typemin(T)` is used. + * `b_upper::Vector{T}`: the dense vector of row upper bounds. If missing, the + value of `typemax(T)` is used. + * `x_lower::Vector{T}`: the dense vector of variable lower bounds. If missing, + the value of `typemin(T)` is used. + * `x_upper::Vector{T}`: the dense vector of variable upper bounds. If missing, + the value of `typemax(T)` is used. + * `c::Vector{T}`: the dense vector of linear objective coefficiennts + * `c_offset::T`: the constant term in the objective function. + * `sense::MOI.OptimizationSense`: the objective sense of the model. + * `variable_to_column::Dict{GenericVariableRef{T},Int}`: a dictionary mapping + JuMP [`GenericVariableRef`](@ref) to the 1-indexed column in the matrix + representation. + * `affine_constraints::Vector{ConstraintRef}`: a vector of [`ConstraintRef`](@ref), + corresponding to the order of rows in the matrix form. + +## Limitations + +The models supported by [`StandardFormMatrix`](@ref) are intentionally limited +to linear programs. + +If your model has integrality, use [`relax_integrality`](@ref) to remove integer +restrictions before calling [`StandardFormMatrix`](@ref). +""" +function StandardFormMatrix(model::GenericModel{T}) where {T} + columns = Dict(var => i for (i, var) in enumerate(all_variables(model))) + n = length(columns) + cache = (; + x_l = fill(typemin(T), n), + x_u = fill(typemax(T), n), + c = zeros(T, n), + c_offset = Ref{T}(zero(T)), + b_l = T[], + b_u = T[], + I = Int[], + J = Int[], + V = T[], + variable_to_column = columns, + bound_constraints = ConstraintRef[], + affine_constraints = ConstraintRef[], + ) + for (F, S) in list_of_constraint_types(model) + _fill_standard_form(model, F, S, cache) + end + _fill_standard_form(model, objective_function_type(model), cache) + return StandardFormMatrix( + SparseArrays.sparse(cache.I, cache.J, cache.V, length(cache.b_l), n), + cache.b_l, + cache.b_u, + cache.x_l, + cache.x_u, + cache.c, + cache.c_offset[], + MOI.get(model, MOI.ObjectiveSense()), + cache.variable_to_column, + cache.affine_constraints, + cache.bound_constraints, + ) +end + +_bounds(s::MOI.LessThan{T}) where {T} = (typemin(T), s.upper) +_bounds(s::MOI.GreaterThan{T}) where {T} = (s.lower, typemax(T)) +_bounds(s::MOI.EqualTo{T}) where {T} = (s.value, s.value) +_bounds(s::MOI.Interval{T}) where {T} = (s.lower, s.upper) + +function _fill_standard_form( + model::GenericModel{T}, + ::Type{GenericVariableRef{T}}, + ::Type{S}, + cache::Any, +) where { + T, + S<:Union{MOI.LessThan{T},MOI.GreaterThan{T},MOI.EqualTo{T},MOI.Interval{T}}, +} + for c in all_constraints(model, GenericVariableRef{T}, S) + push!(cache.bound_constraints, c) + c_obj = constraint_object(c) + i = cache.variable_to_column[c_obj.func] + l, u = _bounds(c_obj.set) + cache.x_l[i] = max(cache.x_l[i], l) + cache.x_u[i] = min(cache.x_u[i], u) + end + return +end + +function _fill_standard_form( + model::GenericModel{T}, + ::Type{F}, + ::Type{S}, + cache::Any, +) where { + T, + F<:GenericAffExpr{T,GenericVariableRef{T}}, + S<:Union{MOI.LessThan{T},MOI.GreaterThan{T},MOI.EqualTo{T},MOI.Interval{T}}, +} + for c in all_constraints(model, F, S) + push!(cache.affine_constraints, c) + c_obj = constraint_object(c) + @assert iszero(c_obj.func.constant) + row = length(cache.b_l) + 1 + l, u = _bounds(c_obj.set) + push!(cache.b_l, l) + push!(cache.b_u, u) + for (x, coef) in c_obj.func.terms + push!(cache.I, row) + push!(cache.J, cache.variable_to_column[x]) + push!(cache.V, coef) + end + end + return +end + +function _fill_standard_form( + ::GenericModel{T}, + ::Type{F}, + ::Type{S}, + ::Any, +) where {T,F,S} + return error( + "Unsupported constraint type in `StandardFormMatrix`: $F -in- $S", + ) +end + +function _fill_standard_form( + model::GenericModel{T}, + ::Type{GenericVariableRef{T}}, + cache::Any, +) where {T} + cache.c[cache.variable_to_column[objective_function(model)]] = one(T) + cache.c_offset[] = zero(T) + return +end + +function _fill_standard_form( + model::GenericModel{T}, + ::Type{GenericAffExpr{T,GenericVariableRef{T}}}, + cache::Any, +) where {T} + f = objective_function(model) + for (k, v) in f.terms + cache.c[cache.variable_to_column[k]] += v + end + cache.c_offset[] = f.constant + return +end + +function _fill_standard_form( + ::GenericModel{T}, + ::Type{F}, + ::Any, +) where {T,F} + return error("Unsupported objective type in `StandardFormMatrix`: $F") +end diff --git a/test/test_standard_form_matrix.jl b/test/test_standard_form_matrix.jl new file mode 100644 index 00000000000..f63f8611c39 --- /dev/null +++ b/test/test_standard_form_matrix.jl @@ -0,0 +1,99 @@ +# Copyright 2017, Iain Dunning, Joey Huchette, Miles Lubin, and contributors +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +module TestStandardFormMatrix + +using JuMP +using Test + +function test_standard_matrix_form() + model = Model() + @variable(model, x >= 1) + @variable(model, 2 <= y) + @variable(model, 3 <= z <= 4) + @constraint(model, x == 5) + @constraint(model, 2x + 3y <= 6) + @constraint(model, -4y >= 5z + 7) + @constraint(model, -1 <= x + y <= 2) + @objective(model, Max, 1 + 2x) + a = StandardFormMatrix(model) + @test Matrix(a.A) == [1 0 0; 0 -4 -5; 2 3 0; 1 1 0] + @test a.b_lower == [5, 7, -Inf, -1] + @test a.b_upper == [5, Inf, 6, 2] + @test a.x_lower == [1, 2, 3] + @test a.x_upper == [Inf, Inf, 4] + @test a.c == [2, 0, 0] + @test a.c_offset == 1 + @test a.sense == MOI.MAX_SENSE + @objective(model, Min, y) + b = StandardFormMatrix(model) + b.sense == MOI.MIN_SENSE + b.c == [0, 1, 0] + b.c_offset == 0 + return +end + +function test_standard_matrix_form_rational() + model = GenericModel{Rational{Int}}() + @variable(model, x >= 1) + @variable(model, 2 <= y) + @variable(model, 3 <= z <= 4) + @constraint(model, x == 5) + @constraint(model, (2 // 3)x + 3y <= 6 // 5) + @constraint(model, -4y >= 5z + 7 // 8) + @constraint(model, -1 <= x + y <= 2) + @objective(model, Min, (1 // 2) - 2x + (1//3)*z) + a = StandardFormMatrix(model) + @test Matrix(a.A) == [1 0 0; 0 -4 -5; (2 // 3) 3 0; 1 1 0] + @test a.b_lower == [5, 7 // 8, -1 // 0, -1] + @test a.b_upper == [5, 1 // 0, 6 // 5, 2] + @test a.x_lower == [1, 2, 3] + @test a.x_upper == [1 // 0, 1//0, 4] + @test a.c == [-2, 0, 1//3] + @test a.c_offset == 1 // 2 + @test a.sense == MOI.MIN_SENSE + return +end + +function test_standard_matrix_form_empty() + model = Model() + a = StandardFormMatrix(model) + @test size(a.A) == (0, 0) + @test isempty(a.b_lower) + @test isempty(a.b_upper) + @test isempty(a.x_lower) + @test isempty(a.x_upper) + @test isempty(a.c) + @test a.c_offset == 0 + @test a.sense == MOI.FEASIBILITY_SENSE + return +end + +function test_standard_matrix_form_bad_constraint() + model = Model() + @variable(model, x, Bin) + @test_throws( + ErrorException( + "Unsupported constraint type in `StandardFormMatrix`: $(VariableRef) -in- $(MOI.ZeroOne)", + ), + StandardFormMatrix(model), + ) + return +end + +function test_standard_matrix_form_bad_objective() + model = Model() + @variable(model, x) + @objective(model, Min, [2x + 1, 3x]) + @test_throws( + ErrorException( + "Unsupported objective type in `StandardFormMatrix`: $(Vector{AffExpr})", + ), + StandardFormMatrix(model), + ) + return +end + +end # module From 62f2f6ed280ffcd80791ca0c995bc71162e1b2a8 Mon Sep 17 00:00:00 2001 From: odow Date: Thu, 16 Nov 2023 09:59:54 +1300 Subject: [PATCH 2/8] Update --- src/lp_sensitivity2.jl | 13 ++++--------- src/standard_form_matrix.jl | 6 +----- test/test_standard_form_matrix.jl | 6 +++--- 3 files changed, 8 insertions(+), 17 deletions(-) diff --git a/src/lp_sensitivity2.jl b/src/lp_sensitivity2.jl index eee95d3a8fd..407b61181af 100644 --- a/src/lp_sensitivity2.jl +++ b/src/lp_sensitivity2.jl @@ -329,17 +329,12 @@ See [`StandardFormMatrix`](@ref) instead. """ function _standard_form_matrix(model::GenericModel{T}) where {T} matrix = StandardFormMatrix(model) - m, n = length(matrix.affine_constraints), length(matrix.variable_to_column) - for row in 1:m - push!(matrix.I, row) - push!(matrix.J, n + row) - push!(matrix.V, -one(T)) - end + I = SparseArrays.spdiagm(fill(-one(T), length(matrix.affine_constraints))) return ( columns = matrix.variable_to_column, - lower = vcat(matrix.x_l, matrix.b_l), - upper = vcat(matrix.x_u, matrix.b_u), - A = SparseArrays.sparse(matrix.I, matrix.J, matrix.V, m, m + n), + lower = vcat(matrix.x_lower, matrix.b_lower), + upper = vcat(matrix.x_upper, matrix.b_upper), + A = hcat(matrix.A, I), bounds = matrix.variable_constraints, constraints = matrix.affine_constraints, ) diff --git a/src/standard_form_matrix.jl b/src/standard_form_matrix.jl index cf5982b60ba..f5eaac8d043 100644 --- a/src/standard_form_matrix.jl +++ b/src/standard_form_matrix.jl @@ -183,10 +183,6 @@ function _fill_standard_form( return end -function _fill_standard_form( - ::GenericModel{T}, - ::Type{F}, - ::Any, -) where {T,F} +function _fill_standard_form(::GenericModel{T}, ::Type{F}, ::Any) where {T,F} return error("Unsupported objective type in `StandardFormMatrix`: $F") end diff --git a/test/test_standard_form_matrix.jl b/test/test_standard_form_matrix.jl index f63f8611c39..c47f6876fa2 100644 --- a/test/test_standard_form_matrix.jl +++ b/test/test_standard_form_matrix.jl @@ -44,14 +44,14 @@ function test_standard_matrix_form_rational() @constraint(model, (2 // 3)x + 3y <= 6 // 5) @constraint(model, -4y >= 5z + 7 // 8) @constraint(model, -1 <= x + y <= 2) - @objective(model, Min, (1 // 2) - 2x + (1//3)*z) + @objective(model, Min, (1 // 2) - 2x + (1 // 3) * z) a = StandardFormMatrix(model) - @test Matrix(a.A) == [1 0 0; 0 -4 -5; (2 // 3) 3 0; 1 1 0] + @test Matrix(a.A) == [1 0 0; 0 -4 -5; (2//3) 3 0; 1 1 0] @test a.b_lower == [5, 7 // 8, -1 // 0, -1] @test a.b_upper == [5, 1 // 0, 6 // 5, 2] @test a.x_lower == [1, 2, 3] @test a.x_upper == [1 // 0, 1//0, 4] - @test a.c == [-2, 0, 1//3] + @test a.c == [-2, 0, 1 // 3] @test a.c_offset == 1 // 2 @test a.sense == MOI.MIN_SENSE return From d5ecdb1515f15507af1fe1c1ff73d10dc0ea77e7 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Thu, 16 Nov 2023 10:04:47 +1300 Subject: [PATCH 3/8] Update test/test_standard_form_matrix.jl --- test/test_standard_form_matrix.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_standard_form_matrix.jl b/test/test_standard_form_matrix.jl index c47f6876fa2..6357b2db1c4 100644 --- a/test/test_standard_form_matrix.jl +++ b/test/test_standard_form_matrix.jl @@ -50,7 +50,7 @@ function test_standard_matrix_form_rational() @test a.b_lower == [5, 7 // 8, -1 // 0, -1] @test a.b_upper == [5, 1 // 0, 6 // 5, 2] @test a.x_lower == [1, 2, 3] - @test a.x_upper == [1 // 0, 1//0, 4] + @test a.x_upper == [1 // 0, 1 // 0, 4] @test a.c == [-2, 0, 1 // 3] @test a.c_offset == 1 // 2 @test a.sense == MOI.MIN_SENSE From ad9c6b8493356267089c8529604467ef6438c82a Mon Sep 17 00:00:00 2001 From: odow Date: Thu, 16 Nov 2023 15:44:15 +1300 Subject: [PATCH 4/8] lp_matrix_data --- src/JuMP.jl | 2 +- ...ndard_form_matrix.jl => lp_matrix_data.jl} | 28 +++++++++++-------- src/lp_sensitivity2.jl | 4 +-- ..._form_matrix.jl => test_lp_matrix_data.jl} | 18 ++++++------ 4 files changed, 29 insertions(+), 23 deletions(-) rename src/{standard_form_matrix.jl => lp_matrix_data.jl} (87%) rename test/{test_standard_form_matrix.jl => test_lp_matrix_data.jl} (85%) diff --git a/src/JuMP.jl b/src/JuMP.jl index 495d1c68d55..1ceb3e88831 100644 --- a/src/JuMP.jl +++ b/src/JuMP.jl @@ -1091,7 +1091,7 @@ include("complement.jl") include("copy.jl") include("feasibility_checker.jl") include("file_formats.jl") -include("standard_form_matrix.jl") +include("lp_matrix_data.jl") include("lp_sensitivity2.jl") include("indicator.jl") include("reified.jl") diff --git a/src/standard_form_matrix.jl b/src/lp_matrix_data.jl similarity index 87% rename from src/standard_form_matrix.jl rename to src/lp_matrix_data.jl index f5eaac8d043..5b23c1860ec 100644 --- a/src/standard_form_matrix.jl +++ b/src/lp_matrix_data.jl @@ -3,7 +3,13 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. -struct StandardFormMatrix{T} +""" + LPMatrixData{T} + +The struct returned by [`lp_matrix_data`](@ref). See [`lp_matrix_data`](@ref) +for a description of the public fields. +""" +struct LPMatrixData{T} A::SparseArrays.SparseMatrixCSC{T,Int} b_lower::Vector{T} b_upper::Vector{T} @@ -18,9 +24,10 @@ struct StandardFormMatrix{T} end """ - StandardFormMatrix(model::GenericModel{T}) + lp_matrix_data(model::GenericModel{T}) -Given a problem in the form: +Given a JuMP model of a linear program, return a [`LPMatrixData{T}`](@ref) +struct storing data for an equivalent linear program in the form: ```math \\begin{aligned} \\min & c^\\top x + c_0\\ @@ -28,11 +35,10 @@ Given a problem in the form: & x_l \\le x \\le x_u \\end{aligned} ``` -return a struct storing problem data in matrix form. ## Fields -The struct returned by [`StandardFormMatrix`](@ref) has the fields: +The struct returned by [`lp_matrix_data`](@ref) has the fields: * `A::SparseArrays.SparseMatrixCSC{T,Int}`: the constraint matrix in sparse matrix form. @@ -55,13 +61,13 @@ The struct returned by [`StandardFormMatrix`](@ref) has the fields: ## Limitations -The models supported by [`StandardFormMatrix`](@ref) are intentionally limited +The models supported by [`lp_matrix_data`](@ref) are intentionally limited to linear programs. If your model has integrality, use [`relax_integrality`](@ref) to remove integer -restrictions before calling [`StandardFormMatrix`](@ref). +restrictions before calling [`lp_matrix_data`](@ref). """ -function StandardFormMatrix(model::GenericModel{T}) where {T} +function lp_matrix_data(model::GenericModel{T}) where {T} columns = Dict(var => i for (i, var) in enumerate(all_variables(model))) n = length(columns) cache = (; @@ -82,7 +88,7 @@ function StandardFormMatrix(model::GenericModel{T}) where {T} _fill_standard_form(model, F, S, cache) end _fill_standard_form(model, objective_function_type(model), cache) - return StandardFormMatrix( + return LPMatrixData( SparseArrays.sparse(cache.I, cache.J, cache.V, length(cache.b_l), n), cache.b_l, cache.b_u, @@ -156,7 +162,7 @@ function _fill_standard_form( ::Any, ) where {T,F,S} return error( - "Unsupported constraint type in `StandardFormMatrix`: $F -in- $S", + "Unsupported constraint type in `lp_matrix_data`: $F -in- $S", ) end @@ -184,5 +190,5 @@ function _fill_standard_form( end function _fill_standard_form(::GenericModel{T}, ::Type{F}, ::Any) where {T,F} - return error("Unsupported objective type in `StandardFormMatrix`: $F") + return error("Unsupported objective type in `lp_matrix_data`: $F") end diff --git a/src/lp_sensitivity2.jl b/src/lp_sensitivity2.jl index 407b61181af..5a0dc446107 100644 --- a/src/lp_sensitivity2.jl +++ b/src/lp_sensitivity2.jl @@ -325,10 +325,10 @@ end """ _standard_form_matrix(model::GenericModel) -See [`StandardFormMatrix`](@ref) instead. +See [`lp_matrix_data`](@ref) instead. """ function _standard_form_matrix(model::GenericModel{T}) where {T} - matrix = StandardFormMatrix(model) + matrix = lp_matrix_data(model) I = SparseArrays.spdiagm(fill(-one(T), length(matrix.affine_constraints))) return ( columns = matrix.variable_to_column, diff --git a/test/test_standard_form_matrix.jl b/test/test_lp_matrix_data.jl similarity index 85% rename from test/test_standard_form_matrix.jl rename to test/test_lp_matrix_data.jl index 6357b2db1c4..c5c906bd9ce 100644 --- a/test/test_standard_form_matrix.jl +++ b/test/test_lp_matrix_data.jl @@ -3,7 +3,7 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. -module TestStandardFormMatrix +module TestLPMatrixData using JuMP using Test @@ -18,7 +18,7 @@ function test_standard_matrix_form() @constraint(model, -4y >= 5z + 7) @constraint(model, -1 <= x + y <= 2) @objective(model, Max, 1 + 2x) - a = StandardFormMatrix(model) + a = lp_matrix_data(model) @test Matrix(a.A) == [1 0 0; 0 -4 -5; 2 3 0; 1 1 0] @test a.b_lower == [5, 7, -Inf, -1] @test a.b_upper == [5, Inf, 6, 2] @@ -28,7 +28,7 @@ function test_standard_matrix_form() @test a.c_offset == 1 @test a.sense == MOI.MAX_SENSE @objective(model, Min, y) - b = StandardFormMatrix(model) + b = lp_matrix_data(model) b.sense == MOI.MIN_SENSE b.c == [0, 1, 0] b.c_offset == 0 @@ -45,7 +45,7 @@ function test_standard_matrix_form_rational() @constraint(model, -4y >= 5z + 7 // 8) @constraint(model, -1 <= x + y <= 2) @objective(model, Min, (1 // 2) - 2x + (1 // 3) * z) - a = StandardFormMatrix(model) + a = lp_matrix_data(model) @test Matrix(a.A) == [1 0 0; 0 -4 -5; (2//3) 3 0; 1 1 0] @test a.b_lower == [5, 7 // 8, -1 // 0, -1] @test a.b_upper == [5, 1 // 0, 6 // 5, 2] @@ -59,7 +59,7 @@ end function test_standard_matrix_form_empty() model = Model() - a = StandardFormMatrix(model) + a = lp_matrix_data(model) @test size(a.A) == (0, 0) @test isempty(a.b_lower) @test isempty(a.b_upper) @@ -76,9 +76,9 @@ function test_standard_matrix_form_bad_constraint() @variable(model, x, Bin) @test_throws( ErrorException( - "Unsupported constraint type in `StandardFormMatrix`: $(VariableRef) -in- $(MOI.ZeroOne)", + "Unsupported constraint type in `lp_matrix_data`: $(VariableRef) -in- $(MOI.ZeroOne)", ), - StandardFormMatrix(model), + lp_matrix_data(model), ) return end @@ -89,9 +89,9 @@ function test_standard_matrix_form_bad_objective() @objective(model, Min, [2x + 1, 3x]) @test_throws( ErrorException( - "Unsupported objective type in `StandardFormMatrix`: $(Vector{AffExpr})", + "Unsupported objective type in `lp_matrix_data`: $(Vector{AffExpr})", ), - StandardFormMatrix(model), + lp_matrix_data(model), ) return end From bb1bc05be8cd173cd467ceb2642cfab9c5ab47cd Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Thu, 16 Nov 2023 18:53:40 +1300 Subject: [PATCH 5/8] Update src/lp_matrix_data.jl Co-authored-by: James Foster <38274066+jd-foster@users.noreply.github.com> --- src/lp_matrix_data.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lp_matrix_data.jl b/src/lp_matrix_data.jl index 5b23c1860ec..bfc5af2d908 100644 --- a/src/lp_matrix_data.jl +++ b/src/lp_matrix_data.jl @@ -26,7 +26,7 @@ end """ lp_matrix_data(model::GenericModel{T}) -Given a JuMP model of a linear program, return a [`LPMatrixData{T}`](@ref) +Given a JuMP model of a linear program, return an [`LPMatrixData{T}`](@ref) struct storing data for an equivalent linear program in the form: ```math \\begin{aligned} From 43b32d6697c01563bea0ea07208fedac25222ad6 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Fri, 17 Nov 2023 09:43:10 +1300 Subject: [PATCH 6/8] Update lp_matrix_data.jl --- src/lp_matrix_data.jl | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/lp_matrix_data.jl b/src/lp_matrix_data.jl index bfc5af2d908..4603f1c8134 100644 --- a/src/lp_matrix_data.jl +++ b/src/lp_matrix_data.jl @@ -161,9 +161,7 @@ function _fill_standard_form( ::Type{S}, ::Any, ) where {T,F,S} - return error( - "Unsupported constraint type in `lp_matrix_data`: $F -in- $S", - ) + return error("Unsupported constraint type in `lp_matrix_data`: $F -in- $S") end function _fill_standard_form( From 968365de026bcd5386cca3b2d5d6d6599cedcd20 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Fri, 17 Nov 2023 09:49:31 +1300 Subject: [PATCH 7/8] Update models.md --- docs/src/manual/models.md | 66 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/docs/src/manual/models.md b/docs/src/manual/models.md index e6e36441de4..adc6112c4c1 100644 --- a/docs/src/manual/models.md +++ b/docs/src/manual/models.md @@ -439,6 +439,72 @@ optimize!(model) reduced_cost(x) # Works ``` +## Get the matrix representation + +Use [`lp_matrix_data`](@ref) to return a data structure that represents the +matrix form of a linear program. + +```jldoctest +julia> begin + model = Model() + @variable(model, x >= 1) + @variable(model, 2 <= y) + @variable(model, 3 <= z <= 4) + @constraint(model, x == 5) + @constraint(model, 2x + 3y <= 6) + @constraint(model, -4y >= 5z + 7) + @constraint(model, -1 <= x + y <= 2) + @objective(model, Max, 1 + 2x) + end; + +julia> data = lp_matrix_data(model); + +julia> data.A +4×3 SparseArrays.SparseMatrixCSC{Float64, Int64} with 7 stored entries: + 1.0 ⋅ ⋅ + ⋅ -4.0 -5.0 + 2.0 3.0 ⋅ + 1.0 1.0 ⋅ + +julia> data.b_lower +4-element Vector{Float64}: + 5.0 + 7.0 + -Inf + -1.0 + +julia> data.b_upper +4-element Vector{Float64}: + 5.0 + Inf + 6.0 + 2.0 + +julia> data.x_lower +3-element Vector{Float64}: + 1.0 + 2.0 + 3.0 + +julia> data.x_upper +3-element Vector{Float64}: + Inf + Inf + 4.0 + +julia> data.c +3-element Vector{Float64}: + 2.0 + 0.0 + 0.0 + +julia> data.c_offset +1.0 + +julia> data.sense +MAX_SENSE::OptimizationSense = 1 +``` + ## Backends !!! info From d269bf231297cd693e913ed34beb710c21ce4607 Mon Sep 17 00:00:00 2001 From: odow Date: Tue, 21 Nov 2023 09:28:21 +1300 Subject: [PATCH 8/8] Update --- src/lp_matrix_data.jl | 12 ++++++------ src/lp_sensitivity2.jl | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/lp_matrix_data.jl b/src/lp_matrix_data.jl index 4603f1c8134..51815622553 100644 --- a/src/lp_matrix_data.jl +++ b/src/lp_matrix_data.jl @@ -18,7 +18,7 @@ struct LPMatrixData{T} c::Vector{T} c_offset::T sense::MOI.OptimizationSense - variable_to_column::Dict{GenericVariableRef{T},Int} + variables::Vector{GenericVariableRef{T}} affine_constraints::Vector{ConstraintRef} variable_constraints::Vector{ConstraintRef} end @@ -53,9 +53,8 @@ The struct returned by [`lp_matrix_data`](@ref) has the fields: * `c::Vector{T}`: the dense vector of linear objective coefficiennts * `c_offset::T`: the constant term in the objective function. * `sense::MOI.OptimizationSense`: the objective sense of the model. - * `variable_to_column::Dict{GenericVariableRef{T},Int}`: a dictionary mapping - JuMP [`GenericVariableRef`](@ref) to the 1-indexed column in the matrix - representation. + * `variables::Vector{GenericVariableRef{T}}`: a vector of [`GenericVariableRef`](@ref), + corresponding to order of the columns in the matrix form. * `affine_constraints::Vector{ConstraintRef}`: a vector of [`ConstraintRef`](@ref), corresponding to the order of rows in the matrix form. @@ -68,7 +67,8 @@ If your model has integrality, use [`relax_integrality`](@ref) to remove integer restrictions before calling [`lp_matrix_data`](@ref). """ function lp_matrix_data(model::GenericModel{T}) where {T} - columns = Dict(var => i for (i, var) in enumerate(all_variables(model))) + variables = all_variables(model) + columns = Dict(var => i for (i, var) in enumerate(variables)) n = length(columns) cache = (; x_l = fill(typemin(T), n), @@ -97,7 +97,7 @@ function lp_matrix_data(model::GenericModel{T}) where {T} cache.c, cache.c_offset[], MOI.get(model, MOI.ObjectiveSense()), - cache.variable_to_column, + variables, cache.affine_constraints, cache.bound_constraints, ) diff --git a/src/lp_sensitivity2.jl b/src/lp_sensitivity2.jl index 5a0dc446107..4bea5dac5bf 100644 --- a/src/lp_sensitivity2.jl +++ b/src/lp_sensitivity2.jl @@ -331,7 +331,7 @@ function _standard_form_matrix(model::GenericModel{T}) where {T} matrix = lp_matrix_data(model) I = SparseArrays.spdiagm(fill(-one(T), length(matrix.affine_constraints))) return ( - columns = matrix.variable_to_column, + columns = Dict(x => i for (i, x) in enumerate(matrix.variables)), lower = vcat(matrix.x_lower, matrix.b_lower), upper = vcat(matrix.x_upper, matrix.b_upper), A = hcat(matrix.A, I),