Drudge tutorial for beginners¶
Get started¶
Drudge is a library built on top of the SymPy computer algebra library for
noncommutative and tensor alegbras. Usually for these style of problems, the
symbolic manipulation and simplification of mathematical expressions requires a
lot of context-dependent information, like the specific commutation rules and
things like the dummy symbols to be used for different ranges. So the primary
entry point for using the library is the Drudge
class, which serves
as a central repository of all kinds of domain-specific informations. To
create a drudge instance, we need to give it a Spark context so that it is
capable of parallelize things. For instance, to run things locally with all
available cores, we can do
>>> from pyspark import SparkContext
>>> spark_ctx = SparkContext('local[*]', 'drudge-tutorial')
For using Spark in cluster computing environment, please refer to the Spark documentation and setting of your cluster. With the spark context created, we can make the main entry point for drudge,
>>> import drudge
>>> dr = drudge.Drudge(spark_ctx)
Then from it, we can create the symbolic expressions as Tensor
objects, which are basically mathematical expressions containing noncommutative
objects and symbolic summations. For the noncommutativity, in spite of the
availability of some basic support of it in SymPy, here we have the
Vec
class to specifically designate the noncommutativity of its
multiplication. It can be created with a label and indexed with SymPy
expressions.
>>> v = drudge.Vec('v')
>>> import sympy
>>> a = sympy.Symbol('a')
>>> str(v[a])
'v[a]'
For the symbolic summations, we have the Range
class, which denotes
a symbolic set that a variable could be summed over. It can be created by just
a label.
>>> l = drudge.Range('L')
With these, we can create tensor objects by using the Drudge.sum()
method,
>>> x = sympy.IndexedBase('x')
>>> tensor = dr.sum((a, l), x[a] * v[a])
>>> str(tensor)
'sum_{a} x[a] * v[a]'
Now we got a symbolic tensor of a sum of vectors modulated by a SymPy IndexedBase. Actually any type of SymPy expression can be used to modulate the noncommutative vectors.
>>> tensor = dr.sum((a, l), sympy.sin(a) * v[a])
>>> str(tensor)
'sum_{a} sin(a) * v[a]'
And we can also have multiple summations and product of the vectors.
>>> b = sympy.Symbol('b')
>>> tensor = dr.sum((a, l), (b, l), x[a, b] * v[a] * v[b])
>>> str(tensor)
'sum_{a, b} x[a, b] * v[a] * v[b]'
Of cause the multiplication of the vectors will not be commutative,
>>> tensor = dr.sum((a, l), (b, l), x[a, b] * v[b] * v[a])
>>> str(tensor)
'sum_{a, b} x[a, b] * v[b] * v[a]'
Normally, for each symbolic range, we have some traditional symbols used as
dummies for summations over them, giving these information to drudge objects
can be very helpful. Here in this demonstration, we can use the
Drudge.set_dumms()
method.
>>> dr.set_dumms(l, sympy.symbols('a b c d'))
[a, b, c, d]
>>> dr.add_resolver_for_dumms()
where the call to the Drudge.add_resolver_for_dumms()
method could
tell the drudge to interpret all the dummy symbols to be over the range that
they are set to. By giving drudge object such domain-specific information, we
can have a lot convenience. For instance, now we can use Einstein summation
convention to create tensor object, without the need to spell all the
summations out.
>>> tensor = dr.einst(x[a, b] * v[a] * v[b])
>>> str(tensor)
'sum_{a, b} x[a, b] * v[a] * v[b]'
Also the drudge knows what to do when more dummies are needed in mathematical operations. For instance, when we multiply things,
>>> tensor = dr.einst(x[a] * v[a])
>>> prod = tensor * tensor
>>> str(prod)
'sum_{a, b} x[a]*x[b] * v[a] * v[b]'
Here the dummy \(b\) is automatically used since the drudge object knows
available dummies for its range. Also the range and the dummies are
automatically added to the name archive of the drudge, which can be access by
Drudge.names
.
>>> p = dr.names
>>> p.L
Range('L')
>>> p.L_dumms
[a, b, c, d]
>>> p.d
d
Here in this example, we set the dummies ourselves by
Drudge.set_dumms()
. Normally, in subclasses of Drudge
for
different specific problems, such setting up is already finished within the
class. We can just directly get what we need from the names archive. There is
also a method Drudge.inject_names()
for the convenience of interactive
work.
Tensor manipulations¶
Now with tensors created by Drudge.sum()
or Drudge.einst()
, a
lot of mathematical operations are available to them. In addition to the
above example of (noncommutative) multiplication, we can also have the linear
algebraic operations of addition and scalar multiplication.
>>> tensor = dr.einst(x[a] * v[a])
>>> y = sympy.IndexedBase('y')
>>> res = tensor + dr.einst(y[a] * v[a])
>>> str(res)
'sum_{a} x[a] * v[a]\n + sum_{a} y[a] * v[a]'
>>> res = 2 * tensor
>>> str(res)
'sum_{a} 2*x[a] * v[a]'
We can also perform some complex substitutions on either the vector or the
amplitude part, by using the Drudge.subst()
method.
>>> t = sympy.IndexedBase('t')
>>> w = drudge.Vec('w')
>>> substed = tensor.subst(v[a], dr.einst(t[a, b] * w[b]))
>>> str(substed)
'sum_{a, b} x[a]*t[a, b] * w[b]'
>>> substed = tensor.subst(x[a], sympy.sin(a))
>>> str(substed)
'sum_{a} sin(a) * v[a]'
Note that here the substituted vector does not have to match the left-hand side
of the substitution exactly, pattern matching is done here. Other mathematical
operations are also available, like symbolic differentiation by
Tensor.diff()
and commutation by |
operator
Tensor.__or__()
.
Tensors are purely mathematical expressions, while the utility class
TensorDef
can be construed as tensor expressions with a left-hand
side. They can be easily created by Drudge.define()
and
Drudge.define_einst()
.
>>> v_def = dr.define_einst(v[a], t[a, b] * w[b])
>>> str(v_def)
'v[a] = sum_{b} t[a, b] * w[b]'
Their method TensorDef.act()
is like a active voice version of
Tensor.subst()
and could come handy when we need to substitute the same
definition in multiple inputs.
>>> res = v_def.act(tensor)
>>> str(res)
'sum_{a, b} x[a]*t[a, b] * w[b]'
More importantly, the definitions can be indexed directly, and the result is
designed to work well inside Drudge.sum()
or Drudge.einst()
.
For instance, for the same result, we could have,
>>> res = dr.einst(x[a] * v_def[a])
>>> str(res)
'sum_{b, a} x[a]*t[a, b] * w[b]'
When the only purpose of a vector or indexed base is to be substituted and we never intend to write tensor expressions directly in terms of them, we can just name the definition with a short name directly and put the actual base inside only. For instance,
>>> c = sympy.Symbol('c')
>>> f = dr.define_einst(sympy.IndexedBase('f')[a, b], x[a, c] * y[c, b])
>>> str(f)
'f[a, b] = sum_{c} x[a, c]*y[c, b]'
>>> str(dr.einst(f[a, a]))
'sum_{b, a} x[a, b]*y[b, a]'
which also demonstrates that the tensor definition facility can also be used for
scalar quantities. TensorDef
is also at the core of the code
optimization and generation facility in the gristmill
package.
Usually for tensorial problems, full simplification requires the utilization of
some symmetries present on the indexed quantities by permutations among their
indices. For instance, an anti-symmetric matrix entry changes sign when we
transpose the two indices. Such information can be told to drudge by using the
Drudge.set_symm()
method, by giving generators of the symmetry group
by Perm
instances. For instance, we can do,
dr.set_symm(x, drudge.Perm([1, 0], drudge.NEG))
Then the master simplification algorithm in Tensor.simplify()
is able
to take full advantage of such information.
>>> tensor = dr.einst(x[a, b] * v[a] * v[b] + x[b, a] * v[a] * v[b])
>>> str(tensor)
'sum_{a, b} x[a, b] * v[a] * v[b]\n + sum_{a, b} x[b, a] * v[a] * v[b]'
>>> str(tensor.simplify())
'0'
Normally, drudge subclasses for specific problems add symmetries for some
important indexed bases in the problem. And some drudge subclasses have helper
methods for the setting of such symmetries, like
FockDrudge.set_n_body_base()
and FockDrudge.set_dbbar_base()
.
For the simplification of the noncommutative vector parts, the base
Drudge
class does not consider any commutation rules among the
vectors. It works on the free algebra, while the subclasses could have the
specific commutation rules added for the algebraic system. For instance,
WickDrudge
add abstract commutation rules where all the commutators
have scalar values. Based on it, its special subclass FockDrudge
implements the canonical commutation relations for bosons and the canonical
anti-commutation relations for fermions. Also based on it, the subclass
CliffordDrudge
is capable of treating all kinds of Clifford
algebras, like geometric algebra, Pauli matrices, Dirac matrices, and Majorana
fermion operators. For algebraic systems where the commutator is not always a
scalar, the abstract base class GenQuadDrudge
can be used for
basically all kinds of commutation rules. For instance, its subclass
SU2LatticeDrudge
can be used for \(\mathfrak{su}(2)\) algebra in
Cartan-Weyl form.
These drudge subclasses only has the mathematical commutation rules implemented,
for convenience in solving problems, many drudge subclasses are built-in with a
lot of domain-specific information like the ranges and dummies, which are listed
in Direct support of different problems. For instance, we can easily see the commutativity of
two particle-hole excitation operators by using the PartHoleDrudge
.
>>> phdr = drudge.PartHoleDrudge(spark_ctx)
>>> t = sympy.IndexedBase('t')
>>> u = sympy.IndexedBase('u')
>>> p = phdr.names
>>> a, i = p.a, p.i
>>> excit1 = phdr.einst(t[a, i] * p.c_dag[a] * p.c_[i])
>>> excit2 = phdr.einst(u[a, i] * p.c_dag[a] * p.c_[i])
>>> comm = excit1 | excit2
>>> str(comm)
'sum_{i, a, j, b} t[a, i]*u[b, j] * c[CR, a] * c[AN, i] * c[CR, b] * c[AN, j]\n + sum_{i, a, j, b} -t[a, i]*u[b, j] * c[CR, b] * c[AN, j] * c[CR, a] * c[AN, i]'
>>> str(comm.simplify())
'0'
Note that here basically all things related to the problem, like the vector for creation and annihilation operator, the conventional dummies \(a\) and \(i\) for particle and hole labels, are directly read from the name archive of the drudge. Problem-specific drudges are supposed to give such convenience.
In addition to providing context-dependent information for general tensor operations, drudge subclasses could also provide additional operations on tensors created from them. For instance, for the above commutator, we can directly compute the expectation value with respect to the Fermi vacuum by
>>> str(comm.eval_fermi_vev())
'0'
These additional operations are called tensor methods and are documented in the drudge subclasses.
Examples on real-world applications¶
In this tutorial, some simple examples are run directly inside a Python
interpreter. Actually drudge is designed to work well inside Jupyter
notebooks. By calling the Tensor.display()
method, tensor objects can
be mathematically displayed in Jupyter sessions. An example of interactive
usage of drudge, we have a sample notebook in docs/examples/ccsd.ipynb
in the project source. Also included is a general script gencc.py
for
the automatic derivation of coupled-cluster theories, mostly to demonstrate
using drudge programmatically. And we also have a script for RCCSD theory
to demonstrate its usage in large-scale spin-explicit coupled-cluster theories.
Note about importing drudge¶
In this tutorial, import drudge
and import sympy
is used and we need to
give fully-qualified name to refer to objects in them. Normally, it can be
convenient to use from drudge import *
to import everything from drudge. For
these cases, it needs to be careful that the importation of all objects from
drudge needs to follow the importation of all objects from SymPy, or the SymPy
Range
class will shallow the actual class for symbolic range in drudge.