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.
- qsengine-1.0.2/.gitattributes +1 -0
- {qsengine-1.0.0 → qsengine-1.0.2}/Cargo.lock +75 -3
- {qsengine-1.0.0 → qsengine-1.0.2}/Cargo.toml +4 -2
- {qsengine-1.0.0 → qsengine-1.0.2}/PKG-INFO +14 -9
- {qsengine-1.0.0 → qsengine-1.0.2}/README.md +12 -7
- {qsengine-1.0.0 → qsengine-1.0.2}/pyproject.toml +2 -1
- {qsengine-1.0.0 → qsengine-1.0.2}/qsengine.pyi +27 -3
- {qsengine-1.0.0 → qsengine-1.0.2}/src/backend.rs +24 -10
- {qsengine-1.0.0 → qsengine-1.0.2}/src/circuit.rs +148 -15
- {qsengine-1.0.0 → qsengine-1.0.2}/src/engine.rs +11 -1
- {qsengine-1.0.0 → qsengine-1.0.2}/src/gates.rs +42 -12
- {qsengine-1.0.0 → qsengine-1.0.2}/src/lib.rs +13 -2
- {qsengine-1.0.0 → qsengine-1.0.2}/src/python.rs +52 -8
- {qsengine-1.0.0 → qsengine-1.0.2}/.gitignore +0 -0
- {qsengine-1.0.0 → qsengine-1.0.2}/LICENSE +0 -0
- {qsengine-1.0.0 → qsengine-1.0.2}/src/gpu.rs +0 -0
- {qsengine-1.0.0 → qsengine-1.0.2}/src/main.rs +0 -0
- {qsengine-1.0.0 → qsengine-1.0.2}/src/shaders/apply_gate.wgsl +0 -0
|
@@ -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.
|
|
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.
|
|
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.
|
|
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,
|
|
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
|
-
|
|
|
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
|
|
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,
|
|
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
|
-
|
|
|
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
|
|
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"
|
|
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") ->
|
|
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") ->
|
|
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(
|
|
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,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
|
-
|
|
62
|
-
|
|
63
|
-
pub fn
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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.
|
|
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.
|
|
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)?.
|
|
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
|
-
|
|
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
|
-
|
|
89
|
-
fn
|
|
90
|
-
for
|
|
91
|
-
|
|
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
|
|
101
|
-
|
|
102
|
-
|
|
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 (
|
|
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
|
|
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
|
|
119
|
-
///
|
|
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<'
|
|
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
|
-
|
|
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<'
|
|
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
|
-
|
|
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
|
|
File without changes
|