Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

State Preparation in MQT ✨ #543

Draft
wants to merge 32 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
1c891fd
Added State Preparation algorithm in new file, added to Cmake
M-J-Hochreiter Jan 31, 2024
6918a79
Merge branch 'main' into main
M-J-Hochreiter Mar 7, 2024
ee328e7
Split StatePreparation into hpp, cpp file
M-J-Hochreiter Jan 13, 2025
43aa73f
Merge remote-tracking branch 'upstream/main'
M-J-Hochreiter Jan 13, 2025
d8f3abd
Added StandardOperation to include, as library changed
M-J-Hochreiter Jan 13, 2025
d7e69f4
🎨 pre-commit fixes
pre-commit-ci[bot] Jan 13, 2025
84b5041
Refactored StatePreparation files to match the other algorithms
M-J-Hochreiter Jan 14, 2025
881cab7
🎨 pre-commit fixes
pre-commit-ci[bot] Jan 14, 2025
a6f4ff1
removed old StatePreparation file
M-J-Hochreiter Jan 14, 2025
9cbc8ce
Merge branch 'main' of https://github.com/M-J-Hochreiter/mqt-core
M-J-Hochreiter Jan 14, 2025
4918724
Compiler error and linting
M-J-Hochreiter Jan 14, 2025
066c4a3
🎨 pre-commit fixes
pre-commit-ci[bot] Jan 14, 2025
1a72f17
Fixed all build issues and linting problems
M-J-Hochreiter Jan 14, 2025
4119d2b
Merge branch 'main' of https://github.com/M-J-Hochreiter/mqt-core
M-J-Hochreiter Jan 14, 2025
63a81d3
🎨 pre-commit fixes
pre-commit-ci[bot] Jan 14, 2025
d29032f
Fixed last compile error
M-J-Hochreiter Jan 14, 2025
d0b7559
Merge branch 'main' of https://github.com/M-J-Hochreiter/mqt-core
M-J-Hochreiter Jan 14, 2025
fc06910
Made zero check more robust
M-J-Hochreiter Jan 14, 2025
9cbec23
🎨 pre-commit fixes
pre-commit-ci[bot] Jan 14, 2025
c41302d
fixed equality check on floating point with bit manipulation
M-J-Hochreiter Jan 16, 2025
036f68a
🎨 pre-commit fixes
pre-commit-ci[bot] Jan 16, 2025
b91836f
empty check with proper function
M-J-Hochreiter Jan 16, 2025
8a8899d
🎨 pre-commit fixes
pre-commit-ci[bot] Jan 16, 2025
d50e1e3
Merge remote-tracking branch 'upstream/main'
M-J-Hochreiter Jan 16, 2025
f281a75
Merge branch 'main' of https://github.com/M-J-Hochreiter/mqt-core
M-J-Hochreiter Jan 16, 2025
79a62f4
fixed new flatten operation function, added first test structure
M-J-Hochreiter Jan 17, 2025
1d862ac
🎨 pre-commit fixes
pre-commit-ci[bot] Jan 17, 2025
95d1832
removed test suffix
M-J-Hochreiter Jan 17, 2025
743da30
Merge branch 'main' of https://github.com/M-J-Hochreiter/mqt-core
M-J-Hochreiter Jan 17, 2025
1e4aba1
Merge remote-tracking branch 'upstream/main'
M-J-Hochreiter Jan 26, 2025
84a32fa
first few signature changes, docstring adapted
M-J-Hochreiter Jan 27, 2025
971d880
🎨 pre-commit fixes
pre-commit-ci[bot] Jan 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions include/mqt-core/algorithms/StatePreparation.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* 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 {
/**
* @brief Prepares a generic quantum state from a list of normalized
* complex amplitudes
*
* Adapted implementation of IBM Qiskit's State Preparation:
* https://github.com/Qiskit/qiskit/blob/e9ccd3f374fd5424214361d47febacfa5919e1e3/qiskit/circuit/library/data_preparation/state_preparation.py
* based on the following paper:
* V. V. Shende, S. S. Bullock and I. L. Markov, "Synthesis of
*quantum-logic circuits," in IEEE Transactions on Computer-Aided Design of
*Integrated Circuits and Systems, vol. 25, no. 6, pp. 1000-1010, June 2006,
*doi: 10.1109/TCAD.2005.855930.
*
* @param amplitudes state (vector) to prepare. Must be normalized and have a
*size that is a power of two
* @return quantum computation that prepares the state
* @throws invalid_argument @p amplitudes is not normalized or its length is not
*a power of two
**/
[[nodiscard]] auto
createStatePreparationCircuit(std::vector<std::complex<double>>& amplitudes)
-> QuantumComputation;
} // namespace qc
287 changes: 287 additions & 0 deletions src/algorithms/StatePreparation.cpp
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;
Copy link
Member

Choose a reason for hiding this comment

