Skip to content

Commit f6e3242

Browse files
authored
[FileFormats.NL] add support for reading binary format (#2718)
1 parent df498f5 commit f6e3242

File tree

2 files changed

+376
-82
lines changed

2 files changed

+376
-82
lines changed

src/FileFormats/NL/read.jl

Lines changed: 93 additions & 52 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
@@ -112,13 +120,27 @@ function _next(::Type{Int}, io::IO, model::_CacheModel)
112120
return y
113121
end
114122

123+
function _next(::Type{Cchar}, io::IO, model::_CacheModel)
124+
if model.is_binary
125+
return read(io, Cchar)
126+
end
127+
byte = UInt8(' ')
128+
while byte == UInt8(' ')
129+
byte = read(io, UInt8)
130+
end
131+
return Cchar(byte)
132+
end
133+
115134
"""
116-
_read_til_newline(io::IO)
135+
_read_til_newline(io::IO, model::_CacheModel)
117136
118137
This function reads until it finds a new line character. This is useful for
119138
skipping comments.
120139
"""
121-
function _read_til_newline(io::IO)
140+
function _read_til_newline(io::IO, model::_CacheModel)
141+
if model.is_binary
142+
return
143+
end
122144
while read(io, UInt8) != UInt8('\n')
123145
end
124146
return
@@ -131,12 +153,12 @@ function _parse_expr(io::IO, model::_CacheModel)
131153
char = Char(read(io, UInt8))
132154
if char == 'o'
133155
opcode = _next(Int, io, model)
134-
_read_til_newline(io)
156+
_read_til_newline(io, model)
135157
arity, op_func = _AMPL_TO_JULIA[opcode]
136158
op_sym = Symbol(op_func)
137159
if arity == -1
138160
arity = _next(Int, io, model)
139-
_read_til_newline(io)
161+
_read_til_newline(io, model)
140162
if op_sym == :sum
141163
op_sym = :+
142164
elseif op_sym == :minimum
@@ -153,12 +175,12 @@ function _parse_expr(io::IO, model::_CacheModel)
153175
return parent
154176
elseif char == 'v'
155177
index = _next(Int, io, model)
156-
_read_til_newline(io)
178+
_read_til_newline(io, model)
157179
return MOI.VariableIndex(index + 1)
158180
else
159181
@assert char == 'n'
160182
ret = _next(Float64, io, model)
161-
_read_til_newline(io)
183+
_read_til_newline(io, model)
162184
return ret
163185
end
164186
end
@@ -270,13 +292,16 @@ function _parse_header(io::IO, model::_CacheModel)
270292
# Line 1
271293
# We don't support the binary format.
272294
byte = read(io, UInt8)
273-
if byte != UInt8('g')
295+
is_binary = false
296+
if byte == UInt8('b')
297+
is_binary = true
298+
elseif byte != UInt8('g')
274299
error("Unable to parse NL file : unsupported mode $(Char(byte))")
275300
end
276301
# L1 has some magic bytes for AMPL internals (to quote David, "The numbers
277302
# on the first line matter to AMPL; for other uses, it is best simply to
278303
# supply the ones shown above.")
279-
_read_til_newline(io)
304+
_read_til_newline(io, model)
280305
# Line 2
281306
# The number of variables
282307
n_var = _next(Int, io, model)
@@ -292,39 +317,39 @@ function _parse_header(io::IO, model::_CacheModel)
292317
# The number of logical constraints. This one is optional, so just read til
293318
# the end of the line.
294319
# @assert _next(Int, io, model) == 0
295-
_read_til_newline(io)
320+
_read_til_newline(io, model)
296321
# Line 3
297322
# The number of nonlinear constraints
298323
@assert _next(Int, io, model) >= 0
299324
# The number of nonlinear objectives
300325
@assert 0 <= _next(Int, io, model) <= 1
301-
_read_til_newline(io)
326+
_read_til_newline(io, model)
302327
# Line 4
303328
# The number of nonlinear network constraints
304329
@assert _next(Int, io, model) == 0
305330
# The number of linear network constraints
306331
@assert _next(Int, io, model) == 0
307-
_read_til_newline(io)
332+
_read_til_newline(io, model)
308333
# Line 5
309334
# The number of nonlienar variables in constraints
310335
nlvc = _next(Int, io, model)
311336
# The number of nonlienar variables in objectives
312337
nlvo = _next(Int, io, model)
313338
# The number of nonlienar variables in constraints and objectives (both)
314339
nlvb = _next(Int, io, model)
315-
_read_til_newline(io)
340+
_read_til_newline(io, model)
316341
# Line 6
317342
# The number of linear network variables
318343
@assert _next(Int, io, model) == 0
319344
# The number of user-defined functions
320345
@assert _next(Int, io, model) == 0
321346
# The number of "arith"
322347
# TODO(odow): I don't know what this is.
323-
@assert _next(Int, io, model) == 0
348+
_next(Int, io, model)
324349
# The "flags" entry. This is mainly used for specifying that we want duals.
325350
# Ignore when reading.
326351
_next(Int, io, model)
327-
_read_til_newline(io)
352+
_read_til_newline(io, model)
328353
# Line 7
329354
# Number of binary variables
330355
nbv = _next(Int, io, model)
@@ -336,25 +361,25 @@ function _parse_header(io::IO, model::_CacheModel)
336361
nlvci = _next(Int, io, model)
337362
# Number of integer variables in nonlinear objectives
338363
nlvoi = _next(Int, io, model)
339-
_read_til_newline(io)
364+
_read_til_newline(io, model)
340365
# Line 8
341366
# Read the number of nonzeros in Jacobian and gradient, but don't do
342367
# anything with that information.
343368
@assert _next(Int, io, model) >= 0
344369
@assert _next(Int, io, model) >= 0
345-
_read_til_newline(io)
370+
_read_til_newline(io, model)
346371
# Line 9
347372
# We don't support reading variable and constraint names, so just ignore
348373
# them
349-
_read_til_newline(io)
374+
_read_til_newline(io, model)
350375
# Line 10
351376
# We don't support reading common subexpressions
352377
for _ in 1:5
353378
if _next(Int, io, model) > 0
354379
error("Unable to parse NL file : we don't support common exprs")
355380
end
356381
end
357-
_read_til_newline(io)
382+
_read_til_newline(io, model)
358383
# ==========================================================================
359384
# Deal with the integrality of variables. This is quite complicated, so go
360385
# read the README in this folder.
@@ -387,6 +412,8 @@ function _parse_header(io::IO, model::_CacheModel)
387412
model.variable_type[offset] = types[i]
388413
end
389414
end
415+
# Delay setting is_binary until the end of the header section
416+
model.is_binary = is_binary
390417
return
391418
end
392419

@@ -412,10 +439,20 @@ end
412439
function _parse_section(io::IO, ::Val{'S'}, model::_CacheModel)
413440
k = _next(Int, io, model)
414441
n = _next(Int, io, model)
415-
suffix = readline(io)
416-
@warn("Skipping suffix: `S$k $n$suffix`")
442+
suffix_name = if model.is_binary
443+
len = _next(Int, io, model)
444+
String(read(io, len))
445+
else
446+
strip(readline(io))
447+
end
448+
@warn("Skipping suffix: `S$k $n $suffix_name`")
449+
# The “4” bit of k indicates whether the suffix is real (that is, double)
450+
# valued or integer valued: (k&4) != 0 --> real valued.
451+
T = ifelse(k & 4 != 0, Float64, Int)
417452
for _ in 1:n
418-
_read_til_newline(io)
453+
_ = _next(Int, io, model)
454+
_ = _next(T, io, model)
455+
_read_til_newline(io, model)
419456
end
420457
return
421458
end
@@ -440,7 +477,7 @@ end
440477

441478
function _parse_section(io::IO, ::Val{'C'}, model::_CacheModel)
442479
index = _next(Int, io, model) + 1
443-
_read_til_newline(io)
480+
_read_til_newline(io, model)
444481
expr = _force_expr(_parse_expr(io, model))
445482
current = model.constraints[index]
446483
if current == :()
@@ -460,7 +497,7 @@ function _parse_section(io::IO, ::Val{'O'}, model::_CacheModel)
460497
@assert sense == 0
461498
model.sense = MOI.MIN_SENSE
462499
end
463-
_read_til_newline(io)
500+
_read_til_newline(io, model)
464501
expr = _force_expr(_parse_expr(io, model))
465502
if model.objective == :()
466503
model.objective = expr
@@ -472,95 +509,99 @@ end
472509

473510
function _parse_section(io::IO, ::Val{'x'}, model::_CacheModel)
474511
index = _next(Int, io, model)
475-
_read_til_newline(io)
512+
_read_til_newline(io, model)
476513
for _ in 1:index
477514
xi = _next(Int, io, model) + 1
478515
v = _next(Float64, io, model)
479516
model.variable_primal[xi] = v
480-
_read_til_newline(io)
517+
_read_til_newline(io, model)
481518
end
482519
return
483520
end
484521

485522
# TODO(odow): we don't read in dual starts.
486523
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)
524+
n = _next(Int, io, model)
525+
_read_til_newline(io, model)
526+
for _ in 1:n
527+
_ = _next(Int, io, model)
528+
_ = _next(Float64, io, model)
529+
_read_til_newline(io, model)
491530
end
492531
return
493532
end
494533

