From e7745756041c70301ecd18d415de6cbd6a5b7b2a Mon Sep 17 00:00:00 2001 From: Noa Aarts Date: Mon, 16 Mar 2026 15:00:28 +0100 Subject: [PATCH 1/2] redo expressivity proxy --- src/all_qas.py | 9 +- src/ga_qas/__init__.py | 8 ++ src/qd_qas/__init__.py | 28 ++++++ src/quantum_circuit/__init__.py | 129 ++++++++++++++++++++++++++ src/quantum_circuit/proxies.py | 94 +++++++++++++++++++ src/quantum_circuit/proxy_config.py | 17 ++++ src/quantum_circuit/qiskit_helpers.py | 11 +++ src/settings.py | 6 ++ src/tf_qas/__init__.py | 8 ++ 9 files changed, 308 insertions(+), 2 deletions(-) create mode 100644 src/ga_qas/__init__.py create mode 100644 src/qd_qas/__init__.py create mode 100644 src/quantum_circuit/__init__.py create mode 100644 src/quantum_circuit/proxies.py create mode 100644 src/quantum_circuit/proxy_config.py create mode 100644 src/quantum_circuit/qiskit_helpers.py create mode 100644 src/settings.py create mode 100644 src/tf_qas/__init__.py diff --git a/src/all_qas.py b/src/all_qas.py index ff11721..7001c66 100755 --- a/src/all_qas.py +++ b/src/all_qas.py @@ -1,7 +1,13 @@ #!/usr/bin/env python from argparse import ArgumentParser +from dataclasses import dataclass from enum import Enum +from ga_qas import GeneticAlgorithmSettings +from qd_qas import QualityDiversitySettings +from settings import QuantumArchitectureSearchSettings +from tf_qas import TrainingFreeSettings + class SearchStrategy(Enum): TFQAS = 1 @@ -9,8 +15,7 @@ class SearchStrategy(Enum): QDQAS = 3 -def main(search_strategy: SearchStrategy): - +def main(search_settings: QualityDiversitySettings | TrainingFreeSettings | GeneticAlgorithmSettings): pass diff --git a/src/ga_qas/__init__.py b/src/ga_qas/__init__.py new file mode 100644 index 0000000..8d947ca --- /dev/null +++ b/src/ga_qas/__init__.py @@ -0,0 +1,8 @@ +from dataclasses import dataclass + +from settings import QuantumArchitectureSearchSettings + + +@dataclass +class QualityDiversitySettings(QuantumArchitectureSearchSettings): + initial_population_size: int diff --git a/src/qd_qas/__init__.py b/src/qd_qas/__init__.py new file mode 100644 index 0000000..59d9311 --- /dev/null +++ b/src/qd_qas/__init__.py @@ -0,0 +1,28 @@ +from dataclasses import dataclass +from typing import Callable + +from settings import QuantumArchitectureSearchSettings + + +@dataclass +class QualityDiversitySettings(QuantumArchitectureSearchSettings): + """ + Hyperparameters for Quality Diversity based Quantum Architecture Search + """ + + initial_population_size: int + """How many (random) circuits should be generated at the beginning""" + + offspring_size: int + """How many offspring should be generated from the previous generation""" + + generation_size: int + """How many of the previous generation offspring become a parent for the next generation""" + + generation_count: int + """How many generations to optimize for""" + + mutation_rate: float + """for each gate in a circuit from the offspring perform a mutation if random [0, 1) < mutation_rate""" + + cost_function: Callable[[ParametrizedQuantumCircuit], float] diff --git a/src/quantum_circuit/__init__.py b/src/quantum_circuit/__init__.py new file mode 100644 index 0000000..62f185f --- /dev/null +++ b/src/quantum_circuit/__init__.py @@ -0,0 +1,129 @@ +import enum +from dataclasses import dataclass + +from qiskit import QuantumCircuit +from qiskit.circuit.parametervector import ParameterVector + +from quantum_circuit.proxy_config import ProxyConfig +from quantum_circuit.qiskit_helpers import build_qiskit_circ + +from .proxies import (calculate_entanglement, calculate_expressivity, + calculate_fidelity) + + +class QuantumType(enum.Enum): + Identity = enum.auto() + Hadamard = enum.auto() + X = enum.auto() + RX = enum.auto() + RXX = enum.auto() + Y = enum.auto() + RY = enum.auto() + RYY = enum.auto() + Z = enum.auto() + RZ = enum.auto() + RZZ = enum.auto() + CX = enum.auto() + CRX = enum.auto() + CZ = enum.auto() + + def is_single_qubit(self): + return self in { + QuantumType.Identity, + QuantumType.Hadamard, + QuantumType.X, + QuantumType.RX, + QuantumType.Y, + QuantumType.RY, + QuantumType.Z, + QuantumType.RZ, + } + + +@dataclass(frozen=True) +class QuantumGate: + type: QuantumType + qubits: tuple[int] | tuple[int, int] + + +class ParametrizedQuantumCircuit: + """ + A Quantum circuit representation to use in the Quantum Architecture Searches. + It has methods for various proxies and stores the results as well, to have everything close. + Allows easy access to the gates inside so it can be used by functions that use it. + """ + + qubits: int + """How many qubits does the circuit contain""" + + gates: list[list[QuantumGate]] + """Gates in the quantum circuit, organised as a list of layers of gates.""" + + _expressivity: float | None = None + """ + Expressivity of the circuit, following the definition from {THE PAPER} + Use `expressivity` to get the value + """ # TODO: link paper + + _entanglement: float | None = None + """ + Entanglement of the circuit, following the definition from {THE PAPER} + Use `entanglement` to get the value + """ # TODO: link paper + + _fidelity: float | None = None + """ + Approximated fidelity of the circuit + Use `fidelity` to get the value + """ + + _qiskit_circ: tuple[QuantumCircuit, ParameterVector] | None = None + + def __init__(self, qubits: int): + """ + Initialise an empty circuit for the passed amount of qubits + """ + self.qubits = qubits + self.gates = [] + + def append_layer(self, layer: list[QuantumGate]): + """ + Add a layer to the end of the existing circuit in-place + """ + self.gates.append(layer) + + @property + def circ(self) -> tuple[QuantumCircuit, ParameterVector]: + """ + Qiskit circuit version to be used for simulations etc. + """ + if self._qiskit_circ is None: + self._qiskit_circ = build_qiskit_circ(self) + + return self._qiskit_circ + + def expressivity(self, config: ProxyConfig) -> float: + """ + Expressivity of the circuit, following the definition from {THE PAPER} + """ # TODO: Link Paper + if self._expressivity is None: + self._expressivity = float(calculate_expressivity(self, config)[0]) + return self._expressivity + + def entanglement(self, config: ProxyConfig) -> float: + """ + Entanglement of the circuit, following the definition from {THE PAPER} + """ # TODO: Link Paper + if self._entanglement is None: + self._entanglement = float(calculate_entanglement(self, config)[0]) + + return self._entanglement + + def fidelity(self, config: ProxyConfig) -> float: + """ + Approximated fidelity of the circuit + """ + if self._fidelity is None: + self._fidelity = float(calculate_fidelity(self, config)[0]) + + return self._fidelity diff --git a/src/quantum_circuit/proxies.py b/src/quantum_circuit/proxies.py new file mode 100644 index 0000000..c385fe0 --- /dev/null +++ b/src/quantum_circuit/proxies.py @@ -0,0 +1,94 @@ +from itertools import pairwise +from typing import TYPE_CHECKING + +import numpy as np +from numpy.typing import NDArray +from qiskit import transpile +from qiskit_aer.backends.aer_simulator import AerSimulator +from qiskit_aer.backends.aerbackend import AerBackend +from scipy import stats +from tqdm import tqdm + +if TYPE_CHECKING: + from quantum_circuit import ParametrizedQuantumCircuit + from quantum_circuit.proxy_config import ProxyConfig + + +def single_circuit_param_fidelity(circ: ParametrizedQuantumCircuit, samples: int, backend: AerBackend) -> float: + qc, thetas = circ.circ + qc.save_statevector() + + tqc = transpile(qc, backend) + + number_of_initial_circuits = 2 * samples + + params: NDArray[np.float64] = np.random.uniform(-np.pi, np.pi, (len(thetas), number_of_initial_circuits)) + + binds = [{param: params[idx] for idx, param in enumerate(thetas.params)}] + + job = backend.run([tqc], parameter_binds=binds) + result = job.result() + + sv = np.array( + [np.asarray(result.get_statevector(i), dtype=np.complex128) for i in range(number_of_initial_circuits)] + ) + left = sv[:samples] + right = sv[samples:] + + return np.power(np.absolute((left * right.conjugate()).sum(-1)), 2) + + +def calculate_fidelity( + circs: list[ParametrizedQuantumCircuit] | ParametrizedQuantumCircuit, config: ProxyConfig +) -> NDArray[np.float64]: + if isinstance(circs, ParametrizedQuantumCircuit): + circs = [circs] + raise NotImplementedError + + +def calculate_expressivity( + circs: list[ParametrizedQuantumCircuit] | ParametrizedQuantumCircuit, config: ProxyConfig +) -> NDArray[np.float64]: + tqdm_depth = 0 + if isinstance(circs, ParametrizedQuantumCircuit): + circs = [circs] + + qubits = config.qubits + samples = config.expressivity_samples + bin_count = config.expressivity_bins + force_recalculate = config.force_recalculate + bins: np.ndarray[tuple[int], np.dtype[np.float64]] = np.linspace(0.0, 1.0, bin_count + 1) + + haar_power = (1 << qubits) - 1 + lower_edges: np.ndarray[tuple[int], np.dtype[np.float64]] = -1.0 * np.power(1.0 - bins[:-1], haar_power) + higher_edges: np.ndarray[tuple[int], np.dtype[np.float64]] = -1.0 * np.power(1.0 - bins[1:], haar_power) + haar_values: np.ndarray[tuple[int], np.dtype[np.float64]] = higher_edges - lower_edges + + backend = AerSimulator(method="statevector") + + fidelities = np.zeros((len(circs))) + for idx, circuit in tqdm(enumerate(circs), position=tqdm_depth, desc="Computing Fidelities", leave=False): + if not force_recalculate and circs[idx]._expressivity is not None: + continue + fidelities[idx] = single_circuit_param_fidelity(circuit, samples, backend) + + expressivity = np.zeros((len(circs))) + for idx, fid in tqdm(enumerate(fidelities), position=tqdm_depth, desc="Computing Expressibility"): + if not force_recalculate and circs[idx]._expressivity is not None: + continue + bin_idx = np.floor(fid * bin_count).astype(int) + num = np.array([len(bin_idx[bin_idx == i]) for i in range(bin_count)]) + + expressivity[idx] = -stats.entropy(num, haar_values) + # NOTE: I'm setting the expressivity here directly, for caching + circs[idx]._expressivity = float(expressivity[idx]) + + return expressivity + + +def calculate_entanglement( + circs: list[ParametrizedQuantumCircuit] | ParametrizedQuantumCircuit, config: ProxyConfig +) -> NDArray[np.float64]: + if isinstance(circs, ParametrizedQuantumCircuit): + circs = [circs] + raise NotImplementedError diff --git a/src/quantum_circuit/proxy_config.py b/src/quantum_circuit/proxy_config.py new file mode 100644 index 0000000..a16e9e6 --- /dev/null +++ b/src/quantum_circuit/proxy_config.py @@ -0,0 +1,17 @@ +from dataclasses import dataclass + + +@dataclass(frozen=True) +class ProxyConfig: + """ + Configuration for various proxies, defaults are included for every setting so only the + non-default values need to be changed when creating one + """ + + qubits: int + force_recalculate: bool = False + + entanglement_samples: int = 1000 + + expressivity_samples: int = 1000 + expressivity_bins: int = 100 diff --git a/src/quantum_circuit/qiskit_helpers.py b/src/quantum_circuit/qiskit_helpers.py new file mode 100644 index 0000000..3daceff --- /dev/null +++ b/src/quantum_circuit/qiskit_helpers.py @@ -0,0 +1,11 @@ +from typing import TYPE_CHECKING + +from qiskit import QuantumCircuit +from qiskit.circuit.parametervector import ParameterVector + +if TYPE_CHECKING: + from quantum_circuit import ParametrizedQuantumCircuit + + +def build_qiskit_circ(pqc: "ParametrizedQuantumCircuit") -> tuple[QuantumCircuit, ParameterVector]: + raise NotImplementedError diff --git a/src/settings.py b/src/settings.py new file mode 100644 index 0000000..6ef7827 --- /dev/null +++ b/src/settings.py @@ -0,0 +1,6 @@ +from dataclasses import dataclass + + +@dataclass +class QuantumArchitectureSearchSettings: + qubits: int diff --git a/src/tf_qas/__init__.py b/src/tf_qas/__init__.py new file mode 100644 index 0000000..cf2f346 --- /dev/null +++ b/src/tf_qas/__init__.py @@ -0,0 +1,8 @@ +from dataclasses import dataclass + +from settings import QuantumArchitectureSearchSettings + + +@dataclass +class TrainingFreeSettings(QuantumArchitectureSearchSettings): + sample_size: int From 93deb42c2745b902b3190a5014f964e045638322 Mon Sep 17 00:00:00 2001 From: Noa Aarts Date: Wed, 18 Mar 2026 10:22:44 +0100 Subject: [PATCH 2/2] pqc to qiskit conversion --- src/quantum_circuit/__init__.py | 17 ++++++++ src/quantum_circuit/qiskit_helpers.py | 58 ++++++++++++++++++++++++++- 2 files changed, 73 insertions(+), 2 deletions(-) diff --git a/src/quantum_circuit/__init__.py b/src/quantum_circuit/__init__.py index 62f185f..f9b6c15 100644 --- a/src/quantum_circuit/__init__.py +++ b/src/quantum_circuit/__init__.py @@ -39,6 +39,17 @@ class QuantumType(enum.Enum): QuantumType.RZ, } + def is_parameterized(self): + return self in { + QuantumType.RX, + QuantumType.RXX, + QuantumType.RY, + QuantumType.RYY, + QuantumType.RZ, + QuantumType.RZZ, + QuantumType.CRX, + } + @dataclass(frozen=True) class QuantumGate: @@ -59,6 +70,9 @@ class ParametrizedQuantumCircuit: gates: list[list[QuantumGate]] """Gates in the quantum circuit, organised as a list of layers of gates.""" + parameters: int + """How many parameters are used the circuit""" + _expressivity: float | None = None """ Expressivity of the circuit, following the definition from {THE PAPER} @@ -90,6 +104,9 @@ class ParametrizedQuantumCircuit: """ Add a layer to the end of the existing circuit in-place """ + for gate in layer: + if gate.type.is_parameterized(): + self.parameters += 1 self.gates.append(layer) @property diff --git a/src/quantum_circuit/qiskit_helpers.py b/src/quantum_circuit/qiskit_helpers.py index 3daceff..a9476bc 100644 --- a/src/quantum_circuit/qiskit_helpers.py +++ b/src/quantum_circuit/qiskit_helpers.py @@ -1,11 +1,65 @@ from typing import TYPE_CHECKING from qiskit import QuantumCircuit +from qiskit.circuit.parameter import Parameter from qiskit.circuit.parametervector import ParameterVector if TYPE_CHECKING: - from quantum_circuit import ParametrizedQuantumCircuit + from quantum_circuit import (ParametrizedQuantumCircuit, QuantumGate, + QuantumType) + + +def add_qubit_gate(circ: QuantumCircuit, gate: QuantumGate, theta: Parameter) -> int: + match gate.type: + case QuantumType.Identity: + return 0 + case QuantumType.Hadamard: + circ.h(gate.qubits[0]) + return 0 + case QuantumType.X: + circ.x(gate.qubits[0]) + return 0 + case QuantumType.RX: + circ.rx(theta, gate.qubits[0]) + return 1 + case QuantumType.RXX: + circ.rxx(theta, gate.qubits[0], gate.qubits[1]) # ty:ignore[index-out-of-bounds] + return 1 + case QuantumType.Y: + circ.y(gate.qubits[0]) + return 0 + case QuantumType.RY: + circ.ry(theta, gate.qubits[0]) + return 1 + case QuantumType.RYY: + circ.ryy(theta, gate.qubits[0], gate.qubits[1]) # ty:ignore[index-out-of-bounds] + return 1 + case QuantumType.Z: + circ.z(gate.qubits[0]) + return 0 + case QuantumType.RZ: + circ.rz(theta, gate.qubits[0]) + return 1 + case QuantumType.RZZ: + circ.rzz(theta, gate.qubits[0], gate.qubits[1]) # ty:ignore[index-out-of-bounds] + return 1 + case QuantumType.CRX: + circ.crx(theta, gate.qubits[0], gate.qubits[1]) # ty:ignore[index-out-of-bounds] + return 1 + case QuantumType.CX: + circ.cx(gate.qubits[0], gate.qubits[1]) # ty:ignore[index-out-of-bounds] + return 0 + raise NotImplementedError def build_qiskit_circ(pqc: "ParametrizedQuantumCircuit") -> tuple[QuantumCircuit, ParameterVector]: - raise NotImplementedError + + circ = QuantumCircuit(pqc.qubits) + thetas = ParameterVector("thetas", circ.parameters) + + current_theta_index = 0 + for layer in pqc.gates: + for gate in layer: + current_theta_index += add_qubit_gate(circ, gate, thetas[current_theta_index]) + + return circ, thetas