427 lines
14 KiB
Python
427 lines
14 KiB
Python
import math
|
|
import random
|
|
from dataclasses import dataclass
|
|
from enum import IntEnum
|
|
from typing import override
|
|
|
|
import numpy as np
|
|
from qiskit import QuantumCircuit as QiskitCircuit
|
|
from qiskit import 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
|
|
I = 11
|
|
|
|
|
|
def single_typ(typ: GateType) -> bool:
|
|
match typ:
|
|
case GateType.I | GateType.H | GateType.RX | GateType.RY | GateType.RZ:
|
|
return True
|
|
case (
|
|
GateType.RXX
|
|
| GateType.RYY
|
|
| GateType.RZZ
|
|
| GateType.CX
|
|
| GateType.CZ
|
|
| GateType.CRX
|
|
):
|
|
return False
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Gate:
|
|
typ: GateType
|
|
qubits: int | tuple[int, int]
|
|
param_idx: int
|
|
|
|
def to_json(self):
|
|
return {
|
|
"type": int(self.typ),
|
|
"qubits": self.qubits,
|
|
"param_idx": self.param_idx,
|
|
}
|
|
|
|
def single(self) -> bool:
|
|
return single_typ(self.typ)
|
|
|
|
|
|
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.typ <= 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.typ == GateType.RX:
|
|
theta = thetas[gate.param_idx]
|
|
qc.rx(theta, gate.qubits)
|
|
elif gate.typ == GateType.RY:
|
|
theta = thetas[gate.param_idx]
|
|
qc.ry(theta, gate.qubits)
|
|
elif gate.typ == GateType.RZ:
|
|
theta = thetas[gate.param_idx]
|
|
qc.rz(theta, gate.qubits)
|
|
elif gate.typ == GateType.H:
|
|
qc.h(gate.qubits)
|
|
elif gate.typ == GateType.I:
|
|
qc.id(gate.qubits)
|
|
elif gate.typ == GateType.RXX:
|
|
theta = thetas[gate.param_idx]
|
|
qc.rxx(theta, *gate.qubits)
|
|
elif gate.typ == GateType.RYY:
|
|
theta = thetas[gate.param_idx]
|
|
qc.ryy(theta, *gate.qubits)
|
|
elif gate.typ == GateType.RZZ:
|
|
theta = thetas[gate.param_idx]
|
|
qc.rzz(theta, *gate.qubits)
|
|
elif gate.typ == GateType.CX:
|
|
qc.cx(*gate.qubits)
|
|
elif gate.typ == GateType.CRX:
|
|
theta = thetas[gate.param_idx]
|
|
qc.crx(theta, *gate.qubits)
|
|
else:
|
|
raise ValueError(f"Unknown gate type: {gate.typ}")
|
|
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.typ:
|
|
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.typ:
|
|
case GateType.H:
|
|
new_layer.append(Gate(gate.typ, gate.qubits, params))
|
|
total_single += 1
|
|
case GateType.RX | GateType.RY | GateType.RZ:
|
|
new_layer.append(Gate(gate.typ, gate.qubits, params))
|
|
params += 1
|
|
total_single += 1
|
|
case GateType.RXX | GateType.RYY | GateType.RZZ:
|
|
new_layer.append(Gate(gate.typ, gate.qubits, params))
|
|
params += 1
|
|
total_double += 1
|
|
case GateType.CX | GateType.CZ:
|
|
new_layer.append(Gate(gate.typ, gate.qubits, params))
|
|
total_double += 1
|
|
case GateType.CRX:
|
|
new_layer.append(Gate(gate.typ, 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, gate_types: list[GateType]
|
|
) -> 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
|
|
if loc + 1 < qubits:
|
|
gate_type = rng.choice(gate_types)
|
|
else:
|
|
gate_type = rng.choice([typ for typ in gate_types if single_typ(typ)])
|
|
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:
|
|
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:
|
|
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:
|
|
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, gates: list[GateType]
|
|
):
|
|
while True:
|
|
yield sample_circuit_random(rng, qubits, depth, gates)
|