Ejemplo n.º 1
0
    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))
Ejemplo n.º 4
0
    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
Ejemplo n.º 5
0
    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]
Ejemplo n.º 6
0
    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)
Ejemplo n.º 7
0
    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
Ejemplo n.º 8
0
    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)
Ejemplo n.º 9
0
    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)
Ejemplo n.º 10
0
    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
Ejemplo n.º 11
0
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)
Ejemplo n.º 12
0
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
Ejemplo n.º 13
0
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)
Ejemplo n.º 14
0
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
Ejemplo n.º 16
0
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)
Ejemplo n.º 18
0
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
Ejemplo n.º 19
0
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
Ejemplo n.º 20
0
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
Ejemplo n.º 21
0
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
Ejemplo n.º 22
0
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)
Ejemplo n.º 23
0
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)
Ejemplo n.º 25
0
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