From 8914c294623deaff68bcde611998c68e4179dc26 Mon Sep 17 00:00:00 2001 From: Jim Garrison Date: Mon, 2 Dec 2024 08:58:37 -0800 Subject: [PATCH] Bring coverage to 100% --- .../simulation/explicit_gradient.py | 2 +- .../simulation/quimb/__init__.py | 29 ++++++---- test/simulation/aer/test_aer_backend.py | 18 ------- test/simulation/conftest.py | 30 +++++++++++ test/simulation/quimb/test_quimb_backend.py | 39 ++++++++++++++ test/simulation/test_gradient.py | 54 ++++++++++++++++++- tox.ini | 2 +- 7 files changed, 144 insertions(+), 30 deletions(-) create mode 100644 test/simulation/conftest.py diff --git a/qiskit_addon_aqc_tensor/simulation/explicit_gradient.py b/qiskit_addon_aqc_tensor/simulation/explicit_gradient.py index f8dcb2c..56cda81 100644 --- a/qiskit_addon_aqc_tensor/simulation/explicit_gradient.py +++ b/qiskit_addon_aqc_tensor/simulation/explicit_gradient.py @@ -112,7 +112,7 @@ def preprocess_circuit_for_backtracking( except KeyError as ex: raise ValueError( f"Expects a gate from the list of basis ones: " - f"'{_basis_gates()}', got '{operation.name}' instead." + f"{sorted(_basis_gates())}, got '{operation.name}' instead." ) from ex action = action_generator(pname2index, operation, qubit_indices, settings) if action is not None: diff --git a/qiskit_addon_aqc_tensor/simulation/quimb/__init__.py b/qiskit_addon_aqc_tensor/simulation/quimb/__init__.py index 5386be9..ddb47fa 100644 --- a/qiskit_addon_aqc_tensor/simulation/quimb/__init__.py +++ b/qiskit_addon_aqc_tensor/simulation/quimb/__init__.py @@ -90,7 +90,9 @@ class QuimbSimulator(TensorNetworkSimulationSettings): #: Whether to display a progress bar while applying gates. progbar: bool = False - def _construct_circuit(self, qc: QuantumCircuit, /, **kwargs): + def _construct_circuit( + self, qc: QuantumCircuit, /, *, out_state: np.ndarray | None = None, **kwargs + ): import qiskit_quimb qc = qc.decompose(AnsatzBlock) @@ -98,6 +100,8 @@ def _construct_circuit(self, qc: QuantumCircuit, /, **kwargs): circ = quimb_circuit_factory(N=qc.num_qubits, **kwargs) gates = qiskit_quimb.quimb_gates(qc) circ.apply_gates(gates, progbar=self.progbar) + if out_state is not None: + out_state[:] = np.squeeze(circ.psi.to_dense()) return circ @@ -111,8 +115,10 @@ def tensornetwork_from_circuit( qc: QuantumCircuit, settings: QuimbSimulator, /, + *, + out_state: Optional[np.ndarray] = None, ) -> quimb.tensor.Circuit: - return settings._construct_circuit(qc) + return settings._construct_circuit(qc, out_state=out_state) @dispatch @@ -171,10 +177,7 @@ def apply_circuit_to_state( Returns: The new state. """ - circuit = settings._construct_circuit(qc, psi0=circ0.psi) - if out_state is not None: - out_state[:] = circuit.psi.to_dense() - return circuit + return settings._construct_circuit(qc, out_state=out_state, psi0=circ0.psi) class QiskitQuimbConversionContext: @@ -257,10 +260,18 @@ def qiskit_ansatz_to_quimb( quimb_gate_ = quimb_gate(op, qubits) if quimb_gate_ is not None: circ.apply_gate(quimb_gate_) - else: + else: # pragma: no cover raise ValueError("A parameter in the circuit has an unexpected type.") for j, _, _ in mapping: - if j == -1: + if j == -1: # pragma: no cover + # NOTE: There seems to be no obvious way to trigger this error. + # Even the following snippet results in the parameter being removed + # from the circuit. + # + # qc = QuantumCircuit(1) + # x = Parameter("x") + # qc.rx(x, 0) + # qc.data[0] = CircuitInstruction(RXGate(np.pi / 3), qubits=[0]) raise ValueError( "Some parameter(s) in the given Qiskit circuit remain unused. " "This use case is not currently supported by the Quimb conversion code." @@ -278,7 +289,7 @@ def recover_parameters_from_quimb( raise ValueError( "The length of the mapping in the provided QiskitQuimbConversionContext " "does not match the number of parametrized gates in the circuit " - f"({len(mapping)}) vs. ({len(quimb_parametrized_gates)})." + f"({len(mapping)} vs. {len(quimb_parametrized_gates)})." ) # `(y - b) / m` is the inversion of the parameter expression, which we # assumed above to be in the form mx + b. diff --git a/test/simulation/aer/test_aer_backend.py b/test/simulation/aer/test_aer_backend.py index d610ccd..1fdc2b8 100644 --- a/test/simulation/aer/test_aer_backend.py +++ b/test/simulation/aer/test_aer_backend.py @@ -12,7 +12,6 @@ import numpy as np import pytest -from qiskit import QuantumCircuit from qiskit.circuit.library import CXGate, XGate from qiskit.providers.basic_provider import BasicSimulator @@ -32,23 +31,6 @@ pytestmark = pytest.mark.skipif(skip_aer_tests, reason="qiskit-aer is not installed") -@pytest.fixture -def bell_qc(): - qc = QuantumCircuit(2) - qc.h(0) - qc.cx(0, 1) - return qc - - -@pytest.fixture -def ghz_qc(): - qc = QuantumCircuit(3) - qc.h(0) - qc.cx(0, 1) - qc.cx(1, 2) - return qc - - class TestQiskitAerBackend: def test_bell_circuit(self, bell_qc, AerSimulator): simulator = AerSimulator(method="matrix_product_state") diff --git a/test/simulation/conftest.py b/test/simulation/conftest.py new file mode 100644 index 0000000..4ddc307 --- /dev/null +++ b/test/simulation/conftest.py @@ -0,0 +1,30 @@ +# This code is a Qiskit project. +# +# (C) Copyright IBM 2024. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +import pytest +from qiskit import QuantumCircuit + + +@pytest.fixture +def bell_qc(): + qc = QuantumCircuit(2) + qc.h(0) + qc.cx(0, 1) + return qc + + +@pytest.fixture +def ghz_qc(): + qc = QuantumCircuit(3) + qc.h(0) + qc.cx(0, 1) + qc.cx(1, 2) + return qc diff --git a/test/simulation/quimb/test_quimb_backend.py b/test/simulation/quimb/test_quimb_backend.py index acdb4cf..0fc01b3 100644 --- a/test/simulation/quimb/test_quimb_backend.py +++ b/test/simulation/quimb/test_quimb_backend.py @@ -16,6 +16,7 @@ from qiskit.circuit import Parameter, QuantumCircuit from qiskit.circuit.library import CXGate, RXGate, XXPlusYYGate +from qiskit_addon_aqc_tensor.objective import OneMinusFidelity from qiskit_addon_aqc_tensor.simulation import ( compute_overlap, tensornetwork_from_circuit, @@ -115,3 +116,41 @@ def test_repeated_parameter(self): e_info.value.args[0] == "Parameter cannot be repeated in circuit, else quimb will attempt to optimize each instance separately." ) + + def test_unspecified_gradient_method(self, quimb): + settings = QuimbSimulator(quimb.tensor.CircuitMPS) + qc = QuantumCircuit(1) + x = Parameter("x") + qc.rx(x, 0) + with pytest.raises(ValueError) as e_info: + OneMinusFidelity(None, qc, settings) + assert ( + e_info.value.args[0] + == "Gradient method unspecified. Please specify an autodiff_backend for the QuimbSimulator object." + ) + + # FIXME: also do this with a random circuit + # FIXME: consolidate these tests across the backends + @pytest.mark.parametrize("circuit_factory", ["Circuit", "CircuitMPS"]) + def test_bell_circuit_statevector(self, bell_qc, quimb, circuit_factory): + simulator = QuimbSimulator(getattr(quimb.tensor, circuit_factory)) + out_state = np.zeros([4], dtype=complex) + tensornetwork_from_circuit(bell_qc, simulator, out_state=out_state) + assert out_state == pytest.approx(np.array([1, 0, 0, 1]) / np.sqrt(2)) + + def test_recovery_num_parameters_mismatch_error(self): + x = Parameter("x") + y = Parameter("y") + qc1 = QuantumCircuit(1) + qc1.rx(1 - x, 0) + qc2 = QuantumCircuit(1) + qc2.rx(1 - x, 0) + qc2.ry(1 - 0.5 * y, 0) + circ1, _ = qiskit_ansatz_to_quimb(qc1, [0.5]) + _, ctx2 = qiskit_ansatz_to_quimb(qc2, [0.5, 0.3]) + with pytest.raises(ValueError) as e_info: + recover_parameters_from_quimb(circ1, ctx2) + assert ( + e_info.value.args[0] + == "The length of the mapping in the provided QiskitQuimbConversionContext does not match the number of parametrized gates in the circuit (2 vs. 1)." + ) diff --git a/test/simulation/test_gradient.py b/test/simulation/test_gradient.py index d40b2d6..142359b 100644 --- a/test/simulation/test_gradient.py +++ b/test/simulation/test_gradient.py @@ -28,6 +28,7 @@ _compute_objective_and_gradient, _preprocess_for_gradient, ) +from qiskit_addon_aqc_tensor.simulation.aer import is_aer_available from qiskit_addon_aqc_tensor.simulation.explicit_gradient import _basis_gates @@ -70,7 +71,7 @@ def _generate_random_ansatz(num_qubits: int): su2_gates=su2_gates, reps=random.randint(1, 2), parameter_prefix="c", - insert_barriers=_get_random_bool(), + insert_barriers=True, ), inplace=True, copy=False, @@ -129,3 +130,54 @@ def vdagger_rhs(thetas): numerical_grad = -2 * np.real(np.conj(f_0) * numerical_grad) assert grad == pytest.approx(numerical_grad, abs=1e-4) + + +@pytest.mark.skipif(not is_aer_available(), reason="qiskit-aer is not installed") +class TestExplicitGradient: + def test_no_parameters_throws_error(self, AerSimulator): + settings = AerSimulator(method="matrix_product_state") + tn = tensornetwork_from_circuit(QuantumCircuit(1), settings) + qc = QuantumCircuit(1) + with pytest.raises(ValueError) as e_info: + OneMinusFidelity(tn, qc, settings) + assert ( + e_info.value.args[0] + == "Expects parametric circuit using ParameterVector object.\nThat is, placeholders are expected rather than concrete\nvalues for the variable parameters. The circuit specified\nhas no variable parameters. Check that the function\nassign_parameters() has not been applied to this circuit." + ) + + def test_non_basis_gate(self, AerSimulator): + settings = AerSimulator(method="matrix_product_state") + tn = tensornetwork_from_circuit(QuantumCircuit(2), settings) + qc = QuantumCircuit(2) + x = Parameter("x") + qc.rx(x, 0) + qc.cp(np.pi / 5, 0, 1) + with pytest.raises(ValueError) as e_info: + OneMinusFidelity(tn, qc, settings) + assert ( + e_info.value.args[0] + == "Expects a gate from the list of basis ones: ['barrier', 'cx', 'cz', 'ecr', 'h', 'rx', 'ry', 'rz', 's', 'sdg', 'x', 'y', 'z'], got 'cp' instead." + ) + + def test_one_qubit_parametrized_pauli_error_messages(self, subtests, AerSimulator): + settings = AerSimulator(method="matrix_product_state") + tn = tensornetwork_from_circuit(QuantumCircuit(1), settings) + x = Parameter("x") + y = Parameter("y") + with subtests.test("Expression with multiple parameters"): + qc = QuantumCircuit(1) + qc.rx(x + y, 0) + with pytest.raises(ValueError) as e_info: + OneMinusFidelity(tn, qc, settings) + assert e_info.value.args[0] == "Expression cannot contain more than one Parameter" + with subtests.test("Nonlinear expression"): + qc = QuantumCircuit(1) + qc.rx(x**3, 0) + with pytest.raises(ValueError) as e_info: + OneMinusFidelity(tn, qc, settings) + assert ( + e_info.value.args[0] + == "ParameterExpression's derivative must be a floating-point number, i.e., the expression must be in the form ax + b." + ) + + # FIXME test target and ansatz have different numbers of qubits diff --git a/tox.ini b/tox.ini index 133663c..6ad23df 100644 --- a/tox.ini +++ b/tox.ini @@ -64,7 +64,7 @@ commands = coverage3 run --source qiskit_addon_aqc_tensor --parallel-mode -m pytest test/ {posargs} coverage3 combine coverage3 html - coverage3 report --fail-under=98 --show-missing + coverage3 report --fail-under=100 --show-missing [testenv:docs] extras =