SymbolicUtils.jl

repository·master·Indexed 20 days ago

https://github.com/juliasymbolics/symbolicutils.jl

A low-level utility library for building Computer Algebra Systems (CAS) in Julia. It provides type-aware symbolic expressions, a pattern-matching rule engine using @rule and @acrule, and simplification tools. Key features include a combinator library for composing rewriters, automatic simplification during construction, Common Subexpression Elimination (CSE), and a caching system via the @cache macro. It allows developers to implement custom symbolic engines by interfacing their own types through TermInterface.jl.

Tokens
9.3K
Snippets
27
Records
51
Agent score
69%

What's inside SymbolicUtils.jl

  1. Overview of SymbolicUtils.jl features

    master

    SymbolicUtils.jl provides a high-performance framework for symbolic programming in Julia, featuring:

    • Fast expressions: Efficient representation of symbolic trees.
    • Rule-based rewriting: A domain-specific language for pattern matching and expression transformation.
    • Combinator library: Tools to compose complex rewriters from simpler ones.
    • Type-aware symbols: Symbols and compound expressions that propagate type information.
    • Automatic simplification: Basic algebraic simplification performed during expression construction.
  2. Overview of SymbolicUtils.jl

    master
    SymbolicUtils.jl is a toolkit for building Computer Algebra Systems (CAS). Unlike a complete CAS (like Symbolics.jl), it provides the low-level utilities required to implement symbolic computing, such as type-aware symbols, rule-based rewriting, and expression simplification. It is designed for developers who want to build custom symbolic engines or implement rule-rewriting systems for their own types.
  3. How to handle constant values with the Const variant

    master

    In SymbolicUtils.jl, non-symbolic values (like numbers or strings) are stored in a Const variant to maintain type-stability.

    Important: Accessing the value inside a Const is type-unstable. To avoid this, use Moshi.Match.@match for pattern matching or the unwrap_const utility. unwrap_const acts as an identity function for any input that is not a Const.

    Checking for constants: Use SymbolicUtils.isconst(x) to check if a BasicSymbolic is a Const variant.

    Construction:

    • Use Const{T}(val) or BSImpl.Const{T}(val).
    • If you pass an array of symbolics to the Const constructor, it returns a Term using SymbolicUtils.array_literal as the operation, allowing standard operations like substitute to work efficiently on arrays.
    # Checking and unwrapping
    if SymbolicUtils.isconst(x)
        val = unwrap_const(x)
    end
  4. Use Code Combinators to construct complex expressions

    master

    The SymbolicUtils.Code module provides combinators to build complex Julia expressions beyond simple function calls. To access these, you must use using SymbolicUtils.Code.

    Supported construction patterns include:

    • Let blocks: Scoped variable bindings.
    • Functions: Supports arguments, keyword arguments, and de-structuring of arguments.
    • Array manipulation: Expressions that set array elements in-place.
    • Array creation: Creates arrays similar in type to reference arrays (supports Array, StaticArrays.SArray, and LabelledArrays.SLArray).
    • Sparse arrays: Expressions for creating sparse array structures.
    • Tuples: Creating tuple expressions.
    using SymbolicUtils.Code
    
    # Available combinators:
    # Assignment, Let, Func, SpawnFetch, SetArray, 
    # MakeArray, MakeSparseArray, MakeTuple, LiteralExpr, ForLoop
  5. Understand the Symbolic expression interface

    master

    Symbolic expressions are represented by specific types: Term{T}, Add{T}, Mul{T}, Pow{T}, or Div{T}. These types denote function calls where arguments are either other expressions or Syms.

    All expression types implement the TermInterface.jl interface, allowing for standardized traversal and manipulation of symbolic trees.

  6. Define symbolic functions and dependent variables

    master

    SymbolicUtils uses FnType{A, R, T} to represent symbolic functions, where A is a tuple of argument symtypes, R is the return symtype, and T is the function's supertype (or Nothing).

    Symbolic Functions

    Use the @syms macro with explicit argument types to create a symbolic function:

    @syms f(::T1, ::T2)::R

    This creates f with symtype FnType{Tuple{T1, T2}, R, Nothing}.

    Dependent Variables

    If you omit argument types, the variable is treated as a dependent variable with unspecified independent variables:

    @syms f(..)::R

    In the expression f(x), f is considered a dependent variable that depends on x rather than a symbolic function call.

    Use SymbolicUtils.is_function_symbolic, SymbolicUtils.is_function_symtype, and SymbolicUtils.is_called_function_symbolic to distinguish between these cases.

    # A symbolic function
    @syms f(::Real)::Real
    
    # A dependent variable
    @syms g(..)::Real
    
    # Usage
    f(x) # Treated as a symbolic function call
    g(x) # Treated as a dependent variable
  7. Understand the BasicSymbolic structure and vartype

    master

    SymbolicUtils uses an Algebraic Data Type (ADT) named BasicSymbolicImpl (aliased as BSImpl) to represent symbolic trees. The primary type is BasicSymbolic.

    Important: Immutability BasicSymbolic objects are IMMUTABLE. Although ismutabletype(BasicSymbolic) returns true, any mutation (including internal field mutation like AddMul.dict) is undefined behavior and will cause hard-to-debug issues. Arrays returned by TermInterface.arguments and TermInterface.sorted_arguments are read-only.

    The vartype Tag In v4, the type parameter in BasicSymbolic{T} represents the vartype, which determines the assumptions made about the symbolic algebra. An expression must be pure in its vartype; operations do not support mixing different vartypes.

    Available vartype values:

    • SymReal: The default behavior.
    • SafeReal: Identical to SymReal, but common factors in the numerator and denominator of a division are not cancelled.
    • TreeReal: Assumes nothing about the algebra and always uses the Term variant to represent an expression.
    using SymbolicUtils
    # BasicSymbolic is the core type
    # vartype (SymReal, SafeReal, TreeReal) determines algebraic assumptions
  8. Use Add and Mul variants for optimized arithmetic

    master

    For associative-commutative addition and multiplication, SymbolicUtils.jl uses a specialized AddMul variant to improve efficiency. This is distinguished by the AddMulVariant enum: AddMulVariant.ADD or AddMulVariant.MUL.

    Multiplication (Mul)

    Represented as coeff * (term1^exp1 * term2^exp2 * ...).

    • coeff: A non-symbolic constant.
    • dict: A map from terms to their exponents.
    • Best Practice: Use the Mul{T}(coeff, dict; type, shape, metadata) constructor. It validates constraints (e.g., coeff must not be zero, dict must not be empty) and returns the appropriate form.

    Addition (Add)

    Represented as coeff + (term1*c1 + term2*c2 + ...).

    • coeff: A non-symbolic constant. 0- dict: A map from terms to their constant non-symbolic coefficients.
    • Best Practice: Use the Add{T}(coeff, dict; type, shape, metadata) constructor. It validates constraints and returns the appropriate form.

    Warning: Using the raw BSImpl.AddMul{T} constructor is faster but skips validation and can lead to undefined behavior if constraints are violated.

    # Preferred way to create multiplication
    m = Mul{T}(2, Dict(x => 2, y => 1); type=T, shape=S, metadata=M)
    
    # Preferred way to create addition
    a = Add{T}(1, Dict(x => 2, y => 3); type=T, shape=S, metadata=M)
  9. Understand Symbolic Array algebra and limitations

    master

    SymbolicUtils implements an array algebra designed to mimic Base Julia Array behavior.

    Key Behaviors:

    • Scalar reduction: Operations like adjoint(::Vector) * (::Vector) return a symbolic scalar rather than a one-element vector.
    • Indexing: Symbolic arrays only support Cartesian indexing. Valid indices include Int, Colon, AbstractRange{Int}, symbolic expressions with integer symtype, or a single CartesianIndex of appropriate dimension. Accessing x[4] on a 2D array is invalid; use x[1, 2] instead.

    Important Limitations:

    • map and mapreduce: All input arrays must have the same shape. promote_symtype and promote_shape are not implemented for these because they require the function itself rather than just its type/shape.
    • Type Instability: Functions like eachindex, iterate, size, axes, ndims, and collect are type-unstable because ndims is not present in the type. Use SymbolicUtils.stable_eachindex for type-stable iteration.
    • ifelse: Both the true and false branches must have identical shapes.
    • Array of Symbolics: To perform symbolic array operations on an array of symbolics, at least one argument must be a symbolic value (not just an array of symbolics) to trigger SymbolicUtils dispatch instead of Base dispatch.
  10. Work with expression shapes

    master

    SymbolicUtils v4 treats arrays as first-class citizens by storing a shape. You can query this using SymbolicUtils.shape(x). There are three categories of shapes:

    1. Known Shape: For variables defined with specific ranges (e.g., @syms x[1:2]), shape(x) returns a vector of UnitRange{Int} (similar to Base.axes). Scalar variables return an empty vector.
    2. Known ndims: For variables where the exact size is unknown but the number of dimensions is specified (e.g., @syms x::Vector{Number}), shape(x) returns a SymbolicUtils.Unknown(ndims) object. Array operations perform best-effort validation and shape calculation.
    3. Unknown ndims: For variables where only the array nature is known (e.g., @syms x::Array{Number}), shape(x) returns SymbolicUtils.Unknown(-1). This disables most shape checking for array operations.
    using SymbolicUtils
    
    # 1. Known shape
    @syms x[1:2] y[-3:6, 4:7] z
    shape(x) # Returns vector of UnitRanges
    
    # 2. Known ndims
    @syms a::Vector{Number} b::Matrix{Number} c::Array{Number, 3}
    shape(a) # Returns SymbolicUtils.Unknown(1)
    
    # 3. Unknown ndims
    @syms d::Array{Number}
    shape(d) # Returns SymbolicUtils.Unknown(-1)
  11. Use defslot variables for optional coefficients

    master

    When matching polynomials or expressions where a coefficient might be implicit (e.g., z^2 instead of 1*z^2), use defslot variables with the syntax ~!a. These variables take a default value if they are not explicitly present in the expression:

    OperationDefault Value
    Multiplication *1
    Addition +0
    2nd argument of ^1

    Example: Matching 3 + 2z + z^2 using ~!c for the z^2 coefficient.

    using SymbolicUtils
    @syms z
    
    # Using defslot variables to catch implicit coefficients
    c2d = @rule ~!a + ~!b*z + ~!c*z^2 => (~a, ~b, ~c)
    
    c2d(3 + 2z + z^2) # Returns (3, 2, 1)