def test_multiply(self): # physical quantum numbers qd = np.random.randint(-2, 3, size=3) # create random matrix product operators op0 = ptn.MPO(qd, [ np.random.randint(-2, 3, size=Di) for Di in [1, 10, 13, 24, 17, 9, 1] ], fill='random') op1 = ptn.MPO(qd, [ np.random.randint(-2, 3, size=Di) for Di in [1, 8, 17, 11, 23, 13, 1] ], fill='random') # MPO multiplication (composition) op = op0 * op1 # reference calculation op_ref = np.dot(op0.as_matrix(), op1.as_matrix()) # relative error err = np.linalg.norm(op.as_matrix() - op_ref) / max( np.linalg.norm(op_ref), 1e-12) self.assertAlmostEqual( err, 0., delta=1e-14, msg='composition of two MPOs must agree with matrix representation' )
def test_add_singlesite(self): # separate test for a single site since implementation is a special case # physical quantum numbers qd = np.random.randint(-2, 3, size=5) # create random matrix product operators acting on a single site # leading and trailing (dummy) virtual bond quantum numbers qD = [np.array([-1]), np.array([-2])] op0 = ptn.MPO(qd, qD, fill='random') op1 = ptn.MPO(qd, qD, fill='random') # MPO addition op = op0 + op1 # reference calculation op_ref = op0.as_matrix() + op1.as_matrix() # relative error err = np.linalg.norm(op.as_matrix() - op_ref) / max( np.linalg.norm(op_ref), 1e-12) self.assertAlmostEqual( err, 0., delta=1e-14, msg='addition two MPOs must agree with matrix representation')
def test_add(self): # physical quantum numbers qd = np.random.randint(-2, 3, size=3) # create random matrix product operators qD0 = [ np.random.randint(-2, 3, size=Di) for Di in [1, 11, 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() op0 = ptn.MPO(qd, qD0, fill='random') op1 = ptn.MPO(qd, qD1, fill='random') # MPO addition op = op0 + op1 # reference calculation op_ref = op0.as_matrix() + op1.as_matrix() # relative error err = np.linalg.norm(op.as_matrix() - op_ref) / max( np.linalg.norm(op_ref), 1e-12) self.assertAlmostEqual( err, 0., delta=1e-14, msg='addition two MPOs must agree with matrix representation')
def cast_to_MPO(mps, qd): """ Cast a matrix product state into MPO form by interpreting the physical dimension as Kronecker product of a pair of dimensions. """ assert np.array_equal(mps.qd, ptn.qnumber_flatten([qd, -qd])) mpo = ptn.MPO(qd, mps.qD, fill=0.0) for i in range(mps.nsites): s = mps.A[i].shape mpo.A[i] = mps.A[i].reshape((len(qd), len(qd), s[1], s[2])).copy() assert ptn.is_qsparse(mpo.A[i], [mpo.qd, -mpo.qd, mpo.qD[i], -mpo.qD[i + 1]]) return mpo
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 main(): # physical local Hilbert space dimension d = 2 # number of lattice sites L = 6 # 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() # realize commutator [H, .] as matrix product operator mpoHcomm = heisenberg_XXZ_comm_MPO(L, J, DH, h) mpoHcomm.zero_qnumbers() print('2-norm of [H, .] operator:', np.linalg.norm(mpoHcomm.as_matrix(), 2)) mpsH = cast_to_MPS(mpoH) print('mpsH.bond_dims:', mpsH.bond_dims) print('ptn.norm(mpsH):', ptn.norm(mpsH)) print('[H, .] applied to H as vector (should be zero): {:g}'.format( np.linalg.norm(np.dot(mpoHcomm.as_matrix(), mpsH.as_vector())))) # initial MPO with random entries (not necessarily Hermitian) Dmax = 40 D = np.minimum( np.minimum(d**(2 * np.arange(L + 1)), d**(2 * (L - np.arange(L + 1)))), Dmax) print('D:', D) np.random.seed(42) op = ptn.MPO(mpoH.qd, [np.zeros(Di, dtype=int) for Di in D], fill='random') # effectively clamp virtual bond dimension for i in range(L): op.A[i][:, :, 2:, :] = 0 op.A[i][:, :, :, 2:] = 0 op.orthonormalize(mode='right') op.orthonormalize(mode='left') # matrix representation op_mat = op.as_matrix() # cast into MPS form psi = cast_to_MPS(op) print('norm of psi:', np.linalg.norm(psi.as_vector())) print('psi.bond_dims:', psi.bond_dims) # check: commutator comm_ref = np.dot(op_mat, mpoH.as_matrix()) - np.dot( mpoH.as_matrix(), op_mat) comm = np.dot(mpoHcomm.as_matrix(), psi.as_vector()).reshape( (2 * L) * [d]).transpose( np.concatenate((2 * np.arange(L, dtype=int), 2 * np.arange(L, dtype=int) + 1))).reshape( (d**L, d**L)) print('commutator reference check error: {:g}'.format( np.linalg.norm(comm - comm_ref))) # initial average energy (should be conserved) en_avr_0 = np.trace(np.dot(mpoH.as_matrix(), op_mat)) en_avr_0_alt = ptn.vdot(mpsH, psi) print('en_avr_0:', en_avr_0) print('abs(en_avr_0_alt - en_avr_0):', abs(en_avr_0_alt - en_avr_0)) # exact Schmidt coefficients (singular values) of initial state sigma_0 = schmidt_coefficients_operator(d, L, op_mat) 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_operator_schmidt_0.pdf') plt.show() print( 'Schmidt coefficients consistency check:', np.linalg.norm( sigma_0 - schmidt_coefficients_wavefunction(d**2, L, psi.as_vector()))) print('entropy of initial state:', entropy((sigma_0 / np.linalg.norm(sigma_0))**2)) # mixed real and imaginary time evolution t = 0.1 + 0.5j # reference calculation: exp(t H) op exp(-t H) op_t_ref = np.dot(np.dot(expm(t * mpoH.as_matrix()), op_mat), expm(-t * mpoH.as_matrix())) # Frobenius norm not preserved by mixed real and imaginary time evolution print('Frobenius norm of initial op: {:g}'.format( np.linalg.norm(op_mat, 'fro'))) print('Frobenius norm of op(t): {:g}'.format( np.linalg.norm(op_t_ref, 'fro'))) # energy should be conserved en_avr_t_ref = np.trace(np.dot(mpoH.as_matrix(), op_t_ref)) print('en_avr_t_ref:', en_avr_t_ref) print('abs(en_avr_t_ref - en_avr_0):', abs(en_avr_t_ref - en_avr_0)) # exact Schmidt coefficients (singular values) of time-evolved state sigma_t = schmidt_coefficients_operator(d, L, op_t_ref) plt.semilogy(np.arange(len(sigma_t)) + 1, sigma_t, '.') plt.xlabel('i') plt.ylabel(r'$\sigma_i$') plt.title( 'Schmidt coefficients of time-evolved state (t = {:g})\n(based on exact time evolution)' .format(-1j * t)) plt.savefig('evolution_operator_schmidt_t.pdf') plt.show() S = entropy((sigma_t / np.linalg.norm(sigma_t))**2) print('entropy of time-evolved state:', S) P = tangent_space_projector(psi) print( 'np.linalg.norm(np.dot(P, mpsH.as_vector()) - mpsH.as_vector()) (in general non-zero):', np.linalg.norm(np.dot(P, mpsH.as_vector()) - mpsH.as_vector())) # number of time steps numsteps = 2**(np.arange(5)) err_op = np.zeros(len(numsteps)) # relative energy error err_en = 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(mpoHcomm, psi_t, dt, n, numiter_lanczos=20) op_t = cast_to_MPO(psi_t, op.qd) err_op[i] = np.linalg.norm(op_t.as_matrix() - op_t_ref, ord=1) en_avr_t = np.trace(np.dot(mpoH.as_matrix(), op_t.as_matrix())) # relative energy error err_en[i] = abs(en_avr_t - en_avr_0) / abs(en_avr_0) dtinv = numsteps / abs(t) plt.loglog(dtinv, err_op, '.-') # show quadratic scaling for comparison plt.loglog(dtinv, 1.75e-2 / dtinv**2, '--') plt.xlabel('1/dt') plt.ylabel( r'$\Vert\mathcal{O}[A](t) - \mathcal{O}_\mathrm{ref}(t)\Vert_1$') plt.title( 'TDVP time evolution (applied to operator) rate of convergence for\nHeisenberg XXZ model (J={:g}, D={:g}, h={:g}), L={}, t={:g}' .format(J, DH, h, L, -1j * t)) plt.savefig('evolution_operator_convergence.pdf') plt.show() plt.loglog(dtinv, err_en, '.-') # show quadratic scaling for comparison plt.loglog(dtinv, 1e-2 / dtinv**2, '--') plt.xlabel('1/dt') plt.ylabel( r'$\frac{\vert\epsilon(t) - \epsilon(0)\vert}{\vert\epsilon(0)\vert}, \quad \epsilon(t) = \mathrm{tr}[H \mathcal{O}[A](t)]$' ) plt.title( 'TDVP time evolution (applied to operator) relative energy error for\nHeisenberg XXZ model (J={:g}, D={:g}, h={:g}), L={}, t={:g}' .format(J, DH, h, L, -1j * t)) plt.savefig('evolution_operator_energy_conv.pdf') plt.show()
def test_orthonormalization(self): # create random matrix product operator d = 4 D = [1, 10, 13, 14, 7, 1] mpo0 = ptn.MPO(np.random.randint(-2, 3, size=d), [np.random.randint(-2, 3, size=Di) for Di in D], fill='random') self.assertEqual(mpo0.bond_dims, D, msg='virtual bond dimensions') # density matrix on full Hilbert space rho = mpo0.as_matrix() # performing left-orthonormalization... cL = mpo0.orthonormalize(mode='left') self.assertLessEqual( mpo0.bond_dims[1], d**2, msg= 'virtual bond dimension can only increase by factor of "d^2" per site' ) for i in range(mpo0.nsites): self.assertTrue( ptn.is_qsparse( mpo0.A[i], [mpo0.qd, -mpo0.qd, mpo0.qD[i], -mpo0.qD[i + 1]]), msg='sparsity pattern of MPO tensors must match quantum numbers' ) rhoL = mpo0.as_matrix() # density matrix should now be normalized self.assertAlmostEqual(np.linalg.norm(rhoL, 'fro'), 1., delta=1e-12, msg='density matrix normalization') # density matrices before and after left-normalization must match # (up to normalization factor) self.assertAlmostEqual( np.linalg.norm(cL * rhoL - rho), 0., delta=1e-10, msg= 'density matrices before and after left-normalization must match') # check left-orthonormalization for i in range(mpo0.nsites): s = mpo0.A[i].shape assert s[0] == d and s[1] == d Q = mpo0.A[i].reshape((s[0] * s[1] * s[2], s[3])) QH_Q = np.dot(Q.conj().T, Q) self.assertAlmostEqual(np.linalg.norm(QH_Q - np.identity(s[3])), 0., delta=1e-12, msg='left-orthonormalization') # performing right-orthonormalization... cR = mpo0.orthonormalize(mode='right') self.assertLessEqual( mpo0.bond_dims[-2], d**2, msg= 'virtual bond dimension can only increase by factor of "d^2" per site' ) for i in range(mpo0.nsites): self.assertTrue( ptn.is_qsparse( mpo0.A[i], [mpo0.qd, -mpo0.qd, mpo0.qD[i], -mpo0.qD[i + 1]]), msg='sparsity pattern of MPO 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' ) rhoR = mpo0.as_matrix() # density matrices must match self.assertAlmostEqual( np.linalg.norm(rhoL - cR * rhoR), 0., delta=1e-10, msg= 'density matrices after left- and right-orthonormalization must match' ) # check right-orthonormalization for i in range(mpo0.nsites): s = mpo0.A[i].shape assert s[0] == d and s[1] == d Q = mpo0.A[i].transpose((0, 1, 3, 2)).reshape( (s[0] * s[1] * s[3], 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='right-orthonormalization')