Skip to content

Commit afa1cbc

Browse files
committed
✨ Expose approximate equivalence checking to Python
1 parent 2dc38c4 commit afa1cbc

File tree

6 files changed

+114
-2
lines changed

6 files changed

+114
-2
lines changed

include/EquivalenceCheckingManager.hpp

+7-1
Original file line numberDiff line numberDiff line change
@@ -219,10 +219,16 @@ class EquivalenceCheckingManager {
219219
void setTraceThreshold(double traceThreshold) {
220220
configuration.functionality.traceThreshold = traceThreshold;
221221
}
222+
void setApproximateCheckingThreshold(double approximateCheckingThreshold) {
223+
configuration.functionality.approximateCheckingThreshold =
224+
approximateCheckingThreshold;
225+
}
222226
void setCheckPartialEquivalence(bool checkPE) {
223227
configuration.functionality.checkPartialEquivalence = checkPE;
224228
}
225-
229+
void setCheckApproximateEquivalence(bool checkAE) {
230+
configuration.functionality.checkApproximateEquivalence = checkAE;
231+
}
226232
// Simulation: These setting may be changed to adjust the kinds of simulations
227233
// that are performed
228234
void setFidelityThreshold(double fidelityThreshold) {

src/Configuration.cpp

+4
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,11 @@ nlohmann::basic_json<> Configuration::json() const {
106106
if (execution.runConstructionChecker || execution.runAlternatingChecker) {
107107
auto& fun = config["functionality"];
108108
fun["trace_threshold"] = functionality.traceThreshold;
109+
fun["approximate_checking_threshold"] =
110+
functionality.approximateCheckingThreshold;
109111
fun["check_partial_equivalence"] = functionality.checkPartialEquivalence;
112+
fun["check_approximate_equivalence"] =
113+
functionality.checkApproximateEquivalence;
110114
}
111115

112116
if (execution.runSimulationChecker) {

src/mqt/qcec/configuration.py

+2
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ class ConfigurationOptions(TypedDict, total=False):
3232
timeout: float
3333
# Functionality
3434
trace_threshold: float
35+
approximate_checking_threshold: float
3536
check_partial_equivalence: bool
37+
check_approximate_equivalence: bool
3638
# Optimizations
3739
backpropagate_output_permutation: bool
3840
elide_permutations: bool

src/mqt/qcec/pyqcec.pyi

+4
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ class Configuration:
4848

4949
class Functionality:
5050
trace_threshold: float
51+
approximate_checking_threshold: float
5152
check_partial_equivalence: bool
53+
check_approximate_equivalence: bool
5254
def __init__(self) -> None: ...
5355

5456
class Optimizations:
@@ -144,7 +146,9 @@ class EquivalenceCheckingManager:
144146
def set_timeout(self, timeout: float = ...) -> None: ...
145147
def set_tolerance(self, tolerance: float = ...) -> None: ...
146148
def set_trace_threshold(self, threshold: float = ...) -> None: ...
149+
def set_approximate_checking_threshold(self, threshold: float = ...) -> None: ...
147150
def set_check_partial_equivalence(self, enable: bool = ...) -> None: ...
151+
def set_check_approximate_equivalence(self, enable: bool = ...) -> None: ...
148152
def set_zx_checker(self, enable: bool = ...) -> None: ...
149153
def store_cex_input(self, enable: bool = ...) -> None: ...
150154
def store_cex_output(self, enable: bool = ...) -> None: ...

src/python/bindings.cpp

+41-1
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,12 @@ PYBIND11_MODULE(pyqcec, m, py::mod_gil_not_used()) {
322322
"Set the :attr:`trace threshold "
323323
"<.Configuration.Functionality.trace_threshold>` used for comparing "
324324
"two unitaries or functionality matrices.")
325+
.def("set_approximate_checking_threshold",
326+
&EquivalenceCheckingManager::setApproximateCheckingThreshold,
327+
"threshold"_a = 1e-8,
328+
"Set the :attr:`approximate checking threshold "
329+
"<.Configuration.Functionality.approximate_checking_threshold>` "
330+
"used for approximate equivalence checking.")
325331
.def(
326332
"set_check_partial_equivalence",
327333
&EquivalenceCheckingManager::setCheckPartialEquivalence,
@@ -331,6 +337,16 @@ PYBIND11_MODULE(pyqcec, m, py::mod_gil_not_used()) {
331337
"they have the same probability for each measurement outcome. "
332338
"If set to false, the checker will output 'not equivalent' for "
333339
"circuits that are partially equivalent but not totally equivalent. ")
340+
.def("set_check_approximate_equivalence",
341+
&EquivalenceCheckingManager::setCheckApproximateEquivalence,
342+
"enable"_a = false,
343+
"Set whether to check for approximate equivalence. Two circuits are "
344+
"approximately equivalent if the Hilbert-Schmidt inner product "
345+
"(process distance) of the two circuits is less than the "
346+
"approximate_checking_threshold. "
347+
"If set to false, the checker will output 'not equivalent' for "
348+
"circuits that are approximately equivalent but not totally "
349+
"equivalent. ")
334350
// Simulation
335351
.def("set_fidelity_threshold",
336352
&EquivalenceCheckingManager::setFidelityThreshold,
@@ -668,6 +684,15 @@ PYBIND11_MODULE(pyqcec, m, py::mod_gil_not_used()) {
668684
"Whenever any decision diagram node differs from this structure by "
669685
"more than the configured threshold, the circuits are concluded to "
670686
"be non-equivalent. Defaults to :code:`1e-8`.")
687+
.def_readwrite(
688+
"approximate_checking_threshold",
689+
&Configuration::Functionality::approximateCheckingThreshold,
690+
"To determine approximate equivalence, the Hilbert-Schmidt inner "
691+
"product (or process distance) between two circuit representations "
692+
"is calculated and compared against the "
693+
"approximate_checking_threshold. If the process distance falls below "
694+
"this threshold, the circuits are considered approximately "
695+
"equivalent. Defaults to :code:`1e-8`.")
671696
.def_readwrite(
672697
"check_partial_equivalence",
673698
&Configuration::Functionality::checkPartialEquivalence,
@@ -679,6 +704,21 @@ PYBIND11_MODULE(pyqcec, m, py::mod_gil_not_used()) {
679704
"output 'not equivalent' for circuits that are partially equivalent "
680705
"but not totally equivalent. In particular, garbage qubits will be "
681706
"treated as if they were measured qubits. Defaults to "
707+
":code:`False`.")
708+
.def_readwrite(
709+
"check_approximate_equivalence",
710+
&Configuration::Functionality::checkApproximateEquivalence,
711+
"To determine approximate equivalence, the Hilbert-Schmidt inner "
712+
"product (or process distance) between two circuit representations "
713+
"is calculated and compared against the "
714+
"approximate_checking_threshold. If the process distance falls below "
715+
"this threshold, the circuits are considered approximately "
716+
"equivalent. If set to :code:`True`, a check for approximate "
717+
"equivalence "
718+
"will be performed. If set to :code:`False`, the checker will "
719+
"output 'not equivalent' for circuits that are approximately "
720+
"equivalent "
721+
"but not totally equivalent. Defaults to "
682722
":code:`False`.");
683723

684724
// simulation options
@@ -752,5 +792,5 @@ PYBIND11_MODULE(pyqcec, m, py::mod_gil_not_used()) {
752792
"to simpler equivalence checking instances as the random "
753793
"instantiation. This option "
754794
"changes how many of those additional checks are performed.");
755-
}
795+
};
756796
} // namespace ec
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
"""Tests the partial equivalence checking support of QCEC."""
2+
3+
from __future__ import annotations
4+
5+
import pytest
6+
from qiskit import QuantumCircuit
7+
8+
from mqt import qcec
9+
10+
11+
@pytest.fixture
12+
def original_circuit() -> QuantumCircuit:
13+
"""Fixture for a simple circuit."""
14+
qc = QuantumCircuit(3)
15+
qc.mcx([0, 1], 2)
16+
return qc
17+
18+
19+
@pytest.fixture
20+
def alternative_circuit() -> QuantumCircuit:
21+
"""Fixture for an approximately equivalent version of the simple circuit."""
22+
qc = QuantumCircuit(3)
23+
qc.id(0)
24+
qc.id(1)
25+
qc.id(2)
26+
return qc
27+
28+
29+
def test_configuration_pec(original_circuit: QuantumCircuit, alternative_circuit: QuantumCircuit) -> None:
30+
"""Test if the flag for approximate equivalence checking works."""
31+
config = qcec.Configuration()
32+
config.execution.run_alternating_checker = True
33+
config.execution.run_construction_checker = False
34+
config.execution.run_simulation_checker = False
35+
config.execution.run_zx_checker = False
36+
config.functionality.check_approximate_equivalence = True
37+
config.functionality.approximate_checking_threshold = 0.3
38+
result = qcec.verify(original_circuit, alternative_circuit, configuration=config)
39+
assert result.equivalence == qcec.EquivalenceCriterion.equivalent
40+
41+
42+
def test_argument_pec(original_circuit: QuantumCircuit, alternative_circuit: QuantumCircuit) -> None:
43+
"""Test if the flag for approximate equivalence checking works."""
44+
config = qcec.Configuration()
45+
config.execution.run_alternating_checker = True
46+
config.execution.run_construction_checker = False
47+
config.execution.run_simulation_checker = False
48+
config.execution.run_zx_checker = False
49+
result = qcec.verify(
50+
original_circuit,
51+
alternative_circuit,
52+
configuration=config,
53+
check_approximate_equivalence=True,
54+
approximate_checking_threshold=0.3,
55+
)
56+
assert result.equivalence == qcec.EquivalenceCriterion.equivalent

0 commit comments

Comments
 (0)