Sequential Quantum Gate Decomposer  v1.9.6
Powerful decomposition of general unitarias into one- and two-qubit gates gates
bindings.cpp
Go to the documentation of this file.
1 /*
2 Copyright 2025 SQUANDER Contributors
3 
4 pybind11 Bindings for Density Matrix Module - Approach B Implementation
5 */
6 
7 #include <pybind11/complex.h>
8 #include <pybind11/numpy.h>
9 #include <pybind11/operators.h>
10 #include <pybind11/pybind11.h>
11 #include <pybind11/stl.h>
12 
13 #include "density_matrix.h"
14 #include "density_operation.h"
15 #include "matrix.h"
16 #include "noise_channel.h"
17 #include "noisy_circuit.h"
18 
19 namespace py = pybind11;
20 using namespace squander::density;
21 
22 // ===================================================================
23 // NumPy <-> DensityMatrix conversion helpers
24 // ===================================================================
25 
26 DensityMatrix numpy_to_density_matrix(py::array_t<std::complex<double>> arr) {
27  auto buf = arr.request();
28 
29  if (buf.ndim != 2) {
30  throw std::runtime_error("Input must be 2D array");
31  }
32  if (buf.shape[0] != buf.shape[1]) {
33  throw std::runtime_error("Input must be square matrix");
34  }
35 
36  int dim = static_cast<int>(buf.shape[0]);
37 
38  // Check if dimension is power of 2
39  int temp = dim;
40  while (temp > 1 && temp % 2 == 0) {
41  temp /= 2;
42  }
43  if (temp != 1) {
44  throw std::runtime_error("Matrix dimension must be power of 2");
45  }
46 
47  // Copy data to DensityMatrix
48  int qbit_num = 0;
49  temp = dim;
50  while (temp > 1) {
51  temp /= 2;
52  qbit_num++;
53  }
54 
55  DensityMatrix rho(qbit_num);
56 
57  auto *src = static_cast<std::complex<double> *>(buf.ptr);
58  for (int i = 0; i < dim; i++) {
59  for (int j = 0; j < dim; j++) {
60  std::complex<double> val = src[i * dim + j];
61  rho(i, j).real = val.real();
62  rho(i, j).imag = val.imag();
63  }
64  }
65 
66  return rho;
67 }
68 
69 py::array_t<std::complex<double>>
71  int dim = rho.get_dim();
72 
73  py::array_t<std::complex<double>> result({dim, dim});
74  auto buf = result.request();
75  auto *ptr = static_cast<std::complex<double> *>(buf.ptr);
76 
77  for (int i = 0; i < dim; i++) {
78  for (int j = 0; j < dim; j++) {
79  QGD_Complex16 val = rho(i, j);
80  ptr[i * dim + j] = std::complex<double>(val.real, val.imag);
81  }
82  }
83 
84  return result;
85 }
86 
87 // ===================================================================
88 // Module Definition
89 // ===================================================================
90 
91 PYBIND11_MODULE(_density_matrix_cpp, m) {
92  m.doc() = R"pbdoc(
93  SQUANDER Density Matrix Module - C++ Backend (Approach B)
94 
95  High-performance density matrix simulation with:
96  - Optimized local kernel application (O(4^N) per gate)
97  - Unified interface for gates and noise channels
98  - Support for parametric noise
99  )pbdoc";
100 
101  // ===============================================================
102  // DensityMatrix class
103  // ===============================================================
104 
105  py::class_<DensityMatrix>(m, "DensityMatrix", R"pbdoc(
106  Quantum density matrix ρ for mixed-state representation.
107 
108  Features:
109  - Automatic memory management
110  - Optimized local unitary application
111  - Quantum properties (purity, entropy, eigenvalues)
112  )pbdoc")
113 
114  .def(py::init<int>(), py::arg("qbit_num"),
115  "Create density matrix for n qubits (initialized to |0⟩⟨0|)")
116 
117  .def(py::init([](py::array_t<std::complex<double>> state) {
118  auto buf = state.request();
119 
120  if (buf.ndim != 1) {
121  throw std::runtime_error("State vector must be 1D");
122  }
123 
124  int dim = static_cast<int>(buf.shape[0]);
125 
126  Matrix state_vec(dim, 1);
127  auto *src = static_cast<std::complex<double> *>(buf.ptr);
128  for (int i = 0; i < dim; i++) {
129  state_vec.get_data()[i].real = src[i].real();
130  state_vec.get_data()[i].imag = src[i].imag();
131  }
132 
133  return DensityMatrix(state_vec);
134  }),
135  py::arg("state_vector"), "Create from state vector: ρ = |ψ⟩⟨ψ|")
136 
137  .def_property_readonly("qbit_num", &DensityMatrix::get_qbit_num)
138  .def_property_readonly("dim", &DensityMatrix::get_dim)
139 
140  .def(
141  "trace",
142  [](const DensityMatrix &self) {
143  QGD_Complex16 tr = self.trace();
144  return std::complex<double>(tr.real, tr.imag);
145  },
146  "Calculate trace: Tr(ρ)")
147 
148  .def("purity", &DensityMatrix::purity, "Calculate purity: Tr(ρ²)")
149  .def("entropy", &DensityMatrix::entropy,
150  "von Neumann entropy: S(ρ) = -Tr(ρ log₂ ρ)")
151  .def("is_valid", &DensityMatrix::is_valid, py::arg("tol") = 1e-10,
152  "Check if valid density matrix")
153  .def("eigenvalues", &DensityMatrix::eigenvalues,
154  "Get eigenvalues (sorted descending)")
155 
156  .def(
157  "apply_unitary",
158  [](DensityMatrix &self, py::array_t<std::complex<double>> U) {
159  auto buf = U.request();
160  if (buf.ndim != 2 || buf.shape[0] != buf.shape[1]) {
161  throw std::runtime_error("Unitary must be square 2D array");
162  }
163 
164  int dim = static_cast<int>(buf.shape[0]);
165  Matrix U_mat(dim, dim);
166  auto *src = static_cast<std::complex<double> *>(buf.ptr);
167  for (int i = 0; i < dim * dim; i++) {
168  U_mat.get_data()[i].real = src[i].real();
169  U_mat.get_data()[i].imag = src[i].imag();
170  }
171 
172  self.apply_unitary(U_mat);
173  },
174  py::arg("U"), "Apply unitary: ρ → UρU†")
175 
176  .def(
177  "apply_single_qubit_unitary",
178  [](DensityMatrix &self, py::array_t<std::complex<double>> u,
179  int target) {
180  auto buf = u.request();
181  if (buf.ndim != 2 || buf.shape[0] != 2 || buf.shape[1] != 2) {
182  throw std::runtime_error("Kernel must be 2x2");
183  }
184 
185  Matrix u_mat(2, 2);
186  auto *src = static_cast<std::complex<double> *>(buf.ptr);
187  for (int i = 0; i < 4; i++) {
188  u_mat.get_data()[i].real = src[i].real();
189  u_mat.get_data()[i].imag = src[i].imag();
190  }
191 
192  self.apply_single_qubit_unitary(u_mat, target);
193  },
194  py::arg("u_2x2"), py::arg("target_qbit"),
195  "Apply single-qubit unitary using optimized local kernel")
196 
197  .def(
198  "apply_local_unitary",
199  [](DensityMatrix &self, py::array_t<std::complex<double>> u,
200  const std::vector<int> &target_qbits) {
201  auto buf = u.request();
202  if (buf.ndim != 2 || buf.shape[0] != buf.shape[1]) {
203  throw std::runtime_error("Kernel must be square 2D array");
204  }
205 
206  int dim = static_cast<int>(buf.shape[0]);
207  Matrix u_mat(dim, dim);
208  auto *src = static_cast<std::complex<double> *>(buf.ptr);
209  for (int i = 0; i < dim * dim; i++) {
210  u_mat.get_data()[i].real = src[i].real();
211  u_mat.get_data()[i].imag = src[i].imag();
212  }
213 
214  self.apply_local_unitary(u_mat, target_qbits);
215  },
216  py::arg("u_kernel"), py::arg("target_qbits"),
217  "Apply a k-qubit local unitary using optimized local kernel")
218 
219  .def("partial_trace", &DensityMatrix::partial_trace, py::arg("trace_out"),
220  "Partial trace over specified qubits")
221  .def("clone", &DensityMatrix::clone, "Create a deep copy")
222 
223  .def_static("maximally_mixed", &DensityMatrix::maximally_mixed,
224  py::arg("qbit_num"), "Create maximally mixed state: ρ = I/2^n")
225 
226  .def(
227  "to_numpy",
228  [](const DensityMatrix &self) {
229  return density_matrix_to_numpy(self);
230  },
231  "Convert to NumPy array")
232 
233  .def_static(
234  "from_numpy",
235  [](py::array_t<std::complex<double>> arr) {
236  return numpy_to_density_matrix(arr);
237  },
238  py::arg("array"), "Create from NumPy array")
239 
240  .def("__repr__", [](const DensityMatrix &self) {
241  return "<DensityMatrix: " + std::to_string(self.get_qbit_num()) +
242  " qubits, purity=" + std::to_string(self.purity()) + ">";
243  });
244 
245  // ===============================================================
246  // OperationInfo struct
247  // ===============================================================
248 
249  py::class_<NoisyCircuit::OperationInfo>(m, "OperationInfo",
250  "Information about a circuit operation")
251  .def_readonly("name", &NoisyCircuit::OperationInfo::name)
252  .def_readonly("is_unitary", &NoisyCircuit::OperationInfo::is_unitary)
253  .def_readonly("param_count", &NoisyCircuit::OperationInfo::param_count)
254  .def_readonly("param_start", &NoisyCircuit::OperationInfo::param_start)
255  .def("__repr__", [](const NoisyCircuit::OperationInfo &self) {
256  return "<OperationInfo: " + self.name +
257  (self.is_unitary ? " (gate)" : " (noise)") +
258  ", params=" + std::to_string(self.param_count) + ">";
259  });
260 
261  // ===============================================================
262  // NoisyCircuit class
263  // ===============================================================
264 
265  py::class_<NoisyCircuit>(m, "NoisyCircuit", R"pbdoc(
266  Noisy quantum circuit for density matrix simulation.
267 
268  Supports unitary gates and noise channels with unified interface.
269  Uses optimized local kernel application for O(4^N) per-gate performance.
270 
271  Example:
272  circuit = NoisyCircuit(2)
273  circuit.add_H(0)
274  circuit.add_CNOT(1, 0)
275  circuit.add_depolarizing(2, error_rate=0.01)
276  circuit.add_RZ(0)
277  circuit.add_phase_damping(0) # Parametric
278 
279  rho = DensityMatrix(2)
280  params = np.array([0.5, 0.02]) # RZ angle, phase damping λ
281  circuit.apply_to(params, rho)
282  )pbdoc")
283 
284  .def(py::init<int>(), py::arg("qbit_num"),
285  "Create empty circuit for n qubits")
286 
287  // Single-qubit constant gates
288  .def("add_H", &NoisyCircuit::add_H, py::arg("target"), "Add Hadamard gate")
289  .def("add_X", &NoisyCircuit::add_X, py::arg("target"), "Add Pauli-X gate")
290  .def("add_Y", &NoisyCircuit::add_Y, py::arg("target"), "Add Pauli-Y gate")
291  .def("add_Z", &NoisyCircuit::add_Z, py::arg("target"), "Add Pauli-Z gate")
292  .def("add_S", &NoisyCircuit::add_S, py::arg("target"), "Add S gate")
293  .def("add_Sdg", &NoisyCircuit::add_Sdg, py::arg("target"), "Add S† gate")
294  .def("add_T", &NoisyCircuit::add_T, py::arg("target"), "Add T gate")
295  .def("add_Tdg", &NoisyCircuit::add_Tdg, py::arg("target"), "Add T† gate")
296  .def("add_SX", &NoisyCircuit::add_SX, py::arg("target"), "Add √X gate")
297 
298  // Single-qubit parametric gates
299  .def("add_RX", &NoisyCircuit::add_RX, py::arg("target"),
300  "Add RX rotation (1 param)")
301  .def("add_RY", &NoisyCircuit::add_RY, py::arg("target"),
302  "Add RY rotation (1 param)")
303  .def("add_RZ", &NoisyCircuit::add_RZ, py::arg("target"),
304  "Add RZ rotation (1 param)")
305  .def("add_U1", &NoisyCircuit::add_U1, py::arg("target"),
306  "Add U1 gate (1 param)")
307  .def("add_U2", &NoisyCircuit::add_U2, py::arg("target"),
308  "Add U2 gate (2 params)")
309  .def("add_U3", &NoisyCircuit::add_U3, py::arg("target"),
310  "Add U3 gate (3 params)")
311 
312  // Two-qubit constant gates
313  .def("add_CNOT", &NoisyCircuit::add_CNOT, py::arg("target"),
314  py::arg("control"), "Add CNOT gate")
315  .def("add_CZ", &NoisyCircuit::add_CZ, py::arg("target"),
316  py::arg("control"), "Add CZ gate")
317  .def("add_CH", &NoisyCircuit::add_CH, py::arg("target"),
318  py::arg("control"), "Add CH gate")
319 
320  // Two-qubit parametric gates
321  .def("add_CRY", &NoisyCircuit::add_CRY, py::arg("target"),
322  py::arg("control"), "Add CRY gate (1 param)")
323  .def("add_CRZ", &NoisyCircuit::add_CRZ, py::arg("target"),
324  py::arg("control"), "Add CRZ gate (1 param)")
325  .def("add_CRX", &NoisyCircuit::add_CRX, py::arg("target"),
326  py::arg("control"), "Add CRX gate (1 param)")
327  .def("add_CP", &NoisyCircuit::add_CP, py::arg("target"),
328  py::arg("control"), "Add CP gate (1 param)")
329 
330  // Noise channels
331  .def(
332  "add_depolarizing",
333  [](NoisyCircuit &self, int qbit_num, py::object p) {
334  if (p.is_none()) {
335  self.add_depolarizing(qbit_num);
336  } else {
337  self.add_depolarizing(qbit_num, p.cast<double>());
338  }
339  },
340  py::arg("qbit_num"), py::arg("error_rate") = py::none(),
341  R"pbdoc(
342  Add depolarizing noise: ρ → (1-p)ρ + p·I/2^n
343 
344  Args:
345  qbit_num: Number of qubits the noise acts on
346  error_rate: Fixed p, or None for parametric (1 param)
347  )pbdoc")
348 
349  .def(
350  "add_local_depolarizing",
351  [](NoisyCircuit &self, int target, py::object p) {
352  if (p.is_none()) {
353  self.add_local_depolarizing(target);
354  } else {
355  self.add_local_depolarizing(target, p.cast<double>());
356  }
357  },
358  py::arg("target"), py::arg("error_rate") = py::none(),
359  R"pbdoc(
360  Add local single-qubit depolarizing noise on one target qubit.
361 
362  Args:
363  target: Target qubit index
364  error_rate: Fixed p, or None for parametric (1 param)
365  )pbdoc")
366 
367  .def(
368  "add_amplitude_damping",
369  [](NoisyCircuit &self, int target, py::object gamma) {
370  if (gamma.is_none()) {
371  self.add_amplitude_damping(target);
372  } else {
373  self.add_amplitude_damping(target, gamma.cast<double>());
374  }
375  },
376  py::arg("target"), py::arg("gamma") = py::none(),
377  R"pbdoc(
378  Add amplitude damping (T1 relaxation)
379 
380  Args:
381  target: Target qubit index
382  gamma: Fixed γ = 1-exp(-t/T1), or None for parametric (1 param)
383  )pbdoc")
384 
385  .def(
386  "add_phase_damping",
387  [](NoisyCircuit &self, int target, py::object lambda_val) {
388  if (lambda_val.is_none()) {
389  self.add_phase_damping(target);
390  } else {
391  self.add_phase_damping(target, lambda_val.cast<double>());
392  }
393  },
394  py::arg("target"), py::arg("lambda_param") = py::none(),
395  R"pbdoc(
396  Add phase damping (T2 dephasing)
397 
398  Args:
399  target: Target qubit index
400  lambda_param: Fixed λ = 1-exp(-t/T2), or None for parametric (1 param)
401  )pbdoc")
402 
403  // Execution
404  .def(
405  "apply_to",
406  [](NoisyCircuit &self, py::array_t<double> params,
407  DensityMatrix &rho) {
408  auto buf = params.request();
409  self.apply_to(static_cast<double *>(buf.ptr), static_cast<int>(buf.size), rho);
410  },
411  py::arg("parameters"), py::arg("density_matrix"),
412  "Apply circuit to density matrix")
413 
414  // Properties
415  .def_property_readonly("qbit_num", &NoisyCircuit::get_qbit_num,
416  "Number of qubits")
417  .def_property_readonly("parameter_num", &NoisyCircuit::get_parameter_num,
418  "Total number of parameters")
419  .def("__len__", &NoisyCircuit::get_operation_count,
420  "Number of operations")
421 
422  // Inspection
423  .def("get_operation_info", &NoisyCircuit::get_operation_info,
424  "Get list of all operations with their info")
425 
426  .def("__repr__", [](const NoisyCircuit &self) {
427  return "<NoisyCircuit: " + std::to_string(self.get_qbit_num()) +
428  " qubits, " + std::to_string(self.get_operation_count()) +
429  " ops, " + std::to_string(self.get_parameter_num()) + " params>";
430  });
431 
432  // ===============================================================
433  // Legacy Noise Channels (for backward compatibility)
434  // ===============================================================
435 
436  py::class_<NoiseChannel, std::shared_ptr<NoiseChannel>>(
437  m, "NoiseChannel", "Base class for quantum noise channels (legacy)")
438  .def("apply", &NoiseChannel::apply, py::arg("density_matrix"),
439  "Apply noise channel to density matrix")
440  .def("get_name", &NoiseChannel::get_name, "Get channel name");
441 
442  py::class_<DepolarizingChannel, NoiseChannel,
443  std::shared_ptr<DepolarizingChannel>>(
444  m, "DepolarizingChannel", "Depolarizing channel (legacy standalone)")
445  .def(py::init<int, double>(), py::arg("qbit_num"), py::arg("error_rate"))
446  .def_property_readonly("error_rate", &DepolarizingChannel::get_error_rate)
447  .def_property_readonly("qbit_num", &DepolarizingChannel::get_qbit_num);
448 
449  py::class_<AmplitudeDampingChannel, NoiseChannel,
450  std::shared_ptr<AmplitudeDampingChannel>>(
451  m, "AmplitudeDampingChannel",
452  "Amplitude damping channel (legacy standalone)")
453  .def(py::init<int, double>(), py::arg("target_qbit"), py::arg("gamma"))
454  .def_property_readonly("gamma", &AmplitudeDampingChannel::get_gamma)
455  .def_property_readonly("target_qbit",
457 
458  py::class_<PhaseDampingChannel, NoiseChannel,
459  std::shared_ptr<PhaseDampingChannel>>(
460  m, "PhaseDampingChannel", "Phase damping channel (legacy standalone)")
461  .def(py::init<int, double>(), py::arg("target_qbit"), py::arg("lambda"))
462  .def_property_readonly("lambda_param", &PhaseDampingChannel::get_lambda)
463  .def_property_readonly("target_qbit",
465 
466  // ===============================================================
467  // Module metadata
468  // ===============================================================
469 
470  m.attr("__version__") = "1.0.0";
471 }
virtual std::string get_name() const =0
Get channel name.
void add_CZ(int target, int control)
void add_RZ(int target)
1 parameter
void add_CRZ(int target, int control)
1 parameter
void add_CNOT(int target, int control)
void add_U2(int target)
2 parameters
void add_U1(int target)
1 parameter
std::vector< OperationInfo > get_operation_info() const
Get information about all operations.
py::array_t< std::complex< double > > density_matrix_to_numpy(const DensityMatrix &rho)
Definition: bindings.cpp:70
scalar * get_data() const
Call to get the pointer to the stored data.
void add_CP(int target, int control)
1 parameter
DensityMatrix clone() const
Create deep copy.
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(ρ²)
void add_U3(int target)
3 parameters
Amplitude damping (T1 relaxation): |1⟩ → |0⟩ decay.
Depolarizing channel: ρ → (1-p)ρ + p·I/2^n.
Definition: noise_channel.h:66
DensityMatrix partial_trace(const std::vector< int > &trace_out) const
Compute partial trace over specified qubits.
virtual void apply(DensityMatrix &rho)=0
Apply noise channel to density matrix.
Base class for quantum noise channels.
Definition: noise_channel.h:31
int get_qbit_num() const
Get number of qubits.
double arg(const QGD_Complex16 &a)
Call to retrieve the phase of a complex number.
Definition: common.cpp:444
DensityMatrix numpy_to_density_matrix(py::array_t< std::complex< double >> arr)
Definition: bindings.cpp:26
PYBIND11_MODULE(_density_matrix_cpp, m)
Definition: bindings.cpp:91
void add_CRY(int target, int control)
1 parameter
std::vector< double > eigenvalues() const
Get eigenvalues (sorted descending)
Header file of complex array storage array with automatic and thread safe reference counting...
Quantum density matrix ρ for mixed-state representation.
Structure type representing complex numbers in the SQUANDER package.
Definition: QGDTypes.h:38
int get_parameter_num() const
Get total number of parameters needed.
int get_qbit_num() const
Get number of qubits.
Double-precision complex matrix (float64).
Definition: matrix.h:38
void add_RY(int target)
1 parameter
Information about a circuit operation.
void add_CH(int target, int control)
size_t get_operation_count() const
Get number of operations in the circuit.
void add_RX(int target)
1 parameter
int get_dim() const
Get matrix dimension (2^qbit_num)
void add_CRX(int target, int control)
1 parameter
double real
the real part of a complex number
Definition: QGDTypes.h:40
Phase damping (T2 dephasing): loss of coherence.
Quantum circuit with noise for density matrix simulation.
Definition: noisy_circuit.h:57
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.