From 0b30c1e75e8f58896d9bd54228a25776da4e8ab3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Thu, 20 Aug 2026 11:27:41 +0200 Subject: [PATCH 1/2] Clearer error message with constrained variables in VectorOfVariables --- src/Utilities/functions.jl | 23 +++++++++++++++++++++++ test/Utilities/test_functions.jl | 13 +++++++++++++ 2 files changed, 36 insertions(+) diff --git a/src/Utilities/functions.jl b/src/Utilities/functions.jl index a512d380db..1de363f608 100644 --- a/src/Utilities/functions.jl +++ b/src/Utilities/functions.jl @@ -466,6 +466,29 @@ function substitute_variables( return substitute_variables.(variable_map, x) end +function substitute_variables( + variable_map::F, + f::MOI.VectorOfVariables, +) where {F<:Function} + variables = MOI.VariableIndex[] + for variable in f.variables + mapped_variable = variable_map(variable) + try + push!(variables, convert(MOI.VariableIndex, mapped_variable)) + catch err + @assert err isa InexactError + error( + "Cannot substitute `VectorOfVariables`: variable `$variable` is mapped " * + "to `$mapped_variable`, not directly to another variable. Variables " * + "constrained on creation can be used in a `VectorOfVariables` function " * + "only when their bridge maps each one directly to another variable, " * + "without a transformation.", + ) + end + end + return MOI.VectorOfVariables(variables) +end + function substitute_variables( variable_map::F, term::MOI.ScalarAffineTerm{T}, diff --git a/test/Utilities/test_functions.jl b/test/Utilities/test_functions.jl index 9182bb2ee2..8b748eef93 100644 --- a/test/Utilities/test_functions.jl +++ b/test/Utilities/test_functions.jl @@ -329,6 +329,19 @@ function test_substitute_variables() int_quad = 3 * y * x @test MOI.Utilities.substitute_variables(vi -> true * y, int_quad) ≈ 3 * y * y + + vector_of_variables = MOI.VectorOfVariables([w, x]) + @test MOI.Utilities.substitute_variables( + vi -> Dict(w => y, x => z)[vi], + vector_of_variables, + ) == MOI.VectorOfVariables([y, z]) + err = try + MOI.Utilities.substitute_variables(vi -> 2.0 * vi, vector_of_variables) + catch err + err + end + @test err isa ErrorException + @test occursin("Variables constrained on creation", sprint(showerror, err)) return end From 1e4a53b1de50d4d947b1cf458dd735ffd4f9ee01 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Fri, 21 Aug 2026 09:57:33 +1200 Subject: [PATCH 2/2] Update --- src/Utilities/functions.jl | 53 ++++++++++++++++++-------------- test/Utilities/test_functions.jl | 37 ++++++++++++++-------- 2 files changed, 55 insertions(+), 35 deletions(-) diff --git a/src/Utilities/functions.jl b/src/Utilities/functions.jl index 1de363f608..39fffd6169 100644 --- a/src/Utilities/functions.jl +++ b/src/Utilities/functions.jl @@ -466,29 +466,6 @@ function substitute_variables( return substitute_variables.(variable_map, x) end -function substitute_variables( - variable_map::F, - f::MOI.VectorOfVariables, -) where {F<:Function} - variables = MOI.VariableIndex[] - for variable in f.variables - mapped_variable = variable_map(variable) - try - push!(variables, convert(MOI.VariableIndex, mapped_variable)) - catch err - @assert err isa InexactError - error( - "Cannot substitute `VectorOfVariables`: variable `$variable` is mapped " * - "to `$mapped_variable`, not directly to another variable. Variables " * - "constrained on creation can be used in a `VectorOfVariables` function " * - "only when their bridge maps each one directly to another variable, " * - "without a transformation.", - ) - end - end - return MOI.VectorOfVariables(variables) -end - function substitute_variables( variable_map::F, term::MOI.ScalarAffineTerm{T}, @@ -566,6 +543,36 @@ function substitute_variables( return MOI.ScalarNonlinearFunction(f.head, new_args) end +function substitute_variables( + variable_map::F, + f::MOI.VectorOfVariables, +) where {F<:Function} + variables = MOI.VariableIndex[] + for (i, x) in enumerate(f.variables) + y = variable_map(x) + try + push!(variables, convert(MOI.VariableIndex, y)) + catch err + @assert err isa InexactError + error( + """ + Cannot substitute variables in the `VectorOfVariables` \ + function because the result is not representable as a \ + `VectorOfVariables` function. + + The variable `$x` in index `$i` is mapped to the expression \ + `$y`, not directly to another `VariableIndex`. + + Note that variables constrained on creation can be used in a \ + `VectorOfVariables` function only when their bridge maps each \ + one directly to another variable, without a transformation. + """, + ) + end + end + return MOI.VectorOfVariables(variables) +end + function substitute_variables( variable_map::F, f::MOI.VectorAffineFunction{T}, diff --git a/test/Utilities/test_functions.jl b/test/Utilities/test_functions.jl index 8b748eef93..01d477af56 100644 --- a/test/Utilities/test_functions.jl +++ b/test/Utilities/test_functions.jl @@ -329,19 +329,32 @@ function test_substitute_variables() int_quad = 3 * y * x @test MOI.Utilities.substitute_variables(vi -> true * y, int_quad) ≈ 3 * y * y + return +end - vector_of_variables = MOI.VectorOfVariables([w, x]) - @test MOI.Utilities.substitute_variables( - vi -> Dict(w => y, x => z)[vi], - vector_of_variables, - ) == MOI.VectorOfVariables([y, z]) - err = try - MOI.Utilities.substitute_variables(vi -> 2.0 * vi, vector_of_variables) - catch err - err - end - @test err isa ErrorException - @test occursin("Variables constrained on creation", sprint(showerror, err)) +function test_substitute_variables_vector_of_variables() + w, x, y, z = MOI.VariableIndex.(1:4) + f1 = MOI.VectorOfVariables([w, x]) + f2 = MOI.VectorOfVariables([y, z]) + v_map = Dict(w => y, x => z) + @test MOI.Utilities.substitute_variables(v -> v_map[v], f1) == f2 + @test_throws( + ErrorException( + """ + Cannot substitute variables in the `VectorOfVariables` function \ + because the result is not representable as a `VectorOfVariables` \ + function. + + The variable `$w` in index `1` is mapped to the expression \ + `$(2.0 * w)`, not directly to another `VariableIndex`. + + Note that variables constrained on creation can be used in a \ + `VectorOfVariables` function only when their bridge maps each \ + one directly to another variable, without a transformation. + """, + ), + MOI.Utilities.substitute_variables(vi -> 2.0 * vi, f1), + ) return end