Skip to content

Commit adde9a4

Browse files
committed
Add extension to GenOpt
1 parent 027d69e commit adde9a4

8 files changed

Lines changed: 375 additions & 23 deletions

File tree

Project.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
99
SolverCore = "ff4d7338-4cf1-434d-91df-b86cb86fb843"
1010

1111
[weakdeps]
12+
GenOpt = "f2c049d8-7489-4223-990c-4f1c121a4cde"
1213
JuMP = "4076af6c-e467-56ae-b986-b466b2749572"
1314
KernelAbstractions = "63c18a36-062a-441e-b654-da1e3ab1ce7c"
1415
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
@@ -19,6 +20,11 @@ SpecialFunctions = "276daf66-3868-5448-9aa4-cd146d93841b"
1920
oneAPI = "8f75cd03-7ff8-4ecb-9b8f-daf728133b1b"
2021

2122
[extensions]
23+
<<<<<<< HEAD
24+
=======
25+
ExaModelsGenOpt = ["GenOpt", "MathOptInterface"]
26+
ExaModelsIpopt = ["MathOptInterface", "NLPModelsIpopt"]
27+
>>>>>>> 88b75aad (Add extension to GenOpt)
2228
ExaModelsJuMP = "JuMP"
2329
ExaModelsKernelAbstractions = "KernelAbstractions"
2430
ExaModelsMOI = "MathOptInterface"
@@ -29,6 +35,11 @@ ExaModelsSpecialFunctions = "SpecialFunctions"
2935

3036
[compat]
3137
Adapt = "4"
38+
<<<<<<< HEAD
39+
=======
40+
GenOpt = "0.2"
41+
Ipopt = "1.11"
42+
>>>>>>> 88b75aad (Add extension to GenOpt)
3243
JuMP = "1"
3344
KernelAbstractions = "0.9"
3445
MathOptInterface = "1.19"

ext/ExaModelsGenOpt.jl

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
module ExaModelsGenOpt
2+
3+
import ExaModels
4+
import GenOpt
5+
import GenOpt: FunctionGenerator, SumGenerator, ContiguousArrayOfVariables, IteratorIndex, Iterator
6+
import MathOptInterface as MOI
7+
8+
# Mark GenOpt function types as extension types
9+
ExaModels.is_extension_type(::Type{<:FunctionGenerator}) = true
10+
ExaModels.is_extension_type(::Type{<:SumGenerator}) = true
11+
12+
# Handle SumGenerator in objective expressions
13+
function ExaModels.exafy_extension_obj_arg(m::SumGenerator, var_to_idx)
14+
return _exagen(m.func, m.iterators, var_to_idx)
15+
end
16+
17+
# Hook to process FunctionGenerator constraints after standard constraints
18+
function ExaModels.copy_extra_constraints!(c, moim, var_to_idx, con_to_idx, T)
19+
con_types = MOI.get(moim, MOI.ListOfConstraintTypesPresent())
20+
for (F, S) in con_types
21+
F <: FunctionGenerator || continue
22+
cis = MOI.get(moim, MOI.ListOfConstraintIndices{F, S}())
23+
c = _copy_generator_constraints!(c, moim, cis, var_to_idx, con_to_idx, T, S)
24+
end
25+
return c
26+
end
27+
28+
function _copy_generator_constraints!(c, moim, cis, var_to_idx, con_to_idx, T, ::Type{S}) where {S}
29+
for ci in cis
30+
func = MOI.get(moim, MOI.ConstraintFunction(), ci)
31+
set = MOI.get(moim, MOI.ConstraintSet(), ci)
32+
con_to_idx[ci] = c.ncon
33+
expr, pars = _exagen(func.func, func.iterators, var_to_idx)
34+
c, _ = ExaModels.add_con(c, expr for p in pars; lcon = _lower_bounds(set, T), ucon = _upper_bounds(set, T))
35+
end
36+
return c
37+
end
38+
39+
# Convert GenOpt expression trees to ExaModels format
40+
41+
exagen::Number, _, _) = α
42+
43+
function exagen(f::MOI.ScalarNonlinearFunction, offsets, var_to_idx)
44+
if f.head == :getindex
45+
v = f.args[1]
46+
if v isa ContiguousArrayOfVariables
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
53+
end
54+
cp = cumprod(v.size)
55+
for i in 3:length(f.args)
56+
idx += cp[i - 2] * (exagen(f.args[i], offsets, var_to_idx) - 1)
57+
end
58+
return ExaModels.Var(idx)
59+
elseif v isa IteratorIndex
60+
@assert length(f.args) == 2
61+
@assert f.args[2] isa Integer
62+
if isnothing(offsets)
63+
@assert isone(f.args[2])
64+
return ExaModels.DataSource()
65+
else
66+
return ExaModels.DataIndexed(ExaModels.DataSource(), offsets[v.value] + f.args[2])
67+
end
68+
else
69+
error("Unexpected the first operand of `getindex` to be of type `$(typeof(v))`")
70+
end
71+
else
72+
return ExaModels.op(f.head)((exagen(e, offsets, var_to_idx) for e in f.args)...)
73+
end
74+
end
75+
76+
function _exagen(func::MOI.ScalarNonlinearFunction, iterators, var_to_idx)
77+
lengths = map(it -> length(first(it.values)), iterators)
78+
if length(lengths) == 1 && lengths[] == 1
79+
cs = nothing
80+
pars = only.(iterators[].values)
81+
else
82+
cs = [0; cumsum(lengths)[1:(end - 1)]]
83+
pars = vec(
84+
map(Base.Iterators.ProductIterator(ntuple(i -> iterators[i].values, length(iterators)))) do I
85+
reduce((i, j) -> tuple(i..., j...), I)
86+
end
87+
)
88+
end
89+
expr = exagen(func, cs, var_to_idx)
90+
return expr, pars
91+
end
92+
93+
# Bound helpers for vector sets used by FunctionGenerator constraints
94+
_lower_bounds(::Union{MOI.Zeros, MOI.Nonnegatives}, T) = zero(T)
95+
_lower_bounds(::MOI.Nonpositives, T) = typemin(T)
96+
_upper_bounds(::Union{MOI.Zeros, MOI.Nonpositives}, T) = zero(T)
97+
_upper_bounds(::MOI.Nonnegatives, T) = typemax(T)
98+
99+
end # module

