diff --git a/README.md b/README.md index ef34d34..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 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 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. 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 new file mode 100644 index 0000000..5e5fde0 --- /dev/null +++ b/releasenotes/notes/add-backend-estimator-v2-145.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Add ``BackendEstimatorV2``, ``EstimatorPub``, and ``BackendEstimatorJob`` for + 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 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..0a25b91 --- /dev/null +++ b/samples/estimator_test.cpp @@ -0,0 +1,78 @@ +/* +# 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 via QRMI Runtime V2 + +#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::primitives; +using namespace Qiskit::service; +using namespace Qiskit::compiler; +using namespace Qiskit::quantum_info; + +using Estimator = BackendEstimatorV2; + +int main() +{ + int num_qubits = 2; + QuantumCircuit circ(num_qubits, 0); + + circ.h(0); + circ.cx(0, 1); + + auto service = QiskitRuntimeService(); + auto backend = service.backend("ibm_torino"); + auto estimator = Estimator(backend, 0.02); + + auto transpiled_circ = transpile(circ, backend); + + 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, observable, 0.02)}); + if (job == nullptr) { + return -1; + } + + auto result = job->result(); + auto pub_result = result[0]; + + 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 new file mode 100644 index 0000000..b7c4dee --- /dev/null +++ b/src/primitives/backend_estimator_job.hpp @@ -0,0 +1,151 @@ +/* +# 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__ + +#include +#include +#include +#include +#include + +#include "primitives/containers/estimator_result.hpp" +#include "providers/job.hpp" +#include "providers/jobstatus.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: + 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(); + } + } + + const std::vector& pubs(void) const + { + return pubs_; + } + + providers::JobStatus status(void) + { + if (!job_) { + return providers::JobStatus::ERROR; + } + return job_->status(); + } + + bool running(void) + { + return status() == providers::JobStatus::RUNNING; + } + + bool queued(void) + { + return status() == providers::JobStatus::QUEUED; + } + + bool done(void) + { + return status() == providers::JobStatus::DONE; + } + + bool cancelled(void) + { + return status() == providers::JobStatus::CANCELLED; + } + + bool in_final_state(void) + { + return is_final_state(status()); + } + + bool cancel(void) + { + return false; + } + + 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; + } + std::this_thread::sleep_for(std::chrono::seconds(1)); + } + + 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..55d15c8 --- /dev/null +++ b/src/primitives/backend_estimator_v2.hpp @@ -0,0 +1,99 @@ +/* +# 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 +#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: + double default_precision_; + providers::BackendV2& backend_; + +public: + 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."); + } + } + + const providers::BackendV2& backend(void) const + { + return backend_; + } + + double default_precision(void) const + { + return default_precision_; + } + + std::shared_ptr run(std::vector 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; + } +}; + +} // namespace primitives +} // namespace Qiskit + +#endif //__qiskitcpp_primitives_backend_estimator_v2_def_hpp__ 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 new file mode 100644 index 0000000..36b7ad2 --- /dev/null +++ b/src/primitives/containers/estimator_pub.hpp @@ -0,0 +1,373 @@ +/* +# 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 +#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 observables. +class EstimatorPub { +protected: + circuit::QuantumCircuit circuit_; + nlohmann::ordered_json observables_; + nlohmann::ordered_json parameter_values_; + double precision_ = 0.0; + bool has_precision_ = false; + +public: + 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); + } + + EstimatorPub(const circuit::QuantumCircuit& circ, const nlohmann::ordered_json& observables) + { + circuit_ = circ; + observables_ = observables; + 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); + } + + EstimatorPub(const EstimatorPub& src) + { + circuit_ = src.circuit_; + observables_ = src.observables_; + parameter_values_ = src.parameter_values_; + precision_ = src.precision_; + has_precision_ = src.has_precision_; + } + + ~EstimatorPub() {} + + const circuit::QuantumCircuit& circuit(void) const + { + return circuit_; + } + + const nlohmann::ordered_json& observables(void) const + { + return observables_; + } + + 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 + { + 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); + } + + 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 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..7c5962e --- /dev/null +++ b/src/primitives/containers/estimator_pub_result.hpp @@ -0,0 +1,230 @@ +/* +# 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 +#include +#include +#include + +#include + +#include "primitives/containers/estimator_pub.hpp" +#include "utils/types.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: + EstimatorPubResult() + { + metadata_ = nlohmann::ordered_json::object(); + } + + 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_; + 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 + { + return pub_; + } + + void set_pub(const EstimatorPub& pub) + { + pub_ = pub; + has_pub_ = true; + } + + 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/primitives/containers/estimator_result.hpp b/src/primitives/containers/estimator_result.hpp new file mode 100644 index 0000000..df9b1c3 --- /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 EstimatorPrimitiveResult +/// @brief A container for multiple estimator pub results. +class EstimatorPrimitiveResult { +protected: + std::vector pub_results_; +public: + EstimatorPrimitiveResult() {} + + 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__ 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; +}