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.
- qsengine-1.0.1/.gitattributes +1 -0
- {qsengine-1.0.0 → qsengine-1.0.1}/Cargo.lock +75 -3
- {qsengine-1.0.0 → qsengine-1.0.1}/Cargo.toml +4 -2
- {qsengine-1.0.0 → qsengine-1.0.1}/PKG-INFO +13 -9
- {qsengine-1.0.0 → qsengine-1.0.1}/README.md +11 -7
- {qsengine-1.0.0 → qsengine-1.0.1}/pyproject.toml +2 -1
- {qsengine-1.0.0 → qsengine-1.0.1}/qsengine.pyi +25 -3
- {qsengine-1.0.0 → qsengine-1.0.1}/src/circuit.rs +37 -1
- {qsengine-1.0.0 → qsengine-1.0.1}/src/gates.rs +42 -12
- {qsengine-1.0.0 → qsengine-1.0.1}/src/lib.rs +4 -1
- {qsengine-1.0.0 → qsengine-1.0.1}/src/python.rs +44 -7
- {qsengine-1.0.0 → qsengine-1.0.1}/.gitignore +0 -0
- {qsengine-1.0.0 → qsengine-1.0.1}/LICENSE +0 -0
- {qsengine-1.0.0 → qsengine-1.0.1}/src/backend.rs +0 -0
- {qsengine-1.0.0 → qsengine-1.0.1}/src/engine.rs +0 -0
- {qsengine-1.0.0 → qsengine-1.0.1}/src/gpu.rs +0 -0
- {qsengine-1.0.0 → qsengine-1.0.1}/src/main.rs +0 -0
- {qsengine-1.0.0 → qsengine-1.0.1}/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.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.
|
|
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.
|
|
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,
|
|
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
|
-
|
|
|
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
|
|
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,
|
|
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
|
-
|
|
|
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
|
|
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"
|
|
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") ->
|
|
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") ->
|
|
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
|
-
|
|
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
|
-
|
|
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) |
|
|
@@ -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
|
|
119
|
-
///
|
|
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<'
|
|
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
|
-
|
|
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<'
|
|
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
|
-
|
|
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
|
|
File without changes
|