Function Building and Compilation (build_function)

At any time, callable functions can be generated from Symbolics IR by using Symbolics.toexpr. This performs some cleaning to return an expression without extraneous pieces that commonly matches expressions one would write in functions like those for differential equation solvers and optimization libraries. These functions can be automatically parallelized and specialize on Julia types like static arrays and sparse matrices.

The core compilation process of Symbolics IR is build_function. build_function takes an operation or an AbstractArray of operations and generates a compilable version of the model for numerical solvers. The form of this output is dependent on the target. By default, the target outputs Julia code, but other formats, such as C, Stan, and MATLAB are available. These can be generated as expressions which can then be evaluated into a callable function, or the compilers for the respective targets can be invoked to directly give back the function handle.

build_function

Symbolics.build_functionFunction
build_function(ex, args...;
               expression = Val{true},
               target = JuliaTarget(),
               parallel=nothing,
               kwargs...)

Generates a numerically-usable function from a Symbolics Num.

Arguments:

  • ex: The Num to compile
  • args: The arguments of the function
  • expression: Whether to generate code or whether to generate the compiled form. By default, expression = Val{true}, which means that the code for the function is returned. If Val{false}, then the returned value is compiled.

Keyword Arguments:

  • target: The output target of the compilation process. Possible options are:
    • JuliaTarget: Generates a Julia function
    • CTarget: Generates a C function
    • StanTarget: Generates a function for compiling with the Stan probabilistic programming language
    • MATLABTarget: Generates an anonymous function for use in MATLAB and Octave environments
  • parallel: The kind of parallelism to use in the generated function. Defaults to SerialForm(), i.e. no parallelism, if ex is a single expression or an array containing <= 1500 non-zero expressions. If ex is an array of > 1500 non-zero expressions, then ShardedForm(80, 4) is used. See below for more on ShardedForm. Note that the parallel forms are not exported and thus need to be chosen like Symbolics.SerialForm(). The choices are:
    • SerialForm(): Serial execution.
    • ShardedForm(cutoff, ncalls): splits the output function into sub-functions which contain at most cutoff number of output rhss. These sub-functions are called by the top-level function that buildfunction returns. This helps in reducing the compile time of the generated function.
    • MultithreadedForm(): Multithreaded execution with a static split, evenly splitting the number of expressions per thread.
  • fname: Used by some targets for the name of the function in the target space.

Note that not all build targets support the full compilation interface. Check the individual target documentation for details.

source

Low-Level Code Generation

Consumers that already maintain a SymbolicUtils.IRStructure can generate Julia function expressions directly with codegen_function.

Symbolics.CodegenFunctionOptionsType
CodegenFunctionOptions(; kwargs...)

Bundle of options controlling code generation in codegen_function.

Historically codegen_function accepted these as a kwargs... splat. Threading a kwargs... bundle means every function that forwards codegen options gets specialized (and recompiled) once per distinct set of keyword arguments, even when the values it forwards are irrelevant to it. CodegenFunctionOptions is a single, non-parametric concrete type: no matter which options are set, its type is always CodegenFunctionOptions, so functions that thread it through only need to be compiled once.

The keyword constructor mirrors the historical keyword arguments of codegen_function exactly, including their defaults. Unknown keyword arguments are ignored (as they were previously silently dropped by codegen_function), preserving backwards compatibility.

Fields:

  • nanmath: rewrite math functions to their NaN-safe variants.
  • wrap_code: a 2-tuple of transformations applied to the out-of-place and in-place Funcs respectively.
  • checkbounds: emit bounds checks (when false, generated code is wrapped in @inbounds).
  • iip_config: a 2-tuple of Bools indicating whether to generate the out-of-place and in-place functions respectively.
  • sort_addmul: sort the arguments of +/* for deterministic codegen.
  • optimize: optimization rules to apply, or nothing.
  • similarto: the array type (or name) to generate for array outputs, or nothing to infer it.
  • outputidxs: explicit output indices for in-place array codegen, or nothing.
  • skipzeros: skip writing structural zeros in in-place array codegen.
source
Symbolics.codegen_functionFunction
codegen_function(ir, expr, args[, options])
codegen_function(ir, expr, args; kwargs...)

Generate out-of-place and in-place Julia function expressions for expr using a SymbolicUtils.IRStructure. args is a vector whose elements describe each generated function argument as either a scalar symbolic value or a collection of symbolic values.

Pass a CodegenFunctionOptions as options, or pass the same settings as keyword arguments. The result is a 2-tuple containing the out-of-place and in-place function expressions. The requested iip_config and the shape of expr determine which variants are implemented.

This is the low-level code-generation interface for consumers that already maintain a SymbolicUtils.IRStructure. Prefer build_function when starting from ordinary symbolic expressions.

source

Target-Specific Definitions

Symbolics._build_functionMethod
_build_function(target::JuliaTarget, rhss::AbstractArray, args...;
                   conv=toexpr,
                   expression = Val{true},
                   expression_module = @__MODULE__(),
                   checkbounds = false,
                   postprocess_fbody=ex -> ex,
                   linenumbers = false,
                   outputidxs=nothing,
                   skipzeros = false,
                   force_SA = false,
                   wrap_code = (nothing, nothing),
                   fillzeros = skipzeros && !(rhss isa SparseMatrixCSC),
                   states = LazyState(),
                   iip_config = (true, true),
                   parallel=nothing, cse = false, kwargs...)

Build function target: JuliaTarget

_build_function(target::JuliaTarget, rhss, args...;
                conv = toexpr,
                expression = Val{true},
                checkbounds = false,
                linenumbers = false,
                headerfun = addheader, outputidxs=nothing,
                convert_oop = true, force_SA = false,
                skipzeros = outputidxs===nothing,
                fillzeros = skipzeros && !(typeof(rhss)<:SparseMatrixCSC),
                parallel=SerialForm(), kwargs...)

Generates a Julia function which can then be utilized for further evaluations. If expression=Val{false}, the return is a Julia function which utilizes RuntimeGeneratedFunctions.jl to be free of world-age issues.

If the rhss is a scalar, the generated function is a function with a scalar output. Otherwise, if it's an AbstractArray, the output is two functions, one for out-of-place AbstractArray output and a second which is a mutating function. The outputted functions match the given argument order, i.e., f(u,p,args...) for the out-of-place and scalar functions and f!(du,u,p,args..) for the in-place version.

Special Keyword Arguments:

  • parallel: The kind of parallelism to use in the generated function. Defaults to SerialForm(), i.e. no parallelism. Note that the parallel forms are not exported and thus need to be chosen like Symbolics.SerialForm(). The choices are:
    • SerialForm(): Serial execution.
    • ShardedForm(cutoff, ncalls): splits the output function into sub-functions which contain at most cutoff number of output rhss. These sub-functions are called by the top-level function that buildfunction returns.
    • MultithreadedForm(): Multithreaded execution with a static split, evenly splitting the number of expressions per thread.
  • conv: The conversion function of symbolic types to Expr. By default, this uses the toexpr function.
  • checkbounds: For whether to enable bounds checking inside the generated function. Defaults to false, meaning that @inbounds is applied.
  • linenumbers: Determines whether the generated function expression retains the line numbers. Defaults to true.
  • convert_oop: Determines whether the OOP version should try to convert the output to match the type of the first input. This is useful for cases like LabelledArrays or other array types that carry extra information. Defaults to true.
  • force_SA: Forces the output of the OOP version to be a StaticArray. Defaults to false, and outputs a static array when the first argument is a static array.
  • similarto: An AbstractArray subtype which controls the type of the returned array for the OOP version. If provided, it ignores the value of force_SA.
  • skipzeros: Whether to skip filling zeros in the in-place version if the filling function is 0.
  • fillzeros: Whether to perform fill(out,0) before the calculations to ensure safety with skipzeros.
source
Symbolics._build_functionMethod

Build function target: CTarget

_build_function(target::CTarget, eqs::Array{<:Equation}, args...;
                conv = toexpr, expression = Val{true},
                fname = :diffeqf,
                lhsname=:du,rhsnames=[Symbol("RHS$i") for i in 1:length(args)],
                libpath=tempname(), compiler=:gcc)

This builds an in-place C function. Only works on arrays of equations. If expression == Val{false}, then this builds a function in C, compiles it, and returns a lambda to that compiled function. These special keyword arguments control the compilation:

  • libpath: the path to store the binary. Defaults to a temporary path.
  • compiler: which C compiler to use. Defaults to :gcc, which is currently the only available option.
source
Symbolics._build_functionMethod

Build function target: StanTarget

_build_function(target::StanTarget, eqs::Array{<:Equation}, vs, ps, iv;
                conv = toexpr, expression = Val{true},
                fname = :diffeqf, lhsname=:internal_var___du,
                rhsnames=[:internal_var___u,:internal_var___p,:internal_var___t])

This builds an in-place Stan function compatible with the Stan differential equation solvers. Unlike other build targets, this one requires (vs, ps, iv) as the function arguments. Only allowed on arrays of equations.

source
Symbolics._build_functionMethod

Build function target: MATLABTarget

_build_function(target::MATLABTarget, eqs::Array{<:Equation}, args...;
                conv = toexpr, expression = Val{true},
                lhsname=:internal_var___du,
                rhsnames=[:internal_var___u,:internal_var___p,:internal_var___t])

This builds an out of place anonymous function @(t,rhsnames[1]) to be used in MATLAB. Compatible with the MATLAB differential equation solvers. Only allowed on expressions, and arrays of expressions.

source

Limitations

build_function