This commit is contained in:
Noa Aarts 2026-02-06 13:49:55 +01:00
parent 9a361417e8
commit 130a64755f
Signed by: noa
GPG key ID: 1850932741EFF672
4 changed files with 85 additions and 194 deletions

View file

@ -4,83 +4,20 @@
import random import random
import sys import sys
from typing import Generator, Never
import matplotlib.pyplot as plt
from qas_flow import Stream from qas_flow import Stream
from quantum_circuit import (Gate, GateType, QuantumCircuit, circ_from_layers, from quantum_circuit import (Gate, GateType, QuantumCircuit, circ_from_layers,
sample_random_generator, single_typ) sample_random_generator, single_typ)
from sampling_hyperparams import sample_hyperspace
gate_set: list[GateType] = [ gate_set: list[GateType] = [GateType.H, GateType.RX, GateType.RY, GateType.RZ, GateType.CRX, GateType.CX]
GateType.H,
GateType.RX,
GateType.RY,
GateType.RZ,
GateType.CRX,
GateType.CX,
]
def sample_hyperspace(
*args: tuple[int, int] | tuple[float, float], seed: int = 2010392991
) -> Generator[tuple[float | int, ...], None, Never]:
minimums: tuple[float | int, ...] = tuple(arg[0] for arg in args)
maximums: tuple[float | int, ...] = tuple(arg[1] for arg in args)
diffs: tuple[float | int, ...] = tuple(
ma - mi for mi, ma in zip(minimums, maximums)
)
idiffs: tuple[float, ...] = tuple(1.0 / diff for diff in diffs)
rng: random.Random = random.Random(seed)
previous_points: set[tuple[float | int, ...]] = set()
def dist(point: tuple[float | int, ...], other: tuple[float | int, ...]) -> float:
return sum(((p - o) * idiff) ** 2 for p, o, idiff in zip(point, other, idiffs))
while True:
sampled_points: list[tuple[float | int, ...]] = [
tuple(
min
+ (
rng.randint(min, min + diff)
if isinstance(min, int) and isinstance(diff, int)
else rng.uniform(min, min + diff)
)
for min, diff in zip(minimums, diffs)
)
for _ in range(10)
]
if len(previous_points) == 0:
previous_points.add(sampled_points[0])
yield sampled_points[0]
min_distances: list[float] = [
min((dist(point, other) for other in previous_points))
for point in sampled_points
]
mdist: float = max(min_distances)
point: tuple[float | int, ...] = sampled_points[
[i for i, j in enumerate(min_distances) if j == mdist][0]
]
previous_points.add(point)
yield point
EXPRESSIBILITY_SAMPLES: int = 2000 EXPRESSIBILITY_SAMPLES: int = 2000
def run_ga_qas( def run_ga_qas(
depth: int, depth: int, qubits: int, generations: int, generation_size: int, parent_amount: int, mutation_rate: float, seed: int
qubits: int,
generations: int,
generation_size: int,
parent_amount: int,
mutation_rate: float,
seed: int,
) -> list[tuple[int, int, int, int, int, float, float, float]]: ) -> list[tuple[int, int, int, int, int, float, float, float]]:
print( print(
@ -90,19 +27,8 @@ def run_ga_qas(
seed_rng = random.Random(seed) seed_rng = random.Random(seed)
initial_population: list[QuantumCircuit] = ( initial_population: list[QuantumCircuit] = (
Stream( Stream(sample_random_generator(random.Random(seed_rng.randint(1000, 1000000000)), qubits, depth, gate_set))
sample_random_generator( .apply(lambda circ: circ.expressibility_estimate(EXPRESSIBILITY_SAMPLES, seed_rng.randint(1000, 1000000000)))
random.Random(seed_rng.randint(1000, 1000000000)),
qubits,
depth,
gate_set,
)
)
.apply(
lambda circ: circ.expressibility_estimate(
EXPRESSIBILITY_SAMPLES, seed_rng.randint(1000, 1000000000)
)
)
.take(generation_size) .take(generation_size)
.collect() .collect()
) )
@ -129,17 +55,13 @@ def run_ga_qas(
if old_gate.single(): if old_gate.single():
child_layers[layer_idx][gate_idx] = Gate( child_layers[layer_idx][gate_idx] = Gate(
main_rng.choice( main_rng.choice([gate for gate in gate_set if single_typ(gate)]),
[gate for gate in gate_set if single_typ(gate)]
),
old_gate.qubits, old_gate.qubits,
old_gate.param_idx, old_gate.param_idx,
) )
else: else:
child_layers[layer_idx][gate_idx] = Gate( child_layers[layer_idx][gate_idx] = Gate(
old_gate.typ, old_gate.typ, (old_gate.qubits[1], old_gate.qubits[0]), old_gate.param_idx
(old_gate.qubits[1], old_gate.qubits[0]),
old_gate.param_idx,
) )
child = circ_from_layers(child_layers, qubits) child = circ_from_layers(child_layers, qubits)
@ -164,35 +86,22 @@ def run_ga_qas(
else: else:
best_circuits.append(offspring[0]) best_circuits.append(offspring[0])
population = offspring population = offspring
print(f"finished seed {seed}", file=sys.stderr) print(f"finished seed {seed}, data: {return_data}", file=sys.stderr)
return return_data return return_data
def run_from_point(pnt: tuple[tuple[int, int, int, int, float], int]): def run_from_point(pnt: tuple[tuple[int, int, int, int, float], int]):
(point, seed) = pnt (point, seed) = pnt
return run_ga_qas( try:
point[0], return run_ga_qas(point[0], point[1], 20, point[2], point[3], point[4], seed)
point[1], except:
20, print(f"There was an error for {point}, {seed}, ignoring it")
point[2], return []
point[3],
point[4],
seed,
)
def print_ret(ret_data): def print_ret(ret_data):
for dat in ret_data: for dat in ret_data:
( (depth, qubits, generation, generation_size, parent_amount, mutation_rate, best_pop, best_offspring) = dat
depth,
qubits,
generation,
generation_size,
parent_amount,
mutation_rate,
best_pop,
best_offspring,
) = dat
print( print(
f"{depth},{qubits},{generation},{generation_size},{parent_amount},{mutation_rate},{best_pop},{best_offspring}", f"{depth},{qubits},{generation},{generation_size},{parent_amount},{mutation_rate},{best_pop},{best_offspring}",
flush=True, flush=True,
@ -201,19 +110,10 @@ def print_ret(ret_data):
def main() -> None: def main() -> None:
rng = random.Random(123456789) rng = random.Random()
results: list[QuantumCircuit] = ( results: list[QuantumCircuit] = (
Stream( Stream(sample_hyperspace((1, 40), (1, 10), (1, 100), (1, 20), (0.0, 1.0), seed=rng.randint(1000, 1000000000)))
sample_hyperspace(
(1, 40),
(1, 10),
(1, 100),
(1, 20),
(0.0, 1.0),
seed=rng.randint(1000, 1000000000),
)
)
.map(lambda point: (point, rng.randint(1000, 1000000000))) .map(lambda point: (point, rng.randint(1000, 1000000000)))
.par_map(run_from_point) .par_map(run_from_point)
.apply(print_ret) .apply(print_ret)

View file

@ -307,7 +307,7 @@ def sample_circuit_layers(
total_single += 1 total_single += 1
else: else:
if loc[1] == qubits: if loc[1] == qubits:
loc[1] == 0 loc: tuple[int, int] = (loc[0], 0)
layer.append(Gate(GateType(gate_type), loc, params)) layer.append(Gate(GateType(gate_type), loc, params))
total_double += 1 total_double += 1
params += param_count(gate_type) params += param_count(gate_type)

View file

@ -0,0 +1,46 @@
import random
from collections.abc import Generator
from typing import Never
def sample_hyperspace(
*args: tuple[int, int] | tuple[float, float], seed: int = 2010392991
) -> Generator[tuple[float | int, ...], None, Never]:
minimums: tuple[float | int, ...] = tuple(arg[0] for arg in args)
maximums: tuple[float | int, ...] = tuple(arg[1] for arg in args)
diffs: tuple[float | int, ...] = tuple(ma - mi for mi, ma in zip(minimums, maximums))
idiffs: tuple[float, ...] = tuple(1.0 / diff for diff in diffs)
rng: random.Random = random.Random(seed)
previous_points: set[tuple[float | int, ...]] = set()
def dist(point: tuple[float | int, ...], other: tuple[float | int, ...]) -> float:
return sum(((p - o) * idiff) ** 2 for p, o, idiff in zip(point, other, idiffs))
while True:
sampled_points: list[tuple[float | int, ...]] = [
tuple(
min
+ (
rng.randint(min, min + diff)
if isinstance(min, int) and isinstance(diff, int)
else rng.uniform(min, min + diff)
)
for min, diff in zip(minimums, diffs)
)
for _ in range(10)
]
if len(previous_points) == 0:
previous_points.add(sampled_points[0])
yield sampled_points[0]
min_distances: list[float] = [
min((dist(point, other) for other in previous_points)) for point in sampled_points
]
mdist: float = max(min_distances)
point: tuple[float | int, ...] = sampled_points[[i for i, j in enumerate(min_distances) if j == mdist][0]]
previous_points.add(point)
yield point

View file

@ -1,22 +1,25 @@
#!/usr/bin/env python #!/usr/bin/env python
from __future__ import annotations from __future__ import annotations
import argparse import argparse
from itertools import repeat
import json import json
import math import math
import random import random
from dataclasses import dataclass from dataclasses import dataclass
from enum import IntEnum from enum import IntEnum
from itertools import repeat
from multiprocessing import Pool from multiprocessing import Pool
from typing import override from typing import override
import numpy as np import numpy as np
from qiskit import QuantumCircuit as QiskitCircuit, transpile from qiskit import QuantumCircuit as QiskitCircuit
from qiskit import transpile
from qiskit.circuit import ParameterVector, ParameterVectorElement from qiskit.circuit import ParameterVector, ParameterVectorElement
from qiskit_aer import AerSimulator from qiskit_aer import AerSimulator
from tqdm import tqdm from tqdm import tqdm
from .qas_flow import Stream from qas_flow import Stream
from .quantum_circuit import QuantumCircuit, GateType, sample_layers_generator from quantum_circuit import GateType, QuantumCircuit, sample_layers_generator
# ---------------------------- # ----------------------------
# Paper hyperparameters/defaults # Paper hyperparameters/defaults
@ -95,9 +98,7 @@ def exact_ground_energy(H: np.ndarray) -> float:
return float(w[0]) return float(w[0])
def statevector_from_bound_circuit( def statevector_from_bound_circuit(backend: AerSimulator, tqc: QiskitCircuit, bind: dict) -> np.ndarray:
backend: AerSimulator, tqc: QiskitCircuit, bind: dict
) -> np.ndarray:
res = backend.run([tqc], parameter_binds=[bind]).result() res = backend.run([tqc], parameter_binds=[bind]).result()
sv = np.asarray(res.get_statevector(0), dtype=np.complex128) sv = np.asarray(res.get_statevector(0), dtype=np.complex128)
return sv return sv
@ -129,9 +130,7 @@ def adam_optimize_tfim_energy(
p = circuit.params p = circuit.params
rng = np.random.default_rng(seed) rng = np.random.default_rng(seed)
theta = rng.uniform( theta = rng.uniform(-2 * math.pi, 2 * math.pi, size=p) # paper init range :contentReference[oaicite:7]{index=7}
-2 * math.pi, 2 * math.pi, size=p
) # paper init range :contentReference[oaicite:7]{index=7}
backend = AerSimulator(method="statevector", seed_simulator=seed) backend = AerSimulator(method="statevector", seed_simulator=seed)
tqc = transpile(qc, backend, optimization_level=0) tqc = transpile(qc, backend, optimization_level=0)
@ -160,10 +159,7 @@ def adam_optimize_tfim_energy(
energies = np.array( energies = np.array(
[ [
energy_expectation_from_sv(H, sv) energy_expectation_from_sv(H, sv)
for sv in [ for sv in [np.asarray(res.get_statevector(k), dtype=np.complex128) for k in range(2 * p)]
np.asarray(res.get_statevector(k), dtype=np.complex128)
for k in range(2 * p)
]
] ]
) )
@ -199,17 +195,9 @@ def adam_optimize_tfim_energy(
def ground_truth_tfim( def ground_truth_tfim(
circ: QuantumCircuit, circ: QuantumCircuit, H: np.ndarray, E0: float, seed: int, lr: float, max_steps: int, tol: float
H: np.ndarray,
E0: float,
seed: int,
lr: float,
max_steps: int,
tol: float,
) -> QuantumCircuit: ) -> QuantumCircuit:
best_E, steps = adam_optimize_tfim_energy( best_E, steps = adam_optimize_tfim_energy(circ, H, seed=seed, lr=lr, max_steps=max_steps, tol=tol)
circ, H, seed=seed, lr=lr, max_steps=max_steps, tol=tol
)
err = best_E - E0 err = best_E - E0
circ.gt_energy = best_E circ.gt_energy = best_E
circ.gt_error = err circ.gt_error = err
@ -226,10 +214,7 @@ def ground_truth_tfim(
def parse_args(): def parse_args():
ap = argparse.ArgumentParser() ap = argparse.ArgumentParser()
ap.add_argument( ap.add_argument(
"--qubits", "--qubits", type=int, default=6, help="TFIM qubit count (paper uses 6). :contentReference[oaicite:8]{index=8}"
type=int,
default=6,
help="TFIM qubit count (paper uses 6). :contentReference[oaicite:8]{index=8}",
) )
ap.add_argument( ap.add_argument(
"--depth", "--depth",
@ -255,34 +240,14 @@ def parse_args():
default=5000, default=5000,
help="R: top circuits kept by paths (paper uses 5000). :contentReference[oaicite:12]{index=12}", help="R: top circuits kept by paths (paper uses 5000). :contentReference[oaicite:12]{index=12}",
) )
ap.add_argument( ap.add_argument("--topk", type=int, default=100, help="K: how many top circuits to output/store.")
"--topk", ap.add_argument("--seed", type=int, default=0, help="RNG seed for sampling/reproducibility.")
type=int, ap.add_argument("--periodic", action="store_true", help="Use periodic TFIM boundary (default true if set).")
default=100, ap.add_argument("--no-periodic", dest="periodic", action="store_false", help="Use open boundary TFIM.")
help="K: how many top circuits to output/store.",
)
ap.add_argument(
"--seed", type=int, default=0, help="RNG seed for sampling/reproducibility."
)
ap.add_argument(
"--periodic",
action="store_true",
help="Use periodic TFIM boundary (default true if set).",
)
ap.add_argument(
"--no-periodic",
dest="periodic",
action="store_false",
help="Use open boundary TFIM.",
)
ap.set_defaults(periodic=True) ap.set_defaults(periodic=True)
# ground-truth options # ground-truth options
ap.add_argument( ap.add_argument("--do_ground_truth", action="store_true", help="Also evaluate ground-truth TFIM VQE performance.")
"--do_ground_truth",
action="store_true",
help="Also evaluate ground-truth TFIM VQE performance.",
)
ap.add_argument( ap.add_argument(
"--gt_budget", "--gt_budget",
type=int, type=int,
@ -295,18 +260,8 @@ def parse_args():
default=0.01, default=0.01,
help="Adam learning rate (paper uses 0.01). :contentReference[oaicite:14]{index=14}", help="Adam learning rate (paper uses 0.01). :contentReference[oaicite:14]{index=14}",
) )
ap.add_argument( ap.add_argument("--gt_max_steps", type=int, default=2000, help="Max Adam steps per circuit (practical cap).")
"--gt_max_steps", ap.add_argument("--gt_tol", type=float, default=1e-7, help="Convergence tolerance for |E_t - E_{t-1}|.")
type=int,
default=2000,
help="Max Adam steps per circuit (practical cap).",
)
ap.add_argument(
"--gt_tol",
type=float,
default=1e-7,
help="Convergence tolerance for |E_t - E_{t-1}|.",
)
ap.add_argument("--dump", type=str, default="dump.json", help="Output JSON file.") ap.add_argument("--dump", type=str, default="dump.json", help="Output JSON file.")
return ap.parse_args() return ap.parse_args()
@ -341,9 +296,7 @@ def main():
final_circuits: list[QuantumCircuit] = [] final_circuits: list[QuantumCircuit] = []
with Pool() as p: with Pool() as p:
for circ in tqdm( for circ in tqdm(
p.imap_unordered( p.imap_unordered(expr_worker, zip(candidates, seeds, repeat(args.expressibility_samples))),
expr_worker, zip(candidates, seeds, repeat(args.expressibility_samples))
),
total=len(candidates), total=len(candidates),
desc="expressibility", desc="expressibility",
): ):
@ -366,15 +319,7 @@ def main():
if queried >= args.gt_budget: if queried >= args.gt_budget:
break break
seed = gt_seed_stream.randint(0, 1_000_000_000) seed = gt_seed_stream.randint(0, 1_000_000_000)
ground_truth_tfim( ground_truth_tfim(circ, H, E0, seed=seed, lr=args.gt_lr, max_steps=args.gt_max_steps, tol=args.gt_tol)
circ,
H,
E0,
seed=seed,
lr=args.gt_lr,
max_steps=args.gt_max_steps,
tol=args.gt_tol,
)
if circ.gt_error is not None and circ.gt_error < min_error: if circ.gt_error is not None and circ.gt_error < min_error:
print(f"new best error for {queried}: {circ.gt_error}") print(f"new best error for {queried}: {circ.gt_error}")
min_error = circ.gt_error min_error = circ.gt_error