qsengine 1.0.0__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.
@@ -0,0 +1 @@
1
+ site/** linguist-vendored
@@ -505,6 +505,16 @@ dependencies = [
505
505
  "libc",
506
506
  ]
507
507
 
508
+ [[package]]
509
+ name = "matrixmultiply"
510
+ version = "0.3.11"
511
+ source = "registry+https://github.com/rust-lang/crates.io-index"
512
+ checksum = "3f607c237553f086e7043417a51df26b2eb899d3caff94e6a67592ff992fedc7"
513
+ dependencies = [
514
+ "autocfg",
515
+ "rawpointer",
516
+ ]
517
+
508
518
  [[package]]
509
519
  name = "metal"
510
520
  version = "0.31.0"
@@ -538,13 +548,28 @@ dependencies = [
538
548
  "log",
539
549
  "num-traits",
540
550
  "once_cell",
541
- "rustc-hash",
551
+ "rustc-hash 1.1.0",
542
552
  "spirv",
543
553
  "strum",
544
554
  "thiserror 2.0.21",
545
555
  "unicode-ident",
546
556
  ]
547
557
 
558
+ [[package]]
559
+ name = "ndarray"
560
+ version = "0.17.2"
561
+ source = "registry+https://github.com/rust-lang/crates.io-index"
562
+ checksum = "520080814a7a6b4a6e9070823bb24b4531daac8c4627e08ba5de8c5ef2f2752d"
563
+ dependencies = [
564
+ "matrixmultiply",
565
+ "num-complex",
566
+ "num-integer",
567
+ "num-traits",
568
+ "portable-atomic",
569
+ "portable-atomic-util",
570
+ "rawpointer",
571
+ ]
572
+
548
573
  [[package]]
549
574
  name = "ndk-sys"
550
575
  version = "0.5.0+25.2.9519653"
@@ -563,6 +588,15 @@ dependencies = [
563
588
  "num-traits",
564
589
  ]
565
590
 
591
+ [[package]]
592
+ name = "num-integer"
593
+ version = "0.1.47"
594
+ source = "registry+https://github.com/rust-lang/crates.io-index"
595
+ checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b"
596
+ dependencies = [
597
+ "num-traits",
598
+ ]
599
+
566
600
  [[package]]
567
601
  name = "num-traits"
568
602
  version = "0.2.19"
@@ -573,6 +607,22 @@ dependencies = [
573
607
  "libm",
574
608
  ]
575
609
 
610
+ [[package]]
611
+ name = "numpy"
612
+ version = "0.30.0"
613
+ source = "registry+https://github.com/rust-lang/crates.io-index"
614
+ checksum = "aa0b9111761885449e894981fa8051dcf69e0bd114aecbd4bcdd830b6be4ed92"
615
+ dependencies = [
616
+ "libc",
617
+ "ndarray",
618
+ "num-complex",
619
+ "num-integer",
620
+ "num-traits",
621
+ "pyo3",
622
+ "pyo3-build-config",
623
+ "rustc-hash 2.1.3",
624
+ ]
625
+
576
626
  [[package]]
577
627
  name = "objc"
578
628
  version = "0.2.7"
@@ -650,6 +700,15 @@ version = "1.13.1"
650
700
  source = "registry+https://github.com/rust-lang/crates.io-index"
651
701
  checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
652
702
 
703
+ [[package]]
704
+ name = "portable-atomic-util"
705
+ version = "0.2.8"
706
+ source = "registry+https://github.com/rust-lang/crates.io-index"
707
+ checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715"
708
+ dependencies = [
709
+ "portable-atomic",
710
+ ]
711
+
653
712
  [[package]]
654
713
  name = "ppv-lite86"
655
714
  version = "0.2.21"
@@ -740,10 +799,11 @@ dependencies = [
740
799
 
741
800
  [[package]]
742
801
  name = "qsengine"
743
- version = "1.0.0"
802
+ version = "1.0.2"
744
803
  dependencies = [
745
804
  "bytemuck",
746
805
  "num-complex",
806
+ "numpy",
747
807
  "pollster",
748
808
  "pyo3",
749
809
  "rand",
@@ -807,6 +867,12 @@ version = "0.6.2"
807
867
  source = "registry+https://github.com/rust-lang/crates.io-index"
808
868
  checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
809
869
 
870
+ [[package]]
871
+ name = "rawpointer"
872
+ version = "0.2.1"
873
+ source = "registry+https://github.com/rust-lang/crates.io-index"
874
+ checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
875
+
810
876
  [[package]]
811
877
  name = "rayon"
812
878
  version = "1.12.0"
@@ -848,6 +914,12 @@ version = "1.1.0"
848
914
  source = "registry+https://github.com/rust-lang/crates.io-index"
849
915
  checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
850
916
 
917
+ [[package]]
918
+ name = "rustc-hash"
919
+ version = "2.1.3"
920
+ source = "registry+https://github.com/rust-lang/crates.io-index"
921
+ checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
922
+
851
923
  [[package]]
852
924
  name = "rustversion"
853
925
  version = "1.0.23"
@@ -1176,7 +1248,7 @@ dependencies = [
1176
1248
  "portable-atomic",
1177
1249
  "profiling",
1178
1250
  "raw-window-handle",
1179
- "rustc-hash",
1251
+ "rustc-hash 1.1.0",
1180
1252
  "smallvec",
1181
1253
  "thiserror 2.0.21",
1182
1254
  "wgpu-core-deps-apple",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "qsengine"
3
- version = "1.0.0"
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)"
@@ -23,7 +23,7 @@ default = ["gpu"]
23
23
  gpu = ["dep:wgpu", "dep:pollster", "dep:bytemuck"]
24
24
  # Python bindings. Off for plain `cargo build/test`; maturin turns it on
25
25
  # (see [tool.maturin] in pyproject.toml).
26
- python = ["dep:pyo3"]
26
+ python = ["dep:pyo3", "dep:numpy"]
27
27
 
28
28
  [dependencies]
29
29
  num-complex = "0.4.6"
@@ -36,3 +36,5 @@ bytemuck = { version = "1", features = ["derive"], optional = true }
36
36
  # abi3-py39: one compiled wheel works on every CPython from 3.9 up.
37
37
  # num-complex: amplitudes convert straight to Python `complex`.
38
38
  pyo3 = { version = "0.30", features = ["abi3-py39", "num-complex"], optional = true }
39
+ # statevector() / probabilities() return NumPy arrays without per-element Python objects.
40
+ numpy = { version = "0.30", optional = true }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qsengine
3
- Version: 1.0.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
@@ -14,8 +14,8 @@ Classifier: Programming Language :: Python :: 3 :: Only
14
14
  Classifier: Programming Language :: Python :: Implementation :: CPython
15
15
  Classifier: Topic :: Scientific/Engineering :: Physics
16
16
  Classifier: Typing :: Typed
17
+ Requires-Dist: numpy>=1.19
17
18
  Requires-Dist: pytest ; extra == 'test'
18
- Requires-Dist: numpy ; extra == 'test'
19
19
  Provides-Extra: test
20
20
  License-File: LICENSE
21
21
  Summary: A fast state-vector quantum simulator for Python, with a Rust core that runs on any CPU or GPU
@@ -58,7 +58,7 @@ print(qc.run(shots=1000, seed=1)) # {'00': 513, '11': 487}
58
58
  - **Fast where it counts.** Small circuits run on the CPU with almost no per-call overhead; from 14 qubits up, the GPU takes over. On a 22-qubit benchmark qsengine's GPU backend is about 5 times faster than Qiskit Aer.
59
59
  - **Any GPU, nothing to install.** The GPU backend uses [wgpu](https://wgpu.rs), so it runs on NVIDIA, AMD, Intel and Apple GPUs through Vulkan, DirectX 12 or Metal. No CUDA, no vendor toolkit.
60
60
  - **Works everywhere.** Without a usable GPU the same code runs on the CPU, in full 64-bit precision.
61
- - **Small, plain API.** One class, eleven gates, three ways to get a result. Mistakes raise a normal Python `ValueError` on the line that made them.
61
+ - **Small, plain API.** One class, nineteen gates plus any 2x2 unitary, three ways to get a result. Mistakes raise a normal Python `ValueError` on the line that made them.
62
62
  - **Built to learn with.** The website walks from bra-ket notation to a complete Deutsch-Jozsa run, and every example on it is real qsengine output.
63
63
 
64
64
  ## Install
@@ -81,16 +81,19 @@ 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
- qc.probabilities() # exact probabilities of all 8 basis states
86
- qc.statevector() # exact complex amplitudes
86
+ qc.probabilities() # exact probabilities of all 8 basis states (NumPy array)
87
+ qc.statevector() # exact complex amplitudes (NumPy array)
87
88
  ```
88
89
 
89
90
  | Gates | Methods |
90
91
  | --- | --- |
91
- | Single qubit | `x`, `y`, `z`, `h`, `s`, `t` |
92
- | Rotations (radians) | `rx(q, theta)`, `ry(q, theta)`, `rz(q, theta)` |
93
- | Two qubit | `cx(control, target)`, `cz(control, target)` |
92
+ | Single qubit | `x`, `y`, `z`, `h`, `s`, `sdg`, `t`, `tdg` |
93
+ | Angles (radians) | `rx(q, theta)`, `ry(q, theta)`, `rz(q, theta)`, `p(q, lam)`, `u(q, theta, phi, lam)` |
94
+ | Two qubit | `cx(control, target)`, `cz(control, target)`, `cp(control, target, lam)`, `crz(control, target, theta)`, `swap(q1, q2)` |
95
+ | Three qubit | `ccx(control1, control2, target)` |
96
+ | Any 2x2 unitary | `unitary(matrix, target, controls=[])` |
94
97
 
95
98
  Bitstrings put **qubit 0 on the right**, the same convention as Qiskit. Pass `seed` to `run()` for repeatable counts.
96
99
 
@@ -124,7 +127,7 @@ Below 14 qubits the CPU backend is 7 to 60 times faster than Aer; from 14 qubits
124
127
 
125
128
  ## Limitations
126
129
 
127
- Version 1.0 measures every qubit at the end of the circuit. There is no SWAP gate and no mid-circuit measurement yet, so algorithms such as teleportation that branch on a measurement are not possible. A register holds at most 30 qubits (16 GiB of memory).
130
+ Version 1.0 measures every qubit at the end of the circuit. There is no mid-circuit measurement yet, so algorithms such as teleportation that branch on a measurement are not possible. A register holds at most 30 qubits (16 GiB of memory).
128
131
 
129
132
  ## Building from source
130
133
 
@@ -157,6 +160,8 @@ npm run dev # local preview
157
160
  npm run build # static site in site/out/
158
161
  ```
159
162
 
163
+ Docs pages are Markdown (`site/app/docs/**/page.mdx`) with `$maths$`, tables and fenced code; `<Output printed="..." />` shows what a snippet prints. To add a page, create its `page.mdx` and list it in `DOCS` in `site/app/content.js`, which drives the sidebar and the previous/next links.
164
+
160
165
  ## Project layout
161
166
 
162
167
  ```text
