From e1b100880b9fe249fc074294d8ec044eeef71225 Mon Sep 17 00:00:00 2001 From: rosspeili Date: Thu, 4 Jun 2026 14:42:59 +0300 Subject: [PATCH 1/2] Add BackendEstimatorV2 interface for issue #145 --- README.md | 4 +- .../notes/add-backend-estimator-v2-145.yaml | 5 + samples/CMakeLists.txt | 1 + samples/estimator_test.cpp | 72 ++++++ src/primitives/backend_estimator_job.hpp | 219 ++++++++++++++++++ src/primitives/backend_estimator_v2.hpp | 63 +++++ src/primitives/containers/estimator_pub.hpp | 101 ++++++++ .../containers/estimator_pub_result.hpp | 78 +++++++ .../containers/estimator_result.hpp | 67 ++++++ 9 files changed, 608 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/add-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_pub.hpp create mode 100644 src/primitives/containers/estimator_pub_result.hpp create mode 100644 src/primitives/containers/estimator_result.hpp diff --git a/README.md b/README.md index ef34d34..ea0ed7f 100644 --- a/README.md +++ b/README.md @@ -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, estimator, or transpiler example, you will need one of qiskit-ibm-runtime C or QRMI or SQC. Then example can be built by setting `QISKIT_IBM_RUNTIME_C_ROOT` or `QRMI_ROOT` or `SQC_ROOT` to cmake. @@ -109,7 +109,7 @@ $ cmake -DQISKIT_ROOT=Path_to_qiskit -DQISKIT_IBM_RUNTIME_C_ROOT="path to qiskit $ make ``` -To run sampler example, set your account information in `$HOME/.qiskit/qiskit-ibm.json` (see https://github.com/Qiskit/qiskit-ibm-runtime?tab=readme-ov-file#save-your-account-on-disk) or setting following environment variables to access Quantum hardware. +To run sampler or estimator examples, set your account information in `$HOME/.qiskit/qiskit-ibm.json` (see https://github.com/Qiskit/qiskit-ibm-runtime?tab=readme-ov-file#save-your-account-on-disk) or setting following environment variables to access Quantum hardware. ``` QISKIT_IBM_TOKEN= diff --git a/releasenotes/notes/add-backend-estimator-v2-145.yaml b/releasenotes/notes/add-backend-estimator-v2-145.yaml new file mode 100644 index 0000000..0280aad --- /dev/null +++ b/releasenotes/notes/add-backend-estimator-v2-145.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add ``BackendEstimatorV2``, ``EstimatorPub``, and ``BackendEstimatorJob`` for + Pauli expectation-value estimation via the existing sampler interface. diff --git a/samples/CMakeLists.txt b/samples/CMakeLists.txt index 8d49a60..3c560f9 100644 --- a/samples/CMakeLists.txt +++ b/samples/CMakeLists.txt @@ -106,5 +106,6 @@ add_application(parameterized_circuit_test parameterized_circuit_test.cpp) if(QRMI_ROOT OR QISKIT_IBM_RUNTIME_C_ROOT OR SQC_ROOT) add_application(sampler_test sampler_test.cpp) + add_application(estimator_test estimator_test.cpp) add_application(transpile_test transpile_test.cpp) endif() diff --git a/samples/estimator_test.cpp b/samples/estimator_test.cpp new file mode 100644 index 0000000..6fcadbb --- /dev/null +++ b/samples/estimator_test.cpp @@ -0,0 +1,72 @@ +/* +# This code is part of Qiskit. +# +# (C) Copyright IBM 2024. +# +# 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 "circuit/quantumcircuit.hpp" +#include "primitives/backend_estimator_v2.hpp" +#ifdef QRMI_ROOT +#include "service/qiskit_runtime_service_qrmi.hpp" +#else +#include "service/qiskit_runtime_service_c.hpp" +#endif + +#include "compiler/transpiler.hpp" + +using namespace Qiskit; +using namespace Qiskit::circuit; +using namespace Qiskit::primitives; +using namespace Qiskit::service; +using namespace Qiskit::compiler; + +using Estimator = BackendEstimatorV2; + +int main() +{ + int num_qubits = 2; + auto qreg = QuantumRegister(num_qubits); + auto creg = ClassicalRegister(num_qubits, std::string("meas")); + QuantumCircuit circ(std::vector({qreg}), + std::vector({creg})); + + circ.h(0); + circ.cx(0, 1); + circ.measure(qreg, creg); + + auto service = QiskitRuntimeService(); + auto backend = service.backend("ibm_torino"); + auto estimator = Estimator(backend, 1000); + + auto transpiled_circ = transpile(circ, backend); + + std::vector> observables = { + {"ZZ", 1.0}, + {"XI", 0.5}, + }; + + auto job = estimator.run({EstimatorPub(transpiled_circ, observables, 1000)}); + if (job == nullptr) { + return -1; + } + + auto result = job->result(); + auto pub_result = result[0]; + std::cout << "EV = " << pub_result.ev() + << " +/- " << pub_result.stddev() << std::endl; + + return 0; +} diff --git a/src/primitives/backend_estimator_job.hpp b/src/primitives/backend_estimator_job.hpp new file mode 100644 index 0000000..e3ba944 --- /dev/null +++ b/src/primitives/backend_estimator_job.hpp @@ -0,0 +1,219 @@ +/* +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017, 2024. +# +# 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__ + +#define _USE_MATH_DEFINES +#include +#include +#include +#include + +#include "compiler/transpiler.hpp" +#include "primitives/backend_sampler_v2.hpp" +#include "primitives/containers/estimator_pub.hpp" +#include "primitives/containers/estimator_result.hpp" +#include "primitives/containers/sampler_pub.hpp" +#include "providers/backend.hpp" +#include "providers/jobstatus.hpp" + +namespace Qiskit { +namespace primitives { + +namespace { + +int bit_value_from_string(const std::string& bitstring, uint_t qubit, uint_t num_qubits) +{ + uint_t pos = num_qubits - 1 - qubit; + return bitstring[pos] == '1' ? 1 : 0; +} + +double pauli_eigenvalue_from_bitstring(const std::string& pauli, const std::string& bitstring) +{ + double value = 1.0; + uint_t n = pauli.size(); + + for (uint_t q = 0; q < n; q++) { + char p = pauli[n - 1 - q]; + + if (p == 'I') { + continue; + } + + int bit = bit_value_from_string(bitstring, q, n); + value *= (bit == 0) ? 1.0 : -1.0; + } + + return value; +} + +void append_basis_rotation(circuit::QuantumCircuit& circ, const std::string& pauli) +{ + uint_t n = pauli.size(); + + for (uint_t q = 0; q < n; q++) { + char p = pauli[n - 1 - q]; + + if (p == 'X') { + circ.h(q); + } else if (p == 'Y') { + circ.rz(-M_PI / 2.0, q); + circ.h(q); + } + } +} + +void append_measurements(circuit::QuantumCircuit& circ, uint_t num_qubits) +{ + for (uint_t q = 0; q < num_qubits; q++) { + circ.measure(q, q); + } +} + +EstimatorPubResult estimate_pub(providers::BackendV2& backend, + uint_t default_shots, + EstimatorPub& pub) +{ + BackendSamplerV2 sampler(backend, default_shots); + double total_ev = 0.0; + double variance_accum = 0.0; + uint_t shots = pub.shots() > 0 ? pub.shots() : default_shots; + + for (const auto& term : pub.observables()) { + const std::string& pauli = term.first; + double coeff = term.second; + uint_t num_qubits = pauli.size(); + + auto meas_circ = pub.circuit().copy(); + append_basis_rotation(meas_circ, pauli); + + if (meas_circ.get_measure_map().empty()) { + append_measurements(meas_circ, num_qubits); + } + + auto isa_circ = compiler::transpile(meas_circ, backend); + SamplerPub sampler_pub(isa_circ, shots); + + auto job = sampler.run({sampler_pub}); + if (job == nullptr) { + continue; + } + + auto primitive_result = job->result(); + auto pub_result = primitive_result[0]; + auto bits = pub_result.data().get_bitstrings(); + + if (bits.empty()) { + continue; + } + + double mean = 0.0; + for (const auto& b : bits) { + mean += pauli_eigenvalue_from_bitstring(pauli, b); + } + mean /= static_cast(bits.size()); + + total_ev += coeff * mean; + variance_accum += coeff * coeff * (1.0 - mean * mean) / static_cast(bits.size()); + } + + EstimatorPubResult result(pub); + result.set_ev(total_ev); + result.set_stddev(std::sqrt(variance_accum)); + return result; +} + +} // namespace + +/// @class BackendEstimatorJob +/// @brief Job class for Backend Estimator primitive. +class BackendEstimatorJob { +protected: + providers::BackendV2& backend_; + uint_t shots_; + std::vector pubs_; + EstimatorResult result_; + bool finished_ = false; +public: + BackendEstimatorJob(providers::BackendV2& backend, + uint_t shots, + std::vector& pubs) + : backend_(backend), shots_(shots), pubs_(pubs) + { + } + + BackendEstimatorJob(const BackendEstimatorJob& other) + : backend_(other.backend_), + shots_(other.shots_), + pubs_(other.pubs_), + result_(other.result_), + finished_(other.finished_) + { + } + + const std::vector& pubs(void) const + { + return pubs_; + } + + providers::JobStatus status(void) + { + return finished_ ? providers::JobStatus::DONE : providers::JobStatus::RUNNING; + } + + bool running(void) + { + return !finished_; + } + + bool done(void) + { + return finished_; + } + + bool cancelled(void) + { + return false; + } + + bool in_final_state(void) + { + return finished_; + } + + bool cancel(void) + { + return false; + } + + EstimatorResult result(void) + { + if (!finished_) { + result_.allocate(pubs_.size()); + for (uint_t i = 0; i < pubs_.size(); i++) { + result_[i] = estimate_pub(backend_, shots_, pubs_[i]); + } + finished_ = true; + } + return result_; + } +}; + +} // 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..db2ac18 --- /dev/null +++ b/src/primitives/backend_estimator_v2.hpp @@ -0,0 +1,63 @@ +/* +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017, 2024. +# +# 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_v2_def_hpp__ +#define __qiskitcpp_primitives_backend_estimator_v2_def_hpp__ + +#include +#include + +#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: + uint_t shots_; + providers::BackendV2& backend_; +public: + /// @brief Create a new BackendEstimatorV2 + /// @param backend The backend to run circuits on + /// @param default_shots The default shots + BackendEstimatorV2(providers::BackendV2& backend, uint_t shots = 1024) + : shots_(shots), backend_(backend) + { + } + + /// @brief Return reference to backend object + const providers::BackendV2& backend(void) const + { + return backend_; + } + + /// @brief Run and collect expectation values from each pub. + /// @param pubs An iterable of estimator pub-like objects. + /// @return BackendEstimatorJob + std::shared_ptr run(std::vector pubs) + { + return std::make_shared(backend_, shots_, pubs); + } +}; + +} // namespace primitives +} // namespace Qiskit + +#endif //__qiskitcpp_primitives_backend_estimator_v2_def_hpp__ diff --git a/src/primitives/containers/estimator_pub.hpp b/src/primitives/containers/estimator_pub.hpp new file mode 100644 index 0000000..a9610d5 --- /dev/null +++ b/src/primitives/containers/estimator_pub.hpp @@ -0,0 +1,101 @@ +/* +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017, 2024. +# +# 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 "circuit/quantumcircuit.hpp" + +namespace Qiskit { +namespace primitives { + +/// @class EstimatorPub +/// @brief Estimator Pub (Primitive Unified Bloc) with an ISA circuit and Pauli observables. +class EstimatorPub { +protected: + circuit::QuantumCircuit circuit_; + std::vector> observables_; + uint_t shots_ = 0; +public: + /// @brief Create a new EstimatorPub + EstimatorPub() {} + + /// @brief Create a new EstimatorPub + /// @param circ an ISA QuantumCircuit + /// @param observables Pauli-string observables as (label, coefficient) pairs + /// @param shots The total number of shots for this estimator pub + EstimatorPub(circuit::QuantumCircuit& circ, + std::vector> observables, + uint_t shots = 0) + { + circuit_ = circ; + observables_ = observables; + shots_ = shots; + } + + /// @brief Create a new EstimatorPub as a copy of src. + /// @param src an EstimatorPub + EstimatorPub(const EstimatorPub& src) + { + circuit_ = src.circuit_; + observables_ = src.observables_; + shots_ = src.shots_; + } + + ~EstimatorPub() {} + + /// @brief Return the QuantumCircuit for this estimator pub + const circuit::QuantumCircuit& circuit(void) const + { + return circuit_; + } + + /// @brief Return Pauli observables for this estimator pub + const std::vector>& observables(void) const + { + return observables_; + } + + /// @brief Return the total number of shots + uint_t shots(void) const + { + return shots_; + } + + /// @brief Return a JSON format of this estimator pub + nlohmann::ordered_json to_json(void) + { + nlohmann::ordered_json obs = nlohmann::json::array(); + for (const auto& term : observables_) { + obs.push_back(nlohmann::json::array({term.first, term.second})); + } + + if (shots_ > 0) { + return nlohmann::json::array({circuit_.to_qasm3(), obs, shots_}); + } + return nlohmann::json::array({circuit_.to_qasm3(), obs}); + } +}; + +} // 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..e8f1290 --- /dev/null +++ b/src/primitives/containers/estimator_pub_result.hpp @@ -0,0 +1,78 @@ +/* +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017, 2024. +# +# 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 "primitives/containers/estimator_pub.hpp" + +namespace Qiskit { +namespace primitives { + +/// @class EstimatorPubResult +/// @brief Result of Estimator Pub (Primitive Unified Bloc). +class EstimatorPubResult { +protected: + EstimatorPub pub_; + double ev_ = 0.0; + double stddev_ = 0.0; +public: + EstimatorPubResult() {} + + EstimatorPubResult(const EstimatorPub& pub) : pub_(pub) {} + + EstimatorPubResult(const EstimatorPubResult& src) + { + pub_ = src.pub_; + ev_ = src.ev_; + stddev_ = src.stddev_; + } + + const EstimatorPub& pub(void) const + { + return pub_; + } + + void set_pub(const EstimatorPub& pub) + { + pub_ = pub; + } + + double ev(void) const + { + return ev_; + } + + double stddev(void) const + { + return stddev_; + } + + void set_ev(double ev) + { + ev_ = ev; + } + + void set_stddev(double stddev) + { + stddev_ = stddev; + } +}; + +} // namespace primitives +} // namespace Qiskit + +#endif //__qiskitcpp_primitives_estimator_pub_result_hpp__ diff --git a/src/primitives/containers/estimator_result.hpp b/src/primitives/containers/estimator_result.hpp new file mode 100644 index 0000000..fc423c2 --- /dev/null +++ b/src/primitives/containers/estimator_result.hpp @@ -0,0 +1,67 @@ +/* +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017, 2024. +# +# 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 "primitives/containers/estimator_pub.hpp" +#include "primitives/containers/estimator_pub_result.hpp" + +namespace Qiskit { +namespace primitives { + +/// @class EstimatorResult +/// @brief A container for multiple estimator pub results. +class EstimatorResult { +protected: + std::vector pub_results_; +public: + EstimatorResult() {} + + void allocate(uint_t num_results) + { + pub_results_.resize(num_results); + } + + uint_t size(void) const + { + return pub_results_.size(); + } + + EstimatorPubResult& operator[](uint_t i) + { + return pub_results_[i]; + } + + const EstimatorPubResult& operator[](uint_t i) const + { + return pub_results_[i]; + } + + 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__ From b699cfe3fb0b3ca0005bdbfa0761c2856c7e85a7 Mon Sep 17 00:00:00 2001 From: rosspeili Date: Tue, 9 Jun 2026 14:10:49 +0300 Subject: [PATCH 2/2] Align estimator with SparseObservable and QRMI Runtime V2 Replace the sampler-decomposition approach with native Runtime Estimator V2 via QRMI (program_id=estimator), SparseObservable pub support, apply_layout, and mock-backend unit tests. --- README.md | 4 +- .../notes/add-backend-estimator-v2-145.yaml | 3 +- samples/CMakeLists.txt | 5 +- samples/estimator_test.cpp | 46 ++- src/circuit/quantumcircuit_def.hpp | 12 + src/primitives/backend_estimator_job.hpp | 208 ++++------ src/primitives/backend_estimator_v2.hpp | 60 ++- src/primitives/backend_sampler_job.hpp | 11 +- src/primitives/containers/estimator_pub.hpp | 332 ++++++++++++++-- .../containers/estimator_pub_result.hpp | 184 ++++++++- .../containers/estimator_result.hpp | 6 +- src/providers/backend.hpp | 10 + src/providers/job.hpp | 10 + src/providers/qrmi_backend.hpp | 25 ++ src/providers/qrmi_estimator_payload.hpp | 77 ++++ src/providers/qrmi_job.hpp | 27 +- src/quantum_info/sparse_observable.hpp | 30 +- test/test_estimator.cpp | 356 ++++++++++++++++++ 18 files changed, 1165 insertions(+), 241 deletions(-) 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 ea0ed7f..2608e2a 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ $ cmake -DQISKIT_ROOT=Path_to_qiskit .. $ make ``` -If you want to build sampler, estimator, 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. @@ -109,7 +109,7 @@ $ cmake -DQISKIT_ROOT=Path_to_qiskit -DQISKIT_IBM_RUNTIME_C_ROOT="path to qiskit $ make ``` -To run sampler or estimator examples, set your account information in `$HOME/.qiskit/qiskit-ibm.json` (see https://github.com/Qiskit/qiskit-ibm-runtime?tab=readme-ov-file#save-your-account-on-disk) or setting following environment variables to access Quantum hardware. +To run sampler or estimator examples, set your account information in `$HOME/.qiskit/qiskit-ibm.json` (see https://github.com/Qiskit/qiskit-ibm-runtime?tab=readme-ov-file#save-your-account-on-disk) or setting following environment variables to access Quantum hardware. For the estimator sample, build with `-DQRMI_ROOT=...`. ``` QISKIT_IBM_TOKEN= diff --git a/releasenotes/notes/add-backend-estimator-v2-145.yaml b/releasenotes/notes/add-backend-estimator-v2-145.yaml index 0280aad..5e5fde0 100644 --- a/releasenotes/notes/add-backend-estimator-v2-145.yaml +++ b/releasenotes/notes/add-backend-estimator-v2-145.yaml @@ -2,4 +2,5 @@ features: - | Add ``BackendEstimatorV2``, ``EstimatorPub``, and ``BackendEstimatorJob`` for - Pauli expectation-value estimation via the existing sampler interface. + IBM Runtime Estimator V2 via QRMI. ``EstimatorPub`` accepts ``SparseObservable`` + inputs, and ``QRMIBackend`` submits native ``estimator`` program jobs on IQP. diff --git a/samples/CMakeLists.txt b/samples/CMakeLists.txt index 3c560f9..9285abf 100644 --- a/samples/CMakeLists.txt +++ b/samples/CMakeLists.txt @@ -106,6 +106,9 @@ add_application(parameterized_circuit_test parameterized_circuit_test.cpp) if(QRMI_ROOT OR QISKIT_IBM_RUNTIME_C_ROOT OR SQC_ROOT) add_application(sampler_test sampler_test.cpp) - add_application(estimator_test estimator_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 index 6fcadbb..0a25b91 100644 --- a/samples/estimator_test.cpp +++ b/samples/estimator_test.cpp @@ -12,61 +12,67 @@ # that they have been altered from the originals. */ -// Test program for estimator +// Test program for estimator via QRMI Runtime V2 #include #include #include "circuit/quantumcircuit.hpp" +#include "compiler/transpiler.hpp" #include "primitives/backend_estimator_v2.hpp" -#ifdef QRMI_ROOT +#include "quantum_info/sparse_observable.hpp" #include "service/qiskit_runtime_service_qrmi.hpp" -#else -#include "service/qiskit_runtime_service_c.hpp" -#endif - -#include "compiler/transpiler.hpp" using namespace Qiskit; using namespace Qiskit::circuit; using namespace Qiskit::primitives; using namespace Qiskit::service; using namespace Qiskit::compiler; +using namespace Qiskit::quantum_info; using Estimator = BackendEstimatorV2; int main() { int num_qubits = 2; - auto qreg = QuantumRegister(num_qubits); - auto creg = ClassicalRegister(num_qubits, std::string("meas")); - QuantumCircuit circ(std::vector({qreg}), - std::vector({creg})); + QuantumCircuit circ(num_qubits, 0); circ.h(0); circ.cx(0, 1); - circ.measure(qreg, creg); auto service = QiskitRuntimeService(); auto backend = service.backend("ibm_torino"); - auto estimator = Estimator(backend, 1000); + auto estimator = Estimator(backend, 0.02); auto transpiled_circ = transpile(circ, backend); - std::vector> observables = { - {"ZZ", 1.0}, - {"XI", 0.5}, - }; + std::vector>> terms; + terms.push_back(std::make_pair(std::string("ZZ"), std::complex(1.0))); + terms.push_back(std::make_pair(std::string("XX"), std::complex(0.5))); + auto observable = SparseObservable::from_list(terms, num_qubits); + observable = observable.apply_layout(transpiled_circ.get_qubit_map()); - auto job = estimator.run({EstimatorPub(transpiled_circ, observables, 1000)}); + auto job = estimator.run({EstimatorPub(transpiled_circ, observable, 0.02)}); if (job == nullptr) { return -1; } auto result = job->result(); auto pub_result = result[0]; - std::cout << "EV = " << pub_result.ev() - << " +/- " << pub_result.stddev() << std::endl; + + std::cout << "===== estimator result for pub[0] =====" << std::endl; + if (!pub_result.evs().empty()) { + std::cout << "evs: " << pub_result.evs()[0] << std::endl; + } + if (!pub_result.stds().empty()) { + std::cout << "stds: " << pub_result.stds()[0] << std::endl; + } + if (pub_result.metadata().contains("shots")) { + std::cout << "Shots: " << pub_result.metadata()["shots"] << std::endl; + } + if (pub_result.metadata().contains("target_precision")) { + std::cout << "Target precision: " << pub_result.metadata()["target_precision"] << std::endl; + } return 0; } diff --git a/src/circuit/quantumcircuit_def.hpp b/src/circuit/quantumcircuit_def.hpp index d8add04..28ce004 100644 --- a/src/circuit/quantumcircuit_def.hpp +++ b/src/circuit/quantumcircuit_def.hpp @@ -322,6 +322,18 @@ class QuantumCircuit return measure_map_; } + /// @brief get qubits to be measured + const std::vector> &get_measure_map(void) const + { + return measure_map_; + } + + /// @brief Return whether this circuit contains measurement operations. + bool has_measurements(void) const + { + return !measure_map_.empty(); + } + /// @brief set global phase /// @param phase global phase value void global_phase(const double phase) diff --git a/src/primitives/backend_estimator_job.hpp b/src/primitives/backend_estimator_job.hpp index e3ba944..b7c4dee 100644 --- a/src/primitives/backend_estimator_job.hpp +++ b/src/primitives/backend_estimator_job.hpp @@ -17,152 +17,44 @@ #ifndef __qiskitcpp_primitives_backend_estimator_job_def_hpp__ #define __qiskitcpp_primitives_backend_estimator_job_def_hpp__ -#define _USE_MATH_DEFINES -#include +#include #include -#include +#include +#include #include -#include "compiler/transpiler.hpp" -#include "primitives/backend_sampler_v2.hpp" -#include "primitives/containers/estimator_pub.hpp" #include "primitives/containers/estimator_result.hpp" -#include "primitives/containers/sampler_pub.hpp" -#include "providers/backend.hpp" +#include "providers/job.hpp" #include "providers/jobstatus.hpp" namespace Qiskit { namespace primitives { -namespace { - -int bit_value_from_string(const std::string& bitstring, uint_t qubit, uint_t num_qubits) -{ - uint_t pos = num_qubits - 1 - qubit; - return bitstring[pos] == '1' ? 1 : 0; -} - -double pauli_eigenvalue_from_bitstring(const std::string& pauli, const std::string& bitstring) -{ - double value = 1.0; - uint_t n = pauli.size(); - - for (uint_t q = 0; q < n; q++) { - char p = pauli[n - 1 - q]; - - if (p == 'I') { - continue; - } - - int bit = bit_value_from_string(bitstring, q, n); - value *= (bit == 0) ? 1.0 : -1.0; - } - - return value; -} - -void append_basis_rotation(circuit::QuantumCircuit& circ, const std::string& pauli) -{ - uint_t n = pauli.size(); - - for (uint_t q = 0; q < n; q++) { - char p = pauli[n - 1 - q]; - - if (p == 'X') { - circ.h(q); - } else if (p == 'Y') { - circ.rz(-M_PI / 2.0, q); - circ.h(q); - } - } -} - -void append_measurements(circuit::QuantumCircuit& circ, uint_t num_qubits) -{ - for (uint_t q = 0; q < num_qubits; q++) { - circ.measure(q, q); - } -} - -EstimatorPubResult estimate_pub(providers::BackendV2& backend, - uint_t default_shots, - EstimatorPub& pub) -{ - BackendSamplerV2 sampler(backend, default_shots); - double total_ev = 0.0; - double variance_accum = 0.0; - uint_t shots = pub.shots() > 0 ? pub.shots() : default_shots; - - for (const auto& term : pub.observables()) { - const std::string& pauli = term.first; - double coeff = term.second; - uint_t num_qubits = pauli.size(); - - auto meas_circ = pub.circuit().copy(); - append_basis_rotation(meas_circ, pauli); - - if (meas_circ.get_measure_map().empty()) { - append_measurements(meas_circ, num_qubits); - } - - auto isa_circ = compiler::transpile(meas_circ, backend); - SamplerPub sampler_pub(isa_circ, shots); - - auto job = sampler.run({sampler_pub}); - if (job == nullptr) { - continue; - } - - auto primitive_result = job->result(); - auto pub_result = primitive_result[0]; - auto bits = pub_result.data().get_bitstrings(); - - if (bits.empty()) { - continue; - } - - double mean = 0.0; - for (const auto& b : bits) { - mean += pauli_eigenvalue_from_bitstring(pauli, b); - } - mean /= static_cast(bits.size()); - - total_ev += coeff * mean; - variance_accum += coeff * coeff * (1.0 - mean * mean) / static_cast(bits.size()); - } - - EstimatorPubResult result(pub); - result.set_ev(total_ev); - result.set_stddev(std::sqrt(variance_accum)); - return result; -} - -} // namespace - /// @class BackendEstimatorJob /// @brief Job class for Backend Estimator primitive. class BackendEstimatorJob { protected: - providers::BackendV2& backend_; - uint_t shots_; + std::shared_ptr job_ = nullptr; std::vector pubs_; - EstimatorResult result_; - bool finished_ = false; + public: - BackendEstimatorJob(providers::BackendV2& backend, - uint_t shots, - std::vector& pubs) - : backend_(backend), shots_(shots), pubs_(pubs) + BackendEstimatorJob(std::shared_ptr job, std::vector& pubs) { + job_ = job; + pubs_ = pubs; } BackendEstimatorJob(const BackendEstimatorJob& other) - : backend_(other.backend_), - shots_(other.shots_), - pubs_(other.pubs_), - result_(other.result_), - finished_(other.finished_) { + job_ = other.job_; + pubs_ = other.pubs_; + } + + ~BackendEstimatorJob() + { + if (job_) { + job_.reset(); + } } const std::vector& pubs(void) const @@ -172,27 +64,35 @@ class BackendEstimatorJob { providers::JobStatus status(void) { - return finished_ ? providers::JobStatus::DONE : providers::JobStatus::RUNNING; + if (!job_) { + return providers::JobStatus::ERROR; + } + return job_->status(); } bool running(void) { - return !finished_; + return status() == providers::JobStatus::RUNNING; + } + + bool queued(void) + { + return status() == providers::JobStatus::QUEUED; } bool done(void) { - return finished_; + return status() == providers::JobStatus::DONE; } bool cancelled(void) { - return false; + return status() == providers::JobStatus::CANCELLED; } bool in_final_state(void) { - return finished_; + return is_final_state(status()); } bool cancel(void) @@ -200,16 +100,48 @@ class BackendEstimatorJob { return false; } - EstimatorResult result(void) + EstimatorPrimitiveResult result(void) { - if (!finished_) { - result_.allocate(pubs_.size()); - for (uint_t i = 0; i < pubs_.size(); i++) { - result_[i] = estimate_pub(backend_, shots_, pubs_[i]); + 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; } - finished_ = true; + std::this_thread::sleep_for(std::chrono::seconds(1)); + } + + if (st != providers::JobStatus::DONE) { + throw std::runtime_error("BackendEstimatorJob did not finish successfully."); } - return result_; + + 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; } }; diff --git a/src/primitives/backend_estimator_v2.hpp b/src/primitives/backend_estimator_v2.hpp index db2ac18..55d15c8 100644 --- a/src/primitives/backend_estimator_v2.hpp +++ b/src/primitives/backend_estimator_v2.hpp @@ -17,7 +17,9 @@ #ifndef __qiskitcpp_primitives_backend_estimator_v2_def_hpp__ #define __qiskitcpp_primitives_backend_estimator_v2_def_hpp__ +#include #include +#include #include #include "primitives/backend_estimator_job.hpp" @@ -28,32 +30,66 @@ namespace Qiskit { namespace primitives { /// @class BackendEstimatorV2 -/// @brief Implementation of EstimatorV2 on a backend +/// @brief Implementation of EstimatorV2 on a backend. class BackendEstimatorV2 { protected: - uint_t shots_; + double default_precision_; providers::BackendV2& backend_; + public: - /// @brief Create a new BackendEstimatorV2 - /// @param backend The backend to run circuits on - /// @param default_shots The default shots - BackendEstimatorV2(providers::BackendV2& backend, uint_t shots = 1024) - : shots_(shots), backend_(backend) + 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 const providers::BackendV2& backend(void) const { return backend_; } - /// @brief Run and collect expectation values from each pub. - /// @param pubs An iterable of estimator pub-like objects. - /// @return BackendEstimatorJob + double default_precision(void) const + { + return default_precision_; + } + std::shared_ptr run(std::vector pubs) { - return std::make_shared(backend_, shots_, pubs); + return run(pubs, default_precision_); + } + + 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; } }; diff --git a/src/primitives/backend_sampler_job.hpp b/src/primitives/backend_sampler_job.hpp index 1512360..d60bf96 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/base/base_primitive_job.hpp" @@ -117,11 +112,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; diff --git a/src/primitives/containers/estimator_pub.hpp b/src/primitives/containers/estimator_pub.hpp index a9610d5..36b7ad2 100644 --- a/src/primitives/containers/estimator_pub.hpp +++ b/src/primitives/containers/estimator_pub.hpp @@ -17,81 +17,353 @@ #ifndef __qiskitcpp_primitives_estimator_pub_def_hpp__ #define __qiskitcpp_primitives_estimator_pub_def_hpp__ -#include +#include +#include +#include +#include #include #include #include +#include + #include "circuit/quantumcircuit.hpp" +#include "quantum_info/sparse_observable.hpp" +#include "utils/types.hpp" namespace Qiskit { namespace primitives { /// @class EstimatorPub -/// @brief Estimator Pub (Primitive Unified Bloc) with an ISA circuit and Pauli observables. +/// @brief Estimator Pub (Primitive Unified Bloc) with an ISA circuit and observables. class EstimatorPub { protected: circuit::QuantumCircuit circuit_; - std::vector> observables_; - uint_t shots_ = 0; + nlohmann::ordered_json observables_; + nlohmann::ordered_json parameter_values_; + double precision_ = 0.0; + bool has_precision_ = false; + public: - /// @brief Create a new EstimatorPub - EstimatorPub() {} + EstimatorPub() + { + observables_ = nlohmann::ordered_json::object(); + parameter_values_ = nullptr; + } + + EstimatorPub(const circuit::QuantumCircuit& circ, const std::string& observable) + { + circuit_ = circ; + observables_ = observable; + parameter_values_ = nullptr; + require_valid(); + } + + EstimatorPub(const circuit::QuantumCircuit& circ, const std::string& observable, double precision) + : EstimatorPub(circ, observable) + { + set_precision(precision); + } - /// @brief Create a new EstimatorPub - /// @param circ an ISA QuantumCircuit - /// @param observables Pauli-string observables as (label, coefficient) pairs - /// @param shots The total number of shots for this estimator pub - EstimatorPub(circuit::QuantumCircuit& circ, - std::vector> observables, - uint_t shots = 0) + EstimatorPub(const circuit::QuantumCircuit& circ, const nlohmann::ordered_json& observables) { circuit_ = circ; observables_ = observables; - shots_ = shots; + parameter_values_ = nullptr; + require_valid(); + } + + EstimatorPub(const circuit::QuantumCircuit& circ, const nlohmann::ordered_json& observables, double precision) + : EstimatorPub(circ, observables) + { + set_precision(precision); + } + + EstimatorPub(const circuit::QuantumCircuit& circ, const std::vector>& observable) + { + circuit_ = circ; + observables_ = pauli_terms_to_json(observable); + parameter_values_ = nullptr; + require_valid(); + } + + EstimatorPub(const circuit::QuantumCircuit& circ, const std::vector>& observable, double precision) + : EstimatorPub(circ, observable) + { + set_precision(precision); + } + + EstimatorPub(const circuit::QuantumCircuit& circ, const quantum_info::SparseObservable& observable) + { + circuit_ = circ; + observables_ = sparse_observable_to_json(observable); + parameter_values_ = nullptr; + require_valid(); + } + + EstimatorPub(const circuit::QuantumCircuit& circ, const quantum_info::SparseObservable& observable, double precision) + : EstimatorPub(circ, observable) + { + set_precision(precision); + } + + 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(); + } + + 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. - /// @param src an EstimatorPub EstimatorPub(const EstimatorPub& src) { circuit_ = src.circuit_; observables_ = src.observables_; - shots_ = src.shots_; + parameter_values_ = src.parameter_values_; + precision_ = src.precision_; + has_precision_ = src.has_precision_; } ~EstimatorPub() {} - /// @brief Return the QuantumCircuit for this estimator pub const circuit::QuantumCircuit& circuit(void) const { return circuit_; } - /// @brief Return Pauli observables for this estimator pub - const std::vector>& observables(void) const + const nlohmann::ordered_json& observables(void) const { return observables_; } - /// @brief Return the total number of shots - uint_t shots(void) const + const nlohmann::ordered_json& parameter_values(void) const + { + return parameter_values_; + } + + void set_parameter_values(const nlohmann::ordered_json& parameter_values) + { + parameter_values_ = parameter_values; + } + + bool has_precision(void) const + { + return has_precision_; + } + + double precision(void) const + { + return precision_; + } + + 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; + } + + uint_t num_observables(void) const { - return shots_; + if (observables_.is_array()) { + return observables_.size(); + } + return 1; + } + + 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 nlohmann::ordered_json to_json(void) { - nlohmann::ordered_json obs = nlohmann::json::array(); - for (const auto& term : observables_) { - obs.push_back(nlohmann::json::array({term.first, term.second})); + 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 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."); } - if (shots_ > 0) { - return nlohmann::json::array({circuit_.to_qasm3(), obs, shots_}); + 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 nlohmann::json::array({circuit_.to_qasm3(), obs}); + return ret; } }; diff --git a/src/primitives/containers/estimator_pub_result.hpp b/src/primitives/containers/estimator_pub_result.hpp index e8f1290..7c5962e 100644 --- a/src/primitives/containers/estimator_pub_result.hpp +++ b/src/primitives/containers/estimator_pub_result.hpp @@ -17,7 +17,15 @@ #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" +#include "utils/types.hpp" namespace Qiskit { namespace primitives { @@ -26,19 +34,62 @@ namespace primitives { /// @brief Result of Estimator Pub (Primitive Unified Bloc). class EstimatorPubResult { protected: + std::vector evs_; + std::vector stds_; + nlohmann::ordered_json metadata_; EstimatorPub pub_; - double ev_ = 0.0; - double stddev_ = 0.0; + bool has_pub_ = false; + public: - EstimatorPubResult() {} + EstimatorPubResult() + { + metadata_ = nlohmann::ordered_json::object(); + } - EstimatorPubResult(const EstimatorPub& pub) : pub_(pub) {} + EstimatorPubResult(const EstimatorPub& pub) + { + pub_ = pub; + has_pub_ = true; + metadata_ = nlohmann::ordered_json::object(); + } EstimatorPubResult(const EstimatorPubResult& src) { + evs_ = src.evs_; + stds_ = src.stds_; + metadata_ = src.metadata_; pub_ = src.pub_; - ev_ = src.ev_; - stddev_ = src.stddev_; + has_pub_ = src.has_pub_; + } + + const std::vector& evs(void) const + { + return evs_; + } + + std::vector& evs(void) + { + return evs_; + } + + const std::vector& stds(void) const + { + return stds_; + } + + std::vector& stds(void) + { + return stds_; + } + + const nlohmann::ordered_json& metadata(void) const + { + return metadata_; + } + + nlohmann::ordered_json& metadata(void) + { + return metadata_; } const EstimatorPub& pub(void) const @@ -49,26 +100,127 @@ class EstimatorPubResult { void set_pub(const EstimatorPub& pub) { pub_ = pub; + has_pub_ = true; } - double ev(void) const + bool from_json(nlohmann::ordered_json& input) { - return ev_; - } + 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; + } - double stddev(void) const - { - return stddev_; + 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; } - void set_ev(double ev) +protected: + static bool parse_numeric_scalar_or_1d(const nlohmann::ordered_json& value, + std::vector& output, + const std::string& name) { - ev_ = ev; + 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; } - void set_stddev(double stddev) + static bool validate_metadata(const nlohmann::ordered_json& metadata) { - stddev_ = stddev; + 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; } }; diff --git a/src/primitives/containers/estimator_result.hpp b/src/primitives/containers/estimator_result.hpp index fc423c2..df9b1c3 100644 --- a/src/primitives/containers/estimator_result.hpp +++ b/src/primitives/containers/estimator_result.hpp @@ -23,13 +23,13 @@ namespace Qiskit { namespace primitives { -/// @class EstimatorResult +/// @class EstimatorPrimitiveResult /// @brief A container for multiple estimator pub results. -class EstimatorResult { +class EstimatorPrimitiveResult { protected: std::vector pub_results_; public: - EstimatorResult() {} + EstimatorPrimitiveResult() {} void allocate(uint_t num_results) { diff --git a/src/providers/backend.hpp b/src/providers/backend.hpp index 27c4ac6..f2a4bc8 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,15 @@ 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. + /// @param precision The target precision for pubs without an explicit precision. + /// @return Job + virtual std::shared_ptr run(std::vector& pubs, double precision) + { + return nullptr; + } + }; } // namespace providers diff --git a/src/providers/job.hpp b/src/providers/job.hpp index 1a74a78..a6144cd 100644 --- a/src/providers/job.hpp +++ b/src/providers/job.hpp @@ -19,6 +19,7 @@ #include "utils/types.hpp" #include "providers/jobstatus.hpp" +#include "primitives/containers/estimator_pub_result.hpp" #include "primitives/containers/sampler_pub_result.hpp" namespace Qiskit { @@ -47,6 +48,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 diff --git a/src/providers/qrmi_backend.hpp b/src/providers/qrmi_backend.hpp index 7df229f..86824bd 100644 --- a/src/providers/qrmi_backend.hpp +++ b/src/providers/qrmi_backend.hpp @@ -19,6 +19,7 @@ #include "utils/types.hpp" #include "providers/backend.hpp" +#include "providers/qrmi_estimator_payload.hpp" #include "providers/qrmi_job.hpp" #include @@ -124,6 +125,30 @@ class QRMIBackend : public BackendV2 { return std::make_shared(qrmi_, job_id); } + /// @brief Run and estimate expectation values from each pub. + std::shared_ptr run(std::vector& input_pubs, double precision) override + { + auto estimator = QRMIEstimatorPayload::build_estimator_input(input_pubs, precision); + std::string estimator_input = estimator.dump(2); + + QrmiPayload payload; + payload.tag = QRMI_PAYLOAD_QISKIT_PRIMITIVE; + payload.QISKIT_PRIMITIVE.input = (char*)estimator_input.c_str(); + payload.QISKIT_PRIMITIVE.program_id = (char*)QRMIEstimatorPayload::estimator_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 diff --git a/src/providers/qrmi_estimator_payload.hpp b/src/providers/qrmi_estimator_payload.hpp new file mode 100644 index 0000000..c9d6699 --- /dev/null +++ b/src/providers/qrmi_estimator_payload.hpp @@ -0,0 +1,77 @@ +/* +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017, 2024. +# +# 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 Build Runtime V2 estimator payloads for QRMI submission. +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..d096cf7 100644 --- a/src/providers/qrmi_job.hpp +++ b/src/providers/qrmi_job.hpp @@ -21,6 +21,7 @@ #include "utils/types.hpp" +#include "primitives/containers/estimator_pub_result.hpp" #include "primitives/containers/sampler_pub_result.hpp" #include "providers/job.hpp" @@ -113,19 +114,41 @@ class QRMIJob : public Job { if (index >= num_results_) return false; + if (!results_.contains("results") || !results_["results"].is_array()) + return false; + result.from_json(results_["results"][index]); return true; } + bool result(uint_t index, primitives::EstimatorPubResult& result) override + { + if (num_results_ == 0) + read_results(); + + if (index >= num_results_) + return false; + + if (!results_.contains("results") || !results_["results"].is_array()) + return false; + + auto item = results_["results"][index]; + return result.from_json(item); + } + protected: void read_results(void) { + if (num_results_ > 0) + return; + 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); - - num_results_ = results_["results"].size(); + if (results_.contains("results") && results_["results"].is_array()) { + num_results_ = results_["results"].size(); + } qrmi_string_free((char *)result); } qrmi_resource_task_stop(qrmi_.get(), job_id_.c_str()); diff --git a/src/quantum_info/sparse_observable.hpp b/src/quantum_info/sparse_observable.hpp index 0e8895d..a405522 100644 --- a/src/quantum_info/sparse_observable.hpp +++ b/src/quantum_info/sparse_observable.hpp @@ -48,7 +48,7 @@ class SparseObservable SparseObservable(const SparseObservable &other) { - obs_ = qk_obs_copy(other.obs_); + obs_ = other.obs_ ? qk_obs_copy(other.obs_) : nullptr; } ~SparseObservable() @@ -73,7 +73,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); @@ -205,16 +205,18 @@ class SparseObservable } std::vector bit_terms(void) const { - std::vector ret(num_terms()); if (obs_) { + const size_t len = qk_obs_len(obs_); + std::vector ret(len); auto terms = qk_obs_bit_terms(obs_); - for (int i = 0; i < ret.size(); i++) + for (size_t i = 0; i < len; i++) { ret[i] = terms[i]; } + return ret; } - return ret; + return std::vector(); } std::vector> coeffs(void) const { @@ -244,15 +246,31 @@ class SparseObservable } 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 reg_t(); + } + + SparseObservable apply_layout(const reg_t& layout) const + { + if (!obs_) { + return SparseObservable(); + } + SparseObservable ret; + ret.obs_ = qk_obs_copy(obs_); + std::vector layout32(layout.size()); + for (size_t i = 0; i < layout.size(); i++) { + layout32[i] = (uint32_t)layout[i]; } + qk_obs_apply_layout(ret.obs_, layout32.data(), (uint32_t)layout32.size()); return ret; } diff --git a/test/test_estimator.cpp b/test/test_estimator.cpp new file mode 100644 index 0000000..f2a91a7 --- /dev/null +++ b/test/test_estimator.cpp @@ -0,0 +1,356 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2024. +// +// 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 "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 stddev, int shots, double target_precision) +{ + nlohmann::ordered_json result; + result["data"]["evs"] = ev; + result["data"]["stds"] = stddev; + 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 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); + std::vector pubs; + pubs.push_back(EstimatorPub(circ, std::string("Z"))); + pubs.push_back(EstimatorPub(circ, std::string("X"), 0.03125)); + + 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["version"].get() != 2 || + payload["support_qiskit"].get() || + !payload["pubs"].is_array() || + payload["pubs"].size() != 2) { + std::cerr << " estimator QRMI payload test : unexpected top-level shape." << std::endl; + return EqualityError; + } + if (!pubs[0].has_precision() || std::abs(pubs[0].precision() - 0.02) > 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); + std::vector pubs; + pubs.push_back(EstimatorPub(circ, std::string("Z"))); + + try { + QRMIEstimatorPayload::build_estimator_input(pubs, 0.02); + } 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; + } + + std::vector>> terms; + terms.push_back(std::make_pair(std::string("ZZ"), std::complex(1.0))); + terms.push_back(std::make_pair(std::string("XX"), std::complex(0.5))); + auto multi_term = SparseObservable::from_list(terms, 2); + auto multi_pub = EstimatorPub(circ, multi_term); + auto multi_payload = multi_pub.to_json(); + + if (!multi_payload[1].is_object() || + !multi_payload[1].contains("ZZ") || + !multi_payload[1].contains("XX") || + std::abs(multi_payload[1]["ZZ"].get() - 1.0) > 1e-12 || + std::abs(multi_payload[1]["XX"].get() - 0.5) > 1e-12) { + std::cerr << " estimator pub sparse observable test : expected ZZ and XX observables." << std::endl; + return EqualityError; + } + return Ok; +} + +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) { + std::cerr << " estimator job result test : unexpected values." << std::endl; + return EqualityError; + } + return Ok; +} + +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 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_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; +} + +#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_measured_circuit); + num_failed += RUN_TEST(test_estimator_job_result); + num_failed += RUN_TEST(test_estimator_result_rejects_too_many_evs); + num_failed += RUN_TEST(test_estimator_job_rejects_failed_state); + return num_failed; +}