Skip to content

Commit a4f4ff0

Browse files
committed
Add tests for non-continguous variable indices
1 parent 4916096 commit a4f4ff0

4 files changed

Lines changed: 87 additions & 17 deletions

File tree

ext/ExaModelsGenOpt.jl

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ ExaModels.is_extension_type(::Type{<:FunctionGenerator}) = true
1010
ExaModels.is_extension_type(::Type{<:SumGenerator}) = true
1111

1212
# Handle SumGenerator in objective expressions
13-
function ExaModels.exafy_extension_obj_arg(m::SumGenerator)
14-
return _exagen(m.func, m.iterators)
13+
function ExaModels.exafy_extension_obj_arg(m::SumGenerator, var_to_idx)
14+
return _exagen(m.func, m.iterators, var_to_idx)
1515
end
1616

1717
# Hook to process FunctionGenerator constraints after standard constraints
@@ -26,32 +26,34 @@ function ExaModels.copy_extra_constraints!(c, moim, var_to_idx, con_to_idx, T)
2626
end
2727

2828
function _copy_generator_constraints!(c, moim, cis, var_to_idx, con_to_idx, T, ::Type{S}) where {S}
29-
# FIXME we assume that `var_to_idx` is the identity
3029
for ci in cis
3130
func = MOI.get(moim, MOI.ConstraintFunction(), ci)
3231
set = MOI.get(moim, MOI.ConstraintSet(), ci)
3332
con_to_idx[ci] = c.ncon
34-
expr, pars = _exagen(func.func, func.iterators)
33+
expr, pars = _exagen(func.func, func.iterators, var_to_idx)
3534
c, _ = ExaModels.add_con(c, expr, pars; lcon = _lower_bounds(set, T), ucon = _upper_bounds(set, T))
3635
end
3736
return c
3837
end
3938

4039
# Convert GenOpt expression trees to ExaModels format
4140

42-
exagen::Number, _) = α
41+
exagen::Number, _, _) = α
4342

44-
function exagen(f::MOI.ScalarNonlinearFunction, offsets)
43+
function exagen(f::MOI.ScalarNonlinearFunction, offsets, var_to_idx)
4544
if f.head == :getindex
4645
v = f.args[1]
4746
if v isa ContiguousArrayOfVariables
48-
idx = exagen(f.args[2], offsets)
49-
if !iszero(v.offset)
50-
idx = v.offset + idx
47+
idx = exagen(f.args[2], offsets, var_to_idx)
48+
# Translate MOI-space offset to ExaModels-space offset using var_to_idx
49+
first_moi_vi = MOI.VariableIndex(v.offset + 1)
50+
exa_offset = var_to_idx[first_moi_vi].idx - 1
51+
if !iszero(exa_offset)
52+
idx = exa_offset + idx
5153
end
5254
cp = cumprod(v.size)
5355
for i in 3:length(f.args)
54-
idx += cp[i-2] * (exagen(f.args[i], offsets) - 1)
56+
idx += cp[i-2] * (exagen(f.args[i], offsets, var_to_idx) - 1)
5557
end
5658
return ExaModels.Var(idx)
5759
elseif v isa IteratorIndex
@@ -67,11 +69,11 @@ function exagen(f::MOI.ScalarNonlinearFunction, offsets)
6769
error("Unexpected the first operand of `getindex` to be of type `$(typeof(v))`")
6870
end
6971
else
70-
return ExaModels.op(f.head)((exagen(e, offsets) for e in f.args)...)
72+
return ExaModels.op(f.head)((exagen(e, offsets, var_to_idx) for e in f.args)...)
7173
end
7274
end
7375

74-
function _exagen(func::MOI.ScalarNonlinearFunction, iterators)
76+
function _exagen(func::MOI.ScalarNonlinearFunction, iterators, var_to_idx)
7577
lengths = map(it -> length(first(it.values)), iterators)
7678
if length(lengths) == 1 && lengths[] == 1
7779
cs = nothing
@@ -82,7 +84,7 @@ function _exagen(func::MOI.ScalarNonlinearFunction, iterators)
8284
reduce((i, j) -> tuple(i..., j...), I)
8385
end)
8486
end
85-
expr = exagen(func, cs)
87+
expr = exagen(func, cs, var_to_idx)
8688
return expr, pars
8789
end
8890

ext/ExaModelsMOI.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -458,7 +458,7 @@ function exafy_obj(o::MOI.ScalarNonlinearFunction, bin, var_to_idx)
458458
constant += m.constant
459459
else
460460
# Try extension hook first (e.g. for SumGenerator)
461-
result = ExaModels.exafy_extension_obj_arg(m)
461+
result = ExaModels.exafy_extension_obj_arg(m, var_to_idx)
462462
if !isnothing(result)
463463
e, p = result
464464
bin = update_bin!(bin, e, p)

src/wrapper.jl

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,14 @@ function is_extension_type end
1818
is_extension_type(::Type) = false
1919

