def construct_comm_opchain(opchain): """Construct operator chain on enlarged physical space to realize commutator.""" # -opchain acting from the left oplist = [np.kron(op, np.identity(len(op))) for op in opchain.oplist] oplist[0] *= -1 opcL = ptn.OpChain(oplist, opchain.qD) # opchain acting from the right oplist = [np.kron(np.identity(len(op)), op.T) for op in opchain.oplist] opcR = ptn.OpChain(oplist, opchain.qD) return [opcL, opcR]
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 heisenberg_XXZ_comm_MPO(L, J, D, h): """Construct commutator with XXZ Heisenberg Hamiltonian 'sum J X X + J Y Y + D Z Z - h Z' on a 1D lattice as MPO.""" # physical quantum numbers (multiplied by 2) qd = np.array([1, -1]) # spin operators Sup = np.array([[0., 1.], [0., 0.]]) Sdn = np.array([[0., 0.], [1., 0.]]) Sz = np.array([[0.5, 0.], [0., -0.5]]) # local two-site and single-site terms lopchains = [ ptn.OpChain([0.5 * J * Sup, Sdn], [2]), ptn.OpChain([0.5 * J * Sdn, Sup], [-2]), ptn.OpChain([D * Sz, Sz], [0]), ptn.OpChain([-h * Sz], []) ] # convert to MPO form, with local terms acting either on first or second physical dimension locopchains = [] for opchain in lopchains: locopchains += construct_comm_opchain(opchain) return ptn.local_opchains_to_MPO(ptn.qnumber_flatten([qd, -qd]), L, locopchains)
def test_from_opchains(self): # dimensions d = 4 L = 5 # physical quantum numbers qd = np.random.randint(-2, 3, size=d) # fictitious operator chains opchains = [] n = np.random.randint(20) for _ in range(n): istart = np.random.randint(L) length = np.random.randint(1, L - istart + 1) oplist = [randn_complex((d, d)) for _ in range(length)] qD = np.random.randint(-2, 3, size=length - 1) # enforce sparsity structure dictated by quantum numbers qDpad = np.pad(qD, 1, mode='constant') for i in range(length): mask = ptn.qnumber_outer_sum( [qd + qDpad[i], -(qd + qDpad[i + 1])]) oplist[i] = np.where(mask == 0, oplist[i], 0) opchains.append(ptn.OpChain(oplist, qD, istart)) # construct MPO representation corresponding to operator chains mpo0 = ptn.MPO.from_opchains(qd, L, opchains) 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' ) # construct full Hamiltonian from operator chains, as reference Href = sum([opc.as_matrix(d, L) for opc in opchains]) # compare self.assertAlmostEqual( np.linalg.norm(mpo0.as_matrix() - Href), 0., delta=1e-10, msg= 'full merging of MPO must be equal to matrix representation of operator chains' )
def test_as_matrix(self): # dimensions d = 3 L = 6 # create random operator chain oplist = [ np.random.normal(size=(d, d)) + 1j * np.random.normal(size=(d, d)) for _ in range(2) ] opchain = ptn.OpChain(oplist=oplist, qD=[0], istart=3) # matrix representation on full Hilbert space A = opchain.as_matrix(d, L) # check dimensions self.assertEqual(A.shape, (d**L, d**L), msg='matrix dimensions') # pad identities on the left and check if matrix representation still matches opchain.pad_identities_left(d) A1 = opchain.as_matrix(d, L) self.assertEqual( np.linalg.norm(A1 - A), 0, msg= 'matrix representation after padding identities on the left must remain unchanged' ) # pad identities on the right and check if matrix representation still matches opchain.pad_identities_right(d, L) self.assertEqual(opchain.length, L, msg='operator chain length') A2 = opchain.as_matrix(d, L) self.assertEqual( np.linalg.norm(A2 - A), 0, msg= 'matrix representation after padding identities on the right must remain unchanged' )