ModelingToolkit: Indexing a parameter by another parameter in an ODEProblem

I have an ODEProblem where I need to index into a parameter using another separate parameter. Consider for example

using ModelingToolkit, DifferentialEquations
function rober(du, u, p, t)
    y₁, y₂, y₃ = u
    k₁, k₂, k₃, d, v = p
    kpa = d[v]
    du[1] = -k₁ * y₁ + k₃ * y₂ * y₃
    du[2] = k₁ * y₁ - k₂ * y₂^2 - k₃ * y₂ * y₃
    du[3] = k₂ * y₂^2
    nothing
end
just_a_dict = Dict([1,2,3,4,5,10,29] .=> [[1,2,3], [1,2,3], [4,5,1], [2,3,1], [4,1,9],[5,1,2],[2,3,1]])
prob = ODEProblem(rober, [1.0, 0.0, 0.0], (0.0, 1e5), (0.04, 3e7, 1e4, just_a_dict, 1))
modelingtoolkitize(prob)

This leads to the following

ERROR: MethodError: no method matching getindex(::Num, ::Num)
Closest candidates are:
  getindex(::Number) at C:\Users\licer\AppData\Local\Programs\Julia-1.7.3\share\julia\base\number.jl:95
  getindex(::Number, ::Integer) at C:\Users\licer\AppData\Local\Programs\Julia-1.7.3\share\julia\base\number.jl:96
  getindex(::Number, ::Integer...) at C:\Users\licer\AppData\Local\Programs\Julia-1.7.3\share\julia\base\number.jl:101
  ...
Stacktrace:
 [1] rober(du::Vector{Num}, u::Vector{Num}, p::NTuple{5, Num}, t::Num)
   @ Main c:\Users\licer\.julia\dev\FVM\dev\sparsity.jl:25
 [2] (::ODEFunction{true, typeof(rober), LinearAlgebra.UniformScaling{Bool}, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, typeof(SciMLBase.DEFAULT_OBSERVED), Nothing})(::Vector{Num}, ::Vararg{Any})
   @ SciMLBase C:\Users\licer\.julia\packages\SciMLBase\IJbT7\src\scimlfunctions.jl:1624
 [3] modelingtoolkitize(prob::ODEProblem{Vector{Float64}, Tuple{Float64, Float64}, true, Tuple{Float64, Float64, Float64, Dict{Int64, Vector{Int64}}, Int64}, ODEFunction{true, typeof(rober), LinearAlgebra.UniformScaling{Bool}, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, typeof(SciMLBase.DEFAULT_OBSERVED), Nothing}, Base.Pairs{Symbol, Union{}, Tuple{}, NamedTuple{(), Tuple{}}}, SciMLBase.StandardODEProblem}; kwargs::Base.Pairs{Symbol, Union{}, Tuple{}, NamedTuple{(), Tuple{}}})       
   @ ModelingToolkit C:\Users\licer\.julia\packages\ModelingToolkit\tMgaW\src\systems\diffeqs\modelingtoolkitize.jl:48
 [4] modelingtoolkitize(prob::ODEProblem{Vector{Float64}, Tuple{Float64, Float64}, true, Tuple{Float64, Float64, Float64, Dict{Int64, Vector{Int64}}, Int64}, ODEFunction{true, typeof(rober), LinearAlgebra.UniformScaling{Bool}, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, typeof(SciMLBase.DEFAULT_OBSERVED), Nothing}, Base.Pairs{Symbol, Union{}, Tuple{}, NamedTuple{(), Tuple{}}}, SciMLBase.StandardODEProblem})
   @ ModelingToolkit C:\Users\licer\.julia\packages\ModelingToolkit\tMgaW\src\systems\diffeqs\modelingtoolkitize.jl:7
 [5] top-level scope
   @ c:\Users\licer\.julia\dev\FVM\dev\sparsity.jl:33

Is there any way to allow this behaviour with kpa = d[v]?

I also have indexing like u[q[1]] or s[k][1], with q a provided variable and k::Num, which gives similar errors, like

using ModelingToolkit, DifferentialEquations
function rober(du, u, p, t)
    y₁, y₂, y₃ = u
    k₁, k₂, k₃, d, v, q = p
    u[q[1]]
    kpa = d[v]
    du[1] = -k₁ * y₁ + k₃ * y₂ * y₃
    du[2] = k₁ * y₁ - k₂ * y₂^2 - k₃ * y₂ * y₃
    du[3] = k₂ * y₂^2
    nothing
end
just_a_dict = Dict([1,2,3,4,5,10,29] .=> [[1,2,3], [1,2,3], [4,5,1], [2,3,1], [4,1,9],[5,1,2],[2,3,1]])
prob = ODEProblem(rober, [1.0, 0.0, 0.0], (0.0, 1e5), (0.04, 3e7, 1e4, just_a_dict, 1, [1, 2]))
modelingtoolkitize(prob)
julia> modelingtoolkitize(prob)
ERROR: ArgumentError: invalid index: α₅ of type Num

though this error is much less clear to me for how it might be resolved / whether it’s the same issue as above.

I think the solution is to just tell MTK not to trace through the lookup. Create a function that does the dictionary indexing:

lookup_kpa(dict, index) = dict[index]

Register that function so it is not traced:

@register_symbolic lookup_kpa(dict, index)

And then use that instead of directly indexing.

function ...
    ...
    kpa = lookup_kpa(d, v)
    ...
end

I don’t know of a way to tell modelingtoolkitize that a parameter has a type other than Num, so it is always going to look for the wrong getindex.

1 Like

Thank you. This does work, but for the latter problem in my question where I’d need to index as e.g. u[v[1]]], this doesn’t seem to work anymore? For example,

using ModelingToolkit, DifferentialEquations, Symbolics
@inline lookup_val(dict, idx) = dict[idx]
@register_symbolic lookup_val(dict, idx)
function rober(du, u, p, t)
    y₁, y₂, y₃ = u
    k₁, k₂, k₃, d, v = p
    kpa = lookup_val(d, v)
    #kpa2 = u[kpa] # <-- here
    kpa2 = lookup_val(u, kpa)
    kpa2 = lookup_val(u, kpa[1])
    du[1] = -k₁ * y₁ + k₃ * y₂ * y₃
    du[2] = k₁ * y₁ - k₂ * y₂^2 - k₃ * y₂ * y₃
    du[3] = k₂ * y₂^2
    nothing
end
just_a_dict = Dict([1,2,3,4,5,10,29] .=> [[1,2,3], [1,2,3], [4,5,1], [2,3,1], [4,1,9],[5,1,2],[2,3,1]])
prob = ODEProblem(rober, [1.0, 0.0, 0.0], (0.0, 1e5), (0.04, 3e7, 1e4, just_a_dict, 1))
modelingtoolkitize(prob)

Neither of the two kpa2 lines seem to work, giving the error

julia> modelingtoolkitize(prob)
ERROR: ArgumentError: invalid index: lookup_val(α₄, α₅) of type Num
Stacktrace:
  [1] to_index(i::Num)
    @ Base .\indices.jl:300
  [2] to_index(A::Vector{Num}, i::Num)
    @ Base .\indices.jl:277
  [3] to_indices
    @ .\indices.jl:333 [inlined]
  [4] to_indices
    @ .\indices.jl:325 [inlined]
  [5] getindex(A::Vector{Num}, I::Num)
    @ Base .\abstractarray.jl:1218
  [6] lookup_val
    @ c:\Users\licer\.julia\dev\FVM\dev\sparsity.jl:54 [inlined]
  [7] rober(du::Vector{Num}, u::Vector{Num}, p::NTuple{5, Num}, t::Num)
    @ Main c:\Users\licer\.julia\dev\FVM\dev\sparsity.jl:61
  [8] (::ODEFunction{true, typeof(rober), LinearAlgebra.UniformScaling{Bool}, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, 
Nothing, typeof(SciMLBase.DEFAULT_OBSERVED), Nothing})(::Vector{Num}, ::Vararg{Any})
    @ SciMLBase C:\Users\licer\.julia\packages\SciMLBase\IJbT7\src\scimlfunctions.jl:1624
  [9] modelingtoolkitize(prob::ODEProblem{Vector{Float64}, Tuple{Float64, Float64}, true, Tuple{Float64, Float64, Float64, Dict{Int64, Vector{Int64}}, Int64}, ODEFunction{true, 
typeof(rober), LinearAlgebra.UniformScaling{Bool}, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, typeof(SciMLBase.DEFAULT_OBSERVED), Nothing}, Base.Pairs{Symbol, Union{}, Tuple{}, NamedTuple{(), Tuple{}}}, SciMLBase.StandardODEProblem}; kwargs::Base.Pairs{Symbol, Union{}, Tuple{}, NamedTuple{(), Tuple{}}})
    @ ModelingToolkit C:\Users\licer\.julia\packages\ModelingToolkit\tMgaW\src\systems\diffeqs\modelingtoolkitize.jl:48
 [10] modelingtoolkitize(prob::ODEProblem{Vector{Float64}, Tuple{Float64, Float64}, true, Tuple{Float64, Float64, Float64, Dict{Int64, Vector{Int64}}, Int64}, ODEFunction{true, 
typeof(rober), LinearAlgebra.UniformScaling{Bool}, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, typeof(SciMLBase.DEFAULT_OBSERVED), Nothing}, Base.Pairs{Symbol, Union{}, Tuple{}, NamedTuple{(), Tuple{}}}, SciMLBase.StandardODEProblem})
    @ ModelingToolkit C:\Users\licer\.julia\packages\ModelingToolkit\tMgaW\src\systems\diffeqs\modelingtoolkitize.jl:7
 [11] top-level scope
    @ c:\Users\licer\.julia\dev\FVM\dev\sparsity.jl:70

I’m not sure what the difference here is. Do I need to somehow register lookup_val as being classed as an index? I imagine that’s not possible since everything is Num.

In this case, you are indexing with kpa[1] while kpa is assumed to be a scalar. modelingtoolkitize works by calling your function with a type Num that redefines operations to record an unevaluated symbolic representation. Since no return type was specified in the registration, it was assumed to be a scalar Num. You can specify a return type to sort this out, but you also have to specify that you want the symbolic scalar lookup to accept a vector:

lookup_arr(d, v) = d[v] 
@register_symbolic lookup_arr(d, v)::Vector{Num}
lookup_val(a, v) = a[v]
@register_symbolic lookup_val(a::Vector{Num}, v)

function ...
    ...
    kpa = lookup_arr(d, v)
    kpa2 = lookup_val(u, kpa[1])
    ...
end

You could also just hide all the indexing behind one function with something like

lookup_val(u, d, v) = u[d[v]]
@register_symbolic lookup_val(u::Vector{Num}, d, v)
2 Likes