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