ext/ExaModelsMOI.jl

Lines changed: 68 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,10 @@ function update_bin!(bin, e, p)
4343
if _update_bin!(bin, e, p) # if update succeeded, return the original bin
4444
return bin
4545
else # if update has failed, return a new bin
46-
return Bin(e, [p], bin)
46+
if p isa Tuple
47+
p = [p]
48+
end
49+
return Bin(e, p, bin)
4750
end
4851
end
4952
function _update_bin!(bin::Bin{E,P,I}, e, p) where {E,P,I}
@@ -61,6 +64,9 @@ end
6164
function check_supported(T, moim)
6265
con_types = MOI.get(moim, MOI.ListOfConstraintTypesPresent())
6366
for (F, S) in con_types
67+
if ExaModels.is_extension_type(F)
68+
continue
69+
end
6470
!(F <: SUPPORTED_FUNC_TYPE_WITH_VAR) && error("Unsupported function type $F.")
6571
if F <: MOI.VariableIndex
6672
!(S <: SUPPORTED_VAR_SET_TYPE) &&
@@ -71,7 +77,7 @@ function check_supported(T, moim)
7177
end
7278

7379
obj_type = MOI.get(moim, MOI.ObjectiveFunctionType())
74-
!(obj_type <: SUPPORTED_FUNC_TYPE_WITH_VAR) &&
80+
!(obj_type <: SUPPORTED_FUNC_TYPE_WITH_VAR || ExaModels.is_extension_type(obj_type)) &&
7581
error("Unsupported objective function type $obj_type.")
7682

7783
obj_sense = MOI.get(moim, MOI.ObjectiveSense())
@@ -204,23 +210,19 @@ function copy_constraints!(c, moim, var_to_idx, T)
204210

205211
con_types = MOI.get(moim, MOI.ListOfConstraintTypesPresent())
206212
for (F, S) in con_types
213+
F <: MOI.VariableIndex && continue
214+
ExaModels.is_extension_type(F) && continue
207215
cis = MOI.get(moim, MOI.ListOfConstraintIndices{F,S}())
208-
if F <: MOI.VariableIndex
209-
for ci in cis
210-
vi = MOI.get(moim, MOI.ConstraintFunction(), ci)
211-
vartype, var_idx = var_to_idx[vi]
212-
if vartype === :variable
213-
con_to_idx[ci] = var_idx
214-
end
215-
end
216-
continue
217-
end
218-
bin, offset =
219-
exafy_con(moim, cis, bin, offset, lcon, ucon, y0, var_to_idx, con_to_idx)
216+
bin, offset = exafy_con(moim, cis, bin, offset, lcon, ucon, y0, var_to_idx, con_to_idx)
220217
end
221218
c, cons = ExaModels.add_con(c, offset; start = y0, lcon = lcon, ucon = ucon)
222219
c = build_constraint!(c, cons, bin)
223220

221+
# Hook for extensions (e.g. GenOpt) to add their constraint types
222+
if applicable(ExaModels.copy_extra_constraints!, c, moim, var_to_idx, con_to_idx, T)
223+
c = ExaModels.copy_extra_constraints!(c, moim, var_to_idx, con_to_idx, T)
224+
end
225+
224226
return c, con_to_idx
225227
end
226228

@@ -323,15 +325,17 @@ function exafy_con(
323325
var_to_idx,
324326
con_to_idx,
325327
) where {V<:Vector{<:MOI.ConstraintIndex}}
326-
l = length(cons)
328+
l = sum(cons) do ci
329+
MOI.dimension(MOI.get(moim, MOI.ConstraintSet(), ci))
330+
end
327331

328332
resize!(lcon, offset + l)
329333
resize!(ucon, offset + l)
330334
resize!(y0, offset + l)
331335
for (i, ci) in enumerate(cons)
332336
func = MOI.get(moim, MOI.ConstraintFunction(), ci)
333337
set = MOI.get(moim, MOI.ConstraintSet(), ci)
334-
con_to_idx[ci] = offset + i
338+
con_to_idx[ci] = offset + 1
335339
start = if MOI.supports(
336340
moim, MOI.ConstraintPrimalStart(), typeof(ci)
337341
)
@@ -342,8 +346,9 @@ function exafy_con(
342346
_exafy_con_update_start(ci, start, y0, con_to_idx)
343347
_exafy_con_update_vector(ci, set, lcon, ucon, con_to_idx)
344348
bin = _exafy_con(ci, func, bin, var_to_idx, con_to_idx)
349+
offset += MOI.dimension(set)
345350
end
346-
return bin, (offset += l)
351+
return bin, offset
347352
end
348353

349354
function _exafy_con_update_start(i, start, y0, con_to_idx)
@@ -451,9 +456,12 @@ function exafy_obj(o::MOI.ScalarNonlinearFunction, bin, var_to_idx)
451456
bin = update_bin!(bin, e, p)
452457
end
453458
constant += m.constant
454-
else
459+
elseif m isa MOI.ScalarNonlinearFunction
455460
e, p = _exafy(m, var_to_idx)
456461
bin = update_bin!(bin, e, p)
462+
else
463+
e, p = ExaModels.exafy_extension_obj_arg(m, var_to_idx)
464+
bin = update_bin!(bin, e, p)
457465
end
458466
end
459467
else
@@ -464,6 +472,15 @@ function exafy_obj(o::MOI.ScalarNonlinearFunction, bin, var_to_idx)
464472
return update_bin!(bin, ExaModels.Null(constant), (1,)) # TODO see if this can be empty tuple
465473
end
466474

475+
# Fallback for extension objective types (e.g. SumGenerator as top-level objective)
476+
function exafy_obj(o, bin, var_to_idx)
477+
if !ExaModels.is_extension_type(typeof(o))
478+
throw(MOI.UnsupportedAttribute(MOI.ObjectiveFunction{typeof(o)}()))
479+
end
480+
e, p = ExaModels.exafy_extension_obj_arg(o, var_to_idx)
481+
return update_bin!(bin, e, p)
482+
end
483+
467484
function _exafy(v::MOI.VariableIndex, var_to_idx, p = ())
468485
i = ExaModels.DataIndexed(ExaModels.DataSource(), length(p) + 1)
469486
vartype, idx = var_to_idx[v]
@@ -481,7 +498,7 @@ function _exafy(i::R, var_to_idx, p) where {R<:Real}
481498
end
482499

483500
function _exafy(e::MOI.ScalarNonlinearFunction, var_to_idx, p = ())
484-
return op(e.head)((begin
501+
return ExaModels.op(e.head)((begin
485502
c, p = _exafy(e, var_to_idx, p)
486503
c
487504
end for e in e.args)...), p
@@ -542,8 +559,7 @@ function _exafy(e::MOI.ScalarQuadraticTerm{T}, var_to_idx, p = ()) where {T}
542559
end
543560
end
544561

545-
# eval can be a performance killer -- we want to explicitly include symbols for frequently used operations.
546-
function op(s::Symbol)
562+
function ExaModels.op(s::Symbol)
547563
# uni/multi
548564
if s === :+
549565
return +
@@ -710,6 +726,14 @@ end
710726

711727
MOI.is_empty(model::Optimizer) = isnothing(model.model)
712728

729+
function MOI.supports_constraint(
730+
::Optimizer,
731+
::Type{F},
732+
::Type{S},
733+
) where {F<:MOI.AbstractFunction, S<:MOI.AbstractSet}
734+
return ExaModels.is_extension_type(F)
735+
end
736+
713737
function MOI.supports_constraint(
714738
::Optimizer,
715739
::Type{<:SUPPORTED_FUNC_TYPE},
@@ -730,6 +754,9 @@ end
730754
function MOI.supports(::Optimizer, ::MOI.ObjectiveFunction{<:SUPPORTED_FUNC_TYPE_WITH_VAR})
731755
return true
732756
end
757+
function MOI.supports(::Optimizer, ::MOI.ObjectiveFunction{F}) where {F}
758+
return ExaModels.is_extension_type(F)
759+
end
733760
function MOI.supports(::Optimizer, ::MOI.VariablePrimalStart, ::Type{MOI.VariableIndex})
734761
return true
735762
end
@@ -884,17 +911,35 @@ function _make_index_map(model::MOI.ModelLike, var_to_idx, con_to_idx)
884911
end
885912
end
886913
for (F, S) in MOI.get(model, MOI.ListOfConstraintTypesPresent())
887-
_make_constraints_map(model, map.con_map[F, S], con_to_idx)
914+
_make_constraints_map(model, map.con_map[F, S], con_to_idx, var_to_idx)
888915
end
889916
return map
890917
end
891918
function _make_constraints_map(
892919
model,
893920
map::MOI.Utilities.DoubleDicts.IndexDoubleDictInner{F,S},
894921
con_to_idx,
922+
var_to_idx,
895923
) where {F,S}
896924
for c in MOI.get(model, MOI.ListOfConstraintIndices{F,S}())
897-
map[c] = typeof(c)(con_to_idx[c])
925+
if haskey(con_to_idx, c)
926+
map[c] = typeof(c)(con_to_idx[c])
927+
end
928+
end
929+
return
930+
end
931+
function _make_constraints_map(
932+
model,
933+
map::MOI.Utilities.DoubleDicts.IndexDoubleDictInner{MOI.VariableIndex,S},
934+
con_to_idx,
935+
var_to_idx,
936+
) where {S}
937+
for c in MOI.get(model, MOI.ListOfConstraintIndices{MOI.VariableIndex,S}())
938+
vi = MOI.get(model, MOI.ConstraintFunction(), c)
939+
entry = var_to_idx[vi]
940+
if entry.type === :variable
941+
map[c] = typeof(c)(entry.idx)
942+
end
898943
end
899944
return
900945
end

src/ExaModels.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ include("deprecated.jl")
6161
include("utils.jl")
6262
include("tags.jl")
6363
include("two_stage.jl")
64+
include("wrapper.jl")
6465

6566
export ExaModel,
6667
ExaCore,

src/wrapper.jl

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Extension points used by ExaModelsMOI and ExaModelsGenOpt extensions
2+
3+
"""
4+
copy_extra_constraints!(c, moim, var_to_idx, con_to_idx, T)
5+
6+
Hook for extensions to add extra constraint types after standard MOI constraints
7+
are processed. Default is a no-op, defined in ExaModelsMOI.
8+
"""
9+
function copy_extra_constraints! end
10+
11+
"""
12+
is_extension_type(::Type{F}) -> Bool
13+
14+
Return `true` if `F` is a function type handled by an extension.
15+
Used by `check_supported` and `supports_constraint` to whitelist extension types.
16+
"""
17+
function is_extension_type end
18+
is_extension_type(::Type) = false
19+
20+
"""
21+
exafy_extension_obj_arg(m, var_to_idx) -> Union{Nothing, Tuple}
22+
23+
Try to convert an objective function argument `m` to an `(expr, pars)` tuple
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.
26+
"""
27+
function exafy_extension_obj_arg end
28+
29+
"""
30+
op(s::Symbol)
31+
32+
Map a Symbol to the corresponding Julia function. Used by both ExaModelsMOI
33+
and ExaModelsGenOpt for expression tree conversion.
34+
"""
35+
function op end

0 commit comments

Comments
 (0)