qsengine 1.0.0__tar.gz → 1.0.1__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.1"
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.1"
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.1
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
@@ -82,15 +82,17 @@ qc.cx(0, 1)
82
82
  qc.cx(1, 2)
83
83
 
84
84
  qc.run(shots=1000) # measurement counts: {'000': ..., '111': ...}
85
- qc.probabilities() # exact probabilities of all 8 basis states
86
- qc.statevector() # exact complex amplitudes
85
+ qc.probabilities() # exact probabilities of all 8 basis states (NumPy array)
86
+ qc.statevector() # exact complex amplitudes (NumPy array)
87
87
  ```
88
88
 
89
89
  | Gates | Methods |
90
90
  | --- | --- |
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)` |
91
+ | Single qubit | `x`, `y`, `z`, `h`, `s`, `sdg`, `t`, `tdg` |
92
+ | Angles (radians) | `rx(q, theta)`, `ry(q, theta)`, `rz(q, theta)`, `p(q, lam)`, `u(q, theta, phi, lam)` |
93
+ | Two qubit | `cx(control, target)`, `cz(control, target)`, `cp(control, target, lam)`, `crz(control, target, theta)`, `swap(q1, q2)` |
94
+ | Three qubit | `ccx(control1, control2, target)` |
95
+ | Any 2x2 unitary | `unitary(matrix, target, controls=[])` |
94
96
 
95
97
  Bitstrings put **qubit 0 on the right**, the same convention as Qiskit. Pass `seed` to `run()` for repeatable counts.
96
98
 
@@ -124,7 +126,7 @@ Below 14 qubits the CPU backend is 7 to 60 times faster than Aer; from 14 qubits
124
126
 
125
127
  ## Limitations
126
128
 
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).
129
+ 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
130
 
129
131
  ## Building from source
130
132
 
@@ -157,6 +159,8 @@ npm run dev # local preview
157
159
  npm run build # static site in site/out/
158
160
  ```
159
161
 
162
+ 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.
163
+
160
164
  ## Project layout
161
165
 
162
166
  ```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
@@ -49,15 +49,17 @@ qc.cx(0, 1)
49
49
  qc.cx(1, 2)
50
50
 
51
51
  qc.run(shots=1000) # measurement counts: {'000': ..., '111': ...}
52
- qc.probabilities() # exact probabilities of all 8 basis states
53
- qc.statevector() # exact complex amplitudes
52
+ qc.probabilities() # exact probabilities of all 8 basis states (NumPy array)
53
+ qc.statevector() # exact complex amplitudes (NumPy array)
54
54
  ```
55
55
 
56
56
  | Gates | Methods |
57
57
  | --- | --- |
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)` |
58
+ | Single qubit | `x`, `y`, `z`, `h`, `s`, `sdg`, `t`, `tdg` |
59
+ | Angles (radians) | `rx(q, theta)`, `ry(q, theta)`, `rz(q, theta)`, `p(q, lam)`, `u(q, theta, phi, lam)` |
60
+ | Two qubit | `cx(control, target)`, `cz(control, target)`, `cp(control, target, lam)`, `crz(control, target, theta)`, `swap(q1, q2)` |
61
+ | Three qubit | `ccx(control1, control2, target)` |
62
+ | Any 2x2 unitary | `unitary(matrix, target, controls=[])` |
61
63
 
62
64
  Bitstrings put **qubit 0 on the right**, the same convention as Qiskit. Pass `seed` to `run()` for repeatable counts.
63
65
 
@@ -91,7 +93,7 @@ Below 14 qubits the CPU backend is 7 to 60 times faster than Aer; from 14 qubits
91
93
 
92
94
  ## Limitations
93
95
 
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).
96
+ 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
97
 
96
98
  ## Building from source
97
99
 
@@ -124,6 +126,8 @@ npm run dev # local preview
124
126
  npm run build # static site in site/out/
125
127
  ```
126
128
 
129
+ 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.
130
+
127
131
  ## Project layout
128
132
 
129
133
  ```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
 
@@ -29,17 +32,36 @@ class QuantumCircuit:
29
32
  def rx(self, qubit: int, theta: float) -> None: ...
