How to tell ModelingToolkit.jl which variables are parameters

I have a Symbolics.Equation that I got from an external source. Now I want to use that equation in ModelingToolkit.jl, but I don’t know how to tell ModelingToolkit.jl which variables are parameters.

For example, here I get my equation from python, and SymPyPythonCall’s conversion from sympy variables to Symbolics.jl variables does not support provenance.

julia> using Symbolics, SymbolicUtils, PythonCall, SymPyPythonCall, ModelingToolkit

julia> pyexec("import sympy", Main)

julia> eq = pyconvert(Equation, pyeval("sympy.core.sympify('Eq(-x*k, diff(diff(f(x), x), x))')", Main))
Differential(x)(Differential(x)(f(x))) ~ -k*x

julia> ODESystem(eq, name="MyODESystem")
ERROR: ArgumentError: Variable k is not a function of independent variable x.
Stacktrace:
 [1] check_variables(dvs::Vector{SymbolicUtils.BasicSymbolic{Real}}, iv::SymbolicUtils.BasicSymbolic{Real})
   @ ModelingToolkit ~/.julia/packages/ModelingToolkit/brJDK/src/utils.jl:132
 [2] ODESystem(tag::UInt64, deqs::Vector{Equation}, iv::SymbolicUtils.BasicSymbolic{Real}, dvs::Vector{SymbolicUtils.BasicSymbolic{Real}}, ps::Vector{Any}, tspan::Nothing, var_to_name::Dict{Any, Any}, ctrls::Vector{Any}, observed::Vector{Equation}, tgrad::Base.RefValue{Vector{Num}}, jac::Base.RefValue{Any}, ctrl_jac::Base.RefValue{Any}, Wfact::Base.RefValue{Matrix{Num}}, Wfact_t::Base.RefValue{Matrix{Num}}, name::String, systems::Vector{ODESystem}, defaults::Dict{Any, Any}, torn_matching::Nothing, connector_type::Nothing, preface::Nothing, cevents::Vector{ModelingToolkit.SymbolicContinuousCallback}, devents::Vector{ModelingToolkit.SymbolicDiscreteCallback}, metadata::Nothing, gui_metadata::Nothing, tearing_state::Nothing, substitutions::Nothing, complete::Bool, discrete_subsystems::Nothing, unknown_states::Nothing; checks::Bool)
   @ ModelingToolkit ~/.julia/packages/ModelingToolkit/brJDK/src/systems/diffeqs/odesystem.jl:152
 [3] ODESystem(deqs::Vector{Equation}, iv::SymbolicUtils.BasicSymbolic{Real}, dvs::Vector{Any}, ps::OrderedCollections.OrderedSet{Any}; controls::Vector{Num}, observed::Vector{Equation}, systems::Vector{ODESystem}, tspan::Nothing, name::String, default_u0::Dict{Any, Any}, default_p::Dict{Any, Any}, defaults::Dict{Any, Any}, connector_type::Nothing, preface::Nothing, continuous_events::Nothing, discrete_events::Nothing, checks::Bool, metadata::Nothing, gui_metadata::Nothing)
   @ ModelingToolkit ~/.julia/packages/ModelingToolkit/brJDK/src/systems/diffeqs/odesystem.jl:218
 [4] ODESystem(eqs::Vector{Equation}, iv::Nothing; kwargs::Base.Pairs{Symbol, String, Tuple{Symbol}, NamedTuple{(:name,), Tuple{String}}})
   @ ModelingToolkit ~/.julia/packages/ModelingToolkit/brJDK/src/systems/diffeqs/odesystem.jl:268
 [5] ODESystem(::Equation; kwargs::Base.Pairs{Symbol, String, Tuple{Symbol}, NamedTuple{(:name,), Tuple{String}}})
   @ ModelingToolkit ~/.julia/packages/ModelingToolkit/brJDK/src/systems/diffeqs/odesystem.jl:303
 [6] top-level scope
   @ REPL[91]:1

julia> fx, k, x = get_variables(eq)
3-element Vector{Any}:
 f(x)
 k
 x

