diff --git a/releasenotes/notes/qasm3-parameterized-export-146.yaml b/releasenotes/notes/qasm3-parameterized-export-146.yaml new file mode 100644 index 0000000..35b2b95 --- /dev/null +++ b/releasenotes/notes/qasm3-parameterized-export-146.yaml @@ -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. diff --git a/src/circuit/quantumcircuit_def.hpp b/src/circuit/quantumcircuit_def.hpp index 83ab98a..16daf0b 100644 --- a/src/circuit/quantumcircuit_def.hpp +++ b/src/circuit/quantumcircuit_def.hpp @@ -21,10 +21,14 @@ #include #include #include +#include #include #include #include #include +#include +#include +#include #include "utils/types.hpp" #include "circuit/parameter.hpp" @@ -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 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 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) @@ -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()); diff --git a/test/test_circuit.cpp b/test/test_circuit.cpp index 53bb692..17ee524 100644 --- a/test/test_circuit.cpp +++ b/test/test_circuit.cpp @@ -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 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 @@ -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;