def test_pauli_expectation_param_qobj(self): """Test PauliExpectation with param_qobj""" q_instance = QuantumInstance( self.backend, seed_simulator=self.seed, seed_transpiler=self.seed, shots=10000 ) qubit_op = (0.1 * I ^ I) + (0.2 * I ^ Z) + (0.3 * Z ^ I) + (0.4 * Z ^ Z) + (0.5 * X ^ X) ansatz = RealAmplitudes(qubit_op.num_qubits) ansatz_circuit_op = CircuitStateFn(ansatz) observable = PauliExpectation().convert(~StateFn(qubit_op)) expect_op = observable.compose(ansatz_circuit_op).reduce() params1 = {} params2 = {} for param in ansatz.parameters: params1[param] = [0] params2[param] = [0, 0] sampler1 = CircuitSampler(backend=q_instance, param_qobj=False) samples1 = sampler1.convert(expect_op, params=params1) val1 = np.real(samples1.eval())[0] samples2 = sampler1.convert(expect_op, params=params2) val2 = np.real(samples2.eval()) sampler2 = CircuitSampler(backend=q_instance, param_qobj=True) samples3 = sampler2.convert(expect_op, params=params1) val3 = np.real(samples3.eval()) samples4 = sampler2.convert(expect_op, params=params2) val4 = np.real(samples4.eval()) np.testing.assert_array_almost_equal([val1] * 2, val2, decimal=2) np.testing.assert_array_almost_equal(val1, val3, decimal=2) np.testing.assert_array_almost_equal([val1] * 2, val4, decimal=2)
def test_circuit_sampler_caching(self, caching): """Test caching all operators works.""" try: from qiskit.providers.aer import Aer except Exception as ex: # pylint: disable=broad-except self.skipTest( "Aer doesn't appear to be installed. Error: '{}'".format( str(ex))) return x = Parameter('x') circuit = QuantumCircuit(1) circuit.ry(x, 0) expr1 = ~StateFn(H) @ StateFn(circuit) expr2 = ~StateFn(X) @ StateFn(circuit) sampler = CircuitSampler(Aer.get_backend('statevector_simulator'), caching=caching) res1 = sampler.convert(expr1, params={x: 0}).eval() res2 = sampler.convert(expr2, params={x: 0}).eval() res3 = sampler.convert(expr1, params={x: 0}).eval() res4 = sampler.convert(expr2, params={x: 0}).eval() self.assertEqual(res1, res3) self.assertEqual(res2, res4) if caching == 'last': self.assertEqual(len(sampler._cached_ops.keys()), 1) else: self.assertEqual(len(sampler._cached_ops.keys()), 2)
def test_parameter_binding_on_listop(self): """Test passing a ListOp with differing parameters works with the circuit sampler.""" try: from qiskit.providers.aer import Aer except Exception as ex: # pylint: disable=broad-except self.skipTest( "Aer doesn't appear to be installed. Error: '{}'".format( str(ex))) return x, y = Parameter('x'), Parameter('y') circuit1 = QuantumCircuit(1) circuit1.p(0.2, 0) circuit2 = QuantumCircuit(1) circuit2.p(x, 0) circuit3 = QuantumCircuit(1) circuit3.p(y, 0) bindings = {x: -0.4, y: 0.4} listop = ListOp( [StateFn(circuit) for circuit in [circuit1, circuit2, circuit3]]) sampler = CircuitSampler(Aer.get_backend('qasm_simulator')) sampled = sampler.convert(listop, params=bindings) self.assertTrue(all(len(op.parameters) == 0 for op in sampled.oplist))
def _eval_aux_ops( self, parameters: np.ndarray, aux_operators: List[OperatorBase], expectation: ExpectationBase, threshold: float = 1e-12, ) -> np.ndarray: # Create new CircuitSampler to avoid breaking existing one's caches. sampler = CircuitSampler(self.quantum_instance) aux_op_meas = expectation.convert( StateFn(ListOp(aux_operators), is_measurement=True)) aux_op_expect = aux_op_meas.compose( CircuitStateFn(self.ansatz.bind_parameters(parameters))) values = np.real(sampler.convert(aux_op_expect).eval()) # Discard values below threshold aux_op_results = values * (np.abs(values) > threshold) # Deal with the aux_op behavior where there can be Nones or Zero qubit Paulis in the list _aux_op_nones = [op is None for op in aux_operators] aux_operator_eigenvalues = [ None if is_none else [result] for (is_none, result) in zip(_aux_op_nones, aux_op_results) ] # As this has mixed types, since it can included None, it needs to explicitly pass object # data type to avoid numpy 1.19 warning message about implicit conversion being deprecated aux_operator_eigenvalues = np.array([aux_operator_eigenvalues], dtype=object) return aux_operator_eigenvalues
def _eval_op(self, state, op, quantum_instance, expectation): # if the operator is empty we simply return 0 if op == 0: # Note, that for some reason the individual results need to be wrapped in lists. # See also: VQE._eval_aux_ops() return [0.0j] exp = ~StateFn(op) @ state # <state|op|state> if quantum_instance is not None: try: sampler = CircuitSampler(quantum_instance) if expectation is not None: exp = expectation.convert(exp) result = sampler.convert(exp).eval() except ValueError: # TODO make this cleaner. The reason for it being here is that some quantum # instances can lead to non-positive statevectors which the Qiskit circuit # Initializer is unable to handle. result = exp.eval() else: result = exp.eval() # Note, that for some reason the individual results need to be wrapped in lists. # See also: VQE._eval_aux_ops() return [result]
def test_quantum_instance_with_backend_shots(self): """Test sampling a circuit where the backend has shots attached.""" try: from qiskit.providers.aer import AerSimulator except Exception as ex: # pylint: disable=broad-except self.skipTest("Aer doesn't appear to be installed. Error: '{}'".format(str(ex))) backend = AerSimulator(shots=10) sampler = CircuitSampler(backend) res = sampler.convert(~Plus @ Plus).eval() self.assertAlmostEqual(res, 1 + 0j, places=2)
def _eval_aux_ops( self, parameters: np.ndarray, aux_operators: ListOrDict[OperatorBase], expectation: ExpectationBase, threshold: float = 1e-12, ) -> ListOrDict[Tuple[complex, complex]]: # Create new CircuitSampler to avoid breaking existing one's caches. sampler = CircuitSampler(self.quantum_instance) if isinstance(aux_operators, dict): list_op = ListOp(list(aux_operators.values())) else: list_op = ListOp(aux_operators) aux_op_meas = expectation.convert(StateFn(list_op, is_measurement=True)) aux_op_expect = aux_op_meas.compose( CircuitStateFn(self.ansatz.bind_parameters(parameters))) aux_op_expect_sampled = sampler.convert(aux_op_expect) # compute means values = np.real(aux_op_expect_sampled.eval()) # compute standard deviations variances = np.real( expectation.compute_variance(aux_op_expect_sampled)) if not isinstance(variances, np.ndarray) and variances == 0.0: # when `variances` is a single value equal to 0., our expectation value is exact and we # manually ensure the variances to be a list of the correct length variances = np.zeros(len(aux_operators), dtype=float) std_devs = np.sqrt(variances / self.quantum_instance.run_config.shots) # Discard values below threshold aux_op_means = values * (np.abs(values) > threshold) # zip means and standard deviations into tuples aux_op_results = zip(aux_op_means, std_devs) # Return None eigenvalues for None operators if aux_operators is a list. # None operators are already dropped in compute_minimum_eigenvalue if aux_operators is a dict. if isinstance(aux_operators, list): aux_operator_eigenvalues = [None] * len(aux_operators) key_value_iterator = enumerate(aux_op_results) else: aux_operator_eigenvalues = {} key_value_iterator = zip(aux_operators.keys(), aux_op_results) for key, value in key_value_iterator: if aux_operators[key] is not None: aux_operator_eigenvalues[key] = value return aux_operator_eigenvalues
def _eval_aux_ops(self, threshold=1e-12): # Create new CircuitSampler to avoid breaking existing one's caches. sampler = CircuitSampler(self.quantum_instance) aux_op_meas = self.expectation.convert(StateFn(ListOp(self.aux_operators), is_measurement=True)) aux_op_expect = aux_op_meas.compose(CircuitStateFn(self.get_optimal_circuit())) values = np.real(sampler.convert(aux_op_expect).eval()) # Discard values below threshold aux_op_results = (values * (np.abs(values) > threshold)) # Deal with the aux_op behavior where there can be Nones or Zero qubit Paulis in the list self._ret['aux_ops'] = [None if is_none else [result] for (is_none, result) in zip(self._aux_op_nones, aux_op_results)] # As this has mixed types, since it can included None, it needs to explicitly pass object # data type to avoid numpy 1.19 warning message about implicit conversion being deprecated self._ret['aux_ops'] = np.array([self._ret['aux_ops']], dtype=object)
def test_single_parameter_binds(self): """Test passing parameter binds as a dictionary to the circuit sampler.""" try: from qiskit.providers.aer import Aer except Exception as ex: # pylint: disable=broad-except self.skipTest("Aer doesn't appear to be installed. Error: '{}'".format(str(ex))) return x = Parameter("x") circuit = QuantumCircuit(1) circuit.ry(x, 0) expr = ~StateFn(H) @ StateFn(circuit) sampler = CircuitSampler(Aer.get_backend("aer_simulator_statevector")) res = sampler.convert(expr, params={x: 0}).eval() self.assertIsInstance(res, complex)
def _eval_aux_ops( self, parameters: np.ndarray, aux_operators: ListOrDict[OperatorBase], expectation: ExpectationBase, threshold: float = 1e-12, ) -> ListOrDict[complex]: # Create new CircuitSampler to avoid breaking existing one's caches. sampler = CircuitSampler(self.quantum_instance) if isinstance(aux_operators, dict): list_op = ListOp(list(aux_operators.values())) else: list_op = ListOp(aux_operators) aux_op_meas = expectation.convert(StateFn(list_op, is_measurement=True)) aux_op_expect = aux_op_meas.compose( CircuitStateFn(self.ansatz.bind_parameters(parameters))) values = np.real(sampler.convert(aux_op_expect).eval()) # Discard values below threshold aux_op_results = values * (np.abs(values) > threshold) # Return None eigenvalues for None operators if aux_operators is a list. # None operators are already dropped in compute_minimum_eigenvalue if aux_operators is a dict. if isinstance(aux_operators, list): aux_operator_eigenvalues = [None] * len(aux_operators) key_value_iterator = enumerate(aux_op_results) else: aux_operator_eigenvalues = {} key_value_iterator = zip(aux_operators.keys(), aux_op_results) for key, value in key_value_iterator: if aux_operators[key] is not None: aux_operator_eigenvalues[key] = value return aux_operator_eigenvalues
class TestAerPauliExpectation(QiskitOpflowTestCase): """Pauli Change of Basis Expectation tests.""" def setUp(self) -> None: super().setUp() try: from qiskit import Aer self.seed = 97 self.backend = Aer.get_backend("aer_simulator") q_instance = QuantumInstance( self.backend, seed_simulator=self.seed, seed_transpiler=self.seed ) self.sampler = CircuitSampler(q_instance, attach_results=True) self.expect = AerPauliExpectation() except Exception as ex: # pylint: disable=broad-except self.skipTest("Aer doesn't appear to be installed. Error: '{}'".format(str(ex))) return def test_pauli_expect_pair(self): """pauli expect pair test""" op = Z ^ Z # wvf = (Pl^Pl) + (Ze^Ze) wvf = CX @ (H ^ I) @ Zero converted_meas = self.expect.convert(~StateFn(op) @ wvf) sampled = self.sampler.convert(converted_meas) self.assertAlmostEqual(sampled.eval(), 0, delta=0.1) def test_pauli_expect_single(self): """pauli expect single test""" # TODO bug in Aer with Y measurements # paulis = [Z, X, Y, I] paulis = [Z, X, I] states = [Zero, One, Plus, Minus, S @ Plus, S @ Minus] for pauli, state in itertools.product(paulis, states): converted_meas = self.expect.convert(~StateFn(pauli) @ state) matmulmean = state.adjoint().to_matrix() @ pauli.to_matrix() @ state.to_matrix() sampled = self.sampler.convert(converted_meas) self.assertAlmostEqual(sampled.eval(), matmulmean, delta=0.1) def test_pauli_expect_op_vector(self): """pauli expect op vector test""" paulis_op = ListOp([X, Y, Z, I]) converted_meas = self.expect.convert(~StateFn(paulis_op)) plus_mean = converted_meas @ Plus sampled_plus = self.sampler.convert(plus_mean) np.testing.assert_array_almost_equal(sampled_plus.eval(), [1, 0, 0, 1], decimal=1) minus_mean = converted_meas @ Minus sampled_minus = self.sampler.convert(minus_mean) np.testing.assert_array_almost_equal(sampled_minus.eval(), [-1, 0, 0, 1], decimal=1) zero_mean = converted_meas @ Zero sampled_zero = self.sampler.convert(zero_mean) np.testing.assert_array_almost_equal(sampled_zero.eval(), [0, 0, 1, 1], decimal=1) sum_zero = (Plus + Minus) * (0.5 ** 0.5) sum_zero_mean = converted_meas @ sum_zero sampled_zero_mean = self.sampler.convert(sum_zero_mean) # !!NOTE!!: Depolarizing channel (Sampling) means interference # does not happen between circuits in sum, so expectation does # not equal expectation for Zero!! np.testing.assert_array_almost_equal(sampled_zero_mean.eval(), [0, 0, 0, 2], decimal=1) def test_pauli_expect_state_vector(self): """pauli expect state vector test""" states_op = ListOp([One, Zero, Plus, Minus]) paulis_op = X converted_meas = self.expect.convert(~StateFn(paulis_op) @ states_op) sampled = self.sampler.convert(converted_meas) # Small test to see if execution results are accessible for composed_op in sampled: self.assertIn("counts", composed_op[0].execution_results) np.testing.assert_array_almost_equal(sampled.eval(), [0, 0, 1, -1], decimal=1) def test_pauli_expect_op_vector_state_vector(self): """pauli expect op vector state vector test""" # TODO Bug in Aer with Y Measurements!! # paulis_op = ListOp([X, Y, Z, I]) paulis_op = ListOp([X, Z, I]) states_op = ListOp([One, Zero, Plus, Minus]) valids = [ [+0, 0, 1, -1], # [+0, 0, 0, 0], [-1, 1, 0, -0], [+1, 1, 1, 1], ] converted_meas = self.expect.convert(~StateFn(paulis_op) @ states_op) sampled = self.sampler.convert(converted_meas) np.testing.assert_array_almost_equal(sampled.eval(), valids, decimal=1) def test_multi_representation_ops(self): """Test observables with mixed representations""" mixed_ops = ListOp([X.to_matrix_op(), H, H + I, X]) converted_meas = self.expect.convert(~StateFn(mixed_ops)) plus_mean = converted_meas @ Plus sampled_plus = self.sampler.convert(plus_mean) np.testing.assert_array_almost_equal( sampled_plus.eval(), [1, 0.5 ** 0.5, (1 + 0.5 ** 0.5), 1], decimal=1 ) @unittest.skip("Skip until https://github.com/Qiskit/qiskit-aer/issues/1249 is closed.") def test_parameterized_qobj(self): """grouped pauli expectation test""" two_qubit_h2 = ( (-1.052373245772859 * I ^ I) + (0.39793742484318045 * I ^ Z) + (-0.39793742484318045 * Z ^ I) + (-0.01128010425623538 * Z ^ Z) + (0.18093119978423156 * X ^ X) ) aer_sampler = CircuitSampler( self.sampler.quantum_instance, param_qobj=True, attach_results=True ) ansatz = RealAmplitudes() ansatz.num_qubits = 2 observable_meas = self.expect.convert(StateFn(two_qubit_h2, is_measurement=True)) ansatz_circuit_op = CircuitStateFn(ansatz) expect_op = observable_meas.compose(ansatz_circuit_op).reduce() def generate_parameters(num): param_bindings = {} for param in ansatz.parameters: values = [] for _ in range(num): values.append(np.random.rand()) param_bindings[param] = values return param_bindings def validate_sampler(ideal, sut, param_bindings): expect_sampled = ideal.convert(expect_op, params=param_bindings).eval() actual_sampled = sut.convert(expect_op, params=param_bindings).eval() self.assertTrue( np.allclose(actual_sampled, expect_sampled), "%s != %s" % (actual_sampled, expect_sampled), ) def get_circuit_templates(sampler): return sampler._transpiled_circ_templates def validate_aer_binding_used(templates): self.assertIsNotNone(templates) def validate_aer_templates_reused(prev_templates, cur_templates): self.assertIs(prev_templates, cur_templates) validate_sampler(self.sampler, aer_sampler, generate_parameters(1)) cur_templates = get_circuit_templates(aer_sampler) validate_aer_binding_used(cur_templates) prev_templates = cur_templates validate_sampler(self.sampler, aer_sampler, generate_parameters(2)) cur_templates = get_circuit_templates(aer_sampler) validate_aer_templates_reused(prev_templates, cur_templates) prev_templates = cur_templates validate_sampler(self.sampler, aer_sampler, generate_parameters(2)) # same num of params cur_templates = get_circuit_templates(aer_sampler) validate_aer_templates_reused(prev_templates, cur_templates) def test_pauli_expectation_param_qobj(self): """Test PauliExpectation with param_qobj""" q_instance = QuantumInstance( self.backend, seed_simulator=self.seed, seed_transpiler=self.seed, shots=10000 ) qubit_op = (0.1 * I ^ I) + (0.2 * I ^ Z) + (0.3 * Z ^ I) + (0.4 * Z ^ Z) + (0.5 * X ^ X) ansatz = RealAmplitudes(qubit_op.num_qubits) ansatz_circuit_op = CircuitStateFn(ansatz) observable = PauliExpectation().convert(~StateFn(qubit_op)) expect_op = observable.compose(ansatz_circuit_op).reduce() params1 = {} params2 = {} for param in ansatz.parameters: params1[param] = [0] params2[param] = [0, 0] sampler1 = CircuitSampler(backend=q_instance, param_qobj=False) samples1 = sampler1.convert(expect_op, params=params1) val1 = np.real(samples1.eval())[0] samples2 = sampler1.convert(expect_op, params=params2) val2 = np.real(samples2.eval()) sampler2 = CircuitSampler(backend=q_instance, param_qobj=True) samples3 = sampler2.convert(expect_op, params=params1) val3 = np.real(samples3.eval()) samples4 = sampler2.convert(expect_op, params=params2) val4 = np.real(samples4.eval()) np.testing.assert_array_almost_equal([val1] * 2, val2, decimal=2) np.testing.assert_array_almost_equal(val1, val3, decimal=2) np.testing.assert_array_almost_equal([val1] * 2, val4, decimal=2) def test_list_pauli_sum(self): """Test AerPauliExpectation for ListOp[PauliSumOp]""" test_op = ListOp([PauliSumOp.from_list([("XX", 1), ("ZI", 3), ("ZZ", 5)])]) observable = AerPauliExpectation().convert(~StateFn(test_op)) self.assertIsInstance(observable, ListOp) self.assertIsInstance(observable[0], CircuitStateFn) self.assertTrue(observable[0].is_measurement) def test_pauli_expectation_non_hermite_op(self): """Test PauliExpectation for non hermitian operator""" exp = ~StateFn(1j * Z) @ One self.assertEqual(self.sampler.convert(self.expect.convert(exp)).eval(), 1j)
class HHL(LinearSolver): r"""Systems of linear equations arise naturally in many real-life applications in a wide range of areas, such as in the solution of Partial Differential Equations, the calibration of financial models, fluid simulation or numerical field calculation. The problem can be defined as, given a matrix :math:`A\in\mathbb{C}^{N\times N}` and a vector :math:`\vec{b}\in\mathbb{C}^{N}`, find :math:`\vec{x}\in\mathbb{C}^{N}` satisfying :math:`A\vec{x}=\vec{b}`. A system of linear equations is called :math:`s`-sparse if :math:`A` has at most :math:`s` non-zero entries per row or column. Solving an :math:`s`-sparse system of size :math:`N` with a classical computer requires :math:`\mathcal{ O }(Ns\kappa\log(1/\epsilon))` running time using the conjugate gradient method. Here :math:`\kappa` denotes the condition number of the system and :math:`\epsilon` the accuracy of the approximation. The HHL is a quantum algorithm to estimate a function of the solution with running time complexity of :math:`\mathcal{ O }(\log(N)s^{2}\kappa^{2}/\epsilon)` when :math:`A` is a Hermitian matrix under the assumptions of efficient oracles for loading the data, Hamiltonian simulation and computing a function of the solution. This is an exponential speed up in the size of the system, however one crucial remark to keep in mind is that the classical algorithm returns the full solution, while the HHL can only approximate functions of the solution vector. Examples: .. jupyter-execute:: import numpy as np from qiskit import QuantumCircuit from qiskit.algorithms.linear_solvers.hhl import HHL from qiskit.algorithms.linear_solvers.matrices import TridiagonalToeplitz from qiskit.algorithms.linear_solvers.observables import MatrixFunctional matrix = TridiagonalToeplitz(2, 1, 1 / 3, trotter_steps=2) right_hand_side = [1.0, -2.1, 3.2, -4.3] observable = MatrixFunctional(1, 1 / 2) rhs = right_hand_side / np.linalg.norm(right_hand_side) # Initial state circuit num_qubits = matrix.num_state_qubits qc = QuantumCircuit(num_qubits) qc.isometry(rhs, list(range(num_qubits)), None) hhl = HHL() solution = hhl.solve(matrix, qc, observable) approx_result = solution.observable References: [1]: Harrow, A. W., Hassidim, A., Lloyd, S. (2009). Quantum algorithm for linear systems of equations. `Phys. Rev. Lett. 103, 15 (2009), 1–15. <https://doi.org/10.1103/PhysRevLett.103.150502>`_ [2]: Carrera Vazquez, A., Hiptmair, R., & Woerner, S. (2020). Enhancing the Quantum Linear Systems Algorithm using Richardson Extrapolation. `arXiv:2009.04484 <http://arxiv.org/abs/2009.04484>`_ """ def __init__(self, epsilon: float = 1e-2, expectation: Optional[ExpectationBase] = None, quantum_instance: Optional[Union[Backend, BaseBackend, QuantumInstance]] = None)\ -> None: r""" Args: epsilon: Error tolerance of the approximation to the solution, i.e. if :math:`x` is the exact solution and :math:`\tilde{x}` the one calculated by the algorithm, then :math:`||x - \tilde{x}|| \le epsilon`. expectation: The expectation converter applied to the expectation values before evaluation. If None then PauliExpectation is used. quantum_instance: Quantum Instance or Backend. If None, a Statevector calculation is done. """ super().__init__() self._epsilon = epsilon # Tolerance for the different parts of the algorithm as per [1] self._epsilon_r = epsilon / 3 # conditioned rotation self._epsilon_s = epsilon / 3 # state preparation self._epsilon_a = epsilon / 6 # hamiltonian simulation self._scaling = None # scaling of the solution if quantum_instance is not None: self._sampler = CircuitSampler(quantum_instance) else: self._sampler = None self._expectation = expectation # For now the default reciprocal implementation is exact self._exact_reciprocal = True # Set the default scaling to 1 self.scaling = 1 @property def quantum_instance(self) -> Optional[QuantumInstance]: """Get the quantum instance. Returns: The quantum instance used to run this algorithm. """ return self._sampler.quantum_instance @quantum_instance.setter def quantum_instance( self, quantum_instance: Union[QuantumInstance, BaseBackend, Backend]) -> None: """Set quantum instance. Args: quantum_instance: The quantum instance used to run this algorithm. """ self._sampler.quantum_instance = quantum_instance @property def scaling(self) -> float: """The scaling of the solution vector.""" return self._scaling @scaling.setter def scaling(self, scaling: float) -> None: """Set the new scaling of the solution vector.""" self._scaling = scaling @property def expectation(self) -> ExpectationBase: """The expectation value algorithm used to construct the expectation measurement from the observable. """ return self._expectation @expectation.setter def expectation(self, expectation: ExpectationBase) -> None: """Set the expectation value algorithm.""" self._expectation = expectation def _get_delta(self, n_l: int, lambda_min: float, lambda_max: float) -> float: """Calculates the scaling factor to represent exactly lambda_min on nl binary digits. Args: n_l: The number of qubits to represent the eigenvalues. lambda_min: the smallest eigenvalue. lambda_max: the largest eigenvalue. Returns: The value of the scaling factor. """ formatstr = "#0" + str(n_l + 2) + "b" lambda_min_tilde = np.abs(lambda_min * (2**n_l - 1) / lambda_max) # floating point precision can cause problems if np.abs(lambda_min_tilde - 1) < 1e-7: lambda_min_tilde = 1 binstr = format(int(lambda_min_tilde), formatstr)[2::] lamb_min_rep = 0 for i, char in enumerate(binstr): lamb_min_rep += int(char) / (2**(i + 1)) return lamb_min_rep def _calculate_norm(self, qc: QuantumCircuit) -> float: """Calculates the value of the euclidean norm of the solution. Args: qc: The quantum circuit preparing the solution x to the system. Returns: The value of the euclidean norm of the solution. """ # Calculate the number of qubits nb = qc.qregs[0].size nl = qc.qregs[1].size na = qc.num_ancillas # Create the Operators Zero and One zero_op = (I + Z) / 2 one_op = (I - Z) / 2 # Norm observable observable = one_op ^ TensoredOp((nl + na) * [zero_op]) ^ (I ^ nb) norm_2 = (~StateFn(observable) @ StateFn(qc)).eval() return np.real(np.sqrt(norm_2) / self.scaling) def _calculate_observable( self, solution: QuantumCircuit, observable: Optional[Union[LinearSystemObservable, BaseOperator]] = None, observable_circuit: Optional[QuantumCircuit] = None, post_processing: Optional[Callable[[Union[float, List[float]]], Union[float, List[float]]]] = None ) -> Tuple[Union[float, List[float]], Union[float, List[float]]]: """Calculates the value of the observable(s) given. Args: solution: The quantum circuit preparing the solution x to the system. observable: Information to be extracted from the solution. observable_circuit: Circuit to be applied to the solution to extract information. post_processing: Function to compute the value of the observable. Returns: The value of the observable(s) and the circuit results before post-processing as a tuple. """ # Get the number of qubits nb = solution.qregs[0].size nl = solution.qregs[1].size na = solution.num_ancillas # if the observable is given construct post_processing and observable_circuit if observable is not None: observable_circuit = observable.observable_circuit(nb) post_processing = observable.post_processing if isinstance(observable, LinearSystemObservable): observable = observable.observable(nb) # in the other case use the identity as observable else: observable = I ^ nb # Create the Operators Zero and One zero_op = (I + Z) / 2 one_op = (I - Z) / 2 is_list = True if not isinstance(observable_circuit, list): is_list = False observable_circuit = [observable_circuit] observable = [observable] expectations = [] for circ, obs in zip(observable_circuit, observable): circuit = QuantumCircuit(solution.num_qubits) circuit.append(solution, circuit.qubits) circuit.append(circ, range(nb)) ob = one_op ^ TensoredOp((nl + na) * [zero_op]) ^ obs expectations.append(~StateFn(ob) @ StateFn(circuit)) if is_list: # execute all in a list op to send circuits in batches expectations = ListOp(expectations) else: expectations = expectations[0] # check if an expectation converter is given if self._expectation is not None: expectations = self._expectation.convert(expectations) # if otherwise a backend was specified, try to set the best expectation value elif self._sampler is not None: if is_list: op = expectations.oplist[0] else: op = expectations self._expectation = ExpectationFactory.build( op, self._sampler.quantum_instance) if self._sampler is not None: expectations = self._sampler.convert(expectations) # evaluate expectation_results = expectations.eval() # apply post_processing result = post_processing(expectation_results, nb, self.scaling) return result, expectation_results def construct_circuit( self, matrix: Union[np.ndarray, QuantumCircuit], vector: Union[np.ndarray, QuantumCircuit]) -> QuantumCircuit: """Construct the HHL circuit. Args: matrix: The matrix specifying the system, i.e. A in Ax=b. vector: The vector specifying the right hand side of the equation in Ax=b. Returns: The HHL circuit. Raises: ValueError: If the input is not in the correct format. ValueError: If the type of the input matrix is not supported. """ # State preparation circuit - default is qiskit if isinstance(vector, QuantumCircuit): nb = vector.num_qubits vector_circuit = vector elif isinstance(vector, np.ndarray): nb = int(np.log2(len(vector))) vector_circuit = QuantumCircuit(nb) vector_circuit.isometry(vector / np.linalg.norm(vector), list(range(nb)), None) # If state preparation is probabilistic the number of qubit flags should increase nf = 1 # Hamiltonian simulation circuit - default is Trotterization if isinstance(matrix, QuantumCircuit): matrix_circuit = matrix elif isinstance(matrix, (list, np.ndarray)): if isinstance(matrix, list): matrix = np.array(matrix) if matrix.shape[0] != matrix.shape[1]: raise ValueError("Input matrix must be square!") if np.log2(matrix.shape[0]) % 1 != 0: raise ValueError("Input matrix dimension must be 2^n!") if not np.allclose(matrix, matrix.conj().T): raise ValueError("Input matrix must be hermitian!") if matrix.shape[0] != 2**vector_circuit.num_qubits: raise ValueError("Input vector dimension does not match input " "matrix dimension! Vector dimension: " + str(vector_circuit.num_qubits) + ". Matrix dimension: " + str(matrix.shape[0])) matrix_circuit = NumPyMatrix(matrix, evolution_time=2 * np.pi) else: raise ValueError(f'Invalid type for matrix: {type(matrix)}.') # Set the tolerance for the matrix approximation if hasattr(matrix_circuit, "tolerance"): matrix_circuit.tolerance = self._epsilon_a # check if the matrix can calculate the condition number and store the upper bound if hasattr(matrix_circuit, "condition_bounds") and matrix_circuit.condition_bounds() is not\ None: kappa = matrix_circuit.condition_bounds()[1] else: kappa = 1 # Update the number of qubits required to represent the eigenvalues nl = max(nb + 1, int(np.log2(kappa)) + 1) # check if the matrix can calculate bounds for the eigenvalues if hasattr(matrix_circuit, "eigs_bounds") and matrix_circuit.eigs_bounds() is not None: lambda_min, lambda_max = matrix_circuit.eigs_bounds() # Constant so that the minimum eigenvalue is represented exactly, since it contributes # the most to the solution of the system delta = self._get_delta(nl, lambda_min, lambda_max) # Update evolution time matrix_circuit.evolution_time = 2 * np.pi * delta / lambda_min # Update the scaling of the solution self.scaling = lambda_min else: delta = 1 / (2**nl) print("The solution will be calculated up to a scaling factor.") if self._exact_reciprocal: reciprocal_circuit = ExactReciprocal(nl, delta) # Update number of ancilla qubits na = matrix_circuit.num_ancillas else: # Calculate breakpoints for the reciprocal approximation num_values = 2**nl constant = delta a = int(round(num_values**(2 / 3))) # pylint: disable=invalid-name # Calculate the degree of the polynomial and the number of intervals r = 2 * constant / a + np.sqrt(np.abs(1 - (2 * constant / a)**2)) degree = min( nb, int( np.log(1 + (16.23 * np.sqrt(np.log(r)**2 + (np.pi / 2)**2) * kappa * (2 * kappa - self._epsilon_r)) / self._epsilon_r))) num_intervals = int( np.ceil(np.log((num_values - 1) / a) / np.log(5))) # Calculate breakpoints and polynomials breakpoints = [] for i in range(0, num_intervals): # Add the breakpoint to the list breakpoints.append(a * (5**i)) # Define the right breakpoint of the interval if i == num_intervals - 1: breakpoints.append(num_values - 1) reciprocal_circuit = PiecewiseChebyshev( lambda x: np.arcsin(constant / x), degree, breakpoints, nl) na = max(matrix_circuit.num_ancillas, reciprocal_circuit.num_ancillas) # Initialise the quantum registers qb = QuantumRegister(nb) # right hand side and solution ql = QuantumRegister(nl) # eigenvalue evaluation qubits if na > 0: qa = AncillaRegister(na) # ancilla qubits qf = QuantumRegister(nf) # flag qubits if na > 0: qc = QuantumCircuit(qb, ql, qa, qf) else: qc = QuantumCircuit(qb, ql, qf) # State preparation qc.append(vector_circuit, qb[:]) # QPE phase_estimation = PhaseEstimation(nl, matrix_circuit) if na > 0: qc.append(phase_estimation, ql[:] + qb[:] + qa[:matrix_circuit.num_ancillas]) else: qc.append(phase_estimation, ql[:] + qb[:]) # Conditioned rotation if self._exact_reciprocal: qc.append(reciprocal_circuit, ql[::-1] + [qf[0]]) else: qc.append(reciprocal_circuit.to_instruction(), ql[:] + [qf[0]] + qa[:reciprocal_circuit.num_ancillas]) # QPE inverse if na > 0: qc.append(phase_estimation.inverse(), ql[:] + qb[:] + qa[:matrix_circuit.num_ancillas]) else: qc.append(phase_estimation.inverse(), ql[:] + qb[:]) return qc def solve(self, matrix: Union[np.ndarray, QuantumCircuit], vector: Union[np.ndarray, QuantumCircuit], observable: Optional[Union[LinearSystemObservable, BaseOperator, List[LinearSystemObservable], List[BaseOperator]]] = None, observable_circuit: Optional[Union[QuantumCircuit, List[QuantumCircuit]]] = None, post_processing: Optional[Callable[[Union[float, List[float]]], Union[float, List[float]]]] = None) \ -> LinearSolverResult: """Tries to solve the given linear system of equations. Args: matrix: The matrix specifying the system, i.e. A in Ax=b. vector: The vector specifying the right hand side of the equation in Ax=b. observable: Optional information to be extracted from the solution. Default is the probability of success of the algorithm. observable_circuit: Optional circuit to be applied to the solution to extract information. Default is `None`. post_processing: Optional function to compute the value of the observable. Default is the raw value of measuring the observable. Raises: ValueError: If an invalid combination of observable, observable_circuit and post_processing is passed. Returns: The result object containing information about the solution vector of the linear system. """ # verify input if observable is not None: if observable_circuit is not None or post_processing is not None: raise ValueError( 'If observable is passed, observable_circuit and post_processing ' 'cannot be set.') solution = LinearSolverResult() solution.state = self.construct_circuit(matrix, vector) solution.euclidean_norm = self._calculate_norm(solution.state) if observable is not None or observable_circuit is not None: solution.observable, solution.circuit_results = \ self._calculate_observable(solution.state, observable, observable_circuit, post_processing) return solution
def eval_observables( quantum_instance: Union[QuantumInstance, BaseBackend, Backend], quantum_state: Union[ Statevector, QuantumCircuit, OperatorBase, ], observables: ListOrDict[OperatorBase], expectation: ExpectationBase, threshold: float = 1e-12, ) -> ListOrDict[Tuple[complex, complex]]: """ Accepts a list or a dictionary of operators and calculates their expectation values - means and standard deviations. They are calculated with respect to a quantum state provided. A user can optionally provide a threshold value which filters mean values falling below the threshold. Args: quantum_instance: A quantum instance used for calculations. quantum_state: An unparametrized quantum circuit representing a quantum state that expectation values are computed against. observables: A list or a dictionary of operators whose expectation values are to be calculated. expectation: An instance of ExpectationBase which defines a method for calculating expectation values. threshold: A threshold value that defines which mean values should be neglected (helpful for ignoring numerical instabilities close to 0). Returns: A list or a dictionary of tuples (mean, standard deviation). Raises: ValueError: If a ``quantum_state`` with free parameters is provided. """ if ( isinstance( quantum_state, (QuantumCircuit, OperatorBase) ) # Statevector cannot be parametrized and len(quantum_state.parameters) > 0 ): raise ValueError( "A parametrized representation of a quantum_state was provided. It is not " "allowed - it cannot have free parameters." ) # Create new CircuitSampler to avoid breaking existing one's caches. sampler = CircuitSampler(quantum_instance) list_op = _prepare_list_op(quantum_state, observables) observables_expect = expectation.convert(list_op) observables_expect_sampled = sampler.convert(observables_expect) # compute means values = np.real(observables_expect_sampled.eval()) # compute standard deviations std_devs = _compute_std_devs( observables_expect_sampled, observables, expectation, quantum_instance ) # Discard values below threshold observables_means = values * (np.abs(values) > threshold) # zip means and standard deviations into tuples observables_results = list(zip(observables_means, std_devs)) # Return None eigenvalues for None operators if observables is a list. # None operators are already dropped in compute_minimum_eigenvalue if observables is a dict. return _prepare_result(observables_results, observables)
class VQE(VariationalAlgorithm, MinimumEigensolver): r"""The Variational Quantum Eigensolver algorithm. `VQE <https://arxiv.org/abs/1304.3061>`__ is a hybrid algorithm that uses a variational technique and interleaves quantum and classical computations in order to find the minimum eigenvalue of the Hamiltonian :math:`H` of a given system. An instance of VQE requires defining two algorithmic sub-components: a trial state (a.k.a. ansatz) which is a :class:`QuantumCircuit`, and one of the classical :mod:`~qiskit.algorithms.optimizers`. The ansatz is varied, via its set of parameters, by the optimizer, such that it works towards a state, as determined by the parameters applied to the variational form, that will result in the minimum expectation value being measured of the input operator (Hamiltonian). An optional array of parameter values, via the *initial_point*, may be provided as the starting point for the search of the minimum eigenvalue. This feature is particularly useful such as when there are reasons to believe that the solution point is close to a particular point. As an example, when building the dissociation profile of a molecule, it is likely that using the previous computed optimal solution as the starting initial point for the next interatomic distance is going to reduce the number of iterations necessary for the variational algorithm to converge. It provides an `initial point tutorial <https://github.com/Qiskit/qiskit-tutorials-community/blob/master /chemistry/h2_vqe_initial_point.ipynb>`__ detailing this use case. The length of the *initial_point* list value must match the number of the parameters expected by the variational form being used. If the *initial_point* is left at the default of ``None``, then VQE will look to the variational form for a preferred value, based on its given initial state. If the variational form returns ``None``, then a random point will be generated within the parameter bounds set, as per above. If the variational form provides ``None`` as the lower bound, then VQE will default it to :math:`-2\pi`; similarly, if the variational form returns ``None`` as the upper bound, the default value will be :math:`2\pi`. .. note:: The VQE stores the parameters of ``var_form`` sorted by name to map the values provided by the optimizer to the circuit. This is done to ensure reproducible results, for example such that running the optimization twice with same random seeds yields the same result. Also, the ``optimal_point`` of the result object can be used as initial point of another VQE run by passing it as ``initial_point`` to the initializer. """ def __init__( self, var_form: Optional[QuantumCircuit] = None, optimizer: Optional[Optimizer] = None, initial_point: Optional[np.ndarray] = None, gradient: Optional[Union[GradientBase, Callable]] = None, expectation: Optional[ExpectationBase] = None, include_custom: bool = False, max_evals_grouped: int = 1, callback: Optional[Callable[[int, np.ndarray, float, float], None]] = None, quantum_instance: Optional[Union[QuantumInstance, BaseBackend, Backend]] = None ) -> None: """ Args: var_form: A parameterized circuit used as Ansatz for the wave function. optimizer: A classical optimizer. initial_point: An optional initial point (i.e. initial parameter values) for the optimizer. If ``None`` then VQE will look to the variational form for a preferred point and if not will simply compute a random one. gradient: An optional gradient function or operator for optimizer. expectation: The Expectation converter for taking the average value of the Observable over the var_form state function. When ``None`` (the default) an :class:`~qiskit.opflow.expectations.ExpectationFactory` is used to select an appropriate expectation based on the operator and backend. When using Aer qasm_simulator backend, with paulis, it is however much faster to leverage custom Aer function for the computation but, although VQE performs much faster with it, the outcome is ideal, with no shot noise, like using a state vector simulator. If you are just looking for the quickest performance when choosing Aer qasm_simulator and the lack of shot noise is not an issue then set `include_custom` parameter here to ``True`` (defaults to ``False``). include_custom: When `expectation` parameter here is None setting this to ``True`` will allow the factory to include the custom Aer pauli expectation. max_evals_grouped: Max number of evaluations performed simultaneously. Signals the given optimizer that more than one set of parameters can be supplied so that potentially the expectation values can be computed in parallel. Typically this is possible when a finite difference gradient is used by the optimizer such that multiple points to compute the gradient can be passed and if computed in parallel improve overall execution time. Deprecated if a gradient operator or function is given. callback: a callback that can access the intermediate data during the optimization. Four parameter values are passed to the callback as follows during each evaluation by the optimizer for its current set of parameters as it works towards the minimum. These are: the evaluation count, the optimizer parameters for the variational form, the evaluated mean and the evaluated standard deviation.` quantum_instance: Quantum Instance or Backend """ validate_min('max_evals_grouped', max_evals_grouped, 1) if var_form is None: var_form = RealAmplitudes() if optimizer is None: optimizer = SLSQP() # set the initial point to the preferred parameters of the variational form if initial_point is None and hasattr(var_form, 'preferred_init_points'): initial_point = var_form.preferred_init_points self._max_evals_grouped = max_evals_grouped self._circuit_sampler = None # type: Optional[CircuitSampler] self._expectation = expectation self._user_valid_expectation = self._expectation is not None self._include_custom = include_custom self._expect_op = None super().__init__(var_form=var_form, optimizer=optimizer, cost_fn=self._energy_evaluation, gradient=gradient, initial_point=initial_point, quantum_instance=quantum_instance) self._ret = VQEResult() self._eval_time = None self._optimizer.set_max_evals_grouped(max_evals_grouped) self._callback = callback self._eval_count = 0 logger.info(self.print_settings()) def _try_set_expectation_value_from_factory( self, operator: OperatorBase) -> None: if operator is not None and self.quantum_instance is not None: self._set_expectation( ExpectationFactory.build(operator=operator, backend=self.quantum_instance, include_custom=self._include_custom)) def _set_expectation(self, exp: ExpectationBase) -> None: self._expectation = exp self._user_valid_expectation = False self._expect_op = None @VariationalAlgorithm.quantum_instance.setter def quantum_instance( self, quantum_instance: Union[QuantumInstance, BaseBackend, Backend]) -> None: """ set quantum_instance """ super(VQE, self.__class__).quantum_instance.__set__(self, quantum_instance) self._circuit_sampler = CircuitSampler( self._quantum_instance, param_qobj=is_aer_provider(self._quantum_instance.backend)) @property def expectation(self) -> ExpectationBase: """ The expectation value algorithm used to construct the expectation measurement from the observable. """ return self._expectation @expectation.setter def expectation(self, exp: ExpectationBase) -> None: self._set_expectation(exp) self._user_valid_expectation = self._expectation is not None def _check_operator_varform(self, operator: OperatorBase): """Check that the number of qubits of operator and variational form match.""" if operator is not None and self.var_form is not None: if operator.num_qubits != self.var_form.num_qubits: # try to set the number of qubits on the variational form, if possible try: self.var_form.num_qubits = operator.num_qubits self._var_form_params = sorted(self.var_form.parameters, key=lambda p: p.name) except AttributeError as ex: raise AlgorithmError( "The number of qubits of the variational form " "does not match the operator, and the variational " "form does not allow setting the number of qubits " " using `num_qubits`.") from ex @VariationalAlgorithm.optimizer.setter # type: ignore def optimizer(self, optimizer: Optimizer): """ Sets optimizer """ super(VQE, self.__class__).optimizer.__set__(self, optimizer) # type: ignore if optimizer is not None: optimizer.set_max_evals_grouped(self._max_evals_grouped) @property def setting(self): """Prepare the setting of VQE as a string.""" ret = "Algorithm: {}\n".format(self.__class__.__name__) params = "" for key, value in self.__dict__.items(): if key[0] == "_": if "initial_point" in key and value is None: params += "-- {}: {}\n".format(key[1:], "Random seed") else: params += "-- {}: {}\n".format(key[1:], value) ret += "{}".format(params) return ret def print_settings(self): """ Preparing the setting of VQE into a string. Returns: str: the formatted setting of VQE """ ret = "\n" ret += "==================== Setting of {} ============================\n".format( self.__class__.__name__) ret += "{}".format(self.setting) ret += "===============================================================\n" if hasattr(self._var_form, 'setting'): ret += "{}".format(self._var_form.setting) elif hasattr(self._var_form, 'print_settings'): ret += "{}".format(self._var_form.print_settings()) elif isinstance(self._var_form, QuantumCircuit): ret += "var_form is a custom circuit" else: ret += "var_form has not been set" ret += "===============================================================\n" ret += "{}".format(self._optimizer.setting) ret += "===============================================================\n" return ret def construct_expectation( self, parameter: Union[List[float], List[Parameter], np.ndarray], operator: OperatorBase, ) -> OperatorBase: r""" Generate the ansatz circuit and expectation value measurement, and return their runnable composition. Args: parameter: Parameters for the ansatz circuit. operator: Qubit operator of the Observable Returns: The Operator equalling the measurement of the ansatz :class:`StateFn` by the Observable's expectation :class:`StateFn`. Raises: AlgorithmError: If no operator has been provided. """ if operator is None: raise AlgorithmError("The operator was never provided.") operator = self._check_operator(operator) if isinstance(self.var_form, QuantumCircuit): param_dict = dict(zip(self._var_form_params, parameter)) # type: Dict wave_function = self.var_form.assign_parameters(param_dict) else: wave_function = self.var_form.construct_circuit(parameter) # Expectation was never created , try to create one if self._expectation is None: self._try_set_expectation_value_from_factory(operator) # If setting the expectation failed, raise an Error: if self._expectation is None: raise AlgorithmError( 'No expectation set and could not automatically set one, please ' 'try explicitly setting an expectation or specify a backend so it ' 'can be chosen automatically.') observable_meas = self.expectation.convert( StateFn(operator, is_measurement=True)) ansatz_circuit_op = CircuitStateFn(wave_function) return observable_meas.compose(ansatz_circuit_op).reduce() def construct_circuit( self, parameter: Union[List[float], List[Parameter], np.ndarray], operator: OperatorBase, ) -> List[QuantumCircuit]: """Return the circuits used to compute the expectation value. Args: parameter: Parameters for the ansatz circuit. operator: Qubit operator of the Observable Returns: A list of the circuits used to compute the expectation value. """ expect_op = self.construct_expectation(parameter, operator).to_circuit_op() circuits = [] # recursively extract circuits def extract_circuits(op): if isinstance(op, CircuitStateFn): circuits.append(op.primitive) elif isinstance(op, ListOp): for op_i in op.oplist: extract_circuits(op_i) extract_circuits(expect_op) return circuits @classmethod def supports_aux_operators(cls) -> bool: return True def _eval_aux_ops(self, aux_operators: List[OperatorBase], threshold: float = 1e-12) -> None: # Create new CircuitSampler to avoid breaking existing one's caches. sampler = CircuitSampler(self.quantum_instance) aux_op_meas = self.expectation.convert( StateFn(ListOp(aux_operators), is_measurement=True)) aux_op_expect = aux_op_meas.compose( CircuitStateFn(self.get_optimal_circuit())) values = np.real(sampler.convert(aux_op_expect).eval()) # Discard values below threshold aux_op_results = (values * (np.abs(values) > threshold)) # Deal with the aux_op behavior where there can be Nones or Zero qubit Paulis in the list _aux_op_nones = [op is None for op in aux_operators] self._ret.aux_operator_eigenvalues = \ [None if is_none else [result] for (is_none, result) in zip(_aux_op_nones, aux_op_results)] # As this has mixed types, since it can included None, it needs to explicitly pass object # data type to avoid numpy 1.19 warning message about implicit conversion being deprecated self._ret.aux_operator_eigenvalues = \ np.array([self._ret.aux_operator_eigenvalues], dtype=object) def _check_operator(self, operator: OperatorBase) -> OperatorBase: """ set operator """ self._expect_op = None self._check_operator_varform(operator) # Expectation was not passed by user, try to create one if not self._user_valid_expectation: self._try_set_expectation_value_from_factory(operator) return operator def compute_minimum_eigenvalue( self, operator: OperatorBase, aux_operators: Optional[List[Optional[OperatorBase]]] = None ) -> MinimumEigensolverResult: super().compute_minimum_eigenvalue(operator, aux_operators) if self.quantum_instance is None: raise AlgorithmError( "A QuantumInstance or Backend " "must be supplied to run the quantum algorithm.") if operator is None: raise AlgorithmError("The operator was never provided.") operator = self._check_operator(operator) # We need to handle the array entries being Optional i.e. having value None if aux_operators: zero_op = I.tensorpower(operator.num_qubits) * 0.0 converted = [] for op in aux_operators: if op is None: converted.append(zero_op) else: converted.append(op) # For some reason Chemistry passes aux_ops with 0 qubits and paulis sometimes. aux_operators = [zero_op if op == 0 else op for op in converted] else: aux_operators = None self._quantum_instance.circuit_summary = True self._eval_count = 0 # Convert the gradient operator into a callable function that is compatible with the # optimization routine. if self._gradient: if isinstance(self._gradient, GradientBase): self._gradient = self._gradient.gradient_wrapper( ~StateFn(operator) @ StateFn(self._var_form), bind_params=self._var_form_params, backend=self._quantum_instance) if not self._expect_op: self._expect_op = self.construct_expectation( self._var_form_params, operator) vqresult = self.find_minimum(initial_point=self.initial_point, var_form=self.var_form, cost_fn=self._energy_evaluation, gradient_fn=self._gradient, optimizer=self.optimizer) self._ret = VQEResult() self._ret.combine(vqresult) if vqresult.optimizer_evals is not None and \ self._eval_count >= vqresult.optimizer_evals: self._eval_count = vqresult.optimizer_evals self._eval_time = vqresult.optimizer_time logger.info( 'Optimization complete in %s seconds.\nFound opt_params %s in %s evals', self._eval_time, vqresult.optimal_point, self._eval_count) self._ret.eigenvalue = vqresult.optimal_value + 0j self._ret.eigenstate = self.get_optimal_vector() self._ret.eigenvalue = self.get_optimal_cost() if aux_operators: self._eval_aux_ops(aux_operators) self._ret.aux_operator_eigenvalues = self._ret.aux_operator_eigenvalues[ 0] self._ret.cost_function_evals = self._eval_count return self._ret def _energy_evaluation( self, parameters: Union[List[float], np.ndarray]) -> Union[float, List[float]]: """Evaluate energy at given parameters for the variational form. This is the objective function to be passed to the optimizer that is used for evaluation. Args: parameters: The parameters for the variational form. Returns: Energy of the hamiltonian of each parameter. Raises: RuntimeError: If the variational form has no parameters. """ num_parameters = self.var_form.num_parameters if self._var_form.num_parameters == 0: raise RuntimeError('The var_form cannot have 0 parameters.') parameter_sets = np.reshape(parameters, (-1, num_parameters)) # Create dict associating each parameter with the lists of parameterization values for it param_bindings = dict( zip(self._var_form_params, parameter_sets.transpose().tolist())) # type: Dict start_time = time() sampled_expect_op = self._circuit_sampler.convert( self._expect_op, params=param_bindings) means = np.real(sampled_expect_op.eval()) if self._callback is not None: variance = np.real( self._expectation.compute_variance(sampled_expect_op)) estimator_error = np.sqrt(variance / self.quantum_instance.run_config.shots) for i, param_set in enumerate(parameter_sets): self._eval_count += 1 self._callback(self._eval_count, param_set, means[i], estimator_error[i]) else: self._eval_count += len(means) end_time = time() logger.info( 'Energy evaluation returned %s - %.5f (ms), eval count: %s', means, (end_time - start_time) * 1000, self._eval_count) return means if len(means) > 1 else means[0] def get_optimal_cost(self) -> float: """Get the minimal cost or energy found by the VQE.""" if self._ret.optimal_point is None: raise AlgorithmError( "Cannot return optimal cost before running the " "algorithm to find optimal params.") return self._ret.optimal_value def get_optimal_circuit(self) -> QuantumCircuit: """Get the circuit with the optimal parameters.""" if self._ret.optimal_point is None: raise AlgorithmError( "Cannot find optimal circuit before running the " "algorithm to find optimal params.") return self.var_form.assign_parameters(self._ret.optimal_parameters) def get_optimal_vector(self) -> Union[List[float], Dict[str, int]]: """Get the simulation outcome of the optimal circuit. """ from qiskit.utils.run_circuits import find_regs_by_name if self._ret.optimal_point is None: raise AlgorithmError( "Cannot find optimal vector before running the " "algorithm to find optimal params.") qc = self.get_optimal_circuit() min_vector = {} if self._quantum_instance.is_statevector: ret = self._quantum_instance.execute(qc) min_vector = ret.get_statevector(qc) else: c = ClassicalRegister(qc.width(), name='c') q = find_regs_by_name(qc, 'q') qc.add_register(c) qc.barrier(q) qc.measure(q, c) ret = self._quantum_instance.execute(qc) counts = ret.get_counts(qc) # normalize, just as done in CircuitSampler.sample_circuits shots = self._quantum_instance._run_config.shots min_vector = {b: (v / shots)**0.5 for (b, v) in counts.items()} return min_vector @property def optimal_params(self) -> List[float]: """The optimal parameters for the variational form.""" if self._ret.optimal_point is None: raise AlgorithmError( "Cannot find optimal params before running the algorithm.") return self._ret.optimal_point
class OpflowQNN(NeuralNetwork): """Opflow Quantum Neural Network.""" def __init__(self, operator: OperatorBase, input_params: Optional[List[Parameter]] = None, weight_params: Optional[List[Parameter]] = None, exp_val: Optional[ExpectationBase] = None, gradient: Optional[Gradient] = None, quantum_instance: Optional[Union[QuantumInstance, BaseBackend, Backend]] = None): """Initializes the Opflow Quantum Neural Network. Args: operator: The parametrized operator that represents the neural network. input_params: The operator parameters that correspond to the input of the network. weight_params: The operator parameters that correspond to the trainable weights. exp_val: The Expected Value converter to be used for the operator. gradient: The Gradient converter to be used for the operator's backward pass. quantum_instance: The quantum instance to evaluate the network. """ self._input_params = list(input_params) or [] self._weight_params = list(weight_params) or [] if isinstance(quantum_instance, (BaseBackend, Backend)): quantum_instance = QuantumInstance(quantum_instance) if quantum_instance: self._quantum_instance = quantum_instance self._circuit_sampler = CircuitSampler( self._quantum_instance, param_qobj=is_aer_provider(self._quantum_instance.backend), caching="all") else: self._quantum_instance = None self._circuit_sampler = None self._operator = operator self._forward_operator = exp_val.convert( operator) if exp_val else operator self._gradient_operator: OperatorBase = None try: gradient = gradient or Gradient() self._gradient_operator = gradient.convert( operator, self._input_params + self._weight_params) except (ValueError, TypeError, OpflowError, QiskitError): logger.warning( 'Cannot compute gradient operator! Continuing without gradients!' ) output_shape = self._get_output_shape_from_op(operator) super().__init__(len(self._input_params), len(self._weight_params), sparse=False, output_shape=output_shape) def _get_output_shape_from_op(self, op: OperatorBase) -> Tuple[int, ...]: """Determines the output shape of a given operator.""" # TODO: should eventually be moved to opflow # this "if" statement is on purpose, to prevent sub-classes. # pylint:disable=unidiomatic-typecheck if type(op) == ListOp: shapes = [] for op_ in op.oplist: shape_ = self._get_output_shape_from_op(op_) shapes += [shape_] if not np.all([shape == shapes[0] for shape in shapes]): raise QiskitMachineLearningError( 'Only supports ListOps with children that return the same shape.' ) if shapes[0] == (1, ): out = op.combo_fn(np.zeros((len(op.oplist)))) else: out = op.combo_fn(np.zeros((len(op.oplist), *shapes[0]))) return out.shape else: return (1, ) @property def operator(self): """ Returns the underlying operator of this QNN.""" return self._operator def _forward( self, input_data: Optional[np.ndarray], weights: Optional[np.ndarray]) -> Union[np.ndarray, SparseArray]: # combine parameter dictionary # take i-th column as values for the i-th param in a batch param_values = { p: input_data[:, i].tolist() for i, p in enumerate(self._input_params) } param_values.update({ p: [weights[i]] * input_data.shape[0] for i, p in enumerate(self._weight_params) }) # evaluate operator if self._circuit_sampler: op = self._circuit_sampler.convert(self._forward_operator, param_values) result = np.real(op.eval()) else: op = self._forward_operator.bind_parameters(param_values) result = np.real(op.eval()) result = np.array(result) return result.reshape(-1, *self.output_shape) def _backward( self, input_data: Optional[np.ndarray], weights: Optional[np.ndarray] ) -> Tuple[Optional[Union[np.ndarray, SparseArray]], Optional[Union[ np.ndarray, SparseArray]]]: # check whether gradient circuit could be constructed if self._gradient_operator is None: return None, None # iterate over rows, each row is an element of a batch batch_size = input_data.shape[0] grad_all = np.zeros((batch_size, *self.output_shape, self.num_inputs + self.num_weights)) for row in range(batch_size): # take i-th column as values for the i-th param in a batch param_values = { p: input_data[row, j] for j, p in enumerate(self._input_params) } param_values.update( {p: weights[j] for j, p in enumerate(self._weight_params)}) # evaluate gradient over all parameters if self._circuit_sampler: grad = self._circuit_sampler.convert(self._gradient_operator, param_values) # TODO: this should not be necessary and is a bug! grad = grad.bind_parameters(param_values) grad = np.real(grad.eval()) else: grad = self._gradient_operator.bind_parameters(param_values) grad = np.real(grad.eval()) grad_all[row, :] = grad.transpose() # split into and return input and weights gradients input_grad = np.array(grad_all[:batch_size, :, :self.num_inputs])\ .reshape(-1, *self.output_shape, self.num_inputs) weights_grad = np.array(grad_all[:batch_size, :, self.num_inputs:])\ .reshape(-1, *self.output_shape, self.num_weights) return input_grad, weights_grad
class PVQD(RealEvolver): """The projected Variational Quantum Dynamics (p-VQD) Algorithm. In each timestep, this algorithm computes the next state with a Trotter formula (specified by the ``evolution`` argument) and projects the timestep onto a variational form (``ansatz``). The projection is determined by maximizing the fidelity of the Trotter-evolved state and the ansatz, using a classical optimization routine. See Ref. [1] for details. The following attributes can be set via the initializer but can also be read and updated once the PVQD object has been constructed. Attributes: ansatz (QuantumCircuit): The parameterized circuit representing the time-evolved state. initial_parameters (np.ndarray): The parameters of the ansatz at time 0. expectation (ExpectationBase): The method to compute expectation values. optimizer (Optional[Union[Optimizer, Minimizer]]): The classical optimization routine used to maximize the fidelity of the Trotter step and ansatz. num_timesteps (Optional[int]): The number of timesteps to take. If None, it is automatically selected to achieve a timestep of approximately 0.01. evolution (Optional[EvolutionSynthesis]): The method to perform the Trotter step. Defaults to first-order Lie-Trotter evolution. use_parameter_shift (bool): If True, use the parameter shift rule for loss function gradients (if the ansatz supports). initial_guess (Optional[np.ndarray]): The starting point for the first classical optimization run, at time 0. Defaults to random values in :math:`[-0.01, 0.01]`. Example: This snippet computes the real time evolution of a quantum Ising model on two neighboring sites and keeps track of the magnetization. .. code-block:: python import numpy as np from qiskit import BasicAer from qiskit.circuit.library import EfficientSU2 from qiskit.opflow import X, Z, I, MatrixExpectation backend = BasicAer.get_backend("statevector_simulator") expectation = MatrixExpectation() hamiltonian = 0.1 * (Z ^ Z) + (I ^ X) + (X ^ I) observable = Z ^ Z ansatz = EfficientSU2(2, reps=1) initial_parameters = np.zeros(ansatz.num_parameters) time = 1 optimizer = L_BFGS_B() # setup the algorithm pvqd = PVQD( ansatz, initial_parameters, num_timesteps=100, optimizer=optimizer, quantum_instance=backend, expectation=expectation ) # specify the evolution problem problem = EvolutionProblem( hamiltonian, time, aux_operators=[hamiltonian, observable] ) # and evolve! result = pvqd.evolve(problem) References: [1] Stefano Barison, Filippo Vicentini, and Giuseppe Carleo (2021), An efficient quantum algorithm for the time evolution of parameterized circuits, `Quantum 5, 512 <https://quantum-journal.org/papers/q-2021-07-28-512/>`_. """ def __init__( self, ansatz: QuantumCircuit, initial_parameters: np.ndarray, expectation: ExpectationBase, optimizer: Optional[Union[Optimizer, Minimizer]] = None, num_timesteps: Optional[int] = None, evolution: Optional[EvolutionSynthesis] = None, use_parameter_shift: bool = True, initial_guess: Optional[np.ndarray] = None, quantum_instance: Optional[Union[Backend, QuantumInstance]] = None, ) -> None: """ Args: ansatz: A parameterized circuit preparing the variational ansatz to model the time evolved quantum state. initial_parameters: The initial parameters for the ansatz. Together with the ansatz, these define the initial state of the time evolution. expectation: The expectation converter to evaluate expectation values. optimizer: The classical optimizers used to minimize the overlap between Trotterization and ansatz. Can be either a :class:`.Optimizer` or a callable using the :class:`.Minimizer` protocol. This argument is optional since it is not required for :meth:`get_loss`, but it has to be set before :meth:`evolve` is called. num_timestep: The number of time steps. If ``None`` it will be set such that the timestep is close to 0.01. evolution: The evolution synthesis to use for the construction of the Trotter step. Defaults to first-order Lie-Trotter decomposition, see also :mod:`~qiskit.synthesis.evolution` for different options. use_parameter_shift: If True, use the parameter shift rule to compute gradients. If False, the optimizer will not be passed a gradient callable. In that case, Qiskit optimizers will use a finite difference rule to approximate the gradients. initial_guess: The initial guess for the first VQE optimization. Afterwards the previous iteration result is used as initial guess. If None, this is set to a random vector with elements in the interval :math:`[-0.01, 0.01]`. quantum_instance: The backend or quantum instance used to evaluate the circuits. """ if evolution is None: evolution = LieTrotter() self.ansatz = ansatz self.initial_parameters = initial_parameters self.num_timesteps = num_timesteps self.optimizer = optimizer self.initial_guess = initial_guess self.expectation = expectation self.evolution = evolution self.use_parameter_shift = use_parameter_shift self._sampler = None self.quantum_instance = quantum_instance @property def quantum_instance(self) -> Optional[QuantumInstance]: """Return the current quantum instance.""" return self._quantum_instance @quantum_instance.setter def quantum_instance(self, quantum_instance: Optional[Union[Backend, QuantumInstance]]) -> None: """Set the quantum instance and circuit sampler.""" if quantum_instance is not None: if not isinstance(quantum_instance, QuantumInstance): quantum_instance = QuantumInstance(quantum_instance) self._sampler = CircuitSampler(quantum_instance) self._quantum_instance = quantum_instance def step( self, hamiltonian: OperatorBase, ansatz: QuantumCircuit, theta: np.ndarray, dt: float, initial_guess: np.ndarray, ) -> Tuple[np.ndarray, float]: """Perform a single time step. Args: hamiltonian: The Hamiltonian under which to evolve. ansatz: The parameterized quantum circuit which attempts to approximate the time-evolved state. theta: The current parameters. dt: The time step. initial_guess: The initial guess for the classical optimization of the fidelity between the next variational state and the Trotter-evolved last state. If None, this is set to a random vector with elements in the interval :math:`[-0.01, 0.01]`. Returns: A tuple consisting of the next parameters and the fidelity of the optimization. """ self._validate_setup() loss, gradient = self.get_loss(hamiltonian, ansatz, dt, theta) if initial_guess is None: initial_guess = np.random.random(self.initial_parameters.size) * 0.01 if isinstance(self.optimizer, Optimizer): optimizer_result = self.optimizer.minimize(loss, initial_guess, gradient) else: optimizer_result = self.optimizer(loss, initial_guess, gradient) # clip the fidelity to [0, 1] fidelity = np.clip(1 - optimizer_result.fun, 0, 1) return theta + optimizer_result.x, fidelity def get_loss( self, hamiltonian: OperatorBase, ansatz: QuantumCircuit, dt: float, current_parameters: np.ndarray, ) -> Tuple[Callable[[np.ndarray], float], Optional[Callable[[np.ndarray], np.ndarray]]]: """Get a function to evaluate the infidelity between Trotter step and ansatz. Args: hamiltonian: The Hamiltonian under which to evolve. ansatz: The parameterized quantum circuit which attempts to approximate the time-evolved state. dt: The time step. current_parameters: The current parameters. Returns: A callable to evaluate the infidelity and, if gradients are supported and required, a second callable to evaluate the gradient of the infidelity. """ self._validate_setup(skip={"optimizer"}) # use Trotterization to evolve the current state trotterized = ansatz.bind_parameters(current_parameters) if isinstance(hamiltonian, MatrixOp): evolution_gate = HamiltonianGate(hamiltonian.primitive, time=dt) else: evolution_gate = PauliEvolutionGate(hamiltonian, time=dt, synthesis=self.evolution) trotterized.append(evolution_gate, ansatz.qubits) # define the overlap of the Trotterized state and the ansatz x = ParameterVector("w", ansatz.num_parameters) shifted = ansatz.assign_parameters(current_parameters + x) overlap = StateFn(trotterized).adjoint() @ StateFn(shifted) converted = self.expectation.convert(overlap) def evaluate_loss( displacement: Union[np.ndarray, List[np.ndarray]] ) -> Union[float, List[float]]: """Evaluate the overlap of the ansatz with the Trotterized evolution. Args: displacement: The parameters for the ansatz. Returns: The fidelity of the ansatz with parameters ``theta`` and the Trotterized evolution. """ if isinstance(displacement, list): displacement = np.asarray(displacement) value_dict = {x_i: displacement[:, i].tolist() for i, x_i in enumerate(x)} else: value_dict = dict(zip(x, displacement)) sampled = self._sampler.convert(converted, params=value_dict) # in principle we could add different loss functions here, but we're currently # not aware of a use-case for a different one than in the paper return 1 - np.abs(sampled.eval()) ** 2 if _is_gradient_supported(ansatz) and self.use_parameter_shift: def evaluate_gradient(displacement: np.ndarray) -> np.ndarray: """Evaluate the gradient with the parameter-shift rule. This is hardcoded here since the gradient framework does not support computing gradients for overlaps. Args: displacement: The parameters for the ansatz. Returns: The gradient. """ # construct lists where each element is shifted by plus (or minus) pi/2 dim = displacement.size plus_shifts = (displacement + np.pi / 2 * np.identity(dim)).tolist() minus_shifts = (displacement - np.pi / 2 * np.identity(dim)).tolist() evaluated = evaluate_loss(plus_shifts + minus_shifts) gradient = (evaluated[:dim] - evaluated[dim:]) / 2 return gradient else: evaluate_gradient = None return evaluate_loss, evaluate_gradient def evolve(self, evolution_problem: EvolutionProblem) -> EvolutionResult: """ Args: evolution_problem: The evolution problem containing the hamiltonian, total evolution time and observables to evaluate. Returns: A result object containing the evolution information and evaluated observables. Raises: ValueError: If the evolution time is not positive or the timestep is too small. NotImplementedError: If the evolution problem contains an initial state. """ self._validate_setup() time = evolution_problem.time observables = evolution_problem.aux_operators hamiltonian = evolution_problem.hamiltonian # determine the number of timesteps and set the timestep num_timesteps = ( int(np.ceil(time / 0.01)) if self.num_timesteps is None else self.num_timesteps ) timestep = time / num_timesteps if evolution_problem.initial_state is not None: raise NotImplementedError( "Setting an initial state for the evolution is not yet supported for PVQD." ) # get the function to evaluate the observables for a given set of ansatz parameters if observables is not None: evaluate_observables = _get_observable_evaluator( self.ansatz, observables, self.expectation, self._sampler ) observable_values = [evaluate_observables(self.initial_parameters)] fidelities = [1] parameters = [self.initial_parameters] times = np.linspace(0, time, num_timesteps + 1).tolist() # +1 to include initial time 0 initial_guess = self.initial_guess for _ in range(num_timesteps): # perform VQE to find the next parameters next_parameters, fidelity = self.step( hamiltonian, self.ansatz, parameters[-1], timestep, initial_guess ) # set initial guess to last parameter update initial_guess = next_parameters - parameters[-1] parameters.append(next_parameters) fidelities.append(fidelity) if observables is not None: observable_values.append(evaluate_observables(next_parameters)) evolved_state = self.ansatz.bind_parameters(parameters[-1]) result = PVQDResult( evolved_state=evolved_state, times=times, parameters=parameters, fidelities=fidelities, estimated_error=1 - np.prod(fidelities), ) if observables is not None: result.observables = observable_values result.aux_ops_evaluated = observable_values[-1] return result def _validate_setup(self, skip=None): """Validate the current setup and raise an error if something misses to run.""" if skip is None: skip = {} required_attributes = {"quantum_instance", "optimizer"}.difference(skip) for attr in required_attributes: if getattr(self, attr, None) is None: raise ValueError(f"The {attr} cannot be None.") if self.num_timesteps is not None and self.num_timesteps <= 0: raise ValueError( f"The number of timesteps must be positive but is {self.num_timesteps}." ) if self.ansatz.num_parameters == 0: raise QiskitError( "The ansatz cannot have 0 parameters, otherwise it cannot be trained." ) if len(self.initial_parameters) != self.ansatz.num_parameters: raise QiskitError( f"Mismatching number of parameters in the ansatz ({self.ansatz.num_parameters}) " f"and the initial parameters ({len(self.initial_parameters)})." )
class TestMatrixExpectation(QiskitOpflowTestCase): """Pauli Change of Basis Expectation tests.""" def setUp(self) -> None: super().setUp() self.seed = 97 backend = BasicAer.get_backend("statevector_simulator") q_instance = QuantumInstance(backend, seed_simulator=self.seed, seed_transpiler=self.seed) self.sampler = CircuitSampler(q_instance, attach_results=True) self.expect = MatrixExpectation() def test_pauli_expect_pair(self): """pauli expect pair test""" op = Z ^ Z # wf = (Pl^Pl) + (Ze^Ze) wf = CX @ (H ^ I) @ Zero converted_meas = self.expect.convert(~StateFn(op) @ wf) self.assertAlmostEqual(converted_meas.eval(), 0, delta=0.1) sampled = self.sampler.convert(converted_meas) self.assertAlmostEqual(sampled.eval(), 0, delta=0.1) def test_pauli_expect_single(self): """pauli expect single test""" paulis = [Z, X, Y, I] states = [Zero, One, Plus, Minus, S @ Plus, S @ Minus] for pauli, state in itertools.product(paulis, states): converted_meas = self.expect.convert(~StateFn(pauli) @ state) matmulmean = state.adjoint().to_matrix() @ pauli.to_matrix( ) @ state.to_matrix() self.assertAlmostEqual(converted_meas.eval(), matmulmean, delta=0.1) sampled = self.sampler.convert(converted_meas) self.assertAlmostEqual(sampled.eval(), matmulmean, delta=0.1) def test_pauli_expect_op_vector(self): """pauli expect op vector test""" paulis_op = ListOp([X, Y, Z, I]) converted_meas = self.expect.convert(~StateFn(paulis_op)) plus_mean = converted_meas @ Plus np.testing.assert_array_almost_equal(plus_mean.eval(), [1, 0, 0, 1], decimal=1) sampled_plus = self.sampler.convert(plus_mean) np.testing.assert_array_almost_equal(sampled_plus.eval(), [1, 0, 0, 1], decimal=1) minus_mean = converted_meas @ Minus np.testing.assert_array_almost_equal(minus_mean.eval(), [-1, 0, 0, 1], decimal=1) sampled_minus = self.sampler.convert(minus_mean) np.testing.assert_array_almost_equal(sampled_minus.eval(), [-1, 0, 0, 1], decimal=1) zero_mean = converted_meas @ Zero np.testing.assert_array_almost_equal(zero_mean.eval(), [0, 0, 1, 1], decimal=1) sampled_zero = self.sampler.convert(zero_mean) np.testing.assert_array_almost_equal(sampled_zero.eval(), [0, 0, 1, 1], decimal=1) sum_zero = (Plus + Minus) * (0.5**0.5) sum_zero_mean = converted_meas @ sum_zero np.testing.assert_array_almost_equal(sum_zero_mean.eval(), [0, 0, 1, 1], decimal=1) sampled_zero = self.sampler.convert(sum_zero) np.testing.assert_array_almost_equal( (converted_meas @ sampled_zero).eval(), [0, 0, 1, 1], decimal=1) for i, op in enumerate(paulis_op.oplist): mat_op = op.to_matrix() np.testing.assert_array_almost_equal( zero_mean.eval()[i], Zero.adjoint().to_matrix() @ mat_op @ Zero.to_matrix(), decimal=1, ) np.testing.assert_array_almost_equal( plus_mean.eval()[i], Plus.adjoint().to_matrix() @ mat_op @ Plus.to_matrix(), decimal=1, ) np.testing.assert_array_almost_equal( minus_mean.eval()[i], Minus.adjoint().to_matrix() @ mat_op @ Minus.to_matrix(), decimal=1, ) def test_pauli_expect_state_vector(self): """pauli expect state vector test""" states_op = ListOp([One, Zero, Plus, Minus]) paulis_op = X converted_meas = self.expect.convert(~StateFn(paulis_op) @ states_op) np.testing.assert_array_almost_equal(converted_meas.eval(), [0, 0, 1, -1], decimal=1) sampled = self.sampler.convert(converted_meas) np.testing.assert_array_almost_equal(sampled.eval(), [0, 0, 1, -1], decimal=1) # Small test to see if execution results are accessible for composed_op in sampled: self.assertIn("statevector", composed_op[1].execution_results) def test_pauli_expect_op_vector_state_vector(self): """pauli expect op vector state vector test""" paulis_op = ListOp([X, Y, Z, I]) states_op = ListOp([One, Zero, Plus, Minus]) valids = [[+0, 0, 1, -1], [+0, 0, 0, 0], [-1, 1, 0, -0], [+1, 1, 1, 1]] converted_meas = self.expect.convert(~StateFn(paulis_op)) np.testing.assert_array_almost_equal( (converted_meas @ states_op).eval(), valids, decimal=1) sampled = self.sampler.convert(states_op) np.testing.assert_array_almost_equal((converted_meas @ sampled).eval(), valids, decimal=1) def test_multi_representation_ops(self): """Test observables with mixed representations""" mixed_ops = ListOp([X.to_matrix_op(), H, H + I, X]) converted_meas = self.expect.convert(~StateFn(mixed_ops)) plus_mean = converted_meas @ Plus sampled_plus = self.sampler.convert(plus_mean) np.testing.assert_array_almost_equal(sampled_plus.eval(), [1, 0.5**0.5, (1 + 0.5**0.5), 1], decimal=1) def test_matrix_expectation_non_hermite_op(self): """Test MatrixExpectation for non hermitian operator""" exp = ~StateFn(1j * Z) @ One self.assertEqual(self.expect.convert(exp).eval(), 1j)
class VQE(VariationalAlgorithm, MinimumEigensolver): r"""The Variational Quantum Eigensolver algorithm. `VQE <https://arxiv.org/abs/1304.3061>`__ is a quantum algorithm that uses a variational technique to find the minimum eigenvalue of the Hamiltonian :math:`H` of a given system. An instance of VQE requires defining two algorithmic sub-components: a trial state (a.k.a. ansatz) which is a :class:`QuantumCircuit`, and one of the classical :mod:`~qiskit.algorithms.optimizers`. The ansatz is varied, via its set of parameters, by the optimizer, such that it works towards a state, as determined by the parameters applied to the ansatz, that will result in the minimum expectation value being measured of the input operator (Hamiltonian). An optional array of parameter values, via the *initial_point*, may be provided as the starting point for the search of the minimum eigenvalue. This feature is particularly useful such as when there are reasons to believe that the solution point is close to a particular point. As an example, when building the dissociation profile of a molecule, it is likely that using the previous computed optimal solution as the starting initial point for the next interatomic distance is going to reduce the number of iterations necessary for the variational algorithm to converge. It provides an `initial point tutorial <https://github.com/Qiskit/qiskit-tutorials-community/blob/master /chemistry/h2_vqe_initial_point.ipynb>`__ detailing this use case. The length of the *initial_point* list value must match the number of the parameters expected by the ansatz being used. If the *initial_point* is left at the default of ``None``, then VQE will look to the ansatz for a preferred value, based on its given initial state. If the ansatz returns ``None``, then a random point will be generated within the parameter bounds set, as per above. If the ansatz provides ``None`` as the lower bound, then VQE will default it to :math:`-2\pi`; similarly, if the ansatz returns ``None`` as the upper bound, the default value will be :math:`2\pi`. """ def __init__( self, ansatz: Optional[QuantumCircuit] = None, optimizer: Optional[Optimizer] = None, initial_point: Optional[np.ndarray] = None, gradient: Optional[Union[GradientBase, Callable]] = None, expectation: Optional[ExpectationBase] = None, include_custom: bool = False, max_evals_grouped: int = 1, callback: Optional[Callable[[int, np.ndarray, float, float], None]] = None, quantum_instance: Optional[Union[QuantumInstance, BaseBackend, Backend]] = None, sort_parameters_by_name: Optional[bool] = None, ) -> None: """ Args: ansatz: A parameterized circuit used as Ansatz for the wave function. optimizer: A classical optimizer. initial_point: An optional initial point (i.e. initial parameter values) for the optimizer. If ``None`` then VQE will look to the ansatz for a preferred point and if not will simply compute a random one. gradient: An optional gradient function or operator for optimizer. expectation: The Expectation converter for taking the average value of the Observable over the ansatz state function. When ``None`` (the default) an :class:`~qiskit.opflow.expectations.ExpectationFactory` is used to select an appropriate expectation based on the operator and backend. When using Aer qasm_simulator backend, with paulis, it is however much faster to leverage custom Aer function for the computation but, although VQE performs much faster with it, the outcome is ideal, with no shot noise, like using a state vector simulator. If you are just looking for the quickest performance when choosing Aer qasm_simulator and the lack of shot noise is not an issue then set `include_custom` parameter here to ``True`` (defaults to ``False``). include_custom: When `expectation` parameter here is None setting this to ``True`` will allow the factory to include the custom Aer pauli expectation. max_evals_grouped: Max number of evaluations performed simultaneously. Signals the given optimizer that more than one set of parameters can be supplied so that potentially the expectation values can be computed in parallel. Typically this is possible when a finite difference gradient is used by the optimizer such that multiple points to compute the gradient can be passed and if computed in parallel improve overall execution time. Deprecated if a gradient operator or function is given. callback: a callback that can access the intermediate data during the optimization. Four parameter values are passed to the callback as follows during each evaluation by the optimizer for its current set of parameters as it works towards the minimum. These are: the evaluation count, the optimizer parameters for the ansatz, the evaluated mean and the evaluated standard deviation.` quantum_instance: Quantum Instance or Backend sort_parameters_by_name: Deprecated. If True, the initial point is bound to the ansatz parameters strictly sorted by name instead of the default circuit order. That means that the ansatz parameters are e.g. sorted as ``x[0] x[1] x[10] x[2] ...`` instead of ``x[0] x[1] x[2] ... x[10]``. Set this to ``True`` to obtain the behavior prior to Qiskit Terra 0.18.0. """ validate_min("max_evals_grouped", max_evals_grouped, 1) if sort_parameters_by_name is not None: warnings.warn( "The ``sort_parameters_by_name`` attribute is deprecated and will be " "removed no sooner than 3 months after the release date of Qiskit Terra " "0.18.0.", DeprecationWarning, stacklevel=2, ) if ansatz is None: ansatz = RealAmplitudes() if optimizer is None: optimizer = SLSQP() if quantum_instance is not None: if not isinstance(quantum_instance, QuantumInstance): quantum_instance = QuantumInstance(quantum_instance) super().__init__() self._max_evals_grouped = max_evals_grouped self._circuit_sampler = None # type: Optional[CircuitSampler] self._expectation = expectation self._include_custom = include_custom # set ansatz -- still supporting pre 0.18.0 sorting self._sort_parameters_by_name = sort_parameters_by_name self._ansatz_params = None self._ansatz = None self.ansatz = ansatz self._optimizer = optimizer self._initial_point = initial_point self._gradient = gradient self._quantum_instance = None if quantum_instance is not None: self.quantum_instance = quantum_instance self._eval_time = None self._eval_count = 0 self._optimizer.set_max_evals_grouped(max_evals_grouped) self._callback = callback logger.info(self.print_settings()) # TODO remove this once the stateful methods are deleted self._ret = None @property def ansatz(self) -> Optional[QuantumCircuit]: """Returns the ansatz.""" return self._ansatz @ansatz.setter def ansatz(self, ansatz: Optional[QuantumCircuit]): """Sets the ansatz. Args: ansatz: The parameterized circuit used as an ansatz. """ self._ansatz = ansatz if ansatz is not None: if self._sort_parameters_by_name: self._ansatz_params = sorted(ansatz.parameters, key=lambda p: p.name) else: self._ansatz_params = list(ansatz.parameters) @property def gradient(self) -> Optional[Union[GradientBase, Callable]]: """Returns the gradient.""" return self._gradient @gradient.setter def gradient(self, gradient: Optional[Union[GradientBase, Callable]]): """Sets the gradient.""" self._gradient = gradient @property def quantum_instance(self) -> Optional[QuantumInstance]: """Returns quantum instance.""" return self._quantum_instance @quantum_instance.setter def quantum_instance( self, quantum_instance: Union[QuantumInstance, BaseBackend, Backend]) -> None: """set quantum_instance""" if not isinstance(quantum_instance, QuantumInstance): quantum_instance = QuantumInstance(quantum_instance) self._quantum_instance = quantum_instance self._circuit_sampler = CircuitSampler(quantum_instance, param_qobj=is_aer_provider( quantum_instance.backend)) @property def initial_point(self) -> Optional[np.ndarray]: """Returns initial point""" return self._initial_point @initial_point.setter def initial_point(self, initial_point: np.ndarray): """Sets initial point""" self._initial_point = initial_point @property def expectation(self) -> Optional[ExpectationBase]: """The expectation value algorithm used to construct the expectation measurement from the observable.""" return self._expectation @expectation.setter def expectation(self, exp: Optional[ExpectationBase]) -> None: self._expectation = exp def _check_operator_ansatz(self, operator: OperatorBase): """Check that the number of qubits of operator and ansatz match.""" if operator is not None and self.ansatz is not None: if operator.num_qubits != self.ansatz.num_qubits: # try to set the number of qubits on the ansatz, if possible try: self.ansatz.num_qubits = operator.num_qubits self._ansatz_params = sorted(self.ansatz.parameters, key=lambda p: p.name) except AttributeError as ex: raise AlgorithmError( "The number of qubits of the ansatz does not match the " "operator, and the ansatz does not allow setting the " "number of qubits using `num_qubits`.") from ex @property def optimizer(self) -> Optional[Optimizer]: """Returns optimizer""" return self._optimizer @optimizer.setter def optimizer(self, optimizer: Optimizer): """Sets optimizer""" self._optimizer = optimizer if optimizer is not None: optimizer.set_max_evals_grouped(self._max_evals_grouped) @property def setting(self): """Prepare the setting of VQE as a string.""" ret = f"Algorithm: {self.__class__.__name__}\n" params = "" for key, value in self.__dict__.items(): if key[0] == "_": if "initial_point" in key and value is None: params += "-- {}: {}\n".format(key[1:], "Random seed") else: params += f"-- {key[1:]}: {value}\n" ret += f"{params}" return ret def print_settings(self): """ Preparing the setting of VQE into a string. Returns: str: the formatted setting of VQE """ ret = "\n" ret += "==================== Setting of {} ============================\n".format( self.__class__.__name__) ret += f"{self.setting}" ret += "===============================================================\n" if self.ansatz is not None: ret += "{}".format(self.ansatz.draw(output="text")) else: ret += "ansatz has not been set" ret += "===============================================================\n" ret += f"{self._optimizer.setting}" ret += "===============================================================\n" return ret def construct_expectation( self, parameter: Union[List[float], List[Parameter], np.ndarray], operator: OperatorBase, return_expectation: bool = False, ) -> Union[OperatorBase, Tuple[OperatorBase, ExpectationBase]]: r""" Generate the ansatz circuit and expectation value measurement, and return their runnable composition. Args: parameter: Parameters for the ansatz circuit. operator: Qubit operator of the Observable return_expectation: If True, return the ``ExpectationBase`` expectation converter used in the construction of the expectation value. Useful e.g. to compute the standard deviation of the expectation value. Returns: The Operator equalling the measurement of the ansatz :class:`StateFn` by the Observable's expectation :class:`StateFn`, and, optionally, the expectation converter. Raises: AlgorithmError: If no operator has been provided. AlgorithmError: If no expectation is passed and None could be inferred via the ExpectationFactory. """ if operator is None: raise AlgorithmError("The operator was never provided.") self._check_operator_ansatz(operator) # if expectation was never created, try to create one if self.expectation is None: expectation = ExpectationFactory.build( operator=operator, backend=self.quantum_instance, include_custom=self._include_custom, ) else: expectation = self.expectation param_dict = dict(zip(self._ansatz_params, parameter)) # type: Dict wave_function = self.ansatz.assign_parameters(param_dict) observable_meas = expectation.convert( StateFn(operator, is_measurement=True)) ansatz_circuit_op = CircuitStateFn(wave_function) expect_op = observable_meas.compose(ansatz_circuit_op).reduce() if return_expectation: return expect_op, expectation return expect_op def construct_circuit( self, parameter: Union[List[float], List[Parameter], np.ndarray], operator: OperatorBase, ) -> List[QuantumCircuit]: """Return the circuits used to compute the expectation value. Args: parameter: Parameters for the ansatz circuit. operator: Qubit operator of the Observable Returns: A list of the circuits used to compute the expectation value. """ expect_op = self.construct_expectation(parameter, operator).to_circuit_op() circuits = [] # recursively extract circuits def extract_circuits(op): if isinstance(op, CircuitStateFn): circuits.append(op.primitive) elif isinstance(op, ListOp): for op_i in op.oplist: extract_circuits(op_i) extract_circuits(expect_op) return circuits @classmethod def supports_aux_operators(cls) -> bool: return True def _eval_aux_ops( self, parameters: np.ndarray, aux_operators: ListOrDict[OperatorBase], expectation: ExpectationBase, threshold: float = 1e-12, ) -> ListOrDict[complex]: # Create new CircuitSampler to avoid breaking existing one's caches. sampler = CircuitSampler(self.quantum_instance) if isinstance(aux_operators, dict): list_op = ListOp(list(aux_operators.values())) else: list_op = ListOp(aux_operators) aux_op_meas = expectation.convert(StateFn(list_op, is_measurement=True)) aux_op_expect = aux_op_meas.compose( CircuitStateFn(self.ansatz.bind_parameters(parameters))) values = np.real(sampler.convert(aux_op_expect).eval()) # Discard values below threshold aux_op_results = values * (np.abs(values) > threshold) # Return None eigenvalues for None operators if aux_operators is a list. # None operators are already dropped in compute_minimum_eigenvalue if aux_operators is a dict. if isinstance(aux_operators, list): aux_operator_eigenvalues = [None] * len(aux_operators) key_value_iterator = enumerate(aux_op_results) else: aux_operator_eigenvalues = {} key_value_iterator = zip(aux_operators.keys(), aux_op_results) for key, value in key_value_iterator: if aux_operators[key] is not None: aux_operator_eigenvalues[key] = value return aux_operator_eigenvalues def compute_minimum_eigenvalue( self, operator: OperatorBase, aux_operators: Optional[ListOrDict[OperatorBase]] = None ) -> MinimumEigensolverResult: super().compute_minimum_eigenvalue(operator, aux_operators) if self.quantum_instance is None: raise AlgorithmError( "A QuantumInstance or Backend must be supplied to run the quantum algorithm." ) self.quantum_instance.circuit_summary = True # this sets the size of the ansatz, so it must be called before the initial point # validation self._check_operator_ansatz(operator) # set an expectation for this algorithm run (will be reset to None at the end) initial_point = _validate_initial_point(self.initial_point, self.ansatz) bounds = _validate_bounds(self.ansatz) # We need to handle the array entries being zero or Optional i.e. having value None if aux_operators: zero_op = I.tensorpower(operator.num_qubits) * 0.0 # Convert the None and zero values when aux_operators is a list. # Drop None and convert zero values when aux_operators is a dict. if isinstance(aux_operators, list): key_op_iterator = enumerate(aux_operators) converted = [zero_op] * len(aux_operators) else: key_op_iterator = aux_operators.items() converted = {} for key, op in key_op_iterator: if op is not None: converted[key] = zero_op if op == 0 else op aux_operators = converted else: aux_operators = None # Convert the gradient operator into a callable function that is compatible with the # optimization routine. if isinstance(self._gradient, GradientBase): gradient = self._gradient.gradient_wrapper( ~StateFn(operator) @ StateFn(self._ansatz), bind_params=self._ansatz_params, backend=self._quantum_instance, ) else: gradient = self._gradient self._eval_count = 0 energy_evaluation, expectation = self.get_energy_evaluation( operator, return_expectation=True) start_time = time() # keep this until Optimizer.optimize is removed try: opt_result = self.optimizer.minimize(fun=energy_evaluation, x0=initial_point, jac=gradient, bounds=bounds) except AttributeError: # self.optimizer is an optimizer with the deprecated interface that uses # ``optimize`` instead of ``minimize``` warnings.warn( "Using an optimizer that is run with the ``optimize`` method is " "deprecated as of Qiskit Terra 0.19.0 and will be unsupported no " "sooner than 3 months after the release date. Instead use an optimizer " "providing ``minimize`` (see qiskit.algorithms.optimizers.Optimizer).", DeprecationWarning, stacklevel=2, ) opt_result = self.optimizer.optimize(len(initial_point), energy_evaluation, gradient, bounds, initial_point) eval_time = time() - start_time result = VQEResult() result.optimal_point = opt_result.x result.optimal_parameters = dict(zip(self._ansatz_params, opt_result.x)) result.optimal_value = opt_result.fun result.cost_function_evals = opt_result.nfev result.optimizer_time = eval_time result.eigenvalue = opt_result.fun + 0j result.eigenstate = self._get_eigenstate(result.optimal_parameters) logger.info( "Optimization complete in %s seconds.\nFound opt_params %s in %s evals", eval_time, result.optimal_point, self._eval_count, ) # TODO delete as soon as get_optimal_vector etc are removed self._ret = result if aux_operators is not None: aux_values = self._eval_aux_ops(opt_result.x, aux_operators, expectation=expectation) result.aux_operator_eigenvalues = aux_values return result def get_energy_evaluation( self, operator: OperatorBase, return_expectation: bool = False, ) -> Callable[[np.ndarray], Union[float, List[float]]]: """Returns a function handle to evaluates the energy at given parameters for the ansatz. This is the objective function to be passed to the optimizer that is used for evaluation. Args: operator: The operator whose energy to evaluate. return_expectation: If True, return the ``ExpectationBase`` expectation converter used in the construction of the expectation value. Useful e.g. to evaluate other operators with the same expectation value converter. Returns: Energy of the hamiltonian of each parameter, and, optionally, the expectation converter. Raises: RuntimeError: If the circuit is not parameterized (i.e. has 0 free parameters). """ num_parameters = self.ansatz.num_parameters if num_parameters == 0: raise RuntimeError( "The ansatz must be parameterized, but has 0 free parameters.") expect_op, expectation = self.construct_expectation( self._ansatz_params, operator, return_expectation=True) def energy_evaluation(parameters): parameter_sets = np.reshape(parameters, (-1, num_parameters)) # Create dict associating each parameter with the lists of parameterization values for it param_bindings = dict( zip(self._ansatz_params, parameter_sets.transpose().tolist())) start_time = time() sampled_expect_op = self._circuit_sampler.convert( expect_op, params=param_bindings) means = np.real(sampled_expect_op.eval()) if self._callback is not None: variance = np.real( expectation.compute_variance(sampled_expect_op)) estimator_error = np.sqrt( variance / self.quantum_instance.run_config.shots) for i, param_set in enumerate(parameter_sets): self._eval_count += 1 self._callback(self._eval_count, param_set, means[i], estimator_error[i]) else: self._eval_count += len(means) end_time = time() logger.info( "Energy evaluation returned %s - %.5f (ms), eval count: %s", means, (end_time - start_time) * 1000, self._eval_count, ) return means if len(means) > 1 else means[0] if return_expectation: return energy_evaluation, expectation return energy_evaluation @deprecate_function(""" The VQE.get_optimal_cost method is deprecated as of Qiskit Terra 0.18.0 and will be removed no sooner than 3 months after the releasedate. This information is part of the returned result object and can be queried as VQEResult.eigenvalue.""") def get_optimal_cost(self) -> float: """Get the minimal cost or energy found by the VQE.""" if self._ret.optimal_point is None: raise AlgorithmError( "Cannot return optimal cost before running the algorithm to find optimal params." ) return self._ret.optimal_value @deprecate_function(""" The VQE.get_optimal_circuit method is deprecated as of Qiskit Terra 0.18.0 and will be removed no sooner than 3 months after the releasedate. This information is part of the returned result object and can be queried as VQEResult.ansatz.bind_parameters(VQEResult.optimal_point).""") def get_optimal_circuit(self) -> QuantumCircuit: """Get the circuit with the optimal parameters.""" if self._ret.optimal_point is None: raise AlgorithmError( "Cannot find optimal circuit before running the " "algorithm to find optimal params.") return self.ansatz.assign_parameters(self._ret.optimal_parameters) @deprecate_function(""" The VQE.get_optimal_vector method is deprecated as of Qiskit Terra 0.18.0 and will be removed no sooner than 3 months after the releasedate. This information is part of the returned result object and can be queried as VQEResult.eigenvector.""") def get_optimal_vector(self) -> Union[List[float], Dict[str, int]]: """Get the simulation outcome of the optimal circuit.""" if self._ret.optimal_parameters is None: raise AlgorithmError( "Cannot find optimal circuit before running the " "algorithm to find optimal vector.") return self._get_eigenstate(self._ret.optimal_parameters) def _get_eigenstate( self, optimal_parameters) -> Union[List[float], Dict[str, int]]: """Get the simulation outcome of the ansatz, provided with parameters.""" optimal_circuit = self.ansatz.bind_parameters(optimal_parameters) state_fn = self._circuit_sampler.convert( StateFn(optimal_circuit)).eval() if self.quantum_instance.is_statevector: state = state_fn.primitive.data # VectorStateFn -> Statevector -> np.array else: state = state_fn.to_dict_fn( ).primitive # SparseVectorStateFn -> DictStateFn -> dict return state @property @deprecate_function(""" The VQE.optimal_params property is deprecated as of Qiskit Terra 0.18.0 and will be removed no sooner than 3 months after the releasedate. This information is part of the returned result object and can be queried as VQEResult.optimal_point.""") def optimal_params(self) -> np.ndarray: """The optimal parameters for the ansatz.""" if self._ret.optimal_point is None: raise AlgorithmError( "Cannot find optimal params before running the algorithm.") return self._ret.optimal_point
class CircuitQNN(SamplingNeuralNetwork): """A Sampling Neural Network based on a given quantum circuit.""" def __init__(self, circuit: QuantumCircuit, input_params: Optional[List[Parameter]] = None, weight_params: Optional[List[Parameter]] = None, sparse: bool = False, sampling: bool = False, interpret: Optional[Callable[[int], Union[int, Tuple[int, ...]]]] = None, output_shape: Union[int, Tuple[int, ...]] = None, gradient: Gradient = None, quantum_instance: Optional[Union[QuantumInstance, BaseBackend, Backend]] = None ) -> None: """Initializes the Circuit Quantum Neural Network. Args: circuit: The (parametrized) quantum circuit that generates the samples of this network. input_params: The parameters of the circuit corresponding to the input. weight_params: The parameters of the circuit corresponding to the trainable weights. sparse: Returns whether the output is sparse or not. sampling: Determines whether the network returns a batch of samples or (possibly sparse) array of probabilities in its forward pass. In case of probabilities, the backward pass returns the probability gradients, while it returns (None, None) in the case of samples. Note that sampling==True will always result in a dense return array independent of the other settings. interpret: A callable that maps the measured integer to another unsigned integer or tuple of unsigned integers. These are used as new indices for the (potentially sparse) output array. If this is used, the output shape of the output needs to be given as a separate argument. output_shape: The output shape of the custom interpretation. The output shape is automatically determined in case of sampling==True. gradient: The gradient converter to be used for the probability gradients. quantum_instance: The quantum instance to evaluate the circuits. Raises: QiskitMachineLearningError: if `interpret` is passed without `output_shape`. """ # copy circuit and add measurements in case non are given self._circuit = circuit.copy() if quantum_instance.is_statevector: if len(self._circuit.clbits) > 0: self._circuit.remove_final_measurements() elif len(self._circuit.clbits) == 0: self._circuit.measure_all() self._input_params = list(input_params or []) self._weight_params = list(weight_params or []) self._interpret = interpret if interpret else lambda x: x sparse_ = sparse output_shape_: Union[int, Tuple[int, ...]] = -1 if sampling: num_samples = quantum_instance.run_config.shots sparse_ = False # infer shape from function ret = self._interpret(0) result = np.array(ret) output_shape_ = (num_samples, *result.shape) if len(result.shape) == 0: output_shape_ = (num_samples, 1) else: if interpret: if output_shape is None: raise QiskitMachineLearningError( 'No output shape given, but required in case of custom interpret!') output_shape_ = output_shape else: output_shape_ = (2**circuit.num_qubits,) self._gradient = gradient if isinstance(quantum_instance, (BaseBackend, Backend)): quantum_instance = QuantumInstance(quantum_instance) self._quantum_instance = quantum_instance self._sampler = CircuitSampler(quantum_instance, param_qobj=False, caching='all') # construct probability gradient opflow object grad_circuit = circuit.copy() grad_circuit.remove_final_measurements() # TODO: ideally this would not be necessary params = list(input_params) + list(weight_params) self._grad_circuit = Gradient().convert(CircuitStateFn(grad_circuit), params) super().__init__(len(self._input_params), len(self._weight_params), sparse_, sampling, output_shape_) @property def circuit(self) -> QuantumCircuit: """Returns the underlying quantum circuit.""" return self._circuit @property def input_params(self) -> List: """Returns the list of input parameters.""" return self._input_params @property def weight_params(self) -> List: """Returns the list of trainable weights parameters.""" return self._weight_params @property def quantum_instance(self) -> QuantumInstance: """Returns the quantum instance to evaluate the circuit.""" return self._quantum_instance @quantum_instance.setter def quantum_instance(self, quantum_instance) -> None: """Sets the quantum instance to evaluate the circuit and make sure circuit has measurements or not depending on the type of backend used. """ self._quantum_instance = quantum_instance # add measurements in case non are given if quantum_instance.is_statevector: if len(self._circuit.clbits) > 0: self._circuit.remove_final_measurements() elif len(self._circuit.clbits) == 0: self._circuit.measure_all() def _sample(self, input_data: np.ndarray, weights: np.ndarray) -> np.ndarray: if self._quantum_instance.is_statevector: raise QiskitMachineLearningError('Sampling does not work with statevector simulator!') # combine parameter dictionary param_values = {p: input_data[i] for i, p in enumerate(self.input_params)} param_values.update({p: weights[i] for i, p in enumerate(self.weight_params)}) # evaluate operator orig_memory = self.quantum_instance.backend_options.get('memory') self.quantum_instance.backend_options['memory'] = True result = self.quantum_instance.execute(self.circuit.bind_parameters(param_values)) self.quantum_instance.backend_options['memory'] = orig_memory # return samples memory = result.get_memory() samples = np.zeros((1, *self.output_shape)) for i, b in enumerate(memory): samples[0, i, :] = self._interpret(int(b, 2)) return samples def _probabilities(self, input_data: np.ndarray, weights: np.ndarray ) -> Union[np.ndarray, SparseArray]: # combine parameter dictionary param_values = {p: input_data[i] for i, p in enumerate(self.input_params)} param_values.update({p: weights[i] for i, p in enumerate(self.weight_params)}) # evaluate operator result = self.quantum_instance.execute( self.circuit.bind_parameters(param_values)) counts = result.get_counts() shots = sum(counts.values()) # initialize probabilities prob: Union[np.ndarray, SparseArray] = None if self.sparse: prob = DOK((1, *self.output_shape)) else: prob = np.zeros((1, *self.output_shape)) # evaluate probabilities for b, v in counts.items(): key = self._interpret(int(b, 2)) if isinstance(key, Integral): key = (cast(int, key),) key = (0, *key) # type: ignore prob[key] += v / shots return prob def _probability_gradients(self, input_data: np.ndarray, weights: np.ndarray ) -> Tuple[Union[np.ndarray, SparseArray], Union[np.ndarray, SparseArray]]: # combine parameter dictionary param_values = {p: input_data[i] for i, p in enumerate(self.input_params)} param_values.update({p: weights[i] for i, p in enumerate(self.weight_params)}) # TODO: additional "bind_parameters" should not be necessary, seems like a bug to be fixed grad = self._sampler.convert(self._grad_circuit, param_values ).bind_parameters(param_values).eval() # TODO: map to dictionary to pretend sparse logic --> needs to be fixed in opflow! input_grad_dicts: List[Dict] = [] if self.num_inputs > 0: input_grad_dicts = [{} for _ in range(self.num_inputs)] for i in range(self.num_inputs): for k in range(2 ** self.circuit.num_qubits): key = self._interpret(k) if not isinstance(key, Integral): # if key is an array-type, cast to hashable tuple key = tuple(cast(Iterable[int], key)) input_grad_dicts[i][key] = (input_grad_dicts[i].get(key, 0.0) + np.real(grad[i][k])) weights_grad_dicts: List[Dict] = [] if self.num_weights > 0: weights_grad_dicts = [{} for _ in range(self.num_weights)] for i in range(self.num_weights): for k in range(2 ** self.circuit.num_qubits): key = self._interpret(k) if not isinstance(key, Integral): # if key is an array-type, cast to hashable tuple key = tuple(cast(Iterable[int], key)) weights_grad_dicts[i][key] = (weights_grad_dicts[i].get(key, 0.0) + np.real(grad[i + self.num_inputs][k])) input_grad: Union[np.ndarray, SparseArray] = None weights_grad: Union[np.ndarray, SparseArray] = None if self._sparse: if self.num_inputs > 0: input_grad = DOK((1, *self.output_shape, self.num_inputs)) else: input_grad = np.zeros((1, *self.output_shape, self.num_inputs)) if self.num_weights > 0: weights_grad = DOK((1, *self.output_shape, self.num_weights)) else: weights_grad = np.zeros((1, *self.output_shape, self.num_weights)) else: input_grad = np.zeros((1, *self.output_shape, self.num_inputs)) weights_grad = np.zeros((1, *self.output_shape, self.num_weights)) for i in range(self.num_inputs): for k, grad in input_grad_dicts[i].items(): key = -1 if isinstance(k, Integral): key = (0, k, i) else: key = (0, *k, i) # type: ignore input_grad[key] = grad for i in range(self.num_weights): for k, grad in weights_grad_dicts[i].items(): key = -1 if isinstance(key, Integral): key = (0, k, i) else: key = (0, *k, i) # type: ignore weights_grad[key] = grad return input_grad, weights_grad
class CircuitQNN(SamplingNeuralNetwork): """A Sampling Neural Network based on a given quantum circuit.""" def __init__(self, circuit: QuantumCircuit, input_params: Optional[List[Parameter]] = None, weight_params: Optional[List[Parameter]] = None, sparse: bool = False, sampling: bool = False, interpret: Optional[Callable[[int], Union[int, Tuple[int, ...]]]] = None, output_shape: Union[int, Tuple[int, ...]] = None, gradient: Gradient = None, quantum_instance: Optional[Union[QuantumInstance, BaseBackend, Backend]] = None ) -> None: """Initializes the Circuit Quantum Neural Network. Args: circuit: The parametrized quantum circuit that generates the samples of this network. input_params: The parameters of the circuit corresponding to the input. weight_params: The parameters of the circuit corresponding to the trainable weights. sparse: Returns whether the output is sparse or not. sampling: Determines whether the network returns a batch of samples or (possibly sparse) array of probabilities in its forward pass. In case of probabilities, the backward pass returns the probability gradients, while it returns (None, None) in the case of samples. Note that sampling==True will always result in a dense return array independent of the other settings. interpret: A callable that maps the measured integer to another unsigned integer or tuple of unsigned integers. These are used as new indices for the (potentially sparse) output array. If this is used, the output shape of the output needs to be given as a separate argument. output_shape: The output shape of the custom interpretation. The output shape is automatically determined in case of sampling==True. gradient: The gradient converter to be used for the probability gradients. quantum_instance: The quantum instance to evaluate the circuits. Raises: QiskitMachineLearningError: if `interpret` is passed without `output_shape`. """ # TODO: need to handle case without a quantum instance if isinstance(quantum_instance, (BaseBackend, Backend)): quantum_instance = QuantumInstance(quantum_instance) self._quantum_instance = quantum_instance self._sampler = CircuitSampler(quantum_instance, param_qobj=False, caching='all') # copy circuit and add measurements in case non are given # TODO: need to be able to handle partial measurements! (partial trace...) self._circuit = circuit.copy() if quantum_instance.is_statevector: if len(self._circuit.clbits) > 0: self._circuit.remove_final_measurements() elif len(self._circuit.clbits) == 0: self._circuit.measure_all() self._input_params = list(input_params or []) self._weight_params = list(weight_params or []) self._interpret = interpret if interpret else lambda x: x sparse_ = False if sampling else sparse output_shape_ = self._compute_output_shape(interpret, output_shape, sampling) # use given gradient or default self._gradient = gradient if gradient else Gradient() # construct probability gradient opflow object self._grad_circuit: QuantumCircuit = None try: grad_circuit = circuit.copy() grad_circuit.remove_final_measurements() # ideally this would not be necessary params = list(input_params) + list(weight_params) self._grad_circuit = self._gradient.convert(StateFn(grad_circuit), params) except (ValueError, TypeError, OpflowError, QiskitError): logger.warning('Cannot compute gradient operator! Continuing without gradients!') super().__init__(len(self._input_params), len(self._weight_params), sparse_, sampling, output_shape_) def _compute_output_shape(self, interpret, output_shape, sampling) -> Tuple[int, ...]: """Validate and compute the output shape.""" # this definition is required by mypy output_shape_: Tuple[int, ...] = (-1,) if sampling: num_samples = self.quantum_instance.run_config.shots ret = self._interpret(0) # infer shape from function result = np.array(ret) if len(result.shape) == 0: output_shape_ = (num_samples, 1) else: output_shape_ = (num_samples, *result.shape) else: if interpret: if output_shape is None: raise QiskitMachineLearningError( 'No output shape given, but required in case of custom interpret!') if isinstance(output_shape, Integral): output_shape = int(output_shape) output_shape_ = (output_shape,) else: output_shape_ = output_shape else: output_shape_ = (2 ** self.circuit.num_qubits,) return output_shape_ @property def circuit(self) -> QuantumCircuit: """Returns the underlying quantum circuit.""" return self._circuit @property def input_params(self) -> List: """Returns the list of input parameters.""" return self._input_params @property def weight_params(self) -> List: """Returns the list of trainable weights parameters.""" return self._weight_params @property def quantum_instance(self) -> QuantumInstance: """Returns the quantum instance to evaluate the circuit.""" return self._quantum_instance @quantum_instance.setter def quantum_instance(self, quantum_instance) -> None: """Sets the quantum instance to evaluate the circuit and make sure circuit has measurements or not depending on the type of backend used. """ self._quantum_instance = quantum_instance # add measurements in case non are given if quantum_instance.is_statevector: if len(self._circuit.clbits) > 0: self._circuit.remove_final_measurements() elif len(self._circuit.clbits) == 0: self._circuit.measure_all() def set_interpret(self, interpret, output_shape=None): """ Change 'interpret' and corresponding 'output_shape'. If self.sampling==True, the output _shape does not have to be set and is inferred from the interpret function. Otherwise, the output_shape needs to be given.""" self._interpret = interpret if interpret else lambda x: x self._output_shape = self._compute_output_shape(interpret, output_shape, self.sampling) def _sample(self, input_data: Optional[np.ndarray], weights: Optional[np.ndarray] ) -> np.ndarray: if self._quantum_instance.is_statevector: raise QiskitMachineLearningError('Sampling does not work with statevector simulator!') # evaluate operator orig_memory = self.quantum_instance.backend_options.get('memory') self.quantum_instance.backend_options['memory'] = True circuits = [] # iterate over rows, each row is an element of a batch rows = input_data.shape[0] for i in range(rows): param_values = {input_param: input_data[i, j] for j, input_param in enumerate(self.input_params)} param_values.update({weight_param: weights[j] for j, weight_param in enumerate(self.weight_params)}) circuits.append(self._circuit.bind_parameters(param_values)) result = self._quantum_instance.execute(circuits) # set the memory setting back self.quantum_instance.backend_options['memory'] = orig_memory # return samples samples = np.zeros((rows, *self.output_shape)) # collect them from all executed circuits for i, circuit in enumerate(circuits): memory = result.get_memory(circuit) for j, b in enumerate(memory): samples[i, j, :] = self._interpret(int(b, 2)) return samples def _probabilities(self, input_data: Optional[np.ndarray], weights: Optional[np.ndarray] ) -> Union[np.ndarray, SparseArray]: # evaluate operator circuits = [] rows = input_data.shape[0] for i in range(rows): param_values = {input_param: input_data[i, j] for j, input_param in enumerate(self.input_params)} param_values.update({weight_param: weights[j] for j, weight_param in enumerate(self.weight_params)}) circuits.append(self._circuit.bind_parameters(param_values)) result = self.quantum_instance.execute(circuits) # initialize probabilities if self.sparse: prob = DOK((rows, *self.output_shape)) else: prob = np.zeros((rows, *self.output_shape)) for i, circuit in enumerate(circuits): counts = result.get_counts(circuit) shots = sum(counts.values()) # evaluate probabilities for b, v in counts.items(): key = self._interpret(int(b, 2)) if isinstance(key, Integral): key = (cast(int, key),) key = (i, *key) # type: ignore prob[key] += v / shots if self.sparse: return prob.to_coo() else: return prob def _probability_gradients(self, input_data: Optional[np.ndarray], weights: Optional[np.ndarray] ) -> Tuple[Union[np.ndarray, SparseArray], Union[np.ndarray, SparseArray]]: # check whether gradient circuit could be constructed if self._grad_circuit is None: return None, None rows = input_data.shape[0] # initialize empty gradients if self._sparse: input_grad = DOK((rows, *self.output_shape, self.num_inputs)) weights_grad = DOK((rows, *self.output_shape, self.num_weights)) else: input_grad = np.zeros((rows, *self.output_shape, self.num_inputs)) weights_grad = np.zeros((rows, *self.output_shape, self.num_weights)) for row in range(rows): param_values = {input_param: input_data[row, j] for j, input_param in enumerate(self.input_params)} param_values.update({weight_param: weights[j] for j, weight_param in enumerate(self.weight_params)}) # TODO: additional "bind_parameters" should not be necessary, # seems like a bug to be fixed grad = self._sampler.convert(self._grad_circuit, param_values ).bind_parameters(param_values).eval() # construct gradients for i in range(self.num_inputs + self.num_weights): coo_grad = coo_matrix(grad[i]) # this works for sparse and dense case # get index for input or weights gradients j = i if i < self.num_inputs else i - self.num_inputs for _, k, val in zip(coo_grad.row, coo_grad.col, coo_grad.data): # interpret integer and construct key key = self._interpret(k) if isinstance(key, Integral): key = (row, int(key), j) else: # if key is an array-type, cast to hashable tuple key = tuple(cast(Iterable[int], key)) key = (row, *key, j) # type: ignore # store value for inputs or weights gradients if i < self.num_inputs: input_grad[key] += np.real(val) else: weights_grad[key] += np.real(val) if self.sparse: return input_grad.to_coo(), weights_grad.to_coo() else: return input_grad, weights_grad
class VQD(VariationalAlgorithm, Eigensolver): r"""The Variational Quantum Deflation algorithm. `VQD <https://arxiv.org/abs/1805.08138>`__ is a quantum algorithm that uses a variational technique to find the k eigenvalues of the Hamiltonian :math:`H` of a given system. The algorithm computes excited state energies of generalised hamiltonians by optimising over a modified cost function where each succesive eigen value is calculated iteratively by introducing an overlap term with all the previously computed eigenstaes that must be minimised, thus ensuring higher energy eigen states are found. An instance of VQD requires defining three algorithmic sub-components: an integer k denoting the number of eigenstates to calculate, a trial state (a.k.a. ansatz)which is a :class:`QuantumCircuit`, and one of the classical :mod:`~qiskit.algorithms.optimizers`. The ansatz is varied, via its set of parameters, by the optimizer, such that it works towards a state, as determined by the parameters applied to the ansatz, that will result in the minimum expectation values being measured of the input operator (Hamiltonian). The algorithm does this by iteratively refining each excited state to be orthogonal to all the previous excited states. An optional array of parameter values, via the *initial_point*, may be provided as the starting point for the search of the minimum eigenvalue. This feature is particularly useful such as when there are reasons to believe that the solution point is close to a particular point. The length of the *initial_point* list value must match the number of the parameters expected by the ansatz being used. If the *initial_point* is left at the default of ``None``, then VQD will look to the ansatz for a preferred value, based on its given initial state. If the ansatz returns ``None``, then a random point will be generated within the parameter bounds set, as per above. If the ansatz provides ``None`` as the lower bound, then VQD will default it to :math:`-2\pi`; similarly, if the ansatz returns ``None`` as the upper bound, the default value will be :math:`2\pi`. """ def __init__( self, ansatz: Optional[QuantumCircuit] = None, k: int = 2, betas: Optional[List[float]] = None, optimizer: Optional[Union[Optimizer, Minimizer]] = None, initial_point: Optional[np.ndarray] = None, gradient: Optional[Union[GradientBase, Callable]] = None, expectation: Optional[ExpectationBase] = None, include_custom: bool = False, max_evals_grouped: int = 1, callback: Optional[Callable[[int, np.ndarray, float, float], None]] = None, quantum_instance: Optional[Union[QuantumInstance, Backend]] = None, ) -> None: """ Args: ansatz: A parameterized circuit used as ansatz for the wave function. k: the number of eigenvalues to return. Returns the lowest k eigenvalues. betas: beta parameter in the VQD paper. Should have size k -1, the number of excited states. It is a hyperparameter that balances the contribution of the overlap term to the cost function and has a default value computed as mean square sum of coefficients of observable. optimizer: A classical optimizer. Can either be a Qiskit optimizer or a callable that takes an array as input and returns a Qiskit or SciPy optimization result. initial_point: An optional initial point (i.e. initial parameter values) for the optimizer. If ``None`` then VQD will look to the ansatz for a preferred point and if not will simply compute a random one. gradient: An optional gradient function or operator for optimizer. Only used to compute the ground state at the moment. expectation: The Expectation converter for taking the average value of the Observable over the ansatz state function. When ``None`` (the default) an :class:`~qiskit.opflow.expectations.ExpectationFactory` is used to select an appropriate expectation based on the operator and backend. When using Aer qasm_simulator backend, with paulis, it is however much faster to leverage custom Aer function for the computation but, although VQD performs much faster with it, the outcome is ideal, with no shot noise, like using a state vector simulator. If you are just looking for the quickest performance when choosing Aer qasm_simulator and the lack of shot noise is not an issue then set `include_custom` parameter here to ``True`` (defaults to ``False``). include_custom: When `expectation` parameter here is None setting this to ``True`` will allow the factory to include the custom Aer pauli expectation. max_evals_grouped: Max number of evaluations performed simultaneously. Signals the given optimizer that more than one set of parameters can be supplied so that multiple points to compute the gradient can be passed and if computed in parallel potentially the expectation values can be computed in parallel. Typically this is possible when a finite difference gradient is used by the optimizer such that improve overall execution time. Deprecated if a gradient operator or function is given. callback: a callback that can access the intermediate data during the optimization. Four parameter values are passed to the callback as follows during each evaluation by the optimizer for its current set of parameters as it works towards the minimum. These are: the evaluation count, the optimizer parameters for the ansatz, the evaluated mean and the evaluated standard deviation.` quantum_instance: Quantum Instance or Backend """ validate_min("max_evals_grouped", max_evals_grouped, 1) super().__init__() self._max_evals_grouped = max_evals_grouped self._circuit_sampler = None # type: Optional[CircuitSampler] self._expectation = None self.expectation = expectation self._include_custom = include_custom # set ansatz -- still supporting pre 0.18.0 sorting self._ansatz = None self.ansatz = ansatz self.k = k self.betas = betas self._optimizer = None self.optimizer = optimizer self._initial_point = None self.initial_point = initial_point self._gradient = None self.gradient = gradient self._quantum_instance = None if quantum_instance is not None: self.quantum_instance = quantum_instance self._eval_time = None self._eval_count = 0 self._callback = None self.callback = callback logger.info(self.print_settings()) @property def ansatz(self) -> QuantumCircuit: """Returns the ansatz.""" return self._ansatz @ansatz.setter def ansatz(self, ansatz: Optional[QuantumCircuit]): """Sets the ansatz. Args: ansatz: The parameterized circuit used as an ansatz. If None is passed, RealAmplitudes is used by default. """ if ansatz is None: ansatz = RealAmplitudes() self._ansatz = ansatz @property def gradient(self) -> Optional[Union[GradientBase, Callable]]: """Returns the gradient.""" return self._gradient @gradient.setter def gradient(self, gradient: Optional[Union[GradientBase, Callable]]): """Sets the gradient.""" self._gradient = gradient @property def quantum_instance(self) -> Optional[QuantumInstance]: """Returns quantum instance.""" return self._quantum_instance @quantum_instance.setter def quantum_instance( self, quantum_instance: Union[QuantumInstance, Backend]) -> None: """Sets a quantum_instance.""" if not isinstance(quantum_instance, QuantumInstance): quantum_instance = QuantumInstance(quantum_instance) self._quantum_instance = quantum_instance self._circuit_sampler = CircuitSampler(quantum_instance, param_qobj=is_aer_provider( quantum_instance.backend)) @property def initial_point(self) -> Optional[np.ndarray]: """Returns initial point.""" return self._initial_point @initial_point.setter def initial_point(self, initial_point: np.ndarray): """Sets initial point""" self._initial_point = initial_point @property def max_evals_grouped(self) -> int: """Returns max_evals_grouped""" return self._max_evals_grouped @max_evals_grouped.setter def max_evals_grouped(self, max_evals_grouped: int): """Sets max_evals_grouped""" self._max_evals_grouped = max_evals_grouped self.optimizer.set_max_evals_grouped(max_evals_grouped) @property def include_custom(self) -> bool: """Returns include_custom""" return self._include_custom @include_custom.setter def include_custom(self, include_custom: bool): """Sets include_custom. If set to another value than the one that was previsously set, the expectation attribute is reset to None. """ if include_custom != self._include_custom: self._include_custom = include_custom self.expectation = None @property def callback( self) -> Optional[Callable[[int, np.ndarray, float, float], None]]: """Returns callback""" return self._callback @callback.setter def callback(self, callback: Optional[Callable[[int, np.ndarray, float, float], None]]): """Sets callback""" self._callback = callback @property def expectation(self) -> Optional[ExpectationBase]: """The expectation value algorithm used to construct the expectation measurement from the observable.""" return self._expectation @expectation.setter def expectation(self, exp: Optional[ExpectationBase]) -> None: self._expectation = exp def _check_operator_ansatz(self, operator: OperatorBase): """Check that the number of qubits of operator and ansatz match.""" if operator is not None and self.ansatz is not None: if operator.num_qubits != self.ansatz.num_qubits: # try to set the number of qubits on the ansatz, if possible try: self.ansatz.num_qubits = operator.num_qubits except AttributeError as ex: raise AlgorithmError( "The number of qubits of the ansatz does not match the " "operator, and the ansatz does not allow setting the " "number of qubits using `num_qubits`.") from ex @property def optimizer(self) -> Optimizer: """Returns optimizer""" return self._optimizer @optimizer.setter def optimizer(self, optimizer: Optional[Optimizer]): """Sets the optimizer attribute. Args: optimizer: The optimizer to be used. If None is passed, SLSQP is used by default. """ if optimizer is None: optimizer = SLSQP() if isinstance(optimizer, Optimizer): optimizer.set_max_evals_grouped(self.max_evals_grouped) self._optimizer = optimizer @property def setting(self): """Prepare the setting of VQD as a string.""" ret = f"Algorithm: {self.__class__.__name__}\n" params = "" for key, value in self.__dict__.items(): if key[0] == "_": if "initial_point" in key and value is None: params += "-- {}: {}\n".format(key[1:], "Random seed") else: params += f"-- {key[1:]}: {value}\n" ret += f"{params}" return ret def print_settings(self): """Preparing the setting of VQD into a string. Returns: str: the formatted setting of VQD. """ ret = "\n" ret += "==================== Setting of {} ============================\n".format( self.__class__.__name__) ret += f"{self.setting}" ret += "===============================================================\n" if self.ansatz is not None: ret += "{}".format(self.ansatz.draw(output="text")) else: ret += "ansatz has not been set" ret += "===============================================================\n" ret += f"{self._optimizer.setting}" ret += "===============================================================\n" return ret def construct_expectation( self, parameter: Union[List[float], List[Parameter], np.ndarray], operator: OperatorBase, return_expectation: bool = False, ) -> Union[OperatorBase, Tuple[OperatorBase, ExpectationBase]]: r""" Generate the ansatz circuit and expectation value measurement, and return their runnable composition. Args: parameter: Parameters for the ansatz circuit. operator: Qubit operator of the Observable return_expectation: If True, return the ``ExpectationBase`` expectation converter used in the construction of the expectation value. Useful e.g. to compute the standard deviation of the expectation value. Returns: The Operator equalling the measurement of the ansatz :class:`StateFn` by the Observable's expectation :class:`StateFn`, and, optionally, the expectation converter. Raises: AlgorithmError: If no operator has been provided. AlgorithmError: If no expectation is passed and None could be inferred via the ExpectationFactory. """ if operator is None: raise AlgorithmError("The operator was never provided.") self._check_operator_ansatz(operator) # if expectation was never created, try to create one if self.expectation is None: expectation = ExpectationFactory.build( operator=operator, backend=self.quantum_instance, include_custom=self._include_custom, ) else: expectation = self.expectation wave_function = self.ansatz.assign_parameters(parameter) observable_meas = expectation.convert( StateFn(operator, is_measurement=True)) ansatz_circuit_op = CircuitStateFn(wave_function) expect_op = observable_meas.compose(ansatz_circuit_op).reduce() if return_expectation: return expect_op, expectation return expect_op def construct_circuit( self, parameter: Union[List[float], List[Parameter], np.ndarray], operator: OperatorBase, ) -> List[QuantumCircuit]: """Return the circuits used to compute the expectation value. Args: parameter: Parameters for the ansatz circuit. operator: Qubit operator of the Observable Returns: A list of the circuits used to compute the expectation value. """ expect_op = self.construct_expectation(parameter, operator).to_circuit_op() circuits = [] # recursively extract circuits def extract_circuits(op): if isinstance(op, CircuitStateFn): circuits.append(op.primitive) elif isinstance(op, ListOp): for op_i in op.oplist: extract_circuits(op_i) extract_circuits(expect_op) return circuits @classmethod def supports_aux_operators(cls) -> bool: return True def _eval_aux_ops( self, parameters: np.ndarray, aux_operators: ListOrDict[OperatorBase], expectation: ExpectationBase, threshold: float = 1e-12, ) -> ListOrDict[Tuple[complex, complex]]: # Create new CircuitSampler to avoid breaking existing one's caches. sampler = CircuitSampler(self.quantum_instance) if isinstance(aux_operators, dict): list_op = ListOp(list(aux_operators.values())) else: list_op = ListOp(aux_operators) aux_op_meas = expectation.convert(StateFn(list_op, is_measurement=True)) aux_op_expect = aux_op_meas.compose( CircuitStateFn(self.ansatz.bind_parameters(parameters))) aux_op_expect_sampled = sampler.convert(aux_op_expect) # compute means values = np.real(aux_op_expect_sampled.eval()) # compute standard deviations variances = np.real( expectation.compute_variance(aux_op_expect_sampled)) if not isinstance(variances, np.ndarray) and variances == 0.0: # when `variances` is a single value equal to 0., our expectation value is exact and we # manually ensure the variances to be a list of the correct length variances = np.zeros(len(aux_operators), dtype=float) std_devs = np.sqrt(variances / self.quantum_instance.run_config.shots) # Discard values below threshold aux_op_means = values * (np.abs(values) > threshold) # zip means and standard deviations into tuples aux_op_results = zip(aux_op_means, std_devs) # Return None eigenvalues for None operators if aux_operators is a list. # None operators are already dropped in compute_minimum_eigenvalue if aux_operators is a dict. if isinstance(aux_operators, list): aux_operator_eigenvalues = [None] * len(aux_operators) key_value_iterator = enumerate(aux_op_results) else: aux_operator_eigenvalues = {} key_value_iterator = zip(aux_operators.keys(), aux_op_results) for key, value in key_value_iterator: if aux_operators[key] is not None: aux_operator_eigenvalues[key] = value return aux_operator_eigenvalues def compute_eigenvalues( self, operator: OperatorBase, aux_operators: Optional[ListOrDict[OperatorBase]] = None ) -> EigensolverResult: super().compute_eigenvalues(operator, aux_operators) if self.quantum_instance is None: raise AlgorithmError( "A QuantumInstance or Backend must be supplied to run the quantum algorithm." ) self.quantum_instance.circuit_summary = True # this sets the size of the ansatz, so it must be called before the initial point # validation self._check_operator_ansatz(operator) # set an expectation for this algorithm run (will be reset to None at the end) initial_point = _validate_initial_point(self.initial_point, self.ansatz) bounds = _validate_bounds(self.ansatz) # We need to handle the array entries being zero or Optional i.e. having value None if aux_operators: zero_op = PauliSumOp.from_list([("I" * self.ansatz.num_qubits, 0)]) # Convert the None and zero values when aux_operators is a list. # Drop None and convert zero values when aux_operators is a dict. if isinstance(aux_operators, list): key_op_iterator = enumerate(aux_operators) converted = [zero_op] * len(aux_operators) else: key_op_iterator = aux_operators.items() converted = {} for key, op in key_op_iterator: if op is not None: converted[key] = zero_op if op == 0 else op aux_operators = converted else: aux_operators = None if self.betas is None: upper_bound = (abs(operator.coeff) if isinstance( operator, PauliOp) else abs(operator.coeff) * sum(abs(operation.coeff) for operation in operator)) self.betas = [upper_bound * 10] * (self.k) logger.info("beta autoevaluated to %s", self.betas[0]) result = VQDResult() result.optimal_point = [] result.optimal_parameters = [] result.optimal_value = [] result.cost_function_evals = [] result.optimizer_time = [] result.eigenvalues = [] result.eigenstates = [] if aux_operators is not None: aux_values = [] for step in range(1, self.k + 1): self._eval_count = 0 energy_evaluation, expectation = self.get_energy_evaluation( step, operator, return_expectation=True, prev_states=result.optimal_parameters) # Convert the gradient operator into a callable function that is compatible with the # optimization routine. Only used for the ground state currently as Gradient() doesnt # support SumOps yet if isinstance(self._gradient, GradientBase): gradient = self._gradient.gradient_wrapper( StateFn(operator, is_measurement=True) @ StateFn( self.ansatz), bind_params=list(self.ansatz.parameters), backend=self._quantum_instance, ) else: gradient = self._gradient start_time = time() if callable(self.optimizer): opt_result = self.optimizer( # pylint: disable=not-callable fun=energy_evaluation, x0=initial_point, jac=gradient, bounds=bounds) else: opt_result = self.optimizer.minimize(fun=energy_evaluation, x0=initial_point, jac=gradient, bounds=bounds) eval_time = time() - start_time result.optimal_point.append(opt_result.x) result.optimal_parameters.append( dict(zip(self.ansatz.parameters, opt_result.x))) result.optimal_value.append(opt_result.fun) result.cost_function_evals.append(opt_result.nfev) result.optimizer_time.append(eval_time) eigenvalue = (StateFn(operator, is_measurement=True).compose( CircuitStateFn( self.ansatz.bind_parameters( result.optimal_parameters[-1]))).reduce().eval()) result.eigenvalues.append(eigenvalue) result.eigenstates.append( self._get_eigenstate(result.optimal_parameters[-1])) if aux_operators is not None: bound_ansatz = self.ansatz.bind_parameters( result.optimal_point[-1]) aux_value = eval_observables(self.quantum_instance, bound_ansatz, aux_operators, expectation=expectation) aux_values.append(aux_value) if step == 1: logger.info( "Ground state optimization complete in %s seconds.\nFound opt_params %s in %s evals", eval_time, result.optimal_point, self._eval_count, ) else: logger.info( ("%s excited state optimization complete in %s s.\nFound opt_parms %s in %s evals" ), str(step - 1), eval_time, result.optimal_point, self._eval_count, ) # To match the siignature of NumpyEigenSolver Result result.eigenstates = ListOp( [StateFn(vec) for vec in result.eigenstates]) result.eigenvalues = np.array(result.eigenvalues) result.optimal_point = np.array(result.optimal_point) result.optimal_value = np.array(result.optimal_value) result.cost_function_evals = np.array(result.cost_function_evals) result.optimizer_time = np.array(result.optimizer_time) if aux_operators is not None: result.aux_operator_eigenvalues = aux_values return result def get_energy_evaluation( self, step: int, operator: OperatorBase, return_expectation: bool = False, prev_states: Optional[List[np.ndarray]] = None, ) -> Callable[[np.ndarray], Union[float, List[float]]]: """Returns a function handle to evaluates the energy at given parameters for the ansatz. This return value is the objective function to be passed to the optimizer for evaluation. Args: step: level of enegy being calculated. 0 for ground, 1 for first excited state and so on. operator: The operator whose energy to evaluate. return_expectation: If True, return the ``ExpectationBase`` expectation converter used in the construction of the expectation value. Useful e.g. to evaluate other operators with the same expectation value converter. prev_states: List of parameters from previous rounds of optimization. Returns: A callable that computes and returns the energy of the hamiltonian of each parameter, and, optionally, the expectation Raises: RuntimeError: If the circuit is not parameterized (i.e. has 0 free parameters). AlgorithmError: If operator was not provided. """ num_parameters = self.ansatz.num_parameters if num_parameters == 0: raise RuntimeError( "The ansatz must be parameterized, but has 0 free parameters.") if operator is None: raise AlgorithmError("The operator was never provided.") if step > 1 and (len(prev_states) + 1) != step: raise RuntimeError( f"Passed previous states of the wrong size." f"Passed array has length {str(len(prev_states))}") self._check_operator_ansatz(operator) overlap_op = [] ansatz_params = self.ansatz.parameters expect_op, expectation = self.construct_expectation( ansatz_params, operator, return_expectation=True) for state in range(step - 1): prev_circ = self.ansatz.bind_parameters(prev_states[state]) overlap_op.append( ~CircuitStateFn(prev_circ) @ CircuitStateFn(self.ansatz)) def energy_evaluation(parameters): parameter_sets = np.reshape(parameters, (-1, num_parameters)) # Create dict associating each parameter with the lists of parameterization values for it param_bindings = dict( zip(ansatz_params, parameter_sets.transpose().tolist())) sampled_expect_op = self._circuit_sampler.convert( expect_op, params=param_bindings) mean = np.real(sampled_expect_op.eval()) for state in range(step - 1): sampled_final_op = self._circuit_sampler.convert( overlap_op[state], params=param_bindings) cost = sampled_final_op.eval() mean += np.real(self.betas[state] * np.conj(cost) * cost) self._eval_count += len(mean) return mean if len(mean) > 1 else mean[0] if return_expectation: return energy_evaluation, expectation return energy_evaluation def _get_eigenstate( self, optimal_parameters) -> Union[List[float], Dict[str, int]]: """Get the simulation outcome of the ansatz, provided with parameters.""" optimal_circuit = self.ansatz.bind_parameters(optimal_parameters) state_fn = self._circuit_sampler.convert( StateFn(optimal_circuit)).eval() if self.quantum_instance.is_statevector: state = state_fn.primitive.data # VectorStateFn -> Statevector -> np.array else: state = state_fn.to_dict_fn( ).primitive # SparseVectorStateFn -> DictStateFn -> dict return state
class TestPauliExpectation(QiskitOpflowTestCase): """Pauli Change of Basis Expectation tests.""" def setUp(self) -> None: super().setUp() self.seed = 97 backend = BasicAer.get_backend("qasm_simulator") q_instance = QuantumInstance(backend, seed_simulator=self.seed, seed_transpiler=self.seed) self.sampler = CircuitSampler(q_instance, attach_results=True) self.expect = PauliExpectation() def test_pauli_expect_pair(self): """pauli expect pair test""" op = Z ^ Z # wf = (Pl^Pl) + (Ze^Ze) wf = CX @ (H ^ I) @ Zero converted_meas = self.expect.convert(~StateFn(op) @ wf) self.assertAlmostEqual(converted_meas.eval(), 0, delta=0.1) sampled = self.sampler.convert(converted_meas) self.assertAlmostEqual(sampled.eval(), 0, delta=0.1) def test_pauli_expect_single(self): """pauli expect single test""" paulis = [Z, X, Y, I] states = [Zero, One, Plus, Minus, S @ Plus, S @ Minus] for pauli, state in itertools.product(paulis, states): converted_meas = self.expect.convert(~StateFn(pauli) @ state) matmulmean = state.adjoint().to_matrix() @ pauli.to_matrix( ) @ state.to_matrix() self.assertAlmostEqual(converted_meas.eval(), matmulmean, delta=0.1) sampled = self.sampler.convert(converted_meas) self.assertAlmostEqual(sampled.eval(), matmulmean, delta=0.1) def test_pauli_expect_op_vector(self): """pauli expect op vector test""" paulis_op = ListOp([X, Y, Z, I]) converted_meas = self.expect.convert(~StateFn(paulis_op)) plus_mean = converted_meas @ Plus np.testing.assert_array_almost_equal(plus_mean.eval(), [1, 0, 0, 1], decimal=1) sampled_plus = self.sampler.convert(plus_mean) np.testing.assert_array_almost_equal(sampled_plus.eval(), [1, 0, 0, 1], decimal=1) minus_mean = converted_meas @ Minus np.testing.assert_array_almost_equal(minus_mean.eval(), [-1, 0, 0, 1], decimal=1) sampled_minus = self.sampler.convert(minus_mean) np.testing.assert_array_almost_equal(sampled_minus.eval(), [-1, 0, 0, 1], decimal=1) zero_mean = converted_meas @ Zero np.testing.assert_array_almost_equal(zero_mean.eval(), [0, 0, 1, 1], decimal=1) sampled_zero = self.sampler.convert(zero_mean) np.testing.assert_array_almost_equal(sampled_zero.eval(), [0, 0, 1, 1], decimal=1) sum_zero = (Plus + Minus) * (0.5**0.5) sum_zero_mean = converted_meas @ sum_zero np.testing.assert_array_almost_equal(sum_zero_mean.eval(), [0, 0, 1, 1], decimal=1) sampled_zero_mean = self.sampler.convert(sum_zero_mean) # !!NOTE!!: Depolarizing channel (Sampling) means interference # does not happen between circuits in sum, so expectation does # not equal expectation for Zero!! np.testing.assert_array_almost_equal(sampled_zero_mean.eval(), [0, 0, 0, 1], decimal=1) for i, op in enumerate(paulis_op.oplist): mat_op = op.to_matrix() np.testing.assert_array_almost_equal( zero_mean.eval()[i], Zero.adjoint().to_matrix() @ mat_op @ Zero.to_matrix(), decimal=1, ) np.testing.assert_array_almost_equal( plus_mean.eval()[i], Plus.adjoint().to_matrix() @ mat_op @ Plus.to_matrix(), decimal=1, ) np.testing.assert_array_almost_equal( minus_mean.eval()[i], Minus.adjoint().to_matrix() @ mat_op @ Minus.to_matrix(), decimal=1, ) def test_pauli_expect_state_vector(self): """pauli expect state vector test""" states_op = ListOp([One, Zero, Plus, Minus]) paulis_op = X converted_meas = self.expect.convert(~StateFn(paulis_op) @ states_op) np.testing.assert_array_almost_equal(converted_meas.eval(), [0, 0, 1, -1], decimal=1) sampled = self.sampler.convert(converted_meas) np.testing.assert_array_almost_equal(sampled.eval(), [0, 0, 1, -1], decimal=1) # Small test to see if execution results are accessible for composed_op in sampled: self.assertIn("counts", composed_op[1].execution_results) def test_pauli_expect_op_vector_state_vector(self): """pauli expect op vector state vector test""" paulis_op = ListOp([X, Y, Z, I]) states_op = ListOp([One, Zero, Plus, Minus]) valids = [[+0, 0, 1, -1], [+0, 0, 0, 0], [-1, 1, 0, -0], [+1, 1, 1, 1]] converted_meas = self.expect.convert(~StateFn(paulis_op) @ states_op) np.testing.assert_array_almost_equal(converted_meas.eval(), valids, decimal=1) sampled = self.sampler.convert(converted_meas) np.testing.assert_array_almost_equal(sampled.eval(), valids, decimal=1) def test_to_matrix_called(self): """test to matrix called in different situations""" qs = 45 states_op = ListOp([Zero ^ qs, One ^ qs, (Zero ^ qs) + (One ^ qs)]) paulis_op = ListOp([Z ^ qs, (I ^ Z ^ I) ^ int(qs / 3)]) # 45 qubit calculation - throws exception if to_matrix is called # massive is False with self.assertRaises(ValueError): states_op.to_matrix() paulis_op.to_matrix() # now set global variable or argument try: algorithm_globals.massive = True with self.assertRaises(MemoryError): states_op.to_matrix() paulis_op.to_matrix() algorithm_globals.massive = False with self.assertRaises(MemoryError): states_op.to_matrix(massive=True) paulis_op.to_matrix(massive=True) finally: algorithm_globals.massive = False def test_not_to_matrix_called(self): """45 qubit calculation - literally will not work if to_matrix is somehow called (in addition to massive=False throwing an error)""" qs = 45 states_op = ListOp([Zero ^ qs, One ^ qs, (Zero ^ qs) + (One ^ qs)]) paulis_op = ListOp([Z ^ qs, (I ^ Z ^ I) ^ int(qs / 3)]) converted_meas = self.expect.convert(~StateFn(paulis_op) @ states_op) np.testing.assert_array_almost_equal(converted_meas.eval(), [[1, -1, 0], [1, -1, 0]]) def test_grouped_pauli_expectation(self): """grouped pauli expectation test""" two_qubit_H2 = ((-1.052373245772859 * I ^ I) + (0.39793742484318045 * I ^ Z) + (-0.39793742484318045 * Z ^ I) + (-0.01128010425623538 * Z ^ Z) + (0.18093119978423156 * X ^ X)) wf = CX @ (H ^ I) @ Zero expect_op = PauliExpectation(group_paulis=False).convert( ~StateFn(two_qubit_H2) @ wf) self.sampler._extract_circuitstatefns(expect_op) num_circuits_ungrouped = len(self.sampler._circuit_ops_cache) self.assertEqual(num_circuits_ungrouped, 5) expect_op_grouped = PauliExpectation(group_paulis=True).convert( ~StateFn(two_qubit_H2) @ wf) q_instance = QuantumInstance( BasicAer.get_backend("statevector_simulator"), seed_simulator=self.seed, seed_transpiler=self.seed, ) sampler = CircuitSampler(q_instance) sampler._extract_circuitstatefns(expect_op_grouped) num_circuits_grouped = len(sampler._circuit_ops_cache) self.assertEqual(num_circuits_grouped, 2) @unittest.skip(reason="IBMQ testing not available in general.") def test_ibmq_grouped_pauli_expectation(self): """pauli expect op vector state vector test""" from qiskit import IBMQ p = IBMQ.load_account() backend = p.get_backend("ibmq_qasm_simulator") q_instance = QuantumInstance(backend, seed_simulator=self.seed, seed_transpiler=self.seed) paulis_op = ListOp([X, Y, Z, I]) states_op = ListOp([One, Zero, Plus, Minus]) valids = [[+0, 0, 1, -1], [+0, 0, 0, 0], [-1, 1, 0, -0], [+1, 1, 1, 1]] converted_meas = self.expect.convert(~StateFn(paulis_op) @ states_op) sampled = CircuitSampler(q_instance).convert(converted_meas) np.testing.assert_array_almost_equal(sampled.eval(), valids, decimal=1) def test_multi_representation_ops(self): """Test observables with mixed representations""" mixed_ops = ListOp([X.to_matrix_op(), H, H + I, X]) converted_meas = self.expect.convert(~StateFn(mixed_ops)) plus_mean = converted_meas @ Plus sampled_plus = self.sampler.convert(plus_mean) np.testing.assert_array_almost_equal(sampled_plus.eval(), [1, 0.5**0.5, (1 + 0.5**0.5), 1], decimal=1) def test_pauli_expectation_non_hermite_op(self): """Test PauliExpectation for non hermitian operator""" exp = ~StateFn(1j * Z) @ One self.assertEqual(self.expect.convert(exp).eval(), 1j) def test_list_pauli_sum_op(self): """Test PauliExpectation for List[PauliSumOp]""" test_op = ListOp([ ~StateFn(PauliSumOp.from_list([("XX", 1), ("ZI", 3), ("ZZ", 5)])) ]) observable = self.expect.convert(test_op) self.assertIsInstance(observable, ListOp) self.assertIsInstance(observable[0][0][0].primitive, PauliSumOp) self.assertIsInstance(observable[0][1][0].primitive, PauliSumOp)
class CircuitQNN(SamplingNeuralNetwork): """A Sampling Neural Network based on a given quantum circuit.""" def __init__( self, circuit: QuantumCircuit, input_params: Optional[List[Parameter]] = None, weight_params: Optional[List[Parameter]] = None, sparse: bool = False, sampling: bool = False, interpret: Optional[Callable[[int], Union[int, Tuple[int, ...]]]] = None, output_shape: Union[int, Tuple[int, ...]] = None, gradient: Gradient = None, quantum_instance: Optional[Union[QuantumInstance, Backend]] = None, input_gradients: bool = False, ) -> None: """ Args: circuit: The parametrized quantum circuit that generates the samples of this network. There will be an attempt to transpile this circuit and cache the transpiled circuit for subsequent usages by the network. If for some reasons the circuit can't be transpiled, e.g. it originates from :class:`~qiskit_machine_learning.circuit.library.RawFeatureVector`, the circuit will be transpiled every time it is required to be executed and only when all parameters are bound. This may impact overall performance on the network. input_params: The parameters of the circuit corresponding to the input. weight_params: The parameters of the circuit corresponding to the trainable weights. sparse: Returns whether the output is sparse or not. sampling: Determines whether the network returns a batch of samples or (possibly sparse) array of probabilities in its forward pass. In case of probabilities, the backward pass returns the probability gradients, while it returns ``(None, None)`` in the case of samples. Note that ``sampling==True`` will always result in a dense return array independent of the other settings. interpret: A callable that maps the measured integer to another unsigned integer or tuple of unsigned integers. These are used as new indices for the (potentially sparse) output array. If this is used, and ``sampling==False``, the output shape of the output needs to be given as a separate argument. If no interpret function is passed, then an identity function will be used by this neural network. output_shape: The output shape of the custom interpretation, only used in the case where an interpret function is provided and ``sampling==False``. Note that in the remaining cases, the output shape is automatically inferred by: ``2^num_qubits`` if ``sampling==False`` and ``interpret==None``, ``(num_samples,1)`` if ``sampling==True`` and ``interpret==None``, and ``(num_samples, interpret_shape)`` if ``sampling==True`` and an interpret function is provided. gradient: The gradient converter to be used for the probability gradients. quantum_instance: The quantum instance to evaluate the circuits. Note that if ``sampling==True``, a statevector simulator is not a valid backend for the quantum instance. input_gradients: Determines whether to compute gradients with respect to input data. Note that this parameter is ``False`` by default, and must be explicitly set to ``True`` for a proper gradient computation when using ``TorchConnector``. Raises: QiskitMachineLearningError: if ``interpret`` is passed without ``output_shape``. """ self._input_params = list(input_params or []) self._weight_params = list(weight_params or []) self._input_gradients = input_gradients sparse = False if sampling else sparse if sparse: _optionals.HAS_SPARSE.require_now("DOK") # copy circuit and add measurements in case non are given # TODO: need to be able to handle partial measurements! (partial trace...) self._circuit = circuit.copy() # we have not transpiled the circuit yet self._circuit_transpiled = False # these original values may be re-used when a quantum instance is set, # but initially it was None self._original_output_shape = output_shape # next line is required by pylint only self._interpret = interpret self._original_interpret = interpret # we need this property in _set_quantum_instance despite it is initialized # in the super class later on, review of SamplingNN is required. self._sampling = sampling # set quantum instance and derive target output_shape and interpret self._set_quantum_instance(quantum_instance, output_shape, interpret) # init super class super().__init__( len(self._input_params), len(self._weight_params), sparse, sampling, self._output_shape, self._input_gradients, ) self._original_circuit = circuit # use given gradient or default self._gradient = gradient or Gradient() # prepare probability gradient opflow object self._construct_gradient_circuit() def _construct_gradient_circuit(self): self._gradient_circuit: OperatorBase = None try: # todo: avoid copying the circuit grad_circuit = self._original_circuit.copy() grad_circuit.remove_final_measurements() # ideally this would not be necessary if self._input_gradients: params = self._input_params + self._weight_params else: params = self._weight_params self._gradient_circuit = self._gradient.convert(StateFn(grad_circuit), params) except (ValueError, TypeError, OpflowError, QiskitError): logger.warning("Cannot compute gradient operator! Continuing without gradients!") def _compute_output_shape(self, interpret, output_shape, sampling) -> Tuple[int, ...]: """Validate and compute the output shape.""" # a safety check cause we use quantum instance below if self._quantum_instance is None: raise QiskitMachineLearningError( "A quantum instance is required to compute output shape!" ) # this definition is required by mypy output_shape_: Tuple[int, ...] = (-1,) # todo: move sampling code to the super class if sampling: if output_shape is not None: # Warn user that output_shape parameter will be ignored logger.warning( "In sampling mode, output_shape will be automatically inferred " "from the number of samples and interpret function, if provided." ) num_samples = self._quantum_instance.run_config.shots if interpret is not None: ret = interpret(0) # infer shape from function result = np.array(ret) if len(result.shape) == 0: output_shape_ = (num_samples, 1) else: output_shape_ = (num_samples, *result.shape) else: output_shape_ = (num_samples, 1) else: if interpret is not None: if output_shape is None: raise QiskitMachineLearningError( "No output shape given, but required in case of custom interpret!" ) if isinstance(output_shape, Integral): output_shape = int(output_shape) output_shape_ = (output_shape,) else: output_shape_ = output_shape else: if output_shape is not None: # Warn user that output_shape parameter will be ignored logger.warning( "No interpret function given, output_shape will be automatically " "determined as 2^num_qubits." ) output_shape_ = (2**self._circuit.num_qubits,) # final validation output_shape_ = self._validate_output_shape(output_shape_) return output_shape_ @property def circuit(self) -> QuantumCircuit: """Returns the underlying quantum circuit.""" return self._circuit @property def input_params(self) -> List: """Returns the list of input parameters.""" return self._input_params @property def weight_params(self) -> List: """Returns the list of trainable weights parameters.""" return self._weight_params @property def interpret(self) -> Optional[Callable[[int], Union[int, Tuple[int, ...]]]]: """Returns interpret function to be used by the neural network. If it is not set in the constructor or can not be implicitly derived (e.g. a quantum instance is not provided), then ``None`` is returned.""" return self._interpret @property def quantum_instance(self) -> QuantumInstance: """Returns the quantum instance to evaluate the circuit.""" return self._quantum_instance @quantum_instance.setter def quantum_instance(self, quantum_instance: Optional[Union[QuantumInstance, Backend]]) -> None: """Sets the quantum instance to evaluate the circuit and make sure circuit has measurements or not depending on the type of backend used. """ self._set_quantum_instance( quantum_instance, self._original_output_shape, self._original_interpret ) def _set_quantum_instance( self, quantum_instance: Optional[Union[QuantumInstance, Backend]], output_shape: Union[int, Tuple[int, ...]], interpret: Optional[Callable[[int], Union[int, Tuple[int, ...]]]], ) -> None: """ Internal method to set a quantum instance and compute/initialize internal properties such as an interpret function, output shape and a sampler. Args: quantum_instance: A quantum instance to set. output_shape: An output shape of the custom interpretation. interpret: A callable that maps the measured integer to another unsigned integer or tuple of unsigned integers. """ if isinstance(quantum_instance, Backend): quantum_instance = QuantumInstance(quantum_instance) self._quantum_instance = quantum_instance if self._quantum_instance is not None: # add measurements in case none are given if self._quantum_instance.is_statevector: if len(self._circuit.clbits) > 0: self._circuit.remove_final_measurements() elif len(self._circuit.clbits) == 0: self._circuit.measure_all() # set interpret and compute output shape self.set_interpret(interpret, output_shape) # prepare sampler self._sampler = CircuitSampler(self._quantum_instance, param_qobj=False, caching="all") # transpile the QNN circuit try: self._circuit = self._quantum_instance.transpile( self._circuit, pass_manager=self._quantum_instance.unbound_pass_manager )[0] self._circuit_transpiled = True except QiskitError: # likely it is caused by RawFeatureVector, we just ignore this error and # transpile circuits when it is required. self._circuit_transpiled = False else: self._output_shape = output_shape @property def input_gradients(self) -> bool: """Returns whether gradients with respect to input data are computed by this neural network in the ``backward`` method or not. By default such gradients are not computed.""" return self._input_gradients @input_gradients.setter def input_gradients(self, input_gradients: bool) -> None: """Turn on/off gradient with respect to input data.""" self._input_gradients = input_gradients self._construct_gradient_circuit() def set_interpret( self, interpret: Optional[Callable[[int], Union[int, Tuple[int, ...]]]], output_shape: Union[int, Tuple[int, ...]] = None, ) -> None: """Change 'interpret' and corresponding 'output_shape'. If self.sampling==True, the output _shape does not have to be set and is inferred from the interpret function. Otherwise, the output_shape needs to be given. Args: interpret: A callable that maps the measured integer to another unsigned integer or tuple of unsigned integers. See constructor for more details. output_shape: The output shape of the custom interpretation, only used in the case where an interpret function is provided and ``sampling==False``. See constructor for more details. """ # save original values self._original_output_shape = output_shape self._original_interpret = interpret # derive target values to be used in computations self._output_shape = self._compute_output_shape(interpret, output_shape, self._sampling) self._interpret = interpret if interpret is not None else lambda x: x def _sample( self, input_data: Optional[np.ndarray], weights: Optional[np.ndarray] ) -> np.ndarray: self._check_quantum_instance("samples") if self._quantum_instance.is_statevector: raise QiskitMachineLearningError("Sampling does not work with statevector simulator!") # evaluate operator orig_memory = self._quantum_instance.backend_options.get("memory") self._quantum_instance.backend_options["memory"] = True circuits = [] # iterate over samples, each sample is an element of a batch num_samples = input_data.shape[0] for i in range(num_samples): param_values = { input_param: input_data[i, j] for j, input_param in enumerate(self._input_params) } param_values.update( {weight_param: weights[j] for j, weight_param in enumerate(self._weight_params)} ) circuits.append(self._circuit.bind_parameters(param_values)) if self._quantum_instance.bound_pass_manager is not None: circuits = self._quantum_instance.transpile( circuits, pass_manager=self._quantum_instance.bound_pass_manager ) result = self._quantum_instance.execute(circuits, had_transpiled=self._circuit_transpiled) # set the memory setting back self._quantum_instance.backend_options["memory"] = orig_memory # return samples samples = np.zeros((num_samples, *self._output_shape)) # collect them from all executed circuits for i, circuit in enumerate(circuits): memory = result.get_memory(circuit) for j, b in enumerate(memory): samples[i, j, :] = self._interpret(int(b, 2)) return samples def _probabilities( self, input_data: Optional[np.ndarray], weights: Optional[np.ndarray] ) -> Union[np.ndarray, SparseArray]: self._check_quantum_instance("probabilities") # evaluate operator circuits = [] num_samples = input_data.shape[0] for i in range(num_samples): param_values = { input_param: input_data[i, j] for j, input_param in enumerate(self._input_params) } param_values.update( {weight_param: weights[j] for j, weight_param in enumerate(self._weight_params)} ) circuits.append(self._circuit.bind_parameters(param_values)) if self._quantum_instance.bound_pass_manager is not None: circuits = self._quantum_instance.transpile( circuits, pass_manager=self._quantum_instance.bound_pass_manager ) result = self._quantum_instance.execute(circuits, had_transpiled=self._circuit_transpiled) # initialize probabilities if self._sparse: # pylint: disable=import-error from sparse import DOK prob = DOK((num_samples, *self._output_shape)) else: prob = np.zeros((num_samples, *self._output_shape)) for i, circuit in enumerate(circuits): counts = result.get_counts(circuit) shots = sum(counts.values()) # evaluate probabilities for b, v in counts.items(): key = self._interpret(int(b, 2)) if isinstance(key, Integral): key = (cast(int, key),) key = (i, *key) # type: ignore prob[key] += v / shots if self._sparse: return prob.to_coo() else: return prob def _probability_gradients( self, input_data: Optional[np.ndarray], weights: Optional[np.ndarray] ) -> Tuple[Union[np.ndarray, SparseArray], Union[np.ndarray, SparseArray]]: self._check_quantum_instance("probability gradients") # check whether gradient circuit could be constructed if self._gradient_circuit is None: return None, None num_samples = input_data.shape[0] # initialize empty gradients input_grad = None # by default we don't have data gradients if self._sparse: # pylint: disable=import-error from sparse import DOK if self._input_gradients: input_grad = DOK((num_samples, *self._output_shape, self._num_inputs)) weights_grad = DOK((num_samples, *self._output_shape, self._num_weights)) else: if self._input_gradients: input_grad = np.zeros((num_samples, *self._output_shape, self._num_inputs)) weights_grad = np.zeros((num_samples, *self._output_shape, self._num_weights)) param_values = { input_param: input_data[:, j] for j, input_param in enumerate(self._input_params) } param_values.update( { weight_param: np.full(num_samples, weights[j]) for j, weight_param in enumerate(self._weight_params) } ) converted_op = self._sampler.convert(self._gradient_circuit, param_values) # if statement is a workaround for https://github.com/Qiskit/qiskit-terra/issues/7608 if len(converted_op.parameters) > 0: # create an list of parameter bindings, each element corresponds to a sample in the dataset param_bindings = [ {param: param_values[i] for param, param_values in param_values.items()} for i in range(num_samples) ] grad = [] # iterate over gradient vectors and bind the correct leftover parameters for g_i, param_i in zip(converted_op, param_bindings): # bind or re-bind remaining values and evaluate the gradient grad.append(g_i.bind_parameters(param_i).eval()) else: grad = converted_op.eval() if self._input_gradients: num_grad_vars = self._num_inputs + self._num_weights else: num_grad_vars = self._num_weights # construct gradients for sample in range(num_samples): for i in range(num_grad_vars): coo_grad = coo_matrix(grad[sample][i]) # this works for sparse and dense case # get index for input or weights gradients if self._input_gradients: grad_index = i if i < self._num_inputs else i - self._num_inputs else: grad_index = i for _, k, val in zip(coo_grad.row, coo_grad.col, coo_grad.data): # interpret integer and construct key key = self._interpret(k) if isinstance(key, Integral): key = (sample, int(key), grad_index) else: # if key is an array-type, cast to hashable tuple key = tuple(cast(Iterable[int], key)) key = (sample, *key, grad_index) # store value for inputs or weights gradients if self._input_gradients: # we compute input gradients first if i < self._num_inputs: input_grad[key] += np.real(val) else: weights_grad[key] += np.real(val) else: weights_grad[key] += np.real(val) # end of for each sample if self._sparse: if self._input_gradients: input_grad = input_grad.to_coo() weights_grad = weights_grad.to_coo() return input_grad, weights_grad def _check_quantum_instance(self, feature: str): if self._quantum_instance is None: raise QiskitMachineLearningError( f"Evaluation of {feature} requires a quantum instance!" )
class OpflowQNN(NeuralNetwork): """Opflow Quantum Neural Network.""" def __init__( self, operator: OperatorBase, input_params: Optional[List[Parameter]] = None, weight_params: Optional[List[Parameter]] = None, exp_val: Optional[ExpectationBase] = None, gradient: Optional[Gradient] = None, quantum_instance: Optional[Union[QuantumInstance, Backend]] = None, input_gradients: bool = False, ): """ Args: operator: The parametrized operator that represents the neural network. input_params: The operator parameters that correspond to the input of the network. weight_params: The operator parameters that correspond to the trainable weights. exp_val: The Expected Value converter to be used for the operator. gradient: The Gradient converter to be used for the operator's backward pass. quantum_instance: The quantum instance to evaluate the network. input_gradients: Determines whether to compute gradients with respect to input data. Note that this parameter is ``False`` by default, and must be explicitly set to ``True`` for a proper gradient computation when using ``TorchConnector``. """ self._input_params = list(input_params) or [] self._weight_params = list(weight_params) or [] self._set_quantum_instance(quantum_instance) self._operator = operator self._forward_operator = exp_val.convert( operator) if exp_val else operator self._gradient = gradient self._input_gradients = input_gradients self._construct_gradient_operator() output_shape = self._compute_output_shape(operator) super().__init__( len(self._input_params), len(self._weight_params), sparse=False, output_shape=output_shape, input_gradients=input_gradients, ) def _construct_gradient_operator(self): self._gradient_operator: OperatorBase = None try: gradient = self._gradient or Gradient() if self._input_gradients: params = self._input_params + self._weight_params else: params = self._weight_params self._gradient_operator = gradient.convert(self._operator, params) except (ValueError, TypeError, OpflowError, QiskitError): logger.warning( "Cannot compute gradient operator! Continuing without gradients!" ) def _compute_output_shape(self, op: OperatorBase) -> Tuple[int, ...]: """Determines the output shape of a given operator.""" # TODO: the whole method should eventually be moved to opflow and rewritten in a better way. # if the operator is a composed one, then we only need to look at the first element of it. if isinstance(op, ComposedOp): return self._compute_output_shape(op.oplist[0].primitive) # this "if" statement is on purpose, to prevent sub-classes. # pylint:disable=unidiomatic-typecheck if type(op) == ListOp: shapes = [self._compute_output_shape(op_) for op_ in op.oplist] if not np.all([shape == shapes[0] for shape in shapes]): raise QiskitMachineLearningError( "Only supports ListOps with children that return the same shape." ) if shapes[0] == (1, ): out = op.combo_fn(np.zeros((len(op.oplist)))) else: out = op.combo_fn(np.zeros((len(op.oplist), *shapes[0]))) return out.shape else: return (1, ) @property def operator(self): """Returns the underlying operator of this QNN.""" return self._operator @property def input_gradients(self) -> bool: """Returns whether gradients with respect to input data are computed by this neural network in the ``backward`` method or not. By default such gradients are not computed.""" return self._input_gradients @input_gradients.setter def input_gradients(self, input_gradients: bool) -> None: """Turn on/off computation of gradients with respect to input data.""" self._input_gradients = input_gradients self._construct_gradient_operator() @property def quantum_instance(self) -> QuantumInstance: """Returns the quantum instance to evaluate the operator.""" return self._quantum_instance @quantum_instance.setter def quantum_instance( self, quantum_instance: Optional[Union[QuantumInstance, Backend]]) -> None: """Sets the quantum instance to evaluate the operator.""" self._set_quantum_instance(quantum_instance) def _set_quantum_instance( self, quantum_instance: Optional[Union[QuantumInstance, Backend]]) -> None: """ Internal method to set a quantum instance and compute/initialize a sampler. Args: quantum_instance: A quantum instance to set. Returns: None. """ if isinstance(quantum_instance, Backend): quantum_instance = QuantumInstance(quantum_instance) self._quantum_instance = quantum_instance if quantum_instance is not None: self._circuit_sampler = CircuitSampler( self._quantum_instance, param_qobj=is_aer_provider(self._quantum_instance.backend), caching="all", ) else: self._circuit_sampler = None def _forward( self, input_data: Optional[np.ndarray], weights: Optional[np.ndarray]) -> Union[np.ndarray, SparseArray]: # combine parameter dictionary # take i-th column as values for the i-th param in a batch param_values = { p: input_data[:, i].tolist() for i, p in enumerate(self._input_params) } param_values.update({ p: [weights[i]] * input_data.shape[0] for i, p in enumerate(self._weight_params) }) # evaluate operator if self._circuit_sampler: op = self._circuit_sampler.convert(self._forward_operator, param_values) result = np.real(op.eval()) else: op = self._forward_operator.bind_parameters(param_values) result = np.real(op.eval()) result = np.array(result) return result.reshape(-1, *self._output_shape) def _backward( self, input_data: Optional[np.ndarray], weights: Optional[np.ndarray] ) -> Tuple[Optional[Union[np.ndarray, SparseArray]], Optional[Union[ np.ndarray, SparseArray]], ]: # check whether gradient circuit could be constructed if self._gradient_operator is None: return None, None num_samples = input_data.shape[0] if self._input_gradients: num_params = self._num_inputs + self._num_weights else: num_params = self._num_weights param_values = { input_param: input_data[:, j] for j, input_param in enumerate(self._input_params) } param_values.update({ weight_param: np.full(num_samples, weights[j]) for j, weight_param in enumerate(self._weight_params) }) if self._circuit_sampler: converted_op = self._circuit_sampler.convert( self._gradient_operator, param_values) # if statement is a workaround for https://github.com/Qiskit/qiskit-terra/issues/7608 if len(converted_op.parameters) > 0: # rebind the leftover parameters and evaluate the gradient grad = self._evaluate_operator(converted_op, num_samples, param_values) else: # all parameters are bound by CircuitSampler, so we evaluate the operator directly grad = np.asarray(converted_op.eval()) else: # we evaluate gradient operator for each sample separately, so we create a list of operators. grad = self._evaluate_operator([self._gradient_operator] * num_samples, num_samples, param_values) grad = np.real(grad) # this is another workaround to fix output shape of the invocation result of CircuitSampler if self._output_shape == (1, ): # at least 3 dimensions: batch, output, num_parameters, but in this case we don't have # output dimension, so we add a dimension that corresponds to the output grad = grad.reshape((num_samples, 1, num_params)) else: # swap last axis that corresponds to parameters and axes correspond to the output shape last_axis = len(grad.shape) - 1 grad = grad.transpose([0, last_axis, *(range(1, last_axis))]) # split into and return input and weights gradients if self._input_gradients: input_grad = grad[:, :, :self._num_inputs].reshape( -1, *self._output_shape, self._num_inputs) weights_grad = grad[:, :, self._num_inputs:].reshape( -1, *self._output_shape, self._num_weights) else: input_grad = None weights_grad = grad.reshape(-1, *self._output_shape, self._num_weights) return input_grad, weights_grad def _evaluate_operator( self, operator: Union[OperatorBase, List[OperatorBase]], num_samples: int, param_values: Dict[Parameter, np.ndarray], ) -> np.ndarray: """ Evaluates an operator or a list of operators for the samples in the dataset. If an operator is passed then it is considered as an iterable that has `num_samples` elements. Usually such operators are obtained as an output from `CircuitSampler`. If a list of operators is passed then each operator in this list is evaluated with a set of values/parameters corresponding to the sample index in the `param_values` as the operator in the list. Args: operator: operator or list of operators to evaluate. num_samples: a total number of samples param_values: parameter values to use for operator evaluation. Returns: the result of operator evaluation as an array. """ # create an list of parameter bindings, each element corresponds to a sample in the dataset param_bindings = [{ param: param_values[i] for param, param_values in param_values.items() } for i in range(num_samples)] grad = [] # iterate over gradient vectors and bind the correct parameters for oper_i, param_i in zip(operator, param_bindings): # bind or re-bind remaining values and evaluate the gradient grad.append(oper_i.bind_parameters(param_i).eval()) return np.asarray(grad)
class OpflowQNN(NeuralNetwork): """Opflow Quantum Neural Network.""" def __init__(self, operator: OperatorBase, input_params: Optional[List[Parameter]] = None, weight_params: Optional[List[Parameter]] = None, exp_val: Optional[ExpectationBase] = None, gradient: Optional[Gradient] = None, quantum_instance: Optional[Union[QuantumInstance, BaseBackend, Backend]] = None): """Initializes the Opflow Quantum Neural Network. Args: operator: The parametrized operator that represents the neural network. input_params: The operator parameters that correspond to the input of the network. weight_params: The operator parameters that correspond to the trainable weights. exp_val: The Expected Value converter to be used for the operator. gradient: The Gradient converter to be used for the operator's backward pass. quantum_instance: The quantum instance to evaluate the network. """ self.operator = operator self.input_params = list(input_params or []) self.weight_params = list(weight_params or []) self.exp_val = exp_val # TODO: currently not used by Gradient! self.gradient = gradient or Gradient() if isinstance(quantum_instance, (BaseBackend, Backend)): quantum_instance = QuantumInstance(quantum_instance) if quantum_instance: self.quantum_instance = quantum_instance self.circuit_sampler = CircuitSampler( self.quantum_instance, param_qobj=is_aer_provider(self.quantum_instance.backend) ) # TODO: replace by extended caching in circuit sampler after merged: "caching='all'" self.gradient_sampler = deepcopy(self.circuit_sampler) else: self.quantum_instance = None self.circuit_sampler = None self.gradient_sampler = None self.forward_operator = self.exp_val.convert(operator) if exp_val else operator self.gradient_operator = self.gradient.convert(operator, self.input_params + self.weight_params) output_shape = self._get_output_shape_from_op(operator) super().__init__(len(self.input_params), len(self.weight_params), sparse=False, output_shape=output_shape) def _get_output_shape_from_op(self, op: OperatorBase) -> Tuple[int, ...]: """Determines the output shape of a given operator.""" # TODO: should eventually be moved to opflow if isinstance(op, ListOp): shapes = [] for op_ in op.oplist: shape_ = self._get_output_shape_from_op(op_) shapes += [shape_] if not np.all([shape == shapes[0] for shape in shapes]): raise QiskitMachineLearningError( 'Only supports ListOps with children that return the same shape.') if shapes[0] == (1,): out = op.combo_fn(np.zeros((len(op.oplist)))) else: out = op.combo_fn(np.zeros((len(op.oplist), *shapes[0]))) return out.shape else: return (1,) def _forward(self, input_data: Optional[np.ndarray], weights: Optional[np.ndarray] ) -> Union[np.ndarray, SparseArray]: # combine parameter dictionary param_values = {p: input_data[i] for i, p in enumerate(self.input_params)} param_values.update({p: weights[i] for i, p in enumerate(self.weight_params)}) # evaluate operator if self.circuit_sampler: op = self.circuit_sampler.convert(self.forward_operator, param_values) result = np.real(op.eval()) else: op = self.forward_operator.bind_parameters(param_values) result = np.real(op.eval()) result = np.array(result) return result.reshape(1, *self.output_shape) def _backward(self, input_data: Optional[np.ndarray], weights: Optional[np.ndarray] ) -> Tuple[Optional[Union[np.ndarray, SparseArray]], Optional[Union[np.ndarray, SparseArray]]]: # combine parameter dictionary param_values = {p: input_data[i] for i, p in enumerate(self.input_params)} param_values.update({p: weights[i] for i, p in enumerate(self.weight_params)}) # evaluate gradient over all parameters if self.gradient_sampler: grad = self.gradient_sampler.convert(self.gradient_operator, param_values) # TODO: this should not be necessary and is a bug! grad = grad.bind_parameters(param_values) grad = np.real(grad.eval()) else: grad = self.gradient_operator.bind_parameters(param_values) grad = np.real(grad.eval()) # split into and return input and weights gradients input_grad = np.array(grad[:len(input_data)]).reshape( 1, *self.output_shape, self.num_inputs) weights_grad = np.array(grad[len(input_data):]).reshape( 1, *self.output_shape, self.num_weights) return input_grad, weights_grad