Skip to content

Commit 3756f44

Browse files
committed
Merge branch 'main' into amc-apply.noise
2 parents 3134538 + 28c86a4 commit 3756f44

File tree

4 files changed

+181
-168
lines changed

4 files changed

+181
-168
lines changed

runtime/common/NoiseModel.cpp

+111-1
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,73 @@
1010
#include "Logger.h"
1111
#include "common/CustomOp.h"
1212
#include "common/EigenDense.h"
13+
#include <numeric>
14+
#include <optional>
15+
1316
namespace cudaq {
1417

18+
// Helper to check whether a matrix is a scaled unitary matrix, i.e., `k * U`
19+
// where U is a unitary matrix. If so, it also returns the `k` factor.
20+
// Otherwise, return a nullopt.
21+
static std::optional<double>
22+
isScaledUnitary(const std::vector<std::complex<double>> &mat, double eps) {
23+
typedef Eigen::Matrix<std::complex<double>, Eigen::Dynamic, Eigen::Dynamic,
24+
Eigen::RowMajor>
25+
RowMajorMatTy;
26+
const int dim = std::log2(mat.size());
27+
Eigen::Map<const RowMajorMatTy> kMat(mat.data(), dim, dim);
28+
if (kMat.isZero(eps))
29+
return 0.0;
30+
// Check that (K_dag * K) is a scaled identity matrix
31+
// i.e., the K matrix is a scaled unitary.
32+
auto kdK = kMat.adjoint() * kMat;
33+
if (!kdK.isDiagonal(eps))
34+
return std::nullopt;
35+
// First element
36+
std::complex<double> val = kdK(0, 0);
37+
if (val.real() > eps && std::abs(val.imag()) < eps) {
38+
auto scaledKdK = (std::complex<double>{1.0} / val) * kdK;
39+
if (scaledKdK.isIdentity())
40+
return std::sqrt(val.real());
41+
}
42+
return std::nullopt;
43+
}
44+
45+
// Helper to determine if a vector of Kraus ops are actually a unitary mixture.
46+
// If so, it returns all the unitaries and the probabilities associated with
47+
// each one of those unitaries.
48+
static std::optional<std::pair<std::vector<double>,
49+
std::vector<std::vector<std::complex<double>>>>>
50+
computeUnitaryMixture(
51+
const std::vector<std::vector<std::complex<double>>> &krausOps,
52+
double tol = 1e-6) {
53+
std::vector<double> probs;
54+
std::vector<std::vector<std::complex<double>>> mats;
55+
const auto scaleMat = [](const std::vector<std::complex<double>> &mat,
56+
double scaleFactor) {
57+
std::vector<std::complex<double>> scaledMat = mat;
58+
// If scaleFactor is 0, then it means the original matrix was likely all
59+
// zeros. In that case, the probability will be 0, so the matrix doesn't
60+
// matter, but we don't want NaNs to trickle in anywhere.
61+
if (scaleFactor != 0.0)
62+
for (auto &x : scaledMat)
63+
x /= scaleFactor;
64+
return scaledMat;
65+
};
66+
for (const auto &op : krausOps) {
67+
const auto scaledFactor = isScaledUnitary(op, tol);
68+
if (!scaledFactor.has_value())
69+
return std::nullopt;
70+
probs.emplace_back(scaledFactor.value() * scaledFactor.value());
71+
mats.emplace_back(scaleMat(op, scaledFactor.value()));
72+
}
73+
74+
if (std::abs(1.0 - std::reduce(probs.begin(), probs.end())) > tol)
75+
return std::nullopt;
76+
77+
return std::make_pair(probs, mats);
78+
}
79+
1580
template <typename EigenMatTy>
1681
bool isIdentity(const EigenMatTy &mat, double threshold = 1e-9) {
1782
EigenMatTy idMat = EigenMatTy::Identity(mat.rows(), mat.cols());
@@ -78,9 +143,52 @@ void validateCompletenessRelation_fp64(const std::vector<kraus_op> &ops) {
78143
"Provided kraus_ops are not completely positive and trace preserving.");
79144
}
80145

146+
void generateUnitaryParameters_fp32(
147+
const std::vector<kraus_op> &ops,
148+
std::vector<std::vector<std::complex<double>>> &unitary_ops,
149+
std::vector<double> &probabilities) {
150+
std::vector<std::vector<std::complex<double>>> double_kraus_ops;
151+
double_kraus_ops.reserve(ops.size());
152+
for (auto &op : ops) {
153+
// WARNING: danger here. We are intentially treating the incoming op as fp32
154+
// type instead of what the compiler thinks it is (fp64). We have to do this
155+
// because this file is compiled with cudaq::real = fp64, but the incoming
156+
// data for this specific routine is actually fp32.
157+
const std::complex<float> *ptr =
158+
reinterpret_cast<const std::complex<float> *>(op.data.data());
159+
// Use 2 * size because pointer arithmetic is on fp32 instead of fp64
160+
double_kraus_ops.emplace_back(
161+
std::vector<std::complex<double>>(ptr, ptr + 2 * op.data.size()));
162+
}
163+
164+
auto asUnitaryMixture = computeUnitaryMixture(double_kraus_ops);
165+
if (asUnitaryMixture.has_value()) {
166+
probabilities = std::move(asUnitaryMixture.value().first);
167+
unitary_ops = std::move(asUnitaryMixture.value().second);
168+
}
169+
}
170+
171+
void generateUnitaryParameters_fp64(
172+
const std::vector<kraus_op> &ops,
173+
std::vector<std::vector<std::complex<double>>> &unitary_ops,
174+
std::vector<double> &probabilities) {
175+
std::vector<std::vector<std::complex<double>>> double_kraus_ops;
176+
double_kraus_ops.reserve(ops.size());
177+
for (auto &op : ops)
178+
double_kraus_ops.emplace_back(
179+
std::vector<std::complex<double>>(op.data.begin(), op.data.end()));
180+
181+
auto asUnitaryMixture = computeUnitaryMixture(double_kraus_ops);
182+
if (asUnitaryMixture.has_value()) {
183+
probabilities = std::move(asUnitaryMixture.value().first);
184+
unitary_ops = std::move(asUnitaryMixture.value().second);
185+
}
186+
}
187+
81188
kraus_channel::kraus_channel(const kraus_channel &other)
82189
: ops(other.ops), noise_type(other.noise_type),
83-
parameters(other.parameters) {}
190+
parameters(other.parameters), unitary_ops(other.unitary_ops),
191+
probabilities(other.probabilities) {}
84192

85193
std::size_t kraus_channel::size() const { return ops.size(); }
86194

@@ -94,6 +202,8 @@ kraus_channel &kraus_channel::operator=(const kraus_channel &other) {
94202
ops = other.ops;
95203
noise_type = other.noise_type;
96204
parameters = other.parameters;
205+
unitary_ops = other.unitary_ops;
206+
probabilities = other.probabilities;
97207
return *this;
98208
}
99209

runtime/common/NoiseModel.h

+40
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,12 @@ struct kraus_op {
120120

121121
void validateCompletenessRelation_fp32(const std::vector<kraus_op> &ops);
122122
void validateCompletenessRelation_fp64(const std::vector<kraus_op> &ops);
123+
void generateUnitaryParameters_fp32(
124+
const std::vector<kraus_op> &ops,
125+
std::vector<std::vector<std::complex<double>>> &, std::vector<double> &);
126+
void generateUnitaryParameters_fp64(
127+
const std::vector<kraus_op> &ops,
128+
std::vector<std::vector<std::complex<double>>> &, std::vector<double> &);
123129

124130
/// @brief A kraus_channel represents a quantum noise channel
125131
/// on specific qubits. The action of the noise channel is
@@ -161,6 +167,16 @@ class kraus_channel {
161167
// corruption.
162168
std::vector<double> parameters;
163169

170+
/// @brief If all Kraus ops are - when scaled - unitary, this holds the
171+
/// unitary versions of those ops. These values are always "double" regardless
172+
/// of whether cudaq::real is float or double.
173+
std::vector<std::vector<std::complex<double>>> unitary_ops;
174+
175+
/// @brief If all Kraus ops are - when scaled - unitary, this holds the
176+
/// probabilities of those ops. These values are always "double" regardless
177+
/// of whether cudaq::real is float or double.
178+
std::vector<double> probabilities;
179+
164180
virtual ~kraus_channel() = default;
165181

166182
/// @brief The nullary constructor
@@ -176,12 +192,14 @@ class kraus_channel {
176192
kraus_channel(std::initializer_list<T> &&...inputLists) {
177193
(ops.emplace_back(std::move(inputLists)), ...);
178194
validateCompleteness();
195+
generateUnitaryParameters();
179196
}
180197

181198
/// @brief The constructor, take qubits and channel kraus_ops as lvalue
182199
/// reference
183200
kraus_channel(const std::vector<kraus_op> &inOps) : ops(inOps) {
184201
validateCompleteness();
202+
generateUnitaryParameters();
185203
}
186204

187205
/// @brief The constructor, take qubits and channel kraus_ops as rvalue
@@ -211,6 +229,23 @@ class kraus_channel {
211229
std::string get_type_name() const {
212230
return get_noise_model_type_name(noise_type);
213231
}
232+
233+
/// @brief Returns whether or not this is a unitary mixture.
234+
bool is_unitary_mixture() const { return !unitary_ops.empty(); }
235+
236+
/// @brief Checks if Kraus ops have unitary representations and saves them if
237+
/// they do. Users should only need to call this if they have modified the
238+
/// Kraus ops and want to recompute these values.
239+
void generateUnitaryParameters() {
240+
unitary_ops.clear();
241+
probabilities.clear();
242+
if constexpr (std::is_same_v<cudaq::complex::value_type, float>) {
243+
generateUnitaryParameters_fp32(ops, this->unitary_ops,
244+
this->probabilities);
245+
return;
246+
}
247+
generateUnitaryParameters_fp64(ops, this->unitary_ops, this->probabilities);
248+
}
214249
};
215250

216251
#define REGISTER_KRAUS_CHANNEL() \
@@ -466,6 +501,7 @@ class depolarization_channel : public kraus_channel {
466501
this->parameters.push_back(probability);
467502
noise_type = noise_model_type::depolarization_channel;
468503
validateCompleteness();
504+
generateUnitaryParameters();
469505
}
470506
depolarization_channel(const real probability)
471507
: depolarization_channel(std::vector<cudaq::real>{probability}) {}
@@ -487,6 +523,8 @@ class amplitude_damping_channel : public kraus_channel {
487523
this->parameters.push_back(probability);
488524
noise_type = noise_model_type::amplitude_damping_channel;
489525
validateCompleteness();
526+
// Note: amplitude damping is non-unitary, so there is no value in calling
527+
// generateUnitaryParameters().
490528
}
491529
amplitude_damping_channel(const real probability)
492530
: amplitude_damping_channel(std::vector<cudaq::real>{probability}) {}
@@ -509,6 +547,7 @@ class bit_flip_channel : public kraus_channel {
509547
this->parameters.push_back(probability);
510548
noise_type = noise_model_type::bit_flip_channel;
511549
validateCompleteness();
550+
generateUnitaryParameters();
512551
}
513552
bit_flip_channel(const real probability)
514553
: bit_flip_channel(std::vector<cudaq::real>{probability}) {}
@@ -532,6 +571,7 @@ class phase_flip_channel : public kraus_channel {
532571
this->parameters.push_back(probability);
533572
noise_type = noise_model_type::phase_flip_channel;
534573
validateCompleteness();
574+
generateUnitaryParameters();
535575
}
536576
phase_flip_channel(const real probability)
537577
: phase_flip_channel(std::vector<cudaq::real>{probability}) {}

0 commit comments

Comments
 (0)