30
33
  def ry(self, qubit: int, theta: float) -> None: ...
31
34
  def rz(self, qubit: int, theta: float) -> None: ...
35
+ def sdg(self, qubit: int) -> None: ...
36
+ def tdg(self, qubit: int) -> None: ...
37
+ def p(self, qubit: int, lam: float) -> None: ...
38
+ def u(self, qubit: int, theta: float, phi: float, lam: float) -> None: ...
32
39
 
33
40
  # Two-qubit gates
34
41
  def cx(self, control: int, target: int) -> None: ...
35
42
  def cz(self, control: int, target: int) -> None: ...
43
+ def cp(self, control: int, target: int, lam: float) -> None: ...
44
+ def crz(self, control: int, target: int, theta: float) -> None: ...
45
+ def swap(self, qubit1: int, qubit2: int) -> None: ...
46
+
47
+ # Three-qubit gates
48
+ def ccx(self, control1: int, control2: int, target: int) -> None: ...
49
+
50
+ # Any 2x2 unitary, optionally controlled
51
+ def unitary(
52
+ self,
53
+ matrix: Union[Sequence[Sequence[complex]], npt.ArrayLike],
54
+ target: int,
55
+ controls: Sequence[int] = ...,
56
+ ) -> None:
57
+ """Apply a 2x2 unitary to `target`; raises ValueError if it is not unitary."""
36
58
 
37
59
  # Execution
38
60
  def run(
39
61
  self, shots: int = 1024, seed: Optional[int] = None, backend: Backend = "auto"
40
62
  ) -> dict[str, int]:
41
63
  """Measure all qubits `shots` times; returns {bitstring: count}."""
42
- def statevector(self, backend: Backend = "auto") -> list[complex]:
64
+ def statevector(self, backend: Backend = "auto") -> npt.NDArray[np.complex128]:
43
65
  """Exact final amplitudes, length 2**num_qubits."""
44
- def probabilities(self, backend: Backend = "auto") -> list[float]:
66
+ def probabilities(self, backend: Backend = "auto") -> npt.NDArray[np.float64]:
45
67
  """Exact final basis-state probabilities, length 2**num_qubits."""
@@ -58,7 +58,16 @@ impl QuantumCircuit {
58
58
  Ok(())
59
59
  }
60
60
 
61
- // Named gates: same names and meaning as on `QuantumBackend`.
61
+ /// Record a user-supplied 2x2 `gate`, after checking it is unitary.
62
+ /// [`add_gate`](Self::add_gate) trusts its matrix; this is the entry point for untrusted input.
63
+ pub fn unitary(&mut self, controls: &[usize], target: usize, gate: Gate) -> Result<()> {
64
+ if !gates::is_unitary(&gate, 1e-9) {
65
+ return Err(QsError::NotUnitary);
66
+ }
67
+ self.add_gate(controls, target, gate)
68
+ }
69
+
70
+ // Named gates. The first eleven match `QuantumBackend`; the rest are circuit-only.
62
71
  pub fn x(&mut self, q: usize) -> Result<()> { self.add_gate(&[], q, gates::X) }
63
72
  pub fn y(&mut self, q: usize) -> Result<()> { self.add_gate(&[], q, gates::Y) }
64
73
  pub fn z(&mut self, q: usize) -> Result<()> { self.add_gate(&[], q, gates::Z) }
