In what order should multiple contractions be evaluated?
How should loops be nested?
Recall: should evaluate \(M_1 M_2\cdots M_n \mathbf{v}\) as \(O(N^2)\) matrix-vector multiplications, rather than \(O(N^3)\) matrix-matrix multiplications followed by matrix-vector multiplication
In general no efficient algorithm to find best way!
einsum can use a “greedy” algorithm (contracting pair of tensors with lowest cost at each step)
Information on contraction order provided by np.einsum_path
First two pages and last two do not link to each other, so \(\boldsymbol{\pi}^T = \begin{pmatrix} \pi_1 & \pi_1 & \pi_2 & \pi_2 \end{pmatrix}\) is a stationary state for any \(\pi_{1,2}\)
Way out is to modify Markov chain to restore ergodicity and give a unique \(\boldsymbol{\pi}\)
Crawler either moves as before with probability \(\alpha\)or moves with probability \(1-\alpha\) to a random webpage \[
\alpha\mathsf{P} + (1-\alpha)\mathbf{t} \mathbf{e}^T
\] where \(\mathbf{e}^T= (1, 1, \ldots 1)\) and \(\mathbf{t}\) is a “teleporting” vector
Matrix has positive (i.e. \(>0\)) entries: there is a unique stationary state (and hence ranking)
Further modification is required to teleport away from “dangling” webpages without any outgoing links
Power method is basis of more sophisticated algorithms such as Lanczos iteration
All based on idea that matrix-vector products preferred over matrix-matrix products
Provide only incomplete information about eigenvalues and eigenvectors
Sparsity
Many matrices that we meet in physical applications are sparse, meaning that most of elements are zero
\(\exists\) following simple result: best low rank approximation of rank \(r\) obtained by taking SVD and discarding all but \(r\) largest singular values from matrix \(\Sigma\)
i.e. retain only \(r\) “most important” directions \(\mathbf{v}_i\in \mathbb{C}^n\) and \(\mathbf{u}_i\in \mathbb{C}^m\)
SVD arises naturally in QM of composite systems (with two subsystems)
Example: two spins \(\mathbf{S}_A\) and \(\mathbf{S}_B\)
Hilbert space of each spin has dimension \(n_{A,B}\equiv 2S_{A,B}+1\), where \(\mathbf{S}_{A,B}^2=S_{A,B}(S_{A,B}+1)\) (e.g. 2 for spin-1/2).
General state lives in \(n_A\times n_B\) dimensional Hilbert space
Write in terms of basis vectors \(\ket{a}_A\) and \(\ket{b}_B\) for A and B subsystems as \[
\ket{\Psi_{AB}} = \sum_{a=1}^{n_A}\sum_{b=1}^{n_B} \psi_{ab}\ket{a}_A\ket{b}_B
\]
Regard components \(\psi_{ab}\) as a matrix and perform SVD
Equivalent to finding new orthonormal bases \(\ket{\tilde n}_{A,B}\) for two spaces s.t. action of \(\psi_{ab}\) maps between basis vectors of two susbsystems (with rescaling)
\(\sigma^{x,y,z}\) are usual Pauli matrices and subscript \(j\) means that matrix acts only the \(j\)th index of the wavefunction
Usually impose periodic boundary conditions: \(\sigma^a_{j+N}=\sigma^a_j\)
In tensor diagram notation
Number of components of wavefunction \(\psi_{a_1,\ldots a_N}\) is \(2^N\)
\[
H\ket{\Psi} = E\ket{\Psi}
\]
Naive matrix-vector multiplication has complexity \(O(2^{2N})\): very bad idea.
Take advantage of structure, using sparsity of Hamiltonian
\(H=\sum_j h_{j,j+1}\) consists of a sum of local terms, each acting on neighbouring pair
Define function that acts on wavefunction with each \(h_{j,j+1}\)
# by Glen Evenbly (c) for www.tensors.net, (v1.2) - last modified 6/2019def doApplyHam(psiIn: np.ndarray, hloc: np.ndarray, N: int, usePBC: bool):""" Applies local Hamiltonian, given as sum of nearest neighbor terms, to an input quantum state. Args: psiIn: vector of length d**N describing the quantum state. hloc: array of ndim=4 describing the nearest neighbor coupling. N: the number of lattice sites. usePBC: sets whether to include periodic boundary term. Returns: np.ndarray: state psi after application of the Hamiltonian. """ d = hloc.shape[0] psiOut = np.zeros(psiIn.size)for k inrange(N -1):# apply local Hamiltonian terms to sites [k,k+1] psiOut += np.tensordot(hloc.reshape(d**2, d**2), psiIn.reshape(d**k, d**2, d**(N -2- k)), axes=[[1], [1]]).transpose(1, 0, 2).reshape(d**N)if usePBC:# apply periodic term psiOut += np.tensordot(hloc.reshape(d, d, d, d), psiIn.reshape(d, d**(N -2), d), axes=[[2, 3], [2, 0]] ).transpose(1, 2, 0).reshape(d**N)return psiOut
Complexity is \(O(N 2^N)\)
\(2^N\) arises from tensor contractions over indices of a pair of sites for each assignment of the remaining \(N-2\) indices
Still exponential, but exponentially better than \(O(4^N)\)!
Use this to instantiate a LinearOperator which is passed into eigenvalue solver (scipy.sparse.linalg.eigsh)
"""by Glen Evenbly (c) for www.tensors.net, (v1.2) - last modified 06/2020"""from scipy.sparse.linalg import LinearOperator, eigshfrom timeit import default_timer as timer# Simulation parametersmodel ='XX'# select 'XX' model of 'ising' modelNsites =18# number of lattice sitesusePBC =True# use periodic or open boundariesnumval =1# number of eigenstates to compute# Define Hamiltonian (quantum XX model)d =2# local dimensionsX = np.array([[0, 1.0], [1.0, 0]])sY = np.array([[0, -1.0j], [1.0j, 0]])sZ = np.array([[1.0, 0], [0, -1.0]])sI = np.array([[1.0, 0], [0, 1.0]])if model =='XX': hloc = (np.real(np.kron(sX, sX) + np.kron(sY, sY))).reshape(2, 2, 2, 2) EnExact =-4/ np.sin(np.pi / Nsites) # Note: only for PBCelif model =='ising': hloc = (-np.kron(sX, sX) +0.5* np.kron(sZ, sI) +0.5* np.kron(sI, sZ) ).reshape(2, 2, 2, 2) EnExact =-2/ np.sin(np.pi / (2* Nsites)) # Note: only for PBC# cast the Hamiltonian 'H' as a linear operatordef doApplyHamClosed(psiIn):return doApplyHam(psiIn, hloc, Nsites, usePBC)H = LinearOperator((2**Nsites, 2**Nsites), matvec=doApplyHamClosed)# do the exact diagstart_time = timer()Energy, psi = eigsh(H, k=numval, which='SA')diag_time = timer() - start_time# check with exact energyEnErr = Energy[0] - EnExact # should equal to zeroprint('NumSites: %d, Time: %1.2f, Energy: %e, EnErr: %e'% (Nsites, diag_time, Energy[0], EnErr))