Skip to content

Array of symbolics #4006

@sablonl

Description

@sablonl

After some testing from my side, I noticed that using arrays of symbolics is far faster than using symbolic arrays (I have a lot of equations where I need to scalarize).

However, where Symbolics.jl provides the function Symbolics.variables to create arrays of symbolics, MTK doesn't seem to have it's counterpart (and the use of Symbolics.variables is often impossible due to the independent variables).

I think it would be great to have functions like

ModelingToolkit.variables(name::Symbol, indices...; T=Real, iv=nothing, default=nothing, symbolic_array=false)
ModelingToolkit.parameters(name::Symbol, indices...; T=Real, iv=nothing, default=nothing, symbolic_array=false)

My knowledge of the MTK internals is very limited but I came up (with a bit of help from Claude) with those naive implementation, that themselves call the macros @variables and @parameters. This, however, doesn't cover the cases where additional metadata are needed, such as @variables x(t) [irreducible=true, ...].

using Symbolics: variable, FnType, map_subscripts, Num, setmetadata, VariableSource, Sym
using ModelingToolkit: @variables, @parameters

export variables, parameters

"""
    variables(name::Symbol, indices...; T=Real, iv=nothing, default=nothing, symbolic_array=false)

Create a multi-dimensional array of individual variables with subscript notation.

# Arguments
- `name::Symbol`: Base name for the variables
- `indices...`: Index ranges for each dimension
- `T=Real`: Type of the variables
- `iv=nothing`: Independent variable (e.g., time). If provided, creates time-dependent variables.
- `default=nothing`: Default value for the variables. If provided, creates variables with default values.
- `symbolic_array=false`: If `true`, creates a classical symbolic array `name(iv)[indices...]`. 
  If `false` (default), creates an array of individual symbolic variables with subscript notation.

# Returns
Array of symbolic variables. If `iv` is provided, returns time-dependent variables compatible
with ModelingToolkit.
"""
function variables(name::Symbol, indices...; T::Type=Real, iv=nothing, default=nothing, symbolic_array::Bool=false)
    # If symbolic_array is true, use the classical syntax
    if symbolic_array
        dims = length.(indices)
        
        if iv === nothing && default === nothing
            expr = quote
                @variables $(name)[$(indices...)]::$T
                $(name)
            end
        elseif iv === nothing && default !== nothing
            expr = quote
                @variables $(name)[$(indices...)]::$T = fill($(T(default)), $(dims...))
                $(name)
            end
        elseif iv !== nothing && default === nothing
            expr = quote
                @variables $(name)($(iv))[$(indices...)]::$T
                $(name)
            end
        else  # iv !== nothing && default !== nothing
            expr = quote
                @variables $(name)($(iv))[$(indices...)]::$T = fill($(T(default)), $(dims...))
                $(name)
            end
        end
        
        return eval(expr)
    end
    
    # Build the array manually using metaprogramming
    vars = Array{Num}(undef, length.(indices)...)
    
    for (i, idx) in enumerate(Iterators.product(indices...))
        # Create variable name with subscripts
        name_ij = Symbol(name, join(map_subscripts.(idx), "ˏ"))
        
        # Build the macro call dynamically based on iv and default
        if iv === nothing && default === nothing
            expr = quote
                @variables $(name_ij)::$T
                $(name_ij)
            end
        elseif iv === nothing && default !== nothing
            expr = quote
                @variables $(name_ij)::$T = $(T(default))
                $(name_ij)
            end
        elseif iv !== nothing && default === nothing
            expr = quote
                @variables $(name_ij)($(iv))::$T
                $(name_ij)
            end
        else  # iv !== nothing && default !== nothing
            expr = quote
                @variables $(name_ij)($(iv))::$T = $(T(default))
                $(name_ij)
            end
        end
        
        vars[i] = eval(expr)
    end
    
    return vars
end

"""
    parameters(name::Symbol, indices...; T=Real, iv=nothing, default=nothing, symbolic_array=false)

Create a multi-dimensional array of parameters with subscript notation.

# Arguments
- `name::Symbol`: Base name for the parameters
- `indices...`: Index ranges for each dimension
- `T=Real`: Type of the parameters
- `iv=nothing`: Independent variable. If provided, creates time-dependent parameters.
- `default=nothing`: Default value for the parameters. If provided, creates parameters with default values.
- `symbolic_array=false`: If `true`, creates a classical symbolic array `name(iv)[indices...]`. 
  If `false` (default), creates an array of individual symbolic parameters with subscript notation.

# Returns
Array of symbolic parameters created using the `@parameters` macro.
"""
function parameters(name::Symbol, indices...; T::Type=Real, iv=nothing, default=nothing, symbolic_array::Bool=false)
    # If symbolic_array is true, use the classical syntax
    if symbolic_array
        dims = length.(indices)
        
        if iv === nothing && default === nothing
            expr = quote
                @parameters $(name)[$(indices...)]::$T
                $(name)
            end
        elseif iv === nothing && default !== nothing
            expr = quote
                @parameters $(name)[$(indices...)]::$T = fill($(T(default)), $(dims...))
                $(name)
            end
        elseif iv !== nothing && default === nothing
            expr = quote
                @parameters $(name)($(iv))[$(indices...)]::$T
                $(name)
            end
        else  # iv !== nothing && default !== nothing
            expr = quote
                @parameters $(name)($(iv))[$(indices...)]::$T = fill($(T(default)), $(dims...))
                $(name)
            end
        end
        
        return eval(expr)
    end
    
    # For parameters, we always use @parameters macro
    params = Array{Num}(undef, length.(indices)...)
    
    for (i, idx) in enumerate(Iterators.product(indices...))
        name_ij = Symbol(name, join(map_subscripts.(idx), "ˏ"))
        
        # Build the macro call dynamically based on iv and default
        if iv === nothing && default === nothing
            expr = quote
                @parameters $(name_ij)::$T
                $(name_ij)
            end
        elseif iv === nothing && default !== nothing
            expr = quote
                @parameters $(name_ij)::$T = $(T(default))
                $(name_ij)
            end
        elseif iv !== nothing && default === nothing
            expr = quote
                @parameters $(name_ij)($(iv))::$T
                $(name_ij)
            end
        else  # iv !== nothing && default !== nothing
            expr = quote
                @parameters $(name_ij)($(iv))::$T = $(T(default))
                $(name_ij)
            end
        end
        
        params[i] = eval(expr)
    end
    
    return params
end

With this probably 'bad' implementation my issue is fixed, but I think it can be a useful functionality for the package.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions