Maestro 0.3.1
Unified interface for quantum circuit simulation
Loading...
Searching...
No Matches
QuantumChannel.h
Go to the documentation of this file.
1
6
7#pragma once
8
9#ifndef _SIMULATORS_QUANTUM_CHANNEL_H_
10#define _SIMULATORS_QUANTUM_CHANNEL_H_
11
12#include <Eigen/Eigen>
13
14#include <algorithm>
15#include <cmath>
16#include <complex>
17#include <cstddef>
18#include <limits>
19#include <stdexcept>
20#include <string>
21#include <utility>
22#include <vector>
23
24namespace Simulators {
25
34 public:
35 using Matrix = Eigen::MatrixXcd;
36 using KrausOperators = std::vector<Matrix>;
37
39 explicit QuantumChannel(KrausOperators krausOperators)
40 : krausOperators_(std::move(krausOperators)), numQubits_(0) {
41 Validate();
42 }
43
44 const KrausOperators& GetKrausOperators() const { return krausOperators_; }
45 size_t GetNumberOfQubits() const { return numQubits_; }
46
60 bool IsApprox(const QuantumChannel& other, double tolerance = 1e-12) const {
61 if (tolerance < 0.0 || !std::isfinite(tolerance))
62 throw std::invalid_argument(
63 "Quantum-channel comparison tolerance must be finite and nonnegative");
64 if (numQubits_ != other.numQubits_) return false;
65
66 const Matrix left = ChoiMatrix();
67 const Matrix right = other.ChoiMatrix();
68 if (left.rows() != right.rows() || left.cols() != right.cols())
69 return false;
70 return (left - right).cwiseAbs().maxCoeff() <= tolerance;
71 }
72
79 const Eigen::Index dimension = krausOperators_.front().rows();
80 const Eigen::Index choiDimension = dimension * dimension;
81 Matrix choi = Matrix::Zero(choiDimension, choiDimension);
82 for (const Matrix& krausOperator : krausOperators_) {
83 const Eigen::Map<const Eigen::VectorXcd> vectorized(
84 krausOperator.data(), choiDimension);
85 choi.noalias() += vectorized * vectorized.adjoint();
86 }
87 return choi;
88 }
89
91 static QuantumChannel BitFlip(double probability) {
92 return Pauli(
93 {1.0 - CheckedProbability(probability), probability, 0.0, 0.0});
94 }
95
97 static QuantumChannel BitPhaseFlip(double probability) {
98 return Pauli(
99 {1.0 - CheckedProbability(probability), 0.0, probability, 0.0});
100 }
101
103 static QuantumChannel PhaseFlip(double probability) {
104 return Pauli(
105 {1.0 - CheckedProbability(probability), 0.0, 0.0, probability});
106 }
107
112 static QuantumChannel Pauli(double px, double py, double pz) {
113 CheckedProbability(px);
114 CheckedProbability(py);
115 CheckedProbability(pz);
116 const double total = px + py + pz;
117 if (!std::isfinite(total) || total > 1.0 + kProbabilityTolerance)
118 throw std::invalid_argument(
119 "Pauli error probabilities must sum to at most one");
120 return Pauli({std::max(0.0, 1.0 - total), px, py, pz});
121 }
122
130 static QuantumChannel Pauli(const std::vector<double>& probabilities) {
131 if (probabilities.empty())
132 throw std::invalid_argument("A Pauli channel must contain probabilities");
133
134 size_t count = probabilities.size();
135 size_t numQubits = 0;
136 while (count > 1 && count % 4 == 0) {
137 count /= 4;
138 ++numQubits;
139 }
140 if (count != 1 || numQubits == 0)
141 throw std::invalid_argument(
142 "A Pauli channel needs exactly 4^n probabilities for n >= 1");
143
144 double total = 0.0;
145 for (const double probability : probabilities) {
146 CheckedProbability(probability);
147 total += probability;
148 }
149 if (!std::isfinite(total) || std::abs(total - 1.0) > kProbabilityTolerance)
150 throw std::invalid_argument(
151 "Pauli channel probabilities must sum to one");
152
153 KrausOperators kraus;
154 kraus.reserve(static_cast<size_t>(
155 std::count_if(probabilities.begin(), probabilities.end(),
156 [](double probability) { return probability > 0.0; })));
157 for (size_t index = 0; index < probabilities.size(); ++index) {
158 if (probabilities[index] == 0.0) continue;
159
160 Matrix pauli(1, 1);
161 pauli(0, 0) = 1.0;
162 size_t divisor = 1;
163 for (size_t q = 0; q < numQubits; ++q) divisor *= 4;
164 for (size_t q = numQubits; q-- > 0;) {
165 divisor /= 4;
166 const size_t pauliId = (index / divisor) % 4;
167 pauli = Kronecker(pauli, PauliMatrix(pauliId));
168 }
169 kraus.emplace_back(std::sqrt(probabilities[index]) * pauli);
170 }
171 return QuantumChannel(std::move(kraus));
172 }
173
178 static QuantumChannel Depolarizing(double errorProbability) {
179 CheckedProbability(errorProbability);
180 return Pauli({1.0 - errorProbability, errorProbability / 3.0,
181 errorProbability / 3.0, errorProbability / 3.0});
182 }
183
188 static QuantumChannel DepolarizingMixing(double mixingProbability) {
189 CheckedProbability(mixingProbability);
190 return Depolarizing(0.75 * mixingProbability);
191 }
192
194 static QuantumChannel AmplitudeDamping(double gamma) {
195 CheckedProbability(gamma);
196 Matrix k0 = Matrix::Zero(2, 2);
197 k0(0, 0) = 1.0;
198 k0(1, 1) = std::sqrt(1.0 - gamma);
199 Matrix k1 = Matrix::Zero(2, 2);
200 k1(0, 1) = std::sqrt(gamma);
201 return QuantumChannel({std::move(k0), std::move(k1)});
202 }
203
208 static QuantumChannel PhaseDamping(double gamma) {
209 CheckedProbability(gamma);
210 Matrix k0 = Matrix::Zero(2, 2);
211 k0(0, 0) = 1.0;
212 k0(1, 1) = std::sqrt(1.0 - gamma);
213 Matrix k1 = Matrix::Zero(2, 2);
214 k1(1, 1) = std::sqrt(gamma);
215 return QuantumChannel({std::move(k0), std::move(k1)});
216 }
217
223 double gamma, double excitedStatePopulation) {
224 CheckedProbability(gamma);
225 CheckedProbability(excitedStatePopulation);
226 const double groundStatePopulation = 1.0 - excitedStatePopulation;
227
228 Matrix k0 = Matrix::Zero(2, 2);
229 k0(0, 0) = std::sqrt(groundStatePopulation);
230 k0(1, 1) = std::sqrt(groundStatePopulation * (1.0 - gamma));
231 Matrix k1 = Matrix::Zero(2, 2);
232 k1(0, 1) = std::sqrt(groundStatePopulation * gamma);
233 Matrix k2 = Matrix::Zero(2, 2);
234 k2(0, 0) = std::sqrt(excitedStatePopulation * (1.0 - gamma));
235 k2(1, 1) = std::sqrt(excitedStatePopulation);
236 Matrix k3 = Matrix::Zero(2, 2);
237 k3(1, 0) = std::sqrt(excitedStatePopulation * gamma);
238 return QuantumChannel(
239 {std::move(k0), std::move(k1), std::move(k2), std::move(k3)});
240 }
241
250 static QuantumChannel ThermalRelaxation(double duration, double t1, double t2,
251 double excitedStatePopulation = 0.0) {
252 if (!std::isfinite(duration) || duration < 0.0)
253 throw std::invalid_argument(
254 "Thermal-relaxation duration must be finite and nonnegative");
255 ValidatePositiveTime(t1, "T1");
256 ValidatePositiveTime(t2, "T2");
257 CheckedProbability(excitedStatePopulation);
258
259 if (std::isinf(t2) && !std::isinf(t1))
260 throw std::invalid_argument("Thermal relaxation requires T2 <= 2*T1");
261 if (std::isfinite(t1) && std::isfinite(t2) &&
262 t2 > 2.0 * t1 * (1.0 + kProbabilityTolerance))
263 throw std::invalid_argument("Thermal relaxation requires T2 <= 2*T1");
264
265 const double coherenceMultiplier =
266 std::isinf(t2) ? 1.0 : std::exp(-duration / t2);
267 const double gamma = std::isinf(t1) ? 0.0 : -std::expm1(-duration / t1);
268 const double up = gamma * excitedStatePopulation;
269 const double down = gamma * (1.0 - excitedStatePopulation);
270
271 // The Choi matrix has a 2x2 block
272 // [[1-up, coherence], [coherence, 1-down]]. Decomposing that block gives
273 // at most two diagonal Kraus operators; excitation and decay add one each.
274 const double a = 1.0 - up;
275 const double c = 1.0 - down;
276 if (coherenceMultiplier * coherenceMultiplier >
277 a * c + kProbabilityTolerance)
278 throw std::invalid_argument(
279 "The supplied T1 and T2 do not define a completely-positive map");
280
281 const double discriminant = std::hypot(a - c, 2.0 * coherenceMultiplier);
282 const double lambdaPlus = 0.5 * (a + c + discriminant);
283 const double lambdaMinus = std::max(0.0, 0.5 * (a + c - discriminant));
284 const double angle = 0.5 * std::atan2(2.0 * coherenceMultiplier, a - c);
285 const double cosine = std::cos(angle);
286 const double sine = std::sin(angle);
287
288 KrausOperators kraus;
289 if (lambdaPlus > kZeroTolerance) {
290 Matrix diagonal = Matrix::Zero(2, 2);
291 diagonal(0, 0) = std::sqrt(lambdaPlus) * cosine;
292 diagonal(1, 1) = std::sqrt(lambdaPlus) * sine;
293 kraus.emplace_back(std::move(diagonal));
294 }
295 if (lambdaMinus > kZeroTolerance) {
296 Matrix diagonal = Matrix::Zero(2, 2);
297 diagonal(0, 0) = -std::sqrt(lambdaMinus) * sine;
298 diagonal(1, 1) = std::sqrt(lambdaMinus) * cosine;
299 kraus.emplace_back(std::move(diagonal));
300 }
301 if (up > kZeroTolerance) {
302 Matrix excitation = Matrix::Zero(2, 2);
303 excitation(1, 0) = std::sqrt(up);
304 kraus.emplace_back(std::move(excitation));
305 }
306 if (down > kZeroTolerance) {
307 Matrix decay = Matrix::Zero(2, 2);
308 decay(0, 1) = std::sqrt(down);
309 kraus.emplace_back(std::move(decay));
310 }
311 return QuantumChannel(std::move(kraus));
312 }
313
315 static QuantumChannel CorrelatedPhaseFlip(double probability) {
316 return CorrelatedPhaseFlip(probability, 1.0);
317 }
318
324 static QuantumChannel CorrelatedPhaseFlip(double probability,
325 double correlation) {
326 CheckedProbability(probability);
327 CheckedProbability(correlation);
328 std::vector<double> probabilities(16, 0.0);
329 const double independent = 1.0 - correlation;
330 probabilities[0] =
331 independent * (1.0 - probability) * (1.0 - probability) +
332 correlation * (1.0 - probability);
333 probabilities[3] = independent * probability * (1.0 - probability);
334 probabilities[12] = probabilities[3];
335 probabilities[15] = independent * probability * probability +
336 correlation * probability;
337 return Pauli(probabilities);
338 }
339
344 static QuantumChannel TwoQubitDepolarizing(double errorProbability) {
345 CheckedProbability(errorProbability);
346 std::vector<double> probabilities(16, errorProbability / 15.0);
347 probabilities[0] = 1.0 - errorProbability;
348 return Pauli(probabilities);
349 }
350
353 double mixingProbability) {
354 CheckedProbability(mixingProbability);
355 return TwoQubitDepolarizing((15.0 / 16.0) * mixingProbability);
356 }
357
358 private:
359 static constexpr double kProbabilityTolerance = 1e-12;
360 static constexpr double kZeroTolerance = 1e-15;
361
362 static double CheckedProbability(double probability) {
363 if (!std::isfinite(probability) || probability < 0.0 || probability > 1.0)
364 throw std::invalid_argument(
365 "Quantum-channel probabilities must be finite and in [0, 1]");
366 return probability;
367 }
368
369 static void ValidatePositiveTime(double value, const char* name) {
370 if (std::isnan(value) || value <= 0.0)
371 throw std::invalid_argument(std::string(name) +
372 " must be positive (infinity is allowed)");
373 }
374
375 static Matrix PauliMatrix(size_t pauliId) {
376 Matrix matrix = Matrix::Zero(2, 2);
377 switch (pauliId) {
378 case 0:
379 matrix.setIdentity();
380 break;
381 case 1:
382 matrix(0, 1) = 1.0;
383 matrix(1, 0) = 1.0;
384 break;
385 case 2:
386 matrix(0, 1) = std::complex<double>(0.0, -1.0);
387 matrix(1, 0) = std::complex<double>(0.0, 1.0);
388 break;
389 case 3:
390 matrix(0, 0) = 1.0;
391 matrix(1, 1) = -1.0;
392 break;
393 default:
394 throw std::invalid_argument("Invalid Pauli index");
395 }
396 return matrix;
397 }
398
399 static Matrix Kronecker(const Matrix& left, const Matrix& right) {
400 Matrix result(left.rows() * right.rows(), left.cols() * right.cols());
401 for (Eigen::Index row = 0; row < left.rows(); ++row)
402 for (Eigen::Index column = 0; column < left.cols(); ++column)
403 result.block(row * right.rows(), column * right.cols(), right.rows(),
404 right.cols()) = left(row, column) * right;
405 return result;
406 }
407
408 void Validate() {
409 if (krausOperators_.empty())
410 throw std::invalid_argument(
411 "A quantum channel must contain at least one Kraus operator");
412
413 const Eigen::Index dimension = krausOperators_.front().rows();
414 if (dimension < 2 || krausOperators_.front().cols() != dimension)
415 throw std::invalid_argument(
416 "Kraus operators must be nonempty square qubit matrices");
417
418 size_t remainingDimension = static_cast<size_t>(dimension);
419 while (remainingDimension > 1 && remainingDimension % 2 == 0) {
420 remainingDimension /= 2;
421 ++numQubits_;
422 }
423 if (remainingDimension != 1)
424 throw std::invalid_argument(
425 "Kraus-operator dimensions must be powers of two");
426
427 Matrix completeness = Matrix::Zero(dimension, dimension);
428 for (const Matrix& krausOperator : krausOperators_) {
429 if (krausOperator.rows() != dimension ||
430 krausOperator.cols() != dimension)
431 throw std::invalid_argument(
432 "All Kraus operators must have the same dimensions");
433 if (!krausOperator.allFinite())
434 throw std::invalid_argument(
435 "Kraus operators must contain only finite values");
436 completeness.noalias() += krausOperator.adjoint() * krausOperator;
437 }
438
439 const Matrix identity = Matrix::Identity(dimension, dimension);
440 const double tolerance = 1e-10 * std::max<Eigen::Index>(1, dimension);
441 if ((completeness - identity).norm() > tolerance)
442 throw std::invalid_argument(
443 "Kraus operators do not define a trace-preserving channel");
444 }
445
446 KrausOperators krausOperators_;
447 size_t numQubits_;
448};
449
450} // namespace Simulators
451
452#endif // _SIMULATORS_QUANTUM_CHANNEL_H_
static QuantumChannel Depolarizing(double errorProbability)
Depolarizing channel in the NoiseModel/QCSim convention: (1-p)rho + p/3 (XrhoX + YrhoY + ZrhoZ).
static QuantumChannel PhaseDamping(double gamma)
Phase damping with coherence multiplier sqrt(1-gamma).
static QuantumChannel Pauli(double px, double py, double pz)
Single-qubit Pauli channel with X, Y and Z error probabilities.
const KrausOperators & GetKrausOperators() const
static QuantumChannel AmplitudeDamping(double gamma)
|1> -> |0> relaxation with probability gamma.
Matrix ChoiMatrix() const
Choi matrix of the channel: sum_k vec(E_k) vec(E_k)^dagger, with vec() stacking the operator column b...
static QuantumChannel GeneralizedAmplitudeDamping(double gamma, double excitedStatePopulation)
Finite-temperature amplitude damping.
static QuantumChannel Pauli(const std::vector< double > &probabilities)
Arbitrary local Pauli channel.
static QuantumChannel DepolarizingMixing(double mixingProbability)
Depolarizing channel in the replacement convention: (1-p)rho + p I/2.
static QuantumChannel CorrelatedPhaseFlip(double probability)
(1-p)rho + p (Z (x) Z) rho (Z (x) Z).
static QuantumChannel BitPhaseFlip(double probability)
(1-p) rho + p Y rho Y.
size_t GetNumberOfQubits() const
QuantumChannel(KrausOperators krausOperators)
Construct and validate a CPTP channel from Kraus operators.
static QuantumChannel ThermalRelaxation(double duration, double t1, double t2, double excitedStatePopulation=0.0)
Hardware-style thermal relaxation for a duration, T1 and T2.
static QuantumChannel CorrelatedPhaseFlip(double probability, double correlation)
Interpolate between independent phase flips and a fully correlated ZZ phase flip.
static QuantumChannel TwoQubitDepolarizingMixing(double mixingProbability)
Two-qubit replacement depolarizing, fully mixed at probability one.
static QuantumChannel PhaseFlip(double probability)
(1-p) rho + p Z rho Z.
static QuantumChannel TwoQubitDepolarizing(double errorProbability)
Two-qubit depolarizing in the total-Pauli-error convention: identity has probability 1-p and each oth...
std::vector< Matrix > KrausOperators
bool IsApprox(const QuantumChannel &other, double tolerance=1e-12) const
Compare two channels as MAPS, not as Kraus lists.
static QuantumChannel BitFlip(double probability)
(1-p) rho + p X rho X.