Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Partial equivalence checking #487

Closed
wants to merge 68 commits into from
Closed
Show file tree
Hide file tree
Changes from 54 commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
481d039
:sparkles: added function to shift the lines of a matrix
reb-ddm Nov 1, 2023
7dd727d
Merge branch 'main' into partial-equivalence-checking
reb-ddm Nov 1, 2023
8ca46a6
extern changes
reb-ddm Nov 1, 2023
3c7f3d2
Merge branch 'main' into partial-equivalence-checking
reb-ddm Nov 4, 2023
f233da2
:construction: First draft of partial equivalence check function, it …
reb-ddm Nov 9, 2023
ac88851
Merge branch 'main' into partial-equivalence-checking
reb-ddm Nov 9, 2023
26253c0
:bug: increased number of qubits in the tests
reb-ddm Nov 9, 2023
b3698d6
:art: improve the structure and comments of the code
reb-ddm Nov 10, 2023
74b8ba5
:twisted_rightwards_arrows: Merge branch 'main' into partial-equivale…
reb-ddm Nov 10, 2023
e590e81
:twisted_rightwards_arrows: Merge branch 'main' into partial-equivale…
reb-ddm Nov 15, 2023
507e8cd
:sparkles: added partial equivalence function that swaps measured qub…
reb-ddm Nov 21, 2023
43f43b5
:zap: Added compute table for setRowsToZero and setColumnsToZero
reb-ddm Nov 23, 2023
1f94d72
:zap: added alternative partial equivalence checker for circuits with…
reb-ddm Nov 27, 2023
dd144c6
:fire: removed redundant tests
reb-ddm Nov 27, 2023
f2d866b
:twisted_rightwards_arrows: Merge branch 'main' into partial-equivale…
reb-ddm Nov 27, 2023
54009b2
:fire: Removed unused include
reb-ddm Nov 27, 2023
d3e886f
:fire: Removed debug print statement and fixed a few comments
reb-ddm Nov 27, 2023
4c45cbe
⏪ revert unwanted submodule changes
burgholzer Nov 27, 2023
bdd50a4
:twisted_rightwards_arrows: Merge branch 'main' into partial-equivale…
reb-ddm Nov 29, 2023
27d781b
:sparkles: Added optional flags to buildFunctionality
reb-ddm Nov 29, 2023
b780cb6
:rotating_light: :white_check_mark: improved lint and code coverage
reb-ddm Nov 30, 2023
d1b4db0
:bug: Fixed bug in helper functions that gave a wrong result for non-…
reb-ddm Dec 6, 2023
1ba5ed1
:bug::zap: Added two compute tables and fixed the elements added to t…
reb-ddm Dec 11, 2023
c10d450
:white_check_mark: added tests and benchmarks for partialEquivalenceC…
reb-ddm Dec 11, 2023
5c67802
Merge branch 'main' into partial-equivalence-checking
reb-ddm Dec 11, 2023
345ef11
:construction: initial draft for generating random benchmarks
reb-ddm Dec 31, 2023
400c13d
implemented PEC algorithm for inverse qubit order and modified tests …
reb-ddm Jan 4, 2024
884bd4e
changes requested in pull request
reb-ddm Jan 4, 2024
244872c
implemented zeroAncilla PEC for new qubit order
reb-ddm Jan 4, 2024
2062f5b
add compute table for shiftAllRows
reb-ddm Jan 6, 2024
ab22a2d
:mute: remove debug cout
reb-ddm Jan 6, 2024
34bd850
:recycle: rename some tests and a compute table
reb-ddm Jan 6, 2024
4d6bc7e
:test_tube: add failing test
reb-ddm Jan 6, 2024
cf9d721
:bug: fixed swaps in PEC
reb-ddm Jan 6, 2024
7ca81b4
:twisted_rightwards_arrows: Merge with main, but now mcx gates are no…
reb-ddm Jan 6, 2024
9b6f206
:construction: first draft of random benchmark generation
reb-ddm Jan 8, 2024
10725bf
Merge branch 'main' into partial-equivalence-checking
reb-ddm Jan 8, 2024
9088192
:art: more consistency with naming of ancilla/ancillae/ancillary qubits
reb-ddm Jan 8, 2024
b1b70f3
:rotating_light: fix linter warnings
reb-ddm Jan 8, 2024
29e2bf6
:fire: remove exception that was only there for debugging purposes
reb-ddm Jan 8, 2024
ca5ca9d
:rotating_light: make some variables const
reb-ddm Jan 8, 2024
0ea0a74
Merge branch 'partial-equivalence-checking' into random-benchmarks
reb-ddm Jan 10, 2024
24fa752
:bug: fixed bug in partialEquivalenceCheck for m=0
reb-ddm Jan 13, 2024
8ce1564
:construction: almost finished implementation of random circuit gener…
reb-ddm Jan 13, 2024
9ae6095
🔀 Merge branch 'main' into partial-equivalence-checking
reb-ddm Jan 16, 2024
098ee90
:recycle: adapt code to use the new isCloseToIdentity function for ze…
reb-ddm Jan 16, 2024
85e242d
🔀Merge branch 'partial-equivalence-checking' into random-benchmarks
reb-ddm Jan 16, 2024
c9ce87a
:bug: bug fix in circuit generation
reb-ddm Jan 22, 2024
dc81bd6
add some pre-generated circuits
reb-ddm Jan 24, 2024
90cbadd
:art: clean up code
reb-ddm Jan 29, 2024
37b5f69
:bug: fix printed message
reb-ddm Jan 29, 2024
72b9b2b
🚚 move verification file to algorithms/
reb-ddm Jan 29, 2024
6926e4e
Merge pull request #1 from reb-ddm/random-benchmarks
reb-ddm Jan 29, 2024
4e9bfde
🔀 Merge branch 'main' into partial-equivalence-checking
reb-ddm Jan 31, 2024
ccd0954
🚨 made everything const
reb-ddm Feb 1, 2024
7e2e9cc
✅ added test for invalid argument
reb-ddm Feb 1, 2024
d9d9922
🎨 changed some names of the tests
reb-ddm Feb 1, 2024
dbaef3e
:art: used qc::Qubit instead of dd::Qubit and used modern C++ random …
reb-ddm Feb 16, 2024
d1a93f8
🔀 Merge branch 'main' into partial-equivalence-checking
reb-ddm Feb 16, 2024
043923c
🚨 made everything const
reb-ddm Feb 16, 2024
ad315c0
:art: moved some functions from Package.hpp to Verification.hpp and r…
reb-ddm Feb 17, 2024
646a044
:fire: removed zeroAncillaPEC and modified corresponding tests
reb-ddm Feb 19, 2024
a562092
:fire: remove redundant cast
reb-ddm Feb 21, 2024
29144ea
:fire: removed all partialEquivalence functions, s.t. they can be mov…
reb-ddm Feb 21, 2024
8a070c2
:fire: removed redundant includes
reb-ddm Feb 21, 2024
c0f5431
✅ added tests for adding multiple ancillaries and garbage
reb-ddm Feb 21, 2024
38f4b46
🔀 Merge branch 'main' into partial-equivalence-checking
reb-ddm Feb 27, 2024
72e1e7c
🔥 removed functions for shifting rows
reb-ddm Mar 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions include/mqt-core/QuantumComputation.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,14 @@ class QuantumComputation {
[[nodiscard]] std::size_t getNqubitsWithoutAncillae() const {
return nqubits;
}
[[nodiscard]] std::size_t getNmeasuredQubits() const {
return static_cast<std::size_t>(
std::count(getGarbage().begin(), getGarbage().end(), false));
reb-ddm marked this conversation as resolved.
Show resolved Hide resolved
}
[[nodiscard]] std::size_t getNgarbageQubits() const {
return static_cast<std::size_t>(
std::count(getGarbage().begin(), getGarbage().end(), true));
}
[[nodiscard]] std::size_t getNcbits() const { return nclassics; }
[[nodiscard]] std::string getName() const { return name; }
[[nodiscard]] const QuantumRegisterMap& getQregs() const { return qregs; }
Expand Down
8 changes: 6 additions & 2 deletions include/mqt-core/dd/FunctionalityConstruction.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@ namespace dd {
using namespace qc;

template <class Config>
MatrixDD buildFunctionality(const QuantumComputation* qc, Package<Config>& dd);
MatrixDD buildFunctionality(const QuantumComputation* qc, Package<Config>& dd,
bool reduceAncillaryQubits = true,
bool reduceGarbageQubits = true);

template <class Config>
MatrixDD buildFunctionalityRecursive(const QuantumComputation* qc,
Package<Config>& dd);
Package<Config>& dd,
bool reduceAncillaryQubits = true,
bool reduceGarbageQubits = true);

template <class Config>
bool buildFunctionalityRecursive(const QuantumComputation* qc,
Expand Down
249 changes: 249 additions & 0 deletions include/mqt-core/dd/Package.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -2439,6 +2439,255 @@ template <class Config> class Package {
nodes[index] = r.p;
return r;
}

// only keeps the first 2^d columns
mEdge setColumnsToZero(const mEdge& e, Qubit d) {
if (e.isTerminal()) {
return e;
}
// the matrix of the current DD has dimensions 2^n x 2^n
const auto n = e.p->v + 1;

if (d >= n) {
return e;
}

std::array<mEdge, NEDGE> edges{};
edges[0] = setColumnsToZero(e.p->e[0], d);
edges[1] = mEdge::zero();
edges[2] = setColumnsToZero(e.p->e[2], d);
edges[3] = mEdge::zero();

auto f = makeDDNode(e.p->v, edges);
f.w = cn.lookup(e.w * f.w);
return f;
}

mEdge keepOnlyIthRow(const mEdge& e, std::int64_t i) {
if (e.isZeroTerminal()) {
return e;
}
if (e.isTerminal()) {
if (i == 0) {
return e;
}
return mEdge::zero();
}
// the matrix of the current DD has dimensions 2^n x 2^n
const auto n = e.p->v + 1;
const auto twoPowNMinusOne = 1 << (n - 1);
std::array<mEdge, NEDGE> edges{};
if (i < twoPowNMinusOne) {
edges[0] = keepOnlyIthRow(e.p->e[0], i);
edges[1] = keepOnlyIthRow(e.p->e[1], i);
edges[2] = mEdge::zero();
edges[3] = mEdge::zero();
} else {
edges[0] = mEdge::zero();
edges[1] = mEdge::zero();
edges[2] = keepOnlyIthRow(e.p->e[2], i - twoPowNMinusOne);
edges[3] = keepOnlyIthRow(e.p->e[3], i - twoPowNMinusOne);
}

auto f = makeDDNode(e.p->v, edges);
f.w = cn.lookup(e.w * f.w);
return f;
}

UnaryComputeTable<mEdge, mEdge> shiftAllMatrixRows{};
Qubit shiftAllMatrixRowsM{0};
Qubit shiftAllMatrixRowsD{0};

/**
Equally divides the rows of the matrix represented by e (of size 2^n x 2^n)
into 2^g parts of size 2^m (where g = n - m).
For each part the (upperOffset + i)-th row is shifted by i*2^d columns.
**/
mEdge shiftAllRowsRecursive(const mEdge& e, Qubit m, Qubit d,
std::int64_t upperOffset) {
if (e.isTerminal() && upperOffset == 0) {
return e;
}
if (e.isTerminal()) {
return mEdge::zero();
}
if (upperOffset == 0 && m == 0) {
return e;
}

// the matrix of the current DD has dimensions 2^n x 2^n
const auto n = e.p->v + 1;
if (upperOffset >= (1 << n) || upperOffset < 0) {
return mEdge::zero();
}

const mEdge eCopy{e.p, Complex::one()};
// check if it's in the compute table with an edge weight of one
// only store elements in the compute table if the offset is 0
if (upperOffset == 0) {
if (const auto* r = shiftAllMatrixRows.lookup(eCopy); r != nullptr) {
auto f = *r;
f.w = cn.lookup(e.w * f.w);
return f;
}
}

if (d >= n && m >= n) {
return keepOnlyIthRow(e, upperOffset);
}

std::array<mEdge, NEDGE> edges{};
auto originalUpperOffset = upperOffset;

// if the current submatrix size is less than 2^m,
// then the offset of the lower half needs to be adapted
const bool adaptOffsetOfLowerMatrixHalf = n <= m;
edges[0] = shiftAllRowsRecursive(e.p->e[0], m, d, upperOffset);
if (n <= d) {
// CASE 1: this (sub)matrix has size < 2^d
// -> we need to keep all the columns of this submatrix
edges[1] = shiftAllRowsRecursive(e.p->e[1], m, d, upperOffset);
if (adaptOffsetOfLowerMatrixHalf) {
upperOffset -= (1 << (n - 1));
}
edges[2] = shiftAllRowsRecursive(e.p->e[2], m, d, upperOffset);
edges[3] = shiftAllRowsRecursive(e.p->e[3], m, d, upperOffset);
} else {
const std::int64_t addedOffset{1 << (n - 1 - d)};
if (upperOffset + addedOffset < (1 << m)) {
// CASE 2: this submatrix doesn't contain ancilla qubits
// -> we don't need to set any columns to zero
edges[1] =
shiftAllRowsRecursive(e.p->e[0], m, d, upperOffset + addedOffset);

if (adaptOffsetOfLowerMatrixHalf) {
upperOffset -= (1 << (n - 1));
}
edges[2] = shiftAllRowsRecursive(e.p->e[2], m, d, upperOffset);
edges[3] =
shiftAllRowsRecursive(e.p->e[2], m, d, upperOffset + addedOffset);
} else {
// CASE 3: the right half of the matrix represents the ancilla qubits,
// therefore it is set to zero
edges[1] = mEdge::zero();
if (adaptOffsetOfLowerMatrixHalf) {
upperOffset -= (1 << (n - 1));
}
edges[2] = shiftAllRowsRecursive(e.p->e[2], m, d, upperOffset);
edges[3] = mEdge::zero();
}
}

auto f = makeDDNode(e.p->v, edges);
// add to the compute table with a weight of 1
if (originalUpperOffset == 0) {
shiftAllMatrixRows.insert(eCopy, f);
}
f.w = cn.lookup(e.w * f.w);
return f;
}

public:
/**
Equally divides the rows of the matrix represented by e into parts
of size 2^m.
For each part we keep only the first 2^d columns
and the i-th row of each part is shifted by i*2^d columns.
**/
mEdge shiftAllRows(const mEdge& e, Qubit m, Qubit d) {
if (shiftAllMatrixRowsM != m || shiftAllMatrixRowsD != d) {
shiftAllMatrixRows.clear();
shiftAllMatrixRowsM = m;
shiftAllMatrixRowsD = d;
}
return shiftAllRowsRecursive(e, m, d, 0);
}

private:
mEdge partialEquivalenceCheckSubroutine(mEdge u, Qubit m, Qubit k,
Qubit extra) {
// add extra ancillary qubits
if (extra > 0) {
if (u.p->v + 1U + extra > nqubits) {
resize(u.p->v + 1U + extra);
}
u = kronecker(makeIdent(extra), u);
}
const auto n = static_cast<Qubit>(u.p->v + 1);
Qubit d = n - k;
u = setColumnsToZero(u, d);
auto u2 = shiftAllRows(u, m, d);
return multiply(conjugateTranspose(u), u2);
}

public:
/**
Checks for partial equivalence between the two circuits u1 and u2,
where the first d qubits of the circuits are the data qubits and
the first m qubits are the measured qubits.
@param u1 DD representation of first circuit
@param u2 DD representation of second circuit
@param d Number of data qubits
@param m Number of measured qubits
@return true if the two circuits u1 and u2 are partially equivalent.
**/
bool partialEquivalenceCheck(mEdge u1, mEdge u2, Qubit d, Qubit m) {
if (m == 0) {
return true;
}
if (u1.isTerminal() && u2.isTerminal()) {
return u1 == u2;
}
if (u1.isZeroTerminal() || u2.isZeroTerminal()) {
return false;
}
// add qubits such that u1 and u2 have the same dimension
if (u1.isTerminal()) {
auto w = u1.w;
u1 = makeIdent(u2.p->v + 1);
u1.w = w;
} else if (u2.isTerminal()) {
auto w = u2.w;
u2 = makeIdent(u1.p->v + 1);
u2.w = w;
} else if (u1.p->v < u2.p->v) {
u1 = kronecker(makeIdent(u2.p->v - u1.p->v), u1);
} else if (u1.p->v > u2.p->v) {
u2 = kronecker(makeIdent(u1.p->v - u2.p->v), u2);
}

const Qubit n = u1.p->v + 1;
Qubit k = n - d;
Qubit extra{0};
if (m > k) {
extra = m - k;
}
k = k + extra;

auto u1Prime = partialEquivalenceCheckSubroutine(u1, m, k, extra);
auto u2Prime = partialEquivalenceCheckSubroutine(u2, m, k, extra);

return u1Prime == u2Prime;
}

/**
Checks for partial equivalence between the two circuits u1 and u2,
where all qubits of the circuits are the data qubits and
the first m qubits are the measured qubits.
@param u1 DD representation of first circuit
@param u2 DD representation of second circuit
@param m Number of measured qubits
@return true if the two circuits u1 and u2 are partially equivalent.
**/
bool zeroAncillaePartialEquivalenceCheck(mEdge u1, mEdge u2, Qubit m) {
auto u1u2 = multiply(u1, conjugateTranspose(u2));
const Qubit n = u1.p->v + 1;
std::vector<bool> garbage(n, false);
for (size_t i = m; i < n; i++) {
garbage[i] = true;
}
return isCloseToIdentity(u1u2, 1.0E-10, garbage, false);
}
};

} // namespace dd
94 changes: 94 additions & 0 deletions include/mqt-core/dd/Verification.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#include "dd/FunctionalityConstruction.hpp"
#include "dd/Package.hpp"

namespace dd {
/**
Checks for partial equivalence between the two circuits c1 and c2
that have no ancilla qubits.
Assumption: the input and output permutations are the same.

@param circuit1 First circuit
@param circuit2 Second circuit
@return true if the two circuits c1 and c2 are partially equivalent.
**/
template <class Config>
bool zeroAncillaePartialEquivalenceCheck(
qc::QuantumComputation c1, qc::QuantumComputation c2,
std::unique_ptr<dd::Package<Config>>& dd) {
if (c1.getNqubits() != c2.getNqubits() ||
c1.getGarbage() != c2.getGarbage()) {
throw std::runtime_error(
"The circuits need to have the same number of qubits and the same "
"permutation of input and output qubits.");
}
c2.invert();
for (auto& gate : c1) {
c2.emplace_back(gate);
}

auto u = buildFunctionality(&c2, *dd, false, false);

return dd->isCloseToIdentity(u, 1.0E-10, c1.getGarbage(), false);
}
// get next garbage qubit after n
inline Qubit getNextGarbage(Qubit n, const std::vector<bool>& garbage) {
while (n < static_cast<Qubit>(garbage.size()) && !garbage.at(n)) {
n++;
}
return n;
}
/**
Checks for partial equivalence between the two circuits c1 and c2.
Assumption: the data qubits are all at the beginning of the input qubits and
the input and output permutations are the same.

@param circuit1 First circuit
@param circuit2 Second circuit
@return true if the two circuits c1 and c2 are partially equivalent.
**/
template <class Config>
bool partialEquivalenceCheck(qc::QuantumComputation c1,
qc::QuantumComputation c2,
std::unique_ptr<dd::Package<Config>>& dd) {

auto d1 = c1.getNqubitsWithoutAncillae();
auto d2 = c2.getNqubitsWithoutAncillae();
auto m1 = c1.getNmeasuredQubits();
auto m2 = c2.getNmeasuredQubits();
if (m1 != m2 || d1 != d2) {
return false;
}
auto n1 = static_cast<Qubit>(c1.getNqubits());
auto n2 = static_cast<Qubit>(c2.getNqubits());
if (d1 == n1 && d2 == n2) {
// no ancilla qubits
return zeroAncillaePartialEquivalenceCheck(c1, c2, dd);
}
// add swaps in order to put the measured (= not garbage) qubits in the end
auto garbage1 = c1.getGarbage();

auto nextGarbage = getNextGarbage(0, garbage1);
// find the first garbage qubit at the end
for (std::int64_t i = std::min(n1, n2) - 1;
i >= static_cast<std::int64_t>(m1); i--) {
if (!garbage1.at(static_cast<Qubit>(i))) {
// swap it to the beginning
c1.swap(static_cast<Qubit>(i), nextGarbage);
c2.swap(static_cast<Qubit>(i), nextGarbage);
++nextGarbage;
nextGarbage = getNextGarbage(nextGarbage, garbage1);
}
}

// partialEquivalenceCheck with dd

auto u1 = buildFunctionality(&c1, *dd, false, false);
auto u2 = buildFunctionality(&c2, *dd, false, false);

return dd->partialEquivalenceCheck(u1, u2, static_cast<Qubit>(d1),
static_cast<Qubit>(m1));
}

std::pair<qc::QuantumComputation, qc::QuantumComputation>
generateRandomBenchmark(size_t n, Qubit d, Qubit m);
} // namespace dd
1 change: 1 addition & 0 deletions src/dd/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ if(NOT TARGET ${MQT_CORE_TARGET_NAME}-dd)
RealNumber.cpp
RealNumberUniqueTable.cpp
Simulation.cpp
Verification.cpp
statistics/MemoryManagerStatistics.cpp
statistics/Statistics.cpp
statistics/TableStatistics.cpp
Expand Down
Loading
Loading