Variant structure and types
This document aims to describe the structure of the Algebraic Data Type (ADT) used to represent a symbolic tree, along with several utility types to allow robustly interacting with it.
SymbolicUtils uses Moshi.jl's ADT structure. The ADT is named BasicSymbolicImpl, and an alias BSImpl is available for convenience. The actual type of a variable is BSImpl.Type, aliased as BasicSymbolic. A BasicSymbolic is considered immutable. Mutating its fields is unsafe behavior.
In SymbolicUtils v3, the type T in BasicSymbolic{T} was the type represented by the symbolic variable. In other words, T was the SymbolicUtils.symtype of the variable.
In SymbolicUtils v4, the symtype is not stored in the type, and is instead a field of the struct. This allows for greatly increased type-stability. The type T in BasicSymbolic{T} now represents a tag known as thw vartype. This flag determines the assumptions made about the symbolic algebra. It can take one of three values:
SymReal: The default behavior.SafeReal: Idential toSymReal, but common factors in the numerator and denominator of a division are not cancelled.TreeReal: Assumes nothing about the algebra, and always uses theTermvariant to represent an expression.
A given expression must be pure in its vartype. In other words, no operation supports operands of different vartypes.
While ismutabletype(BasicSymbolic) returns true, symbolic types are IMMUTABLE. Any mutation is undefined behavior and can lead to very confusing and hard-to-debug issues. This includes internal mutation, such as mutating AddMul.dict. The arrays returned from TermInterface.arguments and TermInterface.sorted_arguments are read-only arrays for this reason.
Expression symtypes
The "symtype" of a symbolic variable/expression is the Julia type that the variable/expression represents. It can be queried with SymbolicUtils.symtype. Note that this query is unstable - the returned type cannot be inferred.
Expression shapes
In SymbolicUtils v4, arrays are first-class citizens. This is implemented by storing the shape of the symbolic. The shape can be queried using SymbolicUtils.shape and is one of two types.
Symbolics with known shape
The most common case is when the shape of a symbolic variable is known. For example:
@syms x[1:2] y[-3:6, 4:7] zAll of the variables created above have known shape. In this case, SymbolicUtils.shape returns a (custom) vector of UnitRange{Int} semantically equivalent to Base.axes. This does not return a Tuple since the number of dimensions cannot be inferred and thus returning a tuple would introduce type-instability. All array operations will perform validation on the shapes of their inputs (e.g. matrix multiplication) and calculates the shape of their outputs.
Scalar variables return an empty vector as their shape.
Symbolics with known ndims
The next most common case is when the exact shape/size of the symbolic is unknown but the number of dimensions is known. For example:
@syms x::Vector{Number} y::Matrix{Number} z::Array{Number, 3}In this case, SymbolicUtils.shape returns a value of type SymbolicUtils.Unknown. This has a single field ndims::Int storing the number of dimensions of the symbolic. Note that a shape of SymbolicUtils.Unknown(0) does not represent a scalar. All array operations will perform as much validation as possible on their arguments. The shape of the result will be calculated on a best-effort basis.
Symbolics with unknown ndims
In this case, nothing is known about the symbolic except that it is an array. For example:
@syms x::Array{Number}Symbolics.shape(x) will return SymbolicUtils.Unknown(-1). This effectively disables most shape checking for array operations.
Variants
struct Const
const val::Any
# ...
endAny non-symbolic values in an expression are stored in a Const variant. This is crucial for type-stability, but it does mean that obtaining the value out of a Const is unstable and should be avoided. This value can be obtained by pattern matching using Moshi.Match.@match or using the unwrap_const utility. unwrap_const will act as an identity function for any input that is not Const, including non-symbolic inputs. Const is the only variant which does not have metadata.
SymbolicUtils.isconst can be used to check if a BasicSymbolic is the Const variant. This variant can be constructed using Const{T}(val) or BSImpl.Const{T}(val), where T is the appropriate vartype.
The Const constructors have an additional special behavior. If given an array of symbolics (or array of array of ... symbolics), it will return a Term (see below) with SymbolicUtils.array_literal as the operation. This allows standard symbolic operations (such as substitute) to work on arrays of symbolics without excessive special-case handling and improved type-stability.
struct Sym
const name::Symbol
const metadata::MetadataT
const shape::ShapeT
const type::TypeT
# ...
endSym represents a symbolic quantity with a given name. This and Const are the two atomic variants. metadata is the symbolic metadata associated with this variable. type is the tag for the type of quantity represented here. shape stores the shape if the variable is an array symbolic.
metadatais eithernothingor a map fromDataTypekeys to arbitrary values. Any
interaction with metadata should be done by providing such a mapping during construction or using getmetadata, setmetadata, hasmetadata.
typeis a Julia type.shapeis as described above.
These three fields are present in all subsequent variants as well.
A Sym can be constructed using Sym{T}(name::Symbol; type, shape, metadata) or BSImpl.Sym{T}(name::Symbol; type, shape, metadata).
struct Term
const f::Any
const args::SmallV{BasicSymbolicImpl.Type{T}}
const metadata::MetadataT
const shape::ShapeT
const type::TypeT
# ...
endTerm is the generic expression form for an operation f applied to the arguments in args. In other words, this represents f(args...). Any constant (non-symbolic) arguments (including arrays of symbolics) are converted to symbolics and wrapped in Const.
A Term can be constructed using Term{T}(f, args; type, shape, metadata) or BSImpl.Term{T}(f, args; type, shape, metadata).
struct AddMul
const coeff::Any
const dict::ACDict{T}
const variant::AddMulVariant.T
const metadata::MetadataT
const shape::ShapeT
const type::TypeT
# ...
endAddMul is a specialized representation for associative-commutative addition and multiplication. The two operations are distinguised using the AddMulVariant EnumX.jl enum. It has two variants: AddMulVariant.ADD and AddMulVariant.MUL.
For multiplication terms, coeff is a constant non-symbolic coefficient multipled with the expression. dict is a map from terms being multiplied to their exponents. For example, 2x^2 * (y + z)^3 is represented with coeff = 2 and dict = ACDict{T}(x => 2, (y + z) => 3). A valid multiplication term is subject to the following constraints:
coeffmust be non-symbolic.- The values of
dictmust be non-symbolic. - The keys of
dictmust not be expressions with^as the operation UNLESS the exponent is symbolic. For example,x^x * y^2is represented withdict = ACDict{T}((x^x) => 1, y => 2). dictmust not be empty.coeffmust not be zero.- If
dicthas only one element,coeffmust not be one. Such a case should be represented as a power term (with^as the operation). - If
dicthas only one element where the key is an addition,coeffmust not be negative one. Such a case should be represented by distributing the negation.
The Mul{T}(coeff, dict; type, shape, metadata) constructor validates these constraints and automatically returns the appropriate alternative form where applicable. It should be preferred. BSImpl.AddMul{T}(coeff, dict, variant; type, shape, metadata) is faster but does not validate the constraints and should be used with caution. Incorrect usage can and will lead to both invalid expressions and undefined behavior.
For addition terms, coeff is a constant non-symbolic coefficient added to the expression. dict is a map from terms being added to the constant non-symbolic coefficients they are multiplied by. For example, to represent 1 + 2x + 3y * z coeff would be 1 and dict would be Dict(x => 2, (y * z) => 3). A valid addition term is subject to the following constraints:
coeffmust be non-symbolic.- The values of
dictmust be non-symbolic. - The keys of
dictmust not be additions expressions represented withAddMul. dictmust not be empty.- If
dicthas only one element,coeffmust not be zero. Such a case should be represented using the appropriate multiplication form.
The Add{T}(coeff, dict; type, shape, metadata) constructor validates these constraints and automatically returns the appropriate alternative form where applicable. It should be preferred. BSImpl.AddMul{T}(coeff, dict, variant; type, shape, metadata) is faster but does not validate the constraints and should be used with caution. Incorrect usage can and will lead to both invalid expressions and undefined behavior.
struct Div
const num::BasicSymbolicImpl.Type{T}
const den::BasicSymbolicImpl.Type{T}
const simplified::Bool
const metadata::MetadataT
const shape::ShapeT
const type::TypeT
# ...
endThe Div variant represents division (where the operation is /). num is the numerator and den is the denominator expression. simplified is a boolean indicating whether this expression is in the most simplified form possible. If it is true, certain algorithms in simplify_fractions will not inspect this term. In almost all cases, it should be provided as false. A valid division term is subject to the following constraints:
- Both the numerator and denominator cannot be
Constvariants. This should instead be represented as aConstvariant wrapping the result of division. - The numerator cannot be zero. This should instead be represented as a
Constwrapping the appropriate zero. - The denominator cannot be one. This should instead be represented as the numerator, possibly wrapped in a
Const. - The denominator cannot be zero. This should instead be represented as a
Constwith some form of infinity. - The denominator cannot be negative one. This should instead be represented as the negation of the numerator.
- Non-symbolic coefficients should be propagated to the numerator if it is a constant or multiplication term.
The Div{T}(num, den, simplified; type, shape, metadata) constructor can be used to build this form. If T is SymReal, the constructor will use quick_cancel to cancel trivially identifiable common factors in the numerator and denominator. It will also perform validation of the above constraints and return the appropriate alternative form where applicable. Some of the constraints can be relaxed for non-scalar algebras. The BSImpl.Div{T}(num, den, simplified; type, shape, metadata) does not perform such validation or transformation.
struct ArrayOp
const output_idx::SmallV{Union{Int, BasicSymbolicImpl.Type{T}}}
const expr::BasicSymbolicImpl.Type{T}
const reduce::Any
const term::Union{BasicSymbolicImpl.Type{T}, Nothing}
const ranges::Dict{BasicSymbolicImpl.Type{T}, StepRange{Int, Int}}
const metadata::MetadataT
const shape::ShapeT
const type::TypeT
endArrayOp is used to represent vectorized operations. This variant should not be created manually. Instead, the @arrayop macro constructs this using a generalized Einstein-summation notation, similar to that of Tullio.jl. Consider the following example:
ex = @arrayop (i, j) A[i, k] * B[k, j] + C[i, j]This represents A * B + C for matrices A, B, C as a vectorized array operation. Some operations, such as broadcasts, are automatically represented as such a form internally. The following description of the fields assumes familiarity with the @arrayop macro.
When processing this macro, the indices i, j, k are converted to use a common global index variable to avoid potential name conflicts with other symbolic variables named i, j, k if ex is used in a larger expression. The output_idx field stores [i, j]. expr stores the expression A[i, k] * B[k, j] + C[i, j], with i, j, k replaced by the common global index variable. reduce is the operation used to reduce indices not present in output_idx (in this example, k). By default, it is +. term stores an expression that represents an equivalent computation to use for printing/code-generation. For example, here A * B + C would be a valid value for term. By default, term is nothing except when the expression is generated via broadcast or a similar operation. ranges is a dictionary mapping indices used in expr (converted to the common global index) to the range of indices over which they should iterate, in case such a range is explicitly provided.
The common global index variable is printed as _1, _2, ... in arrayops. It is not a valid symbolic variable outside of an ArrayOp's expr.
A valid ArrayOp satisfies the following conditions:
output_idxonly contains the integer1or variants of the common global index variable.- Any top-level indexing operations in
expruse common global indices. A top-level indexing operation is a term whose operation isgetindex, and which is not a descendant of any other term whose operation isgetindex. reducemust be a valid reduction operation that can be passed toBase.reduce.- If
termis notnothing, it must be an expression with shapeshapeand typetype. - The keys of
rangesmust be variants of the common global index variable, and must be present inexpr.
The @arrayop macro should be heavily preferred for creating ArrayOps. In case this is not possible (such as in recursive functions like substitute) the ArrayOp constructor should be preferred. This does not allow specifying the type and shape, since these values are tied to the fields of the variant and are thus determined. The BSImpl.ArrayOp constructor should be used with extreme caution, since it does not validate input.
Array arithmetic
SymbolicUtils implements a simple array algebra in addition to the default scalar algebra. Similar to how SymbolicUtils.promote_symtype given a function and symtypes of its arguments returns the symtype of the result, SymbolicUtils.promote_shape does the same for the shapes of the arguments. Implementing both methods is cruicial for correctly using custom functions in symbolic expressions. Without promote_shape, SymbolicUtils will use Unknown(-1) as the shape.
The array algebra implemented aims to mimic that of base Arrays as closely as possible. For example, a symbolic adjoint(::Vector) * (::Vector) will return a symbolic scalar instead of a one-element symbolic vector. promote_shape implementations will propagate the shape information on a best-effort basis. Invalid shapes (such as attempting to multiply a 3-dimensional array) will error. Following are notable exceptions to Base-like behavior:
mapandmapreducerequire that all input arrays have the sameshapepromote_symtypeandpromote_shapeis not implemented formapandmapreduce, since doing so requires the function(s) passed tomapandmapreduceinstead of their types or shapes.- Since
ndimsinformation is not present in the type,eachindex,iterate,size,axes,ndims,collectare type-unstable.SymbolicUtils.stable_eachindexis useful as a type-stable iteration alternative. ifelserequires that both the true and false cases have identical shape.- Symbolic arrays only support cartesian indexing. For example, given
@syms x[1:3, 1:3]accessingx[4]is invalid andx[1, 2]should be used. Valid indices areInt,Colon,AbstractRange{Int}and symbolic expressions with integersymtype. A singleCartesianIndexof appropriate dimension can also be used to symbolically index arrays.
Symbolic array operations are also supported on arrays of symbolics. However, at least one of the arguments to the function must be a symbolic (instead of an array of symbolics) to allow the dispatches defined in SymbolicUtils to be targeted instead of those in Base. To aid in constructing arrays of symbolics, the BS utility is provided. Similar to the T[...] syntax for constructing an array of etype T, BS[...] will construct an array of BasicSymbolics. At least one value in the array must be a symbolic value to infer T in Array{BasicSymbolic{T}, N}. To explicitly specify the vartype, use BS{T}[...].
Symbolic functions and dependent variables
SymbolicUtils defines FnType{A, R, T} for symbolic functions and dependent variables. Here, A is a Tuple{...} of the symtypes of arguments and R is the type returned by the symbolic function. T is the type that the function itself subtypes, or Nothing.
The syntax
@syms f(::T1, ::T2)::Rcreates f with a symtype of FnType{Tuple{T1, T2}, R, Nothing}. This is a symbolic function taking arguments of type T1 and T2, and returning R. Nothing is a sentinel indicating that the supertype of the function is unknown. By contrast,
@syms f(..)::Rcreates f with a symtype of FnType{Tuple, R, Nothing}. SymbolicUtils considers this case to be a dependent variable with as-yet unspecified independent variables. In other words,
@syms x f1(::Real)::Real f2(..)::RealHere, f1(x) is considered a symbolic function f1 called with the argument x and f2(x) is considered a dependent variable that depends on x. The utilities SymbolicUtils.is_function_symbolic, SymbolicUtils.is_function_symtype, SymbolicUtils.is_called_function_symbolic can be used to differentiate between these cases.
API
Basics
SymbolicUtils.BasicSymbolic — TypeAlias for `SymbolicUtils.BasicSymbolicImpl.Type`.SymbolicUtils.@syms — Macro@syms <lhs_expr>[::T1] <lhs_expr>[::T2]...For instance:
@syms foo::Real bar baz(x, y::Real)::ComplexCreate one or more variables. <lhs_expr> can be just a symbol in which case it will be the name of the variable, or a function call in which case a function-like variable which has the same name as the function being called. The Sym type, or in the case of a function-like Sym, the output type of calling the function can be set using the ::T syntax.
Examples:
@syms foo bar::Real baz::Intwill create
variable foo of symtype Number (the default), bar of symtype Real and baz of symtype Int
@syms f(x) g(y::Real, x)::Int h(a::Int, f(b))creates 1-argf2-argg
and 2 arg h. The second argument to h must be a one argument function-like variable. So, h(1, g) will fail and h(1, f) will work.
Formal syntax
Following is a semi-formal CFG of the syntax accepted by this macro:
# any variable accepted by this macro must be a `var`.
# `var` can represent a quantity (`value`) or a function `(fn)`.
var = value | fn
# A `value` is represented as a name followed by a suffix
value = name suffix
# A `name` can be a valid Julia identifier
name = ident |
# Or it can be an interpolated variable, in which case `ident` is assumed to refer to
# a variable in the current scope of type `Symbol` containing the name of this variable.
# Note that in this case the created symbolic variable will be bound to a randomized
# Julia identifier.
"$" ident |
# Or it can be of the form `Foo.Bar.baz` referencing a value accessible as `Foo.Bar.baz`
# in the current scope.
getproperty_literal
getproperty_literal = ident "." getproperty_literal | ident "." ident
# The `suffix` can be empty (no suffix) which defaults the type to `Number`
suffix = "" |
# or it can be a type annotation (setting the type of the prefix). The shape of the result
# is inferred from the type as best it can be. In particular, `Array{T, N}` is inferred
# to have shape `Unknown(N)`.
"::" type |
# or it can be a shape annotation, which sets the shape to the one specified by `ranges`.
# The type defaults to `Array{Number, length(ranges)}`
"[" ranges "]" |
# lastly, it can be a combined shape and type annotation. Here, the type annotation
# sets the `eltype` of the symbolic array.
"[" ranges "]::" type
# `ranges` is either a single `range` or a single range followed by one or more `ranges`.
ranges = range | range "," ranges
# A `range` is simply two bounds separated by a colon, as standard Julia ranges work.
# The range must be non-empty. Each bound can be a literal integer or an identifier
# representing an integer in the current scope.
range = (int | ident) ":" (int | ident) |
# Alternatively, a range can be a Julia expression that evaluates to a range. All identifiers
# used in `expr` are assumed to exist in the current scope.
expr |
# Alternatively, a range can be a Julia expression evaluating to an iterable of ranges,
# followed by the splat operator.
expr "..."
# A function is represented by a function-call syntax `fncall` followed by the `suffix`
# above. The type and shape from `suffix` represent the type and shape of the value
# returned by the symbolic function.
fn = fncall suffix
# a function call is a call `head` followed by a parenthesized list of arguments.
fncall = head "(" args ")"
# A function call head can be a name, representing the name of the symbolic function.
head = ident |
# Alternatively, it can be a parenthesized type-annotated name, where the type annotation
# represents the intended supertype of the function. In other words, if this symbolic
# function were to be replaced by an "actual" function, the type-annotation constrains the
# type of the "actual" function.
"(" ident "::" type ")"
# Arguments to a function is a list of one or more arguments
args = arg | arg "," args
# An argument can take the syntax of a variable (which means we can represent functions of
# functions of functions of...). The type of the variable constrains the type of the
# corresponding argument of the function. The name and shape information is discarded.
arg = var |
# Or an argument can be an unnamed type-annotation, which constrains the type without
# requiring a name.
"::" type |
# Or an argument can be the identifier `..`, which is used as a stand-in for `Vararg{Any}`
".." |
# Or an argument can be a type-annotated `..`, representing `Vararg{type}`. Note that this
# and the previous version of `arg` can only be the last element in `args` due to Julia's
# `Tuple` semantics.
"(..)::" type |
# Or an argument can be a Julia expression followed by a splat operator. This assumes the
# expression evaluates to an iterable of symbolic variables whose `symtype` should be used
# as the argument types. Note that `expr` may be evaluated multiple times in the macro
# expansion.
expr "..."SymbolicUtils.symtype — Functionsymtype(
x::SymbolicUtils.BasicSymbolicImpl.var"typeof(BasicSymbolicImpl)"{T} where T
) -> DataType
Return the Julia type that the given symbolic expression x represents. Can also be called on non-symbolic values, in which case it is equivalent to typeof.
SymbolicUtils.vartype — Functionvartype(x)defined at /home/runner/work/SymbolicUtils.jl/SymbolicUtils.jl/src/types.jl:331.
vartype(_)defined at /home/runner/work/SymbolicUtils.jl/SymbolicUtils.jl/src/types.jl:332.
Extract the variant type of a BasicSymbolic.
SymbolicUtils.SymReal — Typeabstract type SymReal <: SymbolicUtils.SymVariantOne of three possible values of the vartype. This variant is the default and behaves as a typical ideal scalar algebra would be expected to.
SymbolicUtils.SafeReal — Typeabstract type SafeReal <: SymbolicUtils.SymVariantOne of three possible values of the vartype. This variant is identical to SymReal except common terms in the numerator and denominator of a division are not cancelled out.
SymbolicUtils.TreeReal — Typeabstract type TreeReal <: SymbolicUtils.SymVariantOne of three possible values of the vartype. This variant does not assume anything about the algebra and always uses the default tree form for expressions.
SymbolicUtils.shape — Functionshape(x)Get the shape of a value or symbolic expression. Generally equivalent to axes for non-symbolic x, but also works on non-array values.
SymbolicUtils.Unknown — Typestruct UnknownA struct used as the shape of symbolic expressions with unknown size.
Fields
ndims::Int64: An integer >= -1 indicating the number of dimensions of the symbolic expression of unknown size. A value of-1indicates the number of dimensions is also unknown.
SymbolicUtils.AddMulVariant — ModuleAn EnumX.jl enum used to distinguish between addition and multiplication in SymbolicUtils.BSImpl.AddMul.
SymbolicUtils.unwrap_const — Functionunwrap_const(x) -> Any
Extract the constant value from a Const variant, or return the input unchanged.
Arguments
x: Any value, potentially aBasicSymbolicwith aConstvariant
Returns
- The wrapped constant value if
xis aConstvariant ofBasicSymbolic - The input
xunchanged otherwise
Details
This function unwraps constant symbolic expressions to their underlying values. It handles all three symbolic variants (SymReal, SafeReal, TreeReal). For non-Const symbolic expressions or non-symbolic values, returns the input unchanged.
Inner constructors
SymbolicUtils.array_literal — Functionarray_literal(sz::NTuple{N, Int64}, args...) -> Any
Utility function used as the operation of expressions representing an array of symbolic values. See SymbolicUtils.BSImpl.Const for more details.
The first argument sz is the size of the represented array. args... is prod(sz) elements representing the elements of the array in column-major order.
SymbolicUtils.BasicSymbolicImpl.Const — TypeBSImpl.Const{T}(val) where {T}Constructor for a symbolic expression that wraps a constant value val. Also converts arrays/tuples of symbolics to symbolic expressions.
Arguments
val: The value to wrap (can be any type including arrays and tuples)
Returns
BasicSymbolic{T}: AConstvariant or specialized representation
Details
This is the low-level constructor for constant expressions. It handles several special cases:
- If
valis already aBasicSymbolic{T}, returns it unchanged - If
valis aBasicSymbolicof a different variant type, throws an error - If
valis an array containing symbolic elements, creates aTermwithSymbolicUtils.array_literaloperation - If
valis a tuple containing symbolic elements, creates aTermwithtupleoperation - Otherwise, creates a
Constvariant wrapping the value
Extended help
The unsafe flag skips hash consing for performance in internal operations.
SymbolicUtils.BasicSymbolicImpl.Sym — TypeBSImpl.Sym{T}(name::Symbol; metadata = nothing, type, shape = default_shape(type)) where {T}Internal constructor for symbolic variables.
Arguments
name::Symbol: The name of the symbolic variablemetadata: Optional metadata dictionary (default:nothing)type: The symbolic type of the variable (required keyword argument)shape: The shape of the variable (default: inferred fromtype)
Returns
BasicSymbolic{T}: ASymvariant representing the symbolic variable
Details
This is the low-level constructor for symbolic variables. It normalizes the metadata and shape inputs, populates default properties using ordered_override_properties, and performs hash consing. The type parameter determines the Julia type that this symbolic variable represents.
Extended help
The unsafe keyword argument (default: false) can be used to skip hash consing for performance in internal operations.
SymbolicUtils.BasicSymbolicImpl.Term — TypeBSImpl.Term{T}(f, args; metadata = nothing, type, shape = default_shape(type)) where {T}Internal constructor for function application terms.
Arguments
f: The function or operation to applyargs: The arguments to the function (normalized toArgsT{T})metadata: Optional metadata dictionary (default:nothing)type: The result type of the function application (required keyword argument)shape: The shape of the result (default: inferred fromtype)
Returns
BasicSymbolic{T}: ATermvariant representing the function application
Details
This is the low-level constructor for function application expressions. It represents f(args...) symbolically. The constructor normalizes metadata, shape, and arguments, populates default properties, and performs hash consing. The type parameter should be the expected return type of calling f with args.
Extended help
The unsafe keyword argument (default: false) can be used to skip hash consing for performance in internal operations.
SymbolicUtils.BasicSymbolicImpl.AddMul — TypeBSImpl.AddMul{T}(coeff, dict, variant::AddMulVariant.T; metadata = nothing, type, shape = default_shape(type)) where {T}Internal constructor for addition and multiplication expressions.
Arguments
coeff: The leading coefficient (for addition) or coefficient (for multiplication)dict: Dictionary mapping terms to their coefficients/exponents (normalized toACDict{T})variant::AddMulVariant.T: EitherAddMulVariant.ADDorAddMulVariant.MULmetadata: Optional metadata dictionary (default:nothing)type: The result type of the operation (required keyword argument)shape: The shape of the result (default: inferred fromtype)
Returns
BasicSymbolic{T}: AnAddMulvariant representing the sum or product
Details
This is the low-level constructor for optimized addition and multiplication expressions. For addition, represents coeff + sum(k * v for (k, v) in dict). For multiplication, represents coeff * prod(k ^ v for (k, v) in dict). The constructor normalizes all inputs and performs hash consing.
Extended help
The unsafe keyword argument (default: false) can be used to skip hash consing for performance in internal operations.
SymbolicUtils.BasicSymbolicImpl.Div — TypeBSImpl.Div{T}(num, den, simplified::Bool; metadata = nothing, type, shape = default_shape(type)) where {T}Internal constructor for division expressions.
Arguments
num: The numerator (converted toConst{T})den: The denominator (converted toConst{T})simplified::Bool: Whether the division has been simplifiedmetadata: Optional metadata dictionary (default:nothing)type: The result type of the division (required keyword argument)shape: The shape of the result (default: inferred fromtype)
Returns
BasicSymbolic{T}: ADivvariant representing the division
Details
This is the low-level constructor for division expressions. It represents num / den symbolically. Both numerator and denominator are automatically wrapped in Const{T} if not already symbolic. The simplified flag tracks whether simplification has been attempted. The constructor normalizes all inputs and performs hash consing.
Extended help
The unsafe keyword argument (default: false) can be used to skip hash consing for performance in internal operations.
SymbolicUtils.BasicSymbolicImpl.ArrayOp — TypeBSImpl.ArrayOp{T}(output_idx, expr::BasicSymbolic{T}, reduce, term, ranges = default_ranges(T); metadata = nothing, type, shape = default_shape(type)) where {T}Internal constructor for array operation expressions.
Arguments
output_idx: Output indices defining the result array dimensions (normalized toOutIdxT{T})expr::BasicSymbolic{T}: The expression to evaluate for each index combinationreduce: Reduction operation to apply (ornothingfor direct assignment)term: Optional term for accumulation (ornothing)ranges: Dictionary mapping index variables to their ranges (default: empty)metadata: Optional metadata dictionary (default:nothing)type: The result type (required keyword argument, typically an array type)shape: The shape of the result (default: inferred fromtype)
Returns
BasicSymbolic{T}: AnArrayOpvariant representing the array operation
Details
This is the low-level constructor for array comprehension-like operations. It represents operations like [expr for i in range1, j in range2] with optional reduction. The constructor normalizes all inputs, unwraps constants where appropriate, and optionally performs hash consing.
The ArrayOp constructor should be strongly preferred.
Extended help
The unsafe keyword argument (default: false) can be used to skip hash consing for performance in internal operations.
High-level constructors
SymbolicUtils.Const — TypeConst{T}(val) where {T}Alias for BSImpl.Const{T}.
SymbolicUtils.Sym — TypeSym{T}(name; kw...) where {T}Alias for BSImpl.Sym{T}.
SymbolicUtils.Term — TypeTerm{T}(f, args; type = _promote_symtype(f, args), kw...) where {T}Alias for BSImpl.Term{T} except it also unwraps args.
SymbolicUtils.Add — TypeAdd{T}(coeff, dict; kw...) where {T}High-level constructor for addition expressions.
Arguments
coeff: The constant term (additive offset)dict: Dictionary mapping terms to their coefficientskw...: Additional keyword arguments (e.g.,type,shape,metadata,unsafe)
Returns
BasicSymbolic{T}: An optimized representation ofcoeff + sum(k * v for (k, v) in dict)
Details
This constructor maintains invariants required by the AddMul variant. This should be preferred over the BSImpl.AddMul{T} constructor.
SymbolicUtils.Mul — TypeMul{T}(coeff, dict; kw...) where {T}High-level constructor for multiplication expressions.
Arguments
coeff: The multiplicative coefficientdict: Dictionary mapping base terms to their exponentskw...: Additional keyword arguments (e.g.,type,shape,metadata,unsafe)
Returns
BasicSymbolic{T}: An optimized representation ofcoeff * prod(k ^ v for (k, v) in dict)
Details
This constructor maintains invariants required by the AddMul variant. This should be preferred over the BSImpl.AddMul{T} constructor.
SymbolicUtils.Div — TypeDiv{T}(n, d, simplified; type = promote_symtype(/, symtype(n), symtype(d)), kw...) where {T}High-level constructor for division expressions with simplification.
Arguments
n: The numeratord: The denominatorsimplified::Bool: Whether simplification has been attemptedtype: The result type (default: inferred usingpromote_symtype)kw...: Additional keyword arguments (e.g.,shape,metadata,unsafe)
Returns
BasicSymbolic{T}: An optimized representation ofn / d
Details
This constructor creates symbolic division expressions with extensive simplification:
- Zero numerator returns zero
- Unit denominator returns the numerator
- Zero denominator returns
Const{T}(1 // 0)(infinity). Any infinity may be returned. - Nested divisions are flattened
- Constant divisions are evaluated
- Rational coefficients are simplified
- Multiplications in numerator/denominator are handled specially
For non-SafeReal variants, automatic cancellation is attempted using quick_cancel. The simplified flag prevents infinite simplification loops.
SymbolicUtils.ArrayOp — TypeArrayOp{T}(output_idx, expr, reduce, term, ranges; metadata = nothing) where {T}High-level constructor for array operation expressions.
Arguments
output_idx: Output indices defining result dimensionsexpr: Expression to evaluate for each index combinationreduce: Reduction operation (ornothing)term: Optional accumulation term (ornothing)ranges: Dictionary mapping index variables to rangesmetadata: Optional metadata (default:nothing)
Returns
BasicSymbolic{T}: AnArrayOprepresenting the array comprehension
Details
This constructor validates and parses fields of the ArrayOp variant. It is usually never called directly. Prefer using the @arrayop macro.
Extended help
The unsafe keyword argument (default: false) can be used to skip hash consing for performance in internal operations.
SymbolicUtils.@arrayop — Macro@arrayop (idxs...,) expr [idx in range, ...] [options...]Construct a vectorized array operation using Tullio.jl-like generalized Einstein notation. idxs is a tuple corresponding to the indices in the result. expr is the indexed expression. Indices used in expr not present in idxs will be collapsed using the reduce function, which defaults to +. For example, matrix multiplication can be expressed as follows:
@syms A[1:5, 1:5] B[1:5, 1:5]
matmul = @arrayop (i, j) A[i, k] * B[k, j]Here the elements of the collapsed dimension k are reduced using the + operation. To use a different reducer, the reduce option can be supplied:
C = @arrayop (i, j) A[i, k] * B[k, j] reduce=maxNow, C[i, j] is the maximum value of A[i, k] * B[k, j] for across all k.
Singleton dimensions
Arbitrary singleton dimensions can be added in the result by inserting 1 at the desired position in idxs:
C = @arrayop (i, 1, j, 1) A[i, k] * B[k, j]Here, C is a symbolic array of size (5, 1, 5, 1).
Restricted ranges
For any index variable i in expr, all its usages in expr must correspond to axes of identical length. For example:
@syms D[1:3, 1:5]
@arrayop (i, j) A[i, k] * D[k, j]The above usage is invalid, since k in A is used to index an axis of length 5 and in D is used to index an axis of length 3. The iteration range of variables can be manually restricted:
@arrayop (i, j) A[i, k] * D[k, j] k in 1:3This expression is valid. Note that when manually restricting iteration ranges, the range must be a subset of the axes where the iteration variable is used. Here 1:3 is a subset of both 1:5 and 1:3.
Axis offsets
The usages of index variables can be offset.
A2 = @arrayop (i, j) A[i + 1, j] + A[i, j + 1]Here, A2 will have size (4, 4) since SymbolicUtils.jl is able to recognize that i and j can only iterate in the range 1:4. For trivial offsets of the form idx + offset (offset can be negative), the bounds of idx can be inferred. More complicated offsets can be used, but this requires manually specifying ranges of the involved index variables.
A3 = @arrayop (i, j) A[2i - 1, j] i in 1:3In this scenario, it is the responsibility of the user to ensure the arrays are always accessed within their bounds.
Usage with non-standard axes
The index variables are "idealized" indices. This means that as long as the length of all axes where an index variable is used is identical, the bounds of the axes are irrelevant.
@syms E[0:4, 0:4]
F = @arrayop (i, j) A[i, k] * E[k, j]Despite axes(A, 2) being 1:5 and axes(E, 1) being 0:4, the above expression is valid since length(1:5) == 5 == length(0:4). When generating code, index variables will be appropriately offset to index the involved axes.
If the range of an index variable is manually specified, the index variable is no longer "idealized" and the user is responsible for offsetting appropriately. The above example with a manual range for k should be written as:
F2 = @arrayop (i, j) A[i, k] * E[k - 1, j] k in 1:5Result shape
The result is always 1-indexed with axes of appropriate lengths, regardless of the shape of the inputs.
Variant checking
Note that while these utilities are useful, prefer using Moshi.Match.@match to pattern match against different variant types and access their fields.
SymbolicUtils.isconst — Functionisconst(
x::SymbolicUtils.BasicSymbolicImpl.var"typeof(BasicSymbolicImpl)"{T} where T
) -> Bool
Check if a value is a Const variant of BasicSymbolic.
Arguments
x: Value to check (forBasicSymbolicinput returns true ifConst, for others returns false)
Returns
trueifxis aBasicSymbolicwithConstvariant,falseotherwise
SymbolicUtils.issym — Functionissym(
x::SymbolicUtils.BasicSymbolicImpl.var"typeof(BasicSymbolicImpl)"{T} where T
) -> Bool
Check if a value is a Sym variant of BasicSymbolic.
Arguments
x: Value to check (forBasicSymbolicinput returns true ifSym, for others returns false)
Returns
trueifxis aBasicSymbolicwithSymvariant,falseotherwise
SymbolicUtils.isterm — Functionisterm(
x::SymbolicUtils.BasicSymbolicImpl.var"typeof(BasicSymbolicImpl)"{T} where T
) -> Bool
Check if a value is a Term variant of BasicSymbolic.
Arguments
x: Value to check (forBasicSymbolicinput returns true ifTerm, for others returns false)
Returns
trueifxis aBasicSymbolicwithTermvariant,falseotherwise
SymbolicUtils.isaddmul — Functionisaddmul(
x::SymbolicUtils.BasicSymbolicImpl.var"typeof(BasicSymbolicImpl)"{T} where T
) -> Bool
Check if a value is an AddMul variant of BasicSymbolic.
Arguments
x: Value to check (forBasicSymbolicinput returns true ifAddMul, for others returns false)
Returns
trueifxis aBasicSymbolicwithAddMulvariant,falseotherwise
SymbolicUtils.isadd — Functionisadd(
x::SymbolicUtils.BasicSymbolicImpl.var"typeof(BasicSymbolicImpl)"{T} where T
) -> Bool
Check if a value is an addition (AddMul with ADD variant).
Arguments
x: Value to check (forBasicSymbolicinput returns true if addition, for others returns false)
Returns
trueifxis anAddMulwithADDvariant,falseotherwise
SymbolicUtils.ismul — Functionismul(
x::SymbolicUtils.BasicSymbolicImpl.var"typeof(BasicSymbolicImpl)"{T} where T
) -> Bool
Check if a value is a multiplication (AddMul with MUL variant).
Arguments
x: Value to check (forBasicSymbolicinput returns true if multiplication, for others returns false)
Returns
trueifxis anAddMulwithMULvariant,falseotherwise
SymbolicUtils.isdiv — Functionisdiv(
x::SymbolicUtils.BasicSymbolicImpl.var"typeof(BasicSymbolicImpl)"{T} where T
) -> Bool
Check if a value is a Div variant of BasicSymbolic.
Arguments
x: Value to check (forBasicSymbolicinput returns true ifDiv, for others returns false)
Returns
trueifxis aBasicSymbolicwithDivvariant,falseotherwise
SymbolicUtils.ispow — Functionispow(
x::SymbolicUtils.BasicSymbolicImpl.var"typeof(BasicSymbolicImpl)"{T} where T
) -> Bool
Check if a value is a power expression (Term with ^ operation).
Arguments
x: Value to check (forBasicSymbolicinput returns true if power, for others returns false)
Returns
trueifxis aTermwith exponentiation operation,falseotherwise
Details
Power expressions are Term variants where the operation is ^ (6 uses).
SymbolicUtils.isarrayop — Functionisarrayop(
x::SymbolicUtils.BasicSymbolicImpl.var"typeof(BasicSymbolicImpl)"{T} where T
) -> Bool
Check if a value is an ArrayOp variant of BasicSymbolic.
Arguments
x: Value to check (forBasicSymbolicinput returns true ifArrayOp, for others returns false).
Returns
trueifxis aBasicSymbolicwithArrayOpvariant,falseotherwise.
Details
Array operations represent vectorized computations created by the @arrayop macro.
Using custom functions in expressions
SymbolicUtils.promote_symtype — Functionpromote_symtype(f, Ts...) -> Type{Bool}
The result of applying f to arguments of SymbolicUtils.symtype Ts...
julia> promote_symtype(+, Real, Real)
Real
julia> promote_symtype(+, Complex, Real)
Number
julia> @syms f(x)::Complex
(f(::Number)::Complex,)
julia> promote_symtype(f, Number)
ComplexWhen constructing expressions without an explicit symtype, promote_symtype is used to figure out the symtype of the Term.
It is recommended that all type arguments be annotated with SymbolicUtils.TypeT and one method be implemented for any combination of f and the number of arguments. For example, one method is implemented for unary - and one method for binary -. Each method has an if..elseif chain to handle possible types. Any call to promote_type should be typeasserted with ::TypeT.
promote_symtype(f::FnType{X,Y}, arg_symtypes...)The output symtype of applying variable f to arguments of symtype arg_symtypes.... if the arguments are of the wrong type then this function will error.
SymbolicUtils.promote_shape — Functionpromote_shape(f, shs::ShapeT...)The shape of the result of applying f to arguments of shape shs.... It is recommended that implemented methods @nospecialize all the shape arguments.
Symbolic array utilities
SymbolicUtils.stable_eachindex — Functionstable_eachindex(
x::SymbolicUtils.BasicSymbolicImpl.var"typeof(BasicSymbolicImpl)"{T} where T
) -> SymbolicUtils.StableIndices
Returns a type-stable iterator over all indices of a symbolic array x.
This function provides an efficient, allocation-friendly way to iterate over multi-dimensional symbolic arrays. Unlike Base.eachindex, which returns CartesianIndices with type parameters that may be uninferrable for symbolic arrays, stable_eachindex returns a StableIndices iterator that produces StableIndex values in a fully type-stable manner.
Note that the returned iterator does not match the shape of x. In other words, collect(stable_eachindex(x)) will be a vector regardless of the shape of x.
Arguments
x::BasicSymbolic: A symbolic array expression with a known concrete shape.
Returns
StableIndices: An iterator that yieldsStableIndexvalues for each position in the array.
Throws
- This function assumes
xhas a concrete shape (i.e.,shape(x)is aShapeVecT, notUnknown). If the shape is unknown, it will error.
Examples
using SymbolicUtils
# Create a symbolic 2×3 matrix
@variables x[1:2, 1:3]
# Iterate over all indices in a type-stable manner
for idx in stable_eachindex(x)
println("Index: ", idx, " -> Value: ", x[idx])
end
# Compare with regular eachindex
for idx in eachindex(x) # Returns CartesianIndices
println("Index: ", idx, " -> Value: ", x[idx])
endSee also
StableIndices: The iterator type returned by this functionStableIndex: The index type produced byStableIndicesBase.eachindex: The standard Julia function for iterating over array indices
SymbolicUtils.StableIndices — Typestruct StableIndicesAn iterator that produces StableIndex values representing all possible multi-dimensional indices for a given shape in a type-stable, allocation-efficient manner.
This type is used to iterate over multi-dimensional index spaces where each dimension can have its own range (stored in sh). The iterator produces all combinations of indices in column-major order, similar to CartesianIndices, but with better type stability and allocation characteristics.
This is similar to CartesianIndices for symbolic arrays, but avoids type-instability due to the type parameters of CartesianIndices being uninferrable. Note that iterator iterates over multidimensional indices, but is not a multidimensional iterator. In other words, collecting this iterator will return a vector regardless of the number of dimensions it iterates over.
Fields
sh::SymbolicUtils.SmallVec{UnitRange{Int64}, Vector{UnitRange{Int64}}}: A small vector ofUnitRange{Int}values, one for each dimension, defining the range of valid indices for that dimension.
Examples
sh = ShapeVecT([1:2, 1:3])
indices = StableIndices(sh)
for idx in indices
# idx is a StableIndex with values like [1,1], [1,2], [1,3], [2,1], [2,2], [2,3]
endSee also
StableIndex: The index type produced by this iterator.stable_eachindex: Convenience function that returns aStableIndicesiterator for a symbolic array.
SymbolicUtils.StableIndex — Typestruct StableIndexA wrapper around a small vector of integer indices that provides a stable, allocation-efficient representation of multi-dimensional array indices.
This type is used in conjunction with StableIndices to iterate over multi-dimensional index spaces in a type-stable manner. It implements the standard iteration and indexing interfaces.
This is effectively equivalent to CartesianIndex for symbolic arrays, but avoids type-instability due to N in CartesianIndex{N} being uninferrable.
Fields
idxs::SymbolicUtils.SmallVec{Int64, Vector{Int64}}: A small vector storing the indices for each dimension.
See also
StableIndices: An iterator that producesStableIndexvalues.stable_eachindex: Returns aStableIndicesiterator for a symbolic array.
SymbolicUtils.BS — TypeBS[...]
BS{T}[...]BS is a utility defined in SymbolicUtils for constructing arrays of symbolics. Similar to how T[...] creates an Array of eltype T, BS[...] creates an array of eltype BasicSymbolic{T}. To infer the vartype of the result, at least one of the values in ... must be a symbolic. BS{T}[...] can be used to explicitly specify the vartype.
Symbolic function utilities
SymbolicUtils.is_function_symbolic — Functionis_function_symbolic(
x::SymbolicUtils.BasicSymbolicImpl.var"typeof(BasicSymbolicImpl)"{T} where T
) -> Bool
Check if x is a symbolic representing a function (as opposed to a dependent variable). A symbolic function either has a defined signature or the function type defined. For example, all of the below are considered symbolic functions:
@syms f(::Real, ::Real) g(::Real)::Integer h(::Real)[1:2]::Integer (ff::MyCallableT)(..)However, the following is considered a dependent variable with unspecified independent variable:
@syms x(..)See also: SymbolicUtils.is_function_symtype.
SymbolicUtils.is_function_symtype — Functionis_function_symtype(_::Type{T}) -> Bool
Check if the given symtype represents a function (as opposed to a dependent variable).
See also: SymbolicUtils.is_function_symbolic.
SymbolicUtils.is_called_function_symbolic — Functionis_called_function_symbolic(
x::SymbolicUtils.BasicSymbolicImpl.var"typeof(BasicSymbolicImpl)"{T}
) -> Bool
Check if the given symbolic x is the result of calling a symbolic function (as opposed to a dependent variable).
See also: SymbolicUtils.is_function_symbolic.
TermInterface.jl interface
Missing docstring for TermInterface.iscall. Check Documenter's build log for details.
Missing docstring for TermInterface.operation. Check Documenter's build log for details.
Missing docstring for TermInterface.arguments. Check Documenter's build log for details.
Missing docstring for TermInterface.sorted_arguments. Check Documenter's build log for details.
Missing docstring for TermInterface.maketerm. Check Documenter's build log for details.
Miscellaneous utilities
SymbolicUtils.zero_of_vartype — Functionzero_of_vartype(
_::Type{SymReal}
) -> SymbolicUtils.BasicSymbolicImpl.var"typeof(BasicSymbolicImpl)"{SymReal}
Return a Const representing 0 with the provided vartype.
SymbolicUtils.one_of_vartype — Functionone_of_vartype(
_::Type{SymReal}
) -> SymbolicUtils.BasicSymbolicImpl.var"typeof(BasicSymbolicImpl)"{SymReal}
Return a Const representing 1 with the provided vartype.
SymbolicUtils.get_mul_coefficient — Functionget_mul_coefficient(x) -> Any
Extract the numeric coefficient from a multiplication expression.
Arguments
x: A symbolic expression that must be a multiplication
Returns
- The numeric coefficient of the multiplication
Details
This function extracts the leading numeric coefficient from a multiplication expression. For Term variants, it recursively searches for nested multiplications. For AddMul variants with MUL operation, it returns the stored coefficient. Throws an error if the input is not a multiplication expression.
SymbolicUtils.term — Functionterm(f, args...; vartype = SymReal, type = promote_symtype(f, symtype.(args)...), shape = promote_shape(f, SymbolicUtils.shape.(args)...))Create a symbolic term with operation f and arguments args.
Arguments
f: The operation or function head of the termargs...: The arguments to the operationvartype: The variant type for the term (default:SymReal)type: The symbolic type of the term. If not provided, it is inferred usingpromote_symtypeon the function and argument types.shape: The shape of the term. If not provided, it is inferred usingpromote_shapeon the function and argument shapes.
Examples
julia> @syms x y
(x, y)
julia> term(+, x, y)
x + y
julia> term(sin, x)
sin(x)
julia> term(^, x, 2)
x^2SymbolicUtils.add_worker — Functionadd_worker(_, terms)defined at /home/runner/work/SymbolicUtils.jl/SymbolicUtils.jl/src/types.jl:3030.
Add an indexable list or tuple of terms terms with the given vartype. Applicable only for symbolic expressions with numeric or array of numeric symtype.
SymbolicUtils.mul_worker — Functionmul_worker(_, terms)defined at /home/runner/work/SymbolicUtils.jl/SymbolicUtils.jl/src/types.jl:3508.
Multiply an indexable list or tuple of terms terms with the given vartype. Applicable only for symbolic expressions with numeric or array of numeric symtype.
Utility types
SymbolicUtils exposes a plethora of type aliases to allow easily interacting with common types used internally.
SymbolicUtils.MetadataT — TypeType of metadata field for symbolics.
SymbolicUtils.SmallV — Typemutable struct SmallVec{T, Array{T, 1}} <: AbstractArray{T, 1}A custom vector type which does not allocate for small numbers of elements. If the number of elements is known at compile time, it should be passed as a Tuple to the constructor.
SymbolicUtils.ShapeVecT — TypeA small-buffer-optimized AbstractVector. Uses a Backing when the number of elements is within the size of Backing, and allocates a V when the number of elements exceed this limit.
SymbolicUtils.ShapeT — TypeType that represents the SymbolicUtils.shape of symbolics.
SymbolicUtils.TypeT — Typemutable struct DataType <: Type{T}Allowed types for the SymbolicUtils.symtype of symbolics.
SymbolicUtils.ArgsT — TypeThe type of a mutable buffer containing symbolic arguments. Passing this to the
[`SymbolicUtils.Term`](@ref) constructor will avoid allocating a new array.SymbolicUtils.ROArgsT — TypeThe type of a read-only buffer containing symbolic arguments. Passing this to the
[`SymbolicUtils.Term`](@ref) constructor will avoid allocating a new array. This is
the type returned from [`TermInterface.arguments`](@ref).SymbolicUtils.ACDict — TypeThe type of the dictionary stored in [`BSImpl.AddMul`](@ref). Passing this to the
[`SymbolicUtils.Add`](@ref) or [`SymbolicUtils.Mul`](@ref) constructors will avoid
allocating a new dictionary.SymbolicUtils.OutIdxT — TypeThe type of the `output_idxs` field in [`BSImpl.ArrayOp`](@ref).SymbolicUtils.RangesT — TypeThe type of the `ranges` field in [`BSImpl.ArrayOp`](@ref).