Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions releasenotes/notes/qasm3-parameterized-export-146.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
features:
- |
:meth:`.QuantumCircuit.to_qasm3` now exports parameterized circuits. Free
parameter symbols used by the circuit are emitted as ``input float[64]``
declarations, so the generated OpenQASM 3 no longer references undeclared
symbols.
- |
Added :meth:`.QuantumCircuit.parameter_symbols`, which returns the names of
the free parameter symbols used in a circuit, in first-encountered order.
issues:
- |
Exporting a parameterized circuit to OpenQASM 3 is currently limited to
bare parameter symbols as gate arguments; a compound parameter expression
(such as ``2*theta``) raises an exception. Distinct ``Parameter`` objects
that share a symbol name are merged into a single symbol, since the Qiskit
C API does not yet expose a circuit's parameter symbols or their UUIDs.
91 changes: 91 additions & 0 deletions src/circuit/quantumcircuit_def.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,14 @@
#include <functional>
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
#include <iomanip>
#include <cstring>
#include <cassert>
#include <cctype>
#include <cmath>
#include <stdexcept>

#include "utils/types.hpp"
#include "circuit/parameter.hpp"
Expand Down Expand Up @@ -1551,6 +1555,87 @@ class QuantumCircuit

// qasm3

/// @brief Collect the names of the free parameter symbols used in the circuit.
///
/// Names are returned in the order in which each distinct symbol is first
/// encountered while scanning the instructions, and each one keeps its
/// original symbol string.
///
/// @note This is a temporary implementation. The Qiskit C API exposes the
/// number of parameter symbols in a circuit (qk_circuit_num_param_symbols)
/// but not the symbols themselves, nor a way to tell apart two parameters
/// that share a name by their UUID. Until it does, the names are recovered
/// from the per-instruction parameters, with two limitations: a gate
/// argument that is a compound expression (e.g. "2*theta") cannot be turned
/// into a single input name and throws std::runtime_error; and distinct
/// Parameter objects that reuse one name are merged by the C API into a
/// single symbol, so they collapse to one entry rather than being kept
/// apart. Replace the body with the C API once the per-symbol accessors
/// land.
/// @return The free parameter symbol names used by the circuit.
std::vector<std::string> parameter_symbols(void)
{
add_pending_control_flow_op();

// A valid OpenQASM 3 input identifier (and hence a bare parameter
// symbol) starts with a letter or underscore and continues with
// letters, digits or underscores.
auto is_symbol_name = [](const std::string& s) -> bool {
if (s.empty() || (!std::isalpha((unsigned char)s[0]) && s[0] != '_')) {
return false;
}
for (char c : s) {
if (!std::isalnum((unsigned char)c) && c != '_') {
return false;
}
}
return true;
};

std::vector<std::string> symbols;
uint_t nops = qk_circuit_num_instructions(rust_circuit_.get());
for (uint_t i = 0; i < nops; i++) {
QkCircuitInstruction op;
qk_circuit_get_instruction(rust_circuit_.get(), i, &op);
for (uint_t j = 0; j < op.num_params; j++) {
// A parameter that evaluates to a real number carries no symbol.
if (!std::isnan(qk_param_as_real(op.params[j]))) {
continue;
}
char* c_str = qk_param_str(op.params[j]);
std::string name(c_str);
qk_str_free(c_str);

if (!is_symbol_name(name)) {
qk_circuit_instruction_clear(&op);
throw std::runtime_error(
"Qiskit C++ cannot export the parameter expression \"" + name +
"\" to OpenQASM 3 yet: only bare parameter symbols are supported "
"until the Qiskit C API exposes per-symbol access.");
}
if (std::find(symbols.begin(), symbols.end(), name) == symbols.end()) {
symbols.push_back(name);
}
}
qk_circuit_instruction_clear(&op);
}

// Defensive guard: the names we recovered should account for every
// symbol the circuit tracks. A mismatch means a parameter is used in a
// way this scan does not yet handle, so we refuse rather than emit
// OpenQASM 3 that declares the wrong set of inputs.
size_t tracked = qk_circuit_num_param_symbols(rust_circuit_.get());
if (tracked != symbols.size()) {
throw std::runtime_error(
"Qiskit C++ could not recover all parameter symbols for OpenQASM 3 "
"export (the circuit tracks " + std::to_string(tracked) +
" symbols but " + std::to_string(symbols.size()) +
" distinct names were found).");
}

return symbols;
}

