pcb.qaoa
Implementation of the QAOA algorithm.
1"""Implementation of the QAOA algorithm.""" 2 3import numpy as np 4from loguru import logger as logging 5from qiskit import QuantumCircuit 6from qiskit.circuit.library import QAOAAnsatz 7from qiskit.primitives import PrimitiveJob 8from qiskit.providers import BackendV2 as Backend 9from qiskit.quantum_info import SparsePauliOp 10from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager 11from qiskit_aer.primitives import EstimatorV2 as AerEstimator 12from qiskit_ibm_runtime import EstimatorV2 as IBMEstimator 13from qiskit_ibm_runtime import RuntimeJobV2 as IBMRuntimeJob 14from scipy.optimize import minimize 15 16 17def _energy( 18 x: np.ndarray, 19 ansatz: QuantumCircuit, 20 operator: SparsePauliOp, 21 estimator: IBMEstimator | AerEstimator, 22 records: list[tuple[np.ndarray, float, dict]] | None = None, 23) -> float: 24 """ 25 Evaluates a set of parameters for the QAOA ansatz. Returns an array of all 26 resulting energies and a dictionary containing data about the job submitted 27 to IBMQ or Aer. See `_job_to_dict` for the structure of the dictionary. 28 29 Args: 30 parameters (np.ndarray): A `(N, n_qaoa_params)` array of real parameters 31 ansatz (QuantumCircuit): 32 operator (SparsePauliOp): 33 estimator (IBMEstimator | AerEstimator): 34 records (list, optional): If provided, results of this evaluation will 35 be appended. 36 """ 37 if x.ndim != 1: 38 raise ValueError( 39 f"Parameters must be a 1D array, got shape: {x.shape}" 40 ) 41 pubs = [(ansatz, [operator], [x])] 42 job = estimator.run(pubs=pubs) 43 results = job.result() 44 e = float(results[0].data.evs[0]) 45 if records is not None: 46 records.append((x, e, _job_to_dict(job))) 47 return e 48 49 50def _job_to_dict(job: IBMRuntimeJob | PrimitiveJob) -> dict: 51 """ 52 Note: 53 The keys will be prefixed with `ibmq_` even if the job is an Aer job. 54 """ 55 return { 56 "ibmq_jid": job.job_id(), 57 "ibmq_sid": getattr(job, "session_id", None), 58 } 59 60 61def qaoa( 62 operator: SparsePauliOp, 63 backend: Backend, 64 estimator: IBMEstimator | AerEstimator, 65 cost_qc: QuantumCircuit | None = None, 66 n_qaoa_steps: int = 2, 67 pm_optimization_level: int = 3, 68 max_iter: int = 128, 69) -> tuple[ 70 tuple[np.ndarray, float], tuple[np.ndarray, np.ndarray], list[dict] 71]: 72 """ 73 Runs QAOA on the given operator using the given backend. The Trotterization 74 of the cost operator can be given explicitely as a `QuantumCircuit`. 75 Otherwise, it is automatically Trotterized. 76 77 This method submits PUBs in batches, meaning that at every iteration, 78 `batch_size` parameters are tested. 79 80 Warning: 81 This method tries to *MAXIMIZE* the energy of the operator. If you want 82 to minimize, flip the sign of the weights in `operator`. 83 84 Args: 85 operator (SparsePauliOp): The operator whose energy to maximize 86 backend (Backend): 87 estimator (IBMEstimator | AerEstimator): 88 cost_qc (QuantumCircuit | None, optional): 89 n_qaoa_steps (int, optional): 90 pm_optimization_level (int, optional): 91 92 Returns: 93 1. A tuple containing the optimal parameters and the optimal energy. 94 2. A tuple containing array of all parameters that were tried and the 95 array of all resulting energies. 96 3. A list of dict decribing the results of each job. An element looks 97 like: 98 99 { 100 "energy": 0.006450488764709524, 101 "ibmq_job_id": "3c94c73c-385b-4e71-8675-e3e1ad76aa4e", 102 "ibmq_session_id": "cz4h80039f40008scarg", 103 "batch": 3, 104 } 105 """ 106 ansatz = QAOAAnsatz( 107 cost_operator=cost_qc if cost_qc is not None else operator, 108 reps=n_qaoa_steps, 109 ) 110 pm = generate_preset_pass_manager( 111 target=backend.target, optimization_level=pm_optimization_level 112 ) 113 ansatz_isa = pm.run(ansatz) 114 operator_isa = operator.apply_layout(ansatz_isa.layout) 115 logging.debug( 116 "ISA ansatz depth/size: {}/{}", 117 ansatz_isa.depth(), 118 ansatz_isa.size(), 119 ) 120 x0 = np.array( 121 ([np.pi / 2] * (len(ansatz_isa.parameters) // 2)) # β's 122 + ([np.pi] * (len(ansatz_isa.parameters) // 2)) # γ's 123 ) 124 bounds = ( 125 ([(0, np.pi)] * (len(ansatz_isa.parameters) // 2)) # β's 126 + ([(0, 2 * np.pi)] * (len(ansatz_isa.parameters) // 2)) # γ's 127 ) 128 records: list[tuple[np.ndarray, float, dict]] = [] 129 minimize( 130 lambda *args: -1 * _energy(*args), # energy maximization 131 x0=x0, 132 args=(ansatz_isa, operator_isa, estimator, records), 133 method="cobyla", 134 bounds=bounds, 135 options={"maxiter": max_iter}, 136 ) 137 all_x = np.stack([x for x, _, _ in records]) 138 all_e = np.array([e for _, e, _ in records]) 139 results = [ 140 {"energy": float(e), "step": i, **m} 141 for i, (_, e, m) in enumerate(records) 142 ] 143 j = all_e.argmax() 144 return (all_x[j], all_e[j]), (all_x, all_e), results
def
qaoa( operator: qiskit.quantum_info.operators.symplectic.sparse_pauli_op.SparsePauliOp, backend: qiskit.providers.backend.BackendV2, estimator: qiskit_ibm_runtime.estimator.EstimatorV2 | qiskit_aer.primitives.estimator_v2.EstimatorV2, cost_qc: qiskit.circuit.quantumcircuit.QuantumCircuit | None = None, n_qaoa_steps: int = 2, pm_optimization_level: int = 3, max_iter: int = 128) -> tuple[tuple[numpy.ndarray, float], tuple[numpy.ndarray, numpy.ndarray], list[dict]]:
62def qaoa( 63 operator: SparsePauliOp, 64 backend: Backend, 65 estimator: IBMEstimator | AerEstimator, 66 cost_qc: QuantumCircuit | None = None, 67 n_qaoa_steps: int = 2, 68 pm_optimization_level: int = 3, 69 max_iter: int = 128, 70) -> tuple[ 71 tuple[np.ndarray, float], tuple[np.ndarray, np.ndarray], list[dict] 72]: 73 """ 74 Runs QAOA on the given operator using the given backend. The Trotterization 75 of the cost operator can be given explicitely as a `QuantumCircuit`. 76 Otherwise, it is automatically Trotterized. 77 78 This method submits PUBs in batches, meaning that at every iteration, 79 `batch_size` parameters are tested. 80 81 Warning: 82 This method tries to *MAXIMIZE* the energy of the operator. If you want 83 to minimize, flip the sign of the weights in `operator`. 84 85 Args: 86 operator (SparsePauliOp): The operator whose energy to maximize 87 backend (Backend): 88 estimator (IBMEstimator | AerEstimator): 89 cost_qc (QuantumCircuit | None, optional): 90 n_qaoa_steps (int, optional): 91 pm_optimization_level (int, optional): 92 93 Returns: 94 1. A tuple containing the optimal parameters and the optimal energy. 95 2. A tuple containing array of all parameters that were tried and the 96 array of all resulting energies. 97 3. A list of dict decribing the results of each job. An element looks 98 like: 99 100 { 101 "energy": 0.006450488764709524, 102 "ibmq_job_id": "3c94c73c-385b-4e71-8675-e3e1ad76aa4e", 103 "ibmq_session_id": "cz4h80039f40008scarg", 104 "batch": 3, 105 } 106 """ 107 ansatz = QAOAAnsatz( 108 cost_operator=cost_qc if cost_qc is not None else operator, 109 reps=n_qaoa_steps, 110 ) 111 pm = generate_preset_pass_manager( 112 target=backend.target, optimization_level=pm_optimization_level 113 ) 114 ansatz_isa = pm.run(ansatz) 115 operator_isa = operator.apply_layout(ansatz_isa.layout) 116 logging.debug( 117 "ISA ansatz depth/size: {}/{}", 118 ansatz_isa.depth(), 119 ansatz_isa.size(), 120 ) 121 x0 = np.array( 122 ([np.pi / 2] * (len(ansatz_isa.parameters) // 2)) # β's 123 + ([np.pi] * (len(ansatz_isa.parameters) // 2)) # γ's 124 ) 125 bounds = ( 126 ([(0, np.pi)] * (len(ansatz_isa.parameters) // 2)) # β's 127 + ([(0, 2 * np.pi)] * (len(ansatz_isa.parameters) // 2)) # γ's 128 ) 129 records: list[tuple[np.ndarray, float, dict]] = [] 130 minimize( 131 lambda *args: -1 * _energy(*args), # energy maximization 132 x0=x0, 133 args=(ansatz_isa, operator_isa, estimator, records), 134 method="cobyla", 135 bounds=bounds, 136 options={"maxiter": max_iter}, 137 ) 138 all_x = np.stack([x for x, _, _ in records]) 139 all_e = np.array([e for _, e, _ in records]) 140 results = [ 141 {"energy": float(e), "step": i, **m} 142 for i, (_, e, m) in enumerate(records) 143 ] 144 j = all_e.argmax() 145 return (all_x[j], all_e[j]), (all_x, all_e), results
Runs QAOA on the given operator using the given backend. The Trotterization
of the cost operator can be given explicitely as a QuantumCircuit.
Otherwise, it is automatically Trotterized.
This method submits PUBs in batches, meaning that at every iteration,
batch_size parameters are tested.
Warning:
This method tries to MAXIMIZE the energy of the operator. If you want to minimize, flip the sign of the weights in
operator.
Arguments:
- operator (SparsePauliOp): The operator whose energy to maximize
- backend (Backend):
- estimator (IBMEstimator | AerEstimator):
- cost_qc (QuantumCircuit | None, optional):
- n_qaoa_steps (int, optional):
- pm_optimization_level (int, optional):
Returns:
- A tuple containing the optimal parameters and the optimal energy.
- A tuple containing array of all parameters that were tried and the array of all resulting energies.
A list of dict decribing the results of each job. An element looks like:
{ "energy": 0.006450488764709524, "ibmq_job_id": "3c94c73c-385b-4e71-8675-e3e1ad76aa4e", "ibmq_session_id": "cz4h80039f40008scarg", "batch": 3, }