def __init__( self, n_c: int, cost_hamiltonian: IsingOperator, ansatz: Ansatz, inner_optimizer: Optimizer, recorder: RecorderFactory = _recorder, ) -> None: """This is an implementation of recursive QAOA (RQAOA) from https://arxiv.org/abs/1910.08980 page 4. The main idea is that we call QAOA recursively and reduce the size of the cost hamiltonian by 1 on each recursion, until we hit a threshold number of qubits `n_c`. Then, we use brute force to solve the reduced QAOA problem, mapping the reduced solution to the original solution. Args: n_c: The threshold number of qubits at which recursion stops, as described in the original paper. Cannot be greater than number of qubits. cost_hamiltonian: Hamiltonian representing the cost function. ansatz: an Ansatz object with all params (ex. `n_layers`) initialized inner_optimizer: optimizer used for optimization of parameters at each recursion of RQAOA. recorder: recorder object which defines how to store the optimization history. """ n_qubits = count_qubits(change_operator_type(cost_hamiltonian, QubitOperator)) if n_c >= n_qubits or n_c <= 0: raise ValueError( "n_c needs to be a value less than number of qubits and greater than 0." ) self._n_c = n_c self._ansatz = ansatz self._cost_hamiltonian = cost_hamiltonian self._inner_optimizer = inner_optimizer self._recorder = recorder
def _generate_circuit(self, params: Optional[np.ndarray] = None) -> Circuit: """Returns a parametrizable circuit represention of the ansatz. Args: params: parameters of the circuit. """ if params is not None: Warning( "This method retuns a parametrizable circuit, params will be ignored." ) circuit = Circuit() # Prepare initial state circuit += create_layer_of_gates(self.number_of_qubits, RY, self._thetas) # Add time evolution layers cost_circuit = time_evolution( change_operator_type(self._cost_hamiltonian, QubitOperator), sympy.Symbol(f"gamma"), ) for i in range(self.number_of_layers): circuit += cost_circuit.bind( {sympy.Symbol(f"gamma"): sympy.Symbol(f"gamma_{i}")}) circuit += create_layer_of_gates(self.number_of_qubits, RY, -self._thetas) circuit += create_layer_of_gates( self.number_of_qubits, RZ, [-2 * sympy.Symbol(f"beta_{i}")] * self.number_of_qubits, ) circuit += create_layer_of_gates(self.number_of_qubits, RY, self._thetas) return circuit
def _generate_circuit(self, params: Optional[np.ndarray] = None) -> Circuit: """Returns a parametrizable circuit represention of the ansatz. By convention the initial state is taken to be the |+..+> state and is evolved first under the cost Hamiltonian and then the mixer Hamiltonian. Args: params: parameters of the circuit. """ if params is not None: Warning( "This method retuns a parametrizable circuit, params will be ignored." ) circuit = Circuit() qubits = [ Qubit(qubit_index) for qubit_index in range(self.number_of_qubits) ] circuit.qubits = qubits # Prepare initial state circuit += create_layer_of_gates(self.number_of_qubits, "H") # Add time evolution layers pyquil_cost_hamiltonian = qubitop_to_pyquilpauli( change_operator_type(self._cost_hamiltonian, QubitOperator)) pyquil_mixer_hamiltonian = qubitop_to_pyquilpauli( self._mixer_hamiltonian) for i in range(self.number_of_layers): circuit += time_evolution(pyquil_cost_hamiltonian, sympy.Symbol(f"gamma_{i}")) circuit += time_evolution(pyquil_mixer_hamiltonian, sympy.Symbol(f"beta_{i}")) return circuit
def get_expectation_values(self, circuit, qubit_operator, **kwargs): """Run a circuit and measure the expectation values with respect to a given operator. Note: the number of bitstrings measured is derived from self.n_samples - if self.n_samples = None, then this will use self.get_exact_expectation_values Args: circuit (zquantum.core.circuit.Circuit): the circuit to prepare the state qubit_operator (openfermion.ops.QubitOperator): the operator to measure Returns: zquantum.core.measurement.ExpectationValues: the expectation values of each term in the operator """ self.num_circuits_run += 1 self.num_jobs_run += 1 if self.n_samples == None: return self.get_exact_expectation_values(circuit, qubit_operator, **kwargs) else: operator = change_operator_type(qubit_operator, IsingOperator) measurements = self.run_circuit_and_measure(circuit) expectation_values = measurements.get_expectation_values(operator) expectation_values = expectation_values_to_real(expectation_values) return expectation_values
def _generate_circuit(self, params: Optional[np.ndarray] = None) -> Circuit: """Returns a parametrizable circuit represention of the ansatz. By convention the initial state is taken to be the |+..+> state and is evolved first under the cost Hamiltonian and then the mixer Hamiltonian. Args: params: parameters of the circuit. """ if params is not None: Warning( "This method retuns a parametrizable circuit, params will be ignored." ) circuit = Circuit() # Prepare initial state circuit += create_layer_of_gates(self.number_of_qubits, H) # Add time evolution layers cost_circuit = time_evolution( change_operator_type(self._cost_hamiltonian, QubitOperator), sympy.Symbol(f"gamma"), ) mixer_circuit = time_evolution(self._mixer_hamiltonian, sympy.Symbol(f"beta")) for i in range(self.number_of_layers): circuit += cost_circuit.bind( {sympy.Symbol(f"gamma"): sympy.Symbol(f"gamma_{i}")}) circuit += mixer_circuit.bind( {sympy.Symbol(f"beta"): sympy.Symbol(f"beta_{i}")}) return circuit
def test_generate_circuit_with_ising_operator(self, ansatz, symbols_map, target_unitary): # When ansatz.cost_hamiltonian = change_operator_type(ansatz.cost_hamiltonian, IsingOperator) parametrized_circuit = ansatz._generate_circuit() evaluated_circuit = parametrized_circuit.bind(symbols_map) final_unitary = evaluated_circuit.to_unitary() # Then assert compare_unitary(final_unitary, target_unitary, tol=1e-10)
def get_summed_expectation_values( operator: str, measurements: str, use_bessel_correction: Optional[bool] = True): if isinstance(operator, str): operator = load_qubit_operator(operator) operator = change_operator_type(operator, openfermion.IsingOperator) if isinstance(measurements, str): measurements = Measurements.load_from_file(measurements) expectation_values = measurements.get_expectation_values( operator, use_bessel_correction=use_bessel_correction) value_estimate = sum_expectation_values(expectation_values) save_value_estimate(value_estimate, "value-estimate.json")
def test_get_number_of_qubits_with_ising_hamiltonian(self, ansatz): # Given new_cost_hamiltonian = (QubitOperator((0, "Z")) + QubitOperator( (1, "Z")) + QubitOperator((2, "Z"))) new_cost_hamiltonian = change_operator_type(new_cost_hamiltonian, IsingOperator) target_number_of_qubits = 3 # When ansatz.cost_hamiltonian = new_cost_hamiltonian # Then assert ansatz.number_of_qubits == target_number_of_qubits
def test_generate_circuit_with_ising_operator(self, ansatz, number_of_layers, thetas): # When ansatz.cost_hamiltonian = change_operator_type(ansatz.cost_hamiltonian, IsingOperator) parametrized_circuit = ansatz._generate_circuit() symbols_map = create_symbols_map(number_of_layers) target_unitary = create_target_unitary(thetas, number_of_layers) evaluated_circuit = parametrized_circuit.evaluate(symbols_map) final_unitary = evaluated_circuit.to_unitary() # Then assert compare_unitary(final_unitary, target_unitary, tol=1e-10)
def _evaluate_solution_for_hamiltonian(solution: Tuple[int], hamiltonian: QubitOperator) -> float: """Evaluates a solution of a hamiltonian by its calculating expectation value. Args: solution: solution to a problem as a tuple of bits hamiltonian: a Hamiltonian representing a problem. Returns: float: value of a solution. """ hamiltonian = change_operator_type(hamiltonian, IsingOperator) expectation_values = expectation_values_to_real( Measurements([solution]).get_expectation_values(hamiltonian)) return sum(expectation_values.values)
def _generate_circuit(self, params: Optional[np.ndarray] = None) -> Circuit: """Returns a parametrizable circuit represention of the ansatz. Args: params: parameters of the circuit. """ if params is not None: Warning( "This method retuns a parametrizable circuit, params will be ignored." ) circuit = Circuit() qubits = [ Qubit(qubit_index) for qubit_index in range(self.number_of_qubits) ] circuit.qubits = qubits # Prepare initial state circuit += create_layer_of_gates(self.number_of_qubits, "Ry", self._thetas) pyquil_cost_hamiltonian = qubitop_to_pyquilpauli( change_operator_type(self._cost_hamiltonian, QubitOperator)) # Add time evolution layers for i in range(self.number_of_layers): circuit += time_evolution(pyquil_cost_hamiltonian, sympy.Symbol(f"gamma_{i}")) circuit += create_layer_of_gates(self.number_of_qubits, "Ry", -self._thetas) circuit += create_layer_of_gates( self.number_of_qubits, "Rz", [-2 * sympy.Symbol(f"beta_{i}")] * self.number_of_qubits, ) circuit += create_layer_of_gates(self.number_of_qubits, "Ry", self._thetas) return circuit
def _minimize( self, cost_function_factory: Callable[[IsingOperator, Ansatz], CostFunction], initial_params: np.ndarray, keep_history: bool = False, ) -> OptimizeResult: """Args: cost_function_factory: function that generates CostFunction objects given the provided ansatz and cost_hamiltonian. initial_params: initial parameters used for optimization keep_history: flag indicating whether history of cost function evaluations should be recorded. Returns: OptimizeResult with the added entry of: opt_solutions (List[Tuple[int, ...]]): The solution(s) to recursive QAOA as a list of tuples; each tuple is a tuple of bits. """ n_qubits = count_qubits( change_operator_type(self._cost_hamiltonian, QubitOperator) ) qubit_map = _create_default_qubit_map(n_qubits) histories: Dict[str, List[HistoryEntry]] = defaultdict(list) histories["history"] = [] return self._recursive_minimize( cost_function_factory, initial_params, keep_history, cost_hamiltonian=self._cost_hamiltonian, qubit_map=qubit_map, nit=0, nfev=0, histories=histories, )
def get_expectation_values_for_circuitset(self, circuitset, operator, **kwargs): """Run a set of circuits and measure the expectation values with respect to a given operator. Args: circuitset (list of zquantum.core.circuit.Circuit objects): the circuits to prepare the states operator (openfermion.ops.IsingOperator or openfermion.ops.QubitOperator): the operator to measure Returns: list of zquantum.core.measurement.ExpectationValues objects: a list of the expectation values of each term in the operator with respect to the various state preparation circuits """ self.num_circuits_run += len(circuitset) self.num_jobs_run += 1 operator = change_operator_type(operator, IsingOperator) measurements_set = self.run_circuitset_and_measure(circuitset) expectation_values_set = [] for measurements in measurements_set: expectation_values = measurements.get_expectation_values(operator) expectation_values = expectation_values_to_real(expectation_values) expectation_values_set.append(expectation_values) return expectation_values_set
def _recursive_minimize( self, cost_function_factory, initial_params, keep_history, cost_hamiltonian, qubit_map, nit, nfev, histories, ): """A method that recursively calls itself with each recursion reducing 1 term of the cost hamiltonian """ # Set up QAOA circuit ansatz = copy(self._ansatz) ansatz.cost_hamiltonian = cost_hamiltonian cost_function = cost_function_factory( cost_hamiltonian, ansatz, ) if keep_history: cost_function = self.recorder(cost_function) # Run & optimize QAOA opt_results = self.inner_optimizer.minimize(cost_function, initial_params) nit += opt_results.nit nfev += opt_results.nfev if keep_history: histories = extend_histories(cost_function, histories) # Reduce the cost hamiltonian ( term_with_largest_expval, largest_expval, ) = _find_term_with_strongest_correlation( cost_hamiltonian, ansatz, opt_results.opt_params, cost_function_factory, ) new_qubit_map = _update_qubit_map( qubit_map, term_with_largest_expval, largest_expval ) reduced_cost_hamiltonian = _create_reduced_hamiltonian( cost_hamiltonian, term_with_largest_expval, largest_expval, ) # Check new cost hamiltonian has correct amount of qubits assert ( count_qubits(change_operator_type(reduced_cost_hamiltonian, QubitOperator)) == count_qubits(change_operator_type(cost_hamiltonian, QubitOperator)) - 1 # If we have 1 qubit, the reduced cost hamiltonian would be empty and say it has # 0 qubits. or count_qubits( change_operator_type(reduced_cost_hamiltonian, QubitOperator) ) == 0 and count_qubits(change_operator_type(cost_hamiltonian, QubitOperator)) == 2 and self._n_c == 1 ) # Check qubit map has correct amount of qubits assert ( count_qubits(change_operator_type(cost_hamiltonian, QubitOperator)) - 1 == max([l[0] for l in new_qubit_map.values()]) + 1 ) if ( count_qubits(change_operator_type(reduced_cost_hamiltonian, QubitOperator)) > self._n_c ): # If we didn't reach threshold `n_c`, we repeat the the above with the reduced # cost hamiltonian. return self._recursive_minimize( cost_function_factory, initial_params, keep_history, cost_hamiltonian=reduced_cost_hamiltonian, qubit_map=new_qubit_map, nit=nit, nfev=nfev, histories=histories, ) else: best_value, reduced_solutions = solve_problem_by_exhaustive_search( change_operator_type(reduced_cost_hamiltonian, QubitOperator) ) solutions = _map_reduced_solutions_to_original_solutions( reduced_solutions, new_qubit_map ) opt_result = optimization_result( opt_solutions=solutions, opt_value=best_value, opt_params=None, nit=nit, nfev=nfev, **histories, ) return opt_result
def number_of_qubits(self): """Returns number of qubits used for the ansatz circuit.""" return count_qubits( change_operator_type(self._cost_hamiltonian, QubitOperator))