From 6dff51f058c3618105af3a2c8d39c3d39419bcbe Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Mon, 9 Feb 2026 09:22:12 +1300 Subject: [PATCH 1/2] [FileFormats.NL] add support for defined variables --- src/FileFormats/NL/read.jl | 36 ++++++++++++---- test/FileFormats/NL/test_read.jl | 73 ++++++++++++++++++++------------ 2 files changed, 75 insertions(+), 34 deletions(-) diff --git a/src/FileFormats/NL/read.jl b/src/FileFormats/NL/read.jl index 8d500b5322..71a78e35fb 100644 --- a/src/FileFormats/NL/read.jl +++ b/src/FileFormats/NL/read.jl @@ -17,6 +17,7 @@ mutable struct _CacheModel objective::Expr sense::MOI.OptimizationSense complements_map::Dict{Int,Int} + defined_variables::Dict{Int,Expr} function _CacheModel() return new( @@ -32,6 +33,7 @@ mutable struct _CacheModel :(), MOI.FEASIBILITY_SENSE, Dict{Int,Int}(), + Dict{Int,Expr}(), ) end end @@ -179,7 +181,7 @@ function _parse_expr(io::IO, model::_CacheModel) elseif char == 'v' index = _next(Int, io, model) _read_til_newline(io, model) - return MOI.VariableIndex(index + 1) + return _to_variable(model, index) else @assert char == 'n' ret = _next(Float64, io, model) @@ -188,6 +190,13 @@ function _parse_expr(io::IO, model::_CacheModel) end end +function _to_variable(model::_CacheModel, index::Int) + if index >= length(model.variable_primal) + return model.defined_variables[index] + end + return MOI.VariableIndex(index + 1) +end + function _to_model(data::_CacheModel; use_nlp_block::Bool) model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()) x = MOI.add_variables(model, length(data.variable_primal)) @@ -478,13 +487,24 @@ function _parse_section(io::IO, ::Val{'S'}, model::_CacheModel) return end -function _parse_section(::IO, ::Val{'V'}, ::_CacheModel) - return error( - "Unable to parse NL file: defined variable definitions ('V' sections)" * - " are not yet supported. To request support, please open an issue at " * - "https://github.com/jump-dev/MathOptInterface.jl with a reproducible " * - "example.", - ) +function _parse_section(io::IO, ::Val{'V'}, model::_CacheModel) + i = _next(Int, io, model) + j = _next(Int, io, model) + k = _next(Int, io, model) + _read_til_newline(io, model) + affine_terms = Expr(:call, :+) + for l in 1:j + p_l = _to_variable(model, _next(Int, io, model)) + c_l = _next(Float64, io, model) + _read_til_newline(io, model) + push!(affine_terms.args, Expr(:call, :*, c_l, p_l)) + end + expr = _parse_expr(io, model) + if j > 0 + expr = Expr(:call, :+, affine_terms, expr) + end + model.defined_variables[i] = expr + return end function _parse_section(::IO, ::Val{'L'}, ::_CacheModel) diff --git a/test/FileFormats/NL/test_read.jl b/test/FileFormats/NL/test_read.jl index 122b850211..4c093b44fe 100644 --- a/test/FileFormats/NL/test_read.jl +++ b/test/FileFormats/NL/test_read.jl @@ -59,6 +59,7 @@ end function test_parse_expr() model = NL._CacheModel() + NL._resize_variables(model, 4) io = IOBuffer() write(io, "o2\nv0\no2\nn2\no2\nv3\nv1\n") # (* x1 (* 2 (* x4 x2))) @@ -72,6 +73,7 @@ end function test_parse_expr_nary() model = NL._CacheModel() + NL._resize_variables(model, 4) io = IOBuffer() write(io, "o54\n4\no5\nv0\nn2\no5\nv2\nn2\no5\nv3\nn2\no5\nv1\nn2\n") seekstart(io) @@ -84,6 +86,7 @@ end function test_parse_expr_minimum() model = NL._CacheModel() + NL._resize_variables(model, 3) io = IOBuffer() write(io, "o11\n3\nv0\nv1\nv2\n") seekstart(io) @@ -95,6 +98,7 @@ end function test_parse_expr_maximum() model = NL._CacheModel() + NL._resize_variables(model, 3) io = IOBuffer() write(io, "o12\n3\nv0\nv1\nv2\n") seekstart(io) @@ -119,6 +123,7 @@ end function test_parse_expr_atan2() model = NL._CacheModel() + NL._resize_variables(model, 2) io = IOBuffer() write(io, "o48\nv0\nv1\n") seekstart(io) @@ -130,6 +135,7 @@ end function test_parse_expr_atan() model = NL._CacheModel() + NL._resize_variables(model, 1) io = IOBuffer() write(io, "o49\nv0\n") seekstart(io) @@ -378,19 +384,20 @@ end function test_parse_C_J() model = NL._CacheModel() + NL._resize_variables(model, 2) NL._resize_constraints(model, 1) io = IOBuffer() write( io, """ -C0 -o2 -v0 -v1 -J0 2 -0 1.1 -1 2.2 -""", + C0 + o2 + v0 + v1 + J0 2 + 0 1.1 + 1 2.2 + """, ) seekstart(io) NL._parse_section(io, model) @@ -403,19 +410,20 @@ end function test_parse_J_C() model = NL._CacheModel() + NL._resize_variables(model, 2) NL._resize_constraints(model, 1) io = IOBuffer() write( io, """ -J0 2 -0 1.1 -1 2.2 -C0 -o2 -v0 -v1 -""", + J0 2 + 0 1.1 + 1 2.2 + C0 + o2 + v0 + v1 + """, ) seekstart(io) NL._parse_section(io, model) @@ -535,18 +543,31 @@ end function test_parse_V() model = NL._CacheModel() + NL._resize_variables(model, 9) io = IOBuffer() - write(io, "V") - seekstart(io) - @test_throws( - ErrorException( - "Unable to parse NL file: defined variable definitions ('V' sections)" * - " are not yet supported. To request support, please open an issue at " * - "https://github.com/jump-dev/MathOptInterface.jl with a reproducible " * - "example.", - ), - NL._parse_section(io, model), + write( + io, + """ + V9 0 0 #nl(t[2]) + o5 #^ + v0 #x[2] + n2 + V10 2 0 #t[2] + 3 10 + 4 11 + o0 # + + v9 #nl(t[2]) + n1 + """, ) + seekstart(io) + NL._parse_section(io, model) + NL._parse_section(io, model) + v = MOI.VariableIndex.(1:9) + t1 = :($(v[1]) ^ 2.0) + @test model.defined_variables[9] == t1 + @test model.defined_variables[10] == + :((10.0 * $(v[4]) + 11.0 * $(v[5])) + ($t1 + 1.0)) return end From 29649c366e888797f4ea6860270888f601e51dcd Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Mon, 9 Feb 2026 09:48:33 +1300 Subject: [PATCH 2/2] Update --- src/FileFormats/NL/read.jl | 7 +------ test/FileFormats/NL/test_read.jl | 20 -------------------- 2 files changed, 1 insertion(+), 26 deletions(-) diff --git a/src/FileFormats/NL/read.jl b/src/FileFormats/NL/read.jl index 71a78e35fb..0bc97c5d06 100644 --- a/src/FileFormats/NL/read.jl +++ b/src/FileFormats/NL/read.jl @@ -403,12 +403,7 @@ function _parse_header(io::IO, model::_CacheModel) # them _read_til_newline(io, model) # Line 10 - # We don't support reading common subexpressions - for _ in 1:5 - if _next(Int, io, model) > 0 - error("Unable to parse NL file : we don't support common exprs") - end - end + # We support subexpressions, but we don't need to know the details yet. _read_til_newline(io, model) # ========================================================================== # Deal with the integrality of variables. This is quite complicated, so go diff --git a/test/FileFormats/NL/test_read.jl b/test/FileFormats/NL/test_read.jl index 4c093b44fe..dd2c8d7bc4 100644 --- a/test/FileFormats/NL/test_read.jl +++ b/test/FileFormats/NL/test_read.jl @@ -166,26 +166,6 @@ function test_parse_header_assertion_errors() return end -function test_parse_header_common_expressions() - model = NL._CacheModel() - err = ErrorException( - "Unable to parse NL file : we don't support common exprs", - ) - for header in [ - "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\n8 4\n0 0\n1 0 0 0 0\n", - "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\n8 4\n0 0\n0 1 0 0 0\n", - "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\n8 4\n0 0\n0 0 1 0 0\n", - "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\n8 4\n0 0\n0 0 0 1 0\n", - "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\n8 4\n0 0\n0 0 0 0 1\n", - ] - io = IOBuffer() - write(io, header) - seekstart(io) - @test_throws(err, NL._parse_header(io, model)) - end - return -end - function test_parse_y_error() model = NL._CacheModel() NL._resize_variables(model, 4)