From 922ddd95e1967f36548401f1c693f5a22459641a Mon Sep 17 00:00:00 2001 From: GoiBasia Date: Tue, 9 Jun 2026 12:36:54 +0800 Subject: [PATCH 1/4] Implement BackendEstimatorV2 runtime support --- README.md | 6 +- .../notes/backend-estimator-v2-145.yaml | 10 + samples/CMakeLists.txt | 4 + samples/estimator_test.cpp | 121 +++ src/circuit/quantumcircuit_def.hpp | 37 +- src/primitives/backend_estimator_job.hpp | 181 +++++ src/primitives/backend_estimator_v2.hpp | 116 +++ src/primitives/backend_sampler_job.hpp | 2 +- .../containers/estimator_primitive_result.hpp | 82 ++ src/primitives/containers/estimator_pub.hpp | 447 +++++++++++ .../containers/estimator_pub_result.hpp | 259 +++++++ src/providers/backend.hpp | 10 +- src/providers/job.hpp | 15 +- src/providers/qkrt_job.hpp | 2 +- src/providers/qrmi_backend.hpp | 43 +- src/providers/qrmi_estimator_payload.hpp | 75 ++ src/providers/qrmi_job.hpp | 79 +- src/providers/sqc_job.hpp | 2 +- src/quantum_info/sparse_observable.hpp | 56 +- src/service/qiskit_runtime_service_qrmi.hpp | 17 +- src/utils/utils.hpp | 9 +- test/test_circuit.cpp | 24 + test/test_estimator.cpp | 728 ++++++++++++++++++ 23 files changed, 2270 insertions(+), 55 deletions(-) create mode 100644 releasenotes/notes/backend-estimator-v2-145.yaml create mode 100644 samples/estimator_test.cpp create mode 100644 src/primitives/backend_estimator_job.hpp create mode 100644 src/primitives/backend_estimator_v2.hpp create mode 100644 src/primitives/containers/estimator_primitive_result.hpp create mode 100644 src/primitives/containers/estimator_pub.hpp create mode 100644 src/primitives/containers/estimator_pub_result.hpp create mode 100644 src/providers/qrmi_estimator_payload.hpp create mode 100644 test/test_estimator.cpp diff --git a/README.md b/README.md index ef34d34..028dfa0 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ # Qiskit C++ -Qiskit C++ provides a modern C++ (version C++ 11 or later) interface of Qiskit for circuit building, (transpilation and returning samples of quantum circuit outputs ... to be added in the future release), as same as Qiskit Python interfaces. +Qiskit C++ provides a modern C++ (version C++ 11 or later) interface of Qiskit for circuit building, transpilation, sampler jobs, and QRMI-backed estimator jobs, as same as Qiskit Python interfaces. This interface is based on Qiskit C-API introduced in Qiskit 2.1. ## Supported OS @@ -97,7 +97,7 @@ $ cmake -DQISKIT_ROOT=Path_to_qiskit .. $ make ``` -If you want to build sampler or transpiler example, you will need one of qiskit-ibm-runtime C or QRMI or SQC. +If you want to build sampler or transpiler example, you will need one of qiskit-ibm-runtime C or QRMI or SQC. The estimator example currently requires QRMI. Then example can be built by setting `QISKIT_IBM_RUNTIME_C_ROOT` or `QRMI_ROOT` or `SQC_ROOT` to cmake. @@ -116,6 +116,8 @@ QISKIT_IBM_TOKEN= QISKIT_IBM_INSTANCE= ``` +The estimator interface is available through `primitives::BackendEstimatorV2` and currently requires QRMI or another backend that implements `BackendV2::run(std::vector&, double)`. Estimator PUB circuits must be unmeasured and can use Pauli labels, Pauli-label maps, Pauli term vectors, or `quantum_info::SparseObservable` inputs. Empty observables are rejected before backend submission, and malformed estimator result cardinality is rejected while reading provider results. Estimator job cancellation is not currently supported, so `BackendEstimatorJob::cancel()` returns `false`. + To run the sample example with SQC, you also need to set the library options to the environment variable `SQC_LIBS`, which is automatically set by a provided script (see [here](https://github.com/jhpc-quantum/documents/blob/main/SQC_JHPC_Quantum_user_guide.md)), before running cmake. In SQC 0.10.0, `backend_setup.sh` configures `SQC_LIBS` as follows: ``` diff --git a/releasenotes/notes/backend-estimator-v2-145.yaml b/releasenotes/notes/backend-estimator-v2-145.yaml new file mode 100644 index 0000000..372ecfd --- /dev/null +++ b/releasenotes/notes/backend-estimator-v2-145.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + Added `primitives::BackendEstimatorV2` with `EstimatorPub`, + `BackendEstimatorJob`, and estimator result containers. The QRMI backend + can now submit Runtime V2 `estimator` primitive jobs for unmeasured circuits + and Pauli observables, returning expectation values through + `EstimatorPubResult::evs()`. Empty observables are rejected before backend + submission, and estimator result parsing validates that provider `evs` and + `stds` cardinality matches the submitted PUB observables. diff --git a/samples/CMakeLists.txt b/samples/CMakeLists.txt index 8d49a60..9285abf 100644 --- a/samples/CMakeLists.txt +++ b/samples/CMakeLists.txt @@ -108,3 +108,7 @@ if(QRMI_ROOT OR QISKIT_IBM_RUNTIME_C_ROOT OR SQC_ROOT) add_application(sampler_test sampler_test.cpp) add_application(transpile_test transpile_test.cpp) endif() + +if(QRMI_ROOT) + add_application(estimator_test estimator_test.cpp) +endif() diff --git a/samples/estimator_test.cpp b/samples/estimator_test.cpp new file mode 100644 index 0000000..f051465 --- /dev/null +++ b/samples/estimator_test.cpp @@ -0,0 +1,121 @@ +/* +# This code is part of Qiskit. +# +# (C) Copyright IBM 2026. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +*/ + +// Test program for estimator + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "circuit/quantumcircuit.hpp" +#include "compiler/transpiler.hpp" +#include "primitives/backend_estimator_v2.hpp" +#include "quantum_info/sparse_observable.hpp" +#include "service/qiskit_runtime_service_qrmi.hpp" + +using namespace Qiskit; +using namespace Qiskit::circuit; +using namespace Qiskit::compiler; +using namespace Qiskit::primitives; +using namespace Qiskit::quantum_info; +using namespace Qiskit::service; + +using Estimator = BackendEstimatorV2; + +namespace { + +std::string apply_layout_to_pauli_label(const std::string& label, const reg_t& layout, uint_t output_width) +{ + if (layout.empty()) { + if (label.size() != output_width) { + throw std::invalid_argument("Estimator observable width must match the circuit width."); + } + return label; + } + + if (layout.size() < label.size()) { + throw std::invalid_argument("Transpile layout is smaller than the observable width."); + } + + std::string mapped_label(output_width, 'I'); + const uint_t input_width = label.size(); + for (uint_t logical = 0; logical < input_width; logical++) { + const uint_t physical = layout[logical]; + if (physical >= output_width) { + throw std::invalid_argument("Transpile layout maps outside the circuit width."); + } + mapped_label[output_width - 1 - physical] = label[input_width - 1 - logical]; + } + return mapped_label; +} + +std::vector>> apply_layout_to_terms( + const std::vector>>& terms, + const reg_t& layout, + uint_t output_width) +{ + std::vector>> mapped_terms; + mapped_terms.reserve(terms.size()); + for (const auto& term : terms) { + mapped_terms.push_back(std::make_pair( + apply_layout_to_pauli_label(term.first, layout, output_width), + term.second)); + } + return mapped_terms; +} + +} // namespace + +int main() +{ + QuantumCircuit circ(2, 0); + circ.h(0); + circ.cx(0, 1); + + std::vector>> terms; + terms.push_back(std::make_pair(std::string("ZZ"), std::complex(1.0, 0.0))); + terms.push_back(std::make_pair(std::string("XX"), std::complex(0.5, 0.0))); + + auto service = QiskitRuntimeService(); + auto backend = service.backend("ibm_kingston"); + auto estimator = Estimator(backend); + auto transpiled_circ = transpile(circ, backend); + auto mapped_terms = apply_layout_to_terms(terms, transpiled_circ.get_qubit_map(), transpiled_circ.num_qubits()); + auto observable = SparseObservable::from_list(mapped_terms, transpiled_circ.num_qubits()); + + auto job = estimator.run({EstimatorPub(transpiled_circ, observable)}, 0.02); + if (job == nullptr) { + return -1; + } + + auto result = job->result(); + auto evs = result[0].evs(); + auto stds = result[0].stds(); + + std::cout << " ===== estimator result for pub[0] =====" << std::endl; + for (uint_t i = 0; i < evs.size(); i++) { + std::cout << "ev[" << i << "] = " << evs[i]; + if (i < stds.size()) { + std::cout << ", std[" << i << "] = " << stds[i]; + } + std::cout << std::endl; + } + + return 0; +} diff --git a/src/circuit/quantumcircuit_def.hpp b/src/circuit/quantumcircuit_def.hpp index d8add04..83ab98a 100644 --- a/src/circuit/quantumcircuit_def.hpp +++ b/src/circuit/quantumcircuit_def.hpp @@ -322,6 +322,20 @@ class QuantumCircuit return measure_map_; } + /// @brief get qubits to be measured + /// @return a set of qubits + const std::vector> &get_measure_map(void) const + { + return measure_map_; + } + + /// @brief Return whether this circuit contains measurement operations. + /// @return true if there is at least one measurement operation. + bool has_measurements(void) const + { + return !measure_map_.empty(); + } + /// @brief set global phase /// @param phase global phase value void global_phase(const double phase) @@ -1840,10 +1854,25 @@ class QuantumCircuit // Declare registers // After transpilation, qubit registers will be mapped to physical registers, - // so we need to combined them in a single quantum register "q"; + // so we need to combine them in a single quantum register "q" for ordinary + // circuits. Transpiled circuits should use physical qubit references. const std::string qreg_name = "q"; - qasm3 << "qubit[" << num_qubits() << "] " << qreg_name << ";" << std::endl; + const bool physical_qubits = !qubit_map_.empty(); + auto qubit_ref = [physical_qubits, &qreg_name](uint_t index) -> std::string + { + if (physical_qubits) { + return std::string("$") + std::to_string(index); + } + return qreg_name + "[" + std::to_string(index) + "]"; + }; + + if (!physical_qubits) { + qasm3 << "qubit[" << num_qubits() << "] " << qreg_name << ";" << std::endl; + } for(const auto& creg : cregs_) { + if (creg.size() == 0) { + continue; + } qasm3 << "bit[" << creg.size() << "] " << creg.name() << ";" << std::endl; } @@ -1863,7 +1892,7 @@ class QuantumCircuit if (op->num_qubits == op->num_clbits) { for (uint_t j = 0; j < op->num_qubits; j++) { const auto creg_data = recover_reg_data(op->clbits[j]); - qasm3 << creg_data.first << "[" << creg_data.second << "] = " << op->name << " " << qreg_name << "[" << op->qubits[j] << "];" << std::endl; + qasm3 << creg_data.first << "[" << creg_data.second << "] = " << op->name << " " << qubit_ref(op->qubits[j]) << ";" << std::endl; } } } else { @@ -1886,7 +1915,7 @@ class QuantumCircuit if (op->num_qubits > 0) { qasm3 << " "; for (uint_t j = 0; j < op->num_qubits; j++) { - qasm3 << qreg_name << "[" << op->qubits[j] << "]"; + qasm3 << qubit_ref(op->qubits[j]); if (j != op->num_qubits - 1) qasm3 << ", "; } diff --git a/src/primitives/backend_estimator_job.hpp b/src/primitives/backend_estimator_job.hpp new file mode 100644 index 0000000..454b35e --- /dev/null +++ b/src/primitives/backend_estimator_job.hpp @@ -0,0 +1,181 @@ +/* +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017, 2026. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +*/ + +// job class for BackendEstimator + +#ifndef __qiskitcpp_primitives_backend_estimator_job_def_hpp__ +#define __qiskitcpp_primitives_backend_estimator_job_def_hpp__ + +#ifdef _MSC_VER +#define NOMINMAX +#include +#else +#include +#include +#endif + +#include +#include +#include + +#include "primitives/containers/estimator_primitive_result.hpp" +#include "providers/job.hpp" + +namespace Qiskit { +namespace primitives { + +/// @class BackendEstimatorJob +/// @brief Job class for Backend Estimator primitive. +class BackendEstimatorJob { +protected: + std::shared_ptr job_ = nullptr; + std::vector pubs_; + +public: + /// @brief Create a new BackendEstimatorJob. + /// @param job a job pointer to run pubs. + /// @param pubs a list of pubs. + BackendEstimatorJob(std::shared_ptr job, std::vector& pubs) + { + job_ = job; + pubs_ = pubs; + } + + BackendEstimatorJob(const BackendEstimatorJob& other) + { + job_ = other.job_; + pubs_ = other.pubs_; + } + + ~BackendEstimatorJob() + { + if (job_) { + job_.reset(); + } + } + + /// @brief get pubs for this job. + /// @return a list of pub. + const std::vector& pubs(void) const + { + return pubs_; + } + + /// @brief Return the status of the job. + /// @return JobStatus enum. + providers::JobStatus status(void) + { + if (!job_) { + return providers::JobStatus::ERROR; + } + return job_->status(); + } + + /// @brief Return whether the job is actively running. + /// @return true if job is actively running, otherwise false. + bool running(void) + { + return status() == providers::JobStatus::RUNNING; + } + + /// @brief Return whether the job is queued. + /// @return true if job is queued, otherwise false. + bool queued(void) + { + return status() == providers::JobStatus::QUEUED; + } + + /// @brief Return whether the job has successfully run. + /// @return true if successfully run, otherwise false. + bool done(void) + { + return status() == providers::JobStatus::DONE; + } + + /// @brief Return whether the job has been cancelled. + /// @return true if job has been cancelled, otherwise false. + bool cancelled(void) + { + return status() == providers::JobStatus::CANCELLED; + } + + /// @brief Return whether the job is in a final job state such as DONE or ERROR. + /// @return true if job is in a final job state, otherwise false. + bool in_final_state(void) + { + return is_final_state(status()); + } + + /// @brief Attempt to cancel the job. + /// @return false because backend cancellation is not implemented. + bool cancel(void) + { + return false; + } + + /// @brief Return the results of the job. + /// @return estimator primitive result. + EstimatorPrimitiveResult result(void) + { + if (!job_) { + throw std::runtime_error("BackendEstimatorJob has no provider job."); + } + + providers::JobStatus st; + while (true) { + st = job_->status(); + if (is_final_state(st)) { + break; + } +#ifdef _MSC_VER + Sleep(1); +#else + std::this_thread::sleep_for(std::chrono::seconds(1)); +#endif + } + + if (st != providers::JobStatus::DONE) { + throw std::runtime_error("BackendEstimatorJob did not finish successfully."); + } + + uint_t num_results = job_->num_results(); + if (num_results != pubs_.size()) { + throw std::runtime_error("BackendEstimatorJob result count does not match the submitted pubs."); + } + + EstimatorPrimitiveResult result; + result.allocate(num_results); + for (uint_t i = 0; i < num_results; i++) { + result[i].set_pub(pubs_[i]); + if (!job_->result(i, result[i])) { + throw std::runtime_error("BackendEstimatorJob failed to read an estimator pub result."); + } + } + return result; + } + +protected: + static bool is_final_state(providers::JobStatus status) + { + return status == providers::JobStatus::DONE || + status == providers::JobStatus::CANCELLED || + status == providers::JobStatus::FAILED || + status == providers::JobStatus::ERROR; + } +}; + +} // namespace primitives +} // namespace Qiskit + +#endif //__qiskitcpp_primitives_backend_estimator_job_def_hpp__ diff --git a/src/primitives/backend_estimator_v2.hpp b/src/primitives/backend_estimator_v2.hpp new file mode 100644 index 0000000..841466e --- /dev/null +++ b/src/primitives/backend_estimator_v2.hpp @@ -0,0 +1,116 @@ +/* +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017, 2026. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +*/ + +// estimator implementation for a backend + +#ifndef __qiskitcpp_primitives_backend_estimator_def_hpp__ +#define __qiskitcpp_primitives_backend_estimator_def_hpp__ + +#include +#include +#include +#include + +#include "circuit/quantumcircuit.hpp" +#include "primitives/backend_estimator_job.hpp" +#include "primitives/containers/estimator_pub.hpp" +#include "providers/backend.hpp" + +namespace Qiskit { +namespace primitives { + +/// @class BackendEstimatorV2 +/// @brief Implementation of EstimatorV2 on a backend. +class BackendEstimatorV2 { +protected: + double default_precision_; + providers::BackendV2& backend_; + +public: + /// @brief Create a new BackendEstimatorV2. + /// @param backend BackendV2 object. + /// @param default_precision The default target precision. + BackendEstimatorV2(providers::BackendV2& backend, double default_precision = 0.015625) + : default_precision_(default_precision), backend_(backend) + { + if (!valid_precision(default_precision_)) { + throw std::invalid_argument("BackendEstimatorV2 default precision must be finite and non-negative."); + } + } + + /// @brief return reference to backend object. + /// @return backendV2 object. + const providers::BackendV2& backend(void) const + { + return backend_; + } + + /// @brief return default precision. + /// @return default target precision. + double default_precision(void) const + { + return default_precision_; + } + + /// @brief Run and estimate expectation values from each pub. + /// @param pubs An iterable of pub-like objects. + /// @return BackendEstimatorJob. + std::shared_ptr run(std::vector pubs) + { + return run(pubs, default_precision_); + } + + /// @brief Run and estimate expectation values from each pub. + /// @param pubs An iterable of pub-like objects. + /// @param precision The target precision for pubs without an explicit precision. + /// @return BackendEstimatorJob. + std::shared_ptr run(std::vector pubs, double precision) + { + if (!valid_precision(precision)) { + throw std::invalid_argument("Estimator run precision must be finite and non-negative."); + } + + for (uint_t i = 0; i < pubs.size(); i++) { + std::string message; + if (!pubs[i].validate(&message)) { + throw std::invalid_argument(message); + } + + if (pubs[i].circuit().has_measurements()) { + throw std::invalid_argument("Estimator circuits must not contain measurement operations."); + } + + if (!pubs[i].has_precision()) { + pubs[i].set_precision(precision); + } + } + + auto job = backend_.run(pubs, precision); + if (!job) { + return nullptr; + } + return std::make_shared(job, pubs); + } + +protected: + static bool valid_precision(double precision) + { + return std::isfinite(precision) && precision >= 0.0; + } +}; + +} // namespace primitives +} // namespace Qiskit + +#endif //__qiskitcpp_primitives_backend_estimator_def_hpp__ diff --git a/src/primitives/backend_sampler_job.hpp b/src/primitives/backend_sampler_job.hpp index 1512360..26b9a88 100644 --- a/src/primitives/backend_sampler_job.hpp +++ b/src/primitives/backend_sampler_job.hpp @@ -26,6 +26,7 @@ #endif +#include "primitives/containers/sampler_pub.hpp" #include "primitives/base/base_primitive_job.hpp" #include "providers/backend.hpp" #include "providers/job.hpp" @@ -144,4 +145,3 @@ class BackendSamplerJob : public BasePrimitiveJob { #endif //__qiskitcpp_primitives_qrmi_qiskit_ibm_runtime_job_def_hpp__ - diff --git a/src/primitives/containers/estimator_primitive_result.hpp b/src/primitives/containers/estimator_primitive_result.hpp new file mode 100644 index 0000000..05b2d61 --- /dev/null +++ b/src/primitives/containers/estimator_primitive_result.hpp @@ -0,0 +1,82 @@ +/* +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017, 2026. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +*/ + +// estimator primitive result class + +#ifndef __qiskitcpp_primitives_estimator_result_hpp__ +#define __qiskitcpp_primitives_estimator_result_hpp__ + +#include + +#include "primitives/containers/estimator_pub_result.hpp" + +namespace Qiskit { +namespace primitives { + +/// @class EstimatorPrimitiveResult +/// @brief A container for multiple estimator pub results and global metadata. +class EstimatorPrimitiveResult { +protected: + std::vector pub_results_; + +public: + /// @brief Create a new EstimatorPrimitiveResult. + EstimatorPrimitiveResult() {} + + /// @brief allocate pub results. + /// @param num_results number of pub results to be allocated. + void allocate(uint_t num_results) + { + pub_results_.resize(num_results); + } + + /// @brief Return the number of PUBs in this result. + /// @return The number of PUBs. + uint_t size(void) const + { + return pub_results_.size(); + } + + /// @brief Return the pub result. + /// @param i index of pub. + /// @return The pub result. + EstimatorPubResult& operator[](uint_t i) + { + return pub_results_[i]; + } + + /// @brief Return the pub result. + /// @param i index of pub. + /// @return The pub result. + const EstimatorPubResult& operator[](uint_t i) const + { + return pub_results_[i]; + } + + /// @brief set pubs in the results. + /// @param pubs a list of pubs to be set. + void set_pubs(const std::vector& pubs) + { + if (pubs.size() == pub_results_.size()) { + for (uint_t i = 0; i < pub_results_.size(); i++) { + pub_results_[i].set_pub(pubs[i]); + } + } + } +}; + +} // namespace primitives +} // namespace Qiskit + +#endif //__qiskitcpp_primitives_estimator_result_hpp__ diff --git a/src/primitives/containers/estimator_pub.hpp b/src/primitives/containers/estimator_pub.hpp new file mode 100644 index 0000000..dff6f14 --- /dev/null +++ b/src/primitives/containers/estimator_pub.hpp @@ -0,0 +1,447 @@ +/* +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017, 2026. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +*/ + +// estimator pub definition + +#ifndef __qiskitcpp_primitives_estimator_pub_def_hpp__ +#define __qiskitcpp_primitives_estimator_pub_def_hpp__ + +#include +#include +#include +#include +#include +#include + +#include + +#include "circuit/quantumcircuit.hpp" +#include "quantum_info/sparse_observable.hpp" + +namespace Qiskit { +namespace primitives { + +/// @class EstimatorPub +/// @brief Estimator Pub(Primitive Unified Bloc). +class EstimatorPub { +protected: + circuit::QuantumCircuit circuit_; + nlohmann::ordered_json observables_; + nlohmann::ordered_json parameter_values_; + double precision_ = 0.0; + bool has_precision_ = false; + +public: + /// @brief Create a new EstimatorPub. + EstimatorPub() + { + observables_ = nlohmann::ordered_json::object(); + parameter_values_ = nullptr; + } + + /// @brief Create a new EstimatorPub from a Pauli label with unit coefficient. + EstimatorPub(const circuit::QuantumCircuit& circ, const std::string& observable) + { + circuit_ = circ; + observables_ = observable; + parameter_values_ = nullptr; + require_valid(); + } + + /// @brief Create a new EstimatorPub from a Pauli label with unit coefficient. + EstimatorPub(const circuit::QuantumCircuit& circ, const std::string& observable, double precision) + : EstimatorPub(circ, observable) + { + set_precision(precision); + } + + /// @brief Create a new EstimatorPub from a Runtime-compatible observables JSON value. + EstimatorPub(const circuit::QuantumCircuit& circ, const nlohmann::ordered_json& observables) + { + circuit_ = circ; + observables_ = observables; + parameter_values_ = nullptr; + require_valid(); + } + + /// @brief Create a new EstimatorPub from a Runtime-compatible observables JSON value. + EstimatorPub(const circuit::QuantumCircuit& circ, const nlohmann::ordered_json& observables, double precision) + : EstimatorPub(circ, observables) + { + set_precision(precision); + } + + /// @brief Create a new EstimatorPub from real-coefficient Pauli label terms. + EstimatorPub(const circuit::QuantumCircuit& circ, const std::vector>& observable) + { + circuit_ = circ; + observables_ = pauli_terms_to_json(observable); + parameter_values_ = nullptr; + require_valid(); + } + + /// @brief Create a new EstimatorPub from real-coefficient Pauli label terms. + EstimatorPub(const circuit::QuantumCircuit& circ, const std::vector>& observable, double precision) + : EstimatorPub(circ, observable) + { + set_precision(precision); + } + + /// @brief Create a new EstimatorPub from complex-coefficient Pauli label terms. + EstimatorPub(const circuit::QuantumCircuit& circ, const std::vector>>& observable) + { + circuit_ = circ; + observables_ = pauli_terms_to_json(observable); + parameter_values_ = nullptr; + require_valid(); + } + + /// @brief Create a new EstimatorPub from complex-coefficient Pauli label terms. + EstimatorPub(const circuit::QuantumCircuit& circ, const std::vector>>& observable, double precision) + : EstimatorPub(circ, observable) + { + set_precision(precision); + } + + /// @brief Create a new EstimatorPub from a SparseObservable. + EstimatorPub(const circuit::QuantumCircuit& circ, const quantum_info::SparseObservable& observable) + { + circuit_ = circ; + observables_ = sparse_observable_to_json(observable); + parameter_values_ = nullptr; + require_valid(); + } + + /// @brief Create a new EstimatorPub from a SparseObservable. + EstimatorPub(const circuit::QuantumCircuit& circ, const quantum_info::SparseObservable& observable, double precision) + : EstimatorPub(circ, observable) + { + set_precision(precision); + } + + /// @brief Create a new EstimatorPub from a list of SparseObservable objects. + EstimatorPub(const circuit::QuantumCircuit& circ, const std::vector& observables) + { + circuit_ = circ; + observables_ = nlohmann::ordered_json::array(); + for (uint_t i = 0; i < observables.size(); i++) { + observables_.push_back(sparse_observable_to_json(observables[i])); + } + parameter_values_ = nullptr; + require_valid(); + } + + /// @brief Create a new EstimatorPub from a list of SparseObservable objects. + EstimatorPub(const circuit::QuantumCircuit& circ, const std::vector& observables, double precision) + : EstimatorPub(circ, observables) + { + set_precision(precision); + } + + /// @brief Create a new EstimatorPub as a copy of src. + EstimatorPub(const EstimatorPub& src) + { + circuit_ = src.circuit_; + observables_ = src.observables_; + parameter_values_ = src.parameter_values_; + precision_ = src.precision_; + has_precision_ = src.has_precision_; + } + + ~EstimatorPub() {} + + /// @brief Return a QuantumCircuit for this estimator pub. + /// @return a quantum circuit + const circuit::QuantumCircuit& circuit(void) const + { + return circuit_; + } + + /// @brief Return observables for this estimator pub. + /// @return Runtime-compatible observables JSON + const nlohmann::ordered_json& observables(void) const + { + return observables_; + } + + /// @brief Return parameter values for this estimator pub. + /// @return Runtime-compatible parameter-values JSON + const nlohmann::ordered_json& parameter_values(void) const + { + return parameter_values_; + } + + /// @brief Set Runtime-compatible parameter values for this estimator pub. + /// @param parameter_values Runtime-compatible parameter-values JSON + void set_parameter_values(const nlohmann::ordered_json& parameter_values) + { + parameter_values_ = parameter_values; + } + + /// @brief Return whether this pub has an explicit precision. + /// @return true if an explicit target precision has been set. + bool has_precision(void) const + { + return has_precision_; + } + + /// @brief Return the explicit target precision for this pub. + /// @return target precision value. + double precision(void) const + { + return precision_; + } + + /// @brief Set an explicit target precision for this pub. + /// @param precision target precision value. + void set_precision(double precision) + { + if (!valid_precision(precision)) { + throw std::invalid_argument("EstimatorPub precision must be finite and non-negative."); + } + precision_ = precision; + has_precision_ = true; + } + + /// @brief Return the number of observables in this pub. + /// @return number of observables. + uint_t num_observables(void) const + { + if (observables_.is_array()) { + return observables_.size(); + } + return 1; + } + + /// @brief Validate this pub. + /// @param message optional output error message. + /// @return true if valid. + bool validate(std::string* message = nullptr) const + { + if (has_precision_ && !valid_precision(precision_)) { + set_message(message, "EstimatorPub precision must be finite and non-negative."); + return false; + } + + return validate_observable_json(observables_, circuit_.num_qubits(), message); + } + + /// @brief Return a JSON format of this estimator pub. + /// @return a JSON format estimator pub. + nlohmann::ordered_json to_json(void) + { + require_valid(); + + nlohmann::ordered_json ret = nlohmann::ordered_json::array(); + ret.push_back(circuit_.to_qasm3()); + ret.push_back(observables_); + ret.push_back(parameter_values_); + if (has_precision_) { + ret.push_back(precision_); + } + return ret; + } + +protected: + void require_valid(void) const + { + std::string message; + if (!validate(&message)) { + throw std::invalid_argument(message); + } + } + + static bool valid_precision(double precision) + { + return std::isfinite(precision) && precision >= 0.0; + } + + static void set_message(std::string* message, const std::string& value) + { + if (message) { + *message = value; + } + } + + static bool valid_pauli_label(const std::string& label, uint_t width, std::string* message) + { + if (label.size() != width) { + set_message(message, "Estimator observable width must match the circuit width."); + return false; + } + + for (uint_t i = 0; i < label.size(); i++) { + const char ch = label[i]; + if (ch != 'I' && ch != 'X' && ch != 'Y' && ch != 'Z') { + set_message(message, "Estimator observables only support Pauli labels containing I, X, Y, or Z."); + return false; + } + } + return true; + } + + static bool validate_observable_json(const nlohmann::ordered_json& observable, uint_t width, std::string* message) + { + if (observable.is_string()) { + return valid_pauli_label(observable.get(), width, message); + } + + if (observable.is_object()) { + if (observable.empty()) { + set_message(message, "EstimatorPub must contain at least one observable."); + return false; + } + + for (auto it = observable.begin(); it != observable.end(); ++it) { + if (!valid_pauli_label(it.key(), width, message)) { + return false; + } + if (!it.value().is_number()) { + set_message(message, "Estimator observable coefficients must be real numbers."); + return false; + } + const double coeff = it.value().get(); + if (!std::isfinite(coeff)) { + set_message(message, "Estimator observable coefficients must be finite."); + return false; + } + } + return true; + } + + if (observable.is_array()) { + if (observable.empty()) { + set_message(message, "EstimatorPub must contain at least one observable."); + return false; + } + for (uint_t i = 0; i < observable.size(); i++) { + if (observable[i].is_array()) { + set_message(message, "Estimator observable arrays must not be nested."); + return false; + } + if (!validate_observable_json(observable[i], width, message)) { + return false; + } + } + return true; + } + + set_message(message, "Estimator observables must be a Pauli label, Pauli map, or array of observables."); + return false; + } + + static nlohmann::ordered_json pauli_terms_to_json(const std::vector>& observable) + { + if (observable.empty()) { + throw std::invalid_argument("EstimatorPub must contain at least one observable."); + } + + nlohmann::ordered_json ret = nlohmann::ordered_json::object(); + for (uint_t i = 0; i < observable.size(); i++) { + const double coeff = observable[i].second; + if (!std::isfinite(coeff)) { + throw std::invalid_argument("Estimator observable coefficients must be finite."); + } + ret[observable[i].first] = coeff; + } + return ret; + } + + static nlohmann::ordered_json pauli_terms_to_json(const std::vector>>& observable) + { + std::vector> real_terms; + real_terms.reserve(observable.size()); + for (uint_t i = 0; i < observable.size(); i++) { + const auto coeff = observable[i].second; + if (!std::isfinite(coeff.real()) || !std::isfinite(coeff.imag())) { + throw std::invalid_argument("Estimator observable coefficients must be finite."); + } + if (std::abs(coeff.imag()) > 1e-12) { + throw std::invalid_argument("Estimator observable coefficients must be real."); + } + real_terms.push_back(std::make_pair(observable[i].first, coeff.real())); + } + return pauli_terms_to_json(real_terms); + } + + static nlohmann::ordered_json sparse_observable_to_json(const quantum_info::SparseObservable& observable) + { + nlohmann::ordered_json ret = nlohmann::ordered_json::object(); + + const uint_t num_qubits = observable.num_qubits(); + const uint_t num_terms = observable.num_terms(); + const auto coeffs = observable.coeffs(); + const auto bit_terms = observable.bit_terms(); + const auto indices = observable.indices(); + const auto boundaries = observable.boundaries(); + + if (num_terms == 0) { + throw std::invalid_argument("EstimatorPub must contain at least one observable."); + } + + if (coeffs.size() != num_terms || boundaries.size() != num_terms + 1) { + throw std::invalid_argument("SparseObservable has inconsistent term metadata."); + } + + for (uint_t term = 0; term < num_terms; term++) { + if (boundaries[term] > boundaries[term + 1] || boundaries[term + 1] > bit_terms.size() || boundaries[term + 1] > indices.size()) { + throw std::invalid_argument("SparseObservable has inconsistent term boundaries."); + } + + const auto coeff = coeffs[term]; + if (!std::isfinite(coeff.real()) || !std::isfinite(coeff.imag())) { + throw std::invalid_argument("Estimator observable coefficients must be finite."); + } + if (std::abs(coeff.imag()) > 1e-12) { + throw std::invalid_argument("Estimator observable coefficients must be real."); + } + + std::string label(num_qubits, 'I'); + for (uint_t i = boundaries[term]; i < boundaries[term + 1]; i++) { + if (indices[i] >= num_qubits) { + throw std::invalid_argument("SparseObservable term index is outside the observable width."); + } + + char pauli = 'I'; + switch (bit_terms[i]) { + case QkBitTerm_X: + pauli = 'X'; + break; + case QkBitTerm_Y: + pauli = 'Y'; + break; + case QkBitTerm_Z: + pauli = 'Z'; + break; + default: + throw std::invalid_argument("Estimator SparseObservable terms must be Pauli I, X, Y, or Z."); + } + label[num_qubits - 1 - indices[i]] = pauli; + } + + if (ret.contains(label)) { + ret[label] = ret[label].get() + coeff.real(); + } else { + ret[label] = coeff.real(); + } + } + + return ret; + } +}; + +} // namespace primitives +} // namespace Qiskit + +#endif //__qiskitcpp_primitives_estimator_pub_def_hpp__ diff --git a/src/primitives/containers/estimator_pub_result.hpp b/src/primitives/containers/estimator_pub_result.hpp new file mode 100644 index 0000000..47d4efd --- /dev/null +++ b/src/primitives/containers/estimator_pub_result.hpp @@ -0,0 +1,259 @@ +/* +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017, 2026. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +*/ + +// estimator pub result class + +#ifndef __qiskitcpp_primitives_estimator_pub_result_hpp__ +#define __qiskitcpp_primitives_estimator_pub_result_hpp__ + +#include +#include +#include +#include + +#include + +#include "primitives/containers/estimator_pub.hpp" + +namespace Qiskit { +namespace primitives { + +/// @class EstimatorPubResult +/// @brief Result of Estimator Pub(Primitive Unified Bloc). +class EstimatorPubResult { +protected: + std::vector evs_; + std::vector stds_; + nlohmann::ordered_json metadata_; + EstimatorPub pub_; + bool has_pub_ = false; + +public: + /// @brief Create a new EstimatorPubResult. + EstimatorPubResult() + { + metadata_ = nlohmann::ordered_json::object(); + } + + /// @brief Create a new EstimatorPubResult. + /// @param pub a pub for this result. + EstimatorPubResult(const EstimatorPub& pub) + { + pub_ = pub; + has_pub_ = true; + metadata_ = nlohmann::ordered_json::object(); + } + + /// @brief Create a new EstimatorPubResult as a copy of src. + /// @param src copy source. + EstimatorPubResult(const EstimatorPubResult& src) + { + evs_ = src.evs_; + stds_ = src.stds_; + metadata_ = src.metadata_; + pub_ = src.pub_; + has_pub_ = src.has_pub_; + } + + /// @brief Result expectation values for this pub. + /// @return expectation values. + const std::vector& evs(void) const + { + return evs_; + } + + /// @brief Result expectation values for this pub. + /// @return expectation values. + std::vector& evs(void) + { + return evs_; + } + + /// @brief Result standard deviations for this pub. + /// @return standard deviations. + const std::vector& stds(void) const + { + return stds_; + } + + /// @brief Result standard deviations for this pub. + /// @return standard deviations. + std::vector& stds(void) + { + return stds_; + } + + /// @brief Result metadata for this pub. + /// @return metadata. + const nlohmann::ordered_json& metadata(void) const + { + return metadata_; + } + + /// @brief Result metadata for this pub. + /// @return metadata. + nlohmann::ordered_json& metadata(void) + { + return metadata_; + } + + /// @brief get pub for this result. + /// @return pub. + const EstimatorPub& pub(void) const + { + return pub_; + } + + /// @brief set pub for this result. + /// @param pub to be set. + void set_pub(const EstimatorPub& pub) + { + pub_ = pub; + has_pub_ = true; + } + + /// @brief Set pub result from json. + /// @param input Runtime result JSON. + /// @return true if parsing succeeded. + bool from_json(nlohmann::ordered_json& input) + { + if (!input.is_object()) { + std::cerr << " EstimatorPubResult Error : JSON result must be an object." << std::endl; + return false; + } + + if (!input.contains("data") || !input["data"].is_object()) { + std::cerr << " EstimatorPubResult Error : JSON result does not contain data section." << std::endl; + return false; + } + + auto data = input["data"]; + if (!data.contains("evs")) { + std::cerr << " EstimatorPubResult Error : JSON data does not contain evs." << std::endl; + return false; + } + + std::vector evs; + if (!parse_numeric_scalar_or_1d(data["evs"], evs, "evs")) { + return false; + } + + if (has_pub_ && evs.size() != pub_.num_observables()) { + std::cerr << " EstimatorPubResult Error : evs length does not match the submitted observable count." << std::endl; + return false; + } + + std::vector stds; + if (data.contains("stds")) { + if (!parse_numeric_scalar_or_1d(data["stds"], stds, "stds")) { + return false; + } + } else if (data.contains("ensemble_standard_error")) { + if (!parse_numeric_scalar_or_1d(data["ensemble_standard_error"], stds, "ensemble_standard_error")) { + return false; + } + } + + if (!stds.empty() && stds.size() != evs.size()) { + std::cerr << " EstimatorPubResult Error : stds length does not match evs length." << std::endl; + return false; + } + + nlohmann::ordered_json metadata = nlohmann::ordered_json::object(); + if (input.contains("metadata")) { + if (!input["metadata"].is_object()) { + std::cerr << " EstimatorPubResult Error : metadata must be an object." << std::endl; + return false; + } + metadata = input["metadata"]; + if (!validate_metadata(metadata)) { + return false; + } + } + + evs_ = evs; + stds_ = stds; + metadata_ = metadata; + return true; + } + +protected: + static bool parse_numeric_scalar_or_1d(const nlohmann::ordered_json& value, std::vector& output, const std::string& name) + { + output.clear(); + + if (value.is_number()) { + const double number = value.get(); + if (!std::isfinite(number)) { + std::cerr << " EstimatorPubResult Error : " << name << " contains a non-finite value." << std::endl; + return false; + } + output.push_back(number); + return true; + } + + if (!value.is_array()) { + std::cerr << " EstimatorPubResult Error : " << name << " must be a number or one-dimensional array." << std::endl; + return false; + } + + for (uint_t i = 0; i < value.size(); i++) { + if (!value[i].is_number()) { + std::cerr << " EstimatorPubResult Error : " << name << " must be a one-dimensional numeric array." << std::endl; + return false; + } + const double number = value[i].get(); + if (!std::isfinite(number)) { + std::cerr << " EstimatorPubResult Error : " << name << " contains a non-finite value." << std::endl; + return false; + } + output.push_back(number); + } + + return true; + } + + static bool validate_metadata(const nlohmann::ordered_json& metadata) + { + if (metadata.contains("shots")) { + if (!metadata["shots"].is_number_integer() && !metadata["shots"].is_number_unsigned()) { + std::cerr << " EstimatorPubResult Error : metadata.shots must be an integer." << std::endl; + return false; + } + if (metadata["shots"].is_number_integer() && metadata["shots"].get() < 0) { + std::cerr << " EstimatorPubResult Error : metadata.shots must be non-negative." << std::endl; + return false; + } + } + + if (metadata.contains("target_precision")) { + if (!metadata["target_precision"].is_number()) { + std::cerr << " EstimatorPubResult Error : metadata.target_precision must be numeric." << std::endl; + return false; + } + const double target_precision = metadata["target_precision"].get(); + if (!std::isfinite(target_precision) || target_precision < 0.0) { + std::cerr << " EstimatorPubResult Error : metadata.target_precision must be finite and non-negative." << std::endl; + return false; + } + } + + return true; + } +}; + +} // namespace primitives +} // namespace Qiskit + +#endif //__qiskitcpp_primitives_estimator_pub_result_hpp__ diff --git a/src/providers/backend.hpp b/src/providers/backend.hpp index 27c4ac6..d74dd0b 100644 --- a/src/providers/backend.hpp +++ b/src/providers/backend.hpp @@ -19,6 +19,7 @@ #include "utils/types.hpp" #include "transpiler/target.hpp" +#include "primitives/containers/estimator_pub.hpp" #include "primitives/containers/sampler_pub.hpp" #include "providers/job.hpp" @@ -67,6 +68,14 @@ class BackendV2 { /// @return PrimitiveJob virtual std::shared_ptr run(std::vector& circuits, uint_t shots = 0) = 0; + /// @brief Run and estimate expectation values from each pub. + /// @param pubs An iterable of estimator pub-like objects. + /// @return PrimitiveJob + virtual std::shared_ptr run(std::vector& pubs, double precision) + { + return nullptr; + } + }; } // namespace providers @@ -75,4 +84,3 @@ class BackendV2 { #endif //__qiskitcpp_providers_backend_def_hpp__ - diff --git a/src/providers/job.hpp b/src/providers/job.hpp index 1a74a78..12dc1fd 100644 --- a/src/providers/job.hpp +++ b/src/providers/job.hpp @@ -19,6 +19,8 @@ #include "utils/types.hpp" #include "providers/jobstatus.hpp" +#include "primitives/containers/estimator_pub_result.hpp" +#include "primitives/containers/sampler_pub.hpp" #include "primitives/containers/sampler_pub_result.hpp" namespace Qiskit { @@ -32,7 +34,7 @@ class Job { /// @brief Create a new Job Job() {} - ~Job() {} + virtual ~Job() {} /// @brief Return the status of the job. /// @return JobStatus enum. @@ -47,6 +49,15 @@ class Job { /// @param result an output sampler pub result /// @return true if result is successfully set virtual bool result(uint_t index, primitives::SamplerPubResult& result) = 0; + + /// @brief get estimator pub result + /// @param index an index of the result + /// @param result an output estimator pub result + /// @return true if result is successfully set + virtual bool result(uint_t index, primitives::EstimatorPubResult& result) + { + return false; + } }; } // namespace providers @@ -54,5 +65,3 @@ class Job { #endif //__qiskitcpp_providers_job_def_hpp__ - - diff --git a/src/providers/qkrt_job.hpp b/src/providers/qkrt_job.hpp index 352c829..af01f99 100644 --- a/src/providers/qkrt_job.hpp +++ b/src/providers/qkrt_job.hpp @@ -18,6 +18,7 @@ #define __qiskitcpp_providers_Qiskit_Runtime_job_def_hpp__ #include "utils/types.hpp" +#include "primitives/containers/sampler_pub.hpp" #include "primitives/containers/sampler_pub_result.hpp" #include "providers/job.hpp" @@ -132,4 +133,3 @@ class QkrtJob : public providers::Job { #endif //__qiskitcpp_providers_Qiskit_Runtime_backend_def_hpp__ - diff --git a/src/providers/qrmi_backend.hpp b/src/providers/qrmi_backend.hpp index 7df229f..67598c3 100644 --- a/src/providers/qrmi_backend.hpp +++ b/src/providers/qrmi_backend.hpp @@ -19,12 +19,15 @@ #include "utils/types.hpp" #include "providers/backend.hpp" +#include "providers/qrmi_estimator_payload.hpp" #include "providers/qrmi_job.hpp" #include #include "qrmi.h" +#include +#include namespace Qiskit { namespace providers { @@ -124,6 +127,44 @@ class QRMIBackend : public BackendV2 { return std::make_shared(qrmi_, job_id); } + /// @brief Run and estimate expectation values from each pub. + /// @param input_pubs An iterable of estimator pub-like objects. + /// @param precision The default target precision for pubs without an explicit precision. + /// @return PrimitiveJob + std::shared_ptr run(std::vector& input_pubs, double precision) override + { + if (!std::isfinite(precision) || precision < 0.0) { + std::cerr << "QRMI Error : estimator precision must be finite and non-negative." << std::endl; + return nullptr; + } + + nlohmann::ordered_json estimator; + try { + estimator = QRMIEstimatorPayload::build_estimator_input(input_pubs, precision); + } catch (const std::invalid_argument& exc) { + std::cerr << "QRMI Error : " << exc.what() << std::endl; + return nullptr; + } + std::string estimator_input = estimator.dump(2); + const std::string& program_id = QRMIEstimatorPayload::estimator_program_id(); + + QrmiPayload payload; + payload.tag = QRMI_PAYLOAD_QISKIT_PRIMITIVE; + payload.QISKIT_PRIMITIVE.input = (char*)estimator_input.c_str(); + payload.QISKIT_PRIMITIVE.program_id = (char*)program_id.c_str(); + + char *id = NULL; + QrmiReturnCode rc = qrmi_resource_task_start(qrmi_.get(), &payload, &id); + if (rc != QRMI_RETURN_CODE_SUCCESS) { + std::cerr << "QRMI Error : failed to start an estimator task." << std::endl; + return nullptr; + } + std::string job_id = id; + qrmi_string_free((char *)id); + std::cerr << " QRMI Estimator job submitted to " << name_ << ", JOB ID = " << job_id << std::endl; + + return std::make_shared(qrmi_, job_id); + } }; } // namespace providers @@ -131,5 +172,3 @@ class QRMIBackend : public BackendV2 { #endif //__qiskitcpp_providers_backend_def_hpp__ - - diff --git a/src/providers/qrmi_estimator_payload.hpp b/src/providers/qrmi_estimator_payload.hpp new file mode 100644 index 0000000..a25e293 --- /dev/null +++ b/src/providers/qrmi_estimator_payload.hpp @@ -0,0 +1,75 @@ +/* +# This code is part of Qiskit. +# +# (C) Copyright IBM 2026. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +*/ + +// QRMI estimator Runtime V2 payload helpers + +#ifndef __qiskitcpp_providers_qrmi_estimator_payload_hpp__ +#define __qiskitcpp_providers_qrmi_estimator_payload_hpp__ + +#include +#include +#include +#include + +#include + +#include "primitives/containers/estimator_pub.hpp" +#include "utils/types.hpp" + +namespace Qiskit { +namespace providers { + +/// @class QRMIEstimatorPayload +/// @brief QRMI estimator Runtime V2 payload helpers. +class QRMIEstimatorPayload { +public: + static const std::string& estimator_program_id(void) + { + static const std::string program_id = "estimator"; + return program_id; + } + + static nlohmann::ordered_json build_estimator_input(std::vector& input_pubs, double precision) + { + if (!std::isfinite(precision) || precision < 0.0) { + throw std::invalid_argument("Estimator run precision must be finite and non-negative."); + } + + nlohmann::ordered_json estimator; + nlohmann::ordered_json estimator_pubs = nlohmann::ordered_json::array(); + for (uint_t i = 0; i < input_pubs.size(); i++) { + std::string message; + if (!input_pubs[i].validate(&message)) { + throw std::invalid_argument(message); + } + if (input_pubs[i].circuit().has_measurements()) { + throw std::invalid_argument("Estimator circuits must not contain measurement operations."); + } + if (!input_pubs[i].has_precision()) { + input_pubs[i].set_precision(precision); + } + estimator_pubs.push_back(input_pubs[i].to_json()); + } + estimator["pubs"] = estimator_pubs; + estimator["version"] = 2; + estimator["support_qiskit"] = false; + estimator["options"] = nlohmann::ordered_json::object(); + return estimator; + } +}; + +} // namespace providers +} // namespace Qiskit + +#endif //__qiskitcpp_providers_qrmi_estimator_payload_hpp__ diff --git a/src/providers/qrmi_job.hpp b/src/providers/qrmi_job.hpp index b256221..2fbb428 100644 --- a/src/providers/qrmi_job.hpp +++ b/src/providers/qrmi_job.hpp @@ -17,10 +17,16 @@ #ifndef __qiskitcpp_providers_QRMI_job_def_hpp__ #define __qiskitcpp_providers_QRMI_job_def_hpp__ +#include +#include +#include + #include #include "utils/types.hpp" +#include "primitives/containers/estimator_pub_result.hpp" +#include "primitives/containers/sampler_pub.hpp" #include "primitives/containers/sampler_pub_result.hpp" #include "providers/job.hpp" @@ -37,6 +43,7 @@ class QRMIJob : public Job { std::shared_ptr qrmi_ = nullptr; nlohmann::ordered_json results_; // json formatted results (raw output from QRMI) uint_t num_results_ = 0; + bool results_read_ = false; public: /// @brief Create a new QkrtBackend QRMIJob() @@ -59,7 +66,9 @@ class QRMIJob : public Job { { job_id_ = other.job_id_; qrmi_ = other.qrmi_; - num_results_ = 0; + results_ = other.results_; + num_results_ = other.num_results_; + results_read_ = other.results_read_; } ~QRMIJob() @@ -96,8 +105,9 @@ class QRMIJob : public Job { /// @return number of results uint_t num_results(void) override { - if (num_results_ == 0) - read_results(); + if (!results_read_ && !read_results()) { + throw std::runtime_error("QRMIJob failed to read task results."); + } return num_results_; } @@ -107,28 +117,71 @@ class QRMIJob : public Job { /// @return true if result is successfully set bool result(uint_t index, primitives::SamplerPubResult& result) override { - if (num_results_ == 0) - read_results(); + if (!results_read_ && !read_results()) + return false; if (index >= num_results_) return false; - result.from_json(results_["results"][index]); - return true; + return result.from_json(results_["results"][index]); + } + + /// @brief get estimator pub result + /// @param index an index of the result + /// @param result an output estimator pub result + /// @return true if result is successfully set + bool result(uint_t index, primitives::EstimatorPubResult& result) override + { + if (!results_read_ && !read_results()) + return false; + + if (index >= num_results_) + return false; + + return result.from_json(results_["results"][index]); } protected: - void read_results(void) + bool read_results(void) { + if (results_read_) { + return true; + } + char *result = nullptr; int rc = qrmi_resource_task_result(qrmi_.get(), job_id_.c_str(), &result); - if (rc == QRMI_RETURN_CODE_SUCCESS) { - results_ = nlohmann::ordered_json::parse(result); + if (rc != QRMI_RETURN_CODE_SUCCESS) { + return false; + } - num_results_ = results_["results"].size(); + try { + results_ = nlohmann::ordered_json::parse(result); + } catch (const std::exception& e) { qrmi_string_free((char *)result); + stop_task(); + std::cerr << "QRMI Error : failed to parse task result: " << e.what() << std::endl; + return false; + } + + qrmi_string_free((char *)result); + stop_task(); + + if (!results_.is_object() || !results_.contains("results") || !results_["results"].is_array()) { + std::cerr << "QRMI Error : task result does not contain a results array." << std::endl; + return false; + } + + num_results_ = results_["results"].size(); + results_read_ = true; + return true; + } + + void stop_task(void) + { + int rc = qrmi_resource_task_stop(qrmi_.get(), job_id_.c_str()); + if (rc != QRMI_RETURN_CODE_SUCCESS) { + std::cerr << "QRMI Error : failed to stop task after reading results." << std::endl; } - qrmi_resource_task_stop(qrmi_.get(), job_id_.c_str()); } }; @@ -137,5 +190,3 @@ class QRMIJob : public Job { #endif //__qiskitcpp_providers_QRMI_job_def_hpp__ - - diff --git a/src/providers/sqc_job.hpp b/src/providers/sqc_job.hpp index 61ad051..54ad36b 100644 --- a/src/providers/sqc_job.hpp +++ b/src/providers/sqc_job.hpp @@ -21,6 +21,7 @@ #include "utils/types.hpp" +#include "primitives/containers/sampler_pub.hpp" #include "primitives/containers/sampler_pub_result.hpp" #include "providers/job.hpp" @@ -101,4 +102,3 @@ class SQCJob : public Job { #endif //__qiskitcpp_providers_SQC_job_hpp__ - diff --git a/src/quantum_info/sparse_observable.hpp b/src/quantum_info/sparse_observable.hpp index 0e8895d..22b2228 100644 --- a/src/quantum_info/sparse_observable.hpp +++ b/src/quantum_info/sparse_observable.hpp @@ -48,7 +48,40 @@ class SparseObservable SparseObservable(const SparseObservable &other) { - obs_ = qk_obs_copy(other.obs_); + obs_ = other.obs_ ? qk_obs_copy(other.obs_) : nullptr; + } + + SparseObservable &operator=(const SparseObservable &other) + { + if (this != &other) + { + if (obs_) + { + qk_obs_free(obs_); + } + obs_ = other.obs_ ? qk_obs_copy(other.obs_) : nullptr; + } + return *this; + } + + SparseObservable(SparseObservable &&other) + { + obs_ = other.obs_; + other.obs_ = nullptr; + } + + SparseObservable &operator=(SparseObservable &&other) + { + if (this != &other) + { + if (obs_) + { + qk_obs_free(obs_); + } + obs_ = other.obs_; + other.obs_ = nullptr; + } + return *this; } ~SparseObservable() @@ -73,7 +106,7 @@ class SparseObservable return ret; } - static SparseObservable from_label(std::string &label) + static SparseObservable from_label(const std::string &label) { reg_t indices; std::vector boundaries(2); @@ -124,7 +157,7 @@ class SparseObservable return SparseObservable(num_qubits, coeff, terms, indices, boundaries); } - static SparseObservable from_list(std::vector>> &list, uint_t num_qubits_in = 0) + static SparseObservable from_list(const std::vector>> &list, uint_t num_qubits_in = 0) { reg_t indices; std::vector boundaries; @@ -205,16 +238,17 @@ class SparseObservable } std::vector bit_terms(void) const { - std::vector ret(num_terms()); if (obs_) { + std::vector ret(qk_obs_len(obs_)); auto terms = qk_obs_bit_terms(obs_); for (int i = 0; i < ret.size(); i++) { ret[i] = terms[i]; } + return ret; } - return ret; + return std::vector(); } std::vector> coeffs(void) const { @@ -231,29 +265,31 @@ class SparseObservable } reg_t indices(void) const { - reg_t ret(qk_obs_len(obs_)); if (obs_) { + reg_t ret(qk_obs_len(obs_)); auto idx = qk_obs_indices(obs_); for (int i = 0; i < ret.size(); i++) { ret[i] = idx[i]; } + return ret; } - return ret; + return reg_t(); } reg_t boundaries(void) const { - reg_t ret(qk_obs_len(obs_)); if (obs_) { + reg_t ret(num_terms() + 1); auto idx = qk_obs_boundaries(obs_); for (int i = 0; i < ret.size(); i++) { ret[i] = idx[i]; } + return ret; } - return ret; + return reg_t(); } SparseObservable operator+(SparseObservable &rhs) @@ -310,4 +346,4 @@ class SparseObservable } // namespace quantum_info } // namespace Qiskit -#endif //__qiskitcpp_quantum_info_sparse_observable_hpp__ \ No newline at end of file +#endif //__qiskitcpp_quantum_info_sparse_observable_hpp__ diff --git a/src/service/qiskit_runtime_service_qrmi.hpp b/src/service/qiskit_runtime_service_qrmi.hpp index 339f72c..5f2a8b2 100644 --- a/src/service/qiskit_runtime_service_qrmi.hpp +++ b/src/service/qiskit_runtime_service_qrmi.hpp @@ -19,6 +19,7 @@ #include #include +#include #include #include "utils/types.hpp" @@ -175,16 +176,11 @@ class QiskitRuntimeService { _putenv_s(envCRN.c_str(), instance_.c_str()); _putenv_s(envSession.c_str(), session_mode_.c_str()); #else - std::string envEndPoint = name + header + "ENDPOINT=" + url_ + "/api/v1"; - std::string envIAMEndPoint = name + header + "IAM_ENDPOINT=" + iam_url_; - std::string envAPIKey = name + header + "IAM_APIKEY=" + token_; - std::string envCRN = name + header + "SERVICE_CRN=" + instance_; - std::string envSession = name + header + "SESSION_MODE=" + session_mode_; - putenv((char*)envEndPoint.c_str()); - putenv((char*)envIAMEndPoint.c_str()); - putenv((char*)envAPIKey.c_str()); - putenv((char*)envCRN.c_str()); - putenv((char*)envSession.c_str()); + setenv((name + header + "ENDPOINT").c_str(), (url_ + "/api/v1").c_str(), 1); + setenv((name + header + "IAM_ENDPOINT").c_str(), iam_url_.c_str(), 1); + setenv((name + header + "IAM_APIKEY").c_str(), token_.c_str(), 1); + setenv((name + header + "SERVICE_CRN").c_str(), instance_.c_str(), 1); + setenv((name + header + "SESSION_MODE").c_str(), session_mode_.c_str(), 1); #endif std::shared_ptr qrmi(qrmi_resource_new(name.c_str(), type_), qrmi_resource_free); bool is_accessible = false; @@ -212,4 +208,3 @@ class QiskitRuntimeService { #endif //__qiskitcpp_providers_qiskit_runtime_service_QRMI_def_hpp__ - diff --git a/src/utils/utils.hpp b/src/utils/utils.hpp index 6778377..7852b7b 100644 --- a/src/utils/utils.hpp +++ b/src/utils/utils.hpp @@ -70,15 +70,14 @@ inline bool _naive_parity(uint_t x) { #ifdef INTRINSIC_PARITY -bool (*hamming_parity)(uint_t) = &_intrinsic_parity; -uint_t (*popcount)(uint_t) = &_instrinsic_weight; +static bool (*hamming_parity)(uint_t) = &_intrinsic_parity; +static uint_t (*popcount)(uint_t) = &_instrinsic_weight; #else -bool (*hamming_parity)(uint_t) = &_naive_parity; -uint_t (*popcount)(uint_t) = &_naive_weight; +static bool (*hamming_parity)(uint_t) = &_naive_parity; +static uint_t (*popcount)(uint_t) = &_naive_weight; #endif } // namespace Qiskit #endif - diff --git a/test/test_circuit.cpp b/test/test_circuit.cpp index eae4cd8..53bb692 100644 --- a/test/test_circuit.cpp +++ b/test/test_circuit.cpp @@ -641,6 +641,29 @@ static int test_to_qasm3_multi_regs(void) { return Ok; } +static int test_to_qasm3_physical_qubits(void) { + auto circ = QuantumCircuit(156, 0); + circ.h(21); + circ.cx(21, 22); + + std::vector layout({21, 22}); + circ.set_qiskit_circuit(circ.get_rust_circuit(), layout); + + const auto actual = circ.to_qasm3(); + const std::string expected = + "OPENQASM 3.0;\n" + "include \"stdgates.inc\";\n" + "h $21;\n" + "cx $21, $22;\n"; + if (actual != expected) { + std::cerr << " to_qasm3_physical_qubits 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 @@ -652,6 +675,7 @@ int test_circuit(int argc, char** argv) { num_failed += RUN_TEST(test_append); num_failed += RUN_TEST(test_compose); num_failed += RUN_TEST(test_to_qasm3_multi_regs); + num_failed += RUN_TEST(test_to_qasm3_physical_qubits); std::cerr << "=== Number of failed subtests: " << num_failed << std::endl; return num_failed; diff --git a/test/test_estimator.cpp b/test/test_estimator.cpp new file mode 100644 index 0000000..f4fe8cc --- /dev/null +++ b/test/test_estimator.cpp @@ -0,0 +1,728 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2026. +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +#include +#include +#include +#include +#include +#include + +#include + +#include "common.hpp" + +#include "circuit/quantumcircuit.hpp" +#include "primitives/backend_estimator_v2.hpp" +#include "primitives/containers/estimator_pub.hpp" +#include "providers/backend.hpp" +#include "providers/job.hpp" +#include "providers/qrmi_estimator_payload.hpp" +#include "quantum_info/sparse_observable.hpp" + +using namespace Qiskit; +using namespace Qiskit::circuit; +using namespace Qiskit::primitives; +using namespace Qiskit::providers; +using namespace Qiskit::quantum_info; + +class MockEstimatorJob : public Job { +protected: + nlohmann::ordered_json results_; + JobStatus status_; + bool fail_result_ = false; + bool override_num_results_ = false; + uint_t reported_num_results_ = 0; + +public: + MockEstimatorJob(const nlohmann::ordered_json& results, JobStatus status = JobStatus::DONE, bool fail_result = false, bool override_num_results = false, uint_t reported_num_results = 0) + { + results_ = results; + status_ = status; + fail_result_ = fail_result; + override_num_results_ = override_num_results; + reported_num_results_ = reported_num_results; + } + + JobStatus status(void) override + { + return status_; + } + + uint_t num_results(void) override + { + if (override_num_results_) { + return reported_num_results_; + } + return results_.size(); + } + + bool result(uint_t index, SamplerPubResult& result) override + { + return false; + } + + bool result(uint_t index, EstimatorPubResult& result) override + { + if (fail_result_) { + return false; + } + if (index >= results_.size()) { + return false; + } + auto item = results_[index]; + return result.from_json(item); + } +}; + +class MockEstimatorBackend : public BackendV2 { +protected: + transpiler::Target target_; + nlohmann::ordered_json results_; + JobStatus status_ = JobStatus::DONE; + bool fail_result_ = false; + bool override_num_results_ = false; + uint_t reported_num_results_ = 0; + +public: + double received_precision_ = -1.0; + std::vector received_pubs_; + + MockEstimatorBackend(const nlohmann::ordered_json& results) + { + results_ = results; + } + + MockEstimatorBackend(const nlohmann::ordered_json& results, JobStatus status) + { + results_ = results; + status_ = status; + } + + const transpiler::Target& target(void) override + { + return target_; + } + + std::shared_ptr run(std::vector& circuits, uint_t shots = 0) override + { + return nullptr; + } + + std::shared_ptr run(std::vector& pubs, double precision) override + { + received_precision_ = precision; + received_pubs_ = pubs; + return std::make_shared(results_, status_, fail_result_, override_num_results_, reported_num_results_); + } + + void set_fail_result(bool fail_result) + { + fail_result_ = fail_result; + } + + void set_reported_num_results(uint_t reported_num_results) + { + override_num_results_ = true; + reported_num_results_ = reported_num_results; + } +}; + +static nlohmann::ordered_json estimator_result(double ev, double std, int shots, double target_precision) +{ + nlohmann::ordered_json result; + result["data"]["evs"] = ev; + result["data"]["stds"] = std; + result["metadata"]["shots"] = shots; + result["metadata"]["target_precision"] = target_precision; + return result; +} + +static int test_estimator_pub_to_json(void) +{ + auto circ = QuantumCircuit(2, 0); + std::vector> terms; + terms.push_back(std::make_pair(std::string("ZI"), 1.0)); + terms.push_back(std::make_pair(std::string("IZ"), -0.5)); + + auto unresolved_pub = EstimatorPub(circ, terms); + auto unresolved_payload = unresolved_pub.to_json(); + if (unresolved_payload.size() != 3) { + std::cerr << " estimator pub json test : unexpected unresolved PUB shape." << std::endl; + return EqualityError; + } + + auto pub = EstimatorPub(circ, terms, 0.02); + auto payload = pub.to_json(); + + if (payload.size() != 4 || !payload[1].is_object() || !payload[2].is_null()) { + std::cerr << " estimator pub json test : unexpected PUB shape." << std::endl; + return EqualityError; + } + if (std::abs(payload[1]["ZI"].get() - 1.0) > 1e-12 || + std::abs(payload[1]["IZ"].get() + 0.5) > 1e-12 || + std::abs(payload[3].get() - 0.02) > 1e-12) { + std::cerr << " estimator pub json test : unexpected observable or precision values." << std::endl; + return EqualityError; + } + return Ok; +} + +static int test_estimator_qrmi_payload_contract(void) +{ + auto circ = QuantumCircuit(1, 0); + auto default_precision_pub = EstimatorPub(circ, std::string("Z")); + auto explicit_precision_pub = EstimatorPub(circ, std::string("X"), 0.03125); + std::vector pubs; + pubs.push_back(default_precision_pub); + pubs.push_back(explicit_precision_pub); + + auto payload = QRMIEstimatorPayload::build_estimator_input(pubs, 0.02); + + if (QRMIEstimatorPayload::estimator_program_id() != "estimator") { + std::cerr << " estimator QRMI payload test : wrong program id." << std::endl; + return EqualityError; + } + + if (!payload.is_object() || + !payload.contains("version") || + !payload["version"].is_number_integer() || + payload["version"].get() != 2 || + !payload.contains("support_qiskit") || + !payload["support_qiskit"].is_boolean() || + payload["support_qiskit"].get() || + !payload.contains("options") || + !payload["options"].is_object() || + !payload.contains("pubs") || + !payload["pubs"].is_array() || + payload["pubs"].size() != 2) { + std::cerr << " estimator QRMI payload test : unexpected top-level shape." << std::endl; + return EqualityError; + } + + const auto& first_pub = payload["pubs"][0]; + const auto& second_pub = payload["pubs"][1]; + if (!first_pub.is_array() || first_pub.size() != 4 || + !second_pub.is_array() || second_pub.size() != 4 || + !first_pub[0].is_string() || + !first_pub[1].is_string() || + first_pub[1].get() != "Z" || + !first_pub[2].is_null() || + !first_pub[3].is_number() || + std::abs(first_pub[3].get() - 0.02) > 1e-12 || + !second_pub[1].is_string() || + second_pub[1].get() != "X" || + !second_pub[3].is_number() || + std::abs(second_pub[3].get() - 0.03125) > 1e-12) { + std::cerr << " estimator QRMI payload test : unexpected PUB shape." << std::endl; + return EqualityError; + } + + if (!pubs[0].has_precision() || + std::abs(pubs[0].precision() - 0.02) > 1e-12 || + !pubs[1].has_precision() || + std::abs(pubs[1].precision() - 0.03125) > 1e-12) { + std::cerr << " estimator QRMI payload test : precision resolution failed." << std::endl; + return EqualityError; + } + + return Ok; +} + +static int test_estimator_qrmi_payload_rejects_measured_circuit(void) +{ + auto circ = QuantumCircuit(1, 1); + circ.measure(0, 0); + auto pub = EstimatorPub(circ, std::string("Z")); + std::vector pubs; + pubs.push_back(pub); + + try { + auto payload = QRMIEstimatorPayload::build_estimator_input(pubs, 0.02); + (void)payload; + } catch (const std::invalid_argument&) { + return Ok; + } + + std::cerr << " estimator QRMI payload measured circuit test : measurement was not rejected." << std::endl; + return EqualityError; +} + +static int test_estimator_pub_sparse_observable(void) +{ + auto circ = QuantumCircuit(2, 0); + std::string label = "ZI"; + auto observable = SparseObservable::from_label(label); + auto pub = EstimatorPub(circ, observable); + auto payload = pub.to_json(); + + if (!payload[1].is_object() || !payload[1].contains("ZI")) { + std::cerr << " estimator pub sparse observable test : expected ZI observable." << std::endl; + return EqualityError; + } + + return Ok; +} + +static int test_estimator_rejects_empty_json_observable_map(void) +{ + auto circ = QuantumCircuit(1, 0); + nlohmann::ordered_json observables = nlohmann::ordered_json::object(); + + try { + auto pub = EstimatorPub(circ, observables); + (void)pub; + } catch (const std::invalid_argument&) { + return Ok; + } + + std::cerr << " estimator empty JSON observable test : empty map was not rejected." << std::endl; + return EqualityError; +} + +static int test_estimator_rejects_nested_observable_array(void) +{ + auto circ = QuantumCircuit(1, 0); + nlohmann::ordered_json nested = nlohmann::ordered_json::array(); + nested.push_back("Z"); + nested.push_back("X"); + nlohmann::ordered_json observables = nlohmann::ordered_json::array(); + observables.push_back(nested); + + try { + auto pub = EstimatorPub(circ, observables); + (void)pub; + } catch (const std::invalid_argument&) { + return Ok; + } + + std::cerr << " estimator nested observable array test : nested array was not rejected." << std::endl; + return EqualityError; +} + +static int test_estimator_rejects_empty_pauli_terms(void) +{ + auto circ = QuantumCircuit(1, 0); + + try { + std::vector> real_terms; + auto pub = EstimatorPub(circ, real_terms); + (void)pub; + } catch (const std::invalid_argument&) { + try { + std::vector>> complex_terms; + auto pub = EstimatorPub(circ, complex_terms); + (void)pub; + } catch (const std::invalid_argument&) { + return Ok; + } + } + + std::cerr << " estimator empty Pauli terms test : empty terms were not rejected." << std::endl; + return EqualityError; +} + +static int test_estimator_rejects_zero_sparse_observable(void) +{ + auto circ = QuantumCircuit(1, 0); + + try { + auto observable = SparseObservable::zero(1); + auto pub = EstimatorPub(circ, observable); + (void)pub; + } catch (const std::invalid_argument&) { + return Ok; + } + + std::cerr << " estimator zero SparseObservable test : zero observable was not rejected." << std::endl; + return EqualityError; +} + +static int test_estimator_rejects_bad_precision(void) +{ + auto circ = QuantumCircuit(1, 0); + try { + auto pub = EstimatorPub(circ, std::string("Z"), std::nan("")); + (void)pub; + } catch (const std::invalid_argument&) { + return Ok; + } + + std::cerr << " estimator precision test : NaN precision was not rejected." << std::endl; + return EqualityError; +} + +static int test_estimator_rejects_width_mismatch(void) +{ + auto circ = QuantumCircuit(2, 0); + try { + auto pub = EstimatorPub(circ, std::string("ZZZ")); + (void)pub; + } catch (const std::invalid_argument&) { + return Ok; + } + + std::cerr << " estimator width test : width mismatch was not rejected." << std::endl; + return EqualityError; +} + +static int test_estimator_rejects_measured_circuit(void) +{ + auto circ = QuantumCircuit(1, 1); + circ.measure(0, 0); + auto pub = EstimatorPub(circ, std::string("Z")); + nlohmann::ordered_json results = nlohmann::ordered_json::array(); + results.push_back(estimator_result(1.0, 0.0, 100, 0.02)); + MockEstimatorBackend backend(results); + auto estimator = BackendEstimatorV2(backend); + + try { + estimator.run({pub}); + } catch (const std::invalid_argument&) { + return Ok; + } + + std::cerr << " estimator measured circuit test : measurement was not rejected." << std::endl; + return EqualityError; +} + +static int test_estimator_job_result(void) +{ + auto circ = QuantumCircuit(1, 0); + auto pub = EstimatorPub(circ, std::string("Z")); + nlohmann::ordered_json results = nlohmann::ordered_json::array(); + results.push_back(estimator_result(0.75, 0.125, 4096, 0.02)); + MockEstimatorBackend backend(results); + auto estimator = BackendEstimatorV2(backend); + + auto job = estimator.run({pub}, 0.02); + if (!job) { + std::cerr << " estimator job result test : estimator returned nullptr job." << std::endl; + return NullptrError; + } + + auto primitive_result = job->result(); + if (primitive_result.size() != 1 || primitive_result[0].evs().size() != 1 || primitive_result[0].stds().size() != 1) { + std::cerr << " estimator job result test : unexpected result shape." << std::endl; + return EqualityError; + } + if (std::abs(primitive_result[0].evs()[0] - 0.75) > 1e-12 || + std::abs(primitive_result[0].stds()[0] - 0.125) > 1e-12 || + backend.received_pubs_.size() != 1 || + !backend.received_pubs_[0].has_precision() || + std::abs(backend.received_pubs_[0].precision() - 0.02) > 1e-12) { + std::cerr << " estimator job result test : unexpected values." << std::endl; + return EqualityError; + } + + return Ok; +} + +static int test_estimator_result_accepts_one_observable_scalar(void) +{ + auto circ = QuantumCircuit(1, 0); + auto pub = EstimatorPub(circ, std::string("Z")); + auto result = EstimatorPubResult(pub); + + nlohmann::ordered_json input; + input["data"]["evs"] = 0.5; + input["data"]["stds"] = 0.125; + + if (result.from_json(input) && result.evs().size() == 1 && result.stds().size() == 1) { + return Ok; + } + + std::cerr << " estimator scalar result test : one-observable scalar result was rejected." << std::endl; + return EqualityError; +} + +static int test_estimator_result_accepts_one_observable_array(void) +{ + auto circ = QuantumCircuit(1, 0); + auto pub = EstimatorPub(circ, std::string("Z")); + auto result = EstimatorPubResult(pub); + + nlohmann::ordered_json input; + input["data"]["evs"] = nlohmann::ordered_json::array({0.5}); + input["data"]["stds"] = nlohmann::ordered_json::array({0.125}); + + if (result.from_json(input) && result.evs().size() == 1 && result.stds().size() == 1) { + return Ok; + } + + std::cerr << " estimator one-observable array result test : result was rejected." << std::endl; + return EqualityError; +} + +static int test_estimator_result_accepts_multi_observable_array(void) +{ + auto circ = QuantumCircuit(1, 0); + nlohmann::ordered_json observables = nlohmann::ordered_json::array(); + observables.push_back("Z"); + observables.push_back("X"); + auto pub = EstimatorPub(circ, observables); + auto result = EstimatorPubResult(pub); + + nlohmann::ordered_json input; + input["data"]["evs"] = nlohmann::ordered_json::array({0.5, -0.25}); + input["data"]["stds"] = nlohmann::ordered_json::array({0.125, 0.0625}); + + if (result.from_json(input) && result.evs().size() == 2 && result.stds().size() == 2) { + return Ok; + } + + std::cerr << " estimator multi-observable result test : valid result was rejected." << std::endl; + return EqualityError; +} + +static int test_estimator_result_rejects_too_few_evs(void) +{ + auto circ = QuantumCircuit(1, 0); + nlohmann::ordered_json observables = nlohmann::ordered_json::array(); + observables.push_back("Z"); + observables.push_back("X"); + auto pub = EstimatorPub(circ, observables); + auto result = EstimatorPubResult(pub); + + nlohmann::ordered_json input; + input["data"]["evs"] = nlohmann::ordered_json::array({0.5}); + + if (!result.from_json(input)) { + return Ok; + } + + std::cerr << " estimator too-few evs test : short evs array was not rejected." << std::endl; + return EqualityError; +} + +static int test_estimator_result_rejects_too_many_evs(void) +{ + auto circ = QuantumCircuit(1, 0); + auto pub = EstimatorPub(circ, std::string("Z")); + auto result = EstimatorPubResult(pub); + + nlohmann::ordered_json input; + input["data"]["evs"] = nlohmann::ordered_json::array({0.5, -0.25}); + + if (!result.from_json(input)) { + return Ok; + } + + std::cerr << " estimator too-many evs test : long evs array was not rejected." << std::endl; + return EqualityError; +} + +static int test_estimator_result_rejects_stds_mismatch(void) +{ + auto circ = QuantumCircuit(1, 0); + nlohmann::ordered_json observables = nlohmann::ordered_json::array(); + observables.push_back("Z"); + observables.push_back("X"); + auto pub = EstimatorPub(circ, observables); + auto result = EstimatorPubResult(pub); + + nlohmann::ordered_json input; + input["data"]["evs"] = nlohmann::ordered_json::array({0.5, -0.25}); + input["data"]["stds"] = nlohmann::ordered_json::array({0.125}); + + if (!result.from_json(input)) { + return Ok; + } + + std::cerr << " estimator stds mismatch test : mismatched stds were not rejected." << std::endl; + return EqualityError; +} + +static int test_estimator_result_rejects_nested_evs(void) +{ + auto circ = QuantumCircuit(1, 0); + auto pub = EstimatorPub(circ, std::string("Z")); + auto result = EstimatorPubResult(pub); + + nlohmann::ordered_json input; + input["data"]["evs"] = nlohmann::ordered_json::array(); + input["data"]["evs"].push_back(nlohmann::ordered_json::array({1.0})); + + if (!result.from_json(input)) { + return Ok; + } + + std::cerr << " estimator nested evs test : nested evs were not rejected." << std::endl; + return EqualityError; +} + +static int test_estimator_result_rejects_bad_metadata(void) +{ + auto circ = QuantumCircuit(1, 0); + auto pub = EstimatorPub(circ, std::string("Z")); + auto result = EstimatorPubResult(pub); + + nlohmann::ordered_json input; + input["data"]["evs"] = 1.0; + input["metadata"]["shots"] = -1; + + if (!result.from_json(input)) { + return Ok; + } + + std::cerr << " estimator metadata test : negative shots were not rejected." << std::endl; + return EqualityError; +} + +static int test_estimator_result_rejects_bad_target_precision(void) +{ + auto circ = QuantumCircuit(1, 0); + auto pub = EstimatorPub(circ, std::string("Z")); + auto result = EstimatorPubResult(pub); + + nlohmann::ordered_json input; + input["data"]["evs"] = 1.0; + input["metadata"]["target_precision"] = -0.1; + + if (!result.from_json(input)) { + return Ok; + } + + std::cerr << " estimator metadata test : negative target_precision was not rejected." << std::endl; + return EqualityError; +} + +static bool estimator_job_result_throws_for_status(JobStatus status) +{ + auto circ = QuantumCircuit(1, 0); + auto pub = EstimatorPub(circ, std::string("Z")); + nlohmann::ordered_json results = nlohmann::ordered_json::array(); + results.push_back(estimator_result(1.0, 0.0, 100, 0.02)); + MockEstimatorBackend backend(results, status); + auto estimator = BackendEstimatorV2(backend); + auto job = estimator.run({pub}, 0.02); + + try { + auto result = job->result(); + (void)result; + } catch (const std::runtime_error&) { + return true; + } + + return false; +} + +static int test_estimator_job_rejects_error_state(void) +{ + if (estimator_job_result_throws_for_status(JobStatus::ERROR)) { + return Ok; + } + + std::cerr << " estimator error state test : ERROR state produced a result." << std::endl; + return EqualityError; +} + +static int test_estimator_job_rejects_failed_state(void) +{ + if (estimator_job_result_throws_for_status(JobStatus::FAILED)) { + return Ok; + } + + std::cerr << " estimator failed state test : FAILED state produced a result." << std::endl; + return EqualityError; +} + +static int test_estimator_job_rejects_cancelled_state(void) +{ + if (estimator_job_result_throws_for_status(JobStatus::CANCELLED)) { + return Ok; + } + + std::cerr << " estimator cancelled state test : CANCELLED state produced a result." << std::endl; + return EqualityError; +} + +static int test_estimator_job_rejects_provider_read_failure(void) +{ + auto circ = QuantumCircuit(1, 0); + auto pub = EstimatorPub(circ, std::string("Z")); + nlohmann::ordered_json results = nlohmann::ordered_json::array(); + results.push_back(estimator_result(1.0, 0.0, 100, 0.02)); + MockEstimatorBackend backend(results); + backend.set_fail_result(true); + auto estimator = BackendEstimatorV2(backend); + auto job = estimator.run({pub}, 0.02); + + try { + auto result = job->result(); + (void)result; + } catch (const std::runtime_error&) { + return Ok; + } + + std::cerr << " estimator provider read failure test : failed read produced a result." << std::endl; + return EqualityError; +} + +static int test_estimator_job_rejects_result_count_mismatch(void) +{ + auto circ = QuantumCircuit(1, 0); + auto pub = EstimatorPub(circ, std::string("Z")); + nlohmann::ordered_json results = nlohmann::ordered_json::array(); + results.push_back(estimator_result(1.0, 0.0, 100, 0.02)); + MockEstimatorBackend backend(results); + backend.set_reported_num_results(2); + auto estimator = BackendEstimatorV2(backend); + auto job = estimator.run({pub}, 0.02); + + try { + auto result = job->result(); + (void)result; + } catch (const std::runtime_error&) { + return Ok; + } + + std::cerr << " estimator result count mismatch test : mismatched count produced a result." << std::endl; + return EqualityError; +} + +#if defined(_WIN32) +int test_estimator(int argc, char** const argv) +#else +int test_estimator(int argc, char** argv) +#endif +{ + int num_failed = 0; + num_failed += RUN_TEST(test_estimator_pub_to_json); + num_failed += RUN_TEST(test_estimator_qrmi_payload_contract); + num_failed += RUN_TEST(test_estimator_qrmi_payload_rejects_measured_circuit); + num_failed += RUN_TEST(test_estimator_pub_sparse_observable); + num_failed += RUN_TEST(test_estimator_rejects_empty_json_observable_map); + num_failed += RUN_TEST(test_estimator_rejects_nested_observable_array); + num_failed += RUN_TEST(test_estimator_rejects_empty_pauli_terms); + num_failed += RUN_TEST(test_estimator_rejects_zero_sparse_observable); + num_failed += RUN_TEST(test_estimator_rejects_bad_precision); + num_failed += RUN_TEST(test_estimator_rejects_width_mismatch); + num_failed += RUN_TEST(test_estimator_rejects_measured_circuit); + num_failed += RUN_TEST(test_estimator_job_result); + num_failed += RUN_TEST(test_estimator_result_accepts_one_observable_scalar); + num_failed += RUN_TEST(test_estimator_result_accepts_one_observable_array); + num_failed += RUN_TEST(test_estimator_result_accepts_multi_observable_array); + num_failed += RUN_TEST(test_estimator_result_rejects_too_few_evs); + num_failed += RUN_TEST(test_estimator_result_rejects_too_many_evs); + num_failed += RUN_TEST(test_estimator_result_rejects_stds_mismatch); + num_failed += RUN_TEST(test_estimator_result_rejects_nested_evs); + num_failed += RUN_TEST(test_estimator_result_rejects_bad_metadata); + num_failed += RUN_TEST(test_estimator_result_rejects_bad_target_precision); + num_failed += RUN_TEST(test_estimator_job_rejects_error_state); + num_failed += RUN_TEST(test_estimator_job_rejects_failed_state); + num_failed += RUN_TEST(test_estimator_job_rejects_cancelled_state); + num_failed += RUN_TEST(test_estimator_job_rejects_provider_read_failure); + num_failed += RUN_TEST(test_estimator_job_rejects_result_count_mismatch); + + return num_failed; +} From 79d1dfdc4df80e3e589b55a3acd3e2dbbcb0da29 Mon Sep 17 00:00:00 2001 From: Jun Doi Date: Tue, 9 Jun 2026 16:45:44 +0900 Subject: [PATCH 2/4] add SparseObservable::apply_layout --- samples/estimator_test.cpp | 50 ++------------------------ src/quantum_info/sparse_observable.hpp | 12 +++++++ 2 files changed, 15 insertions(+), 47 deletions(-) diff --git a/samples/estimator_test.cpp b/samples/estimator_test.cpp index f051465..5611046 100644 --- a/samples/estimator_test.cpp +++ b/samples/estimator_test.cpp @@ -38,50 +38,6 @@ using namespace Qiskit::service; using Estimator = BackendEstimatorV2; -namespace { - -std::string apply_layout_to_pauli_label(const std::string& label, const reg_t& layout, uint_t output_width) -{ - if (layout.empty()) { - if (label.size() != output_width) { - throw std::invalid_argument("Estimator observable width must match the circuit width."); - } - return label; - } - - if (layout.size() < label.size()) { - throw std::invalid_argument("Transpile layout is smaller than the observable width."); - } - - std::string mapped_label(output_width, 'I'); - const uint_t input_width = label.size(); - for (uint_t logical = 0; logical < input_width; logical++) { - const uint_t physical = layout[logical]; - if (physical >= output_width) { - throw std::invalid_argument("Transpile layout maps outside the circuit width."); - } - mapped_label[output_width - 1 - physical] = label[input_width - 1 - logical]; - } - return mapped_label; -} - -std::vector>> apply_layout_to_terms( - const std::vector>>& terms, - const reg_t& layout, - uint_t output_width) -{ - std::vector>> mapped_terms; - mapped_terms.reserve(terms.size()); - for (const auto& term : terms) { - mapped_terms.push_back(std::make_pair( - apply_layout_to_pauli_label(term.first, layout, output_width), - term.second)); - } - return mapped_terms; -} - -} // namespace - int main() { QuantumCircuit circ(2, 0); @@ -96,10 +52,10 @@ int main() auto backend = service.backend("ibm_kingston"); auto estimator = Estimator(backend); auto transpiled_circ = transpile(circ, backend); - auto mapped_terms = apply_layout_to_terms(terms, transpiled_circ.get_qubit_map(), transpiled_circ.num_qubits()); - auto observable = SparseObservable::from_list(mapped_terms, transpiled_circ.num_qubits()); + auto observable = SparseObservable::from_list(terms); + auto mapped_observable = observable.apply_layout(transpiled_circ.get_qubit_map()); - auto job = estimator.run({EstimatorPub(transpiled_circ, observable)}, 0.02); + auto job = estimator.run({EstimatorPub(transpiled_circ, mapped_observable)}, 0.02); if (job == nullptr) { return -1; } diff --git a/src/quantum_info/sparse_observable.hpp b/src/quantum_info/sparse_observable.hpp index 22b2228..22d946b 100644 --- a/src/quantum_info/sparse_observable.hpp +++ b/src/quantum_info/sparse_observable.hpp @@ -341,6 +341,18 @@ class SparseObservable qk_str_free(str); return ret; } + + SparseObservable apply_layout(const reg_t& layout) + { + std::vector layout32(layout.size()); + for (size_t i = 0; i< layout.size(); i++) { + layout32[i] = (uint32_t)layout[i]; + } + SparseObservable ret; + ret.obs_ = qk_obs_copy(obs_); + qk_obs_apply_layout(ret.obs_, layout32.data(), (uint32_t)layout32.size()); + return ret; + } }; } // namespace quantum_info From 45f5ba92b931af59c39f918ca5bf160d6d022c61 Mon Sep 17 00:00:00 2001 From: GoiBasia Date: Tue, 9 Jun 2026 17:12:15 +0800 Subject: [PATCH 3/4] Avoid Windows header macros in primitive job headers --- src/primitives/backend_estimator_job.hpp | 9 --------- src/primitives/backend_sampler_job.hpp | 12 +----------- 2 files changed, 1 insertion(+), 20 deletions(-) diff --git a/src/primitives/backend_estimator_job.hpp b/src/primitives/backend_estimator_job.hpp index 454b35e..d067b5a 100644 --- a/src/primitives/backend_estimator_job.hpp +++ b/src/primitives/backend_estimator_job.hpp @@ -17,13 +17,8 @@ #ifndef __qiskitcpp_primitives_backend_estimator_job_def_hpp__ #define __qiskitcpp_primitives_backend_estimator_job_def_hpp__ -#ifdef _MSC_VER -#define NOMINMAX -#include -#else #include #include -#endif #include #include @@ -138,11 +133,7 @@ class BackendEstimatorJob { if (is_final_state(st)) { break; } -#ifdef _MSC_VER - Sleep(1); -#else std::this_thread::sleep_for(std::chrono::seconds(1)); -#endif } if (st != providers::JobStatus::DONE) { diff --git a/src/primitives/backend_sampler_job.hpp b/src/primitives/backend_sampler_job.hpp index 26b9a88..db627f1 100644 --- a/src/primitives/backend_sampler_job.hpp +++ b/src/primitives/backend_sampler_job.hpp @@ -17,13 +17,8 @@ #ifndef __qiskitcpp_primitives_qrmi_qiskit_ibm_runtime_job_def_hpp__ #define __qiskitcpp_primitives_qrmi_qiskit_ibm_runtime_job_def_hpp__ -#ifdef _MSC_VER -#define NOMINMAX -#include -#else -#include #include -#endif +#include #include "primitives/containers/sampler_pub.hpp" @@ -118,11 +113,7 @@ class BackendSamplerJob : public BasePrimitiveJob { st = job_->status(); if (st == providers::JobStatus::DONE || st == providers::JobStatus::CANCELLED || st == providers::JobStatus::FAILED) break; -#ifdef _MSC_VER - Sleep(1); -#else std::this_thread::sleep_for(std::chrono::seconds(1)); -#endif } PrimitiveResult result; @@ -144,4 +135,3 @@ class BackendSamplerJob : public BasePrimitiveJob { #endif //__qiskitcpp_primitives_qrmi_qiskit_ibm_runtime_job_def_hpp__ - From 7693f927ec5ea8f57364bdaf61c40eceb0eb7040 Mon Sep 17 00:00:00 2001 From: GoiBasia Date: Tue, 9 Jun 2026 19:01:32 +0800 Subject: [PATCH 4/4] Avoid Windows ERROR macro collisions2 --- src/primitives/backend_estimator_job.hpp | 5 +++++ src/providers/jobstatus.hpp | 6 +++++- src/service/qiskit_runtime_service_c.hpp | 4 +++- src/service/qiskit_runtime_service_qrmi.hpp | 4 +++- src/utils/utils.hpp | 3 +++ test/test_estimator.cpp | 9 +++++++-- 6 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/primitives/backend_estimator_job.hpp b/src/primitives/backend_estimator_job.hpp index d067b5a..4b719c0 100644 --- a/src/primitives/backend_estimator_job.hpp +++ b/src/primitives/backend_estimator_job.hpp @@ -27,6 +27,11 @@ #include "primitives/containers/estimator_primitive_result.hpp" #include "providers/job.hpp" +// Windows headers can define ERROR after jobstatus.hpp has already been parsed. +#ifdef ERROR +#undef ERROR +#endif + namespace Qiskit { namespace primitives { diff --git a/src/providers/jobstatus.hpp b/src/providers/jobstatus.hpp index 4c5a1f1..835df63 100644 --- a/src/providers/jobstatus.hpp +++ b/src/providers/jobstatus.hpp @@ -19,6 +19,11 @@ #include "utils/types.hpp" +// Windows headers define ERROR as a macro, which breaks JobStatus::ERROR. +#ifdef ERROR +#undef ERROR +#endif + namespace Qiskit { namespace providers { @@ -41,4 +46,3 @@ enum class JobStatus { #endif //__qiskitcpp_providers_jobstatus_def_hpp__ - diff --git a/src/service/qiskit_runtime_service_c.hpp b/src/service/qiskit_runtime_service_c.hpp index c3eb6ae..3e61a4e 100644 --- a/src/service/qiskit_runtime_service_c.hpp +++ b/src/service/qiskit_runtime_service_c.hpp @@ -25,6 +25,9 @@ #ifdef _MSC_VER #define NOMINMAX #include +#ifdef ERROR +#undef ERROR +#endif #else #include #include @@ -138,4 +141,3 @@ class QiskitRuntimeService { #endif //__qiskitcpp_providers_qiskit_runtime_service_c_def_hpp__ - diff --git a/src/service/qiskit_runtime_service_qrmi.hpp b/src/service/qiskit_runtime_service_qrmi.hpp index 5f2a8b2..9d868e7 100644 --- a/src/service/qiskit_runtime_service_qrmi.hpp +++ b/src/service/qiskit_runtime_service_qrmi.hpp @@ -28,6 +28,9 @@ #ifdef _MSC_VER #define NOMINMAX #include +#ifdef ERROR +#undef ERROR +#endif #else #include #include @@ -207,4 +210,3 @@ class QiskitRuntimeService { #endif //__qiskitcpp_providers_qiskit_runtime_service_QRMI_def_hpp__ - diff --git a/src/utils/utils.hpp b/src/utils/utils.hpp index 7852b7b..2c7089a 100644 --- a/src/utils/utils.hpp +++ b/src/utils/utils.hpp @@ -25,6 +25,9 @@ #elif defined(_MSC_VER) #define NOMINMAX #include +#ifdef ERROR +#undef ERROR +#endif #endif namespace Qiskit { diff --git a/test/test_estimator.cpp b/test/test_estimator.cpp index f4fe8cc..3ff9305 100644 --- a/test/test_estimator.cpp +++ b/test/test_estimator.cpp @@ -29,6 +29,11 @@ #include "providers/qrmi_estimator_payload.hpp" #include "quantum_info/sparse_observable.hpp" +// Windows headers can define ERROR after jobstatus.hpp has already been parsed. +#ifdef ERROR +#undef ERROR +#endif + using namespace Qiskit; using namespace Qiskit::circuit; using namespace Qiskit::primitives; @@ -137,11 +142,11 @@ class MockEstimatorBackend : public BackendV2 { } }; -static nlohmann::ordered_json estimator_result(double ev, double std, int shots, double target_precision) +static nlohmann::ordered_json estimator_result(double ev, double stddev, int shots, double target_precision) { nlohmann::ordered_json result; result["data"]["evs"] = ev; - result["data"]["stds"] = std; + result["data"]["stds"] = stddev; result["metadata"]["shots"] = shots; result["metadata"]["target_precision"] = target_precision; return result;