Skip to content

Commit 566308b

Browse files
DennisKleinhansDennis KleinhansMoritzWillmann
committed
Increase test coverage (#357)
* add tests for `QCNNEncodingCircuit` * add tests for `QiskitEncodingCircuit` * add tests for `EncodingCircuitDerivatives` * extend tests for `PrunedEncodingCircuit` to cover the `automated_pruning` function * move files in test folders to match the source structure * add tests for the `SingleProbability` observable * add tests for `SummedProbabilities` observable * add tests for `ObservableBase` * add tests for all qnnLoss classes * add tests for `FidelityKernelStatevector` * add tests for `FidelityKernelExpectationValue` * add tests for `FidelityKernel` * add tests for `CustomObservable` and `IsingHamiltonian` * extended tests for `IsingHamiltonian` * add tests for `SinglePauli` and `SummedPaulis` * add tests for `Adam` and `ApproximatedGradients` * add tests for the wrapped optimizers (`SLSQP`, `LBFGSB`, `SPSA`) * add helper function to check equality of two qiskit circuits * extend the circuit tests to check the result circuits against a 'ground-truth circuit' * add tests to test the returned expectation value of the observables * add expectation value test for `SummedPaulis` * add expectation value test for `FidelityKerne` * adapt tests for qnn `ODELoss`: - adpat tests to the changed API - extend tests for order2 loss * Fix handling of optional weights in `SquaredLoss` and `CrossEntropyLoss` * ensure `NLL` returns a real float scalar instead of an array * use `.item()` to extract a scalar from an array instead of just `float(param)` * return list items instead of np arrays in the mocked return value * fix some loss outputs * move `qiskit_circuit_equivalence.py` into `/test` * move the`build_circuit` functions to the top-level as protected * add other objective function to test bounds clipping * add additional objective function to test * Refactor expectation value tests to use parameterized inputs for improved clarity and maintainability * add expected kernel as parameter * Standardized verification of the kernel matrix for identity matrix in the tests * black --------- Co-authored-by: Dennis Kleinhans <dennis.kleinhans@ipa.fraunhofer.de> Co-authored-by: Moritz <44642314+MoritzWillmann@users.noreply.github.com> Co-authored-by: Moritz <moritz.willmann@ipa.fraunhofer.de>
1 parent f8ba4fe commit 566308b

45 files changed

Lines changed: 4376 additions & 13 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/squlearn/encoding_circuit/encoding_circuit_derivatives.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ def helper_hash(diff):
192192
return self._optree_start.copy()
193193
else:
194194
# Check if differentiating tuple is already stored in optree_cache
195-
if self._optree_caching == True and helper_hash((diff_tuple,)) in self._optree_cache:
195+
if self._optree_caching is True and helper_hash((diff_tuple,)) in self._optree_cache:
196196
# If stored -> return
197197
return self._optree_cache[helper_hash((diff_tuple,))].copy()
198198
else:
@@ -201,7 +201,7 @@ def helper_hash(diff):
201201
self._differentiation_from_tuple(diff_tuple[1:]), diff_tuple[0]
202202
)
203203
# Store result in the optree_cache
204-
if self._optree_caching == True:
204+
if self._optree_caching is True:
205205
self._optree_cache[helper_hash((diff_tuple,))] = circ
206206
return circ
207207

@@ -229,7 +229,7 @@ def assign_parameters(
229229
self, optree: OpTreeElementBase, features: np.ndarray, parameters: np.ndarray
230230
) -> OpTreeElementBase:
231231
"""
232-
Assigns numerical values to the ParameterVector elements of the encoding circuit circuit.
232+
Assigns numerical values to the ParameterVector elements of the encoding circuit.
233233
234234
Args:
235235
optree (OperatorBase): OpTree object to be assigned.

src/squlearn/kernel/loss/negative_log_likelihood.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,6 @@ def compute(
8585
np.sum(np.log(np.diagonal(L)))
8686
+ 0.5 * labels.T @ S2
8787
+ 0.5 * len(data) * np.log(2.0 * np.pi)
88-
)
89-
neg_log_lh = neg_log_lh.reshape(-1)
88+
).reshape(-1)
9089

9190
return neg_log_lh

src/squlearn/optimizers/adam.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import abc
21
from collections import deque
32
import numpy as np
43

@@ -145,7 +144,7 @@ def minimize(
145144

146145
x_updated = self.step(x=self.x, grad=gradient)
147146

148-
if bounds != None:
147+
if bounds is not None:
149148
x_updated = np.clip(x_updated, bounds[:, 0], bounds[:, 1])
150149

151150
if self.log_file is not None:

src/squlearn/qnn/loss/cross_entropy_loss.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,11 @@ def value(self, value_dict: dict, **kwargs) -> float:
5454
raise AttributeError("CrossEntropyLoss requires ground_truth.")
5555

5656
ground_truth = kwargs["ground_truth"]
57-
weights = kwargs.get("weights") or np.ones_like(ground_truth)
57+
weights = (
58+
kwargs["weights"]
59+
if "weights" in kwargs and kwargs["weights"] is not None
60+
else np.ones_like(ground_truth)
61+
)
5862

5963
probability_values = np.clip(value_dict["f"], self._eps, 1.0 - self._eps)
6064
if probability_values.ndim == 1:
@@ -94,7 +98,11 @@ def gradient(
9498
raise AttributeError("CrossEntropyLoss requires ground_truth.")
9599

96100
ground_truth = kwargs["ground_truth"]
97-
weights = kwargs.get("weights") or np.ones_like(ground_truth)
101+
weights = (
102+
kwargs["weights"]
103+
if "weights" in kwargs and kwargs["weights"] is not None
104+
else np.ones_like(ground_truth)
105+
)
98106
multiple_output = kwargs.get("multiple_output", False)
99107

100108
probability_values = np.clip(value_dict["f"], self._eps, 1.0 - self._eps)

src/squlearn/qnn/loss/squared_loss.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,11 @@ def value(self, value_dict: dict, **kwargs) -> float:
5353
if "ground_truth" not in kwargs:
5454
raise AttributeError("SquaredLoss requires ground_truth.")
5555
ground_truth = kwargs["ground_truth"]
56-
weights = kwargs.get("weights") or np.ones_like(ground_truth)
56+
weights = (
57+
kwargs["weights"]
58+
if "weights" in kwargs and kwargs["weights"] is not None
59+
else np.ones_like(ground_truth)
60+
)
5761
return np.sum(np.multiply(np.square(value_dict["f"] - ground_truth), weights))
5862

5963
def variance(self, value_dict: dict, **kwargs) -> float:
@@ -76,7 +80,11 @@ def variance(self, value_dict: dict, **kwargs) -> float:
7680
if "ground_truth" not in kwargs:
7781
raise AttributeError("SquaredLoss requires ground_truth.")
7882
ground_truth = kwargs["ground_truth"]
79-
weights = kwargs.get("weights") or np.ones_like(ground_truth)
83+
weights = (
84+
kwargs["weights"]
85+
if "weights" in kwargs and kwargs["weights"] is not None
86+
else np.ones_like(ground_truth)
87+
)
8088

8189
diff_square = np.multiply(weights, np.square(value_dict["f"] - ground_truth))
8290
return np.sum(4 * np.multiply(diff_square, value_dict["var"]))

tests/encoding_circuit/circuit_library/test_chebyshev_pqc.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,76 @@
66
from squlearn.encoding_circuit.encoding_circuit_base import EncodingSlotsMismatchError
77
from squlearn.kernel.lowlevel_kernel import FidelityKernel
88
from squlearn.kernel import QGPR
9+
from tests.qiskit_circuit_equivalence import assert_circuits_equal
10+
11+
12+
def _build_expected_chebyshev_circuit(
13+
num_qubits: int,
14+
num_layers: int,
15+
closed: bool,
16+
entangling_gate: str,
17+
nonlinearity: str,
18+
features: np.ndarray,
19+
parameters: np.ndarray,
20+
):
21+
if nonlinearity == "arccos":
22+
23+
def mapping(a, x):
24+
return a * np.arccos(x)
25+
26+
else:
27+
28+
def mapping(a, x):
29+
return a * np.arctan(x)
30+
31+
QC = QuantumCircuit(num_qubits)
32+
index_offset = 0
33+
feature_offset = 0
34+
35+
if entangling_gate not in ["crz", "rzz"]:
36+
raise ValueError("Unknown entangling gate")
37+
38+
# basis change at beginning
39+
for i in range(num_qubits):
40+
QC.ry(parameters[index_offset % len(parameters)], i)
41+
index_offset += 1
42+
43+
for _ in range(num_layers):
44+
# chebyshev rx encodings
45+
for i in range(num_qubits):
46+
QC.rx(
47+
mapping(
48+
parameters[index_offset % len(parameters)],
49+
features[feature_offset % len(features)],
50+
),
51+
i,
52+
)
53+
index_offset += 1
54+
feature_offset += 1
55+
56+
# even pairs (0,1), (2,3), ...
57+
for i in range(0, num_qubits + (1 if closed else 0) - 1, 2):
58+
if entangling_gate == "crz":
59+
QC.crz(parameters[index_offset % len(parameters)], i, (i + 1) % num_qubits)
60+
else:
61+
QC.rzz(parameters[index_offset % len(parameters)], i, (i + 1) % num_qubits)
62+
index_offset += 1
63+
64+
# odd pairs (1,2), (3,4), ...
65+
if num_qubits > 2:
66+
for i in range(1, num_qubits + (1 if closed else 0) - 1, 2):
67+
if entangling_gate == "crz":
68+
QC.crz(parameters[index_offset % len(parameters)], i, (i + 1) % num_qubits)
69+
else:
70+
QC.rzz(parameters[index_offset % len(parameters)], i, (i + 1) % num_qubits)
71+
index_offset += 1
72+
73+
# final basis change
74+
for i in range(num_qubits):
75+
QC.ry(parameters[index_offset % len(parameters)], i)
76+
index_offset += 1
77+
78+
return QC
979

1080

1181
class TestChebyshevPQC:
@@ -133,3 +203,45 @@ def test_feature_consistency(self):
133203

134204
with pytest.raises(ValueError):
135205
circuit.get_circuit(features, params)
206+
207+
@pytest.mark.parametrize(
208+
"num_qubits,num_layers,closed,entangling_gate,nonlinearity",
209+
[
210+
(2, 1, True, "crz", "arccos"),
211+
(3, 1, False, "crz", "arccos"),
212+
(3, 2, True, "rzz", "arctan"),
213+
(4, 2, False, "rzz", "arccos"),
214+
],
215+
)
216+
def test_get_circuit_matches_ground_truth(
217+
self, num_qubits, num_layers, closed, entangling_gate, nonlinearity
218+
):
219+
220+
circuit = ChebyshevPQC(
221+
num_qubits=num_qubits,
222+
num_layers=num_layers,
223+
closed=closed,
224+
entangling_gate=entangling_gate,
225+
nonlinearity=nonlinearity,
226+
)
227+
228+
num_encoding_slots = circuit.num_encoding_slots
229+
num_features = min(2, num_encoding_slots) if num_encoding_slots >= 2 else 1
230+
features = np.linspace(-0.9, 0.9, num_features)
231+
232+
rng = np.random.RandomState(42)
233+
parameters = rng.uniform(-np.pi, np.pi, size=circuit.num_parameters)
234+
235+
qc_actual = circuit.get_circuit(features=features, parameters=parameters)
236+
237+
qc_expected = _build_expected_chebyshev_circuit(
238+
num_qubits=num_qubits,
239+
num_layers=num_layers,
240+
closed=closed,
241+
entangling_gate=entangling_gate,
242+
nonlinearity=nonlinearity,
243+
features=features,
244+
parameters=parameters,
245+
)
246+
247+
assert_circuits_equal(qc_actual, qc_expected)

tests/encoding_circuit/circuit_library/test_chebyshev_rx.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,58 @@
77
from squlearn.encoding_circuit.encoding_circuit_base import EncodingSlotsMismatchError
88
from squlearn.kernel.lowlevel_kernel import FidelityKernel
99
from squlearn.kernel import QGPR
10+
from tests.qiskit_circuit_equivalence import assert_circuits_equal
11+
12+
13+
def _build_expected_chebyshev_rx_circuit(
14+
num_qubits: int,
15+
num_layers: int,
16+
closed: bool,
17+
nonlinearity: str,
18+
features: np.ndarray,
19+
parameters: np.ndarray,
20+
):
21+
if nonlinearity == "arccos":
22+
23+
def mapping(a, x):
24+
return a * np.arccos(x)
25+
26+
else:
27+
28+
def mapping(a, x):
29+
return a * np.arctan(x)
30+
31+
QC = QuantumCircuit(num_qubits)
32+
index_offset = 0
33+
feature_offset = 0
34+
35+
def entangle_layer_local(QC_local: QuantumCircuit) -> QuantumCircuit:
36+
for i in range(0, num_qubits + (1 if closed else 0) - 1, 2):
37+
QC_local.cx(i, (i + 1) % num_qubits)
38+
if num_qubits > 2:
39+
for i in range(1, num_qubits + (1 if closed else 0) - 1, 2):
40+
QC_local.cx(i, (i + 1) % num_qubits)
41+
return QC_local
42+
43+
for _ in range(num_layers):
44+
for i in range(num_qubits):
45+
QC.rx(
46+
mapping(
47+
parameters[index_offset % len(parameters)],
48+
features[feature_offset % len(features)],
49+
),
50+
i,
51+
)
52+
index_offset += 1
53+
feature_offset += 1
54+
55+
for i in range(num_qubits):
56+
QC.rx(parameters[index_offset % len(parameters)], i)
57+
index_offset += 1
58+
59+
QC = entangle_layer_local(QC)
60+
61+
return QC
1062

1163

1264
class TestChebyshevRx:
@@ -124,3 +176,43 @@ def test_feature_consistency(self):
124176

125177
with pytest.raises(ValueError):
126178
circuit.get_circuit(features, params)
179+
180+
@pytest.mark.parametrize(
181+
"num_qubits,num_layers,closed,nonlinearity",
182+
[
183+
(2, 1, False, "arccos"),
184+
(3, 1, True, "arccos"),
185+
(3, 2, False, "arctan"),
186+
(4, 2, True, "arctan"),
187+
],
188+
)
189+
def test_chebyshev_rx_get_circuit_matches_ground_truth(
190+
self, num_qubits, num_layers, closed, nonlinearity
191+
):
192+
193+
circuit = ChebyshevRx(
194+
num_qubits=num_qubits,
195+
num_layers=num_layers,
196+
closed=closed,
197+
nonlinearity=nonlinearity,
198+
)
199+
200+
num_encoding_slots = circuit.num_encoding_slots
201+
num_features = min(2, num_encoding_slots) if num_encoding_slots >= 2 else 1
202+
features = np.linspace(-0.9, 0.9, num_features)
203+
204+
rng = np.random.RandomState(42)
205+
parameters = rng.uniform(-np.pi, np.pi, size=circuit.num_parameters)
206+
207+
qc_actual = circuit.get_circuit(features=features, parameters=parameters)
208+
209+
qc_expected = _build_expected_chebyshev_rx_circuit(
210+
num_qubits=num_qubits,
211+
num_layers=num_layers,
212+
closed=closed,
213+
nonlinearity=nonlinearity,
214+
features=features,
215+
parameters=parameters,
216+
)
217+
218+
assert_circuits_equal(qc_actual, qc_expected)

0 commit comments

Comments
 (0)