a field-theory motivated approach to computer algebra

Cadabra is fully programmable in Python. At the most basic level this means that you can make functions which combine various Cadabra algorithms together, or write loops which repeat certain Cadabra algorithms. At a more advanced level, you can inspect the expression tree and manipulate individual subexpressions, or construct expressions from elementary building blocks.

Fundamental Cadabra objects: Ex and ExNode

The two fundamental Cadabra objects are the Ex and the ExNode. An object of type Ex represents a mathematical expression, and is what is generated if you type a line containing :=, as in
ex:=A+B; type(ex);
$$\displaystyle{}A+B$$
A + B
<class 'cadabra2.Ex'>
An object of type ExNode is best thought of as an iterator. It can be used to walk an expression tree, and modify it in place (which is somewhat different from normal Python iterators; a point we will return to shortly). The most trivial way to get an iterator is to call the top member of an Ex object; think of this as returning a pointer to the topmost node of an expression,
ex.top(); type(ex.top());
A + B
<class 'cadabra2.ExNode'>
You will also encounter ExNodes when you do a standard Python iteration over the elements of an Ex, as in
for n in ex: type(n); display(n)
<class 'cadabra2.ExNode'>
A + B
<class 'cadabra2.ExNode'>
A
<class 'cadabra2.ExNode'>
B
As you can see, this 'iterates' over the elements of the expression, but in a perhaps somewhat unexpected way. We will discuss this in more detail in the next section. Important to remember from the example above is that the 'pointers' to the individual elements of the expression are ExNode objects. There are various other ways to obtain such pointers, using various types of 'filtering', more on that below as well. Once you have an ExNode pointing to a subexpression in an expression, you can query it further for details about that subexpression.
ex:= A_{m n}; for i in ex.top().free_indices(): display(i)
$$\displaystyle{}A_{m n}$$
A_{m n}
m
n
The example above shows how, starting from an iterator which points to the top of the expression, you can get a new iterator which can iterate over all free indices.

ExNode and Python iterators

Before we continue, we should make a comment on how ExNode objects relate to Python iterators. For many purposes, ExNode objects behave as you expect from Python iterators: they allow you to loop over nodes of an Ex expression, you can call next(...) on them, and so on. However, there are some slight differences, which have to do with the fact that Cadabra wants to give you access to the nodes of the original Ex, so that you can modify this original Ex in place. Consider for instance this example with a Python list of integers, with standard iterators:
q=[1,2,3,4,5]; for element in q: element=0 q;
{}$\big[$$1, 2, 3, 4, 5$$\big]$
{}$\big[$$1, 2, 3, 4, 5$$\big]$
It still produces the original list at the end of the day, because each element is a copy of the element in the list. With ExNodes you can actually modify the original Ex, as this example shows:
ex:=A + B + C + D; for element in ex.top().terms(): element.replace($Q$) ex;
$$\displaystyle{}A+B+C+D$$
A + B + C + D
$$\displaystyle{}Q+Q+Q+Q$$
Q + Q + Q + Q
In this case, element is not an Ex corresponding to each of the 5 terms, but rather an ExNode, which is more like a pointer into the Ex object. The replace member function allows you to replace the building blocks of the original ex expression. If you want to get a proper Ex object (so a copy of the element in the expression over which you are iterating), more like what you would get if iteration over Cadabra's expressions was an ordinary Python iteration, then you can use ExNode.ex():
ex:= A + 2 B + 3 C + 4 D; lst=[] for element in ex.top().terms(): lst.append( element.ex() ) ex; lst[2];
$$\displaystyle{}A+2B+3C+4D$$
A + 2B + 3C + 4D
$$\displaystyle{}A+2B+3C+4D$$
A + 2B + 3C + 4D
$$\displaystyle{}3C$$
3C
Here the list lst contains copies of the individual terms of the ex expression. A good way to remember about this is to keep in mind that Cadabra tries its best to allow you to modify expressions in-place. The ExNode iterators provide that functionality.

Traversing the expression tree

The ExNode iterator can be instructed to traverse expressions in various ways. The most basic iterator is obtained by using standard Python iteration with a for loop,
ex:= A + B + C_{m} D^{m};
$$\displaystyle{}A+B+C_{m} D^{m}$$
A + B + C_{m} D^{m}
for n in ex: print(str(n))
A + B + C_{m} D^{m}
A
B
C_{m} D^{m}
C_{m}
m
D^{m}
m