The 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(const 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(const std::vector<T>& vec) -> bool {
return std::abs(1 - twoNorm(vec)) < EPS;
}

[[nodiscard]] auto kroneckerProduct(const Matrix& matrixA,
const Matrix& matrixB) -> Matrix {
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,
const std::vector<double>& vector)
-> 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the top comment should probably be the @brief in the regular docstring

* @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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
multiplexer.emplace_back<StandardOperation>(Controls{}, 0, targetGate,
std::vector{angles[0]});
multiplexer.emplace_back<StandardOperation>(0, targetGate,
std::vector{angles[0]});

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
Copy link
Member

Choose a reason for hiding this comment

The 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use bit shifts instead of std::pow for efficiency

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not pass the amplitudes vector by value as this will always incur a copy.
Pass it by reference and modify it in-place instead of creating a new vector and returning the new vector.
This will require some adaptations to the general code logic probably.

Also just a general remark: prefer to reserve containers with the right size if you know the size upfront using vec.reserve(X). Furthermore, prefer to use emplace_back over push_back in general.


// creates circuit that takes desired vector to zero
[[nodiscard]] auto
gatesToUncompute(std::vector<std::complex<double>>& amplitudes,
size_t numQubits) -> 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't these use EPS?

// 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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of the unsigned int here should (probably) be Qubit.

}
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What were these kind of errors?
This should work out of the box.

for (auto& control: op->getControls()) {
  control.qubit += static_cast<Qubit>(i);
}

Note the & in the for loop

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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
multiplex(OpType{RY}, thetas, addLastCnot);
multiplex(RY, thetas, addLastCnot);

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());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can simply call the reverse method of the QuantumComputation class here.

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) {
Copy link
Member

Choose a reason for hiding this comment

The 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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now, the function signature suggests that the function will modify the amplitudes vector (since the argument is not a const reference).
This would be fine, as it is more efficient then copying, but it also implies that the amplitude vector is useless after calling this function.
The function docstring should note that the function changes the underlying vector in that case.

I see two options here:

  • either make the parameter a constant reference and perform an explicit copy of the state.
  • or update the docstring to reflect the behavior

(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"};
}

// check if the number of elements in the vector is a power of two
if (amplitudes.empty() ||
(amplitudes.size() & (amplitudes.size() - 1)) != 0) {
throw std::invalid_argument{
"Using State Preparation with vector size that is not a power of 2"};
}
const auto numQubits = static_cast<size_t>(std::log2(amplitudes.size()));
QuantumComputation toZeroCircuit = gatesToUncompute(amplitudes, numQubits);

// invert circuit
CircuitOptimizer::flattenOperations(toZeroCircuit);
toZeroCircuit.invert();

return toZeroCircuit;
}
} // namespace qc
43 changes: 43 additions & 0 deletions test/algorithms/test_statepreparation.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright (c) 2025 Chair for Design Automation, TUM
* All rights reserved.
*
* SPDX-License-Identifier: MIT
*
* Licensed under the MIT License
*/

#include "Definitions.hpp"

Check warning on line 10 in test/algorithms/test_statepreparation.cpp

View workflow job for this annotation

GitHub Actions / 🇨‌ Lint / 🚨 Lint

test/algorithms/test_statepreparation.cpp:10:1 [misc-include-cleaner]

included header Definitions.hpp is not used directly
#include "algorithms/StatePreparation.hpp"
#include "dd/Package.hpp"

Check warning on line 12 in test/algorithms/test_statepreparation.cpp

View workflow job for this annotation

GitHub Actions / 🇨‌ Lint / 🚨 Lint

test/algorithms/test_statepreparation.cpp:12:1 [misc-include-cleaner]

included header Package.hpp is not used directly
#include "ir/QuantumComputation.hpp"

Check warning on line 13 in test/algorithms/test_statepreparation.cpp

View workflow job for this annotation

GitHub Actions / 🇨‌ Lint / 🚨 Lint

test/algorithms/test_statepreparation.cpp:13:1 [misc-include-cleaner]

included header QuantumComputation.hpp is not used directly

#include <cmath>
#include <complex>
#include <gtest/gtest.h>
#include <sstream>

Check warning on line 18 in test/algorithms/test_statepreparation.cpp

View workflow job for this annotation

GitHub Actions / 🇨‌ Lint / 🚨 Lint

test/algorithms/test_statepreparation.cpp:18:1 [misc-include-cleaner]

included header sstream is not used directly
#include <string>

Check warning on line 19 in test/algorithms/test_statepreparation.cpp

View workflow job for this annotation

GitHub Actions / 🇨‌ Lint / 🚨 Lint

test/algorithms/test_statepreparation.cpp:19:1 [misc-include-cleaner]

included header string is not used directly
#include <vector>

class StatePreparation
: public testing::TestWithParam<std::vector<std::complex<double>>> {
protected:
std::vector<std::complex<double>> amplitudes{};

Check warning on line 25 in test/algorithms/test_statepreparation.cpp

View workflow job for this annotation

GitHub Actions / 🇨‌ Lint / 🚨 Lint

test/algorithms/test_statepreparation.cpp:25:47 [readability-redundant-member-init]

initializer for member 'amplitudes' is redundant

void TearDown() override {}
void SetUp() override { amplitudes = GetParam(); }
};

INSTANTIATE_TEST_SUITE_P(
StatePreparation, StatePreparation,
testing::Values(std::vector{std::complex{1 / std::sqrt(2)},
std::complex{-1 / std::sqrt(2)}},
std::vector<std::complex<double>>{
0, std::complex{1 / std::sqrt(2)},
std::complex{-1 / std::sqrt(2)}, 0}));

TEST_P(StatePreparation, StatePreparationCircuitSimulation) {
ASSERT_NO_THROW({ auto qc = qc::createStatePreparationCircuit(amplitudes); });
}

TEST_P(StatePreparation, StatePreparationCircuit) {}
Loading