Structured quantum benchmarks

MQT Core defines each structured quantum benchmark by benchmark-specific parameters and an analytic reference. A benchmark instance can produce a structured QC program, a resolved manifest, and a stable case ID. The generated program returns one classical register named result. Outcome strings are big-endian: the highest-index result bit is the leftmost character.

Discover the catalog

The command-line registry is the current list of available families. Each family has its own instance specification schema. These cells execute the CLI and fail if it exits unsuccessfully; JSON formatting only makes its output easier to read.

1import json
2import subprocess
3
4catalog = subprocess.run(["mqt-core-bench", "list"], check=True, capture_output=True, text=True)
5print(json.dumps(json.loads(catalog.stdout), indent=2))
{
  "benchmarks": [
    {
      "definition_version": 1,
      "id": "bv"
    },
    {
      "definition_version": 1,
      "id": "ghz"
    },
    {
      "definition_version": 1,
      "id": "grover"
    },
    {
      "definition_version": 1,
      "id": "modular-multiplier"
    },
    {
      "definition_version": 1,
      "id": "multiplexer"
    },
    {
      "definition_version": 1,
      "id": "qft"
    },
    {
      "definition_version": 1,
      "id": "qft-adder"
    },
    {
      "definition_version": 1,
      "id": "qpe"
    },
    {
      "definition_version": 1,
      "id": "repeat-until-success"
    },
    {
      "definition_version": 1,
      "id": "teleportation"
    }
  ],
  "schema_version": 1
}
1description = subprocess.run(["mqt-core-bench", "describe", "qft"], check=True, capture_output=True, text=True)
2print(json.dumps(json.loads(description.stdout), indent=2))
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "additionalProperties": false,
  "properties": {
    "benchmark": {
      "const": "qft"
    },
    "parameters": {
      "additionalProperties": false,
      "properties": {
        "method": {
          "default": "standard",
          "enum": [
            "standard",
            "semiclassical"
          ]
        },
        "period_exponent": {
          "maximum": 1074,
          "minimum": 0,
          "type": "integer"
        },
        "qubits": {
          "maximum": 1000000,
          "minimum": 1,
          "type": "integer"
        }
      },
      "required": [
        "qubits",
        "period_exponent"
      ],
      "type": "object"
    },
    "schema_version": {
      "const": 1
    }
  },
  "required": [
    "schema_version",
    "benchmark",
    "parameters"
  ],
  "type": "object",
  "x-mqt-definition-version": 1
}

Configure a typed instance

Python exposes each benchmark through a family-specific type. Parameterized families also expose option types. The QFT input below is the uniform superposition of multiples of two. Both circuit methods use the same logical output and reference.

 1from mqt.core.bench import qft
 2
 3
 4benchmark = qft.QFT(
 5    qft.Options(
 6        qubits=3,
 7        period_exponent=1,
 8        method=qft.Method.SEMICLASSICAL,
 9    )
10)
11print("Method:", benchmark.options.method)
12print("Output:", benchmark.output.name)
13print("Width:", benchmark.output.width)
Method: Method.SEMICLASSICAL
Output: result
Width: 3

Each family validates its instance when it creates one. Fixed families need no options.

Inspect the canonical instance specification and manifest

A canonical instance specification records every resolved default. A manifest also binds the logical output, reference descriptor, family-definition version, and case ID.

 1import json
 2
 3
 4instance_specification = json.loads(benchmark.instance_specification_json)
 5manifest = json.loads(benchmark.manifest_json)
 6print("Instance specification:")
 7print(json.dumps(instance_specification, indent=2))
 8print("\nManifest summary:")
 9print(
10    json.dumps(
11        {
12            "case_id": manifest["case_id"],
13            "outputs": manifest["outputs"],
14            "reference": manifest["reference"],
15        },
16        indent=2,
17    )
18)
Instance specification:
{
  "benchmark": "qft",
  "parameters": {
    "method": "semiclassical",
    "period_exponent": 1,
    "qubits": 3
  },
  "schema_version": 1
}

Manifest summary:
{
  "case_id": "sha256-ceed1b83b1051851f192e0144524d231c8ecd1a441cb363ce1f26babc4bcaaa7",
  "outputs": [
    {
      "name": "result",
      "width": 3
    }
  ],
  "reference": {
    "kind": "analytic",
    "model": "qft_power_of_two_period",
    "outcome_order": "big_endian",
    "output": "result",
    "version": 1
  }
}

Query and evaluate the reference

For three output bits and period exponent one, QFT has two equal peaks.

1probabilities = {
2    outcome: benchmark.probability(outcome) for outcome in ("000", "100", "010")
3}
4assert probabilities == {"000": 0.5, "100": 0.5, "010": 0.0}
5print(json.dumps(probabilities, indent=2))
{
  "000": 0.5,
  "100": 0.5,
  "010": 0.0
}
 1evaluation = benchmark.evaluate({"000": 500, "100": 500})
 2print(
 3    json.dumps(
 4        {
 5            "total_variation_distance": evaluation.total_variation_distance,
 6            "squared_hellinger_fidelity": evaluation.squared_hellinger_fidelity,
 7            "success_probability": evaluation.success_probability,
 8        },
 9        indent=2,
10    )
11)
{
  "total_variation_distance": 0.0,
  "squared_hellinger_fidelity": 1.0,
  "success_probability": null
}

Total variation distance zero and squared Hellinger fidelity one identify an exact distribution. Some benchmark families also report a success probability for a distinguished success outcome.

Generate structured IR

Generation returns a QCProgram, the program type used by the MQT Core MLIR compiler collection. The program can enter the normal compiler pipeline.

1program = benchmark.generate()
2assert program.is_valid
3structured_ir = program.ir
4assert program.copy().to_qco().is_valid
5print(structured_ir)
module {
  func.func @main() -> !cbit.reg<3> attributes {mqt.entry_point} {
    %cst = arith.constant 5.000000e-01 : f64
    %cst_0 = arith.constant 1.5707963267948966 : f64
    %c2 = arith.constant 2 : index
    %c1 = arith.constant 1 : index
    %c0 = arith.constant 0 : index
    %0 = qc.alloc : !qc.qubit
    %1 = cbit.alloc(#cbit.init<zero>) {mqt.register_name = "result"} : !cbit.reg<3>
    scf.for %arg0 = %c0 to %c2 step %c1 {
      qc.h %0 : !qc.qubit
      %4 = arith.subi %arg0, %c1 : index
      %5 = scf.for %arg1 = %c0 to %arg0 step %c1 iter_args(%arg2 = %cst_0) -> (f64) {
        %7 = arith.subi %4, %arg1 : index
        %8 = cbit.load %1[%7] : !cbit.reg<3>
        scf.if %8 {
          qc.p(%arg2) %0 : !qc.qubit
        }
        %9 = arith.mulf %arg2, %cst : f64
        scf.yield %9 : f64
      }
      qc.h %0 : !qc.qubit
      %6 = qc.measure %0 : !qc.qubit -> i1
      cbit.store %6, %1[%arg0] : !cbit.reg<3>
      qc.reset %0 : !qc.qubit
    }
    %2 = scf.for %arg0 = %c0 to %c2 step %c1 iter_args(%arg1 = %cst_0) -> (f64) {
      %4 = arith.subi %c1, %arg0 : index
      %5 = cbit.load %1[%4] : !cbit.reg<3>
      scf.if %5 {
        qc.p(%arg1) %0 : !qc.qubit
      }
      %6 = arith.mulf %arg1, %cst : f64
      scf.yield %6 : f64
    }
    qc.h %0 : !qc.qubit
    %3 = qc.measure %0 : !qc.qubit -> i1
    cbit.store %3, %1[%c2] : !cbit.reg<3>
    qc.reset %0 : !qc.qubit
    qc.dealloc %0 : !qc.qubit
    return %1 : !cbit.reg<3>
  }
}

Run the command-line workflow

The CLI writes the program first and its manifest last. A manifest is therefore the completion marker. Existing output files always cause an error.

Hide code cell source

1import tempfile
2from pathlib import Path
3
4
5temporary = tempfile.TemporaryDirectory()
6root = Path(temporary.name)
7instance_specification_path = root / "instance-specification.json"
8counts_path = root / "counts.json"
9output_directory = root / "generated"
1instance_specification_path.write_text(
2    benchmark.instance_specification_json, encoding="utf-8"
3)
4counts_path.write_text(
5    json.dumps({"schema_version": 1, "counts": {"000": 5, "100": 5}}),
6    encoding="utf-8",
7)
1generation = subprocess.run(
2    ["mqt-core-bench", "generate", "--instance-specification", str(instance_specification_path),
3     "--format", "qc", "--output", str(output_directory)],
4    check=True, capture_output=True, text=True,
5)
6generated = json.loads(generation.stdout)
7print("Generated", generated["benchmark"], "as", generated["format"])
Generated qft as qc
1manifest_path = next(output_directory.glob("*.manifest.json"))
2program_path = next(output_directory.glob("*.qc.mlir"))
3print("Program:", program_path.name)
4print("Manifest:", manifest_path.name)
Program: qft-sha256-ceed1b83b1051851f192e0144524d231c8ecd1a441cb363ce1f26babc4bcaaa7.qc.mlir
Manifest: qft-sha256-ceed1b83b1051851f192e0144524d231c8ecd1a441cb363ce1f26babc4bcaaa7.qc.manifest.json
1evaluation_result = subprocess.run(
2    ["mqt-core-bench", "evaluate", "--manifest", str(manifest_path), "--counts", str(counts_path)],
3    check=True, capture_output=True, text=True,
4)
5metrics = json.loads(evaluation_result.stdout)["metrics"]
6assert metrics["total_variation_distance"] == 0
7print(json.dumps(metrics, indent=2))
{
  "squared_hellinger_fidelity": 1.0,
  "success_probability": null,
  "total_variation_distance": 0.0
}

Use --format jeff instead of --format qc to write a binary jeff program. The output format changes the file name, but not the semantic case ID.

C++ API

The installed MQT::CoreBench target provides typed parameters, references, evaluation, instances, instance specifications, and manifests.

#include "bench/Grover.hpp"

#include <cassert>

int main() {
  const mqt::bench::Grover benchmark{{.markedBitstring = "101"}};
  const auto evaluation = benchmark.evaluate({{"101", 1000}});
  assert(evaluation.successProbability == 1.0);
}
find_package(mqt-core CONFIG REQUIRED)
target_link_libraries(my-benchmark PRIVATE MQT::CoreBench)

The source build also provides MQT::CoreBenchGenerate. It exposes typed mqt::bench::generate(...) overloads from mlir/bench/Generate.h and returns a mlir::QCProgram. This target is not installed until MQT Core installs the wider MLIR compiler API.

Add a benchmark

Adding a family requires five extension points:

  1. Add one (TYPE, STEM, ID, DEFINITION_VERSION) row to include/mqt-core/bench/BenchmarkFamilies.inc. Its expansions provide the public JSON declarations and the synchronized semantic and MLIR registry glue.

  2. Add the typed instance, any options and validation, an analytic reference, and evaluation under include/mqt-core/bench/ and src/bench/. Add the family-specific parameter JSON, reference JSON, parser, and schema body to src/bench/JSON.cpp.

  3. Declare and implement the structured emitter under mlir/bench/, add its source to the program library, and declare the typed generate(...) overload. The catalog supplies the generation wrapper and JSON dispatch row.

  4. Add the explicit Python types in a family registration source under bindings/bench/, register its direct submodule in register_bench.cpp, and add the source to bindings/bench/CMakeLists.txt.

  5. Test the reference, strict instance specification JSON, emitter structure, jeff conversion, and Python generation.

BenchmarkFamilies.inc is the sole family catalog. Do not add a private family list, a generic option map, or a public base class.

Reproducibility contract

Instance specifications reject unknown fields and invalid values. The case ID does not depend on a path or output format. Parsing a manifest checks its resolved parameters, logical output, reference, definition version, and case ID. Before evaluation, normalize backend results to the manifest’s big-endian result order.

Benchmark families

QFT addition

The qft-adder family adds two equal-width operands. REGISTER stores the addend in qubits and applies controlled phases; CONSTANT combines the known addend into one phase per accumulator qubit. Both use the same exact QFT and inverse QFT. WRAP keeps an \(n\)-bit sum, while CARRY keeps an extra sum bit.

 1from mqt.core import mlir
 2from mqt.core.bench import qft_adder
 3
 4adder = qft_adder.QFTAdder(
 5    qft_adder.Options(
 6        addend="110",
 7        accumulator="011",
 8        method=qft_adder.Method.CONSTANT,
 9        overflow=qft_adder.Overflow.CARRY,
10    )
11)
12assert mlir.sample(adder.generate(), shots=128, seed=17) == {"1001": 128}

Operands are big-endian strings; leading zeros set their common width. The accumulator and constant addends must be binary. Register addends may also use + for independently prepared \(|+\rangle\) qubits, such as addend="1+0". Register results concatenate the addend and sum so their correlation remains observable. Constant results contain only the sum. expected_result is the unique logical outcome for basis inputs and None for a superposed addend. The total sum width, including an optional carry bit, is limited to 1024.

Modular multiplier

The modular-multiplier family uses the controlled modular arithmetic circuit from Figures 5 and 6 of Beauregard’s circuit for Shor’s algorithm. It computes control || multiplicand || product, where

\[\mathtt{product} = \mathtt{control} \cdot \mathtt{multiplier} \cdot \mathtt{multiplicand} \bmod \mathtt{modulus}.\]

The product register starts at zero and retains its leading overflow bit; a work qubit must return to zero. This is an out-of-place multiplier.

The classical multiplier and canonical modulus are equal-width binary strings with \(0 < \mathtt{multiplier} < \mathtt{modulus}\). The required multiplicand has the same width and accepts 0, 1, and +, as in the QFT adder. A + prepares an independent \(|+\rangle\) qubit. The control accepts "0", "1", or "+", and defaults to "1". Widths range from 2 to 63 bits.

from mqt.core.bench import modular_multiplier

benchmark = modular_multiplier.ModularMultiplier(
    modular_multiplier.Options(multiplier="011", modulus="101", multiplicand="111")
)
assert benchmark.expected_result == "11110001"  # control=1, input=7, product=1
assert benchmark.evaluate({"11110001": 100}).success_probability == 1.0

Basis inputs have one exact expected_result, so TVD and success probability provide a direct check independent of width. An all-zero output fails for this nonzero example. Test different inputs and both control values to exercise wraparound and the inactive path.

For superposed inputs, expected_result is None. The reference assigns probability \(2^{-k}\) to each allowed input and its correct product, where \(k\) is the number of + input bits, including the control. success_probability is the shot-weighted fraction matching both the configured inputs and the arithmetic relation. With \(S\) shots, empirical TVD is at least \(\max(0,1-S/2^k)\), even for ideal execution. Keep \(k\) small for sampling-based distribution checks at large widths.

Computational-basis measurements cannot detect arbitrary relative-phase errors. Native tests therefore also compare complete coherent states and require clean work-qubit recovery. A single correct basis result does not certify a unitary on every input.

Repeat until success

The repeat-until-success family generalizes the two-\(T\)-gate circuit from Figure 8 of Paetznick and Svore’s repeat-until-success decomposition to \(P=X^{\otimes n}\). Set data_qubits to \(n\) (default 1, range 1–1,000,000); the circuit uses one additional ancilla. Both registers start in zero.

Each attempt applies \((I + i\sqrt{2}P)/\sqrt{3}\) with probability \(3/4\). Failure leaves the data unchanged up to global phase. The circuit restores the ancilla to zero and retries through an unbounded scf.while. Two controlled Pauli strings require \(2n\) CNOT gates per attempt. Structured loops keep the generated QC program compact; execution still takes linear work per attempt.

After success, the data state is \((|0^n\rangle+i\sqrt{2}|1^n\rangle)/\sqrt{3}\). The benchmark measures \(Y\otimes X^{\otimes(n-1)}\) by changing basis and accumulating parity on the first data qubit. The one-bit result has probabilities \(P(0)=1/2+\sqrt{2}/3\) and \(P(1)=1/2-\sqrt{2}/3\) at every width. This readout checks the relative phase without an exponentially large output distribution.

{"schema_version":1,"benchmark":"repeat-until-success","parameters":{"data_qubits":32}}

The empty parameter object still selects one data qubit. Canonical JSON includes data_qubits, and semantic case IDs distinguish widths. The width limit bounds input size; it does not guarantee that a compiler or execution backend supports that many qubits. This family scales width and adaptive execution, while its state remains simple for decision diagrams and its expected \(T\) count stays \(8/3\). It does not model magic-state distillation or cultivation.