Programming in Cadabra
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 theEx
and the ExNode
. An object of type Ex
represents a mathematical expression, and is what is generated if you type a line containing :=
, as inex:=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
ExNode
s when you do a standard Python iteration
over the elements of an Ex
, as infor 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 howExNode
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 ExNode
s 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
TheExNode
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
Querying properties
Properties which you attach to patterns can be queried in Python, though the functionality is somewhat limited. In order to query a pattern for a particular property, use the property's name together with theget
method. An example:A_{m n}::AntiSymmetric.
p1 = AntiSymmetric.get($A_{m n}$)
p1;
\(\displaystyle{}\text{Property AntiSymmetric attached to }A_{m n}.\)
p2 = Symmetric.get($A_{m n}$)
p2;
None
Some properties, like `Weight` have an associated value. You can access these with the appropriate member function,
so for this particular example you would do
x::Weight(value=42, label=field);
Weight.get($x$, label="field").value("field");
\(\displaystyle{}\text{Property Weight attached to }x.\)
42
Expression pattern matching
If you want to check whether an expression matches a particular pattern, use thematch
function of
the Ex
object. By default this is rather strict, requiring that indices match not only their
type but also their name.{m, n, k, l}::Indices(vector).
{a, b, c, d}::Indices(spinor).
$A_{m n}$.matches($A_{k l}$);
True
$A_{m n}$.matches($A_{m n}$);
True
$A_{m n}$.matches($A_{k l}$);
True
$A_{m n}$.matches($A_{a b}$);
False
Wildcard symbols will match any symbol,
$A_{m? n?}$.matches($A_{k l}$);
$A_{m? n?}$.matches($A_{a b}$);
True
True
$A??$.matches($A_{k l}$);
True
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}