2020
"""
21-
exafy_extension_obj_arg(m) -> Union{Nothing, Tuple}
21+
exafy_extension_obj_arg(m, var_to_idx) -> Union{Nothing, Tuple}
2222
2323
Try to convert an objective function argument `m` to an `(expr, pars)` tuple
24-
for ExaModels. Returns `nothing` if the type is not handled by any extension.
24+
for ExaModels. `var_to_idx` maps `MOI.VariableIndex` to `(type, idx)` named tuples.
25+
Returns `nothing` if the type is not handled by any extension.
2526
"""
2627
function exafy_extension_obj_arg end
27-
exafy_extension_obj_arg(m) = nothing
28+
exafy_extension_obj_arg(m, var_to_idx) = nothing
2829

2930
"""
3031
op(s::Symbol)

test/GenOptTest/GenOptTest.jl

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,60 @@ function _get_exa_model(model)
5353
return be.optimizer.model.optimizer.model
5454
end
5555

56+
function parametric_genopt_test()
57+
# Same structure as quadrotor_test but with MOI parameters declared first,
58+
# which shifts MOI variable indices relative to ExaModels internal indices.
59+
# This tests that the GenOpt extension does NOT assume var_to_idx is the identity.
60+
container = GenOpt.ParametrizedArray
61+
62+
N = 3
63+
n = 9
64+
p = 4
65+
dt = 0.1
66+
67+
x0_val = zeros(n)
68+
xf = [1.0, 0, 0, 1, 0, 0, 0, 0, 0]
69+
70+
Q = zeros(n)
71+
Q[1] = 1.0; Q[4] = 1.0
72+
Qf = ones(n)
73+
R = ones(p)
74+
75+
g = 9.81
76+
77+
itr1 = [(i, j, xf[j]) for i in 1:N for j in 1:n if Q[j] != 0]
78+
itr2 = [(j, xf[j]) for j in 1:n]
79+
80+
model = Model()
81+
82+
# Parameters come first, shifting MOI variable indices by 2
83+
@variable(model, par1 in MOI.Parameter(2.0))
84+
@variable(model, par2 in MOI.Parameter(3.0))
85+
86+
@variable(model, x[1:(N+1), 1:n])
87+
@variable(model, u[1:N, 1:p])
88+
89+
@constraint(model, start[i in 1:n], x[1, i] == x0_val[i], container = container)
90+
91+
@constraint(model, [i in 1:N], x[i+1, 1] == x[i, 1] + x[i, 2] * dt, container = container)
92+
@constraint(model, [i in 1:N], x[i+1, 2] == x[i, 2] + (u[i, 1]) * dt, container = container)
93+
@constraint(model, [i in 1:N], x[i+1, 3] == x[i, 3] + x[i, 4] * dt, container = container)
94+
@constraint(model, [i in 1:N], x[i+1, 4] == x[i, 4] + (u[i, 2]) * dt, container = container)
95+
@constraint(model, [i in 1:N], x[i+1, 5] == x[i, 5] + x[i, 6] * dt, container = container)
96+
@constraint(model, [i in 1:N], x[i+1, 6] == x[i, 6] + (u[i, 3] - g) * dt, container = container)
97+
@constraint(model, [i in 1:N], x[i+1, 7] == x[i, 7] + u[i, 1] * dt, container = container)
98+
@constraint(model, [i in 1:N], x[i+1, 8] == x[i, 8] + u[i, 2] * dt, container = container)
99+
@constraint(model, [i in 1:N], x[i+1, 9] == x[i, 9] + u[i, 4] * dt, container = container)
100+
101+
@objective(model, Min,
102+
lazy_sum(0.5 * R[j] * (u[i, j]^2) for i in 1:N, j in 1:p) +
103+
lazy_sum(0.5 * Q[it[2]] * (x[it[1], it[2]] - it[3])^2 for it in itr1) +
104+
lazy_sum(0.5 * Qf[it[1]] * (x[N+1, it[1]] - it[2])^2 for it in itr2),
105+
)
106+
107+
return model
108+
end
109+
56110
function runtests()
57111
@testset "GenOpt extension test" begin
58112
@testset "Quadrotor with ExaModels.Optimizer" begin
@@ -81,6 +135,19 @@ function runtests()
81135
itr_lengths = sort([length(c.itr) for c in nonempty])
82136
@test itr_lengths == [3, 3, 3, 3, 3, 3, 3, 3, 3, 9]
83137
end
138+
139+
@testset "GenOpt with MOI parameters" begin
140+
# Tests that var_to_idx is not assumed to be the identity.
141+
# Parameters before variables shift MOI indices.
142+
model = parametric_genopt_test()
143+
144+
set_optimizer(model, () -> ExaModels.Optimizer(MadNLP.madnlp))
145+
set_optimizer_attribute(model, "print_level", MadNLP.ERROR)
146+
optimize!(model)
147+
148+
# Same problem as quadrotor, should give same objective
149+
@test isapprox(objective_value(model), 8.1797, atol = 1e-3)
150+
end
84151
end
85152
end
86153

0 commit comments

Comments
 (0)