diff --git a/python/tests/backends/test_IonQ.py b/python/tests/backends/test_IonQ.py index 1e510fce264..ed80ca30abb 100644 --- a/python/tests/backends/test_IonQ.py +++ b/python/tests/backends/test_IonQ.py @@ -6,7 +6,7 @@ # the terms of the Apache License 2.0 which accompanies this distribution. # # ============================================================================ # -import cudaq, pytest, os +import cudaq, pytest, os, requests from cudaq import spin import numpy as np from typing import List @@ -362,6 +362,98 @@ def ctrl_z_kernel(): assert counts["0010011"] == 1000 +def test_shot_wise_output_with_memory_and_qpu(): + + url = "http://localhost:{}".format(port) + + # set the mock server target + requests.post(f"{url}/_mock_server_config_target?target=aria-1") + + cudaq.set_target("ionq", url=url, noise='forte-enterprise-1', memory=True) + + @cudaq.kernel + def bell_state(): + qubits = cudaq.qvector(3) + x(qubits[0]) + cx(qubits[0], qubits[1]) + + results = cudaq.sample(bell_state, shots_count=3) + assert (results.get_sequential_data() == ['110', '110', '110']) + + # reset the mock server target + requests.post(f"{url}/_mock_server_config_target?target=") + + +def test_shot_wise_output_with_no_memory_and_qpu(): + + url = "http://localhost:{}".format(port) + + # set the mock server target + requests.post(f"{url}/_mock_server_config_target?target=aria-1") + + cudaq.set_target("ionq", url=url, noise='forte-enterprise-1', memory=False) + + @cudaq.kernel + def bell_state(): + qubits = cudaq.qvector(3) + x(qubits[0]) + cx(qubits[0], qubits[1]) + + results = cudaq.sample(bell_state, shots_count=3) + assert (results.get_sequential_data() == []) + + # reset the mock server target + requests.post(f"{url}/_mock_server_config_target?target=") + + +def test_shot_wise_output_with_memory_and_noise_model(): + + url = "http://localhost:{}".format(port) + + # set the mock server target + requests.post(f"{url}/_mock_server_config_target?target=simulator") + requests.post(f"{url}/_mock_server_config_noise_model?noise=aria-1") + + cudaq.set_target("ionq", url=url, noise='forte-enterprise-1', memory=True) + + @cudaq.kernel + def bell_state(): + qubits = cudaq.qvector(3) + x(qubits[0]) + cx(qubits[0], qubits[1]) + + results = cudaq.sample(bell_state, shots_count=3) + assert (results.get_sequential_data() == ['110', '110', '110']) + + # reset the mock server target + requests.post(f"{url}/_mock_server_config_target?target=") + requests.post(f"{url}/_mock_server_config_noise_model?noise=") + + +def test_shot_wise_output_with_no_memory_and_noise_model(): + + url = "http://localhost:{}".format(port) + + # set the mock server target + requests.post(f"{url}/_mock_server_config_target?target=simulator") + requests.post(f"{url}/_mock_server_config_noise_model?noise=aria-1") + + cudaq.set_target("ionq", url=url, noise='forte-enterprise-1', memory=False) + + @cudaq.kernel + def bell_state(): + qubits = cudaq.qvector(3) + x(qubits[0]) + cx(qubits[0], qubits[1]) + + results = cudaq.sample(bell_state, shots_count=3) + assert (results.get_sequential_data() == []) + + # reset the mock server target + requests.post(f"{url}/_mock_server_config_target?target=") + requests.post(f"{url}/_mock_server_config_noise_model?noise=") + + # leave for gdb debugging if __name__ == "__main__": loc = os.path.abspath(__file__) diff --git a/runtime/cudaq/platform/default/rest/helpers/ionq/IonQServerHelper.cpp b/runtime/cudaq/platform/default/rest/helpers/ionq/IonQServerHelper.cpp index 0225b6fe769..d8b1560e16f 100644 --- a/runtime/cudaq/platform/default/rest/helpers/ionq/IonQServerHelper.cpp +++ b/runtime/cudaq/platform/default/rest/helpers/ionq/IonQServerHelper.cpp @@ -70,6 +70,14 @@ class IonQServerHelper : public ServerHelper { cudaq::sample_result processResults(ServerMessage &postJobResponse, std::string &jobId) override; + /// @brief extract the job shots url from jobs returned by Ion API + std::string getShotsUrl(nlohmann::json_v3_11_1::json &jobs, + const char *DEFAULT_URL); + + /// @brief Verify if shot-wise output was requested by user and can be + /// extracted + bool shotWiseOutputIsNeeded(nlohmann::json_v3_11_1::json &jobs); + private: /// @brief RestClient used for HTTP requests. RestClient client; @@ -103,6 +111,8 @@ void IonQServerHelper::initialize(BackendConfig config) { // Retrieve the noise model setting (if provided) if (config.find("noise") != config.end()) backendConfig["noise_model"] = config["noise"]; + else if (config.find("noise_model") != config.end()) + backendConfig["noise_model"] = config["noise_model"]; // Retrieve the API key from the environment variables bool isTokenRequired = [&]() { auto it = config.find("emulate"); @@ -126,6 +136,12 @@ void IonQServerHelper::initialize(BackendConfig config) { backendConfig["sharpen"] = config["sharpen"]; if (config.find("format") != config.end()) backendConfig["format"] = config["format"]; + + // Enable memory, true by default + if (config.find("memory") != config.end()) + backendConfig["memory"] = config["memory"]; + else + backendConfig["memory"] = "true"; } // Implementation of the getValueOrDefault function @@ -351,6 +367,45 @@ bool IonQServerHelper::jobIsDone(ServerMessage &getJobResponse) { return jobs[0].at("status").get() == "completed"; } +std::string IonQServerHelper::getShotsUrl(nlohmann::json_v3_11_1::json &jobs, + const char *DEFAULT_URL) { + if (!keyExists("url")) + throw std::runtime_error("Key 'url' doesn't exist in backendConfig."); + + std::string base_url = backendConfig.at("url"); + std::string shotsUrl = ""; + + if (!jobs.empty() && jobs[0].contains("results") && + jobs[0].at("results").contains("shots") && + jobs[0].at("results").at("shots").contains("url")) { + shotsUrl = base_url + + jobs[0].at("results").at("shots").at("url").get(); + } + + return shotsUrl; +} + +bool IonQServerHelper::shotWiseOutputIsNeeded( + nlohmann::json_v3_11_1::json &jobs) { + std::string noiseModel = "ideal"; + if (!jobs.empty() && jobs[0].contains("noise") && + jobs[0]["noise"].contains("model")) { + noiseModel = jobs[0]["noise"]["model"].get(); + } + + std::string target = "simulator"; + if (!jobs.empty() && jobs[0].contains("target")) { + target = jobs[0]["target"].get(); + } + + bool targetHasMemoryOption = + keyExists("memory") && backendConfig["memory"] == "true"; + bool noiseModelIsNotIdeal = noiseModel != "ideal"; + bool targetIsQpu = target != "simulator"; + + return targetHasMemoryOption && (targetIsQpu || noiseModelIsNotIdeal); +} + // Process the results from a job cudaq::sample_result IonQServerHelper::processResults(ServerMessage &postJobResponse, @@ -448,9 +503,38 @@ IonQServerHelper::processResults(ServerMessage &postJobResponse, execResults.emplace_back(regCounts, info.registerName); } + // Add shot-wise output if requested by user + bool extractShots = shotWiseOutputIsNeeded(jobs); + auto shotsUrl = getShotsUrl(jobs, DEFAULT_URL); + if (extractShots && shotsUrl != "") { + + std::vector bitStrings; + auto shotsResults = getResults(shotsUrl); + + for (const auto &element : shotsResults.items()) { + assert(nQubits <= 64); + int64_t s = std::stoull(element.value().get()); + std::string bitString = std::bitset<64>(s).to_string(); + auto firstone = bitString.find_first_not_of('0'); + bitString = + (firstone == std::string::npos) ? "0" : bitString.substr(firstone); + if (bitString.size() < static_cast(nQubits)) { + bitString.insert(bitString.begin(), + static_cast(nQubits) - bitString.size(), '0'); + } + // IonQ returns bitstrings in little-endian format + std::reverse(bitString.begin(), bitString.end()); + bitStrings.push_back(bitString); + } + + if (!execResults.empty()) + execResults[0].sequentialData = std::move(bitStrings); + } + // Return a sample result including the global register and all individual // registers. auto ret = cudaq::sample_result(execResults); + return ret; } diff --git a/runtime/cudaq/platform/default/rest/helpers/ionq/ionq.yml b/runtime/cudaq/platform/default/rest/helpers/ionq/ionq.yml index 8a2270fdc40..f1c56ed1a88 100644 --- a/runtime/cudaq/platform/default/rest/helpers/ionq/ionq.yml +++ b/runtime/cudaq/platform/default/rest/helpers/ionq/ionq.yml @@ -41,10 +41,15 @@ target-arguments: - key: debias required: false type: string - platform-arg: debias + platform-arg: debias help-string: "Specify debiasing." - key: sharpen required: false type: string platform-arg: sharpen help-string: "Specify sharpening." + - key: memory + required: false + type: string + platform-arg: memory + help-string: "When `true`, returns sequential data for QPU or noisy simulation runs. Default is `true`." \ No newline at end of file diff --git a/utils/mock_qpu/ionq/__init__.py b/utils/mock_qpu/ionq/__init__.py index 616e37b78fc..196d550090b 100644 --- a/utils/mock_qpu/ionq/__init__.py +++ b/utils/mock_qpu/ionq/__init__.py @@ -39,6 +39,12 @@ class Job(BaseModel): # Save how many qubits were needed for each test (emulates real backend) numQubitsRequired = 0 +# Sets the target for the job +jobTarget = "" + +# Sets the noise model for the job +noiseModel = "" + llvm.initialize() llvm.initialize_native_target() llvm.initialize_native_asmprinter() @@ -141,7 +147,7 @@ async def postJob(job: Job, @app.get("/v0.3/jobs") async def getJob(id: str): global countJobGetRequests, createdJobs, numQubitsRequired - + global jobTarget, noiseModel # Simulate asynchronous execution if countJobGetRequests < 3: countJobGetRequests += 1 @@ -152,9 +158,19 @@ async def getJob(id: str): "jobs": [{ "status": "completed", "qubits": numQubitsRequired, - "results_url": "/v0.3/jobs/{}/results".format(id) + "results_url": "/v0.3/jobs/{}/results".format(id), + "results": { + "shots": { + "url": "/v0.4/jobs/{}/results/shots".format(id) + } + } }] } + if jobTarget: + res["jobs"][0]["target"] = jobTarget + if noiseModel: + res["jobs"][0]["noise"] = {"model": noiseModel} + return res @@ -178,6 +194,37 @@ async def getResults(jobId: str): return res +@app.get("/v0.4/jobs/{jobId}/results/shots") +async def getResults(jobId: str): + global countJobGetRequests, createdJobs + + counts = createdJobs[jobId] + counts.dump() + retData = [] + # Note, the real IonQ backend reverses the bitstring relative to what the + # simulator does, so flip the bitstring with [::-1]. + for bits, count in counts.items(): + for _ in range(count): + retData.append(str(int(bits[::-1], 2))) + + res = retData + return res + + +@app.post("/_mock_server_config_target") +async def set_mock_server_target(target: str): + global jobTarget + jobTarget = target + return {"status": "ok"} + + +@app.post("/_mock_server_config_noise_model") +async def set_mock_server_noise_model(noise: str): + global noiseModel + noiseModel = noise + return {"status": "ok"} + + def startServer(port): uvicorn.run(app, port=port, host='0.0.0.0', log_level="info")