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( """
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: