-
Notifications
You must be signed in to change notification settings - Fork 33
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
State Preparation in MQT ✨ #543
base: main
Are you sure you want to change the base?
Changes from 17 commits
1c891fd
6918a79
ee328e7
43aa73f
d8f3abd
d7e69f4
84b5041
881cab7
a6f4ff1
9cbc8ce
4918724
066c4a3
1a72f17
4119d2b
63a81d3
d29032f
d0b7559
fc06910
9cbec23
c41302d
036f68a
b91836f
8a8899d
d50e1e3
f281a75
79a62f4
1d862ac
95d1832
743da30
1e4aba1
84a32fa
971d880
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
/* | ||
* Copyright (c) 2025 Chair for Design Automation, TUM | ||
* All rights reserved. | ||
* | ||
* SPDX-License-Identifier: MIT | ||
* | ||
* Licensed under the MIT License | ||
*/ | ||
|
||
#pragma once | ||
|
||
#include "ir/QuantumComputation.hpp" | ||
|
||
#include <complex> | ||
#include <vector> | ||
|
||
namespace qc { | ||
/** | ||
* Prepares a generic Quantum State from a list of normalized complex | ||
amplitudes | ||
* Adapted implementation of Qiskit State Preparation: | ||
* | ||
https://github.com/Qiskit/qiskit/blob/main/qiskit/circuit/library/data_preparation/state_preparation.py# | ||
* based on paper: | ||
* Shende, Bullock, Markov. Synthesis of Quantum Logic Circuits (2004) | ||
[`https://ieeexplore.ieee.org/document/1629135`] | ||
* */ | ||
|
||
/** | ||
* @throws invalid_argument when amplitudes are not normalized or length not | ||
* power of 2 | ||
* @param list of complex amplitudes to initialize to | ||
* @return MQT Circuit that initializes a state | ||
* */ | ||
[[nodiscard]] auto | ||
createStatePreparationCircuit(std::vector<std::complex<double>>& amplitudes) | ||
-> QuantumComputation; | ||
} // namespace qc |
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,287 @@ | ||||||||||
/* | ||||||||||
* Copyright (c) 2025 Chair for Design Automation, TUM | ||||||||||
* All rights reserved. | ||||||||||
* | ||||||||||
* SPDX-License-Identifier: MIT | ||||||||||
* | ||||||||||
* Licensed under the MIT License | ||||||||||
*/ | ||||||||||
|
||||||||||
#include "algorithms/StatePreparation.hpp" | ||||||||||
|
||||||||||
#include "Definitions.hpp" | ||||||||||
#include "circuit_optimizer/CircuitOptimizer.hpp" | ||||||||||
#include "ir/QuantumComputation.hpp" | ||||||||||
#include "ir/operations/OpType.hpp" | ||||||||||
#include "ir/operations/Operation.hpp" | ||||||||||
#include "ir/operations/StandardOperation.hpp" | ||||||||||
|
||||||||||
#include <algorithm> | ||||||||||
#include <cmath> | ||||||||||
#include <complex> | ||||||||||
#include <cstddef> | ||||||||||
#include <cstdint> | ||||||||||
#include <iterator> | ||||||||||
#include <numeric> | ||||||||||
#include <tuple> | ||||||||||
#include <vector> | ||||||||||
|
||||||||||
static const double EPS = 1e-10; | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should this be a configurable parameter of the method? |
||||||||||
|
||||||||||
namespace qc { | ||||||||||
using Matrix = std::vector<std::vector<double>>; | ||||||||||
|
||||||||||
template <typename T> [[nodiscard]] auto twoNorm(std::vector<T> vec) -> double { | ||||||||||
double norm = 0; | ||||||||||
for (auto elem : vec) { | ||||||||||
norm += std::norm(elem); | ||||||||||
} | ||||||||||
return sqrt(norm); | ||||||||||
} | ||||||||||
|
||||||||||
template <typename T> | ||||||||||
[[nodiscard]] auto isNormalized(std::vector<T> vec) -> bool { | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||||||
return std::abs(1 - twoNorm(vec)) < EPS; | ||||||||||
} | ||||||||||
|
||||||||||
[[nodiscard]] auto kroneckerProduct(Matrix matrixA, Matrix matrixB) -> Matrix { | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A nd B should be passed as const references here to avoid copying them on every invocation |
||||||||||
size_t const rowA = matrixA.size(); | ||||||||||
size_t const rowB = matrixB.size(); | ||||||||||
size_t const colA = matrixA[0].size(); | ||||||||||
size_t const colB = matrixB[0].size(); | ||||||||||
// initialize size | ||||||||||
Matrix newMatrix{(rowA * rowB), std::vector<double>(colA * colB, 0)}; | ||||||||||
// code taken from RosettaCode slightly adapted | ||||||||||
for (size_t i = 0; i < rowA; ++i) { | ||||||||||
// k loops till rowB | ||||||||||
for (size_t j = 0; j < colA; ++j) { | ||||||||||
// j loops till colA | ||||||||||
for (size_t k = 0; k < rowB; ++k) { | ||||||||||
// l loops till colB | ||||||||||
for (size_t l = 0; l < colB; ++l) { | ||||||||||
// Each element of matrix A is | ||||||||||
// multiplied by whole Matrix B | ||||||||||
// resp and stored as Matrix C | ||||||||||
newMatrix[i * rowB + k][j * colB + l] = matrixA[i][j] * matrixB[k][l]; | ||||||||||
} | ||||||||||
} | ||||||||||
} | ||||||||||
} | ||||||||||
return newMatrix; | ||||||||||
} | ||||||||||
|
||||||||||
[[nodiscard]] auto createIdentity(size_t size) -> Matrix { | ||||||||||
Matrix identity{ | ||||||||||
std::vector<std::vector<double>>(size, std::vector<double>(size, 0))}; | ||||||||||
for (size_t i = 0; i < size; ++i) { | ||||||||||
identity[i][i] = 1; | ||||||||||
} | ||||||||||
return identity; | ||||||||||
} | ||||||||||
|
||||||||||
[[nodiscard]] auto matrixVectorProd(const Matrix& matrix, | ||||||||||
std::vector<double> vector) | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. vector should be a const reference here to avoid a copy on each invocation |
||||||||||
-> std::vector<double> { | ||||||||||
std::vector<double> result; | ||||||||||
for (const auto& matrixVec : matrix) { | ||||||||||
double sum{0}; | ||||||||||
for (size_t i = 0; i < matrixVec.size(); ++i) { | ||||||||||
sum += matrixVec[i] * vector[i]; | ||||||||||
} | ||||||||||
result.push_back(sum); | ||||||||||
} | ||||||||||
return result; | ||||||||||
} | ||||||||||
|
||||||||||
// recursive implementation that returns multiplexer circuit | ||||||||||
/** | ||||||||||
Comment on lines
+98
to
+99
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the top comment should probably be the |
||||||||||
* @param target_gate : Ry or Rz gate to apply to target qubit, multiplexed | ||||||||||
* over all other "select" qubits | ||||||||||
* @param angles : list of rotation angles to apply Ry and Rz | ||||||||||
* @param lastCnot : add last cnot if true | ||||||||||
* @return multiplexer circuit as QuantumComputation | ||||||||||
*/ | ||||||||||
[[nodiscard]] auto multiplex(OpType targetGate, std::vector<double> angles, | ||||||||||
bool lastCnot) -> QuantumComputation { | ||||||||||
size_t const listLen = angles.size(); | ||||||||||
double const localNumQubits = | ||||||||||
std::floor(std::log2(static_cast<double>(listLen))) + 1; | ||||||||||
QuantumComputation multiplexer{static_cast<size_t>(localNumQubits)}; | ||||||||||
// recursion base case | ||||||||||
if (localNumQubits == 1) { | ||||||||||
multiplexer.emplace_back<StandardOperation>(Controls{}, 0, targetGate, | ||||||||||
std::vector{angles[0]}); | ||||||||||
Comment on lines
+114
to
+115
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
should work just fine |
||||||||||
return multiplexer; | ||||||||||
} | ||||||||||
|
||||||||||
Matrix const matrix{std::vector<double>{0.5, 0.5}, | ||||||||||
std::vector<double>{0.5, -0.5}}; | ||||||||||
Comment on lines
+119
to
+120
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. might be worth defining this as static constexpr outside of the function to avoid creating it on every invocation |
||||||||||
Matrix const identity = | ||||||||||
createIdentity(static_cast<size_t>(pow(2., localNumQubits - 2.))); | ||||||||||
Comment on lines
+121
to
+122
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. use bit shifts instead of |
||||||||||
Matrix const angleWeights = kroneckerProduct(matrix, identity); | ||||||||||
|
||||||||||
angles = matrixVectorProd(angleWeights, angles); | ||||||||||
|
||||||||||
std::vector<double> const angles1{ | ||||||||||
std::make_move_iterator(angles.begin()), | ||||||||||
std::make_move_iterator(angles.begin() + | ||||||||||
static_cast<int64_t>(listLen) / 2)}; | ||||||||||
QuantumComputation multiplex1 = multiplex(targetGate, angles1, false); | ||||||||||
|
||||||||||
// append multiplex1 to multiplexer | ||||||||||
multiplexer.emplace_back<Operation>(multiplex1.asOperation()); | ||||||||||
// flips the LSB qubit, control on MSB | ||||||||||
multiplexer.cx(0, static_cast<Qubit>(localNumQubits - 1)); | ||||||||||
|
||||||||||
std::vector<double> const angles2{std::make_move_iterator(angles.begin()) + | ||||||||||
static_cast<int64_t>(listLen) / 2, | ||||||||||
std::make_move_iterator(angles.end())}; | ||||||||||
QuantumComputation multiplex2 = multiplex(targetGate, angles2, false); | ||||||||||
|
||||||||||
// extra efficiency by reversing (!= inverting) second multiplex | ||||||||||
if (listLen > 1) { | ||||||||||
multiplex2.reverse(); | ||||||||||
multiplexer.emplace_back<Operation>(multiplex2.asOperation()); | ||||||||||
} else { | ||||||||||
multiplexer.emplace_back<Operation>(multiplex2.asOperation()); | ||||||||||
} | ||||||||||
|
||||||||||
if (lastCnot) { | ||||||||||
multiplexer.cx(0, static_cast<Qubit>(localNumQubits - 1)); | ||||||||||
} | ||||||||||
|
||||||||||
CircuitOptimizer::flattenOperations(multiplexer); | ||||||||||
return multiplexer; | ||||||||||
} | ||||||||||
|
||||||||||
[[nodiscard]] auto blochAngles(std::complex<double> const complexA, | ||||||||||
std::complex<double> const complexB) | ||||||||||
-> std::tuple<std::complex<double>, double, double> { | ||||||||||
double theta{0}; | ||||||||||
double phi{0}; | ||||||||||
double finalT{0}; | ||||||||||
double const magA = std::abs(complexA); | ||||||||||
double const magB = std::abs(complexB); | ||||||||||
double const finalR = sqrt(pow(magA, 2) + pow(magB, 2)); | ||||||||||
if (finalR > EPS) { | ||||||||||
theta = 2 * acos(magA / finalR); | ||||||||||
double const aAngle = std::arg(complexA); | ||||||||||
double const bAngle = std::arg(complexB); | ||||||||||
finalT = aAngle + bAngle; | ||||||||||
phi = bAngle - aAngle; | ||||||||||
} | ||||||||||
return {finalR * exp(std::complex<double>{0, 1} * finalT / 2.), theta, phi}; | ||||||||||
} | ||||||||||
|
||||||||||
// works out Ry and Rz rotation angles used to disentangle LSB qubit | ||||||||||
// rotations make up block diagonal matrix U | ||||||||||
[[nodiscard]] auto | ||||||||||
rotationsToDisentangle(std::vector<std::complex<double>> amplitudes) | ||||||||||
-> std::tuple<std::vector<std::complex<double>>, std::vector<double>, | ||||||||||
std::vector<double>> { | ||||||||||
std::vector<std::complex<double>> remainingVector; | ||||||||||
std::vector<double> thetas; | ||||||||||
std::vector<double> phis; | ||||||||||
for (size_t i = 0; i < (amplitudes.size() / 2); ++i) { | ||||||||||
auto [remains, theta, phi] = | ||||||||||
blochAngles(amplitudes[2 * i], amplitudes[2 * i + 1]); | ||||||||||
remainingVector.push_back(remains); | ||||||||||
// minus sign because we move it to zero | ||||||||||
thetas.push_back(-theta); | ||||||||||
phis.push_back(-phi); | ||||||||||
} | ||||||||||
return {remainingVector, thetas, phis}; | ||||||||||
} | ||||||||||
Comment on lines
+181
to
+196
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do not pass the Also just a general remark: prefer to reserve containers with the right size if you know the size upfront using |
||||||||||
|
||||||||||
// creates circuit that takes desired vector to zero | ||||||||||
[[nodiscard]] auto | ||||||||||
gatesToUncompute(std::vector<std::complex<double>> amplitudes, size_t numQubits) | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This method should not take its argument by value as this would cause a copy for each invocation. Pass this by reference and modify it in-place. |
||||||||||
-> QuantumComputation { | ||||||||||
QuantumComputation disentangler{numQubits}; | ||||||||||
for (size_t i = 0; i < numQubits; ++i) { | ||||||||||
// rotations to disentangle LSB | ||||||||||
auto [remainingParams, thetas, phis] = rotationsToDisentangle(amplitudes); | ||||||||||
amplitudes = remainingParams; | ||||||||||
// perform required rotations | ||||||||||
bool addLastCnot = true; | ||||||||||
double const phisNorm = twoNorm(phis); | ||||||||||
double const thetasNorm = twoNorm(thetas); | ||||||||||
if (phisNorm != 0 && thetasNorm != 0) { | ||||||||||
addLastCnot = false; | ||||||||||
} | ||||||||||
if (phisNorm != 0) { | ||||||||||
Comment on lines
+211
to
+214
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't these use |
||||||||||
// call multiplex with RZGate | ||||||||||
QuantumComputation rzMultiplexer = | ||||||||||
multiplex(OpType{RZ}, phis, addLastCnot); | ||||||||||
// append rzMultiplexer to disentangler, but it should only attach on | ||||||||||
// qubits i-numQubits, thus "i" is added to the local qubit indices | ||||||||||
for (auto& op : rzMultiplexer) { | ||||||||||
for (auto& target : op->getTargets()) { | ||||||||||
target += static_cast<unsigned int>(i); | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Most of the |
||||||||||
} | ||||||||||
for (auto control : op->getControls()) { | ||||||||||
// there were some errors when accessing the qubit directly and | ||||||||||
// adding to it | ||||||||||
op->setControls( | ||||||||||
Controls{Control{control.qubit + static_cast<unsigned int>(i)}}); | ||||||||||
Comment on lines
+225
to
+228
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What were these kind of errors? for (auto& control: op->getControls()) {
control.qubit += static_cast<Qubit>(i);
} Note the Applies to other places in the code as well |
||||||||||
} | ||||||||||
} | ||||||||||
disentangler.emplace_back<Operation>(rzMultiplexer.asOperation()); | ||||||||||
} | ||||||||||
if (thetasNorm != 0) { | ||||||||||
// call multiplex with RYGate | ||||||||||
QuantumComputation ryMultiplexer = | ||||||||||
multiplex(OpType{RY}, thetas, addLastCnot); | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
simpler and less code to type. applies throughout the whole file. |
||||||||||
// append reversed ry_multiplexer to disentangler, but it should only | ||||||||||
// attach on qubits i-numQubits, thus "i" is added to the local qubit | ||||||||||
// indices | ||||||||||
std::reverse(ryMultiplexer.begin(), ryMultiplexer.end()); | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you can simply call the |
||||||||||
for (auto& op : ryMultiplexer) { | ||||||||||
for (auto& target : op->getTargets()) { | ||||||||||
target += static_cast<unsigned int>(i); | ||||||||||
} | ||||||||||
for (auto control : op->getControls()) { | ||||||||||
// there were some errors when accessing the qubit directly and | ||||||||||
// adding to it | ||||||||||
op->setControls( | ||||||||||
Controls{Control{control.qubit + static_cast<unsigned int>(i)}}); | ||||||||||
} | ||||||||||
} | ||||||||||
disentangler.emplace_back<Operation>(ryMultiplexer.asOperation()); | ||||||||||
} | ||||||||||
} | ||||||||||
// adjust global phase according to the last e^(it) | ||||||||||
double const arg = -std::arg(std::accumulate( | ||||||||||
amplitudes.begin(), amplitudes.end(), std::complex<double>(0, 0))); | ||||||||||
if (arg != 0) { | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. EPS? |
||||||||||
disentangler.gphase(arg); | ||||||||||
} | ||||||||||
return disentangler; | ||||||||||
} | ||||||||||
|
||||||||||
auto createStatePreparationCircuit( | ||||||||||
std::vector<std::complex<double>>& amplitudes) -> QuantumComputation { | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right now, the function signature suggests that the function will modify the I see two options here:
(note that his comment depends on some of the other comments in the review) |
||||||||||
|
||||||||||
if (!isNormalized(amplitudes)) { | ||||||||||
throw std::invalid_argument{ | ||||||||||
"Using State Preparation with Amplitudes that are not normalized"}; | ||||||||||
} | ||||||||||
|
||||||||||
// get number of qubits needed | ||||||||||
double const numQubits = std::log2(amplitudes.size()); | ||||||||||
|
||||||||||
if (numQubits == 0 || std::floor(numQubits) != numQubits) { | ||||||||||
|
||||||||||
throw std::invalid_argument{ | ||||||||||
"Using State Preparation with vector size that is not a power of 2"}; | ||||||||||
} | ||||||||||
|
||||||||||
QuantumComputation toZeroCircuit = | ||||||||||
gatesToUncompute(amplitudes, static_cast<size_t>(numQubits)); | ||||||||||
|
||||||||||
// invert circuit | ||||||||||
CircuitOptimizer::flattenOperations(toZeroCircuit); | ||||||||||
toZeroCircuit.invert(); | ||||||||||
|
||||||||||
return toZeroCircuit; | ||||||||||
} | ||||||||||
} // namespace qc |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A suggestion for a slightly improved docstring here. Note that this will be reformatted by clang-format if you simply accept the suggestion.