qang 0.4.0__py3-none-any.whl
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.
- qang/__init__.py +43 -0
- qang/algorithms.py +310 -0
- qang/ansatze.py +171 -0
- qang/circuits.py +350 -0
- qang/cirq_gate.py +117 -0
- qang/core.py +336 -0
- qang/gradients.py +489 -0
- qang/knitting.py +97 -0
- qang/mixed.py +185 -0
- qang/multiqubit.py +418 -0
- qang/pennylane_gate.py +108 -0
- qang/phase.py +121 -0
- qang/qec.py +150 -0
- qang/qiskit_gate.py +128 -0
- qang/sectors.py +114 -0
- qang/statistics.py +471 -0
- qang/transformations.py +40 -0
- qang-0.4.0.dist-info/METADATA +232 -0
- qang-0.4.0.dist-info/RECORD +22 -0
- qang-0.4.0.dist-info/WHEEL +5 -0
- qang-0.4.0.dist-info/licenses/LICENSE +21 -0
- qang-0.4.0.dist-info/top_level.txt +1 -0
qang/__init__.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
qang — A Unified Angular-Probability Unit for Parametric Quantum Circuit Design.
|
|
3
|
+
|
|
4
|
+
Reference implementation for:
|
|
5
|
+
V. H. Monteverde, "The Qang (qg): A Unified Angular-Probability Unit and
|
|
6
|
+
Metric for Parametric Quantum Circuit Design." ORCID: 0000-0001-8884-4811
|
|
7
|
+
https://doi.org/10.5281/zenodo.22832150
|
|
8
|
+
|
|
9
|
+
This package covers, in order:
|
|
10
|
+
- qang.core the qg_Z / qg_S unit exactly as defined in the paper
|
|
11
|
+
(Section 2), plus the full Bloch-sphere (theta, phi)
|
|
12
|
+
representation consolidated from the author's
|
|
13
|
+
exploratory notebook.
|
|
14
|
+
- qang.gradients the coordinate-singularity limitation from Section 4.1,
|
|
15
|
+
a regularized alternative, and a toy VQE benchmark
|
|
16
|
+
comparing theta-space vs qg-space optimization
|
|
17
|
+
(Future Research Direction #3).
|
|
18
|
+
- qang.mixed generalization to mixed states and POVMs
|
|
19
|
+
(Future Research Direction #4, part 1).
|
|
20
|
+
- qang.multiqubit generalization to multi-qubit tensor-product
|
|
21
|
+
projection profiles (Future Research Direction #4,
|
|
22
|
+
part 2).
|
|
23
|
+
- qang.qiskit_gate qg as a native single-qubit gate for Qiskit
|
|
24
|
+
(Future Research Direction #1). Optional: only
|
|
25
|
+
importable if qiskit is installed.
|
|
26
|
+
- qang.cirq_gate the same for Cirq (optional: needs cirq).
|
|
27
|
+
- qang.statistics shot-noise error propagation theta <-> qg, and
|
|
28
|
+
few-shot qg_Z intervals (Haar-prior Bayes, Wilson).
|
|
29
|
+
- qang.phase the qg_Phi phase unit.
|
|
30
|
+
- qang.transformations closed-form transition probabilities.
|
|
31
|
+
- qang.knitting the sampling cost of circuit cutting in qg units.
|
|
32
|
+
- qang.circuits, qang.ansatze, qang.qec, qang.algorithms
|
|
33
|
+
Qiskit circuit builders, variational ansatze, the
|
|
34
|
+
3-qubit bit-flip code, and textbook algorithms
|
|
35
|
+
(optional: need qiskit).
|
|
36
|
+
|
|
37
|
+
See RESEARCH_NOTES.md for the derivations and results behind each module.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
from .core import Qang, MILLIQANG_PER_QANG
|
|
41
|
+
|
|
42
|
+
__all__ = ["Qang", "MILLIQANG_PER_QANG"]
|
|
43
|
+
__version__ = "0.4.0"
|
qang/algorithms.py
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""
|
|
2
|
+
qang.algorithms -- textbook multi-qubit quantum algorithms built with
|
|
3
|
+
Qiskit, using the same "everything unitary, verify by exact statevector"
|
|
4
|
+
discipline already used throughout qang (qang.qec's deferred-measurement
|
|
5
|
+
error correction; qang.circuits' state constructors).
|
|
6
|
+
|
|
7
|
+
Three algorithms live here:
|
|
8
|
+
|
|
9
|
+
1. Quantum teleportation, using the deferred-measurement principle:
|
|
10
|
+
the textbook version measures the Bell pair and applies classically
|
|
11
|
+
-controlled X/Z corrections, but any measure-then-classically-
|
|
12
|
+
-control circuit has an exactly equivalent fully unitary circuit
|
|
13
|
+
where the "would-be" classical controls are replaced by ordinary
|
|
14
|
+
quantum-controlled gates (Nielsen & Chuang, Section 4.4). That lets
|
|
15
|
+
correctness be checked by exact statevector fidelity instead of by
|
|
16
|
+
sampling -- the same technique qang.qec already relies on.
|
|
17
|
+
|
|
18
|
+
2. Superdense coding -- the "reverse" protocol, sending two classical
|
|
19
|
+
bits over one qubit given a pre-shared Bell pair. Empirically
|
|
20
|
+
important note (verified numerically, not assumed): with the gate
|
|
21
|
+
ordering used here, the decoded bit on qubit 0 recovers the
|
|
22
|
+
original *bit_z* (the Z-correction bit) and the decoded bit on
|
|
23
|
+
qubit 1 recovers the original *bit_x* (the X-correction bit) -- the
|
|
24
|
+
roles are swapped relative to a same-index guess, which is why the
|
|
25
|
+
variable names below say ``bit_z``/``bit_x`` rather than ``b0``/``b1``.
|
|
26
|
+
|
|
27
|
+
3. Grover's algorithm, with an oracle built from an X-sandwich plus a
|
|
28
|
+
phase-kickback multi-controlled-Z (via an H-sandwiched MCXGate) and
|
|
29
|
+
the standard diffusion operator, run for the optimal number of
|
|
30
|
+
iterations (round(pi/4 * sqrt(2**n_qubits))).
|
|
31
|
+
|
|
32
|
+
This module is optional: importing ``qang`` itself never requires Qiskit.
|
|
33
|
+
Only importing *this* module does, and it raises a clear, actionable error
|
|
34
|
+
if Qiskit is not installed (same pattern as qang.qiskit_gate / qang.circuits).
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
import math
|
|
40
|
+
from typing import Tuple
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
from qiskit import QuantumCircuit
|
|
44
|
+
from qiskit.circuit.library import MCXGate
|
|
45
|
+
from qiskit.quantum_info import Statevector, partial_trace, state_fidelity
|
|
46
|
+
|
|
47
|
+
_QISKIT_AVAILABLE = True
|
|
48
|
+
except ImportError: # pragma: no cover - exercised only when qiskit is absent
|
|
49
|
+
_QISKIT_AVAILABLE = False
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _require_qiskit():
|
|
53
|
+
if not _QISKIT_AVAILABLE:
|
|
54
|
+
raise ImportError(
|
|
55
|
+
"qang.algorithms requires Qiskit. Install it with "
|
|
56
|
+
"`pip install qiskit` (and, to run circuits, `pip install qiskit-aer`)."
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# --------------------------------------------------------------------- #
|
|
61
|
+
# 1. Quantum teleportation (deferred-measurement / fully unitary form)
|
|
62
|
+
# --------------------------------------------------------------------- #
|
|
63
|
+
def teleportation_circuit(state_prep_angles: Tuple[float, float] = (0.0, 0.0)) -> "QuantumCircuit":
|
|
64
|
+
"""
|
|
65
|
+
3-qubit teleportation circuit using the deferred-measurement
|
|
66
|
+
principle (no mid-circuit measurement or classical control at all):
|
|
67
|
+
|
|
68
|
+
qubit 0: Alice's message qubit, prepared via RY(theta) then RZ(phi)
|
|
69
|
+
from ``state_prep_angles`` = (theta, phi).
|
|
70
|
+
qubit 1: Alice's half of a shared Bell pair.
|
|
71
|
+
qubit 2: Bob's half of the shared Bell pair -- ends up holding an
|
|
72
|
+
exact copy of qubit 0's original state.
|
|
73
|
+
|
|
74
|
+
Bell-pair prep (H + CX on 1,2), "Bell measurement" on qubits 0,1
|
|
75
|
+
replaced by CX(0,1) + H(0) (unitary, undoes the measurement basis
|
|
76
|
+
change instead of measuring in it), and the classically-controlled
|
|
77
|
+
corrections replaced by quantum-controlled gates: CX(1, 2) and
|
|
78
|
+
CZ(0, 2). Because every step is unitary, the output state's reduced
|
|
79
|
+
density matrix on qubit 2 is *exactly* equal to the input state on
|
|
80
|
+
qubit 0 (verified via partial_trace + state_fidelity in
|
|
81
|
+
tests/test_algorithms.py, not by sampling).
|
|
82
|
+
"""
|
|
83
|
+
_require_qiskit()
|
|
84
|
+
theta, phi = state_prep_angles
|
|
85
|
+
qc = QuantumCircuit(3, name="teleportation")
|
|
86
|
+
qc.ry(theta, 0)
|
|
87
|
+
qc.rz(phi, 0)
|
|
88
|
+
|
|
89
|
+
qc.h(1)
|
|
90
|
+
qc.cx(1, 2)
|
|
91
|
+
|
|
92
|
+
qc.cx(0, 1)
|
|
93
|
+
qc.h(0)
|
|
94
|
+
|
|
95
|
+
qc.cx(1, 2)
|
|
96
|
+
qc.cz(0, 2)
|
|
97
|
+
return qc
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def teleportation_output_fidelity(state_prep_angles: Tuple[float, float] = (0.0, 0.0)) -> float:
|
|
101
|
+
"""
|
|
102
|
+
Build the teleportation circuit, and return the exact state fidelity
|
|
103
|
+
between qubit 2's final reduced state and qubit 0's originally
|
|
104
|
+
prepared (RY, RZ) single-qubit state. Should be 1.0 (up to floating
|
|
105
|
+
-point error) for any angles.
|
|
106
|
+
"""
|
|
107
|
+
_require_qiskit()
|
|
108
|
+
theta, phi = state_prep_angles
|
|
109
|
+
original = QuantumCircuit(1)
|
|
110
|
+
original.ry(theta, 0)
|
|
111
|
+
original.rz(phi, 0)
|
|
112
|
+
original_state = Statevector.from_instruction(original)
|
|
113
|
+
|
|
114
|
+
qc = teleportation_circuit(state_prep_angles)
|
|
115
|
+
full_state = Statevector.from_instruction(qc)
|
|
116
|
+
# qubit 2 is Qiskit index 2; trace out qubits 0 and 1.
|
|
117
|
+
bob_state = partial_trace(full_state, [0, 1])
|
|
118
|
+
return float(state_fidelity(original_state, bob_state))
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# --------------------------------------------------------------------- #
|
|
122
|
+
# 2. Superdense coding
|
|
123
|
+
# --------------------------------------------------------------------- #
|
|
124
|
+
def superdense_coding_circuit(bit_z: int, bit_x: int) -> "QuantumCircuit":
|
|
125
|
+
"""
|
|
126
|
+
2-qubit superdense coding circuit encoding two classical bits
|
|
127
|
+
(``bit_z``, ``bit_x``) into one shared Bell pair using only local
|
|
128
|
+
operations on qubit 0, then decoding via CX(0,1) + H(0).
|
|
129
|
+
|
|
130
|
+
Gate sequence: H(0); CX(0,1) [shared Bell pair]; Z(0) if bit_z;
|
|
131
|
+
X(0) if bit_x; CX(0,1); H(0).
|
|
132
|
+
|
|
133
|
+
IMPORTANT (empirically verified, not a naming guess): after
|
|
134
|
+
decoding, qubit 0's computational-basis value equals the original
|
|
135
|
+
``bit_z``, and qubit 1's value equals the original ``bit_x`` -- the
|
|
136
|
+
roles are swapped relative to same-index qubit/bit correspondence.
|
|
137
|
+
See ``decode_superdense_coding`` and tests/test_algorithms.py, which
|
|
138
|
+
check exactly this mapping for all four two-bit messages.
|
|
139
|
+
"""
|
|
140
|
+
_require_qiskit()
|
|
141
|
+
if bit_z not in (0, 1) or bit_x not in (0, 1):
|
|
142
|
+
raise ValueError(f"bit_z and bit_x must each be 0 or 1, got bit_z={bit_z}, bit_x={bit_x}.")
|
|
143
|
+
|
|
144
|
+
qc = QuantumCircuit(2, name=f"superdense_{bit_z}{bit_x}")
|
|
145
|
+
qc.h(0)
|
|
146
|
+
qc.cx(0, 1)
|
|
147
|
+
|
|
148
|
+
if bit_z == 1:
|
|
149
|
+
qc.z(0)
|
|
150
|
+
if bit_x == 1:
|
|
151
|
+
qc.x(0)
|
|
152
|
+
|
|
153
|
+
qc.cx(0, 1)
|
|
154
|
+
qc.h(0)
|
|
155
|
+
return qc
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def decode_superdense_coding(qc: "QuantumCircuit") -> Tuple[int, int]:
|
|
159
|
+
"""
|
|
160
|
+
Deterministically decode a superdense-coding circuit's output
|
|
161
|
+
(exact statevector, no sampling): returns (bit_z, bit_x) as read off
|
|
162
|
+
qubit 0 and qubit 1's individual (deterministic, 0/1-probability)
|
|
163
|
+
marginals respectively.
|
|
164
|
+
"""
|
|
165
|
+
_require_qiskit()
|
|
166
|
+
sv = Statevector.from_instruction(qc)
|
|
167
|
+
probs_q0 = sv.probabilities(qargs=[0])
|
|
168
|
+
probs_q1 = sv.probabilities(qargs=[1])
|
|
169
|
+
bit_z = int(round(probs_q0[1]))
|
|
170
|
+
bit_x = int(round(probs_q1[1]))
|
|
171
|
+
return bit_z, bit_x
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# --------------------------------------------------------------------- #
|
|
175
|
+
# 3. Grover's algorithm
|
|
176
|
+
# --------------------------------------------------------------------- #
|
|
177
|
+
def grover_oracle(n_qubits: int, marked_state: int) -> "QuantumCircuit":
|
|
178
|
+
"""
|
|
179
|
+
Phase oracle that flips the sign of exactly |marked_state> (an
|
|
180
|
+
integer in [0, 2**n_qubits)), via the standard X-sandwich + phase
|
|
181
|
+
-kickback multi-controlled-Z trick: flip every qubit whose bit in
|
|
182
|
+
``marked_state`` is 0, apply a multi-controlled Z (built from an
|
|
183
|
+
H-sandwiched MCXGate on an ancilla-free all-controls-are-qubits
|
|
184
|
+
version using phase kickback on the last qubit), then undo the
|
|
185
|
+
X-sandwich.
|
|
186
|
+
"""
|
|
187
|
+
_require_qiskit()
|
|
188
|
+
if n_qubits < 1:
|
|
189
|
+
raise ValueError(f"n_qubits must be >= 1, got {n_qubits}.")
|
|
190
|
+
if not (0 <= marked_state < 2 ** n_qubits):
|
|
191
|
+
raise ValueError(f"marked_state must be in [0, 2**n_qubits), got {marked_state}.")
|
|
192
|
+
|
|
193
|
+
qc = QuantumCircuit(n_qubits, name=f"oracle_{marked_state}")
|
|
194
|
+
bits = [(marked_state >> i) & 1 for i in range(n_qubits)] # bits[i] = qubit i's target bit
|
|
195
|
+
|
|
196
|
+
for i, b in enumerate(bits):
|
|
197
|
+
if b == 0:
|
|
198
|
+
qc.x(i)
|
|
199
|
+
|
|
200
|
+
if n_qubits == 1:
|
|
201
|
+
qc.z(0)
|
|
202
|
+
else:
|
|
203
|
+
qc.h(n_qubits - 1)
|
|
204
|
+
qc.append(MCXGate(n_qubits - 1), list(range(n_qubits - 1)) + [n_qubits - 1])
|
|
205
|
+
qc.h(n_qubits - 1)
|
|
206
|
+
|
|
207
|
+
for i, b in enumerate(bits):
|
|
208
|
+
if b == 0:
|
|
209
|
+
qc.x(i)
|
|
210
|
+
return qc
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def grover_diffusion(n_qubits: int) -> "QuantumCircuit":
|
|
214
|
+
"""
|
|
215
|
+
Standard Grover diffusion operator (inversion about the mean):
|
|
216
|
+
H^{\\otimes n}; X^{\\otimes n}; multi-controlled-Z (phase kickback,
|
|
217
|
+
same construction as grover_oracle); X^{\\otimes n}; H^{\\otimes n}.
|
|
218
|
+
"""
|
|
219
|
+
_require_qiskit()
|
|
220
|
+
if n_qubits < 1:
|
|
221
|
+
raise ValueError(f"n_qubits must be >= 1, got {n_qubits}.")
|
|
222
|
+
|
|
223
|
+
qc = QuantumCircuit(n_qubits, name="diffusion")
|
|
224
|
+
for q in range(n_qubits):
|
|
225
|
+
qc.h(q)
|
|
226
|
+
for q in range(n_qubits):
|
|
227
|
+
qc.x(q)
|
|
228
|
+
|
|
229
|
+
if n_qubits == 1:
|
|
230
|
+
qc.z(0)
|
|
231
|
+
else:
|
|
232
|
+
qc.h(n_qubits - 1)
|
|
233
|
+
qc.append(MCXGate(n_qubits - 1), list(range(n_qubits - 1)) + [n_qubits - 1])
|
|
234
|
+
qc.h(n_qubits - 1)
|
|
235
|
+
|
|
236
|
+
for q in range(n_qubits):
|
|
237
|
+
qc.x(q)
|
|
238
|
+
for q in range(n_qubits):
|
|
239
|
+
qc.h(q)
|
|
240
|
+
return qc
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def grover_optimal_iterations(n_qubits: int) -> int:
|
|
244
|
+
"""round(pi/4 * sqrt(2**n_qubits)), the standard optimal number of
|
|
245
|
+
Grover iterations for a single marked item among 2**n_qubits."""
|
|
246
|
+
if n_qubits < 1:
|
|
247
|
+
raise ValueError(f"n_qubits must be >= 1, got {n_qubits}.")
|
|
248
|
+
return round((math.pi / 4.0) * math.sqrt(2 ** n_qubits))
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def grover_circuit(n_qubits: int, marked_state: int, n_iterations: int = None) -> "QuantumCircuit":
|
|
252
|
+
"""
|
|
253
|
+
Full Grover search circuit: uniform superposition (H^{\\otimes n}),
|
|
254
|
+
then ``n_iterations`` repetitions of (oracle, diffusion).
|
|
255
|
+
``n_iterations`` defaults to ``grover_optimal_iterations(n_qubits)``.
|
|
256
|
+
"""
|
|
257
|
+
_require_qiskit()
|
|
258
|
+
if n_iterations is None:
|
|
259
|
+
n_iterations = grover_optimal_iterations(n_qubits)
|
|
260
|
+
oracle = grover_oracle(n_qubits, marked_state)
|
|
261
|
+
diffusion = grover_diffusion(n_qubits)
|
|
262
|
+
|
|
263
|
+
qc = QuantumCircuit(n_qubits, name=f"grover_{marked_state}")
|
|
264
|
+
for q in range(n_qubits):
|
|
265
|
+
qc.h(q)
|
|
266
|
+
for _ in range(n_iterations):
|
|
267
|
+
qc.compose(oracle, inplace=True)
|
|
268
|
+
qc.compose(diffusion, inplace=True)
|
|
269
|
+
return qc
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def grover_success_probability(n_qubits: int, marked_state: int, n_iterations: int = None) -> float:
|
|
273
|
+
"""Exact probability (statevector, no sampling) that measuring the
|
|
274
|
+
Grover circuit's output yields ``marked_state``."""
|
|
275
|
+
_require_qiskit()
|
|
276
|
+
qc = grover_circuit(n_qubits, marked_state, n_iterations)
|
|
277
|
+
sv = Statevector.from_instruction(qc)
|
|
278
|
+
probs = sv.probabilities()
|
|
279
|
+
return float(probs[marked_state])
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
if __name__ == "__main__":
|
|
283
|
+
print("Teleportation fidelity for a few (theta, phi) message states:")
|
|
284
|
+
for theta, phi in [(0.3, 0.0), (1.0, 0.7), (2.5, 1.9)]:
|
|
285
|
+
fid = teleportation_output_fidelity((theta, phi))
|
|
286
|
+
print(f" theta={theta:.2f} phi={phi:.2f} fidelity={fid:.12f}")
|
|
287
|
+
print()
|
|
288
|
+
|
|
289
|
+
print("Superdense coding, all four two-bit messages:")
|
|
290
|
+
for bit_z in (0, 1):
|
|
291
|
+
for bit_x in (0, 1):
|
|
292
|
+
qc = superdense_coding_circuit(bit_z, bit_x)
|
|
293
|
+
decoded_z, decoded_x = decode_superdense_coding(qc)
|
|
294
|
+
print(
|
|
295
|
+
f" sent (bit_z={bit_z}, bit_x={bit_x}) "
|
|
296
|
+
f"-> decoded (bit_z={decoded_z}, bit_x={decoded_x}) "
|
|
297
|
+
f"{'OK' if (decoded_z, decoded_x) == (bit_z, bit_x) else 'MISMATCH'}"
|
|
298
|
+
)
|
|
299
|
+
print()
|
|
300
|
+
|
|
301
|
+
print("Grover's algorithm, boosted probability of the marked state:")
|
|
302
|
+
for n_qubits in [3, 4, 5]:
|
|
303
|
+
marked = (2 ** n_qubits) // 3
|
|
304
|
+
n_iter = grover_optimal_iterations(n_qubits)
|
|
305
|
+
prob = grover_success_probability(n_qubits, marked, n_iter)
|
|
306
|
+
uniform = 1.0 / (2 ** n_qubits)
|
|
307
|
+
print(
|
|
308
|
+
f" n_qubits={n_qubits} marked={marked} iterations={n_iter} "
|
|
309
|
+
f"P(marked)={prob:.6f} (uniform baseline={uniform:.6f})"
|
|
310
|
+
)
|
qang/ansatze.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""
|
|
2
|
+
qang.ansatze -- a small reusable library of variational ansatz
|
|
3
|
+
constructors, including a genuinely qg-native ansatz whose free
|
|
4
|
+
parameters are qg_Z values directly rather than raw angles.
|
|
5
|
+
|
|
6
|
+
Two families:
|
|
7
|
+
|
|
8
|
+
1. Standard building blocks, generalized out of the one-off ansatz
|
|
9
|
+
written for examples/vqe_h2_qg_vs_theta.py so future VQE work in
|
|
10
|
+
qang doesn't have to redefine them:
|
|
11
|
+
|
|
12
|
+
- ``single_excitation_ansatz`` the minimal 1-parameter ansatz
|
|
13
|
+
used for the H2 example (X on the occupied qubit, RY(theta) on
|
|
14
|
+
the virtual qubit, then a CX back to the occupied qubit),
|
|
15
|
+
generalized to an arbitrary qubit pair on an arbitrary register
|
|
16
|
+
size. ``single_excitation_ansatz(2, 0, 1, theta)`` reproduces
|
|
17
|
+
examples/vqe_h2_qg_vs_theta.py's ``h2_ansatz(theta)`` exactly.
|
|
18
|
+
|
|
19
|
+
- ``hardware_efficient_ansatz`` the standard "HEA" pattern used
|
|
20
|
+
across the VQE literature: layers of single-qubit RY rotations
|
|
21
|
+
alternating with a ladder of CX entanglers.
|
|
22
|
+
|
|
23
|
+
2. The qg-native versions of both: ``qg_ry_layer``, and
|
|
24
|
+
``hardware_efficient_ansatz_qg``, whose rotation parameters are
|
|
25
|
+
supplied directly as qg_Z values (qang.core.Qang) rather than raw
|
|
26
|
+
theta angles. This extends Future Research Direction #1's "construct
|
|
27
|
+
circuit parameters natively in qg" (already implemented at the
|
|
28
|
+
single-qubit-gate level in qang.qiskit_gate) up to the ansatz level:
|
|
29
|
+
a full variational layer can now be parameterized entirely in
|
|
30
|
+
physically meaningful qg units.
|
|
31
|
+
|
|
32
|
+
This module is optional: importing ``qang`` itself never requires Qiskit.
|
|
33
|
+
Only importing *this* module does, and it raises a clear, actionable error
|
|
34
|
+
if Qiskit is not installed (same pattern as qang.qiskit_gate).
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
from typing import Sequence, Union
|
|
40
|
+
|
|
41
|
+
from .core import Qang
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
from qiskit import QuantumCircuit
|
|
45
|
+
|
|
46
|
+
_QISKIT_AVAILABLE = True
|
|
47
|
+
except ImportError: # pragma: no cover - exercised only when qiskit is absent
|
|
48
|
+
_QISKIT_AVAILABLE = False
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _require_qiskit():
|
|
52
|
+
if not _QISKIT_AVAILABLE:
|
|
53
|
+
raise ImportError(
|
|
54
|
+
"qang.ansatze requires Qiskit. Install it with "
|
|
55
|
+
"`pip install qiskit` (and, to run circuits, `pip install qiskit-aer`)."
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# --------------------------------------------------------------------- #
|
|
60
|
+
# theta-space ansatze
|
|
61
|
+
# --------------------------------------------------------------------- #
|
|
62
|
+
def single_excitation_ansatz(
|
|
63
|
+
n_qubits: int, occupied_qubit: int, virtual_qubit: int, theta: float
|
|
64
|
+
) -> "QuantumCircuit":
|
|
65
|
+
"""
|
|
66
|
+
Minimal single-parameter excitation ansatz: X on ``occupied_qubit``
|
|
67
|
+
(the Hartree-Fock reference), RY(theta) on ``virtual_qubit``, then a
|
|
68
|
+
CX from ``virtual_qubit`` back to ``occupied_qubit`` to inject the
|
|
69
|
+
excitation. ``single_excitation_ansatz(2, 0, 1, theta)`` reproduces
|
|
70
|
+
examples/vqe_h2_qg_vs_theta.py's ``h2_ansatz`` exactly.
|
|
71
|
+
"""
|
|
72
|
+
_require_qiskit()
|
|
73
|
+
if occupied_qubit == virtual_qubit:
|
|
74
|
+
raise ValueError("occupied_qubit and virtual_qubit must differ.")
|
|
75
|
+
if not (0 <= occupied_qubit < n_qubits and 0 <= virtual_qubit < n_qubits):
|
|
76
|
+
raise ValueError(f"qubit indices must lie in [0, {n_qubits - 1}].")
|
|
77
|
+
|
|
78
|
+
qc = QuantumCircuit(n_qubits, name="single_excitation")
|
|
79
|
+
qc.x(occupied_qubit)
|
|
80
|
+
qc.ry(theta, virtual_qubit)
|
|
81
|
+
qc.cx(virtual_qubit, occupied_qubit)
|
|
82
|
+
return qc
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def hardware_efficient_ansatz(
|
|
86
|
+
n_qubits: int, reps: int, params: Sequence[float]
|
|
87
|
+
) -> "QuantumCircuit":
|
|
88
|
+
"""
|
|
89
|
+
Standard hardware-efficient ansatz: ``reps + 1`` layers of RY
|
|
90
|
+
rotations (one angle per qubit per layer), each pair of consecutive
|
|
91
|
+
layers separated by a ladder of CX entanglers (qubit i -> i+1).
|
|
92
|
+
|
|
93
|
+
``params`` must have exactly ``n_qubits * (reps + 1)`` entries, laid
|
|
94
|
+
out layer-by-layer (params[0:n_qubits] is the first rotation layer,
|
|
95
|
+
and so on).
|
|
96
|
+
"""
|
|
97
|
+
_require_qiskit()
|
|
98
|
+
if reps < 0:
|
|
99
|
+
raise ValueError(f"reps must be >= 0, got {reps}.")
|
|
100
|
+
expected = n_qubits * (reps + 1)
|
|
101
|
+
params = list(params)
|
|
102
|
+
if len(params) != expected:
|
|
103
|
+
raise ValueError(f"expected {expected} params (n_qubits * (reps + 1)), got {len(params)}.")
|
|
104
|
+
|
|
105
|
+
qc = QuantumCircuit(n_qubits, name="hea")
|
|
106
|
+
idx = 0
|
|
107
|
+
for layer in range(reps + 1):
|
|
108
|
+
for q in range(n_qubits):
|
|
109
|
+
qc.ry(params[idx], q)
|
|
110
|
+
idx += 1
|
|
111
|
+
if layer < reps:
|
|
112
|
+
for q in range(n_qubits - 1):
|
|
113
|
+
qc.cx(q, q + 1)
|
|
114
|
+
return qc
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# --------------------------------------------------------------------- #
|
|
118
|
+
# qg-native ansatze: parameters are qg_Z values, not raw angles
|
|
119
|
+
# --------------------------------------------------------------------- #
|
|
120
|
+
def qg_ry_layer(
|
|
121
|
+
qc: "QuantumCircuit", qubits: Sequence[int], qg_values: Sequence[Union[float, Qang]]
|
|
122
|
+
) -> "QuantumCircuit":
|
|
123
|
+
"""
|
|
124
|
+
Append one RY rotation per qubit in ``qubits``, with each angle
|
|
125
|
+
derived from a qg_Z value via ``Qang(qg_value).to_theta()`` rather
|
|
126
|
+
than supplied directly as a raw angle -- the ansatz-layer analogue of
|
|
127
|
+
qang.qiskit_gate.RQangGate, applied across a whole register at once.
|
|
128
|
+
"""
|
|
129
|
+
_require_qiskit()
|
|
130
|
+
if len(qubits) != len(qg_values):
|
|
131
|
+
raise ValueError(
|
|
132
|
+
f"qubits and qg_values must have the same length, got {len(qubits)} and {len(qg_values)}."
|
|
133
|
+
)
|
|
134
|
+
for q, v in zip(qubits, qg_values):
|
|
135
|
+
qg = v if isinstance(v, Qang) else Qang(v, mode="polar")
|
|
136
|
+
qc.ry(qg.to_theta(), q)
|
|
137
|
+
return qc
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def hardware_efficient_ansatz_qg(
|
|
141
|
+
n_qubits: int, reps: int, qg_params: Sequence[Union[float, Qang]]
|
|
142
|
+
) -> "QuantumCircuit":
|
|
143
|
+
"""
|
|
144
|
+
Exactly ``hardware_efficient_ansatz``, except every rotation angle is
|
|
145
|
+
supplied as a qg_Z value (qang.core.Qang or a bare float in
|
|
146
|
+
[-1, 1]) instead of a raw theta -- so the whole variational ansatz can
|
|
147
|
+
be tuned directly in the physically meaningful qg unit end to end,
|
|
148
|
+
extending Future Research Direction #1 from a single gate to a full
|
|
149
|
+
multi-layer ansatz.
|
|
150
|
+
"""
|
|
151
|
+
_require_qiskit()
|
|
152
|
+
if reps < 0:
|
|
153
|
+
raise ValueError(f"reps must be >= 0, got {reps}.")
|
|
154
|
+
expected = n_qubits * (reps + 1)
|
|
155
|
+
qg_params = list(qg_params)
|
|
156
|
+
if len(qg_params) != expected:
|
|
157
|
+
raise ValueError(
|
|
158
|
+
f"expected {expected} qg_params (n_qubits * (reps + 1)), got {len(qg_params)}."
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
qc = QuantumCircuit(n_qubits, name="hea_qg")
|
|
162
|
+
idx = 0
|
|
163
|
+
for layer in range(reps + 1):
|
|
164
|
+
layer_qubits = list(range(n_qubits))
|
|
165
|
+
layer_qg = qg_params[idx: idx + n_qubits]
|
|
166
|
+
qg_ry_layer(qc, layer_qubits, layer_qg)
|
|
167
|
+
idx += n_qubits
|
|
168
|
+
if layer < reps:
|
|
169
|
+
for q in range(n_qubits - 1):
|
|
170
|
+
qc.cx(q, q + 1)
|
|
171
|
+
return qc
|