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 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