Sequential Quantum Gate Decomposer  v1.9.6
Powerful decomposition of general unitarias into one- and two-qubit gates gates
density_matrix.cpp
Go to the documentation of this file.
1 /*
2 Copyright 2025 SQUANDER Contributors
3 
4 Licensed under the Apache License, Version 2.0 (the "License");
5 you may not use this file except in compliance with the License.
6 You may obtain a copy of the License at
7 
8  http://www.apache.org/licenses/LICENSE-2.0
9 
10 Unless required by applicable law or agreed to in writing, software
11 distributed under the License is distributed on an "AS IS" BASIS,
12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 See the License for the specific language governing permissions and
14 limitations under the License.
15 */
16 
17 #include "density_matrix.h"
18 #include "dot.h" // BLAS wrapper from existing SQUANDER
19 #include <algorithm>
20 #include <cmath>
21 #include <cstring>
22 #include <iostream>
23 
24 extern "C" {
25 // LAPACK eigenvalue solver for Hermitian matrices
26 void zheev_(char *jobz, char *uplo, int *n, QGD_Complex16 *a, int *lda,
27  double *w, QGD_Complex16 *work, int *lwork, double *rwork,
28  int *info);
29 }
30 
31 namespace squander {
32 namespace density {
33 
34 // ================================================================
35 // Helper Functions
36 // ================================================================
37 
38 static int calculate_qbit_num(int dim) {
39  int qbit_num = 0;
40  int temp = dim;
41  while (temp > 1) {
42  if (temp % 2 != 0) {
43  throw std::invalid_argument(
44  "DensityMatrix: dimension must be power of 2");
45  }
46  temp /= 2;
47  qbit_num++;
48  }
49  return qbit_num;
50 }
51 
52 // ================================================================
53 // Constructors
54 // ================================================================
55 
57  : matrix_base<QGD_Complex16>(1 << qbit_num, 1 << qbit_num),
58  qbit_num_(qbit_num) {
59  if (qbit_num < 1) {
60  throw std::invalid_argument("DensityMatrix: qbit_num must be >= 1");
61  }
62 
63  int dim = 1 << qbit_num; // 2^qbit_num
64 
65  // Matrix already allocated by base class constructor
66  // references and reference_mutex already created by base class
67  // Just initialize to |0⟩⟨0| (ground state) - only ρ(0,0) = 1
68  memset(data, 0, dim * dim * sizeof(QGD_Complex16));
69  data[0].real = 1.0;
70  data[0].imag = 0.0;
71 }
72 
74  : DensityMatrix(calculate_qbit_num(state_vector.rows)) {
75  // Validate input
76  if (state_vector.cols != 1) {
77  throw std::invalid_argument(
78  "DensityMatrix: state_vector must be column vector (cols=1)");
79  }
80 
81  // Compute outer product: ρ(i,j) = ψ(i) * conj(ψ(j))
82  int dim = state_vector.rows;
83  for (int i = 0; i < dim; i++) {
84  for (int j = 0; j < dim; j++) {
85  QGD_Complex16 psi_i = state_vector.data[i * state_vector.stride];
86  QGD_Complex16 psi_j = state_vector.data[j * state_vector.stride];
87 
88  // Complex multiplication: ψ(i) * conj(ψ(j))
89  data[i * stride + j].real =
90  psi_i.real * psi_j.real + psi_i.imag * psi_j.imag;
91  data[i * stride + j].imag =
92  psi_i.imag * psi_j.real - psi_i.real * psi_j.imag;
93  }
94  }
95 }
96 
98  : matrix_base<QGD_Complex16>(data_in, dim, dim) {
99  // Calculate qbit_num
100  qbit_num_ = 0;
101  int temp = dim;
102  while (temp > 1) {
103  if (temp % 2 != 0) {
104  throw std::invalid_argument(
105  "DensityMatrix: dimension must be power of 2");
106  }
107  temp /= 2;
108  qbit_num_++;
109  }
110 }
111 
113  : matrix_base<QGD_Complex16>(other), qbit_num_(other.qbit_num_) {}
114 
116  : matrix_base<QGD_Complex16>(std::move(other)), qbit_num_(other.qbit_num_) {
117  other.qbit_num_ = 0;
118 }
119 
120 // ================================================================
121 // Assignment Operators
122 // ================================================================
123 
125  if (this != &other) {
127  qbit_num_ = other.qbit_num_;
128  }
129  return *this;
130 }
131 
133  if (this != &other) {
134  matrix_base<QGD_Complex16>::operator=(std::move(other));
135  qbit_num_ = other.qbit_num_;
136  other.qbit_num_ = 0;
137  }
138  return *this;
139 }
140 
142  if (i < 0 || i >= rows || j < 0 || j >= cols) {
143  throw std::out_of_range("DensityMatrix: index out of bounds");
144  }
145  return data[i * stride + j];
146 }
147 
148 const QGD_Complex16 &DensityMatrix::operator()(int i, int j) const {
149  if (i < 0 || i >= rows || j < 0 || j >= cols) {
150  throw std::out_of_range("DensityMatrix: index out of bounds");
151  }
152  return data[i * stride + j];
153 }
154 
155 // ================================================================
156 // Quantum Properties
157 // ================================================================
158 
160  QGD_Complex16 tr;
161  tr.real = 0.0;
162  tr.imag = 0.0;
163 
164  // Sum diagonal elements
165  for (int i = 0; i < rows; i++) {
166  tr.real += data[i * stride + i].real;
167  tr.imag += data[i * stride + i].imag;
168  }
169 
170  return tr;
171 }
172 
173 double DensityMatrix::purity() const {
174  // Compute ρ² using BLAS
175  Matrix rho_matrix(data, rows, cols, stride);
176 
177  // Use existing SQUANDER BLAS wrapper: ρ² = ρ * ρ
178  Matrix rho_squared = dot(rho_matrix, rho_matrix);
179 
180  // Trace of ρ²
181  double pur = 0.0;
182  for (int i = 0; i < rows; i++) {
183  pur += rho_squared.get_data()[i * rho_squared.stride + i].real;
184  }
185 
186  return pur;
187 }
188 
189 double DensityMatrix::entropy() const {
190  std::vector<double> eigs = eigenvalues();
191 
192  double S = 0.0;
193  for (double lambda : eigs) {
194  if (lambda > 1e-14) { // Avoid log(0)
195  S -= lambda * std::log2(lambda);
196  }
197  }
198 
199  return S;
200 }
201 
202 bool DensityMatrix::is_valid(double tol) const {
203  // Check 1: Hermitian
204  if (!is_hermitian(tol)) {
205  return false;
206  }
207 
208  // Check 2: Trace = 1
209  QGD_Complex16 tr = trace();
210  if (std::abs(tr.real - 1.0) > tol || std::abs(tr.imag) > tol) {
211  return false;
212  }
213 
214  // Check 3: Positive semi-definite (all eigenvalues ≥ 0)
215  std::vector<double> eigs = eigenvalues();
216  for (double eig : eigs) {
217  if (eig < -tol) {
218  return false;
219  }
220  }
221 
222  return true;
223 }
224 
225 bool DensityMatrix::is_hermitian(double tol) const {
226  // Check ρ(i,j) = conj(ρ(j,i)) for all i,j
227  for (int i = 0; i < rows; i++) {
228  for (int j = i; j < cols; j++) {
229  QGD_Complex16 rho_ij = data[i * stride + j];
230  QGD_Complex16 rho_ji = data[j * stride + i];
231 
232  // Check real part: Re(ρ(i,j)) = Re(ρ(j,i))
233  if (std::abs(rho_ij.real - rho_ji.real) > tol) {
234  return false;
235  }
236 
237  // Check imaginary part: Im(ρ(i,j)) = -Im(ρ(j,i))
238  if (std::abs(rho_ij.imag + rho_ji.imag) > tol) {
239  return false;
240  }
241  }
242  }
243  return true;
244 }
245 
246 std::vector<double> DensityMatrix::eigenvalues() const {
247  // Copy matrix (LAPACK modifies input)
248  DensityMatrix rho_copy = clone();
249 
250  std::vector<double> eigs(rows);
251 
252  // LAPACK workspace query
253  char jobz = 'N'; // Eigenvalues only (not eigenvectors)
254  char uplo = 'U'; // Upper triangle
255  int n = rows;
256  int lda = stride;
257  int lwork = 2 * n; // Workspace size
258  std::vector<QGD_Complex16> work(lwork);
259  std::vector<double> rwork(3 * n);
260  int info;
261 
262  // Call LAPACK Hermitian eigenvalue solver
263  zheev_(&jobz, &uplo, &n, rho_copy.data, &lda, eigs.data(), work.data(),
264  &lwork, rwork.data(), &info);
265 
266  if (info != 0) {
267  throw std::runtime_error(
268  "DensityMatrix::eigenvalues: LAPACK zheev failed with info=" +
269  std::to_string(info));
270  }
271 
272  // Sort descending (LAPACK returns ascending)
273  std::sort(eigs.rbegin(), eigs.rend());
274 
275  return eigs;
276 }
277 
278 // ================================================================
279 // Operations
280 // ================================================================
281 
283  if (U.rows != rows || U.cols != cols) {
284  throw std::runtime_error(
285  "DensityMatrix::apply_unitary: dimension mismatch. " +
286  std::string("Expected ") + std::to_string(rows) + "x" +
287  std::to_string(cols) + ", got " + std::to_string(U.rows) + "x" +
288  std::to_string(U.cols));
289  }
290 
291  // Wrap as Matrix for BLAS operations
292  Matrix rho_matrix(data, rows, cols, stride);
293  Matrix U_matrix(const_cast<QGD_Complex16 *>(U.data), U.rows, U.cols,
294  U.stride);
295 
296  // Step 1: Compute U * ρ
297  Matrix temp = dot(U_matrix, rho_matrix);
298 
299  // Step 2: Compute (U * ρ) * U†
300  // Create U† (conjugate transpose)
301  Matrix U_dagger = U_matrix.copy();
302  U_dagger.conjugate();
303  U_dagger.transpose();
304 
305  // Compute result and copy back to original data
306  Matrix result = dot(temp, U_dagger);
307  memcpy(data, result.get_data(), rows * cols * sizeof(QGD_Complex16));
308 }
309 
310 // ================================================================
311 // Optimized Local Unitary Application
312 // ================================================================
313 
315  const matrix_base<QGD_Complex16> &u_2x2, int target_qbit) {
316  if (target_qbit < 0 || target_qbit >= qbit_num_) {
317  throw std::runtime_error(
318  "DensityMatrix::apply_single_qubit_unitary: target_qbit out of range");
319  }
320 
321  if (u_2x2.rows != 2 || u_2x2.cols != 2) {
322  throw std::runtime_error(
323  "DensityMatrix::apply_single_qubit_unitary: kernel must be 2x2");
324  }
325 
326  int dim = rows;
327  int target_step = 1 << target_qbit;
328 
329  // Temporary storage for the result
330  std::vector<QGD_Complex16> temp(dim * dim);
331 
332  // The 2×2 kernel elements
333  QGD_Complex16 u00 = u_2x2.data[0];
334  QGD_Complex16 u01 = u_2x2.data[1];
335  QGD_Complex16 u10 = u_2x2.data[2];
336  QGD_Complex16 u11 = u_2x2.data[3];
337 
338  // Conjugates for U†
339  QGD_Complex16 u00_conj = {u00.real, -u00.imag};
340  QGD_Complex16 u01_conj = {u01.real, -u01.imag};
341  QGD_Complex16 u10_conj = {u10.real, -u10.imag};
342  QGD_Complex16 u11_conj = {u11.real, -u11.imag};
343 
344  // Apply ρ' = U ρ U†
345  // For each element ρ'(i,j), we compute the transformation
346  for (int i = 0; i < dim; i++) {
347  int i_bit = (i >> target_qbit) & 1;
348  int i_partner = i ^ target_step;
349 
350  for (int j = 0; j < dim; j++) {
351  int j_bit = (j >> target_qbit) & 1;
352  int j_partner = j ^ target_step;
353 
354  // Get the 2×2 block of ρ
355  QGD_Complex16 r00, r01, r10, r11;
356 
357  if (i_bit == 0 && j_bit == 0) {
358  r00 = data[i * stride + j];
359  r01 = data[i * stride + j_partner];
360  r10 = data[i_partner * stride + j];
361  r11 = data[i_partner * stride + j_partner];
362  } else if (i_bit == 0 && j_bit == 1) {
363  r00 = data[i * stride + j_partner];
364  r01 = data[i * stride + j];
365  r10 = data[i_partner * stride + j_partner];
366  r11 = data[i_partner * stride + j];
367  } else if (i_bit == 1 && j_bit == 0) {
368  r00 = data[i_partner * stride + j];
369  r01 = data[i_partner * stride + j_partner];
370  r10 = data[i * stride + j];
371  r11 = data[i * stride + j_partner];
372  } else {
373  r00 = data[i_partner * stride + j_partner];
374  r01 = data[i_partner * stride + j];
375  r10 = data[i * stride + j_partner];
376  r11 = data[i * stride + j];
377  }
378 
379  // Compute U * block
380  QGD_Complex16 t00, t01, t10, t11;
381 
382  // t = U * r
383  t00.real = u00.real * r00.real - u00.imag * r00.imag +
384  u01.real * r10.real - u01.imag * r10.imag;
385  t00.imag = u00.real * r00.imag + u00.imag * r00.real +
386  u01.real * r10.imag + u01.imag * r10.real;
387 
388  t01.real = u00.real * r01.real - u00.imag * r01.imag +
389  u01.real * r11.real - u01.imag * r11.imag;
390  t01.imag = u00.real * r01.imag + u00.imag * r01.real +
391  u01.real * r11.imag + u01.imag * r11.real;
392 
393  t10.real = u10.real * r00.real - u10.imag * r00.imag +
394  u11.real * r10.real - u11.imag * r10.imag;
395  t10.imag = u10.real * r00.imag + u10.imag * r00.real +
396  u11.real * r10.imag + u11.imag * r10.real;
397 
398  t11.real = u10.real * r01.real - u10.imag * r01.imag +
399  u11.real * r11.real - u11.imag * r11.imag;
400  t11.imag = u10.real * r01.imag + u10.imag * r01.real +
401  u11.real * r11.imag + u11.imag * r11.real;
402 
403  // Compute result = t * U† (right multiply by conjugate transpose)
404  // Select which element of the result we need based on i_bit, j_bit
405  QGD_Complex16 result_elem;
406 
407  if (i_bit == 0 && j_bit == 0) {
408  // result[0][0] = t00 * u00† + t01 * u01†
409  result_elem.real = t00.real * u00_conj.real - t00.imag * u00_conj.imag +
410  t01.real * u01_conj.real - t01.imag * u01_conj.imag;
411  result_elem.imag = t00.real * u00_conj.imag + t00.imag * u00_conj.real +
412  t01.real * u01_conj.imag + t01.imag * u01_conj.real;
413  } else if (i_bit == 0 && j_bit == 1) {
414  // result[0][1] = t00 * u10† + t01 * u11†
415  result_elem.real = t00.real * u10_conj.real - t00.imag * u10_conj.imag +
416  t01.real * u11_conj.real - t01.imag * u11_conj.imag;
417  result_elem.imag = t00.real * u10_conj.imag + t00.imag * u10_conj.real +
418  t01.real * u11_conj.imag + t01.imag * u11_conj.real;
419  } else if (i_bit == 1 && j_bit == 0) {
420  // result[1][0] = t10 * u00† + t11 * u01†
421  result_elem.real = t10.real * u00_conj.real - t10.imag * u00_conj.imag +
422  t11.real * u01_conj.real - t11.imag * u01_conj.imag;
423  result_elem.imag = t10.real * u00_conj.imag + t10.imag * u00_conj.real +
424  t11.real * u01_conj.imag + t11.imag * u01_conj.real;
425  } else {
426  // result[1][1] = t10 * u10† + t11 * u11†
427  result_elem.real = t10.real * u10_conj.real - t10.imag * u10_conj.imag +
428  t11.real * u11_conj.real - t11.imag * u11_conj.imag;
429  result_elem.imag = t10.real * u10_conj.imag + t10.imag * u10_conj.real +
430  t11.real * u11_conj.imag + t11.imag * u11_conj.real;
431  }
432 
433  temp[i * dim + j] = result_elem;
434  }
435  }
436 
437  // Copy result back
438  memcpy(data, temp.data(), dim * dim * sizeof(QGD_Complex16));
439 }
440 
442  const matrix_base<QGD_Complex16> &u_2x2, int target_qbit, int control_qbit) {
443  if (target_qbit < 0 || target_qbit >= qbit_num_) {
444  throw std::runtime_error(
445  "DensityMatrix::apply_two_qubit_unitary: target_qbit out of range");
446  }
447  if (control_qbit < 0 || control_qbit >= qbit_num_) {
448  throw std::runtime_error(
449  "DensityMatrix::apply_two_qubit_unitary: control_qbit out of range");
450  }
451  if (target_qbit == control_qbit) {
452  throw std::runtime_error(
453  "DensityMatrix::apply_two_qubit_unitary: target and control must differ");
454  }
455 
456  if (u_2x2.rows != 2 || u_2x2.cols != 2) {
457  throw std::runtime_error(
458  "DensityMatrix::apply_two_qubit_unitary: kernel must be 2x2");
459  }
460 
461  int dim = rows;
462  int target_step = 1 << target_qbit;
463 
464  // Temporary storage
465  std::vector<QGD_Complex16> temp(dim * dim);
466  memcpy(temp.data(), data, dim * dim * sizeof(QGD_Complex16));
467 
468  // The 2×2 kernel elements
469  QGD_Complex16 u00 = u_2x2.data[0];
470  QGD_Complex16 u01 = u_2x2.data[1];
471  QGD_Complex16 u10 = u_2x2.data[2];
472  QGD_Complex16 u11 = u_2x2.data[3];
473 
474  // For controlled gates, we only apply the 2x2 kernel when control=1
475  // This is equivalent to applying identity when control=0
476 
477  for (int i = 0; i < dim; i++) {
478  int i_control = (i >> control_qbit) & 1;
479 
480  for (int j = 0; j < dim; j++) {
481  int j_control = (j >> control_qbit) & 1;
482 
483  // Only modify elements where control conditions are met
484  // For ρ' = U ρ U†, we need to consider all combinations
485 
486  QGD_Complex16 result = {0.0, 0.0};
487 
488  // Sum over internal indices
489  for (int a = 0; a < 2; a++) {
490  for (int b = 0; b < 2; b++) {
491  // U acts on target when control=1, identity otherwise
492  QGD_Complex16 u_ia, u_jb_conj;
493 
494  int i_target = (i >> target_qbit) & 1;
495  int j_target = (j >> target_qbit) & 1;
496 
497  // Get U element for row transformation
498  if (i_control == 1) {
499  if (i_target == 0 && a == 0)
500  u_ia = u00;
501  else if (i_target == 0 && a == 1)
502  u_ia = u01;
503  else if (i_target == 1 && a == 0)
504  u_ia = u10;
505  else
506  u_ia = u11;
507  } else {
508  // Identity: only diagonal
509  u_ia = (i_target == a) ? QGD_Complex16{1.0, 0.0}
510  : QGD_Complex16{0.0, 0.0};
511  }
512 
513  // Get U† element for column transformation
514  if (j_control == 1) {
515  if (j_target == 0 && b == 0)
516  u_jb_conj = {u00.real, -u00.imag};
517  else if (j_target == 0 && b == 1)
518  u_jb_conj = {u10.real, -u10.imag};
519  else if (j_target == 1 && b == 0)
520  u_jb_conj = {u01.real, -u01.imag};
521  else
522  u_jb_conj = {u11.real, -u11.imag};
523  } else {
524  u_jb_conj = (j_target == b) ? QGD_Complex16{1.0, 0.0}
525  : QGD_Complex16{0.0, 0.0};
526  }
527 
528  // Compute index for ρ element
529  int i_prime = (i & ~target_step) | (a << target_qbit);
530  int j_prime = (j & ~target_step) | (b << target_qbit);
531 
532  QGD_Complex16 rho_ab = data[i_prime * stride + j_prime];
533 
534  // result += u_ia * rho_ab * u_jb_conj
535  QGD_Complex16 prod1;
536  prod1.real = u_ia.real * rho_ab.real - u_ia.imag * rho_ab.imag;
537  prod1.imag = u_ia.real * rho_ab.imag + u_ia.imag * rho_ab.real;
538 
539  result.real +=
540  prod1.real * u_jb_conj.real - prod1.imag * u_jb_conj.imag;
541  result.imag +=
542  prod1.real * u_jb_conj.imag + prod1.imag * u_jb_conj.real;
543  }
544  }
545 
546  temp[i * dim + j] = result;
547  }
548  }
549 
550  memcpy(data, temp.data(), dim * dim * sizeof(QGD_Complex16));
551 }
552 
554  const matrix_base<QGD_Complex16> &u_kernel,
555  const std::vector<int> &target_qbits) {
556 
557  int k = static_cast<int>(target_qbits.size());
558 
559  if (k == 0) {
560  return; // Nothing to do
561  }
562 
563  if (k == 1) {
564  apply_single_qubit_unitary(u_kernel, target_qbits[0]);
565  return;
566  }
567 
568  // Validate
569  int kernel_dim = 1 << k;
570  if (u_kernel.rows != kernel_dim || u_kernel.cols != kernel_dim) {
571  throw std::runtime_error(
572  "DensityMatrix::apply_local_unitary: kernel size mismatch");
573  }
574 
575  for (int q : target_qbits) {
576  if (q < 0 || q >= qbit_num_) {
577  throw std::runtime_error(
578  "DensityMatrix::apply_local_unitary: qubit index out of range");
579  }
580  }
581 
582  int dim = rows;
583  std::vector<QGD_Complex16> temp(dim * dim);
584 
585  // General k-qubit application: ρ'(i,j) = Σ_{a,b} U(i_t,a) ρ(i',j') U†(b,j_t)
586  // where i_t = target bits of i, i' = i with target bits replaced by a
587 
588  for (int i = 0; i < dim; i++) {
589  for (int j = 0; j < dim; j++) {
590  QGD_Complex16 result = {0.0, 0.0};
591 
592  // Extract target bits from i and j
593  int i_target = 0, j_target = 0;
594  for (int ki = 0; ki < k; ki++) {
595  if ((i >> target_qbits[ki]) & 1) {
596  i_target |= (1 << ki);
597  }
598  if ((j >> target_qbits[ki]) & 1) {
599  j_target |= (1 << ki);
600  }
601  }
602 
603  // Sum over all internal indices
604  for (int a = 0; a < kernel_dim; a++) {
605  for (int b = 0; b < kernel_dim; b++) {
606  // Compute indices with replaced target bits
607  int i_prime = i;
608  int j_prime = j;
609  for (int ki = 0; ki < k; ki++) {
610  // Clear target bit
611  i_prime &= ~(1 << target_qbits[ki]);
612  j_prime &= ~(1 << target_qbits[ki]);
613  // Set from a, b
614  if ((a >> ki) & 1) {
615  i_prime |= (1 << target_qbits[ki]);
616  }
617  if ((b >> ki) & 1) {
618  j_prime |= (1 << target_qbits[ki]);
619  }
620  }
621 
622  // U[i_target, a]
623  QGD_Complex16 u_ia = u_kernel.data[i_target * kernel_dim + a];
624  // U†[b, j_target] = conj(U[j_target, b])
625  QGD_Complex16 u_jb = u_kernel.data[j_target * kernel_dim + b];
626  QGD_Complex16 u_jb_conj = {u_jb.real, -u_jb.imag};
627 
628  // ρ[i', j']
629  QGD_Complex16 rho_ab = data[i_prime * stride + j_prime];
630 
631  // result += u_ia * rho_ab * u_jb_conj
632  QGD_Complex16 prod1;
633  prod1.real = u_ia.real * rho_ab.real - u_ia.imag * rho_ab.imag;
634  prod1.imag = u_ia.real * rho_ab.imag + u_ia.imag * rho_ab.real;
635 
636  result.real +=
637  prod1.real * u_jb_conj.real - prod1.imag * u_jb_conj.imag;
638  result.imag +=
639  prod1.real * u_jb_conj.imag + prod1.imag * u_jb_conj.real;
640  }
641  }
642 
643  temp[i * dim + j] = result;
644  }
645  }
646 
647  memcpy(data, temp.data(), dim * dim * sizeof(QGD_Complex16));
648 }
649 
651 DensityMatrix::partial_trace(const std::vector<int> &trace_out) const {
652  // Validate inputs
653  if (trace_out.empty()) {
654  return clone(); // No qubits to trace out
655  }
656 
657  for (int q : trace_out) {
658  if (q < 0 || q >= qbit_num_) {
659  throw std::invalid_argument(
660  "DensityMatrix::partial_trace: qubit index out of range");
661  }
662  }
663 
664  // Calculate dimensions
665  int trace_out_num = static_cast<int>(trace_out.size());
666  int keep_num = qbit_num_ - trace_out_num;
667 
668  if (keep_num < 1) {
669  throw std::invalid_argument(
670  "DensityMatrix::partial_trace: cannot trace out all qubits");
671  }
672 
673  int dim_reduced = 1 << keep_num;
674  int dim_traced = 1 << trace_out_num;
675 
676  // Determine which qubits to keep
677  std::vector<int> keep_qubits;
678  for (int q = 0; q < qbit_num_; q++) {
679  bool should_trace = false;
680  for (int t : trace_out) {
681  if (q == t) {
682  should_trace = true;
683  break;
684  }
685  }
686  if (!should_trace) {
687  keep_qubits.push_back(q);
688  }
689  }
690 
691  // Create reduced density matrix
692  DensityMatrix rho_reduced(keep_num);
693  memset(rho_reduced.data, 0,
694  dim_reduced * dim_reduced * sizeof(QGD_Complex16));
695 
696  // Compute partial trace
697  // ρ_A(i,j) = Σ_k ⟨i,k|ρ|j,k⟩ where k runs over traced qubits
698 
699  for (int i = 0; i < dim_reduced; i++) {
700  for (int j = 0; j < dim_reduced; j++) {
701 
702  QGD_Complex16 sum;
703  sum.real = 0.0;
704  sum.imag = 0.0;
705 
706  // Sum over all basis states of traced-out qubits
707  for (int k = 0; k < dim_traced; k++) {
708  // Construct full basis state indices
709  int full_i = 0;
710  int full_j = 0;
711 
712  for (int q_idx = 0; q_idx < keep_num; q_idx++) {
713  int q = keep_qubits[q_idx];
714  if ((i >> q_idx) & 1) {
715  full_i |= (1 << q);
716  }
717  if ((j >> q_idx) & 1) {
718  full_j |= (1 << q);
719  }
720  }
721 
722  for (int t_idx = 0; t_idx < trace_out_num; t_idx++) {
723  int q = trace_out[t_idx];
724  if ((k >> t_idx) & 1) {
725  full_i |= (1 << q);
726  full_j |= (1 << q);
727  }
728  }
729 
730  // Add ρ(full_i, full_j) to sum
731  sum.real += data[full_i * stride + full_j].real;
732  sum.imag += data[full_i * stride + full_j].imag;
733  }
734 
735  rho_reduced.data[i * dim_reduced + j] = sum;
736  }
737  }
738 
739  return rho_reduced;
740 }
741 
744  memcpy(copy.data, data, rows * stride * sizeof(QGD_Complex16));
745  return copy;
746 }
747 
748 // ================================================================
749 // Static Factory Methods
750 // ================================================================
751 
753  DensityMatrix rho(qbit_num);
754 
755  int dim = 1 << qbit_num;
756  double prob = 1.0 / dim;
757 
758  // Set diagonal to 1/dim (identity matrix scaled)
759  memset(rho.data, 0, dim * dim * sizeof(QGD_Complex16));
760  for (int i = 0; i < dim; i++) {
761  rho.data[i * rho.stride + i].real = prob;
762  rho.data[i * rho.stride + i].imag = 0.0;
763  }
764 
765  return rho;
766 }
767 
768 // ================================================================
769 // Utilities
770 // ================================================================
771 
772 void DensityMatrix::print() const {
773  std::cout << "\n=== Density Matrix ===" << std::endl;
774  std::cout << "Qubits: " << qbit_num_ << std::endl;
775  std::cout << "Dimension: " << rows << " × " << cols << std::endl;
776 
777  QGD_Complex16 tr = trace();
778  std::cout << "Trace: " << tr.real;
779  if (std::abs(tr.imag) > 1e-10) {
780  std::cout << " + " << tr.imag << "i";
781  }
782  std::cout << std::endl;
783 
784  std::cout << "Purity: " << purity() << std::endl;
785  std::cout << "Entropy: " << entropy() << std::endl;
786  std::cout << "Valid: " << (is_valid() ? "Yes" : "No") << std::endl;
787 
788  std::cout << "\nMatrix elements:" << std::endl;
789  for (int i = 0; i < std::min(rows, 8);
790  i++) { // Limit output for large matrices
791  for (int j = 0; j < std::min(cols, 8); j++) {
792  QGD_Complex16 val = data[i * stride + j];
793  std::cout << "(" << val.real << "," << val.imag << ") ";
794  }
795  if (cols > 8)
796  std::cout << "...";
797  std::cout << std::endl;
798  }
799  if (rows > 8)
800  std::cout << "..." << std::endl;
801  std::cout << std::endl;
802 }
803 
805  if (rows != cols) {
806  throw std::runtime_error("DensityMatrix: matrix must be square");
807  }
808 
809  int expected_dim = 1 << qbit_num_;
810  if (rows != expected_dim) {
811  throw std::runtime_error("DensityMatrix: dimension mismatch. Expected " +
812  std::to_string(expected_dim) + ", got " +
813  std::to_string(rows));
814  }
815 }
816 
817 } // namespace density
818 } // namespace squander
Matrix dot(Matrix &A, Matrix &B)
Call to calculate the product of two complex matrices by calling method zgemm3m from the CBLAS librar...
Definition: dot.cpp:38
void apply_local_unitary(const matrix_base< QGD_Complex16 > &u_kernel, const std::vector< int > &target_qbits)
Apply k-qubit unitary using local kernel (general case)
Definition: S.h:11
int stride
The column stride of the array. (The array elements in one row are a_0, a_1, ... a_{cols-1}, 0, 0, 0, 0. The number of zeros is stride-cols)
Definition: matrix_base.hpp:46
matrix_base< QGD_Complex16 > copy() const
Call to create a copy of the matrix.
QGD_Complex16 * data
pointer to the stored data
Definition: matrix_base.hpp:48
DensityMatrix(int qbit_num)
Create density matrix for n qubits.
scalar * get_data() const
Call to get the pointer to the stored data.
DensityMatrix clone() const
Create deep copy.
void zheev_(char *jobz, char *uplo, int *n, QGD_Complex16 *a, int *lda, double *w, QGD_Complex16 *work, int *lwork, double *rwork, int *info)
bool is_valid(double tol=1e-10) const
Check if valid density matrix.
double entropy() const
von Neumann entropy: S(ρ) = -Tr(ρ log₂ ρ)
double purity() const
Calculate purity: Tr(ρ²)
Base Class to store data of arrays and its properties.
Definition: matrix_base.hpp:38
DensityMatrix partial_trace(const std::vector< int > &trace_out) const
Compute partial trace over specified qubits.
void operator=(const matrix_base &mtx)
Assignment operator.
int rows
The number of rows.
Definition: matrix_base.hpp:42
int cols
The number of columns.
Definition: matrix_base.hpp:44
DensityMatrix & operator=(const DensityMatrix &other)
std::vector< double > eigenvalues() const
Get eigenvalues (sorted descending)
bool is_hermitian(double tol) const
Quantum density matrix ρ for mixed-state representation.
QGD_Complex16 trace() const
Calculate trace: Tr(ρ)
void apply_unitary(const matrix_base< QGD_Complex16 > &U)
Apply unitary transformation: ρ → UρU†
Structure type representing complex numbers in the SQUANDER package.
Definition: QGDTypes.h:38
void print() const
Print matrix with properties.
Matrix copy() const
Call to create a copy of the matrix.
Definition: matrix.h:57
Double-precision complex matrix (float64).
Definition: matrix.h:38
void transpose()
Call to transpose (or un-transpose) the matrix for CBLAS functions.
QGD_Complex16 & operator()(int i, int j)
Element access: ρ(i,j)
int qbit_num_
Number of qubits.
double real
the real part of a complex number
Definition: QGDTypes.h:40
void conjugate()
Call to conjugate (or un-conjugate) the matrix for CBLAS functions.
static int calculate_qbit_num(int dim)
void apply_single_qubit_unitary(const matrix_base< QGD_Complex16 > &u_2x2, int target_qbit)
Apply single-qubit unitary using local kernel (optimized)
void apply_two_qubit_unitary(const matrix_base< QGD_Complex16 > &u_2x2, int target_qbit, int control_qbit)
Apply two-qubit controlled unitary using local kernel (optimized)
double imag
the imaginary part of a complex number
Definition: QGDTypes.h:42
static DensityMatrix maximally_mixed(int qbit_num)
Create maximally mixed state: ρ = I/2^n.