qsengine 1.0.1__tar.gz → 1.0.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -799,7 +799,7 @@ dependencies = [
799
799
 
800
800
  [[package]]
801
801
  name = "qsengine"
802
- version = "1.0.1"
802
+ version = "1.0.2"
803
803
  dependencies = [
804
804
  "bytemuck",
805
805
  "num-complex",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "qsengine"
3
- version = "1.0.1"
3
+ version = "1.0.2"
4
4
  edition = "2024"
5
5
  license = "MIT"
6
6
  description = "High-performance state-vector quantum simulator (CPU + portable GPU backends)"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qsengine
3
- Version: 1.0.1
3
+ Version: 1.0.2
4
4
  Classifier: Development Status :: 5 - Production/Stable
5
5
  Classifier: Intended Audience :: Science/Research
6
6
  Classifier: Intended Audience :: Education
@@ -81,6 +81,7 @@ qc.h(0)
81
81
  qc.cx(0, 1)
82
82
  qc.cx(1, 2)
83
83
 
84
+ print(qc) # text drawing, one wire per qubit
84
85
  qc.run(shots=1000) # measurement counts: {'000': ..., '111': ...}
85
86
  qc.probabilities() # exact probabilities of all 8 basis states (NumPy array)
86
87
  qc.statevector() # exact complex amplitudes (NumPy array)
@@ -48,6 +48,7 @@ qc.h(0)
48
48
  qc.cx(0, 1)
49
49
  qc.cx(1, 2)
50
50
 
51
+ print(qc) # text drawing, one wire per qubit
51
52
  qc.run(shots=1000) # measurement counts: {'000': ..., '111': ...}
52
53
  qc.probabilities() # exact probabilities of all 8 basis states (NumPy array)
53
54
  qc.statevector() # exact complex amplitudes (NumPy array)
@@ -21,6 +21,8 @@ class QuantumCircuit:
21
21
  def num_qubits(self) -> int: ...
22
22
  def __len__(self) -> int:
23
23
  """Number of gates recorded so far."""
24
+ def __str__(self) -> str:
25
+ """Text drawing of the circuit, one wire per qubit: `print(qc)`."""
24
26
 
25
27
  # Single-qubit gates
26
28
  def x(self, qubit: int) -> None: ...
@@ -39,6 +39,12 @@ pub trait QuantumBackend {
39
39
  /// Copy of all `2^n` amplitudes, in basis-index order.
40
40
  fn amplitudes(&self) -> Vec<Complex64>;
41
41
 
42
+ /// All `2^n` amplitudes, consuming the backend. Backends that already hold
43
+ /// them in host memory override this to hand the vector over without a copy.
44
+ fn into_amplitudes(self: Box<Self>) -> Vec<Complex64> {
45
+ self.amplitudes()
46
+ }
47
+
42
48
  /// Replace the state with the basis state `|index>` (amplitude 1 there, 0 elsewhere).
43
49
  fn collapse_to(&mut self, index: usize);
44
50
 
@@ -63,6 +69,7 @@ pub trait QuantumBackend {
63
69
  gate: &Gate,
64
70
  ) -> Result<()> {
65
71
  let mask = control_mask(self.num_qubits(), controls, target)?;
72
+ check_finite(gate)?;
66
73
  self.apply_gate_kernel(mask, target, gate);
67
74
  Ok(())
68
75
  }
@@ -122,7 +129,7 @@ pub trait QuantumBackend {
122
129
  /// Picks basis state `i` with probability `|amp_i|^2`, collapses the state
123
130
  /// to `|i>`, and returns `i`. Use [`crate::format_bitstring`] to print it.
124
131
  fn measure(&mut self, rng: &mut dyn RngCore) -> usize {
125
- let cumulative = cumulative_probabilities(&self.probabilities());
132
+ let cumulative = cumulative_probabilities(self.probabilities());
126
133
  let outcome = sample_index(&cumulative, rng);
127
134
  // Global phase is unobservable, so collapsing to amplitude exactly 1 is valid.
128
135
  self.collapse_to(outcome);
@@ -134,7 +141,7 @@ pub trait QuantumBackend {
134
141
  /// Returns `basis index -> number of times seen`. This is how real circuits
135
142
  /// are run: prepare once, sample many times. Costs `O(2^n + shots * n)`.
136
143
  fn sample(&self, shots: usize, rng: &mut dyn RngCore) -> HashMap<usize, usize> {
137
- let cumulative = cumulative_probabilities(&self.probabilities());
144
+ let cumulative = cumulative_probabilities(self.probabilities());
138
145
  let mut counts = HashMap::new();
139
146
  for _ in 0..shots {
140
147
  *counts.entry(sample_index(&cumulative, rng)).or_insert(0) += 1;
@@ -165,15 +172,20 @@ pub fn control_mask(num_qubits: usize, controls: &[usize], target: usize) -> Res
165
172
  Ok(mask)
166
173
  }
167
174
 
168
- /// Running sum of `probs`: `out[i] = probs[0] + ... + probs[i]`.
169
- fn cumulative_probabilities(probs: &[f64]) -> Vec<f64> {
175
+ /// Fail if any entry of `gate` is NaN or infinite, e.g. from `rx(q, float("nan"))`.
176
+ /// Such a gate turns the whole state into NaN, which sampling would silently read as |0...0>.
177
+ pub fn check_finite(gate: &Gate) -> Result<()> {
178
+ if gate.as_flattened().iter().all(|z| z.is_finite()) { Ok(()) } else { Err(QsError::NonFinite) }
179
+ }
180
+
181
+ /// Running sum of `probs`, in place (no second 2^n buffer): `out[i] = probs[0] + ... + probs[i]`.
182
+ fn cumulative_probabilities(mut probs: Vec<f64>) -> Vec<f64> {
183
+ let mut acc = 0.0;
184
+ for p in &mut probs {
185
+ acc += *p;
186
+ *p = acc;
187
+ }
170
188
  probs
171
- .iter()
172
- .scan(0.0, |acc, p| {
173
- *acc += p;
174
- Some(*acc)
175
- })
176
- .collect()
177
189
  }
178
190
 
179
191
  /// Weighted random pick from a cumulative distribution using binary search.
@@ -288,5 +300,7 @@ pub(crate) mod conformance {
288
300
  assert_eq!(s.h(2), Err(QsError::QubitOutOfRange { qubit: 2, num_qubits: 2 }));
289
301
  assert_eq!(s.cx(1, 1), Err(QsError::DuplicateQubit(1)));
290
302
  assert_eq!(s.cx(5, 0), Err(QsError::QubitOutOfRange { qubit: 5, num_qubits: 2 }));
303
+ assert_eq!(s.rx(0, f64::NAN), Err(QsError::NonFinite));
304
+ assert_eq!(s.rz(0, f64::INFINITY), Err(QsError::NonFinite));
291
305
  }
292
306
  }
@@ -9,12 +9,13 @@
9
9
  //! instructions, and the whole simulation happens in Rust in one call, with the
10
10
  //! state vector never crossing into Python.
11
11
 
12
- use crate::backend::{QuantumBackend, control_mask};
12
+ use crate::backend::{QuantumBackend, check_finite, control_mask};
13
13
  use crate::gates::{self, Gate};
14
14
  use crate::{BackendKind, MAX_QUBITS, QsError, Result};
15
15
  use num_complex::Complex64;
16
16
  use rand::RngCore;
17
17
  use std::collections::HashMap;
18
+ use std::fmt;
18
19
 
19
20
  /// One recorded gate: apply `gate` to `target` where all `control_mask` bits are 1.
20
21
  /// These are the exact arguments of [`QuantumBackend::apply_gate_kernel`].
@@ -30,6 +31,22 @@ pub struct Instruction {
30
31
  pub struct QuantumCircuit {
31
32
  num_qubits: usize,
32
33
  instructions: Vec<Instruction>,
34
+ drawing: Vec<DrawOp>,
35
+ }
36
+
37
+ /// One gate as the user wrote it, kept only for drawing: `swap` is one entry
38
+ /// here but three CNOTs in `instructions`.
39
+ #[derive(Debug, Clone, PartialEq)]
40
+ struct DrawOp {
41
+ label: String,
42
+ controls: Vec<usize>,
43
+ targets: Vec<usize>,
44
+ }
45
+
46
+ /// Drawing label for a gate with angle parameters, e.g. `RX(1.57)`.
47
+ fn label(name: &str, angles: &[f64]) -> String {
48
+ let args: Vec<String> = angles.iter().map(|a| format!("{a:.2}")).collect();
49
+ format!("{name}({})", args.join(","))
33
50
  }
34
51
 
35
52
  impl QuantumCircuit {
@@ -38,7 +55,7 @@ impl QuantumCircuit {
38
55
  if num_qubits > MAX_QUBITS {
39
56
  return Err(QsError::TooManyQubits { requested: num_qubits, max: MAX_QUBITS });
40
57
  }
41
- Ok(Self { num_qubits, instructions: Vec::new() })
58
+ Ok(Self { num_qubits, instructions: Vec::new(), drawing: Vec::new() })
42
59
  }
43
60
 
44
61
  pub fn num_qubits(&self) -> usize {
@@ -53,8 +70,15 @@ impl QuantumCircuit {
53
70
  /// Record `gate` on `target`, controlled on every qubit in `controls`
54
71
  /// (empty for a plain single-qubit gate). Every other gate method calls this.
55
72
  pub fn add_gate(&mut self, controls: &[usize], target: usize, gate: Gate) -> Result<()> {
73
+ self.push("U".into(), controls, target, gate)
74
+ }
75
+
76
+ /// Validate and record one gate, with the label [`Display`](fmt::Display) draws for it.
77
+ fn push(&mut self, label: String, controls: &[usize], target: usize, gate: Gate) -> Result<()> {
56
78
  let control_mask = control_mask(self.num_qubits, controls, target)?;
79
+ check_finite(&gate)?;
57
80
  self.instructions.push(Instruction { control_mask, target, gate });
81
+ self.drawing.push(DrawOp { label, controls: controls.to_vec(), targets: vec![target] });
58
82
  Ok(())
59
83
  }
60
84
 
@@ -68,44 +92,46 @@ impl QuantumCircuit {
68
92
  }
69
93
 
70
94
  // Named gates. The first eleven match `QuantumBackend`; the rest are circuit-only.
71
- pub fn x(&mut self, q: usize) -> Result<()> { self.add_gate(&[], q, gates::X) }
72
- pub fn y(&mut self, q: usize) -> Result<()> { self.add_gate(&[], q, gates::Y) }
73
- pub fn z(&mut self, q: usize) -> Result<()> { self.add_gate(&[], q, gates::Z) }
74
- pub fn h(&mut self, q: usize) -> Result<()> { self.add_gate(&[], q, gates::H) }
75
- pub fn s(&mut self, q: usize) -> Result<()> { self.add_gate(&[], q, gates::S) }
76
- pub fn t(&mut self, q: usize) -> Result<()> { self.add_gate(&[], q, gates::T) }
77
- pub fn rx(&mut self, q: usize, theta: f64) -> Result<()> { self.add_gate(&[], q, gates::rx(theta)) }
78
- pub fn ry(&mut self, q: usize, theta: f64) -> Result<()> { self.add_gate(&[], q, gates::ry(theta)) }
79
- pub fn rz(&mut self, q: usize, theta: f64) -> Result<()> { self.add_gate(&[], q, gates::rz(theta)) }
95
+ pub fn x(&mut self, q: usize) -> Result<()> { self.push("X".into(), &[], q, gates::X) }
96
+ pub fn y(&mut self, q: usize) -> Result<()> { self.push("Y".into(), &[], q, gates::Y) }
97
+ pub fn z(&mut self, q: usize) -> Result<()> { self.push("Z".into(), &[], q, gates::Z) }
98
+ pub fn h(&mut self, q: usize) -> Result<()> { self.push("H".into(), &[], q, gates::H) }
99
+ pub fn s(&mut self, q: usize) -> Result<()> { self.push("S".into(), &[], q, gates::S) }
100
+ pub fn t(&mut self, q: usize) -> Result<()> { self.push("T".into(), &[], q, gates::T) }
101
+ pub fn rx(&mut self, q: usize, theta: f64) -> Result<()> { self.push(label("RX", &[theta]), &[], q, gates::rx(theta)) }
102
+ pub fn ry(&mut self, q: usize, theta: f64) -> Result<()> { self.push(label("RY", &[theta]), &[], q, gates::ry(theta)) }
103
+ pub fn rz(&mut self, q: usize, theta: f64) -> Result<()> { self.push(label("RZ", &[theta]), &[], q, gates::rz(theta)) }
80
104
  pub fn cx(&mut self, control: usize, target: usize) -> Result<()> {
81
- self.add_gate(&[control], target, gates::X)
105
+ self.push("X".into(), &[control], target, gates::X)
82
106
  }
83
107
  pub fn cz(&mut self, control: usize, target: usize) -> Result<()> {
84
- self.add_gate(&[control], target, gates::Z)
108
+ self.push("Z".into(), &[control], target, gates::Z)
85
109
  }
86
- pub fn sdg(&mut self, q: usize) -> Result<()> { self.add_gate(&[], q, gates::SDG) }
87
- pub fn tdg(&mut self, q: usize) -> Result<()> { self.add_gate(&[], q, gates::TDG) }
88
- pub fn p(&mut self, q: usize, lambda: f64) -> Result<()> { self.add_gate(&[], q, gates::p(lambda)) }
110
+ pub fn sdg(&mut self, q: usize) -> Result<()> { self.push("S†".into(), &[], q, gates::SDG) }
111
+ pub fn tdg(&mut self, q: usize) -> Result<()> { self.push("T†".into(), &[], q, gates::TDG) }
112
+ pub fn p(&mut self, q: usize, lambda: f64) -> Result<()> { self.push(label("P", &[lambda]), &[], q, gates::p(lambda)) }
89
113
  pub fn u(&mut self, q: usize, theta: f64, phi: f64, lambda: f64) -> Result<()> {
90
- self.add_gate(&[], q, gates::u(theta, phi, lambda))
114
+ self.push(label("U", &[theta, phi, lambda]), &[], q, gates::u(theta, phi, lambda))
91
115
  }
92
116
  pub fn cp(&mut self, control: usize, target: usize, lambda: f64) -> Result<()> {
93
- self.add_gate(&[control], target, gates::p(lambda))
117
+ self.push(label("P", &[lambda]), &[control], target, gates::p(lambda))
94
118
  }
95
119
  pub fn crz(&mut self, control: usize, target: usize, theta: f64) -> Result<()> {
96
- self.add_gate(&[control], target, gates::rz(theta))
120
+ self.push(label("RZ", &[theta]), &[control], target, gates::rz(theta))
97
121
  }
98
122
  /// Toffoli: flip `target` when both controls are |1>.
99
123
  pub fn ccx(&mut self, c1: usize, c2: usize, target: usize) -> Result<()> {
100
- self.add_gate(&[c1, c2], target, gates::X)
124
+ self.push("X".into(), &[c1, c2], target, gates::X)
101
125
  }
102
126
  /// Swap two qubits, as three CNOTs (the kernel only knows controlled 2x2 gates).
103
127
  pub fn swap(&mut self, a: usize, b: usize) -> Result<()> {
104
128
  // Validate first so a bad swap records nothing, not one or two stray CNOTs.
105
129
  control_mask(self.num_qubits, &[a], b)?;
106
- self.cx(a, b)?;
107
- self.cx(b, a)?;
108
- self.cx(a, b)
130
+ for (c, t) in [(a, b), (b, a), (a, b)] {
131
+ self.instructions.push(Instruction { control_mask: 1 << c, target: t, gate: gates::X });
132
+ }
133
+ self.drawing.push(DrawOp { label: "×".into(), controls: vec![], targets: vec![a, b] });
134
+ Ok(())
109
135
  }
110
136
 
111
137
  /// Apply every instruction to an existing backend, with single-qubit gate fusion.
@@ -165,7 +191,7 @@ impl QuantumCircuit {
165
191
 
166
192
  /// Final amplitudes after running the circuit.
167
193
  pub fn statevector(&self, kind: BackendKind) -> Result<Vec<Complex64>> {
168
- Ok(self.simulate(kind)?.amplitudes())
194
+ Ok(self.simulate(kind)?.into_amplitudes())
169
195
  }
170
196
 
171
197
  /// Final basis-state probabilities after running the circuit.
@@ -180,6 +206,51 @@ impl QuantumCircuit {
180
206
  }
181
207
  }
182
208
 
209
+ /// Text drawing, one wire per qubit (q0 on top), one column per gate:
210
+ ///
211
+ /// ```text
212
+ /// q0: ──H──●─
213
+ /// │
214
+ /// q1: ─────X─
215
+ /// ```
216
+ ///
217
+ /// `●` marks a control, `×` the two ends of a swap.
218
+ ///
219
+ /// shortcut: columns are never packed or folded, so a long circuit draws as one very wide line; add folding if learners hit it.
220
+ impl fmt::Display for QuantumCircuit {
221
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
222
+ let n = self.num_qubits;
223
+ let name_w = format!("q{}", n.saturating_sub(1)).len();
224
+ // Even rows are qubit wires, odd rows the gaps between them.
225
+ let mut rows: Vec<String> = (0..(2 * n).saturating_sub(1))
226
+ .map(|r| if r % 2 == 0 { format!("{:>name_w$}: ─", format!("q{}", r / 2)) } else { " ".repeat(name_w + 3) })
227
+ .collect();
228
+
229
+ for op in &self.drawing {
230
+ let w = op.label.chars().count() + 2;
231
+ let mid = w / 2;
232
+ // Rows from the gate's top qubit to its bottom one carry the vertical line.
233
+ let qubits = op.controls.iter().chain(&op.targets);
234
+ let (lo, hi) = (2 * qubits.clone().min().unwrap(), 2 * qubits.max().unwrap());
235
+ let mark = |c: char, fill: &str| format!("{}{c}{}", fill.repeat(mid), fill.repeat(w - mid - 1));
236
+ for (r, row) in rows.iter_mut().enumerate() {
237
+ let (q, wire, inside) = (r / 2, r % 2 == 0, lo <= r && r <= hi);
238
+ let cell = match (wire, inside) {
239
+ (true, _) if op.targets.contains(&q) => format!("─{}─", op.label),
240
+ (true, _) if op.controls.contains(&q) => mark('●', "─"),
241
+ (true, true) => mark('┼', "─"),
242
+ (true, false) => "─".repeat(w),
243
+ (false, true) => mark('│', " "),
244
+ (false, false) => " ".repeat(w),
245
+ };
246
+ row.push_str(&cell);
247
+ }
248
+ }
249
+ let lines: Vec<&str> = rows.iter().map(|r| r.trim_end()).collect();
250
+ f.write_str(&lines.join("\n"))
251
+ }
252
+ }
253
+
183
254
  #[cfg(test)]
184
255
  mod tests {
185
256
  use super::*;
@@ -239,6 +310,32 @@ mod tests {
239
310
  assert!(counts.keys().all(|&k| k == 0 || k == 3));
240
311
  }
241
312
 
313
+ #[test]
314
+ fn non_finite_angles_are_rejected() {
315
+ let mut qc = QuantumCircuit::new(2).unwrap();
316
+ assert_eq!(qc.rx(0, f64::NAN), Err(QsError::NonFinite));
317
+ assert_eq!(qc.cp(0, 1, f64::INFINITY), Err(QsError::NonFinite));
318
+ assert!(qc.instructions().is_empty());
319
+ }
320
+
321
+ #[test]
322
+ fn draws_gates_controls_and_swaps() {
323
+ let mut qc = QuantumCircuit::new(3).unwrap();
324
+ qc.h(0).unwrap();
325
+ qc.cx(0, 2).unwrap();
326
+ qc.rz(1, 0.5).unwrap();
327
+ qc.swap(0, 1).unwrap();
328
+ let expect = "\
329
+ q0: ──H──●────────────×─
330
+ │ │
331
+ q1: ─────┼──RZ(0.50)──×─
332
+ │
333
+ q2: ─────X──────────────";
334
+ assert_eq!(qc.to_string(), expect);
335
+ assert_eq!(qc.instructions().len(), 6); // the swap is still three CNOTs
336
+ assert_eq!(QuantumCircuit::new(1).unwrap().to_string(), "q0: ─");
337
+ }
338
+
242
339
  #[test]
243
340
  fn size_mismatch_is_an_error() {
244
341
  let qc = QuantumCircuit::new(2).unwrap();
@@ -35,7 +35,13 @@ impl QuantumState {
35
35
  if num_qubits > MAX_QUBITS {
36
36
  return Err(QsError::TooManyQubits { requested: num_qubits, max: MAX_QUBITS });
37
37
  }
38
- let mut amplitudes = vec![Complex64::new(0.0, 0.0); 1 << num_qubits];
38
+ // Reserve fallibly: a failed `vec!` aborts the process, which kills the Python interpreter.
39
+ let len = 1usize << num_qubits;
40
+ let mut amplitudes = Vec::new();
41
+ amplitudes
42
+ .try_reserve_exact(len)
43
+ .map_err(|_| QsError::OutOfMemory { bytes: len * size_of::<Complex64>() })?;
44
+ amplitudes.resize(len, Complex64::new(0.0, 0.0));
39
45
  amplitudes[0] = Complex64::new(1.0, 0.0);
40
46
  Ok(Self { num_qubits, amplitudes })
41
47
  }
@@ -113,6 +119,10 @@ impl QuantumBackend for QuantumState {
113
119
  self.amplitudes.clone()
114
120
  }
115
121
 
122
+ fn into_amplitudes(self: Box<Self>) -> Vec<Complex64> {
123
+ self.amplitudes
124
+ }
125
+
116
126
  fn probabilities(&self) -> Vec<f64> {
117
127
  // Override the default to skip the amplitude copy.
118
128
  self.amplitudes.par_iter().map(|a| a.norm_sqr()).collect()
@@ -33,7 +33,7 @@
33
33
  //! ## Python
34
34
  //!
35
35
  //! The `python` module wraps [`QuantumCircuit`] as `qsengine.QuantumCircuit`.
36
- //! Errors map from [`QsError`] to `ValueError` / `RuntimeError`. Build it with
36
+ //! Errors map from [`QsError`] to `ValueError` / `RuntimeError` / `MemoryError`. Build it with
37
37
  //! `maturin develop --release` (see the README).
38
38
 
39
39
  pub mod backend;
@@ -71,6 +71,10 @@ pub enum QsError {
71
71
  SizeMismatch { circuit: usize, backend: usize },
72
72
  /// A user-supplied gate matrix was not a 2x2 unitary.
73
73
  NotUnitary,
74
+ /// A gate angle (or matrix entry) was NaN or infinite.
75
+ NonFinite,
76
+ /// The state vector did not fit in memory.
77
+ OutOfMemory { bytes: usize },
74
78
  /// No usable GPU, or the GPU driver reported an error.
75
79
  Gpu(String),
76
80
  }
@@ -89,6 +93,10 @@ impl fmt::Display for QsError {
89
93
  write!(f, "circuit has {circuit} qubits but backend has {backend}")
90
94
  }
91
95
  QsError::NotUnitary => write!(f, "gate matrix must be a 2x2 unitary"),
96
+ QsError::NonFinite => write!(f, "gate angles must be finite numbers, not NaN or infinity"),
97
+ QsError::OutOfMemory { bytes } => {
98
+ write!(f, "not enough memory for the state vector ({:.1} GiB)", *bytes as f64 / (1u64 << 30) as f64)
99
+ }
92
100
  QsError::Gpu(msg) => write!(f, "GPU error: {msg}"),
93
101
  }
94
102
  }
@@ -11,8 +11,8 @@
11
11
 
12
12
  use crate::{BackendKind, QsError, QuantumCircuit, format_bitstring};
13
13
  use num_complex::Complex64;
14
- use numpy::{PyArray1, ToPyArray};
15
- use pyo3::exceptions::{PyRuntimeError, PyValueError};
14
+ use numpy::{IntoPyArray, PyArray1};
15
+ use pyo3::exceptions::{PyMemoryError, PyRuntimeError, PyValueError};
16
16
  use pyo3::prelude::*;
17
17
  use rand::SeedableRng;
18
18
  use rand::rngs::StdRng;
@@ -23,6 +23,7 @@ impl From<QsError> for PyErr {
23
23
  fn from(e: QsError) -> PyErr {
24
24
  match e {
25
25
  QsError::Gpu(_) => PyRuntimeError::new_err(e.to_string()),
26
+ QsError::OutOfMemory { .. } => PyMemoryError::new_err(e.to_string()),
26
27
  _ => PyValueError::new_err(e.to_string()),
27
28
  }
28
29
  }
@@ -68,6 +69,11 @@ impl PyQuantumCircuit {
68
69
  self.inner.instructions().len()
69
70
  }
70
71
 
72
+ /// Text drawing of the circuit, one wire per qubit: `print(qc)`.
73
+ fn __str__(&self) -> String {
74
+ self.inner.to_string()
75
+ }
76
+
71
77
  fn __repr__(&self) -> String {
72
78
  format!("QuantumCircuit(num_qubits={}, gates={})", self.inner.num_qubits(), self.__len__())
73
79
  }
@@ -156,7 +162,8 @@ impl PyQuantumCircuit {
156
162
  let kind = parse_backend(backend)?;
157
163
  let circuit = self.inner.clone();
158
164
  let amps = py.detach(move || circuit.statevector(kind))?;
159
- Ok(amps.to_pyarray(py))
165
+ // into_pyarray hands NumPy the Rust buffer itself, so no third copy of the state.
166
+ Ok(amps.into_pyarray(py))
160
167
  }
161
168
 
162
169
  /// Exact probability of each of the 2**n basis states after the circuit,
@@ -166,7 +173,7 @@ impl PyQuantumCircuit {
166
173
  let kind = parse_backend(backend)?;
167
174
  let circuit = self.inner.clone();
168
175
  let probs = py.detach(move || circuit.probabilities(kind))?;
169
- Ok(probs.to_pyarray(py))
176
+ Ok(probs.into_pyarray(py))
170
177
  }
171
178
  }
172
179
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes