def test_jellium_hamiltonian_correctly_broken_up(self): grid = Grid(2, 3, 1.) hamiltonian = jellium_model(grid, spinless=True, plane_wave=False) potential_terms, kinetic_terms = ( diagonal_coulomb_potential_and_kinetic_terms_as_arrays(hamiltonian) ) potential = sum(potential_terms, FermionOperator.zero()) kinetic = sum(kinetic_terms, FermionOperator.zero()) true_potential = dual_basis_jellium_model(grid, spinless=True, kinetic=False) true_kinetic = dual_basis_jellium_model(grid, spinless=True, potential=False) for i in range(count_qubits(true_kinetic)): coeff = true_kinetic.terms.get(((i, 1), (i, 0))) if coeff: true_kinetic -= FermionOperator(((i, 1), (i, 0)), coeff) true_potential += FermionOperator(((i, 1), (i, 0)), coeff) self.assertEqual(potential, true_potential) self.assertEqual(kinetic, true_kinetic)
def _fourier_transform_helper(hamiltonian, grid, spinless, phase_factor, vec_func_1, vec_func_2): hamiltonian_t = FermionOperator.zero() normalize_factor = numpy.sqrt(1.0 / float(grid.num_points)) for term in hamiltonian.terms: transformed_term = FermionOperator.identity() for ladder_op_mode, ladder_op_type in term: indices_1 = grid.grid_indices(ladder_op_mode, spinless) vec1 = vec_func_1(indices_1) new_basis = FermionOperator.zero() for indices_2 in grid.all_points_indices(): vec2 = vec_func_2(indices_2) spin = None if spinless else ladder_op_mode % 2 orbital = grid.orbital_id(indices_2, spin) exp_index = phase_factor * 1.0j * numpy.dot(vec1, vec2) if ladder_op_type == 1: exp_index *= -1.0 element = FermionOperator(((orbital, ladder_op_type), ), numpy.exp(exp_index)) new_basis += element new_basis *= normalize_factor transformed_term *= new_basis # Coefficient. transformed_term *= hamiltonian.terms[term] hamiltonian_t += transformed_term return hamiltonian_t
def test_integration_jellium_hamiltonian_with_negation(self): hamiltonian = normal_ordered( jellium_model(Grid(2, 3, 1.), plane_wave=False)) part_a = FermionOperator.zero() part_b = FermionOperator.zero() add_to_a_or_b = 0 # add to a if 0; add to b if 1 for term, coeff in hamiltonian.terms.items(): # Partition terms in the Hamiltonian into part_a or part_b if add_to_a_or_b: part_a += FermionOperator(term, coeff) else: part_b += FermionOperator(term, coeff) add_to_a_or_b ^= 1 reference = normal_ordered(commutator(part_a, part_b)) result = commutator_ordered_diagonal_coulomb_with_two_body_operator( part_a, part_b) self.assertTrue(result.isclose(reference)) negative = commutator_ordered_diagonal_coulomb_with_two_body_operator( part_b, part_a) result += negative self.assertTrue(result.isclose(FermionOperator.zero()))
def test_zero_hamiltonian(self): potential_terms, kinetic_terms = ( diagonal_coulomb_potential_and_kinetic_terms_as_arrays( FermionOperator.zero())) self.assertListEqual(list(potential_terms), []) self.assertListEqual(list(kinetic_terms), [])
def test_commutator(self): operator_a = ( FermionOperator('0^ 0', 0.3) + FermionOperator('1^ 1', 0.1j) + FermionOperator('1^ 0^ 1 0', -0.2) + FermionOperator('1^ 3') + FermionOperator('3^ 0') + FermionOperator('3^ 2', 0.017) - FermionOperator('2^ 3', 1.99) + FermionOperator('3^ 1^ 3 1', .09) + FermionOperator('2^ 0^ 2 0', .126j) + FermionOperator('4^ 2^ 4 2') + FermionOperator('3^ 0^ 3 0')) operator_b = ( FermionOperator('3^ 1', 0.7) + FermionOperator('1^ 3', -9.) + FermionOperator('1^ 0^ 3 0', 0.1) - FermionOperator('3^ 0^ 1 0', 0.11) + FermionOperator('3^ 2^ 3 2') + FermionOperator('3^ 1^ 3 1', -1.37) + FermionOperator('4^ 2^ 4 2') + FermionOperator('4^ 1^ 4 1') + FermionOperator('1^ 0^ 4 0', 16.7) + FermionOperator('1^ 0^ 4 3', 1.67) + FermionOperator('4^ 3^ 5 2', 1.789j) + FermionOperator('6^ 5^ 4 1', -11.789j)) reference = normal_ordered(commutator(operator_a, operator_b)) result = commutator_ordered_diagonal_coulomb_with_two_body_operator( operator_a, operator_b) diff = result - reference self.assertTrue(diff.isclose(FermionOperator.zero()))
def test_simple_hamiltonian(self): hamiltonian = (FermionOperator('3^ 1^ 3 1') + FermionOperator('1^ 1') - FermionOperator('1^ 2') - FermionOperator('2^ 1')) potential_terms, kinetic_terms = ( diagonal_coulomb_potential_and_kinetic_terms_as_arrays(hamiltonian) ) potential = sum(potential_terms, FermionOperator.zero()) kinetic = sum(kinetic_terms, FermionOperator.zero()) self.assertEqual( potential, (FermionOperator('1^ 1') + FermionOperator('3^ 1^ 3 1'))) self.assertEqual(kinetic, (-FermionOperator('1^ 2') - FermionOperator('2^ 1')))
def test_commutes_number_operators(self): com = commutator(FermionOperator('4^ 3^ 4 3'), FermionOperator('2^ 2')) com = normal_ordered(com) self.assertEqual(com, FermionOperator.zero()) com = commutator(BosonOperator('4^ 3^ 4 3'), BosonOperator('2^ 2')) com = normal_ordered(com) self.assertTrue(com == BosonOperator.zero())
def test_commutator(self): operator_a = FermionOperator('') self.assertEqual(FermionOperator.zero(), commutator(operator_a, self.fermion_operator)) operator_b = QubitOperator('X1 Y2') self.assertEqual(commutator(self.qubit_operator, operator_b), (self.qubit_operator * operator_b - operator_b * self.qubit_operator))
def test_add_to_existing_result(self): prior_terms = FermionOperator('0^ 1') operator_a = FermionOperator('2^ 1') operator_b = FermionOperator('0^ 2') commutator_ordered_diagonal_coulomb_with_two_body_operator( operator_a, operator_b, prior_terms=prior_terms) self.assertTrue(prior_terms.isclose(FermionOperator.zero()))
def test_commutes_identity(self): com = commutator(FermionOperator.identity(), FermionOperator('2^ 3', 2.3)) self.assertEqual(com, FermionOperator.zero()) com = commutator(BosonOperator.identity(), BosonOperator('2^ 3', 2.3)) self.assertTrue(com == BosonOperator.zero()) com = commutator(QuadOperator.identity(), QuadOperator('q2 p3', 2.3)) self.assertTrue(com == QuadOperator.zero())
def test_sum_of_ordered_terms_equals_full_hamiltonian_odd_side_len(self): hamiltonian = normal_ordered( fermi_hubbard(5, 5, 1.0, -0.3, periodic=False)) hamiltonian.compress() terms = simulation_ordered_grouped_hubbard_terms_with_info( hamiltonian)[0] terms_total = sum(terms, FermionOperator.zero()) self.assertTrue(terms_total == hamiltonian)
def diagonal_coulomb_potential_and_kinetic_terms_as_arrays(hamiltonian): """Give the potential and kinetic terms of a diagonal Coulomb Hamiltonian as arrays. Args: hamiltonian (FermionOperator): The diagonal Coulomb Hamiltonian to separate the potential and kinetic terms for. Identity is arbitrarily chosen to be part of the potential. Returns: Tuple of (potential_terms, kinetic_terms). Both elements of the tuple are numpy arrays of FermionOperators. """ if not isinstance(hamiltonian, FermionOperator): try: hamiltonian = normal_ordered(get_fermion_operator(hamiltonian)) except TypeError: raise TypeError('hamiltonian must be either a FermionOperator ' 'or DiagonalCoulombHamiltonian.') potential = FermionOperator.zero() kinetic = FermionOperator.zero() for term, coeff in hamiltonian.terms.items(): acted = set(term[i][0] for i in range(len(term))) if len(acted) == len(term) / 2: potential += FermionOperator(term, coeff) else: kinetic += FermionOperator(term, coeff) potential_terms = numpy.array([ FermionOperator(term, coeff) for term, coeff in potential.terms.items() ]) kinetic_terms = numpy.array([ FermionOperator(term, coeff) for term, coeff in kinetic.terms.items() ]) return (potential_terms, kinetic_terms)
def test_commutes_no_intersection(self): com = commutator(FermionOperator('2^ 3'), FermionOperator('4^ 5^ 3')) com = normal_ordered(com) self.assertEqual(com, FermionOperator.zero()) com = commutator(BosonOperator('2^ 3'), BosonOperator('4^ 5^ 3')) com = normal_ordered(com) self.assertTrue(com == BosonOperator.zero()) com = commutator(QuadOperator('q2 p3'), QuadOperator('q4 q5 p3')) com = normal_ordered(com) self.assertTrue(com == QuadOperator.zero())
def test_split_operator_error_operator_VT_order_against_definition(self): hamiltonian = (normal_ordered(fermi_hubbard(3, 3, 1., 4.0)) - 2.3 * FermionOperator.identity()) potential_terms, kinetic_terms = ( diagonal_coulomb_potential_and_kinetic_terms_as_arrays(hamiltonian) ) potential = sum(potential_terms, FermionOperator.zero()) kinetic = sum(kinetic_terms, FermionOperator.zero()) error_operator = ( split_operator_trotter_error_operator_diagonal_two_body( hamiltonian, order='V+T')) # V-then-T ordered double commutators: [V, [T, V]] + [T, [T, V]] / 2 inner_commutator = normal_ordered(commutator(kinetic, potential)) error_operator_definition = normal_ordered( commutator(potential, inner_commutator)) error_operator_definition += normal_ordered( commutator(kinetic, inner_commutator)) / 2.0 error_operator_definition /= 12.0 self.assertEqual(error_operator, error_operator_definition)
def test_diagonal_coulomb_hamiltonian_class(self): hamiltonian = DiagonalCoulombHamiltonian(numpy.array([[1, 1], [1, 1]], dtype=float), numpy.array([[0, 1], [1, 0]], dtype=float), constant=2.3) potential_terms, kinetic_terms = ( diagonal_coulomb_potential_and_kinetic_terms_as_arrays(hamiltonian) ) potential = sum(potential_terms, FermionOperator.zero()) kinetic = sum(kinetic_terms, FermionOperator.zero()) expected_potential = (2.3 * FermionOperator.identity() + FermionOperator('0^ 0') + FermionOperator('1^ 1') - FermionOperator('1^ 0^ 1 0', 2.0)) expected_kinetic = FermionOperator('0^ 1') + FermionOperator('1^ 0') self.assertEqual(potential, expected_potential) self.assertEqual(kinetic, expected_kinetic)
def test_sum_of_ordered_terms_equals_full_side_length_2_hopping_only(self): hamiltonian = normal_ordered( fermi_hubbard(2, 2, 1., 0.0, periodic=False)) hamiltonian.compress() # Unpack result into terms, indices they act on, and whether they're # hopping operators. result = simulation_ordered_grouped_hubbard_terms_with_info( hamiltonian) terms, _, _ = result terms_total = sum(terms, FermionOperator.zero()) self.assertTrue(terms_total == hamiltonian)
def test_canonical_anticommutation_relations(self): op_1 = FermionOperator('3') op_1_dag = FermionOperator('3^') op_2 = FermionOperator('4') op_2_dag = FermionOperator('4^') zero = FermionOperator.zero() one = FermionOperator.identity() self.assertEqual(one, normal_ordered(anticommutator(op_1, op_1_dag))) self.assertEqual(zero, normal_ordered(anticommutator(op_1, op_2))) self.assertEqual(zero, normal_ordered(anticommutator(op_1, op_2_dag))) self.assertEqual(zero, normal_ordered(anticommutator(op_1_dag, op_2))) self.assertEqual(zero, normal_ordered(anticommutator(op_1_dag, op_2_dag))) self.assertEqual(one, normal_ordered(anticommutator(op_2, op_2_dag)))
def test_warning_on_bad_input_first_arg(self): with warnings.catch_warnings(record=True) as w: operator_a = FermionOperator('4^ 3^ 2 1') operator_b = FermionOperator('3^ 2^ 3 2') reference = normal_ordered(commutator(operator_a, operator_b)) result = ( commutator_ordered_diagonal_coulomb_with_two_body_operator( operator_a, operator_b)) self.assertTrue(len(w) == 1) self.assertIn('Defaulted to standard commutator evaluation', str(w[-1].message)) # Result should still be correct in this case. diff = result - reference self.assertTrue(diff.isclose(FermionOperator.zero()))
def double_commutator(op1, op2, op3, indices2=None, indices3=None, is_hopping_operator2=None, is_hopping_operator3=None): """Return the double commutator [op1, [op2, op3]]. Args: op1, op2, op3 (FermionOperators or BosonOperators): operators for the commutator. All three operators must be of the same type. indices2, indices3 (set): The indices op2 and op3 act on. is_hopping_operator2 (bool): Whether op2 is a hopping operator. is_hopping_operator3 (bool): Whether op3 is a hopping operator. Returns: The double commutator of the given operators. """ if is_hopping_operator2 and is_hopping_operator3: indices2 = set(indices2) indices3 = set(indices3) # Determine which indices both op2 and op3 act on. try: intersection, = indices2.intersection(indices3) except ValueError: return FermionOperator.zero() # Remove the intersection from the set of indices, since it will get # cancelled out in the final result. indices2.remove(intersection) indices3.remove(intersection) # Find the indices of the final output hopping operator. index2, = indices2 index3, = indices3 coeff2 = op2.terms[list(op2.terms)[0]] coeff3 = op3.terms[list(op3.terms)[0]] commutator23 = (FermionOperator( ((index2, 1), (index3, 0)), coeff2 * coeff3) + FermionOperator( ((index3, 1), (index2, 0)), -coeff2 * coeff3)) else: commutator23 = normal_ordered(commutator(op2, op3)) return normal_ordered(commutator(op1, commutator23))
def test_hubbard_trotter_error_matches_low_depth_trotter_error(self): hamiltonian = normal_ordered(fermi_hubbard(3, 3, 1., 2.3)) error_operator = ( fermionic_swap_trotter_error_operator_diagonal_two_body( hamiltonian)) error_operator.compress() # Unpack result into terms, indices they act on, and whether # they're hopping operators. result = simulation_ordered_grouped_low_depth_terms_with_info( hamiltonian) terms, indices, is_hopping = result old_error_operator = low_depth_second_order_trotter_error_operator( terms, indices, is_hopping, jellium_only=True) old_error_operator -= error_operator self.assertEqual(old_error_operator, FermionOperator.zero())
def test_1D_jellium_trotter_error_matches_low_depth_trotter_error(self): hamiltonian = normal_ordered( jellium_model( hypercube_grid_with_given_wigner_seitz_radius_and_filling( 1, 5, wigner_seitz_radius=10., spinless=True), spinless=True, plane_wave=False)) error_operator = ( fermionic_swap_trotter_error_operator_diagonal_two_body( hamiltonian)) error_operator.compress() # Unpack result into terms, indices they act on, and whether # they're hopping operators. result = simulation_ordered_grouped_low_depth_terms_with_info( hamiltonian) terms, indices, is_hopping = result old_error_operator = low_depth_second_order_trotter_error_operator( terms, indices, is_hopping, jellium_only=True) old_error_operator -= error_operator self.assertEqual(old_error_operator, FermionOperator.zero())
def test_identity_masks_no_modes(self): mask = bit_mask_of_modes_acted_on_by_fermionic_terms( [FermionOperator.zero()], n_qubits=3) self.assertTrue(numpy.array_equal(mask, numpy.zeros((3, 1))))
def commutator_ordered_diagonal_coulomb_with_two_body_operator( operator_a, operator_b, prior_terms=None): """Compute the commutator of two-body operators provided that both are normal-ordered and that the first only has diagonal Coulomb interactions. Args: operator_a: The first FermionOperator argument of the commutator. All terms must be normal-ordered, and furthermore either hopping operators (i^ j) or diagonal Coulomb operators (i^ i or i^ j^ i j). operator_b: The second FermionOperator argument of the commutator. operator_b can be any arbitrary two-body operator. prior_terms (optional): The initial FermionOperator to add to. Returns: The commutator, or the commutator added to prior_terms if provided. Notes: The function could be readily extended to the case of arbitrary two-body operator_a given that operator_b has the desired form; however, the extra check slows it down without desirable added utility. """ if prior_terms is None: prior_terms = FermionOperator.zero() for term_a in operator_a.terms: coeff_a = operator_a.terms[term_a] for term_b in operator_b.terms: coeff_b = operator_b.terms[term_b] coefficient = coeff_a * coeff_b # If term_a == term_b the terms commute, nothing to add. if term_a == term_b or not term_a or not term_b: continue # Case 1: both operators are two-body, operator_a is i^ j^ i j. if (len(term_a) == len(term_b) == 4 and term_a[0][0] == term_a[2][0] and term_a[1][0] == term_a[3][0]): _commutator_two_body_diagonal_with_two_body( term_a, term_b, coefficient, prior_terms) # Case 2: commutator of a 1-body and a 2-body operator elif (len(term_b) == 4 and len(term_a) == 2) or (len(term_a) == 4 and len(term_b) == 2): _commutator_one_body_with_two_body(term_a, term_b, coefficient, prior_terms) # Case 3: both terms are one-body operators (both length 2) elif len(term_a) == 2 and len(term_b) == 2: _commutator_one_body_with_one_body(term_a, term_b, coefficient, prior_terms) # Final case (case 4): violation of the input promise. Still # compute the commutator, but warn the user. else: warnings.warn('Defaulted to standard commutator evaluation ' 'due to an out-of-spec operator.') additional = FermionOperator.zero() additional.terms[term_a + term_b] = coefficient additional.terms[term_b + term_a] = -coefficient additional = term_reordering.normal_ordered(additional) prior_terms += additional return prior_terms
def low_depth_second_order_trotter_error_operator(terms, indices=None, is_hopping_operator=None, jellium_only=False, verbose=False): """Determine the difference between the exact generator of unitary evolution and the approximate generator given by the second-order Trotter-Suzuki expansion. Args: terms: a list of FermionOperators in the Hamiltonian in the order in which they will be simulated. indices: a set of indices the terms act on in the same order as terms. is_hopping_operator: a list of whether each term is a hopping operator. jellium_only: Whether the terms are from the jellium Hamiltonian only, rather than the full dual basis Hamiltonian (i.e. whether c_i = c for all number operators i^ i, or whether they depend on i as is possible in the general case). verbose: Whether to print percentage progress. Returns: The difference between the true and effective generators of time evolution for a single Trotter step. Notes: follows Equation 9 of Poulin et al.'s work in "The Trotter Step Size Required for Accurate Quantum Simulation of Quantum Chemistry", applied to the "stagger"-based Trotter step for detailed in Kivlichan et al., "Quantum Simulation of Electronic Structure with Linear Depth and Connectivity", arxiv:1711.04789. """ more_info = bool(indices) n_terms = len(terms) if verbose: import time start = time.time() error_operator = FermionOperator.zero() for beta in range(n_terms): if verbose and beta % (n_terms // 30) == 0: print('%4.3f percent done in' % ((float(beta) / n_terms)**3 * 100), time.time() - start) for alpha in range(beta + 1): for alpha_prime in range(beta): # If we have pre-computed info on indices, use it to determine # trivial double commutation. if more_info: if (not trivially_double_commutes_dual_basis_using_term_info( # pylint: disable=C indices[alpha], indices[beta], indices[alpha_prime], is_hopping_operator[alpha], is_hopping_operator[beta], is_hopping_operator[alpha_prime], jellium_only)): # Determine the result of the double commutator. double_com = double_commutator( terms[alpha], terms[beta], terms[alpha_prime], indices[beta], indices[alpha_prime], is_hopping_operator[beta], is_hopping_operator[alpha_prime]) if alpha == beta: double_com /= 2.0 error_operator += double_com # If we don't have more info, check for trivial double # commutation using the terms directly. elif not trivially_double_commutes_dual_basis( terms[alpha], terms[beta], terms[alpha_prime]): double_com = double_commutator(terms[alpha], terms[beta], terms[alpha_prime]) if alpha == beta: double_com /= 2.0 error_operator += double_com error_operator /= 12.0 return error_operator
def test_commutator_hopping_with_double_number_two_intersections(self): com = commutator(FermionOperator('2^ 3'), FermionOperator('3^ 2^ 3 2')) com = normal_ordered(com) self.assertEqual(com, FermionOperator.zero())
def stagger_with_info(hamiltonian, input_ordering, parity, external_potential_at_end=False): """Give terms simulated in a single stagger of a Trotter step. Groups terms into hopping (i^ j + j^ i) and number (i^j^ i j + c_i i^ i + c_j j^ j) operators. Pre-computes term information (indices each operator acts on, as well as whether each operator is a hopping operator). Args: hamiltonian (FermionOperator): The Hamiltonian. input_ordering (list): The initial Jordan-Wigner canonical order. parity (boolean): Whether to determine the terms from the next even (False = 0) or odd (True = 1) stagger. external_potential_at_end (bool): Whether to include the rotations from the external potential at the end of the Trotter step, or intersperse them throughout it. Returns: A 3-tuple of terms from the Hamiltonian that are simulated in the stagger, the indices they act on, and whether they are hopping operators (all in the same order). Notes: The "staggers" used here are the left (parity=False) and right (parity=True) staggers detailed in Kivlichan et al., "Quantum Simulation of Electronic Structure with Linear Depth and Connectivity", arxiv:1711.04789. As such, the Hamiltonian must be in the form discussed in that paper. This constrains it to have only hopping terms (i^ j + j^ i) and potential terms which are products of at most two number operators (n_i or n_i n_j). """ terms_in_layer = [] indices_in_layer = [] is_hopping_operator_in_layer = [] n_qubits = count_qubits(hamiltonian) # A single round of odd-even transposition sort. for i in range(parity, n_qubits - 1, 2): # Always keep the max on the left to avoid having to normal order. left = max(input_ordering[i], input_ordering[i + 1]) right = min(input_ordering[i], input_ordering[i + 1]) # Calculate the hopping operators in the Hamiltonian. left_hopping_operator = FermionOperator( ((left, 1), (right, 0)), hamiltonian.terms.get(((left, 1), (right, 0)), 0.0)) right_hopping_operator = FermionOperator( ((right, 1), (left, 0)), hamiltonian.terms.get(((right, 1), (left, 0)), 0.0)) # Calculate the two-number operator l^ r^ l r in the Hamiltonian. two_number_operator = FermionOperator( ((left, 1), (right, 1), (left, 0), (right, 0)), hamiltonian.terms.get( ((left, 1), (right, 1), (left, 0), (right, 0)), 0.0)) if not external_potential_at_end: # Calculate the left number operator, left^ left. left_number_operator = FermionOperator( ((left, 1), (left, 0)), hamiltonian.terms.get(((left, 1), (left, 0)), 0.0)) # Calculate the right number operator, right^ right. right_number_operator = FermionOperator( ((right, 1), (right, 0)), hamiltonian.terms.get(((right, 1), (right, 0)), 0.0)) # Divide single-number terms by n_qubits-1 to avoid over-accounting # for the interspersed rotations. Each qubit is swapped n_qubits-1 # times total. left_number_operator /= (n_qubits - 1) right_number_operator /= (n_qubits - 1) else: left_number_operator = FermionOperator.zero() right_number_operator = FermionOperator.zero() # If the overall hopping operator isn't close to zero, append it. # Include the indices it acts on and that it's a hopping operator. if not (left_hopping_operator + right_hopping_operator) == FermionOperator.zero(): terms_in_layer.append(left_hopping_operator + right_hopping_operator) indices_in_layer.append(set((left, right))) is_hopping_operator_in_layer.append(True) # If the overall number operator isn't close to zero, append it. # Include the indices it acts on and that it's a number operator. if not (two_number_operator + left_number_operator + right_number_operator) == FermionOperator.zero(): terms_in_layer.append(two_number_operator + left_number_operator + right_number_operator) terms_in_layer[-1].compress() indices_in_layer.append(set((left, right))) is_hopping_operator_in_layer.append(False) # Modify the current Jordan-Wigner canonical ordering in-place. input_ordering[i], input_ordering[i + 1] = (input_ordering[i + 1], input_ordering[i]) return terms_in_layer, indices_in_layer, is_hopping_operator_in_layer
def fermionic_swap_trotter_error_operator_diagonal_two_body( hamiltonian, external_potential_at_end=False): """Compute the fermionic swap network Trotter error of a diagonal two-body Hamiltonian. Args: hamiltonian (FermionOperator): The diagonal Coulomb Hamiltonian to compute the Trotter error for. Returns: error_operator: The second-order Trotter error operator. Notes: Follows Eq 9 of Poulin et al., arXiv:1406.4920, applied to the Trotter step detailed in Kivlichan et al., arxiv:1711.04789. """ single_terms = numpy.array( simulation_ordered_grouped_low_depth_terms_with_info( hamiltonian, external_potential_at_end=external_potential_at_end)[0]) # Cache the halved terms for use in the second commutator. halved_single_terms = single_terms / 2.0 term_mode_mask = bit_mask_of_modes_acted_on_by_fermionic_terms( single_terms, count_qubits(hamiltonian)) error_operator = FermionOperator.zero() for beta, term_beta in enumerate(single_terms): modes_acted_on_by_term_beta = set() for beta_action in term_beta.terms: modes_acted_on_by_term_beta.update( set(operator[0] for operator in beta_action)) beta_mode_mask = numpy.logical_or.reduce( [term_mode_mask[mode] for mode in modes_acted_on_by_term_beta]) # alpha_prime indices that could have a nonzero commutator, i.e. # there's overlap between the modes the corresponding terms act on. valid_alpha_primes = numpy.where(beta_mode_mask)[0] # Only alpha_prime < beta enters the error operator; filter for this. valid_alpha_primes = valid_alpha_primes[valid_alpha_primes < beta] for alpha_prime in valid_alpha_primes: term_alpha_prime = single_terms[alpha_prime] inner_commutator_term = ( commutator_ordered_diagonal_coulomb_with_two_body_operator( term_beta, term_alpha_prime)) modes_acted_on_by_inner_commutator = set() for inner_commutator_action in inner_commutator_term.terms: modes_acted_on_by_inner_commutator.update( set(operator[0] for operator in inner_commutator_action)) # If the inner commutator has no action, the commutator is zero. if not modes_acted_on_by_inner_commutator: continue inner_commutator_mask = numpy.logical_or.reduce([ term_mode_mask[mode] for mode in modes_acted_on_by_inner_commutator ]) # alpha indices that could have a nonzero commutator. valid_alphas = numpy.where(inner_commutator_mask)[0] # Filter so alpha <= beta in the double commutator. valid_alphas = valid_alphas[valid_alphas <= beta] for alpha in valid_alphas: # If alpha = beta, only use half the term. if alpha != beta: outer_term_alpha = single_terms[alpha] else: outer_term_alpha = halved_single_terms[alpha] # Add the partial double commutator to the error operator. commutator_ordered_diagonal_coulomb_with_two_body_operator( outer_term_alpha, inner_commutator_term, prior_terms=error_operator) # Divide by 12 to match the error operator definition. error_operator /= 12.0 return error_operator
def split_operator_trotter_error_operator_diagonal_two_body(hamiltonian, order): """Compute the split-operator Trotter error of a diagonal two-body Hamiltonian. Args: hamiltonian (FermionOperator): The diagonal Coulomb Hamiltonian to compute the Trotter error for. order (str): Whether to simulate the split-operator Trotter step with the kinetic energy T first (order='T+V') or with the potential energy V first (order='V+T'). Returns: error_operator: The second-order Trotter error operator. Notes: The second-order split-operator Trotter error is calculated from the double commutator [T, [V, T]] + [V, [V, T]] / 2 when T is simulated before V (i.e. exp(-iTt/2) exp(-iVt) exp(-iTt/2)), and from the double commutator [V, [T, V]] + [T, [T, V]] / 2 when V is simulated before T, following Equation 9 of "The Trotter Step Size Required for Accurate Quantum Simulation of Quantum Chemistry" by Poulin et al. The Trotter error operator is then obtained by dividing by 12. """ n_qubits = count_qubits(hamiltonian) potential_terms, kinetic_terms = ( diagonal_coulomb_potential_and_kinetic_terms_as_arrays(hamiltonian)) # Cache halved potential and kinetic terms for the second commutator. halved_potential_terms = potential_terms / 2.0 halved_kinetic_terms = kinetic_terms / 2.0 # Assign the outer term of the second commutator based on the ordering. outer_potential_terms = (halved_potential_terms if order == 'T+V' else potential_terms) outer_kinetic_terms = (halved_kinetic_terms if order == 'V+T' else kinetic_terms) potential_mask = bit_mask_of_modes_acted_on_by_fermionic_terms( potential_terms, n_qubits) kinetic_mask = bit_mask_of_modes_acted_on_by_fermionic_terms( kinetic_terms, n_qubits) error_operator = FermionOperator.zero() for potential_term in potential_terms: modes_acted_on_by_potential_term = set() for potential_term_action in potential_term.terms: modes_acted_on_by_potential_term.update( set(operator[0] for operator in potential_term_action)) if not modes_acted_on_by_potential_term: continue potential_term_mode_mask = numpy.logical_or.reduce( [kinetic_mask[mode] for mode in modes_acted_on_by_potential_term]) for kinetic_term in kinetic_terms[potential_term_mode_mask]: inner_commutator_term = ( commutator_ordered_diagonal_coulomb_with_two_body_operator( potential_term, kinetic_term)) modes_acted_on_by_inner_commutator = set() for inner_commutator_action in inner_commutator_term.terms: modes_acted_on_by_inner_commutator.update( set(operator[0] for operator in inner_commutator_action)) if not modes_acted_on_by_inner_commutator: continue inner_commutator_mode_mask = numpy.logical_or.reduce([ potential_mask[mode] for mode in modes_acted_on_by_inner_commutator ]) # halved_potential_terms for T+V order, potential_terms for V+T for outer_potential_term in outer_potential_terms[ inner_commutator_mode_mask]: commutator_ordered_diagonal_coulomb_with_two_body_operator( outer_potential_term, inner_commutator_term, prior_terms=error_operator) inner_commutator_mode_mask = numpy.logical_or.reduce([ kinetic_mask[qubit] for qubit in modes_acted_on_by_inner_commutator ]) # kinetic_terms for T+V order, halved_kinetic_terms for V+T for outer_kinetic_term in outer_kinetic_terms[ inner_commutator_mode_mask]: commutator_ordered_diagonal_coulomb_with_two_body_operator( outer_kinetic_term, inner_commutator_term, prior_terms=error_operator) # Divide by 12 to match the error operator definition. # If order='V+T', also flip the sign to account for inner_commutator_term # not flipping between the different orderings. if order == 'T+V': error_operator /= 12.0 else: error_operator /= -12.0 return error_operator
def test_double_commutator_no_intersection_with_union_of_second_two(self): com = double_commutator(FermionOperator('4^ 3^ 6 5'), FermionOperator('2^ 1 0'), FermionOperator('0^')) self.assertEqual(com, FermionOperator.zero())