diff --git a/.github/pre-commit/spelling_allowlist.txt b/.github/pre-commit/spelling_allowlist.txt index f607aa4092f..fcc4ab69ef0 100644 --- a/.github/pre-commit/spelling_allowlist.txt +++ b/.github/pre-commit/spelling_allowlist.txt @@ -108,6 +108,8 @@ Photonics PyPI Pygments QAOA +QASM +QBRAID QCI QCaaS QEC @@ -169,6 +171,7 @@ amongst ancilla ansatz ansatzes +api archiver arity auxillary @@ -302,6 +305,7 @@ lossy lvalue macOS makefiles +measurementCounts merchantability mps multinomial @@ -314,6 +318,7 @@ natively normalization nullary nvcc +nvq observables optimizer optimizers @@ -335,21 +340,26 @@ preprocessor probability programmatically pybind +qBraid qaoa +qbraid qed qio +qrn quantize quantized qubit qubits qudit qudits +queryable qumode qumodes reStructuredText realtime reconfigurable reproducibility +resultData reusability runtime runtimes diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml index 47cce0eb1e3..8f27dfcaf38 100644 --- a/.github/workflows/integration_tests.yml +++ b/.github/workflows/integration_tests.yml @@ -28,6 +28,7 @@ on: - quantinuum - scaleway - tii + - qbraid single_test_name: type: string required: false @@ -95,6 +96,13 @@ jobs: cudaq_test_image: ${{ steps.vars.outputs.cudaq_nightly_image }}@${{ steps.test_image.outputs.digest }} steps: + - name: Log in to GitHub CR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ github.token }} + - name: Set variables id: vars run: | @@ -112,13 +120,6 @@ jobs: echo "platforms=$(echo $platforms | tr ' ' ,)" >> $GITHUB_OUTPUT echo "cudaq_nightly_image=$cudaq_nightly_image" >> $GITHUB_OUTPUT - - name: Log in to GitHub CR - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ github.token }} - - name: Set up context for buildx run: | docker context create builder_context @@ -191,7 +192,7 @@ jobs: run: | # Determine which providers to test based on inputs and event type if [[ "${{ github.event_name }}" == "schedule" || "${{ inputs.target }}" == "nightly" ]]; then - providers='["anyon", "fermioniq", "infleqtion", "ionq", "iqm", "oqc", "orca", "pasqal", "qci", "quantinuum", "scaleway", "tii"]' + providers='["anyon", "fermioniq", "infleqtion", "ionq", "iqm", "oqc", "orca", "pasqal", "qbraid", "qci", "quantinuum", "scaleway", "tii"]' else # Just run the specified target provider providers="[\"${{ inputs.target }}\"]" @@ -261,6 +262,9 @@ jobs: pasqal) filelist="docs/sphinx/targets/cpp/pasqal.cpp docs/sphinx/targets/python/pasqal.py" ;; + qbraid) + filelist="targettests/qbraid/*.cpp docs/sphinx/targets/cpp/qbraid.cpp docs/sphinx/targets/python/qbraid.py" + ;; qci) filelist="targettests/qci/*.cpp" ;; @@ -380,6 +384,11 @@ jobs: echo "PASQAL_PROJECT_ID=${{ secrets.PASQAL_PROJECT_ID }}" >> $GITHUB_ENV echo "PASQAL_MACHINE_TARGET=EMU_FREE" >> $GITHUB_ENV ;; + qbraid) + echo "### Setting up qBraid account" >> $GITHUB_STEP_SUMMARY + echo "::add-mask::${{ secrets.QBRAID_API_KEY }}" + echo "QBRAID_API_KEY=${{ secrets.QBRAID_API_KEY }}" >> $GITHUB_ENV + ;; qci) echo "### Setting up QCI account" >> $GITHUB_STEP_SUMMARY echo "::add-mask::${{ secrets.QCI_AUTH_TOKEN }}" @@ -671,6 +680,39 @@ jobs: fi ;; + qbraid) + if [[ "$filename" == *.cpp ]]; then + nvq++ -v $filename --target qbraid --qbraid-machine qbraid:qbraid:sim:qir-sv + test_status=$? + if [ $test_status -eq 0 ]; then + ./a.out + test_status=$? + if [ $test_status -eq 0 ]; then + echo ":white_check_mark: Successfully ran test: $filename" >> $GITHUB_STEP_SUMMARY + else + echo ":x: Test failed (failed to execute): $filename" >> $GITHUB_STEP_SUMMARY + test_err_sum=$((test_err_sum+1)) + fi + else + echo ":x: Test failed (failed to compile): $filename" >> $GITHUB_STEP_SUMMARY + test_err_sum=$((test_err_sum+1)) + fi + elif [[ "$filename" == *.py ]]; then + python3 $filename 1> /dev/null + test_status=$? + if [ $test_status -eq 0 ]; then + echo ":white_check_mark: Successfully ran test: $filename" >> $GITHUB_STEP_SUMMARY + else + echo ":x: Test failed (failed to execute): $filename" >> $GITHUB_STEP_SUMMARY + test_err_sum=$((test_err_sum+1)) + fi + else + echo "::warning::Unsupported file type: $filename" + echo ":warning: Test skipped (unsupported file type): $filename" >> $GITHUB_STEP_SUMMARY + test_skip_sum=$((test_skip_sum+1)) + fi + ;; + qci) nvq++ -v $filename --target qci test_status=$? diff --git a/CMakeLists.txt b/CMakeLists.txt index 61e011e4c48..85d94c6086d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -138,6 +138,11 @@ if (NOT DEFINED CUDAQ_ENABLE_SCALEWAY_BACKEND) set(CUDAQ_ENABLE_SCALEWAY_BACKEND ON CACHE BOOL "Enable building the Scaleway target.") endif() +# Enable qBraid target by default. +if (NOT DEFINED CUDAQ_ENABLE_QBRAID_BACKEND) + set(CUDAQ_ENABLE_QBRAID_BACKEND ON CACHE BOOL "Enable building the qBraid target.") +endif() + # Generate a CompilationDatabase (compile_commands.json file) for our build, # for use by clang_complete, YouCompleteMe, etc. set(CMAKE_EXPORT_COMPILE_COMMANDS 1) diff --git a/docs/sphinx/targets/cpp/qbraid.cpp b/docs/sphinx/targets/cpp/qbraid.cpp new file mode 100644 index 00000000000..f7a15a0906e --- /dev/null +++ b/docs/sphinx/targets/cpp/qbraid.cpp @@ -0,0 +1,48 @@ +// Compile and run with: +// ``` +// nvq++ --target qbraid qbraid.cpp -o out.x && ./out.x +// ``` +// This will submit the job to the qBraid ideal simulator target (default). + +#include +#include + +// Define a simple quantum kernel to execute on qBraid. +struct ghz { + // Maximally entangled state between 5 qubits. + auto operator()() __qpu__ { + cudaq::qvector q(5); + h(q[0]); + for (int i = 0; i < 4; i++) { + x(q[i], q[i + 1]); + } + auto result = mz(q); + } +}; + +int main() { + // Submit to qBraid asynchronously (e.g., continue executing + // code in the file until the job has been returned). + auto future = cudaq::sample_async(ghz{}); + // ... classical code to execute in the meantime ... + + // Can write the future to file: + { + std::ofstream out("saveMe.json"); + out << future; + } + + // Then come back and read it in later. + cudaq::async_result readIn; + std::ifstream in("saveMe.json"); + in >> readIn; + + // Get the results of the read in future. + auto async_counts = readIn.get(); + async_counts.dump(); + + // OR: Submit to qBraid synchronously (e.g., wait for the job + // result to be returned before proceeding). + auto counts = cudaq::sample(ghz{}); + counts.dump(); +} diff --git a/docs/sphinx/targets/python/qbraid.py b/docs/sphinx/targets/python/qbraid.py new file mode 100644 index 00000000000..dc61d605709 --- /dev/null +++ b/docs/sphinx/targets/python/qbraid.py @@ -0,0 +1,51 @@ +import cudaq + +# You only have to set the target once! No need to redefine it +# for every execution call on your kernel. +# To use different targets in the same file, you must update +# it via another call to `cudaq.set_target()` +cudaq.set_target("qbraid") + + +# Create the kernel we'd like to execute on qBraid. +@cudaq.kernel +def kernel(): + qvector = cudaq.qvector(2) + h(qvector[0]) + x.ctrl(qvector[0], qvector[1]) + + +# Execute on qBraid and print out the results. + +# Option A: +# By using the asynchronous `cudaq.sample_async`, the remaining +# classical code will be executed while the job is being handled +# by qBraid. This is ideal when submitting via a queue over +# the cloud. +async_results = cudaq.sample_async(kernel) +# ... more classical code to run ... + +# We can either retrieve the results later in the program with +# ``` +# async_counts = async_results.get() +# ``` +# or we can also write the job reference (`async_results`) to +# a file and load it later or from a different process. +file = open("future.txt", "w") +file.write(str(async_results)) +file.close() + +# We can later read the file content and retrieve the job +# information and results. +same_file = open("future.txt", "r") +retrieved_async_results = cudaq.AsyncSampleResult(str(same_file.read())) + +counts = retrieved_async_results.get() +print(counts) + +# Option B: +# By using the synchronous `cudaq.sample`, the execution of +# any remaining classical code in the file will occur only +# after the job has been returned from qBraid. +counts = cudaq.sample(kernel) +print(counts) diff --git a/docs/sphinx/using/backends/cloud.rst b/docs/sphinx/using/backends/cloud.rst index 8c03a4398cc..ebd02e033e8 100644 --- a/docs/sphinx/using/backends/cloud.rst +++ b/docs/sphinx/using/backends/cloud.rst @@ -5,6 +5,7 @@ CUDA-Q provides a number of options to access hardware resources (GPUs and QPUs) .. toctree:: :maxdepth: 1 - + Amazon Braket (braket) Scaleway QaaS (scaleway) + qBraid diff --git a/docs/sphinx/using/backends/cloud/qbraid.rst b/docs/sphinx/using/backends/cloud/qbraid.rst new file mode 100644 index 00000000000..dfa72e53913 --- /dev/null +++ b/docs/sphinx/using/backends/cloud/qbraid.rst @@ -0,0 +1,101 @@ +qBraid +++++++ + +.. _qbraid-backend: + +`qBraid `__ is a cloud platform that brokers access to +quantum simulators and hardware from multiple vendors through a single API. +CUDA-Q can submit OpenQASM 2 jobs to any device exposed by the qBraid service. +See the `qBraid device catalog `__ for the +set of simulators and QPUs currently available. + +Setting Credentials +``````````````````` + +Generate an API key from your `qBraid account `__ +and export it as an environment variable: + +.. code:: bash + + export QBRAID_API_KEY="qbraid_generated_api_key" + +Alternatively, the API key can be passed directly to ``cudaq.set_target`` via +the ``api_key`` argument (see below). + +Submitting +`````````` + +.. tab:: Python + + The target to which quantum kernels are submitted can be controlled with + the ``cudaq.set_target()`` function. + + .. code:: python + + cudaq.set_target("qbraid") + + By default, jobs are submitted to the qBraid state vector simulator + (``qbraid:qbraid:sim:qir-sv``). + + To specify a different qBraid device, set the ``machine`` parameter to its + qBraid device ID. + + .. code:: python + + cudaq.set_target("qbraid", machine="qbraid:qbraid:sim:qir-sv") + + The API key can also be supplied inline instead of through the + ``QBRAID_API_KEY`` environment variable. + + .. code:: python + + cudaq.set_target("qbraid", api_key="qbraid_generated_api_key") + + qBraid devices are cloud-hosted, so local emulation via the ``emulate`` + flag is not supported — all jobs are executed on the qBraid service. + To run without submitting to real hardware, select one of the qBraid + simulator devices (for example, ``qbraid:qbraid:sim:qir-sv``) via the + ``machine`` argument. + + The number of shots for a kernel execution can be set through the + ``shots_count`` argument to ``cudaq.sample`` or ``cudaq.observe``. The + default is 1000. + + .. code:: python + + cudaq.sample(kernel, shots_count=10000) + +.. tab:: C++ + + To target quantum kernel code for execution on qBraid, pass the flag + ``--target qbraid`` to the ``nvq++`` compiler. By default jobs are + submitted to the qBraid state vector simulator + (``qbraid:qbraid:sim:qir-sv``). + + .. code:: bash + + nvq++ --target qbraid src.cpp + + To execute kernels on a different device, pass ``--qbraid-machine`` with + the qBraid device ID: + + .. code:: bash + + nvq++ --target qbraid --qbraid-machine "qbraid:qbraid:sim:qir-sv" src.cpp + + The API key can be passed explicitly with ``--qbraid-api_key`` instead of + being read from ``QBRAID_API_KEY``: + + .. code:: bash + + nvq++ --target qbraid --qbraid-api_key "qbraid_generated_api_key" src.cpp + + qBraid devices are cloud-hosted, so the ``--emulate`` flag is not + supported for this target — all jobs are executed on the qBraid + service. To run without submitting to real hardware, pass + ``--qbraid-machine`` with a qBraid simulator device ID (for example, + ``qbraid:qbraid:sim:qir-sv``). + +To see a complete example for using qBraid's backends, take a look at our +:doc:`Python examples <../../examples/examples>` and +:doc:`C++ examples <../../examples/examples>`. diff --git a/python/tests/backends/test_qbraid.py b/python/tests/backends/test_qbraid.py new file mode 100644 index 00000000000..8aa9b0dff57 --- /dev/null +++ b/python/tests/backends/test_qbraid.py @@ -0,0 +1,240 @@ +# ============================================================================ # +# Copyright (c) 2022 - 2026 NVIDIA Corporation & Affiliates. # +# All rights reserved. # +# # +# This source code and the accompanying materials are made available under # +# the terms of the Apache License 2.0 which accompanies this distribution. # +# ============================================================================ # + +import os +from multiprocessing import Process +from urllib.request import Request, urlopen + +import cudaq +import pytest +from cudaq import spin +from network_utils import check_server_connection + +try: + from utils.mock_qpu.qbraid import startServer +except ImportError: + print("Mock qpu not available, skipping qBraid tests.") + pytest.skip("Mock qpu not available.", allow_module_level=True) + +port = 62454 + +# Default machine for tests. Mirrors the real qBraid device string format. +TEST_MACHINE = "qbraid:qbraid:sim:qir-sv" +TEST_API_KEY = "00000000000000000000000000000000" + +# The qbraid mock server in utils/mock_qpu/qbraid/__init__.py doesn't simulate +# quantum mechanics - it only inspects the QASM for `h` and `measure` ops and +# generates random outcomes for qubits with H. It does NOT model entanglement +# via CNOT. Assertions below reflect the mock's behavior, not physical truth. + + +def _set_qbraid_target(**overrides): + """Call set_target with the canonical qbraid args plus any overrides. + + Uses the documented target arguments (`machine`, `api_key`) plus `url` + which is accepted by the helper for test/mock overrides. + """ + kwargs = { + "url": f"http://localhost:{port}", + "machine": TEST_MACHINE, + "api_key": TEST_API_KEY, + } + kwargs.update(overrides) + cudaq.set_target("qbraid", **kwargs) + + +@pytest.fixture(scope="session", autouse=True) +def startUpMockServer(): + cudaq.set_random_seed(13) + os.environ["QBRAID_API_KEY"] = TEST_API_KEY + + _set_qbraid_target() + + p = Process(target=startServer, args=(port,)) + p.start() + + if not check_server_connection(port): + p.terminate() + pytest.exit("Mock server did not start in time, skipping tests.", + returncode=1) + + yield "Server started." + + p.terminate() + + +@pytest.fixture(scope="function", autouse=True) +def configureTarget(): + _set_qbraid_target() + yield "Running the test." + cudaq.reset_target() + + +def _make_h_kernel(): + """H on q[0], CX to q[1], measure both. Mock only sees H on q[0].""" + kernel = cudaq.make_kernel() + qubits = kernel.qalloc(2) + kernel.h(qubits[0]) + kernel.cx(qubits[0], qubits[1]) + kernel.mz(qubits) + return kernel + + +def test_qbraid_sample(): + counts = cudaq.sample(_make_h_kernel()) + # Mock: q[0] superposition -> {"0","1"}, q[1] fixed -> "0" + # Observed outcomes: "00" and "10" + assert len(counts) == 2 + assert "00" in counts + assert "10" in counts + + +def test_qbraid_sample_async(): + future = cudaq.sample_async(_make_h_kernel()) + counts = future.get() + assert len(counts) == 2 + assert "00" in counts + assert "10" in counts + + +def test_qbraid_sample_async_persist_future(): + future = cudaq.sample_async(_make_h_kernel()) + futureAsString = str(future) + + readIn = cudaq.AsyncSampleResult(futureAsString) + counts = readIn.get() + assert len(counts) == 2 + assert "00" in counts + assert "10" in counts + + +def _make_vqe_ansatz(): + kernel, theta = cudaq.make_kernel(float) + qreg = kernel.qalloc(2) + kernel.x(qreg[0]) + kernel.ry(theta, qreg[1]) + kernel.cx(qreg[1], qreg[0]) + hamiltonian = (5.907 - 2.1433 * spin.x(0) * spin.x(1) - + 2.1433 * spin.y(0) * spin.y(1) + 0.21829 * spin.z(0) - + 6.125 * spin.z(1)) + return kernel, hamiltonian + + +def test_qbraid_observe(): + kernel, hamiltonian = _make_vqe_ansatz() + res = cudaq.observe(kernel, hamiltonian, 0.59) + # Mock outcomes are random; just verify the roundtrip returned a finite value. + val = res.expectation() + assert isinstance(val, float) + assert val == val # NaN check + + +def test_qbraid_observe_async_persist_future(): + kernel, hamiltonian = _make_vqe_ansatz() + + future = cudaq.observe_async(kernel, hamiltonian, 0.59) + futureAsString = str(future) + + readIn = cudaq.AsyncObserveResult(futureAsString, hamiltonian) + res = readIn.get() + val = res.expectation() + assert isinstance(val, float) + assert val == val + + +def test_qbraid_api_key_via_target_arg_without_env_var(): + """When QBRAID_API_KEY env var is absent, api_key kwarg must work.""" + saved = os.environ.pop("QBRAID_API_KEY", None) + try: + _set_qbraid_target(api_key=TEST_API_KEY) + + kernel = cudaq.make_kernel() + qubit = kernel.qalloc() + kernel.h(qubit) + kernel.mz(qubit) + + counts = cudaq.sample(kernel) + assert len(counts) >= 1 + finally: + if saved is not None: + os.environ["QBRAID_API_KEY"] = saved + + +def test_qbraid_machine_alternative_device(): + """A different machine string is accepted via the target arg.""" + _set_qbraid_target(machine="aws:aws:sim:sv1") + + kernel = cudaq.make_kernel() + qubit = kernel.qalloc() + kernel.h(qubit) + kernel.mz(qubit) + + counts = cudaq.sample(kernel) + assert len(counts) >= 1 + + +def _arm_result_status(code: int): + """Force the next /result call on the mock to return the given HTTP code. + + Resets prior test-hook state first so the test is order-independent. + """ + reset_url = f"http://localhost:{port}/test/reset" + arm_url = f"http://localhost:{port}/test/force_next_result_status/{code}" + # POST with empty body; no response parsing needed. + urlopen(Request(reset_url, data=b"", method="POST"), timeout=5).read() + urlopen(Request(arm_url, data=b"", method="POST"), timeout=5).read() + + +def test_qbraid_result_auth_failure(): + """401 on /result -> terminal auth error; message names the status.""" + _arm_result_status(401) + kernel = cudaq.make_kernel() + qubit = kernel.qalloc() + kernel.h(qubit) + kernel.mz(qubit) + with pytest.raises(RuntimeError, match="authentication failed"): + cudaq.sample(kernel) + + +def test_qbraid_result_forbidden(): + """403 on /result -> same terminal auth translation as 401.""" + _arm_result_status(403) + kernel = cudaq.make_kernel() + qubit = kernel.qalloc() + kernel.h(qubit) + kernel.mz(qubit) + with pytest.raises(RuntimeError, match="authentication failed"): + cudaq.sample(kernel) + + +def test_qbraid_result_not_found(): + """404 on /result -> terminal 'result not found' error.""" + _arm_result_status(404) + kernel = cudaq.make_kernel() + qubit = kernel.qalloc() + kernel.h(qubit) + kernel.mz(qubit) + with pytest.raises(RuntimeError, match="result not found"): + cudaq.sample(kernel) + + +def test_qbraid_result_server_error_retries(): + """500 on /result is retryable; hook clears after one call so retry wins.""" + _arm_result_status(500) + kernel = cudaq.make_kernel() + qubit = kernel.qalloc() + kernel.h(qubit) + kernel.mz(qubit) + counts = cudaq.sample(kernel) + assert len(counts) >= 1 + + +# leave for gdb debugging +if __name__ == "__main__": + loc = os.path.abspath(__file__) + pytest.main([loc, "-s"]) diff --git a/runtime/cudaq/platform/default/rest/helpers/CMakeLists.txt b/runtime/cudaq/platform/default/rest/helpers/CMakeLists.txt index 5daa54ea114..4574b6ba8fe 100644 --- a/runtime/cudaq/platform/default/rest/helpers/CMakeLists.txt +++ b/runtime/cudaq/platform/default/rest/helpers/CMakeLists.txt @@ -27,3 +27,6 @@ endif() if(CUDAQ_ENABLE_TII_BACKEND) add_subdirectory(tii) endif() +if(CUDAQ_ENABLE_QBRAID_BACKEND) + add_subdirectory(qbraid) +endif() diff --git a/runtime/cudaq/platform/default/rest/helpers/qbraid/CMakeLists.txt b/runtime/cudaq/platform/default/rest/helpers/qbraid/CMakeLists.txt new file mode 100644 index 00000000000..dac742b6824 --- /dev/null +++ b/runtime/cudaq/platform/default/rest/helpers/qbraid/CMakeLists.txt @@ -0,0 +1,17 @@ +# ============================================================================ # +# Copyright (c) 2022 - 2026 NVIDIA Corporation & Affiliates. # +# All rights reserved. # +# # +# This source code and the accompanying materials are made available under # +# the terms of the Apache License 2.0 which accompanies this distribution. # +# ============================================================================ # +target_sources(cudaq-rest-qpu PRIVATE QbraidServerHelper.cpp) +add_target_config(qbraid) + +add_library(cudaq-serverhelper-qbraid SHARED QbraidServerHelper.cpp ) +target_link_libraries(cudaq-serverhelper-qbraid + PUBLIC + cudaq-common + cudaq-logger +) +install(TARGETS cudaq-serverhelper-qbraid DESTINATION lib) \ No newline at end of file diff --git a/runtime/cudaq/platform/default/rest/helpers/qbraid/QbraidServerHelper.cpp b/runtime/cudaq/platform/default/rest/helpers/qbraid/QbraidServerHelper.cpp new file mode 100644 index 00000000000..c981c055b4a --- /dev/null +++ b/runtime/cudaq/platform/default/rest/helpers/qbraid/QbraidServerHelper.cpp @@ -0,0 +1,391 @@ +/******************************************************************************* + * Copyright (c) 2022 - 2026 NVIDIA Corporation & Affiliates. * + * All rights reserved. * + * * + * This source code and the accompanying materials are made available under * + * the terms of the Apache License 2.0 which accompanies this distribution. * + ******************************************************************************/ + +#include "common/RestClient.h" +#include "common/ServerHelper.h" +#include "cudaq/Support/Version.h" +#include "cudaq/runtime/logger/logger.h" +#include "cudaq/utils/cudaq_utils.h" +#include +#include +#include + +namespace cudaq { + +/// @brief The QbraidServerHelper class extends the ServerHelper class to +/// handle interactions with the qBraid server for submitting and retrieving +/// quantum computation jobs to various qBraid supported devices. +class QbraidServerHelper : public ServerHelper { + static constexpr const char *DEFAULT_URL = "https://api-v2.qbraid.com/api/v1"; + static constexpr const char *DEFAULT_DEVICE = "qbraid:qbraid:sim:qir-sv"; + static constexpr int DEFAULT_QUBITS = 30; + +public: + /// @brief Returns the name of the server helper. + const std::string name() const override { return "qbraid"; } + + /// @brief Initializes the server helper with the provided backend + /// configuration. + void initialize(BackendConfig config) override { + cudaq::info("Initializing qBraid Backend."); + + backendConfig.clear(); + backendConfig["url"] = getValueOrDefault(config, "url", DEFAULT_URL); + backendConfig["user_agent"] = "cudaq/" + std::string(cudaq::getVersion()); + backendConfig["qubits"] = std::to_string(DEFAULT_QUBITS); + + // Accept "machine" as a user-friendly alias for qBraid's device_id + // Usage: cudaq.set_target("qbraid", machine="qbraid:qbraid:sim:qir-sv") + if (!config["machine"].empty()) { + backendConfig["device_id"] = config["machine"]; + } else { + backendConfig["device_id"] = + getValueOrDefault(config, "device_id", DEFAULT_DEVICE); + } + + // Accept api_key from target arguments, fall back to QBRAID_API_KEY env var + // Usage: cudaq.set_target("qbraid", api_key="my-key") + bool isApiKeyRequired = [&]() { + auto it = config.find("emulate"); + if (it != config.end() && it->second == "true") + return false; + return true; + }(); + if (!config["api_key"].empty()) { + backendConfig["api_key"] = config["api_key"]; + } else { + backendConfig["api_key"] = + getEnvVar("QBRAID_API_KEY", "", isApiKeyRequired); + } + backendConfig["job_path"] = backendConfig["url"] + "/jobs"; + + if (!config["shots"].empty()) { + backendConfig["shots"] = config["shots"]; + this->setShots(std::stoul(config["shots"])); + } else { + backendConfig["shots"] = "1000"; + this->setShots(1000); + } + + parseConfigForCommonParams(config); + + cudaq::info("qBraid configuration initialized:"); + for (const auto &[key, value] : backendConfig) { + if (key == "api_key") { + cudaq::info(" api_key = ", value.size()); + } else { + cudaq::info(" {} = {}", key, value); + } + } + } + + /// @brief Creates a quantum computation job using the provided kernel + /// executions and returns the corresponding payload. + ServerJobPayload + createJob(std::vector &circuitCodes) override { + if (backendConfig.find("job_path") == backendConfig.end()) { + throw std::runtime_error( + "job_path not found in config. Was initialize() called?"); + } + + std::vector jobs; + for (auto &circuitCode : circuitCodes) { + ServerMessage job; + job["deviceQrn"] = backendConfig.at("device_id"); + // Use the per-call shots (set via cudaq::sample(..., shots_count=N)) + job["shots"] = shots; + + // v2 API: program is a structured object with format and data + nlohmann::json program; + program["format"] = "qasm2"; + program["data"] = circuitCode.code; + job["program"] = program; + + // v2 API: name is a top-level field (not nested under tags) + if (!circuitCode.name.empty()) { + job["name"] = circuitCode.name; + } + + jobs.push_back(job); + } + + return std::make_tuple(backendConfig.at("job_path"), getHeaders(), jobs); + } + + /// @brief Extracts the job ID from the server's response to a job submission. + std::string extractJobId(ServerMessage &postResponse) override { + // v2 API: jobQrn is nested under data envelope + if (postResponse.contains("data") && + postResponse["data"].contains("jobQrn")) { + return postResponse["data"]["jobQrn"].get(); + } + throw std::runtime_error( + "ServerMessage doesn't contain 'data.jobQrn' key."); + } + + /// @brief Constructs the URL for retrieving a job based on the server's + /// response to a job submission. + std::string constructGetJobPath(ServerMessage &postResponse) override { + // v2 API: use path parameter instead of query parameter + if (postResponse.contains("data") && + postResponse["data"].contains("jobQrn")) { + return backendConfig.at("job_path") + "/" + + postResponse["data"]["jobQrn"].get(); + } + throw std::runtime_error( + "ServerMessage doesn't contain 'data.jobQrn' key."); + } + + /// @brief Constructs the URL for retrieving a job based on a job ID. + std::string constructGetJobPath(std::string &jobId) override { + // v2 API: /jobs/{jobQrn} + return backendConfig.at("job_path") + "/" + jobId; + } + + /// @brief Constructs the URL for retrieving the measurement results of a + /// completed job based on a job ID. + std::string constructGetResultsPath(const std::string &jobId) { + // v2 API: /jobs/{jobQrn}/result + return backendConfig.at("job_path") + "/" + jobId + "/result"; + } + + /// @brief Checks if a job is done based on the server's response to a job + /// retrieval request. + bool jobIsDone(ServerMessage &getJobResponse) override { + std::string status; + + // v2 API: status is nested under data envelope + if (getJobResponse.contains("data") && + getJobResponse["data"].contains("status")) { + status = getJobResponse["data"]["status"].get(); + cudaq::info("Job status from v2 data envelope: {}", status); + } else if (getJobResponse.contains("status")) { + // Fallback: direct status field + status = getJobResponse["status"].get(); + cudaq::info("Job status from direct response: {}", status); + } else { + cudaq::info("Unexpected job response format: {}", getJobResponse.dump()); + throw std::runtime_error("Invalid job response format"); + } + + if (status == "FAILED" || status == "COMPLETED" || status == "CANCELLED") { + return true; + } + + return false; + } + + /// @brief Processes the server's response to a job retrieval request and + /// maps the results back to sample results. + cudaq::sample_result processResults(ServerMessage &getJobResponse, + std::string &jobId) override { + // qbraid's v2 API has a window where status transitions to COMPLETED + // before the result payload is queryable on /result, so /result returns + // {success: false, data: {message: "not yet available"}}. Retry with + // backoff absorbs that race. Exercised deterministically via the mock's + // POST /test/delay_next_results endpoint (see checkResultRetry / + // checkResultRetryExhaustion tests). + const int maxRetries = 3; + const int waitTime = 2; + const float backoffFactor = 2.0; + + for (int attempt = 0; attempt < maxRetries; ++attempt) { + try { + auto resultsPath = constructGetResultsPath(jobId); + auto headers = getHeaders(); + + cudaq::info("Fetching results from v2 endpoint (attempt {}/{}): {}", + attempt + 1, maxRetries, resultsPath); + RestClient client; + auto resultJson = client.get("", resultsPath, headers, true); + + // v2 API: error indicated by success=false + if (resultJson.contains("success") && + resultJson["success"].is_boolean() && + !resultJson["success"].get()) { + std::string errorMsg = "Results not yet available"; + if (resultJson.contains("data") && + resultJson["data"].contains("message")) { + errorMsg = resultJson["data"]["message"].get(); + } + cudaq::info("Results endpoint returned success=false: {}", errorMsg); + + if (attempt == maxRetries - 1) { + throw std::runtime_error("Error retrieving results: " + errorMsg); + } + } + // v2 API: measurementCounts nested under data.resultData + else if (resultJson.contains("data") && + resultJson["data"].contains("resultData") && + resultJson["data"]["resultData"].contains( + "measurementCounts")) { + cudaq::info("Processing results from v2 endpoint"); + CountsDictionary counts; + auto &measurements = + resultJson["data"]["resultData"]["measurementCounts"]; + + for (const auto &[bitstring, count] : measurements.items()) { + counts[bitstring] = + count.is_number() + ? static_cast(count.get()) + : static_cast(count); + } + + // The returned bitstring spans every measured qubit, including + // compiler-generated ancillae that the user never declared. Reduce + // it down to the user-visible qubits using the output_names entry + // populated by the framework (Executor.cpp writes one per submitted + // circuit; Future.cpp re-initializes the helper with that config + // before processResults runs). Mirrors the IonQ / Braket helpers. + cudaq::ExecutionResult fullExecResults{counts}; + auto fullSampleResults = cudaq::sample_result{fullExecResults}; + + std::vector execResults; + + auto outputNamesIt = outputNames.find(jobId); + if (outputNamesIt != outputNames.end() && + !outputNamesIt->second.empty()) { + auto &job_output_names = outputNamesIt->second; + + std::vector qubitNumbers; + qubitNumbers.reserve(job_output_names.size()); + for (auto &[result, info] : job_output_names) + qubitNumbers.push_back(info.qubitNum); + + auto subset = fullSampleResults.get_marginal(qubitNumbers); + execResults.emplace_back(ExecutionResult{subset.to_map()}); + + // Emit one single-bit register per named result so that + // `sample_result::to_map(registerName)` still works. + for (const auto &[result, info] : job_output_names) { + CountsDictionary regCounts; + for (const auto &[bits, count] : fullSampleResults) + regCounts[std::string{bits[info.qubitNum]}] += count; + execResults.emplace_back(regCounts, info.registerName); + } + } else { + // No output_names available: fall back to the full flat counts. + execResults.emplace_back(ExecutionResult{counts}); + } + + return cudaq::sample_result(execResults); + } + + // No valid data yet and no explicit error - retry + if (attempt < maxRetries - 1) { + int sleepTime = (attempt == 0) + ? waitTime + : waitTime * std::pow(backoffFactor, attempt); + cudaq::info("No valid results yet, retrying in {} seconds", + sleepTime); + std::this_thread::sleep_for(std::chrono::seconds(sleepTime)); + } + + } catch (const std::exception &e) { + // RestClient throws std::runtime_error on any non-success HTTP status + // (see runtime/common/RestClient.cpp) with a fixed message format: + // "HTTP Error - status code : : " + // The code isn't exposed as a structured attribute, so we parse it + // out to distinguish terminal client errors (401/403/404/409) from + // transient server/network errors (5xx, parse errors) that retry. + static const std::regex statusRx(R"(status code (\d+))"); + const std::string what = e.what(); + std::smatch match; + int statusCode = 0; + if (std::regex_search(what, match, statusRx)) + statusCode = std::stoi(match[1]); + + // Terminal: auth failures - retrying will not recover. + if (statusCode == 401 || statusCode == 403) + throw std::runtime_error( + "qBraid authentication failed (HTTP " + + std::to_string(statusCode) + + "). Verify QBRAID_API_KEY or api_key target argument."); + + // Terminal: result resource genuinely does not exist. This is + // distinct from the "not yet available" race which returns + // 200 + success=false (handled above). + if (statusCode == 404) + throw std::runtime_error( + "qBraid result not found (HTTP 404) for job " + jobId + + ". The job may have been deleted or never produced results."); + + // Terminal: job reached a non-success terminal state (FAILED or + // CANCELLED). qBraid v2 returns 409 Conflict on /result in that case + // because no measurement data will ever be produced. + if (statusCode == 409) + throw std::runtime_error( + "qBraid job " + jobId + + " did not produce results (HTTP 409). The job likely FAILED " + "or was CANCELLED."); + + // Retryable: 5xx, network errors, JSON parse failures, etc. + cudaq::info("Exception when fetching results (attempt {}/{}): {}", + attempt + 1, maxRetries, what); + if (attempt < maxRetries - 1) { + int sleepTime = (attempt == 0) + ? waitTime + : waitTime * std::pow(backoffFactor, attempt); + cudaq::info("Retrying in {} seconds", sleepTime); + std::this_thread::sleep_for(std::chrono::seconds(sleepTime)); + } + } + } + + throw std::runtime_error("Failed to retrieve measurement counts after " + + std::to_string(maxRetries) + " attempts"); + } + + /// @brief Override the polling interval method + std::chrono::microseconds + nextResultPollingInterval(ServerMessage &postResponse) override { + return std::chrono::seconds(1); + } + +private: + /// @brief Returns the headers for the server requests. + RestHeaders getHeaders() override { + if (backendConfig.find("api_key") == backendConfig.end()) { + throw std::runtime_error( + "API key not found in config. Was initialize() called?"); + } + + RestHeaders headers; + headers["X-API-KEY"] = backendConfig.at("api_key"); + headers["Content-Type"] = "application/json"; + headers["User-Agent"] = backendConfig.at("user_agent"); + return headers; + } + + /// @brief Helper method to retrieve the value of an environment variable. + std::string getEnvVar(const std::string &key, const std::string &defaultVal, + const bool isRequired) const { + const char *env_var = std::getenv(key.c_str()); + if (env_var == nullptr) { + if (isRequired) { + throw std::runtime_error(key + " environment variable is not set."); + } + + return defaultVal; + } + return std::string(env_var); + } + + /// @brief Helper function to get a value from config or return a default + /// value. + std::string getValueOrDefault(const BackendConfig &config, + const std::string &key, + const std::string &defaultValue) const { + return config.find(key) != config.end() ? config.at(key) : defaultValue; + } +}; +} // namespace cudaq + +// Register the QbraidServerHelper with the name "qbraid" in the ServerHelper +// factory +CUDAQ_REGISTER_TYPE(cudaq::ServerHelper, cudaq::QbraidServerHelper, qbraid) diff --git a/runtime/cudaq/platform/default/rest/helpers/qbraid/qbraid.yml b/runtime/cudaq/platform/default/rest/helpers/qbraid/qbraid.yml new file mode 100644 index 00000000000..2b83c3ea7ff --- /dev/null +++ b/runtime/cudaq/platform/default/rest/helpers/qbraid/qbraid.yml @@ -0,0 +1,40 @@ +# ============================================================================ # +# Copyright (c) 2022 - 2026 NVIDIA Corporation & Affiliates. # +# All rights reserved. # +# # +# This source code and the accompanying materials are made available under # +# the terms of the Apache License 2.0 which accompanies this distribution. # +# ============================================================================ # + +name: qbraid +description: "CUDA-Q target for qBraid." +config: + # Tell DefaultQuantumPlatform what QPU subtype to use + platform-qpu: remote_rest + # Tell NVQ++ to generate glue code to set the target backend name + gen-target-backend: true + # Add the rest-qpu library to the link list + link-libs: ["-lcudaq-rest-qpu"] + # Define the JIT lowering pipeline. Mirrors Braket so that + # `combine-measurements` runs and attaches the QIROutputNamesAttrName + # metadata nvq++ needs to thread user-visible qubit indices through to + # `outputNames[jobId]` in the helper. Without that pass the default register + # would contain compiler-generated ancillae and named sub-registers would be + # unreachable (see QbraidServerHelper::processResults). + jit-mid-level-pipeline: "lower-to-cfg,decomposition{basis=h,s,t,rx,ry,rz,x,y,z,x(1)},quake-to-cc-prep,func.func(expand-control-veqs,combine-quantum-alloc,canonicalize,combine-measurements)" + # Tell the rest-qpu that we are generating OpenQASM. + codegen-emission: qasm2 + # Library mode is only for simulators, physical backends must turn this off + library-mode: false + +target-arguments: + - key: machine + required: false + type: string + platform-arg: qpu + help-string: "Specify the qBraid QPU." + - key: api_key + required: false + type: string + platform-arg: api_key + help-string: "Specify the qBraid API key." diff --git a/scripts/validate_pycudaq.sh b/scripts/validate_pycudaq.sh index 738e3b4da46..2197dd05b81 100644 --- a/scripts/validate_pycudaq.sh +++ b/scripts/validate_pycudaq.sh @@ -472,7 +472,8 @@ if [ -d "$root_folder/targets" ]; then skip_example=true elif [ "$t" == "tii" ] || [ "$t" == "scaleway" ] || [ "$t" == "quantum_machines" ] || \ [ "$t" == "quantinuum" ] || [ "$t" == "orca" ] || [ "$t" == "orca-photonics" ] || \ - [ "$t" == "iqm" ] || [ "$t" == "infleqtion" ] || [ "$t" == "anyon" ]; then + [ "$t" == "iqm" ] || [ "$t" == "infleqtion" ] || [ "$t" == "anyon" ] || \ + [ "$t" == "qbraid" ]; then echo "Skipping $ex (remote target '$t' not available)" >&2 skip_example=true fi diff --git a/targettests/execution/bug_qubit.cpp b/targettests/execution/bug_qubit.cpp index 6b33d51778c..d3b3d01d59e 100644 --- a/targettests/execution/bug_qubit.cpp +++ b/targettests/execution/bug_qubit.cpp @@ -17,6 +17,7 @@ // RUN: IQM_QPU_QA=%iqm_tests_dir/Crystal_20.txt %t // RUN: IQM_QPU_QA=%iqm_tests_dir/Crystal_54.txt %t // RUN: nvq++ --target oqc --emulate %s -o %t && %t +// RUN: nvq++ --target qbraid --emulate %s -o %t && %t // RUN: nvq++ --target quantinuum --emulate %s -o %t && %t // RUN: if %braket_avail; then nvq++ --target braket --emulate %s -o %t && %t; fi // RUN: if %qci_avail; then nvq++ --target qci --emulate %s -o %t && %t; fi diff --git a/targettests/execution/callable_kernel_arg.cpp b/targettests/execution/callable_kernel_arg.cpp index 7eeca0e5bbc..a036b046c5a 100644 --- a/targettests/execution/callable_kernel_arg.cpp +++ b/targettests/execution/callable_kernel_arg.cpp @@ -12,6 +12,7 @@ // RUN: nvq++ --target ionq --emulate %s -o %t && %t | FileCheck %s // RUN: nvq++ --target iqm --emulate %s -o %t && IQM_QPU_QA=%iqm_tests_dir/Crystal_5.txt %t | FileCheck %s // RUN: nvq++ --target oqc --emulate %s -o %t && %t | FileCheck %s +// RUN: nvq++ --target qbraid --emulate %s -o %t && %t | FileCheck %s // RUN: nvq++ --target quantinuum --emulate %s -o %t && %t | FileCheck %s // RUN: if %braket_avail; then nvq++ --target braket --emulate %s -o %t && %t | FileCheck %s; fi // RUN: if %qci_avail; then nvq++ --target qci --emulate %s -o %t && %t | FileCheck %s; fi diff --git a/targettests/execution/cudaq_observe-cpp17.cpp b/targettests/execution/cudaq_observe-cpp17.cpp new file mode 100644 index 00000000000..ffd05d7780f --- /dev/null +++ b/targettests/execution/cudaq_observe-cpp17.cpp @@ -0,0 +1,56 @@ +/******************************************************************************* + * Copyright (c) 2022 - 2025 NVIDIA Corporation & Affiliates. * + * All rights reserved. * + * * + * This source code and the accompanying materials are made available under * + * the terms of the Apache License 2.0 which accompanies this distribution. * + ******************************************************************************/ + +// REQUIRES: c++17 +// clang-format off +// RUN: nvq++ %cpp_std --target infleqtion --emulate %s -o %t && %t | FileCheck %s +// RUN: nvq++ %cpp_std --target ionq --emulate %s -o %t && %t | FileCheck %s +// 2 different IQM machines for 2 different topologies +// RUN: nvq++ %cpp_std --target iqm --iqm-machine Adonis --emulate %s -o %t && %t | FileCheck %s +// RUN: nvq++ %cpp_std --target iqm --iqm-machine Apollo --emulate %s -o %t && %t | FileCheck %s +// RUN: nvq++ %cpp_std --target oqc --emulate %s -o %t && %t | FileCheck %s +// RUN: nvq++ %cpp_std --target qbraid --emulate %s -o %t && %t | FileCheck %s +// RUN: nvq++ %cpp_std --target quantinuum --emulate %s -o %t && %t | FileCheck %s +// clang-format on + +#include +#include + +// The example here shows a simple use case for the `cudaq::observe` +// function in computing expected values of provided spin_ops. + +struct ansatz { + auto operator()(double theta) __qpu__ { + cudaq::qvector q(2); + x(q[0]); + ry(theta, q[1]); + cx(q[1], q[0]); + } +}; + +int main() { + + // Build up your spin op algebraically + cudaq::spin_op h = 5.907 - 2.1433 * cudaq::spin_op::x(0) * cudaq::spin_op::x(1) - + 2.1433 * cudaq::spin_op::y(0) * cudaq::spin_op::y(1) + + .21829 * cudaq::spin_op::z(0) - 6.125 * cudaq::spin_op::z(1); + + // Make repeatable for shots-based emulation + cudaq::set_random_seed(13); + + // Observe takes the kernel, the spin_op, and the concrete + // parameters for the kernel + double energy = cudaq::observe(ansatz{}, h, .59); + printf("Energy is %.16lf\n", energy); + return 0; +} + +// Note: seeds 2 and 12 will push this to -2 instead of -1. All all other +// seeds in 1-100 range will be -1.x. + +// CHECK: Energy is -1. diff --git a/targettests/execution/cudaq_observe.cpp b/targettests/execution/cudaq_observe.cpp index a28f7537f2e..230775628f9 100644 --- a/targettests/execution/cudaq_observe.cpp +++ b/targettests/execution/cudaq_observe.cpp @@ -12,6 +12,7 @@ // RUN: nvq++ --target ionq --emulate %s -o %t && %t | FileCheck %s // RUN: nvq++ --target iqm --emulate %s -o %t && IQM_QPU_QA=%iqm_tests_dir/Crystal_5.txt %t | FileCheck %s // RUN: nvq++ --target oqc --emulate %s -o %t && %t | FileCheck %s +// RUN: nvq++ --target qbraid --emulate %s -o %t && %t | FileCheck %s // RUN: nvq++ --target quantinuum --emulate %s -o %t && %t | FileCheck %s // RUN: if %braket_avail; then nvq++ --target braket --emulate %s -o %t && %t | FileCheck %s; fi // RUN: if %qci_avail; then nvq++ --target qci --emulate %s -o %t && %t | FileCheck %s; fi diff --git a/targettests/execution/if_jit.cpp b/targettests/execution/if_jit.cpp index 7f3eb72205d..9bc39c2e3be 100644 --- a/targettests/execution/if_jit.cpp +++ b/targettests/execution/if_jit.cpp @@ -14,6 +14,7 @@ // RUN: nvq++ --target ionq --emulate %s -o %t && %t | FileCheck %s // RUN: nvq++ --target iqm --emulate %s -o %t && IQM_QPU_QA=%iqm_tests_dir/Crystal_5.txt %t | FileCheck %s // RUN: nvq++ --target oqc --emulate %s -o %t && %t | FileCheck %s +// RUN: nvq++ --target qbraid --emulate %s -o %t && %t | FileCheck %s // RUN: nvq++ --target quantinuum --emulate %s -o %t && %t | FileCheck %s // RUN: if %braket_avail; then nvq++ --target braket --emulate %s -o %t && %t | FileCheck %s; fi // RUN: if %qci_avail; then nvq++ --target qci --emulate %s -o %t && %t | FileCheck %s; fi diff --git a/targettests/execution/int8_t.cpp b/targettests/execution/int8_t.cpp index 8323b5f0acb..d38bfd799d7 100644 --- a/targettests/execution/int8_t.cpp +++ b/targettests/execution/int8_t.cpp @@ -12,6 +12,7 @@ // RUN: nvq++ --target ionq --emulate %s -o %t && %t | FileCheck %s // RUN: nvq++ --target iqm --emulate %s -o %t && IQM_QPU_QA=%iqm_tests_dir/Crystal_5.txt %t | FileCheck %s // RUN: nvq++ --target oqc --emulate %s -o %t && %t | FileCheck %s +// RUN: nvq++ --target qbraid --emulate %s -o %t && %t | FileCheck %s // RUN: nvq++ --target quantinuum --emulate %s -o %t && %t | FileCheck %s // RUN: if %braket_avail; then nvq++ --target braket --emulate %s -o %t && %t | FileCheck %s; fi // RUN: if %qci_avail; then nvq++ --target qci --emulate %s -o %t && %t | FileCheck %s; fi diff --git a/targettests/execution/int8_t_free_func.cpp b/targettests/execution/int8_t_free_func.cpp index 0cf8f4bd156..8a7642813aa 100644 --- a/targettests/execution/int8_t_free_func.cpp +++ b/targettests/execution/int8_t_free_func.cpp @@ -12,6 +12,7 @@ // RUN: nvq++ --target ionq --emulate %s -o %t && %t | FileCheck %s // RUN: nvq++ --target iqm --emulate %s -o %t && IQM_QPU_QA=%iqm_tests_dir/Crystal_5.txt %t | FileCheck %s // RUN: nvq++ --target oqc --emulate %s -o %t && %t | FileCheck %s +// RUN: nvq++ --target qbraid --emulate %s -o %t && %t | FileCheck %s // RUN: nvq++ --target quantinuum --emulate %s -o %t && %t | FileCheck %s // RUN: if %braket_avail; then nvq++ --target braket --emulate %s -o %t && %t | FileCheck %s; fi // RUN: if %qci_avail; then nvq++ --target qci --emulate %s -o %t && %t | FileCheck %s; fi diff --git a/targettests/execution/load_value.cpp b/targettests/execution/load_value.cpp index 1d1412980b7..46513671e84 100644 --- a/targettests/execution/load_value.cpp +++ b/targettests/execution/load_value.cpp @@ -12,6 +12,7 @@ // RUN: nvq++ --target ionq --emulate %s -o %t && %t | FileCheck %s // RUN: nvq++ --target iqm --emulate %s -o %t && IQM_QPU_QA=%iqm_tests_dir/Crystal_5.txt %t | FileCheck %s // RUN: nvq++ --target oqc --emulate %s -o %t && %t | FileCheck %s +// RUN: nvq++ --target qbraid --emulate %s -o %t && %t | FileCheck %s // RUN: nvq++ --target quantinuum --emulate %s -o %t && %t | FileCheck %s // RUN: if %braket_avail; then nvq++ --target braket --emulate %s -o %t && %t | FileCheck %s; fi // RUN: if %qci_avail; then nvq++ --target qci --emulate %s -o %t && %t | FileCheck %s; fi diff --git a/targettests/execution/sudoku_2x2-1.cpp b/targettests/execution/sudoku_2x2-1.cpp index 0ee64a18855..df05df508fa 100644 --- a/targettests/execution/sudoku_2x2-1.cpp +++ b/targettests/execution/sudoku_2x2-1.cpp @@ -12,6 +12,7 @@ // RUN: nvq++ --target ionq --emulate %s -o %t && %t | FileCheck %s // RUN: nvq++ --target iqm --emulate %s -o %t && IQM_QPU_QA=%iqm_tests_dir/Crystal_20.txt %t | FileCheck %s // RUN: nvq++ --target oqc --emulate %s -o %t && %t | FileCheck %s +// RUN: nvq++ --target qbraid --emulate %s -o %t && %t | FileCheck %s // RUN: nvq++ --target quantinuum --emulate %s -o %t && %t | FileCheck %s // RUN: if %qci_avail; then nvq++ --target qci --emulate %s -o %t && %t | FileCheck %s; fi // clang-format on diff --git a/targettests/execution/sudoku_2x2-bit_name.cpp b/targettests/execution/sudoku_2x2-bit_name.cpp index 809e237dda3..5ecff676380 100644 --- a/targettests/execution/sudoku_2x2-bit_name.cpp +++ b/targettests/execution/sudoku_2x2-bit_name.cpp @@ -12,6 +12,7 @@ // RUN: nvq++ --target ionq --emulate %s -o %t && %t | FileCheck %s // RUN: nvq++ --target iqm --emulate %s -o %t && IQM_QPU_QA=%iqm_tests_dir/Crystal_20.txt %t | FileCheck %s // RUN: nvq++ --target oqc --emulate %s -o %t && %t | FileCheck %s +// RUN: nvq++ --target qbraid --emulate %s -o %t && %t | FileCheck %s // RUN: nvq++ --target quantinuum --emulate %s -o %t && %t | FileCheck %s // RUN: if %qci_avail; then nvq++ --target qci --emulate %s -o %t && %t | FileCheck %s; fi // clang-format on diff --git a/targettests/execution/sudoku_2x2-reg_name.cpp b/targettests/execution/sudoku_2x2-reg_name.cpp index a75e6f04d0e..6fc79267b65 100644 --- a/targettests/execution/sudoku_2x2-reg_name.cpp +++ b/targettests/execution/sudoku_2x2-reg_name.cpp @@ -12,6 +12,7 @@ // RUN: nvq++ --target ionq --emulate %s -o %t && %t | FileCheck %s // RUN: nvq++ --target iqm --emulate %s -o %t && IQM_QPU_QA=%iqm_tests_dir/Crystal_20.txt %t | FileCheck %s // RUN: nvq++ --target oqc --emulate %s -o %t && %t | FileCheck %s +// RUN: nvq++ --target qbraid --emulate %s -o %t && %t | FileCheck %s // RUN: nvq++ --target quantinuum --emulate %s -o %t && %t | FileCheck %s // RUN: if %qci_avail; then nvq++ --target qci --emulate %s -o %t && %t | FileCheck %s; fi // clang-format on diff --git a/targettests/execution/sudoku_2x2.cpp b/targettests/execution/sudoku_2x2.cpp index ff3906f2595..b86eddcbead 100644 --- a/targettests/execution/sudoku_2x2.cpp +++ b/targettests/execution/sudoku_2x2.cpp @@ -12,6 +12,7 @@ // RUN: nvq++ --target ionq --emulate %s -o %t && %t | FileCheck %s // RUN: nvq++ --target iqm --emulate %s -o %t && IQM_QPU_QA=%iqm_tests_dir/Crystal_20.txt %t | FileCheck %s // RUN: nvq++ --target oqc --emulate %s -o %t && %t | FileCheck %s +// RUN: nvq++ --target qbraid --emulate %s -o %t && %t | FileCheck %s // RUN: nvq++ --target quantinuum --emulate %s -o %t && %t | FileCheck %s // RUN: if %qci_avail; then nvq++ --target qci --emulate %s -o %t && %t | FileCheck %s; fi // clang-format on diff --git a/targettests/execution/swap_gate.cpp b/targettests/execution/swap_gate.cpp index e9d8092dd56..e836b58f99a 100644 --- a/targettests/execution/swap_gate.cpp +++ b/targettests/execution/swap_gate.cpp @@ -12,6 +12,7 @@ // RUN: nvq++ --target ionq --emulate %s -o %t && %t | FileCheck %s // RUN: nvq++ --target iqm --emulate %s -o %t && IQM_QPU_QA=%iqm_tests_dir/Crystal_5.txt %t | FileCheck %s // RUN: nvq++ --target oqc --emulate %s -o %t && %t | FileCheck %s +// RUN: nvq++ --target qbraid --emulate %s -o %t && %t | FileCheck %s // RUN: nvq++ --target quantinuum --emulate %s -o %t && %t | FileCheck %s // RUN: if %braket_avail; then nvq++ --target braket --emulate %s -o %t && %t | FileCheck %s; fi // RUN: if %qci_avail; then nvq++ --target qci --emulate %s -o %t && %t | FileCheck %s; fi diff --git a/targettests/execution/variable_size_qreg.cpp b/targettests/execution/variable_size_qreg.cpp index 9844855ffc8..9d6f35f0adc 100644 --- a/targettests/execution/variable_size_qreg.cpp +++ b/targettests/execution/variable_size_qreg.cpp @@ -12,6 +12,7 @@ // RUN: nvq++ --target ionq --emulate %s -o %t && %t | FileCheck %s // RUN: nvq++ --target iqm --emulate %s -o %t && IQM_QPU_QA=%iqm_tests_dir/Crystal_5.txt %t | FileCheck %s // RUN: nvq++ --target oqc --emulate %s -o %t && %t | FileCheck %s +// RUN: nvq++ --target qbraid --emulate %s -o %t && %t | FileCheck %s // RUN: nvq++ --target quantinuum --emulate %s -o %t && %t | FileCheck %s // RUN: if %braket_avail; then nvq++ --target braket --emulate %s -o %t && %t | FileCheck %s; fi // RUN: if %qci_avail; then nvq++ --target qci --emulate %s -o %t && %t | FileCheck %s; fi diff --git a/targettests/qbraid/bug_qubit.cpp b/targettests/qbraid/bug_qubit.cpp new file mode 100644 index 00000000000..05533521413 --- /dev/null +++ b/targettests/qbraid/bug_qubit.cpp @@ -0,0 +1,10 @@ +/******************************************************************************* + * Copyright (c) 2022 - 2026 NVIDIA Corporation & Affiliates. * + * All rights reserved. * + * * + * This source code and the accompanying materials are made available under * + * the terms of the Apache License 2.0 which accompanies this distribution. * + ******************************************************************************/ + +// RUN: echo skipping +#include "../execution/bug_qubit.cpp" diff --git a/targettests/qbraid/callable_kernel_arg.cpp b/targettests/qbraid/callable_kernel_arg.cpp new file mode 100644 index 00000000000..7a6ca74ee20 --- /dev/null +++ b/targettests/qbraid/callable_kernel_arg.cpp @@ -0,0 +1,10 @@ +/******************************************************************************* + * Copyright (c) 2022 - 2026 NVIDIA Corporation & Affiliates. * + * All rights reserved. * + * * + * This source code and the accompanying materials are made available under * + * the terms of the Apache License 2.0 which accompanies this distribution. * + ******************************************************************************/ + +// RUN: echo skipping +#include "../execution/callable_kernel_arg.cpp" diff --git a/targettests/qbraid/cudaq_observe.cpp b/targettests/qbraid/cudaq_observe.cpp new file mode 100644 index 00000000000..1b75a817e14 --- /dev/null +++ b/targettests/qbraid/cudaq_observe.cpp @@ -0,0 +1,10 @@ +/******************************************************************************* + * Copyright (c) 2022 - 2026 NVIDIA Corporation & Affiliates. * + * All rights reserved. * + * * + * This source code and the accompanying materials are made available under * + * the terms of the Apache License 2.0 which accompanies this distribution. * + ******************************************************************************/ + +// RUN: echo skipping +#include "../execution/cudaq_observe.cpp" diff --git a/targettests/qbraid/if_jit.cpp b/targettests/qbraid/if_jit.cpp new file mode 100644 index 00000000000..3e916bb1e88 --- /dev/null +++ b/targettests/qbraid/if_jit.cpp @@ -0,0 +1,10 @@ +/******************************************************************************* + * Copyright (c) 2022 - 2026 NVIDIA Corporation & Affiliates. * + * All rights reserved. * + * * + * This source code and the accompanying materials are made available under * + * the terms of the Apache License 2.0 which accompanies this distribution. * + ******************************************************************************/ + +// RUN: echo skipping +#include "../execution/if_jit.cpp" diff --git a/targettests/qbraid/int8_t.cpp b/targettests/qbraid/int8_t.cpp new file mode 100644 index 00000000000..2c6751705ec --- /dev/null +++ b/targettests/qbraid/int8_t.cpp @@ -0,0 +1,10 @@ +/******************************************************************************* + * Copyright (c) 2022 - 2026 NVIDIA Corporation & Affiliates. * + * All rights reserved. * + * * + * This source code and the accompanying materials are made available under * + * the terms of the Apache License 2.0 which accompanies this distribution. * + ******************************************************************************/ + +// RUN: echo skipping +#include "../execution/int8_t.cpp" diff --git a/targettests/qbraid/int8_t_free_func.cpp b/targettests/qbraid/int8_t_free_func.cpp new file mode 100644 index 00000000000..7a29487abbb --- /dev/null +++ b/targettests/qbraid/int8_t_free_func.cpp @@ -0,0 +1,10 @@ +/******************************************************************************* + * Copyright (c) 2022 - 2026 NVIDIA Corporation & Affiliates. * + * All rights reserved. * + * * + * This source code and the accompanying materials are made available under * + * the terms of the Apache License 2.0 which accompanies this distribution. * + ******************************************************************************/ + +// RUN: echo skipping +#include "../execution/int8_t_free_func.cpp" diff --git a/targettests/qbraid/load_value.cpp b/targettests/qbraid/load_value.cpp new file mode 100644 index 00000000000..e1aee9db9b5 --- /dev/null +++ b/targettests/qbraid/load_value.cpp @@ -0,0 +1,10 @@ +/******************************************************************************* + * Copyright (c) 2022 - 2026 NVIDIA Corporation & Affiliates. * + * All rights reserved. * + * * + * This source code and the accompanying materials are made available under * + * the terms of the Apache License 2.0 which accompanies this distribution. * + ******************************************************************************/ + +// RUN: echo skipping +#include "../execution/load_value.cpp" diff --git a/targettests/qbraid/sudoku_2x2-1.cpp b/targettests/qbraid/sudoku_2x2-1.cpp new file mode 100644 index 00000000000..3fae8d26e6c --- /dev/null +++ b/targettests/qbraid/sudoku_2x2-1.cpp @@ -0,0 +1,10 @@ +/******************************************************************************* + * Copyright (c) 2022 - 2026 NVIDIA Corporation & Affiliates. * + * All rights reserved. * + * * + * This source code and the accompanying materials are made available under * + * the terms of the Apache License 2.0 which accompanies this distribution. * + ******************************************************************************/ + +// RUN: echo skipping +#include "../execution/sudoku_2x2-1.cpp" diff --git a/targettests/qbraid/sudoku_2x2-bit_name.cpp b/targettests/qbraid/sudoku_2x2-bit_name.cpp new file mode 100644 index 00000000000..f875955b7be --- /dev/null +++ b/targettests/qbraid/sudoku_2x2-bit_name.cpp @@ -0,0 +1,10 @@ +/******************************************************************************* + * Copyright (c) 2022 - 2026 NVIDIA Corporation & Affiliates. * + * All rights reserved. * + * * + * This source code and the accompanying materials are made available under * + * the terms of the Apache License 2.0 which accompanies this distribution. * + ******************************************************************************/ + +// RUN: echo skipping +#include "../execution/sudoku_2x2-bit_name.cpp" diff --git a/targettests/qbraid/sudoku_2x2-reg_name.cpp b/targettests/qbraid/sudoku_2x2-reg_name.cpp new file mode 100644 index 00000000000..17a48caec48 --- /dev/null +++ b/targettests/qbraid/sudoku_2x2-reg_name.cpp @@ -0,0 +1,10 @@ +/******************************************************************************* + * Copyright (c) 2022 - 2026 NVIDIA Corporation & Affiliates. * + * All rights reserved. * + * * + * This source code and the accompanying materials are made available under * + * the terms of the Apache License 2.0 which accompanies this distribution. * + ******************************************************************************/ + +// RUN: echo skipping +#include "../execution/sudoku_2x2-reg_name.cpp" diff --git a/targettests/qbraid/sudoku_2x2.cpp b/targettests/qbraid/sudoku_2x2.cpp new file mode 100644 index 00000000000..090b230072a --- /dev/null +++ b/targettests/qbraid/sudoku_2x2.cpp @@ -0,0 +1,10 @@ +/******************************************************************************* + * Copyright (c) 2022 - 2026 NVIDIA Corporation & Affiliates. * + * All rights reserved. * + * * + * This source code and the accompanying materials are made available under * + * the terms of the Apache License 2.0 which accompanies this distribution. * + ******************************************************************************/ + +// RUN: echo skipping +#include "../execution/sudoku_2x2.cpp" diff --git a/targettests/qbraid/swap_gate.cpp b/targettests/qbraid/swap_gate.cpp new file mode 100644 index 00000000000..c592ce69b31 --- /dev/null +++ b/targettests/qbraid/swap_gate.cpp @@ -0,0 +1,10 @@ +/******************************************************************************* + * Copyright (c) 2022 - 2026 NVIDIA Corporation & Affiliates. * + * All rights reserved. * + * * + * This source code and the accompanying materials are made available under * + * the terms of the Apache License 2.0 which accompanies this distribution. * + ******************************************************************************/ + +// RUN: echo skipping +#include "../execution/swap_gate.cpp" diff --git a/targettests/qbraid/variable_size_qreg.cpp b/targettests/qbraid/variable_size_qreg.cpp new file mode 100644 index 00000000000..cc4845f4df9 --- /dev/null +++ b/targettests/qbraid/variable_size_qreg.cpp @@ -0,0 +1,10 @@ +/******************************************************************************* + * Copyright (c) 2022 - 2026 NVIDIA Corporation & Affiliates. * + * All rights reserved. * + * * + * This source code and the accompanying materials are made available under * + * the terms of the Apache License 2.0 which accompanies this distribution. * + ******************************************************************************/ + +// RUN: echo skipping +#include "../execution/variable_size_qreg.cpp" diff --git a/unittests/backends/CMakeLists.txt b/unittests/backends/CMakeLists.txt index b5d5fb3241a..d04c69fc46d 100644 --- a/unittests/backends/CMakeLists.txt +++ b/unittests/backends/CMakeLists.txt @@ -8,14 +8,14 @@ # List of libraries to link with by default to create a test executable set(default_backend_unittest_libs - fmt::fmt-header-only - cudaq-common + fmt::fmt-header-only + cudaq-common cudaq cudaq-builder cudaq-mlir-runtime cudaq-operator nvqir nvqir-qpp - cudaq-platform-default + cudaq-platform-default gtest_main) if (CUDAQ_ENABLE_REST) list(APPEND default_backend_unittest_libs cudaq-rest-qpu) @@ -34,12 +34,12 @@ set_property(DIRECTORY PROPERTY BACKEND_UNITTEST_LIBS ${default_backend_unittest # Helper function to create an executable to be used by the gtest unit tests # - target: positional argument, name of the executable # - BACKEND: named argument to specify a prefix for the test names -# - BACKEND_CONFIG: if present, the test will set NVQPP_TARGET_BACKEND_CONFIG +# - BACKEND_CONFIG: if present, the test will set NVQPP_TARGET_BACKEND_CONFIG # with this value so the backend gets loaded by a constructor before entering main. # To avoid issues with semicolon the format is: backend key1=value1 key2=value2 # The function will convert this to : backend;key1;value1;key2;value2 # Example: infleqtion emulate=false url=http://localhost:62447 -# - LINK_LIBS: optional argument to provide non-default list of libraries to link with +# - LINK_LIBS: optional argument to provide non-default list of libraries to link with function(add_backend_unittest_executable target) set(singleValues BACKEND BACKEND_CONFIG) set(multiValues SOURCES INCLUDES LINK_LIBS) @@ -99,6 +99,9 @@ if (OPENSSL_FOUND AND CUDAQ_ENABLE_PYTHON AND CUDAQ_TEST_MOCK_SERVERS) if (CUDAQ_ENABLE_SCALEWAY_BACKEND) add_subdirectory(scaleway) endif() + if (CUDAQ_ENABLE_QBRAID_BACKEND) + add_subdirectory(qbraid) + endif() add_subdirectory(extra_payload_provider) add_subdirectory(quake_backend) endif() diff --git a/unittests/backends/qbraid/CMakeLists.txt b/unittests/backends/qbraid/CMakeLists.txt new file mode 100644 index 00000000000..e1e7a1c07e7 --- /dev/null +++ b/unittests/backends/qbraid/CMakeLists.txt @@ -0,0 +1,16 @@ +# ============================================================================ # +# Copyright (c) 2022 - 2026 NVIDIA Corporation & Affiliates. # +# All rights reserved. # +# # +# This source code and the accompanying materials are made available under # +# the terms of the Apache License 2.0 which accompanies this distribution. # +# ============================================================================ # + +add_backend_unittest_executable(test_qbraid + SOURCES QbraidTester.cpp + BACKEND qbraid + BACKEND_CONFIG "qbraid emulate=false url=http://localhost:62454 api_key=00000000000000000000000000000000" +) + +configure_file("QbraidStartServerAndTest.sh.in" "${CMAKE_BINARY_DIR}/unittests/backends/qbraid/QbraidStartServerAndTest.sh" @ONLY) +add_test(NAME qbraid-tests COMMAND bash QbraidStartServerAndTest.sh WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/unittests/backends/qbraid/) diff --git a/unittests/backends/qbraid/QbraidStartServerAndTest.sh.in b/unittests/backends/qbraid/QbraidStartServerAndTest.sh.in new file mode 100644 index 00000000000..4221dc109fa --- /dev/null +++ b/unittests/backends/qbraid/QbraidStartServerAndTest.sh.in @@ -0,0 +1,47 @@ +#!/bin/bash + +# ============================================================================ # +# Copyright (c) 2022 - 2026 NVIDIA Corporation & Affiliates. # +# All rights reserved. # +# # +# This source code and the accompanying materials are made available under # +# the terms of the Apache License 2.0 which accompanies this distribution. # +# ============================================================================ # + +checkServerConnection() { + PYTHONPATH=@CMAKE_BINARY_DIR@/python @Python_EXECUTABLE@ - << EOF +import socket +try: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.connect(("localhost", 62454)) + s.close() +except Exception: + exit(1) +EOF +} + +# Launch the fake server +PYTHONPATH=@CMAKE_BINARY_DIR@/python @Python_EXECUTABLE@ @CMAKE_SOURCE_DIR@/utils/mock_qpu/qbraid/__init__.py & +# we'll need the process id to kill it +pid=$(echo "$!") +n=0 +while ! checkServerConnection; do + sleep 1 + n=$((n+1)) + if [ "$n" -eq "10" ]; then + kill -INT $pid + exit 99 + fi +done +# api_key is passed via the backend config (see CMakeLists BACKEND_CONFIG), +# so we unset QBRAID_API_KEY to force the helper to use the config value. +# checkApiKeyFromTarget asserts the env var is null. +unset QBRAID_API_KEY +# Run the tests +./test_qbraid +# Did they fail? +testsPassed=$? +# kill the server +kill -INT $pid +# return success / failure +exit $testsPassed diff --git a/unittests/backends/qbraid/QbraidTester.cpp b/unittests/backends/qbraid/QbraidTester.cpp new file mode 100644 index 00000000000..9ce3e6d49c6 --- /dev/null +++ b/unittests/backends/qbraid/QbraidTester.cpp @@ -0,0 +1,309 @@ +/******************************************************************************* + * Copyright (c) 2022 - 2026 NVIDIA Corporation & Affiliates. * + * All rights reserved. * + * * + * This source code and the accompanying materials are made available under * + * the terms of the Apache License 2.0 which accompanies this distribution. * + ******************************************************************************/ + +#include "CUDAQTestUtils.h" +#include "common/FmtCore.h" +#include "common/RestClient.h" +#include "cudaq/algorithm.h" +#include +#include +#include +#include + +// Update the backend string to match the QBraid format +std::string mockPort = "62454"; +std::string backendStringTemplate = + "qbraid;emulate;false;url;http://localhost:{}"; + +bool isValidExpVal(double value) { + // The qbraid mock server doesn't simulate quantum mechanics - X0X1 counts + // are uniform random per 1000-shot sample (std dev ~0.03), so the + // expectation value for this VQE Hamiltonian fluctuates around -2.14 by + // a few hundredths per run. The band below is wide enough (~10 sigma) to + // be stable across test runs while still catching corrupt / NaN results. + return value < -1.0 && value > -3.0; +} + +CUDAQ_TEST(QbraidTester, checkSampleSync) { + auto kernel = cudaq::make_kernel(); + auto qubit = kernel.qalloc(2); + kernel.h(qubit[0]); + kernel.mz(qubit[0]); + + auto counts = cudaq::sample(kernel); + counts.dump(); + EXPECT_EQ(counts.size(), 2); +} + +CUDAQ_TEST(QbraidTester, checkSampleAsync) { + auto kernel = cudaq::make_kernel(); + auto qubit = kernel.qalloc(2); + kernel.h(qubit[0]); + kernel.mz(qubit[0]); + + auto future = cudaq::sample_async(kernel); + auto counts = future.get(); + EXPECT_EQ(counts.size(), 2); +} + +CUDAQ_TEST(QbraidTester, checkSampleAsyncLoadFromFile) { + auto kernel = cudaq::make_kernel(); + auto qubit = kernel.qalloc(2); + kernel.h(qubit[0]); + kernel.mz(qubit[0]); + + auto future = cudaq::sample_async(kernel); + { + std::ofstream out("saveMe.json"); + out << future; + } + + cudaq::async_result readIn; + std::ifstream in("saveMe.json"); + in >> readIn; + + auto counts = readIn.get(); + EXPECT_EQ(counts.size(), 2); + + std::remove("saveMe.json"); +} + +CUDAQ_TEST(QbraidTester, checkObserveSync) { + auto [kernel, theta] = cudaq::make_kernel(); + auto qubit = kernel.qalloc(2); + kernel.x(qubit[0]); + kernel.ry(theta, qubit[1]); + kernel.x(qubit[1], qubit[0]); + + using namespace cudaq::spin; + cudaq::spin_op h = 5.907 - 2.1433 * x(0) * x(1) - 2.1433 * y(0) * y(1) + + .21829 * z(0) - 6.125 * z(1); + auto result = cudaq::observe(kernel, h, .59); + result.dump(); + + printf("ENERGY: %lf\n", result.expectation()); + EXPECT_TRUE(isValidExpVal(result.expectation())); +} + +CUDAQ_TEST(QbraidTester, checkObserveAsync) { + auto [kernel, theta] = cudaq::make_kernel(); + auto qubit = kernel.qalloc(2); + kernel.x(qubit[0]); + kernel.ry(theta, qubit[1]); + kernel.x(qubit[1], qubit[0]); + + using namespace cudaq::spin; + cudaq::spin_op h = 5.907 - 2.1433 * x(0) * x(1) - 2.1433 * y(0) * y(1) + + .21829 * z(0) - 6.125 * z(1); + auto future = cudaq::observe_async(kernel, h, .59); + + auto result = future.get(); + result.dump(); + + printf("ENERGY: %lf\n", result.expectation()); + EXPECT_TRUE(isValidExpVal(result.expectation())); +} + +CUDAQ_TEST(QbraidTester, checkObserveAsyncLoadFromFile) { + auto [kernel, theta] = cudaq::make_kernel(); + auto qubit = kernel.qalloc(2); + kernel.x(qubit[0]); + kernel.ry(theta, qubit[1]); + kernel.x(qubit[1], qubit[0]); + + using namespace cudaq::spin; + cudaq::spin_op h = 5.907 - 2.1433 * x(0) * x(1) - 2.1433 * y(0) * y(1) + + .21829 * z(0) - 6.125 * z(1); + auto future = cudaq::observe_async(kernel, h, .59); + + { + std::ofstream out("saveMeObserve.json"); + out << future; + } + + cudaq::async_result readIn(&h); + std::ifstream in("saveMeObserve.json"); + in >> readIn; + + auto result = readIn.get(); + + std::remove("saveMeObserve.json"); + result.dump(); + + printf("ENERGY: %lf\n", result.expectation()); + EXPECT_TRUE(isValidExpVal(result.expectation())); +} + +// Every test in this file runs through the backend configured by +// add_backend_unittest_executable in CMakeLists, which passes api_key via the +// target config (BACKEND_CONFIG). QBRAID_API_KEY env var is NOT set by the +// launch script, so a successful sample here exercises the target-arg path. +CUDAQ_TEST(QbraidTester, checkApiKeyFromTarget) { + ASSERT_EQ(std::getenv("QBRAID_API_KEY"), nullptr) + << "QBRAID_API_KEY should not be set; this test verifies the " + "api_key=... target-arg path."; + + auto kernel = cudaq::make_kernel(); + auto qubit = kernel.qalloc(2); + kernel.h(qubit[0]); + kernel.mz(qubit[0]); + + auto counts = cudaq::sample(kernel); + EXPECT_GE(counts.size(), 1u); +} + +CUDAQ_TEST(QbraidTester, checkJobFailure) { + // Arm the mock to fail the next submitted job. + cudaq::RestClient client; + nlohmann::json body = nlohmann::json::object(); + std::map headers; + auto armed = client.post("http://localhost:62454/", "test/fail_next", body, + headers, /*enableLogging=*/false); + ASSERT_TRUE(armed.value("armed", false)); + + auto kernel = cudaq::make_kernel(); + auto qubit = kernel.qalloc(2); + kernel.h(qubit[0]); + kernel.mz(qubit[0]); + + EXPECT_ANY_THROW({ (void)cudaq::sample(kernel); }); +} + +// Arm the mock to make the next N /result calls return "not yet available", +// so processResults must retry. maxRetries is 3, so 2 delays should succeed. +CUDAQ_TEST(QbraidTester, checkResultRetry) { + cudaq::RestClient client; + nlohmann::json body = nlohmann::json::object(); + std::map headers; + auto armed = + client.post("http://localhost:62454/", "test/delay_next_results/2", body, + headers, /*enableLogging=*/false); + ASSERT_EQ(armed.value("remaining", -1), 2); + + auto kernel = cudaq::make_kernel(); + auto qubit = kernel.qalloc(2); + kernel.h(qubit[0]); + kernel.mz(qubit[0]); + + auto counts = cudaq::sample(kernel); + EXPECT_GE(counts.size(), 1u); +} + +// Arm enough delays to exhaust the retry budget (maxRetries = 3). Sample must +// throw. Uses 10 so the retry loop can never succeed. +CUDAQ_TEST(QbraidTester, checkResultRetryExhaustion) { + cudaq::RestClient client; + nlohmann::json body = nlohmann::json::object(); + std::map headers; + auto armed = + client.post("http://localhost:62454/", "test/delay_next_results/10", body, + headers, /*enableLogging=*/false); + ASSERT_EQ(armed.value("remaining", -1), 10); + + auto kernel = cudaq::make_kernel(); + auto qubit = kernel.qalloc(2); + kernel.h(qubit[0]); + kernel.mz(qubit[0]); + + EXPECT_ANY_THROW({ (void)cudaq::sample(kernel); }); +} + +// Helper: arm the mock to return a specific HTTP status on the next /result. +// Resets prior test-hook state first so the test is order-independent. +static void armResultStatus(int code) { + cudaq::RestClient client; + nlohmann::json body = nlohmann::json::object(); + std::map headers; + (void)client.post("http://localhost:62454/", "test/reset", body, headers, + /*enableLogging=*/false); + auto armed = + client.post("http://localhost:62454/", + "test/force_next_result_status/" + std::to_string(code), body, + headers, /*enableLogging=*/false); + ASSERT_EQ(armed.value("armed_status", -1), code); +} + +// Helper: match a substring in the exception message. +static ::testing::AssertionResult throwsWithMessage(std::function fn, + const std::string &needle) { + try { + fn(); + } catch (const std::exception &e) { + std::string what = e.what(); + if (what.find(needle) != std::string::npos) + return ::testing::AssertionSuccess(); + return ::testing::AssertionFailure() + << "exception message did not contain '" << needle << "'. Actual: '" + << what << "'"; + } + return ::testing::AssertionFailure() << "expected exception, none thrown"; +} + +// 401 on /result -> terminal auth failure, message must name the status. +CUDAQ_TEST(QbraidTester, checkResultAuthFailure) { + armResultStatus(401); + auto kernel = cudaq::make_kernel(); + auto qubit = kernel.qalloc(2); + kernel.h(qubit[0]); + kernel.mz(qubit[0]); + EXPECT_TRUE(throwsWithMessage([&]() { (void)cudaq::sample(kernel); }, + "authentication failed")); +} + +// 403 on /result -> same terminal auth failure translation as 401. +CUDAQ_TEST(QbraidTester, checkResultForbidden) { + armResultStatus(403); + auto kernel = cudaq::make_kernel(); + auto qubit = kernel.qalloc(2); + kernel.h(qubit[0]); + kernel.mz(qubit[0]); + EXPECT_TRUE(throwsWithMessage([&]() { (void)cudaq::sample(kernel); }, + "authentication failed")); +} + +// 404 on /result -> terminal "not found", message must mention the job id. +CUDAQ_TEST(QbraidTester, checkResultNotFound) { + armResultStatus(404); + auto kernel = cudaq::make_kernel(); + auto qubit = kernel.qalloc(2); + kernel.h(qubit[0]); + kernel.mz(qubit[0]); + EXPECT_TRUE(throwsWithMessage([&]() { (void)cudaq::sample(kernel); }, + "result not found")); +} + +// 409 on /result -> terminal. qBraid v2 returns this when the job reached a +// non-success terminal state (FAILED or CANCELLED), so results will never +// appear and the helper must fail fast instead of burning the retry budget. +CUDAQ_TEST(QbraidTester, checkResultConflict) { + armResultStatus(409); + auto kernel = cudaq::make_kernel(); + auto qubit = kernel.qalloc(2); + kernel.h(qubit[0]); + kernel.mz(qubit[0]); + EXPECT_TRUE(throwsWithMessage([&]() { (void)cudaq::sample(kernel); }, + "did not produce results")); +} + +// 500 on /result -> retryable. Force hook fires once then clears, so the +// second attempt succeeds. Sampling must not throw. +CUDAQ_TEST(QbraidTester, checkResultServerErrorRetries) { + armResultStatus(500); + auto kernel = cudaq::make_kernel(); + auto qubit = kernel.qalloc(2); + kernel.h(qubit[0]); + kernel.mz(qubit[0]); + auto counts = cudaq::sample(kernel); + EXPECT_GE(counts.size(), 1u); +} + +int main(int argc, char **argv) { + ::testing::InitGoogleTest(&argc, argv); + auto ret = RUN_ALL_TESTS(); + return ret; +} diff --git a/utils/mock_qpu/__init__.py b/utils/mock_qpu/__init__.py index 8167902c1e1..c508a32c796 100644 --- a/utils/mock_qpu/__init__.py +++ b/utils/mock_qpu/__init__.py @@ -21,6 +21,7 @@ "qci": 62449, "scaleway": 62450, "tii": 62451, + "qbraid": 62452, } diff --git a/utils/mock_qpu/qbraid/__init__.py b/utils/mock_qpu/qbraid/__init__.py new file mode 100644 index 00000000000..7fea100bec5 --- /dev/null +++ b/utils/mock_qpu/qbraid/__init__.py @@ -0,0 +1,347 @@ +# ============================================================================ # +# Copyright (c) 2022 - 2026 NVIDIA Corporation & Affiliates. # +# All rights reserved. # +# # +# This source code and the accompanying materials are made available under # +# the terms of the Apache License 2.0 which accompanies this distribution. # +# ============================================================================ # + +import itertools +import random +import re +import uuid +from typing import Any, Optional + +import uvicorn +from fastapi import FastAPI, Header, HTTPException, Path +from pydantic import BaseModel + +app = FastAPI() + + +class Program(BaseModel): + """Structured program payload for v2 API.""" + + format: str + data: str + + +class Job(BaseModel): + """Data required to submit a quantum job (v2 API).""" + + program: Program + shots: int + deviceQrn: str + name: Optional[str] = None + tags: Optional[dict] = None + + +JOBS_MOCK_DB = {} +JOBS_MOCK_RESULTS = {} +# Testing toggle: when True, the next job submitted via POST /jobs is created +# with status FAILED. Consumed (reset to False) after use. +FAIL_NEXT_JOB = {"enabled": False} +# Testing counter: how many upcoming GET /jobs/{id}/result calls should return +# success=false (simulating the qbraid v2 race where status=COMPLETED before +# results are queryable). Decrements on each /result call until 0. +DELAY_RESULTS_COUNT = {"remaining": 0} +# Testing hook: when set, the next GET /jobs/{id}/result call raises the given +# HTTP status. Consumed (reset to None) after one call. Used to exercise the +# helper's 401/403/404/5xx handling paths. +FORCE_NEXT_RESULT_STATUS = {"code": None} + + +def count_qubits(qasm: str) -> int: + """Extracts the number of qubits from an OpenQASM string.""" + pattern = r"qreg\s+\w+\[(\d+)\];" + + match = re.search(pattern, qasm) + + if match: + return int(match.group(1)) + + raise ValueError("No qreg declaration found in the OpenQASM string.") + + +def simulate_job(qasm: str, num_shots: int) -> dict[str, int]: + """Simulates a quantum job by generating random measurement outcomes based on the circuit.""" + num_qubits = count_qubits(qasm) + + measured_qubits = [] + + measure_pattern = r"measure\s+(\w+)\[(\d+)\]" + measure_matches = re.findall(measure_pattern, qasm) + + hadamard_pattern = r"h\s+(\w+)\[(\d+)\]" + hadamard_matches = re.findall(hadamard_pattern, qasm) + + superposition_qubits = set() + for _, qubit_idx in hadamard_matches: + superposition_qubits.add(int(qubit_idx)) + + for _, qubit_idx in measure_matches: + measured_qubits.append(int(qubit_idx)) + + if not measured_qubits: + measured_qubits = list(range(num_qubits)) + + result = {} + + possible_states = [] + + if measured_qubits: + # Generate strings of the appropriate length for measured qubits + # For superposition qubits, include both 0 and 1 outcomes + for measured_qubit in measured_qubits: + if measured_qubit in superposition_qubits: + if not possible_states: + possible_states = ["0", "1"] + else: + new_states = [] + for state in possible_states: + new_states.append(state + "0") + new_states.append(state + "1") + possible_states = new_states + else: + if not possible_states: + possible_states = ["0"] + else: + possible_states = [state + "0" for state in possible_states] + + if not possible_states: + if superposition_qubits: + possible_states = ["0", "1"] + else: + possible_states = ["0" * num_qubits] + + distribution = random.choices(possible_states, k=num_shots) + result = {state: distribution.count(state) for state in set(distribution)} + + if (num_qubits == 2 and len(measured_qubits) == 1 and + measured_qubits[0] == 0 and 0 in superposition_qubits): + new_result = {} + total_shots = num_shots + half_shots = total_shots // 2 + + new_result["00"] = random.randint(half_shots - half_shots // 4, + half_shots + half_shots // 4) + new_result["01"] = 0 + new_result["10"] = random.randint(half_shots - half_shots // 4, + half_shots + half_shots // 4) + new_result["11"] = 0 + + remaining = total_shots - (new_result["00"] + new_result["10"]) + if remaining > 0: + new_result["00"] += remaining + + result = {k: v for k, v in new_result.items() if v > 0} + + return result + + +def poll_job_status(job_id: str) -> dict[str, Any]: + """Updates the status of a job and returns the updated job data.""" + if job_id not in JOBS_MOCK_DB: + raise HTTPException(status_code=404, detail="Job not found") + + status = JOBS_MOCK_DB[job_id]["status"] + + status_transitions = { + "INITIALIZING": "QUEUED", + "QUEUED": "RUNNING", + "RUNNING": "COMPLETED", + "CANCELLING": "CANCELLED", + } + + new_status = status_transitions.get(status, status) + JOBS_MOCK_DB[job_id]["status"] = new_status + + return {"jobQrn": job_id, **JOBS_MOCK_DB[job_id]} + + +# v2 API: POST /jobs +@app.post("/jobs") +async def postJob(job: Job, + x_api_key: Optional[str] = Header(None, alias="X-API-KEY")): + """Submit a quantum job for execution (v2 API).""" + if x_api_key is None: + raise HTTPException(status_code=401, detail="API key is required") + + newId = str(uuid.uuid4()) + + # Test hook: fail this job immediately if the toggle was armed. + if FAIL_NEXT_JOB["enabled"]: + FAIL_NEXT_JOB["enabled"] = False + job_data = { + "status": "FAILED", + "statusText": "Triggered failure for testing", + **job.model_dump(), + } + JOBS_MOCK_DB[newId] = job_data + return {"success": True, "data": {"jobQrn": newId, "status": "FAILED"}} + + # Extract QASM from the structured program payload + counts = simulate_job(job.program.data, job.shots) + + job_data = {"status": "INITIALIZING", "statusText": "", **job.model_dump()} + + JOBS_MOCK_DB[newId] = job_data + JOBS_MOCK_RESULTS[newId] = counts + + # v2 response: wrapped in success/data envelope + return { + "success": True, + "data": { + "jobQrn": newId, + "status": "INITIALIZING" + } + } + + +# Test-only: arm a failure for the next submitted job. +@app.post("/test/fail_next") +async def armFailNext(): + FAIL_NEXT_JOB["enabled"] = True + return {"armed": True} + + +# Test-only: force the next N /result calls to return success=false. +@app.post("/test/delay_next_results/{count}") +async def armDelayResults(count: int = Path(...)): + DELAY_RESULTS_COUNT["remaining"] = count + return {"remaining": count} + + +# Test-only: force the next GET /result call to return the given HTTP status. +# Consumed after one call. +@app.post("/test/force_next_result_status/{code}") +async def forceNextResultStatus(code: int = Path(...)): + FORCE_NEXT_RESULT_STATUS["code"] = code + return {"armed_status": code} + + +# Test-only: reset all test-hook state so tests are order-independent. +@app.post("/test/reset") +async def resetTestState(): + FAIL_NEXT_JOB["enabled"] = False + DELAY_RESULTS_COUNT["remaining"] = 0 + FORCE_NEXT_RESULT_STATUS["code"] = None + return {"reset": True} + + +# v2 API: GET /jobs/{job_qrn} +@app.get("/jobs/{job_id}") +async def getJob( + job_id: str = Path(...), + x_api_key: Optional[str] = Header(None, alias="X-API-KEY"), +): + """Retrieve the status of a quantum job (v2 API).""" + if x_api_key is None: + raise HTTPException(status_code=401, detail="API key is required") + + job_data = poll_job_status(job_id) + + # v2 response: wrapped in success/data envelope + return {"success": True, "data": job_data} + + +# v2 API: GET /jobs/{job_qrn}/program +@app.get("/jobs/{job_id}/program") +async def getJobProgram( + job_id: str = Path(...), + x_api_key: Optional[str] = Header(None, alias="X-API-KEY"), +): + """Retrieve the program of a quantum job (v2 API).""" + if x_api_key is None: + raise HTTPException(status_code=401, detail="API key is required") + + if job_id not in JOBS_MOCK_DB: + raise HTTPException(status_code=404, detail="Job not found") + + job_data = JOBS_MOCK_DB[job_id] + + # Return the stored program in v2 format: { success, data: { format, data } } + return { + "success": True, + "data": { + "format": job_data.get("program", {}).get("format", "qasm2"), + "data": job_data.get("program", {}).get("data", ""), + }, + } + + +# v2 API: GET /jobs/{job_qrn}/result +@app.get("/jobs/{job_id}/result") +async def getJobResult( + job_id: str = Path(...), + x_api_key: Optional[str] = Header(None, alias="X-API-KEY"), +): + """Retrieve the results of a quantum job (v2 API).""" + # Test hook: if armed, raise the requested status. Checked first so tests + # can force 401/403 even when a valid api key is present. + if FORCE_NEXT_RESULT_STATUS["code"] is not None: + forced = FORCE_NEXT_RESULT_STATUS["code"] + FORCE_NEXT_RESULT_STATUS["code"] = None + raise HTTPException(status_code=forced, + detail=f"Forced HTTP {forced} for test") + + if x_api_key is None: + raise HTTPException(status_code=401, detail="API key is required") + + if job_id not in JOBS_MOCK_DB: + raise HTTPException(status_code=404, detail="Job not found") + + if JOBS_MOCK_DB[job_id]["status"] in {"FAILED", "CANCELLED"}: + raise HTTPException( + status_code=409, + detail="Results unavailable. Job failed or was cancelled.") + + if JOBS_MOCK_DB[job_id]["status"] != "COMPLETED": + # v2: use success=false instead of "error" field + return { + "success": False, + "data": { + "status": JOBS_MOCK_DB[job_id]["status"] + }, + } + + if job_id not in JOBS_MOCK_RESULTS: + raise HTTPException(status_code=500, detail="Job results not found") + + # Test hook: return "not yet available" for the next N /result calls if + # the delay counter is armed. Decrements on each call. + if DELAY_RESULTS_COUNT["remaining"] > 0: + DELAY_RESULTS_COUNT["remaining"] -= 1 + return { + "success": False, + "data": { + "status": + "COMPLETED", + "message": + "Failed to retrieve job results. Please wait, and try again.", + }, + } + + counts = JOBS_MOCK_RESULTS[job_id] + + # v2 response: measurementCounts nested under data.resultData + return { + "success": True, + "data": { + "resultData": { + "measurementCounts": counts + }, + "status": "COMPLETED", + "cost": 0, + "timeStamps": {}, + }, + } + + +def startServer(port): + """Start the REST server.""" + uvicorn.run(app, port=port, host="0.0.0.0", log_level="info") + + +if __name__ == "__main__": + startServer(62454)