495534
function _parse_section(io::IO, ::Val{'r'}, model::_CacheModel)
496-
_read_til_newline(io)
535+
_read_til_newline(io, model)
497536
for i in 1:length(model.constraint_lower)
498-
type = _next(Int, io, model)
499-
if type == 0
537+
type = _next(Cchar, io, model)
538+
if type == Cchar('0')
500539
model.constraint_lower[i] = _next(Float64, io, model)
501540
model.constraint_upper[i] = _next(Float64, io, model)
502-
elseif type == 1
541+
elseif type == Cchar('1')
503542
model.constraint_upper[i] = _next(Float64, io, model)
504-
elseif type == 2
543+
elseif type == Cchar('2')
505544
model.constraint_lower[i] = _next(Float64, io, model)
506-
elseif type == 3
545+
elseif type == Cchar('3')
507546
# Free constraint
508547
else
509-
@assert type == 4
548+
@assert type == Cchar('4')
510549
value = _next(Float64, io, model)
511550
model.constraint_lower[i] = value
512551
model.constraint_upper[i] = value
513552
end
514-
_read_til_newline(io)
553+
_read_til_newline(io, model)
515554
end
516555
return
517556
end
518557

519558
function _parse_section(io::IO, ::Val{'b'}, model::_CacheModel)
520-
_read_til_newline(io)
559+
_read_til_newline(io, model)
521560
for i in 1:length(model.variable_lower)
522-
type = _next(Int, io, model)
523-
if type == 0
561+
type = _next(Cchar, io, model)
562+
if type == Cchar('0')
524563
model.variable_lower[i] = _next(Float64, io, model)
525564
model.variable_upper[i] = _next(Float64, io, model)
526-
elseif type == 1
565+
elseif type == Cchar('1')
527566
model.variable_upper[i] = _next(Float64, io, model)
528-
elseif type == 2
567+
elseif type == Cchar('2')
529568
model.variable_lower[i] = _next(Float64, io, model)
530-
elseif type == 3
569+
elseif type == Cchar('3')
531570
# Free variable
532571
else
533-
@assert type == 4
572+
@assert type == Cchar('4')
534573
value = _next(Float64, io, model)
535574
model.variable_lower[i] = value
536575
model.variable_upper[i] = value
537576
end
538-
_read_til_newline(io)
577+
_read_til_newline(io, model)
539578
end
540579
return
541580
end
542581

