initial part of Genetic Algorithm QAS

This commit is contained in:
Noa Aarts 2026-01-26 10:42:57 +01:00
parent 238caf645b
commit 19e90b3d21
Signed by: noa
GPG key ID: 1850932741EFF672
5 changed files with 535 additions and 258 deletions

409
src/quantum_circuit.py Normal file
View file

@ -0,0 +1,409 @@
from dataclasses import dataclass
import math
import random
from typing import override
import numpy as np
from enum import IntEnum
from qiskit import QuantumCircuit as QiskitCircuit, transpile
from qiskit.circuit import ParameterVector, ParameterVectorElement
from qiskit_aer import AerSimulator
class GateType(IntEnum):
"""
Enum of various (parametrized) gates
"""
RX = 1
RY = 2
RZ = 3
RXX = 4
RYY = 5
RZZ = 6
CX = 7
CZ = 8
CRX = 9
H = 10
@dataclass(frozen=True)
class Gate:
type: GateType
qubits: int | tuple[int, int]
param_idx: int
def to_json(self):
return {
"type": int(self.type),
"qubits": self.qubits,
"param_idx": self.param_idx,
}
def haar_fidelity_pdf(F: np.ndarray, d: int) -> np.ndarray:
# p(F) = (d-1) * (1-F)^(d-2), for F in [0,1]
return (d - 1) * np.power(1.0 - F, d - 2)
def kl_divergence(p: np.ndarray, q: np.ndarray) -> float:
# assumes p, q are normalized and strictly positive
return float(np.sum(p * np.log(p / q)))
@dataclass()
class QuantumCircuit:
qubits: int
gates: list[list[Gate]]
single_qubit_gates: int
two_qubit_gates: int
params: int
paths: int = 0
expressibility: float = float("-inf")
# ground-truth fields (filled later)
gt_energy: float | None = None
gt_error: float | None = None
gt_steps: int | None = None
gt_success: bool | None = None
def calculate_paths(self):
path_counts = [1 for _ in range(self.qubits)]
for layer in self.gates:
for gate in layer:
if gate.type <= 3 or isinstance(gate.qubits, int):
continue
(q1, q2) = gate.qubits
# same logic as your existing file
path_counts[q1] = path_counts[q1] + path_counts[q2]
path_counts[q2] = path_counts[q1]
self.paths = sum(path_counts)
def to_json(self):
return {
"qubits": self.qubits,
"gates": [[gate.to_json() for gate in layer] for layer in self.gates],
"params": self.params,
"paths": self.paths,
"expressibility": self.expressibility,
"gt_energy": self.gt_energy,
"gt_error": self.gt_error,
"gt_steps": self.gt_steps,
"gt_success": self.gt_success,
}
def to_qiskit_ansatz(self) -> tuple[QiskitCircuit, ParameterVector]:
"""Return *just* the parametrized ansatz defined by this circuit."""
qc = QiskitCircuit(self.qubits)
thetas = ParameterVector("theta", self.params)
for layer in self.gates:
for gate in layer:
if gate.type == GateType.RX:
theta = thetas[gate.param_idx]
qc.rx(theta, gate.qubits)
elif gate.type == GateType.RY:
theta = thetas[gate.param_idx]
qc.ry(theta, gate.qubits)
elif gate.type == GateType.RZ:
theta = thetas[gate.param_idx]
qc.rz(theta, gate.qubits)
elif gate.type == GateType.H:
qc.h(gate.qubits)
elif gate.type == GateType.RXX:
theta = thetas[gate.param_idx]
qc.rxx(theta, *gate.qubits)
elif gate.type == GateType.RYY:
theta = thetas[gate.param_idx]
qc.ryy(theta, *gate.qubits)
elif gate.type == GateType.RZZ:
theta = thetas[gate.param_idx]
qc.rzz(theta, *gate.qubits)
elif gate.type == GateType.CX:
qc.cx(*gate.qubits)
elif gate.type == GateType.CRX:
theta = thetas[gate.param_idx]
qc.crx(theta, *gate.qubits)
else:
raise ValueError(f"Unknown gate type: {gate.type}")
return qc, thetas
def to_qiskit_for_expressibility(self) -> tuple[QiskitCircuit, ParameterVector]:
"""Expressibility uses |0...0> input (no TFIM-specific H layer)."""
qc, thetas = self.to_qiskit_ansatz()
qc.save_statevector()
return qc, thetas
def to_qiskit_for_tfim_vqe(self) -> tuple[QiskitCircuit, ParameterVector]:
"""TFIM VQE begins with a layer of Hadamards on all qubits."""
ans, thetas = self.to_qiskit_ansatz()
qc = QiskitCircuit(self.qubits)
qc.h(range(self.qubits))
qc.compose(ans, inplace=True)
qc.save_statevector()
return qc, thetas
def expressibility_estimate(
self, samples: int, seed: int, bins: int = 75, eps: float = 1e-12
) -> "QuantumCircuit":
qc, thetas = self.to_qiskit_for_expressibility()
if self.params <= 0:
self.expressibility = float("-inf")
return self
rng = random.Random(seed)
d = 1 << self.qubits
backend = AerSimulator(method="statevector", seed_simulator=seed)
tqc = transpile(qc, backend, optimization_level=0)
nexp = 2 * samples
# vectorized binds: one circuit, many parameter values
binds: list[dict[ParameterVectorElement, list[float]]] = [
{
param: [rng.random() * math.tau for _ in range(nexp)]
for param in thetas.params
}
]
job = backend.run([tqc], parameter_binds=binds)
result = job.result()
sv = [
np.asarray(result.get_statevector(i), dtype=np.complex128)
for i in range(nexp)
]
left = sv[:samples]
right = sv[samples:]
inners = np.array(
[np.vdot(l, r) for l, r in zip(left, right)], dtype=np.complex128
)
F = (inners.conjugate() * inners).real
hist, edges = np.histogram(F, bins=bins, range=(0.0, 1.0), density=False)
p = hist.astype(np.float64) + eps
p = p / p.sum()
mids = 0.5 * (edges[:-1] + edges[1:])
widths = edges[1:] - edges[:-1]
q = haar_fidelity_pdf(mids, d) * widths
q = q + eps
q = q / q.sum()
kl = kl_divergence(p, q)
self.expressibility = -kl
return self
@override
def __str__(self):
strs = ["-" for _ in range(self.qubits)]
for layer in self.gates:
for i in range(self.qubits):
strs[i] += "---"
idx = 0
for gate in layer:
match gate.type:
case GateType.RX:
strs[gate.qubits] = strs[gate.qubits][:-2] + "RX"
case GateType.RY:
strs[gate.qubits] = strs[gate.qubits][:-2] + "RY"
case GateType.RZ:
strs[gate.qubits] = strs[gate.qubits][:-2] + "RZ"
case GateType.RXX:
(q1, q2) = gate.qubits
strs[q1] = strs[q1][:-2] + f"X{idx}"
strs[q2] = strs[q2][:-2] + f"X{idx}"
idx += 1
case GateType.RYY:
(q1, q2) = gate.qubits
strs[q1] = strs[q1][:-2] + f"Y{idx}"
strs[q2] = strs[q2][:-2] + f"Y{idx}"
idx += 1
case GateType.RZZ:
(q1, q2) = gate.qubits
strs[q1] = strs[q1][:-2] + f"Z{idx}"
strs[q2] = strs[q2][:-2] + f"Z{idx}"
idx += 1
case GateType.CX:
(c, t) = gate.qubits
strs[c] = strs[c][:-2] + f"C{idx}"
strs[t] = strs[t][:-2] + f"X{idx}"
idx += 1
case GateType.CRX:
(c, t) = gate.qubits
strs[c] = strs[c][:-2] + f"C{idx}"
strs[t] = strs[t][:-2] + f"R{idx}"
idx += 1
case GateType.CZ:
(c, t) = gate.qubits
strs[c] = strs[c][:-2] + f"C{idx}"
strs[t] = strs[t][:-2] + f"Z{idx}"
idx += 1
case GateType.H:
strs[gate.qubits] = strs[gate.qubits][:-2] + "H-"
gt = ""
if self.gt_energy is not None:
gt = f"\nGT: E={self.gt_energy:.12f}, err={self.gt_error:.3e}, steps={self.gt_steps}, success={self.gt_success}"
return (
"\n".join(strs)
+ f"\npaths: {self.paths}, expressibility: {self.expressibility}, params: {self.params}"
+ gt
)
def even_parity(qubits: int):
return [(x, x + 1) for x in range(0, qubits, 2)]
def odd_parity(qubits: int):
return [(x, x + 1) for x in range(1, qubits, 2)]
def sample_circuit_layers(
rng: random.Random, qubits: int, depth: int
) -> QuantumCircuit:
even = even_parity(qubits)
odd = odd_parity(qubits)
total_single = 0
total_double = 0
params = 0
gates: list[list[Gate]] = []
for _ in range(depth):
if total_single + total_double >= 36:
break
gate_locations = even if rng.random() < 0.5 else odd
layer = []
for loc in gate_locations:
gate_type = rng.randint(1, 6)
if gate_type >= 4:
if loc[1] == qubits:
continue
layer.append(Gate(GateType(gate_type), loc, params))
total_double += 1
else:
layer.append(Gate(GateType(gate_type), loc[0], params))
total_single += 1
gates.append(layer)
params += 1
return QuantumCircuit(qubits, gates, total_single, total_double, params)
def sample_layers_generator(rng: random.Random, qubits: int, depth: int):
while True:
yield sample_circuit_layers(rng, qubits, depth)
def circ_from_layers(layers: list[list[Gate]], qubits: int) -> QuantumCircuit:
params = 0
total_single = 0
total_double = 0
gates: list[list[Gate]] = []
for layer in layers:
new_layer = []
for gate in layer:
match gate.type:
case GateType.H:
new_layer.append(Gate(gate.type, gate.qubits, params))
total_single += 1
case GateType.RX | GateType.RY | GateType.RZ:
new_layer.append(Gate(gate.type, gate.qubits, params))
params += 1
total_single += 1
case GateType.RXX | GateType.RYY | GateType.RZZ:
new_layer.append(Gate(gate.type, gate.qubits, params))
params += 1
total_double += 1
case GateType.CX | GateType.CZ:
new_layer.append(Gate(gate.type, gate.qubits, params))
total_double += 1
case GateType.CRX:
new_layer.append(Gate(gate.type, gate.qubits, params))
params += 1
total_double += 1
gates.append(new_layer)
return QuantumCircuit(qubits, gates, total_single, total_double, params)
def sample_circuit_random(
rng: random.Random, qubits: int, depth: int
) -> QuantumCircuit:
params = 0
total_single = 0
total_double = 0
gates: list[list[Gate]] = []
for _ in range(depth):
used_qubits: set[int] = set()
layer: list[Gate] = []
for loc in range(qubits):
if loc in used_qubits:
continue
gate_type = rng.choice(
[
GateType.RX,
GateType.RY,
GateType.RZ,
GateType.CX,
GateType.CRX,
GateType.H,
]
)
match gate_type:
case GateType.H:
layer.append(Gate(gate_type, loc, params))
total_single += 1
used_qubits.add(loc)
case GateType.RX | GateType.RY | GateType.RZ:
layer.append(Gate(gate_type, loc, params))
params += 1
total_single += 1
used_qubits.add(loc)
case GateType.RXX | GateType.RYY | GateType.RZZ if loc + 1 < qubits:
layer.append(Gate(gate_type, (loc, loc + 1), params))
params += 1
total_double += 1
used_qubits.add(loc)
used_qubits.add(loc + 1)
case GateType.CX | GateType.CZ if loc + 1 < qubits:
layer.append(
Gate(
gate_type,
(loc, loc + 1) if rng.random() < 0.5 else (loc + 1, loc),
params,
)
)
total_double += 1
used_qubits.add(loc)
used_qubits.add(loc + 1)
case GateType.CRX if loc + 1 < qubits:
layer.append(
Gate(
gate_type,
(loc, loc + 1) if rng.random() < 0.5 else (loc + 1, loc),
params,
)
)
params += 1
total_double += 1
used_qubits.add(loc)
used_qubits.add(loc + 1)
case _:
pass
gates.append(layer)
return QuantumCircuit(qubits, gates, total_single, total_double, params)
def sample_random_generator(rng: random.Random, qubits: int, depth: int):
while True:
yield sample_circuit_random(rng, qubits, depth)