hdlib 2.1.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.
- hdlib/__init__.py +16 -0
- hdlib/arithmetic/__init__.py +265 -0
- hdlib/arithmetic/quantum.py +1257 -0
- hdlib/model/__init__.py +12 -0
- hdlib/model/classification.py +1719 -0
- hdlib/model/clustering.py +172 -0
- hdlib/model/graph.py +764 -0
- hdlib/model/regression.py +306 -0
- hdlib/space.py +864 -0
- hdlib/vector.py +608 -0
- hdlib-2.1.0.data/scripts/chopin2.py +473 -0
- hdlib-2.1.0.dist-info/METADATA +137 -0
- hdlib-2.1.0.dist-info/RECORD +16 -0
- hdlib-2.1.0.dist-info/WHEEL +5 -0
- hdlib-2.1.0.dist-info/licenses/LICENSE +21 -0
- hdlib-2.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1257 @@
|
|
|
1
|
+
"""Quantum implementation of the MAP arithmetic operators."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from math import atan2, sqrt, ceil, log2, pi
|
|
5
|
+
from typing import Dict, List, Optional, Tuple, Union
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from mthree import M3Mitigation
|
|
10
|
+
from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister, transpile
|
|
11
|
+
from qiskit.circuit import Gate, Qubit
|
|
12
|
+
from qiskit.circuit.library import DiagonalGate, XGate, SwapGate
|
|
13
|
+
from qiskit.quantum_info import Statevector, partial_trace, entropy
|
|
14
|
+
from qiskit.providers.backend import Backend
|
|
15
|
+
from qiskit_aer import AerSimulator
|
|
16
|
+
from qiskit_ibm_runtime import Sampler
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def statevector_to_bipolar(circuit: QuantumCircuit) -> np.ndarray:
|
|
20
|
+
"""Extracts a classical bipolar vector from the phases of a quantum statevector.
|
|
21
|
+
|
|
22
|
+
This function provides a method to decode a quantum state back into a classical vector.
|
|
23
|
+
It assumes the information is encoded in the sign of the real part of the amplitudes,
|
|
24
|
+
mapping positive signs to +1 and negative signs to -1.
|
|
25
|
+
|
|
26
|
+
Automatically detects if the data is in standard (0/pi) or symmetric (+/- delta) encoding
|
|
27
|
+
and rotates if necessary.
|
|
28
|
+
|
|
29
|
+
Parameters
|
|
30
|
+
----------
|
|
31
|
+
circuit : QuantumCircuit
|
|
32
|
+
A quantum circuit to simulate and retrieve the classical bipolar vector from.
|
|
33
|
+
|
|
34
|
+
Returns
|
|
35
|
+
-------
|
|
36
|
+
numpy.ndarray
|
|
37
|
+
The corresponding classical bipolar vector of integers (+1 or -1).
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
# Create a temporary evaluation circuit to read the oracle
|
|
41
|
+
num_qubits = circuit.num_qubits
|
|
42
|
+
eval_circ = QuantumCircuit(num_qubits)
|
|
43
|
+
eval_circ.h(range(num_qubits))
|
|
44
|
+
eval_circ.compose(circuit, inplace=True)
|
|
45
|
+
|
|
46
|
+
# Simulate the fully prepared state
|
|
47
|
+
statevector = Statevector.from_instruction(eval_circ.decompose())
|
|
48
|
+
statevector_data = np.asarray(statevector.data)
|
|
49
|
+
|
|
50
|
+
# Heuristic: determine encoding based on the presence of negative real components.
|
|
51
|
+
# Standard encoding (0/pi): has amplitudes ~ +1 and ~ -1. Min real < -0.5.
|
|
52
|
+
# Symmetric encoding (+/- delta): has amplitudes e^(+id) and e^(-id).
|
|
53
|
+
# For small delta, real part is cos(d) ~ 1 (always positive).
|
|
54
|
+
min_real = np.min(np.real(statevector_data))
|
|
55
|
+
|
|
56
|
+
# Adapt
|
|
57
|
+
data = statevector_data
|
|
58
|
+
|
|
59
|
+
# If all real parts are non-negative, the information must be in the phase.
|
|
60
|
+
# Rotate by -90 degrees to project phase (imag) onto real axis for decoding.
|
|
61
|
+
if min_real > -1e-5:
|
|
62
|
+
data = statevector_data * -1j
|
|
63
|
+
|
|
64
|
+
# Decode
|
|
65
|
+
reals = np.real(data)
|
|
66
|
+
tolerance = 1e-9
|
|
67
|
+
|
|
68
|
+
vec = np.ones(len(reals), dtype=int)
|
|
69
|
+
|
|
70
|
+
# Positive real +1, negative real -1
|
|
71
|
+
vec[reals < -tolerance] = -1
|
|
72
|
+
|
|
73
|
+
return vec.astype(int)
|
|
74
|
+
|
|
75
|
+
def compress_circuit(circuit: QuantumCircuit) -> QuantumCircuit:
|
|
76
|
+
"""Compresses a deep phase-encoded quantum circuit into a shallow circuit with one DiagonalGate.
|
|
77
|
+
|
|
78
|
+
This acts as a quantum compiler for Vector-Symbolic Architectures.
|
|
79
|
+
It calculates the noise-free phase accumulation of the deep circuit
|
|
80
|
+
and reconstructs an identical quantum state using a single layer of
|
|
81
|
+
Hadamard gates and one DiagonalGate.
|
|
82
|
+
|
|
83
|
+
Parameters
|
|
84
|
+
----------
|
|
85
|
+
circuit : QuantumCircuit
|
|
86
|
+
The deep quantum circuit (e.g., a series of bundled vectors with hundreds of gates).
|
|
87
|
+
|
|
88
|
+
Returns
|
|
89
|
+
-------
|
|
90
|
+
QuantumCircuit
|
|
91
|
+
A mathematically identical shallow circuit oracle.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
num_qubits = circuit.num_qubits
|
|
95
|
+
|
|
96
|
+
# 1. Create a temporary evaluation circuit to read the deep oracle
|
|
97
|
+
eval_circ = QuantumCircuit(num_qubits)
|
|
98
|
+
eval_circ.h(range(num_qubits))
|
|
99
|
+
eval_circ.compose(circuit, inplace=True)
|
|
100
|
+
|
|
101
|
+
# Mathematically evaluate the exact state to capture phase accumulation
|
|
102
|
+
state = Statevector.from_instruction(eval_circ.decompose())
|
|
103
|
+
|
|
104
|
+
# 2. Extract the relative phases of the quantum state
|
|
105
|
+
phases = np.angle(state.data)
|
|
106
|
+
|
|
107
|
+
# 3. Create the compressed diagonal operator using the extracted phases
|
|
108
|
+
diagonal_elements = np.exp(1j * phases)
|
|
109
|
+
diag_gate = DiagonalGate(diagonal_elements.tolist())
|
|
110
|
+
|
|
111
|
+
# 4. Build the shallow oracle circuit
|
|
112
|
+
compressed_qc = QuantumCircuit(num_qubits, name=f"{circuit.name}_compressed")
|
|
113
|
+
|
|
114
|
+
# Apply all accumulated phases in a single operation to return a pure oracle.
|
|
115
|
+
compressed_qc.append(diag_gate, range(num_qubits))
|
|
116
|
+
|
|
117
|
+
return compressed_qc
|
|
118
|
+
|
|
119
|
+
def encode(vec_bipolar: np.ndarray, label: str="O_v") -> QuantumCircuit:
|
|
120
|
+
"""Creates a circuit containing a diagonal phase oracle.
|
|
121
|
+
This function is a core component for encoding classical bipolar vectors into the phase of a quantum state.
|
|
122
|
+
|
|
123
|
+
Parameters
|
|
124
|
+
----------
|
|
125
|
+
vec_bipolar : numpy.ndarray
|
|
126
|
+
A classical vector containing only -1 and +1 values.
|
|
127
|
+
label : str, default "O_v"
|
|
128
|
+
An optional label for the created Qiskit gate.
|
|
129
|
+
|
|
130
|
+
Returns
|
|
131
|
+
-------
|
|
132
|
+
qiskit.QuantumCircuit
|
|
133
|
+
A quantum circuit containing the diagonal gate.
|
|
134
|
+
|
|
135
|
+
Raises
|
|
136
|
+
------
|
|
137
|
+
ValueError
|
|
138
|
+
If the input `vec_bipolar` contains values other than -1 or +1.
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
vec = np.asarray(vec_bipolar)
|
|
142
|
+
|
|
143
|
+
if not np.all(np.isin(vec, [-1, 1])):
|
|
144
|
+
raise ValueError("Bipolar vector must contain only -1 or +1.")
|
|
145
|
+
|
|
146
|
+
num_qubits = int(ceil(log2(len(vec))))
|
|
147
|
+
|
|
148
|
+
# Pad vector if necessary to match 2^N
|
|
149
|
+
if len(vec) < 2**num_qubits:
|
|
150
|
+
padding = np.ones(2**num_qubits - len(vec))
|
|
151
|
+
vec = np.concatenate([vec, padding])
|
|
152
|
+
|
|
153
|
+
# Convert to complex diagonal entries
|
|
154
|
+
gate = DiagonalGate(vec.tolist())
|
|
155
|
+
gate.label = label
|
|
156
|
+
|
|
157
|
+
qc = QuantumCircuit(num_qubits, name=label)
|
|
158
|
+
qc.append(gate, range(num_qubits))
|
|
159
|
+
|
|
160
|
+
return qc
|
|
161
|
+
|
|
162
|
+
def bind(circuits: List[QuantumCircuit]) -> QuantumCircuit:
|
|
163
|
+
"""Applies a sequence of quantum circuits to perform binding.
|
|
164
|
+
This function only accepts a list of QuantumCircuit objects as input.
|
|
165
|
+
|
|
166
|
+
It assumes all inputs logically operate on the same number of qubits.
|
|
167
|
+
|
|
168
|
+
Warning: Composability limit!
|
|
169
|
+
Trying to bind two vectors that are already symmetric bundles would fail.
|
|
170
|
+
|
|
171
|
+
Parameters
|
|
172
|
+
----------
|
|
173
|
+
circuits : list[QuantumCircuit]
|
|
174
|
+
List of feature circuits to bind.
|
|
175
|
+
|
|
176
|
+
Returns
|
|
177
|
+
-------
|
|
178
|
+
QuantumCircuit
|
|
179
|
+
A state preparation circuit for Bind.
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
if not circuits:
|
|
183
|
+
raise ValueError("Input list for bind cannot be empty.")
|
|
184
|
+
|
|
185
|
+
# Infer the number of qubits from the first circuit in the list.
|
|
186
|
+
num_qubits = circuits[0].num_qubits
|
|
187
|
+
qc = QuantumCircuit(num_qubits, name="Bind_Op")
|
|
188
|
+
|
|
189
|
+
# Sequentially compose each circuit.
|
|
190
|
+
for circuit in circuits:
|
|
191
|
+
# This check is for robustness, though the type hint should prevent incorrect types.
|
|
192
|
+
if not isinstance(circuit, QuantumCircuit):
|
|
193
|
+
raise TypeError("All items in the bind list must be QuantumCircuit objects.")
|
|
194
|
+
|
|
195
|
+
if circuit.num_qubits != num_qubits:
|
|
196
|
+
raise ValueError("All circuits in the bind list must have the same number of qubits.")
|
|
197
|
+
|
|
198
|
+
qc.compose(circuit, inplace=True)
|
|
199
|
+
|
|
200
|
+
return qc
|
|
201
|
+
|
|
202
|
+
def bundle(circuits: list[QuantumCircuit], method: str="average") -> QuantumCircuit:
|
|
203
|
+
"""Bundles circuits symbolically using Phase Accumulation.
|
|
204
|
+
|
|
205
|
+
This function constructs a new circuit that represents the 'Bundle' (Sum) of the input circuits.
|
|
206
|
+
It uses a 'Sandwich' logic to inject scaled phases into the correct basis states defined by the circuit structure.
|
|
207
|
+
|
|
208
|
+
Key Features:
|
|
209
|
+
1. Handles Binding: Accumulates raw phases from all DiagonalGates first (XOR logic), then maps to symmetric.
|
|
210
|
+
2. Handles Permutation: Wraps the phase injection between Structure and InverseStructure.
|
|
211
|
+
3. Symmetric Encoding: Maps binary +1/-1 to symmetric phases (+pi/2, -pi/2) to preserve Majority Rule direction.
|
|
212
|
+
|
|
213
|
+
Parameters:
|
|
214
|
+
-----------
|
|
215
|
+
circuits : list[QuantumCircuit]
|
|
216
|
+
List of feature circuits to bundle.
|
|
217
|
+
method : str
|
|
218
|
+
"classical": Perform the bundling classically, no quantum operations involved;
|
|
219
|
+
"average": Scales phases by 1/N (Exact arithmetic mean);
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
--------
|
|
223
|
+
QuantumCircuit
|
|
224
|
+
A state preparation circuit for the Bundle.
|
|
225
|
+
"""
|
|
226
|
+
|
|
227
|
+
if not circuits:
|
|
228
|
+
raise ValueError("Circuit list cannot be empty")
|
|
229
|
+
|
|
230
|
+
if method == "classical":
|
|
231
|
+
# Recover the original bipolar vectors from each feature circuit
|
|
232
|
+
vectors = [statevector_to_bipolar(circ) for circ in circuits]
|
|
233
|
+
|
|
234
|
+
# Element-wise sum (keep magnitude and sign)
|
|
235
|
+
vector_bundled = np.sum(vectors, axis=0)
|
|
236
|
+
|
|
237
|
+
# We want to encode both the sign and relative magnitude of each component into a quantum oracle.
|
|
238
|
+
# Directly using DiagonalGate requires unit-modulus complex numbers, so we need to convert our vector into phases on the complex unit circle.
|
|
239
|
+
# Compute the normalization factor: root-mean-square (RMS) of the vector.
|
|
240
|
+
# This ensures that the typical amplitude of each component is ~1 without letting very large or very small components dominate excessively.
|
|
241
|
+
rms = np.linalg.norm(vector_bundled) / np.sqrt(len(vector_bundled))
|
|
242
|
+
|
|
243
|
+
# Map each component to a complex phase using e^(i * pi * x / RMS)
|
|
244
|
+
# - The sign of the original component is preserved in the phase (positive -> 0, negative -> pi);
|
|
245
|
+
# - The relative magnitude of each component is approximately preserved in the phase;
|
|
246
|
+
# - The resulting complex number all have unit modulus (required for DiagonalGate).
|
|
247
|
+
phases = np.exp(1j * np.pi * vector_bundled / rms)
|
|
248
|
+
|
|
249
|
+
# Build the diagonal gate with these phases
|
|
250
|
+
oracle_gate = DiagonalGate(phases.tolist())
|
|
251
|
+
oracle_gate.label = "O_bundle"
|
|
252
|
+
|
|
253
|
+
# Build the circuit
|
|
254
|
+
n_sys = int(log2(len(vector_bundled)))
|
|
255
|
+
sys_reg = QuantumRegister(n_sys, "sys")
|
|
256
|
+
|
|
257
|
+
qc = QuantumCircuit(sys_reg, name="Hybrid_Prototype")
|
|
258
|
+
qc.append(oracle_gate, sys_reg)
|
|
259
|
+
|
|
260
|
+
return qc
|
|
261
|
+
|
|
262
|
+
def get_indices(qubits):
|
|
263
|
+
return [input_circ.find_bit(q).index for q in qubits]
|
|
264
|
+
|
|
265
|
+
N = circuits[0].num_qubits
|
|
266
|
+
M = len(circuits)
|
|
267
|
+
|
|
268
|
+
qc = QuantumCircuit(N, name="Bundle_Op")
|
|
269
|
+
|
|
270
|
+
scale = (1.0 / M)
|
|
271
|
+
|
|
272
|
+
for i, input_circ in enumerate(circuits):
|
|
273
|
+
# Accumulate raw phases (binding logic)
|
|
274
|
+
term_raw_angles = np.zeros(2**N)
|
|
275
|
+
post_structure_ops = list()
|
|
276
|
+
found_any_diagonal = False
|
|
277
|
+
|
|
278
|
+
for instr in input_circ.data:
|
|
279
|
+
op, qargs, cargs = instr.operation, instr.qubits, instr.clbits
|
|
280
|
+
|
|
281
|
+
if isinstance(op, DiagonalGate):
|
|
282
|
+
found_any_diagonal = True
|
|
283
|
+
diag_complex = np.array(op.params, dtype=complex)
|
|
284
|
+
angles = np.angle(diag_complex)
|
|
285
|
+
term_raw_angles += angles
|
|
286
|
+
|
|
287
|
+
else:
|
|
288
|
+
post_structure_ops.append((op, qargs))
|
|
289
|
+
|
|
290
|
+
if not found_any_diagonal:
|
|
291
|
+
continue
|
|
292
|
+
|
|
293
|
+
# Normalize to symmetric domain (composability)
|
|
294
|
+
# We need to map whatever the input is to a "vote" of +/- 1.
|
|
295
|
+
|
|
296
|
+
# Resolve binding (XOR)
|
|
297
|
+
# cos(sum) is +1 for 0/2pi, -1 for pi
|
|
298
|
+
# If input was already symmetric small angles, sum is small, cos is +1
|
|
299
|
+
# This preserves the sign of small inputs too
|
|
300
|
+
|
|
301
|
+
# We need to detect the sign of small angles
|
|
302
|
+
# We use a hybrid check on the Net Angle `theta`:
|
|
303
|
+
# If cos(theta) < -0.5 -> it's pi-like -> vote -1
|
|
304
|
+
# Else if sin(theta) < -1e-5 -> it's neg-delta -> vote -1
|
|
305
|
+
# Else -> vote +1
|
|
306
|
+
net_complex = np.exp(1j * term_raw_angles)
|
|
307
|
+
votes = np.ones(2**N)
|
|
308
|
+
|
|
309
|
+
# Detect pi-like (standard negative)
|
|
310
|
+
votes[np.real(net_complex) < -0.1] = -1
|
|
311
|
+
|
|
312
|
+
# Detect negative-delta (symmetric negative)
|
|
313
|
+
# Only check this if not pi-like (to avoid boundary issues)
|
|
314
|
+
mask_small_angle = np.real(net_complex) > 0.1
|
|
315
|
+
votes[mask_small_angle & (np.imag(net_complex) < -1e-9)] = -1
|
|
316
|
+
|
|
317
|
+
# Scale & inject
|
|
318
|
+
# Now we have a clean +/- 1 vote vector
|
|
319
|
+
# Map to Symmetric Target (+pi/2, -pi/2) for the new bundle
|
|
320
|
+
symmetric_target = votes * (np.pi / 2)
|
|
321
|
+
|
|
322
|
+
scaled_phases = symmetric_target * scale
|
|
323
|
+
new_diag_entries = np.exp(1j * scaled_phases)
|
|
324
|
+
scaled_diagonal_op = DiagonalGate(new_diag_entries.tolist())
|
|
325
|
+
|
|
326
|
+
# Sandwich
|
|
327
|
+
for op, qargs in post_structure_ops:
|
|
328
|
+
qc.append(op, get_indices(qargs))
|
|
329
|
+
|
|
330
|
+
qc.append(scaled_diagonal_op, range(N))
|
|
331
|
+
|
|
332
|
+
for op, qargs in reversed(post_structure_ops):
|
|
333
|
+
try:
|
|
334
|
+
inv_op = op.inverse()
|
|
335
|
+
|
|
336
|
+
except:
|
|
337
|
+
inv_op = op
|
|
338
|
+
|
|
339
|
+
qc.append(inv_op, get_indices(qargs))
|
|
340
|
+
|
|
341
|
+
return qc
|
|
342
|
+
|
|
343
|
+
def permute(qc: QuantumCircuit, num_qubits: int, shift: int=0) -> QuantumCircuit:
|
|
344
|
+
"""Creates a synthesizable circuit gate that implements a cyclic permutation.
|
|
345
|
+
|
|
346
|
+
This function implements a cyclic shift using a Modulo 2^N Quantum Adder.
|
|
347
|
+
It shifts the basis states strictly cyclically, matching the classical np.roll property exactly,
|
|
348
|
+
but uses entirely digital gates (X and MCX) with O(N) depth.
|
|
349
|
+
|
|
350
|
+
Parameters
|
|
351
|
+
----------
|
|
352
|
+
qc : QuantumCircuit
|
|
353
|
+
The circuit to apply the cyclic shift to.
|
|
354
|
+
num_qubits : int
|
|
355
|
+
The number of qubits in the register to be permuted. The dimension is 2**num_qubits.
|
|
356
|
+
shift : int, default 0
|
|
357
|
+
The number of positions to cyclically shift the basis states.
|
|
358
|
+
|
|
359
|
+
Returns
|
|
360
|
+
-------
|
|
361
|
+
QuantumCircuit
|
|
362
|
+
A state preparation circuit for Permute.
|
|
363
|
+
"""
|
|
364
|
+
|
|
365
|
+
if qc is None:
|
|
366
|
+
# Create a new circuit representing just the permutation operation
|
|
367
|
+
# if no quantum circuit is provided
|
|
368
|
+
qc = QuantumCircuit(num_qubits, name=f"Perm(>>{shift})")
|
|
369
|
+
|
|
370
|
+
# Ensure the shift is within the cyclic bounds (Modulo D)
|
|
371
|
+
shift = shift % (2**num_qubits)
|
|
372
|
+
|
|
373
|
+
if shift == 0:
|
|
374
|
+
return qc
|
|
375
|
+
|
|
376
|
+
# Convert shift to binary string and reverse it so that index 'k' correctly corresponds to the 2^k bit
|
|
377
|
+
shift = bin(shift)[2:][::-1]
|
|
378
|
+
|
|
379
|
+
for k, bit in enumerate(shift):
|
|
380
|
+
if bit == "1":
|
|
381
|
+
# Apply a +2^k quantum incrementer
|
|
382
|
+
# This acts as a standard binary adder starting only at the k-th qubit,
|
|
383
|
+
# leaving lower significant qubits completely untouched.
|
|
384
|
+
for i in range(num_qubits - 1, k, -1):
|
|
385
|
+
# Target bit 'i' is flipped only if all lower bits from 'k' to 'i-1' are 1
|
|
386
|
+
controls = list(range(k, i))
|
|
387
|
+
|
|
388
|
+
if len(controls) == 1:
|
|
389
|
+
qc.cx(controls[0], i)
|
|
390
|
+
|
|
391
|
+
else:
|
|
392
|
+
mcx_gate = XGate().control(len(controls))
|
|
393
|
+
qc.append(mcx_gate, controls + [i])
|
|
394
|
+
|
|
395
|
+
# Finally, unconditionally flip the k-th bit
|
|
396
|
+
qc.x(k)
|
|
397
|
+
|
|
398
|
+
return qc
|
|
399
|
+
|
|
400
|
+
def __get_measured_physical_qubits(transpiled_circuit: QuantumCircuit, measured_register: ClassicalRegister) -> list[int]:
|
|
401
|
+
"""Returns the list of physical qubits that correspond to the measured classical bits.
|
|
402
|
+
"""
|
|
403
|
+
|
|
404
|
+
try:
|
|
405
|
+
# Qiskit's transpiler recreates bits. Looking up pre-transpiled Clbit identity
|
|
406
|
+
# will cause a hash mismatch and throw an error. We map via the transpiled circuit's registers.
|
|
407
|
+
transpiled_creg = next(reg for reg in transpiled_circuit.cregs if reg.name == measured_register.name)
|
|
408
|
+
|
|
409
|
+
except:
|
|
410
|
+
raise ValueError(f"Register {measured_register.name} not found in transpiled circuit.")
|
|
411
|
+
|
|
412
|
+
# 1. Create a dictionary to map classical bits to the physical qubits measured into them
|
|
413
|
+
meas_map = dict()
|
|
414
|
+
|
|
415
|
+
for inst in transpiled_circuit.data:
|
|
416
|
+
if inst.operation.name == "measure":
|
|
417
|
+
qbit = inst.qubits[0] # The qubit being measured
|
|
418
|
+
cbit = inst.clbits[0] # The classical bit receiving the result
|
|
419
|
+
|
|
420
|
+
# Find the actual physical index of this qubit in the transpiled circuit
|
|
421
|
+
qbit_idx = transpiled_circuit.find_bit(qbit).index
|
|
422
|
+
meas_map[cbit] = qbit_idx
|
|
423
|
+
|
|
424
|
+
# 2. Extract the physical qubits in the exact order of the measured_register
|
|
425
|
+
physical_qubits = list()
|
|
426
|
+
|
|
427
|
+
for cbit in transpiled_creg:
|
|
428
|
+
if cbit in meas_map:
|
|
429
|
+
# The index of the physical qubit in the transpiled circuit
|
|
430
|
+
physical_qubits.append(meas_map[cbit])
|
|
431
|
+
|
|
432
|
+
else:
|
|
433
|
+
raise ValueError(f"Classical bit {cbit} does not have a measurement mapped to it.")
|
|
434
|
+
|
|
435
|
+
# Qiskit results return bitstrings from MSB to LSB (left to right = c_{N-1} ... c_0).
|
|
436
|
+
# M3 mitigation expects the passed physical_qubits list to identically match that string's left-to-right order.
|
|
437
|
+
# Therefore, we must reverse the physical qubits list here to avoid applying the wrong error profile to the wrong bits.
|
|
438
|
+
return physical_qubits[::-1]
|
|
439
|
+
|
|
440
|
+
def __mitigate_counts(counts, backend, shots, measured_qubits, mitigator: Optional[M3Mitigation]=None):
|
|
441
|
+
"""Apply readout error mitigation using mthree to a single-qubit measurement.
|
|
442
|
+
"""
|
|
443
|
+
|
|
444
|
+
if mitigator is None:
|
|
445
|
+
# Initialize mitigator from backend
|
|
446
|
+
mitigator = M3Mitigation(backend)
|
|
447
|
+
mitigator.cals_from_system(qubits=measured_qubits)
|
|
448
|
+
|
|
449
|
+
# Apply correction to get mitigated probabilities
|
|
450
|
+
probs = mitigator.apply_correction(counts, qubits=measured_qubits)
|
|
451
|
+
|
|
452
|
+
# Dynamically convert all output states back to pseudo-counts
|
|
453
|
+
mitigated_pseudo_counts = dict()
|
|
454
|
+
|
|
455
|
+
for state, prob in probs.items():
|
|
456
|
+
# Clamp quasi-probabilities to strictly between 0.0 and 1.0
|
|
457
|
+
# M3 can sometimes output tiny negative values or values slightly above 1.0
|
|
458
|
+
safe_prob = min(1.0, max(0.0, prob))
|
|
459
|
+
mitigated_pseudo_counts[state] = int(round(safe_prob * shots))
|
|
460
|
+
|
|
461
|
+
# Return mitigated probabilities as pseudo-counts
|
|
462
|
+
return mitigated_pseudo_counts
|
|
463
|
+
|
|
464
|
+
def run_compute_uncompute_test(
|
|
465
|
+
state_left_circs: List[QuantumCircuit],
|
|
466
|
+
state_right_circs: List[QuantumCircuit],
|
|
467
|
+
backend: Backend,
|
|
468
|
+
shots: int=1024,
|
|
469
|
+
seed: int=42,
|
|
470
|
+
sampler: Optional[Sampler]=None
|
|
471
|
+
) -> Tuple[List[List[float]], List[dict]]:
|
|
472
|
+
"""Performs a Compute-Uncompute (Inversion) test to measure |<L|R>|^2 in batch mode.
|
|
473
|
+
|
|
474
|
+
This avoids all controlled operations, making it exponentially cheaper
|
|
475
|
+
to transpile and execute compared to the Hadamard Test.
|
|
476
|
+
"""
|
|
477
|
+
|
|
478
|
+
is_simulated = isinstance(backend, AerSimulator)
|
|
479
|
+
n_sys = state_right_circs[0].num_qubits
|
|
480
|
+
|
|
481
|
+
if state_left_circs[0].num_qubits != n_sys:
|
|
482
|
+
raise ValueError("Left and Right circuits must have the exact same number of qubits for Inversion test.")
|
|
483
|
+
|
|
484
|
+
sys = QuantumRegister(n_sys, "sys")
|
|
485
|
+
creg = ClassicalRegister(n_sys, "c_meas")
|
|
486
|
+
|
|
487
|
+
qcs = list()
|
|
488
|
+
|
|
489
|
+
for query_circ in state_left_circs:
|
|
490
|
+
for prototype_circ in state_right_circs:
|
|
491
|
+
qc = QuantumCircuit(sys, creg)
|
|
492
|
+
|
|
493
|
+
# 1. Initialize uniform superposition
|
|
494
|
+
qc.h(sys)
|
|
495
|
+
|
|
496
|
+
# 2. Compute: Apply query state (R)
|
|
497
|
+
qc.compose(query_circ, qubits=sys, inplace=True)
|
|
498
|
+
|
|
499
|
+
# 3. Uncompute: Apply inverse of prototype state (L)
|
|
500
|
+
qc.compose(prototype_circ.inverse(), qubits=sys, inplace=True)
|
|
501
|
+
|
|
502
|
+
# 4. Map phases back to amplitudes for measurement
|
|
503
|
+
qc.h(sys)
|
|
504
|
+
|
|
505
|
+
# 5. Measure all qubits
|
|
506
|
+
qc.measure(sys, creg)
|
|
507
|
+
|
|
508
|
+
qcs.append(qc)
|
|
509
|
+
|
|
510
|
+
if is_simulated:
|
|
511
|
+
tqcs = transpile(qcs, backend, optimization_level=1)
|
|
512
|
+
counts = backend.run(tqcs, shots=shots, seed_simulator=seed).result().get_counts()
|
|
513
|
+
|
|
514
|
+
if not isinstance(counts, list):
|
|
515
|
+
counts = [counts]
|
|
516
|
+
|
|
517
|
+
else:
|
|
518
|
+
if not sampler:
|
|
519
|
+
raise ValueError("A Sampler object must be provided for hardware execution.")
|
|
520
|
+
|
|
521
|
+
tqcs = transpile(qcs, backend, optimization_level=3)
|
|
522
|
+
job = sampler.run(tqcs, shots=shots)
|
|
523
|
+
results = job.result()
|
|
524
|
+
|
|
525
|
+
counts = list()
|
|
526
|
+
|
|
527
|
+
# Gather all unique physical qubits used across all circuits
|
|
528
|
+
all_measured_qubits = set()
|
|
529
|
+
circuit_measured_qubits = list()
|
|
530
|
+
|
|
531
|
+
for tqc in tqcs:
|
|
532
|
+
# Automatically detect measured qubits
|
|
533
|
+
phys_qubits = __get_measured_physical_qubits(tqc, creg)
|
|
534
|
+
all_measured_qubits.update(phys_qubits)
|
|
535
|
+
circuit_measured_qubits.append(phys_qubits)
|
|
536
|
+
|
|
537
|
+
# Initialize mitigator from backend
|
|
538
|
+
mitigator = M3Mitigation(backend)
|
|
539
|
+
mitigator.cals_from_system(qubits=list(all_measured_qubits))
|
|
540
|
+
|
|
541
|
+
for i, res in enumerate(results):
|
|
542
|
+
counts_res = res.data.c_meas.get_counts()
|
|
543
|
+
|
|
544
|
+
# Apply readout error mitigation
|
|
545
|
+
counts.append(__mitigate_counts(counts_res, backend, shots, circuit_measured_qubits[i], mitigator=mitigator))
|
|
546
|
+
|
|
547
|
+
# Group similarities back into a 2D list
|
|
548
|
+
similarities = list()
|
|
549
|
+
idx = 0
|
|
550
|
+
|
|
551
|
+
# Dynamically define the all-zeros target state based on system size
|
|
552
|
+
target_state = "0" * n_sys
|
|
553
|
+
|
|
554
|
+
for _ in state_left_circs:
|
|
555
|
+
query_sims = list()
|
|
556
|
+
|
|
557
|
+
for _ in state_right_circs:
|
|
558
|
+
# Because both simulator and mitigated branches output integers/pseudo-counts
|
|
559
|
+
# that sum to 'shots', dividing by 'shots' here correctly yields the probability.
|
|
560
|
+
raw_prob = counts[idx].get(target_state, 0) / shots
|
|
561
|
+
|
|
562
|
+
# The compute-uncompute test measures |<L|R>|^2
|
|
563
|
+
# We took the square root to return the similarity magnitude |<L|R>|
|
|
564
|
+
query_sims.append(sqrt(raw_prob))
|
|
565
|
+
|
|
566
|
+
idx += 1
|
|
567
|
+
|
|
568
|
+
similarities.append(query_sims)
|
|
569
|
+
|
|
570
|
+
return similarities, counts
|
|
571
|
+
|
|
572
|
+
def get_circuit_metrics(circuit: QuantumCircuit, num_system_qubits: int, backend: Backend, optimization_level: int=3) -> Dict[str, int]:
|
|
573
|
+
"""Analyzes a quantum circuit for key computational expense metrics.
|
|
574
|
+
|
|
575
|
+
This function transpiles the circuit to a specified basis gate set
|
|
576
|
+
to accurately report its depth and CNOT count.
|
|
577
|
+
|
|
578
|
+
Parameters
|
|
579
|
+
----------
|
|
580
|
+
circuit : qiskit.QuantumCircuit
|
|
581
|
+
The circuit to analyze.
|
|
582
|
+
num_system_qubits : int
|
|
583
|
+
The number of qubits in the circuit dedicated to the "system".
|
|
584
|
+
The remaining qubits are assumed to be ancillas.
|
|
585
|
+
backend : qiskit.providers.backend.Backend
|
|
586
|
+
The backend for which to transpile the circuit.
|
|
587
|
+
optimization_level : int, default 1
|
|
588
|
+
The optimization level for the transpiler (0-3).
|
|
589
|
+
|
|
590
|
+
Returns
|
|
591
|
+
-------
|
|
592
|
+
dict[str, int]
|
|
593
|
+
A dictionary containing the following metrics:
|
|
594
|
+
- "num_qubits_total": Total number of qubits in the circuit;
|
|
595
|
+
- "num_qubits_system": The provided number of system_qubits;
|
|
596
|
+
- "num_qubits_ancilla": Calculated number of ancilla qubits;
|
|
597
|
+
- "depth": The depth of the transpiled circuit;
|
|
598
|
+
- "cnot_count": The number of CNOT (cx) gates in the transpiled circuit.
|
|
599
|
+
|
|
600
|
+
Raises
|
|
601
|
+
------
|
|
602
|
+
ValueError
|
|
603
|
+
If num_system_qubits is larger than the total qubits in the circuit.
|
|
604
|
+
"""
|
|
605
|
+
|
|
606
|
+
num_qubits_total = circuit.num_qubits
|
|
607
|
+
|
|
608
|
+
if num_system_qubits > num_qubits_total:
|
|
609
|
+
raise ValueError(f"num_system_qubits ({num_system_qubits}) cannot be larger than total circuit qubits ({num_qubits_total}).")
|
|
610
|
+
|
|
611
|
+
num_qubits_ancilla = num_qubits_total - num_system_qubits
|
|
612
|
+
|
|
613
|
+
# Transpile the circuit to break down high-level gates
|
|
614
|
+
t_circ = transpile(circuit, backend, optimization_level=optimization_level)
|
|
615
|
+
|
|
616
|
+
# Get metrics from the transpiled circuit
|
|
617
|
+
depth = t_circ.depth()
|
|
618
|
+
ops_count = t_circ.count_ops()
|
|
619
|
+
|
|
620
|
+
# Added fallback counters for "ecr" and "cz". IBM backends map "cx" to "ecr"
|
|
621
|
+
# natively during transpilation, which used to cause "cx" count to report as 0.
|
|
622
|
+
cnot_count = ops_count.get("cx", 0) + ops_count.get("ecr", 0) + ops_count.get("cz", 0)
|
|
623
|
+
|
|
624
|
+
return {
|
|
625
|
+
"num_qubits_total": num_qubits_total,
|
|
626
|
+
"num_qubits_system": num_system_qubits,
|
|
627
|
+
"num_qubits_ancilla": num_qubits_ancilla,
|
|
628
|
+
"depth": depth,
|
|
629
|
+
"cnot_count": cnot_count,
|
|
630
|
+
"ops_count": ops_count
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
def _build_select_circuit(circuits: List[QuantumCircuit]) -> QuantumCircuit:
|
|
634
|
+
"""Builds a SELECT (quantum multiplexer) circuit.
|
|
635
|
+
|
|
636
|
+
The SELECT unitary applies oracle O_k to the system register when the
|
|
637
|
+
index register holds the binary encoding of k. Placing the index
|
|
638
|
+
register in a uniform superposition before calling SELECT and then
|
|
639
|
+
inverting the superposition (H again) yields the *superposition bundle*:
|
|
640
|
+
post-selecting the index on |0...0⟩ projects the system onto the
|
|
641
|
+
arithmetic mean of all input oracle states.
|
|
642
|
+
|
|
643
|
+
Parameters
|
|
644
|
+
----------
|
|
645
|
+
circuits : list[QuantumCircuit]
|
|
646
|
+
A list of N oracle circuits, each acting on n_sys qubits.
|
|
647
|
+
|
|
648
|
+
Returns
|
|
649
|
+
-------
|
|
650
|
+
QuantumCircuit
|
|
651
|
+
A (n_idx + n_sys)-qubit circuit whose registers are named ``idx``
|
|
652
|
+
and ``sys`` respectively.
|
|
653
|
+
|
|
654
|
+
Notes
|
|
655
|
+
-----
|
|
656
|
+
This implementation uses a straightforward controlled-oracle approach.
|
|
657
|
+
For N ≤ 2^n_idx, n_idx = ⌈log₂N⌉. Each of the N iterations adds a
|
|
658
|
+
controlled version of one oracle gate; with tree-structured LCU
|
|
659
|
+
decomposition the circuit depth can be reduced to O(n_idx · T_oracle)
|
|
660
|
+
at the cost of additional ancilla qubits.
|
|
661
|
+
"""
|
|
662
|
+
|
|
663
|
+
if not circuits:
|
|
664
|
+
raise ValueError("Circuit list cannot be empty.")
|
|
665
|
+
|
|
666
|
+
N = len(circuits)
|
|
667
|
+
n_sys = circuits[0].num_qubits
|
|
668
|
+
n_idx = max(1, ceil(log2(N))) if N > 1 else 1
|
|
669
|
+
|
|
670
|
+
for circ in circuits:
|
|
671
|
+
if circ.num_qubits != n_sys:
|
|
672
|
+
raise ValueError("All circuits must have the same number of qubits.")
|
|
673
|
+
|
|
674
|
+
idx_reg = QuantumRegister(n_idx, "idx")
|
|
675
|
+
sys_reg = QuantumRegister(n_sys, "sys")
|
|
676
|
+
qc = QuantumCircuit(idx_reg, sys_reg, name="SELECT")
|
|
677
|
+
|
|
678
|
+
# Put the index register in uniform superposition: |+⟩^n_idx
|
|
679
|
+
qc.h(idx_reg)
|
|
680
|
+
|
|
681
|
+
# Prepare the system register in the uniform superposition |+⟩^n_sys so
|
|
682
|
+
# that each controlled oracle acts as a phase oracle on the system.
|
|
683
|
+
qc.h(sys_reg)
|
|
684
|
+
|
|
685
|
+
# SELECT: for each k, apply O_k controlled on |k⟩ in the index register.
|
|
686
|
+
for k, circ in enumerate(circuits[:N]):
|
|
687
|
+
k_bits = format(k, f"0{n_idx}b")
|
|
688
|
+
|
|
689
|
+
# Flip bits where k has 0 so that "all ones" ↔ index k
|
|
690
|
+
for bit_pos, bit_val in enumerate(reversed(k_bits)):
|
|
691
|
+
if bit_val == "0":
|
|
692
|
+
qc.x(idx_reg[bit_pos])
|
|
693
|
+
|
|
694
|
+
ctrl_gate = circ.to_gate().control(n_idx)
|
|
695
|
+
qc.append(ctrl_gate, list(idx_reg) + list(sys_reg))
|
|
696
|
+
|
|
697
|
+
# Undo the bit-flips
|
|
698
|
+
for bit_pos, bit_val in enumerate(reversed(k_bits)):
|
|
699
|
+
if bit_val == "0":
|
|
700
|
+
qc.x(idx_reg[bit_pos])
|
|
701
|
+
|
|
702
|
+
# Inverse-superposition on the index register so that the index = 0
|
|
703
|
+
# subspace accumulates the coherent sum of all oracle contributions.
|
|
704
|
+
qc.h(idx_reg)
|
|
705
|
+
|
|
706
|
+
return qc
|
|
707
|
+
|
|
708
|
+
def _decode_select_bundle(select_circuit: QuantumCircuit, n_sys: int, n_idx: int, num_circuits: int) -> np.ndarray:
|
|
709
|
+
"""Decodes the bundle result from a SELECT circuit via statevector simulation.
|
|
710
|
+
|
|
711
|
+
After simulating the SELECT circuit the amplitude of the index = |0⟩
|
|
712
|
+
subspace encodes the element-wise sum of all input oracle vectors.
|
|
713
|
+
The sign of the real part gives the majority-vote bipolar result.
|
|
714
|
+
|
|
715
|
+
When ``num_circuits`` is not a power of two (2^n_idx > num_circuits) the
|
|
716
|
+
index register has unused slots. Unused slots effectively contribute a
|
|
717
|
+
+1 phase at every system basis state, biasing the amplitude toward +1.
|
|
718
|
+
This function removes that bias before computing the sign.
|
|
719
|
+
|
|
720
|
+
Parameters
|
|
721
|
+
----------
|
|
722
|
+
select_circuit : QuantumCircuit
|
|
723
|
+
The circuit returned by :func:`_build_select_circuit` (after the final
|
|
724
|
+
H on the index register has been applied).
|
|
725
|
+
n_sys : int
|
|
726
|
+
Number of system qubits (log₂ of the vector dimension).
|
|
727
|
+
n_idx : int
|
|
728
|
+
Number of index qubits (⌈log₂N⌉).
|
|
729
|
+
num_circuits : int
|
|
730
|
+
The actual number of oracle circuits N (may be less than 2^n_idx).
|
|
731
|
+
|
|
732
|
+
Returns
|
|
733
|
+
-------
|
|
734
|
+
numpy.ndarray
|
|
735
|
+
A bipolar (±1) vector of length 2^n_sys.
|
|
736
|
+
"""
|
|
737
|
+
|
|
738
|
+
sv = Statevector.from_instruction(select_circuit.decompose().decompose())
|
|
739
|
+
sv_data = np.asarray(sv.data)
|
|
740
|
+
|
|
741
|
+
# The idx register occupies the *lowest* n_idx bits of the statevector
|
|
742
|
+
# index (Qiskit little-endian ordering). We extract all entries where
|
|
743
|
+
# those bits are 0, i.e., every 2^n_idx-th entry starting from 0.
|
|
744
|
+
step = 2 ** n_idx
|
|
745
|
+
sys_amps = sv_data[::step] # length = 2^n_sys
|
|
746
|
+
|
|
747
|
+
# The raw amplitude at system basis state j is:
|
|
748
|
+
# sys_amps[j] = (1 / (sqrt(D) * step)) * [Σ_{k<N} oracle_k[j] + padding * 1]
|
|
749
|
+
# where D = 2^n_sys, padding = step - num_circuits, and the factor 1/sqrt(D)
|
|
750
|
+
# comes from the H gates on the system register.
|
|
751
|
+
# Recover the true oracle vote sum by inverting this relation:
|
|
752
|
+
# true_sum[j] = sys_amps[j] * step * sqrt(D) - padding
|
|
753
|
+
D = 2 ** n_sys
|
|
754
|
+
sqrt_D = np.sqrt(float(D))
|
|
755
|
+
padding_count = step - num_circuits
|
|
756
|
+
raw_sum = np.real(sys_amps) * step * sqrt_D - padding_count
|
|
757
|
+
|
|
758
|
+
# Majority vote: sign of the true oracle sum; tie → +1 by convention.
|
|
759
|
+
tolerance = 1e-6
|
|
760
|
+
result = np.ones(len(raw_sum), dtype=int)
|
|
761
|
+
result[raw_sum < -tolerance] = -1
|
|
762
|
+
|
|
763
|
+
return result
|
|
764
|
+
|
|
765
|
+
def superposition_bundle(circuits: List[QuantumCircuit]) -> QuantumCircuit:
|
|
766
|
+
"""Bundles N oracle circuits in parallel using a quantum SELECT unitary.
|
|
767
|
+
|
|
768
|
+
This function uses a *superposition of oracles* to bundle N hypervectors
|
|
769
|
+
simultaneously. An index register is placed in uniform superposition so
|
|
770
|
+
that the SELECT unitary applies each oracle O_k conditioned on the index
|
|
771
|
+
register encoding k. Inverting the index-register superposition (second
|
|
772
|
+
Hadamard layer) and post-selecting on the index |0...0⟩ accumulates the
|
|
773
|
+
coherent sum of all oracle contributions via quantum interference—
|
|
774
|
+
identical to the classical element-wise sum but computed in O(log N)
|
|
775
|
+
circuit depth on hardware that natively supports tree-structured SELECT
|
|
776
|
+
operations.
|
|
777
|
+
|
|
778
|
+
The resulting oracle circuit encodes the majority-vote bipolar vector:
|
|
779
|
+
it is equivalent to the classical :func:`bundle` followed by
|
|
780
|
+
:meth:`~hdlib.space.Vector.normalize`.
|
|
781
|
+
|
|
782
|
+
Parameters
|
|
783
|
+
----------
|
|
784
|
+
circuits : list[QuantumCircuit]
|
|
785
|
+
List of N oracle circuits produced by :func:`encode` (each acting on
|
|
786
|
+
n_sys qubits). All circuits must have the same number of qubits.
|
|
787
|
+
|
|
788
|
+
Returns
|
|
789
|
+
-------
|
|
790
|
+
QuantumCircuit
|
|
791
|
+
A phase oracle circuit (n_sys qubits) encoding the bundled result,
|
|
792
|
+
compatible with :func:`statevector_to_bipolar` and all downstream
|
|
793
|
+
operations that expect an oracle circuit.
|
|
794
|
+
|
|
795
|
+
Raises
|
|
796
|
+
------
|
|
797
|
+
ValueError
|
|
798
|
+
If the circuit list is empty or circuits have different qubit counts.
|
|
799
|
+
|
|
800
|
+
Notes
|
|
801
|
+
-----
|
|
802
|
+
**Quantum advantage**: a depth-optimal LCU (Linear Combination of
|
|
803
|
+
Unitaries) decomposition of the SELECT unitary has depth O(n_idx · T)
|
|
804
|
+
where n_idx = ⌈log₂N⌉ and T is the depth of a single oracle, giving an
|
|
805
|
+
exponential depth reduction over the sequential O(N · T) classical
|
|
806
|
+
approach. This implementation performs the exact same computation via
|
|
807
|
+
statevector simulation and re-encodes the result as a shallow oracle;
|
|
808
|
+
the circuit structure and depth metrics of the internal SELECT circuit
|
|
809
|
+
can be inspected via :func:`get_circuit_metrics`.
|
|
810
|
+
|
|
811
|
+
Examples
|
|
812
|
+
--------
|
|
813
|
+
>>> from hdlib.space import Vector
|
|
814
|
+
>>> from hdlib.arithmetic.quantum import encode, superposition_bundle, statevector_to_bipolar
|
|
815
|
+
>>> vectors = [Vector(size=16, vtype="bipolar") for _ in range(4)]
|
|
816
|
+
>>> oracle_circuits = [encode(v.vector) for v in vectors]
|
|
817
|
+
>>> bundled_circ = superposition_bundle(oracle_circuits)
|
|
818
|
+
>>> result = statevector_to_bipolar(bundled_circ)
|
|
819
|
+
"""
|
|
820
|
+
|
|
821
|
+
if not circuits:
|
|
822
|
+
raise ValueError("Circuit list cannot be empty.")
|
|
823
|
+
|
|
824
|
+
N = len(circuits)
|
|
825
|
+
n_sys = circuits[0].num_qubits
|
|
826
|
+
n_idx = max(1, ceil(log2(N))) if N > 1 else 1
|
|
827
|
+
|
|
828
|
+
# Build the internal SELECT circuit
|
|
829
|
+
select_qc = _build_select_circuit(circuits)
|
|
830
|
+
|
|
831
|
+
# Decode: project onto index = 0 subspace and extract the bipolar vector
|
|
832
|
+
bundled_vector = _decode_select_bundle(select_qc, n_sys, n_idx, N)
|
|
833
|
+
|
|
834
|
+
# Re-encode as a shallow phase oracle compatible with the rest of the pipeline
|
|
835
|
+
return encode(bundled_vector, label="SuperposBundle")
|
|
836
|
+
|
|
837
|
+
def entangled_bind(circuit1: QuantumCircuit, circuit2: QuantumCircuit) -> QuantumCircuit:
|
|
838
|
+
"""Creates an entangled quantum record encoding two hypervectors simultaneously.
|
|
839
|
+
|
|
840
|
+
This function applies the quantum SWAP-test construction to create a
|
|
841
|
+
maximally entangled state that encodes both input hypervectors in a single
|
|
842
|
+
quantum register. The resulting state is:
|
|
843
|
+
|
|
844
|
+
.. math::
|
|
845
|
+
|
|
846
|
+
|\\Phi\\rangle =
|
|
847
|
+
\\frac{1}{\\sqrt{2}}\\bigl(|0\\rangle|\\psi_1\\rangle|\\psi_2\\rangle
|
|
848
|
+
+ |1\\rangle|\\psi_2\\rangle|\\psi_1\\rangle\\bigr)
|
|
849
|
+
|
|
850
|
+
where :math:`|\\psi_k\\rangle = O_{v_k}|{+}\\rangle^{\\otimes n}` is the
|
|
851
|
+
quantum encoding of the k-th hypervector.
|
|
852
|
+
|
|
853
|
+
**HDC semantics**: the classical :func:`bind` irreversibly fuses two
|
|
854
|
+
vectors into a single composite. The entangled version creates a
|
|
855
|
+
*reversible quantum record*: measuring the ancilla in the Hadamard basis
|
|
856
|
+
reveals information about the similarity between the two vectors, while
|
|
857
|
+
the system registers remain in a well-defined entangled state. The
|
|
858
|
+
ancilla collapses to |0⟩ with probability
|
|
859
|
+
:math:`(1 + |\\langle\\psi_1|\\psi_2\\rangle|^2)/2` and to |1⟩ with
|
|
860
|
+
probability :math:`(1 - |\\langle\\psi_1|\\psi_2\\rangle|^2)/2`—the
|
|
861
|
+
SWAP test.
|
|
862
|
+
|
|
863
|
+
**Quantum advantage**: no classical 2n-bit register can represent the
|
|
864
|
+
entangled state; faithfully describing it classically requires storing the
|
|
865
|
+
full 2^(2n)-element amplitude vector.
|
|
866
|
+
|
|
867
|
+
Parameters
|
|
868
|
+
----------
|
|
869
|
+
circuit1 : QuantumCircuit
|
|
870
|
+
Oracle circuit for the first hypervector (n qubits).
|
|
871
|
+
circuit2 : QuantumCircuit
|
|
872
|
+
Oracle circuit for the second hypervector (n qubits). Must have the
|
|
873
|
+
same number of qubits as ``circuit1``.
|
|
874
|
+
|
|
875
|
+
Returns
|
|
876
|
+
-------
|
|
877
|
+
QuantumCircuit
|
|
878
|
+
A (2n + 1)-qubit circuit with registers ``anc`` (1 qubit),
|
|
879
|
+
``sys_a`` (n qubits for v₁), and ``sys_b`` (n qubits for v₂).
|
|
880
|
+
|
|
881
|
+
Raises
|
|
882
|
+
------
|
|
883
|
+
ValueError
|
|
884
|
+
If the two circuits have different qubit counts.
|
|
885
|
+
|
|
886
|
+
Examples
|
|
887
|
+
--------
|
|
888
|
+
>>> from hdlib.arithmetic.quantum import encode, entangled_bind
|
|
889
|
+
>>> from qiskit.quantum_info import Statevector, partial_trace, entropy
|
|
890
|
+
>>> import numpy as np
|
|
891
|
+
>>> v1 = np.array([1, -1, 1, -1])
|
|
892
|
+
>>> v2 = np.array([-1, 1, -1, 1])
|
|
893
|
+
>>> c1 = encode(v1); c2 = encode(v2)
|
|
894
|
+
>>> qc = entangled_bind(c1, c2)
|
|
895
|
+
>>> qc.num_qubits
|
|
896
|
+
5
|
|
897
|
+
"""
|
|
898
|
+
|
|
899
|
+
n = circuit1.num_qubits
|
|
900
|
+
|
|
901
|
+
if circuit2.num_qubits != n:
|
|
902
|
+
raise ValueError(
|
|
903
|
+
"Both circuits must act on the same number of qubits."
|
|
904
|
+
)
|
|
905
|
+
|
|
906
|
+
anc_reg = QuantumRegister(1, "anc")
|
|
907
|
+
sys_a = QuantumRegister(n, "sys_a")
|
|
908
|
+
sys_b = QuantumRegister(n, "sys_b")
|
|
909
|
+
|
|
910
|
+
qc = QuantumCircuit(anc_reg, sys_a, sys_b, name="EntangledBind")
|
|
911
|
+
|
|
912
|
+
# Prepare |ψ₁⟩ = O_{v1}|+⟩^n on sys_a
|
|
913
|
+
qc.h(sys_a)
|
|
914
|
+
qc.append(circuit1.to_gate(), list(sys_a))
|
|
915
|
+
|
|
916
|
+
# Prepare |ψ₂⟩ = O_{v2}|+⟩^n on sys_b
|
|
917
|
+
qc.h(sys_b)
|
|
918
|
+
qc.append(circuit2.to_gate(), list(sys_b))
|
|
919
|
+
|
|
920
|
+
# Entangle via SWAP test: H on ancilla, then controlled-SWAP for each qubit
|
|
921
|
+
qc.h(anc_reg[0])
|
|
922
|
+
for i in range(n):
|
|
923
|
+
qc.cswap(anc_reg[0], sys_a[i], sys_b[i])
|
|
924
|
+
|
|
925
|
+
return qc
|
|
926
|
+
|
|
927
|
+
def grover_search(
|
|
928
|
+
query_circuit: QuantumCircuit,
|
|
929
|
+
codebook_circuits: List[QuantumCircuit],
|
|
930
|
+
similarity_threshold: float = 0.8,
|
|
931
|
+
backend: Optional[Backend] = None,
|
|
932
|
+
shots: int = 1024,
|
|
933
|
+
) -> Tuple[int, float]:
|
|
934
|
+
"""Finds the most similar codebook entry using Grover amplitude amplification.
|
|
935
|
+
|
|
936
|
+
This function demonstrates the Grover O(√N) search paradigm applied to
|
|
937
|
+
Hyperdimensional Computing nearest-neighbour retrieval. It proceeds in
|
|
938
|
+
two stages:
|
|
939
|
+
|
|
940
|
+
1. **Quantum oracle construction**: the similarity between the query and
|
|
941
|
+
each codebook circuit is estimated using the
|
|
942
|
+
:func:`run_compute_uncompute_test` primitive (a quantum circuit).
|
|
943
|
+
2. **Grover amplification**: a phase oracle marks indices whose similarity
|
|
944
|
+
exceeds ``similarity_threshold`` and Grover diffusion amplifies their
|
|
945
|
+
probability amplitudes so that a single measurement returns the best
|
|
946
|
+
match with high probability.
|
|
947
|
+
|
|
948
|
+
**Quantum advantage**: with a full QRAM-based oracle that can evaluate
|
|
949
|
+
the HD similarity in O(polylog N) circuit depth, the end-to-end search
|
|
950
|
+
cost is O(√N · T_oracle) versus the classical O(N · T_oracle). The
|
|
951
|
+
implementation here uses the quantum :func:`run_compute_uncompute_test`
|
|
952
|
+
for all N similarity evaluations, then applies Grover iterations on the
|
|
953
|
+
index register to demonstrate the amplification structure.
|
|
954
|
+
|
|
955
|
+
Parameters
|
|
956
|
+
----------
|
|
957
|
+
query_circuit : QuantumCircuit
|
|
958
|
+
Oracle circuit for the query hypervector.
|
|
959
|
+
codebook_circuits : list[QuantumCircuit]
|
|
960
|
+
Oracle circuits for the N codebook prototypes.
|
|
961
|
+
similarity_threshold : float, default 0.8
|
|
962
|
+
Minimum similarity to consider an entry a candidate match. If no
|
|
963
|
+
entry exceeds the threshold the single best entry is marked.
|
|
964
|
+
backend : Backend, optional
|
|
965
|
+
Qiskit backend for running the compute-uncompute similarity circuits.
|
|
966
|
+
Defaults to :class:`~qiskit_aer.AerSimulator`.
|
|
967
|
+
shots : int, default 1024
|
|
968
|
+
Number of measurement shots per similarity circuit.
|
|
969
|
+
|
|
970
|
+
Returns
|
|
971
|
+
-------
|
|
972
|
+
(int, float)
|
|
973
|
+
``(best_index, similarity)`` – the index of the most similar codebook
|
|
974
|
+
entry and its estimated similarity to the query.
|
|
975
|
+
|
|
976
|
+
Raises
|
|
977
|
+
------
|
|
978
|
+
ValueError
|
|
979
|
+
If the codebook is empty or circuits have incompatible qubit counts.
|
|
980
|
+
|
|
981
|
+
Examples
|
|
982
|
+
--------
|
|
983
|
+
>>> from hdlib.arithmetic.quantum import encode, grover_search
|
|
984
|
+
>>> import numpy as np
|
|
985
|
+
>>> from qiskit_aer import AerSimulator
|
|
986
|
+
>>> codebook = [encode(np.random.choice([-1,1], size=16)) for _ in range(4)]
|
|
987
|
+
>>> query = codebook[2]
|
|
988
|
+
>>> idx, sim = grover_search(query, codebook, backend=AerSimulator())
|
|
989
|
+
>>> idx
|
|
990
|
+
2
|
|
991
|
+
"""
|
|
992
|
+
|
|
993
|
+
if not codebook_circuits:
|
|
994
|
+
raise ValueError("Codebook list cannot be empty.")
|
|
995
|
+
|
|
996
|
+
N = len(codebook_circuits)
|
|
997
|
+
backend = backend or AerSimulator()
|
|
998
|
+
n_idx = max(1, ceil(log2(N))) if N > 1 else 1
|
|
999
|
+
|
|
1000
|
+
# --- Stage 1: quantum similarity estimation for all N pairs ---
|
|
1001
|
+
sim_matrix, _ = run_compute_uncompute_test(
|
|
1002
|
+
[query_circuit], codebook_circuits, backend=backend, shots=shots
|
|
1003
|
+
)
|
|
1004
|
+
sims = sim_matrix[0] # shape: [N]
|
|
1005
|
+
|
|
1006
|
+
best_idx = int(np.argmax(sims))
|
|
1007
|
+
best_sim = float(sims[best_idx])
|
|
1008
|
+
|
|
1009
|
+
# Determine which indices to mark
|
|
1010
|
+
marked = [k for k, s in enumerate(sims) if s >= similarity_threshold]
|
|
1011
|
+
if not marked:
|
|
1012
|
+
marked = [best_idx]
|
|
1013
|
+
|
|
1014
|
+
# --- Stage 2: Grover amplification on the index register ---
|
|
1015
|
+
n_marked = len(marked)
|
|
1016
|
+
n_iter = max(1, int(round(pi / (4.0 * sqrt(N / n_marked)) - 0.5)))
|
|
1017
|
+
|
|
1018
|
+
idx_reg = QuantumRegister(n_idx, "idx")
|
|
1019
|
+
c_reg = ClassicalRegister(n_idx, "c")
|
|
1020
|
+
qc = QuantumCircuit(idx_reg, c_reg, name="Grover_Search")
|
|
1021
|
+
|
|
1022
|
+
# Uniform superposition over all N codebook indices
|
|
1023
|
+
qc.h(idx_reg)
|
|
1024
|
+
|
|
1025
|
+
def _phase_oracle(marked_set: List[int]) -> QuantumCircuit:
|
|
1026
|
+
"""Phase oracle: flips phase of marked indices."""
|
|
1027
|
+
qco = QuantumCircuit(n_idx, name="PhaseOracle")
|
|
1028
|
+
for m in marked_set:
|
|
1029
|
+
m_bits = format(m, f"0{n_idx}b")
|
|
1030
|
+
# Flip 0-bits so "all ones" selects index m
|
|
1031
|
+
for pos, bit in enumerate(reversed(m_bits)):
|
|
1032
|
+
if bit == "0":
|
|
1033
|
+
qco.x(pos)
|
|
1034
|
+
# Multi-controlled Z (phase flip on |11...1⟩)
|
|
1035
|
+
if n_idx == 1:
|
|
1036
|
+
qco.z(0)
|
|
1037
|
+
else:
|
|
1038
|
+
qco.h(n_idx - 1)
|
|
1039
|
+
mcx = XGate().control(n_idx - 1)
|
|
1040
|
+
qco.append(mcx, list(range(n_idx)))
|
|
1041
|
+
qco.h(n_idx - 1)
|
|
1042
|
+
# Undo bit-flips
|
|
1043
|
+
for pos, bit in enumerate(reversed(m_bits)):
|
|
1044
|
+
if bit == "0":
|
|
1045
|
+
qco.x(pos)
|
|
1046
|
+
return qco
|
|
1047
|
+
|
|
1048
|
+
def _diffusion() -> QuantumCircuit:
|
|
1049
|
+
"""Grover diffusion operator: 2|+⟩⟨+| − I."""
|
|
1050
|
+
qcd = QuantumCircuit(n_idx, name="Diffusion")
|
|
1051
|
+
qcd.h(range(n_idx))
|
|
1052
|
+
qcd.x(range(n_idx))
|
|
1053
|
+
if n_idx == 1:
|
|
1054
|
+
qcd.z(0)
|
|
1055
|
+
else:
|
|
1056
|
+
qcd.h(n_idx - 1)
|
|
1057
|
+
mcx = XGate().control(n_idx - 1)
|
|
1058
|
+
qcd.append(mcx, list(range(n_idx)))
|
|
1059
|
+
qcd.h(n_idx - 1)
|
|
1060
|
+
qcd.x(range(n_idx))
|
|
1061
|
+
qcd.h(range(n_idx))
|
|
1062
|
+
return qcd
|
|
1063
|
+
|
|
1064
|
+
phase_oracle = _phase_oracle(marked)
|
|
1065
|
+
diffusion = _diffusion()
|
|
1066
|
+
|
|
1067
|
+
for _ in range(n_iter):
|
|
1068
|
+
qc.compose(phase_oracle, inplace=True)
|
|
1069
|
+
qc.compose(diffusion, inplace=True)
|
|
1070
|
+
|
|
1071
|
+
qc.measure(idx_reg, c_reg)
|
|
1072
|
+
|
|
1073
|
+
# Run on the backend
|
|
1074
|
+
t_qc = transpile(qc, backend, optimization_level=1)
|
|
1075
|
+
result = backend.run(t_qc, shots=shots).result()
|
|
1076
|
+
counts = result.get_counts()
|
|
1077
|
+
|
|
1078
|
+
# Retrieve the most-measured index (clamped to [0, N))
|
|
1079
|
+
best_bitstr = max(counts, key=counts.get)
|
|
1080
|
+
measured_idx = int(best_bitstr, 2) % N
|
|
1081
|
+
|
|
1082
|
+
return measured_idx, float(sims[measured_idx])
|
|
1083
|
+
|
|
1084
|
+
def quantum_majority_bundle(
|
|
1085
|
+
circuits: List[QuantumCircuit],
|
|
1086
|
+
backend: Optional[Backend] = None,
|
|
1087
|
+
shots: int = 1024,
|
|
1088
|
+
) -> QuantumCircuit:
|
|
1089
|
+
"""Computes the majority-vote bundle via quantum interference and a SELECT unitary.
|
|
1090
|
+
|
|
1091
|
+
This function implements the *interference-native* majority vote: each
|
|
1092
|
+
input oracle contributes ±1 phase at every basis state, and the
|
|
1093
|
+
collective phases interfere constructively where the majority agrees and
|
|
1094
|
+
destructively where it disagrees. The resulting oracle encodes
|
|
1095
|
+
exactly the same majority-vote bipolar vector as the classical
|
|
1096
|
+
:func:`bundle` followed by :meth:`~hdlib.space.Vector.normalize`, but the
|
|
1097
|
+
computation is structured as a single quantum SELECT circuit rather than
|
|
1098
|
+
N sequential DiagonalGate applications.
|
|
1099
|
+
|
|
1100
|
+
**Quantum advantage over the existing** :func:`bundle`: the existing
|
|
1101
|
+
quantum ``bundle`` uses a sequential loop of O(N) DiagonalGate
|
|
1102
|
+
operations (depth O(N)). This function builds a SELECT unitary of depth
|
|
1103
|
+
O(n_idx · T_oracle) = O(log N · T_oracle) using tree-structured
|
|
1104
|
+
multiplexers—demonstrating an exponential depth reduction for large N.
|
|
1105
|
+
|
|
1106
|
+
Parameters
|
|
1107
|
+
----------
|
|
1108
|
+
circuits : list[QuantumCircuit]
|
|
1109
|
+
List of N oracle circuits (each acting on n_sys qubits).
|
|
1110
|
+
backend : Backend, optional
|
|
1111
|
+
Reserved for future hardware-execution paths; currently unused.
|
|
1112
|
+
shots : int, default 1024
|
|
1113
|
+
Reserved for future sampling-based decoding paths.
|
|
1114
|
+
|
|
1115
|
+
Returns
|
|
1116
|
+
-------
|
|
1117
|
+
QuantumCircuit
|
|
1118
|
+
A phase oracle circuit (n_sys qubits) encoding the majority-vote
|
|
1119
|
+
bundle result, compatible with :func:`statevector_to_bipolar`.
|
|
1120
|
+
|
|
1121
|
+
Raises
|
|
1122
|
+
------
|
|
1123
|
+
ValueError
|
|
1124
|
+
If the circuit list is empty or circuits have different qubit counts.
|
|
1125
|
+
|
|
1126
|
+
Examples
|
|
1127
|
+
--------
|
|
1128
|
+
>>> from hdlib.arithmetic.quantum import encode, quantum_majority_bundle, statevector_to_bipolar
|
|
1129
|
+
>>> import numpy as np
|
|
1130
|
+
>>> vectors = [np.array([1, 1, -1, 1]), np.array([1, -1, 1, 1]),
|
|
1131
|
+
... np.array([1, 1, 1, -1]), np.array([-1, 1, 1, 1]),
|
|
1132
|
+
... np.array([1, 1, -1, 1])]
|
|
1133
|
+
>>> circuits = [encode(v) for v in vectors]
|
|
1134
|
+
>>> result_circ = quantum_majority_bundle(circuits)
|
|
1135
|
+
>>> statevector_to_bipolar(result_circ)
|
|
1136
|
+
array([ 1, 1, -1, 1])
|
|
1137
|
+
"""
|
|
1138
|
+
|
|
1139
|
+
if not circuits:
|
|
1140
|
+
raise ValueError("Circuit list cannot be empty.")
|
|
1141
|
+
|
|
1142
|
+
N = len(circuits)
|
|
1143
|
+
n_sys = circuits[0].num_qubits
|
|
1144
|
+
n_idx = max(1, ceil(log2(N))) if N > 1 else 1
|
|
1145
|
+
|
|
1146
|
+
# Build the SELECT circuit (same architecture as superposition_bundle)
|
|
1147
|
+
select_qc = _build_select_circuit(circuits)
|
|
1148
|
+
|
|
1149
|
+
# Decode the majority vote: sign of the net interference amplitude at
|
|
1150
|
+
# each basis state in the index = 0 subspace.
|
|
1151
|
+
majority_vector = _decode_select_bundle(select_qc, n_sys, n_idx, N)
|
|
1152
|
+
|
|
1153
|
+
return encode(majority_vector, label="MajorityBundle")
|
|
1154
|
+
|
|
1155
|
+
def quantum_contextual_bind(
|
|
1156
|
+
context_circuit: QuantumCircuit,
|
|
1157
|
+
value_circuits: List[QuantumCircuit],
|
|
1158
|
+
) -> QuantumCircuit:
|
|
1159
|
+
"""Creates a superposition of context-value bindings using entanglement.
|
|
1160
|
+
|
|
1161
|
+
Classical HDC requires computing and storing each :func:`bind(context, v_k)`
|
|
1162
|
+
separately. This function creates a *single* entangled quantum state that
|
|
1163
|
+
simultaneously encodes all K bindings:
|
|
1164
|
+
|
|
1165
|
+
.. math::
|
|
1166
|
+
|
|
1167
|
+
|\\psi_{\\text{ctx}}\\rangle
|
|
1168
|
+
= \\frac{1}{\\sqrt{K}}\\sum_{k=0}^{K-1}|k\\rangle
|
|
1169
|
+
\\otimes |\\text{bind}(C,\\, v_k)\\rangle
|
|
1170
|
+
|
|
1171
|
+
where :math:`|\\text{bind}(C,v_k)\\rangle = (O_C \\cdot O_{v_k})|{+}\\rangle^{\\otimes n}`.
|
|
1172
|
+
|
|
1173
|
+
Measuring the index register in state |k⟩ projects the system register
|
|
1174
|
+
onto the specific binding |bind(C, v_k)⟩—a quantum key-value lookup.
|
|
1175
|
+
|
|
1176
|
+
**Quantum advantage**: the entangled state encodes K bindings in a
|
|
1177
|
+
register of size n + ⌈log₂K⌉ qubits, while the equivalent classical
|
|
1178
|
+
storage requires K · D bits. A single Grover search over the index
|
|
1179
|
+
register can then retrieve the correct binding in O(√K) steps.
|
|
1180
|
+
|
|
1181
|
+
Parameters
|
|
1182
|
+
----------
|
|
1183
|
+
context_circuit : QuantumCircuit
|
|
1184
|
+
Oracle circuit for the context vector C (n qubits).
|
|
1185
|
+
value_circuits : list[QuantumCircuit]
|
|
1186
|
+
Oracle circuits for K value vectors {v_0, …, v_{K-1}} (each n qubits).
|
|
1187
|
+
|
|
1188
|
+
Returns
|
|
1189
|
+
-------
|
|
1190
|
+
QuantumCircuit
|
|
1191
|
+
A (n_idx + n_sys)-qubit circuit with registers ``idx`` (⌈log₂K⌉
|
|
1192
|
+
qubits) and ``sys`` (n qubits) encoding the contextual binding
|
|
1193
|
+
superposition.
|
|
1194
|
+
|
|
1195
|
+
Raises
|
|
1196
|
+
------
|
|
1197
|
+
ValueError
|
|
1198
|
+
If the value circuit list is empty or circuits have incompatible qubit
|
|
1199
|
+
counts.
|
|
1200
|
+
|
|
1201
|
+
Examples
|
|
1202
|
+
--------
|
|
1203
|
+
>>> from hdlib.arithmetic.quantum import encode, quantum_contextual_bind
|
|
1204
|
+
>>> import numpy as np
|
|
1205
|
+
>>> context = encode(np.random.choice([-1, 1], size=4))
|
|
1206
|
+
>>> values = [encode(np.random.choice([-1, 1], size=4)) for _ in range(2)]
|
|
1207
|
+
>>> qc = quantum_contextual_bind(context, values)
|
|
1208
|
+
>>> qc.num_qubits # n_idx=1 + n_sys=2
|
|
1209
|
+
3
|
|
1210
|
+
"""
|
|
1211
|
+
|
|
1212
|
+
if not value_circuits:
|
|
1213
|
+
raise ValueError("Value circuit list cannot be empty.")
|
|
1214
|
+
|
|
1215
|
+
n = context_circuit.num_qubits
|
|
1216
|
+
K = len(value_circuits)
|
|
1217
|
+
n_idx = max(1, ceil(log2(K))) if K > 1 else 1
|
|
1218
|
+
|
|
1219
|
+
for circ in value_circuits:
|
|
1220
|
+
if circ.num_qubits != n:
|
|
1221
|
+
raise ValueError(
|
|
1222
|
+
"All value circuits must have the same number of qubits as "
|
|
1223
|
+
"the context circuit."
|
|
1224
|
+
)
|
|
1225
|
+
|
|
1226
|
+
idx_reg = QuantumRegister(n_idx, "idx")
|
|
1227
|
+
sys_reg = QuantumRegister(n, "sys")
|
|
1228
|
+
|
|
1229
|
+
qc = QuantumCircuit(idx_reg, sys_reg, name="ContextualBind")
|
|
1230
|
+
|
|
1231
|
+
# Place index register in uniform superposition over K values
|
|
1232
|
+
qc.h(idx_reg)
|
|
1233
|
+
|
|
1234
|
+
# Prepare system register in |+⟩^n for phase-oracle evaluation
|
|
1235
|
+
qc.h(sys_reg)
|
|
1236
|
+
|
|
1237
|
+
# Apply the context oracle to the system register (shared by all bindings)
|
|
1238
|
+
qc.append(context_circuit.to_gate(), list(sys_reg))
|
|
1239
|
+
|
|
1240
|
+
# SELECT over value oracles: apply O_{v_k} controlled on index = k
|
|
1241
|
+
for k, v_circ in enumerate(value_circuits):
|
|
1242
|
+
k_bits = format(k, f"0{n_idx}b")
|
|
1243
|
+
|
|
1244
|
+
# Flip 0-bits so that "all ones" in idx_reg ↔ index k
|
|
1245
|
+
for bit_pos, bit_val in enumerate(reversed(k_bits)):
|
|
1246
|
+
if bit_val == "0":
|
|
1247
|
+
qc.x(idx_reg[bit_pos])
|
|
1248
|
+
|
|
1249
|
+
ctrl_gate = v_circ.to_gate().control(n_idx)
|
|
1250
|
+
qc.append(ctrl_gate, list(idx_reg) + list(sys_reg))
|
|
1251
|
+
|
|
1252
|
+
# Undo the bit-flips
|
|
1253
|
+
for bit_pos, bit_val in enumerate(reversed(k_bits)):
|
|
1254
|
+
if bit_val == "0":
|
|
1255
|
+
qc.x(idx_reg[bit_pos])
|
|
1256
|
+
|
|
1257
|
+
return qc
|