Skip to content

Commit 7c217c2

Browse files
authored
Bring coverage to 100% (#47)
* Bring coverage to 100% * Make sure ansatz and target have same number of qubits * Consolidate and improve statevector tests * Deprecated functions don't need explicit coverage * Work if possible with a non-circuit tensor network
1 parent 99247cf commit 7c217c2

File tree

11 files changed

+230
-38
lines changed

11 files changed

+230
-38
lines changed

qiskit_addon_aqc_tensor/objective.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ def __init__(
8181
"instead of calling the instance directly."
8282
),
8383
)
84-
def __call__(self, x: np.ndarray) -> tuple[float, np.ndarray]:
84+
def __call__(self, x: np.ndarray) -> tuple[float, np.ndarray]: # pragma: no cover
8585
"""Evaluate ``(objective_value, gradient)`` of function at point ``x``.
8686
8787
This method is DEPRECATED since v0.2. The
@@ -117,7 +117,7 @@ def __init__(
117117
target: TensorNetworkState,
118118
ansatz: QuantumCircuit,
119119
settings: TensorNetworkSimulationSettings,
120-
):
120+
): # pragma: no cover
121121
"""Initialize the objective function.
122122
123123
The :class:`.OneMinusFidelity` class is DEPRECATED since v0.2.

qiskit_addon_aqc_tensor/simulation/aer/gradient.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,14 @@
3535

3636
@dispatch
3737
def _preprocess_for_gradient(objective, settings: Union[QiskitAerSimulationSettings, AerSimulator]):
38+
if objective._ansatz is not None:
39+
ansatz_num_qubits = objective._ansatz.num_qubits
40+
target_num_qubits = len(objective._target_tensornetwork.gamma)
41+
if ansatz_num_qubits != target_num_qubits:
42+
raise ValueError(
43+
"Ansatz and target have different numbers of qubits "
44+
f"({ansatz_num_qubits} vs. {target_num_qubits})."
45+
)
3846
gate_actions = preprocess_circuit_for_backtracking(objective._ansatz, settings)
3947
lhs_tensornetwork = tensornetwork_from_circuit(
4048
QuantumCircuit(objective._ansatz.num_qubits), settings

qiskit_addon_aqc_tensor/simulation/explicit_gradient.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ def preprocess_circuit_for_backtracking(
112112
except KeyError as ex:
113113
raise ValueError(
114114
f"Expects a gate from the list of basis ones: "
115-
f"'{_basis_gates()}', got '{operation.name}' instead."
115+
f"{sorted(_basis_gates())}, got '{operation.name}' instead."
116116
) from ex
117117
action = action_generator(pname2index, operation, qubit_indices, settings)
118118
if action is not None:

qiskit_addon_aqc_tensor/simulation/quimb/__init__.py

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -90,14 +90,18 @@ class QuimbSimulator(TensorNetworkSimulationSettings):
9090
#: Whether to display a progress bar while applying gates.
9191
progbar: bool = False
9292

93-
def _construct_circuit(self, qc: QuantumCircuit, /, **kwargs):
93+
def _construct_circuit(
94+
self, qc: QuantumCircuit, /, *, out_state: np.ndarray | None = None, **kwargs
95+
):
9496
import qiskit_quimb
9597

9698
qc = qc.decompose(AnsatzBlock)
9799
quimb_circuit_factory = self.quimb_circuit_factory
98100
circ = quimb_circuit_factory(N=qc.num_qubits, **kwargs)
99101
gates = qiskit_quimb.quimb_gates(qc)
100102
circ.apply_gates(gates, progbar=self.progbar)
103+
if out_state is not None:
104+
out_state[:] = np.squeeze(circ.psi.to_dense())
101105
return circ
102106

103107

@@ -111,8 +115,10 @@ def tensornetwork_from_circuit(
111115
qc: QuantumCircuit,
112116
settings: QuimbSimulator,
113117
/,
118+
*,
119+
out_state: Optional[np.ndarray] = None,
114120
) -> quimb.tensor.Circuit:
115-
return settings._construct_circuit(qc)
121+
return settings._construct_circuit(qc, out_state=out_state)
116122

117123

118124
@dispatch
@@ -171,10 +177,7 @@ def apply_circuit_to_state(
171177
Returns:
172178
The new state.
173179
"""
174-
circuit = settings._construct_circuit(qc, psi0=circ0.psi)
175-
if out_state is not None:
176-
out_state[:] = circuit.psi.to_dense()
177-
return circuit
180+
return settings._construct_circuit(qc, out_state=out_state, psi0=circ0.psi)
178181

179182

180183
class QiskitQuimbConversionContext:
@@ -257,10 +260,18 @@ def qiskit_ansatz_to_quimb(
257260
quimb_gate_ = quimb_gate(op, qubits)
258261
if quimb_gate_ is not None:
259262
circ.apply_gate(quimb_gate_)
260-
else:
263+
else: # pragma: no cover
261264
raise ValueError("A parameter in the circuit has an unexpected type.")
262265
for j, _, _ in mapping:
263-
if j == -1:
266+
if j == -1: # pragma: no cover
267+
# NOTE: There seems to be no obvious way to trigger this error.
268+
# Even the following snippet results in the parameter being removed
269+
# from the circuit.
270+
#
271+
# qc = QuantumCircuit(1)
272+
# x = Parameter("x")
273+
# qc.rx(x, 0)
274+
# qc.data[0] = CircuitInstruction(RXGate(np.pi / 3), qubits=[0])
264275
raise ValueError(
265276
"Some parameter(s) in the given Qiskit circuit remain unused. "
266277
"This use case is not currently supported by the Quimb conversion code."
@@ -278,7 +289,7 @@ def recover_parameters_from_quimb(
278289
raise ValueError(
279290
"The length of the mapping in the provided QiskitQuimbConversionContext "
280291
"does not match the number of parametrized gates in the circuit "
281-
f"({len(mapping)}) vs. ({len(quimb_parametrized_gates)})."
292+
f"({len(mapping)} vs. {len(quimb_parametrized_gates)})."
282293
)
283294
# `(y - b) / m` is the inversion of the parameter expression, which we
284295
# assumed above to be in the form mx + b.
@@ -292,6 +303,20 @@ def _preprocess_for_gradient(objective, settings: QuimbSimulator):
292303
"Gradient method unspecified. Please specify an autodiff_backend "
293304
"for the QuimbSimulator object."
294305
)
306+
if objective._ansatz is not None:
307+
ansatz_num_qubits = objective._ansatz.num_qubits
308+
target = objective._target_tensornetwork
309+
try:
310+
# As implemented by quimb.tensor.Circuit
311+
target_num_qubits = target.N
312+
except AttributeError: # pragma: no cover
313+
# As implemented by quimb.tensor.TensorNetworkGen
314+
target_num_qubits = target.nsites
315+
if ansatz_num_qubits != target_num_qubits:
316+
raise ValueError(
317+
"Ansatz and target have different numbers of qubits "
318+
f"({ansatz_num_qubits} vs. {target_num_qubits})."
319+
)
295320
if settings.autodiff_backend == "explicit":
296321
# FIXME: error if target and/or settings could result in non-MPS, in
297322
# order to prevent a later MethodError from plum

test/simulation/aer/test_aer_backend.py

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212

1313
import numpy as np
1414
import pytest
15-
from qiskit import QuantumCircuit
1615
from qiskit.circuit.library import CXGate, XGate
1716
from qiskit.providers.basic_provider import BasicSimulator
1817

@@ -32,23 +31,6 @@
3231
pytestmark = pytest.mark.skipif(skip_aer_tests, reason="qiskit-aer is not installed")
3332

3433

35-
@pytest.fixture
36-
def bell_qc():
37-
qc = QuantumCircuit(2)
38-
qc.h(0)
39-
qc.cx(0, 1)
40-
return qc
41-
42-
43-
@pytest.fixture
44-
def ghz_qc():
45-
qc = QuantumCircuit(3)
46-
qc.h(0)
47-
qc.cx(0, 1)
48-
qc.cx(1, 2)
49-
return qc
50-
51-
5234
class TestQiskitAerBackend:
5335
def test_bell_circuit(self, bell_qc, AerSimulator):
5436
simulator = AerSimulator(method="matrix_product_state")
@@ -57,12 +39,6 @@ def test_bell_circuit(self, bell_qc, AerSimulator):
5739
bell_mps2 = tensornetwork_from_circuit(bell_qc, settings)
5840
assert compute_overlap(bell_mps1, bell_mps2) == pytest.approx(1)
5941

60-
def test_bell_circuit_statevector(self, bell_qc, AerSimulator):
61-
simulator = AerSimulator(method="matrix_product_state")
62-
out_state = np.zeros([4], dtype=complex)
63-
tensornetwork_from_circuit(bell_qc, simulator, out_state=out_state)
64-
assert out_state == pytest.approx(np.array([1, 0, 0, 1]) / np.sqrt(2))
65-
6642
def test_bell_circuit_log(self, bell_qc, AerSimulator):
6743
simulator = AerSimulator(method="matrix_product_state", mps_log_data=True)
6844
all_log_data: list[str] = []

test/simulation/conftest.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# This code is a Qiskit project.
2+
#
3+
# (C) Copyright IBM 2024.
4+
#
5+
# This code is licensed under the Apache License, Version 2.0. You may
6+
# obtain a copy of this license in the LICENSE.txt file in the root directory
7+
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
8+
#
9+
# Any modifications or derivative works of this code must retain this
10+
# copyright notice, and modified files need to carry a notice indicating
11+
# that they have been altered from the originals.
12+
import pytest
13+
from qiskit import QuantumCircuit
14+
15+
16+
@pytest.fixture
17+
def bell_qc():
18+
qc = QuantumCircuit(2)
19+
qc.h(0)
20+
qc.cx(0, 1)
21+
return qc
22+
23+
24+
@pytest.fixture
25+
def ghz_qc():
26+
qc = QuantumCircuit(3)
27+
qc.h(0)
28+
qc.cx(0, 1)
29+
qc.cx(1, 2)
30+
return qc

test/simulation/quimb/test_quimb_backend.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from qiskit.circuit import Parameter, QuantumCircuit
1717
from qiskit.circuit.library import CXGate, RXGate, XXPlusYYGate
1818

19+
from qiskit_addon_aqc_tensor.objective import MaximizeStateFidelity
1920
from qiskit_addon_aqc_tensor.simulation import (
2021
compute_overlap,
2122
tensornetwork_from_circuit,
@@ -115,3 +116,32 @@ def test_repeated_parameter(self):
115116
e_info.value.args[0]
116117
== "Parameter cannot be repeated in circuit, else quimb will attempt to optimize each instance separately."
117118
)
119+
120+
def test_unspecified_gradient_method(self, quimb):
121+
settings = QuimbSimulator(quimb.tensor.CircuitMPS)
122+
qc = QuantumCircuit(1)
123+
x = Parameter("x")
124+
qc.rx(x, 0)
125+
with pytest.raises(ValueError) as e_info:
126+
MaximizeStateFidelity(None, qc, settings)
127+
assert (
128+
e_info.value.args[0]
129+
== "Gradient method unspecified. Please specify an autodiff_backend for the QuimbSimulator object."
130+
)
131+
132+
def test_recovery_num_parameters_mismatch_error(self):
133+
x = Parameter("x")
134+
y = Parameter("y")
135+
qc1 = QuantumCircuit(1)
136+
qc1.rx(1 - x, 0)
137+
qc2 = QuantumCircuit(1)
138+
qc2.rx(1 - x, 0)
139+
qc2.ry(1 - 0.5 * y, 0)
140+
circ1, _ = qiskit_ansatz_to_quimb(qc1, [0.5])
141+
_, ctx2 = qiskit_ansatz_to_quimb(qc2, [0.5, 0.3])
142+
with pytest.raises(ValueError) as e_info:
143+
recover_parameters_from_quimb(circ1, ctx2)
144+
assert (
145+
e_info.value.args[0]
146+
== "The length of the mapping in the provided QiskitQuimbConversionContext does not match the number of parametrized gates in the circuit (2 vs. 1)."
147+
)

test/simulation/test_gradient.py

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
_compute_objective_and_gradient,
2929
_preprocess_for_gradient,
3030
)
31+
from qiskit_addon_aqc_tensor.simulation.aer import is_aer_available
3132
from qiskit_addon_aqc_tensor.simulation.explicit_gradient import _basis_gates
3233

3334

@@ -70,7 +71,7 @@ def _generate_random_ansatz(num_qubits: int):
7071
su2_gates=su2_gates,
7172
reps=random.randint(1, 2),
7273
parameter_prefix="c",
73-
insert_barriers=_get_random_bool(),
74+
insert_barriers=True,
7475
),
7576
inplace=True,
7677
copy=False,
@@ -129,3 +130,52 @@ def vdagger_rhs(thetas):
129130
numerical_grad = -2 * np.real(np.conj(f_0) * numerical_grad)
130131

131132
assert grad == pytest.approx(numerical_grad, abs=1e-4)
133+
134+
135+
@pytest.mark.skipif(not is_aer_available(), reason="qiskit-aer is not installed")
136+
class TestExplicitGradient:
137+
def test_no_parameters_throws_error(self, AerSimulator):
138+
settings = AerSimulator(method="matrix_product_state")
139+
tn = tensornetwork_from_circuit(QuantumCircuit(1), settings)
140+
qc = QuantumCircuit(1)
141+
with pytest.raises(ValueError) as e_info:
142+
MaximizeStateFidelity(tn, qc, settings)
143+
assert (
144+
e_info.value.args[0]
145+
== "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."
146+
)
147+
148+
def test_non_basis_gate(self, AerSimulator):
149+
settings = AerSimulator(method="matrix_product_state")
150+
tn = tensornetwork_from_circuit(QuantumCircuit(2), settings)
151+
qc = QuantumCircuit(2)
152+
x = Parameter("x")
153+
qc.rx(x, 0)
154+
qc.cp(np.pi / 5, 0, 1)
155+
with pytest.raises(ValueError) as e_info:
156+
MaximizeStateFidelity(tn, qc, settings)
157+
assert (
158+
e_info.value.args[0]
159+
== "Expects a gate from the list of basis ones: ['cx', 'cz', 'ecr', 'h', 'rx', 'ry', 'rz', 's', 'sdg', 'x', 'y', 'z'], got 'cp' instead."
160+
)
161+
162+
def test_one_qubit_parametrized_pauli_error_messages(self, subtests, AerSimulator):
163+
settings = AerSimulator(method="matrix_product_state")
164+
tn = tensornetwork_from_circuit(QuantumCircuit(1), settings)
165+
x = Parameter("x")
166+
y = Parameter("y")
167+
with subtests.test("Expression with multiple parameters"):
168+
qc = QuantumCircuit(1)
169+
qc.rx(x + y, 0)
170+
with pytest.raises(ValueError) as e_info:
171+
MaximizeStateFidelity(tn, qc, settings)
172+
assert e_info.value.args[0] == "Expression cannot contain more than one Parameter"
173+
with subtests.test("Nonlinear expression"):
174+
qc = QuantumCircuit(1)
175+
qc.rx(x**3, 0)
176+
with pytest.raises(ValueError) as e_info:
177+
MaximizeStateFidelity(tn, qc, settings)
178+
assert (
179+
e_info.value.args[0]
180+
== "ParameterExpression's derivative must be a floating-point number, i.e., the expression must be in the form ax + b."
181+
)

test/simulation/test_statevector.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# This code is a Qiskit project.
2+
#
3+
# (C) Copyright IBM 2024.
4+
#
5+
# This code is licensed under the Apache License, Version 2.0. You may
6+
# obtain a copy of this license in the LICENSE.txt file in the root directory
7+
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
8+
#
9+
# Any modifications or derivative works of this code must retain this
10+
# copyright notice, and modified files need to carry a notice indicating
11+
# that they have been altered from the originals.
12+
13+
import numpy as np
14+
import pytest
15+
from qiskit import QuantumCircuit
16+
from qiskit.quantum_info import Statevector
17+
18+
from qiskit_addon_aqc_tensor.simulation import (
19+
tensornetwork_from_circuit,
20+
)
21+
22+
23+
class TestExactStatevector:
24+
def test_bell_circuit_statevector(self, bell_qc, available_backend_fixture):
25+
out_state = np.zeros([4], dtype=complex)
26+
tensornetwork_from_circuit(bell_qc, available_backend_fixture, out_state=out_state)
27+
assert out_state == pytest.approx(np.array([1, 0, 0, 1]) / np.sqrt(2))
28+
29+
def test_random_circuit_statevector(self, available_backend_fixture):
30+
qc = QuantumCircuit(3)
31+
qc.h(0)
32+
qc.cx(0, 1)
33+
qc.ryy(0.3, 1, 2)
34+
qc.rxx(0.7, 0, 2)
35+
out_state = np.zeros([8], dtype=complex)
36+
tensornetwork_from_circuit(qc, available_backend_fixture, out_state=out_state)
37+
abs(np.vdot(Statevector(qc).data, out_state)) == pytest.approx(1)

test/test_objective.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# This code is a Qiskit project.
2+
#
3+
# (C) Copyright IBM 2025.
4+
#
5+
# This code is licensed under the Apache License, Version 2.0. You may
6+
# obtain a copy of this license in the LICENSE.txt file in the root directory
7+
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
8+
#
9+
# Any modifications or derivative works of this code must retain this
10+
# copyright notice, and modified files need to carry a notice indicating
11+
# that they have been altered from the originals.
12+
13+
14+
import pytest
15+
from qiskit import QuantumCircuit
16+
from qiskit.circuit import Parameter
17+
18+
from qiskit_addon_aqc_tensor.objective import MaximizeStateFidelity
19+
from qiskit_addon_aqc_tensor.simulation import (
20+
tensornetwork_from_circuit,
21+
)
22+
23+
24+
# pylint: disable=no-self-use
25+
class TestObjective:
26+
def test_ansatz_target_qubit_mismatch(self, available_backend_fixture):
27+
settings = available_backend_fixture
28+
target = tensornetwork_from_circuit(QuantumCircuit(2), settings)
29+
x = Parameter("x")
30+
ansatz = QuantumCircuit(1)
31+
ansatz.rx(x, 0)
32+
with pytest.raises(ValueError) as e_info:
33+
MaximizeStateFidelity(target, ansatz, settings)
34+
assert (
35+
e_info.value.args[0] == "Ansatz and target have different numbers of qubits (1 vs. 2)."
36+
)

0 commit comments

Comments
 (0)