Skip to content

Commit 832fe53

Browse files
committed
[FileFormats.NL] add support for reading binary format
1 parent 784d0fa commit 832fe53

File tree

2 files changed

+76
-48
lines changed

2 files changed

+76
-48
lines changed

src/FileFormats/NL/read.jl

Lines changed: 68 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
# in the LICENSE.md file or at https://opensource.org/licenses/MIT.
66

77
mutable struct _CacheModel
8+
is_binary::Bool
89
cache::Vector{UInt8}
910
variable_type::Vector{_VariableType}
1011
variable_primal::Vector{Float64}
@@ -17,6 +18,7 @@ mutable struct _CacheModel
1718
sense::MOI.OptimizationSense
1819
function _CacheModel()
1920
return new(
21+
false,
2022
zeros(UInt8, 64),
2123
_VariableType[],
2224
Float64[],
@@ -95,12 +97,18 @@ function _next_token(::Type{T}, io::IO, cache::Vector{UInt8}) where {T}
9597
end
9698

9799
function _next(::Type{Float64}, io::IO, model::_CacheModel)
100+
if model.is_binary
101+
return read(io, Float64)
102+
end
98103
nnz = _next_token(Float64, io, model.cache)
99104
@assert nnz > 0
100105
return parse(Float64, String(model.cache[1:nnz]))
101106
end
102107

103108
function _next(::Type{Int}, io::IO, model::_CacheModel)
109+
if model.is_binary
110+
return convert(Int, read(io, Int32))
111+
end
104112
nnz = _next_token(Int, io, model.cache)
105113
@assert nnz > 0
106114
y = 0
@@ -113,12 +121,15 @@ function _next(::Type{Int}, io::IO, model::_CacheModel)
113121
end
114122

115123
"""
116-
_read_til_newline(io::IO)
124+
_read_til_newline(io::IO, model::_CacheModel)
117125
118126
This function reads until it finds a new line character. This is useful for
119127
skipping comments.
120128
"""
121-
function _read_til_newline(io::IO)
129+
function _read_til_newline(io::IO, model::_CacheModel)
130+
if model.is_binary
131+
return
132+
end
122133
while read(io, UInt8) != UInt8('\n')
123134
end
124135
return
@@ -131,12 +142,12 @@ function _parse_expr(io::IO, model::_CacheModel)
131142
char = Char(read(io, UInt8))
132143
if char == 'o'
133144
opcode = _next(Int, io, model)
134-
_read_til_newline(io)
145+
_read_til_newline(io, model)
135146
arity, op_func = _AMPL_TO_JULIA[opcode]
136147
op_sym = Symbol(op_func)
137148
if arity == -1
138149
arity = _next(Int, io, model)
139-
_read_til_newline(io)
150+
_read_til_newline(io, model)
140151
if op_sym == :sum
141152
op_sym = :+
142153
elseif op_sym == :minimum
@@ -153,12 +164,12 @@ function _parse_expr(io::IO, model::_CacheModel)
153164
return parent
154165
elseif char == 'v'
155166
index = _next(Int, io, model)
156-
_read_til_newline(io)
167+
_read_til_newline(io, model)
157168
return MOI.VariableIndex(index + 1)
158169
else
159170
@assert char == 'n'
160171
ret = _next(Float64, io, model)
161-
_read_til_newline(io)
172+
_read_til_newline(io, model)
162173
return ret
163174
end
164175
end
@@ -270,13 +281,16 @@ function _parse_header(io::IO, model::_CacheModel)
270281
# Line 1
271282
# We don't support the binary format.
272283
byte = read(io, UInt8)
273-
if byte != UInt8('g')
284+
is_binary = false
285+
if byte == UInt8('b')
286+
is_binary = true
287+
elseif byte != UInt8('g')
274288
error("Unable to parse NL file : unsupported mode $(Char(byte))")
275289
end
276290
# L1 has some magic bytes for AMPL internals (to quote David, "The numbers
277291
# on the first line matter to AMPL; for other uses, it is best simply to
278292
# supply the ones shown above.")
279-
_read_til_newline(io)
293+
_read_til_newline(io, model)
280294
# Line 2
281295
# The number of variables
282296
n_var = _next(Int, io, model)
@@ -292,39 +306,39 @@ function _parse_header(io::IO, model::_CacheModel)
292306
# The number of logical constraints. This one is optional, so just read til
293307
# the end of the line.
294308
# @assert _next(Int, io, model) == 0
295-
_read_til_newline(io)
309+
_read_til_newline(io, model)
296310
# Line 3
297311
# The number of nonlinear constraints
298312
@assert _next(Int, io, model) >= 0
299313
# The number of nonlinear objectives
300314
@assert 0 <= _next(Int, io, model) <= 1
301-
_read_til_newline(io)
315+
_read_til_newline(io, model)
302316
# Line 4
303317
# The number of nonlinear network constraints
304318
@assert _next(Int, io, model) == 0
305319
# The number of linear network constraints
306320
@assert _next(Int, io, model) == 0
307-
_read_til_newline(io)
321+
_read_til_newline(io, model)
308322
# Line 5
309323
# The number of nonlienar variables in constraints
310324
nlvc = _next(Int, io, model)
311325
# The number of nonlienar variables in objectives
312326
nlvo = _next(Int, io, model)
313327
# The number of nonlienar variables in constraints and objectives (both)
314328
nlvb = _next(Int, io, model)
315-
_read_til_newline(io)
329+
_read_til_newline(io, model)
316330
# Line 6
317331
# The number of linear network variables
318332
@assert _next(Int, io, model) == 0
319333
# The number of user-defined functions
320334
@assert _next(Int, io, model) == 0
321335
# The number of "arith"
322336
# TODO(odow): I don't know what this is.
323-
@assert _next(Int, io, model) == 0
337+
_next(Int, io, model)
324338
# The "flags" entry. This is mainly used for specifying that we want duals.
325339
# Ignore when reading.
326340
_next(Int, io, model)
327-
_read_til_newline(io)
341+
_read_til_newline(io, model)
328342
# Line 7
329343
# Number of binary variables
330344
nbv = _next(Int, io, model)
@@ -336,25 +350,25 @@ function _parse_header(io::IO, model::_CacheModel)
336350
nlvci = _next(Int, io, model)
337351
# Number of integer variables in nonlinear objectives
338352
nlvoi = _next(Int, io, model)
339-
_read_til_newline(io)
353+
_read_til_newline(io, model)
340354
# Line 8
341355
# Read the number of nonzeros in Jacobian and gradient, but don't do
342356
# anything with that information.
343357
@assert _next(Int, io, model) >= 0
344358
@assert _next(Int, io, model) >= 0
345-
_read_til_newline(io)
359+
_read_til_newline(io, model)
346360
# Line 9
347361
# We don't support reading variable and constraint names, so just ignore
348362
# them
349-
_read_til_newline(io)
363+
_read_til_newline(io, model)
350364
# Line 10
351365
# We don't support reading common subexpressions
352366
for _ in 1:5
353367
if _next(Int, io, model) > 0
354368
error("Unable to parse NL file : we don't support common exprs")
355369
end
356370
end
357-
_read_til_newline(io)
371+
_read_til_newline(io, model)
358372
# ==========================================================================
359373
# Deal with the integrality of variables. This is quite complicated, so go
360374
# read the README in this folder.
@@ -387,6 +401,8 @@ function _parse_header(io::IO, model::_CacheModel)
387401
model.variable_type[offset] = types[i]
388402
end
389403
end
404+
# Delay setting is_binary until the end of the header section
405+
model.is_binary = is_binary
390406
return
391407
end
392408

@@ -415,7 +431,7 @@ function _parse_section(io::IO, ::Val{'S'}, model::_CacheModel)
415431
suffix = readline(io)
416432
@warn("Skipping suffix: `S$k $n$suffix`")
417433
for _ in 1:n
418-
_read_til_newline(io)
434+
_read_til_newline(io, model)
419435
end
420436
return
421437
end
@@ -440,7 +456,7 @@ end
440456

441457
function _parse_section(io::IO, ::Val{'C'}, model::_CacheModel)
442458
index = _next(Int, io, model) + 1
443-
_read_til_newline(io)
459+
_read_til_newline(io, model)
444460
expr = _force_expr(_parse_expr(io, model))
445461
current = model.constraints[index]
446462
if current == :()
@@ -460,7 +476,7 @@ function _parse_section(io::IO, ::Val{'O'}, model::_CacheModel)
460476
@assert sense == 0
461477
model.sense = MOI.MIN_SENSE
462478
end
463-
_read_til_newline(io)
479+
_read_til_newline(io, model)
464480
expr = _force_expr(_parse_expr(io, model))
465481
if model.objective == :()
466482
model.objective = expr
@@ -472,30 +488,36 @@ end
472488

473489
function _parse_section(io::IO, ::Val{'x'}, model::_CacheModel)
474490
index = _next(Int, io, model)
475-
_read_til_newline(io)
491+
_read_til_newline(io, model)
476492
for _ in 1:index
477493
xi = _next(Int, io, model) + 1
478494
v = _next(Float64, io, model)
479495
model.variable_primal[xi] = v
480-
_read_til_newline(io)
496+
_read_til_newline(io, model)
481497
end
482498
return
483499
end
484500

485501
# TODO(odow): we don't read in dual starts.
486502
function _parse_section(io::IO, ::Val{'d'}, model::_CacheModel)
487-
index = _next(Int, io, model)
488-
_read_til_newline(io)
489-
for _ in 1:index
490-
_read_til_newline(io)
503+
n = _next(Int, io, model)
504+
_read_til_newline(io, model)
505+
for _ in 1:n
506+
_ = _next(Int, io, model)
507+
_ = _next(Float64, io, model)
508+
_read_til_newline(io, model)
491509
end
492510
return
493511
end
494512

495513
function _parse_section(io::IO, ::Val{'r'}, model::_CacheModel)
496-
_read_til_newline(io)
514+
_read_til_newline(io, model)
497515
for i in 1:length(model.constraint_lower)
498-
type = _next(Int, io, model)
516+
type = if model.is_binary
517+
parse(Int, read(io, Char))
518+
else
519+
_next(Int, io, model)
520+
end
499521
if type == 0
500522
model.constraint_lower[i] = _next(Float64, io, model)
501523
model.constraint_upper[i] = _next(Float64, io, model)
@@ -511,15 +533,19 @@ function _parse_section(io::IO, ::Val{'r'}, model::_CacheModel)
511533
model.constraint_lower[i] = value
512534
model.constraint_upper[i] = value
513535
end
514-
_read_til_newline(io)
536+
_read_til_newline(io, model)
515537
end
516538
return
517539
end
518540

519541
function _parse_section(io::IO, ::Val{'b'}, model::_CacheModel)
520-
_read_til_newline(io)
542+
_read_til_newline(io, model)
521543
for i in 1:length(model.variable_lower)
522-
type = _next(Int, io, model)
544+
type = if model.is_binary
545+
parse(Int, read(io, Char))
546+
else
547+
_next(Int, io, model)
548+
end
523549
if type == 0
524550
model.variable_lower[i] = _next(Float64, io, model)
525551
model.variable_upper[i] = _next(Float64, io, model)
@@ -535,32 +561,34 @@ function _parse_section(io::IO, ::Val{'b'}, model::_CacheModel)
535561
model.variable_lower[i] = value
536562
model.variable_upper[i] = value
537563
end
538-
_read_til_newline(io)
564+
_read_til_newline(io, model)
539565
end
540566
return
541567
end
542568

543569
# We ignore jacobian counts for now
544570
function _parse_section(io::IO, ::Val{'k'}, model::_CacheModel)
545-
_read_til_newline(io)
546-
for _ in 2:length(model.variable_lower)
547-
_read_til_newline(io)
571+
n = _next(Int, io, model)
572+
_read_til_newline(io, model)
573+
for _ in 1:n
574+
_ = _next(Int, io, model)
575+
_read_til_newline(io, model)
548576
end
549577
return
550578
end
551579

552580
function _parse_section(io::IO, ::Val{'J'}, model::_CacheModel)
553581
i = _next(Int, io, model) + 1
554582
nnz = _next(Int, io, model)
555-
_read_til_newline(io)
583+
_read_til_newline(io, model)
556584
expr = Expr(:call, :+)
557585
for _ in 1:nnz
558586
x = _next(Int, io, model)
559587
c = _next(Float64, io, model)
560588
if !iszero(c)
561589
push!(expr.args, Expr(:call, :*, c, MOI.VariableIndex(x + 1)))
562590
end
563-
_read_til_newline(io)
591+
_read_til_newline(io, model)
564592
end
565593
if length(expr.args) == 1
566594
# Linear part is just zeros
@@ -575,15 +603,15 @@ end
575603
function _parse_section(io::IO, ::Val{'G'}, model::_CacheModel)
576604
i = _next(Int, io, model) + 1
577605
nnz = _next(Int, io, model)
578-
_read_til_newline(io)
606+
_read_til_newline(io, model)
579607
expr = Expr(:call, :+)
580608
for _ in 1:nnz
581609
x = _next(Int, io, model)
582610
c = _next(Float64, io, model)
583611
if !iszero(c)
584612
push!(expr.args, Expr(:call, :*, c, MOI.VariableIndex(x + 1)))
585613
end
586-
_read_til_newline(io)
614+
_read_til_newline(io, model)
587615
end
588616
if length(expr.args) == 1
589617
# Linear part is just zeros

test/FileFormats/NL/read.jl

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -103,14 +103,14 @@ function test_parse_expr_maximum()
103103
return
104104
end
105105

106-
function test_parse_header_binary()
106+
function test_parse_header_unsupported_mode()
107107
model = NL._CacheModel()
108108
NL._resize_variables(model, 4)
109109
io = IOBuffer()
110-
write(io, "b3 1 1 0\n")
110+
write(io, "z3 1 1 0\n")
111111
seekstart(io)
112112
@test_throws(
113-
ErrorException("Unable to parse NL file : unsupported mode b"),
113+
ErrorException("Unable to parse NL file : unsupported mode z"),
114114
NL._parse_header(io, model),
115115
)
116116
return
@@ -147,7 +147,7 @@ function test_parse_header_assertion_errors()
147147
"g4 1 1 0\n3 3 1 0 0 0\n0 0\n0 1\n",
148148
"g3 1 1 0\n4 2 1 0 1 0\n2 1\n0 0\n4 0 0\n1 0 0 1\n",
149149
"g3 1 1 0\n4 2 1 0 1 0\n2 1\n0 0\n4 0 0\n0 1 0 1\n",
150-
"g3 1 1 0\n4 2 1 0 1 0\n2 1\n0 0\n4 0 0\n0 0 1 1\n",
150+
# "g3 1 1 0\n4 2 1 0 1 0\n2 1\n0 0\n4 0 0\n0 0 1 1\n",
151151
"g3 1 1 0\n4 2 1 0 1 0\n2 1\n0 0\n4 0 0\n0 0 0 1\n0 0 0 2 0\n-1 0\n",
152152
"g3 1 1 0\n4 2 1 0 1 0\n2 1\n0 0\n4 0 0\n0 0 0 1\n0 0 0 2 0\n0 -1\n",
153153
]
@@ -323,10 +323,10 @@ function test_parse_k()
323323
write(
324324
io,
325325
"""
326-
k
327-
2 # can stick a comment anywhere
328-
4
329-
""",
326+
k2
327+
2 # can stick a comment anywhere
328+
4
329+
""",
330330
)
331331
seekstart(io)
332332
NL._parse_section(io, model)

0 commit comments

Comments
 (0)