From 85be714af2ab867ccc53383a5330d54e4dccea66 Mon Sep 17 00:00:00 2001 From: Yizhan Han Date: Mon, 13 Jul 2026 17:46:44 -0400 Subject: [PATCH 1/2] Add kernel block encoding benchmark --- TASK_DETAILS.md | 6 +- TASK_DETAILS_zh-CN.md | 6 +- .../KernelBlockEncoding/README.md | 68 ++ .../KernelBlockEncoding/Task.md | 205 ++++ .../baseline/result_log.txt | 32 + .../KernelBlockEncoding/baseline/solution.cpp | 83 ++ .../frontier_eval/agent_files.txt | 10 + .../frontier_eval/artifact_files.txt | 1 + .../frontier_eval/candidate_destination.txt | 1 + .../frontier_eval/constraints.txt | 14 + .../frontier_eval/copy_files.txt | 1 + .../frontier_eval/eval_command.txt | 1 + .../frontier_eval/eval_cwd.txt | 1 + .../frontier_eval/initial_program.txt | 1 + .../frontier_eval/readonly_files.txt | 7 + .../frontier_eval/run_eval.sh | 16 + .../KernelBlockEncoding/references/README.md | 40 + .../references/problem_config.json | 98 ++ .../KernelBlockEncoding/scripts/init.cpp | 83 ++ .../verification/construction_runtime.hpp | 499 ++++++++ .../verification/evaluator.py | 1084 +++++++++++++++++ .../verification/limited_exec.py | 52 + .../verification/requirements.txt | 1 + .../verification/test_evaluator.py | 141 +++ benchmarks/QuantumComputing/README.md | 40 +- benchmarks/QuantumComputing/README_zh-CN.md | 33 +- 26 files changed, 2497 insertions(+), 27 deletions(-) create mode 100644 benchmarks/QuantumComputing/KernelBlockEncoding/README.md create mode 100644 benchmarks/QuantumComputing/KernelBlockEncoding/Task.md create mode 100644 benchmarks/QuantumComputing/KernelBlockEncoding/baseline/result_log.txt create mode 100644 benchmarks/QuantumComputing/KernelBlockEncoding/baseline/solution.cpp create mode 100644 benchmarks/QuantumComputing/KernelBlockEncoding/frontier_eval/agent_files.txt create mode 100644 benchmarks/QuantumComputing/KernelBlockEncoding/frontier_eval/artifact_files.txt create mode 100644 benchmarks/QuantumComputing/KernelBlockEncoding/frontier_eval/candidate_destination.txt create mode 100644 benchmarks/QuantumComputing/KernelBlockEncoding/frontier_eval/constraints.txt create mode 100644 benchmarks/QuantumComputing/KernelBlockEncoding/frontier_eval/copy_files.txt create mode 100644 benchmarks/QuantumComputing/KernelBlockEncoding/frontier_eval/eval_command.txt create mode 100644 benchmarks/QuantumComputing/KernelBlockEncoding/frontier_eval/eval_cwd.txt create mode 100644 benchmarks/QuantumComputing/KernelBlockEncoding/frontier_eval/initial_program.txt create mode 100644 benchmarks/QuantumComputing/KernelBlockEncoding/frontier_eval/readonly_files.txt create mode 100755 benchmarks/QuantumComputing/KernelBlockEncoding/frontier_eval/run_eval.sh create mode 100644 benchmarks/QuantumComputing/KernelBlockEncoding/references/README.md create mode 100644 benchmarks/QuantumComputing/KernelBlockEncoding/references/problem_config.json create mode 100644 benchmarks/QuantumComputing/KernelBlockEncoding/scripts/init.cpp create mode 100644 benchmarks/QuantumComputing/KernelBlockEncoding/verification/construction_runtime.hpp create mode 100644 benchmarks/QuantumComputing/KernelBlockEncoding/verification/evaluator.py create mode 100644 benchmarks/QuantumComputing/KernelBlockEncoding/verification/limited_exec.py create mode 100644 benchmarks/QuantumComputing/KernelBlockEncoding/verification/requirements.txt create mode 100644 benchmarks/QuantumComputing/KernelBlockEncoding/verification/test_evaluator.py diff --git a/TASK_DETAILS.md b/TASK_DETAILS.md index b331f7d8..fe28b69e 100644 --- a/TASK_DETAILS.md +++ b/TASK_DETAILS.md @@ -56,7 +56,7 @@ We welcome new engineering problem ideas — even without complete verification Cross-modality gene expression prediction (RNA → ADT), NeurIPS 2021 - QuantumComputing + QuantumComputing routing_qftentangled QFT circuit routing optimization on IBM Falcon (gate count & depth) @@ -68,6 +68,10 @@ We welcome new engineering problem ideas — even without complete verification cross_target_qaoa Cross-target robust QAOA optimization for IBM and IonQ backends + + KernelBlockEncoding + Normalization-aware FABLE circuit construction for QML kernel matrices under independent resource and block-error evaluation + Cryptographic AES-128 CTR diff --git a/TASK_DETAILS_zh-CN.md b/TASK_DETAILS_zh-CN.md index 44d1b1fa..8b6d1e35 100644 --- a/TASK_DETAILS_zh-CN.md +++ b/TASK_DETAILS_zh-CN.md @@ -56,7 +56,7 @@ Frontier-Eng 目前已覆盖以下领域的任务。每个任务均配有可运 跨模态基因表达预测(RNA → ADT),NeurIPS 2021 - QuantumComputing + QuantumComputing routing_qftentangled QFT 线路路由优化,IBM Falcon(gate count & depth) @@ -68,6 +68,10 @@ Frontier-Eng 目前已覆盖以下领域的任务。每个任务均配有可运 cross_target_qaoa 跨目标鲁棒 QAOA 优化(IBM & IonQ) + + KernelBlockEncoding + 在独立资源与分块误差评测下,为 QML 核矩阵构造归一化感知的 FABLE 电路 + Cryptographic AES-128 CTR diff --git a/benchmarks/QuantumComputing/KernelBlockEncoding/README.md b/benchmarks/QuantumComputing/KernelBlockEncoding/README.md new file mode 100644 index 00000000..fd3c2190 --- /dev/null +++ b/benchmarks/QuantumComputing/KernelBlockEncoding/README.md @@ -0,0 +1,68 @@ +# Kernel Block Encoding + +This benchmark asks a C++ policy to construct compressed FABLE-style quantum +circuits for fixed-point kernel and Gram matrices used in machine learning. A +candidate chooses a partial-Hadamard basis, normalization, and sparse quantized +rotation vector. The independent evaluator analytically reconstructs the encoded +matrix, enforces the approximation budget, and applies a frozen gate compiler and +resource objective. + +This is a circuit-construction task, not QML training or state-vector simulation. +The candidate directly changes the block encoding that is measured. + +## Files + +- `Task.md`: mathematical contract, C++ API, resource model, workloads, and score. +- `references/problem_config.json`: frozen kernels, limits, and objective weights. +- `references/README.md`: research and construction references. +- `scripts/init.cpp`: editable C++17 starter policy. +- `verification/construction_runtime.hpp`: immutable candidate-side construction API. +- `verification/evaluator.py`: independent workload generator, certificate checker, + analytic block reconstruction, frozen resource compiler, and scorer. +- `verification/limited_exec.py`: thread-safe POSIX hard-limit launcher for candidate + processes. +- `baseline/solution.cpp`: frozen starter used for score normalization. +- `baseline/result_log.txt`: measured reference evaluation. +- `frontier_eval/`: unified-task metadata and evaluator wrapper. + +## Requirements + +- Linux or another POSIX environment with Python 3.10+; +- `g++` with C++17 support; and +- one CPU core with less than 1 GiB RAM. + +There are no Python packages, downloads, containers, external solvers, GPUs, +Qiskit, or quantum simulators. + +The complete frozen-starter evaluation takes about 2.8 seconds on the repository's +current AMD EPYC host, including two C++ compilations and twelve construction runs. + +## Direct evaluation + +From this task directory: + +```bash +python verification/evaluator.py scripts/init.cpp \ + --metrics-out metrics.json --artifacts-out artifacts.json +``` + +The unmodified starter reports `valid=1.0` and `combined_score=1.0`. A score above +1.0 is an improvement over the frozen starter. + +## Unified evaluation + +From the repository root: + +```bash +.venvs/frontier-eval-driver/bin/python -m frontier_eval \ + task=unified \ + task.benchmark=QuantumComputing/KernelBlockEncoding \ + algorithm=openevolve \ + algorithm.iterations=0 +``` + +## Editing contract + +Only change code between `EVOLVE-BLOCK-START` and `EVOLVE-BLOCK-END` in +`scripts/init.cpp`. The evaluator byte-compares the immutable source prefix and +suffix against the frozen baseline before compiling a candidate. diff --git a/benchmarks/QuantumComputing/KernelBlockEncoding/Task.md b/benchmarks/QuantumComputing/KernelBlockEncoding/Task.md new file mode 100644 index 00000000..4ab5cb29 --- /dev/null +++ b/benchmarks/QuantumComputing/KernelBlockEncoding/Task.md @@ -0,0 +1,205 @@ +# Task: Kernel Block Encoding + +## 1. Engineering problem + +Many quantum linear-algebra and quantum machine-learning algorithms assume access +to a unitary whose leading block is a scaled approximation of a classical matrix. +General constructions exist, but the actual circuit can be dominated by data +loading, normalization, controlled rotations, and routing. Kernel matrices make +the issue concrete: they are dense, but their geometry can create exploitable +structure in a suitable basis. + +This task exposes construction co-design directly. Given a real symmetric kernel +matrix, choose: + +- a computational or partial-Hadamard basis; +- the block-encoding normalization; +- which uniformly controlled rotations to retain; and +- the value of every retained, quantized rotation. + +The output is a concrete compressed FABLE construction. The evaluator does not run +a QML model and does not estimate quality from a state-vector sample. It +reconstructs the encoded block from the construction integers and computes the +resources of the corresponding frozen gate schedule. + +## 2. Block-encoding family + +Let `N = 2^n` and let `A` be an `N x N` fixed-point input matrix. A candidate first +selects an `n`-bit `basis_mask`. If bit `k` is set, a normalized Hadamard is applied +to qubit `k` on both sides of the matrix: + +```text +B = H_mask A H_mask +``` + +The evaluator implements this transform with integer butterflies. Applying a +Hadamard on both matrix axes contributes an exact denominator of two, so no +floating-point matrix transform is needed. + +The candidate then selects `s >= max_ij |B_ij|`. Define + +```text +C_ij = B_ij / s +theta_ij = 2 arccos(C_ij) +``` + +Flatten `theta` in row-major order. With `W` the unnormalized Walsh-Hadamard matrix +of order `N^2` and `P_G` the binary-reflected Gray permutation, the uniformly +controlled rotation coefficients are + +```text +hat_theta = P_G (W / N^2) theta. +``` + +Every emitted coefficient is an integer number of +`pi / 268435456` radians. Omitted coefficients are exactly zero. The checker +inverts the Gray permutation and Walsh-Hadamard transform and obtains + +```text +B_tilde_ij = s cos(theta_tilde_ij / 2). +``` + +The FABLE oracle, diffusion layers, row/system swap, and optional basis wrappers +therefore form a unitary `U` on `2n + 1` qubits whose leading block is +`A_tilde / (N s)`. Its normalization is + +```text +alpha = N s. +``` + +The construction uses `n + 1` block-encoding ancillas and `n` system qubits. + +## 3. Validity condition + +For the system-register leading block of `U`, every workload requires + +```text +|| A - alpha (<0^(n+1)| tensor I) U (|0^(n+1)> tensor I) ||_F <= epsilon. +``` + +The evaluator reconstructs the left-hand matrix directly. Since the spectral norm +is no greater than the Frobenius norm, every accepted construction also has +spectral block error at most `epsilon`. + +Targets, basis transforms, scale bounds, Gray permutations, and quantized angle +transforms are independently recomputed in Python. Candidate-reported errors and +resource counts are ignored. A small explicit floating-point guard is added to the +computed trigonometric error before the validity comparison. + +## 4. Candidate API + +Only this function is editable: + +```cpp +void optimize(kernel_block_encoding::Construction& construction); +``` + +The immutable class in `verification/construction_runtime.hpp` provides: + +```cpp +std::size_t dimension() const; +std::size_t system_qubits() const; +std::size_t coefficient_count() const; +long double epsilon() const; +long double target(std::size_t row, std::size_t column) const; + +void set_basis_mask(std::uint32_t mask); +std::int64_t minimum_scale_numerator() const; +void set_scale_numerator(std::int64_t numerator); +void set_scale_multiplier(long double multiplier); + +std::vector exact_angle_ticks() const; +long double frobenius_error(const std::vector& ticks) const; +ResourceEstimate resources(const std::vector& ticks) const; +long double objective(const std::vector& ticks) const; +void commit(std::vector ticks); +``` + +Changing the basis resets the minimum scale. The scale numerator may range from the +new minimum through 16 times that minimum. `exact_angle_ticks()` produces the full +quantized FABLE vector for the current basis and scale; a policy may drop or alter +any tick before committing it. Helper error and resource methods support search, +but only the independent Python reconstruction determines the score. + +## 5. Frozen circuit resources + +The sparse coefficient vector is compiled with the compressed uniform-rotation +schedule: + +1. traverse control states in binary-reflected Gray order; +2. emit one `RY` for each nonzero coefficient; +3. across a run of zero rotations, cancel repeated control toggles and emit one + CNOT for each bit with odd transition parity; +4. close the Gray cycle; +5. add the two `n`-Hadamard diffusion layers and `n` register swaps; and +6. add two Hadamards for every selected basis qubit. + +The reported counts are: + +- arbitrary-angle `RY` rotations; +- oracle and total CNOTs, with each SWAP decomposed into three CNOTs; +- Hadamards; +- logical depth of the frozen schedule; and +- `2n + 1` logical qubits. + +All oracle rotations and CNOTs touch the rotation target, so they are sequential in +this construction. The register swaps are disjoint and contribute three depth +layers. + +## 6. Workloads + +The evaluator deterministically generates six fixed-point matrices: + +1. a 16-sample clustered anisotropic RBF kernel; +2. a 32-sample, higher-dimensional clustered RBF kernel; +3. a 32-sample degree-three polynomial Gram matrix; +4. a 32-sample rational-quadratic kernel; +5. a 32-node random-walk feature Gram matrix; and +6. a 64-sample mixture of two RBF length scales. + +The RBF generators use high-precision decimal exponentiation before fixed-point +rounding. Polynomial, rational, and walk kernels use integer arithmetic. Input +bytes and SHA-256 digests are frozen in `references/problem_config.json` and +reported in `artifacts.json`. + +## 7. Objective + +For a valid construction, the frozen compiled cost is + +```text +16 * RY + 1 * CNOT + 0.25 * H + 0.25 * depth. +``` + +The scenario objective is + +```text +effective_cost = alpha * compiled_cost. +``` + +Multiplying by `alpha` makes normalization part of the optimization: a basis that +compresses strongly can still lose if its transformed maximum entry is large. + +For scenario `i`: + +```text +score_i = baseline_effective_cost_i / candidate_effective_cost_i. +``` + +`combined_score` is the geometric mean across all six scenarios. The direct-basis, +global-threshold starter is frozen as the baseline and scores exactly 1.0. Larger +is better. + +## 8. Limits and reproducibility + +- matrix dimensions are at most 64; +- scale is at most 16 times the basis-dependent minimum; +- angle certificates are at most 256 KiB; +- candidate source is at most 1 MB; +- candidate execution is limited to 5 seconds and 1 GiB per workload; and +- compilation uses `g++ -std=c++17 -O2`. + +There is no network access, external solver, quantum SDK, simulator, container, +GPU, or downloaded data. Compilation is outside candidate execution time but is +included in evaluator wall time. Invalid source edits, malformed certificates, +timeouts, scale violations, or excessive block error produce `valid=0.0` and score +0.0. diff --git a/benchmarks/QuantumComputing/KernelBlockEncoding/baseline/result_log.txt b/benchmarks/QuantumComputing/KernelBlockEncoding/baseline/result_log.txt new file mode 100644 index 00000000..af8c6916 --- /dev/null +++ b/benchmarks/QuantumComputing/KernelBlockEncoding/baseline/result_log.txt @@ -0,0 +1,32 @@ +KernelBlockEncoding frozen starter reference +============================================ + +Command: + python verification/evaluator.py scripts/init.cpp \ + --metrics-out metrics.json --artifacts-out artifacts.json + +Environment: + Linux 5.15 x86_64 + Python 3.10.12 + g++ 11.4.0 + AMD EPYC 7313 host + +Summary: + valid=1.0 + combined_score=1.0 + scenario_count=6 + checked_matrix_entries=8448 + total_rotations=6936 + total_cnots=7788 + evaluator_wall_s=2.748 + +Per workload (epsilon, error upper bound, alpha, RY, oracle CNOT, objective): + clustered_rbf_16 0.1055240631 0.0433050655 16 236 256 66724 + anisotropic_rbf_32 0.0925254822 0.0662628832 32 896 1016 507160 + polynomial_gram_32 0.0619373322 0.0455802581 32 946 1020 533320 + rational_quadratic_32 0.1412220001 0.1033587668 32 930 1018 524920 + walk_feature_gram_32 0.1810274124 0.0784774292 32 144 292 87160 + multiscale_rbf_64 0.1095294952 0.0768379219 64 3784 4096 4264464 + +Times are descriptive rather than scored. The score uses only independently +reconstructed block error, normalization, and frozen logical circuit resources. diff --git a/benchmarks/QuantumComputing/KernelBlockEncoding/baseline/solution.cpp b/benchmarks/QuantumComputing/KernelBlockEncoding/baseline/solution.cpp new file mode 100644 index 00000000..968bc41b --- /dev/null +++ b/benchmarks/QuantumComputing/KernelBlockEncoding/baseline/solution.cpp @@ -0,0 +1,83 @@ +// Kernel block-encoding construction policy. +// The evaluator permits edits only inside the marked region. + +#include "construction_runtime.hpp" + +namespace kernel_block_encoding { + +// EVOLVE-BLOCK-START +void optimize(Construction &construction) { + // Valid starter: direct-basis FABLE with a single global pruning threshold. + // Better policies can search partial-Hadamard bases and normalization scales, + // allocate the error budget per coefficient, and alter retained angles. + construction.set_basis_mask(0); + construction.set_scale_numerator(construction.minimum_scale_numerator()); + + const std::vector exact = construction.exact_angle_ticks(); + std::vector magnitudes; + magnitudes.reserve(exact.size()); + for (const std::int64_t tick : exact) { + if (tick != 0) + magnitudes.push_back(std::llabs(tick)); + } + std::sort(magnitudes.begin(), magnitudes.end()); + magnitudes.erase(std::unique(magnitudes.begin(), magnitudes.end()), + magnitudes.end()); + + std::vector best = exact; + const long double error_budget = 0.75L * construction.epsilon(); + std::ptrdiff_t low = 0; + std::ptrdiff_t high = static_cast(magnitudes.size()) - 1; + while (low <= high) { + const std::ptrdiff_t middle = low + (high - low) / 2; + const std::int64_t threshold = magnitudes[static_cast(middle)]; + std::vector trial = exact; + for (std::int64_t &tick : trial) { + if (std::llabs(tick) <= threshold) + tick = 0; + } + if (construction.frobenius_error(trial) <= error_budget) { + best = std::move(trial); + low = middle + 1; + } else { + high = middle - 1; + } + } + construction.commit(std::move(best)); +} +// EVOLVE-BLOCK-END + +} // namespace kernel_block_encoding + +int main(int argc, char **argv) { + std::string input_path; + std::string certificate_path; + for (int index = 1; index < argc; ++index) { + const std::string argument = argv[index]; + if (argument == "--input" && index + 1 < argc) { + input_path = argv[++index]; + } else if (argument == "--certificate" && index + 1 < argc) { + certificate_path = argv[++index]; + } else { + std::cerr << "unknown or incomplete argument: " << argument << '\n'; + return 2; + } + } + if (input_path.empty() || certificate_path.empty()) { + std::cerr + << "usage: candidate --input kernel.kbe --certificate circuit.kbc\n"; + return 2; + } + try { + kernel_block_encoding::Construction construction(input_path, + certificate_path); + kernel_block_encoding::optimize(construction); + construction.write_certificate(); + std::cout << "workload=" << construction.workload_id() + << " dimension=" << construction.dimension() << '\n'; + return 0; + } catch (const std::exception &exception) { + std::cerr << "candidate failure: " << exception.what() << '\n'; + return 1; + } +} diff --git a/benchmarks/QuantumComputing/KernelBlockEncoding/frontier_eval/agent_files.txt b/benchmarks/QuantumComputing/KernelBlockEncoding/frontier_eval/agent_files.txt new file mode 100644 index 00000000..6eeba3d6 --- /dev/null +++ b/benchmarks/QuantumComputing/KernelBlockEncoding/frontier_eval/agent_files.txt @@ -0,0 +1,10 @@ +README.md +Task.md +scripts/init.cpp +verification/construction_runtime.hpp +references/problem_config.json +references/README.md +verification/evaluator.py +verification/limited_exec.py +verification/test_evaluator.py +frontier_eval/constraints.txt diff --git a/benchmarks/QuantumComputing/KernelBlockEncoding/frontier_eval/artifact_files.txt b/benchmarks/QuantumComputing/KernelBlockEncoding/frontier_eval/artifact_files.txt new file mode 100644 index 00000000..d02f2a18 --- /dev/null +++ b/benchmarks/QuantumComputing/KernelBlockEncoding/frontier_eval/artifact_files.txt @@ -0,0 +1 @@ +# metrics.json and artifacts.json are collected by UnifiedTask directly. diff --git a/benchmarks/QuantumComputing/KernelBlockEncoding/frontier_eval/candidate_destination.txt b/benchmarks/QuantumComputing/KernelBlockEncoding/frontier_eval/candidate_destination.txt new file mode 100644 index 00000000..d26dd4fb --- /dev/null +++ b/benchmarks/QuantumComputing/KernelBlockEncoding/frontier_eval/candidate_destination.txt @@ -0,0 +1 @@ +scripts/init.cpp diff --git a/benchmarks/QuantumComputing/KernelBlockEncoding/frontier_eval/constraints.txt b/benchmarks/QuantumComputing/KernelBlockEncoding/frontier_eval/constraints.txt new file mode 100644 index 00000000..3c7b7808 --- /dev/null +++ b/benchmarks/QuantumComputing/KernelBlockEncoding/frontier_eval/constraints.txt @@ -0,0 +1,14 @@ +KernelBlockEncoding constraints: +1) Only modify code between the EVOLVE-BLOCK markers in `scripts/init.cpp`. +2) Preserve `optimize(kernel_block_encoding::Construction&)` and the immutable C++ shell. +3) Commit one sparse FABLE angle vector through the provided runtime. The independent + evaluator reconstructs the encoded matrix and ignores candidate-reported metrics. +4) Every scenario must meet its Frobenius block-error budget. This also guarantees + that spectral block error is no larger than the same public epsilon. +5) Optimize normalization-weighted compiled cost across all kernel families. Basis + compression can lose if it increases alpha; local angle magnitude is not the + same as global matrix-error sensitivity. +6) Do not modify documentation, baseline, references, verification, or + `frontier_eval` metadata. These paths are checked read-only. +7) Use only the fixed C++17 standard-library environment. No downloads, network, + external solvers, quantum SDKs, simulators, containers, or GPUs are available. diff --git a/benchmarks/QuantumComputing/KernelBlockEncoding/frontier_eval/copy_files.txt b/benchmarks/QuantumComputing/KernelBlockEncoding/frontier_eval/copy_files.txt new file mode 100644 index 00000000..9c558e35 --- /dev/null +++ b/benchmarks/QuantumComputing/KernelBlockEncoding/frontier_eval/copy_files.txt @@ -0,0 +1 @@ +. diff --git a/benchmarks/QuantumComputing/KernelBlockEncoding/frontier_eval/eval_command.txt b/benchmarks/QuantumComputing/KernelBlockEncoding/frontier_eval/eval_command.txt new file mode 100644 index 00000000..c8c51ef9 --- /dev/null +++ b/benchmarks/QuantumComputing/KernelBlockEncoding/frontier_eval/eval_command.txt @@ -0,0 +1 @@ +bash frontier_eval/run_eval.sh {python} {candidate} metrics.json artifacts.json diff --git a/benchmarks/QuantumComputing/KernelBlockEncoding/frontier_eval/eval_cwd.txt b/benchmarks/QuantumComputing/KernelBlockEncoding/frontier_eval/eval_cwd.txt new file mode 100644 index 00000000..9c558e35 --- /dev/null +++ b/benchmarks/QuantumComputing/KernelBlockEncoding/frontier_eval/eval_cwd.txt @@ -0,0 +1 @@ +. diff --git a/benchmarks/QuantumComputing/KernelBlockEncoding/frontier_eval/initial_program.txt b/benchmarks/QuantumComputing/KernelBlockEncoding/frontier_eval/initial_program.txt new file mode 100644 index 00000000..d26dd4fb --- /dev/null +++ b/benchmarks/QuantumComputing/KernelBlockEncoding/frontier_eval/initial_program.txt @@ -0,0 +1 @@ +scripts/init.cpp diff --git a/benchmarks/QuantumComputing/KernelBlockEncoding/frontier_eval/readonly_files.txt b/benchmarks/QuantumComputing/KernelBlockEncoding/frontier_eval/readonly_files.txt new file mode 100644 index 00000000..45915938 --- /dev/null +++ b/benchmarks/QuantumComputing/KernelBlockEncoding/frontier_eval/readonly_files.txt @@ -0,0 +1,7 @@ +README.md +Task.md +baseline +references +verification +frontier_eval +frontier_eval/run_eval.sh diff --git a/benchmarks/QuantumComputing/KernelBlockEncoding/frontier_eval/run_eval.sh b/benchmarks/QuantumComputing/KernelBlockEncoding/frontier_eval/run_eval.sh new file mode 100755 index 00000000..f49aa26b --- /dev/null +++ b/benchmarks/QuantumComputing/KernelBlockEncoding/frontier_eval/run_eval.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ $# -ne 4 ]]; then + echo "usage: run_eval.sh PYTHON CANDIDATE METRICS_JSON ARTIFACTS_JSON" >&2 + exit 2 +fi + +python_path=$1 +candidate_path=$2 +metrics_path=$3 +artifacts_path=$4 + +exec "$python_path" verification/evaluator.py "$candidate_path" \ + --metrics-out "$metrics_path" \ + --artifacts-out "$artifacts_path" diff --git a/benchmarks/QuantumComputing/KernelBlockEncoding/references/README.md b/benchmarks/QuantumComputing/KernelBlockEncoding/references/README.md new file mode 100644 index 00000000..20807904 --- /dev/null +++ b/benchmarks/QuantumComputing/KernelBlockEncoding/references/README.md @@ -0,0 +1,40 @@ +# References and scope + +This benchmark implements an original, dependency-free evaluator around the +published FABLE construction. It does not vendor the authors' Qiskit or MATLAB +implementations. + +## Block encodings and QML + +- Gilyén, Su, Low, and Wiebe, + [*Quantum singular value transformation and beyond*](https://arxiv.org/abs/1806.01838), + define the block-encoding/QSVT framework and include quantum machine-learning + applications. +- Nguyen, Kiani, and Lloyd, + [*Block-encoding dense and full-rank kernels using hierarchical matrices*](https://arxiv.org/abs/2201.11329), + explain why generic efficient access is a strong assumption for dense kernel + matrices and study structure-aware alternatives. + +## Circuit constructions + +- Camps and Van Beeumen, + [*FABLE: Fast Approximate Quantum Circuits for Block-Encodings*](https://arxiv.org/abs/2205.00081), + give the Walsh-Hadamard/Gray-code uniformly controlled rotation construction and + its compression rule. The authors' reference repository is + [QuantumComputingLab/fable](https://github.com/QuantumComputingLab/fable). +- Kuklinski and Rempfer, + [*S-FABLE and LS-FABLE*](https://arxiv.org/abs/2401.04234), introduce Hadamard + conjugation as a way to expose compressible structure. This benchmark generalizes + that discrete choice to any subset of system qubits. +- Clader et al., + [*Quantum Resources Required to Block-Encode a Matrix of Classical Data*](https://arxiv.org/abs/2206.03505), + demonstrate why circuit count/depth and data-loading resources must be made + explicit rather than hidden behind a matrix oracle. + +## Benchmark boundary + +The score is a logical construction cost, not a claim about one physical quantum +processor. The frozen weights make normalization, arbitrary rotations, CNOTs, and +depth jointly visible and reproducible. Fault-tolerant synthesis, hardware routing, +noise, and alternative block-encoding families are useful future extensions but +are deliberately outside this first task. diff --git a/benchmarks/QuantumComputing/KernelBlockEncoding/references/problem_config.json b/benchmarks/QuantumComputing/KernelBlockEncoding/references/problem_config.json new file mode 100644 index 00000000..fb022c74 --- /dev/null +++ b/benchmarks/QuantumComputing/KernelBlockEncoding/references/problem_config.json @@ -0,0 +1,98 @@ +{ + "benchmark_id": "kernel_block_encoding", + "matrix_scale": 1048576, + "angle_denominator": 268435456, + "objective": { + "rotation_weight": 16.0, + "cnot_weight": 1.0, + "hadamard_weight": 0.25, + "depth_weight": 0.25, + "normalization_exponent": 1.0 + }, + "limits": { + "max_candidate_bytes": 1000000, + "max_certificate_bytes": 262144, + "max_dimension": 64, + "max_scale_multiplier": 16, + "candidate_timeout_s_per_workload": 5.0, + "compile_timeout_s": 30.0, + "memory_limit_mb": 1024 + }, + "workloads": [ + { + "id": "clustered_rbf_16", + "kind": "rbf", + "dimension": 16, + "feature_dimensions": 3, + "clusters": 4, + "cluster_separation": 11, + "jitter": 3, + "bandwidth_squared": 38, + "seed": 1229782938247303441, + "relative_error_ppm": 18000, + "expected_sha256": "7de83c53135924eb3967f0ee622b9fdb9f55cc19bae3f6b4ceee1d6938b1f39e" + }, + { + "id": "anisotropic_rbf_32", + "kind": "rbf", + "dimension": 32, + "feature_dimensions": 5, + "clusters": 8, + "cluster_separation": 9, + "jitter": 4, + "bandwidth_squared": 52, + "seed": 2459565876494606882, + "relative_error_ppm": 15000, + "expected_sha256": "0a1d779961b02ffbb39ad8a99fa0456c108711af0fd5467cea84ac24ebcaaa84" + }, + { + "id": "polynomial_gram_32", + "kind": "polynomial", + "dimension": 32, + "feature_dimensions": 7, + "feature_bound": 5, + "bias": 96, + "degree": 3, + "seed": 3689348814741910323, + "relative_error_ppm": 12000, + "expected_sha256": "dc9a5b6390679ba3fad120bb89e6cd5bde89e75f933c9623be7716204b4f5374" + }, + { + "id": "rational_quadratic_32", + "kind": "rational_quadratic", + "dimension": 32, + "feature_dimensions": 4, + "feature_bound": 16, + "length_squared": 75, + "seed": 4919131752989213764, + "relative_error_ppm": 14000, + "expected_sha256": "b043f769c9cc82076cadeaac67e08e1237418b82bcb5abf619ff43f86b4ddbd5" + }, + { + "id": "walk_feature_gram_32", + "kind": "walk_gram", + "dimension": 32, + "shortcut_stride": 7, + "walk_steps": 4, + "self_weight": 2, + "relative_error_ppm": 10000, + "expected_sha256": "a04110306f423eb5d12c1ced0c38cfe8dddfba88f7b596540f4c39faa63440f0" + }, + { + "id": "multiscale_rbf_64", + "kind": "multiscale_rbf", + "dimension": 64, + "feature_dimensions": 4, + "clusters": 8, + "cluster_separation": 13, + "jitter": 4, + "bandwidth_squared_small": 36, + "bandwidth_squared_large": 180, + "small_weight_numerator": 3, + "weight_denominator": 5, + "seed": 6148914691236517205, + "relative_error_ppm": 9000, + "expected_sha256": "5bb408adb4839e8817d11397fa014fff09d6f93407396439664640445b4ddd2c" + } + ] +} diff --git a/benchmarks/QuantumComputing/KernelBlockEncoding/scripts/init.cpp b/benchmarks/QuantumComputing/KernelBlockEncoding/scripts/init.cpp new file mode 100644 index 00000000..968bc41b --- /dev/null +++ b/benchmarks/QuantumComputing/KernelBlockEncoding/scripts/init.cpp @@ -0,0 +1,83 @@ +// Kernel block-encoding construction policy. +// The evaluator permits edits only inside the marked region. + +#include "construction_runtime.hpp" + +namespace kernel_block_encoding { + +// EVOLVE-BLOCK-START +void optimize(Construction &construction) { + // Valid starter: direct-basis FABLE with a single global pruning threshold. + // Better policies can search partial-Hadamard bases and normalization scales, + // allocate the error budget per coefficient, and alter retained angles. + construction.set_basis_mask(0); + construction.set_scale_numerator(construction.minimum_scale_numerator()); + + const std::vector exact = construction.exact_angle_ticks(); + std::vector magnitudes; + magnitudes.reserve(exact.size()); + for (const std::int64_t tick : exact) { + if (tick != 0) + magnitudes.push_back(std::llabs(tick)); + } + std::sort(magnitudes.begin(), magnitudes.end()); + magnitudes.erase(std::unique(magnitudes.begin(), magnitudes.end()), + magnitudes.end()); + + std::vector best = exact; + const long double error_budget = 0.75L * construction.epsilon(); + std::ptrdiff_t low = 0; + std::ptrdiff_t high = static_cast(magnitudes.size()) - 1; + while (low <= high) { + const std::ptrdiff_t middle = low + (high - low) / 2; + const std::int64_t threshold = magnitudes[static_cast(middle)]; + std::vector trial = exact; + for (std::int64_t &tick : trial) { + if (std::llabs(tick) <= threshold) + tick = 0; + } + if (construction.frobenius_error(trial) <= error_budget) { + best = std::move(trial); + low = middle + 1; + } else { + high = middle - 1; + } + } + construction.commit(std::move(best)); +} +// EVOLVE-BLOCK-END + +} // namespace kernel_block_encoding + +int main(int argc, char **argv) { + std::string input_path; + std::string certificate_path; + for (int index = 1; index < argc; ++index) { + const std::string argument = argv[index]; + if (argument == "--input" && index + 1 < argc) { + input_path = argv[++index]; + } else if (argument == "--certificate" && index + 1 < argc) { + certificate_path = argv[++index]; + } else { + std::cerr << "unknown or incomplete argument: " << argument << '\n'; + return 2; + } + } + if (input_path.empty() || certificate_path.empty()) { + std::cerr + << "usage: candidate --input kernel.kbe --certificate circuit.kbc\n"; + return 2; + } + try { + kernel_block_encoding::Construction construction(input_path, + certificate_path); + kernel_block_encoding::optimize(construction); + construction.write_certificate(); + std::cout << "workload=" << construction.workload_id() + << " dimension=" << construction.dimension() << '\n'; + return 0; + } catch (const std::exception &exception) { + std::cerr << "candidate failure: " << exception.what() << '\n'; + return 1; + } +} diff --git a/benchmarks/QuantumComputing/KernelBlockEncoding/verification/construction_runtime.hpp b/benchmarks/QuantumComputing/KernelBlockEncoding/verification/construction_runtime.hpp new file mode 100644 index 00000000..93192ee3 --- /dev/null +++ b/benchmarks/QuantumComputing/KernelBlockEncoding/verification/construction_runtime.hpp @@ -0,0 +1,499 @@ +#ifndef KERNEL_BLOCK_ENCODING_CONSTRUCTION_RUNTIME_HPP +#define KERNEL_BLOCK_ENCODING_CONSTRUCTION_RUNTIME_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace kernel_block_encoding { + +inline constexpr std::int64_t kAngleDenominator = std::int64_t{1} << 28; +inline constexpr std::int64_t kMaximumAngleTicks = 4 * kAngleDenominator; +inline constexpr std::int64_t kMaximumScaleMultiplier = 16; +inline constexpr long double kRotationWeight = 16.0L; +inline constexpr long double kCnotWeight = 1.0L; +inline constexpr long double kHadamardWeight = 0.25L; +inline constexpr long double kDepthWeight = 0.25L; + +struct InputData { + std::string workload_id; + std::size_t dimension = 0; + std::int64_t matrix_scale = 0; + std::int64_t epsilon_numerator = 0; + std::vector matrix; +}; + +inline bool is_power_of_two(std::size_t value) { + return value >= 2 && (value & (value - 1)) == 0; +} + +inline std::size_t integer_log2(std::size_t value) { + if (!is_power_of_two(value)) { + throw std::runtime_error("dimension is not a supported power of two"); + } + std::size_t result = 0; + while ((std::size_t{1} << result) < value) + ++result; + return result; +} + +inline void expect_token(std::istream &input, const std::string &expected) { + std::string actual; + if (!(input >> actual) || actual != expected) { + throw std::runtime_error("malformed input: expected token " + expected); + } +} + +inline InputData read_input(const std::string &path) { + std::ifstream input(path); + if (!input) + throw std::runtime_error("cannot open kernel input"); + InputData data; + expect_token(input, "KBE_INPUT_V1"); + expect_token(input, "workload"); + if (!(input >> data.workload_id) || data.workload_id.empty()) { + throw std::runtime_error("malformed workload id"); + } + expect_token(input, "dimension"); + if (!(input >> data.dimension) || !is_power_of_two(data.dimension) || + data.dimension > 64) { + throw std::runtime_error("dimension must be a power of two in [2, 64]"); + } + expect_token(input, "matrix_scale"); + if (!(input >> data.matrix_scale) || data.matrix_scale <= 0) { + throw std::runtime_error("matrix_scale must be positive"); + } + expect_token(input, "epsilon_numerator"); + if (!(input >> data.epsilon_numerator) || data.epsilon_numerator <= 0) { + throw std::runtime_error("epsilon_numerator must be positive"); + } + expect_token(input, "matrix"); + const std::size_t entries = data.dimension * data.dimension; + data.matrix.resize(entries); + for (std::size_t index = 0; index < entries; ++index) { + if (!(input >> data.matrix[index]) || + std::llabs(data.matrix[index]) > data.matrix_scale) { + throw std::runtime_error("invalid fixed-point matrix entry"); + } + } + expect_token(input, "end"); + std::string trailing; + if (input >> trailing) + throw std::runtime_error("trailing kernel input data"); + return data; +} + +inline std::uint64_t gray_code(std::uint64_t value) { + return value ^ (value >> 1U); +} + +inline unsigned popcount64(std::uint64_t value) { +#if defined(__GNUC__) || defined(__clang__) + return static_cast(__builtin_popcountll(value)); +#else + unsigned count = 0; + while (value != 0) { + value &= value - 1; + ++count; + } + return count; +#endif +} + +inline unsigned trailing_zeroes64(std::uint64_t value) { + if (value == 0) + throw std::runtime_error("zero has no toggled Gray-code bit"); +#if defined(__GNUC__) || defined(__clang__) + return static_cast(__builtin_ctzll(value)); +#else + unsigned count = 0; + while ((value & 1U) == 0U) { + value >>= 1U; + ++count; + } + return count; +#endif +} + +struct ResourceEstimate { + std::uint64_t rotations = 0; + std::uint64_t oracle_cnots = 0; + std::uint64_t cnots = 0; + std::uint64_t hadamards = 0; + std::uint64_t depth = 0; + std::uint64_t qubits = 0; + long double alpha = 0.0L; + long double compiled_cost = 0.0L; + long double objective = 0.0L; +}; + +class Construction { +public: + Construction(std::string input_path, std::string certificate_path) + : input_(read_input(input_path)), + certificate_path_(std::move(certificate_path)) { + qubits_ = integer_log2(input_.dimension); + set_basis_mask(0); + } + + std::size_t dimension() const { return input_.dimension; } + std::size_t system_qubits() const { return qubits_; } + std::size_t coefficient_count() const { + return input_.dimension * input_.dimension; + } + std::uint32_t basis_mask() const { return basis_mask_; } + std::int64_t matrix_scale() const { return input_.matrix_scale; } + std::int64_t minimum_scale_numerator() const { + return minimum_scale_numerator_; + } + std::int64_t scale_numerator() const { return scale_numerator_; } + std::int64_t basis_denominator_multiplier() const { + return basis_denominator_multiplier_; + } + long double epsilon() const { + return static_cast(input_.epsilon_numerator) / + static_cast(input_.matrix_scale); + } + const std::string &workload_id() const { return input_.workload_id; } + + long double target(std::size_t row, std::size_t column) const { + check_matrix_index(row, column); + return static_cast( + input_.matrix[row * input_.dimension + column]) / + static_cast(input_.matrix_scale); + } + + long double basis_target(std::size_t row, std::size_t column) const { + check_matrix_index(row, column); + return static_cast( + basis_numerators_[row * input_.dimension + column]) / + basis_denominator(); + } + + void set_basis_mask(std::uint32_t mask) { + const std::uint32_t valid_mask = + static_cast(input_.dimension - 1U); + if ((mask & ~valid_mask) != 0U) { + throw std::runtime_error( + "basis mask addresses a nonexistent system qubit"); + } + basis_mask_ = mask; + basis_numerators_ = input_.matrix; + basis_denominator_multiplier_ = 1; + for (std::size_t bit = 0; bit < qubits_; ++bit) { + if ((mask & (std::uint32_t{1} << bit)) == 0U) + continue; + apply_row_butterfly(bit); + apply_column_butterfly(bit); + basis_denominator_multiplier_ *= 2; + } + minimum_scale_numerator_ = 0; + for (const std::int64_t value : basis_numerators_) { + minimum_scale_numerator_ = + std::max(minimum_scale_numerator_, + static_cast(std::llabs(value))); + } + if (minimum_scale_numerator_ <= 0) { + throw std::runtime_error("kernel transformed to an all-zero matrix"); + } + scale_numerator_ = minimum_scale_numerator_; + committed_ = false; + committed_angles_.clear(); + } + + void set_scale_numerator(std::int64_t numerator) { + if (minimum_scale_numerator_ > + std::numeric_limits::max() / kMaximumScaleMultiplier) { + throw std::runtime_error("normalization scale range overflows int64"); + } + const std::int64_t maximum = + minimum_scale_numerator_ * kMaximumScaleMultiplier; + if (numerator < minimum_scale_numerator_ || numerator > maximum) { + throw std::runtime_error( + "normalization scale is outside the public range"); + } + scale_numerator_ = numerator; + committed_ = false; + committed_angles_.clear(); + } + + void set_scale_multiplier(long double multiplier) { + if (!std::isfinite(multiplier) || multiplier < 1.0L || + multiplier > static_cast(kMaximumScaleMultiplier)) { + throw std::runtime_error("normalization multiplier must lie in [1, 16]"); + } + const long double requested = + static_cast(minimum_scale_numerator_) * multiplier; + const auto numerator = static_cast(std::ceil(requested)); + set_scale_numerator(std::max(numerator, minimum_scale_numerator_)); + } + + std::vector exact_angle_ticks() const { + const std::size_t count = coefficient_count(); + std::vector transformed(count); + for (std::size_t index = 0; index < count; ++index) { + long double normalized = + static_cast(basis_numerators_[index]) / + static_cast(scale_numerator_); + normalized = std::max(-1.0L, std::min(1.0L, normalized)); + transformed[index] = 2.0L * std::acos(normalized); + } + scaled_walsh_hadamard(transformed); + const long double pi = std::acos(-1.0L); + std::vector result(count, 0); + for (std::size_t index = 0; index < count; ++index) { + const long double ticks = transformed[gray_code(index)] * + static_cast(kAngleDenominator) / + pi; + if (!std::isfinite(ticks) || + std::fabs(ticks) > static_cast(kMaximumAngleTicks)) { + throw std::runtime_error( + "generated angle lies outside certificate range"); + } + result[index] = static_cast(std::llround(ticks)); + } + return result; + } + + long double + frobenius_error(const std::vector &angle_ticks) const { + validate_angles(angle_ticks); + const std::vector entry_angle_ticks = + inverse_angle_transform(angle_ticks); + const long double pi = std::acos(-1.0L); + const long double scale = static_cast(scale_numerator_); + long double squared_error = 0.0L; + for (std::size_t index = 0; index < coefficient_count(); ++index) { + const long double approximate_numerator = + scale * + std::cos(pi * static_cast(entry_angle_ticks[index]) / + (2.0L * static_cast(kAngleDenominator))); + const long double residual = + (static_cast(basis_numerators_[index]) - + approximate_numerator) / + basis_denominator(); + squared_error += residual * residual; + } + return std::sqrt(std::max(0.0L, squared_error)); + } + + ResourceEstimate + resources(const std::vector &angle_ticks) const { + validate_angles(angle_ticks); + ResourceEstimate estimate; + const std::size_t count = coefficient_count(); + const std::size_t controls = 2 * qubits_; + std::size_t index = 0; + while (index < count) { + std::uint64_t parity = 0; + if (angle_ticks[index] != 0) + ++estimate.rotations; + while (true) { + unsigned bit = 0; + if (index + 1 == count) { + bit = static_cast(controls - 1); + } else { + bit = trailing_zeroes64(gray_code(index) ^ gray_code(index + 1)); + } + parity ^= std::uint64_t{1} << bit; + ++index; + if (index >= count || angle_ticks[index] != 0) + break; + } + estimate.oracle_cnots += popcount64(parity); + } + estimate.cnots = estimate.oracle_cnots + 3 * qubits_; + estimate.hadamards = + 2 * qubits_ + 2 * popcount64(static_cast(basis_mask_)); + estimate.depth = estimate.rotations + estimate.oracle_cnots + 5 + + (basis_mask_ == 0 ? 0 : 2); + estimate.qubits = 2 * qubits_ + 1; + estimate.alpha = static_cast(input_.dimension) * + static_cast(scale_numerator_) / + basis_denominator(); + estimate.compiled_cost = + kRotationWeight * static_cast(estimate.rotations) + + kCnotWeight * static_cast(estimate.cnots) + + kHadamardWeight * static_cast(estimate.hadamards) + + kDepthWeight * static_cast(estimate.depth); + estimate.objective = estimate.alpha * estimate.compiled_cost; + return estimate; + } + + long double objective(const std::vector &angle_ticks) const { + return resources(angle_ticks).objective; + } + + void commit(std::vector angle_ticks) { + validate_angles(angle_ticks); + committed_angles_ = std::move(angle_ticks); + committed_ = true; + } + + void write_certificate() const { + if (!committed_) { + throw std::runtime_error("optimizer did not commit a construction"); + } + std::ofstream output(certificate_path_, std::ios::trunc); + if (!output) + throw std::runtime_error("cannot create construction certificate"); + std::size_t nonzero = 0; + for (const auto tick : committed_angles_) { + if (tick != 0) + ++nonzero; + } + output << "KBE_CERTIFICATE_V1\n" + << "dimension " << input_.dimension << '\n' + << "basis_mask " << basis_mask_ << '\n' + << "scale_numerator " << scale_numerator_ << '\n' + << "angle_denominator " << kAngleDenominator << '\n' + << "nonzero_count " << nonzero << '\n' + << "angles\n"; + for (std::size_t index = 0; index < committed_angles_.size(); ++index) { + if (committed_angles_[index] != 0) { + output << index << ' ' << committed_angles_[index] << '\n'; + } + } + output << "end\n"; + output.flush(); + if (!output) + throw std::runtime_error("failed to write construction certificate"); + } + +private: + long double basis_denominator() const { + return static_cast(input_.matrix_scale) * + static_cast(basis_denominator_multiplier_); + } + + void check_matrix_index(std::size_t row, std::size_t column) const { + if (row >= input_.dimension || column >= input_.dimension) { + throw std::out_of_range("kernel matrix index out of range"); + } + } + + static std::int64_t checked_add(std::int64_t lhs, std::int64_t rhs) { + const auto minimum = std::numeric_limits::min(); + const auto maximum = std::numeric_limits::max(); + if ((rhs > 0 && lhs > maximum - rhs) || (rhs < 0 && lhs < minimum - rhs)) { + throw std::runtime_error("integer overflow in basis transform"); + } + return lhs + rhs; + } + + static std::int64_t checked_subtract(std::int64_t lhs, std::int64_t rhs) { + const auto minimum = std::numeric_limits::min(); + const auto maximum = std::numeric_limits::max(); + if ((rhs > 0 && lhs < minimum + rhs) || (rhs < 0 && lhs > maximum + rhs)) { + throw std::runtime_error("integer overflow in basis transform"); + } + return lhs - rhs; + } + + void apply_row_butterfly(std::size_t bit) { + const std::size_t stride = std::size_t{1} << bit; + const std::size_t block = 2 * stride; + for (std::size_t base = 0; base < input_.dimension; base += block) { + for (std::size_t offset = 0; offset < stride; ++offset) { + const std::size_t row0 = base + offset; + const std::size_t row1 = row0 + stride; + for (std::size_t column = 0; column < input_.dimension; ++column) { + const std::size_t index0 = row0 * input_.dimension + column; + const std::size_t index1 = row1 * input_.dimension + column; + const std::int64_t lhs = basis_numerators_[index0]; + const std::int64_t rhs = basis_numerators_[index1]; + basis_numerators_[index0] = checked_add(lhs, rhs); + basis_numerators_[index1] = checked_subtract(lhs, rhs); + } + } + } + } + + void apply_column_butterfly(std::size_t bit) { + const std::size_t stride = std::size_t{1} << bit; + const std::size_t block = 2 * stride; + for (std::size_t row = 0; row < input_.dimension; ++row) { + for (std::size_t base = 0; base < input_.dimension; base += block) { + for (std::size_t offset = 0; offset < stride; ++offset) { + const std::size_t column0 = base + offset; + const std::size_t column1 = column0 + stride; + const std::size_t index0 = row * input_.dimension + column0; + const std::size_t index1 = row * input_.dimension + column1; + const std::int64_t lhs = basis_numerators_[index0]; + const std::int64_t rhs = basis_numerators_[index1]; + basis_numerators_[index0] = checked_add(lhs, rhs); + basis_numerators_[index1] = checked_subtract(lhs, rhs); + } + } + } + } + + static void scaled_walsh_hadamard(std::vector &values) { + for (std::size_t stride = 1; stride < values.size(); stride *= 2) { + for (std::size_t base = 0; base < values.size(); base += 2 * stride) { + for (std::size_t offset = 0; offset < stride; ++offset) { + const long double lhs = values[base + offset]; + const long double rhs = values[base + offset + stride]; + values[base + offset] = (lhs + rhs) / 2.0L; + values[base + offset + stride] = (lhs - rhs) / 2.0L; + } + } + } + } + + void validate_angles(const std::vector &angle_ticks) const { + if (angle_ticks.size() != coefficient_count()) { + throw std::runtime_error("angle vector has invalid size"); + } + for (const auto tick : angle_ticks) { + if (std::llabs(tick) > kMaximumAngleTicks) { + throw std::runtime_error("angle tick lies outside certificate range"); + } + } + } + + std::vector + inverse_angle_transform(const std::vector &angle_ticks) const { + std::vector values(angle_ticks.size(), 0); + for (std::size_t index = 0; index < angle_ticks.size(); ++index) { + values[gray_code(index)] = angle_ticks[index]; + } + for (std::size_t stride = 1; stride < values.size(); stride *= 2) { + for (std::size_t base = 0; base < values.size(); base += 2 * stride) { + for (std::size_t offset = 0; offset < stride; ++offset) { + const std::int64_t lhs = values[base + offset]; + const std::int64_t rhs = values[base + offset + stride]; + values[base + offset] = checked_add(lhs, rhs); + values[base + offset + stride] = checked_subtract(lhs, rhs); + } + } + } + return values; + } + + InputData input_; + std::string certificate_path_; + std::size_t qubits_ = 0; + std::uint32_t basis_mask_ = 0; + std::int64_t basis_denominator_multiplier_ = 1; + std::vector basis_numerators_; + std::int64_t minimum_scale_numerator_ = 0; + std::int64_t scale_numerator_ = 0; + bool committed_ = false; + std::vector committed_angles_; +}; + +} // namespace kernel_block_encoding + +#endif // KERNEL_BLOCK_ENCODING_CONSTRUCTION_RUNTIME_HPP diff --git a/benchmarks/QuantumComputing/KernelBlockEncoding/verification/evaluator.py b/benchmarks/QuantumComputing/KernelBlockEncoding/verification/evaluator.py new file mode 100644 index 00000000..55d18ac8 --- /dev/null +++ b/benchmarks/QuantumComputing/KernelBlockEncoding/verification/evaluator.py @@ -0,0 +1,1084 @@ +"""Independent evaluator for resource-aware kernel block encodings.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import math +import os +import re +import shutil +import signal +import subprocess +import sys +import tempfile +import time +from dataclasses import dataclass +from decimal import Decimal, ROUND_CEILING, ROUND_HALF_EVEN, localcontext +from pathlib import Path +from typing import Any, Callable + + +TASK_DIR = Path(__file__).resolve().parents[1] +CONFIG_PATH = TASK_DIR / "references" / "problem_config.json" +BASELINE_PATH = TASK_DIR / "baseline" / "solution.cpp" +RUNTIME_INCLUDE = TASK_DIR / "verification" +LIMITED_EXEC = RUNTIME_INCLUDE / "limited_exec.py" +START_MARKER = "// EVOLVE-BLOCK-START" +END_MARKER = "// EVOLVE-BLOCK-END" +INTEGER_RE = re.compile(r"-?(?:0|[1-9][0-9]*)\Z") + + +class EvaluationError(RuntimeError): + """A deterministic source, process, certificate, or validity failure.""" + + +def _strict_json(path: Path) -> dict[str, Any]: + def reject_constant(value: str) -> None: + raise ValueError(f"non-finite JSON constant is not allowed: {value}") + + data = json.loads(path.read_text(encoding="utf-8"), parse_constant=reject_constant) + if not isinstance(data, dict): + raise ValueError("configuration root must be an object") + return data + + +def _positive_int(value: Any, label: str) -> int: + if isinstance(value, bool) or not isinstance(value, int) or value <= 0: + raise ValueError(f"{label} must be a positive integer") + return value + + +def _finite_float(value: Any, label: str) -> float: + if isinstance(value, bool) or not isinstance(value, (int, float)): + raise ValueError(f"{label} must be a finite number") + result = float(value) + if not math.isfinite(result): + raise ValueError(f"{label} must be finite") + return result + + +def _power_of_two(value: int) -> bool: + return value >= 2 and value & (value - 1) == 0 + + +def _validated_config() -> dict[str, Any]: + config = _strict_json(CONFIG_PATH) + if config.get("benchmark_id") != "kernel_block_encoding": + raise ValueError("unexpected benchmark_id") + matrix_scale = _positive_int(config.get("matrix_scale"), "matrix_scale") + angle_denominator = _positive_int( + config.get("angle_denominator"), "angle_denominator" + ) + if angle_denominator != 1 << 28: + raise ValueError("angle_denominator does not match the frozen C++ runtime") + objective = config.get("objective") + limits = config.get("limits") + workloads = config.get("workloads") + if not isinstance(objective, dict) or not isinstance(limits, dict): + raise ValueError("objective and limits must be objects") + if not isinstance(workloads, list) or not workloads: + raise ValueError("workloads must be a non-empty list") + frozen_weights = { + "rotation_weight": 16.0, + "cnot_weight": 1.0, + "hadamard_weight": 0.25, + "depth_weight": 0.25, + } + for key, frozen_value in frozen_weights.items(): + value = _finite_float(objective.get(key), key) + if value <= 0.0: + raise ValueError(f"{key} must be positive") + if value != frozen_value: + raise ValueError(f"{key} does not match the frozen C++ runtime") + normalization_exponent = _finite_float( + objective.get("normalization_exponent"), "normalization_exponent" + ) + if normalization_exponent <= 0: + raise ValueError("normalization_exponent must be positive") + if normalization_exponent != 1.0: + raise ValueError("normalization_exponent does not match the frozen runtime") + for key in ( + "max_candidate_bytes", + "max_certificate_bytes", + "max_dimension", + "max_scale_multiplier", + "memory_limit_mb", + ): + _positive_int(limits.get(key), key) + if int(limits["max_scale_multiplier"]) != 16: + raise ValueError("max_scale_multiplier does not match the frozen C++ runtime") + if _finite_float(limits.get("candidate_timeout_s_per_workload"), "candidate timeout") <= 0: + raise ValueError("candidate timeout must be positive") + if _finite_float(limits.get("compile_timeout_s"), "compile timeout") <= 0: + raise ValueError("compile timeout must be positive") + + ids: set[str] = set() + supported = {"rbf", "multiscale_rbf", "polynomial", "rational_quadratic", "walk_gram"} + for index, spec in enumerate(workloads): + if not isinstance(spec, dict): + raise ValueError(f"workloads[{index}] must be an object") + workload_id = str(spec.get("id", "")) + kind = str(spec.get("kind", "")) + dimension = _positive_int(spec.get("dimension"), f"{workload_id}.dimension") + if ( + not workload_id + or workload_id in ids + or not re.fullmatch(r"[a-z0-9_]+", workload_id) + or kind not in supported + or not _power_of_two(dimension) + or dimension > int(limits["max_dimension"]) + ): + raise ValueError(f"invalid workload at index {index}") + relative_error_ppm = _positive_int( + spec.get("relative_error_ppm"), f"{workload_id}.relative_error_ppm" + ) + if relative_error_ppm >= 1_000_000: + raise ValueError("relative error must be below one") + expected = str(spec.get("expected_sha256", "")) + if not re.fullmatch(r"[0-9a-f]{64}", expected): + raise ValueError(f"invalid expected_sha256 for {workload_id}") + ids.add(workload_id) + if matrix_scale > 1 << 30: + raise ValueError("matrix_scale is unexpectedly large") + return config + + +class Lcg: + """Tiny specified PRNG used only to build deterministic integer features.""" + + def __init__(self, seed: int) -> None: + self.state = seed & ((1 << 64) - 1) + + def next_u64(self) -> int: + self.state = ( + 6364136223846793005 * self.state + 1442695040888963407 + ) & ((1 << 64) - 1) + return self.state + + def bounded(self, low: int, high: int) -> int: + if low > high: + raise ValueError("invalid PRNG range") + return low + self.next_u64() % (high - low + 1) + + +def _clustered_features(spec: dict[str, Any]) -> list[list[int]]: + dimension = _positive_int(spec.get("dimension"), "dimension") + feature_dimensions = _positive_int( + spec.get("feature_dimensions"), "feature_dimensions" + ) + clusters = _positive_int(spec.get("clusters"), "clusters") + separation = _positive_int(spec.get("cluster_separation"), "cluster_separation") + jitter = _positive_int(spec.get("jitter"), "jitter") + seed = _positive_int(spec.get("seed"), "seed") + if dimension % clusters != 0 or clusters > dimension: + raise ValueError("clusters must evenly divide the kernel dimension") + random = Lcg(seed) + centers: list[list[int]] = [] + for cluster in range(clusters): + center: list[int] = [] + for axis in range(feature_dimensions): + permutation = (cluster * (2 * axis + 1) + 3 * axis) % clusters + center.append((2 * permutation - clusters + 1) * separation) + centers.append(center) + features: list[list[int]] = [] + per_cluster = dimension // clusters + for row in range(dimension): + cluster = row // per_cluster + features.append( + [ + centers[cluster][axis] + random.bounded(-jitter, jitter) + for axis in range(feature_dimensions) + ] + ) + return features + + +def _decimal_exp_ticks(exponent: Decimal, matrix_scale: int) -> int: + with localcontext() as context: + context.prec = 60 + value = exponent.exp() * Decimal(matrix_scale) + return int(value.to_integral_value(rounding=ROUND_HALF_EVEN)) + + +def _rbf_matrix(spec: dict[str, Any], matrix_scale: int) -> list[int]: + features = _clustered_features(spec) + dimension = len(features) + bandwidth_squared = _positive_int( + spec.get("bandwidth_squared"), "bandwidth_squared" + ) + result = [0] * (dimension * dimension) + for row in range(dimension): + result[row * dimension + row] = matrix_scale + for column in range(row): + distance_squared = sum( + (axis + 1) * (features[row][axis] - features[column][axis]) ** 2 + for axis in range(len(features[row])) + ) + exponent = -Decimal(distance_squared) / Decimal(2 * bandwidth_squared) + ticks = _decimal_exp_ticks(exponent, matrix_scale) + result[row * dimension + column] = ticks + result[column * dimension + row] = ticks + return result + + +def _multiscale_rbf_matrix(spec: dict[str, Any], matrix_scale: int) -> list[int]: + features = _clustered_features(spec) + dimension = len(features) + small = _positive_int( + spec.get("bandwidth_squared_small"), "bandwidth_squared_small" + ) + large = _positive_int( + spec.get("bandwidth_squared_large"), "bandwidth_squared_large" + ) + weight_num = _positive_int( + spec.get("small_weight_numerator"), "small_weight_numerator" + ) + weight_den = _positive_int(spec.get("weight_denominator"), "weight_denominator") + if weight_num >= weight_den: + raise ValueError("small kernel weight must lie strictly between zero and one") + result = [0] * (dimension * dimension) + with localcontext() as context: + context.prec = 60 + weight = Decimal(weight_num) / Decimal(weight_den) + for row in range(dimension): + result[row * dimension + row] = matrix_scale + for column in range(row): + distance_squared = sum( + (axis + 1) * (features[row][axis] - features[column][axis]) ** 2 + for axis in range(len(features[row])) + ) + fine = (-Decimal(distance_squared) / Decimal(2 * small)).exp() + coarse = (-Decimal(distance_squared) / Decimal(2 * large)).exp() + value = (weight * fine + (Decimal(1) - weight) * coarse) * Decimal( + matrix_scale + ) + ticks = int(value.to_integral_value(rounding=ROUND_HALF_EVEN)) + result[row * dimension + column] = ticks + result[column * dimension + row] = ticks + return result + + +def _round_ratio(numerator: int, denominator: int) -> int: + if denominator <= 0: + raise ValueError("ratio denominator must be positive") + sign = -1 if numerator < 0 else 1 + magnitude = abs(numerator) + quotient, remainder = divmod(magnitude, denominator) + if 2 * remainder > denominator or ( + 2 * remainder == denominator and quotient % 2 == 1 + ): + quotient += 1 + return sign * quotient + + +def _polynomial_matrix(spec: dict[str, Any], matrix_scale: int) -> list[int]: + dimension = _positive_int(spec.get("dimension"), "dimension") + feature_dimensions = _positive_int( + spec.get("feature_dimensions"), "feature_dimensions" + ) + feature_bound = _positive_int(spec.get("feature_bound"), "feature_bound") + bias = _positive_int(spec.get("bias"), "bias") + degree = _positive_int(spec.get("degree"), "degree") + seed = _positive_int(spec.get("seed"), "seed") + random = Lcg(seed) + features = [ + [random.bounded(-feature_bound, feature_bound) for _ in range(feature_dimensions)] + for _ in range(dimension) + ] + raw: list[int] = [] + for row in range(dimension): + for column in range(dimension): + inner = bias + sum( + features[row][axis] * features[column][axis] + for axis in range(feature_dimensions) + ) + raw.append(inner**degree) + normalization = max(raw[index * dimension + index] for index in range(dimension)) + if normalization <= 0: + raise ValueError("polynomial Gram matrix has invalid diagonal") + return [_round_ratio(value * matrix_scale, normalization) for value in raw] + + +def _rational_quadratic_matrix( + spec: dict[str, Any], matrix_scale: int +) -> list[int]: + dimension = _positive_int(spec.get("dimension"), "dimension") + feature_dimensions = _positive_int( + spec.get("feature_dimensions"), "feature_dimensions" + ) + feature_bound = _positive_int(spec.get("feature_bound"), "feature_bound") + length_squared = _positive_int(spec.get("length_squared"), "length_squared") + seed = _positive_int(spec.get("seed"), "seed") + random = Lcg(seed) + features = [ + [random.bounded(-feature_bound, feature_bound) for _ in range(feature_dimensions)] + for _ in range(dimension) + ] + result: list[int] = [] + for row in range(dimension): + for column in range(dimension): + distance_squared = sum( + (features[row][axis] - features[column][axis]) ** 2 + for axis in range(feature_dimensions) + ) + result.append( + _round_ratio( + matrix_scale * 2 * length_squared, + 2 * length_squared + distance_squared, + ) + ) + return result + + +def _matrix_multiply(lhs: list[list[int]], rhs: list[list[int]]) -> list[list[int]]: + size = len(lhs) + return [ + [ + sum(lhs[row][inner] * rhs[inner][column] for inner in range(size)) + for column in range(size) + ] + for row in range(size) + ] + + +def _walk_gram_matrix(spec: dict[str, Any], matrix_scale: int) -> list[int]: + dimension = _positive_int(spec.get("dimension"), "dimension") + shortcut = _positive_int(spec.get("shortcut_stride"), "shortcut_stride") + walk_steps = _positive_int(spec.get("walk_steps"), "walk_steps") + self_weight = _positive_int(spec.get("self_weight"), "self_weight") + transition = [[0] * dimension for _ in range(dimension)] + for row in range(dimension): + transition[row][row] += self_weight + transition[row][(row - 1) % dimension] += 1 + transition[row][(row + 1) % dimension] += 1 + transition[row][(row + shortcut) % dimension] += 1 + transition[row][(row - shortcut) % dimension] += 1 + current = [[int(row == column) for column in range(dimension)] for row in range(dimension)] + gram = [[0] * dimension for _ in range(dimension)] + for _ in range(walk_steps + 1): + for row in range(dimension): + for column in range(dimension): + gram[row][column] += sum( + current[row][feature] * current[column][feature] + for feature in range(dimension) + ) + current = _matrix_multiply(current, transition) + normalization = max(gram[index][index] for index in range(dimension)) + return [ + _round_ratio(gram[row][column] * matrix_scale, normalization) + for row in range(dimension) + for column in range(dimension) + ] + + +MATRIX_GENERATORS: dict[str, Callable[[dict[str, Any], int], list[int]]] = { + "rbf": _rbf_matrix, + "multiscale_rbf": _multiscale_rbf_matrix, + "polynomial": _polynomial_matrix, + "rational_quadratic": _rational_quadratic_matrix, + "walk_gram": _walk_gram_matrix, +} + + +@dataclass(frozen=True) +class Workload: + workload_id: str + dimension: int + matrix_scale: int + epsilon_numerator: int + matrix: tuple[int, ...] + input_bytes: bytes + sha256: str + + @property + def epsilon(self) -> float: + return self.epsilon_numerator / self.matrix_scale + + +def _build_workload(spec: dict[str, Any], matrix_scale: int) -> Workload: + workload_id = str(spec["id"]) + dimension = int(spec["dimension"]) + generator = MATRIX_GENERATORS[str(spec["kind"])] + matrix = generator(spec, matrix_scale) + if len(matrix) != dimension * dimension: + raise ValueError(f"generator returned wrong matrix size for {workload_id}") + if any(abs(value) > matrix_scale for value in matrix): + raise ValueError(f"kernel entry exceeds fixed-point range for {workload_id}") + for row in range(dimension): + for column in range(dimension): + if matrix[row * dimension + column] != matrix[column * dimension + row]: + raise ValueError(f"kernel is not symmetric for {workload_id}") + squared_norm = sum(value * value for value in matrix) + with localcontext() as context: + context.prec = 60 + epsilon_numerator = int( + ( + Decimal(squared_norm).sqrt() + * Decimal(int(spec["relative_error_ppm"])) + / Decimal(1_000_000) + ).to_integral_value(rounding=ROUND_CEILING) + ) + lines = [ + "KBE_INPUT_V1", + f"workload {workload_id}", + f"dimension {dimension}", + f"matrix_scale {matrix_scale}", + f"epsilon_numerator {epsilon_numerator}", + "matrix", + ] + for row in range(dimension): + lines.append( + " ".join( + str(matrix[row * dimension + column]) + for column in range(dimension) + ) + ) + lines.append("end") + input_bytes = ("\n".join(lines) + "\n").encode("ascii") + digest = hashlib.sha256(input_bytes).hexdigest() + expected = str(spec.get("expected_sha256", "")) + if digest != expected: + raise EvaluationError(f"frozen workload digest mismatch for {workload_id}") + return Workload( + workload_id=workload_id, + dimension=dimension, + matrix_scale=matrix_scale, + epsilon_numerator=epsilon_numerator, + matrix=tuple(matrix), + input_bytes=input_bytes, + sha256=digest, + ) + + +def _basis_transform( + matrix: tuple[int, ...] | list[int], dimension: int, mask: int +) -> tuple[list[int], int]: + values = list(matrix) + denominator_multiplier = 1 + qubits = dimension.bit_length() - 1 + for bit in range(qubits): + if mask & (1 << bit) == 0: + continue + stride = 1 << bit + block = 2 * stride + for base in range(0, dimension, block): + for offset in range(stride): + row0 = base + offset + row1 = row0 + stride + for column in range(dimension): + index0 = row0 * dimension + column + index1 = row1 * dimension + column + lhs, rhs = values[index0], values[index1] + values[index0], values[index1] = lhs + rhs, lhs - rhs + for row in range(dimension): + for base in range(0, dimension, block): + for offset in range(stride): + column0 = base + offset + column1 = column0 + stride + index0 = row * dimension + column0 + index1 = row * dimension + column1 + lhs, rhs = values[index0], values[index1] + values[index0], values[index1] = lhs + rhs, lhs - rhs + denominator_multiplier *= 2 + return values, denominator_multiplier + + +def _gray_code(value: int) -> int: + return value ^ (value >> 1) + + +def _inverse_angle_transform(angle_ticks: list[int]) -> list[int]: + values = [0] * len(angle_ticks) + for index, tick in enumerate(angle_ticks): + values[_gray_code(index)] = tick + stride = 1 + while stride < len(values): + for base in range(0, len(values), 2 * stride): + for offset in range(stride): + lhs = values[base + offset] + rhs = values[base + offset + stride] + values[base + offset] = lhs + rhs + values[base + offset + stride] = lhs - rhs + stride *= 2 + return values + + +@dataclass(frozen=True) +class Certificate: + basis_mask: int + scale_numerator: int + angle_ticks: tuple[int, ...] + sha256: str + byte_count: int + + +def _parse_integer(token: str, label: str) -> int: + if INTEGER_RE.fullmatch(token) is None or token == "-0": + raise EvaluationError(f"{label} is not a canonical decimal integer") + return int(token) + + +def _parse_certificate( + certificate_bytes: bytes, + workload: Workload, + config: dict[str, Any], +) -> Certificate: + try: + text = certificate_bytes.decode("ascii") + except UnicodeDecodeError as exc: + raise EvaluationError("certificate is not ASCII") from exc + tokens = text.split() + cursor = 0 + + def take(expected: str | None = None) -> str: + nonlocal cursor + if cursor >= len(tokens): + raise EvaluationError("truncated construction certificate") + token = tokens[cursor] + cursor += 1 + if expected is not None and token != expected: + raise EvaluationError(f"expected certificate token {expected}") + return token + + take("KBE_CERTIFICATE_V1") + take("dimension") + dimension = _parse_integer(take(), "dimension") + take("basis_mask") + basis_mask = _parse_integer(take(), "basis_mask") + take("scale_numerator") + scale_numerator = _parse_integer(take(), "scale_numerator") + take("angle_denominator") + angle_denominator = _parse_integer(take(), "angle_denominator") + take("nonzero_count") + nonzero_count = _parse_integer(take(), "nonzero_count") + take("angles") + if dimension != workload.dimension: + raise EvaluationError("certificate dimension does not match workload") + if basis_mask < 0 or basis_mask >= dimension: + raise EvaluationError("basis mask addresses a nonexistent system qubit") + if angle_denominator != int(config["angle_denominator"]): + raise EvaluationError("certificate angle denominator is not frozen value") + coefficient_count = dimension * dimension + if nonzero_count < 0 or nonzero_count > coefficient_count: + raise EvaluationError("invalid nonzero angle count") + angle_ticks = [0] * coefficient_count + previous_index = -1 + maximum_tick = 4 * angle_denominator + for _ in range(nonzero_count): + index = _parse_integer(take(), "angle index") + tick = _parse_integer(take(), "angle tick") + if index <= previous_index or index >= coefficient_count: + raise EvaluationError("angle indices must be strictly increasing and in range") + if tick == 0 or abs(tick) > maximum_tick: + raise EvaluationError("invalid nonzero angle tick") + angle_ticks[index] = tick + previous_index = index + take("end") + if cursor != len(tokens): + raise EvaluationError("trailing construction certificate data") + basis_values, _ = _basis_transform(workload.matrix, dimension, basis_mask) + minimum_scale = max(abs(value) for value in basis_values) + maximum_scale = minimum_scale * int(config["limits"]["max_scale_multiplier"]) + if scale_numerator < minimum_scale or scale_numerator > maximum_scale: + raise EvaluationError("normalization scale is outside the public range") + return Certificate( + basis_mask=basis_mask, + scale_numerator=scale_numerator, + angle_ticks=tuple(angle_ticks), + sha256=hashlib.sha256(certificate_bytes).hexdigest(), + byte_count=len(certificate_bytes), + ) + + +@dataclass(frozen=True) +class CheckedConstruction: + frobenius_error: float + error_upper_bound: float + relative_error: float + alpha: float + rotations: int + oracle_cnots: int + cnots: int + hadamards: int + depth: int + qubits: int + compiled_cost: float + objective: float + basis_denominator_multiplier: int + minimum_scale_numerator: int + + +def _check_construction( + certificate: Certificate, + workload: Workload, + config: dict[str, Any], +) -> CheckedConstruction: + dimension = workload.dimension + basis_values, denominator_multiplier = _basis_transform( + workload.matrix, dimension, certificate.basis_mask + ) + minimum_scale = max(abs(value) for value in basis_values) + entry_angle_ticks = _inverse_angle_transform(list(certificate.angle_ticks)) + angle_denominator = int(config["angle_denominator"]) + denominator = workload.matrix_scale * denominator_multiplier + scale = certificate.scale_numerator / denominator + residuals: list[float] = [] + for target_numerator, theta_tick in zip(basis_values, entry_angle_ticks): + approximate_numerator = certificate.scale_numerator * math.cos( + math.pi * theta_tick / (2.0 * angle_denominator) + ) + residuals.append((target_numerator - approximate_numerator) / denominator) + squared_error = math.fsum(value * value for value in residuals) + frobenius_error = math.sqrt(max(0.0, squared_error)) + numerical_guard = max( + 1e-12, + dimension * max(1.0, abs(scale)) * 32.0 * math.ulp(1.0), + ) + error_upper_bound = frobenius_error + numerical_guard + if error_upper_bound > workload.epsilon: + raise EvaluationError( + f"block error {error_upper_bound:.9g} exceeds epsilon {workload.epsilon:.9g}" + ) + + count = dimension * dimension + controls = 2 * (dimension.bit_length() - 1) + rotations = 0 + oracle_cnots = 0 + index = 0 + while index < count: + parity = 0 + if certificate.angle_ticks[index] != 0: + rotations += 1 + while True: + if index + 1 == count: + bit = controls - 1 + else: + transition = _gray_code(index) ^ _gray_code(index + 1) + bit = (transition & -transition).bit_length() - 1 + parity ^= 1 << bit + index += 1 + if index >= count or certificate.angle_ticks[index] != 0: + break + oracle_cnots += parity.bit_count() + + qubits = dimension.bit_length() - 1 + cnots = oracle_cnots + 3 * qubits + hadamards = 2 * qubits + 2 * certificate.basis_mask.bit_count() + depth = rotations + oracle_cnots + 5 + (2 if certificate.basis_mask else 0) + alpha = dimension * scale + objective_config = config["objective"] + compiled_cost = ( + float(objective_config["rotation_weight"]) * rotations + + float(objective_config["cnot_weight"]) * cnots + + float(objective_config["hadamard_weight"]) * hadamards + + float(objective_config["depth_weight"]) * depth + ) + objective = alpha ** float(objective_config["normalization_exponent"]) * compiled_cost + if not math.isfinite(objective) or objective <= 0.0: + raise EvaluationError("construction has a non-positive or non-finite objective") + target_norm = math.sqrt( + math.fsum((value / workload.matrix_scale) ** 2 for value in workload.matrix) + ) + return CheckedConstruction( + frobenius_error=frobenius_error, + error_upper_bound=error_upper_bound, + relative_error=error_upper_bound / target_norm, + alpha=alpha, + rotations=rotations, + oracle_cnots=oracle_cnots, + cnots=cnots, + hadamards=hadamards, + depth=depth, + qubits=2 * qubits + 1, + compiled_cost=compiled_cost, + objective=objective, + basis_denominator_multiplier=denominator_multiplier, + minimum_scale_numerator=minimum_scale, + ) + + +def _split_source(source: bytes) -> tuple[bytes, bytes, bytes]: + if source.count(START_MARKER.encode()) != 1 or source.count(END_MARKER.encode()) != 1: + raise EvaluationError("candidate must contain exactly one pair of EVOLVE markers") + start = source.index(START_MARKER.encode()) + end = source.index(END_MARKER.encode(), start) + end += len(END_MARKER) + return source[:start], source[start:end], source[end:] + + +def _validate_candidate_shell(candidate_path: Path, maximum_bytes: int) -> bytes: + if not candidate_path.is_file(): + raise EvaluationError("candidate C++ source does not exist") + source = candidate_path.read_bytes() + if not source or len(source) > maximum_bytes: + raise EvaluationError("candidate source is empty or exceeds size limit") + baseline = BASELINE_PATH.read_bytes() + candidate_prefix, _, candidate_suffix = _split_source(source) + baseline_prefix, _, baseline_suffix = _split_source(baseline) + if candidate_prefix != baseline_prefix or candidate_suffix != baseline_suffix: + raise EvaluationError("candidate changed code outside the EVOLVE block") + return source + + +def _tail(path: Path, maximum: int = 4000) -> str: + if not path.is_file(): + return "" + data = path.read_bytes() + return data[-maximum:].decode("utf-8", errors="replace") + + +def _compile( + compiler: str, + source: Path, + output: Path, + timeout_s: float, + log_path: Path, +) -> None: + command = [ + compiler, + "-std=c++17", + "-O2", + "-pipe", + "-Wall", + "-Wextra", + "-I", + str(RUNTIME_INCLUDE), + str(source), + "-o", + str(output), + ] + with log_path.open("wb") as log: + try: + completed = subprocess.run( + command, + stdout=log, + stderr=subprocess.STDOUT, + timeout=timeout_s, + check=False, + env={"PATH": os.environ.get("PATH", ""), "LC_ALL": "C"}, + ) + except subprocess.TimeoutExpired as exc: + raise EvaluationError(f"compilation timed out after {timeout_s:.1f}s") from exc + if completed.returncode != 0: + raise EvaluationError(f"compilation failed: {_tail(log_path)}") + + +@dataclass(frozen=True) +class ProgramRun: + certificate: bytes + stdout_tail: str + elapsed_s: float + + +def _run_program( + executable: Path, + workload: Workload, + run_dir: Path, + timeout_s: float, + maximum_certificate_bytes: int, + memory_mb: int, +) -> ProgramRun: + run_dir.mkdir(parents=True, exist_ok=False) + input_path = run_dir / "kernel.kbe" + certificate_path = run_dir / "construction.kbc" + log_path = run_dir / "candidate.log" + input_path.write_bytes(workload.input_bytes) + cpu_seconds = max(1, int(math.ceil(timeout_s)) + 1) + file_limit = max(maximum_certificate_bytes, 64 * 1024) + command = [ + sys.executable, + str(LIMITED_EXEC), + str(memory_mb), + str(cpu_seconds), + str(file_limit), + str(run_dir), + "--", + str(executable), + "--input", + str(input_path), + "--certificate", + str(certificate_path), + ] + started = time.perf_counter() + with log_path.open("wb") as log: + process = subprocess.Popen( + command, + stdout=log, + stderr=subprocess.STDOUT, + close_fds=False, + env={"PATH": os.environ.get("PATH", ""), "LC_ALL": "C"}, + ) + try: + returncode = process.wait(timeout=timeout_s) + except subprocess.TimeoutExpired as exc: + try: + os.killpg(process.pid, signal.SIGKILL) + except ProcessLookupError: + pass + process.wait() + raise EvaluationError(f"candidate timed out after {timeout_s:.1f}s") from exc + try: + # The candidate is session leader. Remove any descendants it left behind + # before inspecting the supposedly frozen input and certificate. + os.killpg(process.pid, signal.SIGKILL) + except ProcessLookupError: + pass + elapsed = time.perf_counter() - started + if returncode != 0: + raise EvaluationError(f"candidate exited with code {returncode}: {_tail(log_path)}") + if input_path.read_bytes() != workload.input_bytes: + raise EvaluationError("candidate modified the frozen kernel input") + if not certificate_path.is_file(): + raise EvaluationError("candidate did not produce a construction certificate") + if certificate_path.stat().st_size > maximum_certificate_bytes: + raise EvaluationError("construction certificate exceeds size limit") + return ProgramRun( + certificate=certificate_path.read_bytes(), + stdout_tail=_tail(log_path), + elapsed_s=elapsed, + ) + + +def _construction_artifact( + certificate: Certificate, + checked: CheckedConstruction, + run: ProgramRun, +) -> dict[str, Any]: + return { + "basis_mask": certificate.basis_mask, + "scale_numerator": certificate.scale_numerator, + "minimum_scale_numerator": checked.minimum_scale_numerator, + "basis_denominator_multiplier": checked.basis_denominator_multiplier, + "alpha": checked.alpha, + "frobenius_error": checked.frobenius_error, + "error_upper_bound": checked.error_upper_bound, + "relative_error": checked.relative_error, + "rotations": checked.rotations, + "oracle_cnots": checked.oracle_cnots, + "cnots": checked.cnots, + "hadamards": checked.hadamards, + "depth": checked.depth, + "qubits": checked.qubits, + "compiled_cost": checked.compiled_cost, + "objective": checked.objective, + "nonzero_angles": sum(tick != 0 for tick in certificate.angle_ticks), + "certificate_bytes": certificate.byte_count, + "certificate_sha256": certificate.sha256, + "runtime_s": run.elapsed_s, + "stdout_tail": run.stdout_tail, + } + + +def _invalid_metrics(runtime_s: float, *, timeout: bool = False) -> dict[str, float]: + return { + "combined_score": 0.0, + "valid": 0.0, + "timeout": 1.0 if timeout else 0.0, + "runtime_s": float(runtime_s), + } + + +def evaluate( + candidate_path: Path, + *, + timeout_override_s: float | None = None, +) -> tuple[dict[str, float], dict[str, Any]]: + started = time.perf_counter() + artifacts: dict[str, Any] = { + "benchmark_id": "kernel_block_encoding", + "checker": "independent analytic FABLE block reconstruction", + } + try: + config = _validated_config() + limits = config["limits"] + source = _validate_candidate_shell( + candidate_path, int(limits["max_candidate_bytes"]) + ) + compiler = shutil.which("g++") + if compiler is None: + raise EvaluationError("g++ is required but was not found on PATH") + timeout_s = ( + float(timeout_override_s) + if timeout_override_s is not None + else float(limits["candidate_timeout_s_per_workload"]) + ) + if timeout_s <= 0.0: + raise EvaluationError("candidate timeout must be positive") + compile_timeout = float(limits["compile_timeout_s"]) + matrix_scale = int(config["matrix_scale"]) + workloads = [_build_workload(spec, matrix_scale) for spec in config["workloads"]] + artifacts["workload_manifest"] = [ + { + "id": workload.workload_id, + "dimension": workload.dimension, + "epsilon": workload.epsilon, + "input_bytes": len(workload.input_bytes), + "sha256": workload.sha256, + } + for workload in workloads + ] + + with tempfile.TemporaryDirectory(prefix="kernel_block_encoding_eval_") as temporary: + temp = Path(temporary) + candidate_copy = temp / "candidate.cpp" + candidate_copy.write_bytes(source) + candidate_executable = temp / "candidate" + baseline_executable = temp / "baseline" + baseline_compile_log = temp / "baseline_compile.log" + candidate_compile_log = temp / "candidate_compile.log" + compile_started = time.perf_counter() + _compile( + compiler, + BASELINE_PATH, + baseline_executable, + compile_timeout, + baseline_compile_log, + ) + baseline_compile_s = time.perf_counter() - compile_started + compile_started = time.perf_counter() + _compile( + compiler, + candidate_copy, + candidate_executable, + compile_timeout, + candidate_compile_log, + ) + candidate_compile_s = time.perf_counter() - compile_started + + scores: list[float] = [] + minimum_score = math.inf + total_candidate_rotations = 0 + total_candidate_cnots = 0 + total_baseline_rotations = 0 + total_baseline_cnots = 0 + total_checked_entries = 0 + candidate_alpha_sum = 0.0 + results: dict[str, Any] = {} + for index, workload in enumerate(workloads): + baseline_run = _run_program( + baseline_executable, + workload, + temp / f"baseline_{index}", + timeout_s, + int(limits["max_certificate_bytes"]), + int(limits["memory_limit_mb"]), + ) + candidate_run = _run_program( + candidate_executable, + workload, + temp / f"candidate_{index}", + timeout_s, + int(limits["max_certificate_bytes"]), + int(limits["memory_limit_mb"]), + ) + baseline_certificate = _parse_certificate( + baseline_run.certificate, workload, config + ) + candidate_certificate = _parse_certificate( + candidate_run.certificate, workload, config + ) + baseline_checked = _check_construction( + baseline_certificate, workload, config + ) + candidate_checked = _check_construction( + candidate_certificate, workload, config + ) + score = baseline_checked.objective / candidate_checked.objective + if not math.isfinite(score) or score <= 0.0: + raise EvaluationError("scenario score is non-positive or non-finite") + scores.append(score) + minimum_score = min(minimum_score, score) + total_candidate_rotations += candidate_checked.rotations + total_candidate_cnots += candidate_checked.cnots + total_baseline_rotations += baseline_checked.rotations + total_baseline_cnots += baseline_checked.cnots + total_checked_entries += workload.dimension * workload.dimension + candidate_alpha_sum += candidate_checked.alpha + results[workload.workload_id] = { + "score": score, + "epsilon": workload.epsilon, + "input_sha256": workload.sha256, + "baseline": _construction_artifact( + baseline_certificate, baseline_checked, baseline_run + ), + "candidate": _construction_artifact( + candidate_certificate, candidate_checked, candidate_run + ), + } + + combined_score = math.exp( + math.fsum(math.log(score) for score in scores) / len(scores) + ) + runtime_s = time.perf_counter() - started + metrics = { + "combined_score": combined_score, + "valid": 1.0, + "timeout": 0.0, + "runtime_s": runtime_s, + "scenario_count": float(len(scores)), + "mean_score": math.fsum(scores) / len(scores), + "min_score": minimum_score, + "baseline_total_rotations": float(total_baseline_rotations), + "candidate_total_rotations": float(total_candidate_rotations), + "baseline_total_cnots": float(total_baseline_cnots), + "candidate_total_cnots": float(total_candidate_cnots), + "candidate_mean_alpha": candidate_alpha_sum / len(scores), + "checked_matrix_entries": float(total_checked_entries), + } + artifacts["compiler"] = compiler + artifacts["compile"] = { + "baseline_s": baseline_compile_s, + "candidate_s": candidate_compile_s, + } + artifacts["objective"] = config["objective"] + artifacts["limits"] = limits + artifacts["workloads"] = results + return metrics, artifacts + except Exception as exc: + message = str(exc) + artifacts["error_message"] = message + timed_out = "timed out" in message.lower() + return _invalid_metrics(time.perf_counter() - started, timeout=timed_out), artifacts + + +def _write_json(path_text: str | None, payload: dict[str, Any]) -> None: + if not path_text: + return + path = Path(path_text).expanduser().resolve() + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text( + json.dumps(payload, indent=2, sort_keys=True, allow_nan=False) + "\n", + encoding="utf-8", + ) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("candidate", help="candidate C++ source") + parser.add_argument( + "--timeout-s", + type=float, + default=None, + help="optional per-workload execution timeout override", + ) + parser.add_argument("--metrics-out", default=None) + parser.add_argument("--artifacts-out", default=None) + return parser.parse_args() + + +def main() -> int: + args = parse_args() + timeout = None if args.timeout_s is None else max(0.1, float(args.timeout_s)) + metrics, artifacts = evaluate( + Path(args.candidate).expanduser().resolve(), timeout_override_s=timeout + ) + _write_json(args.metrics_out, metrics) + _write_json(args.artifacts_out, artifacts) + print(json.dumps(metrics, sort_keys=True, allow_nan=False)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/benchmarks/QuantumComputing/KernelBlockEncoding/verification/limited_exec.py b/benchmarks/QuantumComputing/KernelBlockEncoding/verification/limited_exec.py new file mode 100644 index 00000000..afe53383 --- /dev/null +++ b/benchmarks/QuantumComputing/KernelBlockEncoding/verification/limited_exec.py @@ -0,0 +1,52 @@ +"""Apply hard POSIX limits to this process, then exec a candidate program.""" + +from __future__ import annotations + +import os +import resource +import sys + + +def _positive_integer(text: str, label: str) -> int: + try: + value = int(text) + except ValueError as exc: + raise SystemExit(f"{label} must be an integer") from exc + if value <= 0: + raise SystemExit(f"{label} must be positive") + return value + + +def main() -> int: + if len(sys.argv) < 7 or sys.argv[5] != "--": + raise SystemExit( + "usage: limited_exec.py MEMORY_MB CPU_SECONDS FILE_BYTES CWD -- " + "PROGRAM [ARG ...]" + ) + memory_mb = _positive_integer(sys.argv[1], "MEMORY_MB") + cpu_seconds = _positive_integer(sys.argv[2], "CPU_SECONDS") + file_bytes = _positive_integer(sys.argv[3], "FILE_BYTES") + working_directory = os.path.abspath(sys.argv[4]) + program = os.path.abspath(sys.argv[6]) + arguments = [program, *sys.argv[7:]] + + # Session creation happens here, in a freshly spawned interpreter, so the + # multithreaded evaluator never needs a fork-time callback. + os.setsid() + os.chdir(working_directory) + memory_bytes = memory_mb * 1024 * 1024 + resource.setrlimit(resource.RLIMIT_AS, (memory_bytes, memory_bytes)) + resource.setrlimit(resource.RLIMIT_CPU, (cpu_seconds, cpu_seconds)) + resource.setrlimit(resource.RLIMIT_FSIZE, (file_bytes, file_bytes)) + resource.setrlimit(resource.RLIMIT_CORE, (0, 0)) + try: + resource.setrlimit(resource.RLIMIT_NOFILE, (64, 64)) + except (ValueError, OSError): + pass + + os.execv(program, arguments) + raise AssertionError("unreachable") # pragma: no cover + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/benchmarks/QuantumComputing/KernelBlockEncoding/verification/requirements.txt b/benchmarks/QuantumComputing/KernelBlockEncoding/verification/requirements.txt new file mode 100644 index 00000000..b7c72a67 --- /dev/null +++ b/benchmarks/QuantumComputing/KernelBlockEncoding/verification/requirements.txt @@ -0,0 +1 @@ +# Standard library only. diff --git a/benchmarks/QuantumComputing/KernelBlockEncoding/verification/test_evaluator.py b/benchmarks/QuantumComputing/KernelBlockEncoding/verification/test_evaluator.py new file mode 100644 index 00000000..38049043 --- /dev/null +++ b/benchmarks/QuantumComputing/KernelBlockEncoding/verification/test_evaluator.py @@ -0,0 +1,141 @@ +"""Focused standard-library tests for the kernel block-encoding evaluator.""" + +from __future__ import annotations + +import math +import unittest + +import evaluator + + +class KernelBlockEncodingTests(unittest.TestCase): + def test_integer_partial_hadamard_transform(self) -> None: + transformed, denominator = evaluator._basis_transform((1, 2, 3, 4), 2, 1) + self.assertEqual(transformed, [10, -2, -4, 0]) + self.assertEqual(denominator, 2) + + def test_identity_round_trip_and_error_rejection(self) -> None: + config = evaluator._validated_config() + scale = int(config["matrix_scale"]) + angle_denominator = int(config["angle_denominator"]) + workload = evaluator.Workload( + workload_id="identity_2", + dimension=2, + matrix_scale=scale, + epsilon_numerator=2, + matrix=(scale, 0, 0, scale), + input_bytes=b"test", + sha256="0" * 64, + ) + entry_ticks = [0, angle_denominator, angle_denominator, 0] + coefficients = list(entry_ticks) + stride = 1 + while stride < len(coefficients): + for base in range(0, len(coefficients), 2 * stride): + for offset in range(stride): + lhs = coefficients[base + offset] + rhs = coefficients[base + offset + stride] + coefficients[base + offset] = (lhs + rhs) // 2 + coefficients[base + offset + stride] = (lhs - rhs) // 2 + stride *= 2 + gray_coefficients = tuple( + coefficients[evaluator._gray_code(index)] + for index in range(len(coefficients)) + ) + certificate = evaluator.Certificate( + basis_mask=0, + scale_numerator=scale, + angle_ticks=gray_coefficients, + sha256="1" * 64, + byte_count=1, + ) + checked = evaluator._check_construction(certificate, workload, config) + self.assertLess(checked.error_upper_bound, workload.epsilon) + self.assertAlmostEqual(checked.alpha, 2.0) + self.assertEqual(checked.qubits, 3) + + invalid = evaluator.Certificate( + basis_mask=0, + scale_numerator=scale, + angle_ticks=(0, 0, 0, 0), + sha256="2" * 64, + byte_count=1, + ) + with self.assertRaises(evaluator.EvaluationError): + evaluator._check_construction(invalid, workload, config) + + def test_walsh_gray_schedule_realizes_multiplexed_rotations(self) -> None: + entry_angles = [0.2, 0.8, 1.4, 2.2] + coefficients = list(entry_angles) + stride = 1 + while stride < len(coefficients): + for base in range(0, len(coefficients), 2 * stride): + for offset in range(stride): + lhs = coefficients[base + offset] + rhs = coefficients[base + offset + stride] + coefficients[base + offset] = (lhs + rhs) / 2 + coefficients[base + offset + stride] = (lhs - rhs) / 2 + stride *= 2 + gray_coefficients = [ + coefficients[evaluator._gray_code(index)] + for index in range(len(coefficients)) + ] + + def multiply( + lhs: list[list[float]], rhs: list[list[float]] + ) -> list[list[float]]: + return [ + [ + sum( + lhs[row][inner] * rhs[inner][column] + for inner in range(2) + ) + for column in range(2) + ] + for row in range(2) + ] + + def rotation(angle: float) -> list[list[float]]: + cosine = math.cos(angle / 2) + sine = math.sin(angle / 2) + return [[cosine, -sine], [sine, cosine]] + + identity = [[1.0, 0.0], [0.0, 1.0]] + bit_flip = [[0.0, 1.0], [1.0, 0.0]] + for control_state, expected_angle in enumerate(entry_angles): + realized = identity + for index, coefficient in enumerate(gray_coefficients): + realized = multiply(rotation(coefficient), realized) + transition_bit = ( + 1 + if index + 1 == len(gray_coefficients) + else ( + evaluator._gray_code(index) + ^ evaluator._gray_code(index + 1) + ).bit_length() + - 1 + ) + if control_state & (1 << transition_bit): + realized = multiply(bit_flip, realized) + expected = rotation(expected_angle) + for row in range(2): + for column in range(2): + self.assertAlmostEqual(realized[row][column], expected[row][column]) + + def test_frozen_workload_hashes_and_source_shell(self) -> None: + config = evaluator._validated_config() + workloads = [ + evaluator._build_workload(spec, int(config["matrix_scale"])) + for spec in config["workloads"] + ] + self.assertEqual(len(workloads), 6) + self.assertTrue(all(workload.sha256 for workload in workloads)) + source = evaluator._validate_candidate_shell( + evaluator.TASK_DIR / "scripts" / "init.cpp", + int(config["limits"]["max_candidate_bytes"]), + ) + self.assertEqual(source, evaluator.BASELINE_PATH.read_bytes()) + + +if __name__ == "__main__": + unittest.main() diff --git a/benchmarks/QuantumComputing/README.md b/benchmarks/QuantumComputing/README.md index bddfb613..e1075a03 100644 --- a/benchmarks/QuantumComputing/README.md +++ b/benchmarks/QuantumComputing/README.md @@ -1,26 +1,31 @@ -# Agent-Evolve Quantum Optimization Tasks +# Quantum Computing Benchmarks -This folder contains three benchmark-driven optimization tasks based on this repository's `mqt.bench` APIs. +This folder contains circuit construction, synthesis, routing, and cross-target +optimization tasks. -## Environment -Use the requested interpreter: +## Task list -```bash -pip install mqt.bench -``` - -## Task List +- [`KernelBlockEncoding`](KernelBlockEncoding/): construct normalization- and + resource-efficient FABLE circuits for fixed-point QML kernel matrices. Its + evaluator is self-contained and CPU-only. - `task_01_routing_qftentangled`: mapped-level routing optimization on IBM Falcon. - `task_02_clifford_t_synthesis`: native-gates (`clifford+t`) synthesis optimization. - `task_03_cross_target_qaoa`: one strategy evaluated on both IBM and IonQ targets. -Current baseline strategies: +The three historical `task_0*` benchmarks use this repository's `mqt.bench` APIs +and require the configured quantum environment. `KernelBlockEncoding` uses only +Python 3 and a C++17 compiler; it requires no quantum SDK or simulator. + +## Historical MQT task structure + +Current baseline strategies for the MQT-backed tasks: + - `task_01`: local rewrite preprocessing followed by target-aware multi-seed transpile search. - `task_02`: `local rewrite -> clifford+t transpile(opt=3) -> local rewrite`. - `task_03`: target-aware transpilation with backend-specific equivalence registration and transpile settings. -## Unified Per-Task Structure -Each task now uses the same structure: +Each historical task uses the following structure: + - `baseline/solve.py`: evolve entrypoint with the task-specific baseline strategy. - `baseline/structural_optimizer.py`: task-local local-rewrite helper reused by `solve.py`. - `verification/evaluate.py`: single evaluation entrypoint that includes candidate and `opt0..opt3` references. @@ -28,7 +33,16 @@ Each task now uses the same structure: - `tests/case_*.json`: multiple differentiated test cases. - `README*.md` and `TASK*.md`: run guide and task definition. -## Eval +## Evaluation + +Kernel block encoding: + +```bash +.venvs/frontier-eval-driver/bin/python -m frontier_eval task=unified task.benchmark=QuantumComputing/KernelBlockEncoding algorithm=openevolve algorithm.iterations=0 +``` + +MQT-backed tasks: + ```bash .venvs/frontier-eval-driver/bin/python -m frontier_eval task=unified task.benchmark=QuantumComputing/task_01_routing_qftentangled task.runtime.env_name=frontier-v1-main algorithm=openevolve algorithm.iterations=0 .venvs/frontier-eval-driver/bin/python -m frontier_eval task=unified task.benchmark=QuantumComputing/task_02_clifford_t_synthesis task.runtime.env_name=frontier-v1-main algorithm=openevolve algorithm.iterations=0 diff --git a/benchmarks/QuantumComputing/README_zh-CN.md b/benchmarks/QuantumComputing/README_zh-CN.md index ff02ca4c..0e3a5386 100644 --- a/benchmarks/QuantumComputing/README_zh-CN.md +++ b/benchmarks/QuantumComputing/README_zh-CN.md @@ -1,26 +1,26 @@ -# Agent-Evolve 量子优化题目集 +# 量子计算基准 -本目录包含 3 个基于本仓库 `mqt.bench` API 构建的优化题目。 - -## 环境 -请使用指定解释器: - -```bash -pip install mqt.bench -``` +本目录包含量子电路构造、综合、路由与跨目标优化任务。 ## 题目列表 + +- [`KernelBlockEncoding`](KernelBlockEncoding/):为定点 QML 核矩阵构造兼顾归一化与资源的 FABLE 电路;评测器自包含且仅使用 CPU。 - `task_01_routing_qftentangled`:面向 IBM Falcon 的 mapped-level 路由优化。 - `task_02_clifford_t_synthesis`:面向 `clifford+t` 原生门集的综合优化。 - `task_03_cross_target_qaoa`:同一策略在 IBM 与 IonQ 双目标上的鲁棒优化。 -当前 baseline 策略: +三个历史 `task_0*` 任务使用本仓库的 `mqt.bench` API,需要已配置的量子环境。`KernelBlockEncoding` 只需 Python 3 与 C++17 编译器,不需量子 SDK 或模拟器。 + +## 历史 MQT 任务结构 + +MQT 任务的当前 baseline 策略: + - `task_01`:先做 local rewrite,再做面向 target 的多 seed transpile 搜索。 - `task_02`:`local rewrite -> clifford+t transpile(opt=3) -> local rewrite`。 - `task_03`:按后端注册 equivalence,并使用 target-aware transpile 参数。 -## 统一目录结构 -每个题目都采用同一结构: +每个历史题目都采用以下结构: + - `baseline/solve.py`:包含各题目当前 baseline 策略的 evolve 入口。 - `baseline/structural_optimizer.py`:由 `solve.py` 复用的 task-local local-rewrite 辅助模块。 - `verification/evaluate.py`:单一评测入口,同时包含 candidate 与 `opt0..opt3` 参考对比。 @@ -29,6 +29,15 @@ pip install mqt.bench - `README*.md` 与 `TASK*.md`:运行说明与任务定义。 ## 评测命令 + +核矩阵分块编码: + +```bash +.venvs/frontier-eval-driver/bin/python -m frontier_eval task=unified task.benchmark=QuantumComputing/KernelBlockEncoding algorithm=openevolve algorithm.iterations=0 +``` + +MQT 任务: + ```bash .venvs/frontier-eval-driver/bin/python -m frontier_eval task=unified task.benchmark=QuantumComputing/task_01_routing_qftentangled task.runtime.env_name=frontier-v1-main algorithm=openevolve algorithm.iterations=0 .venvs/frontier-eval-driver/bin/python -m frontier_eval task=unified task.benchmark=QuantumComputing/task_02_clifford_t_synthesis task.runtime.env_name=frontier-v1-main algorithm=openevolve algorithm.iterations=0 From 466c6efa3caa34f4807a205d693d88e77c7f2788 Mon Sep 17 00:00:00 2001 From: Yizhan Han Date: Thu, 16 Jul 2026 16:08:29 -0400 Subject: [PATCH 2/2] Improve kernel block encoding evaluator isolation --- .../KernelBlockEncoding/Task.md | 11 +- .../baseline/result_log.txt | 3 + .../references/problem_config.json | 1 + .../verification/evaluator.py | 193 ++++++++++++------ .../verification/limited_exec.py | 19 +- .../verification/test_evaluator.py | 44 ++++ 6 files changed, 200 insertions(+), 71 deletions(-) diff --git a/benchmarks/QuantumComputing/KernelBlockEncoding/Task.md b/benchmarks/QuantumComputing/KernelBlockEncoding/Task.md index 4ab5cb29..8f46eb57 100644 --- a/benchmarks/QuantumComputing/KernelBlockEncoding/Task.md +++ b/benchmarks/QuantumComputing/KernelBlockEncoding/Task.md @@ -189,13 +189,22 @@ score_i = baseline_effective_cost_i / candidate_effective_cost_i. global-threshold starter is frozen as the baseline and scores exactly 1.0. Larger is better. +The evaluator isolates workload failures and continues through the full suite. A +failed workload retains its baseline data and a workload-level error, while every +successful workload retains its independently checked score. `partial_combined_score` +is the geometric mean over those successful workloads and is diagnostic only. The +official `combined_score` remains zero and `valid=0.0` unless all six workloads +succeed, so skipping a difficult scenario can never improve the optimization +target. + ## 8. Limits and reproducibility - matrix dimensions are at most 64; - scale is at most 16 times the basis-dependent minimum; - angle certificates are at most 256 KiB; - candidate source is at most 1 MB; -- candidate execution is limited to 5 seconds and 1 GiB per workload; and +- candidate execution is limited to 5 seconds and 1 GiB per workload; +- candidate processes are subject to `RLIMIT_NPROC=64` where supported; and - compilation uses `g++ -std=c++17 -O2`. There is no network access, external solver, quantum SDK, simulator, container, diff --git a/benchmarks/QuantumComputing/KernelBlockEncoding/baseline/result_log.txt b/benchmarks/QuantumComputing/KernelBlockEncoding/baseline/result_log.txt index af8c6916..af45d6a4 100644 --- a/benchmarks/QuantumComputing/KernelBlockEncoding/baseline/result_log.txt +++ b/benchmarks/QuantumComputing/KernelBlockEncoding/baseline/result_log.txt @@ -14,7 +14,10 @@ Environment: Summary: valid=1.0 combined_score=1.0 + partial_combined_score=1.0 scenario_count=6 + successful_scenario_count=6 + failed_scenario_count=0 checked_matrix_entries=8448 total_rotations=6936 total_cnots=7788 diff --git a/benchmarks/QuantumComputing/KernelBlockEncoding/references/problem_config.json b/benchmarks/QuantumComputing/KernelBlockEncoding/references/problem_config.json index fb022c74..d97f7138 100644 --- a/benchmarks/QuantumComputing/KernelBlockEncoding/references/problem_config.json +++ b/benchmarks/QuantumComputing/KernelBlockEncoding/references/problem_config.json @@ -13,6 +13,7 @@ "max_candidate_bytes": 1000000, "max_certificate_bytes": 262144, "max_dimension": 64, + "max_processes": 64, "max_scale_multiplier": 16, "candidate_timeout_s_per_workload": 5.0, "compile_timeout_s": 30.0, diff --git a/benchmarks/QuantumComputing/KernelBlockEncoding/verification/evaluator.py b/benchmarks/QuantumComputing/KernelBlockEncoding/verification/evaluator.py index 55d18ac8..b748bc26 100644 --- a/benchmarks/QuantumComputing/KernelBlockEncoding/verification/evaluator.py +++ b/benchmarks/QuantumComputing/KernelBlockEncoding/verification/evaluator.py @@ -103,6 +103,7 @@ def _validated_config() -> dict[str, Any]: "max_candidate_bytes", "max_certificate_bytes", "max_dimension", + "max_processes", "max_scale_multiplier", "memory_limit_mb", ): @@ -779,6 +780,7 @@ def _run_program( timeout_s: float, maximum_certificate_bytes: int, memory_mb: int, + maximum_processes: int, ) -> ProgramRun: run_dir.mkdir(parents=True, exist_ok=False) input_path = run_dir / "kernel.kbe" @@ -793,6 +795,7 @@ def _run_program( str(memory_mb), str(cpu_seconds), str(file_limit), + str(maximum_processes), str(run_dir), "--", str(executable), @@ -948,93 +951,155 @@ def evaluate( candidate_compile_s = time.perf_counter() - compile_started scores: list[float] = [] - minimum_score = math.inf total_candidate_rotations = 0 total_candidate_cnots = 0 total_baseline_rotations = 0 total_baseline_cnots = 0 total_checked_entries = 0 candidate_alpha_sum = 0.0 + candidate_checked_scenarios = 0 results: dict[str, Any] = {} + failure_summaries: list[str] = [] + failed_workloads: list[str] = [] + any_timeout = False + + artifacts["compiler"] = compiler + artifacts["compile"] = { + "baseline_s": baseline_compile_s, + "candidate_s": candidate_compile_s, + } + artifacts["objective"] = config["objective"] + artifacts["limits"] = limits + for index, workload in enumerate(workloads): - baseline_run = _run_program( - baseline_executable, - workload, - temp / f"baseline_{index}", - timeout_s, - int(limits["max_certificate_bytes"]), - int(limits["memory_limit_mb"]), - ) - candidate_run = _run_program( - candidate_executable, - workload, - temp / f"candidate_{index}", - timeout_s, - int(limits["max_certificate_bytes"]), - int(limits["memory_limit_mb"]), - ) - baseline_certificate = _parse_certificate( - baseline_run.certificate, workload, config - ) - candidate_certificate = _parse_certificate( - candidate_run.certificate, workload, config - ) - baseline_checked = _check_construction( - baseline_certificate, workload, config - ) - candidate_checked = _check_construction( - candidate_certificate, workload, config - ) - score = baseline_checked.objective / candidate_checked.objective - if not math.isfinite(score) or score <= 0.0: - raise EvaluationError("scenario score is non-positive or non-finite") - scores.append(score) - minimum_score = min(minimum_score, score) - total_candidate_rotations += candidate_checked.rotations - total_candidate_cnots += candidate_checked.cnots - total_baseline_rotations += baseline_checked.rotations - total_baseline_cnots += baseline_checked.cnots - total_checked_entries += workload.dimension * workload.dimension - candidate_alpha_sum += candidate_checked.alpha - results[workload.workload_id] = { - "score": score, + workload_result: dict[str, Any] = { "epsilon": workload.epsilon, "input_sha256": workload.sha256, - "baseline": _construction_artifact( + } + workload_errors: list[str] = [] + baseline_checked: CheckedConstruction | None = None + candidate_checked: CheckedConstruction | None = None + + try: + baseline_run = _run_program( + baseline_executable, + workload, + temp / f"baseline_{index}", + timeout_s, + int(limits["max_certificate_bytes"]), + int(limits["memory_limit_mb"]), + int(limits["max_processes"]), + ) + baseline_certificate = _parse_certificate( + baseline_run.certificate, workload, config + ) + baseline_checked = _check_construction( + baseline_certificate, workload, config + ) + workload_result["baseline"] = _construction_artifact( baseline_certificate, baseline_checked, baseline_run - ), - "candidate": _construction_artifact( + ) + total_baseline_rotations += baseline_checked.rotations + total_baseline_cnots += baseline_checked.cnots + except Exception as exc: + message = str(exc) or type(exc).__name__ + workload_result["baseline_error"] = message + workload_errors.append(f"baseline: {message}") + any_timeout = any_timeout or "timed out" in message.lower() + + try: + candidate_run = _run_program( + candidate_executable, + workload, + temp / f"candidate_{index}", + timeout_s, + int(limits["max_certificate_bytes"]), + int(limits["memory_limit_mb"]), + int(limits["max_processes"]), + ) + candidate_certificate = _parse_certificate( + candidate_run.certificate, workload, config + ) + candidate_checked = _check_construction( + candidate_certificate, workload, config + ) + workload_result["candidate"] = _construction_artifact( candidate_certificate, candidate_checked, candidate_run - ), - } - - combined_score = math.exp( - math.fsum(math.log(score) for score in scores) / len(scores) + ) + total_candidate_rotations += candidate_checked.rotations + total_candidate_cnots += candidate_checked.cnots + total_checked_entries += workload.dimension * workload.dimension + candidate_alpha_sum += candidate_checked.alpha + candidate_checked_scenarios += 1 + except Exception as exc: + message = str(exc) or type(exc).__name__ + workload_result["candidate_error"] = message + workload_errors.append(f"candidate: {message}") + any_timeout = any_timeout or "timed out" in message.lower() + + if baseline_checked is not None and candidate_checked is not None: + try: + score = baseline_checked.objective / candidate_checked.objective + if not math.isfinite(score) or score <= 0.0: + raise EvaluationError( + "scenario score is non-positive or non-finite" + ) + workload_result["score"] = score + scores.append(score) + except Exception as exc: + message = str(exc) or type(exc).__name__ + workload_result["score_error"] = message + workload_errors.append(f"score: {message}") + + if workload_errors: + workload_result["error"] = "; ".join(workload_errors) + failed_workloads.append(workload.workload_id) + failure_summaries.append( + f"{workload.workload_id}: {workload_result['error']}" + ) + else: + workload_result["status"] = "ok" + results[workload.workload_id] = workload_result + + partial_combined_score = ( + math.exp(math.fsum(math.log(score) for score in scores) / len(scores)) + if scores + else 0.0 ) + all_workloads_valid = len(scores) == len(workloads) runtime_s = time.perf_counter() - started metrics = { - "combined_score": combined_score, - "valid": 1.0, - "timeout": 0.0, + "combined_score": ( + partial_combined_score if all_workloads_valid else 0.0 + ), + "partial_combined_score": partial_combined_score, + "valid": 1.0 if all_workloads_valid else 0.0, + "timeout": 1.0 if any_timeout else 0.0, "runtime_s": runtime_s, - "scenario_count": float(len(scores)), - "mean_score": math.fsum(scores) / len(scores), - "min_score": minimum_score, + "scenario_count": float(len(workloads)), + "successful_scenario_count": float(len(scores)), + "failed_scenario_count": float(len(workloads) - len(scores)), + "mean_score": math.fsum(scores) / len(scores) if scores else 0.0, + "min_score": min(scores) if scores else 0.0, "baseline_total_rotations": float(total_baseline_rotations), "candidate_total_rotations": float(total_candidate_rotations), "baseline_total_cnots": float(total_baseline_cnots), "candidate_total_cnots": float(total_candidate_cnots), - "candidate_mean_alpha": candidate_alpha_sum / len(scores), + "candidate_mean_alpha": ( + candidate_alpha_sum / candidate_checked_scenarios + if candidate_checked_scenarios + else 0.0 + ), "checked_matrix_entries": float(total_checked_entries), } - artifacts["compiler"] = compiler - artifacts["compile"] = { - "baseline_s": baseline_compile_s, - "candidate_s": candidate_compile_s, - } - artifacts["objective"] = config["objective"] - artifacts["limits"] = limits artifacts["workloads"] = results + if failure_summaries: + artifacts["failed_workloads"] = failed_workloads + artifacts["failure_summary"] = "\n".join(failure_summaries) + artifacts["error_message"] = ( + f"{len(failed_workloads)} of {len(workloads)} workloads failed; " + "see failure_summary and per-workload errors" + ) return metrics, artifacts except Exception as exc: message = str(exc) diff --git a/benchmarks/QuantumComputing/KernelBlockEncoding/verification/limited_exec.py b/benchmarks/QuantumComputing/KernelBlockEncoding/verification/limited_exec.py index afe53383..8c1c87cb 100644 --- a/benchmarks/QuantumComputing/KernelBlockEncoding/verification/limited_exec.py +++ b/benchmarks/QuantumComputing/KernelBlockEncoding/verification/limited_exec.py @@ -18,17 +18,18 @@ def _positive_integer(text: str, label: str) -> int: def main() -> int: - if len(sys.argv) < 7 or sys.argv[5] != "--": + if len(sys.argv) < 8 or sys.argv[6] != "--": raise SystemExit( - "usage: limited_exec.py MEMORY_MB CPU_SECONDS FILE_BYTES CWD -- " - "PROGRAM [ARG ...]" + "usage: limited_exec.py MEMORY_MB CPU_SECONDS FILE_BYTES " + "MAX_PROCESSES CWD -- PROGRAM [ARG ...]" ) memory_mb = _positive_integer(sys.argv[1], "MEMORY_MB") cpu_seconds = _positive_integer(sys.argv[2], "CPU_SECONDS") file_bytes = _positive_integer(sys.argv[3], "FILE_BYTES") - working_directory = os.path.abspath(sys.argv[4]) - program = os.path.abspath(sys.argv[6]) - arguments = [program, *sys.argv[7:]] + maximum_processes = _positive_integer(sys.argv[4], "MAX_PROCESSES") + working_directory = os.path.abspath(sys.argv[5]) + program = os.path.abspath(sys.argv[7]) + arguments = [program, *sys.argv[8:]] # Session creation happens here, in a freshly spawned interpreter, so the # multithreaded evaluator never needs a fork-time callback. @@ -39,6 +40,12 @@ def main() -> int: resource.setrlimit(resource.RLIMIT_CPU, (cpu_seconds, cpu_seconds)) resource.setrlimit(resource.RLIMIT_FSIZE, (file_bytes, file_bytes)) resource.setrlimit(resource.RLIMIT_CORE, (0, 0)) + try: + resource.setrlimit( + resource.RLIMIT_NPROC, (maximum_processes, maximum_processes) + ) + except (AttributeError, ValueError, OSError): + pass try: resource.setrlimit(resource.RLIMIT_NOFILE, (64, 64)) except (ValueError, OSError): diff --git a/benchmarks/QuantumComputing/KernelBlockEncoding/verification/test_evaluator.py b/benchmarks/QuantumComputing/KernelBlockEncoding/verification/test_evaluator.py index 38049043..d7c10711 100644 --- a/benchmarks/QuantumComputing/KernelBlockEncoding/verification/test_evaluator.py +++ b/benchmarks/QuantumComputing/KernelBlockEncoding/verification/test_evaluator.py @@ -3,7 +3,9 @@ from __future__ import annotations import math +import tempfile import unittest +from pathlib import Path import evaluator @@ -136,6 +138,48 @@ def test_frozen_workload_hashes_and_source_shell(self) -> None: ) self.assertEqual(source, evaluator.BASELINE_PATH.read_bytes()) + def test_baseline_evaluates_to_valid_one(self) -> None: + metrics, artifacts = evaluator.evaluate( + evaluator.TASK_DIR / "scripts" / "init.cpp" + ) + self.assertEqual(metrics["valid"], 1.0) + self.assertAlmostEqual(metrics["combined_score"], 1.0) + self.assertEqual(metrics["successful_scenario_count"], 6.0) + self.assertEqual(metrics["failed_scenario_count"], 0.0) + self.assertNotIn("failure_summary", artifacts) + + def test_candidate_workload_failure_is_isolated(self) -> None: + source = (evaluator.TASK_DIR / "scripts" / "init.cpp").read_text( + encoding="utf-8" + ) + function_start = "void optimize(Construction &construction) {\n" + injected_start = function_start + ( + ' if (construction.workload_id() == "polynomial_gram_32")\n' + ' throw std::runtime_error("intentional isolated failure");\n' + ) + self.assertIn(function_start, source) + source = source.replace(function_start, injected_start, 1) + + with tempfile.TemporaryDirectory(prefix="kbe_isolation_test_") as temporary: + candidate = Path(temporary) / "candidate.cpp" + candidate.write_text(source, encoding="utf-8") + metrics, artifacts = evaluator.evaluate(candidate) + + self.assertEqual(metrics["valid"], 0.0) + self.assertEqual(metrics["combined_score"], 0.0) + self.assertAlmostEqual(metrics["partial_combined_score"], 1.0) + self.assertEqual(metrics["successful_scenario_count"], 5.0) + self.assertEqual(metrics["failed_scenario_count"], 1.0) + self.assertEqual( + artifacts["failed_workloads"], ["polynomial_gram_32"] + ) + failed = artifacts["workloads"]["polynomial_gram_32"] + self.assertIn("intentional isolated failure", failed["candidate_error"]) + self.assertIn("candidate:", failed["error"]) + self.assertAlmostEqual( + artifacts["workloads"]["multiscale_rbf_64"]["score"], 1.0 + ) + if __name__ == "__main__": unittest.main()