543582
# We ignore jacobian counts for now
544583
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)
584+
n = _next(Int, io, model)
585+
_read_til_newline(io, model)
586+
for _ in 1:n
587+
_ = _next(Int, io, model)
588+
_read_til_newline(io, model)
548589
end
549590
return
550591
end
551592

552593
function _parse_section(io::IO, ::Val{'J'}, model::_CacheModel)
553594
i = _next(Int, io, model) + 1
554595
nnz = _next(Int, io, model)
555-
_read_til_newline(io)
596+
_read_til_newline(io, model)
556597
expr = Expr(:call, :+)
557598
for _ in 1:nnz
558599
x = _next(Int, io, model)
559600
c = _next(Float64, io, model)
560601
if !iszero(c)
561602
push!(expr.args, Expr(:call, :*, c, MOI.VariableIndex(x + 1)))
562603
end
563-
_read_til_newline(io)
604+
_read_til_newline(io, model)
564605
end
565606
if length(expr.args) == 1
566607
# Linear part is just zeros
@@ -575,15 +616,15 @@ end
575616
function _parse_section(io::IO, ::Val{'G'}, model::_CacheModel)
576617
i = _next(Int, io, model) + 1
577618
nnz = _next(Int, io, model)
578-
_read_til_newline(io)
619+
_read_til_newline(io, model)
579620
expr = Expr(:call, :+)
580621
for _ in 1:nnz
581622
x = _next(Int, io, model)
582623
c = _next(Float64, io, model)
583624
if !iszero(c)
584625
push!(expr.args, Expr(:call, :*, c, MOI.VariableIndex(x + 1)))
585626
end
586-
_read_til_newline(io)
627+
_read_til_newline(io, model)
587628
end
588629
if length(expr.args) == 1
589630
# Linear part is just zeros

0 commit comments

Comments
 (0)