10
10
#include " Logger.h"
11
11
#include " common/CustomOp.h"
12
12
#include " common/EigenDense.h"
13
+ #include < numeric>
14
+ #include < optional>
15
+
13
16
namespace cudaq {
14
17
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
+
15
80
template <typename EigenMatTy>
16
81
bool isIdentity (const EigenMatTy &mat, double threshold = 1e-9 ) {
17
82
EigenMatTy idMat = EigenMatTy::Identity (mat.rows (), mat.cols ());
@@ -78,9 +143,52 @@ void validateCompletenessRelation_fp64(const std::vector<kraus_op> &ops) {
78
143
" Provided kraus_ops are not completely positive and trace preserving." );
79
144
}
80
145
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
+
81
188
kraus_channel::kraus_channel (const kraus_channel &other)
82
189
: 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) {}
84
192
85
193
std::size_t kraus_channel::size () const { return ops.size (); }
86
194
@@ -94,6 +202,8 @@ kraus_channel &kraus_channel::operator=(const kraus_channel &other) {
94
202
ops = other.ops ;
95
203
noise_type = other.noise_type ;
96
204
parameters = other.parameters ;
205
+ unitary_ops = other.unitary_ops ;
206
+ probabilities = other.probabilities ;
97
207
return *this ;
98
208
}
99
209
0 commit comments