@@ -74,6 +83,30 @@ impl QuantumCircuit {
74
83
  pub fn cz(&mut self, control: usize, target: usize) -> Result<()> {
75
84
  self.add_gate(&[control], target, gates::Z)
76
85
  }
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)) }
89
+ pub fn u(&mut self, q: usize, theta: f64, phi: f64, lambda: f64) -> Result<()> {
90
+ self.add_gate(&[], q, gates::u(theta, phi, lambda))
91
+ }
92
+ pub fn cp(&mut self, control: usize, target: usize, lambda: f64) -> Result<()> {
93
+ self.add_gate(&[control], target, gates::p(lambda))
94
+ }
95
+ pub fn crz(&mut self, control: usize, target: usize, theta: f64) -> Result<()> {
96
+ self.add_gate(&[control], target, gates::rz(theta))
97
+ }
98
+ /// Toffoli: flip `target` when both controls are |1>.
99
+ pub fn ccx(&mut self, c1: usize, c2: usize, target: usize) -> Result<()> {
100
+ self.add_gate(&[c1, c2], target, gates::X)
101
+ }
102
+ /// Swap two qubits, as three CNOTs (the kernel only knows controlled 2x2 gates).
103
+ pub fn swap(&mut self, a: usize, b: usize) -> Result<()> {
104
+ // Validate first so a bad swap records nothing, not one or two stray CNOTs.
105
+ control_mask(self.num_qubits, &[a], b)?;
106
+ self.cx(a, b)?;
107
+ self.cx(b, a)?;
108
+ self.cx(a, b)
109
+ }
77
110
 
78
111
  /// Apply every instruction to an existing backend, with single-qubit gate fusion.
79
112
  ///
@@ -190,6 +223,9 @@ mod tests {
190
223
  let mut qc = QuantumCircuit::new(2).unwrap();
191
224
  assert_eq!(qc.h(2), Err(QsError::QubitOutOfRange { qubit: 2, num_qubits: 2 }));
192
225
  assert_eq!(qc.cx(0, 0), Err(QsError::DuplicateQubit(0)));
226
+ assert_eq!(qc.swap(1, 1), Err(QsError::DuplicateQubit(1)));
227
+ assert_eq!(qc.swap(0, 5), Err(QsError::QubitOutOfRange { qubit: 5, num_qubits: 2 }));
228
+ assert_eq!(qc.unitary(&[], 0, [[gates::H[0][0]; 2]; 2]), Err(QsError::NotUnitary));
193
229
  assert!(qc.instructions().is_empty());
194
230
  }
195
231
 
@@ -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) |
@@ -69,6 +69,8 @@ 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,
72
74
  /// No usable GPU, or the GPU driver reported an error.
73
75
  Gpu(String),
74
76
  }
@@ -86,6 +88,7 @@ impl fmt::Display for QsError {
86
88
  QsError::SizeMismatch { circuit, backend } => {
87
89
  write!(f, "circuit has {circuit} qubits but backend has {backend}")
88
90
  }
91
+ QsError::NotUnitary => write!(f, "gate matrix must be a 2x2 unitary"),
89
92
  QsError::Gpu(msg) => write!(f, "GPU error: {msg}"),
90
93
  }
91
94
  }
@@ -11,6 +11,7 @@
11
11
 
12
12
  use crate::{BackendKind, QsError, QuantumCircuit, format_bitstring};
13
13
  use num_complex::Complex64;
14
+ use numpy::{PyArray1, ToPyArray};
14
15
  use pyo3::exceptions::{PyRuntimeError, PyValueError};
15
16
  use pyo3::prelude::*;
16
17
  use rand::SeedableRng;
