Maestro 0.3.11
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{
26
35{
36 public:
37 using Matrix = Eigen::MatrixXcd;
38 using KrausOperators = std::vector<Matrix>;
39
41 explicit QuantumChannel(KrausOperators krausOperators) : krausOperators_(std::move(krausOperators)), numQubits_(0)
42 {
43 Validate();
44 }
45
47 {
48 return krausOperators_;
49 }
50
51 size_t GetNumberOfQubits() const
52 {
53 return numQubits_;
54 }
55
61 const std::vector<double> &GetPauliProbabilities() const
62 {
63 return pauliProbabilities_;
64 }
65
79 bool IsApprox(const QuantumChannel &other, double tolerance = 1e-12) const
80 {
81 if (tolerance < 0.0 || !std::isfinite(tolerance))
82 throw std::invalid_argument("Quantum-channel comparison tolerance must be finite and nonnegative");
83 if (numQubits_ != other.numQubits_)
84 return false;
85
86 const Matrix left = ChoiMatrix();
87 const Matrix right = other.ChoiMatrix();
88 if (left.rows() != right.rows() || left.cols() != right.cols())
89 return false;
90 return (left - right).cwiseAbs().maxCoeff() <= tolerance;
91 }
92
99 {
100 const Eigen::Index dimension = krausOperators_.front().rows();
101 const Eigen::Index choiDimension = dimension * dimension;
102 Matrix choi = Matrix::Zero(choiDimension, choiDimension);
103 for (const Matrix &krausOperator : krausOperators_)
104 {
105 const Eigen::Map<const Eigen::VectorXcd> vectorized(krausOperator.data(), choiDimension);
106 choi.noalias() += vectorized * vectorized.adjoint();
107 }
108 return choi;
109 }
110
112 static QuantumChannel BitFlip(double probability)
113 {
114 return Pauli({1.0 - CheckedProbability(probability), probability, 0.0, 0.0});
115 }
116
118 static QuantumChannel BitPhaseFlip(double probability)
119 {
120 return Pauli({1.0 - CheckedProbability(probability), 0.0, probability, 0.0});
121 }
122
124 static QuantumChannel PhaseFlip(double probability)
125 {
126 return Pauli({1.0 - CheckedProbability(probability), 0.0, 0.0, probability});
127 }
128
133 static QuantumChannel Pauli(double px, double py, double pz)
134 {
135 CheckedProbability(px);
136 CheckedProbability(py);
137 CheckedProbability(pz);
138 const double total = px + py + pz;
139 if (!std::isfinite(total) || total > 1.0 + kProbabilityTolerance)
140 throw std::invalid_argument("Pauli error probabilities must sum to at most one");
141 return Pauli({std::max(0.0, 1.0 - total), px, py, pz});
142 }
143
151 static QuantumChannel Pauli(const std::vector<double> &probabilities)
152 {
153 if (probabilities.empty())
154 throw std::invalid_argument("A Pauli channel must contain probabilities");
155
156 size_t count = probabilities.size();
157 size_t numQubits = 0;
158 while (count > 1 && count % 4 == 0)
159 {
160 count /= 4;
161 ++numQubits;
162 }
163 if (count != 1 || numQubits == 0)
164 throw std::invalid_argument("A Pauli channel needs exactly 4^n probabilities for n >= 1");
165
166 double total = 0.0;
167 for (const double probability : probabilities)
168 {
169 CheckedProbability(probability);
170 total += probability;
171 }
172 if (!std::isfinite(total) || std::abs(total - 1.0) > kProbabilityTolerance)
173 throw std::invalid_argument("Pauli channel probabilities must sum to one");
174
175 KrausOperators kraus;
176 kraus.reserve(static_cast<size_t>(std::count_if(probabilities.begin(), probabilities.end(), [](double probability) { return probability > 0.0; })));
177 for (size_t index = 0; index < probabilities.size(); ++index)
178 {
179 if (probabilities[index] == 0.0)
180 continue;
181
182 Matrix pauli(1, 1);
183 pauli(0, 0) = 1.0;
184 size_t divisor = 1;
185 for (size_t q = 0; q < numQubits; ++q)
186 divisor *= 4;
187 for (size_t q = numQubits; q-- > 0;)
188 {
189 divisor /= 4;
190 const size_t pauliId = (index / divisor) % 4;
191 pauli = Kronecker(pauli, PauliMatrix(pauliId));
192 }
193 kraus.emplace_back(std::sqrt(probabilities[index]) * pauli);
194 }
195 QuantumChannel channel(std::move(kraus));
196 channel.pauliProbabilities_ = probabilities;
197 return channel;
198 }
199
204 static QuantumChannel Depolarizing(double errorProbability)
205 {
206 CheckedProbability(errorProbability);
207 return Pauli({1.0 - errorProbability, errorProbability / 3.0, errorProbability / 3.0, errorProbability / 3.0});
208 }
209
214 static QuantumChannel DepolarizingMixing(double mixingProbability)
215 {
216 CheckedProbability(mixingProbability);
217 return Depolarizing(0.75 * mixingProbability);
218 }
219
221 static QuantumChannel AmplitudeDamping(double gamma)
222 {
223 CheckedProbability(gamma);
224 Matrix k0 = Matrix::Zero(2, 2);
225 k0(0, 0) = 1.0;
226 k0(1, 1) = std::sqrt(1.0 - gamma);
227 Matrix k1 = Matrix::Zero(2, 2);
228 k1(0, 1) = std::sqrt(gamma);
229 return QuantumChannel({std::move(k0), std::move(k1)});
230 }
231
236 static QuantumChannel PhaseDamping(double gamma)
237 {
238 CheckedProbability(gamma);
239 Matrix k0 = Matrix::Zero(2, 2);
240 k0(0, 0) = 1.0;
241 k0(1, 1) = std::sqrt(1.0 - gamma);
242 Matrix k1 = Matrix::Zero(2, 2);
243 k1(1, 1) = std::sqrt(gamma);
244 return QuantumChannel({std::move(k0), std::move(k1)});
245 }
246
251 static QuantumChannel GeneralizedAmplitudeDamping(double gamma, double excitedStatePopulation)
252 {
253 CheckedProbability(gamma);
254 CheckedProbability(excitedStatePopulation);
255 const double groundStatePopulation = 1.0 - excitedStatePopulation;
256
257 Matrix k0 = Matrix::Zero(2, 2);
258 k0(0, 0) = std::sqrt(groundStatePopulation);
259 k0(1, 1) = std::sqrt(groundStatePopulation * (1.0 - gamma));
260 Matrix k1 = Matrix::Zero(2, 2);
261 k1(0, 1) = std::sqrt(groundStatePopulation * gamma);
262 Matrix k2 = Matrix::Zero(2, 2);
263 k2(0, 0) = std::sqrt(excitedStatePopulation * (1.0 - gamma));
264 k2(1, 1) = std::sqrt(excitedStatePopulation);
265 Matrix k3 = Matrix::Zero(2, 2);
266 k3(1, 0) = std::sqrt(excitedStatePopulation * gamma);
267 return QuantumChannel({std::move(k0), std::move(k1), std::move(k2), std::move(k3)});
268 }
269
278 static QuantumChannel ThermalRelaxation(double duration, double t1, double t2, double excitedStatePopulation = 0.0)
279 {
280 if (!std::isfinite(duration) || duration < 0.0)
281 throw std::invalid_argument("Thermal-relaxation duration must be finite and nonnegative");
282 ValidatePositiveTime(t1, "T1");
283 ValidatePositiveTime(t2, "T2");
284 CheckedProbability(excitedStatePopulation);
285
286 if (std::isinf(t2) && !std::isinf(t1))
287 throw std::invalid_argument("Thermal relaxation requires T2 <= 2*T1");
288 if (std::isfinite(t1) && std::isfinite(t2) && t2 > 2.0 * t1 * (1.0 + kProbabilityTolerance))
289 throw std::invalid_argument("Thermal relaxation requires T2 <= 2*T1");
290
291 const double coherenceMultiplier = std::isinf(t2) ? 1.0 : std::exp(-duration / t2);
292 const double gamma = std::isinf(t1) ? 0.0 : -std::expm1(-duration / t1);
293 const double up = gamma * excitedStatePopulation;
294 const double down = gamma * (1.0 - excitedStatePopulation);
295
296 // The Choi matrix has a 2x2 block
297 // [[1-up, coherence], [coherence, 1-down]]. Decomposing that block gives
298 // at most two diagonal Kraus operators; excitation and decay add one each.
299 const double a = 1.0 - up;
300 const double c = 1.0 - down;
301 if (coherenceMultiplier * coherenceMultiplier > a * c + kProbabilityTolerance)
302 throw std::invalid_argument("The supplied T1 and T2 do not define a completely-positive map");
303
304 const double discriminant = std::hypot(a - c, 2.0 * coherenceMultiplier);
305 const double lambdaPlus = 0.5 * (a + c + discriminant);
306 const double lambdaMinus = std::max(0.0, 0.5 * (a + c - discriminant));
307 const double angle = 0.5 * std::atan2(2.0 * coherenceMultiplier, a - c);
308 const double cosine = std::cos(angle);
309 const double sine = std::sin(angle);
310
311 KrausOperators kraus;
312 if (lambdaPlus > kZeroTolerance)
313 {
314 Matrix diagonal = Matrix::Zero(2, 2);
315 diagonal(0, 0) = std::sqrt(lambdaPlus) * cosine;
316 diagonal(1, 1) = std::sqrt(lambdaPlus) * sine;
317 kraus.emplace_back(std::move(diagonal));
318 }
319 if (lambdaMinus > kZeroTolerance)
320 {
321 Matrix diagonal = Matrix::Zero(2, 2);
322 diagonal(0, 0) = -std::sqrt(lambdaMinus) * sine;
323 diagonal(1, 1) = std::sqrt(lambdaMinus) * cosine;
324 kraus.emplace_back(std::move(diagonal));
325 }
326 if (up > kZeroTolerance)
327 {
328 Matrix excitation = Matrix::Zero(2, 2);
329 excitation(1, 0) = std::sqrt(up);
330 kraus.emplace_back(std::move(excitation));
331 }
332 if (down > kZeroTolerance)
333 {
334 Matrix decay = Matrix::Zero(2, 2);
335 decay(0, 1) = std::sqrt(down);
336 kraus.emplace_back(std::move(decay));
337 }
338 return QuantumChannel(std::move(kraus));
339 }
340
342 static QuantumChannel CorrelatedPhaseFlip(double probability)
343 {
344 return CorrelatedPhaseFlip(probability, 1.0);
345 }
346
352 static QuantumChannel CorrelatedPhaseFlip(double probability, double correlation)
353 {
354 CheckedProbability(probability);
355 CheckedProbability(correlation);
356 std::vector<double> probabilities(16, 0.0);
357 const double independent = 1.0 - correlation;
358 probabilities[0] = independent * (1.0 - probability) * (1.0 - probability) + correlation * (1.0 - probability);
359 probabilities[3] = independent * probability * (1.0 - probability);
360 probabilities[12] = probabilities[3];
361 probabilities[15] = independent * probability * probability + correlation * probability;
362 return Pauli(probabilities);
363 }
364
369 static QuantumChannel TwoQubitDepolarizing(double errorProbability)
370 {
371 CheckedProbability(errorProbability);
372 std::vector<double> probabilities(16, errorProbability / 15.0);
373 probabilities[0] = 1.0 - errorProbability;
374 return Pauli(probabilities);
375 }
376
378 static QuantumChannel TwoQubitDepolarizingMixing(double mixingProbability)
379 {
380 CheckedProbability(mixingProbability);
381 return TwoQubitDepolarizing((15.0 / 16.0) * mixingProbability);
382 }
383
384 private:
385 static constexpr double kProbabilityTolerance = 1e-12;
386 static constexpr double kZeroTolerance = 1e-15;
387
388 static double CheckedProbability(double probability)
389 {
390 if (!std::isfinite(probability) || probability < 0.0 || probability > 1.0)
391 throw std::invalid_argument("Quantum-channel probabilities must be finite and in [0, 1]");
392 return probability;
393 }
394
395 static void ValidatePositiveTime(double value, const char *name)
396 {
397 if (std::isnan(value) || value <= 0.0)
398 throw std::invalid_argument(std::string(name) + " must be positive (infinity is allowed)");
399 }
400
401 static Matrix PauliMatrix(size_t pauliId)
402 {
403 Matrix matrix = Matrix::Zero(2, 2);
404 switch (pauliId)
405 {
406 case 0:
407 matrix.setIdentity();
408 break;
409 case 1:
410 matrix(0, 1) = 1.0;
411 matrix(1, 0) = 1.0;
412 break;
413 case 2:
414 matrix(0, 1) = std::complex<double>(0.0, -1.0);
415 matrix(1, 0) = std::complex<double>(0.0, 1.0);
416 break;
417 case 3:
418 matrix(0, 0) = 1.0;
419 matrix(1, 1) = -1.0;
420 break;
421 default:
422 throw std::invalid_argument("Invalid Pauli index");
423 }
424 return matrix;
425 }
426
427 static Matrix Kronecker(const Matrix &left, const Matrix &right)
428 {
429 Matrix result(left.rows() * right.rows(), left.cols() * right.cols());
430 for (Eigen::Index row = 0; row < left.rows(); ++row)
431 for (Eigen::Index column = 0; column < left.cols(); ++column)
432 result.block(row * right.rows(), column * right.cols(), right.rows(), right.cols()) = left(row, column) * right;
433 return result;
434 }
435
436 void Validate()
437 {
438 if (krausOperators_.empty())
439 throw std::invalid_argument("A quantum channel must contain at least one Kraus operator");
440
441 const Eigen::Index dimension = krausOperators_.front().rows();
442 if (dimension < 2 || krausOperators_.front().cols() != dimension)
443 throw std::invalid_argument("Kraus operators must be nonempty square qubit matrices");
444
445 size_t remainingDimension = static_cast<size_t>(dimension);
446 while (remainingDimension > 1 && remainingDimension % 2 == 0)
447 {
448 remainingDimension /= 2;
449 ++numQubits_;
450 }
451 if (remainingDimension != 1)
452 throw std::invalid_argument("Kraus-operator dimensions must be powers of two");
453
454 Matrix completeness = Matrix::Zero(dimension, dimension);
455 for (const Matrix &krausOperator : krausOperators_)
456 {
457 if (krausOperator.rows() != dimension || krausOperator.cols() != dimension)
458 throw std::invalid_argument("All Kraus operators must have the same dimensions");
459 if (!krausOperator.allFinite())
460 throw std::invalid_argument("Kraus operators must contain only finite values");
461 completeness.noalias() += krausOperator.adjoint() * krausOperator;
462 }
463
464 const Matrix identity = Matrix::Identity(dimension, dimension);
465 const double tolerance = 1e-10 * std::max<Eigen::Index>(1, dimension);
466 if ((completeness - identity).norm() > tolerance)
467 throw std::invalid_argument("Kraus operators do not define a trace-preserving channel");
468 }
469
470 KrausOperators krausOperators_;
471 size_t numQubits_;
472 std::vector<double> pauliProbabilities_;
473};
474
475} // namespace Simulators
476
477#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.
const std::vector< double > & GetPauliProbabilities() const
Probabilities of a channel built by Pauli(), in Pauli()'s index order; empty for any other channel.
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.
Bounded online routing based on current physical bond dimensions.