The algorithm#

Problem formulation#

In the simplest setting, Pymablock finds a series of the unitary transformation \(\mathcal{U}\) (we use calligraphic letters to denote series) that block-diagonalizes the Hamiltonian

(1)#\[\begin{split}\mathcal{H} = H_0 + \mathcal{H}',\quad H_0 = \begin{pmatrix} H_0^{AA} & 0\\ 0 & H_0^{BB} \end{pmatrix},\end{split}\]

with \(\mathcal{H}' = \mathcal{H}'_{S} + \mathcal{H}'_{R}\) containing an arbitrary number and orders of perturbations with block-diagonal components \(\mathcal{H}'_{S}\) and block-offdiagonal components \(\mathcal{H}'_{R}\), respectively. Pymablock’s algorithm, however, does not just work with \(2 \times 2\) block matrices: it can perturbatively cancel any set of offdiagonal elements of \(\mathcal{H}\). For that we define the selected part of an operator (subscript \(S\)) as the subset of its matrix elements that we want to keep, and the remaining part (subscript \(R\)) as the rest. For example, selected and remaining parts may look like this:

_images/cd5941cc7914681e63a34db61d8e2b48c99791c5040186b28239828804e6a1d6.png

As another generalization compared to the simplest case, the series here may be multivariate, and they represent sums of the form

\[ \mathcal{A} = \sum_{n_1=0}^\infty \sum_{n_2=0}^\infty \cdots \sum_{n_k=0}^\infty \lambda_1^{n_1} \lambda_2^{n_2} \cdots \lambda_k^{n_k} A_{n_1, n_2, \ldots, n_k}, \]

where \(\lambda_i\) are the perturbation parameters and \(A_{n_1, n_2, \ldots, n_k}\) are linear operators.

The problem statement, therefore, is finding \(\mathcal{U}\) and \(\tilde{\mathcal{H}}\) such that:

(2)#\[\tilde{\mathcal{H}} = \mathcal{U}^\dagger \mathcal{H} \mathcal{U},\quad \tilde{\mathcal{H}}_{R} = 0,\quad \mathcal{U}^\dagger \mathcal{U} = 1,\]

where series multiply according to the Cauchy product:

\[ \mathcal{C} = \mathcal{A}\mathcal{B} \Leftrightarrow C_\mathbf{n} = \sum_{\mathbf{m} + \mathbf{p} = \mathbf{n}} A_\mathbf{m} B_\mathbf{p}. \]

How to solve the problem?#

There are many ways to solve this problem that give identical expressions for \(\mathcal{U}\) and \(\tilde{\mathcal{H}}\). We are searching for a procedure that satisfies three additional constraints:

  • It has the same complexity scaling as a Cauchy product.

  • It does not require multiplications by \(H_0\). This is because in perturbation theory, \(n\)-th order corrections to \(\tilde{\mathcal{H}}\) carry \(n\) energy denominators \(1/(E_i - E_j)\) (see here). Therefore, any additional multiplications by \(H_0\) must cancel with additional energy denominators. Multiplying by \(H_0\) is therefore unnecessary work, and it gives longer intermediate expressions.

  • It requires only one Cauchy product by \(\mathcal{H}_{S}\), the selected part of \(\mathcal{H}\). By considering \(\mathcal{H}_{R}=0\), we observe that \(\mathcal{H}_{S}\) must at least enter \(\tilde{\mathcal{H}}\) as an added term, without any products. Moreover, because \(\mathcal{U}\) depends on the entire Hamiltonian, there must be at least one Cauchy product by \(\mathcal{H}'_{S}\). This lower bound turns out to be possible to achieve.

The goal of our algorithm is thus to be efficient and to produce compact results that do not require further simplifications.

Our solution#

To find \(\mathcal{U}\), let us separate it into an identity and \(\mathcal{U}' = \mathcal{W} + \mathcal{V}\):

(3)#\[\mathcal{U} = 1 + \mathcal{U}' = 1 + \mathcal{W} + \mathcal{V},\quad \mathcal{W}^\dagger = \mathcal{W},\quad \mathcal{V}^\dagger = -\mathcal{V}.\]

First, we use the unitarity condition \(\mathcal{U}^\dagger \mathcal{U} = 1\) by substituting \(\mathcal{U}'\) into it and obtain

(4)#\[\begin{split}\toggle{ \mathcal{W} = \texttip{\color{red}{\ldots}}{click to expand} = -\frac{1}{2} \mathcal{U}'^\dagger \mathcal{U}'. }{ \begin{align} \mathcal{W} &= \frac{1}{2}(\mathcal{U}'^\dagger + \mathcal{U}') \\ &= \frac{1}{2} \Big[(1 + \mathcal{U}'^\dagger)(1+\mathcal{U}') - 1 - \mathcal{U}'^\dagger \mathcal{U}' \Big] \\ &= -\frac{1}{2} \mathcal{U}'^\dagger \mathcal{U}'. \end{align} } \endtoggle\end{split}\]

Because \(\mathcal{U}'\) has no \(0\)-th order term, \((\mathcal{U}'^\dagger \mathcal{U}')_\mathbf{n}\) does not depend on the \(\mathbf{n}\)-th order of \(\mathcal{U}'\) nor \(\mathcal{W}\). More generally, a Cauchy product \(\mathcal{A}\mathcal{B}\) where \(\mathcal{A}\) and \(\mathcal{B}\) have no \(0\)-th order terms depends on \(\mathcal{A}_1, \ldots, \mathcal{A}_{n-1}\) and \(\mathcal{B}_1, \ldots, \mathcal{B}_{n-1}\). This allows us to use Cauchy products to define recurrence relations, which we apply throughout the algorithm. Therefore, we compute \(\mathcal{W}\) as a Cauchy product of \(\mathcal{U}'\) with itself. This recurrence relation is the first secret ingredient of Pymablock

To compute \(\mathcal{U}'\) we also need to find \(\mathcal{V}\), which is defined by the requirement \(\tilde{\mathcal{H}}_{R} = 0\). Additionally, we constrain \(\mathcal{V}\) to have no selected part: \(\mathcal{V}_{S} = 0\), so that its norm and the norm of \(\mathcal{U}'\) are minimal. This choice is known as the least action principle and is equivalent to the Schrieffer-Wolff transformation in the \(2\times 2\) block case.

To find \(\mathcal{V}\), we need to first look at the transformed Hamiltonian:

\[ \tilde{\mathcal{H}} = \mathcal{U}^\dagger \mathcal{H} \mathcal{U} = \mathcal{H}_{S} + \mathcal{U}'^\dagger \mathcal{H}_{S} + \mathcal{H}_{S} \mathcal{U}' + \mathcal{U}'^\dagger \mathcal{H}_{S} \mathcal{U}' + \mathcal{U}^\dagger\mathcal{H}'_{R}\mathcal{U}, \]

where we used \(\mathcal{U}=1+\mathcal{U}'\) and \(\mathcal{H} = \mathcal{H}_{S} + \mathcal{H}'_{R}\), since \((H_0)_{R}=0\) by definition.

Because we want to avoid unnecessary products by \(\mathcal{H}_{S}\), we need to get rid of the terms that contain it by replacing them with an alternative expression. Our strategy is to define an auxiliary operator \(\mathcal{X}\) that we can compute without ever multiplying by \(\mathcal{H}_{S}\). Like \(\mathcal{U}'\), \(\mathcal{X}\) needs to be defined via a recurrence relation, which we will find later. Because the expression above has \(\mathcal{H}_{S}\) multiplied by \(\mathcal{U}'\) from the left and from the right, we get rid of these terms by making sure that \(\mathcal{H}_{S}\) multiplies terms from one side only. To achieve this, we choose \(\mathcal{X}=\mathcal{Y}+\mathcal{Z}\) to be the commutator between \(\mathcal{U}'\) and \(\mathcal{H}_{S}\):

(5)#\[\mathcal{X} \equiv [\mathcal{U}', \mathcal{H}_{S}] = \mathcal{Y} + \mathcal{Z}, \quad \mathcal{Y} \equiv [\mathcal{V}, \mathcal{H}_{S}] = \mathcal{Y}^\dagger,\quad \mathcal{Z} \equiv [\mathcal{W}, \mathcal{H}_{S}] = -\mathcal{Z}^\dagger.\]

In the common case, when \(\mathcal{A}_{S}\) is a block-diagonal part of a matrix, \(\mathcal{Y}\) is block off-diagonal. Additionally, in the case of \(2\times 2\) block diagonalization, \(\mathcal{Z}\) is block-diagonal. We use \(\mathcal{H}_{S} \mathcal{U}' = \mathcal{U}' \mathcal{H}_{S} -\mathcal{X}\) to move \(\mathcal{H}_{S}\) through to the right and find

(6)#\[\begin{split}\toggle{ \tilde{\mathcal{H}} = \texttip{\color{red}{\ldots}}{click to expand} = \mathcal{H}_{S} - \mathcal{X} - \mathcal{U}'^\dagger \mathcal{X} + \mathcal{U}^\dagger\mathcal{H}'_{R}\mathcal{U}, }{ \begin{align*} \tilde{\mathcal{H}} &= \mathcal{H}_{S} + \mathcal{U}'^\dagger \mathcal{H}_{S} + (\mathcal{H}_{S} \mathcal{U}') + \mathcal{U}'^\dagger \mathcal{H}_{S} \mathcal{U}' + \mathcal{U}^\dagger\mathcal{H}'_{R}\mathcal{U} \\ &= \mathcal{H}_{S} + \mathcal{U}'^\dagger \mathcal{H}_{S} + \mathcal{U}'\mathcal{H}_{S} - \mathcal{X} + \mathcal{U}'^\dagger (\mathcal{U}' \mathcal{H}_{S} - \mathcal{X}) + \mathcal{U}^\dagger\mathcal{H}_{R}\mathcal{U}\\ &= \mathcal{H}_{S} + (\mathcal{U}'^\dagger + \mathcal{U}' + \mathcal{U}'^\dagger \mathcal{U}')\mathcal{H}_{S} - \mathcal{X} - \mathcal{U}'^\dagger \mathcal{X} + \mathcal{U}^\dagger\mathcal{H}'_{R}\mathcal{U}\\ &= \mathcal{H}_{S} - \mathcal{X} - \mathcal{U}'^\dagger \mathcal{X} + \mathcal{U}^\dagger\mathcal{H}'_{R}\mathcal{U}, \end{align*} } \endtoggle\end{split}\]

where the terms multiplied by \(\mathcal{H}_{S}\) cancel by unitarity.

The transformed Hamiltonian does not contain products by \(\mathcal{H}_{S}\) anymore, but it does depend on \(\mathcal{X}\), an auxiliary operator whose recurrent definition we do not know yet. To find it, we first focus on its anti-Hermitian part, \(\mathcal{Z}\). Since recurrence relations are expressions whose right hand side contains Cauchy products between series, we need to find a way to make a product appear. This is where the unitarity condition \(\mathcal{U}'^\dagger + \mathcal{U}' = -\mathcal{U}'^\dagger \mathcal{U}'\) comes in handy and gives:

(7)#\[\begin{split}\toggle{ \mathcal{Z} = \texttip{\color{red}{\ldots}}{click to expand} = \frac{1}{2}(-\mathcal{U}'^\dagger\mathcal{X} + \mathcal{X}^\dagger\mathcal{U}'). }{ \begin{align} \mathcal{Z} &= \frac{1}{2} (\mathcal{X} - \mathcal{X}^{\dagger}) \\ &= \frac{1}{2}\Big[ (\mathcal{U}' + \mathcal{U}'^{\dagger}) \mathcal{H}_{S} - \mathcal{H}_{S} (\mathcal{U}' + \mathcal{U}'^{\dagger}) \Big] \\ &= \frac{1}{2} \Big[ - \mathcal{U}'^{\dagger} (\mathcal{U}'\mathcal{H}_{S} - \mathcal{H}_{S} \mathcal{U}') + (\mathcal{U}'\mathcal{H}_{S} - \mathcal{H}_{S} \mathcal{U}')^{\dagger} \mathcal{U}' \Big] \\ &= \frac{1}{2} (-\mathcal{U}'^{\dagger} \mathcal{X} + \mathcal{X}^{\dagger} \mathcal{U}'). \end{align} } \endtoggle\end{split}\]

Similar to computing \(W_\mathbf{n}\), computing \(Z_\mathbf{n}\) requires lower orders of \(\mathcal{X}\) and \(\mathcal{U}'\), all blocks included. This is our second secret ingredient✨

Then, we compute \(\mathcal{Y}\) (the Hermitian part of \(\mathcal{X}\)) by requiring that \(\tilde{\mathcal{H}}_{R} = 0\) and find

(8)#\[\mathcal{Y}_{R} = (\mathcal{U}^\dagger \mathcal{H}'_{R} \mathcal{U} - \mathcal{U}'^\dagger \mathcal{X} - \mathcal{Z})_{R}.\]

Once again, despite \(\mathcal{X}\) enters the right hand side, because all the terms lack 0-th order, this defines a recursive relation for \(\mathcal{Y}_{R}\). To find \(\mathcal{Y}_{S}\) we use the definition of \(\mathcal{Y}\) in (5), which fixes \(\mathcal{V}\) as a solution of:

(9)#\[[\mathcal{V}, H_0] = \mathcal{Y} - [\mathcal{V}, \mathcal{H}'_{S}],\]

which is a continuous-time Lyapunov equation for \(\mathcal{V}\). In order for this equation to be satisfiable, the selected part of the right hand side must vanish, since the left hand side only has the remaining part. Therefore we find the selected part of \(\mathcal{Y}\) as:

(10)#\[\mathcal{Y}_{S} = [\mathcal{V}, \mathcal{H}'_{S}]_{S},\]

and it vanishes if \(\mathcal{A}_{S}\) corresponds to a block-diagonal matrix. This is our last secret ingredient✨

The final part is straightforward. Finding \(\mathcal{V}\) from \(\mathcal{Y}\) amounts to solving a Sylvester’s equation, which we only need to solve once for every new order. This is the only step in the algorithm that requires a direct multiplication by \(\mathcal{H}'_{S}\). In the eigenbasis of \(H_0\), the solution of Sylvester’s equation is \(V_{\mathbf{n}, ij} = (\mathcal{Y}_{R} - [\mathcal{V}, \mathcal{H}'_{S}]_{R})_{\mathbf{n}, ij}/(E_i - E_j)\), where \(E_i\) are the eigenvalues of \(H_0\). However, even if the eigenbasis of \(H_0\) is not available, there are efficient algorithms to solve Sylvester’s equation, see below.

Actual algorithm#

We now have the complete algorithm:

  1. Define series \(\mathcal{U}'\) and \(\mathcal{X}\) and make use of their block structure and Hermiticity.

  2. To define the Hermitian part \(\mathcal{U}'\), use \(\mathcal{W} = -\mathcal{U}'^\dagger\mathcal{U}'/2\).

  3. To find the antihermitian part of \(\mathcal{U}'\), solve Sylvester’s equation \([\mathcal{V}, H_0] = (\mathcal{Y} - [\mathcal{V}, \mathcal{H}'_{S}])_{R}\). This requires \(\mathcal{X}\).

  4. To find the antihermitian part of \(\mathcal{X}\), define \(\mathcal{Z} = (-\mathcal{U}'^\dagger\mathcal{X} + \mathcal{X}^\dagger\mathcal{U}')/2\).

  5. For the Hermitian part of \(\mathcal{X}\), use \(\mathcal{Y} = (-\mathcal{U}'^\dagger\mathcal{X} + \mathcal{U}^\dagger\mathcal{H}'_{R}\mathcal{U})_{R} + [\mathcal{V}, \mathcal{H}'_{S}]_{S}\).

  6. Compute the effective Hamiltonian as \(\tilde{\mathcal{H}}_{S} = \mathcal{H}_{S} - \mathcal{X} - \mathcal{U}'^\dagger \mathcal{X} + \mathcal{U}^\dagger\mathcal{H}'_{R}\mathcal{U}\).

How to use Pymablock on large numerical Hamiltonians?#

Solving Sylvester’s equation and computing the matrix products are the most expensive steps of the algorithms for large Hamiltonians. Pymablock can efficiently construct an effective Hamiltonian of a small subspace even when the full Hamiltonian is a sparse matrix that is too costly to diagonalize. It does so by avoiding forming a matrix representation of operators in the implicit subspace—the one without known eigenvectors, and by utilizing the sparsity of the Hamiltonian to compute the Green’s function. To do so, Pymablock uses either the MUMPS sparse solver via the python-mumps wrapper or the KPM method.

This approach was originally introduced in this work.