diff --git a/flake.nix b/flake.nix index e359360..f90c706 100644 --- a/flake.nix +++ b/flake.nix @@ -43,6 +43,7 @@ ppkgs.tqdm ppkgs.qiskit ppkgs.qiskit-aer + ppkgs.matplotlib ])) ]; }; diff --git a/src/__init__.py b/src/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/best_circuits.png b/src/best_circuits.png new file mode 100644 index 0000000..1183ddf Binary files /dev/null and b/src/best_circuits.png differ diff --git a/src/ga_qas.py b/src/ga_qas.py index 6c0d410..e4b1d76 100755 --- a/src/ga_qas.py +++ b/src/ga_qas.py @@ -3,27 +3,42 @@ # "Genetic optimization of ansatz expressibility for enhanced variational quantum algorithm performance" import random -from quantum_circuit import ( - Gate, - GateType, - QuantumCircuit, - circ_from_layers, - sample_random_generator, -) + +import matplotlib.pyplot as plt + from qas_flow import Stream +from quantum_circuit import (Gate, GateType, QuantumCircuit, circ_from_layers, + sample_random_generator, single_typ) -DEPTH = 20 -QUBITS = 5 -GENERATIONS = 20 -GENERATION_SIZE = 20 -PARENT_AMOUNT = 5 -MUTATION_RATE = 0.1 +DEPTH: int = 6 +QUBITS: int = 6 +GENERATIONS: int = 40 +GENERATION_SIZE: int = 60 +PARENT_AMOUNT: int = 10 +MUTATION_RATE: float = 0.1 -def main(): - seed_rng = random.Random(1020381) +gate_set: list[GateType] = [ + GateType.H, + GateType.RX, + GateType.RY, + GateType.RZ, + GateType.CRX, + GateType.CX, +] + + +def plot_best_circuits(best_circuits: list[QuantumCircuit]) -> None: + fig, ax = plt.subplots() + + ax.plot([-circ.expressibility for circ in best_circuits]) + fig.savefig("best_circuits.png") + + +def main() -> None: + seed_rng: random.Random = random.Random(1020381) initial_population: list[QuantumCircuit] = ( - Stream(sample_random_generator(random.Random(101020), QUBITS, DEPTH)) + Stream(sample_random_generator(random.Random(101020), QUBITS, DEPTH, gate_set)) .apply(lambda circ: print(circ)) .apply( lambda circ: circ.expressibility_estimate( @@ -39,6 +54,8 @@ def main(): main_rng = random.Random(2837175) + best_circuits: list[QuantumCircuit] = [] + for generation in range(GENERATIONS): print(f"starting generation {generation}") population.sort(key=lambda qc: qc.expressibility, reverse=True) @@ -55,28 +72,38 @@ def main(): layer = child_layers[layer_idx] gate_idx = main_rng.randrange(len(layer)) old_gate = child_layers[layer_idx][gate_idx] - match old_gate.type: - case GateType.H | GateType.RX | GateType.RY | GateType.RZ: - child_layers[layer_idx][gate_idx] = Gate( - main_rng.choice( - [GateType.H, GateType.RX, GateType.RY, GateType.RZ] - ), - old_gate.qubits, - old_gate.param_idx, - ) - case GateType.CRX | GateType.CX: - child_layers[layer_idx][gate_idx] = Gate( - old_gate.type, - tuple(old_gate.qubits[::-1]), - old_gate.param_idx, - ) - case _: - print(f"unhandled gate: {old_gate}") + + if old_gate.single(): + child_layers[layer_idx][gate_idx] = Gate( + main_rng.choice( + [gate for gate in gate_set if single_typ(gate)] + ), + old_gate.qubits, + old_gate.param_idx, + ) + else: + child_layers[layer_idx][gate_idx] = Gate( + old_gate.typ, + (old_gate.qubits[1], old_gate.qubits[0]), + old_gate.param_idx, + ) + child = circ_from_layers(child_layers, QUBITS) child.expressibility_estimate(2000, seed_rng.randint(1000, 1000000000)) offspring.append(child) + + offspring.sort(key=lambda qc: qc.expressibility, reverse=True) + if population[0].expressibility > offspring[0].expressibility: + print(f"best parent > best child") + best_circuits.append(population[0]) + else: + print(f"best child > best parent") + best_circuits.append(offspring[0]) population = offspring + plot_best_circuits(best_circuits) + plt.show() + if __name__ == "__main__": main() diff --git a/src/quantum_circuit.py b/src/quantum_circuit.py index c817396..f3d7e87 100644 --- a/src/quantum_circuit.py +++ b/src/quantum_circuit.py @@ -1,11 +1,12 @@ -from dataclasses import dataclass import math import random -from typing import override -import numpy as np +from dataclasses import dataclass from enum import IntEnum +from typing import override -from qiskit import QuantumCircuit as QiskitCircuit, transpile +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 @@ -25,21 +26,40 @@ class GateType(IntEnum): 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: - type: GateType + typ: GateType qubits: int | tuple[int, int] param_idx: int def to_json(self): return { - "type": int(self.type), + "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] @@ -71,7 +91,7 @@ class QuantumCircuit: 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): + if gate.typ <= 3 or isinstance(gate.qubits, int): continue (q1, q2) = gate.qubits # same logic as your existing file @@ -99,33 +119,35 @@ class QuantumCircuit: for layer in self.gates: for gate in layer: - if gate.type == GateType.RX: + if gate.typ == GateType.RX: theta = thetas[gate.param_idx] qc.rx(theta, gate.qubits) - elif gate.type == GateType.RY: + elif gate.typ == GateType.RY: theta = thetas[gate.param_idx] qc.ry(theta, gate.qubits) - elif gate.type == GateType.RZ: + elif gate.typ == GateType.RZ: theta = thetas[gate.param_idx] qc.rz(theta, gate.qubits) - elif gate.type == GateType.H: + elif gate.typ == GateType.H: qc.h(gate.qubits) - elif gate.type == GateType.RXX: + 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.type == GateType.RYY: + elif gate.typ == GateType.RYY: theta = thetas[gate.param_idx] qc.ryy(theta, *gate.qubits) - elif gate.type == GateType.RZZ: + elif gate.typ == GateType.RZZ: theta = thetas[gate.param_idx] qc.rzz(theta, *gate.qubits) - elif gate.type == GateType.CX: + elif gate.typ == GateType.CX: qc.cx(*gate.qubits) - elif gate.type == GateType.CRX: + elif gate.typ == GateType.CRX: theta = thetas[gate.param_idx] qc.crx(theta, *gate.qubits) else: - raise ValueError(f"Unknown gate type: {gate.type}") + raise ValueError(f"Unknown gate type: {gate.typ}") return qc, thetas def to_qiskit_for_expressibility(self) -> tuple[QiskitCircuit, ParameterVector]: @@ -205,7 +227,7 @@ class QuantumCircuit: idx = 0 for gate in layer: - match gate.type: + match gate.typ: case GateType.RX: strs[gate.qubits] = strs[gate.qubits][:-2] + "RX" case GateType.RY: @@ -311,23 +333,23 @@ def circ_from_layers(layers: list[list[Gate]], qubits: int) -> QuantumCircuit: for layer in layers: new_layer = [] for gate in layer: - match gate.type: + match gate.typ: case GateType.H: - new_layer.append(Gate(gate.type, gate.qubits, params)) + new_layer.append(Gate(gate.typ, gate.qubits, params)) total_single += 1 case GateType.RX | GateType.RY | GateType.RZ: - new_layer.append(Gate(gate.type, gate.qubits, params)) + 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.type, gate.qubits, params)) + new_layer.append(Gate(gate.typ, gate.qubits, params)) params += 1 total_double += 1 case GateType.CX | GateType.CZ: - new_layer.append(Gate(gate.type, gate.qubits, params)) + new_layer.append(Gate(gate.typ, gate.qubits, params)) total_double += 1 case GateType.CRX: - new_layer.append(Gate(gate.type, gate.qubits, params)) + new_layer.append(Gate(gate.typ, gate.qubits, params)) params += 1 total_double += 1 gates.append(new_layer) @@ -335,7 +357,7 @@ def circ_from_layers(layers: list[list[Gate]], qubits: int) -> QuantumCircuit: def sample_circuit_random( - rng: random.Random, qubits: int, depth: int + rng: random.Random, qubits: int, depth: int, gate_types: list[GateType] ) -> QuantumCircuit: params = 0 total_single = 0 @@ -348,16 +370,10 @@ def sample_circuit_random( 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, - ] - ) + 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)) @@ -368,13 +384,13 @@ def sample_circuit_random( params += 1 total_single += 1 used_qubits.add(loc) - case GateType.RXX | GateType.RYY | GateType.RZZ if loc + 1 < qubits: + 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 if loc + 1 < qubits: + case GateType.CX | GateType.CZ: layer.append( Gate( gate_type, @@ -385,7 +401,7 @@ def sample_circuit_random( total_double += 1 used_qubits.add(loc) used_qubits.add(loc + 1) - case GateType.CRX if loc + 1 < qubits: + case GateType.CRX: layer.append( Gate( gate_type, @@ -404,6 +420,8 @@ def sample_circuit_random( return QuantumCircuit(qubits, gates, total_single, total_double, params) -def sample_random_generator(rng: random.Random, qubits: int, depth: int): +def sample_random_generator( + rng: random.Random, qubits: int, depth: int, gates: list[GateType] +): while True: - yield sample_circuit_random(rng, qubits, depth) + yield sample_circuit_random(rng, qubits, depth, gates)