Q2NS dev
ns-3 module
Loading...
Searching...
No Matches
q2ns-analysis.cc
Go to the documentation of this file.
1/*-----------------------------------------------------------------------------
2 * Q2NS - Quantum Network Simulator
3 * Copyright (c) 2026 quantuminternet.it
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License version 2 as
7 * published by the Free Software Foundation.
8 *---------------------------------------------------------------------------*/
9/**
10 * @file q2ns-analysis.cc
11 * @brief Defines q2ns::analysis.
12 */
13
14#include "ns3/q2ns-analysis.h"
15
16#include "ns3/q2ns-qstate-all.h"
17#include "ns3/q2ns-types.h"
18
19#include <Eigen/Dense>
20
21#include <cmath>
22#include <complex>
23#include <stdexcept>
24
25namespace q2ns::analysis {
26
27namespace {
28
29
30/**
31 * @brief Convert a supported QState backend to a density matrix.
32 *
33 * This helper provides a common density-matrix representation for analysis
34 * routines that are naturally expressed in terms of rho, such as purity,
35 * entropy, and trace distance.
36 *
37 * For ket and stabilizer backends, this may require constructing a dense
38 * matrix representation and can therefore be substantially more expensive than
39 * backend-native calculations.
40 *
41 * @param s Input state.
42 * @return Dense density-matrix representation of s.
43 *
44 * @throws std::runtime_error if the backend is unsupported or if conversion of
45 * a stabilizer state requires unavailable backend-specific functionality.
46 */
47qpp::cmat ToDensityMatrix(const QState& s) {
48 if (const auto* k = dynamic_cast<const QStateKet*>(&s)) {
49 return k->GetDensityMatrix();
50 }
51
52 if (const auto* d = dynamic_cast<const QStateDM*>(&s)) {
53 return d->GetRho();
54 }
55
56 if (const auto* st = dynamic_cast<const QStateStab*>(&s)) {
57#ifdef USE_QPP
58 return qpp::prj(st->GetAffineState().to_ket());
59#else
60 throw std::runtime_error("ToDensityMatrix(QStateStab): unavailable without USE_QPP support");
61#endif
62 }
63
64 throw std::runtime_error("ToDensityMatrix: unsupported QState backend");
65}
66
67
68
69/**
70 * @brief Clamp a scalar metric to a closed interval within numerical tolerance.
71 *
72 * Values slightly outside the target interval due to floating-point roundoff are
73 * clamped back to the nearest endpoint. Values outside the tolerance window are
74 * returned unchanged.
75 *
76 * @param x Input value.
77 * @param lo Lower bound.
78 * @param hi Upper bound.
79 * @param tol Numerical tolerance for endpoint clamping.
80 * @return Clamped value when within tolerance of the interval, otherwise x.
81 */
82double ClampToInterval(double x, double lo, double hi, double tol = 1e-10) {
83 if (x < lo && x > lo - tol) {
84 return lo;
85 }
86 if (x > hi && x < hi + tol) {
87 return hi;
88 }
89 return x;
90}
91
92
93
94/**
95 * @brief Compute the Hermitian square root of a positive semidefinite matrix.
96 *
97 * Small negative eigenvalues caused by numerical roundoff are clamped to zero.
98 *
99 * @param A Input matrix.
100 * @param negEigTol Tolerance for small negative eigenvalues.
101 * @return Hermitian square root of A.
102 */
103Eigen::MatrixXcd HermitianSqrtPSD(const Eigen::MatrixXcd& A, double negEigTol = 1e-12) {
104 if (A.rows() != A.cols()) {
105 throw std::runtime_error("HermitianSqrtPSD: matrix must be square");
106 }
107
108 // Symmetrize first to suppress tiny anti-Hermitian numerical noise.
109 Eigen::MatrixXcd H = 0.5 * (A + A.adjoint());
110
111 Eigen::SelfAdjointEigenSolver<Eigen::MatrixXcd> es(H);
112 if (es.info() != Eigen::Success) {
113 throw std::runtime_error("HermitianSqrtPSD: eigensolver failed");
114 }
115
116 Eigen::VectorXd evals = es.eigenvalues();
117 Eigen::MatrixXcd evecs = es.eigenvectors();
118
119 for (int i = 0; i < evals.size(); ++i) {
120 if (evals[i] < 0.0) {
121 if (evals[i] > -negEigTol) {
122 evals[i] = 0.0;
123 } else {
124 throw std::runtime_error("HermitianSqrtPSD: matrix is not positive semidefinite");
125 }
126 }
127 }
128
129 Eigen::VectorXd sqrtEvals = evals.array().sqrt();
130 return evecs * sqrtEvals.asDiagonal() * evecs.adjoint();
131}
132
133
134
135/**
136 * @brief Compute Uhlmann fidelity between two density matrices.
137 *
138 * The returned value is F(rho, sigma) = (Tr sqrt(sqrt(rho) sigma sqrt(rho)))^2.
139 *
140 * @param rho First density matrix.
141 * @param sigma Second density matrix.
142 * @param negEigTol Tolerance for small negative eigenvalues in intermediate PSD checks.
143 * @return Fidelity in the range [0.0, 1.0], up to numerical tolerance.
144 */
145double UhlmannFidelity(const Eigen::MatrixXcd& rho, const Eigen::MatrixXcd& sigma,
146 double negEigTol = 1e-12) {
147 if (rho.rows() != rho.cols() || sigma.rows() != sigma.cols() || rho.rows() != sigma.rows()) {
148 throw std::runtime_error("UhlmannFidelity: dimension mismatch");
149 }
150
151 Eigen::MatrixXcd sqrtRho = HermitianSqrtPSD(rho, negEigTol);
152 Eigen::MatrixXcd X = sqrtRho * sigma * sqrtRho;
153
154 Eigen::MatrixXcd sqrtX = HermitianSqrtPSD(X, negEigTol);
155 std::complex<double> tr = sqrtX.trace();
156
157 double f = std::real(tr);
158 f = f * f;
159
160 // Clamp tiny numerical drift outside [0, 1].
161 if (f < 0.0 && f > -1e-10) {
162 f = 0.0;
163 }
164 if (f > 1.0 && f < 1.0 + 1e-10) {
165 f = 1.0;
166 }
167
168 return f;
169}
170
171
172
173/**
174 * @brief Compute the trace norm of a Hermitian matrix.
175 *
176 * For a Hermitian matrix H, the trace norm is the sum of the absolute values of
177 * its eigenvalues.
178 *
179 * The input is symmetrized first to suppress tiny anti-Hermitian numerical
180 * noise before diagonalization.
181 *
182 * @param A Input matrix.
183 * @return Trace norm of A.
184 *
185 * @throws std::runtime_error if the matrix is not square or if eigenvalue
186 * decomposition fails.
187 */
188double HermitianTraceNorm(const Eigen::MatrixXcd& A) {
189 if (A.rows() != A.cols()) {
190 throw std::runtime_error("HermitianTraceNorm: matrix must be square");
191 }
192
193 Eigen::MatrixXcd H = 0.5 * (A + A.adjoint());
194
195 Eigen::SelfAdjointEigenSolver<Eigen::MatrixXcd> es(H);
196 if (es.info() != Eigen::Success) {
197 throw std::runtime_error("HermitianTraceNorm: eigensolver failed");
198 }
199
200 return es.eigenvalues().cwiseAbs().sum();
201}
202
203} // namespace
204
205
206
207double Fidelity(const QState& a, const QState& b) {
208 if (a.NumQubits() != b.NumQubits()) {
209 throw std::runtime_error("Fidelity: states must have the same number of qubits");
210 }
211
212 const std::size_t n = a.NumQubits();
213 if (n == 0) {
214 throw std::invalid_argument("Fidelity: empty states are not supported");
215 }
216
217 // Ket-backend fidelity is the squared overlap |<psi|phi>|^2.
218 if (auto ak = dynamic_cast<const QStateKet*>(&a)) {
219 if (auto bk = dynamic_cast<const QStateKet*>(&b)) {
220 const auto& psi = ak->GetKet();
221 const auto& phi = bk->GetKet();
222
223 const auto amp = (psi.adjoint() * phi)(0, 0);
224 double f = std::norm(amp);
225 return ClampToInterval(f, 0.0, 1.0);
226 }
227 }
228
229 // Density-matrix fidelity uses the standard Uhlmann expression.
230 if (auto ad = dynamic_cast<const QStateDM*>(&a)) {
231 if (auto bd = dynamic_cast<const QStateDM*>(&b)) {
232 const auto& rho = ad->GetRho();
233 const auto& sigma = bd->GetRho();
234 return UhlmannFidelity(rho, sigma);
235 }
236 }
237
238 // Stabilizer fidelity is currently evaluated by converting to ket form when
239 // that backend functionality is available.
240 if (auto as = dynamic_cast<const QStateStab*>(&a)) {
241 if (auto bs = dynamic_cast<const QStateStab*>(&b)) {
242#ifdef USE_QPP
243 const auto& psiS = as->GetAffineState();
244 const auto& phiS = bs->GetAffineState();
245
246 const auto psi = psiS.to_ket();
247 const auto phi = phiS.to_ket();
248
249 const auto amp = (psi.adjoint() * phi)(0, 0);
250 double f = std::norm(amp);
251 return ClampToInterval(f, 0.0, 1.0);
252#else
253 throw std::runtime_error("Fidelity(Stab, Stab) is unavailable without USE_QPP support");
254#endif
255 }
256 }
257
258 throw std::runtime_error("Fidelity: mixed backends are not supported. "
259 "Supported pairs are Ket-Ket, DM-DM, and Stab-Stab.");
260}
261
262
263
264double Purity(const QState& s) {
265 const std::size_t n = s.NumQubits();
266 if (n == 0) {
267 throw std::invalid_argument("Purity: empty states are not supported");
268 }
269
270 const auto rho = ToDensityMatrix(s);
271 const std::complex<double> tr = qpp::trace(rho * rho);
272
273 double p = std::real(tr);
274 p = ClampToInterval(p, 0.0, 1.0);
275 return p;
276}
277
278
279
280bool IsPure(const QState& s, double tol) {
281 if (tol < 0.0) {
282 throw std::invalid_argument("IsPure: tolerance must be non-negative");
283 }
284
285 return std::fabs(Purity(s) - 1.0) <= tol;
286}
287
288
289
290double VonNeumannEntropy(const QState& s) {
291 constexpr double negEigTol = 1e-12;
292 constexpr double zeroTol = 1e-15;
293
294 const std::size_t n = s.NumQubits();
295 if (n == 0) {
296 throw std::invalid_argument("VonNeumannEntropy: empty states are not supported");
297 }
298
299 const auto rho = ToDensityMatrix(s);
300
301 // Symmetrize first to suppress tiny anti-Hermitian numerical noise.
302 Eigen::MatrixXcd H = 0.5 * (rho + rho.adjoint());
303
304 Eigen::SelfAdjointEigenSolver<Eigen::MatrixXcd> es(H);
305 if (es.info() != Eigen::Success) {
306 throw std::runtime_error("VonNeumannEntropy: eigensolver failed");
307 }
308
309 const auto evals = es.eigenvalues();
310
311 double entropy = 0.0;
312 for (int i = 0; i < evals.size(); ++i) {
313 double lambda = evals[i];
314
315 if (lambda < 0.0) {
316 if (lambda > -negEigTol) {
317 lambda = 0.0;
318 } else {
319 throw std::runtime_error("VonNeumannEntropy: density matrix is not positive semidefinite");
320 }
321 }
322
323 if (lambda > zeroTol) {
324 entropy -= lambda * std::log2(lambda);
325 }
326 }
327
328 if (entropy < 0.0 && entropy > -1e-10) {
329 entropy = 0.0;
330 }
331
332 return entropy;
333}
334
335
336
337double TraceDistance(const QState& a, const QState& b) {
338 if (a.NumQubits() != b.NumQubits()) {
339 throw std::runtime_error("TraceDistance: states must have the same number of qubits");
340 }
341
342 const std::size_t n = a.NumQubits();
343 if (n == 0) {
344 throw std::invalid_argument("TraceDistance: empty states are not supported");
345 }
346
347 // Restrict to same-backend comparisons only.
348 const bool bothKet = dynamic_cast<const QStateKet*>(&a) && dynamic_cast<const QStateKet*>(&b);
349 const bool bothDm = dynamic_cast<const QStateDM*>(&a) && dynamic_cast<const QStateDM*>(&b);
350 const bool bothStab = dynamic_cast<const QStateStab*>(&a) && dynamic_cast<const QStateStab*>(&b);
351
352 if (!(bothKet || bothDm || bothStab)) {
353 throw std::runtime_error("TraceDistance: mixed backends are not supported. "
354 "Supported pairs are Ket-Ket, DM-DM, and Stab-Stab.");
355 }
356
357 const auto rho = ToDensityMatrix(a);
358 const auto sigma = ToDensityMatrix(b);
359
360 double d = 0.5 * HermitianTraceNorm(rho - sigma);
361 d = ClampToInterval(d, 0.0, 1.0);
362 return d;
363}
364
365} // namespace q2ns::analysis
Density-matrix concrete QState backend using qpp::cmat.
Ket-based concrete QState backend using qpp.
Stabilizer concrete QState backend using stab::AffineState.
Backend-agnostic interface for a quantum state object.
Definition q2ns-qstate.h:51
virtual std::size_t NumQubits() const =0
Return the number of logical qubits in the state.
double VonNeumannEntropy(const QState &s)
Compute the von Neumann entropy of a quantum state in bits.
double Purity(const QState &s)
Compute the purity of a quantum state.
double Fidelity(const QState &a, const QState &b)
Compute fidelity between two QState objects of the same backend type.
double TraceDistance(const QState &a, const QState &b)
Compute the trace distance between two QState objects of the same backend type.
bool IsPure(const QState &s, double tol)
Check whether a quantum state is pure within a numerical tolerance.
double UhlmannFidelity(const Eigen::MatrixXcd &rho, const Eigen::MatrixXcd &sigma, double negEigTol=1e-12)
Compute Uhlmann fidelity between two density matrices.
double HermitianTraceNorm(const Eigen::MatrixXcd &A)
Compute the trace norm of a Hermitian matrix.
Eigen::MatrixXcd HermitianSqrtPSD(const Eigen::MatrixXcd &A, double negEigTol=1e-12)
Compute the Hermitian square root of a positive semidefinite matrix.
double ClampToInterval(double x, double lo, double hi, double tol=1e-10)
Clamp a scalar metric to a closed interval within numerical tolerance.
qpp::cmat ToDensityMatrix(const QState &s)
Convert a supported QState backend to a density matrix.