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.
- {qsengine-1.0.1 → qsengine-1.0.2}/Cargo.lock +1 -1
- {qsengine-1.0.1 → qsengine-1.0.2}/Cargo.toml +1 -1
- {qsengine-1.0.1 → qsengine-1.0.2}/PKG-INFO +2 -1
- {qsengine-1.0.1 → qsengine-1.0.2}/README.md +1 -0
- {qsengine-1.0.1 → qsengine-1.0.2}/qsengine.pyi +2 -0
- {qsengine-1.0.1 → qsengine-1.0.2}/src/backend.rs +24 -10
- {qsengine-1.0.1 → qsengine-1.0.2}/src/circuit.rs +121 -24
- {qsengine-1.0.1 → qsengine-1.0.2}/src/engine.rs +11 -1
- {qsengine-1.0.1 → qsengine-1.0.2}/src/lib.rs +9 -1
- {qsengine-1.0.1 → qsengine-1.0.2}/src/python.rs +11 -4
- {qsengine-1.0.1 → qsengine-1.0.2}/.gitattributes +0 -0
- {qsengine-1.0.1 → qsengine-1.0.2}/.gitignore +0 -0
- {qsengine-1.0.1 → qsengine-1.0.2}/LICENSE +0 -0
- {qsengine-1.0.1 → qsengine-1.0.2}/pyproject.toml +0 -0
- {qsengine-1.0.1 → qsengine-1.0.2}/src/gates.rs +0 -0
- {qsengine-1.0.1 → qsengine-1.0.2}/src/gpu.rs +0 -0
- {qsengine-1.0.1 → qsengine-1.0.2}/src/main.rs +0 -0
- {qsengine-1.0.1 → qsengine-1.0.2}/src/shaders/apply_gate.wgsl +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: qsengine
|
|
3
|
-
Version: 1.0.
|
|
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(
|
|
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(
|
|
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
|
-
///
|
|
169
|
-
|
|
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.
|
|
72
|
-
pub fn y(&mut self, q: usize) -> Result<()> { self.
|
|
73
|
-
pub fn z(&mut self, q: usize) -> Result<()> { self.
|
|
74
|
-
pub fn h(&mut self, q: usize) -> Result<()> { self.
|
|
75
|
-
pub fn s(&mut self, q: usize) -> Result<()> { self.
|
|
76
|
-
pub fn t(&mut self, q: usize) -> Result<()> { self.
|
|
77
|
-
pub fn rx(&mut self, q: usize, theta: f64) -> Result<()> { self.
|
|
78
|
-
pub fn ry(&mut self, q: usize, theta: f64) -> Result<()> { self.
|
|
79
|
-
pub fn rz(&mut self, q: usize, theta: f64) -> Result<()> { self.
|
|
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.
|
|
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.
|
|
108
|
+
self.push("Z".into(), &[control], target, gates::Z)
|
|
85
109
|
}
|
|
86
|
-
pub fn sdg(&mut self, q: usize) -> Result<()> { self.
|
|
87
|
-
pub fn tdg(&mut self, q: usize) -> Result<()> { self.
|
|
88
|
-
pub fn p(&mut self, q: usize, lambda: f64) -> Result<()> { self.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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)?.
|
|
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
|
-
|
|
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::{
|
|
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
|
-
|
|
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.
|
|
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
|
|
File without changes
|