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:
- Delayed execution
- High-performance execution
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
= Transformer().expand() t
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
= Transformer().expand()
t = E('(1+x)^2')
e = t(e) r
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
= Expression.parse('(x+1)^2')
e e.transform().expand().execute()
Examples
For brevity we define T
:
= Transformer() T
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.
= S('x_', 'f')
x_, f = E('f(100)')
e
T.repeat(
T.expand(),- 1) + f(x_ - 2), x_.req_gt(1))
T.replace_all(f(x_), f(x_ )(e)
Transformers can be chained, so the program above can also be rewritten
= S('x_', 'f')
x_, f = E('f(100)')
e
T.repeat(
T.expand().replace_all(f(x_), - 1) + f(x_ - 2), x_.req_gt(1))
f(x_ )(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(),*gamma(x___,z___))
T.replace_all(gamma(x___,y_,y_,z___), D
)
def main_loop():
return T.repeat(
simplify(),-gamma(x___,x_,g5_,z___))
T.replace_all(gamma(x___,g5,x_,z___), )
Multithreading
You can use the map_terms
transformer to map other transformers over all the terms in an expression, in parallel:
= E('(x+1)^30')
e = T.map_terms(T.replace_all(x, x + 1), n_cores=5)
t 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:
= main_loop()
m
for e in [E('gamma(1,2,g5,3,4)'), E('gamma(1,3,4)')]:
# reused m(e)
This is especially important if your transformers create threads, as creating an removing threads is expensive and will increase memory consumption.