symbolica

repository·main·Indexed 21 days ago

https://github.com/symbolica-dev/symbolica

A high-performance computer algebra system (version 2.2.0) designed for large expressions, symbolic rewrites, exact polynomial arithmetic, and optimized numerical evaluation. It provides native APIs for Python and Rust and includes two specialized libraries: Graphica, for the generation, manipulation, and canonization of multi-edge graphs, and Numerica, a mathematics library providing algebraic abstractions, high-performance integers, finite field arithmetic, automatic differentiation, and linear algebra.

Tokens
30.7K
Snippets
101
Records
130
Agent score
70%

What's inside symbolica

  1. Overview of Numerica

    main

    Numerica is a high-performance mathematics library for Rust. It provides advanced numerical abstractions and types, including:

    • Algebraic Abstractions: Rings, Euclidean domains, fields, and floats.
    • High-performance Integers: Automatic up-and-downgrading to arbitrary precision types.
    • Rational Numbers: Includes reconstruction algorithms.
    • Finite Field Arithmetic: Fast arithmetic for finite fields.
    • Error-tracking Floats: Types that propagate precision/error information through computations.
    • Automatic Differentiation: Generic dual numbers supporting multiple variables and higher-order differentiation.
    • Linear Algebra: Matrix operations and linear system solving.
    • Numerical Integration: Vegas algorithm with discrete layer support.
  2. Represent integration results with RationalIntegral

    main

    The RationalIntegral<R, E> struct represents the result of rational-function integration. It decomposes the antiderivative into two distinct parts:

    1. rational_parts: A Vec<RationalPolynomial<R, E>> containing the rational terms of the antiderivative.
    2. logarithmic_parts: A Vec<LogarithmicIntegralTerm<R, E>> containing the logarithmic terms.

    Logarithmic terms can represent either ordinary logarithms or algebraic root sums, depending on whether the coefficient depends on a temporary root-sum variable z.

    // Structure of an integration result
    let integral = RationalIntegral {
        rational_parts: vec![/* ... */],
        logarithmic_parts: vec![/* ... */],
    };
  3. How Pattern and ReplaceWith work together

    main

    Symbolica distinguishes between the Pattern (what you are looking for) and the Replacement (what you put in its place).

    Pattern

    A Pattern can be a literal atom, a wildcard (e.g., x_), a function, or complex structures like sums and products. You can create patterns from symbols using .to_pattern() or by converting a Symbol into a Pattern.

    ReplaceWith

    The right-hand side of a replacement (rhs) can be one of two types:

    1. Pattern: A standard expression used for replacement.
    2. Map: A MatchMap (a closure) that takes the matched wildcards and returns an Atom. This allows for complex, context-dependent replacements that cannot be expressed as a static pattern.

    Replacement Configuration

    You can fine-tune how replacements occur using Replacement or ReplaceBuilder methods:

    • .non_greedy_wildcards(vec![...]): Specifies wildcards that match as little as possible.
    • .level_range(min, max): Limits matching to specific depths in the expression tree.
    • .partial(bool): If true, matches subexpressions; if false, must match the entire expression.
    • .bottom_up(): Traverses the tree bottom-up (replaces deepest matches first).
    • .nested(): Enables nested replacement (acts on the result of a replacement).
    • .repeat(): Repeats the replacement until no more matches are found.
    // Example of using a Map for complex replacement
    let (f, x_) = symbol!("f", "x_");
    let a = function!(f, 1) * function!(f, 3);
    let p = function!(f, x_);
    
    let r = a.replace(p).with_map(move |m| {
        function!(
            f,
            parse!(&format!(
                "p{}",
                m.get(x_).unwrap().to_atom().printer(PrintOptions::file())
            ))
        )
    });
    // Result: f(p1)*f(p3)
  4. Reference ComplexPhase for complex evaluators

    main

    When working with complex number evaluation, ComplexPhase defines how an operation handles real vs. imaginary components:

    • Real: The operation is strictly real.
    • Imag: The operation is strictly imaginary.
    • PartialReal(usize): The operation has a specific number of real arguments.
    • Any: The default phase, allowing any complex input.
  5. Supported Inline ASM Flavors for Numerical Export

    main

    The numerical export system supports three specific inline assembly flavors for generating optimized machine code. These flavors determine the instruction sets and register usage used during the evaluation of complex mathematical expressions:

    • InlineASM::X64: Uses SSE instructions (e.g., movsd, divsd, sqrtsd) and xmm registers.
    • InlineASM::AVX2: Uses AVX2 instructions (e.g., vmovupd, vdivpd, vsqrtpd) and ymm registers.
    • InlineASM::AArch64: Uses ARM64 instructions (e.g., ldr, fdiv, fsqrt) and d or q registers.
    • InlineASM::None: A placeholder used for logic that does not involve inline assembly.
  6. Use HiddenData to control equality and hashing

    main

    If you need to attach complex data to nodes or edges but want to ignore certain parts of that data when comparing two graphs (for equality or hashing), use HiddenData<T, U>.

    • T (Public part): Used for PartialEq, Eq, Hash, PartialOrd, and Ord.
    • U (Private part): Ignored in all comparisons and hashing, but included in the Display output.
    use graphica::HiddenData;
    
    // Only the integer is used for equality/hashing
    let data = HiddenData::new(42, "secret_metadata");
  7. Convert patterns and replacement logic in Python

    main

    When performing pattern matching or replacements, Symbolica provides several conversion types to bridge Python objects and internal patterns:

    ConvertibleToPattern

    Used to define what a pattern can be. It can be a Literal (an expression) or Held (a held expression).

    ConvertibleToOpenPattern

    Allows for patterns that include Transformer logic. It can be Closed (a standard pattern) or Open (a pattern with an attached transformer).

    ConvertibleToReplaceWith

    Defines what to substitute when a pattern matches. It supports:

    • Pattern: A ConvertibleToPattern.
    • Map: A Python Callable (mapping function) with the signature Callable[[dict[Expression, Expression]], Expression]. This allows you to execute arbitrary Python logic during a replacement step.
  8. Use AliasedAtom for symbolic aliasing

    main

    An AliasedAtom is an atom that uses opaque aliases to represent repeated subexpressions, which can reduce the memory footprint of large symbolic expressions. It consists of a root atom and a map of aliases (mapping an alias atom to its original atom body).

    You can create an AliasedAtom from a standard Atom using .into(), or generate one by extracting common subexpressions using .alias_subexpressions().

    To use the aliases in the root expression, call .apply_aliases() or .apply_aliases_nested() (which also updates the bodies of the aliases themselves). To revert to the original unaliased expression, use .into_inner().

    Note: When performing arithmetic operations like add, mul, or pow on two AliasedAtom instances, their alias maps are fused. If alias name conflicts occur, you must provide a resolve function to generate new unique handles. Alternatively, use try_add, try_mul, or try_pow to attempt fusion without a resolver; these will return an Err(Atom) if a conflict is detected.

    use symbolica::prelude::*;
    
    let a: AliasedAtom = parse!("(x+1)^2+(x+1)^3").into();
    // Manually add an alias: s(1) -> x+1
    let b = a.add_alias(parse!("s(1)"), parse!("x+1"));
    
    // The root now uses the alias
    assert_eq!(b.get_root(), &parse!("s(1)^2+s(1)^3"));
    
    // Expand back to original
    let original: Atom = b.into_inner();
    assert_eq!(original, parse!("(x+1)^2+(x+1)^3"));
  9. Handle Symbolica UserData in Python

    main

    Symbolica allows attaching arbitrary user data to expressions. In Python, this is handled via the PythonUserData class and PythonUserDataKey for mapping.

    PythonUserData can represent:

    • None
    • int (Integer)
    • str (String)
    • Atom (A Symbolica expression)
    • list (List of PythonUserData)
    • dict (Map with PythonUserDataKey as keys and PythonUserData as values)
    • bytes (Serialized data)

    PythonUserDataKey can be used as a key in a dictionary and supports:

    • int
    • Atom (Expression)
    • str