The iterator obtained in this way traverses the expression tree node by node, and when you ask it to print what it is pointing to, it prints the entire subtree of the node it is currently visiting. If you are only interested in the name of the node, not the entire expression below it, you can use the .name member of the iterator:
for n in ex: print(str(n.name))
\sum
A
B
\prod
C
m
D
m

Often, this kind of 'brute force' iteration over expression elements is not very useful. A more powerful iterator is obtained by asking for all nodes in the subtree which have a certain name. This can be the name of a tensor, or the name of a special node, such as a product or sum,
for n in ex["C"]: display(n)
C_{m}
for n in ex["\\prod"]: display(n)
C_{m} D^{m}
The above two examples used an iterator obtained directly from an Ex object. Various ways of obtaining iterators over special nodes can be obtained by using member functions of ExNode objects themselves. So one often uses a construction in which one first asks for an iterator to the top of an expression, and then requests from that iterator a new one which can iterate over various special nodes. The example below obtains an iterator over all top-level terms in an expression, and then loops over its values.
for n in ex.top().terms(): display(n)
A
B
C_{m} D^{m}
Two special types of iterators are those which iterate only over all arguments or only over all indices of a sub-expression. These are discussed in the next section.

Arguments and indices

There are various ways to obtain iterators which iterate over all arguments or all indices of an expression. The following example, with a derivative acting on a product, prints the argument of the derivative as well as all free indices.
\nabla{#}::Derivative; ex:= \nabla_{m}{ A^{n}_{p} V^{p} };
$$\displaystyle{}\text{Attached property Derivative to }\nabla{\#}.$$
$$\displaystyle{}\nabla_{m}\left(A^{n}\,_{p} V^{p}\right)$$
\nabla_{m}(A^{n}_{p} V^{p})
for nabla in ex[r'\nabla']: for arg in nabla.args(): print(str(arg)) for i in nabla.free_indices(): print(str(i))
A^{n}_{p} V^{p}
m
n


Example: covariant derivatives

The following example shows how you might implement the expansion of a covariant derivative into partial derivatives and connection terms.
def expand_nabla(ex): for nabla in ex[r'\nabla']: nabla.name=r'\partial' dindex = nabla.indices().__next__() for arg in nabla.args(): ret:=0; for index in arg.free_indices(): t2:= @(arg); if index.parent_rel==sub: t1:= -\Gamma^{p}_{@(dindex) @(index)}; t2[index]:= _{p}; else: t1:= \Gamma^{@(index)}_{@(dindex) p}; t2[index]:= ^{p}; ret += Ex(str(nabla.multiplier)) * t1 * t2 nabla += ret return ex
The sample expressions below show how this automatically takes care of not introducing connections for dummy indices, and how it automatically handles indices which are more complicated than single symbols.
\nabla{#}::Derivative; ex:= 1/2 \nabla_{a}{ h^{b}_{c} }; expand_nabla(ex);
$$\displaystyle{}\text{Attached property Derivative to }\nabla{\#}.$$
$$\displaystyle{}\frac{1}{2}\nabla_{a}{h^{b}\,_{c}}$$
1/2 \nabla_{a}(h^{b}_{c})
$$\displaystyle{}\frac{1}{2}\partial_{a}\left(h^{b}\,_{c}\right)+\frac{1}{2}\Gamma^{b}\,_{a p} h^{p}\,_{c} - \frac{1}{2}\Gamma^{p}\,_{a c} h^{b}\,_{p}$$
1/2 \partial_{a}(h^{b}_{c}) + 1/2 \Gamma^{b}_{a p} h^{p}_{c} - 1/2 \Gamma^{p}_{a c} h^{b}_{p}
ex:= 1/4 \nabla_{a}{ v_{b} w^{b} }; expand_nabla(ex);
$$\displaystyle{}\frac{1}{4}\nabla_{a}\left(v_{b} w^{b}\right)$$
1/4 \nabla_{a}(v_{b} w^{b})
$$\displaystyle{}\frac{1}{4}\partial_{a}\left(v_{b} w^{b}\right)$$
1/4 \partial_{a}(v_{b} w^{b})
ex:= \nabla_{\hat{a}}{ h_{b c} v^{c} }; expand_nabla(ex);
$$\displaystyle{}\nabla_{\widehat{a}}\left(h_{b c} v^{c}\right)$$
\nabla_{\hat{a}}(h_{b c} v^{c})
$$\displaystyle{}\partial_{\widehat{a}}\left(h_{b c} v^{c}\right)-\Gamma^{p}\,_{\widehat{a} b} h_{p c} v^{c}$$
\partial_{\hat{a}}(h_{b c} v^{c})-\Gamma^{p}_{\hat{a} b} h_{p c} v^{c}