diff --git a/.gitignore b/.gitignore index b2cb12437..98238b11b 100644 --- a/.gitignore +++ b/.gitignore @@ -53,6 +53,7 @@ coverage.xml .hypothesis/ .pytest_cache/ cover/ +*.profraw # Translations *.mo diff --git a/include/mqt-core/CircuitOptimizer.hpp b/include/mqt-core/CircuitOptimizer.hpp index 0cc8d1074..63f631b70 100644 --- a/include/mqt-core/CircuitOptimizer.hpp +++ b/include/mqt-core/CircuitOptimizer.hpp @@ -10,9 +10,6 @@ #include namespace qc { -static constexpr std::array DIAGONAL_GATES = { - qc::Barrier, qc::I, qc::Z, qc::S, qc::Sdg, - qc::T, qc::Tdg, qc::P, qc::RZ, qc::RZZ}; class CircuitOptimizer { protected: diff --git a/include/mqt-core/Definitions.hpp b/include/mqt-core/Definitions.hpp index 1b020f4b3..0fd0e0716 100644 --- a/include/mqt-core/Definitions.hpp +++ b/include/mqt-core/Definitions.hpp @@ -120,6 +120,13 @@ constexpr void hashCombine(std::size_t& hash, const std::size_t with) noexcept { hash = combineHash(hash, with); } +/// Pairs do not provide a hash function by default, this is the replacement +template struct PairHash { + size_t operator()(const std::pair& x) const { + return combineHash(std::hash{}(x.first), std::hash{}(x.second)); + } +}; + /** * @brief Function used to mark unreachable code * @details Uses compiler specific extensions if possible. Even if no extension diff --git a/include/mqt-core/datastructures/DirectedAcyclicGraph.hpp b/include/mqt-core/datastructures/DirectedAcyclicGraph.hpp new file mode 100644 index 000000000..aee1f2576 --- /dev/null +++ b/include/mqt-core/datastructures/DirectedAcyclicGraph.hpp @@ -0,0 +1,107 @@ +#pragma once + +#include "Definitions.hpp" +#include "DirectedGraph.hpp" + +#include +#include +#include +#include +#include +#include + +namespace qc { + +template class DirectedAcyclicGraph final : public DirectedGraph { +protected: + // transitive closure matrix to detect cycles + std::vector> closureMatrix; + +public: + auto addVertex(const V& v) -> void override { + DirectedGraph::addVertex(v); + for (auto& row : closureMatrix) { + row.emplace_back(false); + } + closureMatrix.emplace_back(this->nVertices, false); + const auto i = this->mapping.at(v); + closureMatrix[i][i] = true; + } + auto addEdge(const V& u, const V& v) -> void override { + if (this->mapping.find(u) == this->mapping.end()) { + addVertex(u); + } + if (this->mapping.find(v) == this->mapping.end()) { + addVertex(v); + } + std::size_t const i = this->mapping.at(u); + std::size_t const j = this->mapping.at(v); + if (closureMatrix[j][i]) { + std::ostringstream oss; + oss << "Adding edge (" << u << ", " << v << ") would create a cycle."; + throw std::logic_error(oss.str()); + } + DirectedGraph::addEdge(u, v); + for (std::size_t k = 0; k < this->nVertices; ++k) { + if (closureMatrix[k][i]) { + closureMatrix[k][j] = true; + } + if (closureMatrix[j][k]) { + closureMatrix[i][k] = true; + } + } + } + [[nodiscard]] auto isReachable(const V& u, const V& v) const -> bool { + if (this->mapping.find(u) == this->mapping.end()) { + throw std::invalid_argument("Vertex u not in graph."); + } + if (this->mapping.find(v) == this->mapping.end()) { + throw std::invalid_argument("Vertex v not in graph."); + } + return closureMatrix[this->mapping.at(u)][this->mapping.at(v)]; + } + /// Perform a depth-first search on the graph and return the nodes in a + /// topological order + [[nodiscard]] auto orderTopologically() const -> std::vector { + std::stack stack{}; + std::vector result; + result.reserve(this->nVertices); + std::vector visited(this->nVertices, false); + // visitedInDegree is used to count the incoming edges that have been + // visited already such that the resulting order of the nodes is one that + // satisfies a topological ordering + std::vector visitedInDegree(this->nVertices, 0); + // Push nodes with 0 indegree onto the stack + for (std::size_t k = 0; k < this->nVertices; ++k) { + if (this->inDegrees[k] == 0) { + stack.push(k); + visited[k] = true; + } + } + // Perform DFS + while (!stack.empty()) { + const auto u = stack.top(); + stack.pop(); + result.emplace_back(u); + + for (std::size_t k = 0; k < this->nVertices; ++k) { + if (this->adjacencyMatrix[u][k]) { + if (!visited[k]) { + if (++visitedInDegree[k] == this->inDegrees[k]) { + stack.push(k); + visited[k] = true; + } + } + } + } + } + // Otherwise graph has a cycle + assert(result.size() == this->nVertices); + std::vector vertices; + vertices.reserve(this->nVertices); + std::transform(result.cbegin(), result.cend(), std::back_inserter(vertices), + [&](const auto i) { return this->invMapping.at(i); }); + return vertices; + } +}; +} // namespace qc diff --git a/include/mqt-core/datastructures/DirectedGraph.hpp b/include/mqt-core/datastructures/DirectedGraph.hpp new file mode 100644 index 000000000..d03969d89 --- /dev/null +++ b/include/mqt-core/datastructures/DirectedGraph.hpp @@ -0,0 +1,138 @@ +#pragma once + +#include "Definitions.hpp" + +#include +#include +#include + +namespace qc { + +/** + * Class representing generic directed graphs. + * + * @tparam V the type of the vertices in the graph. Must implement `operator<<`. + */ +template class DirectedGraph { + static_assert( + std::is_same() << std::declval()), + std::ostream&>::value, + "V must support `operator<<`."); + +protected: + // the adjecency matrix works with indices + std::vector> adjacencyMatrix; + // the mapping of vertices to indices in the graph are stored in a map + std::unordered_map mapping; + // the inverse mapping is used to get the vertex from the index + std::vector invMapping; + // the number of vertices in the graph + std::size_t nVertices = 0; + // the number of edges in the graph + std::size_t nEdges = 0; + // the in-degrees of the vertices in the graph + std::vector inDegrees; + // the out-degrees of the vertices in the graph + std::vector outDegrees; + +public: + virtual ~DirectedGraph() = default; + virtual auto addVertex(const V& v) -> void { + // check whether the vertex is already in the graph, if so do nothing + if (mapping.find(v) != mapping.end()) { + std::stringstream ss; + ss << "The vertex " << v << " is already in the graph."; + throw std::invalid_argument(ss.str()); + } + mapping[v] = nVertices; + invMapping.emplace_back(v); + ++nVertices; + for (auto& row : adjacencyMatrix) { + row.emplace_back(false); + } + adjacencyMatrix.emplace_back(nVertices, false); + inDegrees.emplace_back(0); + outDegrees.emplace_back(0); + } + virtual auto addEdge(const V& u, const V& v) -> void { + if (mapping.find(u) == mapping.end()) { + addVertex(u); + } + if (mapping.find(v) == mapping.end()) { + addVertex(v); + } + const auto i = mapping.at(u); + const auto j = mapping.at(v); + if (!adjacencyMatrix[i][j]) { + adjacencyMatrix[i][j] = true; + ++outDegrees[i]; + ++inDegrees[j]; + ++nEdges; + } + } + [[nodiscard]] auto getNVertices() const -> std::size_t { return nVertices; } + [[nodiscard]] auto getNEdges() const -> std::size_t { return nEdges; } + [[nodiscard]] auto getInDegree(const V& v) const -> std::size_t { + if (mapping.find(v) == mapping.end()) { + std::stringstream ss; + ss << "The vertex " << v << " is not in the graph."; + throw std::invalid_argument(ss.str()); + } + const auto i = mapping.at(v); + return inDegrees[i]; + } + [[nodiscard]] auto getOutDegree(const V& v) const -> std::size_t { + if (mapping.find(v) == mapping.end()) { + std::stringstream ss; + ss << "The vertex " << v << " is not in the graph."; + throw std::invalid_argument(ss.str()); + } + const auto i = mapping.at(v); + return outDegrees[i]; + } + [[nodiscard]] auto getVertices() const -> std::unordered_set { + return std::accumulate(mapping.cbegin(), mapping.cend(), + std::unordered_set(), + [](auto& acc, const auto& v) { + acc.emplace(v.first); + return acc; + }); + } + [[nodiscard]] auto isEdge(const V& u, const V& v) const -> bool { + if (mapping.find(u) == mapping.end()) { + std::stringstream ss; + ss << "The vertex " << u << " is not in the graph."; + throw std::invalid_argument(ss.str()); + } + if (mapping.find(v) == mapping.end()) { + std::stringstream ss; + ss << "The vertex " << v << " is not in the graph."; + throw std::invalid_argument(ss.str()); + } + const auto i = mapping.at(u); + const auto j = mapping.at(v); + return adjacencyMatrix[i][j]; + } + /// Outputs a string representation of the graph in the DOT format + [[nodiscard]] auto toString() const -> std::string { + std::stringstream ss; + ss << "digraph {\n"; + for (const auto& [v, i] : mapping) { + ss << " " << i << " [label=\"" << v << "\"];\n"; + } + for (std::size_t i = 0; i < nVertices; ++i) { + for (std::size_t j = 0; j < nVertices; ++j) { + if (adjacencyMatrix[i][j]) { + ss << " " << i << " -> " << j << ";\n"; + } + } + } + ss << "}\n"; + return ss.str(); + } + friend auto operator<<(std::ostream& os, + const DirectedGraph& g) -> std::ostream& { + return os << g.toString(); // Using toString() method + } +}; +} // namespace qc diff --git a/include/mqt-core/datastructures/DisjointSet.hpp b/include/mqt-core/datastructures/DisjointSet.hpp new file mode 100644 index 000000000..ded76839a --- /dev/null +++ b/include/mqt-core/datastructures/DisjointSet.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include +#include + +namespace qc { +template struct DisjointSet { + std::unordered_map parent; + std::unordered_map rank; + + template + explicit DisjointSet(const Iterator& begin, const Iterator& end) { + std::for_each(begin, end, [&](const auto& element) { + parent[element] = element; + rank[element] = 0; + }); + } + + T findSet(const T& v) { + if (parent[v] != v) { + parent[v] = findSet(parent[v]); + } + return parent[v]; + } + + void unionSet(const T& x, const T& y) { + const auto& xSet = findSet(x); + const auto& ySet = findSet(y); + if (rank[xSet] > rank[ySet]) { + parent[ySet] = xSet; + } else { + parent[xSet] = ySet; + if (rank[xSet] == rank[ySet]) { + ++rank[ySet]; + } + } + } +}; +} // namespace qc diff --git a/include/mqt-core/datastructures/Layer.hpp b/include/mqt-core/datastructures/Layer.hpp new file mode 100644 index 000000000..978bea57d --- /dev/null +++ b/include/mqt-core/datastructures/Layer.hpp @@ -0,0 +1,142 @@ +#pragma once + +#include "Definitions.hpp" +#include "QuantumComputation.hpp" +#include "datastructures/UndirectedGraph.hpp" +#include "operations/OpType.hpp" +#include "operations/Operation.hpp" + +#include +#include +#include +#include +#include +#include + +namespace qc { +/** + * @brief Class to manage the creation of layers when traversing a quantum + * circuit. + * @details The class uses the DAG of the circuit to create layers of gates that + * can be executed at the same time. It can be used to create the front or look + * ahead layer. + */ +class Layer final { +public: + class DAGVertex; + using ExecutableSet = std::unordered_set>; + using InteractionGraph = UndirectedGraph>; + + class DAGVertex : public std::enable_shared_from_this { + protected: + // if the executableCounter becomes equal to the executableThreshold the + // vertex becomes executable + std::int64_t executableThreshold = 0; + std::int64_t executableCounter = 0; + std::vector> enabledSuccessors; + std::vector> disabledSuccessors; + bool executed = false; + Operation* operation; + ExecutableSet* executableSet; + + /** + * @brief Construct a new DAGVertex + * @details The constructor initializes the vertex with the given operation + * and the given executable set, which is shared among all vertices. + * + * @param op + * @param es + */ + DAGVertex(Operation* op, ExecutableSet& es) + : operation(op), executableSet(&es) {} + + public: + [[nodiscard]] static auto + create(Operation* operation, + ExecutableSet& executableSet) -> std::shared_ptr { + std::shared_ptr v(new DAGVertex(operation, executableSet)); + v->updateExecutableSet(); + return v; + } + [[nodiscard]] auto isExecutable() const { + assert(executableCounter <= executableThreshold); + return (!executed) && executableCounter == executableThreshold; + } + [[nodiscard]] auto isExecuted() const { return executed; } + [[nodiscard]] auto getOperation() const -> const Operation* { + return operation; + } + + protected: + auto incExecutableCounter() { + executableCounter++; + updateExecutableSet(); + } + auto decExecutableCounter() { + executableCounter--; + updateExecutableSet(); + } + /** + * @brief Inserts or removes the vertex from the executable set. + * @warning May not be called from the constructor. + */ + auto updateExecutableSet() -> void { + if (isExecutable()) { + if (const auto& it = executableSet->find(shared_from_this()); + it == executableSet->end()) { + executableSet->insert(shared_from_this()); + } + } else { + if (const auto& it = executableSet->find(shared_from_this()); + it != executableSet->end()) { + executableSet->erase(it); + } + } + } + + public: + auto execute() -> void { + if (isExecutable()) { + executed = true; + for (const auto& successor : disabledSuccessors) { + successor->decExecutableCounter(); + } + for (const auto& successor : enabledSuccessors) { + successor->incExecutableCounter(); + } + updateExecutableSet(); + } else { + throw std::logic_error( + "The vertex is not executable and cannot be executed."); + } + } + auto addEnabledSuccessor(const std::shared_ptr& successor) { + enabledSuccessors.emplace_back(successor); + ++successor->executableThreshold; + successor->updateExecutableSet(); + } + auto addDisabledSuccessor(const std::shared_ptr& successor) { + disabledSuccessors.emplace_back(successor); + --successor->executableThreshold; + successor->updateExecutableSet(); + } + }; + +protected: + ExecutableSet executableSet; + auto constructDAG(const QuantumComputation& qc) -> void; + +public: + Layer() = default; + ~Layer() = default; + explicit Layer(const QuantumComputation& qc) { constructDAG(qc); } + [[nodiscard]] auto getExecutableSet() const -> const ExecutableSet& { + return executableSet; + } + [[nodiscard]] auto + constructInteractionGraph(OpType opType, + std::size_t nControls) const -> InteractionGraph; + [[nodiscard]] auto getExecutablesOfType(OpType opType, std::size_t nControls) + const -> std::vector>; +}; +} // namespace qc diff --git a/include/mqt-core/datastructures/UndirectedGraph.hpp b/include/mqt-core/datastructures/UndirectedGraph.hpp new file mode 100644 index 000000000..76640c026 --- /dev/null +++ b/include/mqt-core/datastructures/UndirectedGraph.hpp @@ -0,0 +1,194 @@ +#pragma once + +#include "Definitions.hpp" + +#include +#include +#include +#include + +namespace qc { + +/** + * Class representing generic undirected directed graphs. + * + * @tparam V the type of the vertices in the graph. Must implement `operator<<`. + */ +template class UndirectedGraph final { + static_assert(std::is_same_v() + << std::declval()), + std::ostream&>, + "V must support `operator<<`."); + +protected: + // the adjecency matrix works with indices + std::vector>> adjacencyMatrix{}; + // the mapping of vertices to indices in the graph are stored in a map + std::unordered_map mapping; + // the inverse mapping is used to get the vertex from the index + std::vector invMapping; + // the number of vertices in the graph + std::size_t nVertices = 0; + // the number of edges in the graph + std::size_t nEdges = 0; + // the degrees of the vertices in the graph + std::vector degrees; + +public: + auto addVertex(const V& v) -> void { + // check whether the vertex is already in the graph, if so do nothing + if (mapping.find(v) == mapping.end()) { + mapping[v] = nVertices; + invMapping.emplace_back(v); + ++nVertices; + for (auto& row : adjacencyMatrix) { + row.emplace_back(std::nullopt); + } + // the first param must be a 1 not nVertices since we are using an upper + // triangular matrix as adjacency matrix instead of a square matrix + adjacencyMatrix.emplace_back(1, std::nullopt); + degrees.emplace_back(0); + } else { + std::stringstream ss; + ss << "The vertex " << v << " is already in the graph."; + throw std::invalid_argument(ss.str()); + } + } + auto addEdge(const V& u, const V& v, const E& e) -> void { + if (mapping.find(u) == mapping.end()) { + addVertex(u); + } + if (mapping.find(v) == mapping.end()) { + addVertex(v); + } + const auto i = mapping.at(u); + const auto j = mapping.at(v); + if (i < j) { + if (adjacencyMatrix[i][j - i] == std::nullopt) { + ++degrees[i]; + if (i != j) { + ++degrees[j]; + } + ++nEdges; + } + adjacencyMatrix[i][j - i] = e; + } else { + if (adjacencyMatrix[j][i - j] == std::nullopt) { + ++degrees[i]; + if (i != j) { + ++degrees[j]; + } + ++nEdges; + } + adjacencyMatrix[j][i - j] = e; + } + } + [[nodiscard]] auto getNVertices() const -> std::size_t { return nVertices; } + [[nodiscard]] auto getNEdges() const -> std::size_t { return nEdges; } + [[nodiscard]] auto getEdge(const V& v, const V& u) const -> E { + const auto i = mapping.at(v); + const auto j = mapping.at(u); + if (i < j ? adjacencyMatrix[i][j - i] != std::nullopt + : adjacencyMatrix[j][i - j] != std::nullopt) { + return i < j ? adjacencyMatrix[i][j - i].value() + : adjacencyMatrix[j][i - j].value(); + } + std::stringstream ss; + ss << "The edge (" << v << ", " << u << ") does not exist."; + throw std::invalid_argument(ss.str()); + } + [[nodiscard]] auto getAdjacentEdges(const V& v) const + -> std::unordered_set, PairHash> { + if (mapping.find(v) == mapping.end()) { + std::stringstream ss; + ss << "The vertex " << v << " is not in the graph."; + throw std::invalid_argument(ss.str()); + } + const auto i = mapping.at(v); + std::unordered_set, PairHash> result; + for (std::size_t j = 0; j < nVertices; ++j) { + if (i < j ? adjacencyMatrix[i][j - i] != std::nullopt + : adjacencyMatrix[j][i - j] != std::nullopt) { + const auto u = invMapping.at(j); + result.emplace(std::make_pair(v, u)); + } + } + return result; + } + [[nodiscard]] auto getNeighbours(const V& v) const -> std::unordered_set { + if (mapping.find(v) == mapping.end()) { + std::stringstream ss; + ss << "The vertex " << v << " is not in the graph."; + throw std::invalid_argument(ss.str()); + } + const auto i = mapping.at(v); + std::unordered_set result; + for (std::size_t j = 0; j < nVertices; ++j) { + if (i < j ? adjacencyMatrix[i][j - i] != std::nullopt + : adjacencyMatrix[j][i - j] != std::nullopt) { + result.emplace(invMapping.at(j)); + } + } + return result; + } + [[nodiscard]] auto getDegree(const V& v) const -> std::size_t { + if (mapping.find(v) == mapping.end()) { + std::stringstream ss; + ss << "The vertex " << v << " is not in the graph."; + throw std::invalid_argument(ss.str()); + } + const auto i = mapping.at(v); + return degrees[i]; + } + [[nodiscard]] auto getVertices() const -> std::unordered_set { + return std::accumulate(mapping.cbegin(), mapping.cend(), + std::unordered_set(), + [](auto& acc, const auto& v) { + acc.emplace(v.first); + return acc; + }); + } + [[nodiscard]] auto isAdjacent(const V& u, const V& v) const -> bool { + if (mapping.find(u) == mapping.end()) { + std::stringstream ss; + ss << "The vertex " << u << " is not in the graph."; + throw std::invalid_argument(ss.str()); + } + if (mapping.find(v) == mapping.end()) { + std::stringstream ss; + ss << "The vertex " << v << " is not in the graph."; + throw std::invalid_argument(ss.str()); + } + const auto i = mapping.at(u); + const auto j = mapping.at(v); + return (i < j && adjacencyMatrix[i][j - i] != std::nullopt) or + (j < i && adjacencyMatrix[j][i - j] != std::nullopt); + } + [[nodiscard]] static auto isAdjacentEdge(const std::pair& e, + const std::pair& f) -> bool { + return e.first == f.first || e.first == f.second || e.second == f.first || + e.second == f.second; + } + /// Outputs a string representation of the graph in the DOT format + [[nodiscard]] auto toString() const -> std::string { + std::stringstream ss; + ss << "graph {\n"; + for (const auto& [v, i] : mapping) { + ss << " " << i << " [label=\"" << v << "\"];\n"; + } + for (std::size_t i = 0; i < nVertices; ++i) { + for (std::size_t j = i + 1; j < nVertices; ++j) { + if (adjacencyMatrix[i][j - i] != std::nullopt) { + ss << " " << i << " -- " << j << ";\n"; + } + } + } + ss << "}\n"; + return ss.str(); + } + friend auto operator<<(std::ostream& os, + const UndirectedGraph& g) -> std::ostream& { + return os << g.toString(); // Using toString() method + } +}; +} // namespace qc diff --git a/include/mqt-core/na/NAComputation.hpp b/include/mqt-core/na/NAComputation.hpp new file mode 100644 index 000000000..00fd7d7bf --- /dev/null +++ b/include/mqt-core/na/NAComputation.hpp @@ -0,0 +1,92 @@ +#pragma once + +#include "na/NADefinitions.hpp" +#include "na/operations/NAOperation.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace na { +class NAComputation { +protected: + std::vector> initialPositions; + std::vector> operations; + +public: + NAComputation() = default; + NAComputation(NAComputation&& qc) noexcept = default; + NAComputation& operator=(NAComputation&& qc) noexcept = default; + NAComputation(const NAComputation& qc) + : initialPositions(qc.initialPositions) { + operations.reserve(qc.operations.size()); + std::transform(qc.operations.cbegin(), qc.operations.cend(), + std::back_inserter(operations), + [](const auto& op) { return op->clone(); }); + } + NAComputation& operator=(const NAComputation& qc) { + if (this != &qc) { + initialPositions = qc.initialPositions; + operations.clear(); + operations.reserve(qc.operations.size()); + std::transform(qc.operations.cbegin(), qc.operations.cend(), + std::back_inserter(operations), + [](const auto& op) { return op->clone(); }); + } + return *this; + } + virtual ~NAComputation() = default; + template auto emplaceBack(std::unique_ptr&& op) -> void { + static_assert(std::is_base_of_v, + "T must be a subclass of NAOperation."); + operations.emplace_back(std::move(op)); + } + template auto emplaceBack(const std::unique_ptr& op) -> void { + static_assert(std::is_base_of_v, + "T must be a subclass of NAOperation."); + operations.emplace_back(std::move(op)); + } + template auto emplaceBack(Args&&... args) -> void { + static_assert(std::is_base_of_v, + "T must be a subclass of NAOperation."); + operations.emplace_back(std::make_unique(args...)); + } + auto clear(const bool clearInitialPositions = true) -> void { + operations.clear(); + initialPositions.clear(); + if (clearInitialPositions) { + initialPositions.clear(); + } + } + [[nodiscard]] auto size() const -> std::size_t { return operations.size(); } + [[nodiscard]] auto + getInitialPositions() const -> const std::vector>& { + return initialPositions; + } + auto emplaceInitialPosition(std::shared_ptr p) -> void { + initialPositions.emplace_back(std::move(p)); + } + [[nodiscard]] auto toString() const -> std::string; + friend auto operator<<(std::ostream& os, + const NAComputation& qc) -> std::ostream& { + return os << qc.toString(); + } + // Iterators (pass-through) + auto begin() noexcept { return operations.begin(); } + [[nodiscard]] auto begin() const noexcept { return operations.begin(); } + [[nodiscard]] auto cbegin() const noexcept { return operations.cbegin(); } + auto end() noexcept { return operations.end(); } + [[nodiscard]] auto end() const noexcept { return operations.end(); } + [[nodiscard]] auto cend() const noexcept { return operations.cend(); } + auto rbegin() noexcept { return operations.rbegin(); } + [[nodiscard]] auto rbegin() const noexcept { return operations.rbegin(); } + [[nodiscard]] auto crbegin() const noexcept { return operations.crbegin(); } + auto rend() noexcept { return operations.rend(); } + [[nodiscard]] auto rend() const noexcept { return operations.rend(); } + [[nodiscard]] auto crend() const noexcept { return operations.crend(); } +}; +} // namespace na diff --git a/include/mqt-core/na/NADefinitions.hpp b/include/mqt-core/na/NADefinitions.hpp new file mode 100644 index 000000000..8a50504b6 --- /dev/null +++ b/include/mqt-core/na/NADefinitions.hpp @@ -0,0 +1,101 @@ +#pragma once + +#include "Definitions.hpp" +#include "operations/CompoundOperation.hpp" +#include "operations/OpType.hpp" +#include "operations/Operation.hpp" + +#include +#include +#include +#include + +namespace na { +/// Class to store two-dimensional coordinates +struct Point { + std::int64_t x; + std::int64_t y; + Point(const std::int64_t xp, const std::int64_t yp) : x(xp), y(yp) {}; + Point(const Point& p) = default; + virtual ~Point() = default; + Point& operator=(const Point& p) = default; + Point operator-(const Point& p) const { return {x - p.x, y - p.y}; } + Point operator-(const Point&& p) const { return {x - p.x, y - p.y}; } + Point operator+(const Point& p) const { return {x + p.x, y + p.y}; } + Point operator+(const Point&& p) const { return {x + p.x, y + p.y}; } + [[nodiscard]] auto length() const -> std::uint64_t { + return static_cast(std::round(std::sqrt(x * x + y * y))); + } + [[nodiscard]] auto toString() const -> std::string { + std::stringstream ss; + ss << "(" << x << ", " << y << ")"; + return ss.str(); + } + friend auto operator<<(std::ostream& os, const Point& obj) -> std::ostream& { + return os << obj.toString(); // Using toString() method + } + [[nodiscard]] auto operator==(const Point& other) const -> bool { + return x == other.x && y == other.y; + } +}; +/// More specific operation type including the number of control qubits +struct FullOpType { + qc::OpType type; + std::size_t nControls; + [[nodiscard]] auto toString() const -> std::string { + return std::string(nControls, 'c') + qc::toString(type); + } + friend auto operator<<(std::ostream& os, + const FullOpType& obj) -> std::ostream& { + return os << obj.toString(); // Using toString() method + } + [[nodiscard]] auto operator==(const FullOpType& other) const -> bool { + return type == other.type && nControls == other.nControls; + } + [[nodiscard]] auto isSingleQubitType() const -> bool { + return isSingleQubitGate(type); + } + [[nodiscard]] auto isTwoQubitType() const -> bool { + return isTwoQubitGate(type); + } + [[nodiscard]] auto isControlledType() const -> bool { return nControls > 0; } +}; + +/** + * @brief Checks whether a gate is global. + * @details A StandardOperation is global if it acts on all qubits. + * A CompoundOperation is global if all its sub-operations are + * StandardOperations of the same type with the same parameters acting on all + * qubits. The latter is what a QASM line like `ry(π) q;` is translated to in + * MQT-core. All other operations are not global. + */ +[[nodiscard]] inline auto isGlobal(const qc::Operation& op, + const std::size_t nQubits) -> bool { + if (op.isStandardOperation()) { + return op.getUsedQubits().size() == nQubits; + } + if (op.isCompoundOperation()) { + const auto ops = dynamic_cast(op); + const auto& params = ops.at(0)->getParameter(); + const auto& type = ops.at(0)->getType(); + return op.getUsedQubits().size() == nQubits && + std::all_of(ops.cbegin(), ops.cend(), [&](const auto& operation) { + return operation->isStandardOperation() && + operation->getNcontrols() == 0 && + operation->getType() == type && + operation->getParameter() == params; + }); + } + return false; +} + +} // namespace na + +/// Hash function for OpType, e.g., for use in unordered_map +template <> struct std::hash { + std::size_t operator()(na::FullOpType const& t) const noexcept { + std::size_t const h1 = std::hash{}(t.type); + std::size_t const h2 = std::hash{}(t.nControls); + return qc::combineHash(h1, h2); + } +}; diff --git a/include/mqt-core/na/operations/NAGlobalOperation.hpp b/include/mqt-core/na/operations/NAGlobalOperation.hpp new file mode 100644 index 000000000..5041d05fa --- /dev/null +++ b/include/mqt-core/na/operations/NAGlobalOperation.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include "Definitions.hpp" +#include "na/NADefinitions.hpp" +#include "na/operations/NAOperation.hpp" + +#include +#include +#include +#include +#include +namespace na { +class NAGlobalOperation : public NAOperation { +protected: + FullOpType type; + std::vector params; + +public: + explicit NAGlobalOperation(const FullOpType opType, + const std::vector& parameters) + : type(opType), params(parameters) { + if (!opType.isSingleQubitType()) { + throw std::invalid_argument("Operation is not single qubit."); + } + } + explicit NAGlobalOperation(const FullOpType opType) + : NAGlobalOperation(opType, {}) {} + [[nodiscard]] auto getParams() const -> const std::vector& { + return params; + } + [[nodiscard]] auto isGlobalOperation() const -> bool override { return true; } + [[nodiscard]] auto toString() const -> std::string override; + [[nodiscard]] auto clone() const -> std::unique_ptr override { + return std::make_unique(*this); + } +}; +} // namespace na diff --git a/include/mqt-core/na/operations/NALocalOperation.hpp b/include/mqt-core/na/operations/NALocalOperation.hpp new file mode 100644 index 000000000..0dc81b726 --- /dev/null +++ b/include/mqt-core/na/operations/NALocalOperation.hpp @@ -0,0 +1,57 @@ +#pragma once + +#include "Definitions.hpp" +#include "na/NADefinitions.hpp" +#include "na/operations/NAOperation.hpp" + +#include +#include +#include +#include +#include +#include +namespace na { +class NALocalOperation : public NAOperation { +protected: + FullOpType type; + std::vector params; + std::vector> positions; + +public: + NALocalOperation(const FullOpType& opType, + const std::vector& parameter, + const std::vector>& pos) + : type(opType), params(parameter), positions(pos) { + if (!opType.isSingleQubitType()) { + throw std::invalid_argument("Operation is not single qubit."); + } + if (opType.isControlledType()) { + throw std::logic_error("Control qubits are not supported."); + } + } + explicit NALocalOperation(const FullOpType& opType, + const std::vector>& pos) + : NALocalOperation(opType, {}, pos) {} + explicit NALocalOperation(const FullOpType& opType, + const std::vector& parameters, + std::shared_ptr pos) + : NALocalOperation(opType, parameters, + std::vector>{std::move(pos)}) {} + explicit NALocalOperation(const FullOpType& opType, + std::shared_ptr pos) + : NALocalOperation(opType, {}, std::move(pos)) {} + [[nodiscard]] auto + getPositions() const -> const std::vector>& { + return positions; + } + [[nodiscard]] auto getParams() const -> const std::vector& { + return params; + } + [[nodiscard]] auto getType() const -> FullOpType { return type; } + [[nodiscard]] auto isLocalOperation() const -> bool override { return true; } + [[nodiscard]] auto toString() const -> std::string override; + [[nodiscard]] auto clone() const -> std::unique_ptr override { + return std::make_unique(*this); + } +}; +} // namespace na diff --git a/include/mqt-core/na/operations/NAOperation.hpp b/include/mqt-core/na/operations/NAOperation.hpp new file mode 100644 index 000000000..207ce2326 --- /dev/null +++ b/include/mqt-core/na/operations/NAOperation.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include +#include +#include +namespace na { +class NAOperation { +public: + [[nodiscard]] virtual auto isShuttlingOperation() const -> bool { + return false; + } + [[nodiscard]] virtual auto isLocalOperation() const -> bool { return false; } + [[nodiscard]] virtual auto isGlobalOperation() const -> bool { return false; } + [[nodiscard]] virtual auto toString() const -> std::string = 0; + friend auto operator<<(std::ostream& os, + const NAOperation& obj) -> std::ostream& { + return os << obj.toString(); // Using toString() method + } + virtual ~NAOperation() = default; + [[nodiscard]] virtual auto clone() const -> std::unique_ptr = 0; +}; +} // namespace na diff --git a/include/mqt-core/na/operations/NAShuttlingOperation.hpp b/include/mqt-core/na/operations/NAShuttlingOperation.hpp new file mode 100644 index 000000000..9784949bf --- /dev/null +++ b/include/mqt-core/na/operations/NAShuttlingOperation.hpp @@ -0,0 +1,58 @@ +#pragma once + +#include "na/NADefinitions.hpp" +#include "na/operations/NAOperation.hpp" + +#include +#include +#include +#include +#include +#include +#include +namespace na { + +enum ShuttleType : std::uint8_t { LOAD, MOVE, STORE }; + +class NAShuttlingOperation : public NAOperation { +protected: + ShuttleType type; + std::vector> start; + std::vector> end; + +public: + explicit NAShuttlingOperation( + const ShuttleType shuttleType, + const std::vector>& startConfig, + const std::vector>& endConfig) + : type(shuttleType), start(startConfig), end(endConfig) { + if (startConfig.size() != endConfig.size()) { + throw std::logic_error("Shuttling operation must have the same number of " + "start and end qubits."); + } + } + explicit NAShuttlingOperation(const ShuttleType shuttleType, + std::shared_ptr startPoint, + std::shared_ptr endPoint) + : NAShuttlingOperation( + shuttleType, + std::vector>{std::move(startPoint)}, + std::vector>{std::move(endPoint)}) {} + [[nodiscard]] auto getType() const -> ShuttleType { return type; } + [[nodiscard]] auto + getStart() const -> const std::vector>& { + return start; + } + [[nodiscard]] auto + getEnd() const -> const std::vector>& { + return end; + } + [[nodiscard]] auto isShuttlingOperation() const -> bool override { + return true; + } + [[nodiscard]] auto toString() const -> std::string override; + [[nodiscard]] auto clone() const -> std::unique_ptr override { + return std::make_unique(*this); + } +}; +} // namespace na diff --git a/include/mqt-core/operations/CompoundOperation.hpp b/include/mqt-core/operations/CompoundOperation.hpp index 0b172aabc..5286c93c7 100644 --- a/include/mqt-core/operations/CompoundOperation.hpp +++ b/include/mqt-core/operations/CompoundOperation.hpp @@ -61,6 +61,15 @@ class CompoundOperation final : public Operation { [[nodiscard]] std::set getUsedQubits() const override; + [[nodiscard]] auto commutesAtQubit(const Operation& other, + const Qubit& qubit) const -> bool override; + + /** + * This refines the inherited method because the inherited method leads to + * false negatives + */ + [[nodiscard]] auto isInverseOf(const Operation& other) const -> bool override; + void invert() override; void apply(const Permutation& permutation) override; diff --git a/include/mqt-core/operations/OpType.hpp b/include/mqt-core/operations/OpType.hpp index a18f8fbc4..18710b4a2 100644 --- a/include/mqt-core/operations/OpType.hpp +++ b/include/mqt-core/operations/OpType.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -62,6 +63,10 @@ enum OpType : std::uint8_t { OpCount }; +/// Enumeration of diagonal gates +static constexpr std::array DIAGONAL_GATES = { + Barrier, I, Z, S, Sdg, T, Tdg, P, RZ, RZZ}; + inline std::string toString(const OpType& opType) { switch (opType) { case None: @@ -189,7 +194,7 @@ inline std::string shortName(const OpType& opType) { } } -inline bool isTwoQubitGate(const OpType& opType) { +[[nodiscard]] inline bool isTwoQubitGate(const OpType& opType) { switch (opType) { case SWAP: case iSWAP: @@ -210,9 +215,34 @@ inline bool isTwoQubitGate(const OpType& opType) { } } -inline std::ostream& operator<<(std::ostream& out, OpType& opType) { - out << toString(opType); - return out; +[[nodiscard]] inline auto isSingleQubitGate(const qc::OpType& type) { + switch (type) { + case U: + case U2: + case P: + case X: + case Y: + case Z: + case H: + case S: + case Sdg: + case T: + case SX: + case SXdg: + case Tdg: + case V: + case Vdg: + case RX: + case RY: + case RZ: + return true; + default: + return false; + } +} + +inline std::ostream& operator<<(std::ostream& out, const OpType& opType) { + return out << toString(opType); } const inline static std::unordered_map diff --git a/include/mqt-core/operations/Operation.hpp b/include/mqt-core/operations/Operation.hpp index 4793bc77f..fc08ea007 100644 --- a/include/mqt-core/operations/Operation.hpp +++ b/include/mqt-core/operations/Operation.hpp @@ -131,6 +131,15 @@ class Operation { [[nodiscard]] virtual bool isSymbolicOperation() const { return false; } + [[nodiscard]] virtual auto isDiagonalGate() const -> bool { + return std::find(DIAGONAL_GATES.begin(), DIAGONAL_GATES.end(), type) != + DIAGONAL_GATES.end(); + } + + [[nodiscard]] virtual auto isSingleQubitGate() const -> bool { + return !isControlled() && qc::isSingleQubitGate(type); + } + [[nodiscard]] virtual bool isControlled() const { return !controls.empty(); } [[nodiscard]] virtual bool actsOn(const Qubit i) const { @@ -171,6 +180,16 @@ class Operation { const RegisterNames& creg, size_t indent, bool openQASM3) const = 0; + /// Checks whether operation commutes with other operation on a given qubit. + [[nodiscard]] virtual auto + commutesAtQubit(const Operation& /*other*/, + const Qubit& /*qubit*/) const -> bool { + return false; + } + + [[nodiscard]] virtual auto + isInverseOf(const Operation& /*other*/) const -> bool; + virtual void invert() = 0; virtual bool operator==(const Operation& rhs) const { return equals(rhs); } diff --git a/include/mqt-core/operations/StandardOperation.hpp b/include/mqt-core/operations/StandardOperation.hpp index 578f9a639..d572189c7 100644 --- a/include/mqt-core/operations/StandardOperation.hpp +++ b/include/mqt-core/operations/StandardOperation.hpp @@ -95,6 +95,9 @@ class StandardOperation : public Operation { const RegisterNames& creg, size_t indent, bool openQASM3) const override; + [[nodiscard]] auto commutesAtQubit(const Operation& other, + const Qubit& qubit) const -> bool override; + void invert() override; protected: diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4b74ac95e..526f2035c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -136,6 +136,9 @@ if(NOT TARGET ${MQT_CORE_TARGET_NAME}) list(APPEND MQT_CORE_TARGETS ${MQT_CORE_TARGET_NAME}) endif() +# add datastructures package +add_subdirectory(datastructures) + # add DD package library add_subdirectory(dd) @@ -145,6 +148,9 @@ add_subdirectory(zx) # add ECC library add_subdirectory(ecc) +# add NA library +add_subdirectory(na) + # ** Note ** The following target will soon be removed from the project. All top-level projects # should switch to using the mqt-core Python package. if(BINDINGS AND NOT TARGET mqt-core-python) diff --git a/src/datastructures/CMakeLists.txt b/src/datastructures/CMakeLists.txt new file mode 100644 index 000000000..bc1346d89 --- /dev/null +++ b/src/datastructures/CMakeLists.txt @@ -0,0 +1,26 @@ +# +# This file is part of the MQT CORE library released under the MIT license. See README.md or go to +# https://github.com/cda-tum/mqt-core for more information. +# + +if(NOT TARGET ${MQT_CORE_TARGET_NAME}-ds) + file(GLOB_RECURSE DS_HEADERS ${MQT_CORE_INCLUDE_BUILD_DIR}/datastructures/**.hpp) + file(GLOB_RECURSE DS_SOURCES **.cpp) + + # add NA package library + add_library(${MQT_CORE_TARGET_NAME}-ds ${DS_HEADERS} ${DS_SOURCES}) + + target_link_libraries(${MQT_CORE_TARGET_NAME}-ds PUBLIC MQT::Core) + target_link_libraries(${MQT_CORE_TARGET_NAME}-ds PRIVATE MQT::ProjectOptions MQT::ProjectWarnings) + + # add MQT alias + add_library(MQT::CoreDS ALIAS ${MQT_CORE_TARGET_NAME}-ds) + set_target_properties( + ${MQT_CORE_TARGET_NAME}-ds + PROPERTIES VERSION ${PROJECT_VERSION} + SOVERSION ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR} + EXPORT_NAME CoreDS) + set(MQT_CORE_TARGETS + ${MQT_CORE_TARGETS} ${MQT_CORE_TARGET_NAME}-ds + PARENT_SCOPE) +endif() diff --git a/src/datastructures/Layer.cpp b/src/datastructures/Layer.cpp new file mode 100644 index 000000000..8b3ec009e --- /dev/null +++ b/src/datastructures/Layer.cpp @@ -0,0 +1,218 @@ +#include "datastructures/Layer.hpp" + +#include "Definitions.hpp" +#include "QuantumComputation.hpp" +#include "datastructures/UndirectedGraph.hpp" +#include "operations/OpType.hpp" + +#include +#include +#include +#include + +namespace qc { + +auto Layer::constructDAG(const QuantumComputation& qc) -> void { + const auto nQubits = qc.getNqubits(); + // For a pair of self-canceling operations like two consecutive X operations + // or RY rotations with opposite angles the first operations is a + // destructive operation that disables operations until the consecutive + // constructive operation enables them again + // --- + // those that add a (+) edge to the current group members + std::vector constructive(nQubits, std::vector>()); + // those that add a (-) edge to the current group members + std::vector destructive(nQubits, std::vector>()); + // those that are already in the current group where all gates commute on + // this qubit + std::vector currentGroup(nQubits, std::vector>()); + // lookahead of 1 serves as a buffer for the next operation on each qubit + std::vector> lookahead(nQubits, nullptr); + // the predecessor of the current group members + std::vector predecessorGroup(nQubits, + std::vector>()); + // all operations acting on a qubit (processed so far) excluding + // constructive and destructive operations + std::vector qubitOperations(nQubits, + std::vector>()); + // iterate over all operations in the quantum circuit + for (const auto& op : qc) { + // create a vertex for the current operation + const auto vertex = DAGVertex::create(op.get(), executableSet); + // iterate over all qubits the operation acts on + for (const auto& qubit : op->getUsedQubits()) { + // check whether the lookahead is empty + if (lookahead[qubit] == nullptr) { + // here: the lookahead is empty + // add the current operation to the lookahead + lookahead[qubit] = vertex; + } else { + // here: the lookahead is not empty + // get the current vertex from the lookahead and store the new + // vertex in the lookahead + // Note: this might seem odd and one might think that the gates can be + // eliminated, however, since we also allow global gates, those might + // cancel out each other on a particular qubit but not on all qubits + // and, hence, cannot be eliminated. + auto current = lookahead[qubit]; + lookahead[qubit] = vertex; + // check whether the current operation is the inverse of the + // lookahead + if (current->getOperation()->isInverseOf( + *lookahead[qubit]->getOperation())) { + // here: the current operation is the inverse of the lookahead + // add an enabling edge from the lookahead to all operations on this + // qubit including the destructive ones + for (const auto& qubitOperation : qubitOperations[qubit]) { + lookahead[qubit]->addEnabledSuccessor(qubitOperation); + } + for (const auto& qubitOperation : destructive[qubit]) { + lookahead[qubit]->addEnabledSuccessor(qubitOperation); + } + // add the lookahead to the constructive group + constructive[qubit].emplace_back(lookahead[qubit]); + // add a disabling edge to all operations on this qubit including + // the destructive ones + for (const auto& qubitOperation : qubitOperations[qubit]) { + current->addDisabledSuccessor(qubitOperation); + } + for (const auto& qubitOperation : destructive[qubit]) { + current->addDisabledSuccessor(qubitOperation); + } + // add an enabling edge to the lookahead + current->addEnabledSuccessor(lookahead[qubit]); + // add the current vertex to the destructive group + destructive[qubit].emplace_back(current); + // clear the lookahead + lookahead[qubit] = nullptr; + } else { + // add an enabling edge from each constructive operation + for (const auto& constructiveOp : constructive[qubit]) { + constructiveOp->addEnabledSuccessor(current); + } + // add a disabling edge from each destructive operation + for (const auto& destructiveOp : destructive[qubit]) { + destructiveOp->addDisabledSuccessor(current); + } + // check whether the current operation commutes with the current + // group members + if (!currentGroup[qubit].empty() and + !(currentGroup[qubit][0]->getOperation()) + ->commutesAtQubit(*current->getOperation(), qubit)) { + // here: the current operation does not commute with the current + // group members and is not the inverse of the lookahead + // --> start a new group + predecessorGroup[qubit].clear(); + predecessorGroup[qubit] = currentGroup[qubit]; + currentGroup[qubit].clear(); + } + // add an enabling edge from each predecessor + for (const auto& predecessor : predecessorGroup[qubit]) { + predecessor->addEnabledSuccessor(current); + } + // add the current vertex to the current group + currentGroup[qubit].emplace_back(current); + qubitOperations[qubit].emplace_back(current); + } + } + } + } + // process the remaining lookahead for every qubit + for (Qubit qubit = 0; qubit < nQubits; ++qubit) { + if (lookahead[qubit] != nullptr) { + const auto current = lookahead[qubit]; + lookahead[qubit] = nullptr; + // add an enabling edge from each constructive operation + for (const auto& constructiveOp : constructive[qubit]) { + constructiveOp->addEnabledSuccessor(current); + } + // add a disabling edge from each destructive operation + for (const auto& destructiveOp : destructive[qubit]) { + destructiveOp->addDisabledSuccessor(current); + } + // check whether the current operation commutes with the current + // group members + if (!currentGroup[qubit].empty() and + !(currentGroup[qubit][0]->getOperation()) + ->commutesAtQubit(*current->getOperation(), qubit)) { + // here: the current operation does not commute with the current + // group members and is not the inverse of the lookahead + // --> start a new group + predecessorGroup[qubit].clear(); + predecessorGroup[qubit] = currentGroup[qubit]; + currentGroup[qubit].clear(); + } + // add an enabling edge from each predecessor + for (const auto& predecessor : predecessorGroup[qubit]) { + predecessor->addEnabledSuccessor(current); + } + // add the current vertex to the current group + currentGroup[qubit].emplace_back(current); + qubitOperations[qubit].emplace_back(current); + } + } +} +auto Layer::constructInteractionGraph(const OpType opType, + const std::size_t nControls) const + -> InteractionGraph { + switch (opType) { + case I: + case H: + case X: + case Y: + case Z: + case S: + case Sdg: + case T: + case Tdg: + case V: + case Vdg: + case U: + case U2: + case P: + case SX: + case SXdg: + case RX: + case RY: + case RZ: + if (nControls == 1) { + break; + } + [[fallthrough]]; + default: + std::stringstream ss; + ss << "The operation type "; + for (std::size_t i = 0; i < nControls; ++i) { + ss << "c"; + } + ss << opType << " is not supported for constructing an interaction graph."; + throw std::invalid_argument(ss.str()); + } + InteractionGraph graph; + for (const auto& vertex : executableSet) { + const auto& gate = vertex->getOperation(); + if (gate->getType() == opType && gate->getNcontrols() == nControls) { + const auto& usedQubits = gate->getUsedQubits(); + if (usedQubits.size() != 2) { + throw std::invalid_argument( + "The interaction graph can only be constructed for two-qubit " + "gates."); + } + graph.addEdge(*usedQubits.begin(), *usedQubits.rbegin(), vertex); + } + } + return graph; +} +auto Layer::getExecutablesOfType(const OpType opType, + const std::size_t nControls) const + -> std::vector> { + std::vector> executables; + for (const auto& vertex : executableSet) { + if ((vertex->getOperation())->getType() == opType and + (vertex->getOperation())->getNcontrols() == nControls) { + executables.emplace_back(vertex); + } + } + return executables; +} +} // namespace qc diff --git a/src/na/CMakeLists.txt b/src/na/CMakeLists.txt new file mode 100644 index 000000000..ee2463fa6 --- /dev/null +++ b/src/na/CMakeLists.txt @@ -0,0 +1,26 @@ +# +# This file is part of the MQT CORE library released under the MIT license. See README.md or go to +# https://github.com/cda-tum/mqt-core for more information. +# + +if(NOT TARGET ${MQT_CORE_TARGET_NAME}-na) + file(GLOB_RECURSE NA_HEADERS ${MQT_CORE_INCLUDE_BUILD_DIR}/na/**.hpp) + file(GLOB_RECURSE NA_SOURCES **.cpp) + + # add NA package library + add_library(${MQT_CORE_TARGET_NAME}-na ${NA_HEADERS} ${NA_SOURCES}) + + target_link_libraries(${MQT_CORE_TARGET_NAME}-na PUBLIC MQT::Core MQT::CoreDS) + target_link_libraries(${MQT_CORE_TARGET_NAME}-na PRIVATE MQT::ProjectOptions MQT::ProjectWarnings) + + # add MQT alias + add_library(MQT::CoreNA ALIAS ${MQT_CORE_TARGET_NAME}-na) + set_target_properties( + ${MQT_CORE_TARGET_NAME}-na + PROPERTIES VERSION ${PROJECT_VERSION} + SOVERSION ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR} + EXPORT_NAME CoreNA) + set(MQT_CORE_TARGETS + ${MQT_CORE_TARGETS} ${MQT_CORE_TARGET_NAME}-na + PARENT_SCOPE) +endif() diff --git a/src/na/NAComputation.cpp b/src/na/NAComputation.cpp new file mode 100644 index 000000000..2de3cbdeb --- /dev/null +++ b/src/na/NAComputation.cpp @@ -0,0 +1,21 @@ +#include "na/NAComputation.hpp" + +#include +#include +#include + +namespace na { +auto NAComputation::toString() const -> std::string { + std::stringstream ss; + ss << "init at "; + for (const auto& p : initialPositions) { + ss << "(" << p->x << ", " << p->y << ")" << ", "; + } + ss.seekp(-2, std::ios_base::end); + ss << ";\n"; + for (const auto& op : operations) { + ss << op->toString(); + } + return ss.str(); +} +} // namespace na diff --git a/src/na/operations/NAGlobalOperation.cpp b/src/na/operations/NAGlobalOperation.cpp new file mode 100644 index 000000000..1981b6773 --- /dev/null +++ b/src/na/operations/NAGlobalOperation.cpp @@ -0,0 +1,22 @@ +#include "na/operations/NAGlobalOperation.hpp" + +#include +#include +#include + +namespace na { +auto NAGlobalOperation::toString() const -> std::string { + std::stringstream ss; + ss << type; + if (!params.empty()) { + ss << "("; + for (const auto& p : params) { + ss << p << ", "; + } + ss.seekp(-2, std::ios_base::end); + ss << ")"; + } + ss << ";\n"; + return ss.str(); +} +} // namespace na diff --git a/src/na/operations/NALocalOperation.cpp b/src/na/operations/NALocalOperation.cpp new file mode 100644 index 000000000..939edd588 --- /dev/null +++ b/src/na/operations/NALocalOperation.cpp @@ -0,0 +1,27 @@ +#include "na/operations/NALocalOperation.hpp" + +#include +#include +#include + +namespace na { +auto NALocalOperation::toString() const -> std::string { + std::stringstream ss; + ss << type; + if (!params.empty()) { + ss << "("; + for (const auto& p : params) { + ss << p << ", "; + } + ss.seekp(-2, std::ios_base::end); + ss << ")"; + } + ss << " at "; + for (const auto& p : positions) { + ss << *p << ", "; + } + ss.seekp(-2, std::ios_base::end); + ss << ";\n"; + return ss.str(); +} +} // namespace na diff --git a/src/na/operations/NAShuttlingOperation.cpp b/src/na/operations/NAShuttlingOperation.cpp new file mode 100644 index 000000000..bc8303bf2 --- /dev/null +++ b/src/na/operations/NAShuttlingOperation.cpp @@ -0,0 +1,34 @@ +#include "na/operations/NAShuttlingOperation.hpp" + +#include +#include +#include + +namespace na { +auto NAShuttlingOperation::toString() const -> std::string { + std::stringstream ss; + switch (type) { + case LOAD: + ss << "load"; + break; + case MOVE: + ss << "move"; + break; + case STORE: + ss << "store"; + break; + } + ss << " "; + for (const auto& p : start) { + ss << *p << ", "; + } + ss.seekp(-2, std::ios_base::end); + ss << " to "; + for (const auto& p : end) { + ss << *p << ", "; + } + ss.seekp(-2, std::ios_base::end); + ss << ";\n"; + return ss.str(); +} +} // namespace na diff --git a/src/operations/CompoundOperation.cpp b/src/operations/CompoundOperation.cpp index cd22c001c..6f48e1aa7 100644 --- a/src/operations/CompoundOperation.cpp +++ b/src/operations/CompoundOperation.cpp @@ -1,8 +1,22 @@ #include "operations/CompoundOperation.hpp" +#include "Definitions.hpp" +#include "Permutation.hpp" +#include "operations/Control.hpp" +#include "operations/OpType.hpp" + +#include +#include #include #include +#include +#include #include +#include +#include +#include +#include +#include namespace qc { CompoundOperation::CompoundOperation() { @@ -153,6 +167,53 @@ std::set CompoundOperation::getUsedQubits() const { } return usedQubits; } + +auto CompoundOperation::commutesAtQubit(const Operation& other, + const Qubit& qubit) const -> bool { + return std::all_of(ops.cbegin(), ops.cend(), + [&other, &qubit](const auto& op) { + return op->commutesAtQubit(other, qubit); + }); +} + +auto CompoundOperation::isInverseOf(const Operation& other) const -> bool { + if (other.isCompoundOperation()) { + // cast other to CompoundOperation + const auto& co = dynamic_cast(other); + if (size() != co.size()) { + return false; + } + // here both compound operations have the same size + if (empty()) { + return true; + } + // transform compound to a QuantumComputation such that the invert method + // and the reorderOperations method can be used to get a canonical form of + // the compound operations + const auto& thisUsedQubits = getUsedQubits(); + assert(!thisUsedQubits.empty()); + const auto thisMaxQubit = + *std::max_element(thisUsedQubits.cbegin(), thisUsedQubits.cend()); + QuantumComputation thisQc(thisMaxQubit + 1); + std::for_each(cbegin(), cend(), + [&](const auto& op) { thisQc.emplace_back(op->clone()); }); + const auto& otherUsedQubits = co.getUsedQubits(); + assert(!otherUsedQubits.empty()); + const auto otherMaxQubit = + *std::max_element(otherUsedQubits.cbegin(), otherUsedQubits.cend()); + QuantumComputation otherQc(otherMaxQubit + 1); + std::for_each(co.cbegin(), co.cend(), + [&](const auto& op) { otherQc.emplace_back(op->clone()); }); + CircuitOptimizer::reorderOperations(thisQc); + otherQc.invert(); + CircuitOptimizer::reorderOperations(otherQc); + return std::equal( + thisQc.cbegin(), thisQc.cend(), otherQc.cbegin(), + [](const auto& op1, const auto& op2) { return *op1 == *op2; }); + } + return false; +} + void CompoundOperation::invert() { for (auto& op : ops) { op->invert(); @@ -178,6 +239,7 @@ bool CompoundOperation::isConvertibleToSingleOperation() const { if (ops.size() != 1) { return false; } + assert(ops.front() != nullptr); if (!ops.front()->isCompoundOperation()) { return true; } diff --git a/src/operations/Operation.cpp b/src/operations/Operation.cpp index d7b14f46b..5bd7aae6a 100644 --- a/src/operations/Operation.cpp +++ b/src/operations/Operation.cpp @@ -181,4 +181,8 @@ void Operation::apply(const Permutation& permutation) { getControls() = permutation.apply(getControls()); } +auto Operation::isInverseOf(const Operation& other) const -> bool { + return operator==(*other.getInverted()); +} + } // namespace qc diff --git a/src/operations/StandardOperation.cpp b/src/operations/StandardOperation.cpp index f176bab81..f8aae42a5 100644 --- a/src/operations/StandardOperation.cpp +++ b/src/operations/StandardOperation.cpp @@ -1,7 +1,10 @@ #include "operations/StandardOperation.hpp" +#include "operations/CompoundOperation.hpp" + #include #include +#include #include #include @@ -496,6 +499,69 @@ void StandardOperation::dumpOpenQASMTeleportation( << qreg[targets[1]].second << ", " << qreg[targets[2]].second << ";\n"; } +auto StandardOperation::commutesAtQubit(const Operation& other, + const Qubit& qubit) const -> bool { + if (other.isCompoundOperation()) { + return other.commutesAtQubit(*this, qubit); + } + // check whether both operations act on the given qubit + if (!actsOn(qubit) || !other.actsOn(qubit)) { + return true; + } + if (controls.find(qubit) != controls.end()) { + // if this is controlled on the given qubit + if (const auto& controls2 = other.getControls(); + controls2.find(qubit) != controls2.end()) { + // if other is controlled on the given qubit + // q: ──■────■── + // | | + return true; + } + // here: qubit is a target of other + return other.isDiagonalGate(); + // true, iff qubit is a target and other is a diagonal gate, e.g., rz + // ┌────┐ + // q: ──■──┤ RZ ├ + // | └────┘ + } + // here: qubit is a target of this + if (const auto& controls2 = other.getControls(); + controls2.find(qubit) != controls2.end()) { + return isDiagonalGate(); + // true, iff qubit is a target and this is a diagonal gate and other is + // controlled, e.g. + // ┌────┐ + // q: ┤ RZ ├──■── + // └────┘ | + } + // here: qubit is a target of both operations + if (isDiagonalGate() && other.isDiagonalGate()) { + // if both operations are diagonal gates, e.g. + // ┌────┐┌────┐ + // q: ┤ RZ ├┤ RZ ├ + // └────┘└────┘ + return true; + } + if (parameter.size() <= 1) { + return type == other.getType() && targets == other.getTargets(); + // true, iff both operations are of the same type, e.g. + // ┌───┐┌───┐ + // q: ┤ E ├┤ E ├ + // | C || C | + // ┤ R ├┤ R ├ + // └───┘└───┘ + // | | + // ──■────┼── + // | + // ───────■── + } + // operations with more than one parameter might not be commutative when the + // parameter are not the same, i.e. a general U3 gate + // TODO: this check might introduce false negatives + return type == other.getType() && targets == other.getTargets() && + parameter == other.getParameter(); +} + void StandardOperation::invert() { switch (type) { // self-inverting gates @@ -575,17 +641,7 @@ void StandardOperation::invert() { case iSWAPdg: type = iSWAP; break; - case None: - case Compound: - case Measure: - case Reset: - case Teleportation: - case ClassicControlled: - case ATrue: - case AFalse: - case MultiATrue: - case MultiAFalse: - case OpCount: + default: throw QFRException("Inverting gate" + toString(type) + " is not supported."); } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 8e358d581..1dfda8271 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,7 +1,12 @@ # add unit tests package_add_test( - ${PROJECT_NAME}-test ${PROJECT_NAME} unittests/test_io.cpp unittests/test_qfr_functionality.cpp - unittests/test_symbolic.cpp unittests/test_qasm3_parser.cpp) + ${PROJECT_NAME}-test + ${PROJECT_NAME} + unittests/test_io.cpp + unittests/test_qfr_functionality.cpp + unittests/test_symbolic.cpp + unittests/test_qasm3_parser.cpp + test_operation.cpp) file(GLOB_RECURSE circuit_optimizer_tests "unittests/circuit_optimizer/*.cpp") package_add_test(${PROJECT_NAME}-test-circuit-optimizer ${PROJECT_NAME} ${circuit_optimizer_tests}) @@ -30,3 +35,6 @@ package_add_test(${PROJECT_NAME}-test-zx ${PROJECT_NAME}-zx ${zx_tests}) package_add_test(${PROJECT_NAME}-test-ecc ${PROJECT_NAME}-ecc unittests/test_ecc_functionality.cpp) target_link_libraries(${PROJECT_NAME}-test-ecc PRIVATE ${PROJECT_NAME}-dd) + +add_subdirectory(datastructures) +add_subdirectory(na) diff --git a/test/datastructures/CMakeLists.txt b/test/datastructures/CMakeLists.txt new file mode 100644 index 000000000..1c4ea607f --- /dev/null +++ b/test/datastructures/CMakeLists.txt @@ -0,0 +1,4 @@ +if(TARGET MQT::CoreDS) + file(GLOB_RECURSE DS_TEST_SOURCES *.cpp) + package_add_test(mqt-core-ds-test MQT::CoreDS ${DS_TEST_SOURCES}) +endif() diff --git a/test/datastructures/test_directed_acyclic_graph.cpp b/test/datastructures/test_directed_acyclic_graph.cpp new file mode 100644 index 000000000..b1d3fc835 --- /dev/null +++ b/test/datastructures/test_directed_acyclic_graph.cpp @@ -0,0 +1,42 @@ +#include "datastructures/DirectedAcyclicGraph.hpp" + +#include "gtest/gtest.h" + +namespace qc { +TEST(DirectedAcyclicGraph, Reachable) { + DirectedAcyclicGraph g; + g.addVertex(0); + g.addEdge(0, 1); + g.addEdge(1, 2); + g.addEdge(2, 3); + EXPECT_ANY_THROW(g.addEdge(3, 1)); + // 0 ────> 1 ———> 2 ———> 3 + // ^ | + // └──────X──────┘ + EXPECT_TRUE(g.isReachable(1, 3)); + EXPECT_FALSE(g.isReachable(3, 1)); +} + +TEST(DirectedAcyclicGraph, TopologicalOrder) { + DirectedAcyclicGraph g; + g.addVertex(6); + g.addVertex(2); + g.addVertex(5); + g.addVertex(4); + g.addEdge(2, 6); + g.addEdge(2, 5); + g.addEdge(2, 4); + g.addEdge(5, 6); + g.addEdge(4, 6); + g.addEdge(4, 5); + // ┌─────────────┐ + // ┌──────┼──────┐ │ + // │ | v v + // 2 ───> 4 ———> 5 ———> 6 + // │ ^ + // └────────────────────┘ + const auto& actual = g.orderTopologically(); + const std::vector expected = {2, 4, 5, 6}; + EXPECT_EQ(actual, expected); +} +} // namespace qc diff --git a/test/datastructures/test_directed_graph.cpp b/test/datastructures/test_directed_graph.cpp new file mode 100644 index 000000000..6ca7799ab --- /dev/null +++ b/test/datastructures/test_directed_graph.cpp @@ -0,0 +1,31 @@ +#include "datastructures/DirectedGraph.hpp" + +#include "gtest/gtest.h" + +namespace qc { +TEST(DirectedGraph, Numbered) { + DirectedGraph g; + g.addVertex(0); + g.addEdge(1, 1); + g.addEdge(1, 2); + g.addEdge(2, 3); + g.addEdge(3, 1); + // ┌────┐ + // 0 └──> 1 ———> 2 ———> 3 + // ^ | + // └─────────────┘ + EXPECT_EQ(g.getNVertices(), 4); + EXPECT_EQ(g.getNEdges(), 4); + EXPECT_EQ(g.getInDegree(2), 1); + EXPECT_EQ(g.getInDegree(1), 2); + EXPECT_EQ(g.getOutDegree(2), 1); + EXPECT_EQ(g.getOutDegree(1), 2); + EXPECT_TRUE(g.isEdge(1, 2)); + EXPECT_FALSE(g.isEdge(2, 1)); + EXPECT_FALSE(g.isEdge(1, 0)); + + EXPECT_ANY_THROW(g.addVertex(1)); + EXPECT_ANY_THROW(std::ignore = g.getInDegree(4)); + EXPECT_ANY_THROW(std::ignore = g.getOutDegree(4)); +} +} // namespace qc diff --git a/test/datastructures/test_disjoint_set.cpp b/test/datastructures/test_disjoint_set.cpp new file mode 100644 index 000000000..b94970050 --- /dev/null +++ b/test/datastructures/test_disjoint_set.cpp @@ -0,0 +1,25 @@ +#include "datastructures/DisjointSet.hpp" + +#include + +namespace qc { +TEST(DisjointSet, FindSet) { + std::vector elements = {1, 2, 3}; + DisjointSet ds(elements.begin(), elements.end()); + EXPECT_EQ(ds.findSet(1), ds.findSet(1)); + EXPECT_NE(ds.findSet(1), ds.findSet(2)); +} + +TEST(DisjointSet, UnionSet) { + std::vector elements = {1, 2, 3}; + DisjointSet ds(elements.begin(), elements.end()); + ds.unionSet(1, 2); + EXPECT_EQ(ds.findSet(1), ds.findSet(2)); + EXPECT_NE(ds.findSet(1), ds.findSet(3)); + EXPECT_NE(ds.findSet(2), ds.findSet(3)); + ds.unionSet(1, 3); + EXPECT_EQ(ds.findSet(1), ds.findSet(2)); + EXPECT_EQ(ds.findSet(1), ds.findSet(3)); + EXPECT_EQ(ds.findSet(2), ds.findSet(3)); +} +} // namespace qc diff --git a/test/datastructures/test_layer.cpp b/test/datastructures/test_layer.cpp new file mode 100644 index 000000000..d5e1d1596 --- /dev/null +++ b/test/datastructures/test_layer.cpp @@ -0,0 +1,174 @@ +#include "Definitions.hpp" +#include "QuantumComputation.hpp" +#include "datastructures/Layer.hpp" +#include "operations/OpType.hpp" +#include "operations/StandardOperation.hpp" + +#include "gtest/gtest.h" +#include +#include +#include +#include +#include +#include +#include + +namespace qc { +TEST(Layer, ExecutableSet1) { + auto qc = QuantumComputation(3); + /* construct the following circuit +┌─────────┐┌─────────┐┌──────────┐ ┌─────────┐┌─────────┐┌──────────┐ +┤ ├┤ Rz(π/4) ├┤ ├─■──■─┤ ├┤ Rz(π/4) ├┤ ├─── +│ │├─────────┤│ │ │ | │ │└─────────┘│ │ +┤ Ry(π/2) ├┤ Rz(π/4) ├┤ Ry(-π/2) ├─■──┼─┤ Ry(π/2) ├───────────┤ Ry(-π/2) ├─■─ +│ │├─────────┤│ │ │ │ │ │ │ │ +┤ ├┤ Rz(π/4) ├┤ ├────■─┤ ├───────────┤ ├─■─ +└─────────┘└─────────┘└──────────┘ └─────────┘ └──────────┘ + (1) (2) (3) (4)(5) (6) (7) (8) (9) + */ + qc.emplace_back(Targets{0, 1, 2}, RY, std::vector{PI_2}); + qc.rz(PI_4, 0); + qc.rz(PI_4, 1); + qc.rz(PI_4, 2); + qc.emplace_back(Targets{0, 1, 2}, RY, std::vector{-PI_2}); + qc.cz(0, 1); + qc.cz(0, 2); + qc.emplace_back(Targets{0, 1, 2}, RY, std::vector{PI_2}); + qc.rz(PI_4, 0); + qc.emplace_back(Targets{0, 1, 2}, RY, std::vector{-PI_2}); + qc.cz(1, 2); + + Layer const layer(qc); + EXPECT_EQ(layer.getExecutableSet().size(), 1); // layer (1) + std::shared_ptr v = *(layer.getExecutableSet()).begin(); + v->execute(); + EXPECT_ANY_THROW(v->execute()); + EXPECT_EQ(layer.getExecutableSet().size(), 3); // layer (2) + v = *(layer.getExecutableSet()).begin(); + v->execute(); + EXPECT_EQ(layer.getExecutableSet().size(), 2); // rest of layer (2) + v = *(layer.getExecutableSet()).begin(); + v->execute(); + EXPECT_EQ(layer.getExecutableSet().size(), 1); // rest of layer (2) + v = *(layer.getExecutableSet()).begin(); + v->execute(); + EXPECT_EQ(layer.getExecutableSet().size(), 1); // layer (3) + v = *(layer.getExecutableSet()).begin(); + v->execute(); + EXPECT_EQ(layer.getExecutableSet().size(), 3); // layer (4), (5), (9) + // execute layer (4) and (5), first pick those two vertices and then execute + // them because when executing the iterator over the executable set is not + // valid anymore + std::vector> vList; + for (const auto& u : layer.getExecutableSet()) { + if (const auto& it = (u->getOperation())->getUsedQubits(); + it.find(0) != it.end()) { + vList.emplace_back(u); + } + } + std::for_each(vList.cbegin(), vList.cend(), + [](const auto& u) { u->execute(); }); + EXPECT_EQ(layer.getExecutableSet().size(), 2); // layer (6), (9) +} + +TEST(Layer, ExecutableSet2) { + QuantumComputation qc{}; + qc = QuantumComputation(8); + qc.cz(1, 2); + qc.cz(1, 6); + qc.cz(2, 7); + qc.cz(3, 4); + qc.cz(3, 5); + qc.cz(4, 5); + qc.cz(4, 6); + qc.cz(4, 7); + qc.cz(5, 7); + qc.cz(6, 7); + const Layer layer(qc); + EXPECT_EQ(layer.getExecutableSet().size(), 10); +} + +TEST(Layer, InteractionGraph) { + QuantumComputation qc{}; + qc = QuantumComputation(8); + qc.cz(1, 2); + qc.cz(1, 6); + qc.cz(2, 7); + qc.cz(3, 4); + qc.cz(3, 5); + qc.cz(4, 5); + qc.cz(4, 6); + qc.cz(4, 7); + qc.cz(5, 7); + qc.cz(6, 7); + const qc::Layer layer(qc); + const auto& graph = layer.constructInteractionGraph(Z, 1); + EXPECT_FALSE(graph.isAdjacent(1, 1)); + EXPECT_TRUE(graph.isAdjacent(1, 2)); + EXPECT_FALSE(graph.isAdjacent(1, 3)); + EXPECT_FALSE(graph.isAdjacent(1, 4)); + EXPECT_FALSE(graph.isAdjacent(1, 5)); + EXPECT_TRUE(graph.isAdjacent(1, 6)); + EXPECT_FALSE(graph.isAdjacent(1, 7)); + EXPECT_TRUE(graph.isAdjacent(2, 1)); + EXPECT_FALSE(graph.isAdjacent(2, 2)); + EXPECT_FALSE(graph.isAdjacent(2, 3)); + EXPECT_FALSE(graph.isAdjacent(2, 4)); + EXPECT_FALSE(graph.isAdjacent(2, 5)); + EXPECT_FALSE(graph.isAdjacent(2, 6)); + EXPECT_TRUE(graph.isAdjacent(2, 7)); + EXPECT_FALSE(graph.isAdjacent(3, 1)); + EXPECT_FALSE(graph.isAdjacent(3, 2)); + EXPECT_FALSE(graph.isAdjacent(3, 3)); + EXPECT_TRUE(graph.isAdjacent(3, 4)); + EXPECT_TRUE(graph.isAdjacent(3, 5)); + EXPECT_FALSE(graph.isAdjacent(3, 6)); + EXPECT_FALSE(graph.isAdjacent(3, 7)); + EXPECT_FALSE(graph.isAdjacent(4, 1)); + EXPECT_FALSE(graph.isAdjacent(4, 2)); + EXPECT_TRUE(graph.isAdjacent(4, 3)); + EXPECT_FALSE(graph.isAdjacent(4, 4)); + EXPECT_TRUE(graph.isAdjacent(4, 5)); + EXPECT_TRUE(graph.isAdjacent(4, 6)); + EXPECT_TRUE(graph.isAdjacent(4, 7)); + EXPECT_FALSE(graph.isAdjacent(5, 1)); + EXPECT_FALSE(graph.isAdjacent(5, 2)); + EXPECT_TRUE(graph.isAdjacent(5, 3)); + EXPECT_TRUE(graph.isAdjacent(5, 4)); + EXPECT_FALSE(graph.isAdjacent(5, 5)); + EXPECT_FALSE(graph.isAdjacent(5, 6)); + EXPECT_TRUE(graph.isAdjacent(5, 7)); + EXPECT_TRUE(graph.isAdjacent(6, 1)); + EXPECT_FALSE(graph.isAdjacent(6, 2)); + EXPECT_FALSE(graph.isAdjacent(6, 3)); + EXPECT_TRUE(graph.isAdjacent(6, 4)); + EXPECT_FALSE(graph.isAdjacent(6, 5)); + EXPECT_FALSE(graph.isAdjacent(6, 6)); + EXPECT_TRUE(graph.isAdjacent(6, 7)); + EXPECT_FALSE(graph.isAdjacent(7, 1)); + EXPECT_TRUE(graph.isAdjacent(7, 2)); + EXPECT_FALSE(graph.isAdjacent(7, 3)); + EXPECT_TRUE(graph.isAdjacent(7, 4)); + EXPECT_TRUE(graph.isAdjacent(7, 5)); + EXPECT_TRUE(graph.isAdjacent(7, 6)); + EXPECT_FALSE(graph.isAdjacent(7, 7)); + + EXPECT_ANY_THROW(std::ignore = layer.constructInteractionGraph(Z, 2)); + EXPECT_ANY_THROW(std::ignore = layer.constructInteractionGraph(RZZ, 0)); +} + +TEST(Layer, ExecutableOfType) { + auto qc = QuantumComputation(3); + qc.cz(0, 1); + qc.cz(0, 2); + qc.emplace_back(Targets{0, 1, 2}, RY, + std::vector{PI_2}); + qc.rz(qc::PI_4, 0); + qc.emplace_back(Targets{0, 1, 2}, RY, std::vector{-PI_2}); + qc.cz(1, 2); + + Layer const layer(qc); + EXPECT_EQ(layer.getExecutablesOfType(Z, 1).size(), 3); + EXPECT_EQ(layer.getExecutablesOfType(RZ, 0).size(), 0); +} +} // namespace qc diff --git a/test/datastructures/test_undirected_graph.cpp b/test/datastructures/test_undirected_graph.cpp new file mode 100644 index 000000000..2f0748c86 --- /dev/null +++ b/test/datastructures/test_undirected_graph.cpp @@ -0,0 +1,27 @@ +#include "datastructures/UndirectedGraph.hpp" + +#include "gtest/gtest.h" + +namespace qc { +TEST(UndirectedGraph, Numbered) { + UndirectedGraph g; + g.addVertex(0); + g.addEdge(1, 1, 0); + g.addEdge(1, 2, 1); + g.addEdge(2, 3, 2); + g.addEdge(3, 1, 3); + EXPECT_EQ(g.getNVertices(), 4); + EXPECT_EQ(g.getNEdges(), 4); + EXPECT_EQ(g.getEdge(1, 2), 1); + EXPECT_EQ(g.getEdge(1, 1), 0); + EXPECT_EQ(g.getDegree(2), 2); + EXPECT_EQ(g.getDegree(1), 3); + EXPECT_TRUE(g.isAdjacent(1, 2)); + EXPECT_FALSE(g.isAdjacent(1, 0)); + EXPECT_TRUE(g.isAdjacentEdge({1, 2}, {2, 3})); + + EXPECT_ANY_THROW(g.addVertex(1)); + EXPECT_ANY_THROW(std::ignore = g.getDegree(4)); + EXPECT_ANY_THROW(std::ignore = g.getEdge(0, 1)); +} +} // namespace qc diff --git a/test/na/CMakeLists.txt b/test/na/CMakeLists.txt new file mode 100644 index 000000000..209b5f3f5 --- /dev/null +++ b/test/na/CMakeLists.txt @@ -0,0 +1,4 @@ +if(TARGET MQT::CoreNA) + file(GLOB_RECURSE NA_TEST_SOURCES *.cpp) + package_add_test(mqt-core-na-test MQT::CoreNA ${NA_TEST_SOURCES}) +endif() diff --git a/test/na/test_nacomputation.cpp b/test/na/test_nacomputation.cpp new file mode 100644 index 000000000..e592c562e --- /dev/null +++ b/test/na/test_nacomputation.cpp @@ -0,0 +1,47 @@ +#include "na/NAComputation.hpp" +#include "na/NADefinitions.hpp" +#include "na/operations/NAGlobalOperation.hpp" +#include "na/operations/NALocalOperation.hpp" +#include "na/operations/NAShuttlingOperation.hpp" +#include "operations/OpType.hpp" + +#include "gtest/gtest.h" +#include +#include + +namespace na { +TEST(NAComputation, General) { + auto qc = NAComputation(); + qc.emplaceInitialPosition(std::make_shared(0, 0)); + qc.emplaceInitialPosition(std::make_shared(1, 0)); + qc.emplaceInitialPosition(std::make_shared(2, 0)); + qc.emplaceBack(std::make_unique( + FullOpType{qc::RZ, 0}, std::vector{qc::PI_2}, + std::make_shared(0, 0))); + qc.emplaceBack(std::make_unique(FullOpType{qc::RY, 0}, + std::vector{qc::PI_2})); + qc.emplaceBack(std::make_unique( + LOAD, + std::vector{std::make_shared(0, 0), std::make_shared(1, 0)}, + std::vector{std::make_shared(0, 1), + std::make_shared(1, 1)})); + qc.emplaceBack(std::make_unique( + MOVE, + std::vector{std::make_shared(0, 1), std::make_shared(1, 1)}, + std::vector{std::make_shared(4, 1), + std::make_shared(5, 1)})); + qc.emplaceBack(std::make_unique( + STORE, + std::vector{std::make_shared(4, 1), std::make_shared(5, 1)}, + std::vector{std::make_shared(4, 0), + std::make_shared(5, 0)})); + std::stringstream ss; + ss << qc; + EXPECT_EQ(ss.str(), "init at (0, 0), (1, 0), (2, 0);\n" + "rz(1.5708) at (0, 0);\n" + "ry(1.5708);\n" + "load (0, 0), (1, 0) to (0, 1), (1, 1);\n" + "move (0, 1), (1, 1) to (4, 1), (5, 1);\n" + "store (4, 1), (5, 1) to (4, 0), (5, 0);\n"); +} +} // namespace na diff --git a/test/na/test_nadefinitions.cpp b/test/na/test_nadefinitions.cpp new file mode 100644 index 000000000..aa915f307 --- /dev/null +++ b/test/na/test_nadefinitions.cpp @@ -0,0 +1,61 @@ +#include "QuantumComputation.hpp" +#include "na/NADefinitions.hpp" +#include "operations/OpType.hpp" + +#include "gtest/gtest.h" +#include +#include +#include + +namespace na { +TEST(NADefinitions, Point) { + const Point p(-1, 2); + EXPECT_EQ(p.x, -1); + EXPECT_EQ(p.y, 2); + EXPECT_EQ(p.length(), 2); + EXPECT_EQ(p.toString(), "(-1, 2)"); + std::stringstream ss; + ss << p; + EXPECT_EQ(ss.str(), "(-1, 2)"); + EXPECT_EQ(p, Point(-1, 2)); + EXPECT_FALSE(p == Point(1, 2)); + EXPECT_EQ(p - Point(1, 2), Point(-2, 0)); + EXPECT_EQ(Point(1, 2) + p, Point(0, 4)); +} + +TEST(NADefinitions, OpType) { + constexpr FullOpType t{qc::OpType::X, 1}; + EXPECT_EQ(t.type, qc::OpType::X); + EXPECT_EQ(t.nControls, 1); + EXPECT_EQ(t.toString(), "cx"); + std::stringstream ss; + ss << t; + EXPECT_EQ(ss.str(), "cx"); + EXPECT_EQ(t, (FullOpType{qc::OpType::X, 1})); + EXPECT_FALSE(t == (FullOpType{qc::OpType::X, 2})); +} + +TEST(NADefinitions, IsGlobal) { + const std::string testfile = "OPENQASM 3.0;\n" + "include \"stdgates.inc\";\n" + "qubit[3] q;\n" + "rz(pi/4) q[0];\n" + "ry(pi/2) q;\n"; + const auto qc = qc::QuantumComputation::fromQASM(testfile); + EXPECT_EQ(qc.getHighestLogicalQubitIndex(), 2); + EXPECT_FALSE(isGlobal(*qc.at(0), 3)); + EXPECT_TRUE(isGlobal(*qc.at(1), 3)); +} + +TEST(NADefinitions, OpTypeHash) { + std::unordered_map map; + map[FullOpType{qc::OpType::X, 1}] = 1; + map[FullOpType{qc::OpType::X, 2}] = 2; + map[FullOpType{qc::OpType::Y, 1}] = 3; + map[FullOpType{qc::OpType::Y, 2}] = 4; + EXPECT_EQ((map[FullOpType{qc::OpType::X, 1}]), 1); + EXPECT_EQ((map[FullOpType{qc::OpType::X, 2}]), 2); + EXPECT_EQ((map[FullOpType{qc::OpType::Y, 1}]), 3); + EXPECT_EQ((map[FullOpType{qc::OpType::Y, 2}]), 4); +} +} // namespace na diff --git a/test/na/test_naoperation.cpp b/test/na/test_naoperation.cpp new file mode 100644 index 000000000..fa340199f --- /dev/null +++ b/test/na/test_naoperation.cpp @@ -0,0 +1,48 @@ +#include "na/operations/NAGlobalOperation.hpp" +#include "na/operations/NALocalOperation.hpp" +#include "na/operations/NAOperation.hpp" +#include "na/operations/NAShuttlingOperation.hpp" + +#include "gtest/gtest.h" + +namespace na { +TEST(NAOperation, ShuttlingOperation) { + const NAShuttlingOperation shuttling( + LOAD, + std::vector{std::make_shared(0, 0), std::make_shared(1, 0)}, + std::vector{std::make_shared(0, 1), + std::make_shared(1, 1)}); + EXPECT_TRUE(shuttling.isShuttlingOperation()); + EXPECT_FALSE(shuttling.isLocalOperation()); + EXPECT_EQ(shuttling.getStart()[1]->x, 1); + EXPECT_EQ(shuttling.getEnd()[0]->x, 0); + EXPECT_ANY_THROW(NAShuttlingOperation( + ShuttleType::STORE, std::vector{std::make_shared(0, 0)}, + std::vector{std::make_shared(0, 1), + std::make_shared(1, 1)})); +} + +TEST(NAOperation, GlobalOperation) { + const NAGlobalOperation op(FullOpType{qc::RY, 0}, std::vector{qc::PI_2}); + EXPECT_FALSE(op.isShuttlingOperation()); + EXPECT_FALSE(op.isLocalOperation()); + EXPECT_TRUE(op.isGlobalOperation()); + EXPECT_DOUBLE_EQ(op.getParams()[0], qc::PI_2); + EXPECT_ANY_THROW(NAGlobalOperation(FullOpType{qc::ECR, 0})); +} + +TEST(NAOperation, LocalOperation) { + const NALocalOperation op(FullOpType{qc::RY, 0}, std::vector{qc::PI_2}, + std::make_shared(0, 0)); + EXPECT_FALSE(op.isShuttlingOperation()); + EXPECT_FALSE(op.isGlobalOperation()); + EXPECT_TRUE(op.isLocalOperation()); + EXPECT_EQ(op.getType(), (FullOpType{qc::RY, 0})); + EXPECT_DOUBLE_EQ(op.getParams()[0], qc::PI_2); + EXPECT_EQ(op.getPositions()[0]->x, 0); + EXPECT_ANY_THROW( + NALocalOperation(FullOpType{qc::ECR, 0}, std::make_shared(0, 0))); + EXPECT_ANY_THROW( + NALocalOperation(FullOpType{qc::RY, 1}, std::make_shared(0, 0))); +} +} // namespace na diff --git a/test/test_operation.cpp b/test/test_operation.cpp new file mode 100644 index 000000000..a7b1bcc1e --- /dev/null +++ b/test/test_operation.cpp @@ -0,0 +1,134 @@ +#include "operations/ClassicControlledOperation.hpp" +#include "operations/CompoundOperation.hpp" +#include "operations/NonUnitaryOperation.hpp" +#include "operations/Operation.hpp" +#include "operations/StandardOperation.hpp" +#include "operations/SymbolicOperation.hpp" + +#include + +TEST(StandardOperation, CommutesAtQubit) { + const qc::StandardOperation op1(0, 1, qc::OpType::RY, std::vector{qc::PI_2}); + const qc::StandardOperation op2(0, 1, qc::OpType::RY, std::vector{-qc::PI_4}); + const qc::StandardOperation op3(0, qc::OpType::RY, std::vector{-qc::PI_4}); + EXPECT_TRUE(op1.commutesAtQubit(op2, 0)); + EXPECT_TRUE(op1.commutesAtQubit(op2, 0)); + EXPECT_FALSE(op1.commutesAtQubit(op3, 0)); + EXPECT_TRUE(op1.commutesAtQubit(op2, 2)); +} + +TEST(CompoundOperation, CommutesAtQubit) { + qc::CompoundOperation op1; + op1.emplace_back(0, qc::OpType::RY, + std::vector{qc::PI_2}); + op1.emplace_back(1, qc::OpType::RX, + std::vector{qc::PI_2}); + qc::CompoundOperation op2; + op2.emplace_back(0, qc::OpType::RY, + std::vector{qc::PI_2}); + op2.emplace_back(1, qc::OpType::RY, + std::vector{qc::PI_2}); + op2.emplace_back(1, qc::OpType::RY, + std::vector{qc::PI_2}); + const qc::StandardOperation op3(0, qc::OpType::RY, std::vector{-qc::PI_4}); + const qc::StandardOperation op4(1, qc::OpType::RY, std::vector{-qc::PI_4}); + EXPECT_TRUE(op1.commutesAtQubit(op3, 0)); + EXPECT_TRUE(op3.commutesAtQubit(op1, 0)); + EXPECT_FALSE(op4.commutesAtQubit(op1, 1)); + EXPECT_TRUE(op4.commutesAtQubit(op2, 1)); + EXPECT_TRUE(op1.commutesAtQubit(op2, 0)); + EXPECT_FALSE(op1.commutesAtQubit(op2, 1)); + EXPECT_TRUE(op1.commutesAtQubit(op2, 2)); +} + +TEST(StandardOperation, IsInverseOf) { + const qc::StandardOperation op1(0, qc::OpType::RY, std::vector{qc::PI_2}); + qc::StandardOperation op1Inv = op1; + op1Inv.invert(); + EXPECT_TRUE(op1.isInverseOf(op1Inv)); + EXPECT_FALSE(op1.isInverseOf(op1)); + const qc::StandardOperation op2(0, qc::OpType::Sdg); + qc::StandardOperation op2Inv = op2; + op2Inv.invert(); + EXPECT_TRUE(op2.isInverseOf(op2Inv)); + EXPECT_FALSE(op2.isInverseOf(op2)); + const qc::StandardOperation op3(0, qc::OpType::X); + EXPECT_FALSE(op3.isInverseOf(op1)); +} + +TEST(CompoundOperation, GlobalIsInverseOf) { + qc::CompoundOperation op1; + op1.emplace_back(0, qc::RY, std::vector{qc::PI_2}); + op1.emplace_back(1, qc::RY, std::vector{qc::PI_2}); + // the actual inverse of op1 + qc::CompoundOperation op2; + op2.emplace_back(0, qc::RY, std::vector{-qc::PI_2}); + op2.emplace_back(1, qc::RY, std::vector{-qc::PI_2}); + // the compound operations with different number of operations + qc::CompoundOperation op3 = op2; + op3.emplace_back(2, qc::RY, std::vector{qc::PI_2}); + // the operations come in different order + qc::CompoundOperation op4; + op4.emplace_back(1, qc::RY, std::vector{-qc::PI_2}); + op4.emplace_back(0, qc::RY, std::vector{-qc::PI_2}); + EXPECT_TRUE(op1.isInverseOf(op2)); + EXPECT_TRUE(op2.isInverseOf(op1)); + EXPECT_FALSE(op1.isInverseOf(op3)); + EXPECT_FALSE(op1.isInverseOf(qc::StandardOperation(0, qc::RY))); + EXPECT_TRUE(op1.isInverseOf(op4)); +} + +TEST(CompoundOperation, IsInverseOf) { + // This functionality is not implemented yet, the function isInversOf leads to + // false negatives + qc::CompoundOperation op1; + op1.emplace_back(0, qc::OpType::RY, + std::vector{-qc::PI_2}); + op1.emplace_back(1, qc::OpType::RY, + std::vector{-qc::PI_2}); + qc::CompoundOperation op2 = op1; + op2.invert(); + EXPECT_TRUE(op1.isInverseOf(op2)); +} + +TEST(OpType, General) { + EXPECT_EQ(qc::toString(qc::RZ), "rz"); + std::stringstream ss; + ss << qc::OpType::RZ; + EXPECT_EQ(ss.str(), "rz"); +} + +TEST(OpType, SingleQubitGate) { + EXPECT_TRUE(qc::isSingleQubitGate(qc::P)); + EXPECT_FALSE(qc::isSingleQubitGate(qc::ECR)); +} + +TEST(NonStandardOperation, IsInverseOf) { + const qc::StandardOperation op(0, qc::I); + EXPECT_FALSE(qc::NonUnitaryOperation(qc::Targets{0}).isInverseOf(op)); + EXPECT_FALSE(qc::SymbolicOperation().isInverseOf(op)); +} + +TEST(NonStandardOperation, CommutesAtQubit) { + const qc::StandardOperation op(0, qc::X); + EXPECT_FALSE(qc::NonUnitaryOperation(qc::Targets{0}).commutesAtQubit(op, 0)); + EXPECT_FALSE( + qc::SymbolicOperation(0, qc::P, {sym::Expression()}) + .commutesAtQubit(op, 0)); +} + +TEST(Operation, IsIndividualGate) { + const qc::StandardOperation op1(0, qc::X); + EXPECT_TRUE(op1.isSingleQubitGate()); + const qc::StandardOperation op2(0, 1, qc::X); + EXPECT_FALSE(op2.isSingleQubitGate()); + const qc::StandardOperation op3(1, qc::RXX); + EXPECT_FALSE(op3.isSingleQubitGate()); +} + +TEST(Operation, IsDiagonalGate) { + const qc::StandardOperation op1(0, qc::X); + EXPECT_FALSE(op1.isDiagonalGate()); + const qc::StandardOperation op2(0, qc::Z); + EXPECT_TRUE(op2.isDiagonalGate()); +}