def test_get_string(): with patch('pyquil.api.QVMConnection') as cxn: cxn.run_and_measure.return_value = [[1] * 10] qaoa = QAOA(cxn, [0]) prog = Program() prog.inst(X(0)) qaoa.get_parameterized_program = lambda: lambda angles: prog samples = 10 bitstring, freq = qaoa.get_string(betas=None, gammas=None, samples=samples) assert len(freq) <= samples assert bitstring[0] == 1 with patch('pyquil.api.QVMConnection') as cxn: cxn.run_and_measure.return_value = [[0, 1] * 10] qaoa = QAOA(qvm=cxn, qubits=[0, 1], embedding={0: 1, 1: 0}) prog = Program() prog.inst(X(0)) qaoa.get_parameterized_program = lambda: lambda angles: prog samples = 10 bitstring, freq = qaoa.get_string(betas=None, gammas=None, samples=samples) assert len(freq) <= samples assert bitstring[0:2] == (1, 0)
def test_get_string(): qvm = qvm_module.SyncConnection() qaoa = QAOA(qvm, n_qubits=1) prog = Program() prog.inst(X(0)) qaoa.get_parameterized_program = lambda: lambda angles: prog samples = 10 bitstring, freq = qaoa.get_string(betas=None, gammas=None, samples=samples) assert len(freq) <= samples assert bitstring[0] == 1
def solve_tsp(weights, connection=None, steps=1, ftol=1.0e-4, xtol=1.0e-4, samples=1000): """ Method for solving travelling salesman problem. :param weights: Weight matrix. weights[i, j] = cost to traverse edge from city i to j. If weights[i, j] = 0, then no edge exists between cities i and j. :param connection: (Optional) connection to the QVM. Default is None. :param steps: (Optional. Default=1) Trotterization order for the QAOA algorithm. :param ftol: (Optional. Default=1.0e-4) ftol parameter for the Nelder-Mead optimizer :param xtol: (Optional. Default=1.0e-4) xtol parameter for the Nelder-Mead optimizer :param samples: (Optional. Default=1000) number of bit string samples to use. """ if connection is None: connection = api.QVMConnection() number_of_cities = weights.shape[0] list_of_qubits = list(range(number_of_cities**2)) number_of_qubits = len(list_of_qubits) cost_operators = create_cost_hamiltonian(weights) driver_operators = create_mixer_operators(number_of_cities) initial_state_program = create_initial_state_program(number_of_cities) minimizer_kwargs = { 'method': 'Nelder-Mead', 'options': { 'ftol': ftol, 'xtol': xtol, 'disp': False } } vqe_option = {'disp': print_fun, 'return_all': True, 'samples': None} qaoa_inst = QAOA(connection, list_of_qubits, steps=steps, cost_ham=cost_operators, ref_ham=driver_operators, driver_ref=initial_state_program, store_basis=True, minimizer=scipy.optimize.minimize, minimizer_kwargs=minimizer_kwargs, vqe_options=vqe_option) betas, gammas = qaoa_inst.get_angles() most_frequent_string, _ = qaoa_inst.get_string(betas, gammas, samples=samples) solution = binary_state_to_points_order(most_frequent_string) return solution
def test_get_string(): with patch('pyquil.api.QuantumComputer') as qc: qc.run.return_value = [[1] * 10] qaoa = QAOA(qc, [0]) prog = Program() prog.inst(X(0)) qaoa.get_parameterized_program = lambda: lambda angles: prog samples = 10 bitstring, freq = qaoa.get_string(betas=None, gammas=None, samples=samples) assert len(freq) <= samples assert bitstring[0] == 1
def solve_tsp(nodes_array, connection=None, steps=3, ftol=1.0e-4, xtol=1.0e-4): """ Method for solving travelling salesman problem. :param nodes_array: An array of points, which represent coordinates of the cities. :param connection: (Optional) connection to the QVM. Default is None. :param steps: (Optional. Default=1) Trotterization order for the QAOA algorithm. :param ftol: (Optional. Default=1.0e-4) ftol parameter for the Nelder-Mead optimizer :param xtol: (Optional. Default=1.0e-4) xtol parameter for the Nelder-Mead optimizer """ if connection is None: connection = api.QVMConnection() list_of_qubits = list(range(len(nodes_array)**2)) number_of_qubits = len(list_of_qubits) cost_operators = create_cost_hamiltonian(nodes_array) driver_operators = create_mixer_operators(len(nodes_array)) initial_state_program = create_initial_state_program(len(nodes_array)) minimizer_kwargs = { 'method': 'Nelder-Mead', 'options': { 'ftol': ftol, 'xtol': xtol, 'disp': False } } vqe_option = {'disp': print_fun, 'return_all': True, 'samples': None} qaoa_inst = QAOA(connection, number_of_qubits, steps=steps, cost_ham=cost_operators, ref_hamiltonian=driver_operators, driver_ref=initial_state_program, store_basis=True, minimizer=scipy.optimize.minimize, minimizer_kwargs=minimizer_kwargs, vqe_options=vqe_option) betas, gammas = qaoa_inst.get_angles() most_frequent_string, _ = qaoa_inst.get_string(betas, gammas, samples=1000) solution = binary_state_to_points_order(most_frequent_string) return solution
class ForestTSPSolver(object): """docstring for TSPSolver""" def __init__(self, nodes_array, steps=3, ftol=1.0e-4, xtol=1.0e-4, all_ones_coefficient=-2): self.nodes_array = nodes_array self.qvm = api.QVMConnection() self.steps = steps self.ftol = ftol self.xtol = xtol self.betas = None self.gammas = None self.qaoa_inst = None self.most_freq_string = None self.number_of_qubits = self.get_number_of_qubits() self.all_ones_coefficient = all_ones_coefficient cost_operators = self.create_cost_operators() driver_operators = self.create_driver_operators() minimizer_kwargs = {'method': 'Nelder-Mead', 'options': {'ftol': self.ftol, 'xtol': self.xtol, 'disp': False}} vqe_option = {'disp': print_fun, 'return_all': True, 'samples': None} self.qaoa_inst = QAOA(self.qvm, list(range(self.number_of_qubits)), steps=self.steps, cost_ham=cost_operators, ref_hamiltonian=driver_operators, store_basis=True, minimizer=scipy.optimize.minimize, minimizer_kwargs=minimizer_kwargs, vqe_options=vqe_option) def solve_tsp(self): self.find_angles() return self.get_solution() def find_angles(self): self.betas, self.gammas = self.qaoa_inst.get_angles() return self.betas, self.gammas def get_results(self): most_freq_string, sampling_results = self.qaoa_inst.get_string(self.betas, self.gammas, samples=10000) self.most_freq_string = most_freq_string return sampling_results.most_common() def get_solution(self): if self.most_freq_string is None: self.most_freq_string, sampling_results = self.qaoa_inst.get_string(self.betas, self.gammas, samples=10000) quantum_order = TSP_utilities.binary_state_to_points_order_full(self.most_freq_string) return quantum_order def create_cost_operators(self): cost_operators = [] cost_operators += self.create_weights_cost_operators() # 0 - 1: 3 # 0 - 2: 4 # 1 - 2: 5 # cost_operators += self.create_single_weight_operator(0, 1, 3) # cost_operators += self.create_single_weight_operator(0, 2, 4) # cost_operators += self.create_single_weight_operator(1, 2, 5) # cost_operators += self.create_single_z_operator(0, 1, 0, 3) # cost_operators += self.create_single_z_operator(0, 1, 0, -3) cost_operators += self.create_penalty_operators_for_bilocation() cost_operators += self.create_penalty_operators_for_repetition() return cost_operators def create_single_weight_operator(self, i, j, weight): # Positive weights makes given connection more probable. cost_operators = [] for t in [0, 1]: cost_operators.append(create_single_z_operator(i, j, t, weight)) # qubit_1 = t * number_of_nodes + i # qubit_2 = (t + 1) * number_of_nodes + j # # cost_operators.append(PauliTerm("I", 0, weight) - PauliTerm("Z", qubit_1, weight) * PauliTerm("Z", qubit_2)) # cost_operators.append(PauliTerm("Z", qubit_1, weight) * PauliTerm("Z", qubit_2) - PauliTerm("I", 0, weight)) # cost_operators.append(PauliTerm("Z", qubit_2, weight) * PauliTerm("Z", qubit_1) - PauliTerm("I", 0, weight)) return cost_operators def create_single_z_operator(self, i, j, t, weight): number_of_nodes = 3 qubit_1 = t * number_of_nodes + i qubit_2 = (t + 1) * number_of_nodes + j cost_operators = [] cost_operators.append(PauliTerm("Z", qubit_1, weight) * PauliTerm("Z", qubit_2) - PauliTerm("I", 0, weight)) cost_operators.append(PauliTerm("Z", qubit_2, weight) * PauliTerm("Z", qubit_1) - PauliTerm("I", 0, weight)) return cost_operators def create_penalty_operators_for_bilocation(self): # Additional cost for visiting more than one node in given time t cost_operators = [] number_of_nodes = len(self.nodes_array) for t in range(number_of_nodes): range_of_qubits = list(range(t * number_of_nodes, (t + 1) * number_of_nodes)) cost_operators += self.create_penalty_operators_for_qubit_range(range_of_qubits) return cost_operators def create_penalty_operators_for_repetition(self): # Additional cost for visiting given node more than one time cost_operators = [] number_of_nodes = len(self.nodes_array) for i in range(number_of_nodes): range_of_qubits = list(range(i, number_of_nodes**2, number_of_nodes)) cost_operators += self.create_penalty_operators_for_qubit_range(range_of_qubits) return cost_operators def create_penalty_operators_for_qubit_range(self, range_of_qubits): cost_operators = [] tsp_matrix = TSP_utilities.get_tsp_matrix(self.nodes_array) weight = -10 * np.max(tsp_matrix) # weight = -0.5 for i in range_of_qubits: if i == range_of_qubits[0]: z_term = PauliTerm("Z", i, weight) all_ones_term = PauliTerm("I", 0, 0.5 * weight) - PauliTerm("Z", i, 0.5 * weight) else: z_term = z_term * PauliTerm("Z", i) all_ones_term = all_ones_term * (PauliTerm("I", 0, 0.5) - PauliTerm("Z", i, 0.5)) z_term = PauliSum([z_term]) cost_operators.append(PauliTerm("I", 0, weight) - z_term + self.all_ones_coefficient * all_ones_term) return cost_operators def create_weights_cost_operators(self): cost_operators = [] number_of_nodes = len(self.nodes_array) tsp_matrix = TSP_utilities.get_tsp_matrix(self.nodes_array) for i in range(number_of_nodes): for j in range(i, number_of_nodes): for t in range(number_of_nodes - 1): weight = -tsp_matrix[i][j] / 2 if tsp_matrix[i][j] != 0: qubit_1 = t * number_of_nodes + i qubit_2 = (t + 1) * number_of_nodes + j cost_operators.append(PauliTerm("I", 0, weight) - PauliTerm("Z", qubit_1, weight) * PauliTerm("Z", qubit_2)) return cost_operators def create_driver_operators(self): driver_operators = [] for i in range(self.number_of_qubits): driver_operators.append(PauliSum([PauliTerm("X", i, -1.0)])) return driver_operators def get_number_of_qubits(self): return len(self.nodes_array)**2
init_betas=initial_betas, init_gammas=initial_gammas, minimizer_kwargs=minimizer_kwargs) # calculating angles using VQE (simulation) betas, gammas = QAOA_inst.get_angles() print(" p =", i) print("Values of betas:", betas) print("Values of gammas:", gammas, '\n') # setting next angles initial_betas = next_angles(betas) initial_gammas = next_angles(gammas) print("Values of betas ansatz:", initial_betas) print("Values of gammas ansatz:", initial_gammas, '\n') # resulting bitstrings and the most common one print("\nAnd the most common measurement is... ") most_common_bitstring, results_counter = QAOA_inst.get_string( betas, gammas, samples=n_samples) print(most_common_bitstring) print(tuple(qubits)) # graphing distribution results.counter_histogram(results_counter, title=" p = " + str(p)) # timing final end = time.time() print("Time ", end - start)
def interp_pyquil(G, p, initial_betas=0.8, initial_gammas=0.35, minimizer_kwargs={ 'method': 'BFGS', 'options': { 'ftol': 1.0e-2, 'xtol': 1.0e-2, 'disp': True } }, n_samples=1024, plot_hist=False): # timing start start = time.time() # setting up connection with the QVM qvm_connection = api.QVMConnection() # labelling qubits according to graph qubits = [int(n) for n in list(G.nodes)] # turning unweighted graphs into weighted graph with weight wij = 1 for (u, v) in G.edges(): if G.edges[u, v] == {}: G.edges[u, v]['weight'] = 1 # constructing cost and mixer hamiltonian elements in list # N.B. This algorithm only works if all the terms in the cost Hamiltonian commute with each other. Hc = hamiltonians.cost(G) Hb = hamiltonians.mixer(G) # saving data from all layers results = {} # Entering Loop if type(initial_betas) == float: p_start = 1 else: p_start = len(initial_betas) for i in range(p_start, p + 1): QAOA_inst = QAOA(qvm_connection, qubits, steps=i, cost_ham=Hc, ref_ham=Hb, init_betas=initial_betas, init_gammas=initial_gammas, minimizer_kwargs=minimizer_kwargs, vqe_options={ 'disp': print, 'return_all': True }) # calculating angles using VQE (simulation) print(" p =", i) betas, gammas = QAOA_inst.get_angles() print("Values of betas:", betas) print("Values of gammas:", gammas, '\n') # resulting bitstrings and the most common one print("\nAnd the most common measurement is... ") most_common_bitstring, results_counter = QAOA_inst.get_string( betas, gammas, samples=n_samples) print(most_common_bitstring) print(tuple(qubits)) # graphing distribution if plot_hist: counter_histogram(results_counter, title=" p = " + str(p)) # timing final end = time.time() print("Time ", end - start) # saving data from this layer results_i = {} results_i['p'] = i results_i['time'] = end - start results_i['graph'] = nx.to_dict_of_dicts(G) results_i['n_nodes'] = len(G.nodes) results_i['n_edges'] = len(G.edges) results_i['most_common_bistring'] = most_common_bitstring results_i['qubit_order'] = tuple(qubits) results_i['counter'] = dict(results_counter) results_i['n_Fp_evals'] = len(QAOA_inst.result['expectation_vals']) results_i['n_samples'] = n_samples results_i['Fp'] = -QAOA_inst.result['fun'] results_i['Fp_sampled'] = sum([ objective_value(G, k, qubits) * v for k, v in results_counter.items() ]) / n_samples results_i['cutvalue_best'] = cutvalue_best(G, results_counter, qubits) results_i['cutvalue_most_sampled'] = cutvalue_most_sampled( G, results_counter, qubits) results_i['minimizer_method'] = minimizer_kwargs['method'] # time symmetry if all(gammas > 0) and all(betas > 0): results_i['gammas'] = gammas.copy() results_i['betas'] = betas.copy() else: results_i['gammas'] = -gammas.copy() results_i['betas'] = -betas.copy() results[i] = results_i # setting next angles initial_betas = next_angles(betas) initial_gammas = next_angles(gammas) print("Values of betas ansatz:", initial_betas) print("Values of gammas ansatz:", initial_gammas, '\n') return results
def ising( h: List[int], J: Dict[Tuple[int, int], int], num_steps: int = 0, verbose: bool = True, rand_seed: int = None, connection: QuantumComputer = None, samples: int = None, initial_beta: List[float] = None, initial_gamma: List[float] = None, minimizer_kwargs: Dict[str, Any] = None, vqe_option: Dict[str, Union[bool, int]] = None ) -> Tuple[List[int], Union[int, float], Program]: """ Ising set up method :param h: External magnetic term of the Ising problem. :param J: Interaction term of the Ising problem. :param num_steps: (Optional.Default=2 * len(h)) Trotterization order for the QAOA algorithm. :param verbose: (Optional.Default=True) Verbosity of the code. :param rand_seed: (Optional. Default=None) random seed when beta and gamma angles are not provided. :param connection: (Optional) connection to the QVM. Default is None. :param samples: (Optional. Default=None) VQE option. Number of samples (circuit preparation and measurement) to use in operator averaging. :param initial_beta: (Optional. Default=None) Initial guess for beta parameters. :param initial_gamma: (Optional. Default=None) Initial guess for gamma parameters. :param minimizer_kwargs: (Optional. Default=None). Minimizer optional arguments. If None set to {'method': 'Nelder-Mead', 'options': {'fatol': 1.0e-2, 'xatol': 1.0e-2, 'disp': False} :param vqe_option: (Optional. Default=None). VQE optional arguments. If None set to vqe_option = {'disp': print_fun, 'return_all': True, 'samples': samples} :return: Most frequent Ising string, Energy of the Ising string, Circuit used to obtain result. """ if num_steps == 0: num_steps = 2 * len(h) n_nodes = len(h) cost_operators = [] driver_operators = [] for i, j in J.keys(): cost_operators.append( PauliSum([PauliTerm("Z", i, J[(i, j)]) * PauliTerm("Z", j)])) for i in range(n_nodes): cost_operators.append(PauliSum([PauliTerm("Z", i, h[i])])) driver_operators.append(PauliSum([PauliTerm("X", i, -1.0)])) if connection is None: qubits = list(sum(J.keys(), ())) connection = get_qc(f"{len(qubits)}q-qvm") if minimizer_kwargs is None: minimizer_kwargs = { 'method': 'Nelder-Mead', 'options': { 'fatol': 1.0e-2, 'xatol': 1.0e-2, 'disp': False } } if vqe_option is None: vqe_option = {'disp': print, 'return_all': True, 'samples': samples} if not verbose: vqe_option['disp'] = None qaoa_inst = QAOA(connection, list(range(n_nodes)), steps=num_steps, init_betas=initial_beta, init_gammas=initial_gamma, cost_ham=cost_operators, ref_ham=driver_operators, minimizer=minimize, minimizer_kwargs=minimizer_kwargs, rand_seed=rand_seed, vqe_options=vqe_option, store_basis=True) betas, gammas = qaoa_inst.get_angles() most_freq_string, sampling_results = qaoa_inst.get_string(betas, gammas) most_freq_string_ising = [ising_trans(it) for it in most_freq_string] energy_ising = energy_value(h, J, most_freq_string_ising) param_prog = qaoa_inst.get_parameterized_program() circuit = param_prog(np.hstack((betas, gammas))) return most_freq_string_ising, energy_ising, circuit
def ising(h, J, num_steps=0, verbose=True, rand_seed=None, connection=None, samples=None, initial_beta=None, initial_gamma=None, minimizer_kwargs=None, vqe_option=None): """ Ising set up method :param h: External magnectic term of the Ising problem. List. :param J: Interaction term of the Ising problem. Dictionary. :param num_steps: (Optional.Default=2 * len(h)) Trotterization order for the QAOA algorithm. :param verbose: (Optional.Default=True) Verbosity of the code. :param rand_seed: (Optional. Default=None) random seed when beta and gamma angles are not provided. :param connection: (Optional) connection to the QVM. Default is None. :param samples: (Optional. Default=None) VQE option. Number of samples (circuit preparation and measurement) to use in operator averaging. :param initial_beta: (Optional. Default=None) Initial guess for beta parameters. :param initial_gamma: (Optional. Default=None) Initial guess for gamma parameters. :param minimizer_kwargs: (Optional. Default=None). Minimizer optional arguments. If None set to {'method': 'Nelder-Mead', 'options': {'ftol': 1.0e-2, 'xtol': 1.0e-2, 'disp': False} :param vqe_option: (Optional. Default=None). VQE optional arguments. If None set to vqe_option = {'disp': print_fun, 'return_all': True, 'samples': samples} :return: Most frequent Ising string, Energy of the Ising string, Circuit used to obtain result. :rtype: List, Integer or float, 'pyquil.quil.Program'. """ if num_steps == 0: num_steps = 2 * len(h) n_nodes = len(h) cost_operators = [] driver_operators = [] for i, j in J.keys(): cost_operators.append( PauliSum([PauliTerm("Z", i, J[(i, j)]) * PauliTerm("Z", j)])) for i in range(n_nodes): cost_operators.append(PauliSum([PauliTerm("Z", i, h[i])])) for i in range(n_nodes): driver_operators.append(PauliSum([PauliTerm("X", i, -1.0)])) if connection is None: connection = CXN if minimizer_kwargs is None: minimizer_kwargs = { 'method': 'Nelder-Mead', 'options': { 'ftol': 1.0e-2, 'xtol': 1.0e-2, 'disp': False } } if vqe_option is None: vqe_option = { 'disp': print_fun, 'return_all': True, 'samples': samples } if not verbose: vqe_option['disp'] = None qaoa_inst = QAOA(connection, range(n_nodes), steps=num_steps, cost_ham=cost_operators, ref_hamiltonian=driver_operators, store_basis=True, rand_seed=rand_seed, init_betas=initial_beta, init_gammas=initial_gamma, minimizer=minimize, minimizer_kwargs=minimizer_kwargs, vqe_options=vqe_option) betas, gammas = qaoa_inst.get_angles() most_freq_string, sampling_results = qaoa_inst.get_string(betas, gammas) most_freq_string_ising = [ising_trans(it) for it in most_freq_string] energy_ising = energy_value(h, J, most_freq_string_ising) param_prog = qaoa_inst.get_parameterized_program() circuit = param_prog(np.hstack((betas, gammas))) return most_freq_string_ising, energy_ising, circuit
def run_experiment(G, p, optimizer, qvm_connection, n_shots=8192, print_result=False): '''Runs (pyQuil) QAOA experiment with the given parameters Inputs Returns a dictionary with the following keys (some are yet to be implemented) - energy - time (in seconds) - iterations - max-cut objective - solution - solution objective ''' n = len(G) # number of nodes / qubits qubits = [int(n) for n in list(G.nodes)] # list of qubits # constructing cost and mixer hamiltonian elements in list Hc = hamiltonians.cost(G) Hb = hamiltonians.mixer(G) # setting up the QAOA circuit initial_beta = 0 initial_gamma = 0 QAOA_inst = QAOA(qvm_connection, qubits, steps=p, cost_ham=Hc, ref_ham=Hb, init_betas=initial_beta, init_gammas=initial_gamma, minimizer_kwargs=optimizer, vqe_options={'samples': n_shots}) # calculating angles using VQE (simulation) angles = QAOA_inst.get_angles() betas, gammas = angles # resulting bitstrings and the most common one most_common_bitstring, results_counter = QAOA_inst.get_string( betas, gammas, samples=n_shots) # Results energy = None time = None iterations = None objective = None solution = most_common_bitstring solution_objective = None distribution = results_counter angles = angles if print_result: print('energy:', energy) print('time:', time, 's') print('max-cut objective:', objective) print('solution:', solution) print('solution objective:', solution_objective) print('anlges:', angles) return { 'energy': energy, 'time': time, 'iterations': iterations, 'max-cut objective': objective, 'solution': solution, 'solution objective': solution_objective, 'distribution': distribution, 'angles': angles }
class ForestTSPSolver(object): """ Class for solving Travelling Salesman Problem (with starting point) using Forest - quantum computing library. It uses QAOA method with operators as described in the following paper: https://arxiv.org/pdf/1709.03489.pdf by Stuart Hadfield et al. """ def __init__(self, distance_matrix, steps=2, ftol=1.0e-3, xtol=1.0e-3, initial_state="all", starting_node=0): self.distance_matrix = distance_matrix self.starting_node = starting_node # Since we fixed the starting city, the effective number of nodes is smaller by 1 self.reduced_number_of_nodes = len(self.distance_matrix) - 1 self.qvm = api.QVMConnection() self.steps = steps self.ftol = ftol self.xtol = xtol self.betas = None self.gammas = None self.qaoa_inst = None self.number_of_qubits = self.get_number_of_qubits() self.solution = None self.distribution = None cost_operators = self.create_phase_separator() driver_operators = self.create_mixer() initial_state_program = self.create_initial_state_program(initial_state) minimizer_kwargs = {'method': 'Nelder-Mead', 'options': {'ftol': self.ftol, 'xtol': self.xtol, 'disp': False}} vqe_option = {'disp': print_fun, 'return_all': True, 'samples': None} self.qaoa_inst = QAOA(self.qvm, self.number_of_qubits, steps=self.steps, cost_ham=cost_operators, ref_hamiltonian=driver_operators, driver_ref=initial_state_program, store_basis=True, minimizer=scipy.optimize.minimize, minimizer_kwargs=minimizer_kwargs, vqe_options=vqe_option) def solve_tsp(self): """ Calculates the optimal angles (betas and gammas) for the QAOA algorithm and returns a list containing the order of nodes. """ self.find_angles() self.calculate_solution() return self.solution, self.distribution def find_angles(self): """ Runs the QAOA algorithm for finding the optimal angles. """ self.betas, self.gammas = self.qaoa_inst.get_angles() return self.betas, self.gammas def calculate_solution(self): """ Samples the QVM for the results of the algorithm and returns a list containing the order of nodes. """ most_frequent_string, sampling_results = self.qaoa_inst.get_string(self.betas, self.gammas, samples=10000) reduced_solution = TSP_utilities.binary_state_to_points_order(most_frequent_string) full_solution = self.get_solution_for_full_array(reduced_solution) self.solution = full_solution all_solutions = sampling_results.keys() distribution = {} for sol in all_solutions: reduced_sol = TSP_utilities.binary_state_to_points_order(sol) full_sol = self.get_solution_for_full_array(reduced_sol) distribution[tuple(full_sol)] = sampling_results[sol] self.distribution = distribution def get_solution_for_full_array(self, reduced_solution): """ Transforms the solution from its reduced version to the full initial version. """ full_solution = reduced_solution for i in range(len(full_solution)): if full_solution[i] >= self.starting_node: full_solution[i] += 1 full_solution.insert(0, self.starting_node) return full_solution def create_phase_separator(self): """ Creates phase-separation operators, which depend on the objective function. """ cost_operators = [] reduced_distance_matrix = np.delete(self.distance_matrix, self.starting_node, axis=0) reduced_distance_matrix = np.delete(reduced_distance_matrix, self.starting_node, axis=1) for t in range(self.reduced_number_of_nodes - 1): for city_1 in range(self.reduced_number_of_nodes): for city_2 in range(self.reduced_number_of_nodes): if city_1 != city_2: distance = reduced_distance_matrix[city_1, city_2] qubit_1 = t * (self.reduced_number_of_nodes) + city_1 qubit_2 = (t + 1) * (self.reduced_number_of_nodes) + city_2 cost_operators.append(PauliTerm("Z", qubit_1, distance) * PauliTerm("Z", qubit_2)) costs_to_starting_node = np.delete(self.distance_matrix[:, self.starting_node], self.starting_node) for city in range(self.reduced_number_of_nodes): distance_from_0 = -costs_to_starting_node[city] qubit = city cost_operators.append(PauliTerm("Z", qubit, distance_from_0)) for city in range(self.reduced_number_of_nodes): distance_from_0 = -costs_to_starting_node[city] qubit = self.number_of_qubits - (self.reduced_number_of_nodes) + city cost_operators.append(PauliTerm("Z", qubit, distance_from_0)) phase_separator = [PauliSum(cost_operators)] return phase_separator def create_mixer(self): """ Creates mixing operators, which depend on the structure of the problem. Indexing comes directly from 4.1.2 from the https://arxiv.org/pdf/1709.03489.pdf article, equations 54 - 58. """ mixer_operators = [] for t in range(self.reduced_number_of_nodes - 1): for city_1 in range(self.reduced_number_of_nodes): for city_2 in range(self.reduced_number_of_nodes): i = t u = city_1 v = city_2 first_part = 1 first_part *= self.s_plus(u, i) first_part *= self.s_plus(v, i+1) first_part *= self.s_minus(u, i+1) first_part *= self.s_minus(v, i) second_part = 1 second_part *= self.s_minus(u, i) second_part *= self.s_minus(v, i+1) second_part *= self.s_plus(u, i+1) second_part *= self.s_plus(v, i) mixer_operators.append(first_part + second_part) return mixer_operators def create_initial_state_program(self, initial_state): """ Creates a pyquil program representing the initial state for the QAOA. As an argument it takes either a list with order of the cities, or a string "all". In the second case the initial state is superposition of all possible states for this problem. """ initial_state_program = pq.Program() if type(initial_state) is list: for i in range(self.reduced_number_of_nodes): initial_state_program.inst(X(i * (self.reduced_number_of_nodes) + initial_state[i])) elif initial_state == "all": vector_of_states = np.zeros(2**self.number_of_qubits) list_of_possible_states = [] initial_order = range(0, self.reduced_number_of_nodes) all_permutations = [list(x) for x in itertools.permutations(initial_order)] for permutation in all_permutations: coding_of_permutation = 0 for i in range(len(permutation)): coding_of_permutation += 2**(i * (self.reduced_number_of_nodes) + permutation[i]) vector_of_states[coding_of_permutation] = 1 initial_state_program = create_arbitrary_state(vector_of_states) return initial_state_program def get_number_of_qubits(self): return (self.reduced_number_of_nodes)**2 def s_plus(self, city, time): qubit = time * (self.reduced_number_of_nodes) + city return PauliTerm("X", qubit) + PauliTerm("Y", qubit, 1j) def s_minus(self, city, time): qubit = time * (self.reduced_number_of_nodes) + city return PauliTerm("X", qubit) - PauliTerm("Y", qubit, 1j)
class ForestTSPSolver(object): """docstring for TSPSolver""" def __init__(self, full_nodes_array, steps=3, ftol=1.0e-4, xtol=1.0e-4, initial_state="all", starting_node=0): reduced_nodes_array, costs_to_starting_node = self.reduce_nodes_array( full_nodes_array, starting_node) self.nodes_array = reduced_nodes_array self.starting_node = starting_node self.full_nodes_array = full_nodes_array self.costs_to_starting_node = costs_to_starting_node self.qvm = api.QVMConnection() self.steps = steps self.ftol = ftol self.xtol = xtol self.betas = None self.gammas = None self.qaoa_inst = None self.most_freq_string = None self.number_of_qubits = self.get_number_of_qubits() self.initial_state = initial_state cost_operators = self.create_phase_separator() driver_operators = self.create_mixer() initial_state_program = self.create_initial_state_program() minimizer_kwargs = { 'method': 'Nelder-Mead', 'options': { 'ftol': self.ftol, 'xtol': self.xtol, 'disp': False } } vqe_option = {'disp': print_fun, 'return_all': True, 'samples': None} self.qaoa_inst = QAOA(self.qvm, list(range(self.number_of_qubits)), steps=self.steps, cost_ham=cost_operators, ref_hamiltonian=driver_operators, driver_ref=initial_state_program, store_basis=True, minimizer=scipy.optimize.minimize, minimizer_kwargs=minimizer_kwargs, vqe_options=vqe_option) def solve_tsp(self): self.find_angles() return self.get_solution() def find_angles(self): self.betas, self.gammas = self.qaoa_inst.get_angles() return self.betas, self.gammas def get_results(self): most_freq_string, sampling_results = self.qaoa_inst.get_string( self.betas, self.gammas, samples=10000) self.most_freq_string = most_freq_string return sampling_results.most_common() def get_solution(self): if self.most_freq_string is None: self.most_freq_string, sampling_results = self.qaoa_inst.get_string( self.betas, self.gammas, samples=10000) reduced_solution = TSP_utilities.binary_state_to_points_order_full( self.most_freq_string) full_solution = self.get_solution_for_full_array(reduced_solution) return full_solution def reduce_nodes_array(self, full_nodes_array, starting_node): reduced_nodes_array = np.delete(full_nodes_array, starting_node, 0) tsp_matrix = TSP_utilities.get_tsp_matrix(full_nodes_array) costs_to_starting_node = np.delete(tsp_matrix[:, starting_node], starting_node) return reduced_nodes_array, costs_to_starting_node def get_solution_for_full_array(self, reduced_solution): full_solution = reduced_solution for i in range(len(full_solution)): if full_solution[i] >= self.starting_node: full_solution[i] += 1 full_solution.insert(0, self.starting_node) return full_solution def create_phase_separator(self): cost_operators = [] for t in range(len(self.nodes_array) - 1): for city_1 in range(len(self.nodes_array)): for city_2 in range(len(self.nodes_array)): if city_1 != city_2: tsp_matrix = TSP_utilities.get_tsp_matrix( self.nodes_array) distance = tsp_matrix[city_1, city_2] qubit_1 = t * len(self.nodes_array) + city_1 qubit_2 = (t + 1) * len(self.nodes_array) + city_2 cost_operators.append( PauliTerm("Z", qubit_1, distance) * PauliTerm("Z", qubit_2)) for city in range(len(self.costs_to_starting_node)): distance_from_0 = -self.costs_to_starting_node[city] qubit = city cost_operators.append(PauliTerm("Z", qubit, distance_from_0)) phase_separator = [PauliSum(cost_operators)] return phase_separator def create_mixer(self): """ Indexing here comes directly from 4.1.2 from paper 1709.03489, equations 54 - 58. """ mixer_operators = [] n = len(self.nodes_array) for t in range(n - 1): for city_1 in range(n): for city_2 in range(n): i = t u = city_1 v = city_2 first_part = 1 first_part *= self.s_plus(u, i) first_part *= self.s_plus(v, i + 1) first_part *= self.s_minus(u, i + 1) first_part *= self.s_minus(v, i) second_part = 1 second_part *= self.s_minus(u, i) second_part *= self.s_minus(v, i + 1) second_part *= self.s_plus(u, i + 1) second_part *= self.s_plus(v, i) mixer_operators.append(first_part + second_part) return mixer_operators def create_initial_state_program(self): """ As an initial state I use state, where in t=i we visit i-th city. """ initial_state = pq.Program() number_of_nodes = len(self.nodes_array) if type(self.initial_state) is list: for i in range(number_of_nodes): initial_state.inst( X(i * number_of_nodes + self.initial_state[i])) elif self.initial_state == "all": vector_of_states = np.zeros(2**self.number_of_qubits) list_of_possible_states = [] initial_order = range(0, number_of_nodes) all_permutations = [ list(x) for x in itertools.permutations(initial_order) ] for permutation in all_permutations: coding_of_permutation = 0 for i in range(len(permutation)): coding_of_permutation += 2**(i * number_of_nodes + permutation[i]) vector_of_states[coding_of_permutation] = 1 initial_state = create_arbitrary_state(vector_of_states) return initial_state def get_number_of_qubits(self): return len(self.nodes_array)**2 def s_plus(self, city, time): qubit = time * len(self.nodes_array) + city return PauliTerm("X", qubit) + PauliTerm("Y", qubit, 1j) def s_minus(self, city, time): qubit = time * len(self.nodes_array) + city return PauliTerm("X", qubit) - PauliTerm("Y", qubit, 1j)
class ForestTSPSolver(object): def __init__(self, distance_matrix, steps=1, ftol=1.0e-2, xtol=1.0e-2, use_constraints=False, add_weight_constraints=True): self.distance_matrix = distance_matrix self.number_of_qubits = self.get_number_of_qubits() self.qvm = get_qc(str(self.number_of_qubits) + "q-qvm") self.steps = steps self.ftol = ftol self.xtol = xtol self.betas = None self.gammas = None self.qaoa_inst = None self.solution = None self.naive_distribution = None self.most_frequent_string = None self.sampling_results = None self.use_constraints = use_constraints self.add_weight_constraints = add_weight_constraints self.sensible_distribution = None cost_operators = self.create_cost_operators() driver_operators = self.create_driver_operators() minimizer_kwargs = { 'method': 'Nelder-Mead', 'options': { 'ftol': self.ftol, 'xtol': self.xtol, 'disp': False } } # vqe_option = {'disp': print_fun, 'return_all': True, # 'samples': None} qubits = list(range(self.number_of_qubits)) self.qaoa_inst = QAOA( self.qvm, qubits, steps=self.steps, cost_ham=cost_operators, ref_ham=driver_operators, store_basis=True, minimizer=scipy.optimize.minimize, minimizer_kwargs=minimizer_kwargs, # vqe_options=vqe_option ) def solve_tsp(self): """ Calculates the optimal angles (betas and gammas) for the QAOA algorithm and returns a list containing the order of nodes. """ self.find_angles() self.calculate_solution() return self.solution, self.naive_distribution def find_angles(self): """ Runs the QAOA algorithm for finding the optimal angles. """ self.betas, self.gammas = self.qaoa_inst.get_angles() print("betas: ", self.betas) print("gammas: ", self.gammas) return self.betas, self.gammas def calculate_solution(self): """ Samples the QVM for the results of the algorithm and returns a list containing the order of nodes. """ most_frequent_string, sampling_results = self.qaoa_inst.get_string( self.betas, self.gammas, samples=10000) self.most_frequent_string = most_frequent_string self.sampling_results = sampling_results self.solution = binary_state_to_points_order(most_frequent_string) print() # uncomment to show raw sampling results print("Raw sampling results: ") print(sampling_results) all_solutions = sampling_results.keys() naive_distribution = {} for sol in all_solutions: points_order_solution = error_binary_state_to_points_order(sol) if tuple(points_order_solution) in naive_distribution.keys( ): # only true during error conditions of qubits naive_distribution[tuple( points_order_solution)] += sampling_results[sol] else: naive_distribution[tuple( points_order_solution)] = sampling_results[sol] # TODO: make use of sensible_distribution as well as naive self.naive_distribution = naive_distribution def create_cost_operators(self): cost_operators = [] if self.add_weight_constraints: cost_operators += self.create_weights_cost_operators() if self.use_constraints: cost_operators += self.create_penalty_operators_for_bilocation() cost_operators += self.create_penalty_operators_for_repetition() return cost_operators def create_penalty_operators_for_bilocation(self): # Additional cost for visiting more than one node in given time t cost_operators = [] number_of_nodes = len(self.distance_matrix) for t in range(number_of_nodes): range_of_qubits = list( range(t * number_of_nodes, (t + 1) * number_of_nodes)) cost_operators += self.create_penalty_operators_for_qubit_range( range_of_qubits) # print() # print("Cost operators for bilocation: ") # print(cost_operators) # uncomment to see cost operator return cost_operators def create_penalty_operators_for_repetition(self): # Additional cost for visiting given node more than one time cost_operators = [] number_of_nodes = len(self.distance_matrix) for i in range(number_of_nodes): range_of_qubits = list( range(i, number_of_nodes**2, number_of_nodes)) cost_operators += self.create_penalty_operators_for_qubit_range( range_of_qubits) # print() # uncomment to see cost operator # print("Cost operators for repetition: ") # print(cost_operators) return cost_operators def create_penalty_operators_for_qubit_range(self, range_of_qubits): cost_operators = [] weight = -100 * np.max(self.distance_matrix) for i in range_of_qubits: if i == range_of_qubits[0]: z_term = PauliTerm("Z", i, weight) all_ones_term = PauliTerm("I", 0, 0.5 * weight) - PauliTerm( "Z", i, 0.5 * weight) else: z_term = z_term * PauliTerm("Z", i) all_ones_term = all_ones_term * (PauliTerm("I", 0, 0.5) - PauliTerm("Z", i, 0.5)) z_term = PauliSum([z_term]) cost_operators.append( PauliTerm("I", 0, weight) - z_term - all_ones_term) return cost_operators def create_weights_cost_operators(self): cost_operators = [] number_of_nodes = len(self.distance_matrix) for i in range(number_of_nodes): for j in range(i, number_of_nodes): for t in range(number_of_nodes - 1): weight = -self.distance_matrix[i][j] / 2 if self.distance_matrix[i][j] != 0: qubit_1 = t * number_of_nodes + i qubit_2 = (t + 1) * number_of_nodes + j cost_operators.append( PauliTerm("I", 0, weight) - PauliTerm("Z", qubit_1, weight) * PauliTerm("Z", qubit_2)) return cost_operators def create_driver_operators(self): driver_operators = [] for i in range(self.number_of_qubits): driver_operators.append(PauliSum([PauliTerm("X", i, -1.0)])) return driver_operators def get_number_of_qubits(self): return len(self.distance_matrix)**2
class ForestTSPSolverImproved(object): def __init__(self, distance_matrix, steps=1, ftol=1.0e-2, xtol=1.0e-2, use_constraints=False): self.costs_to_first_city = distance_matrix[:0] distance_matrix = np.delete(distance_matrix, 0, 0) self.distance_matrix = np.delete(distance_matrix, 0, 1) self.qvm = api.QVMConnection() self.steps = steps self.ftol = ftol self.xtol = xtol self.betas = None self.gammas = None self.qaoa_inst = None self.number_of_qubits = self.get_number_of_qubits() self.solution = None self.naive_distribution = None self.most_frequent_string = None self.sampling_results = None self.use_constraints = use_constraints cost_operators = self.create_cost_operators() driver_operators = self.create_driver_operators() minimizer_kwargs = { 'method': 'Nelder-Mead', 'options': { 'ftol': self.ftol, 'xtol': self.xtol, 'disp': False } } vqe_option = {'disp': print_fun, 'return_all': True, 'samples': None} qubits = list(range(self.number_of_qubits)) self.qaoa_inst = QAOA(self.qvm, qubits, steps=self.steps, cost_ham=cost_operators, ref_ham=driver_operators, store_basis=True, minimizer=scipy.optimize.minimize, minimizer_kwargs=minimizer_kwargs, vqe_options=vqe_option) def solve_tsp(self): """ Calculates the optimal angles (betas and gammas) for the QAOA algorithm and returns a list containing the order of nodes. """ self.find_angles() self.calculate_solution() return self.solution, self.naive_distribution def find_angles(self): """ Runs the QAOA algorithm for finding the optimal angles. """ self.betas, self.gammas = self.qaoa_inst.get_angles() return self.betas, self.gammas def calculate_solution(self): """ Samples the QVM for the results of the algorithm and returns a list containing the order of nodes. """ most_frequent_string, sampling_results = self.qaoa_inst.get_string( self.betas, self.gammas, samples=10000) self.most_frequent_string = most_frequent_string self.sampling_results = sampling_results self.solution = utilities.binary_state_to_points_order_with_fixed_start( most_frequent_string) all_solutions = sampling_results.keys() naive_distribution = {} for sol in all_solutions: points_order_solution = utilities.binary_state_to_points_order_with_fixed_start( sol) if tuple(points_order_solution) in naive_distribution.keys(): naive_distribution[tuple( points_order_solution)] += sampling_results[sol] else: naive_distribution[tuple( points_order_solution)] = sampling_results[sol] self.naive_distribution = naive_distribution def create_cost_operators(self): cost_operators = [] cost_operators += self.create_weights_cost_operators() if self.use_constraints: cost_operators += self.create_penalty_operators_for_bilocation() cost_operators += self.create_penalty_operators_for_repetition() return cost_operators def create_penalty_operators_for_bilocation(self): # Additional cost for visiting more than one node in given time t cost_operators = [] number_of_nodes = len(self.distance_matrix) for t in range(number_of_nodes): range_of_qubits = list( range(t * number_of_nodes, (t + 1) * number_of_nodes)) cost_operators += self.create_penalty_operators_for_qubit_range( range_of_qubits) return cost_operators def create_penalty_operators_for_repetition(self): # Additional cost for visiting given node more than one time cost_operators = [] number_of_nodes = len(self.distance_matrix) for i in range(number_of_nodes): range_of_qubits = list( range(i, number_of_nodes**2, number_of_nodes)) cost_operators += self.create_penalty_operators_for_qubit_range( range_of_qubits) return cost_operators def create_penalty_operators_for_qubit_range(self, range_of_qubits): cost_operators = [] weight = -100 * np.max(self.distance_matrix) for i in range_of_qubits: if i == range_of_qubits[0]: z_term = PauliTerm("Z", i, weight) all_ones_term = PauliTerm("I", 0, 0.5 * weight) - PauliTerm( "Z", i, 0.5 * weight) else: z_term = z_term * PauliTerm("Z", i) all_ones_term = all_ones_term * (PauliTerm("I", 0, 0.5) - PauliTerm("Z", i, 0.5)) z_term = PauliSum([z_term]) cost_operators.append( PauliTerm("I", 0, weight) - z_term - all_ones_term) return cost_operators def create_weights_cost_operators(self): cost_operators = [] number_of_nodes = len(self.distance_matrix) for i in range(number_of_nodes): for j in range(i, number_of_nodes): for t in range(number_of_nodes - 1): weight = -self.distance_matrix[i][j] / 2 if self.distance_matrix[i][j] != 0: qubit_1 = t * number_of_nodes + i qubit_2 = (t + 1) * number_of_nodes + j cost_operators.append( PauliTerm("I", 0, weight) - PauliTerm("Z", qubit_1, weight) * PauliTerm("Z", qubit_2)) for city in range(len(self.costs_to_first_city)): distance_from_0 = -self.costs_to_first_city[city] qubit = city cost_operators.append(PauliTerm("Z", qubit, distance_from_0)) return cost_operators def create_driver_operators(self): driver_operators = [] for i in range(self.number_of_qubits): driver_operators.append(PauliSum([PauliTerm("X", i, -1.0)])) return driver_operators def get_number_of_qubits(self): return len(self.distance_matrix)**2
class OptimizationEngine(object): """ The optimization engine for the VQF algorithm. This class takes a problem encoded as clauses, further encodes it into hamiltonian and solves it using QAOA. Args: clauses (list): List of clauses (sympy expressions) representing the problem. m (int): Number to be factored. Needed only for the purpose of tagging result files. steps (int, optional): Number of steps in the QAOA algorithm. Default: 1 grid_size (int, optional): The resolution of the grid for grid search. Default: None tol (float, optional): Parameter of BFGS optimization method. Gradient norm must be less than tol before successful termination. Default:1e-5 gate_noise (float, optional): Specifies gate noise for qvm. Default: None. verbose (bool): Boolean flag, if True, information about the execution will be printed to the console. Default: False visualize (bool): Flag indicating if visualizations should be created. Default: False Attributes: clauses (list): See Args. grid_size (int): See Args. mapping (dict): Maps variables into qubit indices. qaoa_inst (object): Instance of QAOA class from Grove. samples (int): If noise model is active, specifies how many samples we should take for any given quantum program. ax (object): Matplotlib `axis` object, used for plotting optimization trajectory. """ def __init__(self, clauses, m=None, steps=1, grid_size=None, tol=1e-5, gate_noise=None, verbose=False, visualize=False): self.clauses = clauses self.m = m self.verbose = verbose self.visualize = visualize self.step_by_step_results = None self.optimization_history = None self.gate_noise = gate_noise if grid_size is None: self.grid_size = len(clauses) + len(qubits) else: self.grid_size = grid_size cost_operators, mapping = self.create_operators_from_clauses() self.mapping = mapping driver_operators = self.create_driver_operators() # minimizer_kwargs = {'method': 'BFGS', # 'options': {'gtol': tol, 'disp': False}} # bounds = [(0, np.pi)]*steps + [(0, 2*np.pi)]*steps # minimizer_kwargs = {'method': 'L-BFGS-B', # 'options': {'gtol': tol, 'disp': False}, # 'bounds': bounds} minimizer_kwargs = { 'method': 'Nelder-Mead', 'options': { 'ftol': tol, 'tol': tol, 'disp': False } } if self.verbose: print_fun = print else: print_fun = pass_fun qubits = list(range(len(mapping))) if gate_noise: self.samples = int(1e3) pauli_channel = [gate_noise] * 3 else: self.samples = None pauli_channel = None connection = ForestConnection() qvm = QVM(connection=connection, gate_noise=pauli_channel) topology = nx.complete_graph(len(qubits)) device = NxDevice(topology=topology) qc = QuantumComputer(name="my_qvm", qam=qvm, device=device, compiler=QVMCompiler( device=device, endpoint=connection.compiler_endpoint)) vqe_option = { 'disp': print_fun, 'return_all': True, 'samples': self.samples } self.qaoa_inst = QAOA(qc, qubits, steps=steps, init_betas=None, init_gammas=None, cost_ham=cost_operators, ref_ham=driver_operators, minimizer=scipy.optimize.minimize, minimizer_kwargs=minimizer_kwargs, rand_seed=None, vqe_options=vqe_option, store_basis=True) self.ax = None def create_operators_from_clauses(self): """ Creates cost hamiltonian from clauses. For details see section IIC from the article. """ operators = [] mapping = {} variable_counter = 0 for clause in self.clauses: if clause == 0: continue variables = list(clause.free_symbols) for variable in variables: if str(variable) not in mapping.keys(): mapping[str(variable)] = variable_counter variable_counter += 1 pauli_terms = [] quadratic_pauli_terms = [] if type(clause) == Add: clause_terms = clause.args elif type(clause) == Mul: clause_terms = [clause] for single_term in clause_terms: if len(single_term.free_symbols) == 0: if self.verbose: print("Constant term", single_term) pauli_terms.append(PauliTerm("I", 0, int(single_term))) elif len(single_term.free_symbols) == 1: if self.verbose: print("Single term", single_term) multiplier = 1 if type(single_term) == Mul: multiplier = int(single_term.args[0]) symbol = list(single_term.free_symbols)[0] symbol_id = mapping[str(symbol)] pauli_terms.append( PauliTerm("I", symbol_id, 1 / 2 * multiplier)) pauli_terms.append( PauliTerm("Z", symbol_id, -1 / 2 * multiplier)) elif len(single_term.free_symbols) == 2 and type( single_term) == Mul: if self.verbose: print("Double term", single_term) multiplier = 1 if isinstance(single_term.args[0], Number): multiplier = int(single_term.args[0]) symbol_1 = list(single_term.free_symbols)[0] symbol_2 = list(single_term.free_symbols)[1] symbol_id_1 = mapping[str(symbol_1)] symbol_id_2 = mapping[str(symbol_2)] pauli_term_1 = PauliTerm( "I", symbol_id_1, 1 / 2 * multiplier) - PauliTerm( "Z", symbol_id_1, 1 / 2 * multiplier) pauli_term_2 = PauliTerm("I", symbol_id_2, 1 / 2) - PauliTerm( "Z", symbol_id_2, 1 / 2) quadratic_pauli_terms.append(pauli_term_1 * pauli_term_2) else: Exception( "Terms of orders higher than quadratic are not handled." ) clause_operator = PauliSum(pauli_terms) for quadratic_term in quadratic_pauli_terms: clause_operator += quadratic_term squared_clause_operator = clause_operator**2 if self.verbose: print("C:", clause_operator) print("C**2:", squared_clause_operator) operators.append(squared_clause_operator) return operators, mapping def create_driver_operators(self): """ Creates driver hamiltonian. """ driver_operators = [] for key, value in self.mapping.items(): driver_operators.append(PauliSum([PauliTerm("X", value, -1.0)])) return driver_operators def perform_qaoa(self): """ Finds optimal angles for QAOA. Returns: sampling_results (Counter): Counter, where each element represents a bitstring that has been obtained. mapping (dict): See class description. """ # betas, gammas = self.simple_grid_search_angles(save_data=True) # betas, gammas = self.step_by_step_grid_search_angles(starting_angles=self.step_by_step_results) betas, gammas = self.find_initial_angles_with_interp( starting_angles=self.step_by_step_results) self.step_by_step_results = [betas, gammas] self.qaoa_inst.betas = betas self.qaoa_inst.gammas = gammas betas, gammas, bfgs_evaluations = self.get_angles() self.qaoa_inst.betas = betas self.qaoa_inst.gammas = gammas try: _, sampling_results = self.qaoa_inst.get_string(betas, gammas, samples=10000) except: pdb.set_trace() return sampling_results, self.mapping, bfgs_evaluations def get_angles(self): """ Finds optimal angles with the quantum variational eigensolver method. It's direct copy of the function `get_angles` from Grove. I decided to copy it here to access to the optimization trajectory (`angles_history`). Returns: best_betas, best_gammas (np.arrays): best values of the betas and gammas found. """ stacked_params = np.hstack( (self.qaoa_inst.betas, self.qaoa_inst.gammas)) vqe = VQE(self.qaoa_inst.minimizer, minimizer_args=self.qaoa_inst.minimizer_args, minimizer_kwargs=self.qaoa_inst.minimizer_kwargs) cost_ham = reduce(lambda x, y: x + y, self.qaoa_inst.cost_ham) # maximizing the cost function! param_prog = self.qaoa_inst.get_parameterized_program() result, bfgs_evaluations = vqe.vqe_run(param_prog, cost_ham, stacked_params, qc=self.qaoa_inst.qc, **self.qaoa_inst.vqe_options) best_betas = result.x[:self.qaoa_inst.steps] best_gammas = result.x[self.qaoa_inst.steps:] optimization_trajectory = result.iteration_params energy_history = result.expectation_vals if self.ax is not None and self.visualize and self.qaoa_inst.steps == 1: plot_optimization_trajectory(self.ax, optimization_trajectory) if len(optimization_trajectory) != 0 and len(energy_history) != 0: self.optimization_history = np.hstack([ np.array(optimization_trajectory), np.array([energy_history]).T ]) else: self.optimization_history = np.array([]) return best_betas, best_gammas, bfgs_evaluations def simple_grid_search_angles(self, save_data=False): """ Finds optimal angles for QAOA by performing grid search on all the angles. This is not recommended for higher values of steps parameter, since it results in grid_size**(2*steps) evaluations. Returns: best_betas, best_gammas (np.arrays): best values of the betas and gammas found. """ best_betas = None best_gammas = None best_energy = np.inf # For some reasons np.meshgrid returns columns in order, where values in second # grow slower than in the first one. This a fix to it. if self.qaoa_inst.steps == 1: column_order = [0] else: column_order = [1, 0] + list(range(2, self.qaoa_inst.steps)) new_indices = np.argsort(column_order) beta_ranges = [np.linspace(0, np.pi, self.grid_size) ] * self.qaoa_inst.steps all_betas = np.vstack(np.meshgrid(*beta_ranges)).reshape( self.qaoa_inst.steps, -1).T all_betas = all_betas[:, column_order] gamma_ranges = [np.linspace(0, 2 * np.pi, self.grid_size) ] * self.qaoa_inst.steps all_gammas = np.vstack(np.meshgrid(*gamma_ranges)).reshape( self.qaoa_inst.steps, -1).T all_gammas = all_gammas[:, column_order] vqe = VQE(self.qaoa_inst.minimizer, minimizer_args=self.qaoa_inst.minimizer_args, minimizer_kwargs=self.qaoa_inst.minimizer_kwargs) cost_hamiltonian = reduce(lambda x, y: x + y, self.qaoa_inst.cost_ham) all_energies = [] data_to_save = [] if save_data: file_name = "_".join( [str(self.m), "grid", str(self.grid_size), str(time.time())]) + ".csv" for betas in all_betas: for gammas in all_gammas: stacked_params = np.hstack((betas, gammas)) program = self.qaoa_inst.get_parameterized_program() energy = vqe.expectation(program(stacked_params), cost_hamiltonian, self.samples, self.qaoa_inst.qc) all_energies.append(energy) if self.verbose: print(betas, gammas, energy, end="\r") if save_data: data_to_save.append(np.hstack([betas, gammas, energy])) if energy < best_energy: best_energy = energy best_betas = betas best_gammas = gammas if self.verbose: print("Lowest energy:", best_energy) print("Angles:", best_betas, best_gammas) if save_data: np.savetxt(file_name, np.array(data_to_save), delimiter=",") if self.visualize: if self.qaoa_inst.steps == 1: self.ax = plot_energy_landscape(all_betas, all_gammas, np.array(all_energies), log_legend=True) else: plot_variance_landscape(all_betas, all_gammas, np.array(all_energies)) return best_betas, best_gammas def step_by_step_grid_search_angles(self, starting_angles=None): """ Finds optimal angles for QAOA by performing "step-by-step" grid search. It finds optimal angles by performing grid search on the QAOA instance with steps=1. Then it fixes these angles and performs grid search on the second pair of angles. This method requires steps*grid_size**2 evaluations and hence is more suitable for higger values of steps. Returns: best_betas, best_gammas (np.arrays): best values of the betas and gammas found. """ max_step = self.qaoa_inst.steps if starting_angles is None: all_steps = range(1, max_step + 1) best_betas = np.array([]) best_gammas = np.array([]) elif len(starting_angles[0]) == max_step - 1: all_steps = [max_step] best_betas = starting_angles[0] best_gammas = starting_angles[1] elif len(starting_angles[0]) == max_step: best_betas = starting_angles[0] best_gammas = starting_angles[1] return best_betas, best_gammas else: all_steps = range(1, max_step + 1) best_betas = np.array([]) best_gammas = np.array([]) self.qaoa_inst.betas = best_betas self.qaoa_inst.gammas = best_gammas for current_step in all_steps: if self.verbose: print("step:", current_step, "\n") beta, gamma = self.one_step_grid_search(current_step) best_betas = np.append(best_betas, beta) best_gammas = np.append(best_gammas, gamma) self.qaoa_inst.betas = best_betas self.qaoa_inst.gammas = best_gammas return best_betas, best_gammas def one_step_grid_search(self, current_step): """ Grid search on n-th pair of QAOA angles, where n=current_step. Args: current_step (int): specify on which layer do we perform search. Returns: best_beta, best_gamma (floats): best values of the beta and gamma found. """ self.qaoa_inst.steps = current_step best_beta = None best_gamma = None best_energy = np.inf fixed_betas = self.qaoa_inst.betas fixed_gammas = self.qaoa_inst.gammas beta_range = np.linspace(0, np.pi, self.grid_size) gamma_range = np.linspace(0, 2 * np.pi, self.grid_size) vqe = VQE(self.qaoa_inst.minimizer, minimizer_args=self.qaoa_inst.minimizer_args, minimizer_kwargs=self.qaoa_inst.minimizer_kwargs) cost_hamiltonian = reduce(lambda x, y: x + y, self.qaoa_inst.cost_ham) for beta in beta_range: for gamma in gamma_range: betas = np.append(fixed_betas, beta) gammas = np.append(fixed_gammas, gamma) stacked_params = np.hstack((betas, gammas)) program = self.qaoa_inst.get_parameterized_program() energy = vqe.expectation(program(stacked_params), cost_hamiltonian, self.samples, self.qaoa_inst.qc) print(beta, gamma, end="\r") if energy < best_energy: best_energy = energy best_beta = beta best_gamma = gamma return best_beta, best_gamma def find_initial_angles_with_interp(self, starting_angles): """ Implements INTERP strategy proposed in https://arxiv.org/pdf/1812.01041.pdf, appendix B.1 """ if self.qaoa_inst.steps == 1: betas, gammas = self.simple_grid_search_angles(save_data=False) return betas, gammas p = self.qaoa_inst.steps - 1 if len(starting_angles[0]) == p: previous_betas = np.hstack([0, starting_angles[0], 0]) previous_gammas = np.hstack([0, starting_angles[1], 0]) new_betas = [] new_gammas = [] for i in range(1, p + 2): beta_i = previous_betas[i] beta_i_minus_1 = previous_betas[i - 1] new_beta_i = (i - 1) / p * beta_i_minus_1 + (p - i + 1) / p * beta_i new_betas.append(new_beta_i) gamma_i = previous_gammas[i] gamma_i_minus_1 = previous_gammas[i - 1] new_gamma_i = (i - 1) / p * gamma_i_minus_1 + (p - i + 1) / p * gamma_i new_gammas.append(new_gamma_i) return np.array(new_betas), np.array(new_gammas) elif len(starting_angles[0]) == p + 1: best_betas = starting_angles[0] best_gammas = starting_angles[1] return best_betas, best_gammas else: Exception( "INTERP strategy won't work without proper starting angles.")
def ising_qaoa(h, J, num_steps=0, embedding=None, driver_operators=None, verbose=True, rand_seed=None, connection=None, samples=None, initial_state=None, initial_beta=None, initial_gamma=None, minimizer_kwargs=None, vqe_option=None): """ Ising set up method for QAOA. Supports 2-local as well as k-local interaction terms. :param h: (dict) External magnectic term of the Ising problem. :param J: (dict) Interaction terms of the Ising problem (may be k-local). :param num_steps: (Optional.Default=2 * len(h)) Trotterization order for the QAOA algorithm. :param embedding: (dict) (Optional. Default: Identity dict) Mapping of logical to physical qubits in the QPU hardware graph. Logical qubits must be the dict keys. :param driver_operators: (Optional. Default: X on all qubits.) The mixer/driver Hamiltonian used in QAOA. Can be used to enforce hard constraints and ensure that solution stays in feasible subspace. Must be PauliSum objects. :param verbose: (Optional.Default=True) Verbosity of the code. :param rand_seed: (Optional. Default=None) random seed when beta and gamma angles are not provided. :param connection: (Optional) connection to the QVM. Default is None. :param samples: (Optional. Default=None) VQE option. Number of samples (circuit preparation and measurement) to use in operator averaging. Required when using QPU backend. :param initial_state: (Optional. Default=Superposition of all bitstrings) A quantum circuit to initialize the initial state. Must be a pyquil Program. :param initial_beta: (Optional. Default=None) Initial guess for beta parameters. :param initial_gamma: (Optional. Default=None) Initial guess for gamma parameters. :param minimizer_kwargs: (Optional. Default=None). Minimizer optional arguments. If None set to {'method': 'Nelder-Mead', 'options': {'ftol': 1.0e-2, 'xtol': 1.0e-2, disp': False} :param vqe_option: (Optional. Default=None). VQE optional arguments. If None set to vqe_option = {'disp': print_fun, 'return_all': True, 'samples': samples} :return: Most frequent Ising string, Energy of the Ising string, Circuit used to obtain result. :rtype: List, Integer or float, 'pyquil.quil.Program'. """ n_nodes = len(set([ index for tuple_ in list(J.keys()) for index in tuple_] + list(h.keys()))) if num_steps == 0: num_steps = 2 * len(n_nodes) if embedding is None: embedding = {i:i for i in range(n_nodes)} cost_operators = [] driver_operators = [] for key in J.keys(): # first PauliTerm is multiplied with coefficient obtained from J pauli_product = PauliTerm("Z", embedding[key[0]], J[key]) for i in range(1,len(key)): # multiply with additional Z PauliTerms depending # on the locality of the interaction terms pauli_product *= PauliTerm("Z", embedding[key[i]]) cost_operators.append(PauliSum([pauli_product])) for i in h.keys(): cost_operators.append(PauliSum([PauliTerm("Z", embedding[i], h[i])])) if driver_operators is None: driver_operators = [] # default to X mixer for i in embedding.values(): driver_operators.append(PauliSum([PauliTerm("X", i, -1.0)])) if connection is None: connection = CXN if minimizer_kwargs is None: minimizer_kwargs = {'method': 'Nelder-Mead', 'options': {'ftol': 1.0e-2, 'xtol': 1.0e-2, 'disp': False}} if vqe_option is None: vqe_option = {'disp': print_fun, 'return_all': True, 'samples': samples} if not verbose: vqe_option['disp'] = None qaoa_inst = QAOA(connection, qubits=list(sorted(embedding.values())), steps=num_steps, cost_ham=cost_operators, ref_ham=driver_operators, store_basis=True, rand_seed=rand_seed, embedding=embedding, init_betas=initial_beta, init_gammas=initial_gamma, minimizer=minimize, minimizer_kwargs=minimizer_kwargs, vqe_options=vqe_option) betas, gammas = qaoa_inst.get_angles() most_freq_string, sampling_results = qaoa_inst.get_string(betas, gammas) most_freq_string_ising = [ising_trans(it) for it in most_freq_string] energy_ising = energy_value(h, J, most_freq_string_ising) param_prog = qaoa_inst.get_parameterized_program() circuit = param_prog(np.hstack((betas, gammas))) return most_freq_string_ising, energy_ising, circuit