def test_vdot(self): # physical dimension d = 4 # create random matrix product states psi = ptn.MPS(np.zeros(d, dtype=int), [np.zeros(Di, dtype=int) for Di in [1, 3, 9, 13, 4, 1]], fill='random') chi = ptn.MPS(np.zeros(d, dtype=int), [np.zeros(Di, dtype=int) for Di in [1, 4, 7, 8, 2, 1]], fill='random') # calculate dot product <chi | psi> s = ptn.vdot(chi, psi) # reference value s_ref = np.vdot(chi.as_vector(), psi.as_vector()) # relative error err = abs(s - s_ref) / max(abs(s_ref), 1e-12) self.assertAlmostEqual(err, 0., delta=1e-12, msg='dot product must match reference value')
def test_sub_singlesite(self): # separate test for a single site since implementation is a special case # physical quantum numbers qd = np.random.randint(-2, 3, size=7) # create random matrix product states for a single site # leading and trailing (dummy) virtual bond quantum numbers qD = [np.array([-1]), np.array([-2])] mps0 = ptn.MPS(qd, qD, fill='random') mps1 = ptn.MPS(qd, qD, fill='random') # MPS subtraction mps = mps0 - mps1 # reference calculation mps_ref = mps0.as_vector() - mps1.as_vector() # relative error err = np.linalg.norm(mps.as_vector() - mps_ref) / max( np.linalg.norm(mps_ref), 1e-12) self.assertAlmostEqual( err, 0., delta=1e-14, msg= 'subtraction of two matrix product states must agree with vector representation' )
def test_single_site(self): # number of lattice sites L = 10 # time step can have both real and imaginary parts; # for real-time evolution use purely imaginary dt! dt = 0.02 - 0.05j # number of steps numsteps = 12 # construct matrix product operator representation of Heisenberg Hamiltonian J = 4.0/3 D = 5.0/13 h = -2.0/7 mpoH = ptn.heisenberg_XXZ_MPO(L, J, D, h) # fix total spin quantum number of wavefunction (trailing virtual bond) spin_tot = 2 # enumerate all possible virtual bond quantum numbers (including multiplicities); # will be implicitly reduced by orthonormalization steps below qD = [np.array([0])] for i in range(L-1): qD.append(np.sort(np.array([q + mpoH.qd for q in qD[-1]]).reshape(-1))) qD.append(np.array([2*spin_tot])) # initial wavefunction as MPS with random entries psi = ptn.MPS(mpoH.qd, qD, fill='random') psi.orthonormalize(mode='left') psi.orthonormalize(mode='right') # effectively clamp virtual bond dimension of initial state Dinit = 8 for i in range(L): psi.A[i][:, Dinit:, :] = 0 psi.A[i][:, :, Dinit:] = 0 # orthonormalize again psi.orthonormalize(mode='left') self.assertEqual(psi.qD[-1][0], 2*spin_tot, msg='trailing bond quantum number must not change during orthonormalization') # total spin operator as MPO Sztot = ptn.local_opchains_to_MPO(mpoH.qd, L, [ptn.OpChain([np.diag([0.5, -0.5])], [])]) # explicitly compute average spin spin_avr = ptn.operator_average(psi, Sztot) self.assertAlmostEqual(abs(spin_avr - spin_tot), 0, delta=1e-14, msg='average spin must be equal to prescribed value') # reference time evolution psi_ref = np.dot(expm(-dt*numsteps * mpoH.as_matrix()), psi.as_vector()) # run TDVP time evolution ptn.integrate_local_singlesite(mpoH, psi, dt, numsteps, numiter_lanczos=5) # compare time-evolved wavefunctions self.assertAlmostEqual(np.linalg.norm(psi.as_vector() - psi_ref), 0, delta=2e-5, msg='time-evolved wavefunction obtained by single-site MPS time evolution must match reference')
def test_single_site(self): # number of lattice sites L = 10 # number of left and right sweeps numsweeps = 4 # minimization seems to work better when disabling quantum numbers # (for a given maximal bond dimension) # construct matrix product operator representation of Heisenberg Hamiltonian J = 4.0 / 5 D = 8.0 / 3 h = -2.0 / 7 mpoH = ptn.heisenberg_XXZ_MPO(L, J, D, h) mpoH.zero_qnumbers() # initial wavefunction as MPS with random entries D = [1] + (L - 1) * [28] + [1] psi = ptn.MPS(mpoH.qd, [np.zeros(Di, dtype=int) for Di in D], fill='random') en_min = ptn.calculate_ground_state_local_singlesite( mpoH, psi, numsweeps) # value after last iteration E0 = en_min[-1] # reference spectrum and wavefunctions en_ref, V_ref = np.linalg.eigh(mpoH.as_matrix()) # compare ground state energy self.assertAlmostEqual( E0, en_ref[0], delta=1e-13, msg= 'ground state energy obtained by single-site optimization must match reference' ) # compare ground state wavefunction psi_vec = psi.as_vector() # multiply by phase factor to match (real-valued) reference wavefunction i = np.argmax(abs(psi_vec)) z = psi_vec[i] psi_vec *= z.conj() / abs(z) if V_ref[i, 0] < 0: psi_vec = -psi_vec self.assertAlmostEqual( np.linalg.norm(psi_vec - V_ref[:, 0]), 0, delta=1e-7, msg= 'ground state wavefunction obtained by single-site optimization must match reference' )
def cast_to_MPS(mpo): """ Cast a matrix product operator into MPS form by combining the pair of local physical dimensions into one dimension. """ mps = ptn.MPS(ptn.qnumber_flatten([mpo.qd, -mpo.qd]), mpo.qD, fill=0.0) for i in range(mpo.nsites): s = mpo.A[i].shape mps.A[i] = mpo.A[i].reshape((s[0] * s[1], s[2], s[3])).copy() assert ptn.is_qsparse(mps.A[i], [mps.qd, mps.qD[i], -mps.qD[i + 1]]) return mps
def test_add(self): # physical quantum numbers qd = np.random.randint(-2, 3, size=5) # create random matrix product states qD0 = [np.random.randint(-2, 3, size=Di) for Di in [1, 8, 15, 23, 18, 9, 1]] qD1 = [np.random.randint(-2, 3, size=Di) for Di in [1, 7, 23, 11, 17, 13, 1]] # leading and trailing (dummy) virtual bond quantum numbers must agree qD1[ 0] = qD0[ 0].copy() qD1[-1] = qD0[-1].copy() mps0 = ptn.MPS(qd, qD0, fill='random') mps1 = ptn.MPS(qd, qD1, fill='random') # MPS addition mps = mps0 + mps1 # reference calculation mps_ref = mps0.as_vector() + mps1.as_vector() # relative error err = np.linalg.norm(mps.as_vector() - mps_ref) / max(np.linalg.norm(mps_ref), 1e-12) self.assertAlmostEqual(err, 0., delta=1e-14, msg='addition of two matrix product states must agree with vector representation')
def test_operator_average(self): # physical dimension d = 3 # physical quantum numbers qd = np.random.randint(-1, 2, size=d) # create random matrix product state D = [1, 7, 26, 19, 25, 8, 1] psi = ptn.MPS(qd, [np.random.randint(-1, 2, size=Di) for Di in D], fill='random') # rescale to achieve norm of order 1 for i in range(psi.nsites): psi.A[i] *= 5 # create random matrix product operator D = [1, 5, 16, 14, 17, 4, 1] # set bond quantum numbers to zero since otherwise, # sparsity pattern often leads to <psi | op | psi> = 0 op = ptn.MPO(qd, [np.zeros(Di, dtype=int) for Di in D], fill='random') # calculate average (expectation value) <psi | op | psi> avr = ptn.operator_average(psi, op) # reference value based on full Fock space representation x = psi.as_vector() avr_ref = np.vdot(x, np.dot(op.as_matrix(), x)) # relative error err = abs(avr - avr_ref) / max(abs(avr_ref), 1e-12) self.assertAlmostEqual( err, 0., delta=1e-12, msg='operator average must match reference value')
def test_norm(self): # physical dimension d = 3 # create random matrix product state psi = ptn.MPS( np.zeros(d, dtype=int), [np.zeros(Di, dtype=int) for Di in [1, 3, 5, 8, 7, 2, 1]], fill='random') # calculate the norm of psi using the MPS representation n = ptn.norm(psi) # reference value n_ref = np.linalg.norm(psi.as_vector()) # relative error err = abs(n - n_ref) / max(abs(n_ref), 1e-12) self.assertAlmostEqual( err, 0., delta=1e-12, msg='wavefunction norm must match reference value')
def test_orthonormalization(self): # create random matrix product state d = 7 D = [1, 4, 15, 13, 7, 1] mps0 = ptn.MPS(np.random.randint(-2, 3, size=d), [np.random.randint(-2, 3, size=Di) for Di in D], fill='random') self.assertEqual(mps0.bond_dims, D, msg='virtual bond dimensions') # wavefunction on full Hilbert space psi = mps0.as_vector() # performing left-orthonormalization... cL = mps0.orthonormalize(mode='left') self.assertLessEqual(mps0.bond_dims[1], d, msg='virtual bond dimension can only increase by a factor of "d" per site') for i in range(mps0.nsites): self.assertTrue(ptn.is_qsparse(mps0.A[i], [mps0.qd, mps0.qD[i], -mps0.qD[i+1]]), msg='sparsity pattern of MPS tensors must match quantum numbers') psiL = mps0.as_vector() # wavefunction should now be normalized self.assertAlmostEqual(np.linalg.norm(psiL), 1., delta=1e-12, msg='wavefunction normalization') # wavefunctions before and after left-normalization must match # (up to normalization factor) self.assertAlmostEqual(np.linalg.norm(cL*psiL - psi), 0., delta=1e-10, msg='wavefunctions before and after left-normalization must match') # check left-orthonormalization for i in range(mps0.nsites): s = mps0.A[i].shape assert s[0] == d Q = mps0.A[i].reshape((s[0]*s[1], s[2])) QH_Q = np.dot(Q.conj().T, Q) self.assertAlmostEqual(np.linalg.norm(QH_Q - np.identity(s[2])), 0., delta=1e-12, msg='left-orthonormalization') # performing right-orthonormalization... cR = mps0.orthonormalize(mode='right') self.assertLessEqual(mps0.bond_dims[-2], d, msg='virtual bond dimension can only increase by a factor of "d" per site') for i in range(mps0.nsites): self.assertTrue(ptn.is_qsparse(mps0.A[i], [mps0.qd, mps0.qD[i], -mps0.qD[i+1]]), msg='sparsity pattern of MPS tensors must match quantum numbers') self.assertAlmostEqual(abs(cR), 1., delta=1e-12, msg='normalization factor must have magnitude 1 due to previous left-orthonormalization') psiR = mps0.as_vector() # wavefunctions must match self.assertAlmostEqual(np.linalg.norm(psiL - cR*psiR), 0., delta=1e-10, msg='wavefunctions after left- and right-orthonormalization must match') # check right-orthonormalization for i in range(mps0.nsites): s = mps0.A[i].shape assert s[0] == d Q = mps0.A[i].transpose((0, 2, 1)).reshape((s[0]*s[2], s[1])) QH_Q = np.dot(Q.conj().T, Q) self.assertAlmostEqual(np.linalg.norm(QH_Q - np.identity(s[1])), 0., delta=1e-12, msg='right-orthonormalization')
def main(): # physical local Hilbert space dimension d = 2 # number of lattice sites L = 10 # construct matrix product operator representation of Heisenberg Hamiltonian J = 1.0 DH = 1.2 h = -0.2 mpoH = ptn.heisenberg_XXZ_MPO(L, J, DH, h) mpoH.zero_qnumbers() # initial wavefunction as MPS with random entries Dmax = 20 D = np.minimum(np.minimum(d**np.arange(L + 1), d**(L - np.arange(L + 1))), Dmax) print('D:', D) np.random.seed(42) psi = ptn.MPS(mpoH.qd, [np.zeros(Di, dtype=int) for Di in D], fill='random') # effectively clamp virtual bond dimension for i in range(L): psi.A[i][:, 3:, :] = 0 psi.A[i][:, :, 3:] = 0 psi.orthonormalize(mode='right') psi.orthonormalize(mode='left') # initial average energy (should be conserved) e_avr_0 = ptn.operator_average(psi, mpoH).real print('e_avr_0:', e_avr_0) # exact singular values (Schmidt coefficients) initial state sigma_0 = schmidt_coefficients(d, L, psi.as_vector()) plt.semilogy(np.arange(len(sigma_0)) + 1, sigma_0, '.') plt.xlabel('i') plt.ylabel('$\sigma_i$') plt.title('Schmidt coefficients of initial state') plt.savefig('evolution_schmidt_0.pdf') plt.show() print('entropy of initial state:', entropy((sigma_0 / np.linalg.norm(sigma_0))**2)) # purely real time evolution t = 0.5j # reference calculation psi_ref = np.dot(expm(-t*mpoH.as_matrix()), psi.as_vector()) # exact Schmidt coefficients (singular values) of time-evolved state sigma_t = schmidt_coefficients(d, L, psi_ref) plt.semilogy(np.arange(len(sigma_t)) + 1, sigma_t, '.') plt.xlabel('i') plt.ylabel('$\sigma_i$') plt.title('Schmidt coefficients of time-evolved state (t = {:g})\n(based on exact time evolution)'.format(t.imag)) plt.savefig('evolution_schmidt_t.pdf') plt.show() print('entropy of time-evolved state:', entropy((sigma_t / np.linalg.norm(sigma_t))**2)) # number of time steps numsteps = 2**(np.arange(5)) err = np.zeros(len(numsteps)) for i, n in enumerate(numsteps): print('n:', n) # time step dt = t / n psi_t = copy.deepcopy(psi) ptn.integrate_local_singlesite(mpoH, psi_t, dt, n, numiter_lanczos=10) err[i] = np.linalg.norm(psi_t.as_vector() - psi_ref) # expecting numerically exact energy conservation # (for real time evolution) e_avr_t = ptn.operator_average(psi_t, mpoH).real print('e_avr_t:', e_avr_t) print('abs(e_avr_t - e_avr_0):', abs(e_avr_t - e_avr_0)) dtinv = numsteps / abs(t) plt.loglog(dtinv, err, '.-') # show quadratic scaling for comparison plt.loglog(dtinv, 1.75e-4/dtinv**2, '--') plt.xlabel('1/dt') plt.ylabel(r'$\Vert\psi[A](t) - \psi_\mathrm{ref}(t)\Vert$') plt.title('TDVP time evolution rate of convergence (t = {:g}) for\nHeisenberg XXZ model (J={:g}, D={:g}, h={:g})'.format(t.imag, J, DH, h)) plt.savefig('evolution_convergence.pdf') plt.show()
def main(): # physical dimension d = 3 # fictitious bond dimensions (should be bounded by d^i and d^(L-i)) D = [1, 2, 5, 7, 3, 1] # number of lattice sites L = len(D) - 1 print('L:', L) psi = ptn.MPS(np.zeros(d, dtype=int), [np.zeros(Di, dtype=int) for Di in D], fill='random') # construct MPS derivatives with respect to entries of the A tensors T = [] for i in range(L): s = psi.A[i].shape print('s:', s) for j in range(s[0]): for a in range(s[1]): for b in range(s[2]): # derivative in direction (i, j, a, b) B = np.zeros_like(psi.A[i]) B[j, a, b] = 1 # backup original A[i] tensor Ai = psi.A[i] psi.A[i] = B T.append(psi.as_vector()) # restore A[i] psi.A[i] = Ai T = np.array(T) num_entries = np.sum([Ai.size for Ai in psi.A]) print('T.shape: ', T.shape) print('expected:', (num_entries, d**L)) print('rank of T:', np.linalg.matrix_rank(T)) # number of degrees of freedom based on sandwiching "X" matrices between bonds, # -2 for omitting the leading and trailing entry 1 in D rank = num_entries - ((np.array(D)**2).sum() - 2) print('expected: ', rank) # realization of random X matrices X = [np.identity(1, dtype=complex)] for i in range(L - 1): X.append(np.random.normal(size=(D[i+1], D[i+1])) + 1j*np.random.normal(size=(D[i+1], D[i+1]))) X.append(np.identity(1, dtype=complex)) N = [] for i in range(L): B = np.tensordot(X[i], psi.A[i], axes=(1, 1)).transpose((1, 0, 2)) - \ np.tensordot(psi.A[i], X[i+1], axes=(2, 0)) # backup original A[i] tensor Ai = psi.A[i] psi.A[i] = B N.append(psi.as_vector()) # restore A[i] psi.A[i] = Ai N = np.array(N) print('N.shape:', N.shape) # N should be contained in range of T print('rank of [T, N]:', np.linalg.matrix_rank(np.concatenate((T, N), axis=0)), '(should agree with rank of T)') # should be numerically zero by construction z = np.dot(N.transpose(), np.ones(L)) print('|z|:', np.linalg.norm(z), '(should be numerically zero)') # reference tangent space projector based on T # rank-revealing QR decomposition Q, R, _ = scipy.linalg.qr(T.T, mode='economic', pivoting=True) P_ref = np.dot(Q[:, :rank], Q[:, :rank].conj().T) # construct tangent space projector based on MPS formalism P = tangent_space_projector(psi) # compare print('|P - P_ref|:', np.linalg.norm(P - P_ref), '(should be numerically zero)') # apply projector to psi (psi should remain unaffected) x = psi.as_vector() print('|P psi - psi|:', np.linalg.norm(np.dot(P, x) - x), '(should be numerically zero)') # define another state # fictitious bond dimensions (should be bounded by d^i and d^(L-i)) D = [1, 4, 7, 5, 3, 1] chi = ptn.MPS(np.zeros(d, dtype=int), [np.zeros(Di, dtype=int) for Di in D], fill='random') # tangent space projector corresponding to the sum of two states Psum = tangent_space_projector(psi + chi) # apply projector to psi (should remain unaffected) x = psi.as_vector() print('|Psum psi - psi|:', np.linalg.norm(np.dot(Psum, x) - x), '(should be numerically zero)') # apply projector to chi (should remain unaffected) x = chi.as_vector() print('|Psum chi - chi|:', np.linalg.norm(np.dot(Psum, x) - x), '(should be numerically zero)') # apply projector to psi + chi (should remain unaffected) x = psi.as_vector() + chi.as_vector() print('|Psum (psi + chi) - (psi + chi)|:', np.linalg.norm(np.dot(Psum, x) - x), '(should be numerically zero)')