diff --git a/examples/infeasible.jl b/examples/infeasible.jl index 9432836f..2c97c8c7 100644 --- a/examples/infeasible.jl +++ b/examples/infeasible.jl @@ -21,7 +21,7 @@ function ex_infeasible(::Tv) where{Tv<:Real} m.env.verbose = 1 # Read problem from .mps file and solve - TLP.readmps!(m, joinpath(INSTANCE_DIR, "lpex_inf.mps")) + TLP.loadproblem!(m, joinpath(INSTANCE_DIR, "lpex_inf.mps")) TLP.optimize!(m) # Check status diff --git a/examples/optimal.jl b/examples/optimal.jl index d04705ad..1c800ad7 100644 --- a/examples/optimal.jl +++ b/examples/optimal.jl @@ -20,7 +20,7 @@ function ex_optimal(::Tv) where{Tv<:Real} m.env.verbose = 1 # Read problem and solve - TLP.readmps!(m, joinpath(INSTANCE_DIR, "lpex_opt.mps")) + TLP.loadproblem!(m, joinpath(INSTANCE_DIR, "lpex_opt.mps")) TLP.optimize!(m) # Check status diff --git a/examples/unbounded.jl b/examples/unbounded.jl index c20c4b6b..987665ec 100644 --- a/examples/unbounded.jl +++ b/examples/unbounded.jl @@ -19,7 +19,7 @@ function ex_unbounded(::Tv) where{Tv<:Real} m.env.verbose = 1 # Read problem from .mps file and solve - TLP.readmps!(m, joinpath(INSTANCE_DIR, "lpex_ubd.mps")) + TLP.loadproblem!(m, joinpath(INSTANCE_DIR, "lpex_ubd.mps")) TLP.optimize!(m) # Check status diff --git a/src/Model/Model.jl b/src/Model/Model.jl index b9e2169a..42df52d1 100644 --- a/src/Model/Model.jl +++ b/src/Model/Model.jl @@ -48,6 +48,52 @@ mutable struct Model{Tv<:Real} return m end end + +""" + loadprolem!(m, filename) + +Read problem from file `filename` and load it into `m`. +""" +function loadproblem!(m::Model{Tv}, filename::String) where{Tv<:Real} + empty!(m) + + pb = ProblemData{Tv}() + + dat = readmps(filename) + + # Create rows + conidx = ConstrId[] + sizehint!(conidx, dat.ncon) + for (i, (cname, (bt, lb, ub))) in enumerate(zip(dat.connames, dat.conbounds)) + cidx = new_constraint_index!(pb) + constr = LinearConstraint{Tv}(cidx, cname, lb, ub) + add_constraint!(pb, constr) + push!(conidx, cidx) + end + + # Create variables + varidx = VarId[] + sizehint!(varidx, dat.nvar) + for (j, (vname, c, (bt, lb, ub))) in enumerate(zip(dat.varnames, dat.c, dat.varbounds)) + vidx = new_variable_index!(pb) + var = Variable{Tv}(vidx, vname, c, lb, ub) + add_variable!(pb, var) + push!(varidx, vidx) + end + + # Add coefficients + for (i, j, v) in zip(dat.aI, dat.aJ, dat.aV) + set_coeff!(pb, varidx[j], conidx[i], v) + end + + # Set objective sense and offset + pb.obj_const = dat.c0 + + m.name = dat.name + m.pbdata_raw = pb + + return m +end function empty!(m::Model{Tv}) where{Tv<:Real} # Empty model diff --git a/src/Utils/readmps.jl b/src/Utils/readmps.jl index e54297a3..c9572a1a 100644 --- a/src/Utils/readmps.jl +++ b/src/Utils/readmps.jl @@ -3,7 +3,6 @@ abstract type MPSSection end struct MPSNoSection <: MPSSection end struct MPSName <: MPSSection end struct MPSObjsense <: MPSSection end -struct MPSObjName <: MPSSection end struct MPSRows <: MPSSection end struct MPSColumns <: MPSSection end struct MPSRhs <: MPSSection end @@ -11,13 +10,11 @@ struct MPSRanges <: MPSSection end struct MPSBounds <: MPSSection end struct MPSEndata <: MPSSection end -function MPSSection(sec) +function MPSSection(sec::String) if sec == "NAME" return MPSName() elseif sec == "OBJSENSE" return MPSObjsense() - elseif sec == "OBJNAME" - return MPSObjName() elseif sec == "ROWS" return MPSRows() elseif sec == "COLUMNS" @@ -36,337 +33,479 @@ function MPSSection(sec) end """ - readmps!(m::Model{Tv}, filename) - -Parse a free-MPS file into model `m` + MPSData """ -function readmps!(m::Model{Tv}, fname::String) where{Tv<:Real} - - # First, empty model - empty!(m) - - # Now, parse MPS file - section = "" - d = Dict{String,Any}() +mutable struct MPSData{Tv<:Real} + + MPSFormat::Symbol # :Free of :Fixed + + name::String # Problem name + objname::String # Objective name + rhsname::String # name of RHS field + boundsname::String # name of BOUNDS field + rangename::String # nae of RANGES field + + sec::MPSSection + + varnames::Vector{String} # idx -> name + var2idx::Dict{String, Int} # name -> index + connames::Vector{String} # idx -> name + con2idx::Dict{String, Int} # name -> index + + ncon::Int # number of constraints (rows) + nvar::Int # Number of variables (columns) + + # Objective + objsense::Symbol # Objective sense + c::Vector{Tv} # Objective coefficients + c0::Tv # Objective offset + + # Coefficients in COO format + aI::Vector{Int} + aJ::Vector{Int} + aV::Vector{Tv} + + # Bounds + conbounds::Vector{Tuple{BoundType, Tv, Tv}} + varbounds::Vector{Tuple{BoundType, Tv, Tv}} + + MPSData{Tv}() where{Tv<:Real}= new{Tv}( + :Free, + "", "", "", "", "", MPSNoSection(), + String[], Dict{String, Int}(), String[], Dict{String, Int}(), + 0, 0, + :Min, Tv[], zero(Tv), + Int[], Int[], Tv[], + Tuple{BoundType, Tv, Tv}[], Tuple{BoundType, Tv, Tv}[] + ) +end - con2idx = Dict{String, ConstrId}() # name <-> index correspondence for rows - var2idx = Dict{String, VarId}() # name <-> index correspondence for columns +""" + split_mps_line(s::String) - lb = Dict{VarId, Tv}() # Lower bounds on variables - ub = Dict{VarId, Tv}() # Upper bounds on variables +More efficient implementation than Julia's Base.split +""" +function split_mps_line(s::String) + buf = IOBuffer(s) + S = String[] + sizehint!(S, 5) + while !eof(buf) + # Skip chars and read word + skipchars(isspace, buf) + n = position(buf) + skipchars(!isspace, buf) + m = position(buf) + m > n || break + push!(S, s[1+n:m]) + end + + return S +end - d["nrows"] = 0 - d["ncols"] = 0 - d["rhs"] = "" - d["bound"] = "" - d["range"] = "" +""" - section = MPSNoSection() +""" +function readmps(fname::String) + # MPS files have double float precision at most + dat = MPSData{Float64}() - open(fname) do f - nline = 0 - for ln in eachline(f) - nline += 1 + mps = open(fname) + try + while !eof(mps) + # Read line + ln = readline(mps) - # pass empty lines or comments if length(ln) == 0 || ln[1] == '*' + # Line is empty or a comment continue end - fields = split(ln) - - # check for indicators - if ln[1] != ' ' + if !isspace(ln[1]) # Section header - section = MPSSection(fields[1]) - - isa(section, MPSName) && (m.name = fields[2]) + fields = split_mps_line(ln) + dat.sec = MPSSection(fields[1]) + isa(dat.sec, MPSEndata) && break + if isa(dat.sec, MPSName) + dat.name = length(fields) >= 2 ? fields[2] : "" + end continue end - parseline!(section, m, fields, d, var2idx, con2idx, lb, ub) - - isa(section, MPSEndata) && break + parseline!(dat.sec, dat, ln) end - # End of file reached - end - - # Set variable bounds - for (vidx, var) in m.pbdata_raw.vars - set_bounds!(var, lb[vidx], ub[vidx]) + catch err + close(mps) + rethrow(err) end - return m -end + close(mps) -""" - parseline!(::MPSSection, m::Model{Tv}, fields, d, var2idx, con2idx, lb, ub) + # End of file reached + isa(dat.sec, MPSEndata) || error("File ended without EOF flag.") -Dummy function for now. -""" -function parseline!(::MPSSection, m::Model{Tv}, fields, d, var2idx, con2idx, lb, ub) where{Tv<:Real} - error() + return dat end -""" - parseline!(::MPSNoSection, m::Model{Tv}, fields, d, var2idx, con2idx, lb, ub) - -This should never be called. -""" -function parseline!(::MPSNoSection, m::Model{Tv}, fields, d, var2idx, con2idx, lb, ub) where{Tv<:Real} - error() +function parseline!(::MPSName, dat::MPSData, ln::String) + # parse the name + dat.name = split_mps_line(ln)[2] + return nothing end """ - parseline!(::MPSRows, m::Model{Tv}, fields, d, var2idx, con2idx, lb, ub) - -Parse a line from ROWS section in an MPS file. + parseline!(::MPSRows, dat, ln) + +The current line is expected to have the form +``` + X ZZZZ +``` +where: + * the first character is a space + * `X` is either 'N' (objective row), `E` (equality constraint), `L` (less-or-equal), `G` (greater-or-equal) + * `ZZZZ` cannot contain any space """ -function parseline!(::MPSRows, m::Model{Tv}, fields, d, var2idx, con2idx, lb, ub) where{Tv<:Real} - length(fields) == 2 || error() - - # parse line - # First field can be either of: - # "N" -> row corresponds to objective - # "E" -> equality constraint - # "L" -> less-or-equal inequality constraint - # "G" -> greater-or-equal inequality constraint - if fields[1] == "N" - # objective - con2idx[fields[2]] = ConstrId(0) - - elseif fields[1] == "E" || fields[1] == "L" || fields[1] == "G" +function parseline!(::MPSRows, dat::MPSData{Tv}, ln::String) where{Tv<:Real} + buf = IOBuffer(ln) + # Skip first spaces + skipchars(isspace, buf) + c = read(buf, Char) + skipchars(isspace, buf) + p1 = position(buf) + skipchars(!isspace, buf) + p2 = position(buf) + + rname = ln[1+p1:p2] + + if c == 'N' + if dat.objname == "" + dat.objname = rname + else + # New objective row; ignore + end + elseif c == 'E' || c == 'L' || c == 'G' # constraint - if fields[1] == "E" - lb, ub = zero(Tv), zero(Tv) - elseif fields[1] == "L" - lb, ub = Tv(-Inf), zero(Tv) - elseif fields[1] == "G" - lb, ub = zero(Tv), Tv(Inf) + haskey(dat.con2idx, rname) && error("Dupplicate row name $rname") + dat.ncon += 1 + + # Record constraint name + push!(dat.connames, rname) + dat.con2idx[rname] = dat.ncon + + # Create default bounds + if c == 'E' + push!(dat.conbounds, (TLP_FX, zero(Tv), zero(Tv))) + elseif c == 'L' + push!(dat.conbounds, (TLP_UP, Tv(-Inf), zero(Tv))) + elseif c == 'G' + push!(dat.conbounds, (TLP_LO, zero(Tv), Tv(Inf))) end - - # add constraint to model - ridx = add_constraint!(m, String(fields[2]), lb, ub, VarId[], Float64[]) - con2idx[String(fields[2])] = ridx else # Error in the input - error() + error("Unknown row type: $c") end return nothing end """ - parseline!(::MPSColumns, m::Model{Tv}, fields, d, var2idx, con2idx, lb, ub) + parseline!(::MPSColumns, dat, ln) -Parse a line from COLUMNS section in an MPS file. +The current line is expected to have the form +``` +``` """ -function parseline!(::MPSColumns, m::Model{Tv}, fields, d, var2idx, con2idx, lb, ub) where{Tv<:Real} - # parse a ROWS line of the MPS file - # `ln` is a string that contains the current line - # `fields` contains the 6 fields of that line - - # current line should contain at least three fields - length(fields) >= 3 || error("Line in COLUMNS section has less than three fields.") +function parseline!(::MPSColumns, dat::MPSData{Tv}, ln::String) where{Tv<:Real} + fields = split_mps_line(ln) - fields[2] == "'MARKER'" && (return nothing) + length(fields) >= 3 || error("Incomplete line") + vname = fields[1] + rname = fields[2] - # First field is empty, second field is variable's name - cname = fields[1] - if !haskey(var2idx, cname) + # Ignore Intger markers + rname == "'MARKER'" && return nothing + + coeff = parse(Tv, fields[3]) + + if !haskey(dat.var2idx, vname) # Create new variable - vidx = add_variable!(m, String(cname), zero(Tv), zero(Tv), Tv(Inf)) - var2idx[cname] = vidx - lb[vidx] = zero(Tv) - ub[vidx] = Tv(Inf) - end + dat.nvar += 1 + dat.var2idx[vname] = dat.nvar + push!(dat.varnames, vname) - # Second and third fields are - # the row's name, and the corresponding coefficient - rname = fields[2] # row name - haskey(con2idx, rname) || error("Unknown row.") + # Create default bounds and zero objective coeff + push!(dat.varbounds, (TLP_LO, zero(Tv), Tv(Inf))) - coeff = parse(Float64, fields[3]) # coefficient value - if con2idx[rname].uuid == 0 - # objective coefficient - set_obj_coeff!(m.pbdata_raw.vars[var2idx[cname]], coeff) + push!(dat.c, zero(Tv)) + + j = dat.nvar else - # constraint coefficient - set_coeff!(m.pbdata_raw, var2idx[cname], con2idx[rname], coeff) + # Variable already exists; record its index + j = dat.var2idx[vname] end - # optional other fields - length(fields) >= 5 || (return nothing) + # Add coefficient + if dat.objname == rname + dat.c[j] = coeff + elseif haskey(dat.con2idx, rname) + i = dat.con2idx[rname] + push!(dat.aI, i) + push!(dat.aJ, j) + push!(dat.aV, coeff) + else + # Ignore input + end - rname = fields[4] # row name - haskey(con2idx, rname) || error("Unknown row.") - coeff = parse(Float64, fields[5]) # coefficient value - if con2idx[rname].uuid == 0 - # objective coefficient - set_obj_coeff!(m.pbdata_raw.vars[var2idx[cname]], coeff) + length(fields) >= 5 || return nothing # end of line reached + + # Read fields 4 and 5 + rname = fields[4] + coeff = parse(Tv, fields[5]) + + # Add coefficient + if dat.objname == rname + dat.c[j] = coeff + elseif haskey(dat.con2idx, rname) + i = dat.con2idx[rname] + push!(dat.aI, i) + push!(dat.aJ, j) + push!(dat.aV, coeff) else - # constraint coefficient - set_coeff!(m.pbdata_raw, var2idx[cname], con2idx[rname], coeff) + # Unknown row + # error("Unkown row $rname") end return nothing end """ - parseline!(::MPSRhs, m::Model{Tv}, fields, d, var2idx, con2idx, lb, ub) + parseline!(::MPSRhs, dat, ln) -Parse a line from RHS section in an MPS file. +The current line is expected to have the form +``` +``` """ -function parseline!(::MPSRhs, m::Model{Tv}, fields, d, var2idx, con2idx, lb, ub) where{Tv<:Real} - - # Line should contain at least 3 fields, and up to 5 - length(fields) >= 3 || error("Line is too short.") - - # First field is RHS name (may be empty) - rhsname = fields[1] - if d["rhs"] == "" - # first time RHS is read - d["rhs"] = rhsname - elseif d["rhs"] != rhsname - # other RHS, current line is ignored +function parseline!(::MPSRhs, dat::MPSData{Tv}, ln::String) where{Tv<:Real} + buf = IOBuffer(ln) + # Skip first spaces + skipchars(isspace, buf) + + # Read RHS name field + p1 = position(buf) + skipchars(!isspace, buf) + p2 = position(buf) + eof(buf) && error("Incomplete line") + + rhsname = ln[1+p1:p2] + + if dat.rhsname == "" + dat.rhsname = rhsname + elseif dat.rhsname != rhsname + # Other RHS, current line is ignored return nothing end - # parse line - rname = fields[2] - haskey(con2idx, rname) || error() - - rval = parse(Float64, fields[3]) - # update index and value - if con2idx[rname].uuid == 0 - d["obj_offset"] = -rval - else - # Check type of constraint and update coefficient accordingly - (bt, lb, ub) = get_bounds(m.pbdata_raw.constrs[con2idx[rname]]) + # Read fields 2 and 3 + skipchars(isspace, buf) + p1 = position(buf) + skipchars(!isspace, buf) + p2 = position(buf) + eof(buf) && error("Incomplete line") + rname = ln[1+p1:p2] + + skipchars(isspace, buf) + p1 = position(buf) + skipchars(!isspace, buf) + p2 = position(buf) + rval = parse(Tv, ln[1+p1:p2]) + + if dat.objname == rname + # Objective offset + i = 0 + dat.c0 = -rval + elseif haskey(dat.con2idx, rname) + i = dat.con2idx[rname] + + # update constraint bounds + bt, lb, ub = dat.conbounds[i] if bt == TLP_UP # a'x <= b - set_bounds!(m.pbdata_raw.constrs[con2idx[rname]], Tv(-Inf), rval) + dat.conbounds[i] = (bt, Tv(-Inf), rval) elseif bt == TLP_LO # a'x >= b - set_bounds!(m.pbdata_raw.constrs[con2idx[rname]], rval, Tv(Inf)) - elseif bt == TLP_FR - # This should not happen - error("Got right-hand side for free constraint") + dat.conbounds[i] = (bt, rval, Tv(Inf)) elseif bt == TLP_FX - # a'x = b - set_bounds!(m.pbdata_raw.constrs[con2idx[rname]], rval, rval) - elseif bt == TLP_RG - # This should not happen - error("Got single right-hand side for ranged constraint.") + # a'x == b + dat.conbounds[i] = (bt, rval, rval) + else + error("Got RHS term for row $rname of type $bt") end + else + error("Unkown row $rname") end - if length(fields) < 5 - return nothing - end - # optional fields - rname = fields[4] # row name - haskey(con2idx, rname) || error() - rval = parse(Float64, fields[5]) # coefficient value - # update index and value - if con2idx[rname].uuid == 0 - d["obj_offset"] = -rval - else - # Check type of constraint and update coefficient accordingly - (bt, lb, ub) = get_bounds(m.pbdata_raw.constrs[con2idx[rname]]) + skipchars(isspace, buf) + eof(buf) && return nothing # end of line reached + + # Read fields 4 and 5 + p1 = position(buf) + skipchars(!isspace, buf) + p2 = position(buf) + eof(buf) && error("Incomplete line") + rname = ln[1+p1:p2] + + skipchars(isspace, buf) + p1 = position(buf) + skipchars(!isspace, buf) + p2 = position(buf) + rval = parse(Tv, ln[1+p1:p2]) + + if dat.objname == rname + # Objective offset + i = 0 + dat.c0 = -rval + elseif haskey(dat.con2idx, rname) + i = dat.con2idx[rname] + + # update constraint bounds + bt, lb, ub = dat.conbounds[i] if bt == TLP_UP # a'x <= b - set_bounds!(m.pbdata_raw.constrs[con2idx[rname]], Tv(-Inf), rval) + dat.conbounds[i] = (bt, Tv(-Inf), rval) elseif bt == TLP_LO # a'x >= b - set_bounds!(m.pbdata_raw.constrs[con2idx[rname]], rval, Tv(Inf)) - elseif bt == TLP_FR - # This should not happen - error("Got right-hand side for free constraint") + dat.conbounds[i] = (bt, rval, Tv(Inf)) elseif bt == TLP_FX - # a'x = b - set_bounds!(m.pbdata_raw.constrs[con2idx[rname]], rval, rval) - elseif bt == TLP_RG - # This should not happen - error("Got single right-hand side for ranged constraint.") + # a'x == b + dat.conbounds[i] = (bt, rval, rval) + else + error("Got RHS term for row $rname of type $bt") end + else + error("Unkown row $rname") end return nothing end """ - parseline!(::MPSRanges, m::Model{Tv}, fields, d, var2idx, con2idx, lb, ub) + parseline!(::MPSRanges, dat, ln) -Parse a line from RANGES section in an MPS file. +The current line is expected to have the form +``` +``` """ -function parseline!(::MPSRanges, m::Model{Tv}, fields, d, var2idx, con2idx, lb, ub) where{Tv<:Real} - - length(fields) >= 3 || error("Line too short") - - rngname = fields[1] - if d["range"] == "" - d["range"] = rngname - elseif d["range"] != rngname - # other range vector, ignore +function parseline!(::MPSRanges, dat::MPSData{Tv}, ln::String) where{Tv<:Real} + buf = IOBuffer(ln) + # Skip first spaces + skipchars(isspace, buf) + + # Read RANGE name field + p1 = position(buf) + skipchars(!isspace, buf) + p2 = position(buf) + eof(buf) && error("Incomplete line") + + rgname = ln[1+p1:p2] + + if dat.rangename == "" + dat.rangename = rgname + elseif dat.rangename != rgname + # Other RHS, current line is ignored return nothing end - # parse line - rname = fields[2] - rval = parse(Float64, fields[3]) - haskey(con2idx, rname) || error() - cidx = con2idx[rname] - (bt, lb, ub) = get_bounds(m.pbdata_raw.constrs[cidx]) - if bt == TLP_LO - # `l <= a'x <= Inf` becomes `l <= a'x <= l + |r|` - set_bounds!(m.pbdata_raw.constrs[cidx], lb, lb + abs(rval)) - elseif bt == TLP_UP - # `-Inf <= a'x <= u` becomes `u - |r| <= a'x <= u` - set_bounds!(m.pbdata_raw.constrs[cidx], ub - abs(rval), ub) - elseif bt == TLP_FX && rval >= 0.0 - # `a'x = b` becomes `b <= a'x <= b + |r|` - set_bounds!(m.pbdata_raw.constrs[cidx], lb, lb + rval) - elseif bt == TLP_FX && rval < 0.0 - # `a'x = b` becomes `b - |r| <= a'x <= b` - set_bounds!(m.pbdata_raw.constrs[cidx], ub - abs(rval), ub) + # Read fields 2 and 3 + skipchars(isspace, buf) + p1 = position(buf) + skipchars(!isspace, buf) + p2 = position(buf) + eof(buf) && error("Incomplete line") + rname = ln[1+p1:p2] + + skipchars(isspace, buf) + p1 = position(buf) + skipchars(!isspace, buf) + p2 = position(buf) + rval = parse(Tv, ln[1+p1:p2]) + + if haskey(dat.con2idx, rname) + i = dat.con2idx[rname] + + # update constraint bounds + bt, lb, ub = dat.conbounds[i] + if bt == TLP_UP + # `-Inf <= a'x <= u` becomes `u - |r| <= a'x <= u` + dat.conbounds[i] = (TLP_RG, ub - abs(rval), ub) + elseif bt == TLP_LO + # `l <= a'x <= Inf` becomes `l <= a'x <= l + |r|` + dat.conbounds[i] = (TLP_RG, lb, lb + abs(rval)) + elseif bt == TLP_FX && rval >= zero(Tv) + # `a'x = b` becomes `b <= a'x <= b + |r|` + dat.conbounds[i] = (TLP_RG, lb, lb + rval) + elseif bt == TLP_FX && rval < zero(Tv) + # `a'x = b` becomes `b - |r| <= a'x <= b` + dat.conbounds[i] = (TLP_RG, lb + rval, lb) + else + error("Got RANGE val $rval for row $rname of type $bt") + end else - error("Unkown row type for RANGES: $bt.") + error("Unkown row $rname") end - if length(fields) >= 5 - rname = fields[4] - rngval = parse(Float64, fields[5]) - haskey(con2idx, rname) || error() - cidx = con2idx[rname] - (bt, lb, ub) = get_bounds(m.pbdata_raw.constrs[cidx]) - if bt == TLP_LO - # `l <= a'x <= Inf` becomes `l <= a'x <= l + |r|` - set_bounds!(m.pbdata_raw.constrs[cidx], lb, lb + abs(rval)) - elseif bt == TLP_UP + skipchars(isspace, buf) + eof(buf) && return nothing # end of line reached + + # Read fields 4 and 5 + p1 = position(buf) + skipchars(!isspace, buf) + p2 = position(buf) + eof(buf) && error("Incomplete line") + rname = ln[1+p1:p2] + + skipchars(isspace, buf) + p1 = position(buf) + skipchars(!isspace, buf) + p2 = position(buf) + rval = parse(Tv, ln[1+p1:p2]) + + if haskey(dat.con2idx, rname) + i = dat.con2idx[rname] + + # update constraint bounds + bt, lb, ub = dat.conbounds[i] + if bt == TLP_UP # `-Inf <= a'x <= u` becomes `u - |r| <= a'x <= u` - set_bounds!(m.pbdata_raw.constrs[cidx], ub - abs(rval), ub) - elseif bt == TLP_FX && rval >= 0.0 + dat.conbounds[i] = (TLP_RG, ub - abs(rval), ub) + elseif bt == TLP_LO + # `l <= a'x <= Inf` becomes `l <= a'x <= l + |r|` + dat.conbounds[i] = (TLP_RG, lb, lb + abs(rval)) + elseif bt == TLP_FX && rval >= zero(Tv) # `a'x = b` becomes `b <= a'x <= b + |r|` - set_bounds!(m.pbdata_raw.constrs[cidx], lb, lb + rval) - elseif bt == TLP_FX && rval < 0.0 + dat.conbounds[i] = (TLP_RG, lb, lb + rval) + elseif bt == TLP_FX && rval < zero(Tv) # `a'x = b` becomes `b - |r| <= a'x <= b` - set_bounds!(m.pbdata_raw.constrs[cidx], ub - abs(rval), ub) + dat.conbounds[i] = (TLP_RG, lb + rval, lb) else - error("Unkown row type for RANGES: $bt.") + error("Got RANGE val $rval for row $rname of type $bt") end + else + error("Unkown row $rname") end return nothing end """ - parseline!(::MPSBounds, m::Model{Tv}, fields, d, var2idx, con2idx, lb, ub) + parseline!(::MPSBounds, dat, ln) -Parse a line from RANGES section in an MPS file. +The current line is expected to have the form +``` +``` """ -function parseline!(::MPSBounds, m::Model{Tv}, fields, d, var2idx, con2idx, lb, ub) where{Tv<:Real} +function parseline!(::MPSBounds, dat::MPSData{Tv}, ln::String) where{Tv<:Real} + fields = split_mps_line(ln) + if length(fields) < 3 || ( length(fields) < 4 && ( fields[1] == "LO" || fields[1] == "UP" || fields[1] == "FX" @@ -378,79 +517,78 @@ function parseline!(::MPSBounds, m::Model{Tv}, fields, d, var2idx, con2idx, lb, # first field should be bound type btype = fields[1] - bname = fields[2] - cname = fields[3] - haskey(var2idx, cname) || error() + bdname = fields[2] + colname = fields[3] - # - if d["bound"] == "" - d["bound"] = bname - else - # Some field was already read; if this one is different, skip - d["bound"] == bname || return nothing + if dat.boundsname == "" + dat.boundsname = bdname + elseif dat.boundsname != bdname + # Other BOUNDS, current line is ignored + return nothing end - vidx = var2idx[cname] + haskey(dat.var2idx, colname) || error() + + # Get column index + j = dat.var2idx[colname] + bt, lb, ub = dat.varbounds[j] + if length(fields) == 3 if btype == "FR" - # -Inf < x < Inf - lb[vidx] = -Inf - ub[vidx] = Inf - return + # -∞ < x < ∞ + dat.varbounds[j] = (TLP_FR, Tv(-Inf), Tv(Inf)) elseif btype == "MI" - # -Inf < x - lb[vidx] = -Inf - + # -∞ < x <= ub + if isfinite(ub) + dat.varbounds[j] = (TLP_UP, Tv(-Inf), ub) + else + dat.varbounds[j] = (TLP_FR, Tv(-Inf), Tv(Inf)) + end elseif btype == "PL" - # x < Inf - ub[vidx] = Inf - + # lb <= x < ∞ + if isfinite(lb) + dat.varbounds[j] = (TLP_LO, lb, Tv(Inf)) + else + dat.varbounds[j] = (TLP_FR, Tv(-Inf), Tv(Inf)) + end elseif btype == "BV" - # x = 0 or 1, x binary - # Keep bounds but ignore binary requirement - lb[vidx] = 0.0 - ub[vidx] = 1.0 + # x ∈ {0, 1} + # Record bounds 0 <= x <= 1 but ignore binary requirement + dat.varbounds[j] = (TLP_RG, zero(Tv), one(Tv)) else error("Unknown bound type: $(btype)") end - return - end - - bval = parse(Float64, fields[4]) - if btype == "LO" - # b <= x - lb[vidx] = bval - elseif btype == "UP" - # x <= b - ub[vidx] = bval + return nothing + end + bval = parse(Tv, fields[4]) + + if btype == "LO" || btype == "LI" + # b <= x <= ub + # Ignore integer requirement + if isfinite(ub) + dat.varbounds[j] = (TLP_RG, bval, ub) + else + dat.varbounds[j] = (TLP_LO, bval, Tv(Inf)) + end + + elseif btype == "UP" || btype == "UI" + # lb <= x <= b + # Ignore integer requirement + if isfinite(lb) + dat.varbounds[j] = (TLP_RG, lb, bval) + else + dat.varbounds[j] = (TLP_UP, -Tv(Inf), bval) + end + elseif btype == "FX" - # x = b - lb[vidx] = bval - ub[vidx] = bval - - elseif btype == "LI" - # 0 <= x < Inf, x integer - # Keep bounds but ignore integer requirement - lb[vidx] = bval - - elseif btype == "UI" - # 0 <= x <= b, x integer - # Keep bounds but ignore integer requirement - ub[vidx] = bval + # x == b + dat.varbounds[j] = (TLP_FX, bval, bval) + else # error in bound type error("Unknown bound type: $(btype)") end return nothing -end - -""" - parseline!(::MPSEndata, m::Model{Tv}, fields, d, var2idx, con2idx, lb, ub) - -End of model. Do nothing -""" -function parseline!(::MPSEndata, m::Model{Tv}, fields, d, var2idx, con2idx, lb, ub) where{Tv<:Real} - return nothing end \ No newline at end of file diff --git a/test/reader.jl b/test/reader.jl index 3bd6f765..ac35b61b 100644 --- a/test/reader.jl +++ b/test/reader.jl @@ -1,7 +1,7 @@ function run_tests_mps_reader(::Tv) where{Tv<:Real} m = TLP.Model{Tv}() - TLP.readmps!(m, joinpath(@__DIR__, "test_lp.mps")) + TLP.loadproblem!(m, joinpath(@__DIR__, "test_lp.mps")) @testset "Name" begin @test m.name == "TESTLP"