@@ -25,7 +25,7 @@ print(qc.run(shots=1000, seed=1)) # {'00': 513, '11': 487}
25
25
  - **Fast where it counts.** Small circuits run on the CPU with almost no per-call overhead; from 14 qubits up, the GPU takes over. On a 22-qubit benchmark qsengine's GPU backend is about 5 times faster than Qiskit Aer.
26
26
  - **Any GPU, nothing to install.** The GPU backend uses [wgpu](https://wgpu.rs), so it runs on NVIDIA, AMD, Intel and Apple GPUs through Vulkan, DirectX 12 or Metal. No CUDA, no vendor toolkit.
27
27
  - **Works everywhere.** Without a usable GPU the same code runs on the CPU, in full 64-bit precision.
28
- - **Small, plain API.** One class, eleven gates, three ways to get a result. Mistakes raise a normal Python `ValueError` on the line that made them.
28
+ - **Small, plain API.** One class, nineteen gates plus any 2x2 unitary, three ways to get a result. Mistakes raise a normal Python `ValueError` on the line that made them.
29
29
  - **Built to learn with.** The website walks from bra-ket notation to a complete Deutsch-Jozsa run, and every example on it is real qsengine output.
30
30
 
31
31
  ## Install
@@ -48,16 +48,19 @@ 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
- qc.probabilities() # exact probabilities of all 8 basis states
53
- qc.statevector() # exact complex amplitudes
53
+ qc.probabilities() # exact probabilities of all 8 basis states (NumPy array)
54
+ qc.statevector() # exact complex amplitudes (NumPy array)
54
55
  ```
55
56
 
56
57
  | Gates | Methods |
57
58
  | --- | --- |
58
- | Single qubit | `x`, `y`, `z`, `h`, `s`, `t` |
59
- | Rotations (radians) | `rx(q, theta)`, `ry(q, theta)`, `rz(q, theta)` |
60
- | Two qubit | `cx(control, target)`, `cz(control, target)` |
59
+ | Single qubit | `x`, `y`, `z`, `h`, `s`, `sdg`, `t`, `tdg` |
60
+ | Angles (radians) | `rx(q, theta)`, `ry(q, theta)`, `rz(q, theta)`, `p(q, lam)`, `u(q, theta, phi, lam)` |
61
+ | Two qubit | `cx(control, target)`, `cz(control, target)`, `cp(control, target, lam)`, `crz(control, target, theta)`, `swap(q1, q2)` |
62
+ | Three qubit | `ccx(control1, control2, target)` |
63
+ | Any 2x2 unitary | `unitary(matrix, target, controls=[])` |
61
64
 
62
65
  Bitstrings put **qubit 0 on the right**, the same convention as Qiskit. Pass `seed` to `run()` for repeatable counts.
63
66
 
@@ -91,7 +94,7 @@ Below 14 qubits the CPU backend is 7 to 60 times faster than Aer; from 14 qubits
91
94
 
92
95
  ## Limitations
93
96
 
94
- Version 1.0 measures every qubit at the end of the circuit. There is no SWAP gate and no mid-circuit measurement yet, so algorithms such as teleportation that branch on a measurement are not possible. A register holds at most 30 qubits (16 GiB of memory).
97
+ Version 1.0 measures every qubit at the end of the circuit. There is no mid-circuit measurement yet, so algorithms such as teleportation that branch on a measurement are not possible. A register holds at most 30 qubits (16 GiB of memory).
95
98
 
96
99
  ## Building from source
97
100
 
@@ -124,6 +127,8 @@ npm run dev # local preview
124
127
  npm run build # static site in site/out/
125
128
  ```
126
129
 
130
+ Docs pages are Markdown (`site/app/docs/**/page.mdx`) with `$maths$`, tables and fenced code; `<Output printed="..." />` shows what a snippet prints. To add a page, create its `page.mdx` and list it in `DOCS` in `site/app/content.js`, which drives the sidebar and the previous/next links.
131
+
127
132
  ## Project layout
128
133
 
129
134
  ```text
@@ -8,6 +8,7 @@ description = "A fast state-vector quantum simulator for Python, with a Rust cor
8
8
  readme = "README.md"
9
9
  requires-python = ">=3.9"
10
10
  license = "MIT"
11
+ dependencies = ["numpy>=1.19"]
11
12
  license-files = ["LICENSE"]
12
13
  authors = [{ name = "kaiwave" }]
13
14
  keywords = ["quantum", "quantum computing", "simulator", "statevector", "qubit", "gpu", "rust"]
@@ -36,7 +37,7 @@ Issues = "https://github.com/kaiwave/qsengine/issues"
36
37
  "Release notes" = "https://qsengine.kaiwave.dev/docs/release-notes/"
37
38
 
38
39
  [project.optional-dependencies]
39
- test = ["pytest", "numpy"]
40
+ test = ["pytest"]
40
41
 
41
42
  [tool.maturin]
42
43
  # Build the Rust crate with its Python bindings (and the default GPU backend).
@@ -4,7 +4,10 @@ Bit order: qubit 0 is the rightmost character of every bitstring and bit 0 of
4
4
  every basis index, the same convention as Qiskit.
5
5
  """
6
6
 
7
- from typing import Literal, Optional
7
+ from typing import Literal, Optional, Sequence, Union
8
+
9
+ import numpy as np
10
+ import numpy.typing as npt
8
11
 
9
12
  __version__: str
10
13
 
@@ -18,6 +21,8 @@ class QuantumCircuit:
18
21
  def num_qubits(self) -> int: ...
19
22
  def __len__(self) -> int:
20
23
  """Number of gates recorded so far."""
24
+ def __str__(self) -> str:
25
+ """Text drawing of the circuit, one wire per qubit: `print(qc)`."""
21
26
 
22
27
  # Single-qubit gates
23
28
  def x(self, qubit: int) -> None: ...
@@ -29,17 +34,36 @@ class QuantumCircuit:
29
34
  def rx(self, qubit: int, theta: float) -> None: ...
30
35
  def ry(self, qubit: int, theta: float) -> None: ...
31
36
  def rz(self, qubit: int, theta: float) -> None: ...
37
+ def sdg(self, qubit: int) -> None: ...
38
+ def tdg(self, qubit: int) -> None: ...
39
+ def p(self, qubit: int, lam: float) -> None: ...
40
+ def u(self, qubit: int, theta: float, phi: float, lam: float) -> None: ...
32
41
 
33
42
  # Two-qubit gates
34
43
  def cx(self, control: int, target: int) -> None: ...
35
44
  def cz(self, control: int, target: int) -> None: ...
45
+ def cp(self, control: int, target: int, lam: float) -> None: ...
46
+ def crz(self, control: int, target: int, theta: float) -> None: ...
47
+ def swap(self, qubit1: int, qubit2: int) -> None: ...
48
+
49
+ # Three-qubit gates
50
+ def ccx(self, control1: int, control2: int, target: int) -> None: ...
51
+
52
+ # Any 2x2 unitary, optionally controlled
53
+ def unitary(
54
+ self,
55
+ matrix: Union[Sequence[Sequence[complex]], npt.ArrayLike],
56
+ target: int,
57
+ controls: Sequence[int] = ...,
58
+ ) -> None:
59
+ """Apply a 2x2 unitary to `target`; raises ValueError if it is not unitary."""
36
60
 
37
61
  # Execution
38
62
  def run(
39
63
  self, shots: int = 1024, seed: Optional[int] = None, backend: Backend = "auto"
40
64
  ) -> dict[str, int]:
41
65
  """Measure all qubits `shots` times; returns {bitstring: count}."""
42
- def statevector(self, backend: Backend = "auto") -> list[complex]:
66
+ def statevector(self, backend: Backend = "auto") -> npt.NDArray[np.complex128]:
43
67
  """Exact final amplitudes, length 2**num_qubits."""
44
- def probabilities(self, backend: Backend = "auto") -> list[float]:
68
+ def probabilities(self, backend: Backend = "auto") -> npt.NDArray[np.float64]:
45
69
  """Exact final basis-state probabilities, length 2**num_qubits."""
@@ -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,26 +70,68 @@ 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
 
61
- // Named gates: same names and meaning as on `QuantumBackend`.
62
- pub fn x(&mut self, q: usize) -> Result<()> { self.add_gate(&[], q, gates::X) }
63
- pub fn y(&mut self, q: usize) -> Result<()> { self.add_gate(&[], q, gates::Y) }
64
- pub fn z(&mut self, q: usize) -> Result<()> { self.add_gate(&[], q, gates::Z) }
65
- pub fn h(&mut self, q: usize) -> Result<()> { self.add_gate(&[], q, gates::H) }
66
- pub fn s(&mut self, q: usize) -> Result<()> { self.add_gate(&[], q, gates::S) }
67
- pub fn t(&mut self, q: usize) -> Result<()> { self.add_gate(&[], q, gates::T) }
68
- pub fn rx(&mut self, q: usize, theta: f64) -> Result<()> { self.add_gate(&[], q, gates::rx(theta)) }
69
- pub fn ry(&mut self, q: usize, theta: f64) -> Result<()> { self.add_gate(&[], q, gates::ry(theta)) }
70
- pub fn rz(&mut self, q: usize, theta: f64) -> Result<()> { self.add_gate(&[], q, gates::rz(theta)) }
85
+ /// Record a user-supplied 2x2 `gate`, after checking it is unitary.
86
+ /// [`add_gate`](Self::add_gate) trusts its matrix; this is the entry point for untrusted input.
87
+ pub fn unitary(&mut self, controls: &[usize], target: usize, gate: Gate) -> Result<()> {
88
+ if !gates::is_unitary(&gate, 1e-9) {
89
+ return Err(QsError::NotUnitary);
90
+ }
91
+ self.add_gate(controls, target, gate)
92
+ }
93
+
94
+ // Named gates. The first eleven match `QuantumBackend`; the rest are circuit-only.
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)) }
71
104
  pub fn cx(&mut self, control: usize, target: usize) -> Result<()> {
72
- self.add_gate(&[control], target, gates::X)
105
+ self.push("X".into(), &[control], target, gates::X)
73
106
  }
74
107
  pub fn cz(&mut self, control: usize, target: usize) -> Result<()> {
75
- self.add_gate(&[control], target, gates::Z)
108
+ self.push("Z".into(), &[control], target, gates::Z)
109
+ }
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)) }
113
+ pub fn u(&mut self, q: usize, theta: f64, phi: f64, lambda: f64) -> Result<()> {
114
+ self.push(label("U", &[theta, phi, lambda]), &[], q, gates::u(theta, phi, lambda))
115
+ }
116
+ pub fn cp(&mut self, control: usize, target: usize, lambda: f64) -> Result<()> {
117
+ self.push(label("P", &[lambda]), &[control], target, gates::p(lambda))
118
+ }
119
+ pub fn crz(&mut self, control: usize, target: usize, theta: f64) -> Result<()> {
120
+ self.push(label("RZ", &[theta]), &[control], target, gates::rz(theta))
121
+ }
122
+ /// Toffoli: flip `target` when both controls are |1>.
123
+ pub fn ccx(&mut self, c1: usize, c2: usize, target: usize) -> Result<()> {
124
+ self.push("X".into(), &[c1, c2], target, gates::X)
125
+ }
126
+ /// Swap two qubits, as three CNOTs (the kernel only knows controlled 2x2 gates).
127
+ pub fn swap(&mut self, a: usize, b: usize) -> Result<()> {
128
+ // Validate first so a bad swap records nothing, not one or two stray CNOTs.
129
+ control_mask(self.num_qubits, &[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(())
76
135
  }
77
136
 
78
137
  /// Apply every instruction to an existing backend, with single-qubit gate fusion.
@@ -132,7 +191,7 @@ impl QuantumCircuit {
132
191
 
133
192
  /// Final amplitudes after running the circuit.
134
193
  pub fn statevector(&self, kind: BackendKind) -> Result<Vec<Complex64>> {
135
- Ok(self.simulate(kind)?.amplitudes())
194
+ Ok(self.simulate(kind)?.into_amplitudes())
136
195
  }
137
196
 
138
197
  /// Final basis-state probabilities after running the circuit.
@@ -147,6 +206,51 @@ impl QuantumCircuit {
147
206
  }
148
207
  }
149
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
+
150
254
  #[cfg(test)]
151
255
  mod tests {
152
256
  use super::*;
@@ -190,6 +294,9 @@ mod tests {
190
294
  let mut qc = QuantumCircuit::new(2).unwrap();
191
295
  assert_eq!(qc.h(2), Err(QsError::QubitOutOfRange { qubit: 2, num_qubits: 2 }));
192
296
  assert_eq!(qc.cx(0, 0), Err(QsError::DuplicateQubit(0)));
297
+ assert_eq!(qc.swap(1, 1), Err(QsError::DuplicateQubit(1)));
298
+ assert_eq!(qc.swap(0, 5), Err(QsError::QubitOutOfRange { qubit: 5, num_qubits: 2 }));
299
+ assert_eq!(qc.unitary(&[], 0, [[gates::H[0][0]; 2]; 2]), Err(QsError::NotUnitary));
193
300
  assert!(qc.instructions().is_empty());
194
301
  }
195
302
 
@@ -203,6 +310,32 @@ mod tests {
203
310
  assert!(counts.keys().all(|&k| k == 0 || k == 3));
204
311
  }
205
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
+
206
339
  #[test]
207
340
  fn size_mismatch_is_an_error() {
208
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()
@@ -51,6 +51,37 @@ pub const S: Gate = [[I, O], [O, c(0.0, 1.0)]];
51
51
  /// T (fourth root of Z): |1> -> e^(i*pi/4)|1>.
52
52
  pub const T: Gate = [[I, O], [O, c(FRAC_1_SQRT_2, FRAC_1_SQRT_2)]];
53
53
 
54
+ /// S-dagger, the inverse of S: |1> -> -i|1>.
55
+ pub const SDG: Gate = [[I, O], [O, c(0.0, -1.0)]];
56
+
57
+ /// T-dagger, the inverse of T: |1> -> e^(-i*pi/4)|1>.
58
+ pub const TDG: Gate = [[I, O], [O, c(FRAC_1_SQRT_2, -FRAC_1_SQRT_2)]];
59
+
60
+ /// Phase gate: |1> -> e^(i*lambda)|1>. `p(pi)` is Z, `p(pi/2)` is S.
61
+ pub fn p(lambda: f64) -> Gate {
62
+ [[I, O], [O, Complex64::from_polar(1.0, lambda)]]
63
+ }
64
+
65
+ /// General single-qubit gate U(theta, phi, lambda), the same convention as Qiskit's `UGate`.
66
+ pub fn u(theta: f64, phi: f64, lambda: f64) -> Gate {
67
+ let (s, co) = (theta / 2.0).sin_cos();
68
+ [
69
+ [c(co, 0.0), -Complex64::from_polar(s, lambda)],
70
+ [Complex64::from_polar(s, phi), Complex64::from_polar(co, phi + lambda)],
71
+ ]
72
+ }
73
+
74
+ /// True if `g` is unitary (`g * g^dagger = I`) to within `tol`.
75
+ pub fn is_unitary(g: &Gate, tol: f64) -> bool {
76
+ (0..2).all(|r| {
77
+ (0..2).all(|col| {
78
+ let sum = g[r][0] * g[col][0].conj() + g[r][1] * g[col][1].conj();
79
+ let expect = if r == col { I } else { O };
80
+ (sum - expect).norm() < tol
81
+ })
82
+ })
83
+ }
84
+
54
85
  /// Rotation about the X axis of the Bloch sphere by `theta` radians.
55
86
  pub fn rx(theta: f64) -> Gate {
56
87
  let (s, co) = (theta / 2.0).sin_cos();
@@ -85,21 +116,20 @@ pub fn matmul(a: &Gate, b: &Gate) -> Gate {
85
116
  mod tests {
86
117
  use super::*;
87
118
 
88
- /// U * U^dagger should be the identity for every gate.
89
- fn assert_unitary(g: &Gate) {
90
- for r in 0..2 {
91
- for col in 0..2 {
92
- let sum = g[r][0] * g[col][0].conj() + g[r][1] * g[col][1].conj();
93
- let expect = if r == col { 1.0 } else { 0.0 };
94
- assert!((sum - c(expect, 0.0)).norm() < 1e-12, "not unitary: {g:?}");
95
- }
119
+ #[test]
120
+ fn all_gates_are_unitary() {
121
+ for g in [ID, X, Y, Z, H, S, T, SDG, TDG, rx(0.3), ry(1.1), rz(-2.0), p(0.7), u(0.4, -1.3, 2.2)] {
122
+ assert!(is_unitary(&g, 1e-12), "not unitary: {g:?}");
96
123
  }
124
+ assert!(!is_unitary(&[[I, I], [O, I]], 1e-9));
97
125
  }
98
126
 
99
127
  #[test]
100
- fn all_gates_are_unitary() {
101
- for g in [ID, X, Y, Z, H, S, T, rx(0.3), ry(1.1), rz(-2.0)] {
102
- assert_unitary(&g);
103
- }
128
+ fn named_identities() {
129
+ let close = |a: &Gate, b: &Gate| (0..2).all(|r| (0..2).all(|k| (a[r][k] - b[r][k]).norm() < 1e-12));
130
+ assert!(close(&matmul(&S, &SDG), &ID));
131
+ assert!(close(&matmul(&T, &TDG), &ID));
132
+ assert!(close(&p(std::f64::consts::FRAC_PI_2), &S));
133
+ assert!(close(&u(std::f64::consts::FRAC_PI_2, 0.0, std::f64::consts::PI), &H));
104
134
  }
105
135
  }
@@ -10,7 +10,7 @@
10
10
  //!
11
11
  //! | Module | Purpose |
12
12
  //! |-------------|----------------------------------------------------------------|
13
- //! | [`gates`] | 2x2 gate matrices (X, Y, Z, H, S, T, rotations) |
13
+ //! | [`gates`] | 2x2 gate matrices (Paulis, H, S, T, phase, U, rotations) |
14
14
  //! | [`backend`] | The [`QuantumBackend`] trait: the one API every simulator obeys |
15
15
  //! | [`circuit`] | [`QuantumCircuit`]: record gates now, run them later |
16
16
  //! | [`engine`] | [`QuantumState`]: multithreaded CPU backend (f64 precision) |
@@ -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;
@@ -69,6 +69,12 @@ pub enum QsError {
69
69
  DuplicateQubit(usize),
70
70
  /// A circuit was applied to a backend with a different number of qubits.
71
71
  SizeMismatch { circuit: usize, backend: usize },
72
+ /// A user-supplied gate matrix was not a 2x2 unitary.
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 },
72
78
  /// No usable GPU, or the GPU driver reported an error.
73
79
  Gpu(String),
74
80
  }
@@ -86,6 +92,11 @@ impl fmt::Display for QsError {
86
92
  QsError::SizeMismatch { circuit, backend } => {
87
93
  write!(f, "circuit has {circuit} qubits but backend has {backend}")
88
94
  }
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
+ }
89
100
  QsError::Gpu(msg) => write!(f, "GPU error: {msg}"),
90
101
  }
91
102
  }
@@ -11,7 +11,8 @@
11
11
 
12
12
  use crate::{BackendKind, QsError, QuantumCircuit, format_bitstring};
13
13
  use num_complex::Complex64;
14
- use pyo3::exceptions::{PyRuntimeError, PyValueError};
14
+ use numpy::{IntoPyArray, PyArray1};
15
+ use pyo3::exceptions::{PyMemoryError, PyRuntimeError, PyValueError};
15
16
  use pyo3::prelude::*;
16
17
  use rand::SeedableRng;
17
18
  use rand::rngs::StdRng;
@@ -22,6 +23,7 @@ impl From<QsError> for PyErr {
22
23
  fn from(e: QsError) -> PyErr {
23
24
  match e {
24
25
  QsError::Gpu(_) => PyRuntimeError::new_err(e.to_string()),
26
+ QsError::OutOfMemory { .. } => PyMemoryError::new_err(e.to_string()),
25
27
  _ => PyValueError::new_err(e.to_string()),
26
28
  }
27
29
  }
@@ -67,6 +69,11 @@ impl PyQuantumCircuit {
67
69
  self.inner.instructions().len()
68
70
  }
69
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
+
70
77
  fn __repr__(&self) -> String {
71
78
  format!("QuantumCircuit(num_qubits={}, gates={})", self.inner.num_qubits(), self.__len__())
72
79
  }
@@ -93,6 +100,39 @@ impl PyQuantumCircuit {
93
100
  fn cx(&mut self, control: usize, target: usize) -> PyResult<()> { Ok(self.inner.cx(control, target)?) }
94
101
  /// Controlled-Z: flip the phase of |11> on `control`, `target`.
95
102
  fn cz(&mut self, control: usize, target: usize) -> PyResult<()> { Ok(self.inner.cz(control, target)?) }
103
+ /// S-dagger (inverse of S) on `qubit`.
104
+ fn sdg(&mut self, qubit: usize) -> PyResult<()> { Ok(self.inner.sdg(qubit)?) }
105
+ /// T-dagger (inverse of T) on `qubit`.
106
+ fn tdg(&mut self, qubit: usize) -> PyResult<()> { Ok(self.inner.tdg(qubit)?) }
107
+ /// Phase gate: multiply the |1> amplitude of `qubit` by e^(i*lam).
108
+ fn p(&mut self, qubit: usize, lam: f64) -> PyResult<()> { Ok(self.inner.p(qubit, lam)?) }
109
+ /// General single-qubit gate U(theta, phi, lam), the same convention as Qiskit.
110
+ fn u(&mut self, qubit: usize, theta: f64, phi: f64, lam: f64) -> PyResult<()> {
111
+ Ok(self.inner.u(qubit, theta, phi, lam)?)
112
+ }
113
+ /// Controlled phase: multiply the |11> amplitude of `control`, `target` by e^(i*lam).
114
+ fn cp(&mut self, control: usize, target: usize, lam: f64) -> PyResult<()> { Ok(self.inner.cp(control, target, lam)?) }
115
+ /// Controlled RZ: rotate `target` about Z by `theta` when `control` is |1>.
116
+ fn crz(&mut self, control: usize, target: usize, theta: f64) -> PyResult<()> {
117
+ Ok(self.inner.crz(control, target, theta)?)
118
+ }
119
+ /// Toffoli: flip `target` when `control1` and `control2` are both |1>.
120
+ fn ccx(&mut self, control1: usize, control2: usize, target: usize) -> PyResult<()> {
121
+ Ok(self.inner.ccx(control1, control2, target)?)
122
+ }
123
+ /// Swap the states of `qubit1` and `qubit2` (recorded as three CNOTs).
124
+ fn swap(&mut self, qubit1: usize, qubit2: usize) -> PyResult<()> { Ok(self.inner.swap(qubit1, qubit2)?) }
125
+
126
+ /// Apply any 2x2 unitary `matrix` (nested lists or a NumPy array) to `target`,
127
+ /// optionally only where every qubit in `controls` is |1>.
128
+ #[pyo3(signature = (matrix, target, controls = Vec::new()))]
129
+ fn unitary(&mut self, matrix: Vec<Vec<Complex64>>, target: usize, controls: Vec<usize>) -> PyResult<()> {
130
+ let gate = match matrix.as_slice() {
131
+ [r0, r1] if r0.len() == 2 && r1.len() == 2 => [[r0[0], r0[1]], [r1[0], r1[1]]],
132
+ _ => return Err(PyValueError::new_err("matrix must be 2x2")),
133
+ };
134
+ Ok(self.inner.unitary(&controls, target, gate)?)
135
+ }
96
136
 
97
137
  /// Simulate the circuit and measure every qubit `shots` times.
98
138
  ///
@@ -115,21 +155,25 @@ impl PyQuantumCircuit {
115
155
  Ok(counts.into_iter().map(|(i, c)| (format_bitstring(i, n), c)).collect())
116
156
  }
117
157
 
118
- /// Exact final state as a list of 2**n complex amplitudes, indexed by basis
119
- /// state (wrap with `numpy.array(...)` for vector maths).
158
+ /// Exact final state as a NumPy array of 2**n complex128 amplitudes,
159
+ /// indexed by basis state.
120
160
  #[pyo3(signature = (backend = "auto"))]
121
- fn statevector(&self, py: Python<'_>, backend: &str) -> PyResult<Vec<Complex64>> {
161
+ fn statevector<'py>(&self, py: Python<'py>, backend: &str) -> PyResult<Bound<'py, PyArray1<Complex64>>> {
122
162
  let kind = parse_backend(backend)?;
123
163
  let circuit = self.inner.clone();
124
- Ok(py.detach(move || circuit.statevector(kind))?)
164
+ let amps = py.detach(move || circuit.statevector(kind))?;
165
+ // into_pyarray hands NumPy the Rust buffer itself, so no third copy of the state.
166
+ Ok(amps.into_pyarray(py))
125
167
  }
126
168
 
127
- /// Exact probability of each of the 2**n basis states after the circuit.
169
+ /// Exact probability of each of the 2**n basis states after the circuit,
170
+ /// as a NumPy float64 array.
128
171
  #[pyo3(signature = (backend = "auto"))]
129
- fn probabilities(&self, py: Python<'_>, backend: &str) -> PyResult<Vec<f64>> {
172
+ fn probabilities<'py>(&self, py: Python<'py>, backend: &str) -> PyResult<Bound<'py, PyArray1<f64>>> {
130
173
  let kind = parse_backend(backend)?;
131
174
  let circuit = self.inner.clone();
132
- Ok(py.detach(move || circuit.probabilities(kind))?)
175
+ let probs = py.detach(move || circuit.probabilities(kind))?;
176
+ Ok(probs.into_pyarray(py))
133
177
  }
134
178
  }
135
179
 
File without changes
File without changes
File without changes
File without changes