julia> ODESystem(eq,x,[fx],[k], name="MyODESystem")
ERROR: ArgumentError: k is not a parameter.
Stacktrace:
 [1] check_parameters(ps::Vector{SymbolicUtils.BasicSymbolic{Real}}, iv::SymbolicUtils.BasicSymbolic{Real})
   @ ModelingToolkit ~/.julia/packages/ModelingToolkit/brJDK/src/utils.jl:108
 [2] ODESystem(tag::UInt64, deqs::Vector{Equation}, iv::SymbolicUtils.BasicSymbolic{Real}, dvs::Vector{SymbolicUtils.BasicSymbolic{Real}}, ps::Vector{SymbolicUtils.BasicSymbolic{Real}}, tspan::Nothing, var_to_name::Dict{Any, Any}, ctrls::Vector{Any}, observed::Vector{Equation}, tgrad::Base.RefValue{Vector{Num}}, jac::Base.RefValue{Any}, ctrl_jac::Base.RefValue{Any}, Wfact::Base.RefValue{Matrix{Num}}, Wfact_t::Base.RefValue{Matrix{Num}}, name::String, systems::Vector{ODESystem}, defaults::Dict{Any, Any}, torn_matching::Nothing, connector_type::Nothing, preface::Nothing, cevents::Vector{ModelingToolkit.SymbolicContinuousCallback}, devents::Vector{ModelingToolkit.SymbolicDiscreteCallback}, metadata::Nothing, gui_metadata::Nothing, tearing_state::Nothing, substitutions::Nothing, complete::Bool, discrete_subsystems::Nothing, unknown_states::Nothing; checks::Bool)
   @ ModelingToolkit ~/.julia/packages/ModelingToolkit/brJDK/src/systems/diffeqs/odesystem.jl:153
 [3] ODESystem(deqs::Vector{Equation}, iv::SymbolicUtils.BasicSymbolic{Real}, dvs::Vector{SymbolicUtils.BasicSymbolic{Real}}, ps::Vector{SymbolicUtils.BasicSymbolic{Real}}; controls::Vector{Num}, observed::Vector{Equation}, systems::Vector{ODESystem}, tspan::Nothing, name::String, default_u0::Dict{Any, Any}, default_p::Dict{Any, Any}, defaults::Dict{Any, Any}, connector_type::Nothing, preface::Nothing, continuous_events::Nothing, discrete_events::Nothing, checks::Bool, metadata::Nothing, gui_metadata::Nothing)
   @ ModelingToolkit ~/.julia/packages/ModelingToolkit/brJDK/src/systems/diffeqs/odesystem.jl:218
 [4] ODESystem(::Equation, ::SymbolicUtils.BasicSymbolic{Real}, ::Vararg{Any}; kwargs::Base.Pairs{Symbol, String, Tuple{Symbol}, NamedTuple{(:name,), Tuple{String}}})
   @ ModelingToolkit ~/.julia/packages/ModelingToolkit/brJDK/src/systems/diffeqs/odesystem.jl:303
 [5] top-level scope
   @ REPL[93]:1

How should I tell ModelingToolkit.jl which variables are parameters and which are variables?

I am developing a package/codebase and I needed exactly this information. As far as I can tell, it doens’t exist in the public API. I\ve coded up the following function:


is_variable(x::Num) = is_variable(x.val)
function is_variable(x)
    if x isa ModelingToolkit.SymbolicUtils.Symbolic
        if haskey(x.metadata, ModelingToolkit.Symbolics.VariableSource)
            src = x.metadata[ModelingToolkit.Symbolics.VariableSource]
            return first(src) == :variables
        end
    end
    return false
end

This will return false for “variables” defined as @parameters p = 0.2 c = 0.1 etc.

So you can copy this and change the :variables to :parameters. I think it will work.

Edit: ah, I misunderstood what you need. You need the other-way around. Not to examine if something is a parameter, but to enforce that something is… I know you can create symbolically parameters, so maybe just use their symbol and re-create them using @parameters ?

like

"""
    make_new_named_parameter(variable, value::Num, extra::String, suffix = true)

If `value isa Num`, return `value`. Otherwise, create a new MTK `@parameter`
whose name is created from `variable` by adding the `extra` string.
If `suffix = true` the extra is added at the end after a `_`. Otherwise
it is added at the start, then a `_` and then the variable name.
"""
make_new_named_parameter(v, value::Num, extra, suffix = true) = value
function make_new_named_parameter(v, value::Real, extra, suffix = true)
    # create new string:
    n = string(ModelingToolkit.getname(v))
    newstring = if suffix
        n*"_"*extra
    else
        extra*"_"*n
    end
    varsymbol = Symbol(newstring)
    dummy = (@parameters $(varsymbol) = value)
    return first(dummy)
end

(another function I’ve coded up for the aforementioned project, you don’t need the suffix stsuff)

I figured out a way to do this without using internals:

function mark_parameters(eq::Equation, symbols)
    parameters = Dict(sy => only(@parameters($sy)) for sy in symbols)
    substitutions = Dict((var => parameters[Symbol(var)] for var in get_variables(eq) if Symbol(var) in keys(parameters)))
    substitute(eq, substitutions)
end

It’s still not as nice as it could theoretically be, but it’s not too bad. I’m still open to hearing alternative approaches.

I’m not sure what the code snippets you have shared are for. Would you show how I could use them to pass my equation to ODESystem?