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 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 theTerm
variant to represent an expression.
A given expression must be pure in its vartype
. In other words, no operation supports operands of different vartype
s.
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] z
All 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
# ...
end
Any 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 hvncat
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
# ...
end
Sym
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.
metadata
is eithernothing
or a map fromDataType
keys to arbitrary values. Any
interaction with metadata should be done by providing such a mapping during construction or using getmetadata
, setmetadata
, hasmetadata
.
type
is a Julia type.shape
is 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
# ...
end
Term
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
# ...
end
AddMul
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:
coeff
must be non-symbolic.- The values of
dict
must be non-symbolic. - The keys of
dict
must not be expressions with^
as the operation UNLESS the exponent is symbolic. For example,x^x * y^2
is represented withdict = ACDict{T}((x^x) => 1, y => 2)
. dict
must not be empty.coeff
must not be zero.- If
dict
has only one element,coeff
must not be one. Such a case should be represented as a power term (with^
as the operation). - If
dict
has only one element where the key is an addition,coeff
must 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:
coeff
must be non-symbolic.- The values of
dict
must be non-symbolic. - The keys of
dict
must not be additions expressions represented withAddMul
. dict
must not be empty.- If
dict
has only one element,coeff
must 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
# ...
end
The 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
Const
variants. This should instead be represented as aConst
variant wrapping the result of division. - The numerator cannot be zero. This should instead be represented as a
Const
wrapping 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
Const
with 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
end
ArrayOp
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_idx
only contains the integer1
or variants of the common global index variable.- Any top-level indexing operations in
expr
use 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
. reduce
must be a valid reduction operation that can be passed toBase.reduce
.- If
term
is notnothing
, it must be an expression with shapeshape
and typetype
. - The keys of
ranges
must be variants of the common global index variable, and must be present inexpr
.
The @arrayop
macro should be heavily preferred for creating ArrayOp
s. 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 Array
s 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:
map
andmapreduce
require that all input arrays have the sameshape
promote_symtype
andpromote_shape
is not implemented formap
andmapreduce
, since doing so requires the function(s) passed tomap
andmapreduce
instead of their types or shapes.- Since
ndims
information is not present in the type,eachindex
,iterate
,size
,axes
,ndims
,collect
are type-unstable.safe_eachindex
is useful as a type-stable iteration alternative. ifelse
requires 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 singleCartesianIndex
of 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 BasicSymbolic
s. 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)::R
creates 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(..)::R
creates 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(..)::Real
Here, 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
Missing docstring for SymbolicUtils.BasicSymbolic
. Check Documenter's build log for details.
SymbolicUtils.@syms
— Macro@syms <lhs_expr>[::T1] <lhs_expr>[::T2]...
For instance:
@syms foo::Real bar baz(x, y::Real)::Complex
Create 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::Int
will 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-argf
2-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:260
.
vartype(_)
defined at /home/runner/work/SymbolicUtils.jl/SymbolicUtils.jl/src/types.jl:261
.
Extract the variant type of a BasicSymbolic
.
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 Unknown
A 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-1
indicates the number of dimensions is also unknown.
Missing docstring for SymbolicUtils.AddMulVariant
. Check Documenter's build log for details.
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 aBasicSymbolic
with aConst
variant
Returns
- The wrapped constant value if
x
is aConst
variant ofBasicSymbolic
- The input
x
unchanged 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.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}
: AConst
variant or specialized representation
Details
This is the low-level constructor for constant expressions. It handles several special cases:
- If
val
is already aBasicSymbolic{T}
, returns it unchanged - If
val
is aBasicSymbolic
of a different variant type, throws an error - If
val
is an array containing symbolic elements, creates aTerm
withhvncat
operation - If
val
is a tuple containing symbolic elements, creates aTerm
withtuple
operation - Otherwise, creates a
Const
variant 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}
: ASym
variant 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}
: ATerm
variant 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.ADD
orAddMulVariant.MUL
metadata
: 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}
: AnAddMul
variant 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}
: ADiv
variant 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 (ornothing
for 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}
: AnArrayOp
variant 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}
: AnArrayOp
representing 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=max
Now, 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:3
This 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:3
In 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:5
Result 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 (forBasicSymbolic
input returns true ifConst
, for others returns false)
Returns
true
ifx
is aBasicSymbolic
withConst
variant,false
otherwise
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 (forBasicSymbolic
input returns true ifSym
, for others returns false)
Returns
true
ifx
is aBasicSymbolic
withSym
variant,false
otherwise
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 (forBasicSymbolic
input returns true ifTerm
, for others returns false)
Returns
true
ifx
is aBasicSymbolic
withTerm
variant,false
otherwise
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 (forBasicSymbolic
input returns true ifAddMul
, for others returns false)
Returns
true
ifx
is aBasicSymbolic
withAddMul
variant,false
otherwise
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 (forBasicSymbolic
input returns true if addition, for others returns false)
Returns
true
ifx
is anAddMul
withADD
variant,false
otherwise
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 (forBasicSymbolic
input returns true if multiplication, for others returns false)
Returns
true
ifx
is anAddMul
withMUL
variant,false
otherwise
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 (forBasicSymbolic
input returns true ifDiv
, for others returns false)
Returns
true
ifx
is aBasicSymbolic
withDiv
variant,false
otherwise
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 (forBasicSymbolic
input returns true if power, for others returns false)
Returns
true
ifx
is aTerm
with exponentiation operation,false
otherwise
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 (forBasicSymbolic
input returns true ifArrayOp
, for others returns false).
Returns
true
ifx
is aBasicSymbolic
withArrayOp
variant,false
otherwise.
Details
Array operations represent vectorized computations created by the @arrayop
macro.
Using custom functions in expressions
SymbolicUtils.promote_symtype
— Functionpromote_symtype(f, Ts...)
The result of applying f
to arguments of 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)
Complex
When constructing Term
s without an explicit symtype, promote_symtype
is used to figure out the symtype of the Term.
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...
.
Symbolic array utilities
Missing docstring for SymbolicUtils.safe_eachindex
. Check Documenter's build log for details.
Missing docstring for SymbolicUtils.SafeIndices
. Check Documenter's build log for details.
Missing docstring for SymbolicUtils.SafeIndex
. Check Documenter's build log for details.
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_symtype
on the function and argument types.shape
: The shape of the term. If not provided, it is inferred usingpromote_shape
on 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^2
Missing docstring for SymbolicUtils.add_worker
. Check Documenter's build log for details.
Missing docstring for SymbolicUtils.mul_worker
. Check Documenter's build log for details.
Utility types
SymbolicUtils exposes a plethora of type aliases to allow easily interacting with common types used internally.
Missing docstring for SymbolicUtils.MetadataT
. Check Documenter's build log for details.
Missing docstring for SymbolicUtils.SmallV
. Check Documenter's build log for details.
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.
Missing docstring for SymbolicUtils.ShapeT
. Check Documenter's build log for details.
Missing docstring for SymbolicUtils.TypeT
. Check Documenter's build log for details.
Missing docstring for SymbolicUtils.ArgsT
. Check Documenter's build log for details.
Missing docstring for SymbolicUtils.ROArgsT
. Check Documenter's build log for details.
Missing docstring for SymbolicUtils.ACDict
. Check Documenter's build log for details.