Skip to content

Commit 104db00

Browse files
Presolve fixed variables (#49)
1 parent 2976db4 commit 104db00

File tree

5 files changed

+308
-1
lines changed

5 files changed

+308
-1
lines changed

src/QuadraticModels.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,10 @@ import NLPModels:
2929
SlackModel,
3030
slack_meta
3131

32-
export QuadraticModel
32+
export AbstractQuadraticModel, QuadraticModel, presolve, postsolve!
3333

3434
include("qpmodel.jl")
35+
include("presolve/presolve.jl")
3536

3637
function __init__()
3738
@require QPSReader = "10f199a5-22af-520b-b891-7ce84a7b1bd0" include("qps.jl")

src/presolve/presolve.jl

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
include("remove_ifix.jl")
2+
3+
mutable struct PresolvedQuadraticModel{T, S} <: AbstractQuadraticModel{T, S}
4+
meta::NLPModelMeta{T, S}
5+
counters::Counters
6+
data::QPData{T, S}
7+
xrm::S
8+
end
9+
10+
"""
11+
psqm = presolve(qm::QuadraticModel{T, S}; kwargs...)
12+
13+
Apply a presolve routine to `qm` and returns a `PresolvedQuadraticModel{T, S} <: AbstractQuadraticModel{T, S}`.
14+
The presolve operations currently implemented are:
15+
16+
- [`remove_ifix!`](@ref)
17+
18+
"""
19+
function presolve(qm::QuadraticModel{T, S}; kwargs...) where {T <: Real, S}
20+
21+
psqm = deepcopy(qm)
22+
psdata = psqm.data
23+
lvar, uvar = psqm.meta.lvar, psqm.meta.uvar
24+
lcon, ucon = psqm.meta.lcon, psqm.meta.ucon
25+
nvar, ncon = psqm.meta.nvar, psqm.meta.ncon
26+
27+
ifix = qm.meta.ifix
28+
if length(ifix) > 0
29+
xrm, psdata.c0, nvarps = remove_ifix!(
30+
ifix,
31+
psdata.Hrows,
32+
psdata.Hcols,
33+
psdata.Hvals,
34+
nvar,
35+
psdata.Arows,
36+
psdata.Acols,
37+
psdata.Avals,
38+
psdata.c,
39+
psdata.c0,
40+
lvar,
41+
uvar,
42+
lcon,
43+
ucon,
44+
)
45+
else
46+
nvarps = nvar
47+
xrm = S(undef, 0)
48+
end
49+
50+
# form meta
51+
nnzh = length(psdata.Hvals)
52+
if !(nnzh == length(psdata.Hrows) == length(psdata.Hcols))
53+
error("The length of Hrows, Hcols and Hvals must be the same")
54+
end
55+
nnzj = length(psdata.Avals)
56+
if !(nnzj == length(psdata.Arows) == length(psdata.Acols))
57+
error("The length of Arows, Acols and Avals must be the same")
58+
end
59+
psmeta = NLPModelMeta{T, S}(
60+
nvarps,
61+
lvar = lvar,
62+
uvar = uvar,
63+
ncon = ncon,
64+
lcon = lcon,
65+
ucon = ucon,
66+
nnzj = nnzj,
67+
nnzh = nnzh,
68+
lin = 1:ncon,
69+
nln = Int[],
70+
islp = (ncon == 0);
71+
minimize = qm.meta.minimize,
72+
kwargs...,
73+
)
74+
ps = PresolvedQuadraticModel(psmeta, Counters(), psdata, xrm)
75+
76+
return ps
77+
end
78+
79+
"""
80+
postsolve!(qm::QuadraticModel{T, S}, psqm::PresolvedQuadraticModel{T, S},
81+
x_in::S, x_out::S) where {T, S}
82+
83+
Retrieve the solution `x_out` of the original QP `qm` given the solution of the presolved QP (`psqm`)
84+
`x_in`.
85+
"""
86+
function postsolve!(qm::QuadraticModel{T, S}, psqm::PresolvedQuadraticModel{T, S}, x_in::S, x_out::S) where {T, S}
87+
if length(qm.meta.ifix) > 0
88+
restore_ifix!(qm.meta.ifix, psqm.xrm, x_in, x_out)
89+
else
90+
x_out .= @views x_in[1: qm.meta.nvar]
91+
end
92+
end

src/presolve/remove_ifix.jl

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
# ̃xᵀ̃Hx̃ + ̃ĉᵀx̃ + lⱼ²Hⱼⱼ + cⱼxⱼ + c₀
2+
# ̂c = ̃c + 2lⱼΣₖHⱼₖxₖ , k ≂̸ j
3+
4+
"""
5+
xrm, c0ps, nvarrm = remove_ifix!(ifix, Hrows, Hcols, Hvals, nvar,
6+
Arows, Acols, Avals, c, c0,
7+
lvar, uvar, lcon, ucon)
8+
9+
Remove rows and columns in H, columns in A, and elements in lcon and ucon
10+
corresponding to fixed variables, that are in `ifix`
11+
(They should be the indices `i` where `lvar[i] == uvar[i]`).
12+
13+
Returns the removed elements of `lvar` (or `uvar`), the constant term in the QP
14+
objective `c0ps` resulting from the fixed variables, and the new number of variables `nvarrm`.
15+
`Hrows`, `Hcols`, `Hvals`, `Arows`, `Acols`, `Avals`, `c`, `lvar`, `uvar`,
16+
`lcon` and `ucon` are updated in-place.
17+
"""
18+
function remove_ifix!(
19+
ifix,
20+
Hrows,
21+
Hcols,
22+
Hvals,
23+
nvar,
24+
Arows,
25+
Acols,
26+
Avals,
27+
c::AbstractVector{T},
28+
c0,
29+
lvar,
30+
uvar,
31+
lcon,
32+
ucon,
33+
) where {T}
34+
35+
# assume Hcols is sorted
36+
c0_offset = zero(T)
37+
Hnnz = length(Hrows)
38+
Hrm = 0
39+
Annz = length(Arows)
40+
Arm = 0
41+
# assume ifix is sorted and length(ifix) > 0
42+
nfix = length(ifix)
43+
44+
# remove ifix 1 by 1 in H and A and update QP data
45+
for idxfix = 1:nfix
46+
currentifix = ifix[idxfix]
47+
xifix = lvar[currentifix]
48+
# index of the current fixed variable that takes the number of
49+
# already removed variables into account:
50+
newcurrentifix = currentifix - idxfix + 1
51+
52+
Hwritepos = 1
53+
# shift corresponding to the already removed fixed variables to update c:
54+
shiftHj = 1
55+
if Hnnz > 0
56+
oldHj = Hrows[1]
57+
end
58+
# remove ifix in H and update data
59+
k = 1
60+
while k <= Hnnz && Hcols[k] <= (nvar - idxfix + 1)
61+
Hi, Hj, Hx = Hrows[k], Hcols[k], Hvals[k] # Hj sorted
62+
63+
while (Hj == oldHj) && shiftHj <= idxfix - 1 && Hj + shiftHj - 1 >= ifix[shiftHj]
64+
shiftHj += 1
65+
end
66+
shiftHi = 1
67+
while shiftHi <= idxfix - 1 && Hi + shiftHi - 1 >= ifix[shiftHi]
68+
shiftHi += 1
69+
end
70+
if Hi == Hj == newcurrentifix
71+
Hrm += 1
72+
c0_offset += xifix^2 * Hx / 2
73+
elseif Hi == newcurrentifix
74+
Hrm += 1
75+
c[Hj + shiftHj - 1] += xifix * Hx
76+
elseif Hj == newcurrentifix
77+
Hrm += 1
78+
c[Hi + shiftHi - 1] += xifix * Hx
79+
else # keep Hi, Hj, Hx
80+
Hrows[Hwritepos] = (Hi < newcurrentifix) ? Hi : Hi - 1
81+
Hcols[Hwritepos] = (Hj < newcurrentifix) ? Hj : Hj - 1
82+
Hvals[Hwritepos] = Hx
83+
Hwritepos += 1
84+
end
85+
k += 1
86+
end
87+
88+
# remove ifix in A cols
89+
Awritepos = 1
90+
currentAn = nvar - idxfix + 1 # remove rows if uplo == :U
91+
k = 1
92+
while k <= Annz && Acols[k] <= currentAn
93+
Ai, Aj, Ax = Arows[k], Acols[k], Avals[k]
94+
if Aj == newcurrentifix
95+
Arm += 1
96+
lcon[Ai] -= Ax * xifix
97+
ucon[Ai] -= Ax * xifix
98+
else
99+
if Awritepos != k
100+
Arows[Awritepos] = Ai
101+
Acols[Awritepos] = (Aj < newcurrentifix) ? Aj : Aj - 1
102+
Avals[Awritepos] = Ax
103+
end
104+
Awritepos += 1
105+
end
106+
k += 1
107+
end
108+
109+
# update c0 with c[currentifix] coeff
110+
c0_offset += c[currentifix] * xifix
111+
end
112+
113+
# resize Q and A
114+
if nfix > 0
115+
Hnnz -= Hrm
116+
Annz -= Arm
117+
resize!(Hrows, Hnnz)
118+
resize!(Hcols, Hnnz)
119+
resize!(Hvals, Hnnz)
120+
resize!(Arows, Annz)
121+
resize!(Acols, Annz)
122+
resize!(Avals, Annz)
123+
end
124+
125+
# store removed x values
126+
xrm = lvar[ifix]
127+
128+
# remove coefs in lvar, uvar, c
129+
deleteat!(c, ifix)
130+
deleteat!(lvar, ifix)
131+
deleteat!(uvar, ifix)
132+
133+
# update c0
134+
c0ps = c0 + c0_offset
135+
136+
nvarrm = nvar - nfix
137+
138+
return xrm, c0ps, nvarrm
139+
end
140+
141+
function restore_ifix!(ifix, xrm, x, xout)
142+
# put x and xrm inside xout
143+
cfix, cx = 1, 1
144+
nfix = length(ifix)
145+
for i = 1:length(xout)
146+
if cfix <= nfix && i == ifix[cfix]
147+
xout[i] = xrm[cfix]
148+
cfix += 1
149+
else
150+
xout[i] = x[cx]
151+
cx += 1
152+
end
153+
end
154+
end

test/runtests.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,5 @@ end
7171
SlackModel!(qp)
7272
testSM(qp)
7373
end
74+
75+
include("test_presolve.jl")

test/test_presolve.jl

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
@testset "presolve ifix" begin
2+
H = [
3+
6.0 2.0 1.0
4+
2.0 5.0 2.0
5+
1.0 2.0 4.0
6+
]
7+
c = [-8.0; -3; -3]
8+
A = [
9+
1.0 0.0 1.0
10+
0.0 2.0 1.0
11+
]
12+
b = [0.0; 3]
13+
l = [0.0; 2.0; 0]
14+
u = [Inf; 2.0; Inf]
15+
T = eltype(c)
16+
qp = QuadraticModel(
17+
c,
18+
H,
19+
A = A,
20+
lcon = [-3.0; -4.0],
21+
ucon = [-2.0; Inf],
22+
lvar = l,
23+
uvar = u,
24+
c0 = 0.0,
25+
name = "QM1",
26+
)
27+
28+
psqp = presolve(qp)
29+
30+
c_true = [-4.0; 1.0]
31+
c0_true = 4.0
32+
Hps_true = [
33+
6.0 1.0
34+
1.0 4.0
35+
]
36+
Aps_true = [
37+
1.0 1.0
38+
0.0 1.0
39+
]
40+
lvarps_true, uvarps_true = [0. ; 0.], [Inf; Inf]
41+
42+
@test psqp.data.c == [-4.0; 1.0]
43+
@test psqp.data.c0 == 4.0
44+
Hps = sparse(psqp.data.Hrows, psqp.data.Hcols, psqp.data.Hvals, psqp.meta.ncon, psqp.meta.nvar)
45+
@test norm(Symmetric(Hps, :L) - sparse(Hps_true)) sqrt(eps(T))
46+
Aps = sparse(psqp.data.Arows, psqp.data.Acols, psqp.data.Avals, psqp.meta.ncon, psqp.meta.nvar)
47+
@test norm(Aps - sparse(Aps_true)) sqrt(eps(T))
48+
@test psqp.meta.lvar == lvarps_true
49+
@test psqp.meta.uvar == uvarps_true
50+
@test psqp.xrm == [2.0]
51+
@test psqp.meta.ifix == Int[]
52+
@test psqp.meta.nvar == 2
53+
54+
x_in = [4.0; 7.0]
55+
x_out = zeros(3)
56+
postsolve!(qp, psqp, x_in, x_out)
57+
@test x_out == [4.0; 2.0; 7.0]
58+
end

0 commit comments

Comments
 (0)