@@ -93,6 +94,39 @@ impl PyQuantumCircuit {
93
94
  fn cx(&mut self, control: usize, target: usize) -> PyResult<()> { Ok(self.inner.cx(control, target)?) }
94
95
  /// Controlled-Z: flip the phase of |11> on `control`, `target`.
95
96
  fn cz(&mut self, control: usize, target: usize) -> PyResult<()> { Ok(self.inner.cz(control, target)?) }
97
+ /// S-dagger (inverse of S) on `qubit`.
98
+ fn sdg(&mut self, qubit: usize) -> PyResult<()> { Ok(self.inner.sdg(qubit)?) }
99
+ /// T-dagger (inverse of T) on `qubit`.
100
+ fn tdg(&mut self, qubit: usize) -> PyResult<()> { Ok(self.inner.tdg(qubit)?) }
101
+ /// Phase gate: multiply the |1> amplitude of `qubit` by e^(i*lam).
102
+ fn p(&mut self, qubit: usize, lam: f64) -> PyResult<()> { Ok(self.inner.p(qubit, lam)?) }
103
+ /// General single-qubit gate U(theta, phi, lam), the same convention as Qiskit.
104
+ fn u(&mut self, qubit: usize, theta: f64, phi: f64, lam: f64) -> PyResult<()> {
105
+ Ok(self.inner.u(qubit, theta, phi, lam)?)
106
+ }
107
+ /// Controlled phase: multiply the |11> amplitude of `control`, `target` by e^(i*lam).
108
+ fn cp(&mut self, control: usize, target: usize, lam: f64) -> PyResult<()> { Ok(self.inner.cp(control, target, lam)?) }
109
+ /// Controlled RZ: rotate `target` about Z by `theta` when `control` is |1>.
110
+ fn crz(&mut self, control: usize, target: usize, theta: f64) -> PyResult<()> {
111
+ Ok(self.inner.crz(control, target, theta)?)
112
+ }
113
+ /// Toffoli: flip `target` when `control1` and `control2` are both |1>.
114
+ fn ccx(&mut self, control1: usize, control2: usize, target: usize) -> PyResult<()> {
115
+ Ok(self.inner.ccx(control1, control2, target)?)
116
+ }
117
+ /// Swap the states of `qubit1` and `qubit2` (recorded as three CNOTs).
118
+ fn swap(&mut self, qubit1: usize, qubit2: usize) -> PyResult<()> { Ok(self.inner.swap(qubit1, qubit2)?) }
119
+
120
+ /// Apply any 2x2 unitary `matrix` (nested lists or a NumPy array) to `target`,
121
+ /// optionally only where every qubit in `controls` is |1>.
122
+ #[pyo3(signature = (matrix, target, controls = Vec::new()))]
123
+ fn unitary(&mut self, matrix: Vec<Vec<Complex64>>, target: usize, controls: Vec<usize>) -> PyResult<()> {
124
+ let gate = match matrix.as_slice() {
125
+ [r0, r1] if r0.len() == 2 && r1.len() == 2 => [[r0[0], r0[1]], [r1[0], r1[1]]],
126
+ _ => return Err(PyValueError::new_err("matrix must be 2x2")),
127
+ };
128
+ Ok(self.inner.unitary(&controls, target, gate)?)
129
+ }
96
130
 
97
131
  /// Simulate the circuit and measure every qubit `shots` times.
98
132
  ///
@@ -115,21 +149,24 @@ impl PyQuantumCircuit {
115
149
  Ok(counts.into_iter().map(|(i, c)| (format_bitstring(i, n), c)).collect())
116
150
  }
117
151
 
118
- /// Exact final state as a list of 2**n complex amplitudes, indexed by basis
119
- /// state (wrap with `numpy.array(...)` for vector maths).
152
+ /// Exact final state as a NumPy array of 2**n complex128 amplitudes,
153
+ /// indexed by basis state.
120
154
  #[pyo3(signature = (backend = "auto"))]
121
- fn statevector(&self, py: Python<'_>, backend: &str) -> PyResult<Vec<Complex64>> {
155
+ fn statevector<'py>(&self, py: Python<'py>, backend: &str) -> PyResult<Bound<'py, PyArray1<Complex64>>> {
122
156
  let kind = parse_backend(backend)?;
123
157
  let circuit = self.inner.clone();
124
- Ok(py.detach(move || circuit.statevector(kind))?)
158
+ let amps = py.detach(move || circuit.statevector(kind))?;
159
+ Ok(amps.to_pyarray(py))
125
160
  }
126
161
 
127
- /// Exact probability of each of the 2**n basis states after the circuit.
162
+ /// Exact probability of each of the 2**n basis states after the circuit,
163
+ /// as a NumPy float64 array.
128
164
  #[pyo3(signature = (backend = "auto"))]
129
- fn probabilities(&self, py: Python<'_>, backend: &str) -> PyResult<Vec<f64>> {
165
+ fn probabilities<'py>(&self, py: Python<'py>, backend: &str) -> PyResult<Bound<'py, PyArray1<f64>>> {
130
166
  let kind = parse_backend(backend)?;
131
167
  let circuit = self.inner.clone();
132
- Ok(py.detach(move || circuit.probabilities(kind))?)
168
+ let probs = py.detach(move || circuit.probabilities(kind))?;
169
+ Ok(probs.to_pyarray(py))
133
170
  }
134
171
  }
135
172
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes