diff --git a/src/InfiniteExaModels.jl b/src/InfiniteExaModels.jl index a423aba..cbfa5f0 100644 --- a/src/InfiniteExaModels.jl +++ b/src/InfiniteExaModels.jl @@ -6,6 +6,7 @@ import InfiniteOpt.TranscriptionOpt as _TO include("infiniteopt_backend.jl") include("operators.jl") +include("grouped_constraints.jl") include("transform.jl") export ExaMappingData, ExaTranscriptionBackend diff --git a/src/grouped_constraints.jl b/src/grouped_constraints.jl new file mode 100644 index 0000000..81d28d8 --- /dev/null +++ b/src/grouped_constraints.jl @@ -0,0 +1,248 @@ +# Integer alias for types of InfiniteOpt modelling objects to use in hashing expressions +const _VariableTypeHashingInt = Dict( + InfiniteOpt.FiniteParameterIndex => -2, + InfiniteOpt.ParameterFunctionIndex => -3, + InfiniteOpt.InfiniteVariableIndex => -4, + InfiniteOpt.DerivativeIndex => -4, + InfiniteOpt.SemiInfiniteVariableIndex => -5, + InfiniteOpt.PointVariableIndex => -6, + InfiniteOpt.FiniteVariableIndex => -7, + InfiniteOpt.IndependentParameterIndex => -8, + InfiniteOpt.DependentParameterIndex => -9, + InfiniteOpt.MeasureIndex => -10, +) + +## Extract the following from an expression: +# 1. A hash of the symbolic expression structure +# 2. A list of all variable references in the expression in the order they appear +# 3. A list of all constant values in the expression in the order they appear +function _encode_expr(expr::JuMP.AbstractJuMPScalar) + return _encode_expr(expr, hash(:+), InfiniteOpt.GeneralVariableRef[], Float64[]) +end +function _encode_expr(c::Real, h::UInt, refs, consts) + return hash(-1, h), refs, push!(consts, c) # -1 indicates a symbolic constant +end +function _encode_expr(v::InfiniteOpt.GeneralVariableRef, h::UInt, refs, consts) + if v.index_type in (InfiniteOpt.SemiInfiniteVariableIndex, InfiniteOpt.PointVariableIndex) + group_idxs = InfiniteOpt.parameter_group_int_indices(InfiniteOpt.infinite_variable_ref(v)) + else + group_idxs = InfiniteOpt.parameter_group_int_indices(v) + end + return hash((_VariableTypeHashingInt[v.index_type], group_idxs), h), push!(refs, v), consts +end +function _encode_expr( + expr::Union{JuMP.GenericAffExpr{C, V}, JuMP.GenericQuadExpr{C, V}}, + h::UInt, + refs, + consts + ) where {C, V} + return _encode_expr(convert(JuMP.GenericNonlinearExpr{V}, expr), h, refs, consts) +end +function _encode_expr(expr::JuMP.GenericNonlinearExpr, h::UInt, refs, consts) # TODO remove recursion + h = hash((expr.head, length(expr.args)), h) + for arg in expr.args + h, _, _ = _encode_expr(arg, h, refs, consts) + end + return h, refs, consts +end + +# Traverse expression in same order as _encode_expr and exafy it +function _exafy_grouped_expr( + ::Real, + vrefs::Vector{Any}, + consts::Vector{Any} + ) + return popfirst!(consts) +end +function _exafy_grouped_expr( + ::InfiniteOpt.GeneralVariableRef, + vrefs::Vector{Any}, + consts::Vector{Any} + ) + return popfirst!(vrefs) +end +function _exafy_grouped_expr( + expr::Union{JuMP.GenericAffExpr{C, V}, JuMP.GenericQuadExpr{C, V}}, + vrefs::Vector{Any}, + consts::Vector{Any} + ) where {C, V} + return _exafy_grouped_expr(convert(JuMP.GenericNonlinearExpr{V}, expr), vrefs, consts) +end +function _exafy_grouped_expr( + expr::JuMP.GenericNonlinearExpr, + vrefs::Vector{Any}, + consts::Vector{Any} + ) + return _nl_op(expr.head)((_exafy_grouped_expr(a, vrefs, consts) for a in expr.args)...) +end + +# Print a message about a group +function _group_info_msg(group, msg) + idxs = [JuMP.index(cref).value for cref in group] + @info "$msg constraint group with indices: $(idxs)" + return +end + +# Get the grouped index of a variable based on its direct exaified variable reference +function _get_grouped_idx(em_var::ExaModels.Var, grouped_var::ExaModels.Variable) + idx = em_var.i - grouped_var.offset + @assert 1 <= idx <= grouped_var.size[end] && length(grouped_var.size) == 1 + return idx +end +function _get_grouped_idx( + em_var::Union{ExaModels.Variable, ExaModels.Parameter}, + grouped_var::Union{ExaModels.Variable, ExaModels.Parameter} + ) + idx = (em_var.offset - grouped_var.offset) ÷ em_var.length + 1 + @assert 1 <= idx <= grouped_var.size[end] + return idx +end +function _get_grouped_idx(vref::InfiniteOpt.GeneralVariableRef, data::ExaMappingData) + if vref.index_type in (InfiniteOpt.SemiInfiniteVariableIndex, InfiniteOpt.PointVariableIndex) + vref = InfiniteOpt.infinite_variable_ref(vref) + end + em_var = data[vref] + grouped_var = data.var_to_grouped_var[vref] + return _get_grouped_idx(em_var, grouped_var) +end + +# Given a candidate group of constraint, seek to merge together and add as a single constraint pattern to `core` +function _process_candidate_constraint_group( + core::ExaModels.ExaCore, + data::ExaMappingData, + crefs::Vector{InfiniteOpt.InfOptConstraintRef}, + vref_lists::Vector{Vector{InfiniteOpt.GeneralVariableRef}}, + const_lists::Vector{Vector{Float64}}, + sets::Vector{_MOI.AbstractSet} + ) + # determine which vrefs and consts change across the array + vrefs1 = vref_lists[1] + is_grouped_var = [any(l -> l[i] != vrefs1[i], vref_lists) for i in eachindex(vrefs1)] + consts1 = const_lists[1] + is_grouped_data = [any(l -> l[i] != consts1[i], const_lists) for i in eachindex(consts1)] + # exafy the vrefs + exafied_vrefs = Vector{Any}(undef, length(vrefs1)) + var_itr = Any[(;) for _ in 1:length(crefs)] + group_var_idx = 1 + restricted_idx = 1 + for (i, vref) in enumerate(vrefs1) + if is_grouped_var[i] + if !haskey(data.var_to_grouped_var, vref) + _group_info_msg(crefs, "Failure: $(vref) is not a grouped variable/parameter which prevents adding") + return false, core + end + base_idxs = Tuple(_index_params(vref, data)) + itr_alias = Symbol("grouped_vidx$group_var_idx") + data_src = ExaModels.DataSource() + alias_map = Dict{Int, Symbol}() + var_idxs = (begin + if k > length(base_idxs) + data_src[itr_alias] + elseif base_idxs[k] isa Int # for restricted variables + alias_map[k] = Symbol("restricted_idx$restricted_idx") + restricted_idx += 1 + data_src[alias_map[k]] + else + base_idxs[k] + end + end for k in 1:length(base_idxs)+1) + src_var = data.var_to_grouped_var[vref] + exafied_vrefs[i] = src_var[var_idxs...] + for j in 1:length(crefs) + infvar = vref_lists[j][i] + if data.var_to_grouped_var[infvar] != src_var + _group_info_msg(crefs, "Failure: Unable to correctly set up variable grouping for $(infvar) which prevents adding") + return false, core + end + var_itr[j] = (; var_itr[j]..., itr_alias => _get_grouped_idx(infvar, data)) + if !isempty(alias_map) # add in restricted variables indices if they exist + ridxs = Tuple(_index_params(infvar, data)) + var_itr[j] = merge(var_itr[j], NamedTuple(alias => ridxs[k] for (k, alias) in alias_map)) + end + end + group_var_idx += 1 + else + exafied_vrefs[i] = _exafy(vref, data) + end + end + # exafy the consts + exafied_consts = Vector{Any}(undef, length(consts1)) + const_itr = Any[(;) for _ in 1:length(crefs)] + grouped_const_idx = 1 + for (i, c) in enumerate(consts1) + if is_grouped_data[i] + itr_alias = Symbol("grouped_const$grouped_const_idx") + exafied_consts[i] = ExaModels.DataSource()[itr_alias] + for j in 1:length(crefs) + const_itr[j] = (; const_itr[j]..., itr_alias => const_lists[j][i]) + end + grouped_const_idx += 1 + else + exafied_consts[i] = c + end + end + # build the ExaModels graph for the constraint pattern + raw_expr = JuMP.jump_function(JuMP.constraint_object(first(crefs))) + em_expr = _finalize_expr(_exafy_grouped_expr(raw_expr, exafied_vrefs, exafied_consts)) + # process the iterator + infinite_itr = _get_constraint_iterator(first(crefs), data) + finite_itr = [merge(var_itr[i], const_itr[i]) for i in 1:length(crefs)] + itr = vec([merge(i...) for i in Iterators.product(infinite_itr, finite_itr)]) + # add the constraints to the core + lbs = Vector{Float64}(undef, length(crefs)) + ubs = Vector{Float64}(undef, length(crefs)) + for (i, s) in enumerate(sets) + lbs[i], ubs[i] = _get_constr_bounds(s) + end + full_lbs = repeat(lbs, inner = length(infinite_itr)) + full_ubs = repeat(ubs, inner = length(infinite_itr)) + core, con = ExaModels.add_con(core, em_expr, itr, lcon = full_lbs, ucon = full_ubs) + # save the constraint mappings + inf_len = length(infinite_itr) + for (i, cref) in enumerate(crefs) + base_idx = (i - 1) * inf_len + 1 + sliced_itr = itr[base_idx:base_idx + inf_len - 1] + offset = con.offset + base_idx - 1 + data.constraint_mappings[cref] = ExaModels.Constraint(con.f, sliced_itr, offset, (inf_len,), nothing) + end + return true, core +end + +# Iterate over constraints in the InfiniteOpt model, group by algebraic pattern, and add to the ExaModels core +function _group_and_add_constraints( + core::ExaModels.ExaCore, + data::ExaMappingData, + inf_model::InfiniteOpt.InfiniteModel + ) + # set up dictionaries for tracking patterns + hash_to_patterns = Dict{UInt, Tuple{Vector{Vector{InfiniteOpt.GeneralVariableRef}}, Vector{Vector{Float64}}, Vector{_MOI.AbstractSet}}}() + hash_to_constrs = Dict{UInt, Vector{InfiniteOpt.InfOptConstraintRef}}() + # iterate over constraints and group by hashed algebraic pattern + for cref in JuMP.all_constraints(inf_model) + InfiniteOpt.is_variable_domain_constraint(cref) && continue + isempty(JuMP.owner_model(cref).constraints[JuMP.index(cref)].measure_indices) || continue # TODO: temporary restriction + expr = JuMP.jump_function(JuMP.constraint_object(cref)) + expr isa JuMP.AbstractJuMPScalar || continue + h, vrefs, consts = _encode_expr(expr) + if haskey(hash_to_patterns, h) + push!(hash_to_patterns[h][1], vrefs) + push!(hash_to_patterns[h][2], consts) + push!(hash_to_patterns[h][3], JuMP.moi_set(JuMP.constraint_object(cref))) + push!(hash_to_constrs[h], cref) + else + hash_to_patterns[h] = ([vrefs], [consts], [JuMP.moi_set(JuMP.constraint_object(cref))]) + hash_to_constrs[h] = [cref] + end + end + # process each grouped pattern (requiring at least 2 constraints to be grouped) + for (h, crefs) in hash_to_constrs + if length(crefs) < 2 + continue + end + success, core = _process_candidate_constraint_group(core, data, crefs, hash_to_patterns[h]...) + if success + _group_info_msg(crefs, "Successfully added") + end + end + return core +end \ No newline at end of file diff --git a/src/infiniteopt_backend.jl b/src/infiniteopt_backend.jl index 9cefd61..2dda117 100644 --- a/src/infiniteopt_backend.jl +++ b/src/infiniteopt_backend.jl @@ -31,6 +31,10 @@ struct ExaMappingData Vector{Any} } } + # Point variable info + point_indicies::Dict{InfiniteOpt.GeneralVariableRef, Tuple} + # Finite template constraint metadata + var_to_grouped_var::Dict{InfiniteOpt.GeneralVariableRef, Union{ExaModels.Variable, ExaModels.Parameter}} # Default constructor function ExaMappingData() @@ -52,6 +56,8 @@ struct ExaMappingData Vector{Any} } }(), + Dict{InfiniteOpt.GeneralVariableRef, Tuple}(), + Dict{InfiniteOpt.GeneralVariableRef, Union{ExaModels.Variable, ExaModels.Parameter}}(), ) end end @@ -109,7 +115,7 @@ mutable struct ExaTranscriptionBackend{B} <: InfiniteOpt.AbstractTransformationB end # Constructors -function ExaTranscriptionBackend(; backend = nothing) +function ExaTranscriptionBackend(; backend = nothing, ) return ExaTranscriptionBackend( nothing, nothing, diff --git a/src/transform.jl b/src/transform.jl index 6ccab59..8f80a48 100644 --- a/src/transform.jl +++ b/src/transform.jl @@ -107,14 +107,22 @@ end function _add_finite_variables( core::ExaModels.ExaCore, data::ExaMappingData, - inf_model::InfiniteOpt.InfiniteModel + inf_model::InfiniteOpt.InfiniteModel; + create_variable_groups::Bool = false ) - for vref in JuMP.all_variables(inf_model, InfiniteOpt.FiniteVariable) + vrefs = JuMP.all_variables(inf_model, InfiniteOpt.FiniteVariable) + if create_variable_groups && length(vrefs) > 1 + ex_var = ExaModels.Variable((length(vrefs),), length(vrefs), core.nvar, :x, nothing) + end + for vref in vrefs info = InfiniteOpt.core_object(vref).info # JuMP.VariableInfo _ensure_continuous(info) lb, ub, start = _get_variable_bounds_and_start(info) core, new_var = ExaModels.add_var(core, 1, start = start, lvar = lb, uvar = ub) data.finvar_mappings[vref] = new_var[1] + if create_variable_groups + data.var_to_grouped_var[vref] = ex_var + end end return core end @@ -123,12 +131,20 @@ end function _add_finite_parameters( core::ExaModels.ExaCore, data::ExaMappingData, - inf_model::InfiniteOpt.InfiniteModel + inf_model::InfiniteOpt.InfiniteModel; + create_parameter_groups::Bool = false ) - for pref in JuMP.all_variables(inf_model, InfiniteOpt.FiniteParameter) + prefs = JuMP.all_variables(inf_model, InfiniteOpt.FiniteParameter) + if create_parameter_groups && length(prefs) > 1 + ex_par = ExaModels.Parameter((length(prefs),), length(prefs), core.npar, nothing) + end + for pref in prefs param_val = InfiniteOpt.parameter_value(pref) core, new_par = ExaModels.add_par(core, [param_val]) data.param_mappings[pref] = new_par + if create_parameter_groups + data.var_to_grouped_var[pref] = ex_par + end end return core end @@ -137,14 +153,28 @@ end function _add_infinite_variables( core::ExaModels.ExaCore, data::ExaMappingData, - inf_model::InfiniteOpt.InfiniteModel + inf_model::InfiniteOpt.InfiniteModel; + create_variable_groups::Bool = false ) # Get the raw variables ivrefs = JuMP.all_variables(inf_model, InfiniteOpt.InfiniteVariable) InfiniteOpt.reformulate_high_order_derivatives!(inf_model) drefs = InfiniteOpt.all_derivatives(inf_model) + vrefs = append!(ivrefs, drefs) + # sort by infinite parameter groups (required to capture repeated finite patterns) + if create_variable_groups && length(data.base_itrs) > 1 + all_group_idxs = map(v -> InfiniteOpt.parameter_group_int_indices(v), vrefs) + sorted_int_idxs = sortperm(all_group_idxs) + permute!(vrefs, sorted_int_idxs) + permute!(all_group_idxs, sorted_int_idxs) + group_types = unique(all_group_idxs) + vref_groups = Dict(t => InfiniteOpt.GeneralVariableRef[] for t in group_types) + for (vref, group_idx) in zip(vrefs, all_group_idxs) + push!(vref_groups[group_idx], vref) + end + end # now process and add each infinite variable - for vref in append!(ivrefs, drefs) + for vref in vrefs # retrieve basic information info = InfiniteOpt.core_object(vref).info # JuMP.VariableInfo _ensure_continuous(info) @@ -157,6 +187,25 @@ function _add_infinite_variables( core, new_var = ExaModels.add_var(core, dims...; start = start, lvar = lb, uvar = ub) data.infvar_mappings[vref] = new_var end + # process and store a grouped variable for each group of variables with common infinite parameter groups + if create_variable_groups && length(data.base_itrs) > 1 + for group_idx in group_types + group_vrefs = vref_groups[group_idx] + v1 = data[first(group_vrefs)] + num_vars = v1.length * length(group_vrefs) + ex_var = ExaModels.Variable((v1.size..., length(group_vrefs)), num_vars, v1.offset, :x, nothing) + for vref in group_vrefs + data.var_to_grouped_var[vref] = ex_var + end + end + elseif create_variable_groups + v1 = data[first(vrefs)] + num_vars = v1.length * length(vrefs) + ex_var = ExaModels.Variable((v1.size..., length(vrefs)), num_vars, v1.offset, :x, nothing) + for vref in vrefs + data.var_to_grouped_var[vref] = ex_var + end + end return core end @@ -164,9 +213,25 @@ end function _add_parameter_functions( core::ExaModels.ExaCore, data::ExaMappingData, - inf_model::InfiniteOpt.InfiniteModel + inf_model::InfiniteOpt.InfiniteModel; + create_parameter_groups::Bool = false ) - for pfref in InfiniteOpt.all_parameter_functions(inf_model) + pfrefs = InfiniteOpt.all_parameter_functions(inf_model) + length(pfrefs) == 0 && return core + # sort by infinite parameter groups (required to capture repeated finite patterns) + if create_parameter_groups && length(data.base_itrs) > 1 + all_group_idxs = map(v -> InfiniteOpt.parameter_group_int_indices(v), pfrefs) + sorted_int_idxs = sortperm(all_group_idxs) + permute!(pfrefs, sorted_int_idxs) + permute!(all_group_idxs, sorted_int_idxs) + group_types = unique(all_group_idxs) + pfref_groups = Dict(t => InfiniteOpt.GeneralVariableRef[] for t in group_types) + for (pfref, group_idx) in zip(pfrefs, all_group_idxs) + push!(pfref_groups[group_idx], pfref) + end + end + # iterate and add each parameter function + for pfref in pfrefs # gather the basic information group_idxs = InfiniteOpt.parameter_group_int_indices(pfref) pfunc = InfiniteOpt.core_object(pfref) @@ -182,6 +247,25 @@ function _add_parameter_functions( core, new_par = ExaModels.add_par(core, vals) data.param_mappings[pfref] = new_par end + # process and store a grouped variable for each group of variables with common infinite parameter groups + if create_parameter_groups && length(data.base_itrs) > 1 + for group_idx in group_types + group_pfrefs = pfref_groups[group_idx] + pf1 = data[first(group_pfrefs)] + num_pars = pf1.length * length(group_pfrefs) + ex_var = ExaModels.Parameter((pf1.size..., length(group_pfrefs)), num_pars, pf1.offset, nothing) + for pfref in group_pfrefs + data.var_to_grouped_var[pfref] = ex_var + end + end + elseif create_parameter_groups + pf1 = data[first(pfrefs)] + num_pars = pf1.length * length(pfrefs) + ex_par = ExaModels.Parameter((pf1.size..., length(pfrefs)), num_pars, pf1.offset, nothing) + for pfref in pfrefs + data.var_to_grouped_var[pfref] = ex_par + end + end return core end @@ -212,6 +296,9 @@ function _process_semi_infinite_var(vref, data) else mapped_var = data.infvar_mappings[ivref] end + if haskey(data.var_to_grouped_var, ivref) + data.var_to_grouped_var[vref] = data.var_to_grouped_var[ivref] + end return data.semivar_info[vref] = (mapped_var, indexing) end @@ -269,7 +356,12 @@ function _process_point_var(vref, data) end group_idxs = InfiniteOpt.parameter_group_int_indices(ivref) idxs = Tuple(data.support_to_index[i, s] for (i, s) in zip(group_idxs, supp)) - return data.infvar_mappings[ivref][idxs...] + pt = data.infvar_mappings[ivref][idxs...] + if haskey(data.var_to_grouped_var, ivref) + data.var_to_grouped_var[vref] = data.var_to_grouped_var[ivref] + end + data.point_indicies[vref] = idxs + return data.finvar_mappings[vref] = pt end # Add all the point variables from an InfiniteModel to a ExaCore @@ -281,7 +373,6 @@ function _add_point_variables( for vref in JuMP.all_variables(inf_model, InfiniteOpt.PointVariable) # store the index mapping for the point variable pt = _process_point_var(vref, data) - data.finvar_mappings[vref] = pt # update the bounds and start value if needed info = InfiniteOpt.core_object(vref).info # InfiniteOpt.RestrictedDomainInfo _update_bounds_and_start(core, info, pt) @@ -289,6 +380,52 @@ function _add_point_variables( return end +# Get the index parameters for a variable reference (used by `_map_variable`) +function _index_params( + vref::InfiniteOpt.GeneralVariableRef, + data::ExaMappingData + ) + _index_params(vref, vref.index_type, data) +end +function _index_params( + vref::InfiniteOpt.GeneralVariableRef, + ::Type{V}, + data::ExaMappingData + ) where V <: Union{InfiniteOpt.InfiniteVariableIndex, InfiniteOpt.DerivativeIndex, InfiniteOpt.ParameterFunctionIndex} + group_idxs = InfiniteOpt.parameter_group_int_indices(vref) + data_src = ExaModels.DataSource() + return (data_src[data.group_alias[i]] for i in group_idxs) +end +function _index_params( + vref::InfiniteOpt.GeneralVariableRef, + ::Type{InfiniteOpt.SemiInfiniteVariableIndex}, + data::ExaMappingData + ) + if !haskey(data.semivar_info, vref) + _process_semi_infinite_var(vref, data) + end + _, inds = data.semivar_info[vref] + data_src = ExaModels.DataSource() + return (i isa Int ? i : data_src[i] for i in inds) +end +function _index_params( + vref::InfiniteOpt.GeneralVariableRef, + ::Type{InfiniteOpt.PointVariableIndex}, + data::ExaMappingData + ) + if !haskey(data.finvar_mappings, vref) + _process_point_var(vref, data) + end + return data.point_indicies[vref] +end +function _index_params( + vref::InfiniteOpt.GeneralVariableRef, + type, + data::ExaMappingData + ) + return () +end + # Add user-defined operators to ExaModels function _add_user_operators(inf_model::InfiniteOpt.InfiniteModel) for op in InfiniteOpt.added_nonlinear_operators(inf_model) @@ -310,68 +447,59 @@ function _add_user_operators(inf_model::InfiniteOpt.InfiniteModel) end # Map variable references based on their underlying type (used by `_exafy`) -function _map_variable(vref, ::Type{InfiniteOpt.FiniteVariableIndex}, data_src, data) +function _map_variable(vref, ::Type{InfiniteOpt.FiniteVariableIndex}, data) return data.finvar_mappings[vref] end -function _map_variable(vref, ::Type{InfiniteOpt.PointVariableIndex}, data_src, data) +function _map_variable(vref, ::Type{InfiniteOpt.PointVariableIndex}, data) if haskey(data.finvar_mappings, vref) return data.finvar_mappings[vref] else - var = _process_point_var(vref, data) - data.finvar_mappings[vref] = var - return var + return _process_point_var(vref, data) end end function _map_variable( vref, ::Type{V}, - data_src, data ) where V <: Union{InfiniteOpt.InfiniteVariableIndex, InfiniteOpt.DerivativeIndex} - group_idxs = InfiniteOpt.parameter_group_int_indices(vref) - idx_pars = (data_src[data.group_alias[i]] for i in group_idxs) + idx_pars = _index_params(vref, V, data) return data.infvar_mappings[vref][idx_pars...] end -function _map_variable(vref, ::Type{InfiniteOpt.SemiInfiniteVariableIndex}, data_src, data) - if !haskey(data.semivar_info, vref) - _process_semi_infinite_var(vref, data) - end - ivar, inds = data.semivar_info[vref] - idx_pars = (i isa Int ? i : data_src[i] for i in inds) +function _map_variable(vref, ::Type{InfiniteOpt.SemiInfiniteVariableIndex}, data) + idx_pars = _index_params(vref, InfiniteOpt.SemiInfiniteVariableIndex, data) + ivar, _ = data.semivar_info[vref] return ivar[idx_pars...] end -function _map_variable(vref, ::Type{<:InfiniteOpt.InfiniteParameterIndex}, data_src, data) - return data_src[data.param_alias[vref]] +function _map_variable(vref, ::Type{<:InfiniteOpt.InfiniteParameterIndex}, data) + return ExaModels.DataSource()[data.param_alias[vref]] end -function _map_variable(vref, ::Type{InfiniteOpt.FiniteParameterIndex}, data_src, data) +function _map_variable(vref, ::Type{InfiniteOpt.FiniteParameterIndex}, data) return data.param_mappings[vref][1] end -function _map_variable(vref, ::Type{InfiniteOpt.ParameterFunctionIndex}, data_src, data) - group_idxs = InfiniteOpt.parameter_group_int_indices(vref) - idx_pars = (data_src[data.group_alias[i]] for i in group_idxs) +function _map_variable(vref, ::Type{InfiniteOpt.ParameterFunctionIndex}, data) + idx_pars = _index_params(vref, InfiniteOpt.ParameterFunctionIndex, data) return data.param_mappings[vref][idx_pars...] end -function _map_variable(vref, IdxType, data_src, data) +function _map_variable(vref, IdxType, data) error("Unable to add `$vref` to an ExaModel, it's index type `$IdxType`" * " is not yet supported by InfiniteExaModels.") end -# Convert as InfiniteOpt expression into a ExaModel expression using the DataIndexed `data_src` -function _exafy(vref::InfiniteOpt.GeneralVariableRef, data_src, data) - return _map_variable(vref, vref.index_type, data_src, data) +# Convert as InfiniteOpt expression into a ExaModel expression +function _exafy(vref::InfiniteOpt.GeneralVariableRef, data) + return _map_variable(vref, vref.index_type, data) end -function _exafy(c::Number, data_src, data) +function _exafy(c::Number, data) return c end function _exafy( - aff::JuMP.GenericAffExpr{C, InfiniteOpt.GeneralVariableRef}, - data_src, + aff::JuMP.GenericAffExpr{C, InfiniteOpt.GeneralVariableRef}, data ) where {C} c = JuMP.constant(aff) if !isempty(aff.terms) ex = sum(begin - v_ex = _exafy(v, data_src, data) + v_ex = _exafy(v, data) isone(c) ? v_ex : c * v_ex end for (c, v) in JuMP.linear_terms(aff) ) @@ -381,19 +509,18 @@ function _exafy( end end function _exafy( - quad::JuMP.GenericQuadExpr{C, InfiniteOpt.GeneralVariableRef}, - data_src, + quad::JuMP.GenericQuadExpr{C, InfiniteOpt.GeneralVariableRef}, data ) where {C} - aff = _exafy(quad.aff, data_src, data) + aff = _exafy(quad.aff, data) if !isempty(quad.terms) ex = sum(begin if v1 == v2 - v_ex = _exafy(v1, data_src, data) + v_ex = _exafy(v1, data) isone(c) ? abs2(v_ex) : c * abs2(v_ex) else - v1_ex = _exafy(v1, data_src, data) - v2_ex = _exafy(v2, data_src, data) + v1_ex = _exafy(v1, data) + v2_ex = _exafy(v2, data) isone(c) ? v1_ex * v2_ex : c * v1_ex * v2_ex end end for (c, v1, v2) in JuMP.quad_terms(quad) @@ -405,10 +532,35 @@ function _exafy( end function _exafy( nl::JuMP.GenericNonlinearExpr{InfiniteOpt.GeneralVariableRef}, - data_src, data ) - return _nl_op(nl.head)((_exafy(a, data_src, data) for a in nl.args)...) + return _nl_op(nl.head)((_exafy(a, data) for a in nl.args)...) +end + +# Check if NamedTuple iterator respects the restriction +function _support_in_restriction(restriction, itr, data) + supp = [itr[data.param_alias[p]] for p in restriction.parameter_refs] + return restriction(supp) +end + +# Prepare the constraint iterator over the infinite parameters +function _get_constraint_iterator(cref, data) + group_idxs = InfiniteOpt.parameter_group_int_indices(cref) + # prepare the iterator of NamedTuples (contains support values, iterator values, and constants from parameter functions) + if isempty(group_idxs) # we have a finite constraint + itr = [(;)] + elseif length(group_idxs) == 1 # we only depend on one independent infinite parameter + itr = data.base_itrs[first(group_idxs)] + else # we depend on multiple independent infinite parameters + itrs = map(i -> data.base_itrs[i], group_idxs) + itr = vec([merge(i...) for i in Iterators.product(itrs...)]) + end + # Remove any elements of the iterator that violate the domain restriction + if InfiniteOpt.has_domain_restriction(cref) + restriction = InfiniteOpt.domain_restriction(cref) + itr = filter(i -> _support_in_restriction(restriction, i, data), itr) + end + return itr end # Finalize exafied expressions to avoid scalars @@ -433,12 +585,6 @@ function _get_constr_bounds(set) "if you need support for this constraint type, please open an issue.") end -# Check if NamedTuple iterator respects the restriction -function _support_in_restriction(restriction, itr, data) - supp = [itr[data.param_alias[p]] for p in restriction.parameter_refs] - return restriction(supp) -end - # Add all the constraints from an InfiniteModel to an ExaCore function _add_constraints( core::ExaModels.ExaCore, @@ -446,8 +592,9 @@ function _add_constraints( inf_model::InfiniteOpt.InfiniteModel ) for cref in JuMP.all_constraints(inf_model) - # skip if the constraint is a variable bound or type + # skip if the constraint is a variable bound or already added (as a grouped constraint) InfiniteOpt.is_variable_domain_constraint(cref) && continue + haskey(data.constraint_mappings, cref) && continue # parse the basic information constr = JuMP.constraint_object(cref) if isempty(inf_model.constraints[JuMP.index(cref)].measure_indices) @@ -457,24 +604,10 @@ function _add_constraints( expr = InfiniteOpt.expand_measures(JuMP.jump_function(constr), inf_model) end set = JuMP.moi_set(constr) - group_idxs = InfiniteOpt.parameter_group_int_indices(cref) - # prepare the iterator of NamedTuples (contains support values, iterator values, and constants from parameter functions) - if isempty(group_idxs) # we have a finite constraint - itr = [(;)] - elseif length(group_idxs) == 1 # we only depend on one independent infinite parameter - itr = data.base_itrs[first(group_idxs)] - else # we depend on multiple independent infinite parameters - itrs = map(i -> data.base_itrs[i], group_idxs) - itr = vec([merge(i...) for i in Iterators.product(itrs...)]) - end - # Remove any elements of the iterator that violate the domain restriction - if InfiniteOpt.has_domain_restriction(cref) - restriction = InfiniteOpt.domain_restriction(cref) - itr = filter(i -> _support_in_restriction(restriction, i, data), itr) - end + # prepare the constraint iterator + itr = _get_constraint_iterator(cref, data) # create the ExaModels expression tree based on expr - data_src = ExaModels.DataSource() - em_expr = _finalize_expr(_exafy(expr, data_src, data)) + em_expr = _finalize_expr(_exafy(expr, data)) # get the constraint bounds lb, ub = _get_constr_bounds(set) # create the ExaModels constraint @@ -487,7 +620,8 @@ end # Make dispatch type to pass the data needed by `make_reduced_expr` struct _DerivReductionBackendInfo <: InfiniteOpt.AbstractTransformationBackend data::ExaMappingData - data_src::ExaModels.DataSource + group_constraints::Bool + alias_map::Union{Nothing, Dict{InfiniteOpt.GeneralVariableRef, Symbol}} end # Extend make_reduced_expr to create an ExaModel expression @@ -500,7 +634,7 @@ function InfiniteOpt.make_reduced_expr( ) group_idx = InfiniteOpt.parameter_group_int_index(pref) data = dispatch_data.data - data_src = dispatch_data.data_src + data_src = ExaModels.DataSource() alias = data.group_alias[group_idx] if vref.index_type == InfiniteOpt.SemiInfiniteVariableIndex @assert haskey(data.semivar_info, vref) @@ -514,7 +648,12 @@ function InfiniteOpt.make_reduced_expr( data_src[i] end end for i in inds) - return ivar[idx_pars...] + if dispatch_data.group_constraints + grouped_var = data.var_to_grouped_var[vref] + return grouped_var[idx_pars..., data_src[dispatch_data.alias_map[vref]]] + else + return ivar[idx_pars...] + end else # either an infinite variable or a derivative variable group_idxs = InfiniteOpt.parameter_group_int_indices(vref) idx_pars = (begin @@ -525,7 +664,12 @@ function InfiniteOpt.make_reduced_expr( data_src[g_alias] end end for i in group_idxs) - return data.infvar_mappings[vref][idx_pars...] + if dispatch_data.group_constraints + grouped_var = data.var_to_grouped_var[vref] + return grouped_var[idx_pars..., data_src[dispatch_data.alias_map[vref]]] + else + return data.infvar_mappings[vref][idx_pars...] + end end return end @@ -534,16 +678,30 @@ end function _add_derivative_approximations( core::ExaModels.ExaCore, data::ExaMappingData, - inf_model::InfiniteOpt.InfiniteModel + inf_model::InfiniteOpt.InfiniteModel; + group_constraints::Bool = false ) + # group all the derivatives of the same order, method, and infinite parameter dependencies + signature_to_derivs = Dict{ + Tuple{InfiniteOpt.GeneralVariableRef, Int, Vector{Int}, DataType}, + Tuple{Vector{InfiniteOpt.GeneralVariableRef}, Vector{InfiniteOpt.GeneralVariableRef}} + }() for dref in InfiniteOpt.all_derivatives(inf_model) - # gather the derivative information vref = InfiniteOpt.derivative_argument(dref) pref = InfiniteOpt.operator_parameter(dref) order = InfiniteOpt.derivative_order(dref) - method = InfiniteOpt.derivative_method(dref) - # gather the needed infinite parameter data group_idxs = InfiniteOpt.parameter_group_int_indices(vref) + if !haskey(signature_to_derivs, (pref, order, group_idxs, vref.index_type)) + signature_to_derivs[pref, order, group_idxs, vref.index_type] = + (InfiniteOpt.GeneralVariableRef[], InfiniteOpt.GeneralVariableRef[]) + end + push!(signature_to_derivs[pref, order, group_idxs, vref.index_type][1], dref) + push!(signature_to_derivs[pref, order, group_idxs, vref.index_type][2], vref) + end + # iterate over each group of derivatives and add the approximation equations + for ((pref, order, group_idxs, _), (drefs, vrefs)) in signature_to_derivs + # gather basic info + method = InfiniteOpt.derivative_method(drefs[1]) pref_group = InfiniteOpt.parameter_group_int_index(pref) # sort the base support iterator p_alias = data.param_alias[pref] @@ -555,31 +713,52 @@ function _add_derivative_approximations( end # collect the expression data supps = map(p -> p[p_alias], srt_itr) - idxs, arg_itrs... = InfiniteOpt.derivative_expr_data(dref, order, supps, method) + idxs, arg_itrs... = InfiniteOpt.derivative_expr_data(drefs[1], order, supps, method) # make the iterator aliases = Tuple(Symbol("d_arg$i") for i in eachindex(arg_itrs)) pref_itr = [(; srt_itr[i]..., zip(aliases, args)...) for (i, args...) in zip(idxs, arg_itrs...)] - if length(group_idxs) > 1 - itrs = [g == pref_group ? pref_itr : data.base_itrs[g] for g in group_idxs] - itr = [merge(i...) for i in Iterators.product(itrs...)] - else - itr = pref_itr + itrs = Any[g == pref_group ? pref_itr : data.base_itrs[g] for g in group_idxs] + if group_constraints + push!( + itrs, + [(; :grouped_didx => _get_grouped_idx(dref, data), + :grouped_vidx => _get_grouped_idx(vref, data)) + for (dref, vref) in zip(drefs, vrefs) + ]) end - # make the ExaModel expression tree + itr = length(itrs) > 1 ? vec([merge(i...) for i in Iterators.product(itrs...)]) : pref_itr + # make the ExaModel expression tree and add the constraint(s) data_src = ExaModels.DataSource() - em_expr = InfiniteOpt.make_indexed_derivative_expr( - dref, - vref, - pref, - order, - data_src[data.group_alias[pref_group]], - supps, - _DerivReductionBackendInfo(data, data_src), - method, - (data_src[a] for a in aliases)... + if group_constraints + alias_map = Dict(drefs[1] => :grouped_didx, vrefs[1] => :grouped_vidx) + em_expr = InfiniteOpt.make_indexed_derivative_expr( + drefs[1], + vrefs[1], + pref, + order, + data_src[data.group_alias[pref_group]], + supps, + _DerivReductionBackendInfo(data, group_constraints, alias_map), + method, + (data_src[a] for a in aliases)... ) - # add the constraint - core, _ = ExaModels.add_con(core, em_expr, itr) + core, _ = ExaModels.add_con(core, em_expr, itr) + else + for (dref, vref) in zip(drefs, vrefs) + em_expr = InfiniteOpt.make_indexed_derivative_expr( + dref, + vref, + pref, + order, + data_src[data.group_alias[pref_group]], + supps, + _DerivReductionBackendInfo(data, group_constraints, nothing), + method, + (data_src[a] for a in aliases)... + ) + core, _ = ExaModels.add_con(core, em_expr, itr) + end + end end return core end @@ -588,7 +767,8 @@ end function _add_collocation_restrictions( core::ExaModels.ExaCore, data::ExaMappingData, - inf_model::InfiniteOpt.InfiniteModel + inf_model::InfiniteOpt.InfiniteModel; + group_constraints::Bool = false ) for (pidx, vidxs) in inf_model.piecewise_vars # gather the basic information @@ -605,19 +785,45 @@ function _add_collocation_restrictions( ubs = repeat(2+num_nodes:num_nodes+1:num_supps, inner = num_nodes) pts = filter(i -> !(i in ubs), 2:num_supps-1) pref_itr = [(i1 = ub, i2 = pt) for (ub, pt) in zip(ubs, pts)] - # make the constraints for each infinite variable + # group the variables by their input infinite parameters + group_idxs_to_vrefs = Dict{Vector{Int}, Vector{InfiniteOpt.GeneralVariableRef}}() for vidx in vidxs vref = InfiniteOpt.GeneralVariableRef(inf_model, vidx) group_idxs = InfiniteOpt.parameter_group_int_indices(vref) + if !haskey(group_idxs_to_vrefs, group_idxs) + group_idxs_to_vrefs[group_idxs] = InfiniteOpt.GeneralVariableRef[] + end + push!(group_idxs_to_vrefs[group_idxs], vref) + end + # add the constraints for each group of variables + for (group_idxs, vrefs) in group_idxs_to_vrefs + # prepare the iterator aliases = (data.group_alias[g] for g in group_idxs) itrs = (g == pref_group ? pref_itr : data.base_itrs[g] for g in group_idxs) - itr = vec([merge(i...) for i in Iterators.product(itrs...)]) + if group_constraints + finite_itr = [(; :grouped_vidx => _get_grouped_idx(vref, data)) for vref in vrefs] + itr = vec([merge(i...) for i in Iterators.product(itrs..., finite_itr)]) + else + itr = vec([merge(i...) for i in Iterators.product(itrs...)]) + end + # prepare the variable indices data_src = ExaModels.DataSource() - idx_pars1 = (a == pref_alias ? data_src[:i1] : data_src[a] for a in aliases) - idx_pars2 = (a == pref_alias ? data_src[:i2] : data_src[a] for a in aliases) - ivar = data.infvar_mappings[vref] - em_expr = ivar[idx_pars1...] - ivar[idx_pars2...] - core, _ = ExaModels.add_con(core, em_expr, itr) + alias_tuple = group_constraints ? (aliases..., :grouped_vidx) : Tuple(aliases) + idx_pars1 = (a == pref_alias ? data_src[:i1] : data_src[a] for a in alias_tuple) + idx_pars2 = (a == pref_alias ? data_src[:i2] : data_src[a] for a in alias_tuple) + # create the ExaModel expression tree and add the constraint + if group_constraints + grouped_var = data.var_to_grouped_var[vrefs[1]] + em_expr = grouped_var[idx_pars1...] - grouped_var[idx_pars2...] + core, _ = ExaModels.add_con(core, em_expr, itr) + else + for vref in vrefs + ivar = data.infvar_mappings[vref] + em_expr = ivar[idx_pars1...] - ivar[idx_pars2...] + core, _ = ExaModels.add_con(core, em_expr, itr) + end + end + end end return core @@ -633,7 +839,7 @@ const _ObjMeasureExpansionWarn = string( # Write a finite expression `expr` in a single objective term (this is a generic fallback) function _add_generic_objective_term(core, expr, data) - em_expr = _finalize_expr(_exafy(expr, (;), data)) + em_expr = _finalize_expr(_exafy(expr, data)) return ExaModels.add_obj(core, em_expr, [(;)])[1] end @@ -718,7 +924,7 @@ function _add_objective_aff_term(core, coef, vref, ::Type{InfiniteOpt.MeasureInd mexpr, itr = _process_measure_sum(vref, data) # prepare the examodel expression tree data_src = ExaModels.DataSource() - em_expr = data_src.c * _exafy(coef * mexpr, data_src, data) + em_expr = data_src.c * _exafy(coef * mexpr, data) # add the term to the objective core, _ = ExaModels.add_obj(core, _finalize_expr(em_expr), itr) return core @@ -794,23 +1000,57 @@ end function build_exa_core!( core::ExaModels.ExaCore, data::ExaMappingData, - inf_model::InfiniteOpt.InfiniteModel + inf_model::InfiniteOpt.InfiniteModel; + group_repeated_constraint_patterns = false ) # initial setup _build_base_iterators(data, inf_model) # add the variables and appropriate mappings - core = _add_finite_parameters(core, data, inf_model) - core = _add_finite_variables(core, data, inf_model) - core = _add_infinite_variables(core, data, inf_model) # includes derivatives - core = _add_parameter_functions(core, data, inf_model) + core = _add_finite_parameters( + core, + data, + inf_model, + create_parameter_groups = group_repeated_constraint_patterns + ) + core = _add_finite_variables( + core, + data, + inf_model, + create_variable_groups = group_repeated_constraint_patterns + ) + core = _add_infinite_variables( # includes derivatives + core, + data, + inf_model, + create_variable_groups = group_repeated_constraint_patterns + ) + core = _add_parameter_functions( + core, + data, + inf_model, + create_parameter_groups = group_repeated_constraint_patterns + ) _add_semi_infinite_variables(core, data, inf_model) _add_point_variables(core, data, inf_model) # account for user-defined nonlinear operators _add_user_operators(inf_model) # add the constraints + if group_repeated_constraint_patterns + core = _group_and_add_constraints(core, data, inf_model) # TODO: can eventually replace `_add_constraints` if it works well + end core = _add_constraints(core, data, inf_model) - core = _add_derivative_approximations(core, data, inf_model) - core = _add_collocation_restrictions(core, data, inf_model) + core = _add_derivative_approximations( + core, + data, + inf_model, + group_constraints = group_repeated_constraint_patterns + ) + core = _add_collocation_restrictions( + core, + data, + inf_model, + group_constraints = group_repeated_constraint_patterns + ) # add the objective if there is one expr = JuMP.objective_function(inf_model) sense = JuMP.objective_sense(inf_model) @@ -825,24 +1065,35 @@ end inf_model::InfiniteOpt.InfiniteModel, data::ExaMappingData; [backend = nothing, - concrete_core::Bool = false] + concrete_core::Bool = false, + group_repeated_constraint_patterns = false] # experimental )::ExaModels.ExaCore Create `ExaModels.ExaCore` from `inf_model` using the provided -`ExaMappingData` to store the variable and constraint mappings. +`ExaMappingData` to store the variable and constraint mappings. The setting `concrete_core = true` will create a concrete `ExaModels.ExaCore` type, which is useful for performance in some cases. +Optionally, try to aggregate common algebraic constraint structures +over a named constraint array (e.g., `@constraint(model, con[i=1:N], ...)`) +by setting `group_repeated_constraint_patterns = true`. This is an +experimental feature that may encounter issues and may be removed/modified in the future. """ function ExaModels.ExaCore( inf_model::InfiniteOpt.InfiniteModel, data::ExaMappingData; backend = nothing, - concrete_core::Bool = false + concrete_core::Bool = false, + group_repeated_constraint_patterns = false ) # TODO add support for other float types once InfiniteOpt does minimize = JuMP.objective_sense(inf_model) == _MOI.MIN_SENSE core = ExaModels.ExaCore(; backend = backend, minimize = minimize, concrete = Val(concrete_core)) - return build_exa_core!(core, data, inf_model) + return build_exa_core!( + core, + data, + inf_model; group_repeated_constraint_patterns = group_repeated_constraint_patterns + ) + concrete_core::Bool = false end """ @@ -850,23 +1101,44 @@ end inf_model::InfiniteOpt.InfiniteModel, [data::ExaMappingData]; [backend = nothing, - concrete_core::Bool = false] + concrete_core::Bool = false, + group_repeated_constraint_patterns = false] # experimental )::ExaModels.ExaModel Create an `ExaModels.ExaModel` from `inf_model` and store the mappings in `data`. If `data` is not provided, the mappings cannot be readily extracted. The `concrete_core` setting will create a concrete `ExaModels.ExaCore` type, which is useful for performance in some cases. +Optionally, try to aggregate common algebraic constraint structures +over a named constraint array (e.g., `@constraint(model, con[i=1:N], ...)`) +by setting `group_repeated_constraint_patterns = true`. This is an +experimental feature that may encounter issues and may be removed/modified in the future. """ function ExaModels.ExaModel( inf_model::InfiniteOpt.InfiniteModel, data::ExaMappingData; backend = nothing, - concrete_core::Bool = false + concrete_core::Bool = false, + group_repeated_constraint_patterns = false + ) + core = ExaModels.ExaCore( + inf_model, + data; + backend = backend, + concrete_core = concrete_core, + group_repeated_constraint_patterns = group_repeated_constraint_patterns ) - core = ExaModels.ExaCore(inf_model, data; backend = backend, concrete_core = concrete_core) return ExaModels.ExaModel(core) end -function ExaModels.ExaModel(inf_model::InfiniteOpt.InfiniteModel; backend = nothing) - return ExaModels.ExaModel(inf_model, ExaMappingData(), backend = backend) +function ExaModels.ExaModel( + inf_model::InfiniteOpt.InfiniteModel; + backend = nothing, + group_repeated_constraint_patterns = false +) + return ExaModels.ExaModel( + inf_model, + ExaMappingData(); + backend = backend, + group_repeated_constraint_patterns = group_repeated_constraint_patterns + ) end diff --git a/test/solve.jl b/test/solve.jl index 20e6b59..b089972 100644 --- a/test/solve.jl +++ b/test/solve.jl @@ -16,7 +16,7 @@ tol = 1E-6 yval = value(y) zval = value(z) dyval = value(∂(y, t)) - # test with ExaTranscriptionBackend + # test with ExaTranscriptionBackend @test set_transformation_backend(m, ExaTranscriptionBackend(IpoptSolver)) isa Nothing @test set_silent(m) isa Nothing @test optimize!(m).status == :first_order @@ -92,6 +92,31 @@ end end end +@testset "Dynamic Repeated Constraint Patterns" begin + m = InfiniteModel(Ipopt.Optimizer) + @infinite_parameter(m, t in [0, 1], num_supports = 11, derivative_method = OrthogonalCollocation(3)) + @variable(m, x[1:3], Infinite(t)) + @variable(m, u, Infinite(t)) + @objective(m, Min, ∫(sum((x[i] - 1)^2 for i in 1:3), t)) + @constraint(m, c1[i in 1:3], ∂(x[i], t) == u^(i+2)) + @constraint(m, c2[i in 1:3], x[i] <= i) + @constraint(m, c3[i in 1:3], x[i](0) == 0.1*i) + constant_over_collocation(u, t) + set_silent(m) + optimize!(m) + obj = objective_value(m) + xval = value.(x) + uval = value(u) + @test set_transformation_backend(m, ExaTranscriptionBackend(IpoptSolver)) isa Nothing + @test set_silent(m) isa Nothing + @test optimize!(m, group_repeated_constraint_patterns = true).status == :first_order + @test isapprox(obj, objective_value(m), atol = tol) + for i in 1:3 + @test all(isapprox.(xval[i], value(x[i]), atol = tol)) + end + @test all(isapprox.(uval, value(u), atol = tol)) +end + @testset "User-Defined Operators" begin model = InfiniteModel(Ipopt.Optimizer) set_silent(model) diff --git a/test/transcription.jl b/test/transcription.jl index 7407ccf..664fc52 100644 --- a/test/transcription.jl +++ b/test/transcription.jl @@ -117,9 +117,9 @@ end @test transformation_variable(x, exaBackend) == xMapping @test transformation_variable(y[1], exaBackend) == y1Mapping @test transformation_variable(y[2], exaBackend) == y2Mapping - @test InfiniteExaModels._map_variable(x, x.index_type, 0, exaData) isa ExaModels.ParameterNode{Int} - @test InfiniteExaModels._map_variable(y[1], y[1].index_type, 0, exaData) isa ExaModels.ParameterNode{Int} - @test InfiniteExaModels._map_variable(y[2], y[2].index_type, 0, exaData) isa ExaModels.ParameterNode{Int} + @test InfiniteExaModels._map_variable(x, x.index_type, exaData) isa ExaModels.ParameterNode{Int} + @test InfiniteExaModels._map_variable(y[1], y[1].index_type, exaData) isa ExaModels.ParameterNode{Int} + @test InfiniteExaModels._map_variable(y[2], y[2].index_type, exaData) isa ExaModels.ParameterNode{Int} @test length(exaModel.θ) == 3 @test exaModel.θ[xMapping.offset + 1] == 42 @test exaModel.θ[y1Mapping.offset + 1] == yVals[1]