Skip to content

Commit 1c8e1ca

Browse files
GoiBasiadoichanj
andauthored
Issue #145 Implement BackendEstimatorV2 runtime support(with IBM Quantum Hardware test) (#160)
* Implement BackendEstimatorV2 runtime support * add SparseObservable::apply_layout * Avoid Windows header macros in primitive job headers * Avoid Windows ERROR macro collisions2 --------- Co-authored-by: Jun Doi <doichan@jp.ibm.com>
1 parent 91f4b5d commit 1c8e1ca

25 files changed

Lines changed: 2254 additions & 69 deletions

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
44
# Qiskit C++
55

6-
Qiskit C++ provides a modern C++ (version C++ 11 or later) interface of Qiskit for circuit building, (transpilation and returning samples of quantum circuit outputs ... to be added in the future release), as same as Qiskit Python interfaces.
6+
Qiskit C++ provides a modern C++ (version C++ 11 or later) interface of Qiskit for circuit building, transpilation, sampler jobs, and QRMI-backed estimator jobs, as same as Qiskit Python interfaces.
77
This interface is based on Qiskit C-API introduced in Qiskit 2.1.
88

99
## Supported OS
@@ -97,7 +97,7 @@ $ cmake -DQISKIT_ROOT=Path_to_qiskit ..
9797
$ make
9898
```
9999

100-
If you want to build sampler or transpiler example, you will need one of qiskit-ibm-runtime C or QRMI or SQC.
100+
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.
101101

102102
Then example can be built by setting `QISKIT_IBM_RUNTIME_C_ROOT` or `QRMI_ROOT` or `SQC_ROOT` to cmake.
103103

@@ -116,6 +116,8 @@ QISKIT_IBM_TOKEN=<your API key>
116116
QISKIT_IBM_INSTANCE=<your CRN>
117117
```
118118

119+
The estimator interface is available through `primitives::BackendEstimatorV2` and currently requires QRMI or another backend that implements `BackendV2::run(std::vector<EstimatorPub>&, double)`. Estimator PUB circuits must be unmeasured and can use Pauli labels, Pauli-label maps, Pauli term vectors, or `quantum_info::SparseObservable` inputs. Empty observables are rejected before backend submission, and malformed estimator result cardinality is rejected while reading provider results. Estimator job cancellation is not currently supported, so `BackendEstimatorJob::cancel()` returns `false`.
120+
119121
To run the sample example with SQC, you also need to set the library options to the environment variable `SQC_LIBS`, which is automatically set by a provided script (see [here](https://github.com/jhpc-quantum/documents/blob/main/SQC_JHPC_Quantum_user_guide.md)), before running cmake. In SQC 0.10.0, `backend_setup.sh` configures `SQC_LIBS` as follows:
120122

121123
```
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
features:
3+
- |
4+
Added `primitives::BackendEstimatorV2` with `EstimatorPub`,
5+
`BackendEstimatorJob`, and estimator result containers. The QRMI backend
6+
can now submit Runtime V2 `estimator` primitive jobs for unmeasured circuits
7+
and Pauli observables, returning expectation values through
8+
`EstimatorPubResult::evs()`. Empty observables are rejected before backend
9+
submission, and estimator result parsing validates that provider `evs` and
10+
`stds` cardinality matches the submitted PUB observables.

samples/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,3 +108,7 @@ if(QRMI_ROOT OR QISKIT_IBM_RUNTIME_C_ROOT OR SQC_ROOT)
108108
add_application(sampler_test sampler_test.cpp)
109109
add_application(transpile_test transpile_test.cpp)
110110
endif()
111+
112+
if(QRMI_ROOT)
113+
add_application(estimator_test estimator_test.cpp)
114+
endif()

samples/estimator_test.cpp

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
# This code is part of Qiskit.
3+
#
4+
# (C) Copyright IBM 2026.
5+
#
6+
# This code is licensed under the Apache License, Version 2.0. You may
7+
# obtain a copy of this license in the LICENSE.txt file in the root directory
8+
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
9+
#
10+
# Any modifications or derivative works of this code must retain this
11+
# copyright notice, and modified files need to carry a notice indicating
12+
# that they have been altered from the originals.
13+
*/
14+
15+
// Test program for estimator
16+
17+
#include <cstdint>
18+
#include <cstdlib>
19+
#include <complex>
20+
#include <iostream>
21+
#include <stdexcept>
22+
#include <string>
23+
#include <utility>
24+
#include <vector>
25+
26+
#include "circuit/quantumcircuit.hpp"
27+
#include "compiler/transpiler.hpp"
28+
#include "primitives/backend_estimator_v2.hpp"
29+
#include "quantum_info/sparse_observable.hpp"
30+
#include "service/qiskit_runtime_service_qrmi.hpp"
31+
32+
using namespace Qiskit;
33+
using namespace Qiskit::circuit;
34+
using namespace Qiskit::compiler;
35+
using namespace Qiskit::primitives;
36+
using namespace Qiskit::quantum_info;
37+
using namespace Qiskit::service;
38+
39+
using Estimator = BackendEstimatorV2;
40+
41+
int main()
42+
{
43+
QuantumCircuit circ(2, 0);
44+
circ.h(0);
45+
circ.cx(0, 1);
46+
47+
std::vector<std::pair<std::string, std::complex<double>>> terms;
48+
terms.push_back(std::make_pair(std::string("ZZ"), std::complex<double>(1.0, 0.0)));
49+
terms.push_back(std::make_pair(std::string("XX"), std::complex<double>(0.5, 0.0)));
50+
51+
auto service = QiskitRuntimeService();
52+
auto backend = service.backend("ibm_kingston");
53+
auto estimator = Estimator(backend);
54+
auto transpiled_circ = transpile(circ, backend);
55+
auto observable = SparseObservable::from_list(terms);
56+
auto mapped_observable = observable.apply_layout(transpiled_circ.get_qubit_map());
57+
58+
auto job = estimator.run({EstimatorPub(transpiled_circ, mapped_observable)}, 0.02);
59+
if (job == nullptr) {
60+
return -1;
61+
}
62+
63+
auto result = job->result();
64+
auto evs = result[0].evs();
65+
auto stds = result[0].stds();
66+
67+
std::cout << " ===== estimator result for pub[0] =====" << std::endl;
68+
for (uint_t i = 0; i < evs.size(); i++) {
69+
std::cout << "ev[" << i << "] = " << evs[i];
70+
if (i < stds.size()) {
71+
std::cout << ", std[" << i << "] = " << stds[i];
72+
}
73+
std::cout << std::endl;
74+
}
75+
76+
return 0;
77+
}

src/circuit/quantumcircuit_def.hpp

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,20 @@ class QuantumCircuit
322322
return measure_map_;
323323
}
324324

325+
/// @brief get qubits to be measured
326+
/// @return a set of qubits
327+
const std::vector<std::pair<uint_t, uint_t>> &get_measure_map(void) const
328+
{
329+
return measure_map_;
330+
}
331+
332+
/// @brief Return whether this circuit contains measurement operations.
333+
/// @return true if there is at least one measurement operation.
334+
bool has_measurements(void) const
335+
{
336+
return !measure_map_.empty();
337+
}
338+
325339
/// @brief set global phase
326340
/// @param phase global phase value
327341
void global_phase(const double phase)
@@ -1840,10 +1854,25 @@ class QuantumCircuit
18401854

18411855
// Declare registers
18421856
// After transpilation, qubit registers will be mapped to physical registers,
1843-
// so we need to combined them in a single quantum register "q";
1857+
// so we need to combine them in a single quantum register "q" for ordinary
1858+
// circuits. Transpiled circuits should use physical qubit references.
18441859
const std::string qreg_name = "q";
1845-
qasm3 << "qubit[" << num_qubits() << "] " << qreg_name << ";" << std::endl;
1860+
const bool physical_qubits = !qubit_map_.empty();
1861+
auto qubit_ref = [physical_qubits, &qreg_name](uint_t index) -> std::string
1862+
{
1863+
if (physical_qubits) {
1864+
return std::string("$") + std::to_string(index);
1865+
}
1866+
return qreg_name + "[" + std::to_string(index) + "]";
1867+
};
1868+
1869+
if (!physical_qubits) {
1870+
qasm3 << "qubit[" << num_qubits() << "] " << qreg_name << ";" << std::endl;
1871+
}
18461872
for(const auto& creg : cregs_) {
1873+
if (creg.size() == 0) {
1874+
continue;
1875+
}
18471876
qasm3 << "bit[" << creg.size() << "] " << creg.name() << ";" << std::endl;
18481877
}
18491878

@@ -1863,7 +1892,7 @@ class QuantumCircuit
18631892
if (op->num_qubits == op->num_clbits) {
18641893
for (uint_t j = 0; j < op->num_qubits; j++) {
18651894
const auto creg_data = recover_reg_data(op->clbits[j]);
1866-
qasm3 << creg_data.first << "[" << creg_data.second << "] = " << op->name << " " << qreg_name << "[" << op->qubits[j] << "];" << std::endl;
1895+
qasm3 << creg_data.first << "[" << creg_data.second << "] = " << op->name << " " << qubit_ref(op->qubits[j]) << ";" << std::endl;
18671896
}
18681897
}
18691898
} else {
@@ -1886,7 +1915,7 @@ class QuantumCircuit
18861915
if (op->num_qubits > 0) {
18871916
qasm3 << " ";
18881917
for (uint_t j = 0; j < op->num_qubits; j++) {
1889-
qasm3 << qreg_name << "[" << op->qubits[j] << "]";
1918+
qasm3 << qubit_ref(op->qubits[j]);
18901919
if (j != op->num_qubits - 1)
18911920
qasm3 << ", ";
18921921
}
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
/*
2+
# This code is part of Qiskit.
3+
#
4+
# (C) Copyright IBM 2017, 2026.
5+
#
6+
# This code is licensed under the Apache License, Version 2.0. You may
7+
# obtain a copy of this license in the LICENSE.txt file in the root directory
8+
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
9+
#
10+
# Any modifications or derivative works of this code must retain this
11+
# copyright notice, and modified files need to carry a notice indicating
12+
# that they have been altered from the originals.
13+
*/
14+
15+
// job class for BackendEstimator
16+
17+
#ifndef __qiskitcpp_primitives_backend_estimator_job_def_hpp__
18+
#define __qiskitcpp_primitives_backend_estimator_job_def_hpp__
19+
20+
#include <chrono>
21+
#include <thread>
22+
23+
#include <memory>
24+
#include <stdexcept>
25+
#include <vector>
26+
27+
#include "primitives/containers/estimator_primitive_result.hpp"
28+
#include "providers/job.hpp"
29+
30+
// Windows headers can define ERROR after jobstatus.hpp has already been parsed.
31+
#ifdef ERROR
32+
#undef ERROR
33+
#endif
34+
35+
namespace Qiskit {
36+
namespace primitives {
37+
38+
/// @class BackendEstimatorJob
39+
/// @brief Job class for Backend Estimator primitive.
40+
class BackendEstimatorJob {
41+
protected:
42+
std::shared_ptr<providers::Job> job_ = nullptr;
43+
std::vector<EstimatorPub> pubs_;
44+
45+
public:
46+
/// @brief Create a new BackendEstimatorJob.
47+
/// @param job a job pointer to run pubs.
48+
/// @param pubs a list of pubs.
49+
BackendEstimatorJob(std::shared_ptr<providers::Job> job, std::vector<EstimatorPub>& pubs)
50+
{
51+
job_ = job;
52+
pubs_ = pubs;
53+
}
54+
55+
BackendEstimatorJob(const BackendEstimatorJob& other)
56+
{
57+
job_ = other.job_;
58+
pubs_ = other.pubs_;
59+
}
60+
61+
~BackendEstimatorJob()
62+
{
63+
if (job_) {
64+
job_.reset();
65+
}
66+
}
67+
68+
/// @brief get pubs for this job.
69+
/// @return a list of pub.
70+
const std::vector<EstimatorPub>& pubs(void) const
71+
{
72+
return pubs_;
73+
}
74+
75+
/// @brief Return the status of the job.
76+
/// @return JobStatus enum.
77+
providers::JobStatus status(void)
78+
{
79+
if (!job_) {
80+
return providers::JobStatus::ERROR;
81+
}
82+
return job_->status();
83+
}
84+
85+
/// @brief Return whether the job is actively running.
86+
/// @return true if job is actively running, otherwise false.
87+
bool running(void)
88+
{
89+
return status() == providers::JobStatus::RUNNING;
90+
}
91+
92+
/// @brief Return whether the job is queued.
93+
/// @return true if job is queued, otherwise false.
94+
bool queued(void)
95+
{
96+
return status() == providers::JobStatus::QUEUED;
97+
}
98+
99+
/// @brief Return whether the job has successfully run.
100+
/// @return true if successfully run, otherwise false.
101+
bool done(void)
102+
{
103+
return status() == providers::JobStatus::DONE;
104+
}
105+
106+
/// @brief Return whether the job has been cancelled.
107+
/// @return true if job has been cancelled, otherwise false.
108+
bool cancelled(void)
109+
{
110+
return status() == providers::JobStatus::CANCELLED;
111+
}
112+
113+
/// @brief Return whether the job is in a final job state such as DONE or ERROR.
114+
/// @return true if job is in a final job state, otherwise false.
115+
bool in_final_state(void)
116+
{
117+
return is_final_state(status());
118+
}
119+
120+
/// @brief Attempt to cancel the job.
121+
/// @return false because backend cancellation is not implemented.
122+
bool cancel(void)
123+
{
124+
return false;
125+
}
126+
127+
/// @brief Return the results of the job.
128+
/// @return estimator primitive result.
129+
EstimatorPrimitiveResult result(void)
130+
{
131+
if (!job_) {
132+
throw std::runtime_error("BackendEstimatorJob has no provider job.");
133+
}
134+
135+
providers::JobStatus st;
136+
while (true) {
137+
st = job_->status();
138+
if (is_final_state(st)) {
139+
break;
140+
}
141+
std::this_thread::sleep_for(std::chrono::seconds(1));
142+
}
143+
144+
if (st != providers::JobStatus::DONE) {
145+
throw std::runtime_error("BackendEstimatorJob did not finish successfully.");
146+
}
147+
148+
uint_t num_results = job_->num_results();
149+
if (num_results != pubs_.size()) {
150+
throw std::runtime_error("BackendEstimatorJob result count does not match the submitted pubs.");
151+
}
152+
153+
EstimatorPrimitiveResult result;
154+
result.allocate(num_results);
155+
for (uint_t i = 0; i < num_results; i++) {
156+
result[i].set_pub(pubs_[i]);
157+
if (!job_->result(i, result[i])) {
158+
throw std::runtime_error("BackendEstimatorJob failed to read an estimator pub result.");
159+
}
160+
}
161+
return result;
162+
}
163+
164+
protected:
165+
static bool is_final_state(providers::JobStatus status)
166+
{
167+
return status == providers::JobStatus::DONE ||
168+
status == providers::JobStatus::CANCELLED ||
169+
status == providers::JobStatus::FAILED ||
170+
status == providers::JobStatus::ERROR;
171+
}
172+
};
173+
174+
} // namespace primitives
175+
} // namespace Qiskit
176+
177+
#endif //__qiskitcpp_primitives_backend_estimator_job_def_hpp__

0 commit comments

Comments
 (0)