/// @brief Serialize a QuantumCircuit object as an OpenQASM3 string.
/// @return An OpenQASM3 string.
std::string to_qasm3(void)
Expand Down Expand Up @@ -1848,6 +1933,12 @@ class QuantumCircuit
}
qk_opcounts_clear(&opcounts);

// Declare circuit parameters as OpenQASM 3 inputs so that the emitted
// program references symbols it actually declares.
for (const auto& name : parameter_symbols()) {
qasm3 << "input float[64] " << name << ";" << std::endl;
}

// save ops
uint_t nops;
nops = qk_circuit_num_instructions(rust_circuit_.get());
Expand Down
62 changes: 62 additions & 0 deletions test/test_circuit.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -664,6 +664,65 @@ static int test_to_qasm3_physical_qubits(void) {
return Ok;
}

static int test_parameter_symbols(void) {
auto circ = QuantumCircuit(2, 0);
circ.rx(Parameter("theta"), 0);
circ.ry(Parameter("phi"), 1);

const auto symbols = circ.parameter_symbols();
const std::vector<std::string> expected({"theta", "phi"});
if (symbols != expected) {
std::cerr << " parameter_symbols test : unexpected symbols" << std::endl;
return EqualityError;
}
return Ok;
}

static int test_to_qasm3_parameterized(void) {
auto circ = QuantumCircuit(2, 0);
circ.rx(Parameter("theta"), 0);
circ.ry(Parameter("phi"), 1);

const auto actual = circ.to_qasm3();
const std::string expected =
"OPENQASM 3.0;\n"
"include \"stdgates.inc\";\n"
"input float[64] theta;\n"
"input float[64] phi;\n"
"qubit[2] q;\n"
"rx(theta) q[0];\n"
"ry(phi) q[1];\n";
if (actual != expected) {
std::cerr << " to_qasm3_parameterized test : \n expected:\n" << expected
<< "\n actual:\n" << actual << std::endl;
return EqualityError;
}
return Ok;
}

static int test_to_qasm3_shared_parameter(void) {
// A symbol reused across gates is declared exactly once.
auto theta = Parameter("theta");
auto circ = QuantumCircuit(2, 0);
circ.rx(theta, 0);
circ.rz(theta, 1);

const auto actual = circ.to_qasm3();
const std::string expected =
"OPENQASM 3.0;\n"
"include \"stdgates.inc\";\n"
"input float[64] theta;\n"
"qubit[2] q;\n"
"rx(theta) q[0];\n"
"rz(theta) q[1];\n";
if (actual != expected) {
std::cerr << " to_qasm3_shared_parameter test : \n expected:\n" << expected
<< "\n actual:\n" << actual << std::endl;
return EqualityError;
}
return Ok;
}

#if defined(_WIN32)
int test_circuit(int argc, char** const argv) {
#else
Expand All @@ -676,6 +735,9 @@ int test_circuit(int argc, char** argv) {
num_failed += RUN_TEST(test_compose);
num_failed += RUN_TEST(test_to_qasm3_multi_regs);
num_failed += RUN_TEST(test_to_qasm3_physical_qubits);
num_failed += RUN_TEST(test_parameter_symbols);
num_failed += RUN_TEST(test_to_qasm3_parameterized);
num_failed += RUN_TEST(test_to_qasm3_shared_parameter);

std::cerr << "=== Number of failed subtests: " << num_failed << std::endl;
return num_failed;
Expand Down