initial part of Genetic Algorithm QAS
This commit is contained in:
parent
238caf645b
commit
19e90b3d21
5 changed files with 535 additions and 258 deletions
409
src/quantum_circuit.py
Normal file
409
src/quantum_circuit.py
Normal 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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue