redo expressivity proxy

This commit is contained in:
Noa Aarts 2026-03-16 15:00:28 +01:00
parent 526d615d9b
commit e774575604
Signed by: noa
GPG key ID: 1850932741EFF672
9 changed files with 308 additions and 2 deletions

View file

@ -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

8
src/ga_qas/__init__.py Normal file
View file

@ -0,0 +1,8 @@
from dataclasses import dataclass
from settings import QuantumArchitectureSearchSettings
@dataclass
class QualityDiversitySettings(QuantumArchitectureSearchSettings):
initial_population_size: int

28
src/qd_qas/__init__.py Normal file
View file

@ -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]

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

6
src/settings.py Normal file
View file

@ -0,0 +1,6 @@
from dataclasses import dataclass
@dataclass
class QuantumArchitectureSearchSettings:
qubits: int

8
src/tf_qas/__init__.py Normal file
View file

@ -0,0 +1,8 @@
from dataclasses import dataclass
from settings import QuantumArchitectureSearchSettings
@dataclass
class TrainingFreeSettings(QuantumArchitectureSearchSettings):
sample_size: int