1. Problem Statement
Aim to recover a secret scalar k ∈ Z_8 such that:
Q = kP
in a toy elliptic curve group of order 8, denoted as ⟨P⟩ ⊂ E(F_11). The discrete logarithm problem is reduced to:
Given Q, find k such that Q = kP
This is equivalent to solving:
f(a, b) = a + kb mod 8
where a, b ∈ Z_8, and then compute f(a, b) = aP + bQ. The quantum algorithm will extract k from the interference pattern of the quantum Fourier transformed registers.
2. Quantum Registers
Clock Register: 1 qubit (Bloch clock), used for quantum timekeeping visualization
Register a: 3 qubits for exponent a ∈ {0, …, 7}
Register b: 3 qubits for exponent b ∈ {0, …, 7}
Register p: 3 qubits initialized to ∣0⟩, stores the result f(a, b) ∈ Z_8
Classical Registers: 6 bits (for a and b), 1 bit for clock
3. Bloch Clock Initialization
Initialize the Bloch clock qubit to track quantum 'ticks' as state evolution:
Start in ∣0⟩ -H-> 1/√2 (∣0⟩ + ∣1⟩)
Then evolve it via a sequence of rotations:
Rotation about the X-axis:
RX(θ) = exp(-i * θ * X / 2)
Rotation about the Y-axis:
RY(θ) = exp(-i * θ * Y / 2)
Rotation about the Z-axis:
RZ(θ) = exp(-i * θ * Z / 2)
This produces a non-trivial evolution path on the Bloch sphere, a quantum clock 'tick'.
4. Superposition Preparation
Apply Hadamard gates on the 3 qubits in each of the a and b registers:
H^(⊗3) ∣000⟩ = 1/√8 ∑_(a=0)^7 ∣a⟩
H^(⊗3) ∣000⟩ = 1/√8_(b=0)^7 ∑ ∣b⟩
The total initial state becomes:
1/8 ∑_(a,b=0)^7 ∣a⟩ ∣b⟩ ∣0⟩ ⊗ ∣ψ_clock⟩
5. Oracle Construction U_f
Define a reversible map:
∣a⟩ ∣b⟩ ∣0⟩ -> ∣a⟩ ∣b⟩ ∣f(a, b)⟩
Where f(a, b) = a + kb mod 8
To implement this:
For each bit a_i, controlled add (2^i)(P mod 8)
For each bit b_i, controlled add (2^i)(Q mod 8), where Q = 7P
The modular additions are realized as 3-qubit unitary gates, created as:
∣x⟩ -> ∣x + c mod 8⟩
and controlled on the respective bit of a or b.
6. Barrier and Discarding Point Register
After applying the oracle, a barrier is inserted:
qc.barrier()
Since only the phase relation of a and b is needed, the register p is discarded post-oracle.
7. Apply Quantum Fourier Transform
Apply the QFT to both the a and b registers:
QFT_3 ∣a⟩ = 1/√8 ∑_(u=0)^7 e^(2πiau/8) ∣u⟩
QFT_3 ∣b⟩ = 1/√8 ∑_(v=0)^7 e^(2πibv/8) ∣v⟩
Resulting in the full state:
1/(8√8) ∑_(a,b,u,v) e^((2πi/8)(au + bv))δ_a + kb ≡ 0 ∣u⟩ ∣v⟩
This interference enforces:
u + kv ≡ 0 mod 8
producing a diagonal ridge in the 8x8 measurement space.
8. Measurement
Measure all qubits:
Registers a and b are measured into 6 classical bits
The Bloch clock qubit is measured into 1 classical bit
9. Classical Post-Processing
From each measurement result:
Parse bitstrings to integers a, b
Discard results where gcd(b, 8) != 1
Compute candidate keys:
k = (-a)(b^(-1)) mod 8
From the top 10 most frequent (a, b) pairs, search for a candidate k = 7. Once found, the run is successful.
10. Saving Results
All raw counts from register a, b outcomes and Bloch clock outcomes are saved to a Json file for further visualization.
11. Bloch Clock Visualization
Reconstruct the state of the Bloch clock qubit from its known evolution gates:
H -> R_X(π/4) -> R_Y(π/4) -> R_Z(π/4)
The resulting quantum state vector is visualized on the Bloch sphere using:
plot_bloch_multivector(state)
This allows geometric tracking of 'quantum time' during the computation.
Code:
# Main circuit
# Imports
import logging, json
from math import gcd
import numpy as np
import pandas as pd
from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister, transpile
from qiskit.circuit.library import UnitaryGate, QFT
from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2
from qiskit.visualization import plot_histogram, plot_bloch_multivector
from qiskit.quantum_info import Statevector
import matplotlib.pyplot as plt
# Settings
TOKEN = "YOUR_IBMQ_KEY"
INSTANCE = "YOUR_IBMQ_CRN"
BACKEND = "ibm_torino"
CAL_CSV = "/Users/steventippeconnic/Downloads/ibm_torino_calibrations_2025-06-30T03_55_36Z.csv"
SHOTS = 8192
ORDER = 8
P_IDX = 1
Q_IDX = 7
TARGET_K = 7
# Logging
logging.basicConfig(level=logging .INFO, format="%(asctime)s | %(levelname)s | %(message)s")
log = logging.getLogger(__name__)
# Calibration Based Qubit Selection
def best_qubits(csv_path: str, n: int) -> list[int]:
df = pd .read_csv(csv_path)
df.columns = df.columns.str.strip()
winners = df.sort_values(["√x (sx) error", "T1 (us)", "T2 (us)"], ascending=[True, False, False])["Qubit"].head(n).tolist()
log .info("Best physical qubits: %s", winners)
return winners
N_Q_TOTAL = 3 + 3 + 3
PHYSICAL = best_qubits(CAL_CSV, N_Q_TOTAL)
# Adders
def add_const_mod8_gate(c: int) -> UnitaryGate:
mat = np.zeros((8, 8))
for x in range(8):
mat[(x + c) % 8, x] = 1
return UnitaryGate(mat, label=f"+{c}")
ADDERS = {c: add_const_mod8_gate(c) for c in range(1, 8)}
def controlled_add(qc, ctrl_qubit, point_reg, constant):
qc.append(ADDERS[constant].control(), [ctrl_qubit, *point_reg])
def ecdlp_oracle(qc, a_reg, b_reg, point_reg):
for i in range(3):
constant = (P_IDX * (1 << i)) % ORDER
if constant:
controlled_add(qc, a_reg[i], point_reg, constant)
for i in range(3):
constant = (Q_IDX * (1 << i)) % ORDER
if constant:
controlled_add(qc, b_reg[i], point_reg, constant)
# Circuit
def shor_ecdlp_with_bloch_clock():
clk = QuantumRegister(1, "clk")
a = QuantumRegister(3, "a")
b = QuantumRegister(3, "b")
p = QuantumRegister(3, "p")
c = ClassicalRegister(6, "c")
clk_c = ClassicalRegister(1, "c_clk")
qc = QuantumCircuit(clk, a, b, p, c, clk_c)
qc.h(clk[0])
qc.rx(np.pi / 4, clk[0])
qc.ry(np.pi / 4, clk[0])
qc.rz(np.pi / 4, clk[0])
qc.h(a)
qc.h(b)
ecdlp_oracle(qc, a, b, p)
qc.barrier()
qc.append(QFT(3, do_swaps=False), a)
qc.append(QFT(3, do_swaps=False), b)
qc.measure(a, c[:3])
qc.measure(b, c[3:])
qc.measure(clk[0], clk_c[0])
return qc
# Run on IBMQ
service = QiskitRuntimeService(channel="ibm_cloud", token=TOKEN, instance=INSTANCE)
backend = service.backend(BACKEND)
log .info("Backend → %s", backend .name)
qc = shor_ecdlp_with_bloch_clock()
trans = transpile(qc, backend=backend, initial_layout=PHYSICAL + [0], optimization_level=3)
log .info("Circuit depth %d, gate counts %s", trans.depth(), trans.count_ops())
sampler = SamplerV2(mode=backend)
job = sampler .run([trans], shots=SHOTS)
result = job.result()
# Extract results
cregs = {creg .name: creg.size for creg in trans.cregs}
clk_name = [name for name in cregs if "clk" in name][0]
main_name = [name for name in cregs if "c" == name][0]
clk_counts = result[0].data.__getattribute__(clk_name).get_counts()
main_counts_raw = result[0].data.__getattribute__(main_name).get_counts()
# Postprocess ECC recovery
def bits_to_int(bs): return int(bs[::-1], 2)
counts = {(bits_to_int(k[3:]), bits_to_int(k[:3])): v for k, v in main_counts_raw.items()}
top = sorted(counts.items(), key=lambda kv: kv[1], reverse=True)
k_found = None
for i, ((a_val, b_val), freq) in enumerate(top[:10]):
if gcd(b_val, ORDER) != 1:
continue
inv_b = pow(b_val, -1, ORDER)
k_candidate = (-a_val * inv_b) % ORDER
log .info("Candidate k = %d from (a=%d, b=%d, count=%d)", k_candidate, a_val, b_val, freq)
if k_candidate == TARGET_K:
k_found = k_candidate
print(f"\nSUCCESS — recovered correct k = {k_candidate} from top-{i+1} (a={a_val}, b={b_val}, count={freq})")
break
if k_found is None:
print("\nNo correct key found in top 10; try more shots or examine further.")
# Save results
out = {
"experiment": "ECDLP_8pts_Shors_BlochClock",
"backend": backend .name,
"physical_qubits": PHYSICAL,
"shots": SHOTS,
"counts": main_counts_raw,
"clock_counts": clk_counts
}
JSON_PATH = "/Users/steventippeconnic/Documents/QC/ECDLP_8pts_Shors_Run_Bloch_Clock_0.json"
with open(JSON_PATH, "w") as fp:
json.dump(out, fp, indent=4)
log .info("Results saved → %s", JSON_PATH)
# Bloch Clock Visualization (Ideal)
qc_bloch = QuantumCircuit(1)
qc_bloch.h(0)
qc_bloch.rx(np.pi / 4, 0)
qc_bloch.ry(np.pi / 4, 0)
qc_bloch.rz(np.pi / 4, 0)
state = Statevector.from_instruction(qc_bloch)
plot_bloch_multivector(state)
plt .show()
# End
/////////////////////////////////////////////////////////////////
# Code for all visuals from experiment JSON
# Imports
import json
import matplotlib.pyplot as plt
from qiskit.visualization import plot_histogram
from qiskit.quantum_info import Statevector
from qiskit import QuantumCircuit
import numpy as np
from collections import defaultdict, Counter
from math import gcd, log2
import seaborn as sns
from mpl_toolkits.mplot3d import Axes3D
from collections import defaultdict
from math import gcd, pow
from sympy import mod_inverse
# Load Json results from run
file_path = '/Users/steventippeconnic/Documents/QC/ECDLP_8pts_Shors_Run_Bloch_Clock_0.json'
with open(file_path, 'r') as f:
data = json.load(f)
counts = data['counts']
clock_counts = data['clock_counts']
ORDER = 8
total_shots = sum(clock_counts.values())
frac_0 = clock_counts["0"] / total_shots
frac_1 = clock_counts["1"] / total_shots
def modinv(b, m):
if gcd(b, m) != 1:
return None
x0, x1 = 0, 1
m0 = m
while b > 1:
q = b // m
b, m = m, b % m
x0, x1 = x1 - q * x0, x0
return x1 % m0
def bits_to_int(bs): return int(bs[::-1], 2)
parsed = {(bits_to_int(k[3:]), bits_to_int(k[:3])): v for k, v in counts.items()}
# Scale counts by clock fractions
counts_0 = defaultdict(int)
counts_1 = defaultdict(int)
for bitstr, val in counts.items():
counts_0[bitstr] += val * frac_0
counts_1[bitstr] += val * frac_1
parsed_0 = {(bits_to_int(k[3:]), bits_to_int(k[:3])): v for k, v in counts_0.items()}
parsed_1 = {(bits_to_int(k[3:]), bits_to_int(k[:3])): v for k, v in counts_1.items()}
# ECC Histogram
plot_histogram(counts, title="ECC Bitstring Frequency")
plt.show()
# Clock Collapse Bias
plot_histogram(clock_counts, title="Bloch Clock Collapse Probability")
plt.show()
# Reconstructed Bloch Clock Path
qc_bloch = QuantumCircuit(1)
qc_bloch.h(0)
qc_bloch.rx(np.pi / 4, 0)
qc_bloch.ry(np.pi / 4, 0)
qc_bloch.rz(np.pi / 4, 0)
state = Statevector.from_instruction(qc_bloch)
from qiskit.visualization import plot_bloch_multivector
plot_bloch_multivector(state, title="Bloch Clock Statevector")
plt.show()
# ECC Top Key Candidates
from collections import Counter
def bits_to_int(bs): return int(bs[::-1], 2)
parsed_counts = {
(bits_to_int(k[3:]), bits_to_int(k[:3])): v for k, v in counts.items()
}
top = sorted(parsed_counts.items(), key=lambda kv: kv[1], reverse=True)[:10]
labels = [f"(a={a}, b={b})" for (a, b), _ in top]
values = [v for _, v in top]
plt.bar(labels, values)
plt.title("Top 10 (a, b) Pairs — ECC Ridge Structure")
plt.xticks(rotation=45)
plt.ylabel("Count")
plt.tight_layout()
plt.show()
# Residue-Class Heatmap u + kv ≡ 0 mod 8
residue_matrix = np.zeros((8, 8))
for (a, b), count in parsed.items():
u, v = a, b
for k in range(ORDER):
if (u + k * v) % ORDER == 0:
residue_matrix[u][v] += count
break
plt.figure(figsize=(6,5))
sns.heatmap(residue_matrix, annot=True, fmt='g', cmap='mako')
plt.title("Residue-Class Ridge Support (u + k·v ≡ 0 mod 8)")
plt.xlabel("v")
plt.ylabel("u")
plt.tight_layout()
plt.show()
# Clock-Conditioned Entropy of ECC Output
clock_0_frac = clock_counts["0"] / (clock_counts["0"] + clock_counts["1"])
total_shots = sum(clock_counts.values())
# Conditional counts
counts_0 = {k: v * clock_0_frac for k, v in counts.items()}
counts_1 = {k: v * (1 - clock_0_frac) for k, v in counts.items()}
def entropy(d):
total = sum(d.values())
probs = [v / total for v in d.values() if v > 0]
return -sum(p * log2(p) for p in probs)
ent_0 = entropy(counts_0)
ent_1 = entropy(counts_1)
plt.bar(["Clock = 0", "Clock = 1"], [ent_0, ent_1], color=["blue", "purple"])
plt.title("Entropy of ECC Results Conditioned on Clock Collapse")
plt.ylabel("Shannon Entropy (bits)")
plt.tight_layout()
plt.show()
# Bitwise Symmetry Violation in (a, b)
bit_flips = [0]*6
for k, v in counts.items():
for i in range(6):
if k[i] == '1':
bit_flips[i] += v
total = sum(counts.values())
bit_bias = [f / total for f in bit_flips]
plt.bar(range(6), bit_bias, color="crimson")
plt.title("Bitwise 1-Bias in ECC Register (Symmetry Violation)")
plt.xlabel("Bit index (0 = LSB of b)")
plt.ylabel("P(bit=1)")
plt.xticks(range(6), labels=["b0","b1","b2","a0","a1","a2"])
plt.tight_layout()
plt.show()
# ECC Candidate k Support Bubble Chart
candidate_support = defaultdict(int)
for (a, b), freq in parsed.items():
if gcd(b, ORDER) != 1:
continue
inv_b = mod_inverse(b, ORDER)
k = (-a * inv_b) % ORDER
candidate_support[k] += 1
plt.scatter(list(candidate_support.keys()),
list(candidate_support.values()),
s=[v*10 for v in candidate_support.values()],
color="darkgreen")
plt.title("Support Spread for Each ECC Candidate k")
plt.xlabel("k candidate")
plt.ylabel("Supporting (a, b) Pairs")
plt.xticks(range(8))
plt.tight_layout()
plt.show()
# Clock-Sliced Ridge Fidelity
ridge_k = 7
ridge_0 = []
ridge_1 = []
for (a, b), val in parsed_0.items():
if gcd(b, ORDER) == 1:
inv_b = modinv(b, ORDER)
k = (-a * inv_b) % ORDER
if k == ridge_k:
ridge_0.append(val)
for (a, b), val in parsed_1.items():
if gcd(b, ORDER) == 1:
inv_b = modinv(b, ORDER)
k = (-a * inv_b) % ORDER
if k == ridge_k:
ridge_1.append(val)
plt.bar(["Clock=0", "Clock=1"], [sum(ridge_0), sum(ridge_1)], color=["cyan", "magenta"])
plt.title("Ridge Fidelity per Clock Slice (k = 7)")
plt.ylabel("Total Ridge-Aligned Counts")
plt.tight_layout()
plt.show()
# Clock-Sliced k Candidate Histogram
def extract_k(parsed):
k_hist = defaultdict(int)
for (a, b), v in parsed.items():
if gcd(b, ORDER) != 1:
continue
inv_b = modinv(b, ORDER)
if inv_b is not None:
k = (-a * inv_b) % ORDER
k_hist[k] += v
return k_hist
k0 = extract_k(parsed_0)
k1 = extract_k(parsed_1)
keys = sorted(set(k0.keys()) | set(k1.keys()))
plt.plot(keys, [k0.get(k, 0) for k in keys], label="Clock=0", marker='o')
plt.plot(keys, [k1.get(k, 0) for k in keys], label="Clock=1", marker='s')
plt.title("Candidate k Frequency by Clock Slice")
plt.xlabel("k")
plt.ylabel("Count")
plt.xticks(range(8))
plt.legend()
plt.tight_layout()
plt.show()
# Clock-Sliced Bitstring Overlap Heatmap
all_keys = set(counts_0.keys()).union(counts_1.keys())
heatmap = []
for key in all_keys:
val0 = counts_0.get(key, 0)
val1 = counts_1.get(key, 0)
heatmap.append([val0, val1])
heatmap = np.array(heatmap)
sns.heatmap(heatmap.T, cmap='viridis', cbar=True, xticklabels=False, yticklabels=["Clock=0", "Clock=1"])
plt.title("ECC Bitstring Overlap Across Clock Slices")
plt.xlabel("Bitstring Index (sorted)")
plt.tight_layout()
plt.show()
# Clock-Sliced KL Divergence
def entropy(d):
total = sum(d.values())
if total == 0: return 0
probs = [v / total for v in d.values() if v > 0]
return -sum(p * log2(p) for p in probs)
def uniform_KL(d):
total = sum(d.values())
if total == 0: return 0
uniform_p = 1 / len(d)
return sum((v / total) * log2((v / total) / uniform_p) for v in d.values() if v > 0)
kl_0 = uniform_KL(counts_0)
kl_1 = uniform_KL(counts_1)
plt.bar(["Clock=0", "Clock=1"], [kl_0, kl_1], color=["green", "orange"])
plt.title("KL Divergence from Uniform by Clock Slice")
plt.ylabel("KL Divergence (bits)")
plt.tight_layout()
plt.show()
# End.
//////////////////////////////////////////////////////////
Code to analyze Bloch Clock
# Imports
import base64, io, json, pathlib, zlib
from collections import Counter
import numpy as np
import matplotlib.pyplot as plt
from qiskit import QuantumCircuit
from qiskit.quantum_info import Statevector
# File locations
INFO_PATH = pathlib.Path("/Users/steventippeconnic/Documents/QC/job-d1h1172576rs73cp0nn0-info.json")
RESULT_PATH = pathlib.Path("/Users/steventippeconnic/Documents/QC/job-d1h1172576rs73cp0nn0-result.json")
# Helpers
def decode_field(field_dict) -> np.ndarray:
"""field_dict → np.ndarray (shots × bits or shots × 1 uint8)"""
b64 = field_dict["__value__"]["array"]["__value__"]
raw = zlib.decompress(base64.b64decode(b64))
ndarray = np.load(io.BytesIO(raw), allow_pickle=False)
return ndarray
def unpack_bits(packed: np.ndarray) -> np.ndarray:
"""
(shots,1) uint8 → (shots,6) uint8, LSB-first
Example: 0b b2 b1 b0 a2 a1 a0 (low 3 = a, high 3 = b)
"""
# view as uint8, broadcast to 6 separate bit positions
bits = ((packed[:, None] >> np.arange(6)) & 1).astype(np.uint8)
return bits
def ab_from_bits(bits: np.ndarray) -> tuple[int,int]:
"""Given a 6-length uint8[6] → (a,b). Low-3→a, high-3→b. Flip if needed."""
a = bits[0] | (bits[1] << 1) | (bits[2] << 2) # a0,a1,a2
b = bits[3] | (bits[4] << 1) | (bits[5] << 2) # b0,b1,b2
return a, b
# Load JSON and extract arrays
with RESULT_PATH.open() as f:
res_json = json.load(f)
fields = res_json["__value__"]["pub_results"][0]["__value__"]["data"]["__value__"]["fields"]
c_arr = decode_field(fields["c"]) # (shots,6) *or* (shots,1) packed
clk_arr = decode_field(fields["c_clk"]) # (shots,1) uint8 {0,1}
# Normalise shapes
if c_arr.ndim == 2 and c_arr.shape[1] == 1: # packed
c_bits = unpack_bits(c_arr[:,0])
else: # already 6-bit rows
c_bits = c_arr
clk_bits = clk_arr.reshape(-1).astype(np.uint8)
shots = c_bits.shape[0]
assert clk_bits.size == shots, "Clock register length mismatch"
# Build conditional counters
cnt_clk0, cnt_clk1 = Counter(), Counter()
for bits, clk in zip(c_bits, clk_bits):
a, b = ab_from_bits(bits)
(cnt_clk1 if clk else cnt_clk0)[(a, b)] += 1
tot0, tot1 = sum(cnt_clk0.values()), sum(cnt_clk1.values())
# Reporting
def top(counter, tot, name):
print(f"\n{name} (shots = {tot})")
for (a,b), c in counter.most_common(10):
print(f" (a={a}, b={b}) {c:>4} {100*c/tot:5.2f}%")
print(f"\nTotal shots : {shots}")
print(f"Clock |1⟩ : {tot1} ({100*tot1/shots:.1f} %)")
print(f"Clock |0⟩ : {tot0} ({100*tot0/shots:.1f} %)")
top(cnt_clk1, tot1, "Top-10 | clock = 1")
top(cnt_clk0, tot0, "Top-10 | clock = 0")
# Heat-maps (comment out if running headless)
def heat(counter, title):
grid = np.zeros((8,8), dtype=int)
for (a,b), n in counter.items():
grid[a, b] = n
plt.figure(figsize=(4,4))
plt.title(title)
plt.imshow(grid, origin="lower")
plt.xlabel("b"), plt.ylabel("a")
plt.colorbar(label="counts")
plt.tight_layout()
heat(cnt_clk1, "Heat-map clock = 1")
heat(cnt_clk0, "Heat-map clock = 0")
plt.show()
# Ideal Bloch-clock vector for comparison
qc = QuantumCircuit(1)
qc.h(0)
qc.rx(np.pi/4, 0)
qc.ry(np.pi/4, 0)
qc.rz(np.pi/4, 0)
bloch_sv = Statevector.from_instruction(qc)
bloch_xyz = bloch_sv.expectation_value([[0,1],[1,0]]), \
bloch_sv.expectation_value([[0,-1j],[1j,0]]), \
bloch_sv.expectation_value([[1,0],[0,-1]])
print(f"\nIdeal Bloch-clock ⟨Z⟩ = {np.real(bloch_xyz[2]): .4f}")