Transformers

A transformer can be viewed as a function that, when called, transform an expression that it gets as an argument into another expression. It is similar to a computation graph. The reasons for transformers in Symbolica are twofold:

The delayed execution is explained in the pattern matching where the right-hand side should be transformed, but only after substitution of the wildcard.

High-performance execution in Symbolica can also be achieved even when running from Python. As is well known, Python generally does not achieve performance similar to Rust as it is not a compiled language. However, we can use transformers to build a program that is run entirely in Rust.

A simple example of a transformer is:

from symbolica import Transformer
t = Transformer().expand()
Tip

Transformer().expand() is similar to lambda x: x.expand()

A transformer can be applied to an expression by calling it, and the result is another expression. For example:

from symbolica import Transformer
t = Transformer().expand()
e = E('(1+x)^2')
r = t(e)

which yields x^2+2x+1.

Some functions take a transformer as input. An example is the map() function uses for term streaming and the replace_all() function.

Bound transformers

Transformers can also be bound to an expression, which sets the expression that the transformer will be applied to, without actually executing the transformer. This can be used for delayed execution. Here is an example with replace_all:

from symbolica import *
x, x_, f = S('x', 'x_', 'f')
e = f((x+1)**2)
e.replace_all(f(x_), f(x_.transform().expand()))

Here a bound transformer is created using x_.transform(). Before the transformer is executed, the x_ is replaced by its matched value.

A bound transformer can be executed by calling execute():

from symbolica import Transformer
e = Expression.parse('(x+1)^2')
e.transform().expand().execute()

Examples

For brevity we define T:

T = Transformer()

Below is an example in Python that uses the repeat transformer, which takes a list of transformers that it will repeat until there are no more changes between the input and output expression.

x_, f = S('x_', 'f')
e = E('f(100)')

T.repeat(
    T.expand(),
    T.replace_all(f(x_), f(x_ - 1) + f(x_ - 2), x_.req_gt(1))
)(e)

Transformers can be chained, so the program above can also be rewritten

x_, f = S('x_', 'f')
e = E('f(100)')

T.repeat(
    T.expand().replace_all(f(x_), 
        f(x_ - 1) + f(x_ - 2), x_.req_gt(1))
)(e)

Using transformers, helper functions can be written in Python that can be chained to compose complicated logic:

def simplify():
    return T.repeat(
        T.expand(),
        T.replace_all(gamma(x___,y_,y_,z___), D*gamma(x___,z___))
    )

def main_loop():
    return T.repeat(
        simplify(),
        T.replace_all(gamma(x___,g5,x_,z___), -gamma(x___,x_,g5_,z___))
    )

Multithreading

You can use the map_terms transformer to map other transformers over all the terms in an expression, in parallel:

e = E('(x+1)^30')
t = T.map_terms(T.replace_all(x, x + 1), n_cores=5)
t(e)

The new threads spawned by the transformer will have a stack size of 2MB by default. If your code aborts, you may have to increase the stack size using the environment variable RUST_MIN_STACK, which specifies the stack size of new threads in bytes.

Performance tips

For maximum performance, transformers should be created only once. For example, the transformer main_loop() can be created by storing it in a variable and using that:

m = main_loop()

for e in [E('gamma(1,2,g5,3,4)'), E('gamma(1,3,4)')]:
    m(e) # reused

This is especially important if your transformers create threads, as creating an removing threads is expensive and will increase memory consumption.