diff --git a/Project.toml b/Project.toml index b8a10506a2..7ac2d9ad9a 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "MathOptInterface" uuid = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" -version = "1.41.0" +version = "1.42.0" [deps] BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" diff --git a/docs/src/changelog.md b/docs/src/changelog.md index b6c76c7c70..f3fa1a828d 100644 --- a/docs/src/changelog.md +++ b/docs/src/changelog.md @@ -7,6 +7,21 @@ CurrentModule = MathOptInterface The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## v1.42.0 (July 10, 2025) + +### Added + + - Added an option to disable warnings in [`Utilities.PenaltyRelaxation`](@ref) + (#2774) + +### Fixed + + - Fixed a bug writing objective constant in `MAX_SENSE` with `FileFormats.MPS` + (#2778) + - Fixed a change in how `==(::Expr, ::Expr)` works on for Julia nightly (#2780) + - Fixed a performance bug in the Hessian computation of `Nonlinear.ReverseAD` + (#2783) + ## v1.41.0 (June 9, 2025) ### Added diff --git a/src/FileFormats/MPS/MPS.jl b/src/FileFormats/MPS/MPS.jl index 36f3e94d89..9d6de0173a 100644 --- a/src/FileFormats/MPS/MPS.jl +++ b/src/FileFormats/MPS/MPS.jl @@ -451,7 +451,7 @@ end function _extract_terms_objective(model, var_to_column, coefficients, flip_obj) obj_func = _get_objective(model) _extract_terms(var_to_column, coefficients, "OBJ", obj_func, flip_obj) - return obj_func.constant + return flip_obj ? -obj_func.constant : obj_func.constant end function _var_name( diff --git a/src/Nonlinear/ReverseAD/forward_over_reverse.jl b/src/Nonlinear/ReverseAD/forward_over_reverse.jl index 03ea1d89fc..c531fb9878 100644 --- a/src/Nonlinear/ReverseAD/forward_over_reverse.jl +++ b/src/Nonlinear/ReverseAD/forward_over_reverse.jl @@ -92,16 +92,19 @@ function _eval_hessian_chunk( for s in 1:chunk # If `chunk < chunk_size`, leaves junk in the unused components d.input_ϵ[(idx-1)*chunk_size+s] = ex.seed_matrix[r, offset+s-1] + # Ensure the output is clear in preparation for the chunk + d.output_ϵ[(idx-1)*chunk_size+s] = 0.0 end end _hessian_slice_inner(d, ex, chunk_size) - fill!(d.input_ϵ, 0.0) # collect directional derivatives for r in eachindex(ex.rinfo.local_indices) @inbounds idx = ex.rinfo.local_indices[r] # load output_ϵ into ex.seed_matrix[r,k,k+1,...,k+remaining-1] for s in 1:chunk ex.seed_matrix[r, offset+s-1] = d.output_ϵ[(idx-1)*chunk_size+s] + # Reset the input in preparation for the next chunk + d.input_ϵ[(idx-1)*chunk_size+s] = 0.0 end end return @@ -122,7 +125,6 @@ end end function _hessian_slice_inner(d, ex, ::Type{T}) where {T} - fill!(d.output_ϵ, 0.0) output_ϵ = _reinterpret_unsafe(T, d.output_ϵ) subexpr_forward_values_ϵ = _reinterpret_unsafe(T, d.subexpression_forward_values_ϵ) diff --git a/src/Test/test_nonlinear.jl b/src/Test/test_nonlinear.jl index 1d746a3665..79829fbaf7 100644 --- a/src/Test/test_nonlinear.jl +++ b/src/Test/test_nonlinear.jl @@ -323,7 +323,7 @@ MOI.objective_expr(::FeasibilitySenseEvaluator) = :() function MOI.constraint_expr(::FeasibilitySenseEvaluator, i::Int) @assert i == 1 - return :(x[$(MOI.VariableIndex(1))]^2 == 1) + return :(x[$(MOI.VariableIndex(1))]^2.0 == 1.0) end MOI.eval_objective(d::FeasibilitySenseEvaluator, x) = 0.0 @@ -1072,7 +1072,7 @@ written. External solvers can exclude this test without consequence. function test_nonlinear_Feasibility_internal(::MOI.ModelLike, ::Config) d = FeasibilitySenseEvaluator(true) @test MOI.objective_expr(d) == :() - @test MOI.constraint_expr(d, 1) == :(x[$(MOI.VariableIndex(1))]^2 == 1.0) + @test MOI.constraint_expr(d, 1) == :(x[$(MOI.VariableIndex(1))]^2.0 == 1.0) @test_throws AssertionError MOI.constraint_expr(d, 2) MOI.initialize(d, [:Grad, :Jac, :ExprGraph, :Hess]) @test :Hess in MOI.features_available(d) diff --git a/src/Utilities/penalty_relaxation.jl b/src/Utilities/penalty_relaxation.jl index 3521cc3cce..ed414225fb 100644 --- a/src/Utilities/penalty_relaxation.jl +++ b/src/Utilities/penalty_relaxation.jl @@ -142,6 +142,7 @@ end PenaltyRelaxation( penalties = Dict{MOI.ConstraintIndex,Float64}(); default::Union{Nothing,T} = 1.0, + warn::Bool = true, ) A problem modifier that, when passed to [`MOI.modify`](@ref), destructively @@ -187,6 +188,9 @@ cannot be modified in-place. To modify variable bounds, rewrite them as linear constraints. +If a constraint cannot be modified, a warning is logged and the +constraint is skipped. The warning can be disabled by setting `warn = false`. + ## Example ```jldoctest @@ -242,12 +246,14 @@ true mutable struct PenaltyRelaxation{T} default::Union{Nothing,T} penalties::Dict{MOI.ConstraintIndex,T} + warn::Bool function PenaltyRelaxation( p::Dict{MOI.ConstraintIndex,T}; default::Union{Nothing,T} = one(T), + warn::Bool = true, ) where {T} - return new{T}(default, p) + return new{T}(default, p, warn) end end @@ -286,7 +292,11 @@ function _modify_penalty_relaxation( map[ci] = MOI.modify(model, ci, ScalarPenaltyRelaxation(penalty)) catch err if err isa MethodError && err.f == MOI.modify - @warn("Skipping PenaltyRelaxation for ConstraintIndex{$F,$S}") + if relax.warn + @warn( + "Skipping PenaltyRelaxation for ConstraintIndex{$F,$S}" + ) + end return end rethrow(err) diff --git a/test/FileFormats/MOF/MOF.jl b/test/FileFormats/MOF/MOF.jl index 2e4297503b..5a58acb719 100644 --- a/test/FileFormats/MOF/MOF.jl +++ b/test/FileFormats/MOF/MOF.jl @@ -96,8 +96,8 @@ function HS071(x::Vector{MOI.VariableIndex}) ExprEvaluator( :(x[$x1] * x[$x4] * (x[$x1] + x[$x2] + x[$x3]) + x[$x3]), [ - :(x[$x1] * x[$x2] * x[$x3] * x[$x4] >= 25), - :(x[$x1]^2 + x[$x2]^2 + x[$x3]^2 + x[$x4]^2 == 40), + :(x[$x1] * x[$x2] * x[$x3] * x[$x4] >= 25.0), + :(x[$x1]^2.0 + x[$x2]^2.0 + x[$x3]^2.0 + x[$x4]^2.0 == 40.0), ], ), true, @@ -117,7 +117,9 @@ function test_HS071() target = read(joinpath(@__DIR__, "nlp.mof.json"), String) target = replace(target, r"\s" => "") target = replace(target, "MathOptFormatModel" => "MathOptFormat Model") - @test read(TEST_MOF_FILE, String) == target + # Normalize .0 floats and integer representations in JSON + normalize(x) = replace(x, ".0" => "") + @test normalize(read(TEST_MOF_FILE, String)) == normalize(target) _validate(TEST_MOF_FILE) return end @@ -308,7 +310,7 @@ function test_nonlinear_readingwriting() block = MOI.get(model2, MOI.NLPBlock()) MOI.initialize(block.evaluator, [:ExprGraph]) @test MOI.constraint_expr(block.evaluator, 1) == - :(2 * x[$x] + sin(x[$x])^2 - x[$y] == 1.0) + :(2.0 * x[$x] + sin(x[$x])^2.0 - x[$y] == 1.0) _validate(TEST_MOF_FILE) return end diff --git a/test/FileFormats/MPS/MPS.jl b/test/FileFormats/MPS/MPS.jl index 8a642d3269..e9f770dba2 100644 --- a/test/FileFormats/MPS/MPS.jl +++ b/test/FileFormats/MPS/MPS.jl @@ -1610,6 +1610,57 @@ function test_int_round_trip() return end +function test_obj_constant_min() + model = MOI.FileFormats.MPS.Model() + x = MOI.add_variable(model) + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + f = 1.0 * x + 2.0 + MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f) + io = IOBuffer() + write(io, model) + dest = MOI.FileFormats.MPS.Model() + seekstart(io) + read!(io, dest) + g = MOI.get(dest, MOI.ObjectiveFunction{typeof(f)}()) + @test g.constant == 2.0 + @test MOI.get(dest, MOI.ObjectiveSense()) == MOI.MIN_SENSE + return +end + +function test_obj_constant_max_to_min() + model = MOI.FileFormats.MPS.Model() + x = MOI.add_variable(model) + MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE) + f = 1.0 * x + 2.0 + MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f) + io = IOBuffer() + write(io, model) + dest = MOI.FileFormats.MPS.Model() + seekstart(io) + read!(io, dest) + g = MOI.get(dest, MOI.ObjectiveFunction{typeof(f)}()) + @test g.constant == -2.0 + @test MOI.get(dest, MOI.ObjectiveSense()) == MOI.MIN_SENSE + return +end + +function test_obj_constant_max_to_max() + model = MOI.FileFormats.MPS.Model(; print_objsense = true) + x = MOI.add_variable(model) + MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE) + f = 1.0 * x + 2.0 + MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f) + io = IOBuffer() + write(io, model) + dest = MOI.FileFormats.MPS.Model() + seekstart(io) + read!(io, dest) + g = MOI.get(dest, MOI.ObjectiveFunction{typeof(f)}()) + @test g.constant == 2.0 + @test MOI.get(dest, MOI.ObjectiveSense()) == MOI.MAX_SENSE + return +end + end # TestMPS TestMPS.runtests() diff --git a/test/FileFormats/NL/read.jl b/test/FileFormats/NL/read.jl index 9c2b0a97a6..fcc0574b44 100644 --- a/test/FileFormats/NL/read.jl +++ b/test/FileFormats/NL/read.jl @@ -64,7 +64,8 @@ function test_parse_expr() # (* x1 (* 2 (* x4 x2))) seekstart(io) x = MOI.VariableIndex.(1:4) - @test NL._parse_expr(io, model) == :(*($(x[1]), *(2, *($(x[4]), $(x[2]))))) + @test NL._parse_expr(io, model) == + :(*($(x[1]), *(2.0, *($(x[4]), $(x[2]))))) @test eof(io) return end @@ -76,7 +77,7 @@ function test_parse_expr_nary() seekstart(io) x = MOI.VariableIndex.(1:4) @test NL._parse_expr(io, model) == - :(+($(x[1])^2, $(x[3])^2, $(x[4])^2, $(x[2])^2)) + :(+($(x[1])^2.0, $(x[3])^2.0, $(x[4])^2.0, $(x[2])^2.0)) @test eof(io) return end diff --git a/test/Nonlinear/Nonlinear.jl b/test/Nonlinear/Nonlinear.jl index 4904585963..5c85b4b0b6 100644 --- a/test/Nonlinear/Nonlinear.jl +++ b/test/Nonlinear/Nonlinear.jl @@ -62,7 +62,7 @@ function test_parse_sin_squared() Nonlinear.set_objective(model, :(sin($x)^2)) evaluator = Nonlinear.Evaluator(model) MOI.initialize(evaluator, [:ExprGraph]) - @test MOI.objective_expr(evaluator) == :(sin(x[$x])^2) + @test MOI.objective_expr(evaluator) == :(sin(x[$x])^2.0) return end @@ -72,7 +72,7 @@ function test_parse_ifelse() Nonlinear.set_objective(model, :(ifelse($x, 1, 2))) evaluator = Nonlinear.Evaluator(model) MOI.initialize(evaluator, [:ExprGraph]) - @test MOI.objective_expr(evaluator) == :(ifelse(x[$x], 1, 2)) + @test MOI.objective_expr(evaluator) == :(ifelse(x[$x], 1.0, 2.0)) return end @@ -83,7 +83,7 @@ function test_parse_ifelse_inequality_less() evaluator = Nonlinear.Evaluator(model) MOI.initialize(evaluator, [:ExprGraph]) @test MOI.objective_expr(evaluator) == - :(ifelse(x[$x] < 1, x[$x] - 1, x[$x] + 1)) + :(ifelse(x[$x] < 1.0, x[$x] - 1.0, x[$x] + 1.0)) return end @@ -94,7 +94,7 @@ function test_parse_ifelse_inequality_greater() evaluator = Nonlinear.Evaluator(model) MOI.initialize(evaluator, [:ExprGraph]) @test MOI.objective_expr(evaluator) == - :(ifelse(x[$x] > 1, x[$x] - 1, x[$x] + 1)) + :(ifelse(x[$x] > 1.0, x[$x] - 1.0, x[$x] + 1.0)) return end @@ -105,7 +105,7 @@ function test_parse_ifelse_comparison() evaluator = Nonlinear.Evaluator(model) MOI.initialize(evaluator, [:ExprGraph]) @test MOI.objective_expr(evaluator) == - :(ifelse(0 <= x[$x] <= 1, x[$x] - 1, x[$x] + 1)) + :(ifelse(0.0 <= x[$x] <= 1.0, x[$x] - 1.0, x[$x] + 1.0)) return end @@ -251,7 +251,7 @@ function test_set_objective() @test model.objective == Nonlinear.parse_expression(model, input) evaluator = Nonlinear.Evaluator(model) MOI.initialize(evaluator, [:ExprGraph]) - @test MOI.objective_expr(evaluator) == :(x[$x]^2 + 1) + @test MOI.objective_expr(evaluator) == :(x[$x]^2.0 + 1.0) return end @@ -263,7 +263,7 @@ function test_set_objective_subexpression() Nonlinear.set_objective(model, :($expr^2)) evaluator = Nonlinear.Evaluator(model) MOI.initialize(evaluator, [:ExprGraph]) - @test MOI.objective_expr(evaluator) == :((x[$x]^2 + 1)^2) + @test MOI.objective_expr(evaluator) == :((x[$x]^2.0 + 1.0)^2.0) return end @@ -276,7 +276,7 @@ function test_set_objective_nested_subexpression() Nonlinear.set_objective(model, :($expr_2^2)) evaluator = Nonlinear.Evaluator(model) MOI.initialize(evaluator, [:ExprGraph]) - @test MOI.objective_expr(evaluator) == :(((x[$x]^2 + 1)^2)^2) + @test MOI.objective_expr(evaluator) == :(((x[$x]^2.0 + 1.0)^2.0)^2.0) return end @@ -287,7 +287,7 @@ function test_set_objective_parameter() Nonlinear.set_objective(model, :($x^2 + $p)) evaluator = Nonlinear.Evaluator(model) MOI.initialize(evaluator, [:ExprGraph]) - @test MOI.objective_expr(evaluator) == :(x[$x]^2 + 1.2) + @test MOI.objective_expr(evaluator) == :(x[$x]^2.0 + 1.2) return end @@ -300,7 +300,7 @@ function test_add_constraint_less_than() @test model[c].set == set evaluator = Nonlinear.Evaluator(model) MOI.initialize(evaluator, [:ExprGraph]) - @test MOI.constraint_expr(evaluator, 1) == :(x[$x]^2 + 1 <= 1.0) + @test MOI.constraint_expr(evaluator, 1) == :(x[$x]^2.0 + 1.0 <= 1.0) return end @@ -311,7 +311,7 @@ function test_add_constraint_delete() _ = Nonlinear.add_constraint(model, :(sqrt($x)), MOI.LessThan(1.0)) evaluator = Nonlinear.Evaluator(model) MOI.initialize(evaluator, [:ExprGraph]) - @test MOI.constraint_expr(evaluator, 1) == :(x[$x]^2 + 1 <= 1.0) + @test MOI.constraint_expr(evaluator, 1) == :(x[$x]^2.0 + 1.0 <= 1.0) @test MOI.constraint_expr(evaluator, 2) == :(sqrt(x[$x]) <= 1.0) Nonlinear.delete(model, c1) evaluator = Nonlinear.Evaluator(model) @@ -330,7 +330,7 @@ function test_add_constraint_greater_than() @test model[c].set == set evaluator = Nonlinear.Evaluator(model) MOI.initialize(evaluator, [:ExprGraph]) - @test MOI.constraint_expr(evaluator, 1) == :(x[$x]^2 + 1 >= 1.0) + @test MOI.constraint_expr(evaluator, 1) == :(x[$x]^2.0 + 1.0 >= 1.0) return end @@ -342,7 +342,7 @@ function test_add_constraint_equal_to() @test model[c].set == set evaluator = Nonlinear.Evaluator(model) MOI.initialize(evaluator, [:ExprGraph]) - @test MOI.constraint_expr(evaluator, 1) == :(x[$x]^2 + 1 == 1.0) + @test MOI.constraint_expr(evaluator, 1) == :(x[$x]^2.0 + 1.0 == 1.0) return end @@ -354,7 +354,7 @@ function test_add_constraint_interval() @test model[c].set == set evaluator = Nonlinear.Evaluator(model) MOI.initialize(evaluator, [:ExprGraph]) - @test MOI.constraint_expr(evaluator, 1) == :(-1.0 <= x[$x]^2 + 1 <= 1.0) + @test MOI.constraint_expr(evaluator, 1) == :(-1.0 <= x[$x]^2.0 + 1.0 <= 1.0) return end diff --git a/test/Utilities/penalty_relaxation.jl b/test/Utilities/penalty_relaxation.jl index 226bf8a623..ae6c839f4b 100644 --- a/test/Utilities/penalty_relaxation.jl +++ b/test/Utilities/penalty_relaxation.jl @@ -65,6 +65,25 @@ function test_relax_bounds() return end +function test_relax_no_warn() + input = """ + variables: x, y + minobjective: x + y + x >= 0.0 + y <= 0.0 + x in ZeroOne() + y in Integer() + """ + model = MOI.Utilities.Model{Float64}() + MOI.Utilities.loadfromstring!(model, input) + relaxation = MOI.Utilities.PenaltyRelaxation(; warn = false) + @test_logs MOI.modify(model, relaxation) + dest = MOI.Utilities.Model{Float64}() + MOI.Utilities.loadfromstring!(dest, input) + MOI.Bridges._test_structural_identical(model, dest) + return +end + function test_relax_affine_lessthan